From 33f1ac8b3496a4b27d4e46434770516d6eb00cd2 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 10 Jul 2025 15:09:31 -0700 Subject: [PATCH 001/658] Use installed trusted signing (#34245) Fixes windows nightly build failures Release Notes: - N/A --- .../install_trusted_signing/action.yml | 64 ------------------- .github/workflows/ci.yml | 3 - .github/workflows/release_nightly.yml | 3 - 3 files changed, 70 deletions(-) delete mode 100644 .github/actions/install_trusted_signing/action.yml diff --git a/.github/actions/install_trusted_signing/action.yml b/.github/actions/install_trusted_signing/action.yml deleted file mode 100644 index a99ff08eb1eb1f1b92cdea2c374a62b2384b2237..0000000000000000000000000000000000000000 --- a/.github/actions/install_trusted_signing/action.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: "Trusted Signing on Windows" -description: "Install trusted signing on Windows." - -# Modified from https://github.com/Azure/trusted-signing-action -runs: - using: "composite" - steps: - - name: Set variables - id: set-variables - shell: "pwsh" - run: | - $defaultPath = $env:PSModulePath -split ';' | Select-Object -First 1 - "PSMODULEPATH=$defaultPath" | Out-File -FilePath $env:GITHUB_OUTPUT -Append - - "TRUSTED_SIGNING_MODULE_VERSION=0.5.3" | Out-File -FilePath $env:GITHUB_OUTPUT -Append - "BUILD_TOOLS_NUGET_VERSION=10.0.22621.3233" | Out-File -FilePath $env:GITHUB_OUTPUT -Append - "TRUSTED_SIGNING_NUGET_VERSION=1.0.53" | Out-File -FilePath $env:GITHUB_OUTPUT -Append - "DOTNET_SIGNCLI_NUGET_VERSION=0.9.1-beta.24469.1" | Out-File -FilePath $env:GITHUB_OUTPUT -Append - - - name: Cache TrustedSigning PowerShell module - id: cache-module - uses: actions/cache@v4 - env: - cache-name: cache-module - with: - path: ${{ steps.set-variables.outputs.PSMODULEPATH }}\TrustedSigning\${{ steps.set-variables.outputs.TRUSTED_SIGNING_MODULE_VERSION }} - key: TrustedSigning-${{ steps.set-variables.outputs.TRUSTED_SIGNING_MODULE_VERSION }} - if: ${{ inputs.cache-dependencies == 'true' }} - - - name: Cache Microsoft.Windows.SDK.BuildTools NuGet package - id: cache-buildtools - uses: actions/cache@v4 - env: - cache-name: cache-buildtools - with: - path: ~\AppData\Local\TrustedSigning\Microsoft.Windows.SDK.BuildTools\Microsoft.Windows.SDK.BuildTools.${{ steps.set-variables.outputs.BUILD_TOOLS_NUGET_VERSION }} - key: Microsoft.Windows.SDK.BuildTools-${{ steps.set-variables.outputs.BUILD_TOOLS_NUGET_VERSION }} - if: ${{ inputs.cache-dependencies == 'true' }} - - - name: Cache Microsoft.Trusted.Signing.Client NuGet package - id: cache-tsclient - uses: actions/cache@v4 - env: - cache-name: cache-tsclient - with: - path: ~\AppData\Local\TrustedSigning\Microsoft.Trusted.Signing.Client\Microsoft.Trusted.Signing.Client.${{ steps.set-variables.outputs.TRUSTED_SIGNING_NUGET_VERSION }} - key: Microsoft.Trusted.Signing.Client-${{ steps.set-variables.outputs.TRUSTED_SIGNING_NUGET_VERSION }} - if: ${{ inputs.cache-dependencies == 'true' }} - - - name: Cache SignCli NuGet package - id: cache-signcli - uses: actions/cache@v4 - env: - cache-name: cache-signcli - with: - path: ~\AppData\Local\TrustedSigning\sign\sign.${{ steps.set-variables.outputs.DOTNET_SIGNCLI_NUGET_VERSION }} - key: SignCli-${{ steps.set-variables.outputs.DOTNET_SIGNCLI_NUGET_VERSION }} - if: ${{ inputs.cache-dependencies == 'true' }} - - - name: Install Trusted Signing module - shell: "pwsh" - run: | - Install-Module -Name TrustedSigning -RequiredVersion ${{ steps.set-variables.outputs.TRUSTED_SIGNING_MODULE_VERSION }} -Force -Repository PSGallery - if: ${{ inputs.cache-dependencies != 'true' || steps.cache-module.outputs.cache-hit != 'true' }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ea30df1cc1984c6f0a3104ccd64dce24bb86c57e..11ff9a4acc260cd0c8556a27b288fec6c65ea939 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -785,9 +785,6 @@ jobs: # This exports RELEASE_CHANNEL into env (GITHUB_ENV) script/determine-release-channel.ps1 - - name: Install trusted signing - uses: ./.github/actions/install_trusted_signing - - name: Build Zed installer working-directory: ${{ env.ZED_WORKSPACE }} run: script/bundle-windows.ps1 diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index df9f6ef40fc9faf87c5be82e3f95288012cd4221..4de22b05f2d5cc93e4387a963f631ed5967cb432 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -276,9 +276,6 @@ jobs: Write-Host "Publishing version: $version on release channel nightly" "nightly" | Set-Content -Path "crates/zed/RELEASE_CHANNEL" - - name: Install trusted signing - uses: ./.github/actions/install_trusted_signing - - name: Build Zed installer working-directory: ${{ env.ZED_WORKSPACE }} run: script/bundle-windows.ps1 From 7915b9f93f5c096dad26a5e535ebd793e84ad964 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Thu, 10 Jul 2025 18:23:26 -0500 Subject: [PATCH 002/658] keymap_ui: Add ability to delete user created bindings (#34248) Closes #ISSUE Adds an action and special handling in `KeymapFile::update_keybinding` for removals. If the binding being removed is the last in a keymap section, the keymap section will be removed entirely instead of left empty. Still to do is the ability to unbind/remove non-user created bindings such as those in the default keymap by binding them to `NoAction`, however, this will be done in a follow up PR. Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/settings/src/keymap_file.rs | 239 ++++++++++++++++++++++---- crates/settings/src/settings_json.rs | 157 ++++++++++++----- crates/settings_ui/src/keybindings.rs | 91 ++++++++-- 3 files changed, 399 insertions(+), 88 deletions(-) diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 19bc58ea2342dae25be0bebfcab600771594989c..98dbe4d02af866c595f7f7577b3a9290d23b66d5 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -623,49 +623,55 @@ impl KeymapFile { // We don't want to modify the file if it's invalid. let keymap = Self::parse(&keymap_contents).context("Failed to parse keymap")?; + if let KeybindUpdateOperation::Remove { + target, + target_keybind_source, + } = operation + { + if target_keybind_source != KeybindSource::User { + anyhow::bail!("Cannot remove non-user created keybinding. Not implemented yet"); + } + let target_action_value = target + .action_value() + .context("Failed to generate target action JSON value")?; + let Some((index, keystrokes_str)) = + find_binding(&keymap, &target, &target_action_value) + else { + anyhow::bail!("Failed to find keybinding to remove"); + }; + let is_only_binding = keymap.0[index] + .bindings + .as_ref() + .map_or(true, |bindings| bindings.len() == 1); + let key_path: &[&str] = if is_only_binding { + &[] + } else { + &["bindings", keystrokes_str] + }; + let (replace_range, replace_value) = replace_top_level_array_value_in_json_text( + &keymap_contents, + key_path, + None, + None, + index, + tab_size, + ) + .context("Failed to remove keybinding")?; + keymap_contents.replace_range(replace_range, &replace_value); + return Ok(keymap_contents); + } + if let KeybindUpdateOperation::Replace { source, target, .. } = operation { - let mut found_index = None; let target_action_value = target .action_value() .context("Failed to generate target action JSON value")?; let source_action_value = source .action_value() .context("Failed to generate source action JSON value")?; - 'sections: for (index, section) in keymap.sections().enumerate() { - if section.context != target.context.unwrap_or("") { - continue; - } - if section.use_key_equivalents != target.use_key_equivalents { - continue; - } - let Some(bindings) = §ion.bindings else { - continue; - }; - for (keystrokes, action) in bindings { - let Ok(keystrokes) = keystrokes - .split_whitespace() - .map(Keystroke::parse) - .collect::, _>>() - else { - continue; - }; - if keystrokes.len() != target.keystrokes.len() - || !keystrokes - .iter() - .zip(target.keystrokes) - .all(|(a, b)| a.should_match(b)) - { - continue; - } - if action.0 != target_action_value { - continue; - } - found_index = Some(index); - break 'sections; - } - } - if let Some(index) = found_index { + if let Some((index, keystrokes_str)) = + find_binding(&keymap, &target, &target_action_value) + { if target.context == source.context { // if we are only changing the keybinding (common case) // not the context, etc. Then just update the binding in place @@ -673,7 +679,7 @@ impl KeymapFile { let (replace_range, replace_value) = replace_top_level_array_value_in_json_text( &keymap_contents, - &["bindings", &target.keystrokes_unparsed()], + &["bindings", keystrokes_str], Some(&source_action_value), Some(&source.keystrokes_unparsed()), index, @@ -695,7 +701,7 @@ impl KeymapFile { let (replace_range, replace_value) = replace_top_level_array_value_in_json_text( &keymap_contents, - &["bindings", &target.keystrokes_unparsed()], + &["bindings", keystrokes_str], Some(&source_action_value), Some(&source.keystrokes_unparsed()), index, @@ -725,7 +731,7 @@ impl KeymapFile { let (replace_range, replace_value) = replace_top_level_array_value_in_json_text( &keymap_contents, - &["bindings", &target.keystrokes_unparsed()], + &["bindings", keystrokes_str], None, None, index, @@ -771,6 +777,46 @@ impl KeymapFile { keymap_contents.replace_range(replace_range, &replace_value); } return Ok(keymap_contents); + + fn find_binding<'a, 'b>( + keymap: &'b KeymapFile, + target: &KeybindUpdateTarget<'a>, + target_action_value: &Value, + ) -> Option<(usize, &'b str)> { + for (index, section) in keymap.sections().enumerate() { + if section.context != target.context.unwrap_or("") { + continue; + } + if section.use_key_equivalents != target.use_key_equivalents { + continue; + } + let Some(bindings) = §ion.bindings else { + continue; + }; + for (keystrokes_str, action) in bindings { + let Ok(keystrokes) = keystrokes_str + .split_whitespace() + .map(Keystroke::parse) + .collect::, _>>() + else { + continue; + }; + if keystrokes.len() != target.keystrokes.len() + || !keystrokes + .iter() + .zip(target.keystrokes) + .all(|(a, b)| a.should_match(b)) + { + continue; + } + if &action.0 != target_action_value { + continue; + } + return Some((index, &keystrokes_str)); + } + } + None + } } } @@ -783,6 +829,10 @@ pub enum KeybindUpdateOperation<'a> { target_keybind_source: KeybindSource, }, Add(KeybindUpdateTarget<'a>), + Remove { + target: KeybindUpdateTarget<'a>, + target_keybind_source: KeybindSource, + }, } pub struct KeybindUpdateTarget<'a> { @@ -1300,5 +1350,118 @@ mod tests { ]"# .unindent(), ); + + check_keymap_update( + r#"[ + { + "context": "SomeContext", + "bindings": { + "a": "foo::bar", + "c": "foo::baz", + } + }, + ]"# + .unindent(), + KeybindUpdateOperation::Remove { + target: KeybindUpdateTarget { + context: Some("SomeContext"), + keystrokes: &parse_keystrokes("a"), + action_name: "foo::bar", + use_key_equivalents: false, + input: None, + }, + target_keybind_source: KeybindSource::User, + }, + r#"[ + { + "context": "SomeContext", + "bindings": { + "c": "foo::baz", + } + }, + ]"# + .unindent(), + ); + + check_keymap_update( + r#"[ + { + "context": "SomeContext", + "bindings": { + "a": ["foo::bar", true], + "c": "foo::baz", + } + }, + ]"# + .unindent(), + KeybindUpdateOperation::Remove { + target: KeybindUpdateTarget { + context: Some("SomeContext"), + keystrokes: &parse_keystrokes("a"), + action_name: "foo::bar", + use_key_equivalents: false, + input: Some("true"), + }, + target_keybind_source: KeybindSource::User, + }, + r#"[ + { + "context": "SomeContext", + "bindings": { + "c": "foo::baz", + } + }, + ]"# + .unindent(), + ); + + check_keymap_update( + r#"[ + { + "context": "SomeContext", + "bindings": { + "b": "foo::baz", + } + }, + { + "context": "SomeContext", + "bindings": { + "a": ["foo::bar", true], + } + }, + { + "context": "SomeContext", + "bindings": { + "c": "foo::baz", + } + }, + ]"# + .unindent(), + KeybindUpdateOperation::Remove { + target: KeybindUpdateTarget { + context: Some("SomeContext"), + keystrokes: &parse_keystrokes("a"), + action_name: "foo::bar", + use_key_equivalents: false, + input: Some("true"), + }, + target_keybind_source: KeybindSource::User, + }, + r#"[ + { + "context": "SomeContext", + "bindings": { + "b": "foo::baz", + } + }, + { + "context": "SomeContext", + "bindings": { + "c": "foo::baz", + } + }, + ]"# + .unindent(), + ); } } diff --git a/crates/settings/src/settings_json.rs b/crates/settings/src/settings_json.rs index f569a187699b764bbac43cca8c3799ab043c373b..1aed18b44ad46c78299e71314c62ebd17d4955cb 100644 --- a/crates/settings/src/settings_json.rs +++ b/crates/settings/src/settings_json.rs @@ -353,29 +353,58 @@ pub fn replace_top_level_array_value_in_json_text( let range = cursor.node().range(); let indent_width = range.start_point.column; let offset = range.start_byte; - let value_str = &text[range.start_byte..range.end_byte]; + let text_range = range.start_byte..range.end_byte; + let value_str = &text[text_range.clone()]; let needs_indent = range.start_point.row > 0; - let (mut replace_range, mut replace_value) = - replace_value_in_json_text(value_str, key_path, tab_size, new_value, replace_key); + if new_value.is_none() && key_path.is_empty() { + let mut remove_range = text_range.clone(); + if index == 0 { + while cursor.goto_next_sibling() + && (cursor.node().is_extra() || cursor.node().is_missing()) + {} + 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] + .chars() + .all(|c| c.is_ascii_whitespace()) + { + remove_range.end = remove_range.end + next_newline; + } + } + } else { + while cursor.goto_previous_sibling() + && (cursor.node().is_extra() || cursor.node().is_missing()) + {} + if cursor.node().kind() == "," { + remove_range.start = cursor.node().range().start_byte; + } + } + return 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); - replace_range.start += offset; - replace_range.end += offset; + replace_range.start += offset; + replace_range.end += offset; - if needs_indent { - let increased_indent = format!("\n{space:width$}", space = ' ', width = indent_width); - replace_value = replace_value.replace('\n', &increased_indent); - // replace_value.push('\n'); - } else { - while let Some(idx) = replace_value.find("\n ") { - replace_value.remove(idx + 1); - } - while let Some(idx) = replace_value.find("\n") { - replace_value.replace_range(idx..idx + 1, " "); + if needs_indent { + let increased_indent = format!("\n{space:width$}", space = ' ', width = indent_width); + replace_value = replace_value.replace('\n', &increased_indent); + // replace_value.push('\n'); + } else { + while let Some(idx) = replace_value.find("\n ") { + replace_value.remove(idx + 1); + } + while let Some(idx) = replace_value.find("\n") { + replace_value.replace_range(idx..idx + 1, " "); + } } - } - return Ok((replace_range, replace_value)); + return Ok((replace_range, replace_value)); + } } pub fn append_top_level_array_value_in_json_text( @@ -1005,14 +1034,14 @@ mod tests { input: impl ToString, index: usize, key_path: &[&str], - value: Value, + value: Option, expected: impl ToString, ) { let input = input.to_string(); let result = replace_top_level_array_value_in_json_text( &input, key_path, - Some(&value), + value.as_ref(), None, index, 4, @@ -1023,10 +1052,10 @@ mod tests { pretty_assertions::assert_eq!(expected.to_string(), result_str); } - check_array_replace(r#"[1, 3, 3]"#, 1, &[], json!(2), r#"[1, 2, 3]"#); - check_array_replace(r#"[1, 3, 3]"#, 2, &[], json!(2), r#"[1, 3, 2]"#); - check_array_replace(r#"[1, 3, 3,]"#, 3, &[], json!(2), r#"[1, 3, 3, 2]"#); - check_array_replace(r#"[1, 3, 3,]"#, 100, &[], json!(2), r#"[1, 3, 3, 2]"#); + check_array_replace(r#"[1, 3, 3]"#, 1, &[], Some(json!(2)), r#"[1, 2, 3]"#); + check_array_replace(r#"[1, 3, 3]"#, 2, &[], Some(json!(2)), r#"[1, 3, 2]"#); + check_array_replace(r#"[1, 3, 3,]"#, 3, &[], Some(json!(2)), r#"[1, 3, 3, 2]"#); + check_array_replace(r#"[1, 3, 3,]"#, 100, &[], Some(json!(2)), r#"[1, 3, 3, 2]"#); check_array_replace( r#"[ 1, @@ -1036,7 +1065,7 @@ mod tests { .unindent(), 1, &[], - json!({"foo": "bar", "baz": "qux"}), + Some(json!({"foo": "bar", "baz": "qux"})), r#"[ 1, { @@ -1051,7 +1080,7 @@ mod tests { r#"[1, 3, 3,]"#, 1, &[], - json!({"foo": "bar", "baz": "qux"}), + Some(json!({"foo": "bar", "baz": "qux"})), r#"[1, { "foo": "bar", "baz": "qux" }, 3,]"#, ); @@ -1059,7 +1088,7 @@ mod tests { r#"[1, { "foo": "bar", "baz": "qux" }, 3,]"#, 1, &["baz"], - json!({"qux": "quz"}), + Some(json!({"qux": "quz"})), r#"[1, { "foo": "bar", "baz": { "qux": "quz" } }, 3,]"#, ); @@ -1074,7 +1103,7 @@ mod tests { ]"#, 1, &["baz"], - json!({"qux": "quz"}), + Some(json!({"qux": "quz"})), r#"[ 1, { @@ -1100,7 +1129,7 @@ mod tests { ]"#, 1, &["baz"], - json!("qux"), + Some(json!("qux")), r#"[ 1, { @@ -1127,7 +1156,7 @@ mod tests { ]"#, 1, &["baz"], - json!("qux"), + Some(json!("qux")), r#"[ 1, { @@ -1151,7 +1180,7 @@ mod tests { ]"#, 2, &[], - json!("replaced"), + Some(json!("replaced")), r#"[ 1, // This is element 2 @@ -1169,7 +1198,7 @@ mod tests { .unindent(), 0, &[], - json!("first"), + Some(json!("first")), r#"[ // Empty array with comment "first" @@ -1180,7 +1209,7 @@ mod tests { r#"[]"#.unindent(), 0, &[], - json!("first"), + Some(json!("first")), r#"[ "first" ]"# @@ -1197,7 +1226,7 @@ mod tests { ]"#, 0, &[], - json!({"new": "object"}), + Some(json!({"new": "object"})), r#"[ // Leading comment // Another leading comment @@ -1217,7 +1246,7 @@ mod tests { ]"#, 1, &[], - json!("deep"), + Some(json!("deep")), r#"[ 1, "deep", @@ -1230,7 +1259,7 @@ mod tests { r#"[1,2, 3, 4]"#, 2, &[], - json!("spaced"), + Some(json!("spaced")), r#"[1,2, "spaced", 4]"#, ); @@ -1243,7 +1272,7 @@ mod tests { ]"#, 1, &[], - json!(["a", "b", "c", "d"]), + Some(json!(["a", "b", "c", "d"])), r#"[ [1, 2, 3], [ @@ -1268,7 +1297,7 @@ mod tests { ]"#, 0, &[], - json!("updated"), + Some(json!("updated")), r#"[ /* * This is a @@ -1284,7 +1313,7 @@ mod tests { r#"[true, false, true]"#, 1, &[], - json!(null), + Some(json!(null)), r#"[true, null, true]"#, ); @@ -1293,7 +1322,7 @@ mod tests { r#"[42]"#, 0, &[], - json!({"answer": 42}), + Some(json!({"answer": 42})), r#"[{ "answer": 42 }]"#, ); @@ -1307,7 +1336,7 @@ mod tests { .unindent(), 10, &[], - json!(123), + Some(json!(123)), r#"[ // Comment 1 // Comment 2 @@ -1316,6 +1345,54 @@ mod tests { ]"# .unindent(), ); + + check_array_replace( + r#"[ + { + "key": "value" + }, + { + "key": "value2" + } + ]"# + .unindent(), + 0, + &[], + None, + r#"[ + { + "key": "value2" + } + ]"# + .unindent(), + ); + + check_array_replace( + r#"[ + { + "key": "value" + }, + { + "key": "value2" + }, + { + "key": "value3" + }, + ]"# + .unindent(), + 1, + &[], + None, + r#"[ + { + "key": "value" + }, + { + "key": "value3" + }, + ]"# + .unindent(), + ); } #[test] diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 1f6c0ba8c777869ede42cb9336a727e558eb2dc0..58c2ac8f8e6e0aca5d9047beac3a3353270d5cdb 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -23,7 +23,10 @@ use ui::{ ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, ParentElement as _, Render, SharedString, Styled as _, Tooltip, Window, prelude::*, right_click_menu, }; -use workspace::{Item, ModalView, SerializableItem, Workspace, register_serializable_item}; +use workspace::{ + Item, ModalView, SerializableItem, Workspace, notifications::NotifyTaskExt as _, + register_serializable_item, +}; use crate::{ SettingsUiFeatureFlag, @@ -49,6 +52,8 @@ actions!( EditBinding, /// Creates a new key binding for the selected action. CreateBinding, + /// Deletes the selected key binding. + DeleteBinding, /// Copies the action name to clipboard. CopyAction, /// Copies the context predicate to clipboard. @@ -613,6 +618,21 @@ impl KeymapEditor { self.open_edit_keybinding_modal(true, window, cx); } + fn delete_binding(&mut self, _: &DeleteBinding, window: &mut Window, cx: &mut Context) { + let Some(to_remove) = self.selected_binding().cloned() else { + return; + }; + let Ok(fs) = self + .workspace + .read_with(cx, |workspace, _| workspace.app_state().fs.clone()) + else { + return; + }; + let tab_size = cx.global::().json_tab_size(); + cx.spawn(async move |_, _| remove_keybinding(to_remove, &fs, tab_size).await) + .detach_and_notify_err(window, cx); + } + fn copy_context_to_clipboard( &mut self, _: &CopyContext, @@ -740,6 +760,7 @@ impl Render for KeymapEditor { .on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::edit_binding)) .on_action(cx.listener(Self::create_binding)) + .on_action(cx.listener(Self::delete_binding)) .on_action(cx.listener(Self::copy_action_to_clipboard)) .on_action(cx.listener(Self::copy_context_to_clipboard)) .size_full() @@ -1458,6 +1479,47 @@ async fn save_keybinding_update( Ok(()) } +async fn remove_keybinding( + existing: ProcessedKeybinding, + fs: &Arc, + tab_size: usize, +) -> anyhow::Result<()> { + let Some(ui_key_binding) = existing.ui_key_binding else { + anyhow::bail!("Cannot remove a keybinding that does not exist"); + }; + let keymap_contents = settings::KeymapFile::load_keymap_file(fs) + .await + .context("Failed to load keymap file")?; + + let operation = settings::KeybindUpdateOperation::Remove { + target: settings::KeybindUpdateTarget { + context: existing + .context + .as_ref() + .and_then(KeybindContextString::local_str), + keystrokes: &ui_key_binding.keystrokes, + action_name: &existing.action_name, + use_key_equivalents: false, + input: existing + .action_input + .as_ref() + .map(|input| input.text.as_ref()), + }, + target_keybind_source: existing + .source + .map(|(source, _name)| source) + .unwrap_or(KeybindSource::User), + }; + + let updated_keymap_contents = + settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size) + .context("Failed to update keybinding")?; + fs.atomic_write(paths::keymap_file().clone(), updated_keymap_contents) + .await + .context("Failed to write keymap file")?; + Ok(()) +} + struct KeystrokeInput { keystrokes: Vec, focus_handle: FocusHandle, @@ -1667,16 +1729,25 @@ fn build_keybind_context_menu( .and_then(KeybindContextString::local) .is_none(); - let selected_binding_is_unbound = selected_binding.ui_key_binding.is_none(); + let selected_binding_is_unbound_action = selected_binding.ui_key_binding.is_none(); - menu.action_disabled_when(selected_binding_is_unbound, "Edit", Box::new(EditBinding)) - .action("Create", Box::new(CreateBinding)) - .action("Copy action", Box::new(CopyAction)) - .action_disabled_when( - selected_binding_has_no_context, - "Copy Context", - Box::new(CopyContext), - ) + menu.action_disabled_when( + selected_binding_is_unbound_action, + "Edit", + Box::new(EditBinding), + ) + .action("Create", Box::new(CreateBinding)) + .action_disabled_when( + selected_binding_is_unbound_action, + "Delete", + Box::new(DeleteBinding), + ) + .action("Copy action", Box::new(CopyAction)) + .action_disabled_when( + selected_binding_has_no_context, + "Copy Context", + Box::new(CopyContext), + ) }) } From 94916cd3b6592d1d0f64dbac89574e3b6c88181c Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Thu, 10 Jul 2025 17:35:21 -0600 Subject: [PATCH 003/658] Fix screenshare sources error handling, `is_sharing_screen() == false` on error (#34250) Release Notes: - N/A --- crates/call/src/call_impl/room.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/call/src/call_impl/room.rs b/crates/call/src/call_impl/room.rs index 31ca144cf8a61946318dc518e7ffee29b4c06d6f..7aac72ed46e777a1c70a194cf79f9bad160d1028 100644 --- a/crates/call/src/call_impl/room.rs +++ b/crates/call/src/call_impl/room.rs @@ -1389,10 +1389,17 @@ impl Room { let sources = cx.screen_capture_sources(); cx.spawn(async move |this, cx| { - let sources = sources.await??; - let source = sources.first().context("no display found")?; - - let publication = participant.publish_screenshare_track(&**source, cx).await; + let sources = sources + .await + .map_err(|error| error.into()) + .and_then(|sources| sources); + let source = + sources.and_then(|sources| sources.into_iter().next().context("no display found")); + + let publication = match source { + Ok(source) => participant.publish_screenshare_track(&*source, cx).await, + Err(error) => Err(error), + }; this.update(cx, |this, cx| { let live_kit = this From 87362c602fcee8ecb3b4faea3531910593a8372a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 10 Jul 2025 17:09:37 -0700 Subject: [PATCH 004/658] Assign checksum seed in windows releases (#34252) This will allow windows releases to report panics and telemetry. Release Notes: - N/A --- .github/workflows/ci.yml | 20 +++----------------- .github/workflows/release_nightly.yml | 19 +++---------------- 2 files changed, 6 insertions(+), 33 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 11ff9a4acc260cd0c8556a27b288fec6c65ea939..ea352a9320827e25cfbf4f94dfcb28bdd9fba0d5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,9 @@ env: CARGO_TERM_COLOR: always CARGO_INCREMENTAL: 0 RUST_BACKTRACE: 1 + 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 }} jobs: job_spec: @@ -493,9 +496,6 @@ jobs: APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }} APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} - ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} - DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }} - DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }} steps: - name: Install Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 @@ -578,10 +578,6 @@ jobs: startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') needs: [linux_tests] - env: - ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} - DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }} - DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }} steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -635,10 +631,6 @@ jobs: startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') needs: [linux_tests] - env: - ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} - DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }} - DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }} steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -691,16 +683,12 @@ jobs: || contains(github.event.pull_request.labels.*.name, 'run-bundling') needs: [linux_tests] name: Build Zed on FreeBSD - # env: - # MYTOKEN : ${{ secrets.MYTOKEN }} - # MYTOKEN2: "value2" steps: - uses: actions/checkout@v4 - name: Build FreeBSD remote-server id: freebsd-build uses: vmactions/freebsd-vm@c3ae29a132c8ef1924775414107a97cac042aad5 # v1.2.0 with: - # envs: "MYTOKEN MYTOKEN2" usesh: true release: 13.5 copyback: true @@ -767,8 +755,6 @@ jobs: ACCOUNT_NAME: ${{ vars.AZURE_SIGNING_ACCOUNT_NAME }} CERT_PROFILE_NAME: ${{ vars.AZURE_SIGNING_CERT_PROFILE_NAME }} ENDPOINT: ${{ vars.AZURE_SIGNING_ENDPOINT }} - DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }} - DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }} FILE_DIGEST: SHA256 TIMESTAMP_DIGEST: SHA256 TIMESTAMP_SERVER: "http://timestamp.acs.microsoft.com" diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index 4de22b05f2d5cc93e4387a963f631ed5967cb432..1b9669c5d527f568ea8cc6b3918feae92d8b44e0 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -12,6 +12,9 @@ env: CARGO_TERM_COLOR: always CARGO_INCREMENTAL: 0 RUST_BACKTRACE: 1 + ZED_CLIENT_CHECKSUM_SEED: ${{ secrets.ZED_CLIENT_CHECKSUM_SEED }} + DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }} + DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }} jobs: style: @@ -91,9 +94,6 @@ jobs: APPLE_NOTARIZATION_KEY: ${{ secrets.APPLE_NOTARIZATION_KEY }} APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }} APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }} - 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 }} steps: - name: Install Node uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 @@ -125,10 +125,6 @@ jobs: runs-on: - buildjet-16vcpu-ubuntu-2004 needs: tests - 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 }} steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -164,10 +160,6 @@ jobs: runs-on: - buildjet-16vcpu-ubuntu-2204-arm needs: tests - 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 }} steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -198,9 +190,6 @@ jobs: if: github.repository_owner == 'zed-industries' runs-on: github-8vcpu-ubuntu-2404 needs: tests - env: - DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }} - DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }} name: Build Zed on FreeBSD # env: # MYTOKEN : ${{ secrets.MYTOKEN }} @@ -257,8 +246,6 @@ jobs: ACCOUNT_NAME: ${{ vars.AZURE_SIGNING_ACCOUNT_NAME }} CERT_PROFILE_NAME: ${{ vars.AZURE_SIGNING_CERT_PROFILE_NAME }} ENDPOINT: ${{ vars.AZURE_SIGNING_ENDPOINT }} - DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }} - DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }} FILE_DIGEST: SHA256 TIMESTAMP_DIGEST: SHA256 TIMESTAMP_SERVER: "http://timestamp.acs.microsoft.com" From 842ac984d57676aec610d54eaba73d88262ae931 Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Thu, 10 Jul 2025 20:38:51 -0400 Subject: [PATCH 005/658] git: Intercept signing prompt from GPG when committing (#34096) Closes #30111 - [x] basic implementation - [x] implementation for remote projects - [x] surface error output from GPG if signing fails - [ ] ~~Windows~~ Release Notes: - git: Passphrase prompts from GPG to unlock commit signing keys are now shown in Zed. --- Cargo.lock | 1 + crates/askpass/Cargo.toml | 1 + crates/askpass/src/askpass.rs | 59 +++++++++++++++ crates/fs/src/fake_git_repo.rs | 4 +- crates/git/Cargo.toml | 4 +- crates/git/src/repository.rs | 126 ++++++++++++++++++++++---------- crates/git_ui/src/git_panel.rs | 20 +++-- crates/project/src/git_store.rs | 30 +++++++- crates/proto/proto/git.proto | 1 + 9 files changed, 194 insertions(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 163cba778a6814ede957956889018b9af1fbe7d6..bc6783ce92019f67cfe74afa6b802a863c95dd81 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -607,6 +607,7 @@ dependencies = [ "parking_lot", "smol", "tempfile", + "unindent", "util", "workspace-hack", ] diff --git a/crates/askpass/Cargo.toml b/crates/askpass/Cargo.toml index 0527399af8b6f45ef18650ee5c286c0b51a83608..3241061dc68abc6f597ba9287af94d4e47a08813 100644 --- a/crates/askpass/Cargo.toml +++ b/crates/askpass/Cargo.toml @@ -19,5 +19,6 @@ net.workspace = true parking_lot.workspace = true smol.workspace = true tempfile.workspace = true +unindent.workspace = true util.workspace = true workspace-hack.workspace = true diff --git a/crates/askpass/src/askpass.rs b/crates/askpass/src/askpass.rs index f085a2be72d04d7c1d16f855230011639853ddf2..8a91e748ed4280252eb212dbb21d171d13cc586b 100644 --- a/crates/askpass/src/askpass.rs +++ b/crates/askpass/src/askpass.rs @@ -40,11 +40,21 @@ impl AskPassDelegate { self.tx.send((prompt, tx)).await?; Ok(rx.await?) } + + pub fn new_always_failing() -> Self { + let (tx, _rx) = mpsc::unbounded::<(String, oneshot::Sender)>(); + Self { + tx, + _task: Task::ready(()), + } + } } pub struct AskPassSession { #[cfg(not(target_os = "windows"))] script_path: std::path::PathBuf, + #[cfg(not(target_os = "windows"))] + gpg_script_path: std::path::PathBuf, #[cfg(target_os = "windows")] askpass_helper: String, #[cfg(target_os = "windows")] @@ -59,6 +69,9 @@ const ASKPASS_SCRIPT_NAME: &str = "askpass.sh"; #[cfg(target_os = "windows")] const ASKPASS_SCRIPT_NAME: &str = "askpass.ps1"; +#[cfg(not(target_os = "windows"))] +const GPG_SCRIPT_NAME: &str = "gpg.sh"; + impl AskPassSession { /// This will create a new AskPassSession. /// You must retain this session until the master process exits. @@ -72,6 +85,8 @@ impl AskPassSession { let temp_dir = tempfile::Builder::new().prefix("zed-askpass").tempdir()?; let askpass_socket = temp_dir.path().join("askpass.sock"); let askpass_script_path = temp_dir.path().join(ASKPASS_SCRIPT_NAME); + #[cfg(not(target_os = "windows"))] + let gpg_script_path = temp_dir.path().join(GPG_SCRIPT_NAME); let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>(); let listener = UnixListener::bind(&askpass_socket).context("creating askpass socket")?; #[cfg(not(target_os = "windows"))] @@ -135,9 +150,20 @@ impl AskPassSession { askpass_script_path.display() ); + #[cfg(not(target_os = "windows"))] + { + let gpg_script = generate_gpg_script(); + fs::write(&gpg_script_path, gpg_script) + .await + .with_context(|| format!("creating gpg wrapper script at {gpg_script_path:?}"))?; + make_file_executable(&gpg_script_path).await?; + } + Ok(Self { #[cfg(not(target_os = "windows"))] script_path: askpass_script_path, + #[cfg(not(target_os = "windows"))] + gpg_script_path, #[cfg(target_os = "windows")] secret, @@ -160,6 +186,19 @@ impl AskPassSession { &self.askpass_helper } + #[cfg(not(target_os = "windows"))] + pub fn gpg_script_path(&self) -> Option> { + Some(&self.gpg_script_path) + } + + #[cfg(target_os = "windows")] + pub fn gpg_script_path(&self) -> Option> { + // TODO implement wrapping GPG on Windows. This is more difficult than on Unix + // because we can't use --passphrase-fd with a nonstandard FD, and both --passphrase + // and --passphrase-file are insecure. + None:: + } + // This will run the askpass task forever, resolving as many authentication requests as needed. // The caller is responsible for examining the result of their own commands and cancelling this // future when this is no longer needed. Note that this can only be called once, but due to the @@ -263,3 +302,23 @@ fn generate_askpass_script(zed_path: &std::path::Path, askpass_socket: &std::pat askpass_socket = askpass_socket.display(), ) } + +#[inline] +#[cfg(not(target_os = "windows"))] +fn generate_gpg_script() -> String { + use unindent::Unindent as _; + + r#" + #!/bin/sh + set -eu + + unset GIT_CONFIG_PARAMETERS + GPG_PROGRAM=$(git config gpg.program || echo 'gpg') + PROMPT="Enter passphrase to unlock GPG key:" + PASSPHRASE=$(${GIT_ASKPASS} "${PROMPT}") + + exec "${GPG_PROGRAM}" --batch --no-tty --yes --passphrase-fd 3 --pinentry-mode loopback "$@" 3<, _options: CommitOptions, + _ask_pass: AskPassDelegate, _env: Arc>, - ) -> BoxFuture<'_, Result<()>> { + _cx: AsyncApp, + ) -> BoxFuture<'static, Result<()>> { unimplemented!() } diff --git a/crates/git/Cargo.toml b/crates/git/Cargo.toml index ab2210094da92cd665ee2483642baaacee66d3a4..3591e815a4a509df661dba3a8affffdb15672cc4 100644 --- a/crates/git/Cargo.toml +++ b/crates/git/Cargo.toml @@ -41,9 +41,9 @@ futures.workspace = true workspace-hack.workspace = true [dev-dependencies] +gpui = { workspace = true, features = ["test-support"] } pretty_assertions.workspace = true serde_json.workspace = true +tempfile.workspace = true text = { workspace = true, features = ["test-support"] } unindent.workspace = true -gpui = { workspace = true, features = ["test-support"] } -tempfile.workspace = true diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 2ecd4bb894348cf3fc532a8473e43f0712e61700..a704ba14824bd6391cca6dda823c2f0cb738ffee 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -391,8 +391,12 @@ pub trait GitRepository: Send + Sync { message: SharedString, name_and_email: Option<(SharedString, SharedString)>, options: CommitOptions, + askpass: AskPassDelegate, env: Arc>, - ) -> BoxFuture<'_, Result<()>>; + // This method takes an AsyncApp to ensure it's invoked on the main thread, + // otherwise git-credentials-manager won't work. + cx: AsyncApp, + ) -> BoxFuture<'static, Result<()>>; fn push( &self, @@ -1193,36 +1197,68 @@ impl GitRepository for RealGitRepository { message: SharedString, name_and_email: Option<(SharedString, SharedString)>, options: CommitOptions, + ask_pass: AskPassDelegate, env: Arc>, - ) -> BoxFuture<'_, Result<()>> { + cx: AsyncApp, + ) -> BoxFuture<'static, Result<()>> { let working_directory = self.working_directory(); - self.executor - .spawn(async move { - let mut cmd = new_smol_command("git"); - cmd.current_dir(&working_directory?) - .envs(env.iter()) - .args(["commit", "--quiet", "-m"]) - .arg(&message.to_string()) - .arg("--cleanup=strip"); - - if options.amend { - cmd.arg("--amend"); - } + let executor = cx.background_executor().clone(); + async move { + let working_directory = working_directory?; + let have_user_git_askpass = env.contains_key("GIT_ASKPASS"); + let mut command = new_smol_command("git"); + command.current_dir(&working_directory).envs(env.iter()); - if let Some((name, email)) = name_and_email { - cmd.arg("--author").arg(&format!("{name} <{email}>")); - } + let ask_pass = if have_user_git_askpass { + None + } else { + Some(AskPassSession::new(&executor, ask_pass).await?) + }; - let output = cmd.output().await?; + if let Some(program) = ask_pass + .as_ref() + .and_then(|ask_pass| ask_pass.gpg_script_path()) + { + command.arg("-c").arg(format!( + "gpg.program={}", + program.as_ref().to_string_lossy() + )); + } + command + .args(["commit", "-m"]) + .arg(message.to_string()) + .arg("--cleanup=strip") + .stdin(smol::process::Stdio::null()) + .stdout(smol::process::Stdio::piped()) + .stderr(smol::process::Stdio::piped()); + + if options.amend { + command.arg("--amend"); + } + + if let Some((name, email)) = name_and_email { + command.arg("--author").arg(&format!("{name} <{email}>")); + } + + if let Some(ask_pass) = ask_pass { + command.env("GIT_ASKPASS", ask_pass.script_path()); + let git_process = command.spawn()?; + + run_askpass_command(ask_pass, git_process).await?; + Ok(()) + } else { + let git_process = command.spawn()?; + let output = git_process.output().await?; anyhow::ensure!( output.status.success(), - "Failed to commit:\n{}", + "{}", String::from_utf8_lossy(&output.stderr) ); Ok(()) - }) - .boxed() + } + } + .boxed() } fn push( @@ -2046,12 +2082,16 @@ mod tests { ) .await .unwrap(); - repo.commit( - "Initial commit".into(), - None, - CommitOptions::default(), - Arc::new(checkpoint_author_envs()), - ) + cx.spawn(|cx| { + repo.commit( + "Initial commit".into(), + None, + CommitOptions::default(), + AskPassDelegate::new_always_failing(), + Arc::new(checkpoint_author_envs()), + cx, + ) + }) .await .unwrap(); @@ -2075,12 +2115,16 @@ mod tests { ) .await .unwrap(); - repo.commit( - "Commit after checkpoint".into(), - None, - CommitOptions::default(), - Arc::new(checkpoint_author_envs()), - ) + cx.spawn(|cx| { + repo.commit( + "Commit after checkpoint".into(), + None, + CommitOptions::default(), + AskPassDelegate::new_always_failing(), + Arc::new(checkpoint_author_envs()), + cx, + ) + }) .await .unwrap(); @@ -2213,12 +2257,16 @@ mod tests { ) .await .unwrap(); - repo.commit( - "Initial commit".into(), - None, - CommitOptions::default(), - Arc::new(checkpoint_author_envs()), - ) + cx.spawn(|cx| { + repo.commit( + "Initial commit".into(), + None, + CommitOptions::default(), + AskPassDelegate::new_always_failing(), + Arc::new(checkpoint_author_envs()), + cx, + ) + }) .await .unwrap(); diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index c50e2f8912ef5b4570a7141378f55701151f3f71..cc378af8327cbd0f2568e78d8b3d41c746c49acf 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -1574,10 +1574,15 @@ impl GitPanel { let task = if self.has_staged_changes() { // Repository serializes all git operations, so we can just send a commit immediately - let commit_task = active_repository.update(cx, |repo, cx| { - repo.commit(message.into(), None, options, cx) - }); - cx.background_spawn(async move { commit_task.await? }) + cx.spawn_in(window, async move |this, cx| { + let askpass_delegate = this.update_in(cx, |this, window, cx| { + this.askpass_delegate("git commit", window, cx) + })?; + let commit_task = active_repository.update(cx, |repo, cx| { + repo.commit(message.into(), None, options, askpass_delegate, cx) + })?; + commit_task.await? + }) } else { let changed_files = self .entries @@ -1594,10 +1599,13 @@ impl GitPanel { let stage_task = active_repository.update(cx, |repo, cx| repo.stage_entries(changed_files, cx)); - cx.spawn(async move |_, cx| { + cx.spawn_in(window, async move |this, cx| { stage_task.await?; + let askpass_delegate = this.update_in(cx, |this, window, cx| { + this.askpass_delegate("git commit".to_string(), window, cx) + })?; let commit_task = active_repository.update(cx, |repo, cx| { - repo.commit(message.into(), None, options, cx) + repo.commit(message.into(), None, options, askpass_delegate, cx) })?; commit_task.await? }) diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 9ff3823e0f13a87fdcff944db7ad2d52350a7cce..69fe58aadb39d24acab08ad7f06bba7233f458ac 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -1726,6 +1726,18 @@ impl GitStore { let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; + let askpass = if let Some(askpass_id) = envelope.payload.askpass_id { + make_remote_delegate( + this, + envelope.payload.project_id, + repository_id, + askpass_id, + &mut cx, + ) + } else { + AskPassDelegate::new_always_failing() + }; + let message = SharedString::from(envelope.payload.message); let name = envelope.payload.name.map(SharedString::from); let email = envelope.payload.email.map(SharedString::from); @@ -1739,6 +1751,7 @@ impl GitStore { CommitOptions { amend: options.amend, }, + askpass, cx, ) })? @@ -3462,11 +3475,14 @@ impl Repository { message: SharedString, name_and_email: Option<(SharedString, SharedString)>, options: CommitOptions, + askpass: AskPassDelegate, _cx: &mut App, ) -> oneshot::Receiver> { let id = self.id; + let askpass_delegates = self.askpass_delegates.clone(); + let askpass_id = util::post_inc(&mut self.latest_askpass_id); - self.send_job(Some("git commit".into()), move |git_repo, _cx| async move { + self.send_job(Some("git commit".into()), move |git_repo, cx| async move { match git_repo { RepositoryState::Local { backend, @@ -3474,10 +3490,16 @@ impl Repository { .. } => { backend - .commit(message, name_and_email, options, environment) + .commit(message, name_and_email, options, askpass, environment, cx) .await } RepositoryState::Remote { project_id, client } => { + askpass_delegates.lock().insert(askpass_id, askpass); + let _defer = util::defer(|| { + let askpass_delegate = askpass_delegates.lock().remove(&askpass_id); + debug_assert!(askpass_delegate.is_some()); + }); + let (name, email) = name_and_email.unzip(); client .request(proto::Commit { @@ -3489,9 +3511,9 @@ impl Repository { options: Some(proto::commit::CommitOptions { amend: options.amend, }), + askpass_id: Some(askpass_id), }) - .await - .context("sending commit request")?; + .await?; Ok(()) } diff --git a/crates/proto/proto/git.proto b/crates/proto/proto/git.proto index 1fdef2eea6e6a52203ba2d6160860e1080b999e3..6645e91de71760abdaf22174d207852e7419bbfb 100644 --- a/crates/proto/proto/git.proto +++ b/crates/proto/proto/git.proto @@ -294,6 +294,7 @@ message Commit { optional string email = 5; string message = 6; optional CommitOptions options = 7; + optional uint64 askpass_id = 8; message CommitOptions { bool amend = 1; From 089ce8f6aa1e6534d71fd1eeab800f069d81c755 Mon Sep 17 00:00:00 2001 From: Max Frai Date: Fri, 11 Jul 2025 02:43:52 +0200 Subject: [PATCH 006/658] agent: Allow clicking on the read file tool header to jump to the exact file location (#33161) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Release Notes: - Allow clicking on the header of the read file tool to jump to the exact file location When researching code or when the Agent analyzes context by reading various project files, the read file tool is used. It usually includes line numbers relevant to the current prompt or task. However, it’s often frustrating that the read file header isn’t clickable to view the corresponding code directly. This PR makes the header clickable, allowing users to jump to the referenced file. If start and end lines are specified, it will navigate directly to that exact location. https://github.com/user-attachments/assets/b0125d0b-7166-43dd-924e-dc5585813b0b Co-authored-by: Danilo Leal --- crates/assistant_tools/src/read_file_tool.rs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/crates/assistant_tools/src/read_file_tool.rs b/crates/assistant_tools/src/read_file_tool.rs index 5b9546981734abdba865346896750348c9c9515c..6bbc2fc0897fa92d676ab92392dbadac0447be33 100644 --- a/crates/assistant_tools/src/read_file_tool.rs +++ b/crates/assistant_tools/src/read_file_tool.rs @@ -18,7 +18,6 @@ use serde::{Deserialize, Serialize}; use settings::Settings; use std::sync::Arc; use ui::IconName; -use util::markdown::MarkdownInlineCode; /// If the model requests to read a file whose size exceeds this, then #[derive(Debug, Serialize, Deserialize, JsonSchema)] @@ -78,11 +77,21 @@ impl Tool for ReadFileTool { fn ui_text(&self, input: &serde_json::Value) -> String { match serde_json::from_value::(input.clone()) { Ok(input) => { - let path = MarkdownInlineCode(&input.path); + let path = &input.path; match (input.start_line, input.end_line) { - (Some(start), None) => format!("Read file {path} (from line {start})"), - (Some(start), Some(end)) => format!("Read file {path} (lines {start}-{end})"), - _ => format!("Read file {path}"), + (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), } } Err(_) => "Read file".to_string(), From d52f07b77c6d87141e34bfa8d4279e34313f5780 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 10 Jul 2025 22:00:01 -0300 Subject: [PATCH 007/658] lsp tool: Make "Restart All Servers" always visible (#34255) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Next step is to have a "Restart Current Buffer Server(s)". 😬 Release Notes: - N/A --- crates/language_tools/src/lsp_tool.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/language_tools/src/lsp_tool.rs b/crates/language_tools/src/lsp_tool.rs index d14a6fb78187c0769459c8f51b47f12d944bf69c..fd843916800a552692f53404a5165b85f48d172e 100644 --- a/crates/language_tools/src/lsp_tool.rs +++ b/crates/language_tools/src/lsp_tool.rs @@ -119,6 +119,7 @@ impl LanguageServerState { return menu; }; + let mut first_button_encountered = false; for (i, item) in self.items.iter().enumerate() { if let LspItem::ToggleServersButton { restart } = item { let label = if *restart { @@ -183,7 +184,11 @@ impl LanguageServerState { .ok(); } }); - menu = menu.separator().item(button); + if !first_button_encountered { + menu = menu.separator(); + first_button_encountered = true; + } + menu = menu.item(button); continue; }; @@ -706,6 +711,7 @@ impl LspTool { new_lsp_items.extend(other_servers.into_iter().map(ServerData::into_lsp_item)); if !new_lsp_items.is_empty() { if can_stop_all { + new_lsp_items.push(LspItem::ToggleServersButton { restart: true }); new_lsp_items.push(LspItem::ToggleServersButton { restart: false }); } else if can_restart_all { new_lsp_items.push(LspItem::ToggleServersButton { restart: true }); From 56d0ae6782f57bea093fee0f9b5f715dcd42b983 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Thu, 10 Jul 2025 22:48:49 -0400 Subject: [PATCH 008/658] Don't apply contrast adjustment to decorative chars (#34238) Closes #34234 Release Notes: - Automatic contrast adjustment in terminal is no longer applied to decorative characters used in block art. --- crates/terminal_view/src/terminal_element.rs | 106 ++++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index c34d8926440287ca684d7c93527516c41e2869df..d883959298936cb50458e8f0c9b8cb0c1ad2a3f2 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -494,6 +494,22 @@ impl TerminalElement { } } + /// Checks if a character is a decorative block/box-like character that should + /// preserve its exact colors without contrast adjustment. + /// + /// Fixes https://github.com/zed-industries/zed/issues/34234 - we can + /// expand this list if we run into more similar cases, but the goal + /// is to be conservative here. + fn is_decorative_character(ch: char) -> bool { + matches!( + ch as u32, + // 0x2500..=0x257F Box Drawing + // 0x2580..=0x259F Block Elements + // 0x25A0..=0x25D7 Geometric Shapes (block/box-like subset) + 0x2500..=0x25D7 + ) + } + /// Converts the Alacritty cell styles to GPUI text styles and background color. fn cell_style( indexed: &IndexedCell, @@ -508,7 +524,10 @@ impl TerminalElement { let mut fg = convert_color(&fg, colors); let bg = convert_color(&bg, colors); - fg = color_contrast::ensure_minimum_contrast(fg, bg, minimum_contrast); + // 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); + } // Ghostty uses (175/255) as the multiplier (~0.69), Alacritty uses 0.66, Kitty // uses 0.75. We're using 0.7 because it's pretty well in the middle of that. @@ -1575,6 +1594,91 @@ mod tests { use super::*; use gpui::{AbsoluteLength, Hsla, font}; + #[test] + fn test_is_decorative_character() { + // Box Drawing characters (U+2500 to U+257F) + assert!(TerminalElement::is_decorative_character('─')); // U+2500 + assert!(TerminalElement::is_decorative_character('│')); // U+2502 + assert!(TerminalElement::is_decorative_character('┌')); // U+250C + assert!(TerminalElement::is_decorative_character('┐')); // U+2510 + assert!(TerminalElement::is_decorative_character('└')); // U+2514 + assert!(TerminalElement::is_decorative_character('┘')); // U+2518 + assert!(TerminalElement::is_decorative_character('┼')); // U+253C + + // Block Elements (U+2580 to U+259F) + assert!(TerminalElement::is_decorative_character('▀')); // U+2580 + assert!(TerminalElement::is_decorative_character('▄')); // U+2584 + assert!(TerminalElement::is_decorative_character('█')); // U+2588 + assert!(TerminalElement::is_decorative_character('░')); // U+2591 + assert!(TerminalElement::is_decorative_character('▒')); // U+2592 + assert!(TerminalElement::is_decorative_character('▓')); // U+2593 + + // Geometric Shapes - block/box-like subset (U+25A0 to U+25D7) + assert!(TerminalElement::is_decorative_character('■')); // U+25A0 + assert!(TerminalElement::is_decorative_character('□')); // U+25A1 + assert!(TerminalElement::is_decorative_character('▲')); // U+25B2 + assert!(TerminalElement::is_decorative_character('▼')); // U+25BC + assert!(TerminalElement::is_decorative_character('◆')); // U+25C6 + assert!(TerminalElement::is_decorative_character('●')); // U+25CF + + // The specific character from the issue + assert!(TerminalElement::is_decorative_character('◗')); // U+25D7 + + // Characters that should NOT be considered decorative + assert!(!TerminalElement::is_decorative_character('A')); + assert!(!TerminalElement::is_decorative_character('a')); + assert!(!TerminalElement::is_decorative_character('0')); + assert!(!TerminalElement::is_decorative_character(' ')); + assert!(!TerminalElement::is_decorative_character('←')); // U+2190 (Arrow, not in our ranges) + assert!(!TerminalElement::is_decorative_character('→')); // U+2192 (Arrow, not in our ranges) + assert!(!TerminalElement::is_decorative_character('◘')); // U+25D8 (Just outside our range) + assert!(!TerminalElement::is_decorative_character('◙')); // U+25D9 (Just outside our range) + } + + #[test] + fn test_decorative_character_boundary_cases() { + // Test exact boundaries of our ranges + // Box Drawing range boundaries + assert!(TerminalElement::is_decorative_character('\u{2500}')); // First char + assert!(TerminalElement::is_decorative_character('\u{257F}')); // Last char + assert!(!TerminalElement::is_decorative_character('\u{24FF}')); // Just before + + // Block Elements range boundaries + assert!(TerminalElement::is_decorative_character('\u{2580}')); // First char + assert!(TerminalElement::is_decorative_character('\u{259F}')); // Last char + + // Geometric Shapes subset boundaries + assert!(TerminalElement::is_decorative_character('\u{25A0}')); // First char + assert!(TerminalElement::is_decorative_character('\u{25D7}')); // Last char (◗) + assert!(!TerminalElement::is_decorative_character('\u{25D8}')); // Just after + } + + #[test] + fn test_decorative_characters_bypass_contrast_adjustment() { + // Decorative characters should not be affected by contrast adjustment + + // The specific character from issue #34234 + let problematic_char = '◗'; // U+25D7 + assert!( + TerminalElement::is_decorative_character(problematic_char), + "Character ◗ (U+25D7) should be recognized as decorative" + ); + + // Verify some other commonly used decorative characters + assert!(TerminalElement::is_decorative_character('│')); // Vertical line + assert!(TerminalElement::is_decorative_character('─')); // Horizontal line + assert!(TerminalElement::is_decorative_character('█')); // Full block + assert!(TerminalElement::is_decorative_character('▓')); // Dark shade + assert!(TerminalElement::is_decorative_character('■')); // Black square + assert!(TerminalElement::is_decorative_character('●')); // Black circle + + // Verify normal text characters are NOT decorative + assert!(!TerminalElement::is_decorative_character('A')); + assert!(!TerminalElement::is_decorative_character('1')); + assert!(!TerminalElement::is_decorative_character('$')); + assert!(!TerminalElement::is_decorative_character(' ')); + } + #[test] fn test_contrast_adjustment_logic() { // Test the core contrast adjustment logic without needing full app context From 8812e7cd148301fe9ac37eeb98658d9bef6bced0 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, 11 Jul 2025 17:19:23 +0800 Subject: [PATCH 009/658] =?UTF-8?q?windows:=20Fix=20an=20issue=20where=20d?= =?UTF-8?q?ead=20keys=20that=20require=20holding=20`shift`=20didn=E2=80=99?= =?UTF-8?q?t=20work=20properly=20(#34264)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #34194 Release Notes: - N/A --- crates/gpui/src/platform/windows/keyboard.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/gpui/src/platform/windows/keyboard.rs b/crates/gpui/src/platform/windows/keyboard.rs index f5a148a97e986c8c33d30cb516474d8103d0c5f7..371feb70c25ab593ce612c7a90381a4cffdeff7d 100644 --- a/crates/gpui/src/platform/windows/keyboard.rs +++ b/crates/gpui/src/platform/windows/keyboard.rs @@ -130,11 +130,13 @@ pub(crate) fn generate_key_char( let mut buffer = [0; 8]; let len = unsafe { ToUnicode(vkey.0 as u32, scan_code, Some(&state), &mut buffer, 1 << 2) }; - if len > 0 { - let candidate = String::from_utf16_lossy(&buffer[..len as usize]); - if !candidate.is_empty() && !candidate.chars().next().unwrap().is_control() { - return Some(candidate); - } + match len { + len if len > 0 => String::from_utf16(&buffer[..len as usize]) + .ok() + .filter(|candidate| { + !candidate.is_empty() && !candidate.chars().next().unwrap().is_control() + }), + len if len < 0 => String::from_utf16(&buffer[..(-len as usize)]).ok(), + _ => None, } - None } From 153840199ef563dac69065a5948ed8401265c9c8 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Fri, 11 Jul 2025 02:22:51 -0700 Subject: [PATCH 010/658] linux: Use `randr` as fallback for scale factor in X11 (#34265) Closes #14537 - Adds server-side scale factor detection via `randr` when client-side detection fails using `xrdb/Xft.dpi`. - Adds the `GPUI_X11_SCALE_FACTOR` flag to force a scale factor, which can be a positive number for custom scaling or `randr` for server-side scale factor detection. Release Notes: - Fixed an issue where the scale factor was not detected correctly on X11 systems when `Xft.dpi` is not defined (mostly in cases involving window managers). --- crates/gpui/src/platform/linux/x11/client.rs | 259 ++++++++++++++++++- 1 file changed, 253 insertions(+), 6 deletions(-) diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index 430ce9260b87ae1c4c7c64b463e647a4c6e6c90a..6cff977128ec594d683085e5f2cc24683c9e9ba7 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -77,6 +77,8 @@ pub(crate) const XINPUT_ALL_DEVICES: xinput::DeviceId = 0; /// terminology is both archaic and unclear. pub(crate) const XINPUT_ALL_DEVICE_GROUPS: xinput::DeviceId = 1; +const GPUI_X11_SCALE_FACTOR_ENV: &str = "GPUI_X11_SCALE_FACTOR"; + pub(crate) struct WindowRef { window: X11WindowStatePtr, refresh_state: Option, @@ -424,12 +426,7 @@ impl X11Client { let resource_database = x11rb::resource_manager::new_from_default(&xcb_connection) .context("Failed to create resource database")?; - let scale_factor = resource_database - .get_value("Xft.dpi", "Xft.dpi") - .ok() - .flatten() - .map(|dpi: f32| dpi / 96.0) - .unwrap_or(1.0); + let scale_factor = get_scale_factor(&xcb_connection, &resource_database, x_root_index); let cursor_handle = cursor::Handle::new(&xcb_connection, x_root_index, &resource_database) .context("Failed to initialize cursor theme handler")? .reply() @@ -2272,3 +2269,253 @@ fn create_invisible_cursor( xcb_flush(connection); Ok(cursor) } + +enum DpiMode { + Randr, + Scale(f32), + NotSet, +} + +fn get_scale_factor( + connection: &XCBConnection, + resource_database: &Database, + screen_index: usize, +) -> f32 { + let env_dpi = std::env::var(GPUI_X11_SCALE_FACTOR_ENV) + .ok() + .map(|var| { + if var.to_lowercase() == "randr" { + DpiMode::Randr + } else if let Ok(scale) = var.parse::() { + if valid_scale_factor(scale) { + DpiMode::Scale(scale) + } else { + panic!( + "`{}` must be a positive normal number or `randr`. Got `{}`", + GPUI_X11_SCALE_FACTOR_ENV, var + ); + } + } else if var.is_empty() { + DpiMode::NotSet + } else { + panic!( + "`{}` must be a positive number or `randr`. Got `{}`", + GPUI_X11_SCALE_FACTOR_ENV, var + ); + } + }) + .unwrap_or(DpiMode::NotSet); + + match env_dpi { + DpiMode::Scale(scale) => { + log::info!( + "Using scale factor from {}: {}", + GPUI_X11_SCALE_FACTOR_ENV, + scale + ); + return scale; + } + DpiMode::Randr => { + if let Some(scale) = get_randr_scale_factor(connection, screen_index) { + log::info!( + "Using RandR scale factor from {}=randr: {}", + GPUI_X11_SCALE_FACTOR_ENV, + scale + ); + return scale; + } + log::warn!("Failed to calculate RandR scale factor, falling back to default"); + return 1.0; + } + DpiMode::NotSet => {} + } + + // TODO: Use scale factor from XSettings here + + if let Some(dpi) = resource_database + .get_value::("Xft.dpi", "Xft.dpi") + .ok() + .flatten() + { + let scale = dpi / 96.0; // base dpi + log::info!("Using scale factor from Xft.dpi: {}", scale); + return scale; + } + + if let Some(scale) = get_randr_scale_factor(connection, screen_index) { + log::info!("Using RandR scale factor: {}", scale); + return scale; + } + + log::info!("Using default scale factor: 1.0"); + 1.0 +} + +fn get_randr_scale_factor(connection: &XCBConnection, screen_index: usize) -> Option { + let root = connection.setup().roots.get(screen_index)?.root; + + let version_cookie = connection.randr_query_version(1, 6).ok()?; + let version_reply = version_cookie.reply().ok()?; + if version_reply.major_version < 1 + || (version_reply.major_version == 1 && version_reply.minor_version < 5) + { + return legacy_get_randr_scale_factor(connection, root); // for randr <1.5 + } + + let monitors_cookie = connection.randr_get_monitors(root, true).ok()?; // true for active only + let monitors_reply = monitors_cookie.reply().ok()?; + + let mut fallback_scale: Option = None; + for monitor in monitors_reply.monitors { + if monitor.width_in_millimeters == 0 || monitor.height_in_millimeters == 0 { + continue; + } + let scale_factor = get_dpi_factor( + (monitor.width as u32, monitor.height as u32), + ( + monitor.width_in_millimeters as u64, + monitor.height_in_millimeters as u64, + ), + ); + if monitor.primary { + return Some(scale_factor); + } else if fallback_scale.is_none() { + fallback_scale = Some(scale_factor); + } + } + + fallback_scale +} + +fn legacy_get_randr_scale_factor(connection: &XCBConnection, root: u32) -> Option { + let primary_cookie = connection.randr_get_output_primary(root).ok()?; + let primary_reply = primary_cookie.reply().ok()?; + let primary_output = primary_reply.output; + + let primary_output_cookie = connection + .randr_get_output_info(primary_output, x11rb::CURRENT_TIME) + .ok()?; + let primary_output_info = primary_output_cookie.reply().ok()?; + + // try primary + if primary_output_info.connection == randr::Connection::CONNECTED + && primary_output_info.mm_width > 0 + && primary_output_info.mm_height > 0 + && primary_output_info.crtc != 0 + { + let crtc_cookie = connection + .randr_get_crtc_info(primary_output_info.crtc, x11rb::CURRENT_TIME) + .ok()?; + let crtc_info = crtc_cookie.reply().ok()?; + + if crtc_info.width > 0 && crtc_info.height > 0 { + let scale_factor = get_dpi_factor( + (crtc_info.width as u32, crtc_info.height as u32), + ( + primary_output_info.mm_width as u64, + primary_output_info.mm_height as u64, + ), + ); + return Some(scale_factor); + } + } + + // fallback: full scan + let resources_cookie = connection.randr_get_screen_resources_current(root).ok()?; + let screen_resources = resources_cookie.reply().ok()?; + + let mut crtc_cookies = Vec::with_capacity(screen_resources.crtcs.len()); + for &crtc in &screen_resources.crtcs { + if let Ok(cookie) = connection.randr_get_crtc_info(crtc, x11rb::CURRENT_TIME) { + crtc_cookies.push((crtc, cookie)); + } + } + + let mut crtc_infos: HashMap = HashMap::default(); + let mut valid_outputs: HashSet = 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 valid_outputs.is_empty() { + return None; + } + + let mut output_cookies = Vec::with_capacity(valid_outputs.len()); + for &output in &valid_outputs { + if let Ok(cookie) = connection.randr_get_output_info(output, x11rb::CURRENT_TIME) { + output_cookies.push((output, cookie)); + } + } + let mut output_infos: HashMap = HashMap::default(); + for (output, cookie) in output_cookies { + if let Ok(reply) = cookie.reply() { + output_infos.insert(output, reply); + } + } + + let mut fallback_scale: Option = None; + for crtc_info in crtc_infos.values() { + for &output in &crtc_info.outputs { + if let Some(output_info) = output_infos.get(&output) { + if output_info.connection != randr::Connection::CONNECTED { + continue; + } + + if output_info.mm_width == 0 || output_info.mm_height == 0 { + continue; + } + + let scale_factor = get_dpi_factor( + (crtc_info.width as u32, crtc_info.height as u32), + (output_info.mm_width as u64, output_info.mm_height as u64), + ); + + if output != primary_output && fallback_scale.is_none() { + fallback_scale = Some(scale_factor); + } + } + } + } + + fallback_scale +} + +fn get_dpi_factor((width_px, height_px): (u32, u32), (width_mm, height_mm): (u64, u64)) -> f32 { + let ppmm = ((width_px as f64 * height_px as f64) / (width_mm as f64 * height_mm as f64)).sqrt(); // pixels per mm + + const MM_PER_INCH: f64 = 25.4; + const BASE_DPI: f64 = 96.0; + const QUANTIZE_STEP: f64 = 12.0; // e.g. 1.25 = 15/12, 1.5 = 18/12, 1.75 = 21/12, 2.0 = 24/12 + const MIN_SCALE: f64 = 1.0; + const MAX_SCALE: f64 = 20.0; + + let dpi_factor = + ((ppmm * (QUANTIZE_STEP * MM_PER_INCH / BASE_DPI)).round() / QUANTIZE_STEP).max(MIN_SCALE); + + let validated_factor = if dpi_factor <= MAX_SCALE { + dpi_factor + } else { + MIN_SCALE + }; + + if valid_scale_factor(validated_factor as f32) { + validated_factor as f32 + } else { + log::warn!( + "Calculated DPI factor {} is invalid, using 1.0", + validated_factor + ); + 1.0 + } +} + +#[inline] +fn valid_scale_factor(scale_factor: f32) -> bool { + scale_factor.is_sign_positive() && scale_factor.is_normal() +} From b4cbea50bb7d5d523b9ac7165575151d306ff78a Mon Sep 17 00:00:00 2001 From: localcc Date: Fri, 11 Jul 2025 15:09:10 +0200 Subject: [PATCH 011/658] Fix icon size on Windows (#34277) Closes #34122 Release Notes: - N/A --- crates/zed/build.rs | 4 ++-- crates/zed/resources/windows/app-icon-dev.ico | Bin 156580 -> 158013 bytes .../resources/windows/app-icon-nightly.ico | Bin 159619 -> 185900 bytes .../resources/windows/app-icon-preview.ico | Bin 155339 -> 162443 bytes crates/zed/resources/windows/app-icon.ico | Bin 590611 -> 165478 bytes script/bundle-windows.ps1 | 2 +- 6 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/zed/build.rs b/crates/zed/build.rs index 0cfb3eba9fbecff024697bfffb542ae1b8b8d829..eb18617adde491908e690495917fd55974635642 100644 --- a/crates/zed/build.rs +++ b/crates/zed/build.rs @@ -50,12 +50,12 @@ fn main() { println!("cargo:rustc-link-arg=/stack:{}", 8 * 1024 * 1024); } - let release_channel = option_env!("RELEASE_CHANNEL").unwrap_or("nightly"); - + let release_channel = option_env!("RELEASE_CHANNEL").unwrap_or("dev"); let icon = match release_channel { "stable" => "resources/windows/app-icon.ico", "preview" => "resources/windows/app-icon-preview.ico", "nightly" => "resources/windows/app-icon-nightly.ico", + "dev" => "resources/windows/app-icon-dev.ico", _ => "resources/windows/app-icon-dev.ico", }; let icon = std::path::Path::new(icon); diff --git a/crates/zed/resources/windows/app-icon-dev.ico b/crates/zed/resources/windows/app-icon-dev.ico index de92b6dd3c4a34164a72dec5f2427589e97536a6..1d6367b78853393ab14407748a823898c93e8667 100644 GIT binary patch literal 158013 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

#{-h5M#f4x2;euryTOuxmq=`qgS)`|IET@rJwZ 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 MLdCD3nMnKp0pNdgasU7T literal 156580 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�zKe&w^@9jUhQ~$mF58Djy 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

  • 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

    8I{!Ecl96o$q7vx5mNwag!>#Vf8Jt(KF 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 diff --git a/crates/zed/resources/windows/app-icon-nightly.ico b/crates/zed/resources/windows/app-icon-nightly.ico index 7d5f53f2eec36306e1b423fa3cee0f0642501103..15e06a6e17631ddd9aed52679b328a46ffe482ff 100644 GIT binary patch literal 185900 zcmV*AKySYQ00966000000096X0Csi(09F710Dyo10096X04N9n0L*p)06;(h0096X z04PEL0Q7zW03aX$0096X0H_cE035*r01yxW0096X0B8gN0L0M(0EtjeM-2)Z3IG5A z4M|8uQUCw}0000100;&E003NasAd2FfB;EEK~#9!?EPo3Hp!J9h&?AWtLin^e!jmp z0NTKNjKg3IW`-(nDU+E;KhDXltoPmqMhco)#q>l^H~PExHC2_F=RD^z zgn!9@ed8B?_#OU)Kx6*PCJ_m`=F=waLf z90$bEg2;l%fQ$ok1fnnQ1?5`>o_ww1ukjj^sld_a|7J{s7ze%g4W9Vu2k`jiZ=%0k zgN(eWsrCS}h<*jY0Qr68B9W=Up$Cj1L-x*q9Y)*HR} z3qPc9N5vzsySNENw%7pTctOeFo4@&6^uniq=)VWT3jqB^08be^2gd=-9@fdCieY>G zS7Qd*8m2x(*glYlTOWqJsn`k}26@2U0%RJ5?zDKoiw(#%0Eh7bWDZo*DMZeXNa)}5O+2mDmF&eKbHrPYy&b4*snpRL5v-Yn;{PC z;&bnzY9hMJHOP$DKEEbGKNIiVqqJxnUAR3sbTD=XG8KYf<>fHB{yVo~T!ai$9Uu;? z{9eW`5Mu|#0Op8lv4HP-aE$V7fI+4L8H*SL$XKx76mLShG4;AQEx5dXpvmD3mwX>TD{iSXgJ2zBMYTnuXiAXn& zDDO=cPh#iK0@eE|bqF6Fbu5%2lTKl(542Jnra z|Dm1Jor-nC*KPn{{3%;4|2^jT$>=w7b6LuBTkjzZ8yGi7jICVITp>&!c+z*+eHfV; zWQ;r*ex)Zy@lcR07Tou_TxlaJz7yF>PT)A`i8e>{n{mOs

    xHV;3;4fp1UrWP*O~vAi|a9>_Ql z2poHaVXfER$Rgk{xvzBP-*{3QlX2iMq+M69Jzw*AufG>$XIlA1#MmPa7qZD$dvYzJ z*G*!4J?r)m{CYga5wE{D%pjv}T;8m9HY#It3j}Ce6nlX5yQB?@{TBfG>mc)YwHe93 zbM;Y>DnK78Jo{rm|6i^R;2S^xgV&rs>2yc2yQjrNdm#Rg0sPr<_mkgo%`tp;D&?S_ z!W?@r09P#$L$BrJ*mD_M%V`*co@o7*2a1br0wCB*P9;+x=?Tr(^sQxCOdwkqSjG6c z$<0BwLg>%sJ+A8m$rTG#e%Cff2;G^!ya8DYYZ?T<)v5p>qqu`s3?Kt$hOv#5#luQB zru1NQv5{ysRD3nrRM+`BgJb@vwz+{Awqpsbn}963eZ<%z4wrdx5mg{lPfpzHA2Pvm z$ked)4fyr}zyWS{8=*hb>IUDV%A>qt5E-!Fjz4B9*>uAiuD%K1pN<>*WTDGOZ>s~u zu*nsbR4`5!AV=8h07M4CufXs{HNYS9cOCBgEkxWE$A|5Wai6xDWB~ zg!b`&+s&W2oSyzW-#H&+5#Wetf9x0kaBl$L`1v0MQB|DoJS}_36bk|{IfG)#5hdaETZ?RLzl`mg(e)Be-MT9;|H;{0cD+MP0D_^?9rmIPR3Q z)5UM9eQ?<1_nDql3^H?oEI78s;={B*C6hs@-OrIW5nJuUxEe!us=eE#J*ap6P3}a_7^$|k*0H!)b z^q1PQ7`XZvzCVR=`?6tifN=-lu)R{P5jkxqWdkS=4`SA0YCIe$0omz5GE2m`9b1st z%k$(8AH=Z!1QES4J^eEhT;KvD=JU@JA29#i7XL4P8ZZL5@U&1k()kf3I@i|?>sNlifFnZxSbjfxaAe&7*as%PD2N0(=P6(o z`V-xBw({rtK$2~w0=n|o)z+o_uH*GLxiqh4w`Gy(MjgXOPUYZn>$&GpFZXgkZ|wXE za4ef|Bd2cY^guP>=n=MW3%uqp^}8|L><)Z?BG1ThrkhWL7}mx88PR!00O0lW?=+f3 zQq7qK?tSQob%UR7c}xWB4(0blClzOX12J6OLF`^9n3s*IU)aTzu1pmaB>(p3{=^@y z4dClP|AXKdc6O@r1{L3n;Gi<#NQ_TV6 zz`Rn*!m#%_oi9`AQ(5-4U1siQY(h>y?l2DW=7Q<<)7QFjzWJ4-=Bk{Y;+uh zR7`#&CncG@5!mKd@wE($UBb;~VAyfY~RWgmIR6!w}h>4pkBTQK&3^8T+_7K8#{xkmI@ScKsM zrrOV-FJR!B+xheR?OPK7*tk8ZJnRxgwRRVCZ6{_RGrP8u(eoZ0144HSTeASc%;RIh z1nc$@{F$_GL@@3Mp+6m4Ks$R6b9|9w_;tBPJ1uf~7HP}*@<0BWKO7st*K@yLe0lKB z?oR;tf5{vf;Gf{o!T0Ao4~`aOgn9Fz$}@V%-Iz-{i#@{YrP$@%OEp zYh^iytM)-eh@nLs)^KS-vIp1~tx81j7e(nV-^a~vYPrqtfD1s!SqquVYpF^gS%AZ( zJcQ9B4j1y7hiwK4gI^U7Bj<;dh%j_#GM2J4fGk{nAR{oc@?et9Ph$*Q0lfg}O{*i_ za0i6$aZw>f1b-<97ZRe*f55w4d|$WF3Wq^ z#W-hLLNW}h*)90)5ttbqBId{Y=vPs1P|WjaTP+d&1~HxXjOoTh(>|L_4OcBObf+1} ztLvK>x`%=l6r(X_*3WVJj(u&}CP;-+fr#LHFFStP<;lU;2OzTW?ZZj=TE3>O_YwNl z1dQ8#E92(UMTZXL=6ViEuj*6ZUyh+UPgrDQjREt+L&Sa`efPSe(YPL$nEboU?Tu{^ zhD!wp(==r4{fQjN%n-xsdp>%`&XlvkJA@iJ0b>l9A3cL%dyzNF5)XN2q7ibhZ>vLa zj0pZbze6>@kD+~#Kd-3|5&c>w1{2`Ae2gfAheV_}ju}tu7|?VBsXa^h9-S0|;PDSZiDI#78Mv%xabd{LGEWn>3gQ$QM5$SuM zfH@Ye80$l_+2H|Pvk9Oa>C(=~t%9ld5!%Q4{YQ>Kd2L*M3_qL;(og@t-p<5%m?I7= z1)sW1ShDpEM1P4G+i?S60U$%zj38fr%{T$v^2Ec|%G>C6J_23&Uf*kzA-e$5XcuMz zeo5b-Kwi4$n(&ng1ev*1cI;< z@25?QU}36#M1KJ?b1-jp1K-n)FBYT8BgD7`nFYYTRs;Z%{mabZKbSCO6JU*~%c26= z8Q%RPfBRqj2Jqr9{)8gMVMNafQ(})Gn7?aG^C^H3;}+H~bu@OSH<t@G@UP|YD2GKYduIgLx!znm= z7`xB~^v;z)MvG&wj65eNOCo*PN;bSyiW0#!x1=JV;uJ6rt~y2-9)s*Mm+8#BGs>dx zaZCByeQGRAI|GMSfWP)_kXgXkO53Gt z=+9*IRuokL>4XOc9M*DgsTP){ooDWWbq4}27T=rl^Hgp+W`_^K#hBIrrdF&e--E9k zGyA~E$v|HG(<#z`XMg-x|HW+pU;nvJflNJ?A7x*h*>C{SWcD}0C8kyaNt?@JvirGb zqicY}cmCwN1dgkHI@uYFTL5uD=r6|C<>EDnP}Tbwx^qB=L}XzOEeOsB+k8<9~~pfCO#=>s#JfK;>F z@ZCwCy%SlG)ln7i_PMok9%9}Yp|ARRyPs3*9|}JJPr*G@BCK|&fos~mvR{Fjzh=i^w}T(gSP~K zU=Z+Y|L~J~ZG+Nrul8KVRzF3gpN@4_9D`ig^fycaq$}7qT!P5J%x~)J#tat8d_N!t z4~{`g!=NCTD;8f+)>busq|hSvD?Krkt!8V$7`WyTfZVu2i9ew~om_a3lx!qo8OTs#`qxtNb51Zp87e8MQp1>kZll#i(&)fs{kcA zkQ0#5y7_dx41%>YaAa_d7+P8U)-6CpFx677R~`rQIQlb{T=P`+#z9Jd#)QWJ)1YeZ zz}837wkar^#)soVw$r6STmZ^|b%$`(9S~|nj>X0h5r&nX;zmDHaV-c^F@#R43^FlOMlxyEtGi=XVC92_t(sO8X8DlhYZFq}#2WM{cmilM`>eFSUwwXz~uyPVLO z$D^Y7|9L|t1b+?|FmPO;W&vZD;Mn8rt-p5>WBb{w9aTYzHV!ZS_=B12he&_=rcLM|q zTOX(ZXviqEh*Jh2gDoRlFWo>=tie`GnL@CRA9aKo(=_24z@G>Rep4)dU6eZFim?ZC z#PZfY_U|6(L}XfQF?;y#WGs;f>=MT9P0C7+1LCj& zb3_Q;6)N=5ZSdsBKPcD2&SmqpbNL>_TDStmXLr@jkXsGWW9+P zyRlMBG-z_VIYQW$S`iRA7`y)~%;7KL3M0XcoCxNBz8k>Tf9{hzC9G@$JTk)meSrSa zIH%_MNF`l6UEoae@6m5$mYPo4=4M_zM3oX(0;+j~(4K-h2sJj9s9bDByF3IU zaFkJ8S{PxtRQhn3K1c*q&C#@MFBUNug&b>126D2Y#ZE?ZW)54;V4T~TMwCEd$UOT= z85O-K_R8;&0UzZuxLJPa@~r33iG7wIU@>Vk>5tI(@I0tDYD)Amd={a)MDXd&FS_ zmu0ZbPEZf_4z}JeWW}6k$nrkJ<)npK?~v6*$ZQ3dL(iT@XR|G6HqB^q*Y1S0q>N^iD04=3?j-U zW4g}9HfpHRk0^Rkg0kW%0MK5p;oApE0CO&t<+@4lPgc-y#C}KSEfrf* z3v%@0LlC;Lf6f3O!Pa{PelX%8X+fr!9yVJHZI_Kr(x#qh(j_s6(*2A5*h-~!Z$znw z7_OaA$Wa(3qf28KnM5`s`Q`m^N@4B+G8ncUhRr#|qoS8*nj?&vfw@;s!cbHNQ4f;7 z*oemG#83re_w=^=eE+3w_5hiW8@GRlWA_iF!iZNY;Q!%&_ucqE{EhGae?JAti~ri6 zA~1(PL1ca|J*>vf5YuAZB?Rl@lp80fz0vt`^exC*APo6t*=nf(4~>6w1ldYB*?fiN zQj|cP-;?Y@n0H}4)vYT^Z0mh-=)q{*Ve12#7!=>Y)rSbfTGxHdr5*2#Cdb2tW`y5x}eHD$0Fj(yCXa&R4`xWWZ#> zvHSCe>i>)<;ya$`37-4O-}r-X0AK%ypSb!*Q+nf11NbFd9jMmbk3dRffVk+Hi%w%= z88vFbWp*3#8&!+j7&dQ@yRDy9@uYGY$A$AH@^knp6ZB= zl=0W=NNbRyjdj5j;2NPihhZ(>*EN{!5vutkGKq5khwkz~b=fcLQ;aFj#EQ9wGE?R@-rm02dw#2*v#yyK_>VflNuP@Hz;&%3*|S zA3-uzZvCX6wgQ4Bm7*>vkSS!|rQ?!3Z*(2`h@5tGF)#QSR^wPxMP)IzFlH$yb7-Zl z1Z7_eY1xe2kHkKuYYwAp+T!U+k6bx%7L2J=oo=Kuc3i3~dks*frZIy$KD9!C^6SqM z)xTE2pOcoI5%By^{^pg>{Lp?)_TErJQZBqiNY^esNmdwG|UMyj@BFAuXWe2%+| zKM7q{1&G6$3CPOfH%1LX(s;@(V94pIrJGFj0w5aot0QrhF>bD)AZM{o>{2;vM@CyZ z9rEc4z*bs9rcyI#&s7+1h&)kQj^m~yr+O?32HF9DV*c(jLKF}vCWutu%Q}bUtwS^i z!aINP^;bZ4T;PNOigTYo8wCj{N_XKLlOy_U*L>4A-d zw10jJ*jjD9g0_D;I*ST?l3wTJri6g0*A!>;-&T7F!`Z0Pi@lP!hAapJ6{<~rE{&Nn#7f@&On=k8o4XVA$EA!P7V#pfvs&2Q{S#&oiUAi)b(kAnl}P6HB2>EuqqoMZ}Z zg;X0N_5r4#OFGd_<`=~5@-wA6 z`k_w%sAV3l_Fb6_#6hjHhN%m09*spECk_hej2?Gc&{C+iSZS>jLlddR<@&frCCNp< z$q_L|?A<{2QKlX7P+`p;*3d!=T zLU*~rqnBR6=Hwi{EBZn$A&qq)OS<6fwc;6O#*-iaC>VwKC3>~CVi-d(hG4CMF{C3w z(s5V?4iW9yCC*-X1*^B;l#?=cKXiEmL0h%(+xv6 zX;bOuKzc%pA&hWJ203-fW;;)g0rx)iF_jiiIUu$=)&;s8xBjpdqZM6(qGpr|O3;5l z)jp6`z$Dn#sFf(j)jS}C)iXd(%1_U~1>jTHsv&dZ!4qN5 z*e2-W3VsB}Cd!q;pn?7 z-1(UN?Re0s0w#WNrrOIi%?#>uFQr}7GP9P_n&U{A5}}CFQT2pp4sdgWILMqkS#7N! zzKxgPehV>17$T50`WlHYOG+gv5PGHp@J`CWYg#xkC<^JSUNj(P*!oaP{MvWC#X*jn4WIIUQX8>!eM;R1k{ z?`ll0j$-IwObze1sAjW#pC$ySJTAWKHNn|v6)1f&lZ#abMrl<45nSOL81wW)sp0Ph z^Zx+g|KRGvC%p7)KaS@MKrmMT-}r~07)M1s1v3UduR-QN$~()h_j&&Bp&)o9+O1kS zYxzQ{N|+E@ivF4T%8n%^W25}{ktZEPi^v{P`(sY~7D*{AN<#_D$T%&^cK}e&YCQF+7x2PQ{{U{i z=ccS7W|<|ElzGM&IOkxjl}&)4>@&qvh+t@(3=*Y9Z&jy1(-X>QsVT2Zi5Uq1*jz`8 z$tuFMQ8OuGP0TuQ;VkOxqS}n+b;U#a}be5hFQyA@+NIn4Tj%J)p z3?_;*ogEKb;nrh-b%G?n_2rjgoE>}n#4YD*Rk?-aIE}3W*mpallxu`JyQ3DCaT%D@ zzka9hF(&tg1S9=$!Xfng8{6c0xNpz#_@x%@>7|0BM*len`_0&|7bcUk5veuRu9&FQ zOdDkHe9q|j&M3@?+316>eOtz`!)3-0$cVBHrq*(M$g_Xum^lP6O-A8KM#2j8AyZw2 zdTmE5aGm0*m^ zY}9fTP09~QCp^Jr1JG=*ZXAdiOi|VnkGA++>;k0bu&D(PwplUIT;=jlDF;-7-F_5H5e?hr)A}zb4ZxtN#viOofh+|$Pc}A_XXk+W~ebPMN zSgantcICNGpU-xu_t7efthT5%0{f1BT58 z-MWXX2sfX*4^!*uq9d%}Bf85C$XWT@Q0}jhya|&lkQtMOcw&%=ohFxZA1X|})!6$E z+mTd67VsU&7;H~3uUNGUPNCP0oXT+JbQ4x$F$&d0ppkH+Z2|8z;hEN}xDN?VVXF$! zGkj;HWg-Q^UrbY z$5yqVeaEoT^eRr9XiY?a{kmtC0lf6fKU}l`v@6`z zL~i3C4^HtjLIs{7`>+*@NhqiFgrhCgV0(oU3K+n5GQJ{*=YHx(@Z_i7kL~#i(R&$d zCecQAuT?}>96k~I%e3JZScOSF4NCLQ=>tDL9Fzg1D=@ZHZ)zb%l*m0L(F$3lBt83} z1{fBc7bxi*KWLdsqYWInLXS`gno@ZTBS?LJF7s^*u)mO`Cfms7qm{_!(Isv^_bxpD z)1OAYn1lUFJ^SXu4WMd=_I$lbt6(E`;_06ZOoCsHw*Ql*^R5n>qgOIt7u$0cKC@M|tos5!FdL2O;P_w6} z|M{e)G0i?=cR9J4rrDE4k^nvqVs6PnWt>JBb`%kZpe(xS7;gGt=tJpdV84}zP4S9s z-JQO_GWo_5v3L-)KPQlysdR;5=Z+c4ZuN2vX+``1hARdgQWedWQP>IzD4CWjv1?R1 z>PBbhG9BQcVIAk08k;LNyMVE^svW)9X!{-@KNgp^Q7#Dy5iGQ_LFEx;yv878cTR0| z&X2s2C&Q4@eQ2S?up!Bal)S2kSmFXe`2ePtd!Lnxrl4()OxQF@5ZQR4+aOlUFpuQA zlr#V%S<31G+`YpfpT}x8J1og)8n*fZqW?sxoXCUx=wW!`?>_M{cmcpqfLNJ#y8DA9 z^isj1q|Z+*G?)<`X7-5PWs$8WR?Y0U`g1#+`c(1~@`y@;Js2@WHA&H)DGEV@6cdO; zCr&)30#yqg*WgM&&tZ@;o}$`udxqGZ6@8i9=-8>5Cq`3>*ssA}`py_4Y*nG28~sd_ zXY3parmAHAZFcejchr1|$N{yJ62;em(P+{M20>QTG_`qbqtKjTUfL;RFZ)A2&$Z}ieLEpQT&y~^A08Uoh9K|_@)dszAv*EB& z5HRX&d@YJfvbxa{V(68k-Jza0h+c^?rXE{@TpjM%eJK$wjWbi3PrGI;q)2)m4VKfE zGPsuV*lk?=05gqz{PLvF{C_JO*~A?Pin-Xrae1rwij|RZ>}Bzj)#|zQPbe3Ec>p&8 z;D+wwO#DK&aGP{Z1e413-A#Z4|6D3_qL*GT_>D+U2H7#=kb*7c6cf8^y$MR4l@dvM zN|SCDk@)seQI5x0l%UNx7V*MefBFaCE1LjwF584frQ#apW3z7&wkP`8#v21T7}rPy zR=o#um4G}2oG$tSmroT_&80msM#(G|AcmVcJpF_3!_z>?4Gi5DI7|y2I5KKjjg>gbgMm#1md(bEkW3ISS zoMPF)q_T;_`S^We{0*tdu0mI4#Q%{AK-5(C0X!=g&gu0u6UnlShzPb?NHT190pFfV z*^!-|l&HZn1`}_+S|hE6-!6rUc<}3{snB$HlsNz1GX|6 zbThUxm3UjezKT*xtq@NA`vDaG|Qx5{gWl24DAV!?ErIG*B~iezW7^?v5FQ}XoFj0G}{a3YfRcPdA9a*)JO6BCQ%6qKEL zLKdhdDYl#MW%E=bi_mTG%5VP`-ulwl;pVlnIL*{xgsx^bNJ`RGgktpP-(?TsiaCe; zyZAmb)1<)U7*Nm5#G}CEup6E-XRnaxrurB%Gudnt#-sj#>^BJgxwM((*DE*_;|Ld- z%tmDI8nPcsi+6NX&p?f6Hmo^irem1 zRf&=%5Qj7QIrU!{2h?zcl+X8-Re1IDX1@zJS0fT(1d3F8w1`RSR@oei*+7h{R=moy z7%L7_jA>a7aDuc-y6KiK+R6Sb8KcPk9sM+r4%&1esMD2k`-rjs#}j+9gmq|8A-qhHGk;}`(8DLJ=aHbf*bqJ93 zx5h5yB(C;>iSM3+sgD3RQ`@sO$XGo1`pY}GIm1+k%2qU>`bf9{A~zjT>ga{mYc>dMIy! zW5nGLzE5T5Vf1AzFI@7CP@3;}XeoEW&c>es5Qmj$4AL0fj-zKTHd>;HM=yP=n1Y(3 zm10$KrnQ{Zg<*n0l*TG?xRh}p(#E&1;A~fHEGAzwqn?MRzLBrHvHMcxNJz_(Zy)c` zj%3*WyMOWS=n^wQA_ zD555m)-BXT!J?kuhW8su`DqT}sy+A~GS4?oNheCs$-o%p@|6L^=qJIFyBPw=#EzI< zXWZ-#;;;t6VR35@@A}M-V)OU}s|Sw-j0_u%57Uh-M}jY8)X84(k|?MNeKT}9q6G|w<}U_H?V?6c#DE2xcB7_xYTjKi>PM=@5a zMEo`{#4Dvd1LSJG4&8``$2i7dUn>wv3{eA9%@DdviF-?zZ;bkgwp1cZ{#?VqG|?Wqdo(1WOAv}3I!H1QBajV zTP`K(s^LWG7iTP7a|6+@HAG}o%q?#nqFWJ$?O9gC8M341pzs9%1E$^+tYcW~?yq_X zQ^|k3;zFYpPBjA$EvViHcjqITT}q56mR8r}0;E#|7WdKuptCU50b+lu3cpSw(7V%} z4N*_WFn}rKa?8;q9{qg?2C(vF3zI~ASAyisz^Nt}%vVwQ}Vq32VoY>RFQ8c#m zJ!I5bBlyjfkC1HE^|MNYtE-u!2p zt-*z!fI+B9DAly924paXKMxM*+EYbwS%Kv(`t4~R$>fcpT1-(-h^$w3$nkNFVL|O0g2##oAc^P(^st9%O|^;9Rt=CzZIsZddmEx=}a^AwoujE z*t0!r;kyfv0qjidxT9Wh&URi4z|>+b7Cm^_D1S#-bTRbcVV(5}Q@}>A`LGp8Cgm0o zT`}?;O+@(}*oe8SmJQI(V4K@8)q%8G{fV|aGC~dAscgiynT(I5)sTaLYSGTe{z57V zQ;BE2QoQrH%5T8F)ss$3Ut8^LT*?PXT7>#Am&bnlFl+dYYBAEHWToyV94N+NOgBPl z_eME0kF4Ck)x$^Hnn^!S>NA-MqpDdL0e>BVhLyB+`oG1qzr8Y6gsDW%9D{m2PzG63 z2r9^CpG5*`LXpP0*N0ivDh9Jw!e0G0KhvC^XT%9+RC-k;PeA!hjkq(brb4hOs}|=8 z0Q7$*;Xg#StN;K2zYpe*G6=R=BD5>i%bV!e55;)6+1C@GS5QnJa*jD=C;GN11Zan^ zt`|Aeb%jY4Lw`+Zf=n3*ZnP3JfYDx9p3~WghGUW^qs4{I{kL^<4DG3$GKmuqh2LK9 zFGQWjKs~#SK{E-{=cwj)Wzp~eLzQ~ehn+R4)M3>OByfs!&TE=uAhrnInQWT)-r-rh zVG>Clmy~PoB$bWEd#H{}I1wz%cJMegovBZ zK8L6dEsJq>C*x(eo_i1Of8t}}O4OZXd$YtE zX6DSPF?J8OzOB*bdLKxyjA=k_fApQG4#6Sd?gu{r7+~lx3KMCbQ0Do%qmE_6k>BZs zZXf!It(g$F>b&?Vi+~(#eQR8^cAn|pl1?pI8%8HfA-10oKMnyP%K`eQM*RXysDt|D z5eP&3cuKM;D4MLNuxs{JhTCH3F0MY(VT%yk;=ggGTB0-?(%w%@J|bdd)Qd-met@0H zXv8P>UJ>9a`~h{o&4HDaOZFH#3}MJWf9r+&@ZB4D@#lXN#*RuuF7)*vAcW22q??dS z-2xmm@FWJ<%E35B=+_`Of^1}KnEC*reXRa?N+sn`gr5Soxrxx7N*L)-n1R9ozEzxR zGkJuB@;sd@U@SPx|t499C%K^IQrV+0~<39)Ro$%&V+M^uCCe&rw}nG3gmcrM`cp*aG9V z-}`;+-@S_`KKfz2{)I12{9MyXZ|ng$wus>p2$y4ZXVe@70TequouNH@hsY82axq~z z=4vaS`+Oy_5s5*F2&)M|WUZ)IlOvN`irMRMUR*2LSR#AG{vp6J(oRunZgvkt`$ld} zWZFimsW2~a4Bwn!00i@2OEYA$RlafA53jgcZg!me(ir-DV}kGGVN|cSy@iM0d=YGiSVU%j?43GsI`U=shOp(HBI;KTsSZn7gr5R{M;`ZGY? zafE}v)X1-;l0-i`ARNIf(;OlU7Xor&HwsP1f||IT;YT}{#hp(@ zb*9xSCE2XY;+KWv=67^KVxkx{)zR3WP5@2=-=7J2hl7B3Y73q+CCDtryd-SlyGIh2 zR*c6I6VjMv9zh!CusLn<`scq`xHjuUsZ#eCL|{gC38EUt?ZMQyK&Y~zXIa&b`;C)i zU)sGVB#uBk)oK2jW^C&H(VaMY z#4!AiQ(I#H%3pjUz3ATpcs`z{^K3jQ8h`*dKSB&WqThM*9d(tMjdVj(;Nzc z#MolQv)k>Lu1e`8d)V2HY<-mzWBID1u5l715lg{{N{Xpoj9!WQybs$J-Rh!1!ACMo zHM=Qf9OVpD&CP6D!79t8YY8qR!BniUH8;<&j3HC7=j?`T6rs&wFLrh-C+B1XSOC87 zC1*h$aEzTx_CZ*WElI(sBU8av2NKZ*8>5f#>WF}AR730L2o27mJCC}6G7vgJ8TMxf$dNoJa(1a_&Q##h_@IcJZuXT3G$_|w<-f(n7Z1XSdbx+ijT^Xl z{WXvi3UTb#sG0)|-8oD(M;x{&`46RcU=rO}9UGSExd~0@*h_y%MsM_(Yy+~e%>m-D zDXOZ{H*S&V*wYnk$DQ0QRTwhlyhEPQSW8Q-3gjBZekJ<$YN7OJ6cS#7b&H+Un`A!1 zIeeoe$_Rl;k?#qMuogAouvPl=OuZob!Um2?)C*KC!1|r-{FYQbae&p6J*!j|q|D++ zB?ZOMj!em9qCprZSQB`WbW`$IWbd6aH+cbw8x_XfN)Xt{!9y%0DE2!0Cj(a>OGG&npe7W?Rs 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%0AV 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 literal 159619 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

    gCA#eP(B6B@~0n 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

    $<}oKwgS_qR^+!y<6afPlw*@# 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 LYrwpGhwuI$0rBT` diff --git a/crates/zed/resources/windows/app-icon.ico b/crates/zed/resources/windows/app-icon.ico index 321e90fcfa15d8f84c2619b4d12af892ea5cda66..9c5761b9e9d25361ff30d15d08e524c7be93981e 100644 GIT binary patch literal 165478 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�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 z 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

    vDky&Z7IH+P+BTHvtq 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 TiR= z|2zf2Fy#dlys00Dbk~GqDph#ISO~-aI#7BjN5x3SH9XKMe=MxnWobZ!yketdgilFh$Sb6#f#aJU^7V5j963Dj^!*&nNsYP$N(HL4fVHuC3X9$Ka>KeV_vVqVPn34Q_nwey-tLte9i{y(i!jLznjI(AsWa!e*I>hZ z3z7oqwLg9{xmJ$Mz6l-m>cjP%&DXsOimrIOQ~>lFJ-TzFn{jA?xu06{#T*EzO0365 zyYEfHe9hgXoPU=tgQRoFv5$Kh4mzz=wR?~|=!@vS3tQtu*pI~f7|@gd;K8t0gf8@1 zZDg1rr^JB=*SbxBta`Z3LCW@StR?;M$3a*HcNL zfzkCr6x?|T^pD%5ZS2{&1YcP0iyp?}{b}+1FWC_$wY(oP%)7n!3`WVnj*F)Ev+}@F zRSyWf4*>K|K#+iO7|TOJ50ZHzplbC8;utzSC)U@gJ_?mj-{-`?8?6~LL-tZ#$z>Bj z$;XWlNf%~;kxk9ZJj9UN(7LdiE2d5>vVy;9#2?)x^re!g9<(Y{k71fTQGmInH7$>7 zL?98l>L#&J+x^V2+S`_HW2RQukAIV%MR3m6it-~YWO#)bfGF%sGGQB4|9CYl=D^2a z!C2RRw+Am3Qg|SM<4s?#xGi@>VctDHO>Jub;j>+ptAvaKH5MvtMJPpJUqC6S*tb#h zzH^6;oqX*o6H)ytv@-E!PLgjSYk`*;8v!wSvOhIyvp?rHwecIiz$l4HnMjUknyMm6 zE=vJIfY#FDwbDCYE)!Q5g)XwE&m)-W-8_ADcr^zOYV6Ni)QgnM+|UGID8fh8Ib7Kn zTEDWCo*%InFj9uH!gbJM1HPQ7RGfbQVkTm=UBH&Xp)ANrTMZ5X=KwfJjF~_q55ltu0 zoq8+fKK6ZB^ce?XF4w$>n;+hfaP&y>O7s%Se@d{w`$K}8VBoA|er1R*W9`Dj!6Nm3 zqjvqqj2Pm!?^GcIC9zGAglztWv1cxLB$p0O^|W3JhIL(6^8^{jd>OERW};1c!ZdO1~-xG>s(IbJCrCnuHZlNiN1_aU#dSB z(9sQXVGy)g?nZ8f)EDLDZB8xg-})U$>wXm62oh;0rPsgIkiHR*FeFTw88m>0dW4LR#QDgFLW|&FxqEZUkUoQy66Ixx(`0x%^Cv4=h83{y z8>-E)m*~n$yYP@}iVim3wx?!LvlFFRkuE+FyyetWz?Kj_i%Kusj^4(_>DZp@P@Y(S zYGPiaKI1##gqV-Mhk-C_qAbL$t?*g4WBa!WkZCu6c%H<3<8bTRrs}e%kJ5j?fK*=K zd?p62vi^YV^AVR@0`IhB_|28+x}F#W55$U6?Mat&u}xMjk={tUTWPH*C~~k+c@dQ6 z3%ONLtF-ikaZVi4=o!yiT`#u8VE{kxLzt9$P~&qh0`T84eWt?_viDO^&Qh29z^k{t zuT1r+RY;_S8`Eungs0Xe$Fs(BSx`dnamDQP%^5$i*=7lc>L5C!rMFmc-aX12B^v1% zCS%_#+SIhOtq!3g*pFBPxg$}MTQiUe+t4@pFO?L0p4p6lEBT*J-Y2Cg0^*l^7=xL^ z%94{(H3rj!PcUtVkdY7w%*DB{g%OEnE6n(olV^Zu1_?-&6r=_3C}Y(;D<}bW7L#H7 z8(`U?w2}MJo8(*OX{wr0Go+9@OI$*YZ&-Y-<3^nPH}Ag^xnEB;OBmx(E*i{#Y6-} z=Gggip~@ft+PVv8rieuwD^?qns|srO2~Ra(;pXTqV+P^uzczyt-(_PN+;?X*NQL)? zJ-F=T6*ulNw)paIloCpiRWo`-?ijTaxyct{oChcm2cSpwuEWT(PZA=qL$u;M9E^&| zraDpyu^^RcAPHOF3-RB}5A0b;m&Z0$z9=yzLJ$0( z6pP~XD9Zw$T2GW%F^k%ypAbKd=Qn&Du6=6!m+YCs8=T%4xuy3%d|Ex2UJ;Xz@|1B! zd|`t8gifo=v4@LNndU!Dw%j?T=;uqu=2`@A{ViiT70Q9{4yyU9kWFtqUdcY%F}LzN zg{ckXI z)u<2vOuDiN13U&PU2D^_KUzCop;=ujX;#f|6@tK^A9cH0W(_Phwa1@h)=@e)_0=No znp}09Uh74^z9&Vk{^(kfCRTjrq#D~SIvkh}{n5J3+Mc;!1J**&;=7MMzwoP+D=79h z;nxtD+z^M>FJ9V>%TLoQ>_dOj2Q6RQ6Oqx7p7sKlq9WhMzu9C{6)tGD40r`HBBr;} z|59)VhlIb33F^Y+7!#v;XCAd2HX#beax1oEYQwF(CdxD(E+>KWj81I_KSMMXaf6K( zj^F+eVqkXM?UFow1#<7nnQJ>unG-pm{?>cmu1Rr*26>fHRunEt(a4eE5-z$mq+?L& zSG4VxxOj1BqB46h9n7xW_p*Tb_=_C0y@wiE%;O_m)oH7oq7$;0NJDYdzey3aM*Wd1 zH<_a~ALt2d)Z|x>#VU%p9EqCHJ-RN`;G7vOL=d#qHybzSMNsRR`9g_^Lo0uIP%#(c zR!`Cqm=1an1?xTyoIM)C0C=$W_8e^oQFe(`U0fPm*()#rwMN$J^O8W_#Eh!6Za=Aj zNpw(?s>^Zl*%#DnvK$l8{{iwq4ZkB4;7S`V`v%*Oyia&bowd_DR|wjK<%`o(skoW) z2?=&w+XZm-;>C;4K8*pGOC`7dx&{E;ci+9_0{BW{HhN$p#e2{x+)~q{=not+9jrSS z$ltd{7rqe=d{hXalT%UWjCYtmr_sUciTk^aI>gO@d#XY~=@~B80KfcG!k=|reI6Pb zNho&$031FMMj$3;In3ftA;usAM{?l-E>Ioq(Tleu&;Lc-D}bZ@gYXw#ttQ28Z3mz0 z9>tMn2I$76!V|P*bl^4H063frpbI8Ki&%PsWE@C>J$Fk3*=97YbEc~hWGC8XVSX@8 zm(kUufnaZ$r5AOJfzlpPr+_tfP6Jd1t8!e%uaVrmj>RO4N{Vvk6nJ`+Ve^zd!gw;X zbrDeIy5=mX5W{+iFh@x%;S1vas`rg5^2G{&Wlrr*TI9N%c&&c77+(GAS8+GM%U3i8 zK>2{z{vllHUk(C1I2hjegFl!800W*Ka3foyL3C~{v$%;lY@6ncpiSOa4Ys2d;37@O z0|S5gX%#c@173Eb3GGmc9k5I!Jxerx>3Pt)viunkvN|4C8(QSb>O~Y+5pQeeVsmFn zj(OQnjib2w=qUi0Lms#7ng#$|xNzaY)pGfj-UWcjPi!C=2BZRDU@Y-;8zl+NA}Stb zOHs*-W@iC2n(hWV$IO{AxejI3ia0<*sdqG~Jbzy=9FyJ&2x!dKK*0cgp9Ei5h|#fL4djxB+6cViML(&yHdq{O3ciq6l5o9jE9E=T1sq#MDkN$ z+EMND0gHpM0EmLwT8kw`C_vAW^us!pPv|Iz1hhv)cN|1TrF2!JuU$Gd(*lTPO^7T8 z&R1ig%$Q*hio{j)O|r&qC!8AUGDF0zAgPyJXiI@2nW#DXoHRfJHx^S6n-8f#8DTL1 zHQsz}Ku6yPu!^-ra4cVkvd-3BaGakZsEjfM48SC}(0QV3=$)^7-Rsx|K+TCDY-HPO z?O#4RW=jl}4gP^2c*AgTKrVpV0wAs!anEHxlA=rIbjfz9Y=s;*l3N6d%dR|?NXUYx zDHzrIc?hr+3m}nNpeOy)++VI_F_-vV%6VYktFc*B-Kes9CX>TkHno-mWye_0fU6RW z=`iS|aDrRQuch+ogNGk}_&KK*fXOAiZ5PY}0HTR2qcEb&fT&~3z8IVnxW&Az2UE32 z)^v%fVyYSMbQS}@A`rHoL1jXjwAdD@rV_te@Mr_T9VmmG?T$q}^J!Ud10X(yl0{W2 zBD!32*6g`wK#8o6^PqQgPX+axKm5q;DDqP@f2lW6$~|ZP^s@eLC6ot>nNcEyeOyBk zma^xlDCwQ|B-F+Fq0MZR^|b!;(kp=?-R&n4{4A(k4GFLD9;?PG!Yyby)(c&KHYD{f>2x>Yyb#h=^Ko2Lbmw#9 zsx>{Chw+b^jii%=Ib7Z=0lXQYrxBsJ)azdRy5ZT+e)e#gy8+7HW!gV>i=yWV0KD;y zZyYXP-sd6tC#f#phdE_lb0afjPhVR?K_HL_{!V9x~uMU^`~ zm};G+;vU4b$i#>K3q8%lTO(Nb-*TTng#UdkELa=iy7WiPY&oEDAkC|2MinUFjVxvj z@hRuUkoP6o6d6@Zc)L&SE{1ii~wtx(O?VzQ-DQML+MP|37(t zuE^RHIc{(%CTaixsh8g>)(8S53&3-p{Tu@TFnN2kKLi=m7GeP2^rm6&(j|VEkgcOH z9MBQ=JIN0$i$P50WN9F)0iADYoh$J^>HhkMqXfFrj?b#Vz1*sote>Ry`=3UuLUstg zXc<{bLt**5pfF5EtbL^}1)h6`V;_+(AVw$Y6Jk(K}}pDbcGZWRqs@H zfsdOA8>S%yr1&afZ1%C{Cvz%L1;T_6SGk=8*C~PyE5#$kfkw_Qu5pho#WUKa_CQ{= zLSf#PJ8ka}6-R>hRCo}=XWyM5a_)uA<~MiP4FHs~sR*}_p|=NoMy+&jI7TQjVP9Jr82W(@+RLT_d19TQ($K`d#jAd$|Y-% zBYj$xE!IXUrOh*yWi8ePtMQ)z4b~uaER{DOfvji@1sS6R3g7`;P-0W0b0IgMVyi&u z09o$AIaJw*ago-Hb<+chr7+Wd0z~Me5_~8radbWRl<0KCt4AQ?=ve?Nt5|Yg@ev7U z2E=TBFR8)hG?cgo(xgeFeC_MLYk1Cco+B;*O*;fO;qipPPkIMg0N(J1H*h=P*bUHz zm+B(bNXksCKL9Kc(%liRPFa_7ud=VkLsh!+)h|Z@-eP|bs}FvmZGp`q8rq8EG@_pW zEG5hXy@W4Scq0^^0#JaWP^>e@2ai4W*eL**;&5U1xVZfN8$?`Ze zld^j-H{hWzgj#(~2wnn}7!YZ6uE>CLxVZ&v=){J=G-d7l55UvKi;oT8_`(+q55D(( zcAsCzx%5)pF~jiU7rltH2kyV`Ua4x9c`(V0f{*%R~^nC~m$+aUy; zGSD`xj?!yB0K3EmL8s=BGbIj(*L~MtDF8re#iaCG$i*2tB3NJgVf`)7ggArD~7v8V5}R?PUE5cyNb^-6a|r>G={Yxc)`-e%F#5 z2R;Mc*dI3cC-glweh)Z30MNYXD;FzFzWkCwg6o$ zIoMRy4`w_m4b0UB4-W?t{x5vt3y1gK%2dBZ_+y@_0@T@$3N%!Icvi1BSHeF&u>V$o z1>y0&NELkPLToNd#)=}b}(D)m}m>lJ{Qf&qHIZ;4%m zJy{S$<3$F8oM(nw3Ms{%C_5~mgz#groZ;Gft!Tb%VZmA`5Jb@7-};`{51;pWpT|Po zKvC5K*tI9uGQF2BT@nibmjXW1P&I80^E7f-I*>^tq&X6ZAa_dE>jU)3YsqexYXwAP z1UZ*S>2;eL+$mNPi3gz6RNfm`S+D>$hKsfpiX~_0RXlhai&2m3$E^+c3IFdQ5pOc1&BteGUUhUS68=*MD=7P4o|Zy98dKS0W}s<#)b zD{7?6*If}7T82iDBJUGR3*IeSqLlZ&)x!TJFMjdxo(CTs?z`_^Huat~jfv)rD0)~t zXrDYY@(TwGNZpe>ZmhS&h}aM_zGzrc&;lG(0fni#vPK`EC(gW}Vof)Wh80;OkhOu7 z&MNL|j^38l(FpKQ|77v#JN}tnR@%}EV=uxHbYkgs*{RSW(zWCt=G<_u?=j9ca9+Is z9Cx6FtuJ4j#rue`A#If0U*gM`LR^yJ!WIQ`lTi&11#>=VFDeke&OOFLR#{w+24|B_6rJ%)^x$bXQhQWCdIU)H7Q2j zK(0Nqw=#JPKz06Qajsl^^wCoQaI;eUChm^_;EcKe{%k?fsC*_VXXRiPs%_1Xo;izI zn6+57TM_DHf#LdTOYoHX%QZUIM|5*B6xMjCLR$Od0RWJvSR<&inY`Z)bN9q1&o%Dr z-da2Zc7yNyH&^(Jd$V`7exqMgdEe*%Cx2o1rGNT<6$bt4gJbmI&HjJ!r7s=c{lEii z_E#z6Ef{L{fRG-O8=g^cgtMA)n_xzjbO3&y=}yyetcy@mLwR>h6-7K(DTOP~c6*pu zAgF2@0WH$%d@!bIPTl4$e&p>trsq{g*RHYDe-PSlQ_?cQ7^lkTfuqkQ@OI7*2MuyJ z%Q{X%0DwfX+fQvBw=gvbcB@N&P5Nb=*_8VA6jW-3fT&9zW8aL*qRK4R1RCSxmo5$8 z{7v69eB+B=H0(G|C%q=^*45Qc7~6A>|?`E{KS7fP`Vr1RTn_7`~m`=+FDab zKk8tpL3W8<%a(+`6Ss>5z?vd4w)mV5C<*`vr0QABt47M2^i1~kYt#kgguQ^I&T_kx zLicbV zJc?XrhLZYU)#vTnO-}hn>8YFxOC7?v>?j?Y{-oOyr*jra)bB9?ojpb5pcahbQlMHz zkeDf=(KFM(e^~Nff0)<=XLSYK7#G8E@#4kdna_B}@X_D>->opvEQS&$)ARY$n#-qfECM;*a)H_Nxk(VJ>{szY_cWtQs_GwT2OFDk#Tiv@fz&|J(+^8W$EkB# zpo(TLAf*!4h6)vUz%~{i;w(KKsT8*n!vFK1{`BGXuYWy*m220pNiioE=TJaE9WQa= z!ac*U{K~HkKl8IcJM8Z4FyEobbmnQ@7RA_*aIl@Qr*nX_>EkXM>Z{H~I3UxU%zNASRaKfRxPn#ij<$DR!x{?`8hh}%G3?348N?-3S&!>Hd7HySej#* zKu-m4zBgmcQsOP~usTk}p9>Ep(6y6OG?O#gqY#`u`hFY$Ku|&Z6fGG-7l@rtzOJt| zViIB~erUM)9PE_!K=n_czKt%0$vZlapwAvT3QEI^M~`LL zWY0Q+8?~ZO2S#Bz)$I}RG{Q=}dv-3V!}{uJOKJbnix)3`$teK1g%W+uM|l81rLa~P zE}Z|l<#PFTjV8l^Xa)aQjG`jEy0j<=YieA~gp`?sSbMqf{LJcs`6)qlEtu>!C@0?3CxjTT5gc(R2tZz zGaWsKEJ^f3d&JTG6f{H@AZ6g#1Ne$T6CR96^Qc-#`ZYlGF^`4X%W~Dc(zgPg?(7~n zy`C0F!k;7m$?|}rK@CSN6odqdl1Ks5J{7q^pj;Vs8+Rj?4ypcF%$-> zgfEMG6(nUQ{y7?0*I16+HS9{*;C(&3-Ug6i#7_;jcV|t9`DtQPcHq zT)+$?TVb*lgX#^+uXRm7G~d((hpii|ltyK9#)k;YxG5rIn{^@}t2;Y*9c- zQGFz7a(kEdhxfeaJskWuW)Yd%eOik@`lCM@KK$FiJv`;!dnM3P6}l=APff7?U7~@@ zTm=})3femQzqObTfDp8IdO!>!GV4G{fkZb*1aFBZE`t?~PRgRnP<&}y{I27(Ykm5g zYOMr%^n01B!kX%tLignzcrIpNr^VK$XSt?W7s`s&At;i{D3h^Kxckan{F=u7!8t7g zRa;JDpPG6K<+;^f;%vq1^}!Yop%h?Y7tS(M_RXE@$^wMG0DzKpd19n$nlE2dX=%=+ zt$*(4e(rGg+&PZ;C!vD5+nQ7{0M6~AE|{}t&kq0MqaPjK{Gfnhdi(09ni(4hxtjg!HSbw9{ik-|c6=|p z2-ANNW_3q5l}$mg20<45zeOn5LIz+Yf+`4me_t(N=s5y};FB*xK9-;LC25_0tp@@@ zwGfzMepMil__2}w;K$?N?Lh#>4|1~Xne@5(UZ!I$$*rKoz&5-7%F(XCPT3gee|P)h z!8!zxH_?00p{>B$iq;=gy51 zG^NTRc(N;lJqLhp6fTzdjG@xVaf!DYl0}UEmL3@hybePQ&-SrXqe?<`Bp#uE~yi!?MC7W1k!|#6dcLi2uNNReIn=3M*mp$Jz z`AVaAofiG)Q2_ZD`)AbxwfJQ-hFzW!nz~e`-m*5e7Va$pQ@``5N+LmM=+hJOuta#q z7spWF3j+$Q11*4&cm)y4nSk#J$7mIZ_nOtL0|ImfoFzg6GeyuA>q@Kj%XqnfQUFHY zVqoK|1d#(xI<7cw7KpA3ozV~S4G?!3KmeWt8l`#c?(VWB>vf6p~lz(;)Kv0Qt5%zgIUEr?f_~Q#1puDQbeld{h zvMAo?Jz^b}Lwf)h09>(jzgk`XzlR=r=&Ocdc;plSth1`O_F6gtQqyf0J_vy zq9V{jKkWb2^|S+89JM79-Ce`a(e|;uL(PR;^d4DDcV&zT(10?oz~|=o*kg|kU-o5R zHhl1dAChIiNdOQ9gzU)d(kx&G3TS6M8NT{krsoKd$Ks&hotw zVA06&cMcx>oIH5R1aWyc_OXsi=kJ~1hdW>j0F2E|=-a33C2Qy`4mw+#zYoa68DXo3 zB|6lAsdZ5Z5AkG+c@}x(=dG^2;C{e78ej#05chmEvTv@RF`_x!^=g_o`l`}WZx#SV9;L0z zZ&KF4(hCrW$7zb`|MSQrkNnwT7#<~kib8>?xZ?~MbCaXG^|y~(hi?7*Zsik65|Z$z z|BfzPxbV)!V(~2=08}qiIz#6v_-pwZ2x(U~Q%s2RKO{J%AV~2op*Dc!6?YIUss(1H zDC1&y*iwYL_*Xd~jf2@_3<6|?@!*F`WGW*OAD}SOo|#_`cllQQT@o}$&ygT6{Yiv> z{owK(vH*P1bDlGN_#^)--+VIwU`lxV=V$Mky1ys`eN{@t!FmAFuO& z%a*%$rl6z`20RD=ZPjUMd5W5UCvZ1E%QW0tp!qp^?RIQ#AU}V8ydXhA7@b{hd0^Qf zbcsBB9P3QO2k)?00K^e#gul5tY*3PsA#+#DM<`H(xkai~AVckZ@b;4J`$=gysb$7;3u)+qqsEdb5PoJw5?$kjQw4{O^(0So1cK#;yV z)x4FcHE!G!gJUG%MJi&zLJ%uqfjPy)TnZVv18f2wQRzq(%ZqMASAd_}!Idk+z4zTW zeBu-TH^%~;6aY*K?g+_c7m_Sh$47<_s$-J`kAA^6ag-8tc7y> z?nWDRE{V9_<+(Wku;4!g0eVZo1PJl#6UP+iXVgsG19S6cZeVJjDK#AT)QFaBd_?eb$1y4uZxd7^5=$Oc0L8tV z2t-fMRStX=313@}1Za&|G6DdrTq4!@)@F!w)_5HN!AGehL7lsO9#3PZWn3fa~Ya zp8c`q#>Oiu#9qm7XDZh*wS*}tgmRP-D^QNUnfug~CJ2=BsfF30h>&!v3c)dn$zGU{ z7~xC)xHcWJSOqYv5n|S7*iyTqKcxvteZROhkV524A8ExwZSWF65i1hZN|0$8~@qxmbbiB?phO-PYHbg%w}HU}@Ho4pBXfa%`3UKnM}4rV8&@9H^D@JF9LG;`C?MNAj>i?>kN z=e#n`RNRlDXz91~bx`6q8n>~SRmKHHx^ z85h5*OwpyYu&X@ji&?6cGNDgkEx|i{+<{kMDhs6JH)@2m#%QQ&S;CQOB}xneR92Z9 z`W$j;=3-CPG2@}K@Xnf{F$`SOLw>|3uO*&o9cf))4by9Cdu?YM#*a-NeG0|t) zx)e!p{C_<5*kfNi48xxK{wb%Ekk8GT9&Vqk>^}MA5a|gRYT&5Jv1LokFk!}SVXVb1 zAO}p|QJxGF8UPdG2ijM_5+H}q7zO>JmIg-YC@P~LQ?4@wF8%(;|IgvR`|guigv8ev z=i1Z!GXdH%W_emB{Ik#31qpKnU-hnu9QiS8KpdY34UG^~5LyX7w-$VFVnG-K8uos! z^-$8Jroc|Q&vIle&2DVImOo$A>$+o|YMY|~K!pbOpaMPO3qWX36|3NGbHE@8cw`C_ z0N{)q?z!@Oo~H)^Zv91(kX-pHeNxZY&dx5Mm7n^_pBz5$fe#Gl&!6K$L-cjVi_*`u z`dM}6{b36=P%@r0--0QXCgt!&uwQsaQ8P+9aZ}49l`Ge{i*2{pSUB`+?OK072*F`=Wstvzfn!P5WdncTw?0@JWFc;Ul%} zsnMqOCcIi&#O4R2y4U&iG3pwf7Q2^4^(z>V>%cJOZvUBnVjaNz^F;$trq$e(IclQS0tiUkU9$*y_XI2&%f+y>y~F10k9_1K!%zS8 zyM}-DuRb|kxNvq@ELIY%p!9;g{ZvzROl?W`={zL;m-v@v5YgX({+i?MEd-jzH^;)w zytELV#UfR_v1iFtz|<8fuwr(A!kGQNwqF)L2+cn$1mvx$w10Gsx(z;b@#4jokmXrZ z04WXd)CKTa#sb~~aB%M2xxceqE&q1bxFscA4*mciSoLnU$nhULoFeV)X_dUNdT)lcQ{P>Um#PF(Dz1nV`685+-6SOHqo(S6W z_e{OJD~O$D?8jFDkxEDfUv#*?FRkNaAKm5Z#9Nzc7_YS}D8y{jIO@)7y`&3*Z0|3!A$mf2cDZ!M+ zOof27Ei*lv`&OVMs6Bn2i?KG8jEP)47-iWeuC=d6u4Cp@EIh)X3!0cS8o!vr?@Q;$ z9{VK=3|W<^*9Ti;hT&H)KJv)7sP<37|1<{Rvn>4SbyW<&?z!FX-dJsXp9cU4@*)31 zJpce807*naRN!dU_ycM&XjsT+4lT;#T9QkN(1Q(@!f`Mo&izFDZ|0(BmkvFG`^2|c zQk?ubhf9Enzt$w$WfG%B78y_&hBGqo0|Zr5t6rHnfdFcJ^0JqG+wkT$|IqNXr#&uu+4HZt#e$>kOOP4NP`ZiYs;M4+e@+Cd-odf{r zq->KZ=Iq(Ce|5E5z1b6M@z1wat`;0FDzZ&LLjnK_U#SLJy9_#KV`N&HI0XZ#4jcUR zK)#9T%lqJA&hl{y#HebwfDEkZgV1#TlWVH+*m>1JM_n_UkNc&pkpKenv0c3Q*l_=S z_YN<9$xDW>fBx4G&w0*shR=E0(>M)?O>k<0HHP2Q>Rk(78K2VvI{tKQM^EdH-}eB+ z==~7jPT-^(_8R8@F)Kk4fKFhA`Bxxg#DX#BJ)uq;sN3u!Q%mv{N4sZ89_(2 zHxq7vjCQiUtk%vQ$X+~zA4AC$DnY>I{N^zWB%t>8_lJicerWjQAANH8mw)&N!|(n6 z?+>5;)TdY&?rd*!S>E26s~J$nTx1e(#j&7Lv$f|pu)4v z1Two-US@pBw~yB6vW1f^m)g4A0|0Yj%AmNCp|)C%ii$V;d!Be=iKFj6Iy!py-rnAK za4qHH1~_#A+|0tB;9dX#$^y8)v$ON6&CSgp%>-7|)Wt+rB%b*89rc+BvmF(q_!S}3 z5*!1+SN`tg87}cf#c?U-1AC|me`%*`Xx^c9DCdH>rN8-#d%vsMU#$RnAfICkS&*Mr z!$K|dG7G5|So`|{D&kzrJNUQbVK}&YFzoGL9;n4JRXE$)+On((4^-yRgNi+2e1mj9 z>+eCXt;~tXl%_|=O_#B;G2*#Y@r-_G9ZN+?snZS%6^)tQIoSg{<|v_GSzjOY2JOpe zz?x$X+)^4pW4fiermTRBM0vAi0AQKvW0P~tX^3PVrQawYTWFOkqq?{0f>cGT?E87f z+n&8Gor}ihWJ}8BuANVO4z0y&$c#?!<$ZH0j=fABT05cv0s_=5ix!Q{|C`iuR-Yxj zg{t{=+-{==0r`^C*{FrK2CjJ_NHi>!05^y0htx)QeBJ4L&{MNU^Ub7PuK}Nj)p9tZ z8U*Sdp=<~KjK^QVy)zE*xTTJP(4+vJT8U#9tAZdJJ2mv7Yo{y)Q?#H~)1Tej+xt#c zKoJLU3IL`S?-qaWEdbYdcX$8t#>U1w<^TX12P^<3O=yAvyOSi^ZNGN5S+>xIwli@# zvckwMrhu4kbb{+t^5HCDhga)ug~V$?Da&C1a_3hqt~ z5=8J3Mb=W~ucC2js*gT$xe`;RlyJ=@7h4RdqoZ}?^Tx*dHk59TjZ1nwkOMRo2x6T} ziQhz!>h)U1!I&%Wt1>BAXh5Udj9S>7zNAWvV(D)GGkScyFOwO!26{~~`BXpxSg3WS zwh)8S)^rIFDxLp2f~9Ovd2MaL5%&=VpSprFVzw4q@LDCUH5z*HDjmnhGftU zt@%%#i}wM9@$?qm%-kC6XGdz}e(Vthe*5gKnx6|U4mTh*n5F^Rl69zoFu5J*%(9|` zPN~MfDu`fjFnS^|GDdkb=#%z{(@1^h6_@T^@_z&XcwhB10R@L8P!aFPKKY)ZdZ_l(_ z6hE&08v<$E0#2h(=4RkPnIP7SKUV-?XIKw>8^9zpVNHNy6t27Y4ZPklS!rIR{mS*> z#+==TrD;!fGDx+A1pp|}8@V>4IiG*tC1l~;I$yCeSZ|4n=}prbYmXb^TP8*;fu1oZ zd+JEd`MwHG$%=BHMd2kfgDSd)eECx4j>gxr*pkcNw<4x1F#-DhJ!Q@MyR-h<)ihB{=$ui~r?hW$ zc4#dci(VA+&UVPOx}q8gRAn?+0yDnthG4YptRdJ7fE!=pcZ`NT`=MuCKRWvU%X@oo z3>JV>0B}=`VEvm(0wM_Tcw=MZi+6T+ew~??tFRT#8_6*<=P&UO2BD@G}y8 z2V^OQc9Lv^AY8>F>?D{9ndTktbh7qp(bg))LS;X^2Dg>A!G+04*u#Pqp*C;do?zOe z*9`F7eqf-0i<$`qmL&XhtsmHDsweA-^m}7IYF*oC)`R&`uA?EyNXqXmqvlWCeo63G zU#l+A!aut=UAYFSnlcvgCv&T4*6PA~N!^$dJYAXU3tg~Q7zBq-qU#d=GxwE%Z>FQB z7?}!K4RXa#A^6?xi#L-MoHMGQ0RjTz>|dIcarP0*%rCu9zFXOSoFw*O>;y#9mX~GpaO^sNXPnm0lT5~ z;SX;dNy^@G#Q}2#bvZ3KS}*i(i%n5w#P7q{7zI3#B}@G7Z0R_nfJ&{c5pV9YkUnpj zV>Idk0$OugpX-UG(<~05dm_zAdW9Sbs7j(FM*)B~18QA6g^LI<(Ha5TkO)9Mv%nZP zbBnR0;7L9=tqZGGz!p>)@H;PGzWg&u5a4WDzEdu|TL1vrP@i`Gp7a0P@r`5dmnoXP z!H@0V*_R*lM*UVf>uQ3)7rf^c1Yknn7^oan=?&xo@eknf+H zt+@jxTDsiCd|+lNV{fqm-)pEy=Z(F&et7t;SN8XRL2J>RS^#F!_N~6mTL1_EJoWsA z3%_}MeB*Np8qFAj2FxhZCt8uVmNGnva4%8*5lECt^qCC@y}+EC&idwre<4}`ka{fm zo?cDNS87^QfRLbD)_f}IR4VuJIl^Jnih~His;8oMiJ1^G*D4Yl#E6eNXE@k5;X0!M zOW0!?44GlIN?WrjZAzJJO1~x@$oQ!6ziv#;=KRG=QL1i%0w^N%yNwYXnS#Yr^B}KJnJ?EqsO&=_n zvQ2^MMt;_4H~UQs3o*A=LTAN&F4scrpqS>&%Ve>cPPau3o+RYieyi(29Ul{@;@l!fm~8n#ykL>u>7e z003vy6m!qH^XGnLu^66CPHxlWEq593>N5wgDVDROJtkc+i=-qWidO)I`0Lsfs5bh1 zp71X#lWExH8TH`|&o;BaYC7(3RWM6XT(=TvE9g0hCKL$~W+VZaqQy(FA^m?OjxPKssx(xglWHs@HX{qq+?!ER$U#OnfmM9lHu| zBgeZ0v5GmLCd1{eV;k)#yr|0EhBGPa0m!WavL3#0&wP$E&Khy9kB)B?SAm+3MMEu7 z{%i)PF}o}U7CbI%!=HE5*`PgV;8w;K_K7(33XMseJq6z_tW9;U;-90jROe1~7l@Q_sRu z=-e&b$huUfZj~-m@4#AQ!an<~euoeEtoMtvJ4TC0wdNNeKR{=J0Wk?%PRH{Kg3xWP zG`O_bkh3rAt?7M~&S_^&nhMSPp-co64;#-ksOZT(99Bdr8I1EM|GMnea)+_l@xx#> zM3VNsy-X8PB#^3!liC;ls_u*~)f~P;y7<81ub-=|`VW`CFoF8hOMir^=CfaBOCPqsZfQuIyGmX;7kf0~)k%j*i zjguK=LlQO(sv@NYqA2TKpC5416snrG91gFE3&)u!8>>Fq6OyoeiR6wiJ=T?Dq5_hr4WM(Y${U2O@RD9sz1N%BQibYMeU>OmbSGD|QHK~a;_FV? zkz%0AQj{j*05P*KxN2Tu(vE~Bd7w!(jBW+Fh@K$|=J9as00(`03d!fxXpNRt(&hS%1;}2e=fsRtjg>`T}4Gvo+1p zTo^vKpu>pMdh-N^6pI!i3Y6p~=MOV1BY`$8NLE3V2W3*?ndG(nY z(BU(bMy?l~Xj8iw#_M%4%ag7$ua%mLAp~#-_XQYopDhC&tCS`*h71p`!bho6nzqS;8(H(`u8J?0jB>L80HkU6#)9Y2{an3HTTXk;t!RKwJx&E$ zJ;{+401X9_&zXKWrbK^WDX0R`G^56Ao)lEekSylJd)`pJvCyx8g1R7+-wuwk;UZnq zAd7ww@P~ppelHrp=iiItjXe&lFa`jGei^COhpKayGoA{Xhc=CJ&O*}aMs<(R&XPs< z>w9}yAXAz@hK((nfHTc3^*sOT{@&hS*7Cp92AKXspVo^xB!-Hj{`0I_ru z6-|G6e;s7I3Hit+JVvnJ;{aM<(hRYCZ@tRUDDWvFxH<4k!9zU2@m*5)y$&k$!o=JZ zDm|hp-3UihgkZ@j6~V$r20}6B#J|MRje!Oy6*`xZ$(8{?Q<-u607fi|KuH8y`B8F> zbOj1hkDmP2dFftn0q6mMY-9{Isn`9A$^zG*aEjTCF?NppKho5%ULAXS0b5HnMk3oL zH)zWi8Vlm6t}g+s;|}@cOzXa5`$vVgzLP2nq6t__ZzYbvXGL_WWibpK@JWGz3|eIN z6e~b6Iv(%If`Xp0k6af7E{@&heRrw_UKRtwk0OtvjJ4~KVoyj|FxYckx03a8@ z(f0QC3pO@4|GO$T-ukV#8|_^MQxmPsw@EcC<~1DoEqF^J9i4WnGw@zumIU---HT2g{Qm!P$h3(V$C(h$|O$JPVYisHfZ zPvsZ|t7i}`#@5JaTn1ow6%dxN(?rropiB2>;Ku{Dc8^Lee<(U^BG+O0 zFabGd;)^h?TOWwVIknYP=HWdo^&U&W3IMcNRzn^>ogr^InbAw5(3?A80XUU)aO-b# z9v5duzZ%>pT^t=)3}*!I^1R^a@bK-IFJFGWx}8q}z?9&;&ELZXKtX_u?d|P9y?N%$ z|5S*)B_t5GXDCzP4OOOC_{6kA9B@i;*%0$AX>l-NwrPOgWPgJ-mYSiJ6*Iq|H`!JT zTKn?RHrV?V%jLB|&xCz#hMxuig^|fyq8T5lfza^ThtSyiF~Vu73ZH$&jFkIw&=aTV zxk_(Z6jH8l0RX2~4FW_Weuwr#cX4ByPC(x@eG7ri0Ri%?>2=87Da%k=HD?xpBB-WE z-G7JBuLvyC93_A(Tc6^t>c6iBc*S=*0dw+lbAE@j3idq1kl$`*XXP=A}+%b0%03a6t0RRdJ{G7Ar&i(Vn zVsXA=`qnP9)JXT}b`H~R%!Q7=p;0232w(ZdWt8tVnGmX#0R860*%aq=M)6BvRySdi-wvb-_{d=JQmA^kAl|gnaZ?unxeh5 zX?UKpS%A3K$5pQf9VQwz_2VB301(ZXilHczH@d7VEo#U{S~nfz;{Y<&_~N{ZrZ>*) z=bd2zU2Xeonv0m8iGk)R&iXs+x1D zmBf`=_9=p6)~Ry9dZ%B`lxX`Rv@@-HP8^N#8!f8CF*qY>j45#LY=2|PYXXRiIEBK& z);wojl1H~1`aFuOl{U`}EbEABP3S8oxkX3e5WXR!oLX}~w`*6g{-tZzuKf>M1c(3t z`Or=Qz`V`9i^a2y0G)Yg+~he)O5MRTGo&=DiDERwOo)gx zh8Y2Xne7k*6*>t;%k>~OiaLTqmM7=`?#@rcs16$7zxzg}Gb!(SF2S!xr^n2lp>({S;!qosE0Pv*j1n5}zI!kazUF$mna#Nt4-QAyDE|<@rQ#GS#vTYSPA&?IB zo?r=p!lww284g-!x_sAqtU}0@j7E)r96Ysu@Ia2hu-UP|NVK$Bwfq@T3Xnx>A?`lRjy>>{~Bn(fv;+wYK&{~74 zV-SzIcwn|Bcf}mj{#xv+1BA=R$p!u+YShFHsi=D4VcMUA#!N1|7EawrA&G__~ zOCfcq7$X}udUG{Gu8Yu?pgul6{>{z0Ju^ZwU~b zGPSjQWddyRAw^xZMDd&>#ISm*)1VNE&9{RU&QwGy{b*Tk1n z%ZMit_q({*P6D=?H5^+s?7cV9c)seJa^V^jkLmylWG-Y~{)C3=5f*@|``a0>(Jeir z=WiMT13tAi^12(x#~<0hbm=9-Fu?c!r0xR%DR<|K`Thv$M4%inxd9G$c6MH~T&>;| z84&#MTs04xF$H&pXyTh$45ZIMD`bxi}D*gv@@pZZ0u4% z0(4dXAJV=+8vG`?##)lrGdC43yhh}&Mv7h*E|S1UFiR=AgfhJnXb0AuWeoJ!z0aAv zT%{DJqYwnh!B$ZljgMQ9V59&t=|bj}Oj;jy?HqzmT0O{mookC)h7`zwNAL6$05F>? zdrE+d!OY1DieY3z=VG#^xo|!?wUpH&SaF@BKf2md7v$wiv)Lo21nte7CF9Zh%M<|c z>9yIoEFkeF2VJZO{Rcq^Wi>%)I$|kNcStxxwB)26lZELT_ZE+?gs4$}6|t+X)%8t%kfhG0pNLmHbWz!Lm|dh%suTT*gy01f&x3 zxWIVSiJn__WShMxhek+lVvH5v^Ge#qEdcOpcnUZCbLX(gMu(Y8JG`vZhzIckZ!1%rUC* z*DIOw7-s>3K3|VSMQT=Z*98GjkABV-X<5|tMrvHd#k9CIvtAOmj%nW2?C*^r7Qw2o zs{ufCAHpqrB4IzaBojb@&(G1(Tle?(zgJ}qSplBZWq|J{0DxjZ1P1=C-Lq%^7fRD8 zXRYp-1|igHqB%2)n*E%k)n8m4RsGLE(c~=nIGC*#!Xn&+S+;cQz(X)$+jB;&0xWb< z4mHc>cbN-x2$23oV}TI&zSJm8B6Gvv&;UFGH2|P3i7V#n>;1#^LCt_rs+kX{#5xpE zl<#LPa{w@kz;5nx0ANg9Wtki`rgV1)Gjz%L&{C;$i7e;E)!pj=SObT)*OF-v1@(Gj zyPXVIKGZbAHK)p#)4p>+t)`vP`9YPr?0F{z0I;CMXXXNEocaM?ZH-Ywzp?}saC*yy zT(dAb-l@%0pFPKoSZ{`cqTAc^q!Xhj*JCJr{lvAJ}TiDY7TnbRoHDV?&Lfiv@!nD*ho$eMY0aD#=l*{$Z zJUBwUHLFLhJ~QpYO>%VoP!01ZIOksG)4Fk5Av;K`{9_LQgz-~mLm9UT11(atTv86R z03J`@oTI;34%ZL!3V(4sz0cDN93e|_5upq*03Zad*Um&B6CG1<*O9b^9?`1!yTo75 z@6fiuW8VCefSTTMo^lUxS2(BbuWPkP->U#{j1gu4doJSyKqq>jN|!fK?*b2)Ny=Ft zggAyd0D!Kc{oGs;yQX>pZJmX&buF^LwW2YH2$m$G4+`EqLXfRmN@u$~Jnagt9v@t} z@)d`Nhku|M0GBlY@JU+*kk;*P006!gz%J3&_Rh|au2!r66oMr4^EamYM9_?bMOnIx zXxl}VPcp$Pj0|s*f;476r)&T{IQdxgxtgo)BWFyAKqGd8^8R>vu0BTw>D;}gB3_xN zd|!lx2YVNPdi@tiXbJcPsw*y0+PYII`WYQdx8=4&d`nW(9wh@2;wSMBbb~;$cun4mKHe! zg^tVKPX(s|{6?S?!DWy3l`q@b;&amxY-7scl%iO`mkrRq9nBT~Mjv_u(FT+#>8zSW{^|6h@UMYt9#F|ZQp-mZ zQ0BGiyu+_swW3NG1cHuk-1vjbdwXA}O@OHU?~}d;z}*4>NYzC#AOZl_&unjh=jO)7 z8(QI`zb{|;1A4hYM<*I2jvDQi(yfyql;DB1QQ3S z!Q(ZTn*zxkR^VT$2@Qhh<_P`%ii&YiI^}srP#F!|+efvS@}>QjUf+KR7!TF8ryp5^ znW|FwEV~03ZqfCw&Uw-2?#87MK9QVry&bOII5k?^{!1)ZlLo=73A<^IL2E z%=QyAmAwX(?yUhDf7cF0eetgP*Oc)02Q5VhUT+VKNSH*)7%21AgF8#-A6rmA`5^T9 zPcbh?jZ(88(+5ZMv9)SG(k^wDKGgw*(2^}%qXqzy?SPn#WQQ~Z)Z{~|oc7tMY3yby zo{o;5OR_zUr?wto&B4Ou>GSTbvlOLRlR}^542%gKa!0YX!rkXx53oE*Agr#Zn9Zu6 zbpXH|h!?GBQ<*IYQsp%yi(RM$XoKy50Cq#TPNV*Lao^?0cx#pf0X0RK+%?(0#-Z3L z9TC_F?rietb&%L20YSg)R{@|n0(k0d;v6xmBJ7*n=gkao#^z5UfqaVC{ z`SM%U%}nP1Cw&Uw-2wn;_W%H>764fTp1OPX?1zTM;-1>xJLhOCqnkxInJ5h~0Ot8Q z03Z>(`7os08ltFE;ot18H}j7{mMG2TJ_*`#{^kHw{5aQFWIFo-e(vfgn0v$os$Aq5 zaOOK^ty&BBF!P|mN?0`#joi%S+EQ4AJ;q6K6^OCO)AnHUlsfs~em$ZZr_0KGInJv( zFPL{z9oHft6XmU^j)T;da1|LV1}bY+32C3HV%N6oz;s919isqXy>hN6+l8qWsA(`> zT}8744~=bmI7G5H)V)!P5ygH8y+>6*#a%R}b$jlRJ-2%=_d`J%U^o|U|HM2`E-uY_ z83O?B@I=!;10oGd1R$XKvPJ2pm6Cb~uU)%FC4m1&eJlZhCw&IM-2?y-2S5OTf&n*n z&Yu05#V~von>-1@S*N&;NVU9oDKp6pASmtSSbbF*^fvDES0SrGauVw`p4Sbr%uP2? z0ZoSp({m9#6<1ZkmkaaO4IgwA(=6rwT%OMqyD)SYse+sIy(6IO@G&cBmk)>DB& z6{;NiH!3KInu?FF2$>cTP7T9zWl^(Ok_z}(3X}v=64Rz&r)dBn!g0<|EJ|a$UvcSwDE`onlDm+`a9H(X@L;bb1jouc zDA62&6}@MQXQlJf?MeeT`8!-7F)UQAk6jc;M@B1gt+Vvo_-CncumFV8g^cd2D)>^{q#I#^cH~wIxS*d ziokVeu5$C#XAv0{;Eml_62lBN5Uc=7;4Sv76iXveRbs3(BGBl~)0E$HwKDwL{{H^U zwBQe!{-5++0PiLMKy!(KfQ!{?^(DJ!&%Te$hlkp%q}td7zeo_85=WFF+4T#l32*Ig z^%|J_uxKpC@zrII0$HsSe)ppN-X}$I_dL8d|kq2)AEgoTABSL7S1HGr}(-*AN(D zj4s)KFbym>X^(UVYuaAx3HSC=tKtYaRZ=yw41_XB^W|nE&@@CNWVo>z+R#SHoy?J| zS#noI(F}d-)O1O>&>K?uKocUU%w*eHOAC&R^xL_H@=A|gI-y+0-H8UlDG@<|NnjG|0e)IzL+O% z7eL$R$+Pw3xBxu?&J4pq76AJ9{Lb#~FE190f7>6hC^JBGrX8f{u`Gfd+;N{H6LHYc zrF_W#xNyV5Ru?sAhc0<^;XMY5-UJuKdGid+v~t@509U_@}WpmnLWm6 zE#j-11%>gAkz30iYvSlL;+)!e&y+Y=Q2ExxBLo`X0|4m3fDf`{Bb9T92v8#R+A(;G zzbG$;S=gtDf&NBo0`R^O_!40XGGeQP42HoA{Vu*|6}qrIc;Me#2Y`LofsrpwWR0zJ zbRW6HVQ1M~ESd_ZLQuexXsxuiVtJwBTLc;gR%%LUr3}N?+zwQKQzfWy&}LO5lU8~` z43KZORL*VvLQFGtS9!Ob5x+YH-koTHlJYZM*hLq2!3E_r57dvfZr6ssydgHu}* zw*a>G;L4Tf9~~Y23)K?tngBqM@04tMGJH>ag@Ql?04NsVXnTA6Ez8yFTV?@(nOcW8 zW%eYV3F%o79ZYKM5bAf0F`;|_MyG`Y9Ad$*#V>2kZCueGzeZI`m9C30wOjY&`DV(z z^i===V1Q$T5Yw{)fXw9s0Fh1u0AQuD7*5L=K{h--jTOPZdq!MaBjm-mY8@K@u*QQq z=0u{K*WT170Fr>Drb@C!si%OciCqG<(7ss~1lAy=u93j;?x8UNKra+)iMkvd&4RYz z*Y=g=un`%^jEf?g{i?Qf$-~7Q*IE1Kt%RnHX# zO>CJY06-w!-KvVszueo~d;TyC`x^LnSCjxQQeo}me@EUx+lD|u3IaS!0AOot>!quW zjkl3-%d{n~-R@Fr{LG|DNX{%NcwY%H z_aqc*Wy?eiBmzFD%y}|P4g%9gG4rY)H0WcL5U}2{+8b+OToF%e_d{T~r={{EFbjS@ zN1aQ6R2D5>QqS_C)`D!BgIDVdBTt8k0SrJej@1xoG`Eu+u{DH0GigzeM`Kjh9tKMu zrIQYz<~3){Q{p5f_*ED9>qhHEz<6_$0_u)aFxQ@? zH3_9!YbX&i-s^|HJ#z9~mZythwOMBd@Mmm^p^A|bWKBOMUdzuN&ke9f-CyI@z_Eb8 zxH9^;vp_IFK0N%HD_5@kEmbf{`-lJkNnHWZXmagCe&^m0D*{qF;J>-EyZdX}ET}P& zArdwtA|cScSqYQBauM_QN19b85+itvSy@Fy;6`2>5!1gOjHXGT6Ucft1hKk-7y!`x zSjSbEdAW(e2j2*z>Y$-)vAn~~+?fZx3RLidL0KLG*34ep$uUo?gFXNboYE{573Un! zo%f)yEQK(FDtJ?u6sFylIoHgufFF(SwiLpaNya~=^O>48USPq9-T!6k*=(MJpDGP?tqjUdQ z^Dl35w`BMv-d2U!7p0ictksq73t0iR8R;U{2BCw*m%10XlR zu)VYMQ_JP@t1}@#Hu)y+-Sf;Dt5yzOG94AF<4*I6%f>< zsfQVU+`2TYXlx|_QTrCS2=Xx*;|~)bSor6&Wr2p|nX$93q6Y!DKX&^8-N66bxI!kV zinUP}@m^`8&^$+vdl3t4J@R!Qs`~Oqolz!Q;%fnP#>*{xkmyy_w%IgT}L- z3^Q(Ph6oGY(si6eCQVm}kGzN~RMO8>iX$v+Cg=;XO4BO^0F&FUu?>E#Vquds(78VZ z_z5_WpWSE0Ox_=q<9Iz9W?`LCPk{bL%nOT7PC2CCTn~x>uySk{Rww{Uzm{NJ^e$4e z=({S=Sc9Cso09umxV7|W17&`pxb!RnQ)?*vIsl4cxVXQ!_veOTcu4F1BmbXJ-TJ<> zsX;se-=E~XJ^{m=^-7ux;s8kalN;c0YisLUR~s8Y)HM}#9&KIbTuw0a#Whx}7Yi^J z@K;NAVZ@Sd#YeQ53%$AD#-zVg|C9lHjjc@E)&6%v)p%hlppsO?Sz9!AQQ;c$n~WQl z7)hfNp*Zdj_EM~7OqJ#qhT^p(2_GnLiCSwuiz#Km6Hd97BfKeYfwCx)2Qx5HVdOI< z>{1uVxmNzRsB@IpCy&y~hqC6b=+hi}4+Ecb3xFpC52x~Ju-la8ZgU5ap#VMVo#0bK zIbTiF0}-T}H6L>1!b<{kH0sme1YO(Sg@#T2NBRKcknMxB>04o|6)%gJkMHn-%VsT; z#h*1piJhn$PYFL1m!8FR=;O?iNPB}$jY&Kp@2UYn%o%6}nOlKmG1$n) zu?5Av*$%keSP9tZ0`NB`T4_(HmyT#GmAf?+s&UyFmtW$M@9ul_iG%4f*z(ckc>|Jq za6LvNZ^y{ELnQect70yW1jsL}#Y>QbUyVNkfJCFm(<;SG0e}fxnxle&%=#70qv^tq zQ**Ts(7r_mq0fX`h-L_-NXsZeTLt{Oi29OEFwRbz0LQu9r*mD;k_h2Bl>(T$Kb(*i z3^&yTX^a+U2t;(&Yybe&laTaYvbHp7bvk%}hqexX%@pdQO7q4wwLRiui|Nt9M4_yc zRSUPahlhu+J~%jdzXAX<|KBzF|5&%X2>{TXVm$y7`bRrEJ8xSomoJFSp&=e8n1b6+ zd~{RPX5YJHJ;-F2{?d03v;rBQz@#YVGB=Q@{oBsU7vp2Dzm=+Sb+MPRDbp~(00_DJ zI08QOM-rsOZ&5!UEGTrn$RHH2DeytI!E4E6p6>xc)jjyBXnZ?dKmq3-w1ul-DBXUr)Ly0Ac37OFR)!HT7*hGbM3llGF+>10D^ zHxpXV7dw5(Xc;J2j)Buiz})ncPJV?y!{94p!rs40K!*6RZydMvGbr1DP2LtiFrV`{ z79?kLNy0g4{NzZ^EERg@yd{Y*>yoDefaOx^6Wow&5P+?9@?ElQg2KuaG|vErV>?Q8 zE44_(#Ue$Clus&SqB6ot{(ro`zfax&E@{<2WC2hG!1Xo4latzQz4HW=bz8@Cvk#|} zMGio+0+1Wv9BtO-=H^##o;mZbHX_~StUy{iCTsL|lfu7;_TdUB0W7s(_nRZdKzu!V zy|xDcAaqApRQ>t>nu!_*6(cjBd6Av)U0Q{-Ch$;9o9HfBOnXk?dow@FP_0U6oxQ*i zoUc;dBzidyv(m0a7Np^UqC{JP+I46HJOAW-vHrC8%2zd+3+RTp53yTberk+(i&-MM$r zZo&yIam7kTmu0$W1+M<^2r`P`C^oMKq1nM?>J|B z4NH@nt7)Crsc~SSLNjUn9R)4IK1QED!ChN=DT@*?n+u@byPWM;}w> z6wUwh9x}Z_Pxuvgn*{&>fPw)@@RJLGf&s6eIkWXwHa0iD8%#V6sA^y(#8_SZFGrj6 zg?}$Tz}{Bv%E%U}DYnZvZ9e)8{ig`ag#Ol|2(T$U2RBFdG4{|NtI;)gxz~37od%Mf zAUo_3=%L2D`12_OTB5~Kj@6GOmUw2ua-He?jM^DEkdY5iaZfV$&JtEakWISDl@2GCWlGrleHDeEE$0m< zQvRxVw%i-T%d1l7`Y=h+JLVKdE9O+iJoU_GDz#yig4IRxU*?;Ks-q%*r2H(*CH5q^ zIqEEYjN=(@-1v>ldwVZe4Tk*wF#jX^e`;HAZaAOCyVqj(pT%+A%&Rgt00DwOwX?JH zfz@iY+qTgSuWK~_F#u5O>QjLNec@lCe(Oww-T^E^H#J*UQ;GlpOlD-6RU@4hao@Zv z5r83mLasnYaWDetwR|kQk=s+nVV%aNH5ur6h!iIf0Bt0zZ=V7HZjYPShNzZ6)Wz=N z&NYeq2ULOrnE|Si&W`>Mftk^)e$o`GW}*{CTH?G;`rcIFj|HD<0jx|@tN=h1oQX?; z*N%Ju$u_Se9inxEXi|duB!eMsK9!y~6=KQQVZBHJfQ#*jRV!;ym_S|#f05c2*gDTr zBI6zN{o6F7@OSeMY~_SBHpDN?c_KLX-KH0qZ58cFNGo5doK8reEZr@8$_dz>fEosWYmgB{xFNRlbS)*-zL7uc1^31O!@nC^qx5`$sIwyPN(&= zB|rtXm?=fmk(dH$#3Db@lA)n{I;ggCTjvy4)2ipl?^iHGOoo2%32vBFeLaQ@42+pP zvk_W*S?B7IN_v$4-HsOkY>a1R=2!YGHh#*U!5I)#aEmMGmG^7xWU?#c+!KMawyrv- zQJP&xS{vQf=k0RXeGnPxThG zil9)xs|x92QRw|yjeLHMW}h&vQbv+iJoPjS3tP;hVw%ykQ{U>mR9`KdA}O#L=oX6c zC5-`C0I=7UwRGQ&8#n&o^5x6lsDXb}^asU%DEN>3YIlwKADnO9b-L59XG@y42PQWF zf&r;R;4{u_ZT$kGn1zr7v8nlrKJ8b(uLWlm{!z2)37MLyqotUuu*X8nT~(<(ee9Nu zt~_K6@@aLg0yNMaQF63r3ICvhRs3~AJ1g&v76HjN@XW$gFK8hYS?#rRvtY=2qUswL zHPVzmb6fgAMeI692|G|2p54SZG=iiyXL|7D8C0q7;PyFGIm?CTy#4UQV)s`7aUPg> z7$h|9+fMe6eMaxfOhf`R{BTFHXcQ|v( zpGtMZT2S{aT~-H0oBhNf%6L`)z{~R_P*3O+88&G5o1cq0RyVrl@bK_^4-O9gkp}-k z`@gDy;I4`O$41|c763jQE*k*BfMf}{zO}RSqpQ{G`8;uD4y@t2oMHM3g=gCHJ1+c% zPslYzU`nx!_*rc~l^2TnEokc%m?W6QTspx6C%+a( zM!E75T`YSyd0TY}_7~mYD1ZstL(v%PLNFBo(A2uR*8SLO$N;^YAzodl3pVU~-X!&^ z)`sDpm#x+0Mwa{6xKxT3S-N0yXEf(hH1zmNV$?f*3vYnn$r(O0ZNs!#M#*LpwAE7k;{l)&TF zYV}2Dwzl5Qa@1)R)zfuV5aC*ziXCH|v3j}B-&NmB$f=g^8Bw7$J%)y9E^q6fxAzZ4Q|e)0_JEs z7r;sPdad3QN~G2>vkpzkb`-!1`t5?G2>`&L0`p6*wNc?PYfOx_d3M8|;|{<$uV26Z z=7WQScW3}Pw108~!2CbOE!GlJcf!Rp$n#DZ=84AOdjp^}Fk1n(x3}N6T&=#&eA^wr zc4|_rh--XY-D-f=Bt8#h>jeR(0u(*Z832f1TMo~Rt<_6%s&4ORwx75H=AMdl{3$aC z@8(wWrC`wdg=LZv&K)#wagRx==4#hWFwj6Q%?eKd0Nz!^66h8e^Xr;tL|<<88WI)L zkFQ<<4`AIlVNLO8)q;0qn0){s^JdFbM4kS5`odA|+@D|ly^EDb8#S%5-ZPFOa_ASw zBuZNw2Y3;LqmN_qnF9cYoVnnA=hmFeo9XMBgI}dwY3#f(s*?*O0|zw%P`ktdj*dW2 zA76b~{%H8f2mlb!tsBtU2eknG$^QQSH-y4JsQpjc|6OPPpHxWJAn`l#Qrd3cBAA3f zSpmrO`}8wgTfc|}7}EsM0=73nnCYPp(RCuA^iWhpCx5^n-2`5xJ2HcNcoS)X$5Oq} z1;Qn~vRv&2J5R>N9|SxhuD{|RpQAn6i@J;?V>>c;arb%a#yaE{M3)KBbr67OKGgX| zTAAiWhLsWO`c&FuvXx@quA7hmax^g)>kN6FB}(Xy#$<656#bMHc2RkJJBKwjjh+#2 z>b^POi%U#2?sa}(v7mbzcN)N~W~6{5)(5pE27s$-a+N!(rC$U*zyREjD8AM6pLGF1 zm_7q6Yv~N3FH~C!e3cd3*r0aBH#ne>08aVQv!eYMQcH`y8vtWofN9*X6}dmi`a!h| z(0{`f7Q1%s+TXc$?b^>d?f~{!wfGWbXQ)Jl@W9rb zBmi^{H<%e`m^=zJQx+3ro;B7vgX>tl=G;0COvHjBinF?k_=fTJ)$zTP`;Jb=Rb0;W zS*y|ylgrC{B^a;tONnP(cqXh&5?7S~03V#{Q%IHX#(7VHrRh0bBL98ozq?ZV4`O2uSwHEnA$wG85JXl0vJBj}ySw{~%jNQ(40*Oe zdtSi1KtE^)R&0hEnrM$|H0)`XEif&7-@vgJpS9-A+`LsEv@!r=n=?w<3ecgHo#Jzk zfsJD5NVtUiC?44I)ig7zmM&iI3jDDD&tE*1YUpD?6omL&W&;x|DO#@sS&pQ=uvkaa zNNw9*MVXpf1t2k3kof>74@~Er)(uL-$#Bbj{b0yFh&wKbHv{}p4FgLEN?ZgNDF5d?&$F7m}6w^ML(?$50)g^+8 zaP*3X3l+zVG&;BOvs$!U9Y<1qt!pM#jfV5KCs=~xc`~j{n*vbKIdh<#MV#mP-Lp4v z!3w9lyB|l>#*F0!dr|;##KOQ&a!c)%f09OspZ60(-291h)CQ5oJT` zb(5!NpVi|LfG8FKoRo3}(T)Y}C2e&5vk+rJv;((I&C}umnz1sI;?7;r)I(}-wsnj^ zvtt56dMx-zx46(;6j=vW@dBvt|JV2T_g|~RpQ`>50HA-#5`en@ldCafMeoQ9d;Rt4Yrm|DsEt(#G+p|1i zVx1)f0Kolu>RLGS`X`$-eaYDFvBv#6h_Kkg^3@biUej#Q>*0NtWW+O?NnyLRpOR7ODiN96xqnfm9qc&?PWBhkyuLGfAA z%78u=fLsCBHa9o_+?g|Her(L#nt(^EBhTHV!iq}s4p-)uzSlrDJ}gd?Q03k7#T!$x zUNGClYKS;l9UG`2LA|E%1s%s4Z z2+Ofiz=>xATqFT{Xds4WOL3iOtXxz7Flj?<$D=G5-ygvI7{8-C>73Gg_#JET9Rq!I z#*dEv+sg+BKd6C!x?eAR~a%0}r;gw%)L@ zvGEdU1;l3%y_*StFgr4n$~x zLZzi?c#7!loihTGJcpif3e9bx*Yc~a*qj%UaA(Nc!IdEzR#Jc6QXy3=8fq!z9W|8R z(acS#Q<7D+vnu^E5z)r;XU<;%Jt zBSl8)t=N%Y^kPfnyabr2baZsWpEzs9w4x$U_Sa#*Fb%dW*&=&BD&I{JV2uUz>iHTRRS zN8Ue~|9#&749<4v3;Ny&|8)TXG*46qL@WTg0muqKpI_M7*?C~OTz(ErRfcZU6)GPx z@ma7J5O-m!U^16-xns!5e|5^Nu4 zAt~m5ysLkf_sywF z=FP`FbE?eun6)kX7aiRUJk$nXu`+QXm}#B{_Y45h;Tr9Q>0U`mo9nxu$3VNx3eAzI z%AH1<`x$1Kf!>(A9(r$yYv!L?-{V2R)KcS4rf>Q`mvT52?Os;x|8MVWLT!7pyY@c& zo_p?l?;k=pN~eAR!Hc0Uboh7eXQ`enlf8WMZJ{B;Y`> zMR8_?2og|Qqc%|o4w4u!=_C+|-FEuD`*qIVvTChWwQB8EwQHYy?tAyXvu^Y6d*3;= zYgheh)%vbgLY&jx0_?%!P!hBqF18^-IUL8!9rk93u0-$h^WxD#b<&8vKKZd&n*9<; z@r6kN$i_hH&-(;;w_bnLKl*s1=gsZy_uk*%|3^{sm#u#&`}^L<{u#e>Q2j ze1k~WC_kBR^TmueYMxFE#3(5l4icm!IAsn%hPHG_sAWiuvEa5A=0j1`QS27aGh^L< za4QDpVe=Up1%Ih}UY4nCQm(O8SaZHyh^(Kma+EvK(oG@%s^!tpMtgLcAB-ihb0za3 zynngmtMUc7{%B?>k;>k#hV`uWxRCj6A{Y`iI|O z{mUzV+a}#gFi)`E@kl~_1qmqG!@3xD1UoaLK|(wIF@t=L^= zCOOTQ$97w$ZI>wk)wB0$Q~?+>6Mo$y!*1a-zqsG+{Sz5I9pejD0p%odu~_|Rvcyv< zVA+4B&?x_5HVW~Nl<|${nL~0PjU~=pEJ4OTsUK~fuS0BI>r9{t3y#+u6~6XZ{zCt6v)MaAQbSy zix)5d#b)Tg3QN~uKs{{$gi9OfC82DBTlsLFZ?JxO!dYm_AZJB{7ap^=Pxlk&&B9&^ z*TO||D0FppG&1FIO)ha`Jl8Lnbr^O&`lY$Zcs`Fcop+xLnUqma>r5&mdr>TxY`dHG zc-&ba1nZ?m!0Ti@TLv!<2=Kk2d<4F&PAZsqwX!Ar7rPFMVm$E?&%$F|H>?{8ub>xG z^*k=B1igdxj!9rCDULtGD{L`e0)#D`x4%C?vRFDXq2ty(V(Sd~@@%lh09R6n(*jg& zZs8O_TfZcfD~&fBOyrzp{x7^hpX^}w{KW+x&8?nb==<(sfa`)Gf%FjmhMzKh=D9oN zeY3Qy^`wvh0e8%up(>rRdT6vndZ69gyE}0G{}Q?W!R^l%{;=!+y}R80XmAY^K8BX0Xe_h0XGZ(nFi=Q}`uoj{HzOAcWsqAw zzf>&s7%*qtz?vV}%aC0E316Ay8aH$7%r=^MHJfKa4f!H8qKBy25S`LK+aC@;eSLlX z)8zUGcRve%nfUkKR{gV3JxlZI}e|H!z{yb~*C_I)EbF}bJZgPvT6GMoxG40}AQvVkgA~5a{qfDNFnU-D{8Yd#L z>vYE2XlzV7=B}DMf$gCHVq)L`d4YGJlsKwPDJF=KQ ze#su0Fh5oHbt!x>6AlfVj+r#NV%5TuGm8^1%iVF27_wyf_~F8>Yhk|Z5rWejTv&(% zhI}6g(2zXWbkPnd02mbrGg?fJQ!_h4k1mA>WY3vJ9Oq7!KiudS;RnG<<5y!3MTCOe zdfkbMquD~zA8{(i-Vc^NjfW{a2v{B?E<&PjQXXu2O6`L3K>I2r+=f#d>C6yTG%PFN zp`JnD6QUM^^vbd26ix}@r9nVnhrk!lq8UqMNM<#us~@(pbNqy*Mo7V^|54 z=^$k6YK}TYg|2}f!0ffeB^tR&Xe*uc>c_909#N1@D7TUw2GhEiV%C~ zIL9Kcug+v8XG`C2rp>VF$HO>Y-`spRT=+wLUdH`p#lN$1{l|T?j=j8Bt$|c@D1f~H z;0d@NhT&^>yWKzAY&IWE(=-jkrVmz#kzJcCAKvg~tb%1^UZ9abQ%p#r9glo$o(U~P zsEU6gBVz44V=m&tA3gH2O2V$qR=P-;f$WiB##l~IEUF)ou>u_SWj3wjnzyqR0c}yM z=Y%y9%j)V1dnA0MLRrezZXP`iD~wqv8_mzRG;9!>rEWsPALV`2!YiD}1(q8g$3%e> z+|1%L>piQKJ!3A`dM{D8J_aaV&xTxGL^?%g-{G6sqXh-z0mDmvF1ZQ!6tY{?&;`lc$K&%F$$@W#{s+p=WBUO zg6A%t#@I}d@dvbXcYpt*cXxMi^*hvj(MSGd)_?o)nZ=cSJ`EnQt)I^%3nP`GfD6(N zWhelYfj8Uj_D@}2UVfSqpI9wdc|8fnXfVxQip0x8yG2_^H#+@fu0tV37M20u6WFh~ z${3$tvMvI{iB|gj8SI2ME$v>JiHz}FyB0yT=f!cSl#tfC7YWV2QnW}~J1a<5u21qT z8dV5OeNyrq9VD6O6)>Z#oXeg;$Jykk^FqwY?y5 z{inX)gh%f8`%m56+(Ob{sP#f20A29sEM5PRHd>ecJ#EKIfGiLsl>qqr%e&phw_R*^ zKS}q>BR`0YFp+bhAro2LSg`ve8>F{hco?V7bErhcaLNS&p z603?=g__Rh=oVaioGF9S=F%(Bjv7}SWc0+K`)5^ODM_v1VXUm3nV4q09<#CY8UQkB zuMhR>skCrq^gw6Y$}Bc7joTVutiNA}4z6qkVTX6N;|qoh3>6%pSd%1q`n$FqQt<$c zU~A5jEa8ZuR6;|XxqJl9Tz#ozt|O0hlEHY+OtPdfWDic+|8VK*NZ_Gfd&$gTrU8Z*G2^Zh(d2 zKDyRFm-{_S*S{+OiK>D2xJ)qT1AyS?qk)hg@cWmSm*2J9UjA)p3Erh~9A~5rT5r(L zqj^h7U|HSTn-Fho7baPUZ?gEF1KWB*$y$ZOK7Hv>Ko~Ba9MXtp|0S;pks=>ELDA- z?Flh}`BLWlX6fWeHm9{4TI^NqM_y*OKxcC94MqwU5yKn+ND_sgaP7AE3Hbw%+(!hC zM?+~+q`>a?ho8T>x%nHE-vu35!k@GLSMHW&T6|6|8kXfbP3NewG6q0#oD>L016c`x zP(V-ucDvnQyu950Fj+~Gt0Y$zxd#FxBKM+<4MH4iZ8@Iha6!czAe51Kw7;MLXknZk z4J!&1h9RF7yZ=!l(z*oZHlMvz0FYbO&XMO8hB76CtT`?iBdeiCMoi3t5`}4CWx>CU zy^y_JlD9EQ?kH+)dB_kVwa236PZr{A&MW(4I?^Ggu(T@`j8^>TVTMGot$k#IhJ+m_ z^kw*|Jr%x_7>UF*4q-IUyP^PuZV^O$WrN3NrqEyqEmWio1%4+AX`vV7eaWOe%26mB zMu|L=vB4vxr~qtw^27+Q1x-453V9Y%`GL6}rt#ljzrOxqGU{N^Quy>;gNB%&4%@;-(B6`S>LWPiDeA z?4f!OrEhtbiFcA^BrsxEJ50;E`Zs(ZYWFh(YGLxUI%JK+>zgH$ zF4T-~VX)Sg-#|sUaAeBPWspp{Sungs-h!^@F(jke7}RLMrst9u3;V~($zz~AT{#vT zg7R26fiPsSSB}{Qx}xM>sh);pHo708NoHyyKiQg*H%qwY**jO_9>|#N+7>MwC#(yE zmMV#SVd2!thRFNi^Mx11L|HicN3~G&>4`do=>=qz{kJ!7uKz04^9L#=9+z-nRDjM-h1sUaHyu5tm6tm zN({h*yMsE&0n3Xm0lug}te|Bpd)sje%a=Dv5H&_{hS?jgSDFR@Wz~ci9TzJ-bZE9u zC2Qb#t57CDr7S74z?0C0Vsd76#L$z>1QAX_b79Djww7=>T)UsUI)qk`E#2}kLAGx-4BJF3i$7@jk@8Dy`{!A!skdF zPjWiok_^gbmR7OuiZkowoKrTKfoH63yik6@my>)697Tj$W>)D^rE_tcYnW^FPkn3~ zKQ%Zx?HMvy4U>hHhvi>(DglOC=Lt`Iq2Z(+yR4$l2hAj&$WT{f64jRO(gF{N<0@7 z(yM)?Q?DyIt$7>2$*wSxY^6^a@QYECwW5Dt&S*wQ$Ldp_h|s{#+_4W%Ik)PFa-&a^ z@1{8LSi}{k5iaHdrIOB2gN|?^<_OQPGC{oxXN}VO0IZZ|UZ&$S5^h;Bpb$08=n4O# z32B8sVPyd&{ORoD-?^_zVi;rBv$l$*Vuxhq)Vl0>W~bH}q7nv_Nzs|PbLv|g8}NyqWXe zqrX~{Vt>0k&~@(BgNGCS@b03QP=_?<1FDU;yrIn>1U4=ueW52Oxek1DMF#!Gy)dbb zA-xcN2cX*L+%iT9CvXs4+d#V{C zx8KM+G(w_#kKJrjzfR4?vo!pyQ*YT#?`x5V!zhlG8AP_Wg0q|v-9TdN*L-Nz2ws9G z2X?!>=6{LwiO#soM-2{ydhRE+;#oDC%H|wN^dD4TVaCpeZ}`1_c(gjx>&`0oBLB*f z6P|T2D_`+WWb{g^aL9~^N;tzrJoMRtk5}8J(HWwmqCpWw;Bb1w8>g%9$QjzZcl56J zokwdA-v3!oQ?GwN8$&=%V8sEiA_><7aN;1ZQp`=wVK-h4HwqI;92JH|~hD{)^t&C{pkGH*FTy?Urif>Dd7UCv~rmCQ-y80J>c;N9LC!{@O$BxE( zddj4Hifyrqf+xjWTqRSSU@DJlKpys`xy&l%)Z}y7hj_SUo=66i0cHl?I;w?4KCxdS zn0Y$t;B{!RCm)WCfF>WA?e@)O(S5>%Y-X^7T|Xw1 zYhdf}zC0{9v(wk<{%4V&Eo6r2NsOhkoRH zZk`6693%);)HK$m{i9kcGlk8*gW6L4iI>VMjZ}D58D= zayDb}cw@f?d5MaFlk^3M7Engx-sDri`mLwe@5|ql)BM##)x8dT{+5y_6sl~r$29Rn zPJT3YMO19G7R1GR*7)qvu`ks*ekpLw|1Bv8$xvha@?Awnb8}9yG4IN&nowy5RDvkF z&LzA~?k;AbD(=;&PjVlLnLnuGfIkT!#R2dI<-Tw8LG5lL%!#@hbR?e-=rR2KxLRrS z>&%P-PPoA=pP4B8lU^{0!FoNlJ%O*%fpVnJP_lvukwU+)Ks~~sKsMh;zbt28G}C+) z<~P}ahoDC_`zV(Q(I{-IxNWgLdut!ZLY$F!_ziHqUglTVLugIIA_|NZ6A4ufsZ-jl zcJ!#|BKnBY1%GUhGqvnx?bb5S)>*Y1nDBqvbaLr5OYULxS^dMLd^8iA2A{4?=pV*F zg>VLsSdB~SFhl7ecmE5vh_2NjFS85E#54Q=18%^R)uZx3bp8HK!MT}A-;ox5DK4B^ z#t$UM4}f7~47lDGyHjNv=0q7sBqG7pOzT@^ronGHl8mPBKi>OIqeHgaAMhbsjAAft zx%?6D#hKL2EX4`wKs;4?r-SakHvdQ66w2Urg^>5&h7GSQWB0k100#N~vD@3itVutG zw}!I*q+Sw0-G)$ol1r|7W$ErHK9E#{87*^_;q7!+AzcBbsThe5uIxvTkU9S;rdm~E zsO(-k-!|8>;+rcMt8}%~*@U0Kbk8DoX>G-Fsao5rp3Llw$51{Dmvk4TYZZ1ET~+0R zk)p6+Ymn|fuUna!!`Pc`HP~{P0^huFFy{nI0#~l>T>u-n$YnT}?tOI8;${y5b-M-i zbXb{Z+);r5gdTaxzM4r-kNDH{;sdL}BVn8uol_@B!Gb6YbRv(aC138-l#wI5D)WPh zccjzoHdoKxsqJK$-|aC7VJg^O+flQNi@8}%A5m|QqKr`Nm=kdxsQBqP@_l`8X|G+t zJ};AfflE3CR#B~%2GBfI0g3a=G)=Ie?UBnP#0arbP*HN}L9HpJUVdBmT)7gYb1d({ zcrLpzLN%IB%%>4huf z-zggYUJd=N8YVn@`;mzDIXzJr+RVQ9vLZk_g*J@6N>lCgkymlS=bAPg#;bUl{-OQU zq<8Z7cZ6(^yLJscxUvPK(}un3rEHFC&r&|%Xb$isDO5+!R|`a|Wm`TMaFmb^`K6TL z@cblxSZRSY$3(xvR-`P%*vR6Hq(0eb{54I%2|Z$$BcARj<##i0a2PC}`tiZ57qCLo zVme@{7%3FU)k!M-OYa#`T4xacv-rpxDoV(?zUZT+nPSU zx5uSd=o2#&t$r%h`${qzNBZpr;UqB(dhr&+{0Ez}v5&ftm5`e6gJ3_GrW<|M7^sg= z94~Ys$KRcnc*W|)35fNd0S4R!XY68jPfjEnng`oOu;CM)WfPZYfI; z-%mLbZiLJ_qg7oiOLDzgBqqA(v{ZM0GEM$?)5BdW@eu@ZtbM22JIpzc^YOZexOSl zx%WcG@r?BEt=*CIl7x0cTEw%MLY;R^3ZmqGEBX1PeV>0bow4(6whze~{G<>hCWDIe zVjRM_zBJ(G{br(&Cs8#Z|I;fLxP=-XE-wAX>|@AUN`m{m5B;OIsvNm3c$XyklMH90aKbK_~r{mRvw|ZIO`vHHXnUAKzdTGji0O@ zITcw7hHu5}LY6J42uj1eqQ4`RwPnF-Uu@X7GQJAOr783-9R||0__U5g;6Xy?po!JRg*Z1A18uBMhidqbP#|`HfH$guBL3cS#mFp*a zPMeOaqxR_A%)1E?Rhu{7ir5N`Xxl3qG8|7uco%Sm6@E8H@9K%lN?C|LI=aEI^{Z|T z6!w>55~r6!8U|slS46IoGl8D4!%yg%gxN2JZD=r|f$;XyH<7tsd~Z z-WDwSJGL*Zz)m+jEf(=U)4uIQgz_h(^JDO9#e~gg5_%o0;$7<$yMH3+P*IB!yt2sl9H6Z1)w;Y|85h_Y=`$3EMQrf<{~ zyZs`fQQbJf?<5Y;F~*b=RU`4?;!8pEpFA_2eDpPbfLzgTN*r)X;5n`<7|;Sli*f>y zM2V&7ow5}Ec7Ga?%75P>5~c5}>t%WaEJX)$sVQ;Nm|%Dj-2fR{9EiL!N?WcBbr`>GUm;~ConZE^gBfn5v6k za_=Iwv%<%Y%HMhTl7ukyt&wZ-hn4&dg`G)=_(l}eb|*(Qb%at+$f@uYv{RESEVBq7 zsO!`EC4hJG$GW-@{@t}tLU!U7lBrgBACdSIK5!)#+upQ{$Z2YAz2!L`^+`-GhjF1_ zu!7BXwdTEH{FjjVjd1NCGbPHoW;2F$G!08(6r$|WFGq){w&PyyG zif@*(3o|hN9W9q@y&V?X&;!eUi(|CiRupE7LQdK6h)$Xo^=M3ULXzc8LnicF$UKLN zs84{dgX{}W8_KWDI5b|JNdAfn&R>b8D}UVLILva3G52WxIq`XO zLVoVMASeEdXK4S-qGbt&-ker!NNa|DtaCRa;zgrMigfdhZE}0e1)IuoPQe3Ll8*7Q zVW!`#Kt8(eC4eTV@G-mU-el@;?RpZ`y4Tt#okqzpP25bf~FXoZ8&^B6rf*Gtz=zOo2(BcKbtm zS`5**(t0z2st`=89TR{&i)|NmLm0ga{nfTvBhpsHF0;!*ufON_SpnV$SE}RssdWO8 z@uxAVZX!tKM?t6?0DcYitU7Ch~5cc;xr3Us6)ul51EmCK{{Gt+3 zCyZJGx~>rjW;2kj6oh25fRzEv^c(G~5|<<}nFq7j5&)8hE+n-`*H(rm;JfeBsA_u$+gj7X>og!Ar7Zx{j znrT=RUi;wr-*ngy-6xLk+Ybeiise5+OuMo$3fb71Qu5i_HBy+E=99r-4iFWmmat1^ zDg6-G`_}Gq4v!}(T4&>ud^n+7_S96MBw217@+h1yLZ@j~kjFx2Swt$)vW#*wVkx6r zdnvL(Aw{x+j2?^Y8e%F;#?$-U+2!KD1A=hgcKq$$I?-Do&*%Q4eYC%N3JdU~*eYID*+s4lcfF>)O zfM%_&^6T~wQ)6`Qoal61qM8o8;?HbRmt*cjmHYjh<^ zSFQV@&0`6+nSL(2tQN68ns>SFy3bnIkRO9mx%lew%_G*4NnLjMF$s@(# zt}1DKKC<>maQ>m+*bz6EfQr5+1=($AaWb^8;2S~7WX$oy$E8H&wI_oEv`+MigV4W&9W`pDCqzP$(Wo)#5(`d;^M8_jHd;er&g}@ ziiI}v`xL*>jrfSm)QR`%oub`+xOv(?j;MT1uR)1O>G;*pvT>nL?gb`nCew%`JQhSq8iGaVWnuW(M35}K zulEOMKA}C817RH1%}jY-v?`8IDu^_8h;e^S#t(gBRjnDv^&8k>f7Q=?XT~2BDj#}j zbd~)jkZnQvSx(Vk(LM+CpH+d|ndau^yCu)XJFH^cG%n`e+e!-bP(y8<3jqv)5*@>E zr8k0HAruQYuUl-nEJsLIu7N!0t3&{LiyRM`(F&?o^3gH7--X1-29HFEGY-3k?JleL znh+e0MM7rY4zntmbHtC!Etw8TEeAE>uPCHCt+m96{}Ly2DB2SxmdpB7>g54?>4=OA z%w-&z-^Z|^L#tCpzdwAyf8Q8q%zA(RB`jDUQhOTiD83?zEc#WgD%3#kDClm@N7z@+ zlPe@)Sq)M+hX@2J`ehJG1@eqW{tZRMNPZ~EBH(pbFTeaXQOZz|no#F|b?q>j!RE&C zEdgkyT7CEPS2RvoNz-NzvE+AIT5lK=YZ}S6LsKqGnIk; zlo5Arkq0gZ1umm<;0fUqo)PGK z7AgCUiJqgQA@!moHKfp>zDGkKfIeG&;P zSypXFc(_>-WbTBuc)@$GyEt&OyIAKL6b_^k8xwm4u`L z>tOpnM)&z#>zCf9Z$06pa(+UCVzmf!?CI_Db`qnv-{sx-Va1*uB&}Y=&_^@#JgJ3? z@YwsA<%5l<7WFuFxFv@&?_}k}^6-y_ju|Vtm)XT}jOyvNkmFSA?fD^((xq{FFfT{RBan`iZ~F9~;U&@#x+?&T}3X-^U3& zT%Z#$tI3@#{c_jVBb5UXETR!4E z@M2u*&wxA(#5g%T*g2#Ac(X(K2m4oshcDv8_`gP`QO8v4HHP%L>?8|AGkoh$J~bDl zuJXl{#V4>2az#?l3=s+|y^8Ij5NZJBy^0E<`t7*58!l1{t`RQox#0!Zt-+VFJw#hF zGs++S%En`7+@EWaeHk!A_ON=sHpYL1EB`j@N7~Qg<2$)X7C*UZ$J+Uyh&{F)tKFs> zhxE2tjm0BW;6+&JQM*9wI(73QvUy$X_8caCJB{4|1?BJaf&I6*oHVOl?_u)7r1~zu zAno(2cmoHgjzaFTA~Vh=UQu1dD)>rOr9=VH!+bE}2Zl?)E84qZrSKEcvp!{@i)3s= z%}p%7_^2dtmq0-EoT`PmT8063QrRwFFneSi+6JYXqcVoe+K#Gv9h&xUq|h^-la(1w zgT$EUYSg53#7Di+h;Y6?z4IuVmxlS zy7Usk`!0Ly>vhQpFqo>d2+z{3-x}F-ElOy0NgT?J-fAtmAiwiJz-(a(>s)?Z&z;P) z+nR`*Q=>H!PqNaa#>4U~mE>id-*UNr*=s*`RHqx$Wrg%hgAv7md2e`C&Rc_tvXGJfE4 z0k}F%ViX_(Tsu{5Q{jl$sZGW{@W*0UJ3eOE;p}Rx1W_9+6YULOMCxn2Dq%gPQlxIv z#jTxDH{Le(`95ggyT56VjYVt*KPPiTSDbn_`6__L+yfOlq>**W_A-R7?>6~w7yQ~b zjBEc~O|W5;0?{MWV@v7vB%#yH-ni;f9dxX3>gpsNo6sd$fd}wsg zaDWD+*1MhwXkWf3=pUt#xVATuzTI9S){3sAkPa2$nf+qLA`K>*q>3U~ zs@WM(7mhqI-b-GCc8VqAIF(I!`(!o_qk4%ilqn=Z`X-sw_0df-92Q+wV|A{Q+(L0J zpbPaap@Niz0jI%1#eEHmoS3iP;WA1ON_+u@)n@C!l1;#yuw3?)=h=57ikt5(_K7au z9|v-`nkWa?Z}F;_`%@`Tw-pvlEYC~i*1R_k;Qqa~?n4j$-2x=h!YzEfk{8q*b|=EQ z%GlPGb|`^+$Jjnk_WP-dIU!O%Z*&x>1NFsGUT(FHM5AIm3fQXkAB?@68+cINg7=gp zgz#FyVypNHWB|zCYLX1os%tznnNQqWtG%GmpX)>Fk{S+>#I2j2kQ#yl)$TO#$eH`t z*j7|8KLe#E53?#qJXQne5m9i$V}-B_^fT{QfiTBIAun78phCw3SKOi0@-A#MJWwp0 zZ3TNc5(gaueMYHgi?{zhX$Wovt$3{dYi6>7Dgl|R`8SD-|3zQ`8UQly{VyCtu#g;f ziU^r{ z)p7ncI%prRKssponl$FH&j0tOcpVP^F!_%mCW~td6zh_|;F9>4QYAp_{{Qi_kHo^+ zH||K;{)hcP*H8kA|0SZZLiE5DZ-t0@v3UPqzF6|Y|BTFkJox|Q|DT>bI_Pnm+*Ccx T2@fmu2~d((m#cjFI^=% Date: Fri, 11 Jul 2025 09:29:29 -0400 Subject: [PATCH 012/658] Add initial support for search by keystroke to keybinding editor (#34274) This PR adds preliminary support for searching keybindings by keystrokes in the keybinding editor. Release Notes: - N/A --- assets/keymaps/default-linux.json | 5 +- assets/keymaps/default-macos.json | 3 +- crates/settings_ui/src/keybindings.rs | 251 ++++++++++++++++++++++---- 3 files changed, 218 insertions(+), 41 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 8a46e6c234c20fb8d662c4ecc3004955e293af1d..489e4e6d0cace6119deb510598ea382c018a209e 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -1112,7 +1112,10 @@ "context": "KeymapEditor", "use_key_equivalents": true, "bindings": { - "ctrl-f": "search::FocusSearch" + "ctrl-f": "search::FocusSearch", + "alt-find": "keymap_editor::ToggleKeystrokeSearch", + "alt-ctrl-f": "keymap_editor::ToggleKeystrokeSearch", + "alt-c": "keymap_editor::ToggleConflictFilter" } } ] diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index cb1cf572fbe256f60937ab385e37618932330725..c7ab7c92736918dc6065d05ef11223bbf701e461 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -1211,7 +1211,8 @@ "context": "KeymapEditor", "use_key_equivalents": true, "bindings": { - "cmd-f": "search::FocusSearch" + "cmd-alt-f": "keymap_editor::ToggleKeystrokeSearch", + "cmd-alt-c": "keymap_editor::ToggleConflictFilter" } } ] diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 58c2ac8f8e6e0aca5d9047beac3a3353270d5cdb..56be9481e5bd577edb063a74743272263da5dc6c 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -10,7 +10,7 @@ use feature_flags::FeatureFlagViewExt; use fs::Fs; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - AppContext as _, AsyncApp, ClickEvent, Context, DismissEvent, Entity, EventEmitter, + Action, AppContext as _, AsyncApp, ClickEvent, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Global, KeyContext, Keystroke, ModifiersChangedEvent, ScrollStrategy, StyledText, Subscription, WeakEntity, actions, div, }; @@ -57,7 +57,11 @@ actions!( /// Copies the action name to clipboard. CopyAction, /// Copies the context predicate to clipboard. - CopyContext + CopyContext, + /// Toggles Conflict Filtering + ToggleConflictFilter, + /// Toggle Keystroke search + ToggleKeystrokeSearch, ] ); @@ -143,6 +147,22 @@ impl KeymapEventChannel { } #[derive(Default, PartialEq)] +enum SearchMode { + #[default] + Normal, + KeyStroke, +} + +impl SearchMode { + fn invert(&self) -> Self { + match self { + SearchMode::Normal => SearchMode::KeyStroke, + SearchMode::KeyStroke => SearchMode::Normal, + } + } +} + +#[derive(Default, PartialEq, Copy, Clone)] enum FilterState { #[default] All, @@ -221,11 +241,13 @@ struct KeymapEditor { keybindings: Vec, keybinding_conflict_state: ConflictState, filter_state: FilterState, + search_mode: SearchMode, // corresponds 1 to 1 with keybindings string_match_candidates: Arc>, matches: Vec, table_interaction_state: Entity, filter_editor: Entity, + keystroke_editor: Entity, selected_index: Option, } @@ -245,6 +267,12 @@ impl KeymapEditor { cx.observe_global::(Self::update_keybindings); let table_interaction_state = TableInteractionState::new(window, cx); + let keystroke_editor = cx.new(|cx| { + let mut keystroke_editor = KeystrokeInput::new(window, cx); + keystroke_editor.highlight_on_focus = false; + keystroke_editor + }); + let filter_editor = cx.new(|cx| { let mut editor = Editor::single_line(window, cx); editor.set_placeholder_text("Filter action names…", cx); @@ -260,17 +288,28 @@ impl KeymapEditor { }) .detach(); + cx.subscribe(&keystroke_editor, |this, _, _, cx| { + if matches!(this.search_mode, SearchMode::Normal) { + return; + } + + this.update_matches(cx); + }) + .detach(); + let mut this = Self { workspace, keybindings: vec![], keybinding_conflict_state: ConflictState::default(), filter_state: FilterState::default(), + search_mode: SearchMode::default(), string_match_candidates: Arc::new(vec![]), matches: vec![], focus_handle: focus_handle.clone(), _keymap_subscription, table_interaction_state, filter_editor, + keystroke_editor, selected_index: None, }; @@ -279,30 +318,47 @@ impl KeymapEditor { this } - fn current_query(&self, cx: &mut Context) -> String { + fn current_action_query(&self, cx: &App) -> String { self.filter_editor.read(cx).text(cx) } + fn current_keystroke_query(&self, cx: &App) -> Vec { + match self.search_mode { + SearchMode::KeyStroke => self + .keystroke_editor + .read(cx) + .keystrokes() + .iter() + .cloned() + .collect(), + SearchMode::Normal => Default::default(), + } + } + fn update_matches(&self, cx: &mut Context) { - let query = self.current_query(cx); + let action_query = self.current_action_query(cx); + let keystroke_query = self.current_keystroke_query(cx); - cx.spawn(async move |this, cx| Self::process_query(this, query, cx).await) - .detach(); + cx.spawn(async move |this, cx| { + Self::process_query(this, action_query, keystroke_query, cx).await + }) + .detach(); } async fn process_query( this: WeakEntity, - query: String, + action_query: String, + keystroke_query: Vec, cx: &mut AsyncApp, ) -> anyhow::Result<()> { - let query = command_palette::normalize_action_query(&query); + let action_query = command_palette::normalize_action_query(&action_query); let (string_match_candidates, keybind_count) = this.read_with(cx, |this, _| { (this.string_match_candidates.clone(), this.keybindings.len()) })?; let executor = cx.background_executor().clone(); let mut matches = fuzzy::match_strings( &string_match_candidates, - &query, + &action_query, true, true, keybind_count, @@ -321,7 +377,26 @@ impl KeymapEditor { FilterState::All => {} } - if query.is_empty() { + match this.search_mode { + SearchMode::KeyStroke => { + matches.retain(|item| { + this.keybindings[item.candidate_id] + .ui_key_binding + .as_ref() + .is_some_and(|binding| { + keystroke_query.iter().all(|key| { + binding.keystrokes.iter().any(|keystroke| { + keystroke.key == key.key + && keystroke.modifiers == key.modifiers + }) + }) + }) + }); + } + SearchMode::Normal => {} + } + + if action_query.is_empty() { // apply default sort // sorts by source precedence, and alphabetically by action name within each source matches.sort_by_key(|match_item| { @@ -432,7 +507,7 @@ impl KeymapEditor { let json_language = load_json_language(workspace.clone(), cx).await; let rust_language = load_rust_language(workspace.clone(), cx).await; - let query = this.update(cx, |this, cx| { + let (action_query, keystroke_query) = this.update(cx, |this, cx| { let (key_bindings, string_match_candidates) = Self::process_bindings(json_language, rust_language, cx); @@ -455,10 +530,13 @@ impl KeymapEditor { string: candidate.string.clone(), }) .collect(); - this.current_query(cx) + ( + this.current_action_query(cx), + this.current_keystroke_query(cx), + ) })?; // calls cx.notify - Self::process_query(this, query, cx).await + Self::process_query(this, action_query, keystroke_query, cx).await }) .detach_and_log_err(cx); } @@ -664,6 +742,33 @@ impl KeymapEditor { }; cx.write_to_clipboard(gpui::ClipboardItem::new_string(action.clone())); } + + fn toggle_conflict_filter( + &mut self, + _: &ToggleConflictFilter, + _: &mut Window, + cx: &mut Context, + ) { + self.filter_state = self.filter_state.invert(); + self.update_matches(cx); + } + + fn toggle_keystroke_search( + &mut self, + _: &ToggleKeystrokeSearch, + window: &mut Window, + cx: &mut Context, + ) { + self.search_mode = self.search_mode.invert(); + self.update_matches(cx); + + match self.search_mode { + SearchMode::KeyStroke => { + window.focus(&self.keystroke_editor.focus_handle(cx)); + } + SearchMode::Normal => {} + } + } } #[derive(Clone)] @@ -763,41 +868,97 @@ impl Render for KeymapEditor { .on_action(cx.listener(Self::delete_binding)) .on_action(cx.listener(Self::copy_action_to_clipboard)) .on_action(cx.listener(Self::copy_context_to_clipboard)) + .on_action(cx.listener(Self::toggle_conflict_filter)) + .on_action(cx.listener(Self::toggle_keystroke_search)) .size_full() .p_2() .gap_1() .bg(theme.colors().editor_background) .child( h_flex() + .p_2() + .gap_1() .key_context({ let mut context = KeyContext::new_with_defaults(); context.add("BufferSearchBar"); context }) - .h_8() - .pl_2() - .pr_1() - .py_1() - .border_1() - .border_color(theme.colors().border) - .rounded_lg() - .child(self.filter_editor.clone()) - .when(self.keybinding_conflict_state.any_conflicts(), |this| { - this.child( - IconButton::new("KeymapEditorConflictIcon", IconName::Warning) - .tooltip(Tooltip::text(match self.filter_state { - FilterState::All => "Show conflicts", - FilterState::Conflicts => "Hide conflicts", - })) - .selected_icon_color(Color::Error) - .toggle_state(matches!(self.filter_state, FilterState::Conflicts)) - .on_click(cx.listener(|this, _, _, cx| { - this.filter_state = this.filter_state.invert(); - this.update_matches(cx); - })), - ) - }), + .child( + div() + .size_full() + .h_8() + .pl_2() + .pr_1() + .py_1() + .border_1() + .border_color(theme.colors().border) + .rounded_lg() + .child(self.filter_editor.clone()), + ) + .child( + // TODO: Ask Mikyala if there's a way to get have items be aligned by horizontally + // without embedding a h_flex in another h_flex + h_flex() + .when(self.keybinding_conflict_state.any_conflicts(), |this| { + this.child( + IconButton::new("KeymapEditorConflictIcon", IconName::Warning) + .tooltip({ + let filter_state = self.filter_state; + + move |window, cx| { + Tooltip::for_action( + match filter_state { + FilterState::All => "Show conflicts", + FilterState::Conflicts => "Hide conflicts", + }, + &ToggleConflictFilter, + window, + cx, + ) + } + }) + .selected_icon_color(Color::Error) + .toggle_state(matches!( + self.filter_state, + FilterState::Conflicts + )) + .on_click(|_, window, cx| { + window.dispatch_action( + ToggleConflictFilter.boxed_clone(), + cx, + ); + }), + ) + }) + .child( + IconButton::new("KeymapEditorToggleFiltersIcon", IconName::Filter) + .tooltip(|window, cx| { + Tooltip::for_action( + "Toggle Keystroke Search", + &ToggleKeystrokeSearch, + window, + cx, + ) + }) + .toggle_state(matches!(self.search_mode, SearchMode::KeyStroke)) + .on_click(|_, window, cx| { + window.dispatch_action( + ToggleKeystrokeSearch.boxed_clone(), + cx, + ); + }), + ), + ), ) + .when(matches!(self.search_mode, SearchMode::KeyStroke), |this| { + this.child( + div() + .child(self.keystroke_editor.clone()) + .border_1() + .border_color(theme.colors().border) + .rounded_lg(), + ) + }) .child( Table::new() .interactable(&self.table_interaction_state) @@ -1522,6 +1683,7 @@ async fn remove_keybinding( struct KeystrokeInput { keystrokes: Vec, + highlight_on_focus: bool, focus_handle: FocusHandle, intercept_subscription: Option, _focus_subscriptions: [Subscription; 2], @@ -1536,6 +1698,7 @@ impl KeystrokeInput { ]; Self { keystrokes: Vec::new(), + highlight_on_focus: true, focus_handle, intercept_subscription: None, _focus_subscriptions, @@ -1553,6 +1716,7 @@ impl KeystrokeInput { { if !event.modifiers.modified() { self.keystrokes.pop(); + cx.emit(()); } else { last.modifiers = event.modifiers; } @@ -1562,6 +1726,7 @@ impl KeystrokeInput { key: "".to_string(), key_char: None, }); + cx.emit(()); } cx.stop_propagation(); cx.notify(); @@ -1575,6 +1740,7 @@ impl KeystrokeInput { } else if Some(keystroke) != self.keystrokes.last() { self.keystrokes.push(keystroke.clone()); } + cx.emit(()); cx.stop_propagation(); cx.notify(); } @@ -1589,6 +1755,7 @@ impl KeystrokeInput { && !last.key.is_empty() && last.modifiers == event.keystroke.modifiers { + cx.emit(()); self.keystrokes.push(Keystroke { modifiers: event.keystroke.modifiers, key: "".to_string(), @@ -1629,6 +1796,8 @@ impl KeystrokeInput { } } +impl EventEmitter<()> for KeystrokeInput {} + impl Focusable for KeystrokeInput { fn focus_handle(&self, _cx: &App) -> FocusHandle { self.focus_handle.clone() @@ -1645,9 +1814,11 @@ impl Render for KeystrokeInput { .track_focus(&self.focus_handle) .on_modifiers_changed(cx.listener(Self::on_modifiers_changed)) .on_key_up(cx.listener(Self::on_key_up)) - .focus(|mut style| { - style.border_color = Some(colors.border_focused); - style + .when(self.highlight_on_focus, |this| { + this.focus(|mut style| { + style.border_color = Some(colors.border_focused); + style + }) }) .py_2() .px_3() @@ -1688,6 +1859,7 @@ impl Render for KeystrokeInput { .when(!is_focused, |this| this.icon_color(Color::Muted)) .on_click(cx.listener(|this, _event, _window, cx| { this.keystrokes.pop(); + cx.emit(()); cx.notify(); })), ) @@ -1697,6 +1869,7 @@ impl Render for KeystrokeInput { .when(!is_focused, |this| this.icon_color(Color::Muted)) .on_click(cx.listener(|this, _event, _window, cx| { this.keystrokes.clear(); + cx.emit(()); cx.notify(); })), ), From 3b9bb521f4a4ff53bab73afe4cf40ebba8b2c929 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Fri, 11 Jul 2025 08:51:21 -0500 Subject: [PATCH 013/658] keymap_ui: Only show conflicts between user bindings (#34284) Closes #ISSUE This makes it so conflicts are only shown between user bindings. User bindings that override bindings in the Vim, Base, and Default keymaps are not identified as conflicts Release Notes: - N/A *or* Added/Fixed/Improved ... Co-authored-by: Anthony --- crates/settings_ui/src/keybindings.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 56be9481e5bd577edb063a74743272263da5dc6c..c3d6ae7c3019c1c9138111e8431ef2089ce7abb5 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -193,7 +193,13 @@ impl ConflictState { key_bindings .iter() .enumerate() - .filter(|(_, binding)| !binding.keystroke_text.is_empty()) + .filter(|(_, binding)| { + !binding.keystroke_text.is_empty() + && binding + .source + .as_ref() + .is_some_and(|source| matches!(source.0, KeybindSource::User)) + }) .for_each(|(index, binding)| { action_keybind_mapping .entry(binding.get_action_mapping()) From 10028aaae83d59bb7d2186df4bc12cbb43f94e7d Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Fri, 11 Jul 2025 10:25:09 -0400 Subject: [PATCH 014/658] Ensure *.json recognized as JSONC if checkout folder not `zed` (#34289) Follow-up to: https://github.com/zed-industries/zed/pull/33410 Release Notes: - N/A --- .zed/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.zed/settings.json b/.zed/settings.json index 1ef6bc28f7dffb3fd7b25489f3f6ff0c1b0f74c9..68e05a426f2474cb663aa5ff843905f375170e0f 100644 --- a/.zed/settings.json +++ b/.zed/settings.json @@ -40,7 +40,7 @@ }, "file_types": { "Dockerfile": ["Dockerfile*[!dockerignore]"], - "JSONC": ["assets/**/*.json", "renovate.json"], + "JSONC": ["**/assets/**/*.json", "renovate.json"], "Git Ignore": ["dockerignore"] }, "hard_tabs": false, From d1a6c5d494c8e6debbc0e174d00b0ab14f67f3c1 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Fri, 11 Jul 2025 09:54:08 -0500 Subject: [PATCH 015/658] keymap_ui: Hover tooltip for context (#34290) Closes #ISSUE Ideally the tooltip would only appear if the context was overflowing it's column, but for now, we just unconditionally show a tooltip so that long contexts can be seen. This PR also includes a change to the tooltip element, allowing for tooltips with non-text contents which is used here for syntax highlighting Release Notes: - N/A *or* Added/Fixed/Improved ... Co-authored-by: Anthony --- crates/settings_ui/src/keybindings.rs | 24 ++++++--- crates/ui/src/components/tooltip.rs | 72 ++++++++++++++++++++++----- 2 files changed, 77 insertions(+), 19 deletions(-) diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index c3d6ae7c3019c1c9138111e8431ef2089ce7abb5..4053d41669916fcee749159cbaf9fe465cdecd2c 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -1015,12 +1015,24 @@ impl Render for KeymapEditor { } } }; - let context = binding - .context - .clone() - .map_or(gpui::Empty.into_any_element(), |context| { - context.into_any_element() - }); + let context = binding.context.clone().map_or( + gpui::Empty.into_any_element(), + |context| { + let is_local = context.local().is_some(); + + div() + .id(("keymap context", index)) + .child(context.clone()) + .when(is_local, |this| { + this.tooltip(Tooltip::element({ + move |_, _| { + context.clone().into_any_element() + } + })) + }) + .into_any_element() + }, + ); let source = binding .source .clone() diff --git a/crates/ui/src/components/tooltip.rs b/crates/ui/src/components/tooltip.rs index 647b700c377b4ca6816924e592f698937646b6b8..18c9decc59f6f68b92542fbd56b6fae916195bfd 100644 --- a/crates/ui/src/components/tooltip.rs +++ b/crates/ui/src/components/tooltip.rs @@ -1,3 +1,5 @@ +use std::rc::Rc; + use gpui::{Action, AnyElement, AnyView, AppContext as _, FocusHandle, IntoElement, Render}; use settings::Settings; use theme::ThemeSettings; @@ -7,15 +9,36 @@ use crate::{Color, KeyBinding, Label, LabelSize, StyledExt, h_flex, v_flex}; #[derive(RegisterComponent)] pub struct Tooltip { - title: SharedString, + title: Title, meta: Option, key_binding: Option, } +#[derive(Clone, IntoElement)] +enum Title { + Str(SharedString), + Callback(Rc AnyElement>), +} + +impl From for Title { + fn from(value: SharedString) -> Self { + Title::Str(value) + } +} + +impl RenderOnce for Title { + fn render(self, window: &mut Window, cx: &mut App) -> impl gpui::IntoElement { + match self { + Title::Str(title) => title.into_any_element(), + Title::Callback(element) => element(window, cx), + } + } +} + impl Tooltip { pub fn simple(title: impl Into, cx: &mut App) -> AnyView { cx.new(|_| Self { - title: title.into(), + title: Title::Str(title.into()), meta: None, key_binding: None, }) @@ -26,7 +49,7 @@ impl Tooltip { let title = title.into(); move |_, cx| { cx.new(|_| Self { - title: title.clone(), + title: title.clone().into(), meta: None, key_binding: None, }) @@ -34,15 +57,15 @@ impl Tooltip { } } - pub fn for_action_title>( - title: Title, + pub fn for_action_title>( + title: T, action: &dyn Action, - ) -> impl Fn(&mut Window, &mut App) -> AnyView + use { + ) -> impl Fn(&mut Window, &mut App) -> AnyView + use<T> { let title = title.into(); let action = action.boxed_clone(); move |window, cx| { cx.new(|cx| Self { - title: title.clone(), + title: Title::Str(title.clone()), meta: None, key_binding: KeyBinding::for_action(action.as_ref(), window, cx), }) @@ -60,7 +83,7 @@ impl Tooltip { let focus_handle = focus_handle.clone(); move |window, cx| { cx.new(|cx| Self { - title: title.clone(), + title: Title::Str(title.clone()), meta: None, key_binding: KeyBinding::for_action_in(action.as_ref(), &focus_handle, window, cx), }) @@ -75,7 +98,7 @@ impl Tooltip { cx: &mut App, ) -> AnyView { cx.new(|cx| Self { - title: title.into(), + title: Title::Str(title.into()), meta: None, key_binding: KeyBinding::for_action(action, window, cx), }) @@ -90,7 +113,7 @@ impl Tooltip { cx: &mut App, ) -> AnyView { cx.new(|cx| Self { - title: title.into(), + title: title.into().into(), meta: None, key_binding: KeyBinding::for_action_in(action, focus_handle, window, cx), }) @@ -105,7 +128,7 @@ impl Tooltip { cx: &mut App, ) -> AnyView { cx.new(|cx| Self { - title: title.into(), + title: title.into().into(), meta: Some(meta.into()), key_binding: action.and_then(|action| KeyBinding::for_action(action, window, cx)), }) @@ -121,7 +144,7 @@ impl Tooltip { cx: &mut App, ) -> AnyView { cx.new(|cx| Self { - title: title.into(), + title: title.into().into(), meta: Some(meta.into()), key_binding: action .and_then(|action| KeyBinding::for_action_in(action, focus_handle, window, cx)), @@ -131,12 +154,35 @@ impl Tooltip { pub fn new(title: impl Into<SharedString>) -> Self { Self { - title: title.into(), + title: title.into().into(), meta: None, key_binding: None, } } + pub fn new_element(title: impl Fn(&mut Window, &mut App) -> AnyElement + 'static) -> Self { + Self { + title: Title::Callback(Rc::new(title)), + meta: None, + key_binding: None, + } + } + + pub fn element( + title: impl Fn(&mut Window, &mut App) -> AnyElement + 'static, + ) -> impl Fn(&mut Window, &mut App) -> AnyView { + let title = Title::Callback(Rc::new(title)); + move |_, cx| { + let title = title.clone(); + cx.new(|_| Self { + title: title, + meta: None, + key_binding: None, + }) + .into() + } + } + pub fn meta(mut self, meta: impl Into<SharedString>) -> Self { self.meta = Some(meta.into()); self From a58a75c0f6dcfeeb1578e9a763bb02eb72d35763 Mon Sep 17 00:00:00 2001 From: Finn Evers <dev@bahn.sh> Date: Fri, 11 Jul 2025 17:17:34 +0200 Subject: [PATCH 016/658] keymap_ui: Hide tooltips when context menu is shown (#34286) This PR ensures tooltips are dismissed/not shown once the context menu is opened. It also ensures the context menu is dismissed once the list is scrolled. Release Notes: - N/A --- crates/settings_ui/src/keybindings.rs | 204 ++++++++++++++++---------- 1 file changed, 124 insertions(+), 80 deletions(-) diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 4053d41669916fcee749159cbaf9fe465cdecd2c..159756c975da3c7cf34cac61d0b31ea050dacbfb 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -11,8 +11,8 @@ use fs::Fs; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ Action, AppContext as _, AsyncApp, ClickEvent, Context, DismissEvent, Entity, EventEmitter, - FocusHandle, Focusable, Global, KeyContext, Keystroke, ModifiersChangedEvent, ScrollStrategy, - StyledText, Subscription, WeakEntity, actions, div, + FocusHandle, Focusable, Global, KeyContext, Keystroke, ModifiersChangedEvent, MouseButton, + Point, ScrollStrategy, StyledText, Subscription, WeakEntity, actions, anchored, deferred, div, }; use language::{Language, LanguageConfig, ToOffset as _}; use settings::{BaseKeymap, KeybindSource, KeymapFile, SettingsAssets}; @@ -21,7 +21,7 @@ use util::ResultExt; use ui::{ ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, ParentElement as _, Render, - SharedString, Styled as _, Tooltip, Window, prelude::*, right_click_menu, + SharedString, Styled as _, Tooltip, Window, prelude::*, }; use workspace::{ Item, ModalView, SerializableItem, Workspace, notifications::NotifyTaskExt as _, @@ -187,7 +187,7 @@ struct ConflictState { } impl ConflictState { - fn new(key_bindings: &Vec<ProcessedKeybinding>) -> Self { + fn new(key_bindings: &[ProcessedKeybinding]) -> Self { let mut action_keybind_mapping: HashMap<_, Vec<usize>> = HashMap::default(); key_bindings @@ -255,6 +255,7 @@ struct KeymapEditor { filter_editor: Entity<Editor>, keystroke_editor: Entity<KeystrokeInput>, selected_index: Option<usize>, + context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>, } impl EventEmitter<()> for KeymapEditor {} @@ -267,8 +268,6 @@ impl Focusable for KeymapEditor { impl KeymapEditor { fn new(workspace: WeakEntity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self { - let focus_handle = cx.focus_handle(); - let _keymap_subscription = cx.observe_global::<KeymapEventChannel>(Self::update_keybindings); let table_interaction_state = TableInteractionState::new(window, cx); @@ -311,12 +310,13 @@ impl KeymapEditor { search_mode: SearchMode::default(), string_match_candidates: Arc::new(vec![]), matches: vec![], - focus_handle: focus_handle.clone(), + focus_handle: cx.focus_handle(), _keymap_subscription, table_interaction_state, filter_editor, keystroke_editor, selected_index: None, + context_menu: None, }; this.update_keybindings(cx); @@ -593,6 +593,68 @@ impl KeymapEditor { .and_then(|keybind_index| self.keybindings.get(keybind_index)) } + fn select_index(&mut self, index: usize, cx: &mut Context<Self>) { + if self.selected_index != Some(index) { + self.selected_index = Some(index); + cx.notify(); + } + } + + fn create_context_menu( + &mut self, + position: Point<Pixels>, + window: &mut Window, + cx: &mut Context<Self>, + ) { + self.context_menu = self.selected_binding().map(|selected_binding| { + let selected_binding_has_no_context = selected_binding + .context + .as_ref() + .and_then(KeybindContextString::local) + .is_none(); + + let selected_binding_is_unbound = selected_binding.ui_key_binding.is_none(); + + let context_menu = ContextMenu::build(window, cx, |menu, _window, _cx| { + menu.action_disabled_when( + selected_binding_is_unbound, + "Edit", + Box::new(EditBinding), + ) + .action("Create", Box::new(CreateBinding)) + .action("Copy action", Box::new(CopyAction)) + .action_disabled_when( + selected_binding_has_no_context, + "Copy Context", + Box::new(CopyContext), + ) + }); + + let context_menu_handle = context_menu.focus_handle(cx); + window.defer(cx, move |window, _cx| window.focus(&context_menu_handle)); + let subscription = cx.subscribe_in( + &context_menu, + window, + |this, _, _: &DismissEvent, window, cx| { + this.dismiss_context_menu(window, cx); + }, + ); + (context_menu, position, subscription) + }); + + cx.notify(); + } + + fn dismiss_context_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) { + self.context_menu.take(); + window.focus(&self.focus_handle); + cx.notify(); + } + + fn context_menu_deployed(&self) -> bool { + self.context_menu.is_some() + } + fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) { if let Some(selected) = self.selected_index { let selected = selected + 1; @@ -975,6 +1037,7 @@ impl Render for KeymapEditor { "keymap-editor-table", row_count, cx.processor(move |this, range: Range<usize>, _window, cx| { + let context_menu_deployed = this.context_menu_deployed(); range .filter_map(|index| { let candidate_id = this.matches.get(index)?.candidate_id; @@ -983,21 +1046,23 @@ impl Render for KeymapEditor { let action = div() .child(binding.action_name.clone()) .id(("keymap action", index)) - .tooltip({ - let action_name = binding.action_name.clone(); - let action_docs = binding.action_docs; - move |_, cx| { - let action_tooltip = Tooltip::new( - command_palette::humanize_action_name( - &action_name, - ), - ); - let action_tooltip = match action_docs { - Some(docs) => action_tooltip.meta(docs), - None => action_tooltip, - }; - cx.new(|_| action_tooltip).into() - } + .when(!context_menu_deployed, |this| { + this.tooltip({ + let action_name = binding.action_name.clone(); + let action_docs = binding.action_docs; + move |_, cx| { + let action_tooltip = Tooltip::new( + command_palette::humanize_action_name( + &action_name, + ), + ); + let action_tooltip = match action_docs { + Some(docs) => action_tooltip.meta(docs), + None => action_tooltip, + }; + cx.new(|_| action_tooltip).into() + } + }) }) .into_any_element(); let keystrokes = binding.ui_key_binding.clone().map_or( @@ -1023,7 +1088,7 @@ impl Render for KeymapEditor { div() .id(("keymap context", index)) .child(context.clone()) - .when(is_local, |this| { + .when(is_local && !context_menu_deployed, |this| { this.tooltip(Tooltip::element({ move |_, _| { context.clone().into_any_element() @@ -1055,9 +1120,30 @@ impl Render for KeymapEditor { let row = row .id(("keymap-table-row", row_index)) + .on_any_mouse_down(cx.listener( + move |this, + mouse_down_event: &gpui::MouseDownEvent, + window, + cx| { + match mouse_down_event.button { + MouseButton::Left => { + this.select_index(row_index, cx); + } + + MouseButton::Right => { + this.select_index(row_index, cx); + this.create_context_menu( + mouse_down_event.position, + window, + cx, + ); + } + _ => {} + } + }, + )) .on_click(cx.listener( move |this, event: &ClickEvent, window, cx| { - this.selected_index = Some(row_index); if event.up.click_count == 2 { this.open_edit_keybinding_modal(false, window, cx); } @@ -1071,18 +1157,23 @@ impl Render for KeymapEditor { row.border_color(cx.theme().colors().panel_focused_border) }); - right_click_menu(("keymap-table-row-menu", row_index)) - .trigger(move |_, _, _| row) - .menu({ - let this = cx.weak_entity(); - move |window, cx| { - build_keybind_context_menu(&this, row_index, window, cx) - } - }) - .into_any_element() + row.into_any_element() }), ), ) + .on_scroll_wheel(cx.listener(|this, _, _, cx| { + this.context_menu.take(); + cx.notify(); + })) + .children(self.context_menu.as_ref().map(|(menu, position, _)| { + deferred( + anchored() + .position(*position) + .anchor(gpui::Corner::TopLeft) + .child(menu.clone()), + ) + .with_priority(1) + })) } } @@ -1895,53 +1986,6 @@ impl Render for KeystrokeInput { } } -fn build_keybind_context_menu( - this: &WeakEntity<KeymapEditor>, - item_idx: usize, - window: &mut Window, - cx: &mut App, -) -> Entity<ContextMenu> { - ContextMenu::build(window, cx, |menu, _window, cx| { - let selected_binding = this - .update(cx, |this, _cx| { - this.selected_index = Some(item_idx); - this.selected_binding().cloned() - }) - .ok() - .flatten(); - - let Some(selected_binding) = selected_binding else { - return menu; - }; - - let selected_binding_has_no_context = selected_binding - .context - .as_ref() - .and_then(KeybindContextString::local) - .is_none(); - - let selected_binding_is_unbound_action = selected_binding.ui_key_binding.is_none(); - - menu.action_disabled_when( - selected_binding_is_unbound_action, - "Edit", - Box::new(EditBinding), - ) - .action("Create", Box::new(CreateBinding)) - .action_disabled_when( - selected_binding_is_unbound_action, - "Delete", - Box::new(DeleteBinding), - ) - .action("Copy action", Box::new(CopyAction)) - .action_disabled_when( - selected_binding_has_no_context, - "Copy Context", - Box::new(CopyContext), - ) - }) -} - fn collect_contexts_from_assets() -> Vec<SharedString> { let mut keymap_assets = vec![ util::asset_str::<SettingsAssets>(settings::DEFAULT_KEYMAP_PATH), From c09f484ec4e1c9b1548d810340e7cd05f78cca21 Mon Sep 17 00:00:00 2001 From: morgankrey <morgan@zed.dev> Date: Fri, 11 Jul 2025 10:26:36 -0500 Subject: [PATCH 017/658] collab: Add ability to add tax ID during Stripe Checkout (#34246) ### 1. **Added Tax ID Collection Types** - Created a new `StripeTaxIdCollection` struct with an `enabled` field - Added `tax_id_collection` field to `StripeCreateCheckoutSessionParams` ### 2. **Updated the Stripe Client Interface** - Modified the real Stripe client to handle tax ID collection conversion - Updated the fake Stripe client for testing purposes - Added proper imports across all affected files ### 3. **Enabled Tax ID Collection in Checkout Sessions** - Both `checkout_with_zed_pro` and `checkout_with_zed_pro_trial` methods now enable tax ID collection - The implementation correctly sets `tax_id_collection.enabled = true` for all checkout sessions ### 4. **Key Implementation Details** - Tax ID collection will be shown to new customers and existing customers without tax IDs - Collected tax IDs will be automatically saved to the customer's `tax_ids` array in Stripe - Business names will be saved to the customer's `name` property - The existing `customer_update.name = auto` setting ensures compatibility with tax ID collection Release Notes: - N/A --- crates/collab/src/stripe_billing.rs | 6 ++++-- crates/collab/src/stripe_client.rs | 6 ++++++ .../collab/src/stripe_client/fake_stripe_client.rs | 6 ++++-- .../collab/src/stripe_client/real_stripe_client.rs | 13 +++++++++++-- 4 files changed, 25 insertions(+), 6 deletions(-) diff --git a/crates/collab/src/stripe_billing.rs b/crates/collab/src/stripe_billing.rs index 8bf6c08158b9fa742f0f9e59711c7df80013614d..fdd9653d7cd4cbfeb63e65892c7ccce312c97d97 100644 --- a/crates/collab/src/stripe_billing.rs +++ b/crates/collab/src/stripe_billing.rs @@ -19,8 +19,8 @@ use crate::stripe_client::{ StripeCustomerId, StripeCustomerUpdate, StripeCustomerUpdateAddress, StripeCustomerUpdateName, StripeMeter, StripePrice, StripePriceId, StripeSubscription, StripeSubscriptionId, StripeSubscriptionTrialSettings, StripeSubscriptionTrialSettingsEndBehavior, - StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, UpdateSubscriptionItems, - UpdateSubscriptionParams, + StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, StripeTaxIdCollection, + UpdateSubscriptionItems, UpdateSubscriptionParams, }; pub struct StripeBilling { @@ -252,6 +252,7 @@ impl StripeBilling { name: Some(StripeCustomerUpdateName::Auto), shipping: None, }); + params.tax_id_collection = Some(StripeTaxIdCollection { enabled: true }); let session = self.client.create_checkout_session(params).await?; Ok(session.url.context("no checkout session URL")?) @@ -311,6 +312,7 @@ impl StripeBilling { name: Some(StripeCustomerUpdateName::Auto), shipping: None, }); + params.tax_id_collection = Some(StripeTaxIdCollection { enabled: true }); let session = self.client.create_checkout_session(params).await?; Ok(session.url.context("no checkout session URL")?) diff --git a/crates/collab/src/stripe_client.rs b/crates/collab/src/stripe_client.rs index 9ffcb2ba6c9fde13ebc84b9e7c509851158e0a1e..ec947e12f792661578f9a8a675a0017f321e8fc4 100644 --- a/crates/collab/src/stripe_client.rs +++ b/crates/collab/src/stripe_client.rs @@ -190,6 +190,7 @@ pub struct StripeCreateCheckoutSessionParams<'a> { pub success_url: Option<&'a str>, pub billing_address_collection: Option<StripeBillingAddressCollection>, pub customer_update: Option<StripeCustomerUpdate>, + pub tax_id_collection: Option<StripeTaxIdCollection>, } #[derive(Debug, PartialEq, Eq, Clone, Copy)] @@ -218,6 +219,11 @@ pub struct StripeCreateCheckoutSessionSubscriptionData { pub trial_settings: Option<StripeSubscriptionTrialSettings>, } +#[derive(Debug, PartialEq, Clone)] +pub struct StripeTaxIdCollection { + pub enabled: bool, +} + #[derive(Debug)] pub struct StripeCheckoutSession { pub url: Option<String>, diff --git a/crates/collab/src/stripe_client/fake_stripe_client.rs b/crates/collab/src/stripe_client/fake_stripe_client.rs index 11b210dd0e7aba54148d26de0670f23415ae7cea..9bb08443ec6a5fd04ad11a8e24b1a71b03e4867b 100644 --- a/crates/collab/src/stripe_client/fake_stripe_client.rs +++ b/crates/collab/src/stripe_client/fake_stripe_client.rs @@ -14,8 +14,8 @@ use crate::stripe_client::{ StripeCreateCheckoutSessionSubscriptionData, StripeCreateMeterEventParams, StripeCreateSubscriptionParams, StripeCustomer, StripeCustomerId, StripeCustomerUpdate, StripeMeter, StripeMeterId, StripePrice, StripePriceId, StripeSubscription, - StripeSubscriptionId, StripeSubscriptionItem, StripeSubscriptionItemId, UpdateCustomerParams, - UpdateSubscriptionParams, + StripeSubscriptionId, StripeSubscriptionItem, StripeSubscriptionItemId, StripeTaxIdCollection, + UpdateCustomerParams, UpdateSubscriptionParams, }; #[derive(Debug, Clone)] @@ -38,6 +38,7 @@ pub struct StripeCreateCheckoutSessionCall { pub success_url: Option<String>, pub billing_address_collection: Option<StripeBillingAddressCollection>, pub customer_update: Option<StripeCustomerUpdate>, + pub tax_id_collection: Option<StripeTaxIdCollection>, } pub struct FakeStripeClient { @@ -236,6 +237,7 @@ impl StripeClient for FakeStripeClient { 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 { diff --git a/crates/collab/src/stripe_client/real_stripe_client.rs b/crates/collab/src/stripe_client/real_stripe_client.rs index 7108e8d7597a3afd235c2ae48a4b05c5fc5de014..07dde68d179d8650ef902ae07b05015bf5aa2633 100644 --- a/crates/collab/src/stripe_client/real_stripe_client.rs +++ b/crates/collab/src/stripe_client/real_stripe_client.rs @@ -27,8 +27,8 @@ use crate::stripe_client::{ StripeMeter, StripePrice, StripePriceId, StripePriceRecurring, StripeSubscription, StripeSubscriptionId, StripeSubscriptionItem, StripeSubscriptionItemId, StripeSubscriptionTrialSettings, StripeSubscriptionTrialSettingsEndBehavior, - StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, UpdateCustomerParams, - UpdateSubscriptionParams, + StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, StripeTaxIdCollection, + UpdateCustomerParams, UpdateSubscriptionParams, }; pub struct RealStripeClient { @@ -448,6 +448,7 @@ impl<'a> TryFrom<StripeCreateCheckoutSessionParams<'a>> for CreateCheckoutSessio 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() }) } @@ -590,3 +591,11 @@ impl From<StripeCustomerUpdate> for stripe::CreateCheckoutSessionCustomerUpdate } } } + +impl From<StripeTaxIdCollection> for stripe::CreateCheckoutSessionTaxIdCollection { + fn from(value: StripeTaxIdCollection) -> Self { + stripe::CreateCheckoutSessionTaxIdCollection { + enabled: value.enabled, + } + } +} From 496bf0ec43b74c20cc6e593bb7ee4db419332bf7 Mon Sep 17 00:00:00 2001 From: Ben Kunkle <ben@zed.dev> Date: Fri, 11 Jul 2025 10:32:59 -0500 Subject: [PATCH 018/658] keymap_ui: Ensure keymap UI opens in local workspace (#34291) Closes #ISSUE Use `workspace.with_local_workspace` to ensure the keymap UI is opened in a local workspace, even in remote. This was tested by removing the feature flag handling code, as with the feature flag logic the action does not appear which is likely a bug. Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/settings_ui/src/keybindings.rs | 38 +++++++++++++++++---------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 159756c975da3c7cf34cac61d0b31ea050dacbfb..993ed1bbbf2d55c041c6063febfbc992940b0d26 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -71,20 +71,30 @@ pub fn init(cx: &mut App) { cx.on_action(|_: &OpenKeymapEditor, cx| { workspace::with_active_or_new_workspace(cx, move |workspace, window, cx| { - let existing = workspace - .active_pane() - .read(cx) - .items() - .find_map(|item| item.downcast::<KeymapEditor>()); - - if let Some(existing) = existing { - workspace.activate_item(&existing, true, true, window, cx); - } else { - let keymap_editor = - cx.new(|cx| KeymapEditor::new(workspace.weak_handle(), window, cx)); - workspace.add_item_to_active_pane(Box::new(keymap_editor), None, true, window, cx); - } - }); + workspace + .with_local_workspace(window, cx, |workspace, window, cx| { + let existing = workspace + .active_pane() + .read(cx) + .items() + .find_map(|item| item.downcast::<KeymapEditor>()); + + if let Some(existing) = existing { + workspace.activate_item(&existing, true, true, window, cx); + } else { + let keymap_editor = + cx.new(|cx| KeymapEditor::new(workspace.weak_handle(), window, cx)); + workspace.add_item_to_active_pane( + Box::new(keymap_editor), + None, + true, + window, + cx, + ); + } + }) + .detach(); + }) }); cx.observe_new(|_workspace: &mut Workspace, window, cx| { From 993e0f55ec88f3209f00198e8a6872f43bd6340d Mon Sep 17 00:00:00 2001 From: Conrad Irwin <conrad.irwin@gmail.com> Date: Fri, 11 Jul 2025 09:38:42 -0600 Subject: [PATCH 019/658] ACP follow (#34235) Closes #ISSUE Release Notes: - N/A --------- Co-authored-by: Agus Zubiaga <agus@zed.dev> Co-authored-by: Anthony Eid <hello@anthonyeid.me> Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com> --- Cargo.lock | 5 +- Cargo.toml | 2 +- assets/keymaps/default-linux.json | 3 +- assets/keymaps/default-macos.json | 3 +- crates/acp/Cargo.toml | 1 + crates/acp/src/acp.rs | 389 +++++++++++++--- crates/agent_ui/src/acp/thread_view.rs | 563 +++++++++++++++++++++--- crates/agent_ui/src/agent_diff.rs | 273 +++++++++--- crates/agent_ui/src/agent_panel.rs | 27 +- crates/agent_ui/src/message_editor.rs | 10 +- crates/assistant_tool/src/action_log.rs | 22 +- 11 files changed, 1090 insertions(+), 208 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bc6783ce92019f67cfe74afa6b802a863c95dd81..624126c163b6bd30ff06a9ea3db2c0c2cddfccb0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,6 +9,7 @@ dependencies = [ "agent_servers", "agentic-coding-protocol", "anyhow", + "assistant_tool", "async-pipe", "buffer_diff", "editor", @@ -263,9 +264,9 @@ dependencies = [ [[package]] name = "agentic-coding-protocol" -version = "0.0.6" +version = "0.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1ac0351749af7bf53c65042ef69fefb9351aa8b7efa0a813d6281377605c37d" +checksum = "a75f520bcc049ebe40c8c99427aa61b48ad78a01bcc96a13b350b903dcfb9438" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index fd5cbff545351e3248f0d700c199e47f632d32d8..7e3b43e58a1132a48b14eca2ad9f13004c386b7d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -404,7 +404,7 @@ zlog_settings = { path = "crates/zlog_settings" } # External crates # -agentic-coding-protocol = "0.0.6" +agentic-coding-protocol = "0.0.7" 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/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 489e4e6d0cace6119deb510598ea382c018a209e..c660383d10bfa5e206e812303269fd8c28ddc076 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -320,7 +320,8 @@ "bindings": { "enter": "agent::Chat", "up": "agent::PreviousHistoryMessage", - "down": "agent::NextHistoryMessage" + "down": "agent::NextHistoryMessage", + "shift-ctrl-r": "agent::OpenAgentDiff" } }, { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index c7ab7c92736918dc6065d05ef11223bbf701e461..dc109d94aa012d886da4cec1a70d5d3995e05f93 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -371,7 +371,8 @@ "bindings": { "enter": "agent::Chat", "up": "agent::PreviousHistoryMessage", - "down": "agent::NextHistoryMessage" + "down": "agent::NextHistoryMessage", + "shift-ctrl-r": "agent::OpenAgentDiff" } }, { diff --git a/crates/acp/Cargo.toml b/crates/acp/Cargo.toml index dae6292e28fb6a345de4c06bb2a65da7f3ebad4c..1570aeaef083e692928d5bb3f6da8de2d79f5c0b 100644 --- a/crates/acp/Cargo.toml +++ b/crates/acp/Cargo.toml @@ -20,6 +20,7 @@ gemini = [] agent_servers.workspace = true agentic-coding-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/src/acp.rs b/crates/acp/src/acp.rs index ddb7c50f7a2f28156fd5c9c1eb3460e3771a1fbd..0aa57513a7e4cb1b45c623163148880e1f77f63a 100644 --- a/crates/acp/src/acp.rs +++ b/crates/acp/src/acp.rs @@ -2,14 +2,19 @@ pub use acp::ToolCallId; use agent_servers::AgentServer; use agentic_coding_protocol::{self as acp, UserMessageChunk}; use anyhow::{Context as _, Result, anyhow}; +use assistant_tool::ActionLog; use buffer_diff::BufferDiff; use editor::{MultiBuffer, PathKey}; use futures::{FutureExt, channel::oneshot, future::BoxFuture}; use gpui::{AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity}; use itertools::Itertools; -use language::{Anchor, Buffer, Capability, LanguageRegistry, OffsetRangeExt as _}; +use language::{ + Anchor, Buffer, BufferSnapshot, Capability, LanguageRegistry, OffsetRangeExt as _, Point, + text_diff, +}; use markdown::Markdown; -use project::Project; +use project::{AgentLocation, Project}; +use std::collections::HashMap; use std::error::Error; use std::fmt::{Formatter, Write}; use std::{ @@ -159,6 +164,18 @@ impl AgentThreadEntry { Self::ToolCall(too_call) => too_call.to_markdown(cx), } } + + pub fn diff(&self) -> Option<&Diff> { + if let AgentThreadEntry::ToolCall(ToolCall { + content: Some(ToolCallContent::Diff { diff }), + .. + }) = self + { + Some(&diff) + } else { + None + } + } } #[derive(Debug)] @@ -168,6 +185,7 @@ pub struct ToolCall { pub icon: IconName, pub content: Option<ToolCallContent>, pub status: ToolCallStatus, + pub locations: Vec<acp::ToolCallLocation>, } impl ToolCall { @@ -328,6 +346,8 @@ impl ToolCallContent { pub struct Diff { pub multibuffer: Entity<MultiBuffer>, pub path: PathBuf, + pub new_buffer: Entity<Buffer>, + pub old_buffer: Entity<Buffer>, _task: Task<Result<()>>, } @@ -362,6 +382,7 @@ impl Diff { let task = cx.spawn({ let multibuffer = multibuffer.clone(); let path = path.clone(); + let new_buffer = new_buffer.clone(); async move |cx| { diff_task.await?; @@ -401,6 +422,8 @@ impl Diff { Self { multibuffer, path, + new_buffer, + old_buffer, _task: task, } } @@ -421,6 +444,8 @@ pub struct AcpThread { entries: Vec<AgentThreadEntry>, title: SharedString, project: Entity<Project>, + action_log: Entity<ActionLog>, + shared_buffers: HashMap<Entity<Buffer>, BufferSnapshot>, send_task: Option<Task<()>>, connection: Arc<acp::AgentConnection>, child_status: Option<Task<Result<()>>>, @@ -522,7 +547,11 @@ impl AcpThread { } }); + let action_log = cx.new(|_| ActionLog::new(project.clone())); + Self { + action_log, + shared_buffers: Default::default(), entries: Default::default(), title: "ACP Thread".into(), project, @@ -534,6 +563,14 @@ impl AcpThread { }) } + pub fn action_log(&self) -> &Entity<ActionLog> { + &self.action_log + } + + pub fn project(&self) -> &Entity<Project> { + &self.project + } + #[cfg(test)] pub fn fake( stdin: async_pipe::PipeWriter, @@ -558,7 +595,11 @@ impl AcpThread { } }); + let action_log = cx.new(|_| ActionLog::new(project.clone())); + Self { + action_log, + shared_buffers: Default::default(), entries: Default::default(), title: "ACP Thread".into(), project, @@ -589,6 +630,26 @@ impl AcpThread { } } + pub fn has_pending_edit_tool_calls(&self) -> bool { + for entry in self.entries.iter().rev() { + match entry { + AgentThreadEntry::UserMessage(_) => return false, + AgentThreadEntry::ToolCall(ToolCall { + status: + ToolCallStatus::Allowed { + status: acp::ToolCallStatus::Running, + .. + }, + content: Some(ToolCallContent::Diff { .. }), + .. + }) => return true, + AgentThreadEntry::ToolCall(_) | AgentThreadEntry::AssistantMessage(_) => {} + } + } + + false + } + pub fn push_entry(&mut self, entry: AgentThreadEntry, cx: &mut Context<Self>) { self.entries.push(entry); cx.emit(AcpThreadEvent::NewEntry); @@ -644,65 +705,63 @@ impl AcpThread { pub fn request_tool_call( &mut self, - label: String, - icon: acp::Icon, - content: Option<acp::ToolCallContent>, - confirmation: acp::ToolCallConfirmation, + tool_call: acp::RequestToolCallConfirmationParams, cx: &mut Context<Self>, ) -> ToolCallRequest { let (tx, rx) = oneshot::channel(); let status = ToolCallStatus::WaitingForConfirmation { confirmation: ToolCallConfirmation::from_acp( - confirmation, + tool_call.confirmation, self.project.read(cx).languages().clone(), cx, ), respond_tx: tx, }; - let id = self.insert_tool_call(label, status, icon, content, cx); + let id = self.insert_tool_call(tool_call.tool_call, status, cx); ToolCallRequest { id, outcome: rx } } pub fn push_tool_call( &mut self, - label: String, - icon: acp::Icon, - content: Option<acp::ToolCallContent>, + request: acp::PushToolCallParams, cx: &mut Context<Self>, ) -> acp::ToolCallId { let status = ToolCallStatus::Allowed { status: acp::ToolCallStatus::Running, }; - self.insert_tool_call(label, status, icon, content, cx) + self.insert_tool_call(request, status, cx) } fn insert_tool_call( &mut self, - label: String, + tool_call: acp::PushToolCallParams, status: ToolCallStatus, - icon: acp::Icon, - content: Option<acp::ToolCallContent>, cx: &mut Context<Self>, ) -> acp::ToolCallId { let language_registry = self.project.read(cx).languages().clone(); let id = acp::ToolCallId(self.entries.len() as u64); - - self.push_entry( - AgentThreadEntry::ToolCall(ToolCall { - id, - label: cx.new(|cx| { - Markdown::new(label.into(), Some(language_registry.clone()), None, cx) - }), - icon: acp_icon_to_ui_icon(icon), - content: content - .map(|content| ToolCallContent::from_acp(content, language_registry, cx)), - status, + let call = ToolCall { + id, + label: cx.new(|cx| { + Markdown::new( + tool_call.label.into(), + Some(language_registry.clone()), + None, + cx, + ) }), - cx, - ); + icon: acp_icon_to_ui_icon(tool_call.icon), + content: tool_call + .content + .map(|content| ToolCallContent::from_acp(content, language_registry, cx)), + locations: tool_call.locations, + status, + }; + + self.push_entry(AgentThreadEntry::ToolCall(call), cx); id } @@ -804,14 +863,16 @@ impl AcpThread { false } - pub fn initialize(&self) -> impl use<> + Future<Output = Result<acp::InitializeResponse>> { + pub fn initialize( + &self, + ) -> impl use<> + Future<Output = Result<acp::InitializeResponse, acp::Error>> { let connection = self.connection.clone(); - async move { Ok(connection.request(acp::InitializeParams).await?) } + async move { connection.request(acp::InitializeParams).await } } - pub fn authenticate(&self) -> impl use<> + Future<Output = Result<()>> { + pub fn authenticate(&self) -> impl use<> + Future<Output = Result<(), acp::Error>> { let connection = self.connection.clone(); - async move { Ok(connection.request(acp::AuthenticateParams).await?) } + async move { connection.request(acp::AuthenticateParams).await } } #[cfg(test)] @@ -819,7 +880,7 @@ impl AcpThread { &mut self, message: &str, cx: &mut Context<Self>, - ) -> BoxFuture<'static, Result<()>> { + ) -> BoxFuture<'static, Result<(), acp::Error>> { self.send( acp::SendUserMessageParams { chunks: vec![acp::UserMessageChunk::Text { @@ -834,7 +895,7 @@ impl AcpThread { &mut self, message: acp::SendUserMessageParams, cx: &mut Context<Self>, - ) -> BoxFuture<'static, Result<()>> { + ) -> BoxFuture<'static, Result<(), acp::Error>> { let agent = self.connection.clone(); self.push_entry( AgentThreadEntry::UserMessage(UserMessage::from_acp( @@ -865,7 +926,7 @@ impl AcpThread { .boxed() } - pub fn cancel(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> { + pub fn cancel(&mut self, cx: &mut Context<Self>) -> Task<Result<(), acp::Error>> { let agent = self.connection.clone(); if self.send_task.take().is_some() { @@ -898,13 +959,123 @@ impl AcpThread { } } } - }) + })?; + Ok(()) }) } else { Task::ready(Ok(())) } } + pub fn read_text_file( + &self, + request: acp::ReadTextFileParams, + cx: &mut Context<Self>, + ) -> Task<Result<String>> { + let project = self.project.clone(); + let action_log = self.action_log.clone(); + cx.spawn(async move |this, cx| { + let load = project.update(cx, |project, cx| { + let path = project + .project_path_for_absolute_path(&request.path, cx) + .context("invalid path")?; + anyhow::Ok(project.open_buffer(path, cx)) + }); + let buffer = load??.await?; + + action_log.update(cx, |action_log, cx| { + action_log.buffer_read(buffer.clone(), cx); + })?; + project.update(cx, |project, cx| { + let position = buffer + .read(cx) + .snapshot() + .anchor_before(Point::new(request.line.unwrap_or_default(), 0)); + project.set_agent_location( + Some(AgentLocation { + buffer: buffer.downgrade(), + position, + }), + cx, + ); + })?; + let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot())?; + this.update(cx, |this, _| { + let text = snapshot.text(); + this.shared_buffers.insert(buffer.clone(), snapshot); + text + }) + }) + } + + pub fn write_text_file( + &self, + path: PathBuf, + content: String, + cx: &mut Context<Self>, + ) -> Task<Result<()>> { + let project = self.project.clone(); + let action_log = self.action_log.clone(); + cx.spawn(async move |this, cx| { + let load = project.update(cx, |project, cx| { + let path = project + .project_path_for_absolute_path(&path, cx) + .context("invalid path")?; + anyhow::Ok(project.open_buffer(path, cx)) + }); + let buffer = load??.await?; + let snapshot = this.update(cx, |this, cx| { + this.shared_buffers + .get(&buffer) + .cloned() + .unwrap_or_else(|| buffer.read(cx).snapshot()) + })?; + let edits = cx + .background_executor() + .spawn(async move { + let old_text = snapshot.text(); + text_diff(old_text.as_str(), &content) + .into_iter() + .map(|(range, replacement)| { + ( + snapshot.anchor_after(range.start) + ..snapshot.anchor_before(range.end), + replacement, + ) + }) + .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, + ); + }); + + action_log.update(cx, |action_log, cx| { + action_log.buffer_read(buffer.clone(), cx); + }); + buffer.update(cx, |buffer, cx| { + buffer.edit(edits, None, cx); + }); + 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 + }) + } + pub fn child_status(&mut self) -> Option<Task<Result<()>>> { self.child_status.take() } @@ -930,7 +1101,7 @@ impl acp::Client for AcpClientDelegate { async fn stream_assistant_message_chunk( &self, params: acp::StreamAssistantMessageChunkParams, - ) -> Result<()> { + ) -> Result<(), acp::Error> { let cx = &mut self.cx.clone(); cx.update(|cx| { @@ -947,45 +1118,37 @@ impl acp::Client for AcpClientDelegate { async fn request_tool_call_confirmation( &self, request: acp::RequestToolCallConfirmationParams, - ) -> Result<acp::RequestToolCallConfirmationResponse> { + ) -> Result<acp::RequestToolCallConfirmationResponse, acp::Error> { let cx = &mut self.cx.clone(); let ToolCallRequest { id, outcome } = cx .update(|cx| { - self.thread.update(cx, |thread, cx| { - thread.request_tool_call( - request.label, - request.icon, - request.content, - request.confirmation, - cx, - ) - }) + self.thread + .update(cx, |thread, cx| thread.request_tool_call(request, cx)) })? .context("Failed to update thread")?; Ok(acp::RequestToolCallConfirmationResponse { id, - outcome: outcome.await?, + outcome: outcome.await.map_err(acp::Error::into_internal_error)?, }) } async fn push_tool_call( &self, request: acp::PushToolCallParams, - ) -> Result<acp::PushToolCallResponse> { + ) -> Result<acp::PushToolCallResponse, acp::Error> { let cx = &mut self.cx.clone(); let id = cx .update(|cx| { - self.thread.update(cx, |thread, cx| { - thread.push_tool_call(request.label, request.icon, request.content, cx) - }) + self.thread + .update(cx, |thread, cx| thread.push_tool_call(request, cx)) })? .context("Failed to update thread")?; Ok(acp::PushToolCallResponse { id }) } - async fn update_tool_call(&self, request: acp::UpdateToolCallParams) -> Result<()> { + async fn update_tool_call(&self, request: acp::UpdateToolCallParams) -> Result<(), acp::Error> { let cx = &mut self.cx.clone(); cx.update(|cx| { @@ -997,6 +1160,34 @@ impl acp::Client for AcpClientDelegate { Ok(()) } + + async fn read_text_file( + &self, + request: acp::ReadTextFileParams, + ) -> Result<acp::ReadTextFileResponse, acp::Error> { + let content = self + .cx + .update(|cx| { + self.thread + .update(cx, |thread, cx| thread.read_text_file(request, cx)) + })? + .context("Failed to update thread")? + .await?; + Ok(acp::ReadTextFileResponse { content }) + } + + async fn write_text_file(&self, request: acp::WriteTextFileParams) -> Result<(), acp::Error> { + self.cx + .update(|cx| { + self.thread.update(cx, |thread, cx| { + thread.write_text_file(request.path, request.content, cx) + }) + })? + .context("Failed to update thread")? + .await?; + + Ok(()) + } } fn acp_icon_to_ui_icon(icon: acp::Icon) -> IconName { @@ -1100,6 +1291,80 @@ mod tests { ); } + #[gpui::test] + async fn test_edits_concurrently_to_user(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + 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 (worktree, pathbuf) = project + .update(cx, |project, cx| { + project.find_or_create_worktree(path!("/tmp/foo"), true, cx) + }) + .await + .unwrap(); + let buffer = project + .update(cx, |project, cx| { + project.open_buffer((worktree.read(cx).id(), pathbuf), cx) + }) + .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::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::WriteTextFileParams { + path: path!("/tmp/foo").into(), + content: "one\ntwo\nthree\nfour\nfive\n".to_string(), + }) + })? + .await + .unwrap(); + Ok(()) + } + }) + }); + + let request = thread.update(cx, |thread, cx| { + thread.send_raw("Extend the count in /tmp/foo", cx) + }); + read_file_rx.await.ok(); + buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, "zero\n".to_string())], None, cx); + }); + cx.run_until_parked(); + assert_eq!( + buffer.read_with(cx, |buffer, _| buffer.text()), + "zero\none\ntwo\nthree\nfour\nfive\n" + ); + assert_eq!( + String::from_utf8(fs.read_file_sync(path!("/tmp/foo")).unwrap()).unwrap(), + "zero\none\ntwo\nthree\nfour\nfive\n" + ); + request.await.unwrap(); + } + #[gpui::test] async fn test_succeeding_canceled_toolcall(cx: &mut TestAppContext) { init_test(cx); @@ -1124,6 +1389,7 @@ mod tests { label: "Fetch".to_string(), icon: acp::Icon::Globe, content: None, + locations: vec![], }) })? .await @@ -1553,7 +1819,7 @@ mod tests { acp::SendUserMessageParams, Entity<FakeAcpServer>, AsyncApp, - ) -> LocalBoxFuture<'static, Result<()>>, + ) -> LocalBoxFuture<'static, Result<(), acp::Error>>, >, >, } @@ -1565,21 +1831,24 @@ mod tests { } impl acp::Agent for FakeAgent { - async fn initialize(&self) -> Result<acp::InitializeResponse> { + async fn initialize(&self) -> Result<acp::InitializeResponse, acp::Error> { Ok(acp::InitializeResponse { is_authenticated: true, }) } - async fn authenticate(&self) -> Result<()> { + async fn authenticate(&self) -> Result<(), acp::Error> { Ok(()) } - async fn cancel_send_message(&self) -> Result<()> { + async fn cancel_send_message(&self) -> Result<(), acp::Error> { Ok(()) } - async fn send_user_message(&self, request: acp::SendUserMessageParams) -> Result<()> { + async fn send_user_message( + &self, + request: acp::SendUserMessageParams, + ) -> Result<(), acp::Error> { let mut cx = self.cx.clone(); let handler = self .server @@ -1589,7 +1858,7 @@ mod tests { if let Some(handler) = handler { handler(request, self.server.clone(), self.cx.clone()).await } else { - anyhow::bail!("No handler for on_user_message") + Err(anyhow::anyhow!("No handler for on_user_message").into()) } } } @@ -1624,7 +1893,7 @@ mod tests { handler: impl for<'a> Fn(acp::SendUserMessageParams, Entity<FakeAcpServer>, AsyncApp) -> F + 'static, ) where - F: Future<Output = Result<()>> + 'static, + F: Future<Output = Result<(), acp::Error>> + 'static, { self.on_user_message .replace(Rc::new(move |request, server, cx| { diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 2e3bf54837ca2edd479687437fad90283456f36c..3db5e52a0a93a3f4e62a4da38977968a19dcf06e 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1,33 +1,37 @@ +use std::collections::BTreeMap; use std::path::Path; use std::rc::Rc; use std::sync::Arc; use std::time::Duration; use agentic_coding_protocol::{self as acp}; +use assistant_tool::ActionLog; +use buffer_diff::BufferDiff; use collections::{HashMap, HashSet}; use editor::{ AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode, - EditorStyle, MinimapVisibility, MultiBuffer, + EditorStyle, MinimapVisibility, MultiBuffer, PathKey, }; use file_icons::FileIcons; use futures::channel::oneshot; use gpui::{ - Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId, Focusable, - Hsla, Length, ListOffset, ListState, SharedString, StyleRefinement, Subscription, TextStyle, - TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, div, list, percentage, - prelude::*, pulsating_between, + 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, }; -use gpui::{FocusHandle, Task}; use language::language_settings::SoftWrap; use language::{Buffer, Language}; use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle}; use parking_lot::Mutex; use project::Project; use settings::Settings as _; +use text::Anchor; use theme::ThemeSettings; -use ui::{Disclosure, Tooltip, prelude::*}; +use ui::{Disclosure, Divider, DividerColor, KeyBinding, Tooltip, prelude::*}; use util::ResultExt; -use workspace::Workspace; +use workspace::{CollaboratorId, Workspace}; use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage}; use ::acp::{ @@ -38,6 +42,8 @@ use ::acp::{ use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet}; use crate::acp::message_history::MessageHistory; +use crate::agent_diff::AgentDiff; +use crate::{AgentDiffPane, Follow, KeepAll, OpenAgentDiff, RejectAll}; const RESPONSE_PADDING_X: Pixels = px(19.); @@ -53,6 +59,7 @@ pub struct AcpThreadView { auth_task: Option<Task<()>>, expanded_tool_calls: HashSet<ToolCallId>, expanded_thinking_blocks: HashSet<(usize, usize)>, + edits_expanded: bool, message_history: MessageHistory<acp::SendUserMessageParams>, } @@ -62,7 +69,7 @@ enum ThreadState { }, Ready { thread: Entity<AcpThread>, - _subscription: Subscription, + _subscription: [Subscription; 2], }, LoadError(LoadError), Unauthenticated { @@ -136,9 +143,9 @@ impl AcpThreadView { ); Self { - workspace, + workspace: workspace.clone(), project: project.clone(), - thread_state: Self::initial_state(project, window, cx), + thread_state: Self::initial_state(workspace, project, window, cx), message_editor, mention_set, diff_editors: Default::default(), @@ -147,11 +154,13 @@ impl AcpThreadView { auth_task: None, expanded_tool_calls: HashSet::default(), expanded_thinking_blocks: HashSet::default(), + edits_expanded: false, message_history: MessageHistory::new(), } } fn initial_state( + workspace: WeakEntity<Workspace>, project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>, @@ -219,15 +228,23 @@ impl AcpThreadView { this.update_in(cx, |this, window, cx| { match result { Ok(()) => { - let subscription = + 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()); + this.list_state .splice(0..0, thread.read(cx).entries().len()); + AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx); + this.thread_state = ThreadState::Ready { thread, - _subscription: subscription, + _subscription: [thread_subscription, action_log_subscription], }; + cx.notify(); } Err(err) => { @@ -250,7 +267,7 @@ impl AcpThreadView { cx.notify(); } - fn thread(&self) -> Option<&Entity<AcpThread>> { + pub fn thread(&self) -> Option<&Entity<AcpThread>> { match &self.thread_state { ThreadState::Ready { thread, .. } | ThreadState::Unauthenticated { thread } => { Some(thread) @@ -281,7 +298,6 @@ impl AcpThreadView { let mut ix = 0; let mut chunks: Vec<acp::UserMessageChunk> = Vec::new(); - let project = self.project.clone(); self.message_editor.update(cx, |editor, cx| { let text = editor.text(cx); @@ -377,6 +393,33 @@ impl AcpThreadView { ); } + fn open_agent_diff(&mut self, _: &OpenAgentDiff, window: &mut Window, cx: &mut Context<Self>) { + if let Some(thread) = self.thread() { + AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err(); + } + } + + fn open_edited_buffer( + &mut self, + buffer: &Entity<Buffer>, + window: &mut Window, + cx: &mut Context<Self>, + ) { + let Some(thread) = self.thread() else { + return; + }; + + let Some(diff) = + AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err() + else { + return; + }; + + diff.update(cx, |diff, cx| { + diff.move_to_path(PathKey::for_buffer(&buffer, cx), window, cx) + }) + } + fn set_draft_message( message_editor: Entity<Editor>, mention_set: Arc<Mutex<MentionSet>>, @@ -464,7 +507,8 @@ impl AcpThreadView { let count = self.list_state.item_count(); match event { AcpThreadEvent::NewEntry => { - self.sync_thread_entry_view(thread.read(cx).entries().len() - 1, window, cx); + let index = thread.read(cx).entries().len() - 1; + self.sync_thread_entry_view(index, window, cx); self.list_state.splice(count..count, 1); } AcpThreadEvent::EntryUpdated(index) => { @@ -537,15 +581,7 @@ impl AcpThreadView { fn entry_diff_multibuffer(&self, entry_ix: usize, cx: &App) -> Option<Entity<MultiBuffer>> { let entry = self.thread()?.read(cx).entries().get(entry_ix)?; - if let AgentThreadEntry::ToolCall(ToolCall { - content: Some(ToolCallContent::Diff { diff }), - .. - }) = &entry - { - Some(diff.multibuffer.clone()) - } else { - None - } + entry.diff().map(|diff| diff.multibuffer.clone()) } fn authenticate(&mut self, window: &mut Window, cx: &mut Context<Self>) { @@ -566,7 +602,8 @@ impl AcpThreadView { Markdown::new(format!("Error: {err}").into(), None, None, cx) })) } else { - this.thread_state = Self::initial_state(project.clone(), window, cx) + this.thread_state = + Self::initial_state(this.workspace.clone(), project.clone(), window, cx) } this.auth_task.take() }) @@ -1529,6 +1566,357 @@ impl AcpThreadView { container.into_any() } + fn render_edits_bar( + &self, + thread_entity: &Entity<AcpThread>, + window: &mut Window, + cx: &Context<Self>, + ) -> Option<AnyElement> { + let thread = thread_entity.read(cx); + let action_log = thread.action_log(); + let changed_buffers = action_log.read(cx).changed_buffers(cx); + + if changed_buffers.is_empty() { + return None; + } + + let editor_bg_color = cx.theme().colors().editor_background; + let active_color = cx.theme().colors().element_selected; + let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3)); + + let pending_edits = thread.has_pending_edit_tool_calls(); + let expanded = self.edits_expanded; + + v_flex() + .mt_1() + .mx_2() + .bg(bg_edit_files_disclosure) + .border_1() + .border_b_0() + .border_color(cx.theme().colors().border) + .rounded_t_md() + .shadow(vec![gpui::BoxShadow { + color: gpui::black().opacity(0.15), + offset: point(px(1.), px(-1.)), + blur_radius: px(3.), + spread_radius: px(0.), + }]) + .child(self.render_edits_bar_summary( + action_log, + &changed_buffers, + expanded, + pending_edits, + window, + cx, + )) + .when(expanded, |parent| { + parent.child(self.render_edits_bar_files( + action_log, + &changed_buffers, + pending_edits, + cx, + )) + }) + .into_any() + .into() + } + + fn render_edits_bar_summary( + &self, + action_log: &Entity<ActionLog>, + changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>, + expanded: bool, + pending_edits: bool, + window: &mut Window, + cx: &Context<Self>, + ) -> Div { + const EDIT_NOT_READY_TOOLTIP_LABEL: &str = "Wait until file edits are complete."; + + let focus_handle = self.focus_handle(cx); + + h_flex() + .p_1() + .justify_between() + .when(expanded, |this| { + this.border_b_1().border_color(cx.theme().colors().border) + }) + .child( + h_flex() + .id("edits-container") + .cursor_pointer() + .w_full() + .gap_1() + .child(Disclosure::new("edits-disclosure", expanded)) + .map(|this| { + if pending_edits { + this.child( + Label::new(format!( + "Editing {} {}…", + changed_buffers.len(), + if changed_buffers.len() == 1 { + "file" + } else { + "files" + } + )) + .color(Color::Muted) + .size(LabelSize::Small) + .with_animation( + "edit-label", + Animation::new(Duration::from_secs(2)) + .repeat() + .with_easing(pulsating_between(0.3, 0.7)), + |label, delta| label.alpha(delta), + ), + ) + } else { + this.child( + Label::new("Edits") + .size(LabelSize::Small) + .color(Color::Muted), + ) + .child(Label::new("•").size(LabelSize::XSmall).color(Color::Muted)) + .child( + Label::new(format!( + "{} {}", + changed_buffers.len(), + if changed_buffers.len() == 1 { + "file" + } else { + "files" + } + )) + .size(LabelSize::Small) + .color(Color::Muted), + ) + } + }) + .on_click(cx.listener(|this, _, _, cx| { + this.edits_expanded = !this.edits_expanded; + cx.notify(); + })), + ) + .child( + h_flex() + .gap_1() + .child( + IconButton::new("review-changes", IconName::ListTodo) + .icon_size(IconSize::Small) + .tooltip({ + let focus_handle = focus_handle.clone(); + move |window, cx| { + Tooltip::for_action_in( + "Review Changes", + &OpenAgentDiff, + &focus_handle, + window, + cx, + ) + } + }) + .on_click(cx.listener(|_, _, window, cx| { + window.dispatch_action(OpenAgentDiff.boxed_clone(), cx); + })), + ) + .child(Divider::vertical().color(DividerColor::Border)) + .child( + Button::new("reject-all-changes", "Reject All") + .label_size(LabelSize::Small) + .disabled(pending_edits) + .when(pending_edits, |this| { + this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL)) + }) + .key_binding( + KeyBinding::for_action_in( + &RejectAll, + &focus_handle.clone(), + 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.reject_all_edits(cx).detach(); + }) + }) + }), + ) + .child( + Button::new("keep-all-changes", "Keep All") + .label_size(LabelSize::Small) + .disabled(pending_edits) + .when(pending_edits, |this| { + this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL)) + }) + .key_binding( + 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); + }) + }) + }), + ), + ) + } + + fn render_edits_bar_files( + &self, + action_log: &Entity<ActionLog>, + changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>, + pending_edits: bool, + cx: &Context<Self>, + ) -> Div { + let editor_bg_color = cx.theme().colors().editor_background; + + v_flex().children(changed_buffers.into_iter().enumerate().flat_map( + |(index, (buffer, _diff))| { + let file = buffer.read(cx).file()?; + let path = file.path(); + + let file_path = path.parent().and_then(|parent| { + let parent_str = parent.to_string_lossy(); + + if parent_str.is_empty() { + None + } else { + Some( + Label::new(format!("/{}{}", parent_str, std::path::MAIN_SEPARATOR_STR)) + .color(Color::Muted) + .size(LabelSize::XSmall) + .buffer_font(cx), + ) + } + }); + + let file_name = path.file_name().map(|name| { + Label::new(name.to_string_lossy().to_string()) + .size(LabelSize::XSmall) + .buffer_font(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(|| { + Icon::new(IconName::File) + .color(Color::Muted) + .size(IconSize::Small) + }); + + let overlay_gradient = linear_gradient( + 90., + linear_color_stop(editor_bg_color, 1.), + linear_color_stop(editor_bg_color.opacity(0.2), 0.), + ); + + let element = h_flex() + .group("edited-code") + .id(("file-container", index)) + .relative() + .py_1() + .pl_2() + .pr_1() + .gap_2() + .justify_between() + .bg(editor_bg_color) + .when(index < changed_buffers.len() - 1, |parent| { + parent.border_color(cx.theme().colors().border).border_b_1() + }) + .child( + h_flex() + .id(("file-name", index)) + .pr_8() + .gap_1p5() + .max_w_full() + .overflow_x_scroll() + .child(file_icon) + .child(h_flex().gap_0p5().children(file_name).children(file_path)) + .on_click({ + let buffer = buffer.clone(); + cx.listener(move |this, _, window, cx| { + this.open_edited_buffer(&buffer, window, cx); + }) + }), + ) + .child( + h_flex() + .gap_1() + .visible_on_hover("edited-code") + .child( + Button::new("review", "Review") + .label_size(LabelSize::Small) + .on_click({ + let buffer = buffer.clone(); + cx.listener(move |this, _, window, cx| { + this.open_edited_buffer(&buffer, window, cx); + }) + }), + ) + .child(Divider::vertical().color(DividerColor::BorderVariant)) + .child( + Button::new("reject-file", "Reject") + .label_size(LabelSize::Small) + .disabled(pending_edits) + .on_click({ + let buffer = buffer.clone(); + let action_log = action_log.clone(); + move |_, _, cx| { + action_log.update(cx, |action_log, cx| { + action_log + .reject_edits_in_ranges( + buffer.clone(), + vec![Anchor::MIN..Anchor::MAX], + cx, + ) + .detach_and_log_err(cx); + }) + } + }), + ) + .child( + Button::new("keep-file", "Keep") + .label_size(LabelSize::Small) + .disabled(pending_edits) + .on_click({ + let buffer = buffer.clone(); + let action_log = action_log.clone(); + move |_, _, cx| { + action_log.update(cx, |action_log, cx| { + action_log.keep_edits_in_range( + buffer.clone(), + Anchor::MIN..Anchor::MAX, + cx, + ); + }) + } + }), + ), + ) + .child( + div() + .id("gradient-overlay") + .absolute() + .h_full() + .w_12() + .top_0() + .bottom_0() + .right(px(152.)) + .bg(overlay_gradient), + ); + + Some(element) + }, + )) + } + fn render_message_editor(&mut self, cx: &mut Context<Self>) -> AnyElement { let settings = ThemeSettings::get_global(cx); let font_size = TextSize::Small @@ -1559,6 +1947,76 @@ impl AcpThreadView { .into_any() } + fn render_send_button(&self, cx: &mut Context<Self>) -> 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) + .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")) + }) + .into_any_element() + } else { + IconButton::new("stop-generation", IconName::StopFilled) + .icon_color(Color::Error) + .style(ButtonStyle::Tinted(ui::TintColor::Error)) + .tooltip(move |window, cx| { + Tooltip::for_action("Stop Generation", &editor::actions::Cancel, window, cx) + }) + .on_click(cx.listener(|this, _event, _, cx| this.cancel(cx))) + .into_any_element() + } + } + + fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement { + let following = self + .workspace + .read_with(cx, |workspace, _| { + workspace.is_being_followed(CollaboratorId::Agent) + }) + .unwrap_or(false); + + IconButton::new("follow-agent", IconName::Crosshair) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .toggle_state(following) + .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) + } else { + Tooltip::with_meta( + "Follow Agent", + Some(&Follow), + "Track the agent's location as it reads and edits files.", + window, + cx, + ) + } + }) + .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(); + })) + } + fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement { let workspace = self.workspace.clone(); MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| { @@ -1673,10 +2131,6 @@ impl Focusable for AcpThreadView { impl Render for AcpThreadView { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { - let text = self.message_editor.read(cx).text(cx); - let is_editor_empty = text.is_empty(); - let focus_handle = self.message_editor.focus_handle(cx); - let open_as_markdown = IconButton::new("open-as-markdown", IconName::DocumentText) .icon_size(IconSize::XSmall) .icon_color(Color::Ignored) @@ -1702,6 +2156,7 @@ impl Render for AcpThreadView { .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)) .child(match &self.thread_state { ThreadState::Unauthenticated { .. } => v_flex() .p_2() @@ -1755,6 +2210,7 @@ impl Render for AcpThreadView { .child(LoadingLabel::new("").size(LabelSize::Small)) .into(), }) + .children(self.render_edits_bar(&thread, window, cx)) } else { this.child(self.render_empty_state(false, cx)) } @@ -1782,47 +2238,12 @@ impl Render for AcpThreadView { .border_t_1() .border_color(cx.theme().colors().border) .child(self.render_message_editor(cx)) - .child({ - let thread = self.thread(); - - h_flex().justify_end().child( - if thread.map_or(true, |thread| { - thread.read(cx).status() == ThreadStatus::Idle - }) { - IconButton::new("send-message", IconName::Send) - .icon_color(Color::Accent) - .style(ButtonStyle::Filled) - .disabled(thread.is_none() || is_editor_empty) - .on_click({ - let focus_handle = focus_handle.clone(); - move |_event, window, cx| { - focus_handle.dispatch_action(&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")) - }) - } else { - IconButton::new("stop-generation", IconName::StopFilled) - .icon_color(Color::Error) - .style(ButtonStyle::Tinted(ui::TintColor::Error)) - .tooltip(move |window, cx| { - Tooltip::for_action( - "Stop Generation", - &editor::actions::Cancel, - window, - cx, - ) - }) - .on_click(cx.listener(|this, _event, _, cx| this.cancel(cx))) - }, - ) - }), + .child( + h_flex() + .justify_between() + .child(self.render_follow_toggle(cx)) + .child(self.render_send_button(cx)), + ), ) } } diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index 1a0f3ff27d83a98d343985b3f827aab26afd192a..31fb0dd69fbbd133888eb26d14643d816c810554 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -1,7 +1,9 @@ use crate::{Keep, KeepAll, OpenAgentDiff, Reject, RejectAll}; -use agent::{Thread, ThreadEvent}; +use acp::{AcpThread, AcpThreadEvent}; +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::{ @@ -41,16 +43,108 @@ use zed_actions::assistant::ToggleFocus; pub struct AgentDiffPane { multibuffer: Entity<MultiBuffer>, editor: Entity<Editor>, - thread: Entity<Thread>, + thread: AgentDiffThread, focus_handle: FocusHandle, workspace: WeakEntity<Workspace>, title: SharedString, _subscriptions: Vec<Subscription>, } +#[derive(PartialEq, Eq, Clone)] +pub enum AgentDiffThread { + Native(Entity<Thread>), + AcpThread(Entity<AcpThread>), +} + +impl AgentDiffThread { + fn project(&self, cx: &App) -> Entity<Project> { + match self { + AgentDiffThread::Native(thread) => thread.read(cx).project().clone(), + AgentDiffThread::AcpThread(thread) => thread.read(cx).project().clone(), + } + } + fn action_log(&self, cx: &App) -> Entity<ActionLog> { + match self { + AgentDiffThread::Native(thread) => thread.read(cx).action_log().clone(), + AgentDiffThread::AcpThread(thread) => thread.read(cx).action_log().clone(), + } + } + + fn summary(&self, cx: &App) -> ThreadSummary { + match self { + AgentDiffThread::Native(thread) => thread.read(cx).summary().clone(), + AgentDiffThread::AcpThread(thread) => ThreadSummary::Ready(thread.read(cx).title()), + } + } + + fn is_generating(&self, cx: &App) -> bool { + match self { + AgentDiffThread::Native(thread) => thread.read(cx).is_generating(), + AgentDiffThread::AcpThread(thread) => { + thread.read(cx).status() == acp::ThreadStatus::Generating + } + } + } + + fn has_pending_edit_tool_uses(&self, cx: &App) -> bool { + match self { + AgentDiffThread::Native(thread) => thread.read(cx).has_pending_edit_tool_uses(), + AgentDiffThread::AcpThread(thread) => thread.read(cx).has_pending_edit_tool_calls(), + } + } + + fn downgrade(&self) -> WeakAgentDiffThread { + match self { + AgentDiffThread::Native(thread) => WeakAgentDiffThread::Native(thread.downgrade()), + AgentDiffThread::AcpThread(thread) => { + WeakAgentDiffThread::AcpThread(thread.downgrade()) + } + } + } +} + +impl From<Entity<Thread>> for AgentDiffThread { + fn from(entity: Entity<Thread>) -> Self { + AgentDiffThread::Native(entity) + } +} + +impl From<Entity<AcpThread>> for AgentDiffThread { + fn from(entity: Entity<AcpThread>) -> Self { + AgentDiffThread::AcpThread(entity) + } +} + +#[derive(PartialEq, Eq, Clone)] +pub enum WeakAgentDiffThread { + Native(WeakEntity<Thread>), + AcpThread(WeakEntity<AcpThread>), +} + +impl WeakAgentDiffThread { + pub fn upgrade(&self) -> Option<AgentDiffThread> { + match self { + WeakAgentDiffThread::Native(weak) => weak.upgrade().map(AgentDiffThread::Native), + WeakAgentDiffThread::AcpThread(weak) => weak.upgrade().map(AgentDiffThread::AcpThread), + } + } +} + +impl From<WeakEntity<Thread>> for WeakAgentDiffThread { + fn from(entity: WeakEntity<Thread>) -> Self { + WeakAgentDiffThread::Native(entity) + } +} + +impl From<WeakEntity<AcpThread>> for WeakAgentDiffThread { + fn from(entity: WeakEntity<AcpThread>) -> Self { + WeakAgentDiffThread::AcpThread(entity) + } +} + impl AgentDiffPane { pub fn deploy( - thread: Entity<Thread>, + thread: impl Into<AgentDiffThread>, workspace: WeakEntity<Workspace>, window: &mut Window, cx: &mut App, @@ -61,14 +155,16 @@ impl AgentDiffPane { } pub fn deploy_in_workspace( - thread: Entity<Thread>, + thread: impl Into<AgentDiffThread>, workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>, ) -> Entity<Self> { + let thread = thread.into(); let existing_diff = workspace .items_of_type::<AgentDiffPane>(cx) .find(|diff| diff.read(cx).thread == thread); + if let Some(existing_diff) = existing_diff { workspace.activate_item(&existing_diff, true, true, window, cx); existing_diff @@ -81,7 +177,7 @@ impl AgentDiffPane { } pub fn new( - thread: Entity<Thread>, + thread: AgentDiffThread, workspace: WeakEntity<Workspace>, window: &mut Window, cx: &mut Context<Self>, @@ -89,7 +185,7 @@ impl AgentDiffPane { let focus_handle = cx.focus_handle(); let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - let project = thread.read(cx).project().clone(); + let project = thread.project(cx).clone(); let editor = cx.new(|cx| { let mut editor = Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx); @@ -100,16 +196,27 @@ impl AgentDiffPane { editor }); - let action_log = thread.read(cx).action_log().clone(); + let action_log = thread.action_log(cx).clone(); + let mut this = Self { - _subscriptions: vec![ - cx.observe_in(&action_log, window, |this, _action_log, window, cx| { - this.update_excerpts(window, cx) - }), - cx.subscribe(&thread, |this, _thread, event, cx| { - this.handle_thread_event(event, cx) - }), - ], + _subscriptions: [ + Some( + 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, + }, + ] + .into_iter() + .flatten() + .collect(), title: SharedString::default(), multibuffer, editor, @@ -123,8 +230,7 @@ impl AgentDiffPane { } fn update_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) { - let thread = self.thread.read(cx); - let changed_buffers = thread.action_log().read(cx).changed_buffers(cx); + let changed_buffers = self.thread.action_log(cx).read(cx).changed_buffers(cx); let mut paths_to_delete = self.multibuffer.read(cx).paths().collect::<HashSet<_>>(); for (buffer, diff_handle) in changed_buffers { @@ -211,7 +317,7 @@ impl AgentDiffPane { } fn update_title(&mut self, cx: &mut Context<Self>) { - let new_title = self.thread.read(cx).summary().unwrap_or("Agent Changes"); + let new_title = self.thread.summary(cx).unwrap_or("Agent Changes"); if new_title != self.title { self.title = new_title; cx.emit(EditorEvent::TitleChanged); @@ -275,14 +381,15 @@ impl AgentDiffPane { fn keep_all(&mut self, _: &KeepAll, _window: &mut Window, cx: &mut Context<Self>) { self.thread - .update(cx, |thread, cx| thread.keep_all_edits(cx)); + .action_log(cx) + .update(cx, |action_log, cx| action_log.keep_all_edits(cx)) } } fn keep_edits_in_selection( editor: &mut Editor, buffer_snapshot: &MultiBufferSnapshot, - thread: &Entity<Thread>, + thread: &AgentDiffThread, window: &mut Window, cx: &mut Context<Editor>, ) { @@ -297,7 +404,7 @@ fn keep_edits_in_selection( fn reject_edits_in_selection( editor: &mut Editor, buffer_snapshot: &MultiBufferSnapshot, - thread: &Entity<Thread>, + thread: &AgentDiffThread, window: &mut Window, cx: &mut Context<Editor>, ) { @@ -311,7 +418,7 @@ fn reject_edits_in_selection( fn keep_edits_in_ranges( editor: &mut Editor, buffer_snapshot: &MultiBufferSnapshot, - thread: &Entity<Thread>, + thread: &AgentDiffThread, ranges: Vec<Range<editor::Anchor>>, window: &mut Window, cx: &mut Context<Editor>, @@ -326,8 +433,8 @@ fn keep_edits_in_ranges( for hunk in &diff_hunks_in_ranges { let buffer = multibuffer.read(cx).buffer(hunk.buffer_id); if let Some(buffer) = buffer { - thread.update(cx, |thread, cx| { - thread.keep_edits_in_range(buffer, hunk.buffer_range.clone(), cx) + thread.action_log(cx).update(cx, |action_log, cx| { + action_log.keep_edits_in_range(buffer, hunk.buffer_range.clone(), cx) }); } } @@ -336,7 +443,7 @@ fn keep_edits_in_ranges( fn reject_edits_in_ranges( editor: &mut Editor, buffer_snapshot: &MultiBufferSnapshot, - thread: &Entity<Thread>, + thread: &AgentDiffThread, ranges: Vec<Range<editor::Anchor>>, window: &mut Window, cx: &mut Context<Editor>, @@ -362,8 +469,9 @@ fn reject_edits_in_ranges( for (buffer, ranges) in ranges_by_buffer { thread - .update(cx, |thread, cx| { - thread.reject_edits_in_ranges(buffer, ranges, cx) + .action_log(cx) + .update(cx, |action_log, cx| { + action_log.reject_edits_in_ranges(buffer, ranges, cx) }) .detach_and_log_err(cx); } @@ -461,7 +569,7 @@ impl Item for AgentDiffPane { } fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement { - let summary = self.thread.read(cx).summary().unwrap_or("Agent Changes"); + let summary = self.thread.summary(cx).unwrap_or("Agent Changes"); Label::new(format!("Review: {}", summary)) .color(if params.selected { Color::Default @@ -641,7 +749,7 @@ impl Render for AgentDiffPane { } } -fn diff_hunk_controls(thread: &Entity<Thread>) -> editor::RenderDiffHunkControlsFn { +fn diff_hunk_controls(thread: &AgentDiffThread) -> editor::RenderDiffHunkControlsFn { let thread = thread.clone(); Arc::new( @@ -676,7 +784,7 @@ fn render_diff_hunk_controls( hunk_range: Range<editor::Anchor>, is_created_file: bool, line_height: Pixels, - thread: &Entity<Thread>, + thread: &AgentDiffThread, editor: &Entity<Editor>, window: &mut Window, cx: &mut App, @@ -1112,11 +1220,8 @@ impl Render for AgentDiffToolbar { return Empty.into_any(); }; - let has_pending_edit_tool_use = agent_diff - .read(cx) - .thread - .read(cx) - .has_pending_edit_tool_uses(); + let has_pending_edit_tool_use = + agent_diff.read(cx).thread.has_pending_edit_tool_uses(cx); if has_pending_edit_tool_use { return div().px_2().child(spinner_icon).into_any(); @@ -1187,8 +1292,8 @@ pub enum EditorState { } struct WorkspaceThread { - thread: WeakEntity<Thread>, - _thread_subscriptions: [Subscription; 2], + thread: WeakAgentDiffThread, + _thread_subscriptions: (Subscription, Subscription), singleton_editors: HashMap<WeakEntity<Buffer>, HashMap<WeakEntity<Editor>, Subscription>>, _settings_subscription: Subscription, _workspace_subscription: Option<Subscription>, @@ -1212,23 +1317,23 @@ impl AgentDiff { pub fn set_active_thread( workspace: &WeakEntity<Workspace>, - thread: &Entity<Thread>, + thread: impl Into<AgentDiffThread>, window: &mut Window, cx: &mut App, ) { Self::global(cx).update(cx, |this, cx| { - this.register_active_thread_impl(workspace, thread, window, cx); + this.register_active_thread_impl(workspace, thread.into(), window, cx); }); } fn register_active_thread_impl( &mut self, workspace: &WeakEntity<Workspace>, - thread: &Entity<Thread>, + thread: AgentDiffThread, window: &mut Window, cx: &mut Context<Self>, ) { - let action_log = thread.read(cx).action_log().clone(); + let action_log = thread.action_log(cx).clone(); let action_log_subscription = cx.observe_in(&action_log, window, { let workspace = workspace.clone(); @@ -1237,17 +1342,25 @@ impl AgentDiff { } }); - let thread_subscription = cx.subscribe_in(&thread, window, { - let workspace = workspace.clone(); - move |this, _thread, event, window, cx| { - this.handle_thread_event(&workspace, event, window, cx) - } - }); + let thread_subscription = match &thread { + 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, { + let workspace = workspace.clone(); + move |this, thread, event, window, cx| { + this.handle_acp_thread_event(&workspace, thread, event, window, cx) + } + }), + }; 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]; + workspace_thread._thread_subscriptions = (action_log_subscription, thread_subscription); self.update_reviewing_editors(&workspace, window, cx); return; } @@ -1272,7 +1385,7 @@ impl AgentDiff { workspace.clone(), WorkspaceThread { thread: thread.downgrade(), - _thread_subscriptions: [action_log_subscription, thread_subscription], + _thread_subscriptions: (action_log_subscription, thread_subscription), singleton_editors: HashMap::default(), _settings_subscription: settings_subscription, _workspace_subscription: workspace_subscription, @@ -1319,7 +1432,7 @@ impl AgentDiff { fn register_review_action<T: Action>( workspace: &mut Workspace, - review: impl Fn(&Entity<Editor>, &Entity<Thread>, &mut Window, &mut App) -> PostReviewState + review: impl Fn(&Entity<Editor>, &AgentDiffThread, &mut Window, &mut App) -> PostReviewState + 'static, this: &Entity<AgentDiff>, ) { @@ -1338,7 +1451,7 @@ impl AgentDiff { }); } - fn handle_thread_event( + fn handle_native_thread_event( &mut self, workspace: &WeakEntity<Workspace>, event: &ThreadEvent, @@ -1380,6 +1493,40 @@ impl AgentDiff { } } + fn handle_acp_thread_event( + &mut self, + workspace: &WeakEntity<Workspace>, + thread: &Entity<AcpThread>, + event: &AcpThreadEvent, + window: &mut Window, + cx: &mut Context<Self>, + ) { + match event { + AcpThreadEvent::NewEntry => { + if thread + .read(cx) + .entries() + .last() + .and_then(|entry| entry.diff()) + .is_some() + { + self.update_reviewing_editors(workspace, window, cx); + } + } + AcpThreadEvent::EntryUpdated(ix) => { + if thread + .read(cx) + .entries() + .get(*ix) + .and_then(|entry| entry.diff()) + .is_some() + { + self.update_reviewing_editors(workspace, window, cx); + } + } + } + } + fn handle_workspace_event( &mut self, workspace: &Entity<Workspace>, @@ -1485,7 +1632,7 @@ impl AgentDiff { return; }; - let action_log = thread.read(cx).action_log(); + let action_log = thread.action_log(cx); let changed_buffers = action_log.read(cx).changed_buffers(cx); let mut unaffected = self.reviewing_editors.clone(); @@ -1510,7 +1657,7 @@ impl AgentDiff { multibuffer.add_diff(diff_handle.clone(), cx); }); - let new_state = if thread.read(cx).is_generating() { + let new_state = if thread.is_generating(cx) { EditorState::Generating } else { EditorState::Reviewing @@ -1606,7 +1753,7 @@ impl AgentDiff { fn keep_all( editor: &Entity<Editor>, - thread: &Entity<Thread>, + thread: &AgentDiffThread, window: &mut Window, cx: &mut App, ) -> PostReviewState { @@ -1626,7 +1773,7 @@ impl AgentDiff { fn reject_all( editor: &Entity<Editor>, - thread: &Entity<Thread>, + thread: &AgentDiffThread, window: &mut Window, cx: &mut App, ) -> PostReviewState { @@ -1646,7 +1793,7 @@ impl AgentDiff { fn keep( editor: &Entity<Editor>, - thread: &Entity<Thread>, + thread: &AgentDiffThread, window: &mut Window, cx: &mut App, ) -> PostReviewState { @@ -1659,7 +1806,7 @@ impl AgentDiff { fn reject( editor: &Entity<Editor>, - thread: &Entity<Thread>, + thread: &AgentDiffThread, window: &mut Window, cx: &mut App, ) -> PostReviewState { @@ -1682,7 +1829,7 @@ impl AgentDiff { fn review_in_active_editor( &mut self, workspace: &mut Workspace, - review: impl Fn(&Entity<Editor>, &Entity<Thread>, &mut Window, &mut App) -> PostReviewState, + review: impl Fn(&Entity<Editor>, &AgentDiffThread, &mut Window, &mut App) -> PostReviewState, window: &mut Window, cx: &mut Context<Self>, ) -> Option<Task<Result<()>>> { @@ -1703,7 +1850,7 @@ impl AgentDiff { 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.read(cx).action_log().read(cx).changed_buffers(cx); + 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); @@ -1801,8 +1948,9 @@ mod tests { }) .await .unwrap(); - let thread = thread_store.update(cx, |store, cx| store.create_thread(cx)); - let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone()); + let thread = + AgentDiffThread::Native(thread_store.update(cx, |store, cx| store.create_thread(cx))); + let action_log = cx.read(|cx| thread.action_log(cx)); let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); @@ -1988,8 +2136,9 @@ mod tests { }); // Set the active thread + let thread = AgentDiffThread::Native(thread); cx.update(|window, cx| { - AgentDiff::set_active_thread(&workspace.downgrade(), &thread, window, cx) + AgentDiff::set_active_thread(&workspace.downgrade(), thread.clone(), window, cx) }); let buffer1 = project diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 8485c5f09218341081407478bc70650579c44154..7f3addc1f4927ce2e17b9a71169afbb07e62e65d 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -8,6 +8,7 @@ use db::kvp::{Dismissable, KEY_VALUE_STORE}; use serde::{Deserialize, Serialize}; use crate::NewAcpThread; +use crate::agent_diff::AgentDiffThread; use crate::language_model_selector::ToggleModelSelector; use crate::{ AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode, @@ -624,7 +625,7 @@ impl AgentPanel { } }; - AgentDiff::set_active_thread(&workspace, &thread, window, cx); + AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx); let weak_panel = weak_self.clone(); @@ -845,7 +846,7 @@ impl AgentPanel { let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx); self.set_active_view(thread_view, window, cx); - AgentDiff::set_active_thread(&self.workspace, &thread, window, cx); + AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx); } fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) { @@ -890,11 +891,20 @@ impl AgentPanel { cx.spawn_in(window, async move |this, cx| { let thread_view = cx.new_window_entity(|window, cx| { - crate::acp::AcpThreadView::new(workspace, project, window, cx) + crate::acp::AcpThreadView::new(workspace.clone(), project, window, cx) })?; this.update_in(cx, |this, window, cx| { - this.set_active_view(ActiveView::AcpThread { thread_view }, window, cx); + this.set_active_view( + ActiveView::AcpThread { + thread_view: thread_view.clone(), + }, + window, + cx, + ); }) + .log_err(); + + anyhow::Ok(()) }) .detach(); } @@ -1050,7 +1060,7 @@ impl AgentPanel { let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx); self.set_active_view(thread_view, window, cx); - AgentDiff::set_active_thread(&self.workspace, &thread, window, cx); + AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx); } pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) { @@ -1181,7 +1191,12 @@ impl AgentPanel { let thread = thread.read(cx).thread().clone(); self.workspace .update(cx, |workspace, cx| { - AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx) + AgentDiffPane::deploy_in_workspace( + AgentDiffThread::Native(thread), + workspace, + window, + cx, + ) }) .log_err(); } diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index 25c62c5fb32c805d72b4dfd46d7aa6f38579b07c..d2b136f274f98842ee248016b400883083ab62d5 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -2,6 +2,7 @@ use std::collections::BTreeMap; use std::rc::Rc; use std::sync::Arc; +use crate::agent_diff::AgentDiffThread; use crate::agent_model_selector::AgentModelSelector; use crate::language_model_selector::ToggleModelSelector; use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip}; @@ -475,9 +476,12 @@ impl MessageEditor { window: &mut Window, cx: &mut Context<Self>, ) { - if let Ok(diff) = - AgentDiffPane::deploy(self.thread.clone(), self.workspace.clone(), window, cx) - { + if let Ok(diff) = AgentDiffPane::deploy( + AgentDiffThread::Native(self.thread.clone()), + self.workspace.clone(), + window, + cx, + ) { let path_key = multi_buffer::PathKey::for_buffer(&buffer, cx); diff.update(cx, |diff, cx| diff.move_to_path(path_key, window, cx)); } diff --git a/crates/assistant_tool/src/action_log.rs b/crates/assistant_tool/src/action_log.rs index 2071a1f444b547197fabc252fddb1f9bd165ae67..e983075cd1e6db22af77856d50a43ebf812de825 100644 --- a/crates/assistant_tool/src/action_log.rs +++ b/crates/assistant_tool/src/action_log.rs @@ -8,7 +8,7 @@ use language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint}; use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle}; use std::{cmp, ops::Range, sync::Arc}; use text::{Edit, Patch, Rope}; -use util::RangeExt; +use util::{RangeExt, ResultExt as _}; /// Tracks actions performed by tools in a thread pub struct ActionLog { @@ -47,6 +47,10 @@ impl ActionLog { self.edited_since_project_diagnostics_check } + pub fn latest_snapshot(&self, buffer: &Entity<Buffer>) -> Option<text::BufferSnapshot> { + Some(self.tracked_buffers.get(buffer)?.snapshot.clone()) + } + fn track_buffer_internal( &mut self, buffer: Entity<Buffer>, @@ -715,6 +719,22 @@ impl ActionLog { cx.notify(); } + pub fn reject_all_edits(&mut self, cx: &mut Context<Self>) -> Task<()> { + let futures = self.changed_buffers(cx).into_keys().map(|buffer| { + let reject = self.reject_edits_in_ranges(buffer, vec![Anchor::MIN..Anchor::MAX], cx); + + async move { + reject.await.log_err(); + } + }); + + let task = futures::future::join_all(futures); + + cx.spawn(async move |_, _| { + task.await; + }) + } + /// Returns the set of buffers that contain edits that haven't been reviewed by the user. pub fn changed_buffers(&self, cx: &App) -> BTreeMap<Entity<Buffer>, Entity<BufferDiff>> { self.tracked_buffers From 70351360d7985b1cc5b82b84820934c8e9f4b49c Mon Sep 17 00:00:00 2001 From: Alisina Bahadori <alisina.bm@gmail.com> Date: Fri, 11 Jul 2025 11:50:48 -0400 Subject: [PATCH 020/658] Fix bad kerning in integrated terminal (#34292) Closes #16869 Release Notes: - (preview only): Fix bad kerning in integrated terminal. --- crates/terminal_view/src/terminal_element.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index d883959298936cb50458e8f0c9b8cb0c1ad2a3f2..d05f6bb5da2075ec3e9829e9e93ba858f520f207 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -127,7 +127,7 @@ impl BatchedTextRun { cx: &mut App, ) { let pos = Point::new( - (origin.x + self.start_point.column as f32 * dimensions.cell_width).floor(), + origin.x + self.start_point.column as f32 * dimensions.cell_width, origin.y + self.start_point.line as f32 * dimensions.line_height, ); From d65855c4a187cf17e0012e87edffda3a36cbba3e Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Fri, 11 Jul 2025 13:15:06 -0300 Subject: [PATCH 021/658] git: Change merge conflict button labels (#34297) Following feedback that "Take Ours" and "Take Theirs" was confusing, leading to users not knowing what exactly happened with each of these buttons. It's now "Use HEAD" and "Use Origin", which also match what is written in Git markers, helping parse them out more easily. Future improvement is to have the actual branch target name in the "Use Origin" button. Release Notes: - git: Improved merge conflict buttons clarity by changing labels to "Use HEAD" and "Use Origin". --- crates/git_ui/src/conflict_view.rs | 42 ++++++++---------------------- 1 file changed, 11 insertions(+), 31 deletions(-) diff --git a/crates/git_ui/src/conflict_view.rs b/crates/git_ui/src/conflict_view.rs index 8eadf70830fbdf75e9077f3859d443f0aec12849..c07b99b875b6674286840c02ea2bf69ca5a6296b 100644 --- a/crates/git_ui/src/conflict_view.rs +++ b/crates/git_ui/src/conflict_view.rs @@ -11,10 +11,7 @@ use gpui::{ use language::{Anchor, Buffer, BufferId}; use project::{ConflictRegion, ConflictSet, ConflictSetUpdate, ProjectItem as _}; use std::{ops::Range, sync::Arc}; -use ui::{ - ActiveTheme, AnyElement, Element as _, StatefulInteractiveElement, Styled, - StyledTypography as _, Window, div, h_flex, rems, -}; +use ui::{ActiveTheme, Element as _, Styled, Window, prelude::*}; use util::{ResultExt as _, debug_panic, maybe}; pub(crate) struct ConflictAddon { @@ -391,20 +388,15 @@ fn render_conflict_buttons( cx: &mut BlockContext, ) -> AnyElement { h_flex() + .id(cx.block_id) .h(cx.line_height) - .items_end() .ml(cx.margins.gutter.width) - .id(cx.block_id) - .gap_0p5() + .items_end() + .gap_1() + .bg(cx.theme().colors().editor_background) .child( - div() - .id("ours") - .px_1() - .child("Take Ours") - .rounded_t(rems(0.2)) - .text_ui_sm(cx) - .hover(|this| this.bg(cx.theme().colors().element_background)) - .cursor_pointer() + Button::new("head", "Use HEAD") + .label_size(LabelSize::Small) .on_click({ let editor = editor.clone(); let conflict = conflict.clone(); @@ -423,14 +415,8 @@ fn render_conflict_buttons( }), ) .child( - div() - .id("theirs") - .px_1() - .child("Take Theirs") - .rounded_t(rems(0.2)) - .text_ui_sm(cx) - .hover(|this| this.bg(cx.theme().colors().element_background)) - .cursor_pointer() + Button::new("origin", "Use Origin") + .label_size(LabelSize::Small) .on_click({ let editor = editor.clone(); let conflict = conflict.clone(); @@ -449,14 +435,8 @@ fn render_conflict_buttons( }), ) .child( - div() - .id("both") - .px_1() - .child("Take Both") - .rounded_t(rems(0.2)) - .text_ui_sm(cx) - .hover(|this| this.bg(cx.theme().colors().element_background)) - .cursor_pointer() + Button::new("both", "Use Both") + .label_size(LabelSize::Small) .on_click({ let editor = editor.clone(); let conflict = conflict.clone(); From d0e01dbd8f4dafbe9dd6720de0e3fe7033ae7820 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga <agus@zed.dev> Date: Fri, 11 Jul 2025 13:24:41 -0300 Subject: [PATCH 022/658] Improve thread message history (#34299) - Keep history across threads - Reset position when edited Release Notes: - N/A --------- Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com> --- crates/agent_ui/src/acp.rs | 1 + crates/agent_ui/src/acp/message_history.rs | 12 ++- crates/agent_ui/src/acp/thread_view.rs | 95 +++++++++++++--------- crates/agent_ui/src/agent_panel.rs | 15 +++- 4 files changed, 79 insertions(+), 44 deletions(-) diff --git a/crates/agent_ui/src/acp.rs b/crates/agent_ui/src/acp.rs index 23ada8d77a536391de6968ba9f9c0ee351967266..cc476b1a862b11d964f731cbb0d5ac8e9f100e59 100644 --- a/crates/agent_ui/src/acp.rs +++ b/crates/agent_ui/src/acp.rs @@ -2,4 +2,5 @@ mod completion_provider; mod message_history; mod thread_view; +pub use message_history::MessageHistory; pub use thread_view::AcpThreadView; diff --git a/crates/agent_ui/src/acp/message_history.rs b/crates/agent_ui/src/acp/message_history.rs index 6d9626627af3c4cc7a13d4347069ff7349725afb..d0fb1f09908f5505f94777b68b59e18df715c2c4 100644 --- a/crates/agent_ui/src/acp/message_history.rs +++ b/crates/agent_ui/src/acp/message_history.rs @@ -3,19 +3,25 @@ pub struct MessageHistory<T> { current: Option<usize>, } -impl<T> MessageHistory<T> { - pub fn new() -> Self { +impl<T> Default for MessageHistory<T> { + fn default() -> Self { MessageHistory { items: Vec::new(), current: None, } } +} +impl<T> MessageHistory<T> { 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; @@ -46,7 +52,7 @@ mod tests { #[test] fn test_prev_next() { - let mut history = MessageHistory::new(); + let mut history = MessageHistory::default(); // Test empty history assert_eq!(history.prev(), None); diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 3db5e52a0a93a3f4e62a4da38977968a19dcf06e..353d712afd5b3561898c88867c1da2b7109ad681 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1,3 +1,4 @@ +use std::cell::RefCell; use std::collections::BTreeMap; use std::path::Path; use std::rc::Rc; @@ -53,6 +54,8 @@ pub struct AcpThreadView { thread_state: ThreadState, diff_editors: HashMap<EntityId, Entity<Editor>>, message_editor: Entity<Editor>, + message_set_from_history: bool, + _message_editor_subscription: Subscription, mention_set: Arc<Mutex<MentionSet>>, last_error: Option<Entity<Markdown>>, list_state: ListState, @@ -60,7 +63,7 @@ pub struct AcpThreadView { expanded_tool_calls: HashSet<ToolCallId>, expanded_thinking_blocks: HashSet<(usize, usize)>, edits_expanded: bool, - message_history: MessageHistory<acp::SendUserMessageParams>, + message_history: Rc<RefCell<MessageHistory<acp::SendUserMessageParams>>>, } enum ThreadState { @@ -81,6 +84,7 @@ impl AcpThreadView { pub fn new( workspace: WeakEntity<Workspace>, project: Entity<Project>, + message_history: Rc<RefCell<MessageHistory<acp::SendUserMessageParams>>>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { @@ -125,6 +129,17 @@ 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(); + } + this.message_set_from_history = false; + } + }); + + let mention_set = mention_set.clone(); + let list_state = ListState::new( 0, gpui::ListAlignment::Bottom, @@ -147,6 +162,8 @@ impl AcpThreadView { project: project.clone(), thread_state: Self::initial_state(workspace, project, window, cx), message_editor, + message_set_from_history: false, + _message_editor_subscription: message_editor_subscription, mention_set, diff_editors: Default::default(), list_state: list_state, @@ -155,7 +172,7 @@ impl AcpThreadView { expanded_tool_calls: HashSet::default(), expanded_thinking_blocks: HashSet::default(), edits_expanded: false, - message_history: MessageHistory::new(), + message_history, } } @@ -358,7 +375,7 @@ impl AcpThreadView { editor.remove_creases(mention_set.lock().drain(), cx) }); - self.message_history.push(message); + self.message_history.borrow_mut().push(message); } fn previous_history_message( @@ -367,11 +384,11 @@ impl AcpThreadView { window: &mut Window, cx: &mut Context<Self>, ) { - Self::set_draft_message( + self.message_set_from_history = Self::set_draft_message( self.message_editor.clone(), self.mention_set.clone(), self.project.clone(), - self.message_history.prev(), + self.message_history.borrow_mut().prev(), window, cx, ); @@ -383,43 +400,16 @@ impl AcpThreadView { window: &mut Window, cx: &mut Context<Self>, ) { - Self::set_draft_message( + self.message_set_from_history = Self::set_draft_message( self.message_editor.clone(), self.mention_set.clone(), self.project.clone(), - self.message_history.next(), + self.message_history.borrow_mut().next(), window, cx, ); } - fn open_agent_diff(&mut self, _: &OpenAgentDiff, window: &mut Window, cx: &mut Context<Self>) { - if let Some(thread) = self.thread() { - AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err(); - } - } - - fn open_edited_buffer( - &mut self, - buffer: &Entity<Buffer>, - window: &mut Window, - cx: &mut Context<Self>, - ) { - let Some(thread) = self.thread() else { - return; - }; - - let Some(diff) = - AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err() - else { - return; - }; - - diff.update(cx, |diff, cx| { - diff.move_to_path(PathKey::for_buffer(&buffer, cx), window, cx) - }) - } - fn set_draft_message( message_editor: Entity<Editor>, mention_set: Arc<Mutex<MentionSet>>, @@ -427,15 +417,11 @@ impl AcpThreadView { message: Option<&acp::SendUserMessageParams>, window: &mut Window, cx: &mut Context<Self>, - ) { + ) -> bool { cx.notify(); let Some(message) = message else { - message_editor.update(cx, |editor, cx| { - editor.clear(window, cx); - editor.remove_creases(mention_set.lock().drain(), cx) - }); - return; + return false; }; let mut text = String::new(); @@ -495,6 +481,35 @@ impl AcpThreadView { mention_set.lock().insert(crease_id, project_path); } } + + true + } + + fn open_agent_diff(&mut self, _: &OpenAgentDiff, window: &mut Window, cx: &mut Context<Self>) { + if let Some(thread) = self.thread() { + AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err(); + } + } + + fn open_edited_buffer( + &mut self, + buffer: &Entity<Buffer>, + window: &mut Window, + cx: &mut Context<Self>, + ) { + let Some(thread) = self.thread() else { + return; + }; + + let Some(diff) = + AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err() + else { + return; + }; + + diff.update(cx, |diff, cx| { + diff.move_to_path(PathKey::for_buffer(&buffer, cx), window, cx) + }) } fn handle_thread_event( diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 7f3addc1f4927ce2e17b9a71169afbb07e62e65d..18e43dd51eaca2699bf6feeddd20386b145da628 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1,3 +1,4 @@ +use std::cell::RefCell; use std::ops::Range; use std::path::Path; use std::rc::Rc; @@ -433,6 +434,8 @@ pub struct AgentPanel { configuration_subscription: Option<Subscription>, local_timezone: UtcOffset, active_view: ActiveView, + acp_message_history: + Rc<RefCell<crate::acp::MessageHistory<agentic_coding_protocol::SendUserMessageParams>>>, previous_view: Option<ActiveView>, history_store: Entity<HistoryStore>, history: Entity<ThreadHistory>, @@ -699,6 +702,7 @@ 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, @@ -888,10 +892,17 @@ impl AgentPanel { fn new_gemini_thread(&mut self, window: &mut Window, cx: &mut Context<Self>) { let workspace = self.workspace.clone(); let project = self.project.clone(); + let message_history = self.acp_message_history.clone(); cx.spawn_in(window, async move |this, cx| { let thread_view = cx.new_window_entity(|window, cx| { - crate::acp::AcpThreadView::new(workspace.clone(), project, window, cx) + crate::acp::AcpThreadView::new( + workspace.clone(), + project, + message_history, + window, + cx, + ) })?; this.update_in(cx, |this, window, cx| { this.set_active_view( @@ -1432,6 +1443,8 @@ impl AgentPanel { self.active_view = new_view; } + self.acp_message_history.borrow_mut().reset_position(); + self.focus_handle(cx).focus(window); } From af71e15ea09ebc452f0a0dbf3c57d165f4ddf99e Mon Sep 17 00:00:00 2001 From: Finn Evers <finn@zed.dev> Date: Fri, 11 Jul 2025 19:10:39 +0200 Subject: [PATCH 023/658] editor: Fix scrolling stuttering at the top of multibuffers (#34295) Release Notes: - Fixed an issue where scrolling would stutter at the top of multibuffers. --- crates/editor/src/scroll.rs | 105 +++++++++++++----------------------- 1 file changed, 37 insertions(+), 68 deletions(-) diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index b3007d3091d79074b99b3fe6f2d7b00003f72015..fcc54ef7780e4cd882db12b3ecab08edb48e8807 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -216,80 +216,49 @@ impl ScrollManager { window: &mut Window, cx: &mut Context<Editor>, ) { - let (new_anchor, top_row) = if scroll_position.y <= 0. && scroll_position.x <= 0. { - ( - ScrollAnchor { - anchor: Anchor::min(), - offset: scroll_position.max(&gpui::Point::default()), - }, - 0, - ) - } else if scroll_position.y <= 0. { - let buffer_point = map - .clip_point( - DisplayPoint::new(DisplayRow(0), scroll_position.x as u32), - Bias::Left, - ) - .to_point(map); - let anchor = map.buffer_snapshot.anchor_at(buffer_point, Bias::Right); - - ( - ScrollAnchor { - anchor: anchor, - offset: scroll_position.max(&gpui::Point::default()), - }, - 0, - ) - } else { - let scroll_top = scroll_position.y; - let scroll_top = match EditorSettings::get_global(cx).scroll_beyond_last_line { - ScrollBeyondLastLine::OnePage => scroll_top, - ScrollBeyondLastLine::Off => { - if let Some(height_in_lines) = self.visible_line_count { - let max_row = map.max_point().row().0 as f32; - scroll_top.min(max_row - height_in_lines + 1.).max(0.) - } else { - scroll_top - } + let scroll_top = scroll_position.y.max(0.); + let scroll_top = match EditorSettings::get_global(cx).scroll_beyond_last_line { + ScrollBeyondLastLine::OnePage => scroll_top, + ScrollBeyondLastLine::Off => { + if let Some(height_in_lines) = self.visible_line_count { + let max_row = map.max_point().row().0 as f32; + scroll_top.min(max_row - height_in_lines + 1.).max(0.) + } else { + scroll_top } - ScrollBeyondLastLine::VerticalScrollMargin => { - if let Some(height_in_lines) = self.visible_line_count { - let max_row = map.max_point().row().0 as f32; - scroll_top - .min(max_row - height_in_lines + 1. + self.vertical_scroll_margin) - .max(0.) - } else { - scroll_top - } + } + ScrollBeyondLastLine::VerticalScrollMargin => { + if let Some(height_in_lines) = self.visible_line_count { + let max_row = map.max_point().row().0 as f32; + scroll_top + .min(max_row - height_in_lines + 1. + self.vertical_scroll_margin) + .max(0.) + } else { + scroll_top } - }; + } + }; - let scroll_top_row = DisplayRow(scroll_top as u32); - let scroll_top_buffer_point = map - .clip_point( - DisplayPoint::new(scroll_top_row, scroll_position.x as u32), - Bias::Left, - ) - .to_point(map); - let top_anchor = map - .buffer_snapshot - .anchor_at(scroll_top_buffer_point, Bias::Right); - - ( - ScrollAnchor { - anchor: top_anchor, - offset: point( - scroll_position.x.max(0.), - scroll_top - top_anchor.to_display_point(map).row().as_f32(), - ), - }, - scroll_top_buffer_point.row, + let scroll_top_row = DisplayRow(scroll_top as u32); + let scroll_top_buffer_point = map + .clip_point( + DisplayPoint::new(scroll_top_row, scroll_position.x as u32), + Bias::Left, ) - }; + .to_point(map); + let top_anchor = map + .buffer_snapshot + .anchor_at(scroll_top_buffer_point, Bias::Right); self.set_anchor( - new_anchor, - top_row, + ScrollAnchor { + anchor: top_anchor, + offset: point( + scroll_position.x.max(0.), + scroll_top - top_anchor.to_display_point(map).row().as_f32(), + ), + }, + scroll_top_buffer_point.row, local, autoscroll, workspace_id, From cd024b8870b9bca46921b63ee6d3f93dbd7b863c Mon Sep 17 00:00:00 2001 From: localcc <kate@zed.dev> Date: Fri, 11 Jul 2025 19:28:48 +0200 Subject: [PATCH 024/658] Add licenses.md for Windows build (#34272) Release Notes: - N/A --- script/bundle-windows.ps1 | 8 +++++++ script/generate-licenses.ps1 | 44 ++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 script/generate-licenses.ps1 diff --git a/script/bundle-windows.ps1 b/script/bundle-windows.ps1 index 81d71699b9fb98984dde7df59eee0e1309d73ad3..01a1114c26acaeb8eacd2eb22ebd05a43fabc7e5 100644 --- a/script/bundle-windows.ps1 +++ b/script/bundle-windows.ps1 @@ -56,6 +56,13 @@ function PrepareForBundle { New-Item -Path "$innoDir\tools" -ItemType Directory -Force } +function GenerateLicenses { + $oldErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + . $PSScriptRoot/generate-licenses.ps1 + $ErrorActionPreference = $oldErrorActionPreference +} + function BuildZedAndItsFriends { Write-Output "Building Zed and its friends, for channel: $channel" # Build zed.exe, cli.exe and auto_update_helper.exe @@ -238,6 +245,7 @@ $innoDir = "$env:ZED_WORKSPACE\inno" CheckEnvironmentVariables PrepareForBundle +GenerateLicenses BuildZedAndItsFriends MakeAppx SignZedAndItsFriends diff --git a/script/generate-licenses.ps1 b/script/generate-licenses.ps1 new file mode 100644 index 0000000000000000000000000000000000000000..52a6fe0118b9979be23d0c584bc0facdd4ce8f1e --- /dev/null +++ b/script/generate-licenses.ps1 @@ -0,0 +1,44 @@ +$CARGO_ABOUT_VERSION="0.7" +$outputFile=$args[0] ? $args[0] : "$(Get-Location)/assets/licenses.md" +$templateFile="script/licenses/template.md.hbs" + +New-Item -Path "$outputFile" -ItemType File -Value "" -Force + +@( + "# ###### THEME LICENSES ######\n" + Get-Content assets/themes/LICENSES + "\n# ###### ICON LICENSES ######\n" + Get-Content assets/icons/LICENSES + "\n# ###### CODE LICENSES ######\n" +) | Add-Content -Path $outputFile + +$versionOutput = cargo about --version +if (-not ($versionOutput -match "cargo-about $CARGO_ABOUT_VERSION")) { + Write-Host "Installing cargo-about@^$CARGO_ABOUT_VERSION..." + cargo install "cargo-about@^$CARGO_ABOUT_VERSION" +} else { + Write-Host "cargo-about@^$CARGO_ABOUT_VERSION" is already installed +} + +Write-Host "Generating cargo licenses" + +$failFlag = $env:ALLOW_MISSING_LICENSES ? "--fail" : "" +$args = @('about', 'generate', $failFlag, '-c', 'script/licenses/zed-licenses.toml', $templateFile, '-o', $outputFile) | Where-Object { $_ } +cargo @args + +Write-Host "Applying replacements" +$replacements = @{ + '"' = '"' + ''' = "'" + '=' = '=' + '`' = '`' + '<' = '<' + '>' = '>' +} +$content = Get-Content $outputFile +foreach ($find in $replacements.keys) { + $content = $content -replace $find, $replacements[$find] +} +$content | Set-Content $outputFile + +Write-Host "generate-licenses completed. See $outputFile" From 90bf602ceb764f6c50ece6954a0f05ca9ba500a9 Mon Sep 17 00:00:00 2001 From: Finn Evers <finn@zed.dev> Date: Fri, 11 Jul 2025 19:34:45 +0200 Subject: [PATCH 025/658] Reduce number of snapshots and notifies during editor scrolling (#34228) We not do not create new snapshots anymore when autoscrolling horizontally and also do not notify any longer should the new scroll position match the old one. Release Notes: - N/A --------- Co-authored-by: Michael Sloan <mgsloan@gmail.com> --- crates/editor/src/element.rs | 59 +++++++++++----------- crates/editor/src/scroll.rs | 29 +++++++---- crates/editor/src/scroll/autoscroll.rs | 68 +++++++++++++++----------- 3 files changed, 89 insertions(+), 67 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 014583912fd3233d3bc1872188a15679d766ff2b..8355fa1d66ba9e378cc2276592c8d988e5e91512 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -8035,23 +8035,25 @@ impl Element for EditorElement { } }; - // TODO: Autoscrolling for both axes - let mut autoscroll_request = None; - let mut autoscroll_containing_element = false; - let mut autoscroll_horizontally = false; - self.editor.update(cx, |editor, cx| { - autoscroll_request = editor.autoscroll_request(); - autoscroll_containing_element = + let ( + autoscroll_request, + autoscroll_containing_element, + needs_horizontal_autoscroll, + ) = self.editor.update(cx, |editor, cx| { + let autoscroll_request = editor.autoscroll_request(); + let autoscroll_containing_element = autoscroll_request.is_some() || editor.has_pending_selection(); - // TODO: Is this horizontal or vertical?! - autoscroll_horizontally = editor.autoscroll_vertically( - bounds, - line_height, - max_scroll_top, - window, - cx, - ); - snapshot = editor.snapshot(window, cx); + + let (needs_horizontal_autoscroll, was_scrolled) = editor + .autoscroll_vertically(bounds, line_height, max_scroll_top, window, cx); + if was_scrolled.0 { + snapshot = editor.snapshot(window, cx); + } + ( + autoscroll_request, + autoscroll_containing_element, + needs_horizontal_autoscroll, + ) }); let mut scroll_position = snapshot.scroll_position(); @@ -8460,10 +8462,12 @@ impl Element for EditorElement { ); self.editor.update(cx, |editor, cx| { - let clamped = editor.scroll_manager.clamp_scroll_left(scroll_max.x); + if editor.scroll_manager.clamp_scroll_left(scroll_max.x) { + scroll_position.x = scroll_position.x.min(scroll_max.x); + } - let autoscrolled = if autoscroll_horizontally { - editor.autoscroll_horizontally( + if needs_horizontal_autoscroll.0 + && let Some(new_scroll_position) = editor.autoscroll_horizontally( start_row, editor_content_width, scroll_width, @@ -8472,13 +8476,8 @@ impl Element for EditorElement { window, cx, ) - } else { - false - }; - - if clamped || autoscrolled { - snapshot = editor.snapshot(window, cx); - scroll_position = snapshot.scroll_position(); + { + scroll_position = new_scroll_position; } }); @@ -8593,7 +8592,9 @@ impl Element for EditorElement { } } else { log::error!( - "bug: line_ix {} is out of bounds - row_infos.len(): {}, line_layouts.len(): {}, crease_trailers.len(): {}", + "bug: line_ix {} is out of bounds - row_infos.len(): {}, \ + line_layouts.len(): {}, \ + crease_trailers.len(): {}", line_ix, row_infos.len(), line_layouts.len(), @@ -8839,7 +8840,7 @@ impl Element for EditorElement { underline: None, strikethrough: None, }], - None + None, ); let space_invisible = window.text_system().shape_line( "•".into(), @@ -8852,7 +8853,7 @@ impl Element for EditorElement { underline: None, strikethrough: None, }], - None + None, ); let mode = snapshot.mode.clone(); diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index fcc54ef7780e4cd882db12b3ecab08edb48e8807..7310d6d3c05f4532f0a9ff5d27b86f0efdf24791 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -27,6 +27,8 @@ use workspace::{ItemId, WorkspaceId}; pub const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28); const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); +pub struct WasScrolled(pub(crate) bool); + #[derive(Default)] pub struct ScrollbarAutoHide(pub bool); @@ -215,7 +217,7 @@ impl ScrollManager { workspace_id: Option<WorkspaceId>, window: &mut Window, cx: &mut Context<Editor>, - ) { + ) -> WasScrolled { let scroll_top = scroll_position.y.max(0.); let scroll_top = match EditorSettings::get_global(cx).scroll_beyond_last_line { ScrollBeyondLastLine::OnePage => scroll_top, @@ -264,7 +266,7 @@ impl ScrollManager { workspace_id, window, cx, - ); + ) } fn set_anchor( @@ -276,7 +278,7 @@ impl ScrollManager { workspace_id: Option<WorkspaceId>, window: &mut Window, cx: &mut Context<Editor>, - ) { + ) -> WasScrolled { let adjusted_anchor = if self.forbid_vertical_scroll { ScrollAnchor { offset: gpui::Point::new(anchor.offset.x, self.anchor.offset.y), @@ -286,10 +288,14 @@ impl ScrollManager { anchor }; + self.autoscroll_request.take(); + if self.anchor == adjusted_anchor { + return WasScrolled(false); + } + self.anchor = adjusted_anchor; cx.emit(EditorEvent::ScrollPositionChanged { local, autoscroll }); self.show_scrollbars(window, cx); - self.autoscroll_request.take(); if let Some(workspace_id) = workspace_id { let item_id = cx.entity().entity_id().as_u64() as ItemId; @@ -311,6 +317,8 @@ impl ScrollManager { .detach() } cx.notify(); + + WasScrolled(true) } pub fn show_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Editor>) { @@ -521,13 +529,13 @@ impl Editor { scroll_position: gpui::Point<f32>, window: &mut Window, cx: &mut Context<Self>, - ) { + ) -> WasScrolled { let mut position = scroll_position; if self.scroll_manager.forbid_vertical_scroll { let current_position = self.scroll_position(cx); position.y = current_position.y; } - self.set_scroll_position_internal(position, true, false, window, cx); + self.set_scroll_position_internal(position, true, false, window, cx) } /// Scrolls so that `row` is at the top of the editor view. @@ -559,7 +567,7 @@ impl Editor { autoscroll: bool, window: &mut Window, cx: &mut Context<Self>, - ) { + ) -> WasScrolled { let map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); self.set_scroll_position_taking_display_map( scroll_position, @@ -568,7 +576,7 @@ impl Editor { map, window, cx, - ); + ) } fn set_scroll_position_taking_display_map( @@ -579,7 +587,7 @@ impl Editor { display_map: DisplaySnapshot, window: &mut Window, cx: &mut Context<Self>, - ) { + ) -> WasScrolled { hide_hover(self, cx); let workspace_id = self.workspace.as_ref().and_then(|workspace| workspace.1); @@ -593,7 +601,7 @@ impl Editor { scroll_position }; - self.scroll_manager.set_scroll_position( + let editor_was_scrolled = self.scroll_manager.set_scroll_position( adjusted_position, &display_map, local, @@ -605,6 +613,7 @@ impl Editor { self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); self.refresh_colors(false, None, window, cx); + editor_was_scrolled } pub fn scroll_position(&self, cx: &mut Context<Self>) -> gpui::Point<f32> { diff --git a/crates/editor/src/scroll/autoscroll.rs b/crates/editor/src/scroll/autoscroll.rs index 340277633a2c63131997f9eca76316ccf6c3ad39..e8a1f8da734685f85091b3bd28a2fb1a0be89208 100644 --- a/crates/editor/src/scroll/autoscroll.rs +++ b/crates/editor/src/scroll/autoscroll.rs @@ -1,6 +1,6 @@ use crate::{ DisplayRow, Editor, EditorMode, LineWithInvisibles, RowExt, SelectionEffects, - display_map::ToDisplayPoint, + display_map::ToDisplayPoint, scroll::WasScrolled, }; use gpui::{Bounds, Context, Pixels, Window, px}; use language::Point; @@ -99,19 +99,21 @@ impl AutoscrollStrategy { } } +pub(crate) struct NeedsHorizontalAutoscroll(pub(crate) bool); + impl Editor { pub fn autoscroll_request(&self) -> Option<Autoscroll> { self.scroll_manager.autoscroll_request() } - pub fn autoscroll_vertically( + pub(crate) fn autoscroll_vertically( &mut self, bounds: Bounds<Pixels>, line_height: Pixels, max_scroll_top: f32, window: &mut Window, cx: &mut Context<Editor>, - ) -> bool { + ) -> (NeedsHorizontalAutoscroll, WasScrolled) { let viewport_height = bounds.size.height; let visible_lines = viewport_height / line_height; let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); @@ -129,12 +131,14 @@ impl Editor { scroll_position.y = max_scroll_top; } - if original_y != scroll_position.y { - self.set_scroll_position(scroll_position, window, cx); - } + let editor_was_scrolled = if original_y != scroll_position.y { + self.set_scroll_position(scroll_position, window, cx) + } else { + WasScrolled(false) + }; let Some((autoscroll, local)) = self.scroll_manager.autoscroll_request.take() else { - return false; + return (NeedsHorizontalAutoscroll(false), editor_was_scrolled); }; let mut target_top; @@ -212,7 +216,7 @@ impl Editor { target_bottom = target_top + 1.; } - match strategy { + let was_autoscrolled = match strategy { AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => { let margin = margin.min(self.scroll_manager.vertical_scroll_margin); let target_top = (target_top - margin).max(0.0); @@ -225,39 +229,42 @@ impl Editor { if needs_scroll_up && !needs_scroll_down { scroll_position.y = target_top; - self.set_scroll_position_internal(scroll_position, local, true, window, cx); - } - if !needs_scroll_up && needs_scroll_down { + } else if !needs_scroll_up && needs_scroll_down { scroll_position.y = target_bottom - visible_lines; - self.set_scroll_position_internal(scroll_position, local, true, window, cx); + } + + if needs_scroll_up ^ needs_scroll_down { + self.set_scroll_position_internal(scroll_position, local, true, window, cx) + } else { + WasScrolled(false) } } AutoscrollStrategy::Center => { scroll_position.y = (target_top - margin).max(0.0); - self.set_scroll_position_internal(scroll_position, local, true, window, cx); + self.set_scroll_position_internal(scroll_position, local, true, window, cx) } AutoscrollStrategy::Focused => { let margin = margin.min(self.scroll_manager.vertical_scroll_margin); scroll_position.y = (target_top - margin).max(0.0); - self.set_scroll_position_internal(scroll_position, local, true, window, cx); + self.set_scroll_position_internal(scroll_position, local, true, window, cx) } AutoscrollStrategy::Top => { scroll_position.y = (target_top).max(0.0); - self.set_scroll_position_internal(scroll_position, local, true, window, cx); + self.set_scroll_position_internal(scroll_position, local, true, window, cx) } AutoscrollStrategy::Bottom => { scroll_position.y = (target_bottom - visible_lines).max(0.0); - self.set_scroll_position_internal(scroll_position, local, true, window, cx); + self.set_scroll_position_internal(scroll_position, local, true, window, cx) } AutoscrollStrategy::TopRelative(lines) => { scroll_position.y = target_top - lines as f32; - self.set_scroll_position_internal(scroll_position, local, true, window, cx); + self.set_scroll_position_internal(scroll_position, local, true, window, cx) } AutoscrollStrategy::BottomRelative(lines) => { scroll_position.y = target_bottom + lines as f32; - self.set_scroll_position_internal(scroll_position, local, true, window, cx); + self.set_scroll_position_internal(scroll_position, local, true, window, cx) } - } + }; self.scroll_manager.last_autoscroll = Some(( self.scroll_manager.anchor.offset, @@ -266,7 +273,8 @@ impl Editor { strategy, )); - true + let was_scrolled = WasScrolled(editor_was_scrolled.0 || was_autoscrolled.0); + (NeedsHorizontalAutoscroll(true), was_scrolled) } pub(crate) fn autoscroll_horizontally( @@ -278,7 +286,7 @@ impl Editor { layouts: &[LineWithInvisibles], window: &mut Window, cx: &mut Context<Self>, - ) -> bool { + ) -> Option<gpui::Point<f32>> { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let selections = self.selections.all::<Point>(cx); let mut scroll_position = self.scroll_manager.scroll_position(&display_map); @@ -319,22 +327,26 @@ impl Editor { target_right = target_right.min(scroll_width); if target_right - target_left > viewport_width { - return false; + return None; } let scroll_left = self.scroll_manager.anchor.offset.x * em_advance; let scroll_right = scroll_left + viewport_width; - if target_left < scroll_left { + 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); - true + self.set_scroll_position_internal(scroll_position, true, 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); - true + self.set_scroll_position_internal(scroll_position, true, true, window, cx) + } else { + WasScrolled(false) + }; + + if was_scrolled.0 { + Some(scroll_position) } else { - false + None } } From 0bd65829f7b1971daf12bb7cf96d61f1b5091956 Mon Sep 17 00:00:00 2001 From: Julia Ryan <juliaryan3.14@gmail.com> Date: Fri, 11 Jul 2025 10:49:52 -0700 Subject: [PATCH 026/658] Truncate multi-line debug value hints (#34305) Release Notes: - Multi-line debug inline values are now truncated. Co-authored-by: Anthony Eid <hello@anthonyeid.me> --- crates/debugger_ui/src/tests/inline_values.rs | 31 +++++++++++++++++++ crates/editor/src/editor.rs | 5 +-- crates/project/src/debugger/dap_store.rs | 7 ++++- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/crates/debugger_ui/src/tests/inline_values.rs b/crates/debugger_ui/src/tests/inline_values.rs index 45cab2a3063a8741d01efb54059667026a646879..9f921ec969debc5247d531469c5132e8485c163b 100644 --- a/crates/debugger_ui/src/tests/inline_values.rs +++ b/crates/debugger_ui/src/tests/inline_values.rs @@ -2241,3 +2241,34 @@ func main() { ) .await; } + +#[gpui::test] +async fn test_trim_multi_line_inline_value(executor: BackgroundExecutor, cx: &mut TestAppContext) { + let variables = [("y", "hello\n world")]; + + let before = r#" +fn main() { + let y = "hello\n world"; +} +"# + .unindent(); + + let after = r#" +fn main() { + let y: hello… = "hello\n world"; +} +"# + .unindent(); + + test_inline_values_util( + &variables, + &[], + &before, + &after, + None, + rust_lang(), + executor, + cx, + ) + .await; +} diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d7e6e42659dd98f32bbe4bad17ca9411ee8d453b..63dc857891790bb61dc985caa5db2c091bd4e27c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -19655,8 +19655,9 @@ impl Editor { Anchor::in_buffer(excerpt_id, buffer_id, hint.position), hint.text(), ); - - new_inlays.push(inlay); + if !inlay.text.chars().contains(&'\n') { + new_inlays.push(inlay); + } }); } diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index 19e64adb2d6e412e25f49170109af1596273ea34..f4f4b50dab696f257978a35593e090d4efdff97e 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -560,6 +560,11 @@ impl DapStore { fn format_value(mut value: String) -> String { const LIMIT: usize = 100; + if let Some(index) = value.find("\n") { + value.truncate(index); + value.push_str("…"); + } + if value.len() > LIMIT { let mut index = LIMIT; // If index isn't a char boundary truncate will cause a panic @@ -567,7 +572,7 @@ impl DapStore { index -= 1; } value.truncate(index); - value.push_str("..."); + value.push_str("…"); } format!(": {}", value) From 6f6c2915b29d449fb9cb73dd10612633933bf13c Mon Sep 17 00:00:00 2001 From: Agus Zubiaga <agus@zed.dev> Date: Fri, 11 Jul 2025 14:52:21 -0300 Subject: [PATCH 027/658] Display and jump to tool locations (#34304) Release Notes: - N/A --- crates/acp/src/acp.rs | 8 ++ crates/agent_ui/src/acp/thread_view.rs | 120 ++++++++++++++++++++++--- 2 files changed, 115 insertions(+), 13 deletions(-) diff --git a/crates/acp/src/acp.rs b/crates/acp/src/acp.rs index 0aa57513a7e4cb1b45c623163148880e1f77f63a..f89eab203206cba795693d3d5f0655abe06d83e1 100644 --- a/crates/acp/src/acp.rs +++ b/crates/acp/src/acp.rs @@ -176,6 +176,14 @@ impl AgentThreadEntry { None } } + + pub fn locations(&self) -> Option<&[acp::ToolCallLocation]> { + if let AgentThreadEntry::ToolCall(ToolCall { locations, .. }) = self { + Some(locations) + } else { + None + } + } } #[derive(Debug)] diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 353d712afd5b3561898c88867c1da2b7109ad681..7ab395815f734d8f5d0b20eb5b49419361b4627d 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -925,10 +925,43 @@ impl AcpThreadView { .size(IconSize::Small) .color(Color::Muted), ) - .child(self.render_markdown( - tool_call.label.clone(), - default_markdown_style(needs_confirmation, window, cx), - )), + .child(if tool_call.locations.len() == 1 { + let name = tool_call.locations[0] + .path + .file_name() + .unwrap_or_default() + .display() + .to_string(); + + h_flex() + .id(("open-tool-call-location", entry_ix)) + .child(name) + .w_full() + .max_w_full() + .pr_1() + .gap_0p5() + .cursor_pointer() + .rounded_sm() + .opacity(0.8) + .hover(|label| { + label.opacity(1.).bg(cx + .theme() + .colors() + .element_hover + .opacity(0.5)) + }) + .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() + }), ) .child( h_flex() @@ -988,15 +1021,19 @@ impl AcpThreadView { cx: &Context<Self>, ) -> AnyElement { match content { - ToolCallContent::Markdown { markdown } => self - .render_markdown(markdown.clone(), default_markdown_style(false, window, cx)) - .into_any_element(), + ToolCallContent::Markdown { markdown } => { + div() + .p_2() + .child(self.render_markdown( + markdown.clone(), + default_markdown_style(false, window, cx), + )) + .into_any_element() + } ToolCallContent::Diff { - diff: Diff { - path, multibuffer, .. - }, + diff: Diff { multibuffer, .. }, .. - } => self.render_diff_editor(multibuffer, path), + } => self.render_diff_editor(multibuffer), } } @@ -1416,10 +1453,9 @@ impl AcpThreadView { } } - fn render_diff_editor(&self, multibuffer: &Entity<MultiBuffer>, path: &Path) -> AnyElement { + fn render_diff_editor(&self, multibuffer: &Entity<MultiBuffer>) -> AnyElement { v_flex() .h_full() - .child(path.to_string_lossy().to_string()) .child( if let Some(editor) = self.diff_editors.get(&multibuffer.entity_id()) { editor.clone().into_any_element() @@ -2076,6 +2112,64 @@ impl AcpThreadView { } } + fn open_tool_call_location( + &self, + entry_ix: usize, + location_ix: usize, + window: &mut Window, + cx: &mut Context<Self>, + ) -> Option<()> { + let location = self + .thread()? + .read(cx) + .entries() + .get(entry_ix)? + .locations()? + .get(location_ix)?; + + let project_path = self + .project + .read(cx) + .find_project_path(&location.path, cx)?; + + let open_task = self + .workspace + .update(cx, |worskpace, cx| { + worskpace.open_path(project_path, None, true, window, cx) + }) + .log_err()?; + + window + .spawn(cx, async move |cx| { + let item = open_task.await?; + + let Some(active_editor) = item.downcast::<Editor>() else { + return anyhow::Ok(()); + }; + + 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; + editor.change_selections(Default::default(), window, cx, |selections| { + selections.select_anchor_ranges([first_hunk_start..first_hunk_start]); + }) + } + })?; + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + + None + } + pub fn open_thread_as_markdown( &self, workspace: Entity<Workspace>, From 0797f7b66e4187a599b88532545da46071f854f4 Mon Sep 17 00:00:00 2001 From: Ben Kunkle <ben@zed.dev> Date: Fri, 11 Jul 2025 13:07:04 -0500 Subject: [PATCH 028/658] keymap_ui: Show existing keystrokes as placeholders in edit modal (#34307) Closes #ISSUE Previously, the keystroke input would be empty, even when editing an existing binding. This meant you had to re-enter the bindings even if you just wanted to edit the context. Now, the existing keystrokes are rendered as a placeholder, are re-shown if newly entered keystrokes are cleared, and will be returned from the `KeystrokeInput::keystrokes()` method if no new keystrokes were entered. Additionally fixed a bug in `KeymapFile::update_keybinding` where semantically identical contexts would be treated as unequal due to formatting differences. Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/settings/src/keymap_file.rs | 7 +++- crates/settings_ui/src/keybindings.rs | 60 +++++++++++++++++++++------ 2 files changed, 54 insertions(+), 13 deletions(-) diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 98dbe4d02af866c595f7f7577b3a9290d23b66d5..102522f71d8e4c4f9453e7101fec898f3c7c0689 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -783,8 +783,12 @@ impl KeymapFile { target: &KeybindUpdateTarget<'a>, target_action_value: &Value, ) -> Option<(usize, &'b str)> { + let target_context_parsed = + KeyBindingContextPredicate::parse(target.context.unwrap_or("")).ok(); for (index, section) in keymap.sections().enumerate() { - if section.context != target.context.unwrap_or("") { + let section_context_parsed = + KeyBindingContextPredicate::parse(§ion.context).ok(); + if section_context_parsed != target_context_parsed { continue; } if section.use_key_equivalents != target.use_key_equivalents { @@ -835,6 +839,7 @@ pub enum KeybindUpdateOperation<'a> { }, } +#[derive(Debug)] pub struct KeybindUpdateTarget<'a> { pub context: Option<&'a str>, pub keystrokes: &'a [Keystroke], diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 993ed1bbbf2d55c041c6063febfbc992940b0d26..1718f3d283dc50e743a3fe3996e03513a20a2ba6 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -283,7 +283,7 @@ impl KeymapEditor { let table_interaction_state = TableInteractionState::new(window, cx); let keystroke_editor = cx.new(|cx| { - let mut keystroke_editor = KeystrokeInput::new(window, cx); + let mut keystroke_editor = KeystrokeInput::new(None, window, cx); keystroke_editor.highlight_on_focus = false; keystroke_editor }); @@ -632,6 +632,11 @@ impl KeymapEditor { Box::new(EditBinding), ) .action("Create", Box::new(CreateBinding)) + .action_disabled_when( + selected_binding_is_unbound, + "Delete", + Box::new(DeleteBinding), + ) .action("Copy action", Box::new(CopyAction)) .action_disabled_when( selected_binding_has_no_context, @@ -1298,7 +1303,16 @@ impl KeybindingEditorModal { window: &mut Window, cx: &mut App, ) -> Self { - let keybind_editor = cx.new(|cx| KeystrokeInput::new(window, cx)); + let keybind_editor = cx.new(|cx| { + KeystrokeInput::new( + editing_keybind + .ui_key_binding + .as_ref() + .map(|keybinding| keybinding.keystrokes.clone()), + window, + cx, + ) + }); let context_editor = cx.new(|cx| { let mut editor = Editor::single_line(window, cx); @@ -1802,6 +1816,7 @@ async fn remove_keybinding( struct KeystrokeInput { keystrokes: Vec<Keystroke>, + placeholder_keystrokes: Option<Vec<Keystroke>>, highlight_on_focus: bool, focus_handle: FocusHandle, intercept_subscription: Option<Subscription>, @@ -1809,7 +1824,11 @@ struct KeystrokeInput { } impl KeystrokeInput { - fn new(window: &mut Window, cx: &mut Context<Self>) -> Self { + fn new( + placeholder_keystrokes: Option<Vec<Keystroke>>, + window: &mut Window, + cx: &mut Context<Self>, + ) -> Self { let focus_handle = cx.focus_handle(); let _focus_subscriptions = [ cx.on_focus_in(&focus_handle, window, Self::on_focus_in), @@ -1817,6 +1836,7 @@ impl KeystrokeInput { ]; Self { keystrokes: Vec::new(), + placeholder_keystrokes, highlight_on_focus: true, focus_handle, intercept_subscription: None, @@ -1904,6 +1924,11 @@ impl KeystrokeInput { } fn keystrokes(&self) -> &[Keystroke] { + if let Some(placeholders) = self.placeholder_keystrokes.as_ref() + && self.keystrokes.is_empty() + { + return placeholders; + } if self .keystrokes .last() @@ -1913,6 +1938,25 @@ impl KeystrokeInput { } return &self.keystrokes; } + + fn render_keystrokes(&self) -> impl Iterator<Item = Div> { + let (keystrokes, color) = if let Some(placeholders) = self.placeholder_keystrokes.as_ref() + && self.keystrokes.is_empty() + { + (placeholders, Color::Placeholder) + } else { + (&self.keystrokes, Color::Default) + }; + keystrokes.iter().map(move |keystroke| { + h_flex().children(ui::render_keystroke( + keystroke, + Some(color), + Some(rems(0.875).into()), + ui::PlatformStyle::platform(), + false, + )) + }) + } } impl EventEmitter<()> for KeystrokeInput {} @@ -1958,15 +2002,7 @@ impl Render for KeystrokeInput { .justify_center() .flex_wrap() .gap(ui::DynamicSpacing::Base04.rems(cx)) - .children(self.keystrokes.iter().map(|keystroke| { - h_flex().children(ui::render_keystroke( - keystroke, - None, - Some(rems(0.875).into()), - ui::PlatformStyle::platform(), - false, - )) - })), + .children(self.render_keystrokes()), ) .child( h_flex() From fbead09c30ca1b1371ae485958fc685f1bcf2d59 Mon Sep 17 00:00:00 2001 From: Lukas Spiss <35728419+Spissable@users.noreply.github.com> Date: Fri, 11 Jul 2025 19:26:46 +0100 Subject: [PATCH 029/658] go: Write `envFile` properties back to `env` config (#34300) Closes https://github.com/zed-industries/zed/issues/32984 Note that while https://github.com/zed-industries/zed/pull/33666 did the reading of the `envFile` just fine, the read values were never passed along. This was mentioned by [this comment](https://github.com/zed-industries/zed/pull/33666#issuecomment-3060785970) and also confirmed by myself. With the changes here, I successfully debugged a project of mine and all the environment variables from my `.env` were present. Release Notes: - Fix Go debugger ignoring env vars from the envFile setting. --- crates/dap_adapters/src/go.rs | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/crates/dap_adapters/src/go.rs b/crates/dap_adapters/src/go.rs index d32f5cbf3426f1b669132e74e389862e7944267b..22d8262b93e36b17e548ae4dcc9bb725da8ca7cb 100644 --- a/crates/dap_adapters/src/go.rs +++ b/crates/dap_adapters/src/go.rs @@ -547,6 +547,7 @@ async fn handle_envs( } }; + let mut env_vars = HashMap::default(); for path in env_files { let Some(path) = path .and_then(|s| PathBuf::from_str(s).ok()) @@ -556,13 +557,33 @@ async fn handle_envs( }; if let Ok(file) = fs.open_sync(&path).await { - envs.extend(dotenvy::from_read_iter(file).filter_map(Result::ok)) + let file_envs: HashMap<String, String> = dotenvy::from_read_iter(file) + .filter_map(Result::ok) + .collect(); + envs.extend(file_envs.iter().map(|(k, v)| (k.clone(), v.clone()))); + env_vars.extend(file_envs); } else { warn!("While starting Go debug session: failed to read env file {path:?}"); }; } + let mut env_obj: serde_json::Map<String, Value> = serde_json::Map::new(); + + for (k, v) in env_vars { + env_obj.insert(k, Value::String(v)); + } + + if let Some(existing_env) = config.get("env").and_then(|v| v.as_object()) { + for (k, v) in existing_env { + env_obj.insert(k.clone(), v.clone()); + } + } + + if !env_obj.is_empty() { + config.insert("env".to_string(), Value::Object(env_obj)); + } + // remove envFile now that it's been handled - config.remove("entry"); + config.remove("envFile"); Some(()) } From 625a4b90a50e4cc01198d86b0e16fbe43f5b815e Mon Sep 17 00:00:00 2001 From: Mikayla Maki <mikayla@zed.dev> Date: Fri, 11 Jul 2025 12:02:40 -0700 Subject: [PATCH 030/658] Tinker with the reporting of telemetry events (#34239) Release Notes: - N/A --------- Co-authored-by: Katie Geer <katie@zed.dev> --- crates/editor/src/editor.rs | 5 ++++- crates/editor/src/items.rs | 8 +++++++- crates/title_bar/src/onboarding_banner.rs | 6 ++++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 63dc857891790bb61dc985caa5db2c091bd4e27c..a25a96cdabd30d43a60b6ab61b2e0ed2e3e41de7 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2322,7 +2322,10 @@ impl Editor { editor.update_lsp_data(false, None, window, cx); } - editor.report_editor_event("Editor Opened", None, cx); + if editor.mode.is_full() { + editor.report_editor_event("Editor Opened", None, cx); + } + editor } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 2e4631a62b16db51476c5ce5918bdc973806381e..4e6e471f48ed9841c15463d3760be9d55664eefa 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -813,7 +813,13 @@ impl Item for Editor { window: &mut Window, cx: &mut Context<Self>, ) -> Task<Result<()>> { - self.report_editor_event("Editor Saved", None, cx); + // Add meta data tracking # of auto saves + if options.autosave { + self.report_editor_event("Editor Autosaved", None, cx); + } else { + self.report_editor_event("Editor Saved", None, cx); + } + let buffers = self.buffer().clone().read(cx).all_buffers(); let buffers = buffers .into_iter() diff --git a/crates/title_bar/src/onboarding_banner.rs b/crates/title_bar/src/onboarding_banner.rs index 8ed6e956af4a5789708d1f1995f6fe82aee5dc96..e7cf0cd2d9326b68973935a7815ef281a01b03c3 100644 --- a/crates/title_bar/src/onboarding_banner.rs +++ b/crates/title_bar/src/onboarding_banner.rs @@ -51,7 +51,6 @@ impl OnboardingBanner { } fn dismiss(&mut self, cx: &mut Context<Self>) { - telemetry::event!("Banner Dismissed", source = self.source); persist_dismissed(&self.source, cx); self.dismissed = true; cx.notify(); @@ -144,7 +143,10 @@ impl Render for OnboardingBanner { div().border_l_1().border_color(border_color).child( IconButton::new("close", IconName::Close) .icon_size(IconSize::Indicator) - .on_click(cx.listener(|this, _, _window, cx| this.dismiss(cx))) + .on_click(cx.listener(|this, _, _window, cx| { + telemetry::event!("Banner Dismissed", source = this.source); + this.dismiss(cx) + })) .tooltip(|window, cx| { Tooltip::with_meta( "Close Announcement Banner", From c3edc2cfc1968434bccfd3be58e48cade939cbc5 Mon Sep 17 00:00:00 2001 From: Julia Ryan <juliaryan3.14@gmail.com> Date: Fri, 11 Jul 2025 12:34:53 -0700 Subject: [PATCH 031/658] DAP log view improvements (#34311) Now DAP logs show the label of each session which makes it much easier to pick out the right one. Also "initialization sequence" now shows up correctly when that view is selected. Release Notes: - N/A --------- Co-authored-by: Cole Miller <cole@zed.dev> --- crates/debugger_tools/src/dap_log.rs | 83 ++++++++++++++++++---------- crates/debugger_ui/src/session.rs | 3 +- 2 files changed, 56 insertions(+), 30 deletions(-) diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index f2f193cad451772146f6fd39e13a75f29f13292b..f53b6403b235ec43a141c67b6f7108316df18adb 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -32,12 +32,19 @@ use workspace::{ ui::{Button, Clickable, ContextMenu, Label, LabelCommon, PopoverMenu, h_flex}, }; +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum View { + AdapterLogs, + RpcMessages, + InitializationSequence, +} + struct DapLogView { editor: Entity<Editor>, focus_handle: FocusHandle, log_store: Entity<LogStore>, editor_subscriptions: Vec<Subscription>, - current_view: Option<(SessionId, LogKind)>, + current_view: Option<(SessionId, View)>, project: Entity<Project>, _subscriptions: Vec<Subscription>, } @@ -77,6 +84,7 @@ struct DebugAdapterState { id: SessionId, log_messages: VecDeque<SharedString>, rpc_messages: RpcMessages, + session_label: SharedString, adapter_name: DebugAdapterName, has_adapter_logs: bool, is_terminated: bool, @@ -121,12 +129,18 @@ impl MessageKind { } impl DebugAdapterState { - fn new(id: SessionId, adapter_name: DebugAdapterName, has_adapter_logs: bool) -> Self { + fn new( + id: SessionId, + adapter_name: DebugAdapterName, + session_label: SharedString, + has_adapter_logs: bool, + ) -> Self { Self { id, log_messages: VecDeque::new(), rpc_messages: RpcMessages::new(), adapter_name, + session_label, has_adapter_logs, is_terminated: false, } @@ -371,18 +385,21 @@ impl LogStore { return None; }; - let (adapter_name, has_adapter_logs) = session.read_with(cx, |session, _| { - ( - session.adapter(), - session - .adapter_client() - .map_or(false, |client| client.has_adapter_logs()), - ) - }); + let (adapter_name, session_label, has_adapter_logs) = + session.read_with(cx, |session, _| { + ( + session.adapter(), + session.label(), + session + .adapter_client() + .map_or(false, |client| client.has_adapter_logs()), + ) + }); state.insert(DebugAdapterState::new( id.session_id, adapter_name, + session_label, has_adapter_logs, )); @@ -506,12 +523,13 @@ impl Render for DapLogToolbarItemView { current_client .map(|sub_item| { Cow::Owned(format!( - "{} ({}) - {}", + "{} - {} - {}", sub_item.adapter_name, - sub_item.session_id.0, + sub_item.session_label, match sub_item.selected_entry { - LogKind::Adapter => ADAPTER_LOGS, - LogKind::Rpc => RPC_MESSAGES, + View::AdapterLogs => ADAPTER_LOGS, + View::RpcMessages => RPC_MESSAGES, + View::InitializationSequence => INITIALIZATION_SEQUENCE, } )) }) @@ -529,8 +547,8 @@ impl Render for DapLogToolbarItemView { .pl_2() .child( Label::new(format!( - "{}. {}", - row.session_id.0, row.adapter_name, + "{} - {}", + row.adapter_name, row.session_label )) .color(workspace::ui::Color::Muted), ) @@ -669,9 +687,16 @@ impl DapLogView { let events_subscriptions = cx.subscribe(&log_store, |log_view, _, event, cx| match event { Event::NewLogEntry { id, entry, kind } => { - if log_view.current_view == Some((id.session_id, *kind)) - && log_view.project == *id.project - { + let is_current_view = match (log_view.current_view, *kind) { + (Some((i, View::AdapterLogs)), LogKind::Adapter) + | (Some((i, View::RpcMessages)), LogKind::Rpc) + if i == id.session_id => + { + log_view.project == *id.project + } + _ => false, + }; + if is_current_view { log_view.editor.update(cx, |editor, cx| { editor.set_read_only(false); let last_point = editor.buffer().read(cx).len(cx); @@ -768,10 +793,11 @@ impl DapLogView { .map(|state| DapMenuItem { session_id: state.id, adapter_name: state.adapter_name.clone(), + session_label: state.session_label.clone(), has_adapter_logs: state.has_adapter_logs, selected_entry: self .current_view - .map_or(LogKind::Adapter, |(_, kind)| kind), + .map_or(View::AdapterLogs, |(_, kind)| kind), }) .collect::<Vec<_>>() }) @@ -789,7 +815,7 @@ impl DapLogView { .map(|state| log_contents(state.iter().cloned())) }); if let Some(rpc_log) = rpc_log { - self.current_view = Some((id.session_id, LogKind::Rpc)); + self.current_view = Some((id.session_id, View::RpcMessages)); let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx); let language = self.project.read(cx).languages().language_for_name("JSON"); editor @@ -830,7 +856,7 @@ impl DapLogView { .map(|state| log_contents(state.iter().cloned())) }); if let Some(message_log) = message_log { - self.current_view = Some((id.session_id, LogKind::Adapter)); + self.current_view = Some((id.session_id, View::AdapterLogs)); let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, window, cx); editor .read(cx) @@ -859,7 +885,7 @@ impl DapLogView { .map(|state| log_contents(state.iter().cloned())) }); if let Some(rpc_log) = rpc_log { - self.current_view = Some((id.session_id, LogKind::Rpc)); + self.current_view = Some((id.session_id, View::InitializationSequence)); let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx); let language = self.project.read(cx).languages().language_for_name("JSON"); editor @@ -899,11 +925,12 @@ fn log_contents(lines: impl Iterator<Item = SharedString>) -> String { } #[derive(Clone, PartialEq)] -pub(crate) struct DapMenuItem { - pub session_id: SessionId, - pub adapter_name: DebugAdapterName, - pub has_adapter_logs: bool, - pub selected_entry: LogKind, +struct DapMenuItem { + session_id: SessionId, + session_label: SharedString, + adapter_name: DebugAdapterName, + has_adapter_logs: bool, + selected_entry: View, } const ADAPTER_LOGS: &str = "Adapter Logs"; diff --git a/crates/debugger_ui/src/session.rs b/crates/debugger_ui/src/session.rs index 482297b13671a969c166e154b43a6c854f231e5c..2118249518ba0231dd854f5d23e25b8a2890e425 100644 --- a/crates/debugger_ui/src/session.rs +++ b/crates/debugger_ui/src/session.rs @@ -11,7 +11,7 @@ use project::worktree_store::WorktreeStore; use rpc::proto; use running::RunningState; use std::{cell::OnceCell, sync::OnceLock}; -use ui::{Indicator, Tooltip, prelude::*}; +use ui::{Indicator, prelude::*}; use util::truncate_and_trailoff; use workspace::{ CollaboratorId, FollowableItem, ViewId, Workspace, @@ -158,7 +158,6 @@ impl DebugSession { h_flex() .id("session-label") - .tooltip(Tooltip::text(format!("Session {}", self.session_id(cx).0,))) .ml(depth * px(16.0)) .gap_2() .when_some(icon, |this, indicator| this.child(indicator)) From 206cce67831c4dd387e9e67887ef3c4f1e438ae9 Mon Sep 17 00:00:00 2001 From: Ben Kunkle <ben@zed.dev> Date: Fri, 11 Jul 2025 16:23:14 -0500 Subject: [PATCH 032/658] keymap_ui: Support unbinding non-user defined keybindings (#34318) Closes #ISSUE Makes it so that `KeymapFile::update_keybinding` treats removals of bindings that weren't user-defined as creating a new binding to `zed::NoAction`. Release Notes: - N/A *or* Added/Fixed/Improved ... --- Cargo.lock | 1 + crates/settings/src/keymap_file.rs | 21 +++--- crates/settings_ui/Cargo.toml | 1 + crates/settings_ui/src/keybindings.rs | 97 ++++++++++++++++----------- 4 files changed, 73 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 624126c163b6bd30ff06a9ea3db2c0c2cddfccb0..05393285764c9e159c572cd0452694c888a39c47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14677,6 +14677,7 @@ dependencies = [ "schemars", "search", "serde", + "serde_json", "settings", "theme", "tree-sitter-json", diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 102522f71d8e4c4f9453e7101fec898f3c7c0689..78e306ed632dde43c3671ce3f746a9d198893f3c 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -607,8 +607,8 @@ impl KeymapFile { mut keymap_contents: String, tab_size: usize, ) -> Result<String> { - // if trying to replace a keybinding that is not user-defined, treat it as an add operation match operation { + // if trying to replace a keybinding that is not user-defined, treat it as an add operation KeybindUpdateOperation::Replace { target_keybind_source: target_source, source, @@ -616,6 +616,16 @@ impl KeymapFile { } if target_source != KeybindSource::User => { operation = KeybindUpdateOperation::Add(source); } + // if trying to remove a keybinding that is not user-defined, treat it as creating a binding + // that binds it to `zed::NoAction` + KeybindUpdateOperation::Remove { + mut target, + target_keybind_source, + } if target_keybind_source != KeybindSource::User => { + target.action_name = gpui::NoAction.name(); + target.input.take(); + operation = KeybindUpdateOperation::Add(target); + } _ => {} } @@ -623,14 +633,7 @@ impl KeymapFile { // We don't want to modify the file if it's invalid. let keymap = Self::parse(&keymap_contents).context("Failed to parse keymap")?; - if let KeybindUpdateOperation::Remove { - target, - target_keybind_source, - } = operation - { - if target_keybind_source != KeybindSource::User { - anyhow::bail!("Cannot remove non-user created keybinding. Not implemented yet"); - } + if let KeybindUpdateOperation::Remove { target, .. } = operation { let target_action_value = target .action_value() .context("Failed to generate target action JSON value")?; diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml index 7af240bd7419610ab7267439bee993ddfb194c5f..2791876117f9c15941195eb850ee6f515e2e63a5 100644 --- a/crates/settings_ui/Cargo.toml +++ b/crates/settings_ui/Cargo.toml @@ -31,6 +31,7 @@ project.workspace = true schemars.workspace = true search.workspace = true serde.workspace = true +serde_json.workspace = true settings.workspace = true theme.workspace = true tree-sitter-json.workspace = true diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 1718f3d283dc50e743a3fe3996e03513a20a2ba6..7fc2e070875451a6d6351ad891876b8350692baf 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -397,11 +397,10 @@ impl KeymapEditor { SearchMode::KeyStroke => { matches.retain(|item| { this.keybindings[item.candidate_id] - .ui_key_binding - .as_ref() - .is_some_and(|binding| { + .keystrokes() + .is_some_and(|keystrokes| { keystroke_query.iter().all(|key| { - binding.keystrokes.iter().any(|keystroke| { + keystrokes.iter().any(|keystroke| { keystroke.key == key.key && keystroke.modifiers == key.modifiers }) @@ -623,7 +622,7 @@ impl KeymapEditor { .and_then(KeybindContextString::local) .is_none(); - let selected_binding_is_unbound = selected_binding.ui_key_binding.is_none(); + let selected_binding_is_unbound = selected_binding.keystrokes().is_none(); let context_menu = ContextMenu::build(window, cx, |menu, _window, _cx| { menu.action_disabled_when( @@ -876,6 +875,12 @@ impl ProcessedKeybinding { .cloned(), ) } + + fn keystrokes(&self) -> Option<&[Keystroke]> { + self.ui_key_binding + .as_ref() + .map(|binding| binding.keystrokes.as_slice()) + } } #[derive(Clone, Debug, IntoElement, PartialEq, Eq, Hash)] @@ -1303,16 +1308,8 @@ impl KeybindingEditorModal { window: &mut Window, cx: &mut App, ) -> Self { - let keybind_editor = cx.new(|cx| { - KeystrokeInput::new( - editing_keybind - .ui_key_binding - .as_ref() - .map(|keybinding| keybinding.keystrokes.clone()), - window, - cx, - ) - }); + let keybind_editor = cx + .new(|cx| KeystrokeInput::new(editing_keybind.keystrokes().map(Vec::from), window, cx)); let context_editor = cx.new(|cx| { let mut editor = Editor::single_line(window, cx); @@ -1398,6 +1395,24 @@ impl KeybindingEditorModal { } } + fn validate_action_input(&self, cx: &App) -> anyhow::Result<Option<String>> { + let input = self + .input_editor + .as_ref() + .map(|editor| editor.read(cx).text(cx)); + + let value = input + .as_ref() + .map(|input| { + serde_json::from_str(input).context("Failed to parse action input as JSON") + }) + .transpose()?; + + cx.build_action(&self.editing_keybind.action_name, value) + .context("Failed to validate action input")?; + Ok(input) + } + fn save(&mut self, cx: &mut Context<Self>) { let existing_keybind = self.editing_keybind.clone(); let fs = self.fs.clone(); @@ -1425,6 +1440,14 @@ impl KeybindingEditorModal { return; } + let new_input = match self.validate_action_input(cx) { + Err(input_err) => { + self.set_error(InputError::error(input_err.to_string()), cx); + return; + } + Ok(input) => input, + }; + let action_mapping: ActionMapping = ( ui::text_for_keystrokes(&new_keystrokes, cx).into(), new_context @@ -1481,6 +1504,7 @@ impl KeybindingEditorModal { existing_keybind, &new_keystrokes, new_context.as_deref(), + new_input.as_deref(), &fs, tab_size, ) @@ -1711,6 +1735,7 @@ async fn save_keybinding_update( existing: ProcessedKeybinding, new_keystrokes: &[Keystroke], new_context: Option<&str>, + new_input: Option<&str>, fs: &Arc<dyn Fs>, tab_size: usize, ) -> anyhow::Result<()> { @@ -1718,41 +1743,36 @@ async fn save_keybinding_update( .await .context("Failed to load keymap file")?; - let existing_keystrokes = existing - .ui_key_binding - .as_ref() - .map(|keybinding| keybinding.keystrokes.as_slice()) - .unwrap_or_default(); - - let existing_context = existing - .context - .as_ref() - .and_then(KeybindContextString::local_str); - - let input = existing - .action_input - .as_ref() - .map(|input| input.text.as_ref()); - let operation = if !create { + let existing_keystrokes = existing.keystrokes().unwrap_or_default(); + let existing_context = existing + .context + .as_ref() + .and_then(KeybindContextString::local_str); + let existing_input = existing + .action_input + .as_ref() + .map(|input| input.text.as_ref()); + settings::KeybindUpdateOperation::Replace { target: settings::KeybindUpdateTarget { context: existing_context, keystrokes: existing_keystrokes, action_name: &existing.action_name, use_key_equivalents: false, - input, + input: existing_input, }, target_keybind_source: existing .source - .map(|(source, _name)| source) + .as_ref() + .map(|(source, _name)| *source) .unwrap_or(KeybindSource::User), source: settings::KeybindUpdateTarget { context: new_context, keystrokes: new_keystrokes, action_name: &existing.action_name, use_key_equivalents: false, - input, + input: new_input, }, } } else { @@ -1761,7 +1781,7 @@ async fn save_keybinding_update( keystrokes: new_keystrokes, action_name: &existing.action_name, use_key_equivalents: false, - input, + input: new_input, }) }; let updated_keymap_contents = @@ -1778,7 +1798,7 @@ async fn remove_keybinding( fs: &Arc<dyn Fs>, tab_size: usize, ) -> anyhow::Result<()> { - let Some(ui_key_binding) = existing.ui_key_binding else { + let Some(keystrokes) = existing.keystrokes() else { anyhow::bail!("Cannot remove a keybinding that does not exist"); }; let keymap_contents = settings::KeymapFile::load_keymap_file(fs) @@ -1791,7 +1811,7 @@ async fn remove_keybinding( .context .as_ref() .and_then(KeybindContextString::local_str), - keystrokes: &ui_key_binding.keystrokes, + keystrokes, action_name: &existing.action_name, use_key_equivalents: false, input: existing @@ -1801,7 +1821,8 @@ async fn remove_keybinding( }, target_keybind_source: existing .source - .map(|(source, _name)| source) + .as_ref() + .map(|(source, _name)| *source) .unwrap_or(KeybindSource::User), }; From 67c765a99abc30af83eaf0756237d9471302d7b6 Mon Sep 17 00:00:00 2001 From: Ben Kunkle <ben@zed.dev> Date: Fri, 11 Jul 2025 17:06:06 -0500 Subject: [PATCH 033/658] keymap_ui: Dual-phase focus for keystroke input (#34312) Closes #ISSUE An idea I and @MrSubidubi came up with, to improve UX around the keystroke input. Currently, there's a hard tradeoff with what to focus first in the edit keybind modal, if we focus the keystroke input, it makes keybind modification very easy, however, if you don't want to edit a keybind, you must use the mouse to escape the keystroke input before editing something else - breaking keyboard navigation. The idea in this PR is to have a dual-phased focus system for the keystroke input. There is an outer focus that has some sort of visual indicator to communicate it is focused (currently a border). While the outer focus region is focused, keystrokes are not intercepted. Then there is a keybind (currently hardcoded to `enter`) to enter the inner focus where keystrokes are intercepted, and which must be exited using the mouse. When the inner focus region is focused, there is a visual indicator for the fact it is "recording" (currently a hacked together red pulsing recording icon) <details><summary>Video</summary> https://github.com/user-attachments/assets/490538d0-f092-4df1-a53a-a47d7efe157b </details> Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/settings_ui/src/keybindings.rs | 88 +++++++++++++++++++-------- 1 file changed, 62 insertions(+), 26 deletions(-) diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 7fc2e070875451a6d6351ad891876b8350692baf..c78a370f1232eb219b7ba31d3113f2427672fccd 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -10,9 +10,10 @@ use feature_flags::FeatureFlagViewExt; use fs::Fs; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - Action, AppContext as _, AsyncApp, ClickEvent, Context, DismissEvent, Entity, EventEmitter, - FocusHandle, Focusable, Global, KeyContext, Keystroke, ModifiersChangedEvent, MouseButton, - Point, ScrollStrategy, StyledText, Subscription, WeakEntity, actions, anchored, deferred, div, + Action, AnimationExt, AppContext as _, AsyncApp, ClickEvent, Context, DismissEvent, Entity, + EventEmitter, FocusHandle, Focusable, Global, KeyContext, KeyDownEvent, Keystroke, + ModifiersChangedEvent, MouseButton, Point, ScrollStrategy, StyledText, Subscription, + WeakEntity, actions, anchored, deferred, div, }; use language::{Language, LanguageConfig, ToOffset as _}; use settings::{BaseKeymap, KeybindSource, KeymapFile, SettingsAssets}; @@ -1839,7 +1840,8 @@ struct KeystrokeInput { keystrokes: Vec<Keystroke>, placeholder_keystrokes: Option<Vec<Keystroke>>, highlight_on_focus: bool, - focus_handle: FocusHandle, + outer_focus_handle: FocusHandle, + inner_focus_handle: FocusHandle, intercept_subscription: Option<Subscription>, _focus_subscriptions: [Subscription; 2], } @@ -1850,16 +1852,18 @@ impl KeystrokeInput { window: &mut Window, cx: &mut Context<Self>, ) -> Self { - let focus_handle = cx.focus_handle(); + let outer_focus_handle = cx.focus_handle(); + let inner_focus_handle = cx.focus_handle(); let _focus_subscriptions = [ - cx.on_focus_in(&focus_handle, window, Self::on_focus_in), - cx.on_focus_out(&focus_handle, window, Self::on_focus_out), + cx.on_focus_in(&inner_focus_handle, window, Self::on_inner_focus_in), + cx.on_focus_out(&inner_focus_handle, window, Self::on_inner_focus_out), ]; Self { keystrokes: Vec::new(), placeholder_keystrokes, highlight_on_focus: true, - focus_handle, + inner_focus_handle, + outer_focus_handle, intercept_subscription: None, _focus_subscriptions, } @@ -1926,7 +1930,7 @@ impl KeystrokeInput { cx.notify(); } - fn on_focus_in(&mut self, _window: &mut Window, cx: &mut Context<Self>) { + fn on_inner_focus_in(&mut self, _window: &mut Window, cx: &mut Context<Self>) { if self.intercept_subscription.is_none() { let listener = cx.listener(|this, event: &gpui::KeystrokeEvent, _window, cx| { this.handle_keystroke(&event.keystroke, cx); @@ -1935,13 +1939,14 @@ impl KeystrokeInput { } } - fn on_focus_out( + fn on_inner_focus_out( &mut self, _event: gpui::FocusOutEvent, _window: &mut Window, - _cx: &mut Context<Self>, + cx: &mut Context<Self>, ) { self.intercept_subscription.take(); + cx.notify(); } fn keystrokes(&self) -> &[Keystroke] { @@ -1984,26 +1989,18 @@ impl EventEmitter<()> for KeystrokeInput {} impl Focusable for KeystrokeInput { fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() + self.outer_focus_handle.clone() } } impl Render for KeystrokeInput { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let colors = cx.theme().colors(); - let is_focused = self.focus_handle.is_focused(window); + let is_inner_focused = self.inner_focus_handle.is_focused(window); return h_flex() - .id("keybinding_input") - .track_focus(&self.focus_handle) - .on_modifiers_changed(cx.listener(Self::on_modifiers_changed)) - .on_key_up(cx.listener(Self::on_key_up)) - .when(self.highlight_on_focus, |this| { - this.focus(|mut style| { - style.border_color = Some(colors.border_focused); - style - }) - }) + .id("keystroke-input") + .track_focus(&self.outer_focus_handle) .py_2() .px_3() .gap_2() @@ -2014,10 +2011,31 @@ impl Render for KeystrokeInput { .rounded_md() .overflow_hidden() .bg(colors.editor_background) - .border_1() + .border_2() .border_color(colors.border_variant) + .focus(|mut s| { + s.border_color = Some(colors.border_focused); + s + }) + .on_key_down(cx.listener(|this, event: &KeyDownEvent, window, cx| { + // TODO: replace with action + if !event.keystroke.modifiers.modified() && event.keystroke.key == "enter" { + window.focus(&this.inner_focus_handle); + cx.notify(); + } + })) .child( h_flex() + .id("keystroke-input-inner") + .track_focus(&self.inner_focus_handle) + .on_modifiers_changed(cx.listener(Self::on_modifiers_changed)) + .on_key_up(cx.listener(Self::on_key_up)) + .when(self.highlight_on_focus, |this| { + this.focus(|mut style| { + style.border_color = Some(colors.border_focused); + style + }) + }) .w_full() .min_w_0() .justify_center() @@ -2029,10 +2047,28 @@ impl Render for KeystrokeInput { h_flex() .gap_0p5() .flex_none() + .when(is_inner_focused, |this| { + this.child( + Icon::new(IconName::Circle) + .color(Color::Error) + .with_animation( + "recording-pulse", + gpui::Animation::new(std::time::Duration::from_secs(1)) + .repeat() + .with_easing(gpui::pulsating_between(0.8, 1.0)), + { + let color = Color::Error.color(cx); + move |this, delta| { + this.color(Color::Custom(color.opacity(delta))) + } + }, + ), + ) + }) .child( IconButton::new("backspace-btn", IconName::Delete) .tooltip(Tooltip::text("Delete Keystroke")) - .when(!is_focused, |this| this.icon_color(Color::Muted)) + .when(!is_inner_focused, |this| this.icon_color(Color::Muted)) .on_click(cx.listener(|this, _event, _window, cx| { this.keystrokes.pop(); cx.emit(()); @@ -2042,7 +2078,7 @@ impl Render for KeystrokeInput { .child( IconButton::new("clear-btn", IconName::Eraser) .tooltip(Tooltip::text("Clear Keystrokes")) - .when(!is_focused, |this| this.icon_color(Color::Muted)) + .when(!is_inner_focused, |this| this.icon_color(Color::Muted)) .on_click(cx.listener(|this, _event, _window, cx| { this.keystrokes.clear(); cx.emit(()); From 12bc8907d926ca719ae182ac3aba0ba7e226f9d7 Mon Sep 17 00:00:00 2001 From: vipex <101529155+vipexv@users.noreply.github.com> Date: Sat, 12 Jul 2025 00:23:04 +0200 Subject: [PATCH 034/658] Recall empty, unsaved buffers on app load (#33475) Closes #33342 This PR implements serialization of pinned tabs regardless of their state (empty, untitled, etc.) The root cause was that empty untitled tabs were being skipped during serialization but their pinned state was still being persisted, leading to a mismatch between the stored pinned count and actual restorable tabs, this issue lead to a crash which was patched by @JosephTLyons, but this PR aims to be a proper fix. **Note**: I'm still evaluating the best approach for this fix. Currently exploring whether it's necessary to store the pinned state in the database schema or if there's a simpler solution that doesn't require schema changes. --- **Edit from Joseph** We ended up going with altering our recall logic, where we always restore all editors, even those that are new, empty, and unsaved. This prevents the crash that #33335 patched because we are no longer skipping the restoration of pinned editors that have no text and haven't been saved, throwing off the count dealing with the number of pinned items. This solution is rather simple, but I think it's fine. We simply just restore everything the same, no conditional dropping of anything. This is also consistent with VS Code, which also restores all editors, regardless of whether or not a new, unsaved buffers have content or not. https://github.com/zed-industries/zed/tree/alt-solution-for-%2333342 Release Notes: - N/A --------- Co-authored-by: Joseph T. Lyons <JosephTLyons@gmail.com> --- crates/editor/src/items.rs | 48 +++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 4e6e471f48ed9841c15463d3760be9d55664eefa..ca635a2132790e809258c8bd63fbd3a1c3edcdb3 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1226,7 +1226,20 @@ impl SerializableItem for Editor { abs_path: None, contents: None, .. - } => Task::ready(Err(anyhow!("No path or contents found for buffer"))), + } => window.spawn(cx, async move |cx| { + let buffer = project + .update(cx, |project, cx| project.create_buffer(cx))? + .await?; + + cx.update(|window, cx| { + cx.new(|cx| { + let mut editor = Editor::for_buffer(buffer, Some(project), window, cx); + + editor.read_metadata_from_db(item_id, workspace_id, window, cx); + editor + }) + }) + }), } } @@ -2098,5 +2111,38 @@ mod tests { assert!(editor.has_conflict(cx)); // The editor should have a conflict }); } + + // Test case 5: Deserialize with no path, no content, no language, and no old mtime (new, empty, unsaved buffer) + { + let project = Project::test(fs.clone(), [path!("/file.rs").as_ref()], cx).await; + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + + let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap(); + + let item_id = 10000 as ItemId; + let serialized_editor = SerializedEditor { + abs_path: None, + contents: None, + language: None, + mtime: None, + }; + + DB.save_serialized_editor(item_id, workspace_id, serialized_editor) + .await + .unwrap(); + + let deserialized = + deserialize_editor(item_id, workspace_id, workspace, project, cx).await; + + deserialized.update(cx, |editor, cx| { + assert_eq!(editor.text(cx), ""); + assert!(!editor.is_dirty(cx)); + assert!(!editor.has_conflict(cx)); + + let buffer = editor.buffer().read(cx).as_singleton().unwrap().read(cx); + assert!(buffer.file().is_none()); + }); + } } } From 625ce12a3e170fb3f8a1e48a9e04b804b2d462e2 Mon Sep 17 00:00:00 2001 From: Cole Miller <cole@zed.dev> Date: Fri, 11 Jul 2025 19:20:35 -0400 Subject: [PATCH 035/658] Revert "git: Intercept signing prompt from GPG when committing" (#34306) Reverts zed-industries/zed#34096 This introduced a regression, because the unlocked key can't benefit from caching. Release Notes: - N/A --- Cargo.lock | 1 - crates/askpass/Cargo.toml | 1 - crates/askpass/src/askpass.rs | 59 --------------- crates/fs/src/fake_git_repo.rs | 4 +- crates/git/Cargo.toml | 4 +- crates/git/src/repository.rs | 126 ++++++++++---------------------- crates/git_ui/src/git_panel.rs | 20 ++--- crates/project/src/git_store.rs | 30 +------- crates/proto/proto/git.proto | 2 +- 9 files changed, 53 insertions(+), 194 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 05393285764c9e159c572cd0452694c888a39c47..31c92b951fc63162118b8e95a341dd236d229094 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -608,7 +608,6 @@ dependencies = [ "parking_lot", "smol", "tempfile", - "unindent", "util", "workspace-hack", ] diff --git a/crates/askpass/Cargo.toml b/crates/askpass/Cargo.toml index 3241061dc68abc6f597ba9287af94d4e47a08813..0527399af8b6f45ef18650ee5c286c0b51a83608 100644 --- a/crates/askpass/Cargo.toml +++ b/crates/askpass/Cargo.toml @@ -19,6 +19,5 @@ net.workspace = true parking_lot.workspace = true smol.workspace = true tempfile.workspace = true -unindent.workspace = true util.workspace = true workspace-hack.workspace = true diff --git a/crates/askpass/src/askpass.rs b/crates/askpass/src/askpass.rs index 8a91e748ed4280252eb212dbb21d171d13cc586b..f085a2be72d04d7c1d16f855230011639853ddf2 100644 --- a/crates/askpass/src/askpass.rs +++ b/crates/askpass/src/askpass.rs @@ -40,21 +40,11 @@ impl AskPassDelegate { self.tx.send((prompt, tx)).await?; Ok(rx.await?) } - - pub fn new_always_failing() -> Self { - let (tx, _rx) = mpsc::unbounded::<(String, oneshot::Sender<String>)>(); - Self { - tx, - _task: Task::ready(()), - } - } } pub struct AskPassSession { #[cfg(not(target_os = "windows"))] script_path: std::path::PathBuf, - #[cfg(not(target_os = "windows"))] - gpg_script_path: std::path::PathBuf, #[cfg(target_os = "windows")] askpass_helper: String, #[cfg(target_os = "windows")] @@ -69,9 +59,6 @@ const ASKPASS_SCRIPT_NAME: &str = "askpass.sh"; #[cfg(target_os = "windows")] const ASKPASS_SCRIPT_NAME: &str = "askpass.ps1"; -#[cfg(not(target_os = "windows"))] -const GPG_SCRIPT_NAME: &str = "gpg.sh"; - impl AskPassSession { /// This will create a new AskPassSession. /// You must retain this session until the master process exits. @@ -85,8 +72,6 @@ impl AskPassSession { let temp_dir = tempfile::Builder::new().prefix("zed-askpass").tempdir()?; let askpass_socket = temp_dir.path().join("askpass.sock"); let askpass_script_path = temp_dir.path().join(ASKPASS_SCRIPT_NAME); - #[cfg(not(target_os = "windows"))] - let gpg_script_path = temp_dir.path().join(GPG_SCRIPT_NAME); let (askpass_opened_tx, askpass_opened_rx) = oneshot::channel::<()>(); let listener = UnixListener::bind(&askpass_socket).context("creating askpass socket")?; #[cfg(not(target_os = "windows"))] @@ -150,20 +135,9 @@ impl AskPassSession { askpass_script_path.display() ); - #[cfg(not(target_os = "windows"))] - { - let gpg_script = generate_gpg_script(); - fs::write(&gpg_script_path, gpg_script) - .await - .with_context(|| format!("creating gpg wrapper script at {gpg_script_path:?}"))?; - make_file_executable(&gpg_script_path).await?; - } - Ok(Self { #[cfg(not(target_os = "windows"))] script_path: askpass_script_path, - #[cfg(not(target_os = "windows"))] - gpg_script_path, #[cfg(target_os = "windows")] secret, @@ -186,19 +160,6 @@ impl AskPassSession { &self.askpass_helper } - #[cfg(not(target_os = "windows"))] - pub fn gpg_script_path(&self) -> Option<impl AsRef<OsStr>> { - Some(&self.gpg_script_path) - } - - #[cfg(target_os = "windows")] - pub fn gpg_script_path(&self) -> Option<impl AsRef<OsStr>> { - // TODO implement wrapping GPG on Windows. This is more difficult than on Unix - // because we can't use --passphrase-fd with a nonstandard FD, and both --passphrase - // and --passphrase-file are insecure. - None::<std::path::PathBuf> - } - // This will run the askpass task forever, resolving as many authentication requests as needed. // The caller is responsible for examining the result of their own commands and cancelling this // future when this is no longer needed. Note that this can only be called once, but due to the @@ -302,23 +263,3 @@ fn generate_askpass_script(zed_path: &std::path::Path, askpass_socket: &std::pat askpass_socket = askpass_socket.display(), ) } - -#[inline] -#[cfg(not(target_os = "windows"))] -fn generate_gpg_script() -> String { - use unindent::Unindent as _; - - r#" - #!/bin/sh - set -eu - - unset GIT_CONFIG_PARAMETERS - GPG_PROGRAM=$(git config gpg.program || echo 'gpg') - PROMPT="Enter passphrase to unlock GPG key:" - PASSPHRASE=$(${GIT_ASKPASS} "${PROMPT}") - - exec "${GPG_PROGRAM}" --batch --no-tty --yes --passphrase-fd 3 --pinentry-mode loopback "$@" 3<<EOF - ${PASSPHRASE} - EOF - "#.unindent() -} diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index 3b9d7861b43dc7a548b7ee58a05c40cf2a68e0b5..40a292e0401df931cc17d04ed71219917292ab1f 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -375,10 +375,8 @@ impl GitRepository for FakeGitRepository { _message: gpui::SharedString, _name_and_email: Option<(gpui::SharedString, gpui::SharedString)>, _options: CommitOptions, - _ask_pass: AskPassDelegate, _env: Arc<HashMap<String, String>>, - _cx: AsyncApp, - ) -> BoxFuture<'static, Result<()>> { + ) -> BoxFuture<'_, Result<()>> { unimplemented!() } diff --git a/crates/git/Cargo.toml b/crates/git/Cargo.toml index 3591e815a4a509df661dba3a8affffdb15672cc4..ab2210094da92cd665ee2483642baaacee66d3a4 100644 --- a/crates/git/Cargo.toml +++ b/crates/git/Cargo.toml @@ -41,9 +41,9 @@ futures.workspace = true workspace-hack.workspace = true [dev-dependencies] -gpui = { workspace = true, features = ["test-support"] } pretty_assertions.workspace = true serde_json.workspace = true -tempfile.workspace = true text = { workspace = true, features = ["test-support"] } unindent.workspace = true +gpui = { workspace = true, features = ["test-support"] } +tempfile.workspace = true diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index a704ba14824bd6391cca6dda823c2f0cb738ffee..2ecd4bb894348cf3fc532a8473e43f0712e61700 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -391,12 +391,8 @@ pub trait GitRepository: Send + Sync { message: SharedString, name_and_email: Option<(SharedString, SharedString)>, options: CommitOptions, - askpass: AskPassDelegate, env: Arc<HashMap<String, String>>, - // This method takes an AsyncApp to ensure it's invoked on the main thread, - // otherwise git-credentials-manager won't work. - cx: AsyncApp, - ) -> BoxFuture<'static, Result<()>>; + ) -> BoxFuture<'_, Result<()>>; fn push( &self, @@ -1197,68 +1193,36 @@ impl GitRepository for RealGitRepository { message: SharedString, name_and_email: Option<(SharedString, SharedString)>, options: CommitOptions, - ask_pass: AskPassDelegate, env: Arc<HashMap<String, String>>, - cx: AsyncApp, - ) -> BoxFuture<'static, Result<()>> { + ) -> BoxFuture<'_, Result<()>> { let working_directory = self.working_directory(); - let executor = cx.background_executor().clone(); - async move { - let working_directory = working_directory?; - let have_user_git_askpass = env.contains_key("GIT_ASKPASS"); - let mut command = new_smol_command("git"); - command.current_dir(&working_directory).envs(env.iter()); - - let ask_pass = if have_user_git_askpass { - None - } else { - Some(AskPassSession::new(&executor, ask_pass).await?) - }; - - if let Some(program) = ask_pass - .as_ref() - .and_then(|ask_pass| ask_pass.gpg_script_path()) - { - command.arg("-c").arg(format!( - "gpg.program={}", - program.as_ref().to_string_lossy() - )); - } - - command - .args(["commit", "-m"]) - .arg(message.to_string()) - .arg("--cleanup=strip") - .stdin(smol::process::Stdio::null()) - .stdout(smol::process::Stdio::piped()) - .stderr(smol::process::Stdio::piped()); - - if options.amend { - command.arg("--amend"); - } + self.executor + .spawn(async move { + let mut cmd = new_smol_command("git"); + cmd.current_dir(&working_directory?) + .envs(env.iter()) + .args(["commit", "--quiet", "-m"]) + .arg(&message.to_string()) + .arg("--cleanup=strip"); + + if options.amend { + cmd.arg("--amend"); + } - if let Some((name, email)) = name_and_email { - command.arg("--author").arg(&format!("{name} <{email}>")); - } + if let Some((name, email)) = name_and_email { + cmd.arg("--author").arg(&format!("{name} <{email}>")); + } - if let Some(ask_pass) = ask_pass { - command.env("GIT_ASKPASS", ask_pass.script_path()); - let git_process = command.spawn()?; + let output = cmd.output().await?; - run_askpass_command(ask_pass, git_process).await?; - Ok(()) - } else { - let git_process = command.spawn()?; - let output = git_process.output().await?; anyhow::ensure!( output.status.success(), - "{}", + "Failed to commit:\n{}", String::from_utf8_lossy(&output.stderr) ); Ok(()) - } - } - .boxed() + }) + .boxed() } fn push( @@ -2082,16 +2046,12 @@ mod tests { ) .await .unwrap(); - cx.spawn(|cx| { - repo.commit( - "Initial commit".into(), - None, - CommitOptions::default(), - AskPassDelegate::new_always_failing(), - Arc::new(checkpoint_author_envs()), - cx, - ) - }) + repo.commit( + "Initial commit".into(), + None, + CommitOptions::default(), + Arc::new(checkpoint_author_envs()), + ) .await .unwrap(); @@ -2115,16 +2075,12 @@ mod tests { ) .await .unwrap(); - cx.spawn(|cx| { - repo.commit( - "Commit after checkpoint".into(), - None, - CommitOptions::default(), - AskPassDelegate::new_always_failing(), - Arc::new(checkpoint_author_envs()), - cx, - ) - }) + repo.commit( + "Commit after checkpoint".into(), + None, + CommitOptions::default(), + Arc::new(checkpoint_author_envs()), + ) .await .unwrap(); @@ -2257,16 +2213,12 @@ mod tests { ) .await .unwrap(); - cx.spawn(|cx| { - repo.commit( - "Initial commit".into(), - None, - CommitOptions::default(), - AskPassDelegate::new_always_failing(), - Arc::new(checkpoint_author_envs()), - cx, - ) - }) + repo.commit( + "Initial commit".into(), + None, + CommitOptions::default(), + Arc::new(checkpoint_author_envs()), + ) .await .unwrap(); diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index cc378af8327cbd0f2568e78d8b3d41c746c49acf..c50e2f8912ef5b4570a7141378f55701151f3f71 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -1574,15 +1574,10 @@ impl GitPanel { let task = if self.has_staged_changes() { // Repository serializes all git operations, so we can just send a commit immediately - cx.spawn_in(window, async move |this, cx| { - let askpass_delegate = this.update_in(cx, |this, window, cx| { - this.askpass_delegate("git commit", window, cx) - })?; - let commit_task = active_repository.update(cx, |repo, cx| { - repo.commit(message.into(), None, options, askpass_delegate, cx) - })?; - commit_task.await? - }) + let commit_task = active_repository.update(cx, |repo, cx| { + repo.commit(message.into(), None, options, cx) + }); + cx.background_spawn(async move { commit_task.await? }) } else { let changed_files = self .entries @@ -1599,13 +1594,10 @@ impl GitPanel { let stage_task = active_repository.update(cx, |repo, cx| repo.stage_entries(changed_files, cx)); - cx.spawn_in(window, async move |this, cx| { + cx.spawn(async move |_, cx| { stage_task.await?; - let askpass_delegate = this.update_in(cx, |this, window, cx| { - this.askpass_delegate("git commit".to_string(), window, cx) - })?; let commit_task = active_repository.update(cx, |repo, cx| { - repo.commit(message.into(), None, options, askpass_delegate, cx) + repo.commit(message.into(), None, options, cx) })?; commit_task.await? }) diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 69fe58aadb39d24acab08ad7f06bba7233f458ac..9ff3823e0f13a87fdcff944db7ad2d52350a7cce 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -1726,18 +1726,6 @@ impl GitStore { let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; - let askpass = if let Some(askpass_id) = envelope.payload.askpass_id { - make_remote_delegate( - this, - envelope.payload.project_id, - repository_id, - askpass_id, - &mut cx, - ) - } else { - AskPassDelegate::new_always_failing() - }; - let message = SharedString::from(envelope.payload.message); let name = envelope.payload.name.map(SharedString::from); let email = envelope.payload.email.map(SharedString::from); @@ -1751,7 +1739,6 @@ impl GitStore { CommitOptions { amend: options.amend, }, - askpass, cx, ) })? @@ -3475,14 +3462,11 @@ impl Repository { message: SharedString, name_and_email: Option<(SharedString, SharedString)>, options: CommitOptions, - askpass: AskPassDelegate, _cx: &mut App, ) -> oneshot::Receiver<Result<()>> { let id = self.id; - let askpass_delegates = self.askpass_delegates.clone(); - let askpass_id = util::post_inc(&mut self.latest_askpass_id); - self.send_job(Some("git commit".into()), move |git_repo, cx| async move { + self.send_job(Some("git commit".into()), move |git_repo, _cx| async move { match git_repo { RepositoryState::Local { backend, @@ -3490,16 +3474,10 @@ impl Repository { .. } => { backend - .commit(message, name_and_email, options, askpass, environment, cx) + .commit(message, name_and_email, options, environment) .await } RepositoryState::Remote { project_id, client } => { - askpass_delegates.lock().insert(askpass_id, askpass); - let _defer = util::defer(|| { - let askpass_delegate = askpass_delegates.lock().remove(&askpass_id); - debug_assert!(askpass_delegate.is_some()); - }); - let (name, email) = name_and_email.unzip(); client .request(proto::Commit { @@ -3511,9 +3489,9 @@ impl Repository { options: Some(proto::commit::CommitOptions { amend: options.amend, }), - askpass_id: Some(askpass_id), }) - .await?; + .await + .context("sending commit request")?; Ok(()) } diff --git a/crates/proto/proto/git.proto b/crates/proto/proto/git.proto index 6645e91de71760abdaf22174d207852e7419bbfb..6593062ed2b9c4e840fb60a033187c5fd22d0860 100644 --- a/crates/proto/proto/git.proto +++ b/crates/proto/proto/git.proto @@ -294,7 +294,7 @@ message Commit { optional string email = 5; string message = 6; optional CommitOptions options = 7; - optional uint64 askpass_id = 8; + reserved 8; message CommitOptions { bool amend = 1; From 5b61b8c8ed6469c3b12aed6d396561d2c4aee17b Mon Sep 17 00:00:00 2001 From: Cole Miller <cole@zed.dev> Date: Fri, 11 Jul 2025 21:01:09 -0400 Subject: [PATCH 036/658] agent: Fix crash with pathological fetch output (#34253) Closes #34029 The crash is due to a stack overflow in our `html_to_markdown` conversion; I've added a maximum depth of 200 for the recursion in that crate to guard against this kind of thing. Separately, we were treating all content-types other than `text/plain` and `application/json` as HTML; I've changed this to only treat `text/html` and `application/xhtml+xml` as HTML, and fall back to plaintext. (In the original crash, the content-type was `application/octet-stream`.) Release Notes: - agent: Fixed a potential crash when fetching large non-HTML files. --- crates/assistant_tools/src/fetch_tool.rs | 5 ++--- crates/html_to_markdown/src/markdown_writer.rs | 6 ++++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/assistant_tools/src/fetch_tool.rs b/crates/assistant_tools/src/fetch_tool.rs index c8fa600e831e60ea22bfbaa8b54a9be3b142c567..54d49359baeae51ab4c575824bd5b42728925a66 100644 --- a/crates/assistant_tools/src/fetch_tool.rs +++ b/crates/assistant_tools/src/fetch_tool.rs @@ -69,10 +69,9 @@ impl FetchTool { .to_str() .context("invalid Content-Type header")?; let content_type = match content_type { - "text/html" => ContentType::Html, - "text/plain" => ContentType::Plaintext, + "text/html" | "application/xhtml+xml" => ContentType::Html, "application/json" => ContentType::Json, - _ => ContentType::Html, + _ => ContentType::Plaintext, }; match content_type { diff --git a/crates/html_to_markdown/src/markdown_writer.rs b/crates/html_to_markdown/src/markdown_writer.rs index a9caf7afa7a5a275ff4ecabaf38150f4fd2127a3..c32205ae7b349239d0a41a5a63c0793d958b4eea 100644 --- a/crates/html_to_markdown/src/markdown_writer.rs +++ b/crates/html_to_markdown/src/markdown_writer.rs @@ -119,8 +119,10 @@ impl MarkdownWriter { .push_back(current_element.clone()); } - for child in node.children.borrow().iter() { - self.visit_node(child, handlers)?; + if self.current_element_stack.len() < 200 { + for child in node.children.borrow().iter() { + self.visit_node(child, handlers)?; + } } if let Some(current_element) = current_element { From e070c81687b70302c5df358f461c510f5cace6e5 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov <kirill@zed.dev> Date: Sat, 12 Jul 2025 14:42:14 +0300 Subject: [PATCH 037/658] Remove remaining plugin-related language server adapters (#34334) Follow-up of https://github.com/zed-industries/zed/pull/34208 Release Notes: - N/A --- crates/language/src/language_registry.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index ff17d6dd9a9d7bb250f15d358d11eb23ef8f188f..ab3c0f9b37d89fed2af57c5dc689589a9f025649 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -334,6 +334,9 @@ impl LanguageRegistry { if let Some(adapters) = state.lsp_adapters.get_mut(language_name) { adapters.retain(|adapter| &adapter.name != name) } + state.all_lsp_adapters.remove(name); + state.available_lsp_adapters.remove(name); + state.version += 1; state.reload_count += 1; *state.subscription.0.borrow_mut() = (); From 46834d31f139b7d9d1fdf1a1e072437f7f128e29 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Sat, 12 Jul 2025 11:48:19 -0300 Subject: [PATCH 038/658] Refine status bar design (#34324) Experimenting with a set of standardized icons and polishing spacing a little bit. Release Notes: - N/A --- assets/icons/debug.svg | 13 ++++++++++++- assets/icons/file_tree.svg | 6 +++--- assets/icons/git_branch_small.svg | 11 ++++++----- assets/icons/list_tree.svg | 8 +++++++- assets/icons/terminal_alt.svg | 5 +++++ assets/icons/user_group.svg | 4 +++- assets/icons/zed_assistant.svg | 2 +- crates/icons/src/icons.rs | 1 + crates/search/src/search_status_button.rs | 18 ++++++------------ crates/terminal_view/src/terminal_panel.rs | 2 +- .../ui/src/components/progress/progress_bar.rs | 3 +-- crates/workspace/src/dock.rs | 9 +++++---- crates/workspace/src/status_bar.rs | 15 ++++++++------- 13 files changed, 59 insertions(+), 38 deletions(-) create mode 100644 assets/icons/terminal_alt.svg diff --git a/assets/icons/debug.svg b/assets/icons/debug.svg index 8cea0c460402fbb36769aa0aaadab9f80513d101..ff51e42b1a9483f4f6d0382d67aa34bd3405f1ff 100644 --- a/assets/icons/debug.svg +++ b/assets/icons/debug.svg @@ -1 +1,12 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-bug"><path d="m8 2 1.88 1.88"/><path d="M14.12 3.88 16 2"/><path d="M9 7.13v-1a3.003 3.003 0 1 1 6 0v1"/><path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6"/><path d="M12 20v-9"/><path d="M6.53 9C4.6 8.8 3 7.1 3 5"/><path d="M6 13H2"/><path d="M3 21c0-2.1 1.7-3.9 3.8-4"/><path d="M20.97 5c0 2.1-1.6 3.8-3.5 4"/><path d="M22 13h-4"/><path d="M17.2 17c2.1.1 3.8 1.9 3.8 4"/></svg> +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M5.49219 2.29071L6.41455 3.1933" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M9.61816 3.1933L10.508 2.29071" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M5.7042 5.89221V5.15749C5.69033 4.85975 5.73943 4.56239 5.84856 4.28336C5.95768 4.00434 6.12456 3.74943 6.33913 3.53402C6.55369 3.31862 6.81149 3.14718 7.09697 3.03005C7.38245 2.91292 7.68969 2.85254 8.00014 2.85254C8.3106 2.85254 8.61784 2.91292 8.90332 3.03005C9.18879 3.14718 9.44659 3.31862 9.66116 3.53402C9.87572 3.74943 10.0426 4.00434 10.1517 4.28336C10.2609 4.56239 10.31 4.85975 10.2961 5.15749V5.89221" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M8.00006 13.0426C6.13263 13.0426 4.60474 11.6005 4.60474 9.83792V8.23558C4.60474 7.66895 4.84322 7.12554 5.26772 6.72487C5.69221 6.32421 6.26796 6.09912 6.86829 6.09912H9.13184C9.73217 6.09912 10.3079 6.32421 10.7324 6.72487C11.1569 7.12554 11.3954 7.66895 11.3954 8.23558V9.83792C11.3954 11.6005 9.86749 13.0426 8.00006 13.0426Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M4.60452 6.25196C3.51235 6.13878 2.60693 5.17677 2.60693 3.9884" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M4.60462 8.81659H2.34106" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M2.4541 13.3186C2.4541 12.1302 3.41611 11.1116 4.60448 11.0551" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M13.0761 3.9884C13.0761 5.17677 12.1706 6.13878 11.0955 6.25196" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M13.6591 8.81659H11.3955" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M11.3955 11.0551C12.5839 11.1116 13.5459 12.1302 13.5459 13.3186" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/assets/icons/file_tree.svg b/assets/icons/file_tree.svg index 4c921b135183b7b58126f16c68f39aec22677285..a140cd70b12d1be180d2c683d59400212969c47a 100644 --- a/assets/icons/file_tree.svg +++ b/assets/icons/file_tree.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="M3.03125 3V3.03125M3.03125 3.03125V9M3.03125 3.03125C3.03125 5 6 5 6 5M3.03125 9C3.03125 11 6 11 6 11M3.03125 9V12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> - <rect x="8" y="2.5" width="6" height="5" rx="1.5" fill="black"/> - <rect x="8" y="8.46875" width="6" height="5.0625" rx="1.5" fill="black"/> +<path d="M3 3V3.03125M3 3.03125V9M3 3.03125C3 5 5.96875 5 5.96875 5M3 9C3 11 5.96875 11 5.96875 11M3 9V12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<rect x="8" y="3" width="5.5" height="4" rx="1.5" fill="black"/> +<rect x="8" y="9" width="5.5" height="4" rx="1.5" fill="black"/> </svg> diff --git a/assets/icons/git_branch_small.svg b/assets/icons/git_branch_small.svg index d23fc176ac797fff35c6c9d35176d5e03c6170fe..22832d6fedfc5221c31c81eae497f8172b59c21e 100644 --- a/assets/icons/git_branch_small.svg +++ b/assets/icons/git_branch_small.svg @@ -1,6 +1,7 @@ -<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path fill-rule="evenodd" clip-rule="evenodd" d="M3.75 3.25C4.02614 3.25 4.25 3.02614 4.25 2.75C4.25 2.47386 4.02614 2.25 3.75 2.25C3.47386 2.25 3.25 2.47386 3.25 2.75C3.25 3.02614 3.47386 3.25 3.75 3.25ZM3.75 4.25C4.57843 4.25 5.25 3.57843 5.25 2.75C5.25 1.92157 4.57843 1.25 3.75 1.25C2.92157 1.25 2.25 1.92157 2.25 2.75C2.25 3.57843 2.92157 4.25 3.75 4.25Z" fill="black"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M8.25 3.25C8.52614 3.25 8.75 3.02614 8.75 2.75C8.75 2.47386 8.52614 2.25 8.25 2.25C7.97386 2.25 7.75 2.47386 7.75 2.75C7.75 3.02614 7.97386 3.25 8.25 3.25ZM8.25 4.25C9.07843 4.25 9.75 3.57843 9.75 2.75C9.75 1.92157 9.07843 1.25 8.25 1.25C7.42157 1.25 6.75 1.92157 6.75 2.75C6.75 3.57843 7.42157 4.25 8.25 4.25Z" fill="black"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M3.75 9.75C4.02614 9.75 4.25 9.52614 4.25 9.25C4.25 8.97386 4.02614 8.75 3.75 8.75C3.47386 8.75 3.25 8.97386 3.25 9.25C3.25 9.52614 3.47386 9.75 3.75 9.75ZM3.75 10.75C4.57843 10.75 5.25 10.0784 5.25 9.25C5.25 8.42157 4.57843 7.75 3.75 7.75C2.92157 7.75 2.25 8.42157 2.25 9.25C2.25 10.0784 2.92157 10.75 3.75 10.75Z" fill="black"/> -<path fill-rule="evenodd" clip-rule="evenodd" d="M3.25 3.75H4.25V5.59609C4.67823 5.35824 5.24991 5.25 6 5.25H7.25017C7.5262 5.25 7.75 5.02625 7.75 4.75V3.75H8.75V4.75C8.75 5.57832 8.07871 6.25 7.25017 6.25H6C5.14559 6.25 4.77639 6.41132 4.59684 6.56615C4.42571 6.71373 4.33877 6.92604 4.25 7.30651V8.25H3.25V3.75Z" fill="black"/> +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<circle cx="5" cy="12" r="1.25" stroke="black" stroke-width="1.5"/> +<path d="M5 11V5" stroke="black" stroke-width="1.5"/> +<path d="M5 10C5 10 5.5 8 7 8C7.73103 8 8.69957 8 9.50049 8C10.3289 8 11 7.32843 11 6.5V5" stroke="black" stroke-width="1.5"/> +<circle cx="5" cy="4" r="1.25" stroke="black" stroke-width="1.5"/> +<circle cx="11" cy="4" r="1.25" stroke="black" stroke-width="1.5"/> </svg> diff --git a/assets/icons/list_tree.svg b/assets/icons/list_tree.svg index 8cf157ec135d13395fc8ac66d8f8086f0d199a2e..09872a60f7ed9c85e89f06b7384b083a7f4b5779 100644 --- a/assets/icons/list_tree.svg +++ b/assets/icons/list_tree.svg @@ -1 +1,7 @@ -<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-list-tree"><path d="M21 12h-8"/><path d="M21 6H8"/><path d="M21 18h-8"/><path d="M3 6v4c0 1.1.9 2 2 2h3"/><path d="M3 10v6c0 1.1.9 2 2 2h3"/></svg> \ No newline at end of file +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M13.5 8H9.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M13.5 4L6.5 4" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M13.5 12H9.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M3 3.5V6.33333C3 7.25 3.72 8 4.6 8H7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M3 6V10.5C3 11.325 3.72 12 4.6 12H7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/assets/icons/terminal_alt.svg b/assets/icons/terminal_alt.svg new file mode 100644 index 0000000000000000000000000000000000000000..7afb89db2130b8d9233c1662d7fcf86f63de305a --- /dev/null +++ b/assets/icons/terminal_alt.svg @@ -0,0 +1,5 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M11.8889 3H4.11111C3.49746 3 3 3.49746 3 4.11111V11.8889C3 12.5025 3.49746 13 4.11111 13H11.8889C12.5025 13 13 12.5025 13 11.8889V4.11111C13 3.49746 12.5025 3 11.8889 3Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M8.37939 10.3243H10.3794" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M5.64966 9.32837L7.64966 7.32837L5.64966 5.32837" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/assets/icons/user_group.svg b/assets/icons/user_group.svg index aa99277646653c899ee049547e5574b76b25b840..ac1f7bdc633190f88b202d9e5ae7430af225aecd 100644 --- a/assets/icons/user_group.svg +++ b/assets/icons/user_group.svg @@ -1,3 +1,5 @@ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M5.9 8.00002C7.44656 8.00002 8.7 6.74637 8.7 5.20002C8.7 3.65368 7.44656 2.40002 5.9 2.40002C4.35344 2.40002 3.1 3.65368 3.1 5.20002C3.1 6.74637 4.35344 8.00002 5.9 8.00002ZM7.00906 9.05002H4.79094C2.69684 9.05002 1 10.7475 1 12.841C1 13.261 1.3395 13.6 1.75819 13.6H10.0409C10.4609 13.6 10.8 13.261 10.8 12.841C10.8 10.7475 9.1025 9.05002 7.00906 9.05002ZM11.4803 9.40002H9.86484C10.87 10.2247 11.5 11.4585 11.5 12.841C11.5 13.121 11.4169 13.3791 11.2812 13.6H14.3C14.6872 13.6 15 13.285 15 12.8803C15 10.9663 13.4338 9.40002 11.4803 9.40002ZM10.45 8.00002C11.8041 8.00002 12.9 6.90409 12.9 5.55002C12.9 4.19596 11.8041 3.10002 10.45 3.10002C9.90072 3.10002 9.39913 3.28716 8.9905 3.59243C9.2425 4.07631 9.4 4.61815 9.4 5.20002C9.4 5.97702 9.13903 6.69059 8.70897 7.27181C9.15281 7.72002 9.7675 8.00002 10.45 8.00002Z" fill="white"/> +<path d="M6.79118 8.27005C8.27568 8.27005 9.4791 7.06663 9.4791 5.58214C9.4791 4.09765 8.27568 2.89423 6.79118 2.89423C5.30669 2.89423 4.10327 4.09765 4.10327 5.58214C4.10327 7.06663 5.30669 8.27005 6.79118 8.27005Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M6.79112 8.60443C4.19441 8.60443 2.08936 10.7095 2.08936 13.3062H11.4929C11.4929 10.7095 9.38784 8.60443 6.79112 8.60443Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M14.6984 12.9263C14.6984 10.8893 13.4895 8.99736 12.2806 8.09067C12.6779 7.79254 12.9957 7.40104 13.2057 6.95083C13.4157 6.50062 13.5115 6.00558 13.4846 5.50952C13.4577 5.01346 13.309 4.53168 13.0515 4.10681C12.7941 3.68194 12.4358 3.3271 12.0085 3.07367" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> </svg> diff --git a/assets/icons/zed_assistant.svg b/assets/icons/zed_assistant.svg index 693d86f929ff170f08edf3d2a0a7a28af17a30bf..d21252de8c234611ddd41caff287e3fc0d540ed3 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 2L6.72534 5.87534C6.6601 6.07367 6.5492 6.25392 6.40155 6.40155C6.25392 6.5492 6.07367 6.6601 5.87534 6.72534L2 8L5.87534 9.27466C6.07367 9.3399 6.25392 9.4508 6.40155 9.59845C6.5492 9.74608 6.6601 9.92633 6.72534 10.1247L8 14L9.27466 10.1247C9.3399 9.92633 9.4508 9.74608 9.59845 9.59845C9.74608 9.4508 9.92633 9.3399 10.1247 9.27466L14 8L10.1247 6.72534C9.92633 6.6601 9.74608 6.5492 9.59845 6.40155C9.4508 6.25392 9.3399 6.07367 9.27466 5.87534L8 2Z" fill="black" fill-opacity="0.15" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<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.5" 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"/> </svg> diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index 76a04e036dc92d9ebbf848fc34bb21195862b99a..4151e6b2ea03c92bd43064e93a6a37fd0bc3cbf2 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -247,6 +247,7 @@ pub enum IconName { SwatchBook, Tab, Terminal, + TerminalAlt, TextSnippet, ThumbsDown, ThumbsUp, diff --git a/crates/search/src/search_status_button.rs b/crates/search/src/search_status_button.rs index fcdf36041f282376716aac3bde78baf8a667a68e..ff2ee1641d07a68c52e88a9686e90b2f3f40c4c5 100644 --- a/crates/search/src/search_status_button.rs +++ b/crates/search/src/search_status_button.rs @@ -1,9 +1,6 @@ use editor::EditorSettings; use settings::Settings as _; -use ui::{ - ButtonCommon, ButtonLike, Clickable, Color, Context, Icon, IconName, IconSize, ParentElement, - Render, Styled, Tooltip, Window, h_flex, -}; +use ui::{ButtonCommon, Clickable, Context, Render, Tooltip, Window, prelude::*}; use workspace::{ItemHandle, StatusItemView}; pub struct SearchButton; @@ -16,18 +13,15 @@ impl SearchButton { impl Render for SearchButton { fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement { - let button = h_flex().gap_2(); + let button = div(); + if !EditorSettings::get_global(cx).search.button { - return button; + return button.w_0().invisible(); } button.child( - ButtonLike::new("project-search-indicator") - .child( - Icon::new(IconName::MagnifyingGlass) - .size(IconSize::Small) - .color(Color::Default), - ) + IconButton::new("project-search-indicator", IconName::MagnifyingGlass) + .icon_size(IconSize::Small) .tooltip(|window, cx| { Tooltip::for_action( "Project Search", diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index f6eee3065ca974449315ab2ac519de1acb5da11e..cb1e3628848e9e850fa0c13f8b659259a1e6fd48 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -1437,7 +1437,7 @@ impl Panel for TerminalPanel { if (self.is_enabled(cx) || !self.has_no_terminals(cx)) && TerminalSettings::get_global(cx).button { - Some(IconName::Terminal) + Some(IconName::TerminalAlt) } else { None } diff --git a/crates/ui/src/components/progress/progress_bar.rs b/crates/ui/src/components/progress/progress_bar.rs index 67b6be6723fc9441a96321003f7194121467ea14..5cc5abd36d041bc03676410983020b94ac8d8809 100644 --- a/crates/ui/src/components/progress/progress_bar.rs +++ b/crates/ui/src/components/progress/progress_bar.rs @@ -69,8 +69,7 @@ impl RenderOnce for ProgressBar { .w_full() .h(px(8.0)) .rounded_full() - .py(px(2.0)) - .px(px(4.0)) + .p(px(2.0)) .bg(self.bg_color) .shadow(vec![gpui::BoxShadow { color: gpui::black().opacity(0.08), diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 8fcd55b784fc4202a34a1a34f72590933bb0f3d1..c8301dcf352f63db50586ee3a5e603a4583f6280 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -221,9 +221,9 @@ pub enum DockPosition { impl DockPosition { fn label(&self) -> &'static str { match self { - Self::Left => "left", - Self::Bottom => "bottom", - Self::Right => "right", + Self::Left => "Left", + Self::Bottom => "Bottom", + Self::Right => "Right", } } @@ -864,7 +864,7 @@ impl Render for PanelButtons { let action = dock.toggle_action(); let tooltip: SharedString = - format!("Close {} dock", dock.position.label()).into(); + format!("Close {} Dock", dock.position.label()).into(); (action, tooltip) } else { @@ -923,6 +923,7 @@ impl Render for PanelButtons { .collect(); let has_buttons = !buttons.is_empty(); + h_flex() .gap_1() .children(buttons) diff --git a/crates/workspace/src/status_bar.rs b/crates/workspace/src/status_bar.rs index 798d49eec5c7e0ea8d8c4bc7427389af8de2c658..edeb382de7d386b37d81b2649af85cf97f9e8b31 100644 --- a/crates/workspace/src/status_bar.rs +++ b/crates/workspace/src/status_bar.rs @@ -42,7 +42,7 @@ impl Render for StatusBar { .justify_between() .gap(DynamicSpacing::Base08.rems(cx)) .py(DynamicSpacing::Base04.rems(cx)) - .px(DynamicSpacing::Base08.rems(cx)) + .px(DynamicSpacing::Base06.rems(cx)) .bg(cx.theme().colors().status_bar_background) .map(|el| match window.window_decorations() { Decorations::Server => el, @@ -58,22 +58,23 @@ impl Render for StatusBar { .border_b(px(1.0)) .border_color(cx.theme().colors().status_bar_background), }) - .child(self.render_left_tools(cx)) - .child(self.render_right_tools(cx)) + .child(self.render_left_tools()) + .child(self.render_right_tools()) } } impl StatusBar { - fn render_left_tools(&self, cx: &mut Context<Self>) -> impl IntoElement { + fn render_left_tools(&self) -> impl IntoElement { h_flex() - .gap(DynamicSpacing::Base04.rems(cx)) + .gap_1() .overflow_x_hidden() .children(self.left_items.iter().map(|item| item.to_any())) } - fn render_right_tools(&self, cx: &mut Context<Self>) -> impl IntoElement { + fn render_right_tools(&self) -> impl IntoElement { h_flex() - .gap(DynamicSpacing::Base04.rems(cx)) + .gap_1() + .overflow_x_hidden() .children(self.right_items.iter().rev().map(|item| item.to_any())) } } From 1b6e212ebaf5bc8797edacefff119a4f49fb0287 Mon Sep 17 00:00:00 2001 From: Cole Miller <cole@zed.dev> Date: Sat, 12 Jul 2025 11:27:18 -0400 Subject: [PATCH 039/658] debugger: Fix endless restarts when connecting to TCP adapters over SSH (#34328) Closes #34323 Closes #34313 The previous PR #33932 introduced a way to "close" the `pending_requests` buffer of the `TransportDelegate`, preventing any more requests from being added. This prevents pending requests from accumulating without ever being drained during the shutdown sequence; without it, some of our tests hang at this point (due to using a single-threaded executor). The bug occurred because we were closing `pending_requests` whenever we detected the server side of the transport shut down, and this closed state stuck around and interfered with the retry logic for SSH+TCP adapter connections. This PR fixes the bug by only closing `pending_requests` on session shutdown, and adds a regression test covering the SSH retry logic. Release Notes: - debugger: Fixed a bug causing SSH connections to some adapters (Python, Go, JavaScript) to fail and restart endlessly. --- Cargo.lock | 1 + crates/collab/Cargo.toml | 1 + .../remote_editing_collaboration_tests.rs | 167 ++++++++- crates/dap/src/adapters.rs | 10 +- crates/dap/src/client.rs | 32 +- crates/dap/src/transport.rs | 345 +++++++++++------- crates/debugger_ui/src/session.rs | 2 +- crates/debugger_ui/src/session/running.rs | 2 +- 8 files changed, 429 insertions(+), 131 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 31c92b951fc63162118b8e95a341dd236d229094..c07d311db96eaf1c11fba41d566ebdae7ba3b439 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3109,6 +3109,7 @@ dependencies = [ "context_server", "ctor", "dap", + "dap-types", "dap_adapters", "dashmap 6.1.0", "debugger_ui", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 55c15cac5ac84c9d166c54a127dd18b2237b9bd9..7b536a2d24bd408d7fa49e80453ec463c95e5347 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -94,6 +94,7 @@ context_server.workspace = true ctor.workspace = true dap = { workspace = true, features = ["test-support"] } dap_adapters = { workspace = true, features = ["test-support"] } +dap-types.workspace = true debugger_ui = { workspace = true, features = ["test-support"] } editor = { workspace = true, features = ["test-support"] } extension.workspace = true diff --git a/crates/collab/src/tests/remote_editing_collaboration_tests.rs b/crates/collab/src/tests/remote_editing_collaboration_tests.rs index 7aeb381c02beeb6165e44ccd5bbd72f5744cc964..8ab6e6910c88880bc8b6451d972e39b5c2315812 100644 --- a/crates/collab/src/tests/remote_editing_collaboration_tests.rs +++ b/crates/collab/src/tests/remote_editing_collaboration_tests.rs @@ -2,6 +2,7 @@ use crate::tests::TestServer; use call::ActiveCall; use collections::{HashMap, HashSet}; +use dap::{Capabilities, adapters::DebugTaskDefinition, transport::RequestHandling}; use debugger_ui::debugger_panel::DebugPanel; use extension::ExtensionHostProxy; use fs::{FakeFs, Fs as _, RemoveOptions}; @@ -22,6 +23,7 @@ use language::{ use node_runtime::NodeRuntime; use project::{ ProjectPath, + debugger::session::ThreadId, lsp_store::{FormatTrigger, LspFormatTarget}, }; use remote::SshRemoteClient; @@ -29,7 +31,11 @@ use remote_server::{HeadlessAppState, HeadlessProject}; use rpc::proto; use serde_json::json; use settings::SettingsStore; -use std::{path::Path, sync::Arc}; +use std::{ + path::Path, + sync::{Arc, atomic::AtomicUsize}, +}; +use task::TcpArgumentsTemplate; use util::path; #[gpui::test(iterations = 10)] @@ -688,3 +694,162 @@ async fn test_remote_server_debugger( shutdown_session.await.unwrap(); } + +#[gpui::test] +async fn test_slow_adapter_startup_retries( + cx_a: &mut TestAppContext, + server_cx: &mut TestAppContext, + executor: BackgroundExecutor, +) { + cx_a.update(|cx| { + release_channel::init(SemanticVersion::default(), cx); + command_palette_hooks::init(cx); + zlog::init_test(); + dap_adapters::init(cx); + }); + server_cx.update(|cx| { + release_channel::init(SemanticVersion::default(), cx); + dap_adapters::init(cx); + }); + let (opts, server_ssh) = SshRemoteClient::fake_server(cx_a, server_cx); + let remote_fs = FakeFs::new(server_cx.executor()); + remote_fs + .insert_tree( + path!("/code"), + json!({ + "lib.rs": "fn one() -> usize { 1 }" + }), + ) + .await; + + // User A connects to the remote project via SSH. + server_cx.update(HeadlessProject::init); + let remote_http_client = Arc::new(BlockedHttpClient); + let node = NodeRuntime::unavailable(); + let languages = Arc::new(LanguageRegistry::new(server_cx.executor())); + let _headless_project = server_cx.new(|cx| { + client::init_settings(cx); + HeadlessProject::new( + HeadlessAppState { + session: server_ssh, + fs: remote_fs.clone(), + http_client: remote_http_client, + node_runtime: node, + languages, + extension_host_proxy: Arc::new(ExtensionHostProxy::new()), + }, + cx, + ) + }); + + let client_ssh = SshRemoteClient::fake_client(opts, cx_a).await; + let mut server = TestServer::start(server_cx.executor()).await; + let client_a = server.create_client(cx_a, "user_a").await; + cx_a.update(|cx| { + debugger_ui::init(cx); + command_palette_hooks::init(cx); + }); + let (project_a, _) = client_a + .build_ssh_project(path!("/code"), client_ssh.clone(), cx_a) + .await; + + let (workspace, cx_a) = client_a.build_workspace(&project_a, cx_a); + + let debugger_panel = workspace + .update_in(cx_a, |_workspace, window, cx| { + cx.spawn_in(window, DebugPanel::load) + }) + .await + .unwrap(); + + workspace.update_in(cx_a, |workspace, window, cx| { + workspace.add_panel(debugger_panel, window, cx); + }); + + cx_a.run_until_parked(); + let debug_panel = workspace + .update(cx_a, |workspace, cx| workspace.panel::<DebugPanel>(cx)) + .unwrap(); + + let workspace_window = cx_a + .window_handle() + .downcast::<workspace::Workspace>() + .unwrap(); + + let count = Arc::new(AtomicUsize::new(0)); + let session = debugger_ui::tests::start_debug_session_with( + &workspace_window, + cx_a, + DebugTaskDefinition { + adapter: "fake-adapter".into(), + label: "test".into(), + config: json!({ + "request": "launch" + }), + tcp_connection: Some(TcpArgumentsTemplate { + port: None, + host: None, + timeout: None, + }), + }, + move |client| { + let count = count.clone(); + client.on_request_ext::<dap::requests::Initialize, _>(move |_seq, _request| { + if count.fetch_add(1, std::sync::atomic::Ordering::SeqCst) < 5 { + return RequestHandling::Exit; + } + RequestHandling::Respond(Ok(Capabilities::default())) + }); + }, + ) + .unwrap(); + cx_a.run_until_parked(); + + let client = session.update(cx_a, |session, _| session.adapter_client().unwrap()); + client + .fake_event(dap::messages::Events::Stopped(dap::StoppedEvent { + reason: dap::StoppedEventReason::Pause, + description: None, + thread_id: Some(1), + preserve_focus_hint: None, + text: None, + all_threads_stopped: None, + hit_breakpoint_ids: None, + })) + .await; + + cx_a.run_until_parked(); + + let active_session = debug_panel + .update(cx_a, |this, _| this.active_session()) + .unwrap(); + + let running_state = active_session.update(cx_a, |active_session, _| { + active_session.running_state().clone() + }); + + assert_eq!( + client.id(), + running_state.read_with(cx_a, |running_state, _| running_state.session_id()) + ); + assert_eq!( + ThreadId(1), + running_state.read_with(cx_a, |running_state, _| running_state + .selected_thread_id() + .unwrap()) + ); + + let shutdown_session = workspace.update(cx_a, |workspace, cx| { + workspace.project().update(cx, |project, cx| { + project.dap_store().update(cx, |dap_store, cx| { + dap_store.shutdown_session(session.read(cx).session_id(), cx) + }) + }) + }); + + client_ssh.update(cx_a, |a, _| { + a.shutdown_processes(Some(proto::ShutdownRemoteServer {}), executor) + }); + + shutdown_session.await.unwrap(); +} diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index d9f26b3b348985f2e52423cb217b1c1446960bbf..bd36b073872c38b20af0a6b6a95f06e0374fe2d0 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -442,10 +442,18 @@ impl DebugAdapter for FakeAdapter { _: Option<Vec<String>>, _: &mut AsyncApp, ) -> Result<DebugAdapterBinary> { + let connection = task_definition + .tcp_connection + .as_ref() + .map(|connection| TcpArguments { + host: connection.host(), + port: connection.port.unwrap_or(17), + timeout: connection.timeout, + }); Ok(DebugAdapterBinary { command: Some("command".into()), arguments: vec![], - connection: None, + connection, envs: HashMap::default(), cwd: None, request_args: StartDebuggingRequestArguments { diff --git a/crates/dap/src/client.rs b/crates/dap/src/client.rs index ff082e3b765b0baac294cf310a50b54534ae9bd1..86a15b2d8a9fa3ce00cdaa2536fb3decce948aec 100644 --- a/crates/dap/src/client.rs +++ b/crates/dap/src/client.rs @@ -2,7 +2,7 @@ use crate::{ adapters::DebugAdapterBinary, transport::{IoKind, LogKind, TransportDelegate}, }; -use anyhow::{Context as _, Result}; +use anyhow::Result; use dap_types::{ messages::{Message, Response}, requests::Request, @@ -110,9 +110,7 @@ impl DebugAdapterClient { self.transport_delegate .pending_requests .lock() - .as_mut() - .context("client is closed")? - .insert(sequence_id, callback_tx); + .insert(sequence_id, callback_tx)?; log::debug!( "Client {} send `{}` request with sequence_id: {}", @@ -170,6 +168,7 @@ impl DebugAdapterClient { pub fn kill(&self) { log::debug!("Killing DAP process"); self.transport_delegate.transport.lock().kill(); + self.transport_delegate.pending_requests.lock().shutdown(); } pub fn has_adapter_logs(&self) -> bool { @@ -184,11 +183,34 @@ impl DebugAdapterClient { } #[cfg(any(test, feature = "test-support"))] - pub fn on_request<R: dap_types::requests::Request, F>(&self, handler: F) + pub fn on_request<R: dap_types::requests::Request, F>(&self, mut handler: F) where F: 'static + Send + FnMut(u64, R::Arguments) -> Result<R::Response, dap_types::ErrorResponse>, + { + use crate::transport::RequestHandling; + + self.transport_delegate + .transport + .lock() + .as_fake() + .on_request::<R, _>(move |seq, request| { + RequestHandling::Respond(handler(seq, request)) + }); + } + + #[cfg(any(test, feature = "test-support"))] + pub fn on_request_ext<R: dap_types::requests::Request, F>(&self, handler: F) + where + F: 'static + + Send + + FnMut( + u64, + R::Arguments, + ) -> crate::transport::RequestHandling< + Result<R::Response, dap_types::ErrorResponse>, + >, { self.transport_delegate .transport diff --git a/crates/dap/src/transport.rs b/crates/dap/src/transport.rs index 14370f66e458309e3551a769bd79d29529a0cf3d..6dadf1cf35d0fc01f91b55a471e2be1e0c7b1be9 100644 --- a/crates/dap/src/transport.rs +++ b/crates/dap/src/transport.rs @@ -49,6 +49,12 @@ pub enum IoKind { StdErr, } +#[cfg(any(test, feature = "test-support"))] +pub enum RequestHandling<T> { + Respond(T), + Exit, +} + type LogHandlers = Arc<Mutex<SmallVec<[(LogKind, IoHandler); 2]>>>; pub trait Transport: Send + Sync { @@ -76,7 +82,11 @@ async fn start( ) -> Result<Box<dyn Transport>> { #[cfg(any(test, feature = "test-support"))] if cfg!(any(test, feature = "test-support")) { - return Ok(Box::new(FakeTransport::start(cx).await?)); + if let Some(connection) = binary.connection.clone() { + return Ok(Box::new(FakeTransport::start_tcp(connection, cx).await?)); + } else { + return Ok(Box::new(FakeTransport::start_stdio(cx).await?)); + } } if binary.connection.is_some() { @@ -90,11 +100,57 @@ async fn start( } } +pub(crate) struct PendingRequests { + inner: Option<HashMap<u64, oneshot::Sender<Result<Response>>>>, +} + +impl PendingRequests { + fn new() -> Self { + Self { + inner: Some(HashMap::default()), + } + } + + fn flush(&mut self, e: anyhow::Error) { + let Some(inner) = self.inner.as_mut() else { + return; + }; + for (_, sender) in inner.drain() { + sender.send(Err(e.cloned())).ok(); + } + } + + pub(crate) fn insert( + &mut self, + sequence_id: u64, + callback_tx: oneshot::Sender<Result<Response>>, + ) -> anyhow::Result<()> { + let Some(inner) = self.inner.as_mut() else { + bail!("client is closed") + }; + inner.insert(sequence_id, callback_tx); + Ok(()) + } + + pub(crate) fn remove( + &mut self, + sequence_id: u64, + ) -> anyhow::Result<Option<oneshot::Sender<Result<Response>>>> { + let Some(inner) = self.inner.as_mut() else { + bail!("client is closed"); + }; + Ok(inner.remove(&sequence_id)) + } + + pub(crate) fn shutdown(&mut self) { + self.flush(anyhow!("transport shutdown")); + self.inner = None; + } +} + pub(crate) struct TransportDelegate { log_handlers: LogHandlers, - // TODO this should really be some kind of associative channel - pub(crate) pending_requests: - Arc<Mutex<Option<HashMap<u64, oneshot::Sender<Result<Response>>>>>>, + pub(crate) pending_requests: Arc<Mutex<PendingRequests>>, pub(crate) transport: Mutex<Box<dyn Transport>>, pub(crate) server_tx: smol::lock::Mutex<Option<Sender<Message>>>, tasks: Mutex<Vec<Task<()>>>, @@ -108,7 +164,7 @@ impl TransportDelegate { transport: Mutex::new(transport), log_handlers, server_tx: Default::default(), - pending_requests: Arc::new(Mutex::new(Some(HashMap::default()))), + pending_requests: Arc::new(Mutex::new(PendingRequests::new())), tasks: Default::default(), }) } @@ -151,24 +207,10 @@ impl TransportDelegate { Ok(()) => { pending_requests .lock() - .take() - .into_iter() - .flatten() - .for_each(|(_, request)| { - request - .send(Err(anyhow!("debugger shutdown unexpectedly"))) - .ok(); - }); + .flush(anyhow!("debugger shutdown unexpectedly")); } Err(e) => { - pending_requests - .lock() - .take() - .into_iter() - .flatten() - .for_each(|(_, request)| { - request.send(Err(e.cloned())).ok(); - }); + pending_requests.lock().flush(e); } } })); @@ -286,7 +328,7 @@ impl TransportDelegate { async fn recv_from_server<Stdout>( server_stdout: Stdout, mut message_handler: DapMessageHandler, - pending_requests: Arc<Mutex<Option<HashMap<u64, oneshot::Sender<Result<Response>>>>>>, + pending_requests: Arc<Mutex<PendingRequests>>, log_handlers: Option<LogHandlers>, ) -> Result<()> where @@ -303,14 +345,10 @@ impl TransportDelegate { ConnectionResult::Timeout => anyhow::bail!("Timed out when connecting to debugger"), ConnectionResult::ConnectionReset => { log::info!("Debugger closed the connection"); - break Ok(()); + return Ok(()); } ConnectionResult::Result(Ok(Message::Response(res))) => { - let tx = pending_requests - .lock() - .as_mut() - .context("client is closed")? - .remove(&res.request_seq); + let tx = pending_requests.lock().remove(res.request_seq)?; if let Some(tx) = tx { if let Err(e) = tx.send(Self::process_response(res)) { log::trace!("Did not send response `{:?}` for a cancelled", e); @@ -704,8 +742,7 @@ impl Drop for StdioTransport { } #[cfg(any(test, feature = "test-support"))] -type RequestHandler = - Box<dyn Send + FnMut(u64, serde_json::Value) -> dap_types::messages::Response>; +type RequestHandler = Box<dyn Send + FnMut(u64, serde_json::Value) -> RequestHandling<Response>>; #[cfg(any(test, feature = "test-support"))] type ResponseHandler = Box<dyn Send + Fn(Response)>; @@ -716,23 +753,38 @@ pub struct FakeTransport { request_handlers: Arc<Mutex<HashMap<&'static str, RequestHandler>>>, // for reverse request responses response_handlers: Arc<Mutex<HashMap<&'static str, ResponseHandler>>>, - - stdin_writer: Option<PipeWriter>, - stdout_reader: Option<PipeReader>, message_handler: Option<Task<Result<()>>>, + kind: FakeTransportKind, +} + +#[cfg(any(test, feature = "test-support"))] +pub enum FakeTransportKind { + Stdio { + stdin_writer: Option<PipeWriter>, + stdout_reader: Option<PipeReader>, + }, + Tcp { + connection: TcpArguments, + executor: BackgroundExecutor, + }, } #[cfg(any(test, feature = "test-support"))] impl FakeTransport { pub fn on_request<R: dap_types::requests::Request, F>(&self, mut handler: F) where - F: 'static + Send + FnMut(u64, R::Arguments) -> Result<R::Response, ErrorResponse>, + F: 'static + + Send + + FnMut(u64, R::Arguments) -> RequestHandling<Result<R::Response, ErrorResponse>>, { self.request_handlers.lock().insert( R::COMMAND, Box::new(move |seq, args| { let result = handler(seq, serde_json::from_value(args).unwrap()); - let response = match result { + let RequestHandling::Respond(response) = result else { + return RequestHandling::Exit; + }; + let response = match response { Ok(response) => Response { seq: seq + 1, request_seq: seq, @@ -750,7 +802,7 @@ impl FakeTransport { message: None, }, }; - response + RequestHandling::Respond(response) }), ); } @@ -764,86 +816,75 @@ impl FakeTransport { .insert(R::COMMAND, Box::new(handler)); } - async fn start(cx: &mut AsyncApp) -> Result<Self> { - use dap_types::requests::{Request, RunInTerminal, StartDebugging}; - use serde_json::json; - - let (stdin_writer, stdin_reader) = async_pipe::pipe(); - let (stdout_writer, stdout_reader) = async_pipe::pipe(); - - let mut this = Self { + async fn start_tcp(connection: TcpArguments, cx: &mut AsyncApp) -> Result<Self> { + Ok(Self { request_handlers: Arc::new(Mutex::new(HashMap::default())), response_handlers: Arc::new(Mutex::new(HashMap::default())), - stdin_writer: Some(stdin_writer), - stdout_reader: Some(stdout_reader), message_handler: None, - }; + kind: FakeTransportKind::Tcp { + connection, + executor: cx.background_executor().clone(), + }, + }) + } - let request_handlers = this.request_handlers.clone(); - let response_handlers = this.response_handlers.clone(); + async fn handle_messages( + request_handlers: Arc<Mutex<HashMap<&'static str, RequestHandler>>>, + response_handlers: Arc<Mutex<HashMap<&'static str, ResponseHandler>>>, + stdin_reader: PipeReader, + stdout_writer: PipeWriter, + ) -> Result<()> { + use dap_types::requests::{Request, RunInTerminal, StartDebugging}; + use serde_json::json; + + let mut reader = BufReader::new(stdin_reader); let stdout_writer = Arc::new(smol::lock::Mutex::new(stdout_writer)); + let mut buffer = String::new(); - this.message_handler = Some(cx.background_spawn(async move { - let mut reader = BufReader::new(stdin_reader); - let mut buffer = String::new(); + loop { + match TransportDelegate::receive_server_message(&mut reader, &mut buffer, None).await { + ConnectionResult::Timeout => { + anyhow::bail!("Timed out when connecting to debugger"); + } + ConnectionResult::ConnectionReset => { + log::info!("Debugger closed the connection"); + break Ok(()); + } + ConnectionResult::Result(Err(e)) => break Err(e), + ConnectionResult::Result(Ok(message)) => { + match message { + Message::Request(request) => { + // redirect reverse requests to stdout writer/reader + if request.command == RunInTerminal::COMMAND + || request.command == StartDebugging::COMMAND + { + let message = + serde_json::to_string(&Message::Request(request)).unwrap(); - loop { - match TransportDelegate::receive_server_message(&mut reader, &mut buffer, None) - .await - { - ConnectionResult::Timeout => { - anyhow::bail!("Timed out when connecting to debugger"); - } - ConnectionResult::ConnectionReset => { - log::info!("Debugger closed the connection"); - break Ok(()); - } - ConnectionResult::Result(Err(e)) => break Err(e), - ConnectionResult::Result(Ok(message)) => { - match message { - Message::Request(request) => { - // redirect reverse requests to stdout writer/reader - if request.command == RunInTerminal::COMMAND - || request.command == StartDebugging::COMMAND + let mut writer = stdout_writer.lock().await; + writer + .write_all( + TransportDelegate::build_rpc_message(message).as_bytes(), + ) + .await + .unwrap(); + writer.flush().await.unwrap(); + } else { + let response = if let Some(handle) = + request_handlers.lock().get_mut(request.command.as_str()) { - let message = - serde_json::to_string(&Message::Request(request)).unwrap(); - - let mut writer = stdout_writer.lock().await; - writer - .write_all( - TransportDelegate::build_rpc_message(message) - .as_bytes(), - ) - .await - .unwrap(); - writer.flush().await.unwrap(); + handle(request.seq, request.arguments.unwrap_or(json!({}))) } else { - let response = if let Some(handle) = - request_handlers.lock().get_mut(request.command.as_str()) - { - handle(request.seq, request.arguments.unwrap_or(json!({}))) - } else { - panic!("No request handler for {}", request.command); - }; - let message = - serde_json::to_string(&Message::Response(response)) - .unwrap(); - - let mut writer = stdout_writer.lock().await; - writer - .write_all( - TransportDelegate::build_rpc_message(message) - .as_bytes(), - ) - .await - .unwrap(); - writer.flush().await.unwrap(); - } - } - Message::Event(event) => { + panic!("No request handler for {}", request.command); + }; + let response = match response { + RequestHandling::Respond(response) => response, + RequestHandling::Exit => { + break Err(anyhow!("exit in response to request")); + } + }; let message = - serde_json::to_string(&Message::Event(event)).unwrap(); + serde_json::to_string(&Message::Response(response)).unwrap(); let mut writer = stdout_writer.lock().await; writer @@ -854,20 +895,56 @@ impl FakeTransport { .unwrap(); writer.flush().await.unwrap(); } - Message::Response(response) => { - if let Some(handle) = - response_handlers.lock().get(response.command.as_str()) - { - handle(response); - } else { - log::error!("No response handler for {}", response.command); - } + } + Message::Event(event) => { + let message = serde_json::to_string(&Message::Event(event)).unwrap(); + + let mut writer = stdout_writer.lock().await; + writer + .write_all(TransportDelegate::build_rpc_message(message).as_bytes()) + .await + .unwrap(); + writer.flush().await.unwrap(); + } + Message::Response(response) => { + if let Some(handle) = + response_handlers.lock().get(response.command.as_str()) + { + handle(response); + } else { + log::error!("No response handler for {}", response.command); } } } } } - })); + } + } + + async fn start_stdio(cx: &mut AsyncApp) -> Result<Self> { + let (stdin_writer, stdin_reader) = async_pipe::pipe(); + let (stdout_writer, stdout_reader) = async_pipe::pipe(); + let kind = FakeTransportKind::Stdio { + stdin_writer: Some(stdin_writer), + stdout_reader: Some(stdout_reader), + }; + + let mut this = Self { + request_handlers: Arc::new(Mutex::new(HashMap::default())), + response_handlers: Arc::new(Mutex::new(HashMap::default())), + message_handler: None, + kind, + }; + + let request_handlers = this.request_handlers.clone(); + let response_handlers = this.response_handlers.clone(); + + this.message_handler = Some(cx.background_spawn(Self::handle_messages( + request_handlers, + response_handlers, + stdin_reader, + stdout_writer, + ))); Ok(this) } @@ -876,7 +953,10 @@ impl FakeTransport { #[cfg(any(test, feature = "test-support"))] impl Transport for FakeTransport { fn tcp_arguments(&self) -> Option<TcpArguments> { - None + match &self.kind { + FakeTransportKind::Stdio { .. } => None, + FakeTransportKind::Tcp { connection, .. } => Some(connection.clone()), + } } fn connect( @@ -887,12 +967,33 @@ impl Transport for FakeTransport { Box<dyn AsyncRead + Unpin + Send + 'static>, )>, > { - let result = util::maybe!({ - Ok(( - Box::new(self.stdin_writer.take().context("Cannot reconnect")?) as _, - Box::new(self.stdout_reader.take().context("Cannot reconnect")?) as _, - )) - }); + let result = match &mut self.kind { + FakeTransportKind::Stdio { + stdin_writer, + stdout_reader, + } => util::maybe!({ + Ok(( + Box::new(stdin_writer.take().context("Cannot reconnect")?) as _, + Box::new(stdout_reader.take().context("Cannot reconnect")?) as _, + )) + }), + FakeTransportKind::Tcp { executor, .. } => { + let (stdin_writer, stdin_reader) = async_pipe::pipe(); + let (stdout_writer, stdout_reader) = async_pipe::pipe(); + + let request_handlers = self.request_handlers.clone(); + let response_handlers = self.response_handlers.clone(); + + self.message_handler = Some(executor.spawn(Self::handle_messages( + request_handlers, + response_handlers, + stdin_reader, + stdout_writer, + ))); + + Ok((Box::new(stdin_writer) as _, Box::new(stdout_reader) as _)) + } + }; Task::ready(result) } diff --git a/crates/debugger_ui/src/session.rs b/crates/debugger_ui/src/session.rs index 2118249518ba0231dd854f5d23e25b8a2890e425..3c4c830b46cc27faa5f6dc6a4243041d094fe2e1 100644 --- a/crates/debugger_ui/src/session.rs +++ b/crates/debugger_ui/src/session.rs @@ -122,7 +122,7 @@ impl DebugSession { .to_owned() } - pub(crate) fn running_state(&self) -> &Entity<RunningState> { + pub fn running_state(&self) -> &Entity<RunningState> { &self.running_state } diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index af8c14aef77d0886071dfd899d8de5adff0d3ed6..d308fc9bd26b930e8b88e2ebaf194e8e30642238 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -1459,7 +1459,7 @@ impl RunningState { } } - pub(crate) fn selected_thread_id(&self) -> Option<ThreadId> { + pub fn selected_thread_id(&self) -> Option<ThreadId> { self.thread_id } From 13ddd5e4cb102c1901043de4e64222a2b69b3804 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov <kirill@zed.dev> Date: Sat, 12 Jul 2025 18:27:52 +0300 Subject: [PATCH 040/658] Return back the guards when goto targets are queried for (#34340) Closes https://github.com/zed-industries/zed/issues/34310 Follow-up of https://github.com/zed-industries/zed/pull/29359 Release Notes: - Fixed goto definition not working in remote projects in certain conditions --- crates/project/src/project.rs | 40 ++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 22ec8438a2b55f458b1ac0520f15fcabaf90087c..f9c59d2e95002d6d21d1eac9f8d2053838339e4d 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -3364,8 +3364,14 @@ impl Project { cx: &mut Context<Self>, ) -> Task<Result<Vec<LocationLink>>> { let position = position.to_point_utf16(buffer.read(cx)); - self.lsp_store.update(cx, |lsp_store, cx| { + let guard = self.retain_remotely_created_models(cx); + let task = self.lsp_store.update(cx, |lsp_store, cx| { lsp_store.definitions(buffer, position, cx) + }); + cx.spawn(async move |_, _| { + let result = task.await; + drop(guard); + result }) } @@ -3376,8 +3382,14 @@ impl Project { cx: &mut Context<Self>, ) -> Task<Result<Vec<LocationLink>>> { let position = position.to_point_utf16(buffer.read(cx)); - self.lsp_store.update(cx, |lsp_store, cx| { + let guard = self.retain_remotely_created_models(cx); + let task = self.lsp_store.update(cx, |lsp_store, cx| { lsp_store.declarations(buffer, position, cx) + }); + cx.spawn(async move |_, _| { + let result = task.await; + drop(guard); + result }) } @@ -3388,8 +3400,14 @@ impl Project { cx: &mut Context<Self>, ) -> Task<Result<Vec<LocationLink>>> { let position = position.to_point_utf16(buffer.read(cx)); - self.lsp_store.update(cx, |lsp_store, cx| { + let guard = self.retain_remotely_created_models(cx); + let task = self.lsp_store.update(cx, |lsp_store, cx| { lsp_store.type_definitions(buffer, position, cx) + }); + cx.spawn(async move |_, _| { + let result = task.await; + drop(guard); + result }) } @@ -3400,8 +3418,14 @@ impl Project { cx: &mut Context<Self>, ) -> Task<Result<Vec<LocationLink>>> { let position = position.to_point_utf16(buffer.read(cx)); - self.lsp_store.update(cx, |lsp_store, cx| { + let guard = self.retain_remotely_created_models(cx); + let task = self.lsp_store.update(cx, |lsp_store, cx| { lsp_store.implementations(buffer, position, cx) + }); + cx.spawn(async move |_, _| { + let result = task.await; + drop(guard); + result }) } @@ -3412,8 +3436,14 @@ impl Project { cx: &mut Context<Self>, ) -> Task<Result<Vec<Location>>> { let position = position.to_point_utf16(buffer.read(cx)); - self.lsp_store.update(cx, |lsp_store, cx| { + let guard = self.retain_remotely_created_models(cx); + let task = self.lsp_store.update(cx, |lsp_store, cx| { lsp_store.references(buffer, position, cx) + }); + cx.spawn(async move |_, _| { + let result = task.await; + drop(guard); + result }) } From a8cc92730363691c624f3a4671654c1e17f4f9ed Mon Sep 17 00:00:00 2001 From: Cole Miller <cole@zed.dev> Date: Sat, 12 Jul 2025 11:56:05 -0400 Subject: [PATCH 041/658] debugger: Improve appearance of session list for JavaScript debugging (#34322) This PR updates the debugger panel's session list to be more useful in some cases that are commonly hit when using the JavaScript adapter. We make two adjustments, which only apply to JavaScript sessions: - For a child session that's the only child of a root session, we collapse it with its parent. This imitates what VS Code does in the "call stack" view for JavaScript sessions. - When a session has exactly one thread, we label the session with that thread's name, instead of the session label provided by the DAP. VS Code also makes this adjustment, which surfaces more useful information when working with browser sessions. Closes #33072 Release Notes: - debugger: Improved the appearance of JavaScript sessions in the debug panel's session list. --------- Co-authored-by: Julia <julia@zed.dev> Co-authored-by: Remco Smits <djsmits12@gmail.com> --- .../src/activity_indicator.rs | 2 +- crates/dap/src/adapters.rs | 8 + crates/dap_adapters/src/javascript.rs | 8 + crates/debugger_tools/src/dap_log.rs | 3 +- crates/debugger_ui/src/debugger_panel.rs | 96 +++-- crates/debugger_ui/src/dropdown_menus.rs | 390 ++++++++++++------ crates/debugger_ui/src/session.rs | 86 ++-- .../debugger_ui/src/tests/debugger_panel.rs | 6 +- crates/project/src/debugger/dap_store.rs | 5 +- crates/project/src/debugger/session.rs | 19 +- crates/project_panel/src/project_panel.rs | 1 + 11 files changed, 390 insertions(+), 234 deletions(-) diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index b07c5418218c5045ae8018c9be9ec6fd07446544..aee25fc9e39d533409b980782fa8f0cac3977935 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -448,7 +448,7 @@ impl ActivityIndicator { .into_any_element(), ), message: format!("Debug: {}", session.read(cx).adapter()), - tooltip_message: Some(session.read(cx).label().to_string()), + tooltip_message: session.read(cx).label().map(|label| label.to_string()), on_click: None, }); } diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index bd36b073872c38b20af0a6b6a95f06e0374fe2d0..0c88f37ff8bfaad92ef5d6223b43c9bd6d91ad1d 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -378,6 +378,14 @@ pub trait DebugAdapter: 'static + Send + Sync { fn label_for_child_session(&self, _args: &StartDebuggingRequestArguments) -> Option<String> { None } + + fn compact_child_session(&self) -> bool { + false + } + + fn prefer_thread_name(&self) -> bool { + false + } } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index 76c1d1fb7bb3b2b3a534293957b43919a079a888..a51377cd76dd7ab1702c263378d0bf2904f27a6f 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -534,6 +534,14 @@ impl DebugAdapter for JsDebugAdapter { .filter(|name| !name.is_empty())?; Some(label.to_owned()) } + + fn compact_child_session(&self) -> bool { + true + } + + fn prefer_thread_name(&self) -> bool { + true + } } fn normalize_task_type(task_type: &mut Value) { diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index f53b6403b235ec43a141c67b6f7108316df18adb..b806381d251c6595a5dd12022dc3d1df8b71739f 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -399,7 +399,8 @@ impl LogStore { state.insert(DebugAdapterState::new( id.session_id, adapter_name, - session_label, + session_label + .unwrap_or_else(|| format!("Session {} (child)", id.session_id.0).into()), has_adapter_logs, )); diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index c90a2878e925ceb7d44a80c87bc8d9a7945b4fa0..184aedafc2dd10752158350e53f673a4d0122dbb 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -9,6 +9,7 @@ use crate::{ ToggleExpandItem, ToggleSessionPicker, ToggleThreadPicker, persistence, spawn_task_or_modal, }; use anyhow::{Context as _, Result, anyhow}; +use collections::IndexMap; use dap::adapters::DebugAdapterName; use dap::debugger_settings::DebugPanelDockPosition; use dap::{ @@ -26,7 +27,7 @@ use text::ToPoint as _; use itertools::Itertools as _; use language::Buffer; -use project::debugger::session::{Session, SessionStateEvent}; +use project::debugger::session::{Session, SessionQuirks, SessionStateEvent}; use project::{DebugScenarioContext, Fs, ProjectPath, TaskSourceKind, WorktreeId}; use project::{Project, debugger::session::ThreadStatus}; use rpc::proto::{self}; @@ -63,13 +64,14 @@ pub enum DebugPanelEvent { pub struct DebugPanel { size: Pixels, - sessions: Vec<Entity<DebugSession>>, active_session: Option<Entity<DebugSession>>, project: Entity<Project>, workspace: WeakEntity<Workspace>, focus_handle: FocusHandle, context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>, debug_scenario_scheduled_last: bool, + pub(crate) sessions_with_children: + IndexMap<Entity<DebugSession>, Vec<WeakEntity<DebugSession>>>, pub(crate) thread_picker_menu_handle: PopoverMenuHandle<ContextMenu>, pub(crate) session_picker_menu_handle: PopoverMenuHandle<ContextMenu>, fs: Arc<dyn Fs>, @@ -100,7 +102,7 @@ impl DebugPanel { Self { size: px(300.), - sessions: vec![], + sessions_with_children: Default::default(), active_session: None, focus_handle, breakpoint_list: BreakpointList::new( @@ -138,8 +140,9 @@ impl DebugPanel { }); } - pub(crate) fn sessions(&self) -> Vec<Entity<DebugSession>> { - self.sessions.clone() + #[cfg(test)] + pub(crate) fn sessions(&self) -> impl Iterator<Item = Entity<DebugSession>> { + self.sessions_with_children.keys().cloned() } pub fn active_session(&self) -> Option<Entity<DebugSession>> { @@ -185,12 +188,20 @@ impl DebugPanel { cx: &mut Context<Self>, ) { let dap_store = self.project.read(cx).dap_store(); + let Some(adapter) = DapRegistry::global(cx).adapter(&scenario.adapter) else { + return; + }; + let quirks = SessionQuirks { + compact: adapter.compact_child_session(), + prefer_thread_name: adapter.prefer_thread_name(), + }; let session = dap_store.update(cx, |dap_store, cx| { dap_store.new_session( - scenario.label.clone(), + Some(scenario.label.clone()), DebugAdapterName(scenario.adapter.clone()), task_context.clone(), None, + quirks, cx, ) }); @@ -363,14 +374,15 @@ impl DebugPanel { }; let dap_store_handle = self.project.read(cx).dap_store().clone(); - let label = curr_session.read(cx).label().clone(); + let label = curr_session.read(cx).label(); + let quirks = curr_session.read(cx).quirks(); let adapter = curr_session.read(cx).adapter().clone(); let binary = curr_session.read(cx).binary().cloned().unwrap(); let task_context = curr_session.read(cx).task_context().clone(); let curr_session_id = curr_session.read(cx).session_id(); - self.sessions - .retain(|session| session.read(cx).session_id(cx) != curr_session_id); + self.sessions_with_children + .retain(|session, _| session.read(cx).session_id(cx) != curr_session_id); let task = dap_store_handle.update(cx, |dap_store, cx| { dap_store.shutdown_session(curr_session_id, cx) }); @@ -379,7 +391,7 @@ impl DebugPanel { task.await.log_err(); let (session, task) = dap_store_handle.update(cx, |dap_store, cx| { - let session = dap_store.new_session(label, adapter, task_context, None, cx); + let session = dap_store.new_session(label, adapter, task_context, None, quirks, cx); let task = session.update(cx, |session, cx| { session.boot(binary, worktree, dap_store_handle.downgrade(), cx) @@ -425,6 +437,7 @@ impl DebugPanel { let dap_store_handle = self.project.read(cx).dap_store().clone(); let label = self.label_for_child_session(&parent_session, request, cx); let adapter = parent_session.read(cx).adapter().clone(); + 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"); return; @@ -438,6 +451,7 @@ impl DebugPanel { adapter, task_context, Some(parent_session.clone()), + quirks, cx, ); @@ -463,8 +477,8 @@ impl DebugPanel { cx: &mut Context<Self>, ) { let Some(session) = self - .sessions - .iter() + .sessions_with_children + .keys() .find(|other| entity_id == other.entity_id()) .cloned() else { @@ -498,15 +512,14 @@ impl DebugPanel { } session.update(cx, |session, cx| session.shutdown(cx)).ok(); this.update(cx, |this, cx| { - this.sessions.retain(|other| entity_id != other.entity_id()); - + this.retain_sessions(|other| entity_id != other.entity_id()); if let Some(active_session_id) = this .active_session .as_ref() .map(|session| session.entity_id()) { if active_session_id == entity_id { - this.active_session = this.sessions.first().cloned(); + this.active_session = this.sessions_with_children.keys().next().cloned(); } } cx.notify() @@ -976,8 +989,8 @@ impl DebugPanel { cx: &mut Context<Self>, ) { if let Some(session) = self - .sessions - .iter() + .sessions_with_children + .keys() .find(|session| session.read(cx).session_id(cx) == session_id) { self.activate_session(session.clone(), window, cx); @@ -990,7 +1003,7 @@ impl DebugPanel { window: &mut Window, cx: &mut Context<Self>, ) { - debug_assert!(self.sessions.contains(&session_item)); + debug_assert!(self.sessions_with_children.contains_key(&session_item)); session_item.focus_handle(cx).focus(window); session_item.update(cx, |this, cx| { this.running_state().update(cx, |this, cx| { @@ -1261,18 +1274,27 @@ impl DebugPanel { parent_session: &Entity<Session>, request: &StartDebuggingRequestArguments, cx: &mut Context<'_, Self>, - ) -> SharedString { + ) -> 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 label.into(); + return Some(label.into()); } } - let mut label = parent_session.read(cx).label().clone(); - if !label.ends_with("(child)") { - label = format!("{label} (child)").into(); + None + } + + fn retain_sessions(&mut self, keep: impl Fn(&Entity<DebugSession>) -> bool) { + self.sessions_with_children + .retain(|session, _| keep(session)); + for children in self.sessions_with_children.values_mut() { + children.retain(|child| { + let Some(child) = child.upgrade() else { + return false; + }; + keep(&child) + }); } - label } } @@ -1302,11 +1324,11 @@ async fn register_session_inner( let serialized_layout = persistence::get_serialized_layout(adapter_name).await; let debug_session = this.update_in(cx, |this, window, cx| { let parent_session = this - .sessions - .iter() + .sessions_with_children + .keys() .find(|p| Some(p.read(cx).session_id(cx)) == session.read(cx).parent_id(cx)) .cloned(); - this.sessions.retain(|session| { + this.retain_sessions(|session| { !session .read(cx) .running_state() @@ -1337,13 +1359,23 @@ async fn register_session_inner( ) .detach(); let insert_position = this - .sessions - .iter() + .sessions_with_children + .keys() .position(|session| Some(session) == parent_session.as_ref()) .map(|position| position + 1) - .unwrap_or(this.sessions.len()); + .unwrap_or(this.sessions_with_children.len()); // Maintain topological sort order of sessions - this.sessions.insert(insert_position, debug_session.clone()); + let (_, old) = this.sessions_with_children.insert_before( + insert_position, + debug_session.clone(), + Default::default(), + ); + debug_assert!(old.is_none()); + if let Some(parent_session) = parent_session { + this.sessions_with_children + .entry(parent_session) + .and_modify(|children| children.push(debug_session.downgrade())); + } debug_session })?; @@ -1383,7 +1415,7 @@ impl Panel for DebugPanel { cx: &mut Context<Self>, ) { if position.axis() != self.position(window, cx).axis() { - self.sessions.iter().for_each(|session_item| { + self.sessions_with_children.keys().for_each(|session_item| { session_item.update(cx, |item, cx| { item.running_state() .update(cx, |state, _| state.invert_axies()) diff --git a/crates/debugger_ui/src/dropdown_menus.rs b/crates/debugger_ui/src/dropdown_menus.rs index f93aceae094db9a75b9550021c97bb9723ad6811..dca15eb0527cfc78bd137889a1910e6b32abf98c 100644 --- a/crates/debugger_ui/src/dropdown_menus.rs +++ b/crates/debugger_ui/src/dropdown_menus.rs @@ -1,16 +1,82 @@ -use std::time::Duration; +use std::{rc::Rc, time::Duration}; use collections::HashMap; -use gpui::{Animation, AnimationExt as _, Entity, Transformation, percentage}; +use gpui::{Animation, AnimationExt as _, Entity, Transformation, WeakEntity, percentage}; use project::debugger::session::{ThreadId, ThreadStatus}; use ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*}; -use util::truncate_and_trailoff; +use util::{maybe, truncate_and_trailoff}; use crate::{ debugger_panel::DebugPanel, session::{DebugSession, running::RunningState}, }; +struct SessionListEntry { + ancestors: Vec<Entity<DebugSession>>, + leaf: Entity<DebugSession>, +} + +impl SessionListEntry { + pub(crate) fn label_element(&self, depth: usize, cx: &mut App) -> AnyElement { + const MAX_LABEL_CHARS: usize = 150; + + let mut label = String::new(); + for ancestor in &self.ancestors { + label.push_str(&ancestor.update(cx, |ancestor, cx| { + ancestor.label(cx).unwrap_or("(child)".into()) + })); + label.push_str(" » "); + } + label.push_str( + &self + .leaf + .update(cx, |leaf, cx| leaf.label(cx).unwrap_or("(child)".into())), + ); + let label = truncate_and_trailoff(&label, MAX_LABEL_CHARS); + + let is_terminated = self + .leaf + .read(cx) + .running_state + .read(cx) + .session() + .read(cx) + .is_terminated(); + let icon = { + if is_terminated { + Some(Indicator::dot().color(Color::Error)) + } else { + match self + .leaf + .read(cx) + .running_state + .read(cx) + .thread_status(cx) + .unwrap_or_default() + { + project::debugger::session::ThreadStatus::Stopped => { + Some(Indicator::dot().color(Color::Conflict)) + } + _ => Some(Indicator::dot().color(Color::Success)), + } + } + }; + + h_flex() + .id("session-label") + .ml(depth * px(16.0)) + .gap_2() + .when_some(icon, |this, indicator| this.child(indicator)) + .justify_between() + .child( + Label::new(label) + .size(LabelSize::Small) + .when(is_terminated, |this| this.strikethrough()), + ) + .into_any_element() + } +} + impl DebugPanel { fn dropdown_label(label: impl Into<SharedString>) -> Label { const MAX_LABEL_CHARS: usize = 50; @@ -25,145 +91,205 @@ impl DebugPanel { window: &mut Window, cx: &mut Context<Self>, ) -> Option<impl IntoElement> { - if let Some(running_state) = running_state { - let sessions = self.sessions().clone(); - let weak = cx.weak_entity(); - let running_state = running_state.read(cx); - let label = if let Some(active_session) = active_session.clone() { - active_session.read(cx).session(cx).read(cx).label() - } else { - SharedString::new_static("Unknown Session") - }; + let running_state = running_state?; + + let mut session_entries = Vec::with_capacity(self.sessions_with_children.len() * 3); + let mut sessions_with_children = self.sessions_with_children.iter().peekable(); - let is_terminated = running_state.session().read(cx).is_terminated(); - let is_started = active_session - .is_some_and(|session| session.read(cx).session(cx).read(cx).is_started()); - - let session_state_indicator = if is_terminated { - Indicator::dot().color(Color::Error).into_any_element() - } else if !is_started { - 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() + while let Some((root, children)) = sessions_with_children.next() { + let root_entry = if let Ok([single_child]) = <&[_; 1]>::try_from(children.as_slice()) + && let Some(single_child) = single_child.upgrade() + && single_child.read(cx).quirks.compact + { + sessions_with_children.next(); + SessionListEntry { + leaf: single_child.clone(), + ancestors: vec![root.clone()], + } } else { - match running_state.thread_status(cx).unwrap_or_default() { - ThreadStatus::Stopped => { - Indicator::dot().color(Color::Conflict).into_any_element() - } - _ => Indicator::dot().color(Color::Success).into_any_element(), + SessionListEntry { + leaf: root.clone(), + ancestors: Vec::new(), } }; + session_entries.push(root_entry); + + session_entries.extend( + sessions_with_children + .by_ref() + .take_while(|(session, _)| { + session + .read(cx) + .session(cx) + .read(cx) + .parent_id(cx) + .is_some() + }) + .map(|(session, _)| SessionListEntry { + leaf: session.clone(), + ancestors: vec![], + }), + ); + } - let trigger = h_flex() - .gap_2() - .child(session_state_indicator) - .justify_between() - .child( - DebugPanel::dropdown_label(label) - .when(is_terminated, |this| this.strikethrough()), + let weak = cx.weak_entity(); + let trigger_label = if let Some(active_session) = active_session.clone() { + active_session.update(cx, |active_session, cx| { + active_session.label(cx).unwrap_or("(child)".into()) + }) + } else { + SharedString::new_static("Unknown Session") + }; + let running_state = running_state.read(cx); + + let is_terminated = running_state.session().read(cx).is_terminated(); + let is_started = active_session + .is_some_and(|session| session.read(cx).session(cx).read(cx).is_started()); + + let session_state_indicator = if is_terminated { + Indicator::dot().color(Color::Error).into_any_element() + } else if !is_started { + 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() + } else { + match running_state.thread_status(cx).unwrap_or_default() { + ThreadStatus::Stopped => Indicator::dot().color(Color::Conflict).into_any_element(), + _ => Indicator::dot().color(Color::Success).into_any_element(), + } + }; - Some( - DropdownMenu::new_with_element( - "debugger-session-list", - trigger, - ContextMenu::build(window, cx, move |mut this, _, cx| { - let context_menu = cx.weak_entity(); - let mut session_depths = HashMap::default(); - for session in sessions.into_iter() { - let weak_session = session.downgrade(); - let weak_session_id = weak_session.entity_id(); - let session_id = session.read(cx).session_id(cx); - let parent_depth = session - .read(cx) - .session(cx) - .read(cx) - .parent_id(cx) - .and_then(|parent_id| session_depths.get(&parent_id).cloned()); - let self_depth = - *session_depths.entry(session_id).or_insert_with(|| { - parent_depth.map(|depth| depth + 1).unwrap_or(0usize) - }); - this = this.custom_entry( - { - let weak = weak.clone(); - let context_menu = context_menu.clone(); - move |_, cx| { - weak_session - .read_with(cx, |session, cx| { - let context_menu = context_menu.clone(); - - let id: SharedString = - format!("debug-session-{}", session_id.0) - .into(); - - h_flex() - .w_full() - .group(id.clone()) - .justify_between() - .child(session.label_element(self_depth, cx)) - .child( - IconButton::new( - "close-debug-session", - IconName::Close, - ) - .visible_on_hover(id.clone()) - .icon_size(IconSize::Small) - .on_click({ - let weak = weak.clone(); - move |_, window, cx| { - weak.update(cx, |panel, cx| { - panel.close_session( - weak_session_id, - window, - cx, - ); - }) - .ok(); - context_menu - .update(cx, |this, cx| { - this.cancel( - &Default::default(), - window, - cx, - ); - }) - .ok(); - } - }), - ) - .into_any_element() - }) - .unwrap_or_else(|_| div().into_any_element()) - } - }, - { - let weak = weak.clone(); - move |window, cx| { - weak.update(cx, |panel, cx| { - panel.activate_session(session.clone(), window, cx); - }) - .ok(); - } - }, - ); + let trigger = h_flex() + .gap_2() + .child(session_state_indicator) + .justify_between() + .child( + DebugPanel::dropdown_label(trigger_label) + .when(is_terminated, |this| this.strikethrough()), + ) + .into_any_element(); + + let menu = DropdownMenu::new_with_element( + "debugger-session-list", + trigger, + ContextMenu::build(window, cx, move |mut this, _, cx| { + let context_menu = cx.weak_entity(); + let mut session_depths = HashMap::default(); + for session_entry in session_entries { + let session_id = session_entry.leaf.read(cx).session_id(cx); + let parent_depth = session_entry + .ancestors + .first() + .unwrap_or(&session_entry.leaf) + .read(cx) + .session(cx) + .read(cx) + .parent_id(cx) + .and_then(|parent_id| session_depths.get(&parent_id).cloned()); + let self_depth = *session_depths + .entry(session_id) + .or_insert_with(|| parent_depth.map(|depth| depth + 1).unwrap_or(0usize)); + this = this.custom_entry( + { + let weak = weak.clone(); + let context_menu = context_menu.clone(); + let ancestors: Rc<[_]> = session_entry + .ancestors + .iter() + .map(|session| session.downgrade()) + .collect(); + let leaf = session_entry.leaf.downgrade(); + move |window, cx| { + Self::render_session_menu_entry( + weak.clone(), + context_menu.clone(), + ancestors.clone(), + leaf.clone(), + self_depth, + window, + cx, + ) + } + }, + { + let weak = weak.clone(); + let leaf = session_entry.leaf.clone(); + move |window, cx| { + weak.update(cx, |panel, cx| { + panel.activate_session(leaf.clone(), window, cx); + }) + .ok(); + } + }, + ); + } + this + }), + ) + .style(DropdownStyle::Ghost) + .handle(self.session_picker_menu_handle.clone()); + + Some(menu) + } + + fn render_session_menu_entry( + weak: WeakEntity<DebugPanel>, + context_menu: WeakEntity<ContextMenu>, + ancestors: Rc<[WeakEntity<DebugSession>]>, + leaf: WeakEntity<DebugSession>, + self_depth: usize, + _window: &mut Window, + cx: &mut App, + ) -> AnyElement { + let Some(session_entry) = maybe!({ + let ancestors = ancestors + .iter() + .map(|ancestor| ancestor.upgrade()) + .collect::<Option<Vec<_>>>()?; + let leaf = leaf.upgrade()?; + Some(SessionListEntry { ancestors, leaf }) + }) else { + return div().into_any_element(); + }; + + let id: SharedString = format!( + "debug-session-{}", + session_entry.leaf.read(cx).session_id(cx).0 + ) + .into(); + let session_entity_id = session_entry.leaf.entity_id(); + + h_flex() + .w_full() + .group(id.clone()) + .justify_between() + .child(session_entry.label_element(self_depth, cx)) + .child( + IconButton::new("close-debug-session", IconName::Close) + .visible_on_hover(id.clone()) + .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); + }) + .ok(); + context_menu + .update(cx, |this, cx| { + this.cancel(&Default::default(), window, cx); + }) + .ok(); } - this }), - ) - .style(DropdownStyle::Ghost) - .handle(self.session_picker_menu_handle.clone()), ) - } else { - None - } + .into_any_element() } pub(crate) fn render_thread_dropdown( diff --git a/crates/debugger_ui/src/session.rs b/crates/debugger_ui/src/session.rs index 3c4c830b46cc27faa5f6dc6a4243041d094fe2e1..73cfef78cc6410196441ff974f09b5abe3d86916 100644 --- a/crates/debugger_ui/src/session.rs +++ b/crates/debugger_ui/src/session.rs @@ -5,14 +5,13 @@ use dap::client::SessionId; use gpui::{ App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, }; -use project::Project; use project::debugger::session::Session; use project::worktree_store::WorktreeStore; +use project::{Project, debugger::session::SessionQuirks}; use rpc::proto; use running::RunningState; -use std::{cell::OnceCell, sync::OnceLock}; -use ui::{Indicator, prelude::*}; -use util::truncate_and_trailoff; +use std::cell::OnceCell; +use ui::prelude::*; use workspace::{ CollaboratorId, FollowableItem, ViewId, Workspace, item::{self, Item}, @@ -20,8 +19,8 @@ use workspace::{ pub struct DebugSession { remote_id: Option<workspace::ViewId>, - running_state: Entity<RunningState>, - label: OnceLock<SharedString>, + pub(crate) running_state: Entity<RunningState>, + pub(crate) quirks: SessionQuirks, stack_trace_view: OnceCell<Entity<StackTraceView>>, _worktree_store: WeakEntity<WorktreeStore>, workspace: WeakEntity<Workspace>, @@ -57,6 +56,7 @@ impl DebugSession { cx, ) }); + let quirks = session.read(cx).quirks(); cx.new(|cx| Self { _subscriptions: [cx.subscribe(&running_state, |_, _, _, cx| { @@ -64,7 +64,7 @@ impl DebugSession { })], remote_id: None, running_state, - label: OnceLock::new(), + quirks, stack_trace_view: OnceCell::new(), _worktree_store: project.read(cx).worktree_store().downgrade(), workspace, @@ -110,65 +110,29 @@ impl DebugSession { .update(cx, |state, cx| state.shutdown(cx)); } - pub(crate) fn label(&self, cx: &App) -> SharedString { - if let Some(label) = self.label.get() { - return label.clone(); - } - - let session = self.running_state.read(cx).session(); - - self.label - .get_or_init(|| session.read(cx).label()) - .to_owned() + pub(crate) fn label(&self, cx: &mut App) -> Option<SharedString> { + let session = self.running_state.read(cx).session().clone(); + session.update(cx, |session, cx| { + let session_label = session.label(); + let quirks = session.quirks(); + let mut single_thread_name = || { + let threads = session.threads(cx); + match threads.as_slice() { + [(thread, _)] => Some(SharedString::from(&thread.name)), + _ => None, + } + }; + if quirks.prefer_thread_name { + single_thread_name().or(session_label) + } else { + session_label.or_else(single_thread_name) + } + }) } pub fn running_state(&self) -> &Entity<RunningState> { &self.running_state } - - pub(crate) fn label_element(&self, depth: usize, cx: &App) -> AnyElement { - const MAX_LABEL_CHARS: usize = 150; - - let label = self.label(cx); - let label = truncate_and_trailoff(&label, MAX_LABEL_CHARS); - - let is_terminated = self - .running_state - .read(cx) - .session() - .read(cx) - .is_terminated(); - let icon = { - if is_terminated { - Some(Indicator::dot().color(Color::Error)) - } else { - match self - .running_state - .read(cx) - .thread_status(cx) - .unwrap_or_default() - { - project::debugger::session::ThreadStatus::Stopped => { - Some(Indicator::dot().color(Color::Conflict)) - } - _ => Some(Indicator::dot().color(Color::Success)), - } - } - }; - - h_flex() - .id("session-label") - .ml(depth * px(16.0)) - .gap_2() - .when_some(icon, |this, indicator| this.child(indicator)) - .justify_between() - .child( - Label::new(label) - .size(LabelSize::Small) - .when(is_terminated, |this| this.strikethrough()), - ) - .into_any_element() - } } impl EventEmitter<DebugPanelItemEvent> for DebugSession {} diff --git a/crates/debugger_ui/src/tests/debugger_panel.rs b/crates/debugger_ui/src/tests/debugger_panel.rs index 05bca8131ac9734b1635a90c22026424f1c5cf2e..505df09cfb2b47821cb59448801f014f923be8f1 100644 --- a/crates/debugger_ui/src/tests/debugger_panel.rs +++ b/crates/debugger_ui/src/tests/debugger_panel.rs @@ -427,7 +427,7 @@ async fn test_handle_start_debugging_request( let sessions = workspace .update(cx, |workspace, _window, cx| { let debug_panel = workspace.panel::<DebugPanel>(cx).unwrap(); - debug_panel.read(cx).sessions() + debug_panel.read(cx).sessions().collect::<Vec<_>>() }) .unwrap(); assert_eq!(sessions.len(), 1); @@ -451,7 +451,7 @@ async fn test_handle_start_debugging_request( .unwrap() .read(cx) .session(cx); - let current_sessions = debug_panel.read(cx).sessions(); + let current_sessions = debug_panel.read(cx).sessions().collect::<Vec<_>>(); assert_eq!(active_session, current_sessions[1].read(cx).session(cx)); assert_eq!( active_session.read(cx).parent_session(), @@ -1796,7 +1796,7 @@ async fn test_debug_adapters_shutdown_on_app_quit( let panel = workspace.panel::<DebugPanel>(cx).unwrap(); panel.read_with(cx, |panel, _| { assert!( - !panel.sessions().is_empty(), + panel.sessions().next().is_some(), "Debug session should be active" ); }); diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs index f4f4b50dab696f257978a35593e090d4efdff97e..d494088b1379dc3820260ae99a98582cc24db319 100644 --- a/crates/project/src/debugger/dap_store.rs +++ b/crates/project/src/debugger/dap_store.rs @@ -6,6 +6,7 @@ use super::{ }; use crate::{ InlayHint, InlayHintLabel, ProjectEnvironment, ResolveState, + debugger::session::SessionQuirks, project_settings::ProjectSettings, terminals::{SshCommand, wrap_for_ssh}, worktree_store::WorktreeStore, @@ -385,10 +386,11 @@ impl DapStore { pub fn new_session( &mut self, - label: SharedString, + label: Option<SharedString>, adapter: DebugAdapterName, task_context: TaskContext, parent_session: Option<Entity<Session>>, + quirks: SessionQuirks, cx: &mut Context<Self>, ) -> Entity<Session> { let session_id = SessionId(util::post_inc(&mut self.next_session_id)); @@ -406,6 +408,7 @@ impl DapStore { label, adapter, task_context, + quirks, cx, ); diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 59c35da4cac4328dc109b8463ef02868b4885d63..59feb504c55c05af031742b3d97c203c465b9f93 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -151,6 +151,12 @@ pub struct RunningMode { messages_tx: UnboundedSender<Message>, } +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +pub struct SessionQuirks { + pub compact: bool, + pub prefer_thread_name: bool, +} + fn client_source(abs_path: &Path) -> dap::Source { dap::Source { name: abs_path @@ -656,7 +662,7 @@ pub struct OutputToken(pub usize); pub struct Session { pub mode: Mode, id: SessionId, - label: SharedString, + label: Option<SharedString>, adapter: DebugAdapterName, pub(super) capabilities: Capabilities, child_session_ids: HashSet<SessionId>, @@ -679,6 +685,7 @@ pub struct Session { background_tasks: Vec<Task<()>>, restart_task: Option<Task<()>>, task_context: TaskContext, + quirks: SessionQuirks, } trait CacheableCommand: Any + Send + Sync { @@ -792,9 +799,10 @@ impl Session { breakpoint_store: Entity<BreakpointStore>, session_id: SessionId, parent_session: Option<Entity<Session>>, - label: SharedString, + label: Option<SharedString>, adapter: DebugAdapterName, task_context: TaskContext, + quirks: SessionQuirks, cx: &mut App, ) -> Entity<Self> { cx.new::<Self>(|cx| { @@ -848,6 +856,7 @@ impl Session { label, adapter, task_context, + quirks, }; this @@ -1022,7 +1031,7 @@ impl Session { self.adapter.clone() } - pub fn label(&self) -> SharedString { + pub fn label(&self) -> Option<SharedString> { self.label.clone() } @@ -2481,4 +2490,8 @@ impl Session { pub fn thread_state(&self, thread_id: ThreadId) -> Option<ThreadStatus> { self.thread_states.thread_state(thread_id) } + + pub fn quirks(&self) -> SessionQuirks { + self.quirks + } } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 0ec9bac33f89c81527e520322555d6d1071273b4..15869fc5acc4d1fac8777cf957ed7676f4f1a426 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -4301,6 +4301,7 @@ impl ProjectPanel { .collect::<Vec<_>>(); let components_len = components.len(); + // TODO this can underflow let active_index = components_len - 1 - folded_ancestors.current_ancestor_depth; From 833bc6979aaecaf091dcd834874d9f329856ce5a Mon Sep 17 00:00:00 2001 From: Remco Smits <djsmits12@gmail.com> Date: Sat, 12 Jul 2025 20:24:49 +0200 Subject: [PATCH 042/658] debugger: Fix correctly determine replace range for debug console completions (#33959) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up #33868 This PR fixes a few issues with determining the completion range for client‑ and variable‑list completions. 1. Non‑word completions We previously supported only word characters and _, using their combined length to compute the start offset. In PHP, however, an expression can contain `$`, `-`, `>`, `[`, `]`, `(`, and `)`. Because these characters weren’t treated as word characters, the start offset stopped at them, even when the preceding character was part of a word. 2. Trailing characters inside the search text When autocompletion occurred in the middle of the search text, we didn’t account for trailing characters. As a result, the start offset was off by the number of characters after the cursor. For example, replacing res with result in print(res) produced `print(rresult)` because the trailing `)` wasn’t subtracted from the start offset. The following completions are correctly covered now: - **Before** `$aut` -> `$aut$author` **After** `$aut` -> `$author` - **Before** `$author->na` -> `$author->na$author->name` **After** `$author->na` -> `$author->name` - **Before** `$author->books[` -> `$author->books[$author->books[0]` **After** `$author->books[` -> `$author->books[0]` - **Before** `print(res)` -> `print(rresult)` **After** `print(res)` -> `print(result)` **Before** https://github.com/user-attachments/assets/b530cf31-8d4d-45e6-9650-18574f14314c https://github.com/user-attachments/assets/52475b7b-2bf2-4749-98ec-0dc933fcc364 **After** https://github.com/user-attachments/assets/c065701b-31c9-4e0a-b584-d1daffe3a38c https://github.com/user-attachments/assets/455ebb3e-632e-4a57-aea8-d214d2992c06 Release Notes: - Debugger: Fixed autocompletion not always replacing the correct search text --- .../src/session/running/console.rs | 138 +++++++++++++----- 1 file changed, 101 insertions(+), 37 deletions(-) diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index 9375c8820b0eb335f1d36534f219f339ec587df1..1385bec54ef77222485cd642174d50aa60fa289a 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -12,7 +12,7 @@ use gpui::{ Action as _, AppContext, Context, Corner, Entity, FocusHandle, Focusable, HighlightStyle, Hsla, Render, Subscription, Task, TextStyle, WeakEntity, actions, }; -use language::{Buffer, CodeLabel, ToOffset}; +use language::{Anchor, Buffer, CodeLabel, TextBufferSnapshot, ToOffset}; use menu::{Confirm, SelectNext, SelectPrevious}; use project::{ Completion, CompletionResponse, @@ -637,27 +637,13 @@ impl ConsoleQueryBarCompletionProvider { }); let snapshot = buffer.read(cx).text_snapshot(); - let query = snapshot.text(); - let replace_range = { - let buffer_offset = buffer_position.to_offset(&snapshot); - let reversed_chars = snapshot.reversed_chars_for_range(0..buffer_offset); - let mut word_len = 0; - for ch in reversed_chars { - if ch.is_alphanumeric() || ch == '_' { - word_len += 1; - } else { - break; - } - } - let word_start_offset = buffer_offset - word_len; - let start_anchor = snapshot.anchor_at(word_start_offset, Bias::Left); - start_anchor..buffer_position - }; + let buffer_text = snapshot.text(); + cx.spawn(async move |_, cx| { const LIMIT: usize = 10; let matches = fuzzy::match_strings( &string_matches, - &query, + &buffer_text, true, true, LIMIT, @@ -672,7 +658,12 @@ impl ConsoleQueryBarCompletionProvider { let variable_value = variables.get(&string_match.string)?; Some(project::Completion { - replace_range: replace_range.clone(), + replace_range: Self::replace_range_for_completion( + &buffer_text, + buffer_position, + string_match.string.as_bytes(), + &snapshot, + ), new_text: string_match.string.clone(), label: CodeLabel { filter_range: 0..string_match.string.len(), @@ -697,6 +688,28 @@ impl ConsoleQueryBarCompletionProvider { }) } + fn replace_range_for_completion( + buffer_text: &String, + buffer_position: Anchor, + new_bytes: &[u8], + snapshot: &TextBufferSnapshot, + ) -> Range<Anchor> { + let buffer_offset = buffer_position.to_offset(&snapshot); + let buffer_bytes = &buffer_text.as_bytes()[0..buffer_offset]; + + let mut prefix_len = 0; + for i in (0..new_bytes.len()).rev() { + if buffer_bytes.ends_with(&new_bytes[0..i]) { + prefix_len = i; + break; + } + } + + let start = snapshot.clip_offset(buffer_offset - prefix_len, Bias::Left); + + snapshot.anchor_before(start)..buffer_position + } + const fn completion_type_score(completion_type: CompletionItemType) -> usize { match completion_type { CompletionItemType::Field | CompletionItemType::Property => 0, @@ -744,6 +757,8 @@ impl ConsoleQueryBarCompletionProvider { cx.background_executor().spawn(async move { let completions = completion_task.await?; + let buffer_text = snapshot.text(); + let completions = completions .into_iter() .map(|completion| { @@ -753,26 +768,14 @@ impl ConsoleQueryBarCompletionProvider { .as_ref() .unwrap_or(&completion.label) .to_owned(); - let buffer_text = snapshot.text(); - let buffer_bytes = buffer_text.as_bytes(); - let new_bytes = new_text.as_bytes(); - - let mut prefix_len = 0; - for i in (0..new_bytes.len()).rev() { - if buffer_bytes.ends_with(&new_bytes[0..i]) { - prefix_len = i; - break; - } - } - - let buffer_offset = buffer_position.to_offset(&snapshot); - let start = buffer_offset - prefix_len; - let start = snapshot.clip_offset(start, Bias::Left); - let start = snapshot.anchor_before(start); - let replace_range = start..buffer_position; project::Completion { - replace_range, + replace_range: Self::replace_range_for_completion( + &buffer_text, + buffer_position, + new_text.as_bytes(), + &snapshot, + ), new_text, label: CodeLabel { filter_range: 0..completion.label.len(), @@ -944,3 +947,64 @@ fn color_fetcher(color: ansi::Color) -> fn(&Theme) -> Hsla { }; color_fetcher } + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::init_test; + use editor::test::editor_test_context::EditorTestContext; + use gpui::TestAppContext; + use language::Point; + + #[track_caller] + fn assert_completion_range( + input: &str, + expect: &str, + replacement: &str, + cx: &mut EditorTestContext, + ) { + cx.set_state(input); + + let buffer_position = + cx.editor(|editor, _, cx| editor.selections.newest::<Point>(cx).start); + + let snapshot = &cx.buffer_snapshot(); + + let replace_range = ConsoleQueryBarCompletionProvider::replace_range_for_completion( + &cx.buffer_text(), + snapshot.anchor_before(buffer_position), + replacement.as_bytes(), + &snapshot, + ); + + cx.update_editor(|editor, _, cx| { + editor.edit( + vec![( + snapshot.offset_for_anchor(&replace_range.start) + ..snapshot.offset_for_anchor(&replace_range.end), + replacement, + )], + cx, + ); + }); + + pretty_assertions::assert_eq!(expect, cx.display_text()); + } + + #[gpui::test] + async fn test_determine_completion_replace_range(cx: &mut TestAppContext) { + init_test(cx); + + let mut cx = EditorTestContext::new(cx).await; + + assert_completion_range("resˇ", "result", "result", &mut cx); + assert_completion_range("print(resˇ)", "print(result)", "result", &mut cx); + assert_completion_range("$author->nˇ", "$author->name", "$author->name", &mut cx); + assert_completion_range( + "$author->books[ˇ", + "$author->books[0]", + "$author->books[0]", + &mut cx, + ); + } +} From 970a1066f59bece8bba552431fc86675bcc7e72b Mon Sep 17 00:00:00 2001 From: Cole Miller <cole@zed.dev> Date: Sat, 12 Jul 2025 15:04:26 -0400 Subject: [PATCH 043/658] git: Handle shift-click to stage a range of entries in the panel (#34296) Release Notes: - git: shift-click can now be used to stage a range of entries in the git panel. --- assets/keymaps/default-linux.json | 1 + assets/keymaps/default-macos.json | 1 + assets/keymaps/vim.json | 1 + crates/fs/src/fake_git_repo.rs | 40 ++- crates/git/src/git.rs | 4 +- crates/git_ui/src/git_panel.rs | 456 ++++++++++++++++++++--------- crates/ui/src/components/toggle.rs | 19 +- 7 files changed, 373 insertions(+), 149 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index c660383d10bfa5e206e812303269fd8c28ddc076..4022bea34ba07c415e4a9620cef1a23a8816de04 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -856,6 +856,7 @@ "alt-shift-y": "git::UnstageFile", "ctrl-alt-y": "git::ToggleStaged", "space": "git::ToggleStaged", + "shift-space": "git::StageRange", "tab": "git_panel::FocusEditor", "shift-tab": "git_panel::FocusEditor", "escape": "git_panel::ToggleFocus", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index dc109d94aa012d886da4cec1a70d5d3995e05f93..ffe8a344b30884b3911c9efb0ed2afc72ba1182b 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -930,6 +930,7 @@ "enter": "menu::Confirm", "cmd-alt-y": "git::ToggleStaged", "space": "git::ToggleStaged", + "shift-space": "git::StageRange", "cmd-y": "git::StageFile", "cmd-shift-y": "git::UnstageFile", "alt-down": "git_panel::FocusEditor", diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 571192a4791846011318238ade9aad84091bca4d..5437be39baa39326a4a54ade4f0bd2096e089566 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -841,6 +841,7 @@ "i": "git_panel::FocusEditor", "x": "git::ToggleStaged", "shift-x": "git::StageAll", + "g x": "git::StageRange", "shift-u": "git::UnstageAll" } }, diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index 40a292e0401df931cc17d04ed71219917292ab1f..8a4f7c03bb84369061f60b9807fdb87df296a342 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -1,7 +1,7 @@ -use crate::FakeFs; +use crate::{FakeFs, Fs}; use anyhow::{Context as _, Result}; use collections::{HashMap, HashSet}; -use futures::future::{self, BoxFuture}; +use futures::future::{self, BoxFuture, join_all}; use git::{ blame::Blame, repository::{ @@ -356,18 +356,46 @@ impl GitRepository for FakeGitRepository { fn stage_paths( &self, - _paths: Vec<RepoPath>, + paths: Vec<RepoPath>, _env: Arc<HashMap<String, String>>, ) -> BoxFuture<'_, Result<()>> { - unimplemented!() + Box::pin(async move { + let contents = paths + .into_iter() + .map(|path| { + let abs_path = self.dot_git_path.parent().unwrap().join(&path); + Box::pin(async move { (path.clone(), self.fs.load(&abs_path).await.ok()) }) + }) + .collect::<Vec<_>>(); + let contents = join_all(contents).await; + self.with_state_async(true, move |state| { + for (path, content) in contents { + if let Some(content) = content { + state.index_contents.insert(path, content); + } else { + state.index_contents.remove(&path); + } + } + Ok(()) + }) + .await + }) } fn unstage_paths( &self, - _paths: Vec<RepoPath>, + paths: Vec<RepoPath>, _env: Arc<HashMap<String, String>>, ) -> BoxFuture<'_, Result<()>> { - unimplemented!() + self.with_state_async(true, move |state| { + for path in paths { + match state.head_contents.get(&path) { + Some(content) => state.index_contents.insert(path, content.clone()), + None => state.index_contents.remove(&path), + }; + } + Ok(()) + }) } fn commit( diff --git a/crates/git/src/git.rs b/crates/git/src/git.rs index 92cf58b2adafc692d8407982247d82f03d57fd78..fccedaa80989a91dab1a9f53804cba7072ed65d4 100644 --- a/crates/git/src/git.rs +++ b/crates/git/src/git.rs @@ -31,8 +31,10 @@ actions!( git, [ // per-hunk - /// Toggles the staged state of the hunk at cursor. + /// Toggles the staged state of the hunk or status entry at cursor. ToggleStaged, + /// Stage status entries between an anchor entry and the cursor. + StageRange, /// Stages the current hunk and moves to the next one. StageAndNext, /// Unstages the current hunk and moves to the next one. diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index c50e2f8912ef5b4570a7141378f55701151f3f71..52bed2cc793944ac5f849786fa998c43fa420321 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -30,10 +30,9 @@ use git::{ExpandCommitEditor, RestoreTrackedFiles, StageAll, TrashUntrackedFiles use gpui::{ Action, Animation, AnimationExt as _, AsyncApp, AsyncWindowContext, Axis, ClickEvent, Corner, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext, - ListHorizontalSizingBehavior, ListSizingBehavior, Modifiers, ModifiersChangedEvent, - MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy, Subscription, Task, - Transformation, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, percentage, - uniform_list, + ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton, MouseDownEvent, Point, + PromptLevel, ScrollStrategy, Subscription, Task, Transformation, UniformListScrollHandle, + WeakEntity, actions, anchored, deferred, percentage, uniform_list, }; use itertools::Itertools; use language::{Buffer, File}; @@ -48,7 +47,7 @@ use panel::{ PanelHeader, panel_button, panel_editor_container, panel_editor_style, panel_filled_button, panel_icon_button, }; -use project::git_store::RepositoryEvent; +use project::git_store::{RepositoryEvent, RepositoryId}; use project::{ Fs, Project, ProjectPath, git_store::{GitStoreEvent, Repository}, @@ -212,14 +211,14 @@ impl GitHeaderEntry { #[derive(Debug, PartialEq, Eq, Clone)] enum GitListEntry { - GitStatusEntry(GitStatusEntry), + Status(GitStatusEntry), Header(GitHeaderEntry), } impl GitListEntry { fn status_entry(&self) -> Option<&GitStatusEntry> { match self { - GitListEntry::GitStatusEntry(entry) => Some(entry), + GitListEntry::Status(entry) => Some(entry), _ => None, } } @@ -323,7 +322,6 @@ pub struct GitPanel { pub(crate) commit_editor: Entity<Editor>, conflicted_count: usize, conflicted_staged_count: usize, - current_modifiers: Modifiers, add_coauthors: bool, generate_commit_message_task: Option<Task<Option<()>>>, entries: Vec<GitListEntry>, @@ -355,9 +353,16 @@ pub struct GitPanel { show_placeholders: bool, local_committer: Option<GitCommitter>, local_committer_task: Option<Task<()>>, + bulk_staging: Option<BulkStaging>, _settings_subscription: Subscription, } +#[derive(Clone, Debug, PartialEq, Eq)] +struct BulkStaging { + repo_id: RepositoryId, + anchor: RepoPath, +} + const MAX_PANEL_EDITOR_LINES: usize = 6; pub(crate) fn commit_message_editor( @@ -497,7 +502,6 @@ impl GitPanel { commit_editor, conflicted_count: 0, conflicted_staged_count: 0, - current_modifiers: window.modifiers(), add_coauthors: true, generate_commit_message_task: None, entries: Vec::new(), @@ -529,6 +533,7 @@ impl GitPanel { entry_count: 0, horizontal_scrollbar, vertical_scrollbar, + bulk_staging: None, _settings_subscription, }; @@ -735,16 +740,6 @@ impl GitPanel { } } - fn handle_modifiers_changed( - &mut self, - event: &ModifiersChangedEvent, - _: &mut Window, - cx: &mut Context<Self>, - ) { - self.current_modifiers = event.modifiers; - cx.notify(); - } - fn scroll_to_selected_entry(&mut self, cx: &mut Context<Self>) { if let Some(selected_entry) = self.selected_entry { self.scroll_handle @@ -1265,10 +1260,18 @@ impl GitPanel { return; }; let (stage, repo_paths) = match entry { - GitListEntry::GitStatusEntry(status_entry) => { + GitListEntry::Status(status_entry) => { if status_entry.status.staging().is_fully_staged() { + if let Some(op) = self.bulk_staging.clone() + && op.anchor == status_entry.repo_path + { + self.bulk_staging = None; + } + (false, vec![status_entry.clone()]) } else { + self.set_bulk_staging_anchor(status_entry.repo_path.clone(), cx); + (true, vec![status_entry.clone()]) } } @@ -1383,6 +1386,13 @@ impl GitPanel { } } + fn stage_range(&mut self, _: &git::StageRange, _window: &mut Window, cx: &mut Context<Self>) { + let Some(index) = self.selected_entry else { + return; + }; + self.stage_bulk(index, cx); + } + fn stage_selected(&mut self, _: &git::StageFile, _window: &mut Window, cx: &mut Context<Self>) { let Some(selected_entry) = self.get_selected_entry() else { return; @@ -2449,6 +2459,11 @@ impl GitPanel { } fn update_visible_entries(&mut self, cx: &mut Context<Self>) { + let bulk_staging = self.bulk_staging.take(); + let last_staged_path_prev_index = bulk_staging + .as_ref() + .and_then(|op| self.entry_by_path(&op.anchor, cx)); + self.entries.clear(); self.single_staged_entry.take(); self.single_tracked_entry.take(); @@ -2465,7 +2480,7 @@ impl GitPanel { let mut changed_entries = Vec::new(); let mut new_entries = Vec::new(); let mut conflict_entries = Vec::new(); - let mut last_staged = None; + let mut single_staged_entry = None; let mut staged_count = 0; let mut max_width_item: Option<(RepoPath, usize)> = None; @@ -2503,7 +2518,7 @@ impl GitPanel { if staging.has_staged() { staged_count += 1; - last_staged = Some(entry.clone()); + single_staged_entry = Some(entry.clone()); } let width_estimate = Self::item_width_estimate( @@ -2534,27 +2549,27 @@ impl GitPanel { let mut pending_staged_count = 0; let mut last_pending_staged = None; - let mut pending_status_for_last_staged = None; + let mut pending_status_for_single_staged = None; 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(); } - if let Some(last_staged) = &last_staged { + if let Some(single_staged) = &single_staged_entry { if pending .entries .iter() - .any(|entry| entry.repo_path == last_staged.repo_path) + .any(|entry| entry.repo_path == single_staged.repo_path) { - pending_status_for_last_staged = Some(pending.target_status); + pending_status_for_single_staged = Some(pending.target_status); } } } if conflict_entries.len() == 0 && staged_count == 1 && pending_staged_count == 0 { - match pending_status_for_last_staged { + match pending_status_for_single_staged { Some(TargetStatus::Staged) | None => { - self.single_staged_entry = last_staged; + self.single_staged_entry = single_staged_entry; } _ => {} } @@ -2570,11 +2585,8 @@ impl GitPanel { self.entries.push(GitListEntry::Header(GitHeaderEntry { header: Section::Conflict, })); - self.entries.extend( - conflict_entries - .into_iter() - .map(GitListEntry::GitStatusEntry), - ); + self.entries + .extend(conflict_entries.into_iter().map(GitListEntry::Status)); } if changed_entries.len() > 0 { @@ -2583,31 +2595,39 @@ impl GitPanel { header: Section::Tracked, })); } - self.entries.extend( - changed_entries - .into_iter() - .map(GitListEntry::GitStatusEntry), - ); + self.entries + .extend(changed_entries.into_iter().map(GitListEntry::Status)); } if new_entries.len() > 0 { self.entries.push(GitListEntry::Header(GitHeaderEntry { header: Section::New, })); self.entries - .extend(new_entries.into_iter().map(GitListEntry::GitStatusEntry)); + .extend(new_entries.into_iter().map(GitListEntry::Status)); } if let Some((repo_path, _)) = max_width_item { self.max_width_item_index = self.entries.iter().position(|entry| match entry { - GitListEntry::GitStatusEntry(git_status_entry) => { - git_status_entry.repo_path == repo_path - } + GitListEntry::Status(git_status_entry) => git_status_entry.repo_path == repo_path, GitListEntry::Header(_) => false, }); } self.update_counts(repo); + let bulk_staging_anchor_new_index = bulk_staging + .as_ref() + .filter(|op| op.repo_id == repo.id) + .and_then(|op| self.entry_by_path(&op.anchor, cx)); + if bulk_staging_anchor_new_index == last_staged_path_prev_index + && let Some(index) = bulk_staging_anchor_new_index + && let Some(entry) = self.entries.get(index) + && let Some(entry) = entry.status_entry() + && self.entry_staging(entry) == StageStatus::Staged + { + self.bulk_staging = bulk_staging; + } + self.select_first_entry_if_none(cx); let suggested_commit_message = self.suggest_commit_message(cx); @@ -3743,7 +3763,7 @@ impl GitPanel { for ix in range { match &this.entries.get(ix) { - Some(GitListEntry::GitStatusEntry(entry)) => { + Some(GitListEntry::Status(entry)) => { items.push(this.render_entry( ix, entry, @@ -4000,8 +4020,6 @@ impl GitPanel { let marked = self.marked_entries.contains(&ix); let status_style = GitPanelSettings::get_global(cx).status_style; let status = entry.status; - let modifiers = self.current_modifiers; - let shift_held = modifiers.shift; let has_conflict = status.is_conflicted(); let is_modified = status.is_modified(); @@ -4120,12 +4138,6 @@ impl GitPanel { cx.stop_propagation(); }, ) - // .on_secondary_mouse_down(cx.listener( - // move |this, event: &MouseDownEvent, window, cx| { - // this.deploy_entry_context_menu(event.position, ix, window, cx); - // cx.stop_propagation(); - // }, - // )) .child( div() .id(checkbox_wrapper_id) @@ -4137,46 +4149,35 @@ impl GitPanel { .disabled(!has_write_access) .fill() .elevation(ElevationIndex::Surface) - .on_click({ + .on_click_ext({ let entry = entry.clone(); - cx.listener(move |this, _, window, cx| { - if !has_write_access { - return; - } - this.toggle_staged_for_entry( - &GitListEntry::GitStatusEntry(entry.clone()), - window, - cx, - ); - cx.stop_propagation(); - }) + let this = cx.weak_entity(); + move |_, click, window, cx| { + this.update(cx, |this, cx| { + if !has_write_access { + return; + } + if click.modifiers().shift { + this.stage_bulk(ix, cx); + } else { + this.toggle_staged_for_entry( + &GitListEntry::Status(entry.clone()), + window, + cx, + ); + } + cx.stop_propagation(); + }) + .ok(); + } }) .tooltip(move |window, cx| { let is_staged = entry_staging.is_fully_staged(); let action = if is_staged { "Unstage" } else { "Stage" }; - let tooltip_name = if shift_held { - format!("{} section", action) - } else { - action.to_string() - }; - - let meta = if shift_held { - format!( - "Release shift to {} single entry", - action.to_lowercase() - ) - } else { - format!("Shift click to {} section", action.to_lowercase()) - }; + let tooltip_name = action.to_string(); - Tooltip::with_meta( - tooltip_name, - Some(&ToggleStaged), - meta, - window, - cx, - ) + Tooltip::for_action(tooltip_name, &ToggleStaged, window, cx) }), ), ) @@ -4242,6 +4243,41 @@ impl GitPanel { panel }) } + + fn stage_bulk(&mut self, mut index: usize, cx: &mut Context<'_, Self>) { + let Some(op) = self.bulk_staging.as_ref() else { + return; + }; + let Some(mut anchor_index) = self.entry_by_path(&op.anchor, cx) else { + return; + }; + if let Some(entry) = self.entries.get(index) + && let Some(entry) = entry.status_entry() + { + self.set_bulk_staging_anchor(entry.repo_path.clone(), cx); + } + if index < anchor_index { + std::mem::swap(&mut index, &mut anchor_index); + } + let entries = self + .entries + .get(anchor_index..=index) + .unwrap_or_default() + .iter() + .filter_map(|entry| entry.status_entry().cloned()) + .collect::<Vec<_>>(); + self.change_file_stage(true, entries, cx); + } + + fn set_bulk_staging_anchor(&mut self, path: RepoPath, cx: &mut Context<'_, GitPanel>) { + let Some(repo) = self.active_repository.as_ref() else { + return; + }; + self.bulk_staging = Some(BulkStaging { + repo_id: repo.read(cx).id, + anchor: path, + }); + } } fn current_language_model(cx: &Context<'_, GitPanel>) -> Option<Arc<dyn LanguageModel>> { @@ -4279,9 +4315,9 @@ impl Render for GitPanel { .id("git_panel") .key_context(self.dispatch_context(window, cx)) .track_focus(&self.focus_handle) - .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed)) .when(has_write_access && !project.is_read_only(cx), |this| { this.on_action(cx.listener(Self::toggle_staged_for_selected)) + .on_action(cx.listener(Self::stage_range)) .on_action(cx.listener(GitPanel::commit)) .on_action(cx.listener(GitPanel::amend)) .on_action(cx.listener(GitPanel::cancel)) @@ -4953,7 +4989,7 @@ impl Component for PanelRepoFooter { #[cfg(test)] mod tests { - use git::status::StatusCode; + use git::status::{StatusCode, UnmergedStatus, UnmergedStatusCode}; use gpui::{TestAppContext, VisualTestContext}; use project::{FakeFs, WorktreeSettings}; use serde_json::json; @@ -5052,13 +5088,13 @@ mod tests { GitListEntry::Header(GitHeaderEntry { header: Section::Tracked }), - GitListEntry::GitStatusEntry(GitStatusEntry { + GitListEntry::Status(GitStatusEntry { abs_path: path!("/root/zed/crates/gpui/gpui.rs").into(), repo_path: "crates/gpui/gpui.rs".into(), status: StatusCode::Modified.worktree(), staging: StageStatus::Unstaged, }), - GitListEntry::GitStatusEntry(GitStatusEntry { + GitListEntry::Status(GitStatusEntry { abs_path: path!("/root/zed/crates/util/util.rs").into(), repo_path: "crates/util/util.rs".into(), status: StatusCode::Modified.worktree(), @@ -5067,54 +5103,6 @@ mod tests { ], ); - // TODO(cole) restore this once repository deduplication is implemented properly. - //cx.update_window_entity(&panel, |panel, window, cx| { - // panel.select_last(&Default::default(), window, cx); - // assert_eq!(panel.selected_entry, Some(2)); - // panel.open_diff(&Default::default(), window, cx); - //}); - //cx.run_until_parked(); - - //let worktree_roots = workspace.update(cx, |workspace, cx| { - // workspace - // .worktrees(cx) - // .map(|worktree| worktree.read(cx).abs_path()) - // .collect::<Vec<_>>() - //}); - //pretty_assertions::assert_eq!( - // worktree_roots, - // vec![ - // Path::new(path!("/root/zed/crates/gpui")).into(), - // Path::new(path!("/root/zed/crates/util/util.rs")).into(), - // ] - //); - - //project.update(cx, |project, cx| { - // let git_store = project.git_store().read(cx); - // // The repo that comes from the single-file worktree can't be selected through the UI. - // let filtered_entries = filtered_repository_entries(git_store, cx) - // .iter() - // .map(|repo| repo.read(cx).worktree_abs_path.clone()) - // .collect::<Vec<_>>(); - // assert_eq!( - // filtered_entries, - // [Path::new(path!("/root/zed/crates/gpui")).into()] - // ); - // // But we can select it artificially here. - // let repo_from_single_file_worktree = git_store - // .repositories() - // .values() - // .find(|repo| { - // repo.read(cx).worktree_abs_path.as_ref() - // == Path::new(path!("/root/zed/crates/util/util.rs")) - // }) - // .unwrap() - // .clone(); - - // // Paths still make sense when we somehow activate a repo that comes from a single-file worktree. - // repo_from_single_file_worktree.update(cx, |repo, cx| repo.set_as_active_repository(cx)); - //}); - let handle = cx.update_window_entity(&panel, |panel, _, _| { std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(())) }); @@ -5127,13 +5115,13 @@ mod tests { GitListEntry::Header(GitHeaderEntry { header: Section::Tracked }), - GitListEntry::GitStatusEntry(GitStatusEntry { + GitListEntry::Status(GitStatusEntry { abs_path: path!("/root/zed/crates/gpui/gpui.rs").into(), repo_path: "crates/gpui/gpui.rs".into(), status: StatusCode::Modified.worktree(), staging: StageStatus::Unstaged, }), - GitListEntry::GitStatusEntry(GitStatusEntry { + GitListEntry::Status(GitStatusEntry { abs_path: path!("/root/zed/crates/util/util.rs").into(), repo_path: "crates/util/util.rs".into(), status: StatusCode::Modified.worktree(), @@ -5142,4 +5130,196 @@ mod tests { ], ); } + + #[gpui::test] + async fn test_bulk_staging(cx: &mut TestAppContext) { + use GitListEntry::*; + + init_test(cx); + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/root", + json!({ + "project": { + ".git": {}, + "src": { + "main.rs": "fn main() {}", + "lib.rs": "pub fn hello() {}", + "utils.rs": "pub fn util() {}" + }, + "tests": { + "test.rs": "fn test() {}" + }, + "new_file.txt": "new content", + "another_new.rs": "// new file", + "conflict.txt": "conflicted content" + } + }), + ) + .await; + + fs.set_status_for_repo( + Path::new(path!("/root/project/.git")), + &[ + (Path::new("src/main.rs"), StatusCode::Modified.worktree()), + (Path::new("src/lib.rs"), StatusCode::Modified.worktree()), + (Path::new("tests/test.rs"), StatusCode::Modified.worktree()), + (Path::new("new_file.txt"), FileStatus::Untracked), + (Path::new("another_new.rs"), FileStatus::Untracked), + (Path::new("src/utils.rs"), FileStatus::Untracked), + ( + Path::new("conflict.txt"), + UnmergedStatus { + first_head: UnmergedStatusCode::Updated, + second_head: UnmergedStatusCode::Updated, + } + .into(), + ), + ], + ); + + let project = Project::test(fs.clone(), [Path::new(path!("/root/project"))], cx).await; + let workspace = + cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + + cx.read(|cx| { + project + .read(cx) + .worktrees(cx) + .nth(0) + .unwrap() + .read(cx) + .as_local() + .unwrap() + .scan_complete() + }) + .await; + + cx.executor().run_until_parked(); + + let panel = workspace.update(cx, GitPanel::new).unwrap(); + + let handle = cx.update_window_entity(&panel, |panel, _, _| { + std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(())) + }); + cx.executor().advance_clock(2 * UPDATE_DEBOUNCE); + handle.await; + + let entries = panel.read_with(cx, |panel, _| panel.entries.clone()); + #[rustfmt::skip] + pretty_assertions::assert_matches!( + entries.as_slice(), + &[ + Header(GitHeaderEntry { header: Section::Conflict }), + Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }), + Header(GitHeaderEntry { header: Section::Tracked }), + Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }), + Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }), + Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }), + Header(GitHeaderEntry { header: Section::New }), + Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }), + Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }), + Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }), + ], + ); + + let second_status_entry = entries[3].clone(); + panel.update_in(cx, |panel, window, cx| { + panel.toggle_staged_for_entry(&second_status_entry, window, cx); + }); + + panel.update_in(cx, |panel, window, cx| { + panel.selected_entry = Some(7); + panel.stage_range(&git::StageRange, window, cx); + }); + + cx.read(|cx| { + project + .read(cx) + .worktrees(cx) + .nth(0) + .unwrap() + .read(cx) + .as_local() + .unwrap() + .scan_complete() + }) + .await; + + cx.executor().run_until_parked(); + + let handle = cx.update_window_entity(&panel, |panel, _, _| { + std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(())) + }); + cx.executor().advance_clock(2 * UPDATE_DEBOUNCE); + handle.await; + + let entries = panel.read_with(cx, |panel, _| panel.entries.clone()); + #[rustfmt::skip] + pretty_assertions::assert_matches!( + entries.as_slice(), + &[ + Header(GitHeaderEntry { header: Section::Conflict }), + Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }), + Header(GitHeaderEntry { header: Section::Tracked }), + Status(GitStatusEntry { staging: StageStatus::Staged, .. }), + Status(GitStatusEntry { staging: StageStatus::Staged, .. }), + Status(GitStatusEntry { staging: StageStatus::Staged, .. }), + Header(GitHeaderEntry { header: Section::New }), + Status(GitStatusEntry { staging: StageStatus::Staged, .. }), + Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }), + Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }), + ], + ); + + let third_status_entry = entries[4].clone(); + panel.update_in(cx, |panel, window, cx| { + panel.toggle_staged_for_entry(&third_status_entry, window, cx); + }); + + panel.update_in(cx, |panel, window, cx| { + panel.selected_entry = Some(9); + panel.stage_range(&git::StageRange, window, cx); + }); + + cx.read(|cx| { + project + .read(cx) + .worktrees(cx) + .nth(0) + .unwrap() + .read(cx) + .as_local() + .unwrap() + .scan_complete() + }) + .await; + + cx.executor().run_until_parked(); + + let handle = cx.update_window_entity(&panel, |panel, _, _| { + std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(())) + }); + cx.executor().advance_clock(2 * UPDATE_DEBOUNCE); + handle.await; + + let entries = panel.read_with(cx, |panel, _| panel.entries.clone()); + #[rustfmt::skip] + pretty_assertions::assert_matches!( + entries.as_slice(), + &[ + Header(GitHeaderEntry { header: Section::Conflict }), + Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }), + Header(GitHeaderEntry { header: Section::Tracked }), + Status(GitStatusEntry { staging: StageStatus::Staged, .. }), + Status(GitStatusEntry { staging: StageStatus::Unstaged, .. }), + Status(GitStatusEntry { staging: StageStatus::Staged, .. }), + Header(GitHeaderEntry { header: Section::New }), + Status(GitStatusEntry { staging: StageStatus::Staged, .. }), + Status(GitStatusEntry { staging: StageStatus::Staged, .. }), + Status(GitStatusEntry { staging: StageStatus::Staged, .. }), + ], + ); + } } diff --git a/crates/ui/src/components/toggle.rs b/crates/ui/src/components/toggle.rs index 7a12e1f44509ba9f116305c1b4710da3c7e47001..4672a0cfc2d78e50a2b15e1225f388ca99d0d3a9 100644 --- a/crates/ui/src/components/toggle.rs +++ b/crates/ui/src/components/toggle.rs @@ -1,5 +1,6 @@ use gpui::{ - AnyElement, AnyView, ElementId, Hsla, IntoElement, Styled, Window, div, hsla, prelude::*, + AnyElement, AnyView, ClickEvent, ElementId, Hsla, IntoElement, Styled, Window, div, hsla, + prelude::*, }; use std::sync::Arc; @@ -44,7 +45,7 @@ pub struct Checkbox { toggle_state: ToggleState, disabled: bool, placeholder: bool, - on_click: Option<Box<dyn Fn(&ToggleState, &mut Window, &mut App) + 'static>>, + on_click: Option<Box<dyn Fn(&ToggleState, &ClickEvent, &mut Window, &mut App) + 'static>>, filled: bool, style: ToggleStyle, tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView>>, @@ -83,6 +84,16 @@ impl Checkbox { pub fn on_click( mut self, handler: impl Fn(&ToggleState, &mut Window, &mut App) + 'static, + ) -> Self { + self.on_click = Some(Box::new(move |state, _, window, cx| { + handler(state, window, cx) + })); + self + } + + pub fn on_click_ext( + mut self, + handler: impl Fn(&ToggleState, &ClickEvent, &mut Window, &mut App) + 'static, ) -> Self { self.on_click = Some(Box::new(handler)); self @@ -226,8 +237,8 @@ impl RenderOnce for Checkbox { .when_some( self.on_click.filter(|_| !self.disabled), |this, on_click| { - this.on_click(move |_, window, cx| { - on_click(&self.toggle_state.inverse(), window, cx) + this.on_click(move |click, window, cx| { + on_click(&self.toggle_state.inverse(), click, window, cx) }) }, ) From 8f6b9f0d65a8fec611a54ed053936cdb035c8082 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Sat, 12 Jul 2025 19:16:35 -0400 Subject: [PATCH 044/658] debugger: Allow users to shutdown debug sessions while they're booting (#34362) This solves problems where users couldn't shut down sessions while locators or build tasks are running. I renamed `debugger::Session::Mode` enum to `SessionState` to be more clear when it's referenced in other crates. I also embedded the boot task that is created in `SessionState::Building` variant. This allows sessions to shut down all created threads in their boot process in a clean and idiomatic way. Finally, I added a method on terminal that allows killing the active task. Release Notes: - Debugger: Allow shutting down debug sessions while they're booting up --- crates/debugger_ui/src/debugger_panel.rs | 67 +++++++---- crates/debugger_ui/src/session/running.rs | 28 ++++- crates/project/src/debugger/session.rs | 128 +++++++++++++--------- crates/terminal/src/pty_info.rs | 4 + crates/terminal/src/terminal.rs | 8 ++ 5 files changed, 156 insertions(+), 79 deletions(-) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 184aedafc2dd10752158350e53f673a4d0122dbb..37064d5d5dba5075e52e6a6047cea77133a1ccfb 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -27,7 +27,7 @@ use text::ToPoint as _; use itertools::Itertools as _; use language::Buffer; -use project::debugger::session::{Session, SessionQuirks, SessionStateEvent}; +use project::debugger::session::{Session, SessionQuirks, SessionState, SessionStateEvent}; use project::{DebugScenarioContext, Fs, ProjectPath, TaskSourceKind, WorktreeId}; use project::{Project, debugger::session::ThreadStatus}; use rpc::proto::{self}; @@ -36,7 +36,7 @@ use std::sync::{Arc, LazyLock}; use task::{DebugScenario, TaskContext}; use tree_sitter::{Query, StreamingIterator as _}; use ui::{ContextMenu, Divider, PopoverMenuHandle, Tooltip, prelude::*}; -use util::{ResultExt, maybe}; +use util::{ResultExt, debug_panic, maybe}; use workspace::SplitDirection; use workspace::item::SaveOptions; use workspace::{ @@ -278,22 +278,34 @@ impl DebugPanel { } }); - cx.spawn(async move |_, cx| { - if let Err(error) = task.await { - log::error!("{error}"); - session - .update(cx, |session, cx| { - session - .console_output(cx) - .unbounded_send(format!("error: {}", error)) - .ok(); - session.shutdown(cx) - })? - .await; + let boot_task = cx.spawn({ + let session = session.clone(); + + async move |_, cx| { + if let Err(error) = task.await { + log::error!("{error}"); + session + .update(cx, |session, cx| { + session + .console_output(cx) + .unbounded_send(format!("error: {}", error)) + .ok(); + session.shutdown(cx) + })? + .await; + } + anyhow::Ok(()) } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); + }); + + session.update(cx, |session, _| match &mut session.mode { + SessionState::Building(state_task) => { + *state_task = Some(boot_task); + } + SessionState::Running(_) => { + debug_panic!("Session state should be in building because we are just starting it"); + } + }); } pub(crate) fn rerun_last_session( @@ -826,13 +838,24 @@ impl DebugPanel { .on_click(window.listener_for( &running_state, |this, _, _window, cx| { - this.stop_thread(cx); + if this.session().read(cx).is_building() { + this.session().update(cx, |session, cx| { + session.shutdown(cx).detach() + }); + } else { + this.stop_thread(cx); + } + }, + )) + .disabled(active_session.as_ref().is_none_or( + |session| { + session + .read(cx) + .session(cx) + .read(cx) + .is_terminated() }, )) - .disabled( - thread_status != ThreadStatus::Stopped - && thread_status != ThreadStatus::Running, - ) .tooltip({ let focus_handle = focus_handle.clone(); let label = if capabilities diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index d308fc9bd26b930e8b88e2ebaf194e8e30642238..264d46370f5d8b85a92f6f3dac1a319371a8ce03 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -34,7 +34,7 @@ use loaded_source_list::LoadedSourceList; use module_list::ModuleList; use project::{ DebugScenarioContext, Project, WorktreeId, - debugger::session::{Session, SessionEvent, ThreadId, ThreadStatus}, + debugger::session::{self, Session, SessionEvent, SessionStateEvent, ThreadId, ThreadStatus}, terminals::TerminalKind, }; use rpc::proto::ViewId; @@ -770,6 +770,15 @@ impl RunningState { cx.on_focus_out(&focus_handle, window, |this, _, window, cx| { this.serialize_layout(window, cx); }), + cx.subscribe( + &session, + |this, session, event: &SessionStateEvent, cx| match event { + SessionStateEvent::Shutdown if session.read(cx).is_building() => { + this.shutdown(cx); + } + _ => {} + }, + ), ]; let mut pane_close_subscriptions = HashMap::default(); @@ -884,6 +893,7 @@ impl RunningState { let weak_project = project.downgrade(); let weak_workspace = workspace.downgrade(); let is_local = project.read(cx).is_local(); + cx.spawn_in(window, async move |this, cx| { let DebugScenario { adapter, @@ -1599,9 +1609,21 @@ impl RunningState { }) .log_err(); - self.session.update(cx, |session, cx| { + let is_building = self.session.update(cx, |session, cx| { session.shutdown(cx).detach(); - }) + matches!(session.mode, session::SessionState::Building(_)) + }); + + if is_building { + self.debug_terminal.update(cx, |terminal, cx| { + if let Some(view) = terminal.terminal.as_ref() { + view.update(cx, |view, cx| { + view.terminal() + .update(cx, |terminal, _| terminal.kill_active_task()) + }) + } + }) + } } pub fn stop_thread(&self, cx: &mut Context<Self>) { diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 59feb504c55c05af031742b3d97c203c465b9f93..f526d5a3fececae2e9b45b2c935aa00f1283d845 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -134,8 +134,8 @@ pub struct Watcher { pub presentation_hint: Option<VariablePresentationHint>, } -pub enum Mode { - Building, +pub enum SessionState { + Building(Option<Task<Result<()>>>), Running(RunningMode), } @@ -560,15 +560,15 @@ impl RunningMode { } } -impl Mode { +impl SessionState { pub(super) fn request_dap<R: LocalDapCommand>(&self, request: R) -> Task<Result<R::Response>> where <R::DapRequest as dap::requests::Request>::Response: 'static, <R::DapRequest as dap::requests::Request>::Arguments: 'static + Send, { match self { - Mode::Running(debug_adapter_client) => debug_adapter_client.request(request), - Mode::Building => Task::ready(Err(anyhow!( + SessionState::Running(debug_adapter_client) => debug_adapter_client.request(request), + SessionState::Building(_) => Task::ready(Err(anyhow!( "no adapter running to send request: {request:?}" ))), } @@ -577,13 +577,13 @@ impl Mode { /// Did this debug session stop at least once? pub(crate) fn has_ever_stopped(&self) -> bool { match self { - Mode::Building => false, - Mode::Running(running_mode) => running_mode.has_ever_stopped, + SessionState::Building(_) => false, + SessionState::Running(running_mode) => running_mode.has_ever_stopped, } } fn stopped(&mut self) { - if let Mode::Running(running) = self { + if let SessionState::Running(running) = self { running.has_ever_stopped = true; } } @@ -660,7 +660,7 @@ type IsEnabled = bool; pub struct OutputToken(pub usize); /// Represents a current state of a single debug adapter and provides ways to mutate it. pub struct Session { - pub mode: Mode, + pub mode: SessionState, id: SessionId, label: Option<SharedString>, adapter: DebugAdapterName, @@ -828,10 +828,9 @@ impl Session { BreakpointStoreEvent::SetDebugLine | BreakpointStoreEvent::ClearDebugLines => {} }) .detach(); - // cx.on_app_quit(Self::on_app_quit).detach(); let this = Self { - mode: Mode::Building, + mode: SessionState::Building(None), id: session_id, child_session_ids: HashSet::default(), parent_session, @@ -869,8 +868,8 @@ impl Session { pub fn worktree(&self) -> Option<Entity<Worktree>> { match &self.mode { - Mode::Building => None, - Mode::Running(local_mode) => local_mode.worktree.upgrade(), + SessionState::Building(_) => None, + SessionState::Running(local_mode) => local_mode.worktree.upgrade(), } } @@ -929,7 +928,18 @@ impl Session { ) .await?; this.update(cx, |this, cx| { - this.mode = Mode::Running(mode); + match &mut this.mode { + SessionState::Building(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" + ); + } + }; + this.mode = SessionState::Running(mode); cx.emit(SessionStateEvent::Running); })?; @@ -1022,8 +1032,8 @@ impl Session { pub fn binary(&self) -> Option<&DebugAdapterBinary> { match &self.mode { - Mode::Building => None, - Mode::Running(running_mode) => Some(&running_mode.binary), + SessionState::Building(_) => None, + SessionState::Running(running_mode) => Some(&running_mode.binary), } } @@ -1068,26 +1078,26 @@ impl Session { pub fn is_started(&self) -> bool { match &self.mode { - Mode::Building => false, - Mode::Running(running) => running.is_started, + SessionState::Building(_) => false, + SessionState::Running(running) => running.is_started, } } pub fn is_building(&self) -> bool { - matches!(self.mode, Mode::Building) + matches!(self.mode, SessionState::Building(_)) } pub fn as_running_mut(&mut self) -> Option<&mut RunningMode> { match &mut self.mode { - Mode::Running(local_mode) => Some(local_mode), - Mode::Building => None, + SessionState::Running(local_mode) => Some(local_mode), + SessionState::Building(_) => None, } } pub fn as_running(&self) -> Option<&RunningMode> { match &self.mode { - Mode::Running(local_mode) => Some(local_mode), - Mode::Building => None, + SessionState::Running(local_mode) => Some(local_mode), + SessionState::Building(_) => None, } } @@ -1229,7 +1239,7 @@ impl Session { let adapter_id = self.adapter().to_string(); let request = Initialize { adapter_id }; - let Mode::Running(running) = &self.mode else { + let SessionState::Running(running) = &self.mode else { return Task::ready(Err(anyhow!( "Cannot send initialize request, task still building" ))); @@ -1278,10 +1288,12 @@ impl Session { cx: &mut Context<Self>, ) -> Task<Result<()>> { match &self.mode { - Mode::Running(local_mode) => { + SessionState::Running(local_mode) => { local_mode.initialize_sequence(&self.capabilities, initialize_rx, dap_store, cx) } - Mode::Building => Task::ready(Err(anyhow!("cannot initialize, still building"))), + SessionState::Building(_) => { + Task::ready(Err(anyhow!("cannot initialize, still building"))) + } } } @@ -1292,7 +1304,7 @@ impl Session { cx: &mut Context<Self>, ) { match &mut self.mode { - Mode::Running(local_mode) => { + SessionState::Running(local_mode) => { if !matches!( self.thread_states.thread_state(active_thread_id), Some(ThreadStatus::Stopped) @@ -1316,7 +1328,7 @@ impl Session { }) .detach(); } - Mode::Building => {} + SessionState::Building(_) => {} } } @@ -1596,7 +1608,7 @@ impl Session { fn request_inner<T: LocalDapCommand + PartialEq + Eq + Hash>( capabilities: &Capabilities, - mode: &Mode, + mode: &SessionState, request: T, process_result: impl FnOnce( &mut Self, @@ -1916,28 +1928,36 @@ impl Session { self.thread_states.exit_all_threads(); cx.notify(); - let task = if self - .capabilities - .supports_terminate_request - .unwrap_or_default() - { - self.request( - TerminateCommand { - restart: Some(false), - }, - Self::clear_active_debug_line_response, - cx, - ) - } else { - self.request( - DisconnectCommand { - restart: Some(false), - terminate_debuggee: Some(true), - suspend_debuggee: Some(false), - }, - Self::clear_active_debug_line_response, - cx, - ) + let task = match &mut self.mode { + SessionState::Running(_) => { + if self + .capabilities + .supports_terminate_request + .unwrap_or_default() + { + self.request( + TerminateCommand { + restart: Some(false), + }, + Self::clear_active_debug_line_response, + cx, + ) + } else { + self.request( + DisconnectCommand { + restart: Some(false), + terminate_debuggee: Some(true), + suspend_debuggee: Some(false), + }, + Self::clear_active_debug_line_response, + cx, + ) + } + } + SessionState::Building(build_task) => { + build_task.take(); + Task::ready(Some(())) + } }; cx.emit(SessionStateEvent::Shutdown); @@ -1987,8 +2007,8 @@ impl Session { pub fn adapter_client(&self) -> Option<Arc<DebugAdapterClient>> { match self.mode { - Mode::Running(ref local) => Some(local.client.clone()), - Mode::Building => None, + SessionState::Running(ref local) => Some(local.client.clone()), + SessionState::Building(_) => None, } } @@ -2452,7 +2472,7 @@ impl Session { } pub fn is_attached(&self) -> bool { - let Mode::Running(local_mode) = &self.mode else { + let SessionState::Running(local_mode) = &self.mode else { return false; }; local_mode.binary.request_args.request == StartDebuggingRequestArgumentsRequest::Attach diff --git a/crates/terminal/src/pty_info.rs b/crates/terminal/src/pty_info.rs index d9515afbf751a0027bc8e911334b2db13bf03935..802470493cc6c883008640af2b8f374254e3299d 100644 --- a/crates/terminal/src/pty_info.rs +++ b/crates/terminal/src/pty_info.rs @@ -121,6 +121,10 @@ impl PtyProcessInfo { } } + pub(crate) fn kill_current_process(&mut self) -> bool { + self.refresh().map_or(false, |process| process.kill()) + } + fn load(&mut self) -> Option<ProcessInfo> { let process = self.refresh()?; let cwd = process.cwd().map_or(PathBuf::new(), |p| p.to_owned()); diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 032a750d1a58cce592ac64e8e8aa35cd0b07df99..72307b697acccaa2763943499f7d2bb7212c163b 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1824,6 +1824,14 @@ 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(); + } + } + } + pub fn task(&self) -> Option<&TaskState> { self.task.as_ref() } From 1cadff931122fb882b166433b54681fdcba0a3db Mon Sep 17 00:00:00 2001 From: Smit Barmase <heysmitbarmase@gmail.com> Date: Sat, 12 Jul 2025 17:44:30 -0700 Subject: [PATCH 045/658] project_panel: Fix sticky items horizontal scroll and hover propagation (#34367) Release Notes: - Fixed horizontal scrolling not working for sticky items in the Project Panel. - Fixed issue where hovering over the last sticky item in the Project Panel showed a hovered state on the entry behind it. - Improved behavior when clicking a sticky item in the Project Panel so it scrolls just enough for the item to no longer be sticky. --- crates/project_panel/src/project_panel.rs | 23 +++- crates/ui/src/components/sticky_items.rs | 146 +++++++++++----------- 2 files changed, 95 insertions(+), 74 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 15869fc5acc4d1fac8777cf957ed7676f4f1a426..6144612cd7bd9ed16159028d5bbf0086ca409a6b 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -3961,8 +3961,14 @@ impl ProjectPanel { linear_color_stop(shadow_color_bottom, 0.), )); + let id: ElementId = if is_sticky { + SharedString::from(format!("project_panel_sticky_item_{}", entry_id.to_usize())).into() + } else { + (entry_id.to_proto() as usize).into() + }; + div() - .id(entry_id.to_proto() as usize) + .id(id.clone()) .relative() .group(GROUP_NAME) .cursor_pointer() @@ -3973,6 +3979,9 @@ impl ProjectPanel { .border_color(border_color) .hover(|style| style.bg(bg_hover_color).border_color(border_hover_color)) .when(show_sticky_shadow, |this| this.child(sticky_shadow)) + .when(is_sticky, |this| { + this.block_mouse_except_scroll() + }) .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)) @@ -4183,6 +4192,16 @@ impl ProjectPanel { .unwrap_or(ScrollStrategy::Top); this.scroll_handle.scroll_to_item(index, strategy); cx.notify(); + // move down by 1px so that clicked item + // don't count as sticky anymore + cx.on_next_frame(window, |_, window, cx| { + cx.on_next_frame(window, |this, _, cx| { + let mut offset = this.scroll_handle.offset(); + offset.y += px(1.); + this.scroll_handle.set_offset(offset); + cx.notify(); + }); + }); return; } } @@ -4201,7 +4220,7 @@ impl ProjectPanel { }), ) .child( - ListItem::new(entry_id.to_proto() as usize) + ListItem::new(id) .indent_level(depth) .indent_step_size(px(settings.indent_size)) .spacing(match settings.entry_spacing { diff --git a/crates/ui/src/components/sticky_items.rs b/crates/ui/src/components/sticky_items.rs index 218f7aae3510213afeed9d80a28428ce9c0df28a..ca8b336a5aa97101f29d394399f36bda2fcc44b9 100644 --- a/crates/ui/src/components/sticky_items.rs +++ b/crates/ui/src/components/sticky_items.rs @@ -149,47 +149,7 @@ where ) -> AnyElement { let entries = (self.compute_fn)(visible_range.clone(), window, cx); - struct StickyAnchor<T> { - entry: T, - index: usize, - } - - let mut sticky_anchor = None; - let mut last_item_is_drifting = false; - - let mut iter = entries.iter().enumerate().peekable(); - while let Some((ix, current_entry)) = iter.next() { - let depth = current_entry.depth(); - - if depth < ix { - sticky_anchor = Some(StickyAnchor { - entry: current_entry.clone(), - index: visible_range.start + ix, - }); - break; - } - - if let Some(&(_next_ix, next_entry)) = iter.peek() { - let next_depth = next_entry.depth(); - let next_item_outdented = next_depth + 1 == depth; - - let depth_same_as_index = depth == ix; - let depth_greater_than_index = depth == ix + 1; - - if next_item_outdented && (depth_same_as_index || depth_greater_than_index) { - if depth_greater_than_index { - last_item_is_drifting = true; - } - sticky_anchor = Some(StickyAnchor { - entry: current_entry.clone(), - index: visible_range.start + ix, - }); - break; - } - } - } - - let Some(sticky_anchor) = sticky_anchor else { + let Some(sticky_anchor) = find_sticky_anchor(&entries, visible_range.start) else { return StickyItemsElement { drifting_element: None, drifting_decoration: None, @@ -203,23 +163,21 @@ where let mut elements = (self.render_fn)(sticky_anchor.entry, window, cx); let items_count = elements.len(); - let indents: SmallVec<[usize; 8]> = { - elements - .iter() - .enumerate() - .map(|(ix, _)| anchor_depth.saturating_sub(items_count.saturating_sub(ix))) - .collect() - }; + let indents: SmallVec<[usize; 8]> = (0..items_count) + .map(|ix| anchor_depth.saturating_sub(items_count.saturating_sub(ix))) + .collect(); let mut last_decoration_element = None; let mut rest_decoration_elements = SmallVec::new(); - let available_space = size( - AvailableSpace::Definite(bounds.size.width), + let expanded_width = bounds.size.width + scroll_offset.x.abs(); + + let decor_available_space = size( + AvailableSpace::Definite(expanded_width), AvailableSpace::Definite(bounds.size.height), ); - let drifting_y_offset = if last_item_is_drifting { + let drifting_y_offset = if sticky_anchor.drifting { let scroll_top = -scroll_offset.y; let anchor_top = item_height * (sticky_anchor.index + 1); let sticky_area_height = item_height * items_count; @@ -228,7 +186,7 @@ where Pixels::ZERO }; - let (drifting_indent, rest_indents) = if last_item_is_drifting && !indents.is_empty() { + let (drifting_indent, rest_indents) = if sticky_anchor.drifting && !indents.is_empty() { let last = indents[indents.len() - 1]; let rest: SmallVec<[usize; 8]> = indents[..indents.len() - 1].iter().copied().collect(); (Some(last), rest) @@ -236,11 +194,14 @@ where (None, indents) }; + let base_origin = bounds.origin - point(px(0.), scroll_offset.y); + for decoration in &self.decorations { if let Some(drifting_indent) = drifting_indent { let drifting_indent_vec: SmallVec<[usize; 8]> = [drifting_indent].into_iter().collect(); - let sticky_origin = bounds.origin - scroll_offset + + let sticky_origin = base_origin + point(px(0.), item_height * rest_indents.len() + drifting_y_offset); let decoration_bounds = Bounds::new(sticky_origin, bounds.size); @@ -252,13 +213,13 @@ where window, cx, ); - drifting_dec.layout_as_root(available_space, window, cx); + drifting_dec.layout_as_root(decor_available_space, window, cx); drifting_dec.prepaint_at(sticky_origin, window, cx); last_decoration_element = Some(drifting_dec); } if !rest_indents.is_empty() { - let decoration_bounds = Bounds::new(bounds.origin - scroll_offset, bounds.size); + let decoration_bounds = Bounds::new(base_origin, bounds.size); let mut rest_dec = decoration.as_ref().compute( &rest_indents, decoration_bounds, @@ -267,46 +228,45 @@ where window, cx, ); - rest_dec.layout_as_root(available_space, window, cx); + rest_dec.layout_as_root(decor_available_space, window, cx); rest_dec.prepaint_at(bounds.origin, window, cx); rest_decoration_elements.push(rest_dec); } } let (mut drifting_element, mut rest_elements) = - if last_item_is_drifting && !elements.is_empty() { + if sticky_anchor.drifting && !elements.is_empty() { let last = elements.pop().unwrap(); (Some(last), elements) } else { (None, elements) }; - for (ix, element) in rest_elements.iter_mut().enumerate() { - let sticky_origin = bounds.origin - scroll_offset + point(px(0.), item_height * ix); - let element_available_space = size( - AvailableSpace::Definite(bounds.size.width), - AvailableSpace::Definite(item_height), - ); - - element.layout_as_root(element_available_space, window, cx); - element.prepaint_at(sticky_origin, window, cx); - } + let element_available_space = size( + AvailableSpace::Definite(expanded_width), + AvailableSpace::Definite(item_height), + ); + // order of prepaint is important here + // mouse events checks hitboxes in reverse insertion order if let Some(ref mut drifting_element) = drifting_element { - let sticky_origin = bounds.origin - scroll_offset + let sticky_origin = base_origin + point( px(0.), item_height * rest_elements.len() + drifting_y_offset, ); - let element_available_space = size( - AvailableSpace::Definite(bounds.size.width), - AvailableSpace::Definite(item_height), - ); drifting_element.layout_as_root(element_available_space, window, cx); drifting_element.prepaint_at(sticky_origin, window, cx); } + for (ix, element) in rest_elements.iter_mut().enumerate() { + let sticky_origin = base_origin + point(px(0.), item_height * ix); + + element.layout_as_root(element_available_space, window, cx); + element.prepaint_at(sticky_origin, window, cx); + } + StickyItemsElement { drifting_element, drifting_decoration: last_decoration_element, @@ -317,6 +277,48 @@ where } } +struct StickyAnchor<T> { + entry: T, + index: usize, + drifting: bool, +} + +fn find_sticky_anchor<T: StickyCandidate + Clone>( + entries: &SmallVec<[T; 8]>, + visible_range_start: usize, +) -> Option<StickyAnchor<T>> { + let mut iter = entries.iter().enumerate().peekable(); + while let Some((ix, current_entry)) = iter.next() { + let depth = current_entry.depth(); + + if depth < ix { + return Some(StickyAnchor { + entry: current_entry.clone(), + index: visible_range_start + ix, + drifting: false, + }); + } + + if let Some(&(_next_ix, next_entry)) = iter.peek() { + let next_depth = next_entry.depth(); + let next_item_outdented = next_depth + 1 == depth; + + let depth_same_as_index = depth == ix; + let depth_greater_than_index = depth == ix + 1; + + if next_item_outdented && (depth_same_as_index || depth_greater_than_index) { + return Some(StickyAnchor { + entry: current_entry.clone(), + index: visible_range_start + ix, + drifting: depth_greater_than_index, + }); + } + } + } + + None +} + /// A decoration for a [`StickyItems`]. This can be used for various things, /// such as rendering indent guides, or other visual effects. pub trait StickyItemsDecoration { From 0af7d32b7decd0da3729f1c48f2813438ca6de05 Mon Sep 17 00:00:00 2001 From: Finn Evers <finn@zed.dev> Date: Sun, 13 Jul 2025 23:41:41 +0200 Subject: [PATCH 046/658] keymap_ui: Dismiss context menu less frequently (#34387) This PR fixes an issue where the context menu in the keymap UI would be immediately dismissed after being opened when using a trackpad on MacOS. Right clicking on MacOS almost always fires a scroll event with a delta of 0 pixels right after (which is not the case when using a mouse). The fired scroll event caused the context menu to be removed on the next frame. This change ensures the menu is only removed when a vertical scroll is actually happening. Release Notes: - N/A --- crates/settings_ui/src/keybindings.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index c78a370f1232eb219b7ba31d3113f2427672fccd..210ec827cc556165291c6f676396efc6e1d573a8 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -11,9 +11,9 @@ use fs::Fs; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ Action, AnimationExt, AppContext as _, AsyncApp, ClickEvent, Context, DismissEvent, Entity, - EventEmitter, FocusHandle, Focusable, Global, KeyContext, KeyDownEvent, Keystroke, - ModifiersChangedEvent, MouseButton, Point, ScrollStrategy, StyledText, Subscription, - WeakEntity, actions, anchored, deferred, div, + EventEmitter, FocusHandle, Focusable, Global, IsZero, KeyContext, KeyDownEvent, Keystroke, + ModifiersChangedEvent, MouseButton, Point, ScrollStrategy, ScrollWheelEvent, StyledText, + Subscription, WeakEntity, actions, anchored, deferred, div, }; use language::{Language, LanguageConfig, ToOffset as _}; use settings::{BaseKeymap, KeybindSource, KeymapFile, SettingsAssets}; @@ -1182,9 +1182,13 @@ impl Render for KeymapEditor { }), ), ) - .on_scroll_wheel(cx.listener(|this, _, _, cx| { - this.context_menu.take(); - cx.notify(); + .on_scroll_wheel(cx.listener(|this, event: &ScrollWheelEvent, _, cx| { + // This ensures that the menu is not dismissed in cases where scroll events + // with a delta of zero are emitted + if !event.delta.pixel_delta(px(1.)).y.is_zero() { + this.context_menu.take(); + cx.notify(); + } })) .children(self.context_menu.as_ref().map(|(menu, position, _)| { deferred( From 85d12548a1a867b07cb47e1ca3edd52468184255 Mon Sep 17 00:00:00 2001 From: Somtoo Chukwurah <chukwurahb@gmail.com> Date: Sun, 13 Jul 2025 19:35:03 -0400 Subject: [PATCH 047/658] linux: Add `file_finder::Toggle` key binding (#34380) This fixes a bug on linux where repeated presses of p while holding down the ctrl modifier navigates through options in reverse. Closes #34379 The main issue is the default biding of ctrl-p on linux is menu::SelectPrevious hence in context "context": "FileFinder || (FileFinder > Picker > Editor)" it would navigate in reverse Release Notes: - Fixed `file_finder::Toggle` on Linux not scrolling forward --- assets/keymaps/default-linux.json | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 4022bea34ba07c415e4a9620cef1a23a8816de04..2a4b8acd445dfdb40b468fec3cd3d7184c694583 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -999,6 +999,7 @@ { "context": "FileFinder || (FileFinder > Picker > Editor)", "bindings": { + "ctrl-p": "file_finder::Toggle", "ctrl-shift-a": "file_finder::ToggleSplitMenu", "ctrl-shift-i": "file_finder::ToggleFilterMenu" } From 51df8a17ef32196acac6e3125c0f2d6297300e63 Mon Sep 17 00:00:00 2001 From: Smit Barmase <heysmitbarmase@gmail.com> Date: Mon, 14 Jul 2025 06:59:45 +0530 Subject: [PATCH 048/658] project_panel: Do not render a single sticky entry when scrolled all the way to the top (#34389) Fixes root entry not expanding/collapsing on nightly. Regressed in https://github.com/zed-industries/zed/pull/34367. Release Notes: - N/A --- crates/project_panel/src/project_panel.rs | 58 +++++++++++------------ 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 6144612cd7bd9ed16159028d5bbf0086ca409a6b..9f799b5be6d4a9e94d23a6a920da257b0962d4e6 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -186,7 +186,6 @@ struct EntryDetails { #[derive(Debug, PartialEq, Eq, Clone)] struct StickyDetails { sticky_index: usize, - is_last: bool, } /// Permanently deletes the selected file or directory. @@ -3938,29 +3937,6 @@ impl ProjectPanel { } }; - let show_sticky_shadow = details.sticky.as_ref().map_or(false, |item| { - if item.is_last { - let is_scrollable = self.scroll_handle.is_scrollable(); - let is_scrolled = self.scroll_handle.offset().y < px(0.); - is_scrollable && is_scrolled - } else { - false - } - }); - let shadow_color_top = hsla(0.0, 0.0, 0.0, 0.1); - let shadow_color_bottom = hsla(0.0, 0.0, 0.0, 0.); - let sticky_shadow = div() - .absolute() - .left_0() - .bottom_neg_1p5() - .h_1p5() - .w_full() - .bg(linear_gradient( - 0., - linear_color_stop(shadow_color_top, 1.), - linear_color_stop(shadow_color_bottom, 0.), - )); - let id: ElementId = if is_sticky { SharedString::from(format!("project_panel_sticky_item_{}", entry_id.to_usize())).into() } else { @@ -3978,7 +3954,6 @@ impl ProjectPanel { .border_r_2() .border_color(border_color) .hover(|style| style.bg(bg_hover_color).border_color(border_hover_color)) - .when(show_sticky_shadow, |this| this.child(sticky_shadow)) .when(is_sticky, |this| { this.block_mouse_except_scroll() }) @@ -4944,7 +4919,6 @@ impl ProjectPanel { .unwrap_or_default(); let sticky_details = Some(StickyDetails { sticky_index: index, - is_last: index == last_item_index, }); let details = self.details_for_entry( entry, @@ -4956,7 +4930,24 @@ impl ProjectPanel { window, cx, ); - self.render_entry(entry.id, details, window, cx).into_any() + self.render_entry(entry.id, details, window, cx) + .when(index == last_item_index, |this| { + let shadow_color_top = hsla(0.0, 0.0, 0.0, 0.1); + let shadow_color_bottom = hsla(0.0, 0.0, 0.0, 0.); + let sticky_shadow = div() + .absolute() + .left_0() + .bottom_neg_1p5() + .h_1p5() + .w_full() + .bg(linear_gradient( + 0., + linear_color_stop(shadow_color_top, 1.), + linear_color_stop(shadow_color_bottom, 0.), + )); + this.child(sticky_shadow) + }) + .into_any() }) .collect() } @@ -4990,7 +4981,16 @@ impl Render for ProjectPanel { let indent_size = ProjectPanelSettings::get_global(cx).indent_size; let show_indent_guides = ProjectPanelSettings::get_global(cx).indent_guides.show == ShowIndentGuides::Always; - let show_sticky_scroll = ProjectPanelSettings::get_global(cx).sticky_scroll; + let show_sticky_entries = { + if ProjectPanelSettings::get_global(cx).sticky_scroll { + let is_scrollable = self.scroll_handle.is_scrollable(); + let is_scrolled = self.scroll_handle.offset().y < px(0.); + is_scrollable && is_scrolled + } else { + false + } + }; + let is_local = project.is_local(); if has_worktree { @@ -5282,7 +5282,7 @@ impl Render for ProjectPanel { }), ) }) - .when(show_sticky_scroll, |list| { + .when(show_sticky_entries, |list| { let sticky_items = ui::sticky_items( cx.entity().clone(), |this, range, window, cx| { From f50041779dcfd7a76c8aec293361c60c53f02d51 Mon Sep 17 00:00:00 2001 From: Sergei Surovtsev <97428129+stillonearth@users.noreply.github.com> Date: Mon, 14 Jul 2025 08:00:03 +0300 Subject: [PATCH 049/658] Language independent hotkeys (#34053) Addresses #10972 Closes #24950 Closes #24499 Adds _key_en_ to _Keystroke_ that is derived from key's scan code. This is more lightweight approach than #32529 Currently has been tested on x11 and windows. Mac code hasn't been implemented yet. Release Notes: - linux: When typing non-ASCII keys on Linux we will now also match keybindings against the QWERTY-equivalent layout. This should allow most of Zed's builtin shortcuts to work out of the box on most keyboard layouts. **Breaking change**: If you had been using `keysym` names in your keyboard shortcut file (`ctrl-cyrillic_yeru`, etc.) you should now use the QWERTY-equivalent characters instead. --------- Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com> --- crates/gpui/src/platform/keystroke.rs | 3 + crates/gpui/src/platform/linux/platform.rs | 71 +++++++++++++++++++ .../gpui/src/platform/linux/wayland/client.rs | 20 ++++-- crates/gpui/src/platform/linux/x11/client.rs | 9 ++- docs/src/key-bindings.md | 18 +++-- 5 files changed, 108 insertions(+), 13 deletions(-) diff --git a/crates/gpui/src/platform/keystroke.rs b/crates/gpui/src/platform/keystroke.rs index 40387a820230cfc0f73f90643c082619ceaa595a..69d87ebdcb2510e31431409fb19172344d14c5bc 100644 --- a/crates/gpui/src/platform/keystroke.rs +++ b/crates/gpui/src/platform/keystroke.rs @@ -13,6 +13,9 @@ pub struct Keystroke { /// key is the character printed on the key that was pressed /// e.g. for option-s, key is "s" + /// On layouts that do not have ascii keys (e.g. Thai) + /// this will be the ASCII-equivalent character (q instead of ๆ), + /// and the typed character will be present in key_char. pub key: String, /// key_char is the character that could have been typed when diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index af53899b437c244fd06d43b7963920c9596b94a0..8c9fd672b617abbc52c4a5263d4a19984d09c6ca 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -706,12 +706,81 @@ pub(super) fn log_cursor_icon_warning(message: impl std::fmt::Display) { } } +#[cfg(any(feature = "wayland", feature = "x11"))] +pub(crate) enum KeycodeSource { + X11, + Wayland, +} + +#[cfg(any(feature = "wayland", feature = "x11"))] +impl KeycodeSource { + fn guess_ascii(&self, keycode: Keycode, shift: bool) -> Option<char> { + let raw = match self { + // For historical reasons X11 adds 8 to keycodes + Self::X11 => keycode.raw() - 8, + // For no particular reason, wayland doesn't. + Self::Wayland => keycode.raw(), + }; + let c = match (raw, shift) { + (16, _) => 'q', + (17, _) => 'w', + (18, _) => 'e', + (19, _) => 'r', + (20, _) => 't', + (21, _) => 'y', + (22, _) => 'u', + (23, _) => 'i', + (24, _) => 'o', + (25, _) => 'p', + (26, false) => '[', + (26, true) => '{', + (27, false) => ']', + (27, true) => '}', + (30, _) => 'a', + (31, _) => 's', + (32, _) => 'd', + (33, _) => 'f', + (34, _) => 'g', + (35, _) => 'h', + (36, _) => 'j', + (37, _) => 'k', + (38, _) => 'l', + (39, false) => ';', + (39, true) => ':', + (40, false) => '\'', + (40, true) => '"', + (41, false) => '`', + (41, true) => '~', + (43, false) => '\\', + (43, true) => '|', + (44, _) => 'z', + (45, _) => 'x', + (46, _) => 'c', + (47, _) => 'v', + (48, _) => 'b', + (49, _) => 'n', + (50, _) => 'm', + (51, false) => ',', + (51, true) => '>', + (52, false) => '.', + (52, true) => '<', + (53, false) => '/', + (53, true) => '?', + + _ => return None, + }; + + Some(c) + } +} + #[cfg(any(feature = "wayland", feature = "x11"))] impl crate::Keystroke { pub(super) fn from_xkb( state: &State, mut modifiers: crate::Modifiers, keycode: Keycode, + source: KeycodeSource, ) -> Self { let key_utf32 = state.key_get_utf32(keycode); let key_utf8 = state.key_get_utf8(keycode); @@ -773,6 +842,8 @@ impl crate::Keystroke { let name = xkb::keysym_get_name(key_sym).to_lowercase(); if key_sym.is_keypad_key() { name.replace("kp_", "") + } else if let Some(key_en) = source.guess_ascii(keycode, modifiers.shift) { + String::from(key_en) } else { name } diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 57d1dcec04ee6aa1828c98286c9115df4ccb6d44..b3a101568f627bb31c9be76c3eb222e24442b9d3 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -69,7 +69,6 @@ use super::{ window::{ImeInput, WaylandWindowStatePtr}, }; -use crate::platform::{PlatformWindow, blade::BladeContext}; use crate::{ AnyWindowHandle, Bounds, Capslock, CursorStyle, DOUBLE_CLICK_INTERVAL, DevicePixels, DisplayId, FileDropEvent, ForegroundExecutor, KeyDownEvent, KeyUpEvent, Keystroke, LinuxCommon, @@ -78,6 +77,10 @@ use crate::{ PlatformInput, PlatformKeyboardLayout, Point, SCROLL_LINES, ScaledPixels, ScrollDelta, ScrollWheelEvent, Size, TouchPhase, WindowParams, point, px, size, }; +use crate::{ + KeycodeSource, + platform::{PlatformWindow, blade::BladeContext}, +}; use crate::{ SharedString, platform::linux::{ @@ -1293,8 +1296,12 @@ 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); + let mut keystroke = Keystroke::from_xkb( + &keymap_state, + state.modifiers, + keycode, + KeycodeSource::Wayland, + ); if let Some(mut compose) = state.compose_state.take() { compose.feed(keysym); match compose.status() { @@ -1379,7 +1386,12 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr { } wl_keyboard::KeyState::Released if !keysym.is_modifier_key() => { let input = PlatformInput::KeyUp(KeyUpEvent { - keystroke: Keystroke::from_xkb(keymap_state, state.modifiers, keycode), + keystroke: Keystroke::from_xkb( + keymap_state, + state.modifiers, + keycode, + KeycodeSource::Wayland, + ), }); if state.repeat.current_keycode == Some(keycode) { diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index 6cff977128ec594d683085e5f2cc24683c9e9ba7..56d4f5367d7abfc86dff1492e81a13300bda2cd5 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -1,4 +1,4 @@ -use crate::{Capslock, xcb_flush}; +use crate::{Capslock, KeycodeSource, xcb_flush}; use core::str; use std::{ cell::RefCell, @@ -1034,7 +1034,8 @@ impl X11Client { xkb_state.latched_layout, xkb_state.locked_layout, ); - let mut keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code); + let mut keystroke = + crate::Keystroke::from_xkb(&state.xkb, modifiers, code, KeycodeSource::X11); let keysym = state.xkb.key_get_one_sym(code); if keysym.is_modifier_key() { return Some(()); @@ -1102,7 +1103,8 @@ impl X11Client { xkb_state.latched_layout, xkb_state.locked_layout, ); - let keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code); + let keystroke = + crate::Keystroke::from_xkb(&state.xkb, modifiers, code, KeycodeSource::X11); let keysym = state.xkb.key_get_one_sym(code); if keysym.is_modifier_key() { return Some(()); @@ -1326,6 +1328,7 @@ impl X11Client { &state.xkb, state.modifiers, event.detail.into(), + KeycodeSource::X11, )); let (mut ximc, mut xim_handler) = state.take_xim()?; drop(state); diff --git a/docs/src/key-bindings.md b/docs/src/key-bindings.md index 8a956b518591720bde777a68c0ac587cef712ce2..b2beaf9ffbcdd3c3635b5aec028767eb10b41a28 100644 --- a/docs/src/key-bindings.md +++ b/docs/src/key-bindings.md @@ -136,17 +136,17 @@ When this happens, and both bindings are active in the current context, Zed will ### Non-QWERTY keyboards -As of Zed 0.162.0, Zed has some support for non-QWERTY keyboards on macOS. Better support for non-QWERTY keyboards on Linux is planned. +Zed's support for non-QWERTY keyboards is still a work in progress. -There are roughly three categories of keyboard to consider: +If your keyboard can type the full ASCII ranges (DVORAK, COLEMAK, etc.) then shortcuts should work as you expect. -Keyboards that support full ASCII (QWERTY, DVORAK, COLEMAK, etc.). On these keyboards bindings are resolved based on the character that would be generated by the key. So to type `cmd-[`, find the key labeled `[` and press it with command. +Otherwise, read on... -Keyboards that are mostly non-ASCII, but support full ASCII when the command key is pressed. For example Cyrillic keyboards, Armenian, Hebrew, etc. On these keyboards bindings are resolved based on the character that would be generated by typing the key with command pressed. So to type `ctrl-a`, find the key that generates `cmd-a`. For these keyboards, keyboard shortcuts are displayed in the app using their ASCII equivalents. If the ASCII-equivalents are not printed on your keyboard, you can use the macOS keyboard viewer and holding down the `cmd` key to find things (though often the ASCII equivalents are in a QWERTY layout). +#### macOS -Finally keyboards that support extended Latin alphabets (usually ISO keyboards) require the most support. For example French AZERTY, German QWERTZ, etc. On these keyboards it is often not possible to type the entire ASCII range without option. To ensure that shortcuts _can_ be typed without option, keyboard shortcuts are mapped to "key equivalents" in the same way as [macOS](). This mapping is defined per layout, and is a compromise between leaving keyboard shortcuts triggered by the same character they are defined with, keeping shortcuts in the same place as a QWERTY layout, and moving shortcuts out of the way of system shortcuts. +On Cyrillic, Hebrew, Armenian, and other keyboards that are mostly non-ASCII; macOS automatically maps keys to the ASCII range when `cmd` is held. Zed takes this a step further and it can always match key-presses against either the ASCII layout, or the real layout regardless of modifiers, and regardless of the `use_key_equivalents` setting. For example in Thai, pressing `ctrl-ๆ` will match bindings associated with `ctrl-q` or `ctrl-ๆ` -For example on a German QWERTZ keyboard, the `cmd->` shortcut is moved to `cmd-:` because `cmd->` is the system window switcher and this is where that shortcut is typed on a QWERTY keyboard. `cmd-+` stays the same because + is still typeable without option, and as a result, `cmd-[` and `cmd-]` become `cmd-ö` and `cmd-ä`, moving out of the way of the `+` key. +On keyboards that support extended Latin alphabets (French AZERTY, German QWERTZ, etc.) it is often not possible to type the entire ASCII range without `option`. This introduces an ambiguity, `option-2` produces `@`. To ensure that all the builtin keyboard shortcuts can still be typed on these keyboards we move key-bindings around. For example, shortcuts bound to `@` on QWERTY are moved to `"` on a Spanish layout. This mapping is based on the macOS system defaults and can be seen by running `dev: Open Key Context View` from the command palette. If you are defining shortcuts in your personal keymap, you can opt into the key equivalent mapping by setting `use_key_equivalents` to `true` in your keymap: @@ -161,6 +161,12 @@ If you are defining shortcuts in your personal keymap, you can opt into the key ] ``` +### Linux + +Since v0.196.0 on Linux if the key that you type doesn't produce an ASCII character then we use the QWERTY-layout equivalent key for keyboard shortcuts. This means that many shortcuts can be typed on many layouts. + +We do not yet move shortcuts around to ensure that all the builtin shortcuts can be typed on every layout; so if there are some ASCII characters that cannot be typed, and your keyboard layout has different ASCII characters on the same keys as would be needed to type them, you may need to add custom key bindings to make this work. We do intend to fix this at some point, and help is very much wanted! + ## Tips and tricks ### Disabling a binding From e4effa5e014ae79004550bac73a91d98abc95796 Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon <oleksiy@zed.dev> Date: Mon, 14 Jul 2025 12:44:29 +0300 Subject: [PATCH 050/658] linux: Fix keycodes mapping on Wayland (#34396) We are already converting Wayland keycodes to X11's; double conversion results in a wrong mapping. Release Notes: - N/A --- crates/gpui/src/platform/linux/platform.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index 8c9fd672b617abbc52c4a5263d4a19984d09c6ca..743cac71f37d44ea187b0301bdf43c305cac8d04 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -715,12 +715,10 @@ pub(crate) enum KeycodeSource { #[cfg(any(feature = "wayland", feature = "x11"))] impl KeycodeSource { fn guess_ascii(&self, keycode: Keycode, shift: bool) -> Option<char> { - let raw = match self { - // For historical reasons X11 adds 8 to keycodes - Self::X11 => keycode.raw() - 8, - // For no particular reason, wayland doesn't. - Self::Wayland => keycode.raw(), - }; + // For historical reasons, X11 adds 8 to keycodes. + // Wayland doesn't, but by this point, our own Wayland client + // has added 8 for X11 compatibility. + let raw = keycode.raw() - 8; let c = match (raw, shift) { (16, _) => 'q', (17, _) => 'w', From cf1ce1beedb5ff46e4025355071eef281102870e Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Mon, 14 Jul 2025 16:18:56 +0530 Subject: [PATCH 051/658] languages: Fix ESLint diagnostics not getting shown (#33814) Closes #33442 Release Notes: - Resolved an issue where the ESLint language server returned an empty string for the CodeDescription.href field in diagnostics, leading to missing diagnostics in editor. --- Cargo.lock | 3 +-- Cargo.toml | 2 +- crates/languages/src/typescript.rs | 2 +- crates/project/src/lsp_command.rs | 4 ++-- crates/project/src/lsp_store.rs | 12 +++++++++--- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c07d311db96eaf1c11fba41d566ebdae7ba3b439..97b1e3321112e6a2edd3da92703e846c24005f56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9660,12 +9660,11 @@ dependencies = [ [[package]] name = "lsp-types" version = "0.95.1" -source = "git+https://github.com/zed-industries/lsp-types?rev=c9c189f1c5dd53c624a419ce35bc77ad6a908d18#c9c189f1c5dd53c624a419ce35bc77ad6a908d18" +source = "git+https://github.com/zed-industries/lsp-types?rev=6add7052b598ea1f40f7e8913622c3958b009b60#6add7052b598ea1f40f7e8913622c3958b009b60" dependencies = [ "bitflags 1.3.2", "serde", "serde_json", - "serde_repr", "url", ] diff --git a/Cargo.toml b/Cargo.toml index 7e3b43e58a1132a48b14eca2ad9f13004c386b7d..dedf5700524eb5041025656d9b3f6b3a8a2325c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -492,7 +492,7 @@ libc = "0.2" 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 = "c9c189f1c5dd53c624a419ce35bc77ad6a908d18" } +lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "6add7052b598ea1f40f7e8913622c3958b009b60" } markup5ever_rcdom = "0.3.0" metal = "0.29" moka = { version = "0.12.10", features = ["sync"] } diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index 32c45dfa886358124e4331420c2bcc3c8a349514..3c1ecdcd5c51ee0d10a886327bac488b44621f6d 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -863,7 +863,7 @@ impl LspAdapter for EsLintLspAdapter { }, "experimental": { "useFlatConfig": use_flat_config, - }, + } }); let override_options = cx.update(|cx| { diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 8ed37164361b64ceab7f46837bd89cf81e2a4550..4538dc4cda59a5478f8fb68cae2fd6862310382b 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -3822,7 +3822,7 @@ impl GetDocumentDiagnostics { code, code_description: match diagnostic.code_description { Some(code_description) => Some(CodeDescription { - href: lsp::Url::parse(&code_description).unwrap(), + href: Some(lsp::Url::parse(&code_description).unwrap()), }), None => None, }, @@ -3898,7 +3898,7 @@ impl GetDocumentDiagnostics { tags, code_description: diagnostic .code_description - .map(|desc| desc.href.to_string()), + .and_then(|desc| desc.href.map(|url| url.to_string())), message: diagnostic.message, data: diagnostic.data.as_ref().map(|data| data.to_string()), }) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index e7afac0689755a589513a4e99e35e8fe69b5219e..fd626cf2d6889668447b90cf88f963804fd65eba 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -9130,7 +9130,13 @@ impl LspStore { } }; - let lsp::ProgressParamsValue::WorkDone(progress) = progress.value; + let progress = match progress.value { + lsp::ProgressParamsValue::WorkDone(progress) => progress, + lsp::ProgressParamsValue::WorkspaceDiagnostic(_) => { + return; + } + }; + let language_server_status = if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { status @@ -10512,7 +10518,7 @@ impl LspStore { code_description: diagnostic .code_description .as_ref() - .map(|d| d.href.clone()), + .and_then(|d| d.href.clone()), severity: diagnostic.severity.unwrap_or(DiagnosticSeverity::ERROR), markdown: adapter.as_ref().and_then(|adapter| { adapter.diagnostic_message_to_markdown(&diagnostic.message) @@ -10539,7 +10545,7 @@ impl LspStore { code_description: diagnostic .code_description .as_ref() - .map(|c| c.href.clone()), + .and_then(|d| d.href.clone()), severity: DiagnosticSeverity::INFORMATION, markdown: adapter.as_ref().and_then(|adapter| { adapter.diagnostic_message_to_markdown(&info.message) From 84124c60db66416748e2f0971f2c232a0dbad5fe Mon Sep 17 00:00:00 2001 From: feeiyu <158308373+feeiyu@users.noreply.github.com> Date: Mon, 14 Jul 2025 19:04:54 +0800 Subject: [PATCH 052/658] Fix cannot select in terminal when copy_on_select is enabled (#34131) Closes #33989 ![terminal_select](https://github.com/user-attachments/assets/5027d2f2-f2b3-43a4-8262-3c266fdc5256) Release Notes: - N/A --- crates/terminal/src/terminal.rs | 22 +++++++++++----------- crates/terminal_view/src/terminal_view.rs | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 72307b697acccaa2763943499f7d2bb7212c163b..6e359414d76b0a10a1cbfdef8bfe868fff3eb1fa 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -162,7 +162,8 @@ enum InternalEvent { UpdateSelection(Point<Pixels>), // Adjusted mouse position, should open FindHyperlink(Point<Pixels>, bool), - Copy, + // Whether keep selection when copy + Copy(Option<bool>), // Vi mode events ToggleViMode, ViMotion(ViMotion), @@ -931,13 +932,13 @@ impl Terminal { } } - InternalEvent::Copy => { + InternalEvent::Copy(keep_selection) => { if let Some(txt) = term.selection_to_string() { cx.write_to_clipboard(ClipboardItem::new_string(txt)); - - let settings = TerminalSettings::get_global(cx); - - if !settings.keep_selection_on_copy { + if !keep_selection.unwrap_or_else(|| { + let settings = TerminalSettings::get_global(cx); + settings.keep_selection_on_copy + }) { self.events.push_back(InternalEvent::SetSelection(None)); } } @@ -1108,8 +1109,8 @@ impl Terminal { .push_back(InternalEvent::SetSelection(selection)); } - pub fn copy(&mut self) { - self.events.push_back(InternalEvent::Copy); + pub fn copy(&mut self, keep_selection: Option<bool>) { + self.events.push_back(InternalEvent::Copy(keep_selection)); } pub fn clear(&mut self) { @@ -1267,8 +1268,7 @@ impl Terminal { } "y" => { - self.events.push_back(InternalEvent::Copy); - self.events.push_back(InternalEvent::SetSelection(None)); + self.copy(Some(false)); return; } @@ -1653,7 +1653,7 @@ impl Terminal { } } else { if e.button == MouseButton::Left && setting.copy_on_select { - self.copy(); + self.copy(Some(true)); } //Hyperlinks diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 76ec9dcb2591a3d0f5483507f25a7c036464e93d..bad3ebd479fbf2deeaeeef08974a03d900c35389 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -715,7 +715,7 @@ impl TerminalView { ///Attempt to paste the clipboard into the terminal fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) { - self.terminal.update(cx, |term, _| term.copy()); + self.terminal.update(cx, |term, _| term.copy(None)); cx.notify(); } From 00ec2437710127328bc11e6c7c57f27837606c14 Mon Sep 17 00:00:00 2001 From: vipex <101529155+vipexv@users.noreply.github.com> Date: Mon, 14 Jul 2025 13:06:40 +0200 Subject: [PATCH 053/658] pane: 'Close others' now closes relative to right-clicked tab (#34355) Closes #33445 Fixed the "Close others" context menu action to close tabs relative to the right-clicked tab instead of the currently active tab. Previously, when right-clicking on an inactive tab and selecting "Close others", it would keep the active tab open rather than the right-clicked tab. ## Before/After https://github.com/user-attachments/assets/d76854c3-c490-4a41-8166-309dec26ba8a ## Changes - Modified `close_inactive_items()` method to accept an optional `target_item_id` parameter - Updated context menu handler to pass the right-clicked tab's ID as the target - Maintained backward compatibility by defaulting to active tab when no target is specified - Updated all existing call sites to pass `None` for the new parameter Release Notes: - Fixed: "Close others" context menu action now correctly keeps the right-clicked tab open instead of the active tab --- crates/collab/src/tests/following_tests.rs | 2 +- crates/editor/src/editor_tests.rs | 4 ++-- crates/workspace/src/pane.rs | 12 ++++++++++-- crates/workspace/src/workspace.rs | 2 ++ 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index a77112213f195190e613c2382300bfbbeca70066..3aa86a434dac611be260eb7f281d9067812c15ac 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -1013,7 +1013,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T // and some of which were originally opened by client B. workspace_b.update_in(cx_b, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.close_inactive_items(&Default::default(), window, cx) + pane.close_inactive_items(&Default::default(), None, window, cx) .detach(); }); }); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index b9078301c771372dccf596c107cb96f9352f0251..0d61f2f8586279a49358be68e78b596d6d6ea70e 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -21465,7 +21465,7 @@ println!("5"); .unwrap(); pane_1 .update_in(cx, |pane, window, cx| { - pane.close_inactive_items(&CloseInactiveItems::default(), window, cx) + pane.close_inactive_items(&CloseInactiveItems::default(), None, window, cx) }) .await .unwrap(); @@ -21501,7 +21501,7 @@ println!("5"); .unwrap(); pane_2 .update_in(cx, |pane, window, cx| { - pane.close_inactive_items(&CloseInactiveItems::default(), window, cx) + pane.close_inactive_items(&CloseInactiveItems::default(), None, window, cx) }) .await .unwrap(); diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 56db7fa57009b739909bcfa40c4b0a28967f776b..233d435f4b3209d90e42e88e5180b727045ae641 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1346,6 +1346,7 @@ impl Pane { pub fn close_inactive_items( &mut self, action: &CloseInactiveItems, + target_item_id: Option<EntityId>, window: &mut Window, cx: &mut Context<Self>, ) -> Task<Result<()>> { @@ -1353,7 +1354,11 @@ impl Pane { return Task::ready(Ok(())); } - let active_item_id = self.active_item_id(); + let active_item_id = match target_item_id { + Some(result) => result, + None => self.active_item_id(), + }; + let pinned_item_ids = self.pinned_item_ids(); self.close_items( @@ -2596,6 +2601,7 @@ impl Pane { .handler(window.handler_for(&pane, move |pane, window, cx| { pane.close_inactive_items( &close_inactive_items_action, + Some(item_id), window, cx, ) @@ -3505,7 +3511,7 @@ impl Render for Pane { ) .on_action( cx.listener(|pane: &mut Self, action: &CloseInactiveItems, window, cx| { - pane.close_inactive_items(action, window, cx) + pane.close_inactive_items(action, None, window, cx) .detach_and_log_err(cx); }), ) @@ -5841,6 +5847,7 @@ mod tests { save_intent: None, close_pinned: false, }, + None, window, cx, ) @@ -6206,6 +6213,7 @@ mod tests { save_intent: None, close_pinned: false, }, + None, window, cx, ) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 125b5bb98a7dd056880305e631c44bea815a6009..99ab3cf6b716991da672fbc8b24f176b41a4f541 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2777,6 +2777,7 @@ impl Workspace { save_intent: None, close_pinned: false, }, + None, window, cx, ) @@ -9452,6 +9453,7 @@ mod tests { save_intent: Some(SaveIntent::Save), close_pinned: true, }, + None, window, cx, ) From 2edf85f054480c3773e616779ecab4592391816d Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon <oleksiy@zed.dev> Date: Mon, 14 Jul 2025 14:26:15 +0300 Subject: [PATCH 054/658] evals: Switch disable_cursor_blinking to determenistic asserts (#34398) Release Notes: - N/A --- .../assistant_tools/src/edit_agent/evals.rs | 24 +++++++------ .../disable_cursor_blinking/possible-01.diff | 28 +++++++++++++++ .../disable_cursor_blinking/possible-02.diff | 29 ++++++++++++++++ .../disable_cursor_blinking/possible-03.diff | 34 +++++++++++++++++++ .../disable_cursor_blinking/possible-04.diff | 33 ++++++++++++++++++ 5 files changed, 137 insertions(+), 11 deletions(-) create mode 100644 crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-01.diff create mode 100644 crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-02.diff create mode 100644 crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-03.diff create mode 100644 crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-04.diff diff --git a/crates/assistant_tools/src/edit_agent/evals.rs b/crates/assistant_tools/src/edit_agent/evals.rs index d2ee03f08f142b024b69eeaea739ba121c35b375..c7af7dc64e4507dda9e7140e22652c50501e3e75 100644 --- a/crates/assistant_tools/src/edit_agent/evals.rs +++ b/crates/assistant_tools/src/edit_agent/evals.rs @@ -365,17 +365,23 @@ fn eval_disable_cursor_blinking() { // Model | Pass rate // ============================================ // - // claude-3.7-sonnet | 0.99 (2025-06-14) - // claude-sonnet-4 | 0.85 (2025-06-14) - // gemini-2.5-pro-preview-latest | 0.97 (2025-06-16) - // gemini-2.5-flash-preview-04-17 | - // gpt-4.1 | + // claude-3.7-sonnet | 0.59 (2025-07-14) + // claude-sonnet-4 | 0.81 (2025-07-14) + // gemini-2.5-pro | 0.95 (2025-07-14) + // gemini-2.5-flash-preview-04-17 | 0.78 (2025-07-14) + // gpt-4.1 | 0.00 (2025-07-14) (follows edit_description too literally) let input_file_path = "root/editor.rs"; let input_file_content = include_str!("evals/fixtures/disable_cursor_blinking/before.rs"); let edit_description = "Comment out the call to `BlinkManager::enable`"; + let possible_diffs = vec![ + include_str!("evals/fixtures/disable_cursor_blinking/possible-01.diff"), + include_str!("evals/fixtures/disable_cursor_blinking/possible-02.diff"), + include_str!("evals/fixtures/disable_cursor_blinking/possible-03.diff"), + include_str!("evals/fixtures/disable_cursor_blinking/possible-04.diff"), + ]; eval( 100, - 0.95, + 0.51, 0.05, EvalInput::from_conversation( vec![ @@ -433,11 +439,7 @@ fn eval_disable_cursor_blinking() { ), ], Some(input_file_content.into()), - EvalAssertion::judge_diff(indoc! {" - - Calls to BlinkManager in `observe_window_activation` were commented out - - The call to `blink_manager.enable` above the call to show_cursor_names was commented out - - All the edits have valid indentation - "}), + EvalAssertion::assert_diff_any(possible_diffs), ), ); } diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-01.diff b/crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-01.diff new file mode 100644 index 0000000000000000000000000000000000000000..1a38a1967f94c974de491c712babb7882020d697 --- /dev/null +++ b/crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-01.diff @@ -0,0 +1,28 @@ +--- before.rs 2025-07-07 11:37:48.434629001 +0300 ++++ expected.rs 2025-07-14 10:33:53.346906775 +0300 +@@ -1780,11 +1780,11 @@ + cx.observe_window_activation(window, |editor, window, cx| { + let active = window.is_window_active(); + editor.blink_manager.update(cx, |blink_manager, cx| { +- if active { +- blink_manager.enable(cx); +- } else { +- blink_manager.disable(cx); +- } ++ // if active { ++ // blink_manager.enable(cx); ++ // } else { ++ // blink_manager.disable(cx); ++ // } + }); + }), + ], +@@ -18463,7 +18463,7 @@ + } + + self.blink_manager.update(cx, |blink_manager, cx| { +- blink_manager.enable(cx); ++ // blink_manager.enable(cx); + }); + self.show_cursor_names(window, cx); + self.buffer.update(cx, |buffer, cx| { diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-02.diff b/crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-02.diff new file mode 100644 index 0000000000000000000000000000000000000000..b484cce48f71b232ddaa947a73940b8bf11846c6 --- /dev/null +++ b/crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-02.diff @@ -0,0 +1,29 @@ +@@ -1778,13 +1778,13 @@ + cx.observe_global_in::<SettingsStore>(window, Self::settings_changed), + observe_buffer_font_size_adjustment(cx, |_, cx| cx.notify()), + cx.observe_window_activation(window, |editor, window, cx| { +- let active = window.is_window_active(); ++ // let active = window.is_window_active(); + editor.blink_manager.update(cx, |blink_manager, cx| { +- if active { +- blink_manager.enable(cx); +- } else { +- blink_manager.disable(cx); +- } ++ // if active { ++ // blink_manager.enable(cx); ++ // } else { ++ // blink_manager.disable(cx); ++ // } + }); + }), + ], +@@ -18463,7 +18463,7 @@ + } + + self.blink_manager.update(cx, |blink_manager, cx| { +- blink_manager.enable(cx); ++ // blink_manager.enable(cx); + }); + self.show_cursor_names(window, cx); + self.buffer.update(cx, |buffer, cx| { diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-03.diff b/crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-03.diff new file mode 100644 index 0000000000000000000000000000000000000000..431e34e48a250bff80efbd5a2cc20ecc25be1020 --- /dev/null +++ b/crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-03.diff @@ -0,0 +1,34 @@ +@@ -1774,17 +1774,17 @@ + cx.observe(&buffer, Self::on_buffer_changed), + cx.subscribe_in(&buffer, window, Self::on_buffer_event), + cx.observe_in(&display_map, window, Self::on_display_map_changed), +- cx.observe(&blink_manager, |_, _, cx| cx.notify()), ++ // cx.observe(&blink_manager, |_, _, cx| cx.notify()), + cx.observe_global_in::<SettingsStore>(window, Self::settings_changed), + observe_buffer_font_size_adjustment(cx, |_, cx| cx.notify()), + cx.observe_window_activation(window, |editor, window, cx| { +- let active = window.is_window_active(); ++ // let active = window.is_window_active(); + editor.blink_manager.update(cx, |blink_manager, cx| { +- if active { +- blink_manager.enable(cx); +- } else { +- blink_manager.disable(cx); +- } ++ // if active { ++ // blink_manager.enable(cx); ++ // } else { ++ // blink_manager.disable(cx); ++ // } + }); + }), + ], +@@ -18463,7 +18463,7 @@ + } + + self.blink_manager.update(cx, |blink_manager, cx| { +- blink_manager.enable(cx); ++ // blink_manager.enable(cx); + }); + self.show_cursor_names(window, cx); + self.buffer.update(cx, |buffer, cx| { diff --git a/crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-04.diff b/crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-04.diff new file mode 100644 index 0000000000000000000000000000000000000000..64a6b85dd3751407db65da74656b66ee1beaf58b --- /dev/null +++ b/crates/assistant_tools/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-04.diff @@ -0,0 +1,33 @@ +@@ -1774,17 +1774,17 @@ + cx.observe(&buffer, Self::on_buffer_changed), + cx.subscribe_in(&buffer, window, Self::on_buffer_event), + cx.observe_in(&display_map, window, Self::on_display_map_changed), +- cx.observe(&blink_manager, |_, _, cx| cx.notify()), ++ // cx.observe(&blink_manager, |_, _, cx| cx.notify()), + cx.observe_global_in::<SettingsStore>(window, Self::settings_changed), + observe_buffer_font_size_adjustment(cx, |_, cx| cx.notify()), + cx.observe_window_activation(window, |editor, window, cx| { + let active = window.is_window_active(); + editor.blink_manager.update(cx, |blink_manager, cx| { +- if active { +- blink_manager.enable(cx); +- } else { +- blink_manager.disable(cx); +- } ++ // if active { ++ // blink_manager.enable(cx); ++ // } else { ++ // blink_manager.disable(cx); ++ // } + }); + }), + ], +@@ -18463,7 +18463,7 @@ + } + + self.blink_manager.update(cx, |blink_manager, cx| { +- blink_manager.enable(cx); ++ // blink_manager.enable(cx); + }); + self.show_cursor_names(window, cx); + self.buffer.update(cx, |buffer, cx| { From 6f9e052edb347d6af5fc98015657928ba4300790 Mon Sep 17 00:00:00 2001 From: Brian Donovan <1938+eventualbuddha@users.noreply.github.com> Date: Mon, 14 Jul 2025 05:26:17 -0700 Subject: [PATCH 055/658] languages: Add JS/TS generator functions to outline (#34388) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Functions like `function* iterateElements() {}` would not show up in the editor's navigation outline. With this change, they do. | **Before** | **After** |-|-| |<img width="453" height="280" alt="Screenshot 2025-07-13 at 4 58 22 PM" src="https://github.com/user-attachments/assets/822f0774-bda2-4855-a6dd-80ba82fffaf3" />|<img width="564" height="373" alt="Screenshot 2025-07-13 at 4 58 55 PM" src="https://github.com/user-attachments/assets/f4f6b84f-cd26-49b7-923b-724860eb18ad" />| Note that I decided to use Zed's agent assistance features to do this PR as a sort of test run. I don't normally code with an AI assistant, but figured it might be good in this case since I'm unfamiliar with the codebase. I must say I was fairly impressed. All the changes in this PR were done by Claude Sonnet 4, though I have done a manual review to ensure the changes look sane and tested the changes by running the re-built `zed` binary with a toy project. Closes #21631 Release Notes: - Fixed JS/TS outlines to show generator functions. --- crates/languages/src/javascript/outline.scm | 9 ++++ crates/languages/src/tsx/outline.scm | 9 ++++ crates/languages/src/typescript.rs | 56 +++++++++++++++++++++ crates/languages/src/typescript/outline.scm | 9 ++++ 4 files changed, 83 insertions(+) diff --git a/crates/languages/src/javascript/outline.scm b/crates/languages/src/javascript/outline.scm index 99aa4bdfd5ad530505ebb90dc075e5ca405a5451..026c71e1f91d323ff2370828f330e4a4944e74db 100644 --- a/crates/languages/src/javascript/outline.scm +++ b/crates/languages/src/javascript/outline.scm @@ -14,6 +14,15 @@ "(" @context ")" @context)) @item +(generator_function_declaration + "async"? @context + "function" @context + "*" @context + name: (_) @name + parameters: (formal_parameters + "(" @context + ")" @context)) @item + (interface_declaration "interface" @context name: (_) @name) @item diff --git a/crates/languages/src/tsx/outline.scm b/crates/languages/src/tsx/outline.scm index df6ffa5aec8aa1b23b8179d0c341231feea5c0b5..5dafe791e493d03f6a73fa7c155ebb03072dc4d5 100644 --- a/crates/languages/src/tsx/outline.scm +++ b/crates/languages/src/tsx/outline.scm @@ -18,6 +18,15 @@ "(" @context ")" @context)) @item +(generator_function_declaration + "async"? @context + "function" @context + "*" @context + name: (_) @name + parameters: (formal_parameters + "(" @context + ")" @context)) @item + (interface_declaration "interface" @context name: (_) @name) @item diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index 3c1ecdcd5c51ee0d10a886327bac488b44621f6d..34b9c3224eecf9c86dd61cf64cb0d5e33572a810 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -1075,6 +1075,62 @@ mod tests { ); } + #[gpui::test] + async fn test_generator_function_outline(cx: &mut TestAppContext) { + let language = crate::language("javascript", tree_sitter_typescript::LANGUAGE_TSX.into()); + + let text = r#" + function normalFunction() { + console.log("normal"); + } + + function* simpleGenerator() { + yield 1; + yield 2; + } + + async function* asyncGenerator() { + yield await Promise.resolve(1); + } + + function* generatorWithParams(start, end) { + for (let i = start; i <= end; i++) { + yield i; + } + } + + class TestClass { + *methodGenerator() { + yield "method"; + } + + async *asyncMethodGenerator() { + yield "async method"; + } + } + "# + .unindent(); + + let buffer = cx.new(|cx| language::Buffer::local(text, cx).with_language(language, cx)); + let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None).unwrap()); + assert_eq!( + outline + .items + .iter() + .map(|item| (item.text.as_str(), item.depth)) + .collect::<Vec<_>>(), + &[ + ("function normalFunction()", 0), + ("function* simpleGenerator()", 0), + ("async function* asyncGenerator()", 0), + ("function* generatorWithParams( )", 0), + ("class TestClass", 0), + ("*methodGenerator()", 1), + ("async *asyncMethodGenerator()", 1), + ] + ); + } + #[gpui::test] async fn test_package_json_discovery(executor: BackgroundExecutor, cx: &mut TestAppContext) { cx.update(|cx| { diff --git a/crates/languages/src/typescript/outline.scm b/crates/languages/src/typescript/outline.scm index df6ffa5aec8aa1b23b8179d0c341231feea5c0b5..5dafe791e493d03f6a73fa7c155ebb03072dc4d5 100644 --- a/crates/languages/src/typescript/outline.scm +++ b/crates/languages/src/typescript/outline.scm @@ -18,6 +18,15 @@ "(" @context ")" @context)) @item +(generator_function_declaration + "async"? @context + "function" @context + "*" @context + name: (_) @name + parameters: (formal_parameters + "(" @context + ")" @context)) @item + (interface_declaration "interface" @context name: (_) @name) @item From c6a6db97547bec38ee9e3fca50fa0c437d9f687f Mon Sep 17 00:00:00 2001 From: Peter Tripp <peter@zed.dev> Date: Mon, 14 Jul 2025 09:16:25 -0400 Subject: [PATCH 056/658] emacs: Fix cmd-f not working in Terminal (#34400) Release Notes: - N/A --- assets/keymaps/default-macos.json | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index ffe8a344b30884b3911c9efb0ed2afc72ba1182b..85766ecc3eb63a4bebb027f08c31850e6e0a8eca 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -1098,6 +1098,7 @@ "ctrl-cmd-space": "terminal::ShowCharacterPalette", "cmd-c": "terminal::Copy", "cmd-v": "terminal::Paste", + "cmd-f": "buffer_search::Deploy", "cmd-a": "editor::SelectAll", "cmd-k": "terminal::Clear", "cmd-n": "workspace::NewTerminal", From a2f5c47e2d91053a5e1a73d4eb7c65b5086a7fbc Mon Sep 17 00:00:00 2001 From: Peter Tripp <peter@zed.dev> Date: Mon, 14 Jul 2025 09:23:51 -0400 Subject: [PATCH 057/658] Add `editor::ToggleFoldAll` action (#34317) In multibuffers adds the ability to alt-click to fold/unfold all excepts. In singleton buffers it adds the ability to toggle back and forth between `editor::FoldAll` and `editor::UnfoldAll`. Bind it in your keymap with: ```json { "context": "Editor && (mode == full || multibuffer)", "bindings": { "cmd-k cmd-o": "editor::ToggleFoldAll" } }, ``` <img width="253" height="99" alt="Screenshot 2025-07-11 at 17 04 25" src="https://github.com/user-attachments/assets/94de8275-d2ee-4cf8-a46c-a698ccdb60e3" /> Release Notes: - Add ability to fold all excerpts in a multibuffer (alt-click) and in singleton buffers `editor::ToggleFoldAll` --- crates/editor/src/actions.rs | 2 ++ crates/editor/src/editor.rs | 40 ++++++++++++++++++++++++++++++++++++ crates/editor/src/element.rs | 32 +++++++++++++++++++++-------- 3 files changed, 65 insertions(+), 9 deletions(-) diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 70ec8ea00f52dccbab9e2a3ad4856599a8a94acf..e12bdf7a0707ad6f0a6d05c6d7577dd38e525335 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -410,6 +410,8 @@ actions!( ToggleFold, /// Toggles recursive folding at the current position. ToggleFoldRecursive, + /// Toggles all folds in a buffer or all excerpts in multibuffer. + ToggleFoldAll, /// Formats the entire document. Format, /// Formats only the selected text. diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a25a96cdabd30d43a60b6ab61b2e0ed2e3e41de7..5065564fdba2f0abf6f0fd6390c1528c2a95bf44 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -17075,6 +17075,46 @@ impl Editor { } } + pub fn toggle_fold_all( + &mut self, + _: &actions::ToggleFoldAll, + window: &mut Window, + cx: &mut Context<Self>, + ) { + if self.buffer.read(cx).is_singleton() { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let has_folds = display_map + .folds_in_range(0..display_map.buffer_snapshot.len()) + .next() + .is_some(); + + if has_folds { + self.unfold_all(&actions::UnfoldAll, window, cx); + } else { + self.fold_all(&actions::FoldAll, window, cx); + } + } else { + let buffer_ids = self.buffer.read(cx).excerpt_buffer_ids(); + let should_unfold = buffer_ids + .iter() + .any(|buffer_id| self.is_buffer_folded(*buffer_id, cx)); + + self.toggle_fold_multiple_buffers = cx.spawn_in(window, async move |editor, cx| { + editor + .update_in(cx, |editor, _, cx| { + for buffer_id in buffer_ids { + if should_unfold { + editor.unfold_buffer(buffer_id, cx); + } else { + editor.fold_buffer(buffer_id, cx); + } + } + }) + .ok(); + }); + } + } + fn fold_at_level( &mut self, fold_at: &FoldAtLevel, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 8355fa1d66ba9e378cc2276592c8d988e5e91512..f9a31cffb10a57859b8030b9ab59a7866ab69d15 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -9,7 +9,7 @@ use crate::{ 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, + ToggleFold, ToggleFoldAll, code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP}, display_map::{ Block, BlockContext, BlockStyle, ChunkRendererId, DisplaySnapshot, EditorMargins, @@ -416,6 +416,7 @@ impl EditorElement { register_action(editor, window, Editor::fold_recursive); register_action(editor, window, Editor::toggle_fold); register_action(editor, window, Editor::toggle_fold_recursive); + register_action(editor, window, Editor::toggle_fold_all); register_action(editor, window, Editor::unfold_lines); register_action(editor, window, Editor::unfold_recursive); register_action(editor, window, Editor::unfold_all); @@ -3620,24 +3621,37 @@ impl EditorElement { .tooltip({ let focus_handle = focus_handle.clone(); move |window, cx| { - Tooltip::for_action_in( + Tooltip::with_meta_in( "Toggle Excerpt Fold", - &ToggleFold, + Some(&ToggleFold), + "Alt+click to toggle all", &focus_handle, window, cx, ) } }) - .on_click(move |_, _, cx| { - 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); + }); + } } }), ), From 6673c7cd4c42f2a583a1ff7d73e6bda1413910d4 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 14 Jul 2025 16:32:06 +0200 Subject: [PATCH 058/658] debugger: Add memory view (#33955) This is mostly setting up the UI for now; I expect it to be the biggest chunk of work. Release Notes: - debugger: Added memory view --------- Co-authored-by: Anthony Eid <hello@anthonyeid.me> Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com> Co-authored-by: Mikayla Maki <mikayla@zed.dev> --- Cargo.lock | 14 + Cargo.toml | 1 + assets/icons/location_edit.svg | 1 + crates/debugger_ui/Cargo.toml | 3 + crates/debugger_ui/src/debugger_panel.rs | 2 + crates/debugger_ui/src/persistence.rs | 17 +- crates/debugger_ui/src/session/running.rs | 54 +- .../src/session/running/memory_view.rs | 902 ++++++++++++++++++ .../src/session/running/variable_list.rs | 174 ++-- crates/debugger_ui/src/tests/module_list.rs | 1 - crates/gpui/src/elements/div.rs | 2 +- crates/icons/src/icons.rs | 1 + crates/project/Cargo.toml | 2 + crates/project/src/debugger.rs | 2 + crates/project/src/debugger/dap_command.rs | 93 ++ crates/project/src/debugger/memory.rs | 384 ++++++++ crates/project/src/debugger/session.rs | 148 ++- crates/ui/src/components/context_menu.rs | 2 +- 18 files changed, 1732 insertions(+), 71 deletions(-) create mode 100644 assets/icons/location_edit.svg create mode 100644 crates/debugger_ui/src/session/running/memory_view.rs create mode 100644 crates/project/src/debugger/memory.rs diff --git a/Cargo.lock b/Cargo.lock index 97b1e3321112e6a2edd3da92703e846c24005f56..da46d191efb0723f2411ba506df259f4cb56ceaf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4393,12 +4393,15 @@ dependencies = [ "futures 0.3.31", "fuzzy", "gpui", + "hex", "indoc", "itertools 0.14.0", "language", "log", "menu", + "notifications", "parking_lot", + "parse_int", "paths", "picker", "pretty_assertions", @@ -11276,6 +11279,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "parse_int" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c464266693329dd5a8715098c7f86e6c5fd5d985018b8318f53d9c6c2b21a31" +dependencies = [ + "num-traits", +] + [[package]] name = "partial-json-fixer" version = "0.5.3" @@ -12319,6 +12331,7 @@ dependencies = [ "anyhow", "askpass", "async-trait", + "base64 0.22.1", "buffer_diff", "circular-buffer", "client", @@ -12364,6 +12377,7 @@ dependencies = [ "sha2", "shellexpand 2.1.2", "shlex", + "smallvec", "smol", "snippet", "snippet_provider", diff --git a/Cargo.toml b/Cargo.toml index dedf5700524eb5041025656d9b3f6b3a8a2325c3..e270dd1891f8bb6b3e2cea4916ffdf242905f1e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -507,6 +507,7 @@ ordered-float = "2.1.1" 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" 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/assets/icons/location_edit.svg b/assets/icons/location_edit.svg new file mode 100644 index 0000000000000000000000000000000000000000..de82e8db4e05da232d024d6a92e329fd15a94ff0 --- /dev/null +++ b/assets/icons/location_edit.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-location-edit-icon lucide-location-edit"><path d="M17.97 9.304A8 8 0 0 0 2 10c0 4.69 4.887 9.562 7.022 11.468"/><path d="M21.378 16.626a1 1 0 0 0-3.004-3.004l-4.01 4.012a2 2 0 0 0-.506.854l-.837 2.87a.5.5 0 0 0 .62.62l2.87-.837a2 2 0 0 0 .854-.506z"/><circle cx="10" cy="10" r="3"/></svg> diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index fe9640b7b9e2276ab16066672d267cb4ce432c43..ebb135c1d9fc56e21b40bd0a4f9850d72286d866 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -40,12 +40,15 @@ file_icons.workspace = true futures.workspace = true fuzzy.workspace = true gpui.workspace = true +hex.workspace = true indoc.workspace = true itertools.workspace = true language.workspace = true log.workspace = true menu.workspace = true +notifications.workspace = true parking_lot.workspace = true +parse_int.workspace = true paths.workspace = true picker.workspace = true pretty_assertions.workspace = true diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index 37064d5d5dba5075e52e6a6047cea77133a1ccfb..bf5f31391885edf89beea3e8648df13f68258a77 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -2,6 +2,7 @@ use crate::persistence::DebuggerPaneItem; use crate::session::DebugSession; use crate::session::running::RunningState; use crate::session::running::breakpoint_list::BreakpointList; + use crate::{ ClearAllBreakpoints, Continue, CopyDebugAdapterArguments, Detach, FocusBreakpointList, FocusConsole, FocusFrames, FocusLoadedSources, FocusModules, FocusTerminal, FocusVariables, @@ -1804,6 +1805,7 @@ impl Render for DebugPanel { .child(breakpoint_list) .child(Divider::vertical()) .child(welcome_experience) + .child(Divider::vertical()) } else { this.items_end() .child(welcome_experience) diff --git a/crates/debugger_ui/src/persistence.rs b/crates/debugger_ui/src/persistence.rs index d15244c3496b5cb42bf1b4151e075f624862aec0..3a0ad7a40e60d4dc28f2086b94a0a43186978542 100644 --- a/crates/debugger_ui/src/persistence.rs +++ b/crates/debugger_ui/src/persistence.rs @@ -11,7 +11,7 @@ use workspace::{Member, Pane, PaneAxis, Workspace}; use crate::session::running::{ self, DebugTerminal, RunningState, SubView, breakpoint_list::BreakpointList, console::Console, - loaded_source_list::LoadedSourceList, module_list::ModuleList, + loaded_source_list::LoadedSourceList, memory_view::MemoryView, module_list::ModuleList, stack_frame_list::StackFrameList, variable_list::VariableList, }; @@ -24,6 +24,7 @@ pub(crate) enum DebuggerPaneItem { Modules, LoadedSources, Terminal, + MemoryView, } impl DebuggerPaneItem { @@ -36,6 +37,7 @@ impl DebuggerPaneItem { DebuggerPaneItem::Modules, DebuggerPaneItem::LoadedSources, DebuggerPaneItem::Terminal, + DebuggerPaneItem::MemoryView, ]; VARIANTS } @@ -43,6 +45,9 @@ impl DebuggerPaneItem { pub(crate) fn is_supported(&self, capabilities: &Capabilities) -> bool { match self { DebuggerPaneItem::Modules => capabilities.supports_modules_request.unwrap_or_default(), + DebuggerPaneItem::MemoryView => capabilities + .supports_read_memory_request + .unwrap_or_default(), DebuggerPaneItem::LoadedSources => capabilities .supports_loaded_sources_request .unwrap_or_default(), @@ -59,6 +64,7 @@ impl DebuggerPaneItem { DebuggerPaneItem::Modules => SharedString::new_static("Modules"), DebuggerPaneItem::LoadedSources => SharedString::new_static("Sources"), DebuggerPaneItem::Terminal => SharedString::new_static("Terminal"), + DebuggerPaneItem::MemoryView => SharedString::new_static("Memory View"), } } pub(crate) fn tab_tooltip(self) -> SharedString { @@ -80,6 +86,7 @@ impl DebuggerPaneItem { DebuggerPaneItem::Terminal => { "Provides an interactive terminal session within the debugging environment." } + DebuggerPaneItem::MemoryView => "Allows inspection of memory contents.", }; SharedString::new_static(tooltip) } @@ -204,6 +211,7 @@ pub(crate) fn deserialize_pane_layout( breakpoint_list: &Entity<BreakpointList>, loaded_sources: &Entity<LoadedSourceList>, terminal: &Entity<DebugTerminal>, + memory_view: &Entity<MemoryView>, subscriptions: &mut HashMap<EntityId, Subscription>, window: &mut Window, cx: &mut Context<RunningState>, @@ -228,6 +236,7 @@ pub(crate) fn deserialize_pane_layout( breakpoint_list, loaded_sources, terminal, + memory_view, subscriptions, window, cx, @@ -298,6 +307,12 @@ pub(crate) fn deserialize_pane_layout( DebuggerPaneItem::Terminal, cx, )), + DebuggerPaneItem::MemoryView => Box::new(SubView::new( + memory_view.focus_handle(cx), + memory_view.clone().into(), + DebuggerPaneItem::MemoryView, + cx, + )), }) .collect(); diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 264d46370f5d8b85a92f6f3dac1a319371a8ce03..2651a94520eddaba74a891e3a46eca9e019ea3f3 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -1,16 +1,17 @@ pub(crate) mod breakpoint_list; pub(crate) mod console; pub(crate) mod loaded_source_list; +pub(crate) mod memory_view; pub(crate) mod module_list; pub mod stack_frame_list; pub mod variable_list; - use std::{any::Any, ops::ControlFlow, path::PathBuf, sync::Arc, time::Duration}; use crate::{ ToggleExpandItem, new_process_modal::resolve_path, persistence::{self, DebuggerPaneItem, SerializedLayout}, + session::running::memory_view::MemoryView, }; use super::DebugPanelItemEvent; @@ -81,6 +82,7 @@ pub struct RunningState { _schedule_serialize: Option<Task<()>>, pub(crate) scenario: Option<DebugScenario>, pub(crate) scenario_context: Option<DebugScenarioContext>, + memory_view: Entity<MemoryView>, } impl RunningState { @@ -676,14 +678,36 @@ impl RunningState { let session_id = session.read(cx).session_id(); let weak_state = cx.weak_entity(); let stack_frame_list = cx.new(|cx| { - StackFrameList::new(workspace.clone(), session.clone(), weak_state, window, cx) + StackFrameList::new( + workspace.clone(), + session.clone(), + weak_state.clone(), + window, + cx, + ) }); let debug_terminal = parent_terminal.unwrap_or_else(|| cx.new(|cx| DebugTerminal::empty(window, cx))); - - let variable_list = - cx.new(|cx| VariableList::new(session.clone(), stack_frame_list.clone(), window, cx)); + let memory_view = cx.new(|cx| { + MemoryView::new( + session.clone(), + workspace.clone(), + stack_frame_list.downgrade(), + window, + cx, + ) + }); + let variable_list = cx.new(|cx| { + VariableList::new( + session.clone(), + stack_frame_list.clone(), + memory_view.clone(), + weak_state.clone(), + window, + cx, + ) + }); let module_list = cx.new(|cx| ModuleList::new(session.clone(), workspace.clone(), cx)); @@ -795,6 +819,7 @@ impl RunningState { &breakpoint_list, &loaded_source_list, &debug_terminal, + &memory_view, &mut pane_close_subscriptions, window, cx, @@ -823,6 +848,7 @@ impl RunningState { let active_pane = panes.first_pane(); Self { + memory_view, session, workspace, focus_handle, @@ -1234,6 +1260,12 @@ impl RunningState { item_kind, cx, )), + DebuggerPaneItem::MemoryView => Box::new(SubView::new( + self.memory_view.focus_handle(cx), + self.memory_view.clone().into(), + item_kind, + cx, + )), } } @@ -1418,7 +1450,14 @@ impl RunningState { &self.module_list } - pub(crate) fn activate_item(&self, item: DebuggerPaneItem, window: &mut Window, cx: &mut App) { + pub(crate) fn activate_item( + &mut self, + item: DebuggerPaneItem, + window: &mut Window, + cx: &mut Context<Self>, + ) { + self.ensure_pane_item(item, window, cx); + let (variable_list_position, pane) = self .panes .panes() @@ -1430,9 +1469,10 @@ impl RunningState { .map(|view| (view, pane)) }) .unwrap(); + pane.update(cx, |this, cx| { this.activate_item(variable_list_position, true, true, window, cx); - }) + }); } #[cfg(test)] diff --git a/crates/debugger_ui/src/session/running/memory_view.rs b/crates/debugger_ui/src/session/running/memory_view.rs new file mode 100644 index 0000000000000000000000000000000000000000..e9dcb0839d104e6a241378fb9f201f20196bed87 --- /dev/null +++ b/crates/debugger_ui/src/session/running/memory_view.rs @@ -0,0 +1,902 @@ +use std::{fmt::Write, ops::RangeInclusive, sync::LazyLock, time::Duration}; + +use editor::{Editor, EditorElement, EditorStyle}; +use gpui::{ + Action, AppContext, DismissEvent, Empty, Entity, FocusHandle, Focusable, MouseButton, + MouseMoveEvent, Point, ScrollStrategy, ScrollWheelEvent, Stateful, Subscription, Task, + TextStyle, UniformList, UniformListScrollHandle, WeakEntity, actions, anchored, bounds, + deferred, point, size, uniform_list, +}; +use notifications::status_toast::{StatusToast, ToastIcon}; +use project::debugger::{MemoryCell, session::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, +}; +use util::ResultExt; +use workspace::Workspace; + +use crate::session::running::stack_frame_list::StackFrameList; + +actions!(debugger, [GoToSelectedAddress]); + +pub(crate) struct MemoryView { + workspace: WeakEntity<Workspace>, + scroll_handle: UniformListScrollHandle, + scroll_state: ScrollbarState, + show_scrollbar: bool, + stack_frame_list: WeakEntity<StackFrameList>, + hide_scrollbar_task: Option<Task<()>>, + focus_handle: FocusHandle, + view_state: ViewState, + query_editor: Entity<Editor>, + session: Entity<Session>, + width_picker_handle: PopoverMenuHandle<ContextMenu>, + is_writing_memory: bool, + open_context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>, +} + +impl Focusable for MemoryView { + fn focus_handle(&self, _: &ui::App) -> FocusHandle { + self.focus_handle.clone() + } +} +#[derive(Clone, Debug)] +struct Drag { + start_address: u64, + end_address: u64, +} + +impl Drag { + fn contains(&self, address: u64) -> bool { + let range = self.memory_range(); + range.contains(&address) + } + + fn memory_range(&self) -> RangeInclusive<u64> { + if self.start_address < self.end_address { + self.start_address..=self.end_address + } else { + self.end_address..=self.start_address + } + } +} +#[derive(Clone, Debug)] +enum SelectedMemoryRange { + DragUnderway(Drag), + DragComplete(Drag), +} + +impl SelectedMemoryRange { + fn contains(&self, address: u64) -> bool { + match self { + SelectedMemoryRange::DragUnderway(drag) => drag.contains(address), + SelectedMemoryRange::DragComplete(drag) => drag.contains(address), + } + } + fn is_dragging(&self) -> bool { + matches!(self, SelectedMemoryRange::DragUnderway(_)) + } + fn drag(&self) -> &Drag { + match self { + SelectedMemoryRange::DragUnderway(drag) => drag, + SelectedMemoryRange::DragComplete(drag) => drag, + } + } +} + +#[derive(Clone)] +struct ViewState { + /// Uppermost row index + base_row: u64, + /// How many cells per row do we have? + line_width: ViewWidth, + selection: Option<SelectedMemoryRange>, +} + +impl ViewState { + fn new(base_row: u64, line_width: ViewWidth) -> Self { + Self { + base_row, + line_width, + selection: None, + } + } + fn row_count(&self) -> u64 { + // This was picked fully arbitrarily. There's no incentive for us to care about page sizes other than the fact that it seems to be a good + // middle ground for data size. + const PAGE_SIZE: u64 = 4096; + PAGE_SIZE / self.line_width.width as u64 + } + fn schedule_scroll_down(&mut self) { + self.base_row = self.base_row.saturating_add(1) + } + fn schedule_scroll_up(&mut self) { + self.base_row = self.base_row.saturating_sub(1); + } +} + +static HEX_BYTES_MEMOIZED: LazyLock<[SharedString; 256]> = + LazyLock::new(|| std::array::from_fn(|byte| SharedString::from(format!("{byte:02X}")))); +static UNKNOWN_BYTE: SharedString = SharedString::new_static("??"); +impl MemoryView { + pub(crate) fn new( + session: Entity<Session>, + workspace: WeakEntity<Workspace>, + stack_frame_list: WeakEntity<StackFrameList>, + window: &mut Window, + cx: &mut Context<Self>, + ) -> Self { + let view_state = ViewState::new(0, WIDTHS[4].clone()); + let scroll_handle = UniformListScrollHandle::default(); + + let query_editor = cx.new(|cx| Editor::single_line(window, cx)); + + let scroll_state = ScrollbarState::new(scroll_handle.clone()); + let mut this = Self { + workspace, + scroll_state, + scroll_handle, + stack_frame_list, + show_scrollbar: false, + hide_scrollbar_task: None, + focus_handle: cx.focus_handle(), + view_state, + query_editor, + session, + width_picker_handle: Default::default(), + is_writing_memory: true, + open_context_menu: None, + }; + this.change_query_bar_mode(false, window, cx); + this + } + fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) { + 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<Self>) -> Option<Stateful<Div>> { + if !(self.show_scrollbar || self.scroll_state.is_dragging()) { + return None; + } + Some( + div() + .occlude() + .id("memory-view-vertical-scrollbar") + .on_mouse_move(cx.listener(|this, evt, _, cx| { + this.handle_drag(evt); + 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.scroll_state.clone())), + ) + } + + fn render_memory(&self, cx: &mut Context<Self>) -> UniformList { + let weak = cx.weak_entity(); + let session = self.session.clone(); + let view_state = self.view_state.clone(); + uniform_list( + "debugger-memory-view", + self.view_state.row_count() as usize, + move |range, _, cx| { + let mut line_buffer = Vec::with_capacity(view_state.line_width.width as usize); + let memory_start = + (view_state.base_row + range.start as u64) * view_state.line_width.width as u64; + let memory_end = (view_state.base_row + range.end as u64) + * view_state.line_width.width as u64 + - 1; + let mut memory = session.update(cx, |this, cx| { + this.read_memory(memory_start..=memory_end, cx) + }); + let mut rows = Vec::with_capacity(range.end - range.start); + for ix in range { + line_buffer.extend((&mut memory).take(view_state.line_width.width as usize)); + rows.push(render_single_memory_view_line( + &line_buffer, + ix as u64, + weak.clone(), + cx, + )); + line_buffer.clear(); + } + rows + }, + ) + .track_scroll(self.scroll_handle.clone()) + .on_scroll_wheel(cx.listener(|this, evt: &ScrollWheelEvent, window, _| { + let delta = evt.delta.pixel_delta(window.line_height()); + let scroll_handle = this.scroll_state.scroll_handle(); + let size = scroll_handle.content_size(); + let viewport = scroll_handle.viewport(); + let current_offset = scroll_handle.offset(); + let first_entry_offset_boundary = size.height / this.view_state.row_count() as f32; + let last_entry_offset_boundary = size.height - first_entry_offset_boundary; + if first_entry_offset_boundary + viewport.size.height > current_offset.y.abs() { + // The topmost entry is visible, hence if we're scrolling up, we need to load extra lines. + this.view_state.schedule_scroll_up(); + } else if last_entry_offset_boundary < current_offset.y.abs() + viewport.size.height { + this.view_state.schedule_scroll_down(); + } + scroll_handle.set_offset(current_offset + point(px(0.), delta.y)); + })) + } + fn render_query_bar(&self, cx: &Context<Self>) -> impl IntoElement { + EditorElement::new( + &self.query_editor, + Self::editor_style(&self.query_editor, cx), + ) + } + pub(super) fn go_to_memory_reference( + &mut self, + memory_reference: &str, + evaluate_name: Option<&str>, + stack_frame_id: Option<u64>, + cx: &mut Context<Self>, + ) { + use parse_int::parse; + let Ok(as_address) = parse::<u64>(&memory_reference) else { + return; + }; + let access_size = evaluate_name + .map(|typ| { + self.session.update(cx, |this, cx| { + this.data_access_size(stack_frame_id, typ, cx) + }) + }) + .unwrap_or_else(|| Task::ready(None)); + cx.spawn(async move |this, cx| { + let access_size = access_size.await.unwrap_or(1); + this.update(cx, |this, cx| { + this.view_state.selection = Some(SelectedMemoryRange::DragComplete(Drag { + start_address: as_address, + end_address: as_address + access_size - 1, + })); + this.jump_to_address(as_address, cx); + }) + .ok(); + }) + .detach(); + } + + fn handle_drag(&mut self, evt: &MouseMoveEvent) { + if !evt.dragging() { + return; + } + if !self.scroll_state.is_dragging() + && !self + .view_state + .selection + .as_ref() + .is_some_and(|selection| selection.is_dragging()) + { + return; + } + let row_count = self.view_state.row_count(); + debug_assert!(row_count > 1); + let scroll_handle = self.scroll_state.scroll_handle(); + let viewport = scroll_handle.viewport(); + let (top_area, bottom_area) = { + let size = size(viewport.size.width, viewport.size.height / 10.); + ( + bounds(viewport.origin, size), + bounds( + point(viewport.origin.x, viewport.origin.y + size.height * 2.), + size, + ), + ) + }; + + if bottom_area.contains(&evt.position) { + //ix == row_count - 1 { + self.view_state.schedule_scroll_down(); + } else if top_area.contains(&evt.position) { + self.view_state.schedule_scroll_up(); + } + } + + fn editor_style(editor: &Entity<Editor>, cx: &Context<Self>) -> EditorStyle { + let is_read_only = editor.read(cx).read_only(cx); + let settings = ThemeSettings::get_global(cx); + let theme = cx.theme(); + let text_style = TextStyle { + color: if is_read_only { + theme.colors().text_muted + } else { + theme.colors().text + }, + font_family: settings.buffer_font.family.clone(), + font_features: settings.buffer_font.features.clone(), + font_size: TextSize::Small.rems(cx).into(), + font_weight: settings.buffer_font.weight, + + ..Default::default() + }; + EditorStyle { + background: theme.colors().editor_background, + local_player: theme.players().local(), + text: text_style, + ..Default::default() + } + } + + fn render_width_picker(&self, window: &mut Window, cx: &mut Context<Self>) -> DropdownMenu { + let weak = cx.weak_entity(); + let selected_width = self.view_state.line_width.clone(); + DropdownMenu::new( + "memory-view-width-picker", + selected_width.label.clone(), + ContextMenu::build(window, cx, |mut this, window, cx| { + for width in &WIDTHS { + let weak = weak.clone(); + let width = width.clone(); + this = this.entry(width.label.clone(), None, move |_, cx| { + _ = weak.update(cx, |this, _| { + // Convert base ix between 2 line widths to keep the shown memory address roughly the same. + // All widths are powers of 2, so the conversion should be lossless. + match this.view_state.line_width.width.cmp(&width.width) { + std::cmp::Ordering::Less => { + // We're converting up. + let shift = width.width.trailing_zeros() + - this.view_state.line_width.width.trailing_zeros(); + this.view_state.base_row >>= shift; + } + std::cmp::Ordering::Greater => { + // We're converting down. + let shift = this.view_state.line_width.width.trailing_zeros() + - width.width.trailing_zeros(); + this.view_state.base_row <<= shift; + } + _ => {} + } + this.view_state.line_width = width.clone(); + }); + }); + } + if let Some(ix) = WIDTHS + .iter() + .position(|width| width.width == selected_width.width) + { + for _ in 0..=ix { + this.select_next(&Default::default(), window, cx); + } + } + this + }), + ) + .handle(self.width_picker_handle.clone()) + } + + fn page_down(&mut self, _: &menu::SelectLast, _: &mut Window, cx: &mut Context<Self>) { + self.view_state.base_row = self + .view_state + .base_row + .overflowing_add(self.view_state.row_count()) + .0; + cx.notify(); + } + fn page_up(&mut self, _: &menu::SelectFirst, _: &mut Window, cx: &mut Context<Self>) { + self.view_state.base_row = self + .view_state + .base_row + .overflowing_sub(self.view_state.row_count()) + .0; + cx.notify(); + } + + fn change_query_bar_mode( + &mut self, + is_writing_memory: bool, + window: &mut Window, + cx: &mut Context<Self>, + ) { + if is_writing_memory == self.is_writing_memory { + return; + } + if !self.is_writing_memory { + self.query_editor.update(cx, |this, cx| { + this.clear(window, cx); + this.set_placeholder_text("Write to Selected Memory Range", cx); + }); + self.is_writing_memory = true; + self.query_editor.focus_handle(cx).focus(window); + } else { + self.query_editor.update(cx, |this, cx| { + this.clear(window, cx); + this.set_placeholder_text("Go to Memory Address / Expression", cx); + }); + self.is_writing_memory = false; + } + } + + fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) { + if let Some(SelectedMemoryRange::DragComplete(drag)) = &self.view_state.selection { + // Go into memory writing mode. + if !self.is_writing_memory { + let should_return = self.session.update(cx, |session, cx| { + if !session + .capabilities() + .supports_write_memory_request + .unwrap_or_default() + { + let adapter_name = session.adapter(); + // We cannot write memory with this adapter. + _ = self.workspace.update(cx, |this, cx| { + this.toggle_status_toast( + StatusToast::new(format!( + "Debug Adapter `{adapter_name}` does not support writing to memory" + ), cx, |this, cx| { + cx.spawn(async move |this, cx| { + cx.background_executor().timer(Duration::from_secs(2)).await; + _ = this.update(cx, |_, cx| { + cx.emit(DismissEvent) + }); + }).detach(); + this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error)) + }), + cx, + ); + }); + true + } else { + false + } + }); + if should_return { + return; + } + + self.change_query_bar_mode(true, window, cx); + } else if self.query_editor.focus_handle(cx).is_focused(window) { + let mut text = self.query_editor.read(cx).text(cx); + if text.chars().any(|c| !c.is_ascii_hexdigit()) { + // Interpret this text as a string and oh-so-conveniently convert it. + text = text.bytes().map(|byte| format!("{:02x}", byte)).collect(); + } + self.session.update(cx, |this, cx| { + let range = drag.memory_range(); + + if let Ok(as_hex) = hex::decode(text) { + this.write_memory(*range.start(), &as_hex, cx); + } + }); + self.change_query_bar_mode(false, window, cx); + } + + cx.notify(); + return; + } + // Just change the currently viewed address. + if !self.query_editor.focus_handle(cx).is_focused(window) { + return; + } + self.jump_to_query_bar_address(cx); + } + + fn jump_to_query_bar_address(&mut self, cx: &mut Context<Self>) { + use parse_int::parse; + let text = self.query_editor.read(cx).text(cx); + + let Ok(as_address) = parse::<u64>(&text) else { + return self.jump_to_expression(text, cx); + }; + self.jump_to_address(as_address, cx); + } + + fn jump_to_address(&mut self, address: u64, cx: &mut Context<Self>) { + self.view_state.base_row = (address & !0xfff) / self.view_state.line_width.width as u64; + let line_ix = (address & 0xfff) / self.view_state.line_width.width as u64; + self.scroll_handle + .scroll_to_item(line_ix as usize, ScrollStrategy::Center); + cx.notify(); + } + + fn jump_to_expression(&mut self, expr: String, cx: &mut Context<Self>) { + let Ok(selected_frame) = self + .stack_frame_list + .update(cx, |this, _| this.opened_stack_frame_id()) + else { + return; + }; + let reference = self.session.update(cx, |this, cx| { + this.memory_reference_of_expr(selected_frame, expr, cx) + }); + cx.spawn(async move |this, cx| { + if let Some(reference) = reference.await { + _ = this.update(cx, |this, cx| { + let Ok(address) = parse_int::parse::<u64>(&reference) else { + return; + }; + this.jump_to_address(address, cx); + }); + } + }) + .detach(); + } + + fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) { + self.view_state.selection = None; + cx.notify(); + } + + /// Jump to memory pointed to by selected memory range. + fn go_to_address( + &mut self, + _: &GoToSelectedAddress, + window: &mut Window, + cx: &mut Context<Self>, + ) { + let Some(SelectedMemoryRange::DragComplete(drag)) = self.view_state.selection.clone() + else { + return; + }; + let range = drag.memory_range(); + let Some(memory): Option<Vec<u8>> = self.session.update(cx, |this, cx| { + this.read_memory(range, cx).map(|cell| cell.0).collect() + }) else { + return; + }; + if memory.len() > 8 { + return; + } + let zeros_to_write = 8 - memory.len(); + let mut acc = String::from("0x"); + acc.extend(std::iter::repeat("00").take(zeros_to_write)); + let as_query = memory.into_iter().rev().fold(acc, |mut acc, byte| { + _ = write!(&mut acc, "{:02x}", byte); + acc + }); + self.query_editor.update(cx, |this, cx| { + this.set_text(as_query, window, cx); + }); + self.jump_to_query_bar_address(cx); + } + + fn deploy_memory_context_menu( + &mut self, + range: RangeInclusive<u64>, + position: Point<Pixels>, + window: &mut Window, + cx: &mut Context<Self>, + ) { + let session = self.session.clone(); + let context_menu = ContextMenu::build(window, cx, |menu, _, cx| { + let range_too_large = range.end() - range.start() > std::mem::size_of::<u64>() as u64; + let memory_unreadable = |cx| { + session.update(cx, |this, cx| { + this.read_memory(range.clone(), cx) + .any(|cell| cell.0.is_none()) + }) + }; + menu.action_disabled_when( + range_too_large || memory_unreadable(cx), + "Go To Selected Address", + GoToSelectedAddress.boxed_clone(), + ) + .context(self.focus_handle.clone()) + }); + + cx.focus_view(&context_menu, window); + let subscription = cx.subscribe_in( + &context_menu, + window, + |this, _, _: &DismissEvent, window, cx| { + if this.open_context_menu.as_ref().is_some_and(|context_menu| { + context_menu.0.focus_handle(cx).contains_focused(window, cx) + }) { + cx.focus_self(window); + } + this.open_context_menu.take(); + cx.notify(); + }, + ); + + self.open_context_menu = Some((context_menu, position, subscription)); + } +} + +#[derive(Clone)] +struct ViewWidth { + width: u8, + label: SharedString, +} + +impl ViewWidth { + const fn new(width: u8, label: &'static str) -> Self { + Self { + width, + label: SharedString::new_static(label), + } + } +} + +static WIDTHS: [ViewWidth; 7] = [ + ViewWidth::new(1, "1 byte"), + ViewWidth::new(2, "2 bytes"), + ViewWidth::new(4, "4 bytes"), + ViewWidth::new(8, "8 bytes"), + ViewWidth::new(16, "16 bytes"), + ViewWidth::new(32, "32 bytes"), + ViewWidth::new(64, "64 bytes"), +]; + +fn render_single_memory_view_line( + memory: &[MemoryCell], + ix: u64, + weak: gpui::WeakEntity<MemoryView>, + cx: &mut App, +) -> AnyElement { + let Ok(view_state) = weak.update(cx, |this, _| this.view_state.clone()) else { + return div().into_any(); + }; + let base_address = (view_state.base_row + ix) * view_state.line_width.width as u64; + + h_flex() + .id(( + "memory-view-row-full", + ix * view_state.line_width.width as u64, + )) + .size_full() + .gap_x_2() + .child( + div() + .child( + Label::new(format!("{:016X}", base_address)) + .buffer_font(cx) + .size(ui::LabelSize::Small) + .color(Color::Muted), + ) + .px_1() + .border_r_1() + .border_color(Color::Muted.color(cx)), + ) + .child( + h_flex() + .id(( + "memory-view-row-raw-memory", + ix * view_state.line_width.width as u64, + )) + .px_1() + .children(memory.iter().enumerate().map(|(cell_ix, cell)| { + let weak = weak.clone(); + div() + .id(("memory-view-row-raw-memory-cell", cell_ix as u64)) + .px_0p5() + .when_some(view_state.selection.as_ref(), |this, selection| { + this.when(selection.contains(base_address + cell_ix as u64), |this| { + let weak = weak.clone(); + + this.bg(Color::Accent.color(cx)).when( + !selection.is_dragging(), + |this| { + let selection = selection.drag().memory_range(); + this.on_mouse_down( + MouseButton::Right, + move |click, window, cx| { + _ = weak.update(cx, |this, cx| { + this.deploy_memory_context_menu( + selection.clone(), + click.position, + window, + cx, + ) + }); + cx.stop_propagation(); + }, + ) + }, + ) + }) + }) + .child( + Label::new( + cell.0 + .map(|val| HEX_BYTES_MEMOIZED[val as usize].clone()) + .unwrap_or_else(|| UNKNOWN_BYTE.clone()), + ) + .buffer_font(cx) + .when(cell.0.is_none(), |this| this.color(Color::Muted)) + .size(ui::LabelSize::Small), + ) + .on_drag( + Drag { + start_address: base_address + cell_ix as u64, + end_address: base_address + cell_ix as u64, + }, + { + let weak = weak.clone(); + move |drag, _, _, cx| { + _ = weak.update(cx, |this, _| { + this.view_state.selection = + Some(SelectedMemoryRange::DragUnderway(drag.clone())); + }); + + cx.new(|_| Empty) + } + }, + ) + .on_drop({ + let weak = weak.clone(); + move |drag: &Drag, _, cx| { + _ = weak.update(cx, |this, _| { + this.view_state.selection = + Some(SelectedMemoryRange::DragComplete(Drag { + start_address: drag.start_address, + end_address: base_address + cell_ix as u64, + })); + }); + } + }) + .drag_over(move |style, drag: &Drag, _, cx| { + _ = weak.update(cx, |this, _| { + this.view_state.selection = + Some(SelectedMemoryRange::DragUnderway(Drag { + start_address: drag.start_address, + end_address: base_address + cell_ix as u64, + })); + }); + + style + }) + })), + ) + .child( + h_flex() + .id(( + "memory-view-row-ascii-memory", + ix * view_state.line_width.width as u64, + )) + .h_full() + .px_1() + .mr_4() + // .gap_x_1p5() + .border_x_1() + .border_color(Color::Muted.color(cx)) + .children(memory.iter().enumerate().map(|(ix, cell)| { + let as_character = char::from(cell.0.unwrap_or(0)); + let as_visible = if as_character.is_ascii_graphic() { + as_character + } else { + '·' + }; + div() + .px_0p5() + .when_some(view_state.selection.as_ref(), |this, selection| { + this.when(selection.contains(base_address + ix as u64), |this| { + this.bg(Color::Accent.color(cx)) + }) + }) + .child( + Label::new(format!("{as_visible}")) + .buffer_font(cx) + .when(cell.0.is_none(), |this| this.color(Color::Muted)) + .size(ui::LabelSize::Small), + ) + })), + ) + .into_any() +} + +impl Render for MemoryView { + fn render( + &mut self, + window: &mut ui::Window, + cx: &mut ui::Context<Self>, + ) -> impl ui::IntoElement { + let (icon, tooltip_text) = if self.is_writing_memory { + (IconName::Pencil, "Edit memory at a selected address") + } else { + ( + IconName::LocationEdit, + "Change address of currently viewed memory", + ) + }; + v_flex() + .id("Memory-view") + .on_action(cx.listener(Self::cancel)) + .on_action(cx.listener(Self::go_to_address)) + .p_1() + .on_action(cx.listener(Self::confirm)) + .on_action(cx.listener(Self::page_down)) + .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() + .mb_0p5() + .gap_1() + .child( + h_flex() + .w_full() + .rounded_md() + .border_1() + .gap_x_2() + .px_2() + .py_0p5() + .mb_0p5() + .bg(cx.theme().colors().editor_background) + .when_else( + self.query_editor + .focus_handle(cx) + .contains_focused(window, cx), + |this| this.border_color(cx.theme().colors().border_focused), + |this| this.border_color(cx.theme().colors().border_transparent), + ) + .child( + div() + .id("memory-view-editor-icon") + .child(Icon::new(icon).size(ui::IconSize::XSmall)) + .tooltip(Tooltip::text(tooltip_text)), + ) + .child(self.render_query_bar(cx)), + ) + .child(self.render_width_picker(window, cx)), + ) + .child(Divider::horizontal()) + .child( + v_flex() + .size_full() + .on_mouse_move(cx.listener(|this, evt: &MouseMoveEvent, _, _| { + this.handle_drag(evt); + })) + .child(self.render_memory(cx).size_full()) + .children(self.open_context_menu.as_ref().map(|(menu, position, _)| { + deferred( + anchored() + .position(*position) + .anchor(gpui::Corner::TopLeft) + .child(menu.clone()), + ) + .with_priority(1) + })) + .children(self.render_vertical_scrollbar(cx)), + ) + } +} diff --git a/crates/debugger_ui/src/session/running/variable_list.rs b/crates/debugger_ui/src/session/running/variable_list.rs index bdb095bde3e4295bf96cff7d02012e4a4ea9d5bd..c7df449ee6c6ea55e1420080f216fd07aed5f370 100644 --- a/crates/debugger_ui/src/session/running/variable_list.rs +++ b/crates/debugger_ui/src/session/running/variable_list.rs @@ -1,3 +1,5 @@ +use crate::session::running::{RunningState, memory_view::MemoryView}; + use super::stack_frame_list::{StackFrameList, StackFrameListEvent}; use dap::{ ScopePresentationHint, StackFrameId, VariablePresentationHint, VariablePresentationHintKind, @@ -7,13 +9,14 @@ use editor::Editor; use gpui::{ Action, AnyElement, ClickEvent, ClipboardItem, Context, DismissEvent, Empty, Entity, FocusHandle, Focusable, Hsla, MouseButton, MouseDownEvent, Point, Stateful, Subscription, - TextStyleRefinement, UniformListScrollHandle, actions, anchored, deferred, uniform_list, + TextStyleRefinement, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, + uniform_list, }; use menu::{SelectFirst, SelectLast, SelectNext, SelectPrevious}; use project::debugger::session::{Session, SessionEvent, Watcher}; use std::{collections::HashMap, ops::Range, sync::Arc}; use ui::{ContextMenu, ListItem, ScrollableHandle, Scrollbar, ScrollbarState, Tooltip, prelude::*}; -use util::debug_panic; +use util::{debug_panic, maybe}; actions!( variable_list, @@ -32,6 +35,8 @@ actions!( AddWatch, /// Removes the selected variable from the watch list. RemoveWatch, + /// Jump to variable's memory location. + GoToMemory, ] ); @@ -86,30 +91,30 @@ impl EntryPath { } #[derive(Debug, Clone, PartialEq)] -enum EntryKind { +enum DapEntry { Watcher(Watcher), Variable(dap::Variable), Scope(dap::Scope), } -impl EntryKind { +impl DapEntry { fn as_watcher(&self) -> Option<&Watcher> { match self { - EntryKind::Watcher(watcher) => Some(watcher), + DapEntry::Watcher(watcher) => Some(watcher), _ => None, } } fn as_variable(&self) -> Option<&dap::Variable> { match self { - EntryKind::Variable(dap) => Some(dap), + DapEntry::Variable(dap) => Some(dap), _ => None, } } fn as_scope(&self) -> Option<&dap::Scope> { match self { - EntryKind::Scope(dap) => Some(dap), + DapEntry::Scope(dap) => Some(dap), _ => None, } } @@ -117,38 +122,38 @@ impl EntryKind { #[cfg(test)] fn name(&self) -> &str { match self { - EntryKind::Watcher(watcher) => &watcher.expression, - EntryKind::Variable(dap) => &dap.name, - EntryKind::Scope(dap) => &dap.name, + DapEntry::Watcher(watcher) => &watcher.expression, + DapEntry::Variable(dap) => &dap.name, + DapEntry::Scope(dap) => &dap.name, } } } #[derive(Debug, Clone, PartialEq)] struct ListEntry { - dap_kind: EntryKind, + entry: DapEntry, path: EntryPath, } impl ListEntry { fn as_watcher(&self) -> Option<&Watcher> { - self.dap_kind.as_watcher() + self.entry.as_watcher() } fn as_variable(&self) -> Option<&dap::Variable> { - self.dap_kind.as_variable() + self.entry.as_variable() } fn as_scope(&self) -> Option<&dap::Scope> { - self.dap_kind.as_scope() + self.entry.as_scope() } fn item_id(&self) -> ElementId { use std::fmt::Write; - let mut id = match &self.dap_kind { - EntryKind::Watcher(watcher) => format!("watcher-{}", watcher.expression), - EntryKind::Variable(dap) => format!("variable-{}", dap.name), - EntryKind::Scope(dap) => format!("scope-{}", dap.name), + let mut id = match &self.entry { + DapEntry::Watcher(watcher) => format!("watcher-{}", watcher.expression), + DapEntry::Variable(dap) => format!("variable-{}", dap.name), + DapEntry::Scope(dap) => format!("scope-{}", dap.name), }; for name in self.path.indices.iter() { _ = write!(id, "-{}", name); @@ -158,10 +163,10 @@ impl ListEntry { fn item_value_id(&self) -> ElementId { use std::fmt::Write; - let mut id = match &self.dap_kind { - EntryKind::Watcher(watcher) => format!("watcher-{}", watcher.expression), - EntryKind::Variable(dap) => format!("variable-{}", dap.name), - EntryKind::Scope(dap) => format!("scope-{}", dap.name), + let mut id = match &self.entry { + DapEntry::Watcher(watcher) => format!("watcher-{}", watcher.expression), + DapEntry::Variable(dap) => format!("variable-{}", dap.name), + DapEntry::Scope(dap) => format!("scope-{}", dap.name), }; for name in self.path.indices.iter() { _ = write!(id, "-{}", name); @@ -188,13 +193,17 @@ pub struct VariableList { focus_handle: FocusHandle, edited_path: Option<(EntryPath, Entity<Editor>)>, disabled: bool, + memory_view: Entity<MemoryView>, + weak_running: WeakEntity<RunningState>, _subscriptions: Vec<Subscription>, } impl VariableList { - pub fn new( + pub(crate) fn new( session: Entity<Session>, stack_frame_list: Entity<StackFrameList>, + memory_view: Entity<MemoryView>, + weak_running: WeakEntity<RunningState>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { @@ -234,6 +243,8 @@ impl VariableList { edited_path: None, entries: Default::default(), entry_states: Default::default(), + weak_running, + memory_view, } } @@ -284,7 +295,7 @@ impl VariableList { scope.variables_reference, scope.variables_reference, EntryPath::for_scope(&scope.name), - EntryKind::Scope(scope), + DapEntry::Scope(scope), ) }) .collect::<Vec<_>>(); @@ -298,7 +309,7 @@ impl VariableList { watcher.variables_reference, watcher.variables_reference, EntryPath::for_watcher(watcher.expression.clone()), - EntryKind::Watcher(watcher.clone()), + DapEntry::Watcher(watcher.clone()), ) }) .collect::<Vec<_>>(), @@ -309,9 +320,9 @@ impl VariableList { while let Some((container_reference, variables_reference, mut path, dap_kind)) = stack.pop() { match &dap_kind { - EntryKind::Watcher(watcher) => path = path.with_child(watcher.expression.clone()), - EntryKind::Variable(dap) => path = path.with_name(dap.name.clone().into()), - EntryKind::Scope(dap) => path = path.with_child(dap.name.clone().into()), + DapEntry::Watcher(watcher) => path = path.with_child(watcher.expression.clone()), + DapEntry::Variable(dap) => path = path.with_name(dap.name.clone().into()), + DapEntry::Scope(dap) => path = path.with_child(dap.name.clone().into()), } let var_state = self @@ -336,7 +347,7 @@ impl VariableList { }); entries.push(ListEntry { - dap_kind, + entry: dap_kind, path: path.clone(), }); @@ -349,7 +360,7 @@ impl VariableList { variables_reference, child.variables_reference, path.with_child(child.name.clone().into()), - EntryKind::Variable(child), + DapEntry::Variable(child), ) })); } @@ -380,9 +391,9 @@ impl VariableList { pub fn completion_variables(&self, _cx: &mut Context<Self>) -> Vec<dap::Variable> { self.entries .iter() - .filter_map(|entry| match &entry.dap_kind { - EntryKind::Variable(dap) => Some(dap.clone()), - EntryKind::Scope(_) | EntryKind::Watcher { .. } => None, + .filter_map(|entry| match &entry.entry { + DapEntry::Variable(dap) => Some(dap.clone()), + DapEntry::Scope(_) | DapEntry::Watcher { .. } => None, }) .collect() } @@ -400,12 +411,12 @@ impl VariableList { .get(ix) .and_then(|entry| Some(entry).zip(self.entry_states.get(&entry.path)))?; - match &entry.dap_kind { - EntryKind::Watcher { .. } => { + match &entry.entry { + DapEntry::Watcher { .. } => { Some(self.render_watcher(entry, *state, window, cx)) } - EntryKind::Variable(_) => Some(self.render_variable(entry, *state, window, cx)), - EntryKind::Scope(_) => Some(self.render_scope(entry, *state, cx)), + DapEntry::Variable(_) => Some(self.render_variable(entry, *state, window, cx)), + DapEntry::Scope(_) => Some(self.render_scope(entry, *state, cx)), } }) .collect() @@ -562,6 +573,51 @@ impl VariableList { } } + fn jump_to_variable_memory( + &mut self, + _: &GoToMemory, + window: &mut Window, + cx: &mut Context<Self>, + ) { + _ = maybe!({ + let selection = self.selection.as_ref()?; + let entry = self.entries.iter().find(|entry| &entry.path == selection)?; + let var = entry.entry.as_variable()?; + let memory_reference = var.memory_reference.as_deref()?; + + let sizeof_expr = if var.type_.as_ref().is_some_and(|t| { + t.chars() + .all(|c| c.is_whitespace() || c.is_alphabetic() || c == '*') + }) { + var.type_.as_deref() + } else { + var.evaluate_name + .as_deref() + .map(|name| name.strip_prefix("/nat ").unwrap_or_else(|| name)) + }; + self.memory_view.update(cx, |this, cx| { + this.go_to_memory_reference( + memory_reference, + sizeof_expr, + self.selected_stack_frame_id, + cx, + ); + }); + let weak_panel = self.weak_running.clone(); + + window.defer(cx, move |window, cx| { + _ = weak_panel.update(cx, |this, cx| { + this.activate_item( + crate::persistence::DebuggerPaneItem::MemoryView, + window, + cx, + ); + }); + }); + Some(()) + }); + } + fn deploy_list_entry_context_menu( &mut self, entry: ListEntry, @@ -584,6 +640,7 @@ impl VariableList { menu.action("Edit Value", EditVariable.boxed_clone()) }) .action("Watch Variable", AddWatch.boxed_clone()) + .action("Go To Memory", GoToMemory.boxed_clone()) }) .when(entry.as_watcher().is_some(), |menu| { menu.action("Copy Name", CopyVariableName.boxed_clone()) @@ -628,10 +685,10 @@ impl VariableList { return; }; - let variable_name = match &entry.dap_kind { - EntryKind::Variable(dap) => dap.name.clone(), - EntryKind::Watcher(watcher) => watcher.expression.to_string(), - EntryKind::Scope(_) => return, + let variable_name = match &entry.entry { + DapEntry::Variable(dap) => dap.name.clone(), + DapEntry::Watcher(watcher) => watcher.expression.to_string(), + DapEntry::Scope(_) => return, }; cx.write_to_clipboard(ClipboardItem::new_string(variable_name)); @@ -651,10 +708,10 @@ impl VariableList { return; }; - let variable_value = match &entry.dap_kind { - EntryKind::Variable(dap) => dap.value.clone(), - EntryKind::Watcher(watcher) => watcher.value.to_string(), - EntryKind::Scope(_) => return, + let variable_value = match &entry.entry { + DapEntry::Variable(dap) => dap.value.clone(), + DapEntry::Watcher(watcher) => watcher.value.to_string(), + DapEntry::Scope(_) => return, }; cx.write_to_clipboard(ClipboardItem::new_string(variable_value)); @@ -669,10 +726,10 @@ impl VariableList { return; }; - let variable_value = match &entry.dap_kind { - EntryKind::Watcher(watcher) => watcher.value.to_string(), - EntryKind::Variable(variable) => variable.value.clone(), - EntryKind::Scope(_) => return, + let variable_value = match &entry.entry { + DapEntry::Watcher(watcher) => watcher.value.to_string(), + DapEntry::Variable(variable) => variable.value.clone(), + DapEntry::Scope(_) => return, }; let editor = Self::create_variable_editor(&variable_value, window, cx); @@ -753,7 +810,7 @@ impl VariableList { "{}{} {}{}", INDENT.repeat(state.depth - 1), if state.is_expanded { "v" } else { ">" }, - entry.dap_kind.name(), + entry.entry.name(), if self.selection.as_ref() == Some(&entry.path) { " <=== selected" } else { @@ -770,8 +827,8 @@ impl VariableList { pub(crate) fn scopes(&self) -> Vec<dap::Scope> { self.entries .iter() - .filter_map(|entry| match &entry.dap_kind { - EntryKind::Scope(scope) => Some(scope), + .filter_map(|entry| match &entry.entry { + DapEntry::Scope(scope) => Some(scope), _ => None, }) .cloned() @@ -785,10 +842,10 @@ impl VariableList { let mut idx = 0; for entry in self.entries.iter() { - match &entry.dap_kind { - EntryKind::Watcher { .. } => continue, - EntryKind::Variable(dap) => scopes[idx].1.push(dap.clone()), - EntryKind::Scope(scope) => { + match &entry.entry { + DapEntry::Watcher { .. } => continue, + DapEntry::Variable(dap) => scopes[idx].1.push(dap.clone()), + DapEntry::Scope(scope) => { if scopes.len() > 0 { idx += 1; } @@ -806,8 +863,8 @@ impl VariableList { pub(crate) fn variables(&self) -> Vec<dap::Variable> { self.entries .iter() - .filter_map(|entry| match &entry.dap_kind { - EntryKind::Variable(variable) => Some(variable), + .filter_map(|entry| match &entry.entry { + DapEntry::Variable(variable) => Some(variable), _ => None, }) .cloned() @@ -1358,6 +1415,7 @@ impl Render for VariableList { .on_action(cx.listener(Self::edit_variable)) .on_action(cx.listener(Self::add_watcher)) .on_action(cx.listener(Self::remove_watcher)) + .on_action(cx.listener(Self::jump_to_variable_memory)) .child( uniform_list( "variable-list", diff --git a/crates/debugger_ui/src/tests/module_list.rs b/crates/debugger_ui/src/tests/module_list.rs index 49cfd6fcf88339c7d040d56d575dafce50f8d0f2..09c90cbc4a3af71aa9fb7273cf3535e9f7ece592 100644 --- a/crates/debugger_ui/src/tests/module_list.rs +++ b/crates/debugger_ui/src/tests/module_list.rs @@ -111,7 +111,6 @@ async fn test_module_list(executor: BackgroundExecutor, cx: &mut TestAppContext) }); running_state.update_in(cx, |this, window, cx| { - this.ensure_pane_item(DebuggerPaneItem::Modules, window, cx); this.activate_item(DebuggerPaneItem::Modules, window, cx); cx.refresh_windows(); }); diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index 6e05b384e15492f6ebd137004f0f13fd4a6d549c..cb53276bc2a879168e718e210b03b7af2061ad52 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -903,7 +903,7 @@ pub trait InteractiveElement: Sized { /// Apply the given style when the given data type is dragged over this element fn drag_over<S: 'static>( mut self, - f: impl 'static + Fn(StyleRefinement, &S, &Window, &App) -> StyleRefinement, + f: impl 'static + Fn(StyleRefinement, &S, &mut Window, &mut App) -> StyleRefinement, ) -> Self { self.interactivity().drag_over_styles.push(( TypeId::of::<S>(), diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index 4151e6b2ea03c92bd43064e93a6a37fd0bc3cbf2..29f7a8f50dd626a4d2cfa7032e5cf391c81de4fe 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -163,6 +163,7 @@ pub enum IconName { ListTree, ListX, LoadCircle, + LocationEdit, LockOutlined, LspDebug, LspRestart, diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 729d61aab53a60a2d6f59c7b066b00ecc49ab913..57d6d6ca283af0fd51ed10622f55edc9fb086e7e 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -31,6 +31,7 @@ aho-corasick.workspace = true anyhow.workspace = true askpass.workspace = true async-trait.workspace = true +base64.workspace = true buffer_diff.workspace = true circular-buffer.workspace = true client.workspace = true @@ -72,6 +73,7 @@ settings.workspace = true sha2.workspace = true shellexpand.workspace = true shlex.workspace = true +smallvec.workspace = true smol.workspace = true snippet.workspace = true snippet_provider.workspace = true diff --git a/crates/project/src/debugger.rs b/crates/project/src/debugger.rs index d078988a51bb4f7e6ab824913e2d6584e1bd0d2e..6c22468040097768688d93cde0720320a9e45be9 100644 --- a/crates/project/src/debugger.rs +++ b/crates/project/src/debugger.rs @@ -15,7 +15,9 @@ pub mod breakpoint_store; pub mod dap_command; pub mod dap_store; pub mod locators; +mod memory; pub mod session; #[cfg(any(feature = "test-support", test))] pub mod test; +pub use memory::MemoryCell; diff --git a/crates/project/src/debugger/dap_command.rs b/crates/project/src/debugger/dap_command.rs index 411bacd3ba1557b7392eb0e981df1a4297772b31..1a3587024ecb593f8cdabf99c951aef26c471fa4 100644 --- a/crates/project/src/debugger/dap_command.rs +++ b/crates/project/src/debugger/dap_command.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use anyhow::{Context as _, Ok, Result}; +use base64::Engine; use dap::{ Capabilities, ContinueArguments, ExceptionFilterOptions, InitializeRequestArguments, InitializeRequestArgumentsPathFormat, NextArguments, SetVariableResponse, SourceBreakpoint, @@ -1774,3 +1775,95 @@ impl DapCommand for LocationsCommand { }) } } + +#[derive(Debug, Hash, PartialEq, Eq)] +pub(crate) struct ReadMemory { + pub(crate) memory_reference: String, + pub(crate) offset: Option<u64>, + pub(crate) count: u64, +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct ReadMemoryResponse { + pub(super) address: Arc<str>, + pub(super) unreadable_bytes: Option<u64>, + pub(super) content: Arc<[u8]>, +} + +impl LocalDapCommand for ReadMemory { + type Response = ReadMemoryResponse; + type DapRequest = dap::requests::ReadMemory; + const CACHEABLE: bool = true; + + fn is_supported(capabilities: &Capabilities) -> bool { + capabilities + .supports_read_memory_request + .unwrap_or_default() + } + fn to_dap(&self) -> <Self::DapRequest as dap::requests::Request>::Arguments { + dap::ReadMemoryArguments { + memory_reference: self.memory_reference.clone(), + offset: self.offset, + count: self.count, + } + } + + fn response_from_dap( + &self, + message: <Self::DapRequest as dap::requests::Request>::Response, + ) -> Result<Self::Response> { + let data = if let Some(data) = message.data { + base64::engine::general_purpose::STANDARD + .decode(data) + .log_err() + .context("parsing base64 data from DAP's ReadMemory response")? + } else { + vec![] + }; + + Ok(ReadMemoryResponse { + address: message.address.into(), + content: data.into(), + unreadable_bytes: message.unreadable_bytes, + }) + } +} + +impl LocalDapCommand for dap::DataBreakpointInfoArguments { + type Response = dap::DataBreakpointInfoResponse; + type DapRequest = dap::requests::DataBreakpointInfo; + const CACHEABLE: bool = true; + fn is_supported(capabilities: &Capabilities) -> bool { + capabilities.supports_data_breakpoints.unwrap_or_default() + } + fn to_dap(&self) -> <Self::DapRequest as dap::requests::Request>::Arguments { + self.clone() + } + + fn response_from_dap( + &self, + message: <Self::DapRequest as dap::requests::Request>::Response, + ) -> Result<Self::Response> { + Ok(message) + } +} + +impl LocalDapCommand for dap::WriteMemoryArguments { + type Response = dap::WriteMemoryResponse; + type DapRequest = dap::requests::WriteMemory; + fn is_supported(capabilities: &Capabilities) -> bool { + capabilities + .supports_write_memory_request + .unwrap_or_default() + } + fn to_dap(&self) -> <Self::DapRequest as dap::requests::Request>::Arguments { + self.clone() + } + + fn response_from_dap( + &self, + message: <Self::DapRequest as dap::requests::Request>::Response, + ) -> Result<Self::Response> { + Ok(message) + } +} diff --git a/crates/project/src/debugger/memory.rs b/crates/project/src/debugger/memory.rs new file mode 100644 index 0000000000000000000000000000000000000000..fec3c344c5a433eebb3a1f314a8fd911bd603022 --- /dev/null +++ b/crates/project/src/debugger/memory.rs @@ -0,0 +1,384 @@ +//! This module defines the format in which memory of debuggee is represented. +//! +//! 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. +//! Note that per DAP we don't know what the address space layout is, so we can't optimize off of it. +//! Note that while we optimize for a paged layout, we also want to be able to represent memory that is not paged. +//! This use case is relevant to embedded folks. Furthermore, we cater to default 4k page size. +//! It is picked arbitrarily as a ubiquous default - other than that, the underlying format of Zed's memory storage should not be relevant +//! to the users of this module. + +use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc}; + +use gpui::BackgroundExecutor; +use smallvec::SmallVec; + +const PAGE_SIZE: u64 = 4096; + +/// Represents the contents of a single page. We special-case unmapped pages to be allocation-free, +/// since they're going to make up the majority of the memory in a program space (even though the user might not even get to see them - ever). +#[derive(Clone, Debug)] +pub(super) enum PageContents { + /// Whole page is unreadable. + Unmapped, + Mapped(Arc<MappedPageContents>), +} + +impl PageContents { + #[cfg(test)] + fn mapped(contents: Vec<u8>) -> Self { + PageContents::Mapped(Arc::new(MappedPageContents( + vec![PageChunk::Mapped(contents.into())].into(), + ))) + } +} + +#[derive(Clone, Debug)] +enum PageChunk { + Mapped(Arc<[u8]>), + Unmapped(u64), +} + +impl PageChunk { + fn len(&self) -> u64 { + match self { + PageChunk::Mapped(contents) => contents.len() as u64, + PageChunk::Unmapped(size) => *size, + } + } +} + +impl MappedPageContents { + fn len(&self) -> u64 { + self.0.iter().map(|chunk| chunk.len()).sum() + } +} +/// We hope for the whole page to be mapped in a single chunk, but we do leave the possibility open +/// of having interleaved read permissions in a single page; debuggee's execution environment might either +/// have a different page size OR it might not have paged memory layout altogether +/// (which might be relevant to embedded systems). +/// +/// As stated previously, the concept of a page in this module has to do more +/// with optimizing fetching of the memory and not with the underlying bits and pieces +/// of the memory of a debuggee. + +#[derive(Default, Debug)] +pub(super) struct MappedPageContents( + /// Most of the time there should be only one chunk (either mapped or unmapped), + /// but we do leave the possibility open of having multiple regions of memory in a single page. + SmallVec<[PageChunk; 1]>, +); + +type MemoryAddress = u64; +#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Ord, Eq)] +#[repr(transparent)] +pub(super) struct PageAddress(u64); + +impl PageAddress { + pub(super) fn iter_range( + range: RangeInclusive<PageAddress>, + ) -> impl Iterator<Item = PageAddress> { + let mut current = range.start().0; + let end = range.end().0; + + std::iter::from_fn(move || { + if current > end { + None + } else { + let addr = PageAddress(current); + current += PAGE_SIZE; + Some(addr) + } + }) + } +} + +pub(super) struct Memory { + pages: BTreeMap<PageAddress, PageContents>, +} + +/// Represents a single memory cell (or None if a given cell is unmapped/unknown). +#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Ord, Eq)] +#[repr(transparent)] +pub struct MemoryCell(pub Option<u8>); + +impl Memory { + pub(super) fn new() -> Self { + Self { + pages: Default::default(), + } + } + + pub(super) fn memory_range_to_page_range( + range: RangeInclusive<MemoryAddress>, + ) -> RangeInclusive<PageAddress> { + let start_page = (range.start() / PAGE_SIZE) * PAGE_SIZE; + let end_page = (range.end() / PAGE_SIZE) * PAGE_SIZE; + PageAddress(start_page)..=PageAddress(end_page) + } + + pub(super) fn build_page(&self, page_address: PageAddress) -> Option<MemoryPageBuilder> { + if self.pages.contains_key(&page_address) { + // We already know the state of this page. + None + } else { + Some(MemoryPageBuilder::new(page_address)) + } + } + + pub(super) fn insert_page(&mut self, address: PageAddress, page: PageContents) { + self.pages.insert(address, page); + } + + pub(super) fn memory_range(&self, range: RangeInclusive<MemoryAddress>) -> MemoryIterator { + let pages = Self::memory_range_to_page_range(range.clone()); + let pages = self + .pages + .range(pages) + .map(|(address, page)| (*address, page.clone())) + .collect::<Vec<_>>(); + MemoryIterator::new(range, pages.into_iter()) + } + + pub(crate) fn clear(&mut self, background_executor: &BackgroundExecutor) { + let memory = std::mem::take(&mut self.pages); + background_executor + .spawn(async move { + drop(memory); + }) + .detach(); + } +} + +/// Builder for memory pages. +/// +/// Memory reads in DAP are sequential (or at least we make them so). +/// ReadMemory response includes `unreadableBytes` property indicating the number of bytes +/// that could not be read after the last successfully read byte. +/// +/// We use it as follows: +/// - We start off with a "large" 1-page ReadMemory request. +/// - 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. +/// +/// 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, + base_address: PageAddress, + left_to_read: u64, +} + +/// Represents a chunk of memory of which we don't know if it's mapped or unmapped; thus we need +/// to issue a request to figure out it's state. +pub(super) struct UnknownMemory { + pub(super) address: MemoryAddress, + pub(super) size: u64, +} + +impl MemoryPageBuilder { + fn new(base_address: PageAddress) -> Self { + Self { + chunks: Default::default(), + base_address, + left_to_read: PAGE_SIZE, + } + } + + pub(super) fn build(self) -> (PageAddress, PageContents) { + debug_assert_eq!(self.left_to_read, 0); + debug_assert_eq!( + self.chunks.len(), + PAGE_SIZE, + "Expected `build` to be called on a fully-fetched page" + ); + let contents = if let Some(first) = self.chunks.0.first() + && self.chunks.len() == 1 + && matches!(first, PageChunk::Unmapped(PAGE_SIZE)) + { + PageContents::Unmapped + } else { + PageContents::Mapped(Arc::new(MappedPageContents(self.chunks.0))) + }; + (self.base_address, contents) + } + /// Drives the fetching of memory, in an iterator-esque style. + pub(super) fn next_request(&self) -> Option<UnknownMemory> { + if self.left_to_read == 0 { + None + } else { + let offset_in_current_page = PAGE_SIZE - self.left_to_read; + Some(UnknownMemory { + address: self.base_address.0 + offset_in_current_page, + size: self.left_to_read, + }) + } + } + pub(super) fn unknown(&mut self, bytes: u64) { + if bytes == 0 { + return; + } + self.left_to_read -= bytes; + self.chunks.0.push(PageChunk::Unmapped(bytes)); + } + pub(super) fn known(&mut self, data: Arc<[u8]>) { + if data.is_empty() { + return; + } + self.left_to_read -= data.len() as u64; + self.chunks.0.push(PageChunk::Mapped(data)); + } +} + +fn page_contents_into_iter(data: Arc<MappedPageContents>) -> Box<dyn Iterator<Item = MemoryCell>> { + let mut data_range = 0..data.0.len(); + let iter = std::iter::from_fn(move || { + let data = &data; + let data_ref = data.clone(); + data_range.next().map(move |index| { + let contents = &data_ref.0[index]; + match contents { + PageChunk::Mapped(items) => { + let chunk_range = 0..items.len(); + let items = items.clone(); + Box::new( + chunk_range + .into_iter() + .map(move |ix| MemoryCell(Some(items[ix]))), + ) as Box<dyn Iterator<Item = MemoryCell>> + } + PageChunk::Unmapped(len) => { + Box::new(std::iter::repeat_n(MemoryCell(None), *len as usize)) + } + } + }) + }) + .flatten(); + + Box::new(iter) +} +/// Defines an iteration over a range of memory. Some of this memory might be unmapped or straight up missing. +/// Thus, this iterator alternates between synthesizing values and yielding known memory. +pub struct MemoryIterator { + start: MemoryAddress, + end: MemoryAddress, + current_known_page: Option<(PageAddress, Box<dyn Iterator<Item = MemoryCell>>)>, + pages: std::vec::IntoIter<(PageAddress, PageContents)>, +} + +impl MemoryIterator { + fn new( + range: RangeInclusive<MemoryAddress>, + pages: std::vec::IntoIter<(PageAddress, PageContents)>, + ) -> Self { + Self { + start: *range.start(), + end: *range.end(), + current_known_page: None, + pages, + } + } + fn fetch_next_page(&mut self) -> bool { + if let Some((mut address, chunk)) = self.pages.next() { + let mut contents = match chunk { + PageContents::Unmapped => None, + PageContents::Mapped(mapped_page_contents) => { + Some(page_contents_into_iter(mapped_page_contents)) + } + }; + + if address.0 < self.start { + // Skip ahead till our iterator is at the start of the range + + //address: 20, start: 25 + // + let to_skip = self.start - address.0; + address.0 += to_skip; + if let Some(contents) = &mut contents { + contents.nth(to_skip as usize - 1); + } + } + self.current_known_page = contents.map(|contents| (address, contents)); + true + } else { + false + } + } +} +impl Iterator for MemoryIterator { + type Item = MemoryCell; + + fn next(&mut self) -> Option<Self::Item> { + if self.start > self.end { + return None; + } + if let Some((current_page_address, current_memory_chunk)) = self.current_known_page.as_mut() + { + 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 !self.fetch_next_page() { + self.start += 1; + return Some(MemoryCell(None)); + } else { + self.next() + } + } +} + +#[cfg(test)] +mod tests { + use crate::debugger::{ + MemoryCell, + memory::{MemoryIterator, PageAddress, PageContents}, + }; + + #[test] + fn iterate_over_unmapped_memory() { + let empty_iterator = MemoryIterator::new(0..=127, Default::default()); + let actual = empty_iterator.collect::<Vec<_>>(); + let expected = vec![MemoryCell(None); 128]; + assert_eq!(actual.len(), expected.len()); + assert_eq!(actual, expected); + } + + #[test] + fn iterate_over_partially_mapped_memory() { + let it = MemoryIterator::new( + 0..=127, + vec![(PageAddress(5), PageContents::mapped(vec![1]))].into_iter(), + ); + let actual = it.collect::<Vec<_>>(); + let expected = std::iter::repeat_n(MemoryCell(None), 5) + .chain(std::iter::once(MemoryCell(Some(1)))) + .chain(std::iter::repeat_n(MemoryCell(None), 122)) + .collect::<Vec<_>>(); + assert_eq!(actual.len(), expected.len()); + assert_eq!(actual, expected); + } + + #[test] + fn reads_from_the_middle_of_a_page() { + let partial_iter = MemoryIterator::new( + 20..=30, + vec![(PageAddress(0), PageContents::mapped((0..255).collect()))].into_iter(), + ); + let actual = partial_iter.collect::<Vec<_>>(); + let expected = (20..=30) + .map(|val| MemoryCell(Some(val))) + .collect::<Vec<_>>(); + assert_eq!(actual.len(), expected.len()); + assert_eq!(actual, expected); + } +} diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index f526d5a3fececae2e9b45b2c935aa00f1283d845..53c13e13c3cd03ae14e68daa9c5e13f4a5515a1e 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -1,4 +1,6 @@ use crate::debugger::breakpoint_store::BreakpointSessionState; +use crate::debugger::dap_command::ReadMemory; +use crate::debugger::memory::{self, Memory, MemoryIterator, MemoryPageBuilder, PageAddress}; use super::breakpoint_store::{ BreakpointStore, BreakpointStoreEvent, BreakpointUpdatedReason, SourceBreakpoint, @@ -13,6 +15,7 @@ use super::dap_command::{ }; use super::dap_store::DapStore; use anyhow::{Context as _, Result, anyhow}; +use base64::Engine; use collections::{HashMap, HashSet, IndexMap}; use dap::adapters::{DebugAdapterBinary, DebugAdapterName}; use dap::messages::Response; @@ -26,7 +29,7 @@ use dap::{ use dap::{ ExceptionBreakpointsFilter, ExceptionFilterOptions, OutputEvent, OutputEventCategory, RunInTerminalRequestArguments, StackFramePresentationHint, StartDebuggingRequestArguments, - StartDebuggingRequestArgumentsRequest, VariablePresentationHint, + StartDebuggingRequestArgumentsRequest, VariablePresentationHint, WriteMemoryArguments, }; use futures::SinkExt; use futures::channel::mpsc::UnboundedSender; @@ -42,6 +45,7 @@ use serde_json::Value; use smol::stream::StreamExt; use std::any::TypeId; use std::collections::BTreeMap; +use std::ops::RangeInclusive; use std::u64; use std::{ any::Any, @@ -52,7 +56,7 @@ use std::{ }; use task::TaskContext; use text::{PointUtf16, ToPointUtf16}; -use util::ResultExt; +use util::{ResultExt, maybe}; use worktree::Worktree; #[derive(Debug, Copy, Clone, Hash, PartialEq, PartialOrd, Ord, Eq)] @@ -685,6 +689,7 @@ pub struct Session { background_tasks: Vec<Task<()>>, restart_task: Option<Task<()>>, task_context: TaskContext, + memory: memory::Memory, quirks: SessionQuirks, } @@ -855,6 +860,7 @@ impl Session { label, adapter, task_context, + memory: memory::Memory::new(), quirks, }; @@ -1664,6 +1670,11 @@ impl Session { self.invalidate_command_type::<ModulesCommand>(); self.invalidate_command_type::<LoadedSourcesCommand>(); self.invalidate_command_type::<ThreadsCommand>(); + self.invalidate_command_type::<ReadMemory>(); + let executor = self.as_running().map(|running| running.executor.clone()); + if let Some(executor) = executor { + self.memory.clear(&executor); + } } fn invalidate_state(&mut self, key: &RequestSlot) { @@ -1736,6 +1747,135 @@ impl Session { &self.modules } + // CodeLLDB returns the size of a pointed-to-memory, which we can use to make the experience of go-to-memory better. + pub fn data_access_size( + &mut self, + frame_id: Option<u64>, + evaluate_name: &str, + cx: &mut Context<Self>, + ) -> Task<Option<u64>> { + let request = self.request( + EvaluateCommand { + expression: format!("?${{sizeof({evaluate_name})}}"), + frame_id, + + context: Some(EvaluateArgumentsContext::Repl), + source: None, + }, + |_, response, _| response.ok(), + cx, + ); + cx.background_spawn(async move { + let result = request.await?; + result.result.parse().ok() + }) + } + + pub fn memory_reference_of_expr( + &mut self, + frame_id: Option<u64>, + expression: String, + cx: &mut Context<Self>, + ) -> Task<Option<String>> { + let request = self.request( + EvaluateCommand { + expression, + frame_id, + + context: Some(EvaluateArgumentsContext::Repl), + source: None, + }, + |_, response, _| response.ok(), + cx, + ); + cx.background_spawn(async move { + let result = request.await?; + result.memory_reference + }) + } + + pub fn write_memory(&mut self, address: u64, data: &[u8], cx: &mut Context<Self>) { + let data = base64::engine::general_purpose::STANDARD.encode(data); + self.request( + WriteMemoryArguments { + memory_reference: address.to_string(), + data, + allow_partial: None, + offset: None, + }, + |this, response, cx| { + this.memory.clear(cx.background_executor()); + this.invalidate_command_type::<ReadMemory>(); + this.invalidate_command_type::<VariablesCommand>(); + cx.emit(SessionEvent::Variables); + response.ok() + }, + cx, + ) + .detach(); + } + pub fn read_memory( + &mut self, + range: RangeInclusive<u64>, + cx: &mut Context<Self>, + ) -> MemoryIterator { + // This function is a bit more involved when it comes to fetching data. + // Since we attempt to read memory in pages, we need to account for some parts + // of memory being unreadable. Therefore, we start off by fetching a page per request. + // In case that fails, we try to re-fetch smaller regions until we have the full range. + let page_range = Memory::memory_range_to_page_range(range.clone()); + for page_address in PageAddress::iter_range(page_range) { + self.read_single_page_memory(page_address, cx); + } + self.memory.memory_range(range) + } + + fn read_single_page_memory(&mut self, page_start: PageAddress, cx: &mut Context<Self>) { + _ = maybe!({ + let builder = self.memory.build_page(page_start)?; + + self.memory_read_fetch_page_recursive(builder, cx); + Some(()) + }); + } + fn memory_read_fetch_page_recursive( + &mut self, + mut builder: MemoryPageBuilder, + cx: &mut Context<Self>, + ) { + let Some(next_request) = builder.next_request() else { + // We're done fetching. Let's grab the page and insert it into our memory store. + let (address, contents) = builder.build(); + self.memory.insert_page(address, contents); + + return; + }; + let size = next_request.size; + self.fetch( + ReadMemory { + memory_reference: format!("0x{:X}", next_request.address), + offset: Some(0), + count: next_request.size, + }, + move |this, memory, cx| { + if let Ok(memory) = memory { + builder.known(memory.content); + if let Some(unknown) = memory.unreadable_bytes { + builder.unknown(unknown); + } + // This is the recursive bit: if we're not yet done with + // the whole page, we'll kick off a new request with smaller range. + // Note that this function is recursive only conceptually; + // since it kicks off a new request with callback, we don't need to worry about stack overflow. + this.memory_read_fetch_page_recursive(builder, cx); + } else { + builder.unknown(size); + } + }, + cx, + ); + } + pub fn ignore_breakpoints(&self) -> bool { self.ignore_breakpoints } @@ -2378,6 +2518,8 @@ impl Session { move |this, response, cx| { let response = response.log_err()?; this.invalidate_command_type::<VariablesCommand>(); + this.invalidate_command_type::<ReadMemory>(); + this.memory.clear(cx.background_executor()); this.refresh_watchers(stack_frame_id, cx); cx.emit(SessionEvent::Variables); Some(response) @@ -2417,6 +2559,8 @@ impl Session { cx.spawn(async move |this, cx| { let response = request.await; this.update(cx, |this, cx| { + this.memory.clear(cx.background_executor()); + this.invalidate_command_type::<ReadMemory>(); match response { Ok(response) => { let event = dap::OutputEvent { diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index b5bdfdd8bbd6afda4ee1c2e08d50c4b9691a6892..3ba73f6dff54d919622d7961bdd8adbb8f9df8b6 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -668,7 +668,7 @@ impl ContextMenu { } } - fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) { + pub fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context<Self>) { if let Some(ix) = self.selected_index { let next_index = ix + 1; if self.items.len() <= next_index { From eca36c502e40e724d4ac6ad2f53750a4b7a39d30 Mon Sep 17 00:00:00 2001 From: Marshall Bowers <git@maxdeviant.com> Date: Mon, 14 Jul 2025 12:03:19 -0400 Subject: [PATCH 059/658] Route all LLM traffic through `cloud.zed.dev` (#34404) This PR makes it so all LLM traffic is routed through `cloud.zed.dev`. We're already routing `llm.zed.dev` to `cloud.zed.dev` on the server, but we want to standardize on `cloud.zed.dev` moving forward. Release Notes: - N/A --- Cargo.lock | 2 -- crates/feature_flags/src/feature_flags.rs | 11 ------- crates/http_client/src/http_client.rs | 15 ++-------- crates/language_models/Cargo.toml | 1 - crates/language_models/src/provider/cloud.rs | 31 +++++--------------- crates/web_search_providers/Cargo.toml | 1 - crates/web_search_providers/src/cloud.rs | 13 ++------ crates/zeta/src/zeta.rs | 10 ++----- 8 files changed, 13 insertions(+), 71 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index da46d191efb0723f2411ba506df259f4cb56ceaf..742c3515c99f34bf0e7f48f4e22327cd5e517017 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9036,7 +9036,6 @@ dependencies = [ "credentials_provider", "deepseek", "editor", - "feature_flags", "fs", "futures 0.3.31", "google_ai", @@ -18390,7 +18389,6 @@ version = "0.1.0" dependencies = [ "anyhow", "client", - "feature_flags", "futures 0.3.31", "gpui", "http_client", diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs index 9252977f7539ee4ac674e37f1b7c73d4651423d7..da85133bb9b6e201c271811e08fff9920f5503c5 100644 --- a/crates/feature_flags/src/feature_flags.rs +++ b/crates/feature_flags/src/feature_flags.rs @@ -98,17 +98,6 @@ impl FeatureFlag for AcpFeatureFlag { const NAME: &'static str = "acp"; } -pub struct ZedCloudFeatureFlag {} - -impl FeatureFlag for ZedCloudFeatureFlag { - const NAME: &'static str = "zed-cloud"; - - fn enabled_for_staff() -> bool { - // Require individual opt-in, for now. - false - } -} - pub trait FeatureFlagViewExt<V: 'static> { fn observe_flag<T: FeatureFlag, F>(&mut self, window: &Window, callback: F) -> Subscription where diff --git a/crates/http_client/src/http_client.rs b/crates/http_client/src/http_client.rs index c60a56002f5234f1085c65e25cfc0f1549fdbdb1..eebab86e21222a40f7c1e3c3285b63b523ecfd3b 100644 --- a/crates/http_client/src/http_client.rs +++ b/crates/http_client/src/http_client.rs @@ -226,21 +226,10 @@ impl HttpClientWithUrl { } /// Builds a Zed LLM URL using the given path. - pub fn build_zed_llm_url( - &self, - path: &str, - query: &[(&str, &str)], - use_cloud: bool, - ) -> Result<Url> { + pub fn build_zed_llm_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> { let base_url = self.base_url(); let base_api_url = match base_url.as_ref() { - "https://zed.dev" => { - if use_cloud { - "https://cloud.zed.dev" - } else { - "https://llm.zed.dev" - } - } + "https://zed.dev" => "https://cloud.zed.dev", "https://staging.zed.dev" => "https://llm-staging.zed.dev", "http://localhost:3000" => "http://localhost:8787", other => other, diff --git a/crates/language_models/Cargo.toml b/crates/language_models/Cargo.toml index 514443ddec57cff2efa94261f7c94dce25f609cd..0f248edd574819aee9ac1311ed23de30be48b21e 100644 --- a/crates/language_models/Cargo.toml +++ b/crates/language_models/Cargo.toml @@ -28,7 +28,6 @@ credentials_provider.workspace = true copilot.workspace = true deepseek = { workspace = true, features = ["schemars"] } editor.workspace = true -feature_flags.workspace = true fs.workspace = true futures.workspace = true google_ai = { workspace = true, features = ["schemars"] } diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index ede84713f17fca49ded8f7c927763d7c907a90c6..c044a318b878d49f2d37acd60fed29d565ae14a4 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -2,7 +2,6 @@ use anthropic::AnthropicModelMode; use anyhow::{Context as _, Result, anyhow}; use chrono::{DateTime, Utc}; use client::{Client, ModelRequestUsage, UserStore, zed_urls}; -use feature_flags::{FeatureFlagAppExt as _, ZedCloudFeatureFlag}; use futures::{ AsyncBufReadExt, FutureExt, Stream, StreamExt, future::BoxFuture, stream::BoxStream, }; @@ -137,7 +136,6 @@ impl State { cx: &mut Context<Self>, ) -> Self { let refresh_llm_token_listener = RefreshLlmTokenListener::global(cx); - let use_cloud = cx.has_flag::<ZedCloudFeatureFlag>(); Self { client: client.clone(), @@ -165,7 +163,7 @@ impl State { .await; } - let response = Self::fetch_models(client, llm_api_token, use_cloud).await?; + let response = Self::fetch_models(client, llm_api_token).await?; this.update(cx, |this, cx| { this.update_models(response, cx); }) @@ -184,7 +182,7 @@ impl State { let llm_api_token = this.llm_api_token.clone(); cx.spawn(async move |this, cx| { llm_api_token.refresh(&client).await?; - let response = Self::fetch_models(client, llm_api_token, use_cloud).await?; + let response = Self::fetch_models(client, llm_api_token).await?; this.update(cx, |this, cx| { this.update_models(response, cx); }) @@ -268,18 +266,13 @@ impl State { async fn fetch_models( client: Arc<Client>, llm_api_token: LlmApiToken, - use_cloud: bool, ) -> Result<ListModelsResponse> { let http_client = &client.http_client(); let token = llm_api_token.acquire(&client).await?; let request = http_client::Request::builder() .method(Method::GET) - .uri( - http_client - .build_zed_llm_url("/models", &[], use_cloud)? - .as_ref(), - ) + .uri(http_client.build_zed_llm_url("/models", &[])?.as_ref()) .header("Authorization", format!("Bearer {token}")) .body(AsyncBody::empty())?; let mut response = http_client @@ -543,7 +536,6 @@ impl CloudLanguageModel { llm_api_token: LlmApiToken, app_version: Option<SemanticVersion>, body: CompletionBody, - use_cloud: bool, ) -> Result<PerformLlmCompletionResponse> { let http_client = &client.http_client(); @@ -551,11 +543,9 @@ impl CloudLanguageModel { let mut refreshed_token = false; loop { - let request_builder = http_client::Request::builder().method(Method::POST).uri( - http_client - .build_zed_llm_url("/completions", &[], use_cloud)? - .as_ref(), - ); + let request_builder = http_client::Request::builder() + .method(Method::POST) + .uri(http_client.build_zed_llm_url("/completions", &[])?.as_ref()); let request_builder = if let Some(app_version) = app_version { request_builder.header(ZED_VERSION_HEADER_NAME, app_version.to_string()) } else { @@ -782,7 +772,6 @@ impl LanguageModel for CloudLanguageModel { let model_id = self.model.id.to_string(); let generate_content_request = into_google(request, model_id.clone(), GoogleModelMode::Default); - let use_cloud = cx.has_flag::<ZedCloudFeatureFlag>(); async move { let http_client = &client.http_client(); let token = llm_api_token.acquire(&client).await?; @@ -798,7 +787,7 @@ impl LanguageModel for CloudLanguageModel { .method(Method::POST) .uri( http_client - .build_zed_llm_url("/count_tokens", &[], use_cloud)? + .build_zed_llm_url("/count_tokens", &[])? .as_ref(), ) .header("Content-Type", "application/json") @@ -847,9 +836,6 @@ impl LanguageModel for CloudLanguageModel { let intent = request.intent; let mode = request.mode; let app_version = cx.update(|cx| AppVersion::global(cx)).ok(); - let use_cloud = cx - .update(|cx| cx.has_flag::<ZedCloudFeatureFlag>()) - .unwrap_or(false); let thinking_allowed = request.thinking_allowed; match self.model.provider { zed_llm_client::LanguageModelProvider::Anthropic => { @@ -888,7 +874,6 @@ impl LanguageModel for CloudLanguageModel { provider_request: serde_json::to_value(&request) .map_err(|e| anyhow!(e))?, }, - use_cloud, ) .await .map_err(|err| match err.downcast::<ApiError>() { @@ -941,7 +926,6 @@ impl LanguageModel for CloudLanguageModel { provider_request: serde_json::to_value(&request) .map_err(|e| anyhow!(e))?, }, - use_cloud, ) .await?; @@ -982,7 +966,6 @@ impl LanguageModel for CloudLanguageModel { provider_request: serde_json::to_value(&request) .map_err(|e| anyhow!(e))?, }, - use_cloud, ) .await?; diff --git a/crates/web_search_providers/Cargo.toml b/crates/web_search_providers/Cargo.toml index 208cb63593f0647970c1b576f1733740ee99196c..2e052796c48601565e5e6870f9848f8dcd9354b1 100644 --- a/crates/web_search_providers/Cargo.toml +++ b/crates/web_search_providers/Cargo.toml @@ -14,7 +14,6 @@ path = "src/web_search_providers.rs" [dependencies] anyhow.workspace = true client.workspace = true -feature_flags.workspace = true futures.workspace = true gpui.workspace = true http_client.workspace = true diff --git a/crates/web_search_providers/src/cloud.rs b/crates/web_search_providers/src/cloud.rs index 79ccf97e47aacdeaa1da0cc2f063b3937e4f955f..adf79b0ff68c4d569dbf7cd40951c7c6c9761583 100644 --- a/crates/web_search_providers/src/cloud.rs +++ b/crates/web_search_providers/src/cloud.rs @@ -2,7 +2,6 @@ use std::sync::Arc; use anyhow::{Context as _, Result}; use client::Client; -use feature_flags::{FeatureFlagAppExt as _, ZedCloudFeatureFlag}; use futures::AsyncReadExt as _; use gpui::{App, AppContext, Context, Entity, Subscription, Task}; use http_client::{HttpClient, Method}; @@ -63,10 +62,7 @@ impl WebSearchProvider for CloudWebSearchProvider { let client = state.client.clone(); let llm_api_token = state.llm_api_token.clone(); let body = WebSearchBody { query }; - let use_cloud = cx.has_flag::<ZedCloudFeatureFlag>(); - cx.background_spawn(async move { - perform_web_search(client, llm_api_token, body, use_cloud).await - }) + cx.background_spawn(async move { perform_web_search(client, llm_api_token, body).await }) } } @@ -74,7 +70,6 @@ async fn perform_web_search( client: Arc<Client>, llm_api_token: LlmApiToken, body: WebSearchBody, - use_cloud: bool, ) -> Result<WebSearchResponse> { const MAX_RETRIES: usize = 3; @@ -91,11 +86,7 @@ async fn perform_web_search( let request = http_client::Request::builder() .method(Method::POST) - .uri( - http_client - .build_zed_llm_url("/web_search", &[], use_cloud)? - .as_ref(), - ) + .uri(http_client.build_zed_llm_url("/web_search", &[])?.as_ref()) .header("Content-Type", "application/json") .header("Authorization", format!("Bearer {token}")) .body(serde_json::to_string(&body)?.into())?; diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 12d3d4bfbc7aae1c5b2ed2d36c7a89dd1f526723..87cd1e604c3fd422c2ea9c218cbed755e72925cf 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -8,7 +8,6 @@ mod rate_completion_modal; pub(crate) use completion_diff_element::*; use db::kvp::KEY_VALUE_STORE; -use feature_flags::{FeatureFlagAppExt as _, ZedCloudFeatureFlag}; pub use init::*; use inline_completion::DataCollectionState; use license_detection::LICENSE_FILES_TO_CHECK; @@ -391,7 +390,6 @@ impl Zeta { let client = self.client.clone(); let llm_token = self.llm_token.clone(); let app_version = AppVersion::global(cx); - let use_cloud = cx.has_flag::<ZedCloudFeatureFlag>(); let buffer = buffer.clone(); @@ -482,7 +480,6 @@ impl Zeta { llm_token, app_version, body, - use_cloud, }) .await; let (response, usage) = match response { @@ -748,7 +745,6 @@ and then another llm_token, app_version, body, - use_cloud, .. } = params; @@ -764,7 +760,7 @@ and then another } else { request_builder.uri( http_client - .build_zed_llm_url("/predict_edits/v2", &[], use_cloud)? + .build_zed_llm_url("/predict_edits/v2", &[])? .as_ref(), ) }; @@ -824,7 +820,6 @@ and then another let client = self.client.clone(); let llm_token = self.llm_token.clone(); let app_version = AppVersion::global(cx); - let use_cloud = cx.has_flag::<ZedCloudFeatureFlag>(); cx.spawn(async move |this, cx| { let http_client = client.http_client(); let mut response = llm_token_retry(&llm_token, &client, |token| { @@ -835,7 +830,7 @@ and then another } else { request_builder.uri( http_client - .build_zed_llm_url("/predict_edits/accept", &[], use_cloud)? + .build_zed_llm_url("/predict_edits/accept", &[])? .as_ref(), ) }; @@ -1131,7 +1126,6 @@ struct PerformPredictEditsParams { pub llm_token: LlmApiToken, pub app_version: SemanticVersion, pub body: PredictEditsBody, - pub use_cloud: bool, } #[derive(Error, Debug)] From 45d0686129694ef19d83697a1d6f0cd0f47440f9 Mon Sep 17 00:00:00 2001 From: Conrad Irwin <conrad.irwin@gmail.com> Date: Mon, 14 Jul 2025 11:03:16 -0600 Subject: [PATCH 060/658] Remove unused KeycodeSource (#34403) Release Notes: - N/A --- crates/gpui/src/platform/linux/platform.rs | 117 ++++++++---------- .../gpui/src/platform/linux/wayland/client.rs | 20 +-- crates/gpui/src/platform/linux/x11/client.rs | 9 +- 3 files changed, 59 insertions(+), 87 deletions(-) diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index 743cac71f37d44ea187b0301bdf43c305cac8d04..bab44e0069319030915814f2d9a6b471d1e56ebb 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -707,69 +707,57 @@ pub(super) fn log_cursor_icon_warning(message: impl std::fmt::Display) { } #[cfg(any(feature = "wayland", feature = "x11"))] -pub(crate) enum KeycodeSource { - X11, - Wayland, -} - -#[cfg(any(feature = "wayland", feature = "x11"))] -impl KeycodeSource { - fn guess_ascii(&self, keycode: Keycode, shift: bool) -> Option<char> { - // For historical reasons, X11 adds 8 to keycodes. - // Wayland doesn't, but by this point, our own Wayland client - // has added 8 for X11 compatibility. - let raw = keycode.raw() - 8; - let c = match (raw, shift) { - (16, _) => 'q', - (17, _) => 'w', - (18, _) => 'e', - (19, _) => 'r', - (20, _) => 't', - (21, _) => 'y', - (22, _) => 'u', - (23, _) => 'i', - (24, _) => 'o', - (25, _) => 'p', - (26, false) => '[', - (26, true) => '{', - (27, false) => ']', - (27, true) => '}', - (30, _) => 'a', - (31, _) => 's', - (32, _) => 'd', - (33, _) => 'f', - (34, _) => 'g', - (35, _) => 'h', - (36, _) => 'j', - (37, _) => 'k', - (38, _) => 'l', - (39, false) => ';', - (39, true) => ':', - (40, false) => '\'', - (40, true) => '"', - (41, false) => '`', - (41, true) => '~', - (43, false) => '\\', - (43, true) => '|', - (44, _) => 'z', - (45, _) => 'x', - (46, _) => 'c', - (47, _) => 'v', - (48, _) => 'b', - (49, _) => 'n', - (50, _) => 'm', - (51, false) => ',', - (51, true) => '>', - (52, false) => '.', - (52, true) => '<', - (53, false) => '/', - (53, true) => '?', - - _ => return None, - }; - - Some(c) - } +fn guess_ascii(keycode: Keycode, shift: bool) -> Option<char> { + let c = match (keycode.raw(), shift) { + (24, _) => 'q', + (25, _) => 'w', + (26, _) => 'e', + (27, _) => 'r', + (28, _) => 't', + (29, _) => 'y', + (30, _) => 'u', + (31, _) => 'i', + (32, _) => 'o', + (33, _) => 'p', + (34, false) => '[', + (34, true) => '{', + (35, false) => ']', + (35, true) => '}', + (38, _) => 'a', + (39, _) => 's', + (40, _) => 'd', + (41, _) => 'f', + (42, _) => 'g', + (43, _) => 'h', + (44, _) => 'j', + (45, _) => 'k', + (46, _) => 'l', + (47, false) => ';', + (47, true) => ':', + (48, false) => '\'', + (48, true) => '"', + (49, false) => '`', + (49, true) => '~', + (51, false) => '\\', + (51, true) => '|', + (52, _) => 'z', + (53, _) => 'x', + (54, _) => 'c', + (55, _) => 'v', + (56, _) => 'b', + (57, _) => 'n', + (58, _) => 'm', + (59, false) => ',', + (59, true) => '>', + (60, false) => '.', + (60, true) => '<', + (61, false) => '/', + (61, true) => '?', + + _ => return None, + }; + + Some(c) } #[cfg(any(feature = "wayland", feature = "x11"))] @@ -778,7 +766,6 @@ impl crate::Keystroke { state: &State, mut modifiers: crate::Modifiers, keycode: Keycode, - source: KeycodeSource, ) -> Self { let key_utf32 = state.key_get_utf32(keycode); let key_utf8 = state.key_get_utf8(keycode); @@ -840,7 +827,7 @@ impl crate::Keystroke { let name = xkb::keysym_get_name(key_sym).to_lowercase(); if key_sym.is_keypad_key() { name.replace("kp_", "") - } else if let Some(key_en) = source.guess_ascii(keycode, modifiers.shift) { + } else if let Some(key_en) = guess_ascii(keycode, modifiers.shift) { String::from(key_en) } else { name diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index b3a101568f627bb31c9be76c3eb222e24442b9d3..57d1dcec04ee6aa1828c98286c9115df4ccb6d44 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -69,6 +69,7 @@ use super::{ window::{ImeInput, WaylandWindowStatePtr}, }; +use crate::platform::{PlatformWindow, blade::BladeContext}; use crate::{ AnyWindowHandle, Bounds, Capslock, CursorStyle, DOUBLE_CLICK_INTERVAL, DevicePixels, DisplayId, FileDropEvent, ForegroundExecutor, KeyDownEvent, KeyUpEvent, Keystroke, LinuxCommon, @@ -77,10 +78,6 @@ use crate::{ PlatformInput, PlatformKeyboardLayout, Point, SCROLL_LINES, ScaledPixels, ScrollDelta, ScrollWheelEvent, Size, TouchPhase, WindowParams, point, px, size, }; -use crate::{ - KeycodeSource, - platform::{PlatformWindow, blade::BladeContext}, -}; use crate::{ SharedString, platform::linux::{ @@ -1296,12 +1293,8 @@ 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, - KeycodeSource::Wayland, - ); + let mut keystroke = + Keystroke::from_xkb(&keymap_state, state.modifiers, keycode); if let Some(mut compose) = state.compose_state.take() { compose.feed(keysym); match compose.status() { @@ -1386,12 +1379,7 @@ impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandClientStatePtr { } wl_keyboard::KeyState::Released if !keysym.is_modifier_key() => { let input = PlatformInput::KeyUp(KeyUpEvent { - keystroke: Keystroke::from_xkb( - keymap_state, - state.modifiers, - keycode, - KeycodeSource::Wayland, - ), + keystroke: Keystroke::from_xkb(keymap_state, state.modifiers, keycode), }); if state.repeat.current_keycode == Some(keycode) { diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index 56d4f5367d7abfc86dff1492e81a13300bda2cd5..6cff977128ec594d683085e5f2cc24683c9e9ba7 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -1,4 +1,4 @@ -use crate::{Capslock, KeycodeSource, xcb_flush}; +use crate::{Capslock, xcb_flush}; use core::str; use std::{ cell::RefCell, @@ -1034,8 +1034,7 @@ impl X11Client { xkb_state.latched_layout, xkb_state.locked_layout, ); - let mut keystroke = - crate::Keystroke::from_xkb(&state.xkb, modifiers, code, KeycodeSource::X11); + let mut keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code); let keysym = state.xkb.key_get_one_sym(code); if keysym.is_modifier_key() { return Some(()); @@ -1103,8 +1102,7 @@ impl X11Client { xkb_state.latched_layout, xkb_state.locked_layout, ); - let keystroke = - crate::Keystroke::from_xkb(&state.xkb, modifiers, code, KeycodeSource::X11); + let keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code); let keysym = state.xkb.key_get_one_sym(code); if keysym.is_modifier_key() { return Some(()); @@ -1328,7 +1326,6 @@ impl X11Client { &state.xkb, state.modifiers, event.detail.into(), - KeycodeSource::X11, )); let (mut ximc, mut xim_handler) = state.take_xim()?; drop(state); From 4848bd705e66042624ff041f91259d735e0dd929 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 14 Jul 2025 19:18:09 +0200 Subject: [PATCH 061/658] docs/debugger: Remove mention of onboarding calls (#34414) Closes #ISSUE Release Notes: - N/A --- docs/src/debugger.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/src/debugger.md b/docs/src/debugger.md index f10461a1603baeb76b30f60a36f945be0b4895df..02c17c412785c2c05f4c4b8e83cc12f0df980e0d 100644 --- a/docs/src/debugger.md +++ b/docs/src/debugger.md @@ -8,9 +8,6 @@ Zed implements the client side of the protocol, and various _debug adapters_ imp This protocol enables features like setting breakpoints, stepping through code, inspecting variables, and more, in a consistent manner across different programming languages and runtime environments. -> We currently offer onboarding support for users. We are eager to hear from you if you encounter any issues or have suggestions for improvement for our debugging experience. -> You can schedule a call via [Cal.com](https://cal.com/team/zed-research/debugger) - ## Supported Languages To debug code written in a specific language, Zed needs to find a debug adapter for that language. Some debug adapters are provided by Zed without additional setup, and some are provided by [language extensions](./extensions/debugger-extensions.md). The following languages currently have debug adapters available: From 8b6b039b6393cba2588625a6dcf8f519b74863e1 Mon Sep 17 00:00:00 2001 From: domi <152265924+domi413@users.noreply.github.com> Date: Mon, 14 Jul 2025 19:20:19 +0200 Subject: [PATCH 062/658] vim: Add missing normal mode binding for signature help overload (#34278) Closes #ISSUE related https://github.com/zed-industries/zed/pull/33199 --- assets/keymaps/vim.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 5437be39baa39326a4a54ade4f0bd2096e089566..dcb52e5250335531ebfa6e4146614ee8b9adf73a 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -466,7 +466,7 @@ } }, { - "context": "vim_mode == insert && showing_signature_help && !showing_completions", + "context": "(vim_mode == insert || vim_mode == normal) && showing_signature_help && !showing_completions", "bindings": { "ctrl-p": "editor::SignatureHelpPrevious", "ctrl-n": "editor::SignatureHelpNext" From fd5650d4ed5663e0135c854cac5d6d01d1a3b100 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Mon, 14 Jul 2025 13:45:46 -0400 Subject: [PATCH 063/658] debugger: A support for data breakpoint's on variables (#34391) Closes #ISSUE Release Notes: - N/A *or* Added/Fixed/Improved ... --------- Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com> Co-authored-by: Mikayla Maki <mikayla@zed.dev> --- crates/debugger_ui/src/debugger_ui.rs | 2 + .../src/session/running/breakpoint_list.rs | 177 ++++++++++++++-- .../src/session/running/memory_view.rs | 79 +++++++- .../src/session/running/variable_list.rs | 189 ++++++++++++++---- crates/project/src/debugger/dap_command.rs | 148 ++++++++++++-- crates/project/src/debugger/session.rs | 82 +++++++- 6 files changed, 589 insertions(+), 88 deletions(-) diff --git a/crates/debugger_ui/src/debugger_ui.rs b/crates/debugger_ui/src/debugger_ui.rs index 2056232e9bd6912bbd1b4b7da7b51b769a47e63a..c932f1b600effa424db6b995d8128dca5c29594f 100644 --- a/crates/debugger_ui/src/debugger_ui.rs +++ b/crates/debugger_ui/src/debugger_ui.rs @@ -83,6 +83,8 @@ actions!( Rerun, /// Toggles expansion of the selected item in the debugger UI. ToggleExpandItem, + /// Set a data breakpoint on the selected variable or memory region. + ToggleDataBreakpoint, ] ); diff --git a/crates/debugger_ui/src/session/running/breakpoint_list.rs b/crates/debugger_ui/src/session/running/breakpoint_list.rs index 78c87db2e6f2a1f9d54368b875d1e86b3ac5789f..6ac4b1c878e448cd98ba8aa3f297b1c642151c4e 100644 --- a/crates/debugger_ui/src/session/running/breakpoint_list.rs +++ b/crates/debugger_ui/src/session/running/breakpoint_list.rs @@ -24,10 +24,10 @@ use project::{ }; use ui::{ ActiveTheme, AnyElement, App, ButtonCommon, Clickable, Color, Context, Disableable, Div, - Divider, FluentBuilder as _, Icon, IconButton, IconName, IconSize, Indicator, - InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ListItem, ParentElement, - Render, RenderOnce, Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, - Styled, Toggleable, Tooltip, Window, div, h_flex, px, v_flex, + 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, }; use util::ResultExt; use workspace::Workspace; @@ -46,6 +46,7 @@ actions!( pub(crate) enum SelectedBreakpointKind { Source, Exception, + Data, } pub(crate) struct BreakpointList { workspace: WeakEntity<Workspace>, @@ -188,6 +189,9 @@ impl BreakpointList { BreakpointEntryKind::ExceptionBreakpoint(bp) => { (SelectedBreakpointKind::Exception, bp.is_enabled) } + BreakpointEntryKind::DataBreakpoint(bp) => { + (SelectedBreakpointKind::Data, bp.0.is_enabled) + } }) }) } @@ -391,7 +395,8 @@ impl BreakpointList { let row = line_breakpoint.breakpoint.row; self.go_to_line_breakpoint(path, row, window, cx); } - BreakpointEntryKind::ExceptionBreakpoint(_) => {} + BreakpointEntryKind::DataBreakpoint(_) + | BreakpointEntryKind::ExceptionBreakpoint(_) => {} } } @@ -421,6 +426,10 @@ impl BreakpointList { let id = exception_breakpoint.id.clone(); self.toggle_exception_breakpoint(&id, cx); } + BreakpointEntryKind::DataBreakpoint(data_breakpoint) => { + let id = data_breakpoint.0.dap.data_id.clone(); + self.toggle_data_breakpoint(&id, cx); + } } cx.notify(); } @@ -441,7 +450,7 @@ impl BreakpointList { let row = line_breakpoint.breakpoint.row; self.edit_line_breakpoint(path, row, BreakpointEditAction::Toggle, cx); } - BreakpointEntryKind::ExceptionBreakpoint(_) => {} + _ => {} } cx.notify(); } @@ -490,6 +499,14 @@ impl BreakpointList { cx.notify(); } + fn toggle_data_breakpoint(&mut self, id: &str, cx: &mut Context<Self>) { + if let Some(session) = &self.session { + session.update(cx, |this, cx| { + this.toggle_data_breakpoint(&id, cx); + }); + } + } + fn toggle_exception_breakpoint(&mut self, id: &str, cx: &mut Context<Self>) { if let Some(session) = &self.session { session.update(cx, |this, cx| { @@ -642,6 +659,7 @@ impl BreakpointList { SelectedBreakpointKind::Exception => { "Exception Breakpoints cannot be removed from the breakpoint list" } + SelectedBreakpointKind::Data => "Remove data breakpoint from a breakpoint list", }); let toggle_label = selection_kind.map(|(_, is_enabled)| { if is_enabled { @@ -783,8 +801,20 @@ impl Render for BreakpointList { weak: weak.clone(), }) }); - self.breakpoints - .extend(breakpoints.chain(exception_breakpoints)); + let data_breakpoints = self.session.as_ref().into_iter().flat_map(|session| { + session + .read(cx) + .data_breakpoints() + .map(|state| BreakpointEntry { + kind: BreakpointEntryKind::DataBreakpoint(DataBreakpoint(state.clone())), + weak: weak.clone(), + }) + }); + self.breakpoints.extend( + breakpoints + .chain(data_breakpoints) + .chain(exception_breakpoints), + ); v_flex() .id("breakpoint-list") .key_context("BreakpointList") @@ -905,7 +935,11 @@ impl LineBreakpoint { .ok(); } }) - .child(Indicator::icon(Icon::new(icon_name)).color(Color::Debugger)) + .child( + Icon::new(icon_name) + .color(Color::Debugger) + .size(IconSize::XSmall), + ) .on_mouse_down(MouseButton::Left, move |_, _, _| {}); ListItem::new(SharedString::from(format!( @@ -996,6 +1030,103 @@ struct ExceptionBreakpoint { data: ExceptionBreakpointsFilter, is_enabled: bool, } +#[derive(Clone, Debug)] +struct DataBreakpoint(project::debugger::session::DataBreakpointState); + +impl DataBreakpoint { + fn render( + &self, + props: SupportedBreakpointProperties, + strip_mode: Option<ActiveBreakpointStripMode>, + ix: usize, + is_selected: bool, + focus_handle: FocusHandle, + list: WeakEntity<BreakpointList>, + ) -> ListItem { + let color = if self.0.is_enabled { + Color::Debugger + } else { + Color::Muted + }; + 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() + .start_slot( + div() + .id(SharedString::from(format!( + "data-breakpoint-ui-item-{}-click-handler", + self.0.dap.data_id + ))) + .tooltip({ + let focus_handle = focus_handle.clone(); + move |window, cx| { + Tooltip::for_action_in( + if is_enabled { + "Disable Data Breakpoint" + } else { + "Enable Data Breakpoint" + }, + &ToggleEnableBreakpoint, + &focus_handle, + window, + cx, + ) + } + }) + .on_click({ + let list = list.clone(); + move |_, _, cx| { + list.update(cx, |this, cx| { + this.toggle_data_breakpoint(&id, cx); + }) + .ok(); + } + }) + .cursor_pointer() + .child( + Icon::new(IconName::Binary) + .color(color) + .size(IconSize::Small), + ), + ) + .child( + h_flex() + .w_full() + .mr_4() + .py_0p5() + .justify_between() + .child( + v_flex() + .py_1() + .gap_1() + .min_h(px(26.)) + .justify_center() + .id(("data-breakpoint-label", ix)) + .child( + Label::new(self.0.context.human_readable_label()) + .size(LabelSize::Small) + .line_height_style(ui::LineHeightStyle::UiLabel), + ), + ) + .child(BreakpointOptionsStrip { + props, + breakpoint: BreakpointEntry { + kind: BreakpointEntryKind::DataBreakpoint(self.clone()), + weak: list, + }, + is_selected, + focus_handle, + strip_mode, + index: ix, + }), + ) + .toggle_state(is_selected) + } +} impl ExceptionBreakpoint { fn render( @@ -1062,7 +1193,11 @@ impl ExceptionBreakpoint { } }) .cursor_pointer() - .child(Indicator::icon(Icon::new(IconName::Flame)).color(color)), + .child( + Icon::new(IconName::Flame) + .color(color) + .size(IconSize::Small), + ), ) .child( h_flex() @@ -1105,6 +1240,7 @@ impl ExceptionBreakpoint { enum BreakpointEntryKind { LineBreakpoint(LineBreakpoint), ExceptionBreakpoint(ExceptionBreakpoint), + DataBreakpoint(DataBreakpoint), } #[derive(Clone, Debug)] @@ -1140,6 +1276,14 @@ impl BreakpointEntry { focus_handle, self.weak.clone(), ), + BreakpointEntryKind::DataBreakpoint(data_breakpoint) => data_breakpoint.render( + props.for_data_breakpoints(), + strip_mode, + ix, + is_selected, + focus_handle, + self.weak.clone(), + ), } } @@ -1155,6 +1299,11 @@ impl BreakpointEntry { exception_breakpoint.id ) .into(), + BreakpointEntryKind::DataBreakpoint(data_breakpoint) => format!( + "data-breakpoint-control-strip--{}", + data_breakpoint.0.dap.data_id + ) + .into(), } } @@ -1172,8 +1321,8 @@ impl BreakpointEntry { BreakpointEntryKind::LineBreakpoint(line_breakpoint) => { line_breakpoint.breakpoint.condition.is_some() } - // We don't support conditions on exception breakpoints - BreakpointEntryKind::ExceptionBreakpoint(_) => false, + // We don't support conditions on exception/data breakpoints + _ => false, } } @@ -1225,6 +1374,10 @@ impl SupportedBreakpointProperties { // TODO: we don't yet support conditions for exception breakpoints at the data layer, hence all props are disabled here. Self::empty() } + fn for_data_breakpoints(self) -> Self { + // TODO: we don't yet support conditions for data breakpoints at the data layer, hence all props are disabled here. + Self::empty() + } } #[derive(IntoElement)] struct BreakpointOptionsStrip { diff --git a/crates/debugger_ui/src/session/running/memory_view.rs b/crates/debugger_ui/src/session/running/memory_view.rs index e9dcb0839d104e6a241378fb9f201f20196bed87..9d946449544ecfd1a9c91c11918aaf1becb3d4d0 100644 --- a/crates/debugger_ui/src/session/running/memory_view.rs +++ b/crates/debugger_ui/src/session/running/memory_view.rs @@ -1,4 +1,10 @@ -use std::{fmt::Write, ops::RangeInclusive, sync::LazyLock, time::Duration}; +use std::{ + cell::LazyCell, + fmt::Write, + ops::RangeInclusive, + sync::{Arc, LazyLock}, + time::Duration, +}; use editor::{Editor, EditorElement, EditorStyle}; use gpui::{ @@ -8,7 +14,7 @@ use gpui::{ deferred, point, size, uniform_list, }; use notifications::status_toast::{StatusToast, ToastIcon}; -use project::debugger::{MemoryCell, session::Session}; +use project::debugger::{MemoryCell, dap_command::DataBreakpointContext, session::Session}; use settings::Settings; use theme::ThemeSettings; use ui::{ @@ -20,7 +26,7 @@ use ui::{ use util::ResultExt; use workspace::Workspace; -use crate::session::running::stack_frame_list::StackFrameList; +use crate::{ToggleDataBreakpoint, session::running::stack_frame_list::StackFrameList}; actions!(debugger, [GoToSelectedAddress]); @@ -446,6 +452,48 @@ impl MemoryView { } } + fn toggle_data_breakpoint( + &mut self, + _: &crate::ToggleDataBreakpoint, + _: &mut Window, + cx: &mut Context<Self>, + ) { + let Some(SelectedMemoryRange::DragComplete(selection)) = self.view_state.selection.clone() + else { + return; + }; + let range = selection.memory_range(); + let context = Arc::new(DataBreakpointContext::Address { + address: range.start().to_string(), + bytes: Some(*range.end() - *range.start()), + }); + + self.session.update(cx, |this, cx| { + 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 { + return; + }; + _ = this.update(cx, |this, cx| { + this.create_data_breakpoint( + context, + data_id.clone(), + dap::DataBreakpoint { + data_id, + access_type: None, + condition: None, + hit_condition: None, + }, + cx, + ); + }); + } + }) + .detach(); + }) + } + fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) { if let Some(SelectedMemoryRange::DragComplete(drag)) = &self.view_state.selection { // Go into memory writing mode. @@ -599,18 +647,30 @@ impl MemoryView { let session = self.session.clone(); let context_menu = ContextMenu::build(window, cx, |menu, _, cx| { let range_too_large = range.end() - range.start() > std::mem::size_of::<u64>() as u64; - let memory_unreadable = |cx| { + let caps = session.read(cx).capabilities(); + let supports_data_breakpoints = caps.supports_data_breakpoints.unwrap_or_default() + && caps.supports_data_breakpoint_bytes.unwrap_or_default(); + let memory_unreadable = LazyCell::new(|| { session.update(cx, |this, cx| { this.read_memory(range.clone(), cx) .any(|cell| cell.0.is_none()) }) - }; - menu.action_disabled_when( - range_too_large || memory_unreadable(cx), + }); + + let mut menu = menu.action_disabled_when( + range_too_large || *memory_unreadable, "Go To Selected Address", GoToSelectedAddress.boxed_clone(), - ) - .context(self.focus_handle.clone()) + ); + + if supports_data_breakpoints { + menu = menu.action_disabled_when( + *memory_unreadable, + "Set Data Breakpoint", + ToggleDataBreakpoint.boxed_clone(), + ); + } + menu.context(self.focus_handle.clone()) }); cx.focus_view(&context_menu, window); @@ -834,6 +894,7 @@ impl Render for MemoryView { .on_action(cx.listener(Self::go_to_address)) .p_1() .on_action(cx.listener(Self::confirm)) + .on_action(cx.listener(Self::toggle_data_breakpoint)) .on_action(cx.listener(Self::page_down)) .on_action(cx.listener(Self::page_up)) .size_full() diff --git a/crates/debugger_ui/src/session/running/variable_list.rs b/crates/debugger_ui/src/session/running/variable_list.rs index c7df449ee6c6ea55e1420080f216fd07aed5f370..b158314b507317227a6b94c8916c7a2c125f2380 100644 --- a/crates/debugger_ui/src/session/running/variable_list.rs +++ b/crates/debugger_ui/src/session/running/variable_list.rs @@ -13,7 +13,10 @@ use gpui::{ uniform_list, }; use menu::{SelectFirst, SelectLast, SelectNext, SelectPrevious}; -use project::debugger::session::{Session, SessionEvent, Watcher}; +use project::debugger::{ + dap_command::DataBreakpointContext, + session::{Session, SessionEvent, Watcher}, +}; use std::{collections::HashMap, ops::Range, sync::Arc}; use ui::{ContextMenu, ListItem, ScrollableHandle, Scrollbar, ScrollbarState, Tooltip, prelude::*}; use util::{debug_panic, maybe}; @@ -220,6 +223,7 @@ impl VariableList { SessionEvent::Variables | SessionEvent::Watchers => { this.build_entries(cx); } + _ => {} }), cx.on_focus_out(&focus_handle, window, |this, _, _, cx| { @@ -625,50 +629,156 @@ impl VariableList { window: &mut Window, cx: &mut Context<Self>, ) { - let supports_set_variable = self - .session - .read(cx) - .capabilities() - .supports_set_variable - .unwrap_or_default(); - - let context_menu = ContextMenu::build(window, cx, |menu, _, _| { - menu.when(entry.as_variable().is_some(), |menu| { - menu.action("Copy Name", CopyVariableName.boxed_clone()) - .action("Copy Value", CopyVariableValue.boxed_clone()) - .when(supports_set_variable, |menu| { - menu.action("Edit Value", EditVariable.boxed_clone()) + let (supports_set_variable, supports_data_breakpoints, supports_go_to_memory) = + self.session.read_with(cx, |session, _| { + ( + session + .capabilities() + .supports_set_variable + .unwrap_or_default(), + session + .capabilities() + .supports_data_breakpoints + .unwrap_or_default(), + session + .capabilities() + .supports_read_memory_request + .unwrap_or_default(), + ) + }); + let can_toggle_data_breakpoint = entry + .as_variable() + .filter(|_| supports_data_breakpoints) + .and_then(|variable| { + let variables_reference = self + .entry_states + .get(&entry.path) + .map(|state| state.parent_reference)?; + Some(self.session.update(cx, |session, cx| { + session.data_breakpoint_info( + Arc::new(DataBreakpointContext::Variable { + variables_reference, + name: variable.name.clone(), + bytes: None, + }), + None, + cx, + ) + })) + }); + + let focus_handle = self.focus_handle.clone(); + cx.spawn_in(window, async move |this, cx| { + let can_toggle_data_breakpoint = if let Some(task) = can_toggle_data_breakpoint { + task.await.is_some() + } else { + true + }; + cx.update(|window, cx| { + let context_menu = ContextMenu::build(window, cx, |menu, _, _| { + menu.when_some(entry.as_variable(), |menu, _| { + menu.action("Copy Name", CopyVariableName.boxed_clone()) + .action("Copy Value", CopyVariableValue.boxed_clone()) + .when(supports_set_variable, |menu| { + menu.action("Edit Value", EditVariable.boxed_clone()) + }) + .when(supports_go_to_memory, |menu| { + menu.action("Go To Memory", GoToMemory.boxed_clone()) + }) + .action("Watch Variable", AddWatch.boxed_clone()) + .when(can_toggle_data_breakpoint, |menu| { + menu.action( + "Toggle Data Breakpoint", + crate::ToggleDataBreakpoint.boxed_clone(), + ) + }) }) - .action("Watch Variable", AddWatch.boxed_clone()) - .action("Go To Memory", GoToMemory.boxed_clone()) - }) - .when(entry.as_watcher().is_some(), |menu| { - menu.action("Copy Name", CopyVariableName.boxed_clone()) - .action("Copy Value", CopyVariableValue.boxed_clone()) - .when(supports_set_variable, |menu| { - menu.action("Edit Value", EditVariable.boxed_clone()) + .when(entry.as_watcher().is_some(), |menu| { + menu.action("Copy Name", CopyVariableName.boxed_clone()) + .action("Copy Value", CopyVariableValue.boxed_clone()) + .when(supports_set_variable, |menu| { + menu.action("Edit Value", EditVariable.boxed_clone()) + }) + .action("Remove Watch", RemoveWatch.boxed_clone()) }) - .action("Remove Watch", RemoveWatch.boxed_clone()) + .context(focus_handle.clone()) + }); + + _ = this.update(cx, |this, cx| { + cx.focus_view(&context_menu, window); + let subscription = cx.subscribe_in( + &context_menu, + window, + |this, _, _: &DismissEvent, window, cx| { + if this.open_context_menu.as_ref().is_some_and(|context_menu| { + context_menu.0.focus_handle(cx).contains_focused(window, cx) + }) { + cx.focus_self(window); + } + this.open_context_menu.take(); + cx.notify(); + }, + ); + + this.open_context_menu = Some((context_menu, position, subscription)); + }); }) - .context(self.focus_handle.clone()) + }) + .detach(); + } + + fn toggle_data_breakpoint( + &mut self, + _: &crate::ToggleDataBreakpoint, + _window: &mut Window, + cx: &mut Context<Self>, + ) { + let Some(entry) = self + .selection + .as_ref() + .and_then(|selection| self.entries.iter().find(|entry| &entry.path == selection)) + else { + return; + }; + + let Some((name, var_ref)) = entry.as_variable().map(|var| &var.name).zip( + self.entry_states + .get(&entry.path) + .map(|state| state.parent_reference), + ) else { + return; + }; + + let context = Arc::new(DataBreakpointContext::Variable { + variables_reference: var_ref, + name: name.clone(), + bytes: None, + }); + let data_breakpoint = self.session.update(cx, |session, cx| { + session.data_breakpoint_info(context.clone(), None, cx) }); - cx.focus_view(&context_menu, window); - let subscription = cx.subscribe_in( - &context_menu, - window, - |this, _, _: &DismissEvent, window, cx| { - if this.open_context_menu.as_ref().is_some_and(|context_menu| { - context_menu.0.focus_handle(cx).contains_focused(window, cx) - }) { - cx.focus_self(window); - } - this.open_context_menu.take(); + let session = self.session.downgrade(); + cx.spawn(async move |_, cx| { + let Some(data_id) = data_breakpoint.await.and_then(|info| info.data_id) else { + return; + }; + _ = session.update(cx, |session, cx| { + session.create_data_breakpoint( + context, + data_id.clone(), + dap::DataBreakpoint { + data_id, + access_type: None, + condition: None, + hit_condition: None, + }, + cx, + ); cx.notify(); - }, - ); - - self.open_context_menu = Some((context_menu, position, subscription)); + }); + }) + .detach(); } fn copy_variable_name( @@ -1415,6 +1525,7 @@ impl Render for VariableList { .on_action(cx.listener(Self::edit_variable)) .on_action(cx.listener(Self::add_watcher)) .on_action(cx.listener(Self::remove_watcher)) + .on_action(cx.listener(Self::toggle_data_breakpoint)) .on_action(cx.listener(Self::jump_to_variable_memory)) .child( uniform_list( diff --git a/crates/project/src/debugger/dap_command.rs b/crates/project/src/debugger/dap_command.rs index 1a3587024ecb593f8cdabf99c951aef26c471fa4..1cb611680c34342db9645dab44240b73df13cc56 100644 --- a/crates/project/src/debugger/dap_command.rs +++ b/crates/project/src/debugger/dap_command.rs @@ -11,6 +11,7 @@ use dap::{ proto_conversions::ProtoConversion, requests::{Continue, Next}, }; + use rpc::proto; use serde_json::Value; use util::ResultExt; @@ -813,7 +814,7 @@ impl DapCommand for RestartCommand { } } -#[derive(Debug, Hash, PartialEq, Eq)] +#[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct VariablesCommand { pub variables_reference: u64, pub filter: Option<VariablesArgumentsFilter>, @@ -1667,6 +1668,130 @@ impl LocalDapCommand for SetBreakpoints { Ok(message.breakpoints) } } + +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub enum DataBreakpointContext { + Variable { + variables_reference: u64, + name: String, + bytes: Option<u64>, + }, + Expression { + expression: String, + frame_id: Option<u64>, + }, + Address { + address: String, + bytes: Option<u64>, + }, +} + +impl DataBreakpointContext { + pub fn human_readable_label(&self) -> String { + match self { + DataBreakpointContext::Variable { name, .. } => format!("Variable: {}", name), + DataBreakpointContext::Expression { expression, .. } => { + format!("Expression: {}", expression) + } + DataBreakpointContext::Address { address, bytes } => { + let mut label = format!("Address: {}", address); + if let Some(bytes) = bytes { + label.push_str(&format!( + " ({} byte{})", + bytes, + if *bytes == 1 { "" } else { "s" } + )); + } + label + } + } + } +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub(crate) struct DataBreakpointInfoCommand { + pub context: Arc<DataBreakpointContext>, + pub mode: Option<String>, +} + +impl LocalDapCommand for DataBreakpointInfoCommand { + type Response = dap::DataBreakpointInfoResponse; + type DapRequest = dap::requests::DataBreakpointInfo; + const CACHEABLE: bool = true; + + // todo(debugger): We should expand this trait in the future to take a &self + // Depending on this command is_supported could be differentb + fn is_supported(capabilities: &Capabilities) -> bool { + capabilities.supports_data_breakpoints.unwrap_or(false) + } + + fn to_dap(&self) -> <Self::DapRequest as dap::requests::Request>::Arguments { + let (variables_reference, name, frame_id, as_address, bytes) = match &*self.context { + DataBreakpointContext::Variable { + variables_reference, + name, + bytes, + } => ( + Some(*variables_reference), + name.clone(), + None, + Some(false), + *bytes, + ), + DataBreakpointContext::Expression { + expression, + frame_id, + } => (None, expression.clone(), *frame_id, Some(false), None), + DataBreakpointContext::Address { address, bytes } => { + (None, address.clone(), None, Some(true), *bytes) + } + }; + + dap::DataBreakpointInfoArguments { + variables_reference, + name, + frame_id, + bytes, + as_address, + mode: self.mode.clone(), + } + } + + fn response_from_dap( + &self, + message: <Self::DapRequest as dap::requests::Request>::Response, + ) -> Result<Self::Response> { + Ok(message) + } +} + +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub(crate) struct SetDataBreakpointsCommand { + pub breakpoints: Vec<dap::DataBreakpoint>, +} + +impl LocalDapCommand for SetDataBreakpointsCommand { + type Response = Vec<dap::Breakpoint>; + type DapRequest = dap::requests::SetDataBreakpoints; + + fn is_supported(capabilities: &Capabilities) -> bool { + capabilities.supports_data_breakpoints.unwrap_or(false) + } + + fn to_dap(&self) -> <Self::DapRequest as dap::requests::Request>::Arguments { + dap::SetDataBreakpointsArguments { + breakpoints: self.breakpoints.clone(), + } + } + + fn response_from_dap( + &self, + message: <Self::DapRequest as dap::requests::Request>::Response, + ) -> Result<Self::Response> { + Ok(message.breakpoints) + } +} + #[derive(Clone, Debug, Hash, PartialEq)] pub(super) enum SetExceptionBreakpoints { Plain { @@ -1776,7 +1901,7 @@ impl DapCommand for LocationsCommand { } } -#[derive(Debug, Hash, PartialEq, Eq)] +#[derive(Clone, Debug, Hash, PartialEq, Eq)] pub(crate) struct ReadMemory { pub(crate) memory_reference: String, pub(crate) offset: Option<u64>, @@ -1829,25 +1954,6 @@ impl LocalDapCommand for ReadMemory { } } -impl LocalDapCommand for dap::DataBreakpointInfoArguments { - type Response = dap::DataBreakpointInfoResponse; - type DapRequest = dap::requests::DataBreakpointInfo; - const CACHEABLE: bool = true; - fn is_supported(capabilities: &Capabilities) -> bool { - capabilities.supports_data_breakpoints.unwrap_or_default() - } - fn to_dap(&self) -> <Self::DapRequest as dap::requests::Request>::Arguments { - self.clone() - } - - fn response_from_dap( - &self, - message: <Self::DapRequest as dap::requests::Request>::Response, - ) -> Result<Self::Response> { - Ok(message) - } -} - impl LocalDapCommand for dap::WriteMemoryArguments { type Response = dap::WriteMemoryResponse; type DapRequest = dap::requests::WriteMemory; diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 53c13e13c3cd03ae14e68daa9c5e13f4a5515a1e..cf157ce4f92173d3202be0470d01d049c1c5e87e 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -1,17 +1,17 @@ use crate::debugger::breakpoint_store::BreakpointSessionState; -use crate::debugger::dap_command::ReadMemory; +use crate::debugger::dap_command::{DataBreakpointContext, ReadMemory}; use crate::debugger::memory::{self, Memory, MemoryIterator, MemoryPageBuilder, PageAddress}; use super::breakpoint_store::{ BreakpointStore, BreakpointStoreEvent, BreakpointUpdatedReason, SourceBreakpoint, }; use super::dap_command::{ - self, Attach, ConfigurationDone, ContinueCommand, DisconnectCommand, EvaluateCommand, - Initialize, Launch, LoadedSourcesCommand, LocalDapCommand, LocationsCommand, ModulesCommand, - NextCommand, PauseCommand, RestartCommand, RestartStackFrameCommand, ScopesCommand, - SetExceptionBreakpoints, SetVariableValueCommand, StackTraceCommand, StepBackCommand, - StepCommand, StepInCommand, StepOutCommand, TerminateCommand, TerminateThreadsCommand, - ThreadsCommand, VariablesCommand, + self, Attach, ConfigurationDone, ContinueCommand, DataBreakpointInfoCommand, DisconnectCommand, + EvaluateCommand, Initialize, Launch, LoadedSourcesCommand, LocalDapCommand, LocationsCommand, + ModulesCommand, NextCommand, PauseCommand, RestartCommand, RestartStackFrameCommand, + ScopesCommand, SetDataBreakpointsCommand, SetExceptionBreakpoints, SetVariableValueCommand, + StackTraceCommand, StepBackCommand, StepCommand, StepInCommand, StepOutCommand, + TerminateCommand, TerminateThreadsCommand, ThreadsCommand, VariablesCommand, }; use super::dap_store::DapStore; use anyhow::{Context as _, Result, anyhow}; @@ -138,6 +138,13 @@ pub struct Watcher { pub presentation_hint: Option<VariablePresentationHint>, } +#[derive(Debug, Clone, PartialEq)] +pub struct DataBreakpointState { + pub dap: dap::DataBreakpoint, + pub is_enabled: bool, + pub context: Arc<DataBreakpointContext>, +} + pub enum SessionState { Building(Option<Task<Result<()>>>), Running(RunningMode), @@ -686,6 +693,7 @@ pub struct Session { pub(crate) breakpoint_store: Entity<BreakpointStore>, ignore_breakpoints: bool, exception_breakpoints: BTreeMap<String, (ExceptionBreakpointsFilter, IsEnabled)>, + data_breakpoints: BTreeMap<String, DataBreakpointState>, background_tasks: Vec<Task<()>>, restart_task: Option<Task<()>>, task_context: TaskContext, @@ -780,6 +788,7 @@ pub enum SessionEvent { request: RunInTerminalRequestArguments, sender: mpsc::Sender<Result<u32>>, }, + DataBreakpointInfo, ConsoleOutput, } @@ -856,6 +865,7 @@ impl Session { is_session_terminated: false, ignore_breakpoints: false, breakpoint_store, + data_breakpoints: Default::default(), exception_breakpoints: Default::default(), label, adapter, @@ -1670,6 +1680,7 @@ impl Session { self.invalidate_command_type::<ModulesCommand>(); self.invalidate_command_type::<LoadedSourcesCommand>(); self.invalidate_command_type::<ThreadsCommand>(); + self.invalidate_command_type::<DataBreakpointInfoCommand>(); self.invalidate_command_type::<ReadMemory>(); let executor = self.as_running().map(|running| running.executor.clone()); if let Some(executor) = executor { @@ -1906,6 +1917,10 @@ impl Session { } } + pub fn data_breakpoints(&self) -> impl Iterator<Item = &DataBreakpointState> { + self.data_breakpoints.values() + } + pub fn exception_breakpoints( &self, ) -> impl Iterator<Item = &(ExceptionBreakpointsFilter, IsEnabled)> { @@ -1939,6 +1954,45 @@ impl Session { } } + pub fn toggle_data_breakpoint(&mut self, id: &str, cx: &mut Context<'_, Session>) { + if let Some(state) = self.data_breakpoints.get_mut(id) { + state.is_enabled = !state.is_enabled; + self.send_exception_breakpoints(cx); + } + } + + fn send_data_breakpoints(&mut self, cx: &mut Context<Self>) { + if let Some(mode) = self.as_running() { + let breakpoints = self + .data_breakpoints + .values() + .filter_map(|state| state.is_enabled.then(|| state.dap.clone())) + .collect(); + let command = SetDataBreakpointsCommand { breakpoints }; + mode.request(command).detach_and_log_err(cx); + } + } + + pub fn create_data_breakpoint( + &mut self, + context: Arc<DataBreakpointContext>, + data_id: String, + dap: dap::DataBreakpoint, + cx: &mut Context<Self>, + ) { + if self.data_breakpoints.remove(&data_id).is_none() { + self.data_breakpoints.insert( + data_id, + DataBreakpointState { + dap, + is_enabled: true, + context, + }, + ); + } + self.send_data_breakpoints(cx); + } + pub fn breakpoints_enabled(&self) -> bool { self.ignore_breakpoints } @@ -2500,6 +2554,20 @@ impl Session { .unwrap_or_default() } + pub fn data_breakpoint_info( + &mut self, + context: Arc<DataBreakpointContext>, + mode: Option<String>, + cx: &mut Context<Self>, + ) -> Task<Option<dap::DataBreakpointInfoResponse>> { + let command = DataBreakpointInfoCommand { + context: context.clone(), + mode, + }; + + self.request(command, |_, response, _| response.ok(), cx) + } + pub fn set_variable_value( &mut self, stack_frame_id: u64, From 32f5132bde7a7c061b8a1671759922a618f7bde4 Mon Sep 17 00:00:00 2001 From: Richard Feldman <oss@rtfeldman.com> Date: Mon, 14 Jul 2025 14:18:41 -0400 Subject: [PATCH 064/658] Fix contrast adjustment for Powerline separators (#34417) It turns out Starship is using custom Powerline separators in the Unicode private reserved character range. This addresses some issues seen in the comments of #34234 Release Notes: - Fix automatic contrast adjustment for Powerline separators --- crates/terminal_view/src/terminal_element.rs | 48 ++++++++++++++------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index d05f6bb5da2075ec3e9829e9e93ba858f520f207..083c07de9c2e98a7aad7dcb4f4abeb09874afec0 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -497,16 +497,24 @@ impl TerminalElement { /// Checks if a character is a decorative block/box-like character that should /// preserve its exact colors without contrast adjustment. /// - /// Fixes https://github.com/zed-industries/zed/issues/34234 - we can - /// expand this list if we run into more similar cases, but the goal - /// is to be conservative here. + /// This specifically targets characters used as visual connectors, separators, + /// and borders where color matching with adjacent backgrounds is critical. + /// Regular icons (git, folders, etc.) are excluded as they need to remain readable. + /// + /// Fixes https://github.com/zed-industries/zed/issues/34234 fn is_decorative_character(ch: char) -> bool { matches!( ch as u32, - // 0x2500..=0x257F Box Drawing - // 0x2580..=0x259F Block Elements - // 0x25A0..=0x25D7 Geometric Shapes (block/box-like subset) - 0x2500..=0x25D7 + // Unicode Box Drawing and Block Elements + 0x2500..=0x257F // Box Drawing (└ ┐ ─ │ etc.) + | 0x2580..=0x259F // Block Elements (▀ ▄ █ ░ ▒ ▓ etc.) + | 0x25A0..=0x25FF // Geometric Shapes (■ ▶ ● etc. - includes triangular/circular separators) + + // Private Use Area - Powerline separator symbols only + | 0xE0B0..=0xE0B7 // Powerline separators: triangles (E0B0-E0B3) and half circles (E0B4-E0B7) + | 0xE0B8..=0xE0BF // Additional Powerline separators: angles, flames, etc. + | 0xE0C0..=0xE0C8 // Powerline separators: pixelated triangles, curves + | 0xE0CC..=0xE0D4 // Powerline separators: rounded triangles, ice/lego style ) } @@ -1623,16 +1631,26 @@ mod tests { // The specific character from the issue assert!(TerminalElement::is_decorative_character('◗')); // U+25D7 + assert!(TerminalElement::is_decorative_character('◘')); // U+25D8 (now included in Geometric Shapes) + assert!(TerminalElement::is_decorative_character('◙')); // U+25D9 (now included in Geometric Shapes) + + // Powerline symbols (Private Use Area) + assert!(TerminalElement::is_decorative_character('\u{E0B0}')); // Powerline right triangle + assert!(TerminalElement::is_decorative_character('\u{E0B2}')); // Powerline left triangle + assert!(TerminalElement::is_decorative_character('\u{E0B4}')); // Powerline right half circle (the actual issue!) + assert!(TerminalElement::is_decorative_character('\u{E0B6}')); // Powerline left half circle // Characters that should NOT be considered decorative - assert!(!TerminalElement::is_decorative_character('A')); - assert!(!TerminalElement::is_decorative_character('a')); - assert!(!TerminalElement::is_decorative_character('0')); - assert!(!TerminalElement::is_decorative_character(' ')); + assert!(!TerminalElement::is_decorative_character('A')); // Regular letter + assert!(!TerminalElement::is_decorative_character('$')); // Symbol + assert!(!TerminalElement::is_decorative_character(' ')); // Space assert!(!TerminalElement::is_decorative_character('←')); // U+2190 (Arrow, not in our ranges) assert!(!TerminalElement::is_decorative_character('→')); // U+2192 (Arrow, not in our ranges) - assert!(!TerminalElement::is_decorative_character('◘')); // U+25D8 (Just outside our range) - assert!(!TerminalElement::is_decorative_character('◙')); // U+25D9 (Just outside our range) + assert!(!TerminalElement::is_decorative_character('\u{F00C}')); // Font Awesome check (icon, needs contrast) + assert!(!TerminalElement::is_decorative_character('\u{E711}')); // Devicons (icon, needs contrast) + assert!(!TerminalElement::is_decorative_character('\u{EA71}')); // Codicons folder (icon, needs contrast) + assert!(!TerminalElement::is_decorative_character('\u{F401}')); // Octicons (icon, needs contrast) + assert!(!TerminalElement::is_decorative_character('\u{1F600}')); // Emoji (not in our ranges) } #[test] @@ -1649,8 +1667,8 @@ mod tests { // Geometric Shapes subset boundaries assert!(TerminalElement::is_decorative_character('\u{25A0}')); // First char - assert!(TerminalElement::is_decorative_character('\u{25D7}')); // Last char (◗) - assert!(!TerminalElement::is_decorative_character('\u{25D8}')); // Just after + assert!(TerminalElement::is_decorative_character('\u{25FF}')); // Last char + assert!(!TerminalElement::is_decorative_character('\u{2600}')); // Just after } #[test] From 37e73e327778e7e073d1b89680740dfec61f9302 Mon Sep 17 00:00:00 2001 From: Michael Sloan <michael@zed.dev> Date: Mon, 14 Jul 2025 12:34:33 -0600 Subject: [PATCH 065/658] Only depend on scap x11 feature when gpui x11 feature is enabled (#34251) Release Notes: - N/A --- Cargo.lock | 3 ++- Cargo.toml | 2 +- crates/gpui/Cargo.toml | 1 + tooling/workspace-hack/Cargo.toml | 8 ++++++++ 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 742c3515c99f34bf0e7f48f4e22327cd5e517017..7e34f1e0555dfe3b60a53e084bea519cfd0557f2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14111,7 +14111,7 @@ dependencies = [ [[package]] name = "scap" version = "0.0.8" -source = "git+https://github.com/zed-industries/scap?rev=28dd306ff2e3374404936dec778fc1e975b8dd12#28dd306ff2e3374404936dec778fc1e975b8dd12" +source = "git+https://github.com/zed-industries/scap?rev=270538dc780f5240723233ff901e1054641ed318#270538dc780f5240723233ff901e1054641ed318" dependencies = [ "anyhow", "cocoa 0.25.0", @@ -19660,6 +19660,7 @@ dependencies = [ "rustix 1.0.7", "rustls 0.23.26", "rustls-webpki 0.103.1", + "scap", "schemars", "scopeguard", "sea-orm", diff --git a/Cargo.toml b/Cargo.toml index e270dd1891f8bb6b3e2cea4916ffdf242905f1e0..f96240531f26232cf37d9560adf919c3f33f21e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -547,7 +547,7 @@ rustc-demangle = "0.1.23" rustc-hash = "2.1.0" rustls = { version = "0.23.26" } rustls-platform-verifier = "0.5.0" -scap = { git = "https://github.com/zed-industries/scap", rev = "28dd306ff2e3374404936dec778fc1e975b8dd12", default-features = false } +scap = { git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318", default-features = false } schemars = { version = "1.0", features = ["indexmap2"] } semver = "1.0" serde = { version = "1.0", features = ["derive", "rc"] } diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 4e718b442867d2335715e2e963fa9330d9f04525..d3462e9e9c1a0ad98a9e16787ee2281d5ff6f028 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -66,6 +66,7 @@ x11 = [ "x11-clipboard", "filedescriptor", "open", + "scap?/x11", ] screen-capture = [ "scap", diff --git a/tooling/workspace-hack/Cargo.toml b/tooling/workspace-hack/Cargo.toml index d4019ab85440a1ed5b44f064aa20e01dac872518..59f84492848a7320fe86f17d6431d2e7ed96d831 100644 --- a/tooling/workspace-hack/Cargo.toml +++ b/tooling/workspace-hack/Cargo.toml @@ -429,6 +429,7 @@ rand-274715c4dabd11b0 = { package = "rand", version = "0.9" } ring = { version = "0.17", features = ["std"] } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "param", "pipe", "process", "pty", "shm", "stdio", "system", "termios", "time"] } rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] } +scap = { git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318", default-features = false, features = ["x11"] } 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"] } @@ -468,6 +469,7 @@ rand-274715c4dabd11b0 = { package = "rand", version = "0.9" } ring = { version = "0.17", features = ["std"] } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "param", "pipe", "process", "pty", "shm", "stdio", "system", "termios", "time"] } rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] } +scap = { git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318", default-features = false, features = ["x11"] } scopeguard = { version = "1" } sync_wrapper = { version = "1", default-features = false, features = ["futures"] } tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] } @@ -507,6 +509,7 @@ rand-274715c4dabd11b0 = { package = "rand", version = "0.9" } ring = { version = "0.17", features = ["std"] } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "param", "pipe", "process", "pty", "shm", "stdio", "system", "termios", "time"] } rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] } +scap = { git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318", default-features = false, features = ["x11"] } 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"] } @@ -546,6 +549,7 @@ rand-274715c4dabd11b0 = { package = "rand", version = "0.9" } ring = { version = "0.17", features = ["std"] } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "param", "pipe", "process", "pty", "shm", "stdio", "system", "termios", "time"] } rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] } +scap = { git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318", default-features = false, features = ["x11"] } scopeguard = { version = "1" } sync_wrapper = { version = "1", default-features = false, features = ["futures"] } tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] } @@ -567,6 +571,7 @@ itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" } naga = { version = "25", features = ["spv-out", "wgsl-in"] } ring = { version = "0.17", features = ["std"] } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event"] } +scap = { git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318", default-features = false, features = ["x11"] } scopeguard = { version = "1" } sync_wrapper = { version = "1", default-features = false, features = ["futures"] } tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] } @@ -594,6 +599,7 @@ naga = { version = "25", features = ["spv-out", "wgsl-in"] } proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] } ring = { version = "0.17", features = ["std"] } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event"] } +scap = { git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318", default-features = false, features = ["x11"] } scopeguard = { version = "1" } sync_wrapper = { version = "1", default-features = false, features = ["futures"] } tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] } @@ -638,6 +644,7 @@ rand-274715c4dabd11b0 = { package = "rand", version = "0.9" } ring = { version = "0.17", features = ["std"] } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "param", "pipe", "process", "pty", "shm", "stdio", "system", "termios", "time"] } rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] } +scap = { git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318", default-features = false, features = ["x11"] } 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"] } @@ -677,6 +684,7 @@ rand-274715c4dabd11b0 = { package = "rand", version = "0.9" } ring = { version = "0.17", features = ["std"] } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "param", "pipe", "process", "pty", "shm", "stdio", "system", "termios", "time"] } rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] } +scap = { git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318", default-features = false, features = ["x11"] } scopeguard = { version = "1" } sync_wrapper = { version = "1", default-features = false, features = ["futures"] } tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] } From 363a26505161c62eb624cdd27bcd0a94fe658a3b Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" <JosephTLyons@gmail.com> Date: Mon, 14 Jul 2025 16:24:05 -0400 Subject: [PATCH 066/658] Add test for running `Close Others` on an inactive item (#34425) Adds a test for the changes added in: https://github.com/zed-industries/zed/pull/34355 Release Notes: - N/A --- crates/workspace/src/pane.rs | 37 ++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 233d435f4b3209d90e42e88e5180b727045ae641..4d3f6823b36e58c898f192ebb95e4ee274133580 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -5857,6 +5857,43 @@ mod tests { assert_item_labels(&pane, ["A!", "B!", "E*"], cx); } + #[gpui::test] + async fn test_running_close_inactive_items_via_an_inactive_item(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 = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); + + add_labeled_item(&pane, "A", false, cx); + assert_item_labels(&pane, ["A*"], cx); + + let item_b = add_labeled_item(&pane, "B", false, cx); + assert_item_labels(&pane, ["A", "B*"], cx); + + add_labeled_item(&pane, "C", false, cx); + add_labeled_item(&pane, "D", false, cx); + add_labeled_item(&pane, "E", false, cx); + assert_item_labels(&pane, ["A", "B", "C", "D", "E*"], cx); + + pane.update_in(cx, |pane, window, cx| { + pane.close_inactive_items( + &CloseInactiveItems { + save_intent: None, + close_pinned: false, + }, + Some(item_b.item_id()), + window, + cx, + ) + }) + .await + .unwrap(); + assert_item_labels(&pane, ["B*"], cx); + } + #[gpui::test] async fn test_close_clean_items(cx: &mut TestAppContext) { init_test(cx); From 26ba6e7e0067ca7d3554372eb4005dd5ebc1fc17 Mon Sep 17 00:00:00 2001 From: Finn Evers <finn@zed.dev> Date: Mon, 14 Jul 2025 23:29:27 +0200 Subject: [PATCH 067/658] editor: Improve minimap performance (#33067) This PR aims to improve the minimap performace. This is primarily achieved by disabling/removing stuff that is not shown in the minimal as well as by assuring the display map is not updated during minimap prepaint. This should already be much better in parts, as the block map as well as the fold map will be less frequently updated due to the minimap prepainting (optimally, they should never be, but I think we're not quite there yet). For this, I had to remove block rendering support for the minimap, which is not as bad as it sounds: Practically, we were currently not rendering most blocks anyway, there were issues due to this (e.g. scrolling any visible block offscreen in the main editor causes scroll jumps currently) and in the long run, the minimap will most likely need its own block map or a different approach anyway. The existing implementation caused resizes to occur very frequently for practically no benefit. Can pull this out into a separate PR if requested, most likely makes the other changes here easier to discuss. This is WIP as we are still hitting some code path here we definitely should not be hitting. E.g. there seems to be a rerender roughly every second if the window is unfocused but visible which does not happen when the minimap is disabled. While this primarily focuses on the minimap, it also touches a few other small parts not related to the minimap where I noticed we were doing too much stuff during prepaint. Happy for any feedback there aswell. Putting this up here already so we have a place to discuss the changes early if needed. Release Notes: - Improved performance with the minimap enabled. - Fixed an issue where interacting with blocks in the editor would sometimes not properly work with the minimap enabled. --- crates/agent_ui/src/inline_assistant.rs | 3 - crates/agent_ui/src/text_thread_editor.rs | 2 - crates/diagnostics/src/diagnostic_renderer.rs | 1 - crates/diagnostics/src/diagnostics.rs | 1 - crates/editor/src/display_map.rs | 7 - crates/editor/src/display_map/block_map.rs | 28 --- crates/editor/src/editor.rs | 131 +++++++------ crates/editor/src/editor_tests.rs | 2 - crates/editor/src/element.rs | 176 +++++++++--------- crates/git_ui/src/conflict_view.rs | 1 - crates/repl/src/session.rs | 1 - docs/src/development.md | 42 +++++ 12 files changed, 209 insertions(+), 186 deletions(-) diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs index c9c173a68be5191e77690e826378ca52d3db9684..65b72cbba5f15aa3f527b77939a80abff7a05c05 100644 --- a/crates/agent_ui/src/inline_assistant.rs +++ b/crates/agent_ui/src/inline_assistant.rs @@ -660,7 +660,6 @@ impl InlineAssistant { height: Some(prompt_editor_height), render: build_assist_editor_renderer(prompt_editor), priority: 0, - render_in_minimap: false, }, BlockProperties { style: BlockStyle::Sticky, @@ -675,7 +674,6 @@ impl InlineAssistant { .into_any_element() }), priority: 0, - render_in_minimap: false, }, ]; @@ -1451,7 +1449,6 @@ impl InlineAssistant { .into_any_element() }), priority: 0, - render_in_minimap: false, }); } diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index de7606dbfb333e524c81435432050cfba1b71831..2941da19653fa6ebbd581663ed675af6b57a2d30 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -1256,7 +1256,6 @@ impl TextThreadEditor { ), priority: usize::MAX, render: render_block(MessageMetadata::from(message)), - render_in_minimap: false, }; let mut new_blocks = vec![]; let mut block_index_to_message = vec![]; @@ -1858,7 +1857,6 @@ impl TextThreadEditor { .into_any_element() }), priority: 0, - render_in_minimap: false, }) }) .collect::<Vec<_>>(); diff --git a/crates/diagnostics/src/diagnostic_renderer.rs b/crates/diagnostics/src/diagnostic_renderer.rs index 77bb249733f612ede3017e1cff592927b40e8d43..ce7b253702a01e24e7f4a457ac418572e0fa2729 100644 --- a/crates/diagnostics/src/diagnostic_renderer.rs +++ b/crates/diagnostics/src/diagnostic_renderer.rs @@ -144,7 +144,6 @@ impl editor::DiagnosticRenderer for DiagnosticRenderer { style: BlockStyle::Flex, render: Arc::new(move |bcx| block.render_block(editor.clone(), bcx)), priority: 1, - render_in_minimap: false, } }) .collect() diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 1daa9025b64f2a783409ba5ebe10214ed55c362b..b2e0a682056356cddd077d42418a2b4fa763cffa 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -656,7 +656,6 @@ impl ProjectDiagnosticsEditor { block.render_block(editor.clone(), bcx) }), priority: 1, - render_in_minimap: false, } }); let block_ids = this.editor.update(cx, |editor, cx| { diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index aa2408d6d9b616f2b1436d9bc66f42bd87506d19..5425d5a8b970a9e4febbc55a4a64e31d1f373d55 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -271,7 +271,6 @@ impl DisplayMap { height: Some(height), style, priority, - render_in_minimap: true, } }), ); @@ -1663,7 +1662,6 @@ pub mod tests { height: Some(height), render: Arc::new(|_| div().into_any()), priority, - render_in_minimap: true, } }) .collect::<Vec<_>>(); @@ -2029,7 +2027,6 @@ pub mod tests { style: BlockStyle::Sticky, render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }], cx, ); @@ -2227,7 +2224,6 @@ pub mod tests { style: BlockStyle::Sticky, render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, BlockProperties { placement: BlockPlacement::Below( @@ -2237,7 +2233,6 @@ pub mod tests { style: BlockStyle::Sticky, render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, ], cx, @@ -2344,7 +2339,6 @@ pub mod tests { style: BlockStyle::Sticky, render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }], cx, ) @@ -2420,7 +2414,6 @@ pub mod tests { style: BlockStyle::Fixed, render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }], cx, ); diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index ea754da03f70ff87e28bb73a614fad6b66d7e4c2..c761e0d69ceea5a8e36441df410071016dc1f200 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -193,7 +193,6 @@ pub struct CustomBlock { style: BlockStyle, render: Arc<Mutex<RenderBlock>>, priority: usize, - pub(crate) render_in_minimap: bool, } #[derive(Clone)] @@ -205,7 +204,6 @@ pub struct BlockProperties<P> { pub style: BlockStyle, pub render: RenderBlock, pub priority: usize, - pub render_in_minimap: bool, } impl<P: Debug> Debug for BlockProperties<P> { @@ -1044,7 +1042,6 @@ impl BlockMapWriter<'_> { render: Arc::new(Mutex::new(block.render)), style: block.style, priority: block.priority, - render_in_minimap: block.render_in_minimap, }); self.0.custom_blocks.insert(block_ix, new_block.clone()); self.0.custom_blocks_by_id.insert(id, new_block); @@ -1079,7 +1076,6 @@ impl BlockMapWriter<'_> { style: block.style, render: block.render.clone(), priority: block.priority, - render_in_minimap: block.render_in_minimap, }; let new_block = Arc::new(new_block); *block = new_block.clone(); @@ -1976,7 +1972,6 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -1984,7 +1979,6 @@ mod tests { height: Some(2), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -1992,7 +1986,6 @@ mod tests { height: Some(3), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, ]); @@ -2217,7 +2210,6 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -2225,7 +2217,6 @@ mod tests { height: Some(2), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -2233,7 +2224,6 @@ mod tests { height: Some(3), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, ]); @@ -2322,7 +2312,6 @@ mod tests { render: Arc::new(|_| div().into_any()), height: Some(1), priority: 0, - render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -2330,7 +2319,6 @@ mod tests { render: Arc::new(|_| div().into_any()), height: Some(1), priority: 0, - render_in_minimap: true, }, ]); @@ -2370,7 +2358,6 @@ mod tests { height: Some(4), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }])[0]; let blocks_snapshot = block_map.read(wraps_snapshot, Default::default()); @@ -2424,7 +2411,6 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -2432,7 +2418,6 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -2440,7 +2425,6 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, ]); let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default()); @@ -2455,7 +2439,6 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -2463,7 +2446,6 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -2471,7 +2453,6 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, ]); let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default()); @@ -2571,7 +2552,6 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -2579,7 +2559,6 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -2587,7 +2566,6 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, ]); let excerpt_blocks_3 = writer.insert(vec![ @@ -2597,7 +2575,6 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -2605,7 +2582,6 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }, ]); @@ -2653,7 +2629,6 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }]); let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default()); let blocks = blocks_snapshot @@ -3011,7 +2986,6 @@ mod tests { height: Some(height), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, } }) .collect::<Vec<_>>(); @@ -3032,7 +3006,6 @@ mod tests { style: props.style, render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, })); for (block_properties, block_id) in block_properties.iter().zip(block_ids) { @@ -3557,7 +3530,6 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }])[0]; let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default()); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 5065564fdba2f0abf6f0fd6390c1528c2a95bf44..919d7cc9c75044c8d28f6a2281646238aed10b8e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1795,6 +1795,7 @@ impl Editor { ); let full_mode = mode.is_full(); + let is_minimap = mode.is_minimap(); let diagnostics_max_severity = if full_mode { EditorSettings::get_global(cx) .diagnostics_max_severity @@ -1855,13 +1856,19 @@ impl Editor { let selections = SelectionsCollection::new(display_map.clone(), buffer.clone()); - let blink_manager = cx.new(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx)); + let blink_manager = cx.new(|cx| { + let mut blink_manager = BlinkManager::new(CURSOR_BLINK_INTERVAL, cx); + if is_minimap { + blink_manager.disable(cx); + } + blink_manager + }); let soft_wrap_mode_override = matches!(mode, EditorMode::SingleLine { .. }) .then(|| language_settings::SoftWrap::None); let mut project_subscriptions = Vec::new(); - if mode.is_full() { + if full_mode { if let Some(project) = project.as_ref() { project_subscriptions.push(cx.subscribe_in( project, @@ -1972,18 +1979,23 @@ impl Editor { let inlay_hint_settings = inlay_hint_settings(selections.newest_anchor().head(), &buffer_snapshot, cx); let focus_handle = cx.focus_handle(); - cx.on_focus(&focus_handle, window, Self::handle_focus) - .detach(); - cx.on_focus_in(&focus_handle, window, Self::handle_focus_in) - .detach(); - cx.on_focus_out(&focus_handle, window, Self::handle_focus_out) - .detach(); - cx.on_blur(&focus_handle, window, Self::handle_blur) - .detach(); - cx.observe_pending_input(window, Self::observe_pending_input) - .detach(); - - let show_indent_guides = if matches!(mode, EditorMode::SingleLine { .. }) { + if !is_minimap { + cx.on_focus(&focus_handle, window, Self::handle_focus) + .detach(); + cx.on_focus_in(&focus_handle, window, Self::handle_focus_in) + .detach(); + cx.on_focus_out(&focus_handle, window, Self::handle_focus_out) + .detach(); + cx.on_blur(&focus_handle, window, Self::handle_blur) + .detach(); + cx.observe_pending_input(window, Self::observe_pending_input) + .detach(); + } + + let show_indent_guides = if matches!( + mode, + EditorMode::SingleLine { .. } | EditorMode::Minimap { .. } + ) { Some(false) } else { None @@ -2049,10 +2061,10 @@ impl Editor { minimap_visibility: MinimapVisibility::for_mode(&mode, cx), offset_content: !matches!(mode, EditorMode::SingleLine { .. }), show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs, - show_gutter: mode.is_full(), - show_line_numbers: None, + show_gutter: full_mode, + show_line_numbers: (!full_mode).then_some(false), use_relative_line_numbers: None, - disable_expand_excerpt_buttons: false, + disable_expand_excerpt_buttons: !full_mode, show_git_diff_gutter: None, show_code_actions: None, show_runnables: None, @@ -2086,7 +2098,7 @@ impl Editor { document_highlights_task: None, linked_editing_range_task: None, pending_rename: None, - searchable: true, + searchable: !is_minimap, cursor_shape: EditorSettings::get_global(cx) .cursor_shape .unwrap_or_default(), @@ -2094,9 +2106,9 @@ impl Editor { autoindent_mode: Some(AutoindentMode::EachLine), collapse_matches: false, workspace: None, - input_enabled: true, - use_modal_editing: mode.is_full(), - read_only: mode.is_minimap(), + input_enabled: !is_minimap, + use_modal_editing: full_mode, + read_only: is_minimap, use_autoclose: true, use_auto_surround: true, auto_replace_emoji_shortcode: false, @@ -2112,11 +2124,10 @@ impl Editor { edit_prediction_preview: EditPredictionPreview::Inactive { released_too_fast: false, }, - inline_diagnostics_enabled: mode.is_full(), - diagnostics_enabled: mode.is_full(), + inline_diagnostics_enabled: full_mode, + diagnostics_enabled: full_mode, inline_value_cache: InlineValueCache::new(inlay_hint_settings.show_value_hints), inlay_hint_cache: InlayHintCache::new(inlay_hint_settings), - gutter_hovered: false, pixel_position_of_newest_cursor: None, last_bounds: None, @@ -2139,9 +2150,10 @@ impl Editor { show_git_blame_inline: false, show_selection_menu: None, show_git_blame_inline_delay_task: None, - git_blame_inline_enabled: ProjectSettings::get_global(cx).git.inline_blame_enabled(), + git_blame_inline_enabled: full_mode + && ProjectSettings::get_global(cx).git.inline_blame_enabled(), render_diff_hunk_controls: Arc::new(render_diff_hunk_controls), - serialize_dirty_buffers: !mode.is_minimap() + serialize_dirty_buffers: !is_minimap && ProjectSettings::get_global(cx) .session .restore_unsaved_buffers, @@ -2152,27 +2164,31 @@ impl Editor { breakpoint_store, gutter_breakpoint_indicator: (None, None), hovered_diff_hunk_row: None, - _subscriptions: vec![ - cx.observe(&buffer, Self::on_buffer_changed), - cx.subscribe_in(&buffer, window, Self::on_buffer_event), - cx.observe_in(&display_map, window, Self::on_display_map_changed), - cx.observe(&blink_manager, |_, _, cx| cx.notify()), - cx.observe_global_in::<SettingsStore>(window, Self::settings_changed), - observe_buffer_font_size_adjustment(cx, |_, cx| cx.notify()), - cx.observe_window_activation(window, |editor, window, cx| { - let active = window.is_window_active(); - editor.blink_manager.update(cx, |blink_manager, cx| { - if active { - blink_manager.enable(cx); - } else { - blink_manager.disable(cx); - } - }); - if active { - editor.show_mouse_cursor(cx); - } - }), - ], + _subscriptions: (!is_minimap) + .then(|| { + vec![ + cx.observe(&buffer, Self::on_buffer_changed), + cx.subscribe_in(&buffer, window, Self::on_buffer_event), + cx.observe_in(&display_map, window, Self::on_display_map_changed), + cx.observe(&blink_manager, |_, _, cx| cx.notify()), + cx.observe_global_in::<SettingsStore>(window, Self::settings_changed), + observe_buffer_font_size_adjustment(cx, |_, cx| cx.notify()), + cx.observe_window_activation(window, |editor, window, cx| { + let active = window.is_window_active(); + editor.blink_manager.update(cx, |blink_manager, cx| { + if active { + blink_manager.enable(cx); + } else { + blink_manager.disable(cx); + } + }); + if active { + editor.show_mouse_cursor(cx); + } + }), + ] + }) + .unwrap_or_default(), tasks_update_task: None, pull_diagnostics_task: Task::ready(()), colors: None, @@ -2203,6 +2219,11 @@ impl Editor { selection_drag_state: SelectionDragState::None, folding_newlines: Task::ready(()), }; + + if is_minimap { + return editor; + } + if let Some(breakpoints) = editor.breakpoint_store.as_ref() { editor ._subscriptions @@ -10445,7 +10466,6 @@ impl Editor { cloned_prompt.clone().into_any_element() }), priority: 0, - render_in_minimap: true, }]; let focus_handle = bp_prompt.focus_handle(cx); @@ -16141,7 +16161,6 @@ impl Editor { } }), priority: 0, - render_in_minimap: true, }], Some(Autoscroll::fit()), cx, @@ -18044,7 +18063,7 @@ impl Editor { parent: cx.weak_entity(), }, self.buffer.clone(), - self.project.clone(), + None, Some(self.display_map.clone()), window, cx, @@ -19928,14 +19947,12 @@ impl Editor { } fn settings_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) { - let new_severity = if self.diagnostics_enabled() { - EditorSettings::get_global(cx) + if self.diagnostics_enabled() { + let new_severity = EditorSettings::get_global(cx) .diagnostics_max_severity - .unwrap_or(DiagnosticSeverity::Hint) - } else { - DiagnosticSeverity::Off - }; - self.set_max_diagnostics_severity(new_severity, cx); + .unwrap_or(DiagnosticSeverity::Hint); + self.set_max_diagnostics_severity(new_severity, cx); + } self.tasks_update_task = Some(self.refresh_runnables(window, cx)); self.update_edit_prediction_settings(cx); self.refresh_inline_completion(true, false, window, cx); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 0d61f2f8586279a49358be68e78b596d6d6ea70e..6e0b949cb75e64741e06fff924efd1d15daebb45 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -5081,7 +5081,6 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, - render_in_minimap: true, }], Some(Autoscroll::fit()), cx, @@ -5124,7 +5123,6 @@ async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) { style: BlockStyle::Sticky, render: Arc::new(|_| gpui::div().into_any_element()), priority: 0, - render_in_minimap: true, }], None, cx, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index f9a31cffb10a57859b8030b9ab59a7866ab69d15..06fb52cdb3a63baa16f932ae0062b290016657ef 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2094,16 +2094,19 @@ impl EditorElement { window: &mut Window, cx: &mut App, ) -> HashMap<DisplayRow, AnyElement> { - if self.editor.read(cx).mode().is_minimap() { - return HashMap::default(); - } - - let max_severity = match ProjectSettings::get_global(cx) - .diagnostics - .inline - .max_severity - .unwrap_or_else(|| self.editor.read(cx).diagnostics_max_severity) - .into_lsp() + let max_severity = match self + .editor + .read(cx) + .inline_diagnostics_enabled() + .then(|| { + ProjectSettings::get_global(cx) + .diagnostics + .inline + .max_severity + .unwrap_or_else(|| self.editor.read(cx).diagnostics_max_severity) + .into_lsp() + }) + .flatten() { Some(max_severity) => max_severity, None => return HashMap::default(), @@ -2619,9 +2622,6 @@ impl EditorElement { window: &mut Window, cx: &mut App, ) -> Option<Vec<IndentGuideLayout>> { - if self.editor.read(cx).mode().is_minimap() { - return None; - } let indent_guides = self.editor.update(cx, |editor, cx| { editor.indent_guides(visible_buffer_range, snapshot, cx) })?; @@ -3085,9 +3085,9 @@ impl EditorElement { window: &mut Window, cx: &mut App, ) -> Arc<HashMap<MultiBufferRow, LineNumberLayout>> { - let include_line_numbers = snapshot.show_line_numbers.unwrap_or_else(|| { - EditorSettings::get_global(cx).gutter.line_numbers && snapshot.mode.is_full() - }); + let include_line_numbers = snapshot + .show_line_numbers + .unwrap_or_else(|| EditorSettings::get_global(cx).gutter.line_numbers); if !include_line_numbers { return Arc::default(); } @@ -3400,22 +3400,18 @@ impl EditorElement { div() .size_full() - .children( - (!snapshot.mode.is_minimap() || custom.render_in_minimap).then(|| { - custom.render(&mut BlockContext { - window, - app: cx, - anchor_x, - margins: editor_margins, - line_height, - em_width, - block_id, - selected, - max_width: text_hitbox.size.width.max(*scroll_width), - editor_style: &self.style, - }) - }), - ) + .child(custom.render(&mut BlockContext { + window, + app: cx, + anchor_x, + margins: editor_margins, + line_height, + em_width, + block_id, + selected, + max_width: text_hitbox.size.width.max(*scroll_width), + editor_style: &self.style, + })) .into_any() } @@ -6776,7 +6772,7 @@ impl EditorElement { } fn paint_mouse_listeners(&mut self, layout: &EditorLayout, window: &mut Window, cx: &mut App) { - if self.editor.read(cx).mode.is_minimap() { + if layout.mode.is_minimap() { return; } @@ -7903,9 +7899,14 @@ impl Element for EditorElement { line_height: Some(self.style.text.line_height), ..Default::default() }; - let focus_handle = self.editor.focus_handle(cx); - window.set_view_id(self.editor.entity_id()); - window.set_focus_handle(&focus_handle, cx); + + let is_minimap = self.editor.read(cx).mode.is_minimap(); + + if !is_minimap { + let focus_handle = self.editor.focus_handle(cx); + window.set_view_id(self.editor.entity_id()); + window.set_focus_handle(&focus_handle, cx); + } let rem_size = self.rem_size(cx); window.with_rem_size(rem_size, |window| { @@ -8343,18 +8344,22 @@ impl Element for EditorElement { window, cx, ); - let new_renrerer_widths = line_layouts - .iter() - .flat_map(|layout| &layout.fragments) - .filter_map(|fragment| { - if let LineFragment::Element { id, size, .. } = fragment { - Some((*id, size.width)) - } else { - None - } - }); - if self.editor.update(cx, |editor, cx| { - editor.update_renderer_widths(new_renrerer_widths, cx) + let new_renderer_widths = (!is_minimap).then(|| { + line_layouts + .iter() + .flat_map(|layout| &layout.fragments) + .filter_map(|fragment| { + if let LineFragment::Element { id, size, .. } = fragment { + Some((*id, size.width)) + } else { + None + } + }) + }); + if new_renderer_widths.is_some_and(|new_renderer_widths| { + self.editor.update(cx, |editor, cx| { + editor.update_renderer_widths(new_renderer_widths, cx) + }) }) { // If the fold widths have changed, we need to prepaint // the element again to account for any changes in @@ -8417,27 +8422,31 @@ impl Element for EditorElement { let sticky_header_excerpt_id = sticky_header_excerpt.as_ref().map(|top| top.excerpt.id); - let blocks = window.with_element_namespace("blocks", |window| { - self.render_blocks( - start_row..end_row, - &snapshot, - &hitbox, - &text_hitbox, - editor_width, - &mut scroll_width, - &editor_margins, - em_width, - gutter_dimensions.full_width(), - line_height, - &mut line_layouts, - &local_selections, - &selected_buffer_ids, - is_row_soft_wrapped, - sticky_header_excerpt_id, - window, - cx, - ) - }); + let blocks = (!is_minimap) + .then(|| { + window.with_element_namespace("blocks", |window| { + self.render_blocks( + start_row..end_row, + &snapshot, + &hitbox, + &text_hitbox, + editor_width, + &mut scroll_width, + &editor_margins, + em_width, + gutter_dimensions.full_width(), + line_height, + &mut line_layouts, + &local_selections, + &selected_buffer_ids, + is_row_soft_wrapped, + sticky_header_excerpt_id, + window, + cx, + ) + }) + }) + .unwrap_or_else(|| Ok((Vec::default(), HashMap::default()))); let (mut blocks, row_block_types) = match blocks { Ok(blocks) => blocks, Err(resized_blocks) => { @@ -8969,19 +8978,21 @@ impl Element for EditorElement { window: &mut Window, cx: &mut App, ) { - let focus_handle = self.editor.focus_handle(cx); - let key_context = self - .editor - .update(cx, |editor, cx| editor.key_context(window, cx)); - - window.set_key_context(key_context); - window.handle_input( - &focus_handle, - ElementInputHandler::new(bounds, self.editor.clone()), - cx, - ); - self.register_actions(window, cx); - self.register_key_listeners(window, cx, layout); + if !layout.mode.is_minimap() { + let focus_handle = self.editor.focus_handle(cx); + let key_context = self + .editor + .update(cx, |editor, cx| editor.key_context(window, cx)); + + window.set_key_context(key_context); + window.handle_input( + &focus_handle, + ElementInputHandler::new(bounds, self.editor.clone()), + cx, + ); + self.register_actions(window, cx); + self.register_key_listeners(window, cx, layout); + } let text_style = TextStyleRefinement { font_size: Some(self.style.text.font_size), @@ -10290,7 +10301,6 @@ mod tests { height: Some(3), render: Arc::new(|cx| div().h(3. * cx.window.line_height()).into_any()), priority: 0, - render_in_minimap: true, }], None, cx, diff --git a/crates/git_ui/src/conflict_view.rs b/crates/git_ui/src/conflict_view.rs index c07b99b875b6674286840c02ea2bf69ca5a6296b..0bbb9411be9ef8a8e6b73d11cc4d01126570741f 100644 --- a/crates/git_ui/src/conflict_view.rs +++ b/crates/git_ui/src/conflict_view.rs @@ -297,7 +297,6 @@ fn conflicts_updated( move |cx| render_conflict_buttons(&conflict, excerpt_id, editor_handle.clone(), cx) }), priority: 0, - render_in_minimap: true, }) } let new_block_ids = editor.insert_blocks(blocks, None, cx); diff --git a/crates/repl/src/session.rs b/crates/repl/src/session.rs index 18d41f3eae97ce4288d95e1e0eabb57d4b47adec..729a6161350652a90fcf9687593a2f115481a945 100644 --- a/crates/repl/src/session.rs +++ b/crates/repl/src/session.rs @@ -90,7 +90,6 @@ impl EditorBlock { style: BlockStyle::Sticky, render: Self::create_output_area_renderer(execution_view.clone(), on_close.clone()), priority: 0, - render_in_minimap: false, }; let block_id = editor.insert_blocks([block], None, cx)[0]; diff --git a/docs/src/development.md b/docs/src/development.md index 980b47aa4d98bd639dacae39293d7d3a94560380..046d515fede061160eff9c4a4bcb7cd1cd63b09e 100644 --- a/docs/src/development.md +++ b/docs/src/development.md @@ -37,6 +37,48 @@ development build, run Zed with the following environment variable set: ZED_DEVELOPMENT_USE_KEYCHAIN=1 ``` +## Performance Measurements + +Zed includes a frame time measurement system that can be used to profile how long it takes to render each frame. This is particularly useful when comparing rendering performance between different versions or when optimizing frame rendering code. + +### Using ZED_MEASUREMENTS + +To enable performance measurements, set the `ZED_MEASUREMENTS` environment variable: + +```sh +export ZED_MEASUREMENTS=1 +``` + +When enabled, Zed will print frame rendering timing information to stderr, showing how long each frame takes to render. + +### Performance Comparison Workflow + +Here's a typical workflow for comparing frame rendering performance between different versions: + +1. **Enable measurements:** + + ```sh + export ZED_MEASUREMENTS=1 + ``` + +2. **Test the first version:** + + - Checkout the commit you want to measure + - Run Zed in release mode and use it for 5-10 seconds: `cargo run --release &> version-a` + +3. **Test the second version:** + + - Checkout another commit you want to compare + - Run Zed in release mode and use it for 5-10 seconds: `cargo run --release &> version-b` + +4. **Generate comparison:** + + ```sh + script/histogram version-a version-b + ``` + +The `script/histogram` tool can accept as many measurement files as you like and will generate a histogram visualization comparing the frame rendering performance data between the provided versions. + ## Contributor links - [CONTRIBUTING.md](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md) From ce63a6ddd8b78bcdb37cb8bed8566792c0f8ba6d Mon Sep 17 00:00:00 2001 From: Peter Tripp <peter@zed.dev> Date: Mon, 14 Jul 2025 18:16:28 -0400 Subject: [PATCH 068/658] Exclude .repo folders by default (#34431) These are used by [Google's `repo` tool](https://android.googlesource.com/tools/repo) used for Android for managing hundreds of git subprojects. Originally reported in: - https://github.com/zed-industries/zed/issues/34302 Release Notes: - Add Google Repo `.repo` folders to default `file_scan_exclusions` --- assets/settings/default.json | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/settings/default.json b/assets/settings/default.json index dc892bd6a33443df5d3d2a0fa2b8a8d9fca41acd..edf07fdbf98e745998f3fac553de2b0a5d78cefd 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1135,6 +1135,7 @@ "**/.svn", "**/.hg", "**/.jj", + "**/.repo", "**/CVS", "**/.DS_Store", "**/Thumbs.db", From 440beb8a90b06c579b0b0a50abec126f7e5baa62 Mon Sep 17 00:00:00 2001 From: Peter Tripp <peter@zed.dev> Date: Mon, 14 Jul 2025 18:16:43 -0400 Subject: [PATCH 069/658] Improve Java LSP documentation (#34410) Remove references to [ABckh/zed-java-eclipse-jdtls](https://github.com/ABckh/zed-java-eclipse-jdtls) which hasn't seen a new version in 10 months (2024-10-01). Release Notes: - N/A --- docs/src/configuring-languages.md | 6 +-- docs/src/languages/java.md | 61 +++++++++---------------------- 2 files changed, 20 insertions(+), 47 deletions(-) diff --git a/docs/src/configuring-languages.md b/docs/src/configuring-languages.md index 42128cad6f6e2ad3879d570051a5aeded47520ea..52b7a3f7b82aeb3f2f19dcd63ef64c34251f1cd8 100644 --- a/docs/src/configuring-languages.md +++ b/docs/src/configuring-languages.md @@ -221,11 +221,11 @@ Most of the servers would rely on this way of configuring only. Apart of the LSP-related server configuration options, certain servers in Zed allow configuring the way binary is launched by Zed. -Languages mention in the documentation, whether they support it or not and their defaults for the configuration values: +Language servers are automatically downloaded or launched if found in your path, if you wish to specify an explicit alternate binary you can specify that in settings: ```json - "languages": { - "Markdown": { + "lsp": { + "rust-analyzer": { "binary": { // Whether to fetch the binary from the internet, or attempt to find locally. "ignore_system_version": false, diff --git a/docs/src/languages/java.md b/docs/src/languages/java.md index 70bafab476d36c83660077bfc6a25199445411de..0312cb3bd7e8b14ccedee7aacded456cc3e06e97 100644 --- a/docs/src/languages/java.md +++ b/docs/src/languages/java.md @@ -1,12 +1,8 @@ # Java -There are two extensions that provide Java language support for Zed: - -- Zed Java: [zed-extensions/java](https://github.com/zed-extensions/java) and -- Java with Eclipse JDTLS: [zed-java-eclipse-jdtls](https://github.com/ABckh/zed-java-eclipse-jdtls). - -Both use: +Java language support in Zed is provided by: +- Zed Java: [zed-extensions/java](https://github.com/zed-extensions/java) - Tree-sitter: [tree-sitter/tree-sitter-java](https://github.com/tree-sitter/tree-sitter-java) - Language Server: [eclipse-jdtls/eclipse.jdt.ls](https://github.com/eclipse-jdtls/eclipse.jdt.ls) @@ -25,11 +21,9 @@ Or manually download and install [OpenJDK 23](https://jdk.java.net/23/). You can install either by opening {#action zed::Extensions}({#kb zed::Extensions}) and searching for `java`. -We recommend you install one or the other and not both. - ## Settings / Initialization Options -Both extensions will automatically download the language server, see: [Manual JDTLS Install](#manual-jdts-install) below if you'd prefer to manage that yourself. +The extension will automatically download the language server, see: [Manual JDTLS Install](#manual-jdts-install) below if you'd prefer to manage that yourself. For available `initialization_options` please see the [Initialize Request section of the Eclipse.jdt.ls Wiki](https://github.com/eclipse-jdtls/eclipse.jdt.ls/wiki/Running-the-JAVA-LS-server-from-the-command-line#initialize-request). @@ -47,21 +41,25 @@ You can add these customizations to your Zed Settings by launching {#action zed: } ``` -### Java with Eclipse JDTLS settings +## Example Configs + +### JDTLS Binary + +By default, zed will look in your `PATH` for a `jdtls` binary, if you wish to specify an explicit binary you can do so via settings: ```json -{ "lsp": { - "java": { - "settings": {}, - "initialization_options": {} + "jdtls": { + "binary": { + "path": "/path/to/java/bin/jdtls", + // "arguments": [], + // "env": {}, + "ignore_system_version": true + } } } -} ``` -## Example Configs - ### Zed Java Initialization Options There are also many more options you can pass directly to the language server, for example: @@ -152,27 +150,9 @@ There are also many more options you can pass directly to the language server, f } ``` -### Java with Eclipse JTDLS Configuration {#zed-java-eclipse-configuration} - -Configuration options match those provided in the [redhat-developer/vscode-java extension](https://github.com/redhat-developer/vscode-java#supported-vs-code-settings). - -For example, to enable [Lombok Support](https://github.com/redhat-developer/vscode-java/wiki/Lombok-support): - -```json -{ - "lsp": { - "java": { - "settings": { - "java.jdt.ls.lombokSupport.enabled:": true - } - } - } -} -``` - ## Manual JDTLS Install -If you prefer, you can install JDTLS yourself and both extensions can be configured to use that instead. +If you prefer, you can install JDTLS yourself and the extension can be configured to use that instead. - MacOS: `brew install jdtls` - Arch: [`jdtls` from AUR](https://aur.archlinux.org/packages/jdtls) @@ -184,12 +164,5 @@ Or manually download install: ## See also -- [Zed Java Readme](https://github.com/zed-extensions/java) -- [Java with Eclipse JDTLS Readme](https://github.com/ABckh/zed-java-eclipse-jdtls) - -## Support - -If you have issues with either of these plugins, please open issues on their respective repositories: - +- [Zed Java Repo](https://github.com/zed-extensions/java) - [Zed Java Issues](https://github.com/zed-extensions/java/issues) -- [Java with Eclipse JDTLS Issues](https://github.com/ABckh/zed-java-eclipse-jdtls/issues) From 8dca4d150eb43dad60568d5bb0ec3b5c66722d77 Mon Sep 17 00:00:00 2001 From: Alvaro Parker <64918109+AlvaroParker@users.noreply.github.com> Date: Tue, 15 Jul 2025 04:47:35 -0400 Subject: [PATCH 070/658] Fix border and minimap flickering on pane split (#33973) Closes #33972 As noted on https://github.com/zed-industries/zed/pull/31390#discussion_r2147473526, when splitting panes and having a border size set for the active pane, or the minimap visibility configured to the active editor only, zed will shortly show a flicker of the border or the minimap on the pane that's being deactivated. Release Notes: - Fixed an issue where pane activations would sometimes have a brief delay, causing a flicker in the process. --- crates/workspace/src/workspace.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 99ab3cf6b716991da672fbc8b24f176b41a4f541..dc2c6516dd6892feb7749daf5bf654db94bfb11e 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3842,11 +3842,13 @@ impl Workspace { if *local { 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); } - serialize_workspace = *focus_changed || pane != self.active_pane(); } pane::Event::UserSavedItem { item, save_intent } => { cx.emit(Event::UserSavedItem { From 52f2b3255781999fda28a2f6d3c94402c4b29fa9 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 15 Jul 2025 13:07:29 +0200 Subject: [PATCH 071/658] extension_cli: Copy over snippet file when bundling extensions (#34450) Closes #30670 Release Notes: - Fixed snippets from extensions not working. --- crates/extension_cli/src/main.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/crates/extension_cli/src/main.rs b/crates/extension_cli/src/main.rs index 45a7e3b6412ea9fba02a6394394b3ca9fc5bc58f..ab4a9cddb0fa13421677772d1c07c1a8d9234d76 100644 --- a/crates/extension_cli/src/main.rs +++ b/crates/extension_cli/src/main.rs @@ -289,6 +289,24 @@ async fn copy_extension_resources( } } + if let Some(snippets_path) = manifest.snippets.as_ref() { + let parent = snippets_path.parent(); + if let Some(parent) = parent.filter(|p| p.components().next().is_some()) { + fs::create_dir_all(output_dir.join(parent))?; + } + copy_recursive( + fs.as_ref(), + &extension_path.join(&snippets_path), + &output_dir.join(&snippets_path), + CopyOptions { + overwrite: true, + ignore_if_exists: false, + }, + ) + .await + .with_context(|| format!("failed to copy snippets from '{}'", snippets_path.display()))?; + } + Ok(()) } From 848a86a385ecb7b1cc5fd933d848e64a0602c229 Mon Sep 17 00:00:00 2001 From: Marshall Bowers <git@maxdeviant.com> Date: Tue, 15 Jul 2025 09:01:01 -0400 Subject: [PATCH 072/658] collab: Sync model overages for all active Zed Pro subscriptions (#34452) Release Notes: - N/A --- crates/collab/src/api/billing.rs | 145 +++++++++++------- .../src/db/queries/billing_subscriptions.rs | 27 ++++ 2 files changed, 117 insertions(+), 55 deletions(-) diff --git a/crates/collab/src/api/billing.rs b/crates/collab/src/api/billing.rs index c8df066cbf1bbefd0515000a093d34371842c387..77ed9a9ea88504506d058eb0762914620326cc54 100644 --- a/crates/collab/src/api/billing.rs +++ b/crates/collab/src/api/billing.rs @@ -5,7 +5,7 @@ use axum::{ routing::{get, post}, }; use chrono::{DateTime, SecondsFormat, Utc}; -use collections::HashSet; +use collections::{HashMap, HashSet}; use reqwest::StatusCode; use sea_orm::ActiveValue; use serde::{Deserialize, Serialize}; @@ -21,12 +21,13 @@ use stripe::{ PaymentMethod, Subscription, SubscriptionId, SubscriptionStatus, }; use util::{ResultExt, maybe}; +use zed_llm_client::LanguageModelProvider; use crate::api::events::SnowflakeRow; use crate::db::billing_subscription::{ StripeCancellationReason, StripeSubscriptionStatus, SubscriptionKind, }; -use crate::llm::db::subscription_usage_meter::CompletionMode; +use crate::llm::db::subscription_usage_meter::{self, CompletionMode}; use crate::llm::{AGENT_EXTENDED_TRIAL_FEATURE_FLAG, DEFAULT_MAX_MONTHLY_SPEND}; use crate::rpc::{ResultExt as _, Server}; use crate::stripe_client::{ @@ -1416,18 +1417,21 @@ async fn sync_model_request_usage_with_stripe( let usage_meters = llm_db .get_current_subscription_usage_meters(Utc::now()) .await?; - let usage_meters = usage_meters - .into_iter() - .filter(|(_, usage)| !staff_user_ids.contains(&usage.user_id)) - .collect::<Vec<_>>(); - let user_ids = usage_meters - .iter() - .map(|(_, usage)| usage.user_id) - .collect::<HashSet<UserId>>(); - let billing_subscriptions = app - .db - .get_active_zed_pro_billing_subscriptions(user_ids) - .await?; + let mut usage_meters_by_user_id = + HashMap::<UserId, Vec<subscription_usage_meter::Model>>::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") @@ -1451,59 +1455,90 @@ async fn sync_model_request_usage_with_stripe( .find_price_by_lookup_key("claude-3-7-sonnet-requests-max") .await?; - let usage_meter_count = usage_meters.len(); + 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), + ]; - log::info!("Stripe usage sync: Syncing {usage_meter_count} usage meters"); + let billing_subscription_count = billing_subscriptions.len(); - for (usage_meter, usage) in usage_meters { + log::info!("Stripe usage sync: Syncing {billing_subscription_count} Zed Pro subscriptions"); + + for (user_id, (billing_customer, billing_subscription)) in billing_subscriptions { maybe!(async { - let Some((billing_customer, billing_subscription)) = - billing_subscriptions.get(&usage.user_id) - else { - bail!( - "Attempted to sync usage meter for user who is not a Stripe customer: {}", - usage.user_id - ); - }; + 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 model = llm_db.model_by_id(usage_meter.model_id)?; + let usage_meters = usage_meters_by_user_id.get(&user_id); - let (price, meter_event_name) = match model.name.as_str() { - "claude-opus-4" => match usage_meter.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 usage_meter.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 usage_meter.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") + 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:?}") } - }, - 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 - .subscribe_to_price(&stripe_subscription_id, price) - .await?; - stripe_billing - .bill_model_request_usage( - &stripe_customer_id, - meter_event_name, - usage_meter.requests, - ) - .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(()) }) @@ -1512,7 +1547,7 @@ async fn sync_model_request_usage_with_stripe( } log::info!( - "Stripe usage sync: Synced {usage_meter_count} usage meters in {:?}", + "Stripe usage sync: Synced {billing_subscription_count} Zed Pro subscriptions in {}", Utc::now() - started_at ); diff --git a/crates/collab/src/db/queries/billing_subscriptions.rs b/crates/collab/src/db/queries/billing_subscriptions.rs index f25d0abeaaba9b303d915350d138557e268824f9..9f82e3dbc4938d2c9a60f9c16ed69c485c3997be 100644 --- a/crates/collab/src/db/queries/billing_subscriptions.rs +++ b/crates/collab/src/db/queries/billing_subscriptions.rs @@ -199,6 +199,33 @@ impl Database { pub async fn get_active_zed_pro_billing_subscriptions( &self, + ) -> Result<HashMap<UserId, (billing_customer::Model, billing_subscription::Model)>> { + 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<UserId>, ) -> Result<HashMap<UserId, (billing_customer::Model, billing_subscription::Model)>> { self.transaction(|tx| { From a65c0b2bfffd8092fa4db4c54e5add9b54bb5386 Mon Sep 17 00:00:00 2001 From: Marshall Bowers <git@maxdeviant.com> Date: Tue, 15 Jul 2025 09:16:49 -0400 Subject: [PATCH 073/658] collab: Fix typo in log message (#34455) This PR fixes a small typo in a log message. Release Notes: - N/A --- crates/collab/src/api/billing.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/collab/src/api/billing.rs b/crates/collab/src/api/billing.rs index 77ed9a9ea88504506d058eb0762914620326cc54..00688a1e82be056a06e08a84013d4e95474bc971 100644 --- a/crates/collab/src/api/billing.rs +++ b/crates/collab/src/api/billing.rs @@ -1428,7 +1428,7 @@ async fn sync_model_request_usage_with_stripe( 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 {}", + "Stripe usage sync: Retrieved {} Zed Pro subscriptions in {}", billing_subscriptions.len(), Utc::now() - get_zed_pro_subscriptions_started_at ); From 858e176a1cfaeb00aed7f6b0d84c09aa13a09380 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 15 Jul 2025 10:45:59 -0300 Subject: [PATCH 074/658] Refine keymap UI design (#34437) Release Notes: - N/A --------- Co-authored-by: Ben Kunkle <Ben.kunkle@gmail.com> --- Cargo.lock | 2 + assets/icons/play_filled.svg | 3 + crates/icons/src/icons.rs | 1 + crates/settings_ui/Cargo.toml | 2 + crates/settings_ui/src/keybindings.rs | 557 +++++++++++++++++--------- crates/ui_input/src/ui_input.rs | 45 ++- 6 files changed, 398 insertions(+), 212 deletions(-) create mode 100644 assets/icons/play_filled.svg diff --git a/Cargo.lock b/Cargo.lock index 7e34f1e0555dfe3b60a53e084bea519cfd0557f2..7ba4f1f6e1335f8d5e576d6fe737b88758349550 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14684,6 +14684,7 @@ dependencies = [ "language", "log", "menu", + "notifications", "paths", "project", "schemars", @@ -14695,6 +14696,7 @@ dependencies = [ "tree-sitter-json", "tree-sitter-rust", "ui", + "ui_input", "util", "workspace", "workspace-hack", diff --git a/assets/icons/play_filled.svg b/assets/icons/play_filled.svg new file mode 100644 index 0000000000000000000000000000000000000000..387304ef0438cc7e5583267abc4b63624d4231df --- /dev/null +++ b/assets/icons/play_filled.svg @@ -0,0 +1,3 @@ +<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M5 4L10 7L5 10V4Z" fill="black" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index 29f7a8f50dd626a4d2cfa7032e5cf391c81de4fe..3c24ee59f6edf99b6f8ccf3093e83cf3ebea86c5 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -191,6 +191,7 @@ pub enum IconName { Play, PlayAlt, PlayBug, + PlayFilled, Plus, PocketKnife, Power, diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml index 2791876117f9c15941195eb850ee6f515e2e63a5..4502d994e7e64583c10f06e3542c462f17757ff9 100644 --- a/crates/settings_ui/Cargo.toml +++ b/crates/settings_ui/Cargo.toml @@ -26,6 +26,7 @@ gpui.workspace = true language.workspace = true log.workspace = true menu.workspace = true +notifications.workspace = true paths.workspace = true project.workspace = true schemars.workspace = true @@ -37,6 +38,7 @@ theme.workspace = true tree-sitter-json.workspace = true tree-sitter-rust.workspace = true ui.workspace = true +ui_input.workspace = true util.workspace = true workspace-hack.workspace = true workspace.workspace = true diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 210ec827cc556165291c6f676396efc6e1d573a8..a5008e17a0e9d892a8b9f8589dbb2a61063b4527 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -10,20 +10,22 @@ use feature_flags::FeatureFlagViewExt; use fs::Fs; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - Action, AnimationExt, AppContext as _, AsyncApp, ClickEvent, Context, DismissEvent, Entity, - EventEmitter, FocusHandle, Focusable, Global, IsZero, KeyContext, KeyDownEvent, Keystroke, - ModifiersChangedEvent, MouseButton, Point, ScrollStrategy, ScrollWheelEvent, StyledText, - Subscription, WeakEntity, actions, anchored, deferred, div, + Action, Animation, AnimationExt, AppContext as _, AsyncApp, ClickEvent, Context, DismissEvent, + Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Global, IsZero, KeyContext, + KeyDownEvent, Keystroke, ModifiersChangedEvent, MouseButton, Point, ScrollStrategy, + ScrollWheelEvent, StyledText, Subscription, WeakEntity, actions, anchored, deferred, div, }; use language::{Language, LanguageConfig, ToOffset as _}; +use notifications::status_toast::{StatusToast, ToastIcon}; use settings::{BaseKeymap, KeybindSource, KeymapFile, SettingsAssets}; use util::ResultExt; use ui::{ - ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, ParentElement as _, Render, - SharedString, Styled as _, Tooltip, Window, prelude::*, + ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, Modal, ModalFooter, ModalHeader, + ParentElement as _, Render, Section, SharedString, Styled as _, Tooltip, Window, prelude::*, }; +use ui_input::SingleLineInput; use workspace::{ Item, ModalView, SerializableItem, Workspace, notifications::NotifyTaskExt as _, register_serializable_item, @@ -637,7 +639,8 @@ impl KeymapEditor { "Delete", Box::new(DeleteBinding), ) - .action("Copy action", Box::new(CopyAction)) + .separator() + .action("Copy Action", Box::new(CopyAction)) .action_disabled_when( selected_binding_has_no_context, "Copy Context", @@ -845,9 +848,15 @@ impl KeymapEditor { self.search_mode = self.search_mode.invert(); self.update_matches(cx); + // Update the keystroke editor to turn the `search` bool on + self.keystroke_editor.update(cx, |keystroke_editor, cx| { + keystroke_editor.set_search_mode(self.search_mode == SearchMode::KeyStroke); + cx.notify(); + }); + match self.search_mode { SearchMode::KeyStroke => { - window.focus(&self.keystroke_editor.focus_handle(cx)); + window.focus(&self.keystroke_editor.read(cx).recording_focus_handle(cx)); } SearchMode::Normal => {} } @@ -964,41 +973,60 @@ impl Render for KeymapEditor { .gap_1() .bg(theme.colors().editor_background) .child( - h_flex() + v_flex() .p_2() - .gap_1() - .key_context({ - let mut context = KeyContext::new_with_defaults(); - context.add("BufferSearchBar"); - context - }) - .child( - div() - .size_full() - .h_8() - .pl_2() - .pr_1() - .py_1() - .border_1() - .border_color(theme.colors().border) - .rounded_lg() - .child(self.filter_editor.clone()), - ) + .gap_2() .child( - // TODO: Ask Mikyala if there's a way to get have items be aligned by horizontally - // without embedding a h_flex in another h_flex h_flex() + .gap_2() + .child( + div() + .key_context({ + let mut context = KeyContext::new_with_defaults(); + context.add("BufferSearchBar"); + context + }) + .size_full() + .h_8() + .pl_2() + .pr_1() + .py_1() + .border_1() + .border_color(theme.colors().border) + .rounded_lg() + .child(self.filter_editor.clone()), + ) + .child( + IconButton::new( + "KeymapEditorToggleFiltersIcon", + IconName::Keyboard, + ) + .shape(ui::IconButtonShape::Square) + .tooltip(|window, cx| { + Tooltip::for_action( + "Search by Keystroke", + &ToggleKeystrokeSearch, + window, + cx, + ) + }) + .toggle_state(matches!(self.search_mode, SearchMode::KeyStroke)) + .on_click(|_, window, cx| { + window.dispatch_action(ToggleKeystrokeSearch.boxed_clone(), cx); + }), + ) .when(self.keybinding_conflict_state.any_conflicts(), |this| { this.child( IconButton::new("KeymapEditorConflictIcon", IconName::Warning) + .shape(ui::IconButtonShape::Square) .tooltip({ let filter_state = self.filter_state; move |window, cx| { Tooltip::for_action( match filter_state { - FilterState::All => "Show conflicts", - FilterState::Conflicts => "Hide conflicts", + FilterState::All => "Show Conflicts", + FilterState::Conflicts => "Hide Conflicts", }, &ToggleConflictFilter, window, @@ -1006,7 +1034,7 @@ impl Render for KeymapEditor { ) } }) - .selected_icon_color(Color::Error) + .selected_icon_color(Color::Warning) .toggle_state(matches!( self.filter_state, FilterState::Conflicts @@ -1018,36 +1046,22 @@ impl Render for KeymapEditor { ); }), ) - }) - .child( - IconButton::new("KeymapEditorToggleFiltersIcon", IconName::Filter) - .tooltip(|window, cx| { - Tooltip::for_action( - "Toggle Keystroke Search", - &ToggleKeystrokeSearch, - window, - cx, - ) - }) - .toggle_state(matches!(self.search_mode, SearchMode::KeyStroke)) - .on_click(|_, window, cx| { - window.dispatch_action( - ToggleKeystrokeSearch.boxed_clone(), - cx, - ); - }), - ), - ), + }), + ) + .when(matches!(self.search_mode, SearchMode::KeyStroke), |this| { + this.child( + div() + .map(|this| { + if self.keybinding_conflict_state.any_conflicts() { + this.pr(rems_from_px(54.)) + } else { + this.pr_7() + } + }) + .child(self.keystroke_editor.clone()), + ) + }), ) - .when(matches!(self.search_mode, SearchMode::KeyStroke), |this| { - this.child( - div() - .child(self.keystroke_editor.clone()) - .border_1() - .border_color(theme.colors().border) - .rounded_lg(), - ) - }) .child( Table::new() .interactable(&self.table_interaction_state) @@ -1063,20 +1077,17 @@ impl Render for KeymapEditor { .filter_map(|index| { let candidate_id = this.matches.get(index)?.candidate_id; let binding = &this.keybindings[candidate_id]; + let action_name = binding.action_name.clone(); let action = div() - .child(binding.action_name.clone()) .id(("keymap action", index)) + .child(command_palette::humanize_action_name(&action_name)) .when(!context_menu_deployed, |this| { this.tooltip({ let action_name = binding.action_name.clone(); let action_docs = binding.action_docs; move |_, cx| { - let action_tooltip = Tooltip::new( - command_palette::humanize_action_name( - &action_name, - ), - ); + let action_tooltip = Tooltip::new(&action_name); let action_tooltip = match action_docs { Some(docs) => action_tooltip.meta(docs), None => action_tooltip, @@ -1285,11 +1296,12 @@ struct KeybindingEditorModal { editing_keybind: ProcessedKeybinding, editing_keybind_idx: usize, keybind_editor: Entity<KeystrokeInput>, - context_editor: Entity<Editor>, + context_editor: Entity<SingleLineInput>, input_editor: Option<Entity<Editor>>, fs: Arc<dyn Fs>, error: Option<InputError>, keymap_editor: Entity<KeymapEditor>, + workspace: WeakEntity<Workspace>, } impl ModalView for KeybindingEditorModal {} @@ -1316,25 +1328,28 @@ impl KeybindingEditorModal { let keybind_editor = cx .new(|cx| KeystrokeInput::new(editing_keybind.keystrokes().map(Vec::from), window, cx)); - let context_editor = cx.new(|cx| { - let mut editor = Editor::single_line(window, cx); + let context_editor: Entity<SingleLineInput> = cx.new(|cx| { + let input = SingleLineInput::new(window, cx, "Keybinding Context") + .label("Edit Context") + .label_size(LabelSize::Default); if let Some(context) = editing_keybind .context .as_ref() .and_then(KeybindContextString::local) { - editor.set_text(context.clone(), window, cx); - } else { - editor.set_placeholder_text("Keybinding context", cx); + input.editor().update(cx, |editor, cx| { + editor.set_text(context.clone(), window, cx); + }); } - cx.spawn(async |editor, cx| { + let editor_entity = input.editor().clone(); + cx.spawn(async move |_input_handle, cx| { let contexts = cx .background_spawn(async { collect_contexts_from_assets() }) .await; - editor + editor_entity .update(cx, |editor, _cx| { editor.set_completion_provider(Some(std::rc::Rc::new( KeyContextCompletionProvider { contexts }, @@ -1344,17 +1359,19 @@ impl KeybindingEditorModal { }) .detach_and_log_err(cx); - editor + input }); let input_editor = editing_keybind.action_schema.clone().map(|_schema| { cx.new(|cx| { let mut editor = Editor::auto_height_unbounded(1, window, cx); + let workspace = workspace.clone(); + if let Some(input) = editing_keybind.action_input.clone() { editor.set_text(input.text, window, cx); } else { // TODO: default value from schema? - editor.set_placeholder_text("Action input", cx); + editor.set_placeholder_text("Action Input", cx); } cx.spawn(async |editor, cx| { let json_language = load_json_language(workspace, cx).await; @@ -1383,6 +1400,7 @@ impl KeybindingEditorModal { input_editor, error: None, keymap_editor, + workspace, } } @@ -1431,7 +1449,7 @@ impl KeybindingEditorModal { let tab_size = cx.global::<settings::SettingsStore>().json_tab_size(); let new_context = self .context_editor - .read_with(cx, |editor, cx| editor.text(cx)); + .read_with(cx, |input, cx| input.editor().read(cx).text(cx)); let new_context = new_context.is_empty().not().then_some(new_context); let new_context_err = new_context.as_deref().and_then(|context| { gpui::KeyBindingContextPredicate::parse(context) @@ -1503,6 +1521,25 @@ impl KeybindingEditorModal { let create = self.creating; + let status_toast = StatusToast::new( + format!( + "Saved edits to the {} action.", + command_palette::humanize_action_name(&self.editing_keybind.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 + }, + ); + + self.workspace + .update(cx, |workspace, cx| { + workspace.toggle_status_toast(status_toast, cx); + }) + .log_err(); + cx.spawn(async move |this, cx| { if let Err(err) = save_keybinding_update( create, @@ -1533,90 +1570,96 @@ impl KeybindingEditorModal { impl Render for KeybindingEditorModal { fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let theme = cx.theme().colors(); - let input_base = || { - div() - .w_full() - .py_2() - .px_3() - .min_h_8() - .rounded_md() - .bg(theme.editor_background) - .border_1() - .border_color(theme.border_variant) - }; - - v_flex() - .w(rems(34.)) - .elevation_3(cx) - .child( - v_flex() - .p_3() - .child(Label::new("Edit Keystroke")) - .child( - Label::new("Input the desired keystroke for the selected action.") - .color(Color::Muted) - .mb_2(), - ) - .child(self.keybind_editor.clone()), - ) - .when_some(self.input_editor.clone(), |this, editor| { - this.child( - v_flex() - .p_3() - .pt_0() - .child(Label::new("Edit Input")) - .child( - Label::new("Input the desired input to the binding.") - .color(Color::Muted) - .mb_2(), - ) - .child(input_base().child(editor)), + let action_name = + command_palette::humanize_action_name(&self.editing_keybind.action_name).to_string(); + + v_flex().w(rems(34.)).elevation_3(cx).child( + Modal::new("keybinding_editor_modal", None) + .header( + ModalHeader::new().child( + v_flex() + .pb_1p5() + .mb_1() + .gap_0p5() + .border_b_1() + .border_color(theme.border_variant) + .child(Label::new(action_name)) + .when_some(self.editing_keybind.action_docs, |this, docs| { + this.child( + Label::new(docs).size(LabelSize::Small).color(Color::Muted), + ) + }), + ), ) - }) - .child( - v_flex() - .p_3() - .pt_0() - .child(Label::new("Edit Context")) - .child( - Label::new("Input the desired context for the binding.") - .color(Color::Muted) - .mb_2(), - ) - .child(input_base().child(self.context_editor.clone())), - ) - .when_some(self.error.as_ref(), |this, error| { - this.child( - div().p_2().child( - Banner::new() - .map(|banner| match error { - InputError::Error(_) => banner.severity(ui::Severity::Error), - InputError::Warning(_) => banner.severity(ui::Severity::Warning), + .section( + Section::new().child( + v_flex() + .gap_2() + .child( + v_flex() + .child(Label::new("Edit Keystroke")) + .gap_1() + .child(self.keybind_editor.clone()), + ) + .when_some(self.input_editor.clone(), |this, editor| { + this.child( + v_flex() + .mt_1p5() + .gap_1() + .child(Label::new("Edit Arguments")) + .child( + div() + .w_full() + .py_1() + .px_1p5() + .rounded_lg() + .bg(theme.editor_background) + .border_1() + .border_color(theme.border_variant) + .child(editor), + ), + ) }) - // 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()))), + .child(self.context_editor.clone()) + .when_some(self.error.as_ref(), |this, error| { + this.child( + Banner::new() + .map(|banner| match error { + InputError::Error(_) => { + banner.severity(ui::Severity::Error) + } + InputError::Warning(_) => { + banner.severity(ui::Severity::Warning) + } + }) + // 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())), + ), + ) + }), ), ) - }) - .child( - h_flex() - .p_2() - .w_full() - .gap_1() - .justify_end() - .border_t_1() - .border_color(theme.border_variant) - .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| Self::save(this, cx)), - ), + .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(cx); + }, + ))), ), - ) + ), + ) } } @@ -1848,6 +1891,7 @@ struct KeystrokeInput { inner_focus_handle: FocusHandle, intercept_subscription: Option<Subscription>, _focus_subscriptions: [Subscription; 2], + search: bool, } impl KeystrokeInput { @@ -1870,6 +1914,7 @@ impl KeystrokeInput { outer_focus_handle, intercept_subscription: None, _focus_subscriptions, + search: false, } } @@ -1987,6 +2032,14 @@ impl KeystrokeInput { )) }) } + + fn recording_focus_handle(&self, _cx: &App) -> FocusHandle { + self.inner_focus_handle.clone() + } + + fn set_search_mode(&mut self, search: bool) { + self.search = search; + } } impl EventEmitter<()> for KeystrokeInput {} @@ -2000,7 +2053,84 @@ impl Focusable for KeystrokeInput { impl Render for KeystrokeInput { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let colors = cx.theme().colors(); - let is_inner_focused = self.inner_focus_handle.is_focused(window); + let is_focused = self.outer_focus_handle.contains_focused(window, cx); + let is_recording = self.inner_focus_handle.is_focused(window); + + let horizontal_padding = rems_from_px(64.); + + let recording_bg_color = colors + .editor_background + .blend(colors.text_accent.opacity(0.1)); + + let recording_indicator = h_flex() + .h_4() + .pr_1() + .gap_0p5() + .border_1() + .border_color(colors.border) + .bg(colors + .editor_background + .blend(colors.text_accent.opacity(0.1))) + .rounded_sm() + .child( + Icon::new(IconName::Circle) + .size(IconSize::Small) + .color(Color::Error) + .with_animation( + "recording-pulse", + Animation::new(std::time::Duration::from_secs(2)) + .repeat() + .with_easing(gpui::pulsating_between(0.4, 0.8)), + { + let color = Color::Error.color(cx); + move |this, delta| this.color(Color::Custom(color.opacity(delta))) + }, + ), + ) + .child( + Label::new("REC") + .size(LabelSize::XSmall) + .weight(FontWeight::SEMIBOLD) + .color(Color::Error), + ); + + let search_indicator = h_flex() + .h_4() + .pr_1() + .gap_0p5() + .border_1() + .border_color(colors.border) + .bg(colors + .editor_background + .blend(colors.text_accent.opacity(0.1))) + .rounded_sm() + .child( + Icon::new(IconName::Circle) + .size(IconSize::Small) + .color(Color::Accent) + .with_animation( + "recording-pulse", + Animation::new(std::time::Duration::from_secs(2)) + .repeat() + .with_easing(gpui::pulsating_between(0.4, 0.8)), + { + let color = Color::Accent.color(cx); + move |this, delta| this.color(Color::Custom(color.opacity(delta))) + }, + ), + ) + .child( + Label::new("SEARCH") + .size(LabelSize::XSmall) + .weight(FontWeight::SEMIBOLD) + .color(Color::Accent), + ); + + let record_icon = if self.search { + IconName::MagnifyingGlass + } else { + IconName::PlayFilled + }; return h_flex() .id("keystroke-input") @@ -2008,18 +2138,23 @@ impl Render for KeystrokeInput { .py_2() .px_3() .gap_2() - .min_h_8() + .min_h_10() .w_full() .flex_1() .justify_between() - .rounded_md() + .rounded_lg() .overflow_hidden() - .bg(colors.editor_background) - .border_2() + .map(|this| { + if is_recording { + this.bg(recording_bg_color) + } else { + this.bg(colors.editor_background) + } + }) + .border_1() .border_color(colors.border_variant) - .focus(|mut s| { - s.border_color = Some(colors.border_focused); - s + .when(is_focused, |parent| { + parent.border_color(colors.border_focused) }) .on_key_down(cx.listener(|this, event: &KeyDownEvent, window, cx| { // TODO: replace with action @@ -2028,19 +2163,29 @@ impl Render for KeystrokeInput { cx.notify(); } })) + .child( + h_flex() + .w(horizontal_padding) + .gap_0p5() + .justify_start() + .flex_none() + .when(is_recording, |this| { + this.map(|this| { + if self.search { + this.child(search_indicator) + } else { + this.child(recording_indicator) + } + }) + }), + ) .child( h_flex() .id("keystroke-input-inner") .track_focus(&self.inner_focus_handle) .on_modifiers_changed(cx.listener(Self::on_modifiers_changed)) .on_key_up(cx.listener(Self::on_key_up)) - .when(self.highlight_on_focus, |this| { - this.focus(|mut style| { - style.border_color = Some(colors.border_focused); - style - }) - }) - .w_full() + .size_full() .min_w_0() .justify_center() .flex_wrap() @@ -2049,40 +2194,52 @@ impl Render for KeystrokeInput { ) .child( h_flex() + .w(horizontal_padding) .gap_0p5() + .justify_end() .flex_none() - .when(is_inner_focused, |this| { - this.child( - Icon::new(IconName::Circle) - .color(Color::Error) - .with_animation( - "recording-pulse", - gpui::Animation::new(std::time::Duration::from_secs(1)) - .repeat() - .with_easing(gpui::pulsating_between(0.8, 1.0)), - { - let color = Color::Error.color(cx); - move |this, delta| { - this.color(Color::Custom(color.opacity(delta))) + .map(|this| { + if is_recording { + this.child( + IconButton::new("stop-record-btn", IconName::StopFilled) + .shape(ui::IconButtonShape::Square) + .map(|this| { + if self.search { + this.tooltip(Tooltip::text("Stop Searching")) + } else { + this.tooltip(Tooltip::text("Stop Recording")) } - }, - ), - ) + }) + .icon_color(Color::Error) + .on_click(cx.listener(|this, _event, window, _cx| { + this.outer_focus_handle.focus(window); + })), + ) + } else { + this.child( + IconButton::new("record-btn", record_icon) + .shape(ui::IconButtonShape::Square) + .map(|this| { + if self.search { + this.tooltip(Tooltip::text("Start Searching")) + } else { + this.tooltip(Tooltip::text("Start Recording")) + } + }) + .when(!is_focused, |this| this.icon_color(Color::Muted)) + .on_click(cx.listener(|this, _event, window, _cx| { + this.inner_focus_handle.focus(window); + })), + ) + } }) .child( - IconButton::new("backspace-btn", IconName::Delete) - .tooltip(Tooltip::text("Delete Keystroke")) - .when(!is_inner_focused, |this| this.icon_color(Color::Muted)) - .on_click(cx.listener(|this, _event, _window, cx| { - this.keystrokes.pop(); - cx.emit(()); - cx.notify(); - })), - ) - .child( - IconButton::new("clear-btn", IconName::Eraser) + IconButton::new("clear-btn", IconName::Delete) + .shape(ui::IconButtonShape::Square) .tooltip(Tooltip::text("Clear Keystrokes")) - .when(!is_inner_focused, |this| this.icon_color(Color::Muted)) + .when(!is_recording || !is_focused, |this| { + this.icon_color(Color::Muted) + }) .on_click(cx.listener(|this, _event, _window, cx| { this.keystrokes.clear(); cx.emit(()); diff --git a/crates/ui_input/src/ui_input.rs b/crates/ui_input/src/ui_input.rs index bd99814cb30534165ad2bfba3911233e2946271b..ca2dea36df00ecb5f80fd535e98b80c1f0502141 100644 --- a/crates/ui_input/src/ui_input.rs +++ b/crates/ui_input/src/ui_input.rs @@ -27,6 +27,8 @@ pub struct SingleLineInput { /// /// Its position is determined by the [`FieldLabelLayout`]. label: Option<SharedString>, + /// The size of the label text. + label_size: LabelSize, /// The placeholder text for the text field. placeholder: SharedString, /// Exposes the underlying [`Entity<Editor>`] to allow for customizing the editor beyond the provided API. @@ -59,6 +61,7 @@ impl SingleLineInput { Self { label: None, + label_size: LabelSize::Small, placeholder: placeholder_text, editor, start_icon: None, @@ -76,6 +79,11 @@ impl SingleLineInput { self } + pub fn label_size(mut self, size: LabelSize) -> Self { + self.label_size = size; + self + } + pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context<Self>) { self.disabled = disabled; self.editor @@ -138,7 +146,7 @@ impl Render for SingleLineInput { .when_some(self.label.clone(), |this, label| { this.child( Label::new(label) - .size(LabelSize::Small) + .size(self.label_size) .color(if self.disabled { Color::Disabled } else { @@ -148,16 +156,17 @@ impl Render for SingleLineInput { }) .child( h_flex() + .min_w_48() + .min_h_8() + .w_full() .px_2() .py_1p5() - .bg(style.background_color) + .flex_grow() .text_color(style.text_color) - .rounded_md() + .rounded_lg() + .bg(style.background_color) .border_1() .border_color(style.border_color) - .min_w_48() - .w_full() - .flex_grow() .when_some(self.start_icon, |this, icon| { this.gap_1() .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted)) @@ -173,16 +182,28 @@ impl Component for SingleLineInput { } fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> { - let input_1 = - cx.new(|cx| SingleLineInput::new(window, cx, "placeholder").label("Some Label")); + let input_small = + cx.new(|cx| SingleLineInput::new(window, cx, "placeholder").label("Small Label")); + + let input_regular = cx.new(|cx| { + SingleLineInput::new(window, cx, "placeholder") + .label("Regular Label") + .label_size(LabelSize::Default) + }); Some( v_flex() .gap_6() - .children(vec![example_group(vec![single_example( - "Default", - div().child(input_1.clone()).into_any_element(), - )])]) + .children(vec![example_group(vec![ + single_example( + "Small Label (Default)", + div().child(input_small.clone()).into_any_element(), + ), + single_example( + "Regular Label", + div().child(input_regular.clone()).into_any_element(), + ), + ])]) .into_any_element(), ) } From 050ed85d711acc2914f4beb9be7ea942959091ed Mon Sep 17 00:00:00 2001 From: Hilmar Wiegand <me@hwgnd.de> Date: Tue, 15 Jul 2025 16:03:57 +0200 Subject: [PATCH 075/658] Add severity argument to GoToDiagnostic actions (#33995) This PR adds a `severity` argument so severity can be defined when navigating through diagnostics. This allows keybinds like the following: ```json { "] e": ["editor::GoToDiagnostic", { "severity": "error" }], "[ e": ["editor::GoToDiagnostic", { "severity": "error" }] } ``` I've added test comments and a test. Let me know if there's anything else you need! Release Notes: - Add `severity` argument to `editor::GoToDiagnostic`, `editor::GoToPreviousDiagnostic`, `project_panel::SelectNextDiagnostic` and `project_panel::SelectPrevDiagnostic` actions --- assets/keymaps/default-linux.json | 4 +- assets/keymaps/default-macos.json | 4 +- crates/diagnostics/src/diagnostics_tests.rs | 151 ++++++++++++++++++-- crates/diagnostics/src/items.rs | 11 +- crates/editor/src/actions.rs | 23 ++- crates/editor/src/editor.rs | 15 +- crates/editor/src/editor_tests.rs | 8 +- crates/project/src/project_settings.rs | 73 ++++++++++ crates/project_panel/src/project_panel.rs | 33 +++-- crates/vim/src/command.rs | 27 +++- crates/zed/src/zed/app_menus.rs | 7 +- crates/zed/src/zed/quick_action_bar.rs | 7 +- 12 files changed, 312 insertions(+), 51 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 2a4b8acd445dfdb40b468fec3cd3d7184c694583..02d08347fee1c6d4c29db76f93206f7ed45b884f 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -475,8 +475,8 @@ "ctrl-/": ["editor::ToggleComments", { "advance_downwards": false }], "ctrl-u": "editor::UndoSelection", "ctrl-shift-u": "editor::RedoSelection", - "f8": "editor::GoToDiagnostic", - "shift-f8": "editor::GoToPreviousDiagnostic", + "f8": ["editor::GoToDiagnostic", { "severity": { "min": "hint", "max": "error" } }], + "shift-f8": ["editor::GoToPreviousDiagnostic", { "severity": { "min": "hint", "max": "error" } }], "f2": "editor::Rename", "f12": "editor::GoToDefinition", "alt-f12": "editor::GoToDefinitionSplit", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 85766ecc3eb63a4bebb027f08c31850e6e0a8eca..ecb8648978bd677d526e5bdf921383ab6e4c8753 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -528,8 +528,8 @@ "cmd-/": ["editor::ToggleComments", { "advance_downwards": false }], "cmd-u": "editor::UndoSelection", "cmd-shift-u": "editor::RedoSelection", - "f8": "editor::GoToDiagnostic", - "shift-f8": "editor::GoToPreviousDiagnostic", + "f8": ["editor::GoToDiagnostic", { "severity": { "min": "hint", "max": "error" } }], + "shift-f8": ["editor::GoToPreviousDiagnostic", { "severity": { "min": "hint", "max": "error" } }], "f2": "editor::Rename", "f12": "editor::GoToDefinition", "alt-f12": "editor::GoToDefinitionSplit", diff --git a/crates/diagnostics/src/diagnostics_tests.rs b/crates/diagnostics/src/diagnostics_tests.rs index 0d47eaf367d6e28708cdf34258fc6080ba500c86..1364aaf853b9b9b0bb44969013a532234da656a3 100644 --- a/crates/diagnostics/src/diagnostics_tests.rs +++ b/crates/diagnostics/src/diagnostics_tests.rs @@ -14,7 +14,10 @@ use indoc::indoc; use language::{DiagnosticSourceKind, Rope}; use lsp::LanguageServerId; use pretty_assertions::assert_eq; -use project::FakeFs; +use project::{ + FakeFs, + project_settings::{GoToDiagnosticSeverity, GoToDiagnosticSeverityFilter}, +}; use rand::{Rng, rngs::StdRng, seq::IteratorRandom as _}; use serde_json::json; use settings::SettingsStore; @@ -1005,7 +1008,7 @@ async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext) cx.run_until_parked(); cx.update_editor(|editor, window, cx| { - editor.go_to_diagnostic(&GoToDiagnostic, window, cx); + editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx); assert_eq!( editor .active_diagnostic_group() @@ -1047,7 +1050,7 @@ async fn active_diagnostics_dismiss_after_invalidation(cx: &mut TestAppContext) "}); cx.update_editor(|editor, window, cx| { - editor.go_to_diagnostic(&GoToDiagnostic, window, cx); + editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx); assert_eq!(editor.active_diagnostic_group(), None); }); cx.assert_editor_state(indoc! {" @@ -1126,7 +1129,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) { // Fourth diagnostic cx.update_editor(|editor, window, cx| { - editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx); + editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx); }); cx.assert_editor_state(indoc! {" fn func(abc def: i32) -> ˇu32 { @@ -1135,7 +1138,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) { // Third diagnostic cx.update_editor(|editor, window, cx| { - editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx); + editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx); }); cx.assert_editor_state(indoc! {" fn func(abc ˇdef: i32) -> u32 { @@ -1144,7 +1147,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) { // Second diagnostic, same place cx.update_editor(|editor, window, cx| { - editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx); + editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx); }); cx.assert_editor_state(indoc! {" fn func(abc ˇdef: i32) -> u32 { @@ -1153,7 +1156,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) { // First diagnostic cx.update_editor(|editor, window, cx| { - editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx); + editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx); }); cx.assert_editor_state(indoc! {" fn func(abcˇ def: i32) -> u32 { @@ -1162,7 +1165,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) { // Wrapped over, fourth diagnostic cx.update_editor(|editor, window, cx| { - editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx); + editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx); }); cx.assert_editor_state(indoc! {" fn func(abc def: i32) -> ˇu32 { @@ -1181,7 +1184,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) { // First diagnostic cx.update_editor(|editor, window, cx| { - editor.go_to_diagnostic(&GoToDiagnostic, window, cx); + editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx); }); cx.assert_editor_state(indoc! {" fn func(abcˇ def: i32) -> u32 { @@ -1190,7 +1193,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) { // Second diagnostic cx.update_editor(|editor, window, cx| { - editor.go_to_diagnostic(&GoToDiagnostic, window, cx); + editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx); }); cx.assert_editor_state(indoc! {" fn func(abc ˇdef: i32) -> u32 { @@ -1199,7 +1202,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) { // Third diagnostic, same place cx.update_editor(|editor, window, cx| { - editor.go_to_diagnostic(&GoToDiagnostic, window, cx); + editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx); }); cx.assert_editor_state(indoc! {" fn func(abc ˇdef: i32) -> u32 { @@ -1208,7 +1211,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) { // Fourth diagnostic cx.update_editor(|editor, window, cx| { - editor.go_to_diagnostic(&GoToDiagnostic, window, cx); + editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx); }); cx.assert_editor_state(indoc! {" fn func(abc def: i32) -> ˇu32 { @@ -1217,7 +1220,7 @@ async fn cycle_through_same_place_diagnostics(cx: &mut TestAppContext) { // Wrapped around, first diagnostic cx.update_editor(|editor, window, cx| { - editor.go_to_diagnostic(&GoToDiagnostic, window, cx); + editor.go_to_diagnostic(&GoToDiagnostic::default(), window, cx); }); cx.assert_editor_state(indoc! {" fn func(abcˇ def: i32) -> u32 { @@ -1441,6 +1444,128 @@ async fn test_diagnostics_with_code(cx: &mut TestAppContext) { ); } +#[gpui::test] +async fn go_to_diagnostic_with_severity(cx: &mut TestAppContext) { + init_test(cx); + + 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.set_state(indoc! {"error warning info hiˇnt"}); + + cx.update(|_, cx| { + lsp_store.update(cx, |lsp_store, cx| { + lsp_store + .update_diagnostics( + LanguageServerId(0), + lsp::PublishDiagnosticsParams { + uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(), + version: None, + diagnostics: vec![ + lsp::Diagnostic { + range: lsp::Range::new( + lsp::Position::new(0, 0), + lsp::Position::new(0, 5), + ), + severity: Some(lsp::DiagnosticSeverity::ERROR), + ..Default::default() + }, + lsp::Diagnostic { + range: lsp::Range::new( + lsp::Position::new(0, 6), + lsp::Position::new(0, 13), + ), + severity: Some(lsp::DiagnosticSeverity::WARNING), + ..Default::default() + }, + lsp::Diagnostic { + range: lsp::Range::new( + lsp::Position::new(0, 14), + lsp::Position::new(0, 18), + ), + severity: Some(lsp::DiagnosticSeverity::INFORMATION), + ..Default::default() + }, + lsp::Diagnostic { + range: lsp::Range::new( + lsp::Position::new(0, 19), + lsp::Position::new(0, 23), + ), + severity: Some(lsp::DiagnosticSeverity::HINT), + ..Default::default() + }, + ], + }, + None, + DiagnosticSourceKind::Pushed, + &[], + cx, + ) + .unwrap() + }); + }); + cx.run_until_parked(); + + macro_rules! go { + ($severity:expr) => { + cx.update_editor(|editor, window, cx| { + editor.go_to_diagnostic( + &GoToDiagnostic { + severity: $severity, + }, + window, + cx, + ); + }); + }; + } + + // Default, should cycle through all diagnostics + go!(GoToDiagnosticSeverityFilter::default()); + cx.assert_editor_state(indoc! {"ˇerror warning info hint"}); + go!(GoToDiagnosticSeverityFilter::default()); + cx.assert_editor_state(indoc! {"error ˇwarning info hint"}); + go!(GoToDiagnosticSeverityFilter::default()); + cx.assert_editor_state(indoc! {"error warning ˇinfo hint"}); + go!(GoToDiagnosticSeverityFilter::default()); + cx.assert_editor_state(indoc! {"error warning info ˇhint"}); + go!(GoToDiagnosticSeverityFilter::default()); + cx.assert_editor_state(indoc! {"ˇerror warning info hint"}); + + let only_info = GoToDiagnosticSeverityFilter::Only(GoToDiagnosticSeverity::Information); + go!(only_info); + cx.assert_editor_state(indoc! {"error warning ˇinfo hint"}); + go!(only_info); + cx.assert_editor_state(indoc! {"error warning ˇinfo hint"}); + + let no_hints = GoToDiagnosticSeverityFilter::Range { + min: GoToDiagnosticSeverity::Information, + max: GoToDiagnosticSeverity::Error, + }; + + go!(no_hints); + cx.assert_editor_state(indoc! {"ˇerror warning info hint"}); + go!(no_hints); + cx.assert_editor_state(indoc! {"error ˇwarning info hint"}); + go!(no_hints); + cx.assert_editor_state(indoc! {"error warning ˇinfo hint"}); + go!(no_hints); + cx.assert_editor_state(indoc! {"ˇerror warning info hint"}); + + let warning_info = GoToDiagnosticSeverityFilter::Range { + min: GoToDiagnosticSeverity::Information, + max: GoToDiagnosticSeverity::Warning, + }; + + go!(warning_info); + cx.assert_editor_state(indoc! {"error ˇwarning info hint"}); + go!(warning_info); + cx.assert_editor_state(indoc! {"error warning ˇinfo hint"}); + go!(warning_info); + cx.assert_editor_state(indoc! {"error ˇwarning info hint"}); +} + fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { zlog::init_test(); diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index b5f9e901bbc819414c93ed6300a41a1731699379..4eea5e7e1f7b2fe6d17821615461650266619392 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -6,7 +6,7 @@ use gpui::{ WeakEntity, Window, }; use language::Diagnostic; -use project::project_settings::ProjectSettings; +use project::project_settings::{GoToDiagnosticSeverityFilter, ProjectSettings}; use settings::Settings; use ui::{Button, ButtonLike, Color, Icon, IconName, Label, Tooltip, h_flex, prelude::*}; use workspace::{StatusItemView, ToolbarItemEvent, Workspace, item::ItemHandle}; @@ -77,7 +77,7 @@ impl Render for DiagnosticIndicator { .tooltip(|window, cx| { Tooltip::for_action( "Next Diagnostic", - &editor::actions::GoToDiagnostic, + &editor::actions::GoToDiagnostic::default(), window, cx, ) @@ -156,7 +156,12 @@ impl DiagnosticIndicator { fn go_to_next_diagnostic(&mut self, window: &mut Window, cx: &mut Context<Self>) { if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) { editor.update(cx, |editor, cx| { - editor.go_to_diagnostic_impl(editor::Direction::Next, window, cx); + editor.go_to_diagnostic_impl( + editor::Direction::Next, + GoToDiagnosticSeverityFilter::default(), + window, + cx, + ); }) } } diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index e12bdf7a0707ad6f0a6d05c6d7577dd38e525335..c4866179c1d98bc1a36001b469cabe875ea42806 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -1,6 +1,7 @@ //! This module contains all actions supported by [`Editor`]. use super::*; use gpui::{Action, actions}; +use project::project_settings::GoToDiagnosticSeverityFilter; use schemars::JsonSchema; use util::serde::default_true; @@ -265,6 +266,24 @@ pub enum UuidVersion { V7, } +/// Goes to the next diagnostic in the file. +#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] +#[action(namespace = editor)] +#[serde(deny_unknown_fields)] +pub struct GoToDiagnostic { + #[serde(default)] + pub severity: GoToDiagnosticSeverityFilter, +} + +/// Goes to the previous diagnostic in the file. +#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] +#[action(namespace = editor)] +#[serde(deny_unknown_fields)] +pub struct GoToPreviousDiagnostic { + #[serde(default)] + pub severity: GoToDiagnosticSeverityFilter, +} + actions!( debugger, [ @@ -424,8 +443,6 @@ actions!( GoToDefinition, /// Goes to definition in a split pane. GoToDefinitionSplit, - /// Goes to the next diagnostic in the file. - GoToDiagnostic, /// Goes to the next diff hunk. GoToHunk, /// Goes to the previous diff hunk. @@ -440,8 +457,6 @@ actions!( GoToParentModule, /// Goes to the previous change in the file. GoToPreviousChange, - /// Goes to the previous diagnostic in the file. - GoToPreviousDiagnostic, /// Goes to the type definition of the symbol at cursor. GoToTypeDefinition, /// Goes to type definition in a split pane. diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 919d7cc9c75044c8d28f6a2281646238aed10b8e..72470c0a7d13cc75f83102d2c61c23c478063053 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -134,7 +134,7 @@ use project::{ session::{Session, SessionEvent}, }, git_store::{GitStoreEvent, RepositoryEvent}, - project_settings::DiagnosticSeverity, + project_settings::{DiagnosticSeverity, GoToDiagnosticSeverityFilter}, }; pub use git::blame::BlameRenderer; @@ -15086,7 +15086,7 @@ impl Editor { pub fn go_to_diagnostic( &mut self, - _: &GoToDiagnostic, + action: &GoToDiagnostic, window: &mut Window, cx: &mut Context<Self>, ) { @@ -15094,12 +15094,12 @@ impl Editor { return; } self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); - self.go_to_diagnostic_impl(Direction::Next, window, cx) + self.go_to_diagnostic_impl(Direction::Next, action.severity, window, cx) } pub fn go_to_prev_diagnostic( &mut self, - _: &GoToPreviousDiagnostic, + action: &GoToPreviousDiagnostic, window: &mut Window, cx: &mut Context<Self>, ) { @@ -15107,12 +15107,13 @@ impl Editor { return; } self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); - self.go_to_diagnostic_impl(Direction::Prev, window, cx) + self.go_to_diagnostic_impl(Direction::Prev, action.severity, window, cx) } pub fn go_to_diagnostic_impl( &mut self, direction: Direction, + severity: GoToDiagnosticSeverityFilter, window: &mut Window, cx: &mut Context<Self>, ) { @@ -15128,9 +15129,11 @@ impl Editor { fn filtered( snapshot: EditorSnapshot, + severity: GoToDiagnosticSeverityFilter, diagnostics: impl Iterator<Item = DiagnosticEntry<usize>>, ) -> impl Iterator<Item = DiagnosticEntry<usize>> { diagnostics + .filter(move |entry| severity.matches(entry.diagnostic.severity)) .filter(|entry| entry.range.start != entry.range.end) .filter(|entry| !entry.diagnostic.is_unnecessary) .filter(move |entry| !snapshot.intersects_fold(entry.range.start)) @@ -15139,12 +15142,14 @@ impl Editor { let snapshot = self.snapshot(window, cx); let before = filtered( snapshot.clone(), + severity, buffer .diagnostics_in_range(0..selection.start) .filter(|entry| entry.range.start <= selection.start), ); let after = filtered( snapshot, + severity, buffer .diagnostics_in_range(selection.start..buffer.len()) .filter(|entry| entry.range.start >= selection.start), diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 6e0b949cb75e64741e06fff924efd1d15daebb45..43c9c0db659210f68c59e225f1103405c2f14dc1 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -14734,7 +14734,7 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu executor.run_until_parked(); cx.update_editor(|editor, window, cx| { - editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx); + editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx); }); cx.assert_editor_state(indoc! {" @@ -14743,7 +14743,7 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu "}); cx.update_editor(|editor, window, cx| { - editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx); + editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx); }); cx.assert_editor_state(indoc! {" @@ -14752,7 +14752,7 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu "}); cx.update_editor(|editor, window, cx| { - editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx); + editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx); }); cx.assert_editor_state(indoc! {" @@ -14761,7 +14761,7 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu "}); cx.update_editor(|editor, window, cx| { - editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx); + editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx); }); cx.assert_editor_state(indoc! {" diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index 1c35f1652232113ed83c41fc6dee3d6b32251358..a85d90fe33575ecd15fd7f55166b35e2489e222b 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -326,6 +326,79 @@ impl DiagnosticSeverity { } } +/// Determines the severity of the diagnostic that should be moved to. +#[derive(PartialEq, PartialOrd, Clone, Copy, Debug, Eq, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum GoToDiagnosticSeverity { + /// Errors + Error = 3, + /// Warnings + Warning = 2, + /// Information + Information = 1, + /// Hints + Hint = 0, +} + +impl From<lsp::DiagnosticSeverity> for GoToDiagnosticSeverity { + fn from(severity: lsp::DiagnosticSeverity) -> Self { + match severity { + lsp::DiagnosticSeverity::ERROR => Self::Error, + lsp::DiagnosticSeverity::WARNING => Self::Warning, + lsp::DiagnosticSeverity::INFORMATION => Self::Information, + lsp::DiagnosticSeverity::HINT => Self::Hint, + _ => Self::Error, + } + } +} + +impl GoToDiagnosticSeverity { + pub fn min() -> Self { + Self::Hint + } + + pub fn max() -> Self { + Self::Error + } +} + +/// Allows filtering diagnostics that should be moved to. +#[derive(PartialEq, Clone, Copy, Debug, Deserialize, JsonSchema)] +#[serde(untagged)] +pub enum GoToDiagnosticSeverityFilter { + /// Move to diagnostics of a specific severity. + Only(GoToDiagnosticSeverity), + + /// Specify a range of severities to include. + Range { + /// Minimum severity to move to. Defaults no "error". + #[serde(default = "GoToDiagnosticSeverity::min")] + min: GoToDiagnosticSeverity, + /// Maximum severity to move to. Defaults to "hint". + #[serde(default = "GoToDiagnosticSeverity::max")] + max: GoToDiagnosticSeverity, + }, +} + +impl Default for GoToDiagnosticSeverityFilter { + fn default() -> Self { + Self::Range { + min: GoToDiagnosticSeverity::min(), + max: GoToDiagnosticSeverity::max(), + } + } +} + +impl GoToDiagnosticSeverityFilter { + pub fn matches(&self, severity: lsp::DiagnosticSeverity) -> bool { + let severity: GoToDiagnosticSeverity = severity.into(); + match self { + Self::Only(target) => *target == severity, + Self::Range { min, max } => severity >= *min && severity <= *max, + } + } +} + #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct GitSettings { /// Whether or not to show the git gutter. diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 9f799b5be6d4a9e94d23a6a920da257b0962d4e6..8f4aa12354d34245003219b9fa385631a4838c25 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -33,6 +33,7 @@ use project::{ Entry, EntryKind, Fs, GitEntry, GitEntryRef, GitTraversal, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId, git_store::{GitStoreEvent, git_traversal::ChildEntriesGitIter}, + project_settings::GoToDiagnosticSeverityFilter, relativize_path, }; use project_panel_settings::{ @@ -206,6 +207,24 @@ struct Trash { pub skip_prompt: bool, } +/// Selects the next entry with diagnostics. +#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] +#[action(namespace = project_panel)] +#[serde(deny_unknown_fields)] +struct SelectNextDiagnostic { + #[serde(default)] + pub severity: GoToDiagnosticSeverityFilter, +} + +/// Selects the previous entry with diagnostics. +#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] +#[action(namespace = project_panel)] +#[serde(deny_unknown_fields)] +struct SelectPrevDiagnostic { + #[serde(default)] + pub severity: GoToDiagnosticSeverityFilter, +} + actions!( project_panel, [ @@ -255,10 +274,6 @@ actions!( SelectNextGitEntry, /// Selects the previous entry with git changes. SelectPrevGitEntry, - /// Selects the next entry with diagnostics. - SelectNextDiagnostic, - /// Selects the previous entry with diagnostics. - SelectPrevDiagnostic, /// Selects the next directory. SelectNextDirectory, /// Selects the previous directory. @@ -1954,7 +1969,7 @@ impl ProjectPanel { fn select_prev_diagnostic( &mut self, - _: &SelectPrevDiagnostic, + action: &SelectPrevDiagnostic, _: &mut Window, cx: &mut Context<Self>, ) { @@ -1973,7 +1988,8 @@ impl ProjectPanel { && entry.is_file() && self .diagnostics - .contains_key(&(worktree_id, entry.path.to_path_buf())) + .get(&(worktree_id, entry.path.to_path_buf())) + .is_some_and(|severity| action.severity.matches(*severity)) }, cx, ); @@ -1989,7 +2005,7 @@ impl ProjectPanel { fn select_next_diagnostic( &mut self, - _: &SelectNextDiagnostic, + action: &SelectNextDiagnostic, _: &mut Window, cx: &mut Context<Self>, ) { @@ -2008,7 +2024,8 @@ impl ProjectPanel { && entry.is_file() && self .diagnostics - .contains_key(&(worktree_id, entry.path.to_path_buf())) + .get(&(worktree_id, entry.path.to_path_buf())) + .is_some_and(|severity| action.severity.matches(*severity)) }, cx, ); diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index c001f55a41c9d488240cb59fcc70ba111cca988b..74aed815a2aae5cffaa9e4c7a33b028305658258 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -1106,13 +1106,28 @@ fn generate_commands(_: &App) -> Vec<VimCommand> { VimCommand::str(("cl", "ist"), "diagnostics::Deploy"), VimCommand::new(("cc", ""), editor::actions::Hover), VimCommand::new(("ll", ""), editor::actions::Hover), - VimCommand::new(("cn", "ext"), editor::actions::GoToDiagnostic).range(wrap_count), - VimCommand::new(("cp", "revious"), editor::actions::GoToPreviousDiagnostic) + VimCommand::new(("cn", "ext"), editor::actions::GoToDiagnostic::default()) .range(wrap_count), - VimCommand::new(("cN", "ext"), editor::actions::GoToPreviousDiagnostic).range(wrap_count), - VimCommand::new(("lp", "revious"), editor::actions::GoToPreviousDiagnostic) - .range(wrap_count), - VimCommand::new(("lN", "ext"), editor::actions::GoToPreviousDiagnostic).range(wrap_count), + VimCommand::new( + ("cp", "revious"), + editor::actions::GoToPreviousDiagnostic::default(), + ) + .range(wrap_count), + VimCommand::new( + ("cN", "ext"), + editor::actions::GoToPreviousDiagnostic::default(), + ) + .range(wrap_count), + VimCommand::new( + ("lp", "revious"), + editor::actions::GoToPreviousDiagnostic::default(), + ) + .range(wrap_count), + VimCommand::new( + ("lN", "ext"), + editor::actions::GoToPreviousDiagnostic::default(), + ) + .range(wrap_count), VimCommand::new(("j", "oin"), JoinLines).range(select_range), VimCommand::new(("fo", "ld"), editor::actions::FoldSelectedRanges).range(act_on_range), VimCommand::new(("foldo", "pen"), editor::actions::UnfoldLines) diff --git a/crates/zed/src/zed/app_menus.rs b/crates/zed/src/zed/app_menus.rs index dac9f6495b83cca08257fb551e8f87667ea483de..ddab724f4ad4e435d5e332dc55122f6677adfffa 100644 --- a/crates/zed/src/zed/app_menus.rs +++ b/crates/zed/src/zed/app_menus.rs @@ -199,8 +199,11 @@ pub fn app_menus() -> Vec<Menu> { MenuItem::action("Go to Type Definition", editor::actions::GoToTypeDefinition), MenuItem::action("Find All References", editor::actions::FindAllReferences), MenuItem::separator(), - MenuItem::action("Next Problem", editor::actions::GoToDiagnostic), - MenuItem::action("Previous Problem", editor::actions::GoToPreviousDiagnostic), + MenuItem::action("Next Problem", editor::actions::GoToDiagnostic::default()), + MenuItem::action( + "Previous Problem", + editor::actions::GoToPreviousDiagnostic::default(), + ), ], }, Menu { diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index 36d446579a1c5cc2da010579a8e78ea3d2ed7076..c95d86c84f72a77010e06e9fec5b96d0c060fee4 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -255,8 +255,11 @@ impl Render for QuickActionBar { .action("Go to Symbol", Box::new(ToggleOutline)) .action("Go to Line/Column", Box::new(ToggleGoToLine)) .separator() - .action("Next Problem", Box::new(GoToDiagnostic)) - .action("Previous Problem", Box::new(GoToPreviousDiagnostic)) + .action("Next Problem", Box::new(GoToDiagnostic::default())) + .action( + "Previous Problem", + Box::new(GoToPreviousDiagnostic::default()), + ) .separator() .action_disabled_when(!has_diff_hunks, "Next Hunk", Box::new(GoToHunk)) .action_disabled_when( From 7ab8f431a743fb24cc8359b85d577cd45e46a6da Mon Sep 17 00:00:00 2001 From: Ben Brandt <benjamin.j.brandt@gmail.com> Date: Tue, 15 Jul 2025 16:28:27 +0200 Subject: [PATCH 076/658] Update to acp 0.0.9 (#34463) Release Notes: - N/A --- Cargo.lock | 52 ++++++++++++++++++++++--------- Cargo.toml | 2 +- crates/acp/src/acp.rs | 8 +++-- tooling/workspace-hack/Cargo.toml | 4 +-- 4 files changed, 47 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7ba4f1f6e1335f8d5e576d6fe737b88758349550..0a5a1a01fe69e250a10c0fd26867d69d0337336f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -264,16 +264,18 @@ dependencies = [ [[package]] name = "agentic-coding-protocol" -version = "0.0.7" +version = "0.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75f520bcc049ebe40c8c99427aa61b48ad78a01bcc96a13b350b903dcfb9438" +checksum = "0e276b798eddd02562a339340a96919d90bbfcf78de118fdddc932524646fac7" dependencies = [ "anyhow", "chrono", + "derive_more 2.0.1", "futures 0.3.31", "log", "parking_lot", "schemars", + "semver", "serde", "serde_json", ] @@ -676,7 +678,7 @@ dependencies = [ "anyhow", "async-trait", "collections", - "derive_more", + "derive_more 0.99.19", "extension", "futures 0.3.31", "gpui", @@ -739,7 +741,7 @@ dependencies = [ "clock", "collections", "ctor", - "derive_more", + "derive_more 0.99.19", "futures 0.3.31", "gpui", "icons", @@ -775,7 +777,7 @@ dependencies = [ "clock", "collections", "component", - "derive_more", + "derive_more 0.99.19", "editor", "feature_flags", "fs", @@ -1232,7 +1234,7 @@ version = "0.1.0" dependencies = [ "anyhow", "collections", - "derive_more", + "derive_more 0.99.19", "gpui", "parking_lot", "rodio", @@ -2925,7 +2927,7 @@ dependencies = [ "cocoa 0.26.0", "collections", "credentials_provider", - "derive_more", + "derive_more 0.99.19", "feature_flags", "fs", "futures 0.3.31", @@ -3113,7 +3115,7 @@ dependencies = [ "dap_adapters", "dashmap 6.1.0", "debugger_ui", - "derive_more", + "derive_more 0.99.19", "editor", "envy", "extension", @@ -3318,7 +3320,7 @@ name = "command_palette_hooks" version = "0.1.0" dependencies = [ "collections", - "derive_more", + "derive_more 0.99.19", "gpui", "workspace-hack", ] @@ -4525,6 +4527,27 @@ 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" @@ -6223,7 +6246,7 @@ dependencies = [ "askpass", "async-trait", "collections", - "derive_more", + "derive_more 0.99.19", "futures 0.3.31", "git2", "gpui", @@ -7240,7 +7263,7 @@ dependencies = [ "core-video", "cosmic-text", "ctor", - "derive_more", + "derive_more 0.99.19", "embed-resource", "env_logger 0.11.8", "etagere", @@ -7786,7 +7809,7 @@ version = "0.1.0" dependencies = [ "anyhow", "bytes 1.10.1", - "derive_more", + "derive_more 0.99.19", "futures 0.3.31", "http 1.3.1", "log", @@ -8224,7 +8247,7 @@ dependencies = [ "async-trait", "cargo_metadata", "collections", - "derive_more", + "derive_more 0.99.19", "extension", "fs", "futures 0.3.31", @@ -14163,6 +14186,7 @@ dependencies = [ "indexmap", "ref-cast", "schemars_derive", + "semver", "serde", "serde_json", ] @@ -16124,7 +16148,7 @@ version = "0.1.0" dependencies = [ "anyhow", "collections", - "derive_more", + "derive_more 0.99.19", "fs", "futures 0.3.31", "gpui", diff --git a/Cargo.toml b/Cargo.toml index f96240531f26232cf37d9560adf919c3f33f21e2..5403f279c80666f37e3d837e200ba0ba0b100a9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -404,7 +404,7 @@ zlog_settings = { path = "crates/zlog_settings" } # External crates # -agentic-coding-protocol = "0.0.7" +agentic-coding-protocol = { version = "0.0.9" } 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/src/acp.rs b/crates/acp/src/acp.rs index f89eab203206cba795693d3d5f0655abe06d83e1..8351aeaee0ef1d12a6db938aa3949d7bd19ccb43 100644 --- a/crates/acp/src/acp.rs +++ b/crates/acp/src/acp.rs @@ -875,7 +875,7 @@ impl AcpThread { &self, ) -> impl use<> + Future<Output = Result<acp::InitializeResponse, acp::Error>> { let connection = self.connection.clone(); - async move { connection.request(acp::InitializeParams).await } + async move { connection.initialize().await } } pub fn authenticate(&self) -> impl use<> + Future<Output = Result<(), acp::Error>> { @@ -1839,8 +1839,12 @@ mod tests { } impl acp::Agent for FakeAgent { - async fn initialize(&self) -> Result<acp::InitializeResponse, acp::Error> { + async fn initialize( + &self, + params: acp::InitializeParams, + ) -> Result<acp::InitializeResponse, acp::Error> { Ok(acp::InitializeResponse { + protocol_version: params.protocol_version, is_authenticated: true, }) } diff --git a/tooling/workspace-hack/Cargo.toml b/tooling/workspace-hack/Cargo.toml index 59f84492848a7320fe86f17d6431d2e7ed96d831..530c2cf925017cb4a1d833290567e08b07a408ed 100644 --- a/tooling/workspace-hack/Cargo.toml +++ b/tooling/workspace-hack/Cargo.toml @@ -107,7 +107,7 @@ 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"] } +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"] } @@ -240,7 +240,7 @@ 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"] } +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 05065985e70969d4b81830224bdd02c5efe76a18 Mon Sep 17 00:00:00 2001 From: tidely <43219534+tidely@users.noreply.github.com> Date: Tue, 15 Jul 2025 17:37:15 +0300 Subject: [PATCH 077/658] cli: Remove manual `std::io::copy` implementation (#34409) Removes a manual implementation of `std::io::copy`. The internal buffer of `std::io::copy` is also 8 kB and behaves exactly the same. On Linux `std::io::copy` also has access to some better performing file copying. Release Notes: - N/A --- crates/cli/src/main.rs | 30 +++++++----------------------- 1 file changed, 7 insertions(+), 23 deletions(-) diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index d6ddf79ea6cafd53efcad1d9979afbc47f3d6083..287c62b753f1ce875ca38a9f2caa62b906e6ee27 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -315,19 +315,19 @@ fn main() -> Result<()> { }); let stdin_pipe_handle: Option<JoinHandle<anyhow::Result<()>>> = - stdin_tmp_file.map(|tmp_file| { + stdin_tmp_file.map(|mut tmp_file| { thread::spawn(move || { - let stdin = std::io::stdin().lock(); - if io::IsTerminal::is_terminal(&stdin) { - return Ok(()); + let mut stdin = std::io::stdin().lock(); + if !io::IsTerminal::is_terminal(&stdin) { + io::copy(&mut stdin, &mut tmp_file)?; } - return pipe_to_tmp(stdin, tmp_file); + Ok(()) }) }); - let anonymous_fd_pipe_handles: Vec<JoinHandle<anyhow::Result<()>>> = anonymous_fd_tmp_files + let anonymous_fd_pipe_handles: Vec<_> = anonymous_fd_tmp_files .into_iter() - .map(|(file, tmp_file)| thread::spawn(move || pipe_to_tmp(file, tmp_file))) + .map(|(mut file, mut tmp_file)| thread::spawn(move || io::copy(&mut file, &mut tmp_file))) .collect(); if args.foreground { @@ -349,22 +349,6 @@ fn main() -> Result<()> { Ok(()) } -fn pipe_to_tmp(mut src: impl io::Read, mut dest: fs::File) -> Result<()> { - let mut buffer = [0; 8 * 1024]; - loop { - let bytes_read = match src.read(&mut buffer) { - Err(err) if err.kind() == io::ErrorKind::Interrupted => continue, - res => res?, - }; - if bytes_read == 0 { - break; - } - io::Write::write_all(&mut dest, &buffer[..bytes_read])?; - } - io::Write::flush(&mut dest)?; - Ok(()) -} - fn anonymous_fd(path: &str) -> Option<fs::File> { #[cfg(target_os = "linux")] { From d1abba0d336b18de77906c1e95180c4c88170faa Mon Sep 17 00:00:00 2001 From: tidely <43219534+tidely@users.noreply.github.com> Date: Tue, 15 Jul 2025 17:39:33 +0300 Subject: [PATCH 078/658] gpui: Reduce manual shifting & other minor improvements (#34407) Minor cleanup in gpui. - Reduce manual shifting by using `u32::to_be_bytes` - Remove eager `Vec` allocation when listing registered actions - Remove unnecessary return statements - Replace manual `if let Some(_)` with `.as_deref_mut()` Release Notes: - N/A --- crates/docs_preprocessor/src/main.rs | 1 - crates/gpui/src/action.rs | 10 ++++------ crates/gpui/src/app.rs | 6 +----- crates/gpui/src/color.rs | 19 +++++++------------ crates/zed/src/main.rs | 1 - 5 files changed, 12 insertions(+), 25 deletions(-) diff --git a/crates/docs_preprocessor/src/main.rs b/crates/docs_preprocessor/src/main.rs index c8e945c7e83564d162e0b939b92169b905558393..8eeeb6f0c5a105e186bdeac3e83807e50db721ea 100644 --- a/crates/docs_preprocessor/src/main.rs +++ b/crates/docs_preprocessor/src/main.rs @@ -243,7 +243,6 @@ struct ActionDef { fn dump_all_gpui_actions() -> Vec<ActionDef> { let mut actions = gpui::generate_list_of_all_registered_actions() - .into_iter() .map(|action| ActionDef { name: action.name, human_name: command_palette::humanize_action_name(action.name), diff --git a/crates/gpui/src/action.rs b/crates/gpui/src/action.rs index e099bfec28c3e3f15267348694f60c961df6f086..b179076cd5f0da826ca0d5da5e2a5a41cbb5e806 100644 --- a/crates/gpui/src/action.rs +++ b/crates/gpui/src/action.rs @@ -403,12 +403,10 @@ impl ActionRegistry { /// Useful for transforming the list of available actions into a /// format suited for static analysis such as in validating keymaps, or /// generating documentation. -pub fn generate_list_of_all_registered_actions() -> Vec<MacroActionData> { - let mut actions = Vec::new(); - for builder in inventory::iter::<MacroActionBuilder> { - actions.push(builder.0()); - } - actions +pub fn generate_list_of_all_registered_actions() -> impl Iterator<Item = MacroActionData> { + inventory::iter::<MacroActionBuilder> + .into_iter() + .map(|builder| builder.0()) } mod no_action { diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 6cecfcc0e42b239dc98db9391650f0618530d52c..957c7c4be6e2b818c9a750f5b0e0037a29607eda 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1250,11 +1250,7 @@ impl App { .downcast::<T>() .unwrap() .update(cx, |entity_state, cx| { - if let Some(window) = window { - on_new(entity_state, Some(window), cx); - } else { - on_new(entity_state, None, cx); - } + on_new(entity_state, window.as_deref_mut(), cx) }) }, ), diff --git a/crates/gpui/src/color.rs b/crates/gpui/src/color.rs index 7fc9c24393907d3991edcf9ae82b25eee419e766..a16c8f46bef28300deaeb4a603beabb1045f6b14 100644 --- a/crates/gpui/src/color.rs +++ b/crates/gpui/src/color.rs @@ -12,18 +12,13 @@ use std::{ /// Convert an RGB hex color code number to a color type pub fn rgb(hex: u32) -> Rgba { - let r = ((hex >> 16) & 0xFF) as f32 / 255.0; - let g = ((hex >> 8) & 0xFF) as f32 / 255.0; - let b = (hex & 0xFF) as f32 / 255.0; + let [_, r, g, b] = hex.to_be_bytes().map(|b| (b as f32) / 255.0); Rgba { r, g, b, a: 1.0 } } /// Convert an RGBA hex color code number to [`Rgba`] pub fn rgba(hex: u32) -> Rgba { - let r = ((hex >> 24) & 0xFF) as f32 / 255.0; - let g = ((hex >> 16) & 0xFF) as f32 / 255.0; - let b = ((hex >> 8) & 0xFF) as f32 / 255.0; - let a = (hex & 0xFF) as f32 / 255.0; + let [r, g, b, a] = hex.to_be_bytes().map(|b| (b as f32) / 255.0); Rgba { r, g, b, a } } @@ -63,14 +58,14 @@ impl Rgba { if other.a >= 1.0 { other } else if other.a <= 0.0 { - return *self; + *self } else { - return Rgba { + Rgba { r: (self.r * (1.0 - other.a)) + (other.r * other.a), g: (self.g * (1.0 - other.a)) + (other.g * other.a), b: (self.b * (1.0 - other.a)) + (other.b * other.a), a: self.a, - }; + } } } } @@ -494,12 +489,12 @@ impl Hsla { if alpha >= 1.0 { other } else if alpha <= 0.0 { - return self; + self } else { let converted_self = Rgba::from(self); let converted_other = Rgba::from(other); let blended_rgb = converted_self.blend(converted_other); - return Hsla::from(blended_rgb); + Hsla::from(blended_rgb) } } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 59f432faafbef0c607dd75d8f4623cc2be7b959a..6309c3a1373b2ce30db1f7eac0d1449f52ed4f7d 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -1391,7 +1391,6 @@ fn dump_all_gpui_actions() { documentation: Option<&'static str>, } let mut actions = gpui::generate_list_of_all_registered_actions() - .into_iter() .map(|action| ActionDef { name: action.name, human_name: command_palette::humanize_action_name(action.name), From bd78f2c49324169a9938628a339114ee143eabce Mon Sep 17 00:00:00 2001 From: tidely <43219534+tidely@users.noreply.github.com> Date: Tue, 15 Jul 2025 17:42:37 +0300 Subject: [PATCH 079/658] project: Use `checked_sub` for next/previous in search history (#34408) Use `checked_sub` instead of checking for bounds manually. Also greatly simplifies the logic for `next` and `previous`. Removing other manual bounds checks as well Release Notes: - N/A --- crates/project/src/search_history.rs | 32 ++++++++-------------------- crates/project/src/terminals.rs | 4 ++-- 2 files changed, 11 insertions(+), 25 deletions(-) diff --git a/crates/project/src/search_history.rs b/crates/project/src/search_history.rs index 382d04f8e47c8005de28670b756c0701f12d5dbc..b84c2e09820196e68ce013b1de453d4c0c283391 100644 --- a/crates/project/src/search_history.rs +++ b/crates/project/src/search_history.rs @@ -72,18 +72,12 @@ impl SearchHistory { } pub fn next(&mut self, cursor: &mut SearchHistoryCursor) -> Option<&str> { - let history_size = self.history.len(); - if history_size == 0 { - return None; - } - let selected = cursor.selection?; - if selected == history_size - 1 { - return None; - } let next_index = selected + 1; + + let next = self.history.get(next_index)?; cursor.selection = Some(next_index); - Some(&self.history[next_index]) + Some(next) } pub fn current(&self, cursor: &SearchHistoryCursor) -> Option<&str> { @@ -92,25 +86,17 @@ impl SearchHistory { .and_then(|selected_ix| self.history.get(selected_ix).map(|s| s.as_str())) } + /// Get the previous history entry using the given `SearchHistoryCursor`. + /// Uses the last element in the history when there is no cursor. pub fn previous(&mut self, cursor: &mut SearchHistoryCursor) -> Option<&str> { - let history_size = self.history.len(); - if history_size == 0 { - return None; - } - let prev_index = match cursor.selection { - Some(selected_index) => { - if selected_index == 0 { - return None; - } else { - selected_index - 1 - } - } - None => history_size - 1, + Some(index) => index.checked_sub(1)?, + None => self.history.len().checked_sub(1)?, }; + let previous = self.history.get(prev_index)?; cursor.selection = Some(prev_index); - Some(&self.history[prev_index]) + Some(previous) } } diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 385fdf9082baaf86bcdb547841cc98d161c5c508..b612d7b8094a8e1b4e093df512ca4128ba6f8ecb 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -169,7 +169,7 @@ impl Project { .read(cx) .get_cli_environment() .unwrap_or_default(); - env.extend(settings.env.clone()); + env.extend(settings.env); match self.ssh_details(cx) { Some(SshDetails { @@ -247,7 +247,7 @@ impl Project { .unwrap_or_default(); // Then extend it with the explicit env variables from the settings, so they take // precedence. - env.extend(settings.env.clone()); + env.extend(settings.env); let local_path = if is_ssh_terminal { None } else { path.clone() }; From 0671a4d5ae74a8a79b67a2cce0666a176e8a5a1d Mon Sep 17 00:00:00 2001 From: Taylor Beever <taybeever@gmail.com> Date: Tue, 15 Jul 2025 08:44:40 -0600 Subject: [PATCH 080/658] Allow for venv activation script to use `pyenv` (#33119) Release Notes: - Allows for configuration and use of `pyenv` as a virtual environment provider --- crates/project/src/terminals.rs | 45 +++++++++++++++--------- crates/terminal/src/terminal_settings.rs | 5 +++ 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index b612d7b8094a8e1b4e093df512ca4128ba6f8ecb..d3aec588ec126f64557f064c9c3d0fe225e284e3 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -523,36 +523,47 @@ impl Project { }, terminal_settings::ActivateScript::Nushell => "overlay use", terminal_settings::ActivateScript::PowerShell => ".", + terminal_settings::ActivateScript::Pyenv => "pyenv", _ => "source", }; let activate_script_name = match venv_settings.activate_script { - terminal_settings::ActivateScript::Default => "activate", + terminal_settings::ActivateScript::Default + | terminal_settings::ActivateScript::Pyenv => "activate", terminal_settings::ActivateScript::Csh => "activate.csh", terminal_settings::ActivateScript::Fish => "activate.fish", terminal_settings::ActivateScript::Nushell => "activate.nu", terminal_settings::ActivateScript::PowerShell => "activate.ps1", }; - let path = venv_base_directory - .join(match std::env::consts::OS { - "windows" => "Scripts", - _ => "bin", - }) - .join(activate_script_name) - .to_string_lossy() - .to_string(); - let quoted = shlex::try_quote(&path).ok()?; + let line_ending = match std::env::consts::OS { "windows" => "\r", _ => "\n", }; - smol::block_on(self.fs.metadata(path.as_ref())) - .ok() - .flatten()?; - Some(format!( - "{} {} ; clear{}", - activate_keyword, quoted, line_ending - )) + if venv_settings.venv_name.is_empty() { + let path = venv_base_directory + .join(match std::env::consts::OS { + "windows" => "Scripts", + _ => "bin", + }) + .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 + )) + } else { + Some(format!( + "{activate_keyword} {activate_script_name} {name}; clear{line_ending}", + name = venv_settings.venv_name + )) + } } fn activate_python_virtual_environment( diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs index 31c32dbdca22a73fddda7cd9334c6cde76a99c8b..f5d7d5b306fc428f9aa876effa54ae410b2c4a7f 100644 --- a/crates/terminal/src/terminal_settings.rs +++ b/crates/terminal/src/terminal_settings.rs @@ -95,12 +95,14 @@ pub enum VenvSettings { /// to the current working directory. We recommend overriding this /// in your project's settings, rather than globally. activate_script: Option<ActivateScript>, + venv_name: Option<String>, directories: Option<Vec<PathBuf>>, }, } pub struct VenvSettingsContent<'a> { pub activate_script: ActivateScript, + pub venv_name: &'a str, pub directories: &'a [PathBuf], } @@ -110,9 +112,11 @@ impl VenvSettings { VenvSettings::Off => None, VenvSettings::On { activate_script, + venv_name, directories, } => Some(VenvSettingsContent { activate_script: activate_script.unwrap_or(ActivateScript::Default), + venv_name: venv_name.as_deref().unwrap_or(""), directories: directories.as_deref().unwrap_or(&[]), }), } @@ -128,6 +132,7 @@ pub enum ActivateScript { Fish, Nushell, PowerShell, + Pyenv, } #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] From 5f3e7a5f917b678755a84cca6495ca8a922cb072 Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Tue, 15 Jul 2025 21:00:57 +0530 Subject: [PATCH 081/658] lsp: Wait for shutdown response before sending exit notification (#33417) Follow up: #18634 Closes #33328 Release Notes: - Fixed language server shutdown process to prevent race conditions and improper termination by waiting for shutdown confirmation before closing connections. --- crates/lsp/src/lsp.rs | 7 ++++--- crates/vim/src/test.rs | 4 ---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 53dc24a21a93fecee9a320a44a9b9c46655f31be..ad32d2dd34c57d5dca94b3b8ada699bb2e0a0e2a 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -874,8 +874,6 @@ impl LanguageServer { &executor, (), ); - let exit = Self::notify_internal::<notification::Exit>(&outbound_tx, &()); - outbound_tx.close(); let server = self.server.clone(); let name = self.name.clone(); @@ -901,7 +899,8 @@ impl LanguageServer { } response_handlers.lock().take(); - exit?; + Self::notify_internal::<notification::Exit>(&outbound_tx, &()).ok(); + outbound_tx.close(); output_done.recv().await; server.lock().take().map(|mut child| child.kill()); log::debug!("language server shutdown finished"); @@ -1508,6 +1507,8 @@ impl FakeLanguageServer { } }); + fake.set_request_handler::<request::Shutdown, _, _>(|_, _| async move { Ok(()) }); + (server, fake) } #[cfg(target_os = "windows")] diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 2db1d4a20cb7c4162ca2e795f880ece500d88e0f..ce04b621cb91c7b6b7da57bd1e1b74e9c0e00bbc 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -1006,8 +1006,6 @@ async fn test_rename(cx: &mut gpui::TestAppContext) { cx.assert_state("const afterˇ = 2; console.log(after)", Mode::Normal) } -// TODO: this test is flaky on our linux CI machines -#[cfg(target_os = "macos")] #[gpui::test] async fn test_remap(cx: &mut gpui::TestAppContext) { let mut cx = VimTestContext::new(cx, true).await; @@ -1048,8 +1046,6 @@ async fn test_remap(cx: &mut gpui::TestAppContext) { cx.simulate_keystrokes("g x"); cx.assert_state("1234fooˇ56789", Mode::Normal); - cx.executor().allow_parking(); - // test command cx.update(|_, cx| { cx.bind_keys([KeyBinding::new( From d7bb1c1d0e3d0f5d0c8a14df5b208decf71d1863 Mon Sep 17 00:00:00 2001 From: teapo <75266237+4teapo@users.noreply.github.com> Date: Tue, 15 Jul 2025 17:41:45 +0200 Subject: [PATCH 082/658] lsp: Fix workspace diagnostics lag & add streaming support (#34022) Closes https://github.com/zed-industries/zed/issues/33980 Closes https://github.com/zed-industries/zed/discussions/33979 - Switches to the debounce task pattern for diagnostic summary computations, which most importantly lets us do them only once when a large number of DiagnosticUpdated events are received at once. - Makes workspace diagnostic requests not time out if a partial result is received. - Makes diagnostics from workspace diagnostic partial results get merged. There might be some related areas where we're not fully complying with the LSP spec but they may be outside the scope of what this PR should include. Release Notes: - Added support for streaming LSP workspace diagnostics. - Fixed editor freeze from large LSP workspace diagnostic responses. --- Cargo.lock | 1 + crates/collab/Cargo.toml | 1 + crates/collab/src/tests/editor_tests.rs | 207 ++++++++++++++++-- crates/diagnostics/src/diagnostics.rs | 13 +- crates/diagnostics/src/items.rs | 15 +- crates/lsp/src/lsp.rs | 61 +++++- crates/project/src/lsp_store.rs | 247 ++++++++++++++-------- crates/project_panel/src/project_panel.rs | 14 +- crates/workspace/src/pane.rs | 15 +- 9 files changed, 460 insertions(+), 114 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0a5a1a01fe69e250a10c0fd26867d69d0337336f..de808ff263088b952c60befb84d6c6ce786a19e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3168,6 +3168,7 @@ dependencies = [ "session", "settings", "sha2", + "smol", "sqlx", "strum 0.27.1", "subtle", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 7b536a2d24bd408d7fa49e80453ec463c95e5347..242694d96365d218060601df7030b18552ee1e9b 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -127,6 +127,7 @@ sea-orm = { version = "1.1.0-rc.1", features = ["sqlx-sqlite"] } serde_json.workspace = true session = { workspace = true, features = ["test-support"] } settings = { workspace = true, features = ["test-support"] } +smol.workspace = true sqlx = { version = "0.8", features = ["sqlite"] } task.workspace = true theme.workspace = true diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 2cc3ca76d1b639cc479cb44cde93a73570d5eb7f..73ab2b8167e9537f57f33da22d079882fb7e8818 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -2246,8 +2246,11 @@ async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppCo }); } -#[gpui::test(iterations = 10)] -async fn test_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { +async fn test_lsp_pull_diagnostics( + should_stream_workspace_diagnostic: bool, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { let mut server = TestServer::start(cx_a.executor()).await; let executor = cx_a.executor(); let client_a = server.create_client(cx_a, "user_a").await; @@ -2396,12 +2399,25 @@ async fn test_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestApp let closure_workspace_diagnostics_pulls_made = workspace_diagnostics_pulls_made.clone(); let closure_workspace_diagnostics_pulls_result_ids = workspace_diagnostics_pulls_result_ids.clone(); + let (workspace_diagnostic_cancel_tx, closure_workspace_diagnostic_cancel_rx) = + smol::channel::bounded::<()>(1); + let (closure_workspace_diagnostic_received_tx, workspace_diagnostic_received_rx) = + smol::channel::bounded::<()>(1); + let expected_workspace_diagnostic_token = lsp::ProgressToken::String(format!( + "workspace/diagnostic-{}-1", + fake_language_server.server.server_id() + )); + let closure_expected_workspace_diagnostic_token = expected_workspace_diagnostic_token.clone(); let mut workspace_diagnostics_pulls_handle = fake_language_server .set_request_handler::<lsp::request::WorkspaceDiagnosticRequest, _, _>( move |params, _| { let workspace_requests_made = closure_workspace_diagnostics_pulls_made.clone(); let workspace_diagnostics_pulls_result_ids = closure_workspace_diagnostics_pulls_result_ids.clone(); + let workspace_diagnostic_cancel_rx = closure_workspace_diagnostic_cancel_rx.clone(); + let workspace_diagnostic_received_tx = closure_workspace_diagnostic_received_tx.clone(); + let expected_workspace_diagnostic_token = + closure_expected_workspace_diagnostic_token.clone(); async move { let workspace_request_count = workspace_requests_made.fetch_add(1, atomic::Ordering::Release) + 1; @@ -2411,6 +2427,21 @@ async fn test_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestApp .await .extend(params.previous_result_ids.into_iter().map(|id| id.value)); } + if should_stream_workspace_diagnostic && !workspace_diagnostic_cancel_rx.is_closed() + { + assert_eq!( + params.partial_result_params.partial_result_token, + Some(expected_workspace_diagnostic_token) + ); + workspace_diagnostic_received_tx.send(()).await.unwrap(); + workspace_diagnostic_cancel_rx.recv().await.unwrap(); + workspace_diagnostic_cancel_rx.close(); + // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#partialResults + // > The final response has to be empty in terms of result values. + return Ok(lsp::WorkspaceDiagnosticReportResult::Report( + lsp::WorkspaceDiagnosticReport { items: Vec::new() }, + )); + } Ok(lsp::WorkspaceDiagnosticReportResult::Report( lsp::WorkspaceDiagnosticReport { items: vec![ @@ -2479,7 +2510,11 @@ async fn test_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestApp }, ); - workspace_diagnostics_pulls_handle.next().await.unwrap(); + if should_stream_workspace_diagnostic { + workspace_diagnostic_received_rx.recv().await.unwrap(); + } else { + workspace_diagnostics_pulls_handle.next().await.unwrap(); + } assert_eq!( 1, workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire), @@ -2503,10 +2538,10 @@ async fn test_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestApp "Expected single diagnostic, but got: {all_diagnostics:?}" ); let diagnostic = &all_diagnostics[0]; - let expected_messages = [ - expected_workspace_pull_diagnostics_main_message, - expected_pull_diagnostic_main_message, - ]; + let mut expected_messages = vec![expected_pull_diagnostic_main_message]; + if !should_stream_workspace_diagnostic { + expected_messages.push(expected_workspace_pull_diagnostics_main_message); + } assert!( expected_messages.contains(&diagnostic.diagnostic.message.as_str()), "Expected {expected_messages:?} on the host, but got: {}", @@ -2556,6 +2591,70 @@ async fn test_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestApp version: None, }, ); + + if should_stream_workspace_diagnostic { + fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams { + token: expected_workspace_diagnostic_token.clone(), + value: lsp::ProgressParamsValue::WorkspaceDiagnostic( + lsp::WorkspaceDiagnosticReportResult::Report(lsp::WorkspaceDiagnosticReport { + items: vec![ + lsp::WorkspaceDocumentDiagnosticReport::Full( + lsp::WorkspaceFullDocumentDiagnosticReport { + uri: lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(), + version: None, + full_document_diagnostic_report: + lsp::FullDocumentDiagnosticReport { + result_id: Some(format!( + "workspace_{}", + workspace_diagnostics_pulls_made + .fetch_add(1, atomic::Ordering::Release) + + 1 + )), + items: vec![lsp::Diagnostic { + range: lsp::Range { + start: lsp::Position { + line: 0, + character: 1, + }, + end: lsp::Position { + line: 0, + character: 2, + }, + }, + severity: Some(lsp::DiagnosticSeverity::ERROR), + message: + expected_workspace_pull_diagnostics_main_message + .to_string(), + ..lsp::Diagnostic::default() + }], + }, + }, + ), + lsp::WorkspaceDocumentDiagnosticReport::Full( + lsp::WorkspaceFullDocumentDiagnosticReport { + uri: lsp::Url::from_file_path(path!("/a/lib.rs")).unwrap(), + version: None, + full_document_diagnostic_report: + lsp::FullDocumentDiagnosticReport { + result_id: Some(format!( + "workspace_{}", + workspace_diagnostics_pulls_made + .fetch_add(1, atomic::Ordering::Release) + + 1 + )), + items: Vec::new(), + }, + }, + ), + ], + }), + ), + }); + }; + + let mut workspace_diagnostic_start_count = + workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire); + executor.run_until_parked(); editor_a_main.update(cx_a, |editor, cx| { let snapshot = editor.buffer().read(cx).snapshot(cx); @@ -2599,7 +2698,7 @@ async fn test_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestApp ); executor.run_until_parked(); assert_eq!( - 1, + workspace_diagnostic_start_count, workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire), "Workspace diagnostics should not be changed as the remote client does not initialize the workspace diagnostics pull" ); @@ -2646,7 +2745,7 @@ async fn test_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestApp ); executor.run_until_parked(); assert_eq!( - 1, + workspace_diagnostic_start_count, workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire), "The remote client still did not anything to trigger the workspace diagnostics pull" ); @@ -2673,6 +2772,75 @@ async fn test_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestApp ); } }); + + if should_stream_workspace_diagnostic { + fake_language_server.notify::<lsp::notification::Progress>(&lsp::ProgressParams { + token: expected_workspace_diagnostic_token.clone(), + value: lsp::ProgressParamsValue::WorkspaceDiagnostic( + lsp::WorkspaceDiagnosticReportResult::Report(lsp::WorkspaceDiagnosticReport { + items: vec![lsp::WorkspaceDocumentDiagnosticReport::Full( + lsp::WorkspaceFullDocumentDiagnosticReport { + uri: lsp::Url::from_file_path(path!("/a/lib.rs")).unwrap(), + version: None, + full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport { + result_id: Some(format!( + "workspace_{}", + workspace_diagnostics_pulls_made + .fetch_add(1, atomic::Ordering::Release) + + 1 + )), + items: vec![lsp::Diagnostic { + range: lsp::Range { + start: lsp::Position { + line: 0, + character: 1, + }, + end: lsp::Position { + line: 0, + character: 2, + }, + }, + severity: Some(lsp::DiagnosticSeverity::ERROR), + message: expected_workspace_pull_diagnostics_lib_message + .to_string(), + ..lsp::Diagnostic::default() + }], + }, + }, + )], + }), + ), + }); + workspace_diagnostic_start_count = + workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire); + workspace_diagnostic_cancel_tx.send(()).await.unwrap(); + workspace_diagnostics_pulls_handle.next().await.unwrap(); + executor.run_until_parked(); + editor_b_lib.update(cx_b, |editor, cx| { + let snapshot = editor.buffer().read(cx).snapshot(cx); + let all_diagnostics = snapshot + .diagnostics_in_range(0..snapshot.len()) + .collect::<Vec<_>>(); + let expected_messages = [ + expected_workspace_pull_diagnostics_lib_message, + // TODO bug: the pushed diagnostics are not being sent to the client when they open the corresponding buffer. + // expected_push_diagnostic_lib_message, + ]; + assert_eq!( + all_diagnostics.len(), + 1, + "Expected pull diagnostics, but got: {all_diagnostics:?}" + ); + for diagnostic in all_diagnostics { + assert!( + expected_messages.contains(&diagnostic.diagnostic.message.as_str()), + "The client should get both push and pull messages: {expected_messages:?}, but got: {}", + diagnostic.diagnostic.message + ); + } + }); + }; + { assert!( diagnostics_pulls_result_ids.lock().await.len() > 0, @@ -2701,7 +2869,7 @@ async fn test_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestApp ); workspace_diagnostics_pulls_handle.next().await.unwrap(); assert_eq!( - 2, + workspace_diagnostic_start_count + 1, workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire), "After client lib.rs edits, the workspace diagnostics request should follow" ); @@ -2720,7 +2888,7 @@ async fn test_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestApp ); workspace_diagnostics_pulls_handle.next().await.unwrap(); assert_eq!( - 3, + workspace_diagnostic_start_count + 2, workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire), "After client main.rs edits, the workspace diagnostics pull should follow" ); @@ -2739,7 +2907,7 @@ async fn test_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestApp ); workspace_diagnostics_pulls_handle.next().await.unwrap(); assert_eq!( - 4, + workspace_diagnostic_start_count + 3, workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire), "After host main.rs edits, the workspace diagnostics pull should follow" ); @@ -2769,7 +2937,7 @@ async fn test_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestApp ); workspace_diagnostics_pulls_handle.next().await.unwrap(); assert_eq!( - 5, + workspace_diagnostic_start_count + 4, workspace_diagnostics_pulls_made.load(atomic::Ordering::Acquire), "Another workspace diagnostics pull should happen after the diagnostics refresh server request" ); @@ -2840,6 +3008,19 @@ async fn test_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestApp }); } +#[gpui::test(iterations = 10)] +async fn test_non_streamed_lsp_pull_diagnostics( + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + test_lsp_pull_diagnostics(false, cx_a, cx_b).await; +} + +#[gpui::test(iterations = 10)] +async fn test_streamed_lsp_pull_diagnostics(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + test_lsp_pull_diagnostics(true, cx_a, cx_b).await; +} + #[gpui::test(iterations = 10)] async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { let mut server = TestServer::start(cx_a.executor()).await; diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index b2e0a682056356cddd077d42418a2b4fa763cffa..ba64ba0eedbbb51d9c599a48634ff88ab632a9ed 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -80,6 +80,7 @@ pub(crate) struct ProjectDiagnosticsEditor { include_warnings: bool, update_excerpts_task: Option<Task<Result<()>>>, cargo_diagnostics_fetch: CargoDiagnosticsFetchState, + diagnostic_summary_update: Task<()>, _subscription: Subscription, } @@ -179,7 +180,16 @@ impl ProjectDiagnosticsEditor { path, } => { this.paths_to_update.insert(path.clone()); - this.summary = project.read(cx).diagnostic_summary(false, cx); + let project = project.clone(); + this.diagnostic_summary_update = cx.spawn(async move |this, cx| { + cx.background_executor() + .timer(Duration::from_millis(30)) + .await; + this.update(cx, |this, cx| { + this.summary = project.read(cx).diagnostic_summary(false, cx); + }) + .log_err(); + }); cx.emit(EditorEvent::TitleChanged); if this.editor.focus_handle(cx).contains_focused(window, cx) || this.focus_handle.contains_focused(window, cx) { @@ -276,6 +286,7 @@ impl ProjectDiagnosticsEditor { 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); diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index 4eea5e7e1f7b2fe6d17821615461650266619392..7ac6d101f315674cec4fd07f4ad2df0830284124 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -9,6 +9,7 @@ use language::Diagnostic; use project::project_settings::{GoToDiagnosticSeverityFilter, ProjectSettings}; use settings::Settings; use ui::{Button, ButtonLike, Color, Icon, IconName, Label, Tooltip, h_flex, prelude::*}; +use util::ResultExt; use workspace::{StatusItemView, ToolbarItemEvent, Workspace, item::ItemHandle}; use crate::{Deploy, IncludeWarnings, ProjectDiagnosticsEditor}; @@ -20,6 +21,7 @@ pub struct DiagnosticIndicator { current_diagnostic: Option<Diagnostic>, _observe_active_editor: Option<Subscription>, diagnostics_update: Task<()>, + diagnostic_summary_update: Task<()>, } impl Render for DiagnosticIndicator { @@ -135,8 +137,16 @@ impl DiagnosticIndicator { } project::Event::DiagnosticsUpdated { .. } => { - this.summary = project.read(cx).diagnostic_summary(false, cx); - cx.notify(); + this.diagnostic_summary_update = cx.spawn(async move |this, cx| { + cx.background_executor() + .timer(Duration::from_millis(30)) + .await; + this.update(cx, |this, cx| { + this.summary = project.read(cx).diagnostic_summary(false, cx); + cx.notify(); + }) + .log_err(); + }); } _ => {} @@ -150,6 +160,7 @@ impl DiagnosticIndicator { current_diagnostic: None, _observe_active_editor: None, diagnostics_update: Task::ready(()), + diagnostic_summary_update: Task::ready(()), } } diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index ad32d2dd34c57d5dca94b3b8ada699bb2e0a0e2a..4248f910eedd2b9a242365569318ad0d9b32510b 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -1106,6 +1106,7 @@ impl LanguageServer { pub fn binary(&self) -> &LanguageServerBinary { &self.binary } + /// Sends a RPC request to the language server. /// /// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#requestMessage) @@ -1125,16 +1126,40 @@ impl LanguageServer { ) } - fn request_internal<T>( + /// Sends a RPC request to the language server, with a custom timer, a future which when becoming + /// ready causes the request to be timed out with the future's output message. + /// + /// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#requestMessage) + pub fn request_with_timer<T: request::Request, U: Future<Output = String>>( + &self, + params: T::Params, + timer: U, + ) -> impl LspRequestFuture<T::Result> + use<T, U> + where + T::Result: 'static + Send, + { + Self::request_internal_with_timer::<T, U>( + &self.next_id, + &self.response_handlers, + &self.outbound_tx, + &self.executor, + timer, + params, + ) + } + + fn request_internal_with_timer<T, U>( next_id: &AtomicI32, response_handlers: &Mutex<Option<HashMap<RequestId, ResponseHandler>>>, outbound_tx: &channel::Sender<String>, executor: &BackgroundExecutor, + timer: U, params: T::Params, - ) -> impl LspRequestFuture<T::Result> + use<T> + ) -> impl LspRequestFuture<T::Result> + use<T, U> where T::Result: 'static + Send, T: request::Request, + U: Future<Output = String>, { let id = next_id.fetch_add(1, SeqCst); let message = serde_json::to_string(&Request { @@ -1179,7 +1204,6 @@ impl LanguageServer { .context("failed to write to language server's stdin"); let outbound_tx = outbound_tx.downgrade(); - let mut timeout = executor.timer(LSP_REQUEST_TIMEOUT).fuse(); let started = Instant::now(); LspRequest::new(id, async move { if let Err(e) = handle_response { @@ -1216,14 +1240,41 @@ impl LanguageServer { } } - _ = timeout => { - log::error!("Cancelled LSP request task for {method:?} id {id} which took over {LSP_REQUEST_TIMEOUT:?}"); + message = timer.fuse() => { + log::error!("Cancelled LSP request task for {method:?} id {id} {message}"); ConnectionResult::Timeout } } }) } + fn request_internal<T>( + next_id: &AtomicI32, + response_handlers: &Mutex<Option<HashMap<RequestId, ResponseHandler>>>, + outbound_tx: &channel::Sender<String>, + executor: &BackgroundExecutor, + params: T::Params, + ) -> impl LspRequestFuture<T::Result> + use<T> + where + T::Result: 'static + Send, + T: request::Request, + { + Self::request_internal_with_timer::<T, _>( + next_id, + response_handlers, + outbound_tx, + executor, + Self::default_request_timer(executor.clone()), + params, + ) + } + + pub fn default_request_timer(executor: BackgroundExecutor) -> impl Future<Output = String> { + executor + .timer(LSP_REQUEST_TIMEOUT) + .map(|_| format!("which took over {LSP_REQUEST_TIMEOUT:?}")) + } + /// Sends a RPC notification to the language server. /// /// [LSP Specification](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#notificationMessage) diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index fd626cf2d6889668447b90cf88f963804fd65eba..e4078393ee20fb906d6501bd6820e73a46bf9c39 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -29,7 +29,7 @@ use clock::Global; use collections::{BTreeMap, BTreeSet, HashMap, HashSet, btree_map}; use futures::{ AsyncWriteExt, Future, FutureExt, StreamExt, - future::{Shared, join_all}, + future::{Either, Shared, join_all, pending, select}, select, select_biased, stream::FuturesUnordered, }; @@ -85,9 +85,11 @@ use std::{ cmp::{Ordering, Reverse}, convert::TryInto, ffi::OsStr, + future::ready, iter, mem, ops::{ControlFlow, Range}, path::{self, Path, PathBuf}, + pin::pin, rc::Rc, sync::Arc, time::{Duration, Instant}, @@ -7585,7 +7587,8 @@ impl LspStore { diagnostics, |_, _, _| false, cx, - ) + )?; + Ok(()) } pub fn merge_diagnostic_entries( @@ -9130,13 +9133,39 @@ impl LspStore { } }; - let progress = match progress.value { - lsp::ProgressParamsValue::WorkDone(progress) => progress, - lsp::ProgressParamsValue::WorkspaceDiagnostic(_) => { - return; + match progress.value { + lsp::ProgressParamsValue::WorkDone(progress) => { + self.handle_work_done_progress( + progress, + language_server_id, + disk_based_diagnostics_progress_token, + token, + cx, + ); } - }; + lsp::ProgressParamsValue::WorkspaceDiagnostic(report) => { + if let Some(LanguageServerState::Running { + workspace_refresh_task: Some(workspace_refresh_task), + .. + }) = self + .as_local_mut() + .and_then(|local| local.language_servers.get_mut(&language_server_id)) + { + workspace_refresh_task.progress_tx.try_send(()).ok(); + self.apply_workspace_diagnostic_report(language_server_id, report, cx) + } + } + } + } + fn handle_work_done_progress( + &mut self, + progress: lsp::WorkDoneProgress, + language_server_id: LanguageServerId, + disk_based_diagnostics_progress_token: Option<String>, + token: String, + cx: &mut Context<Self>, + ) { let language_server_status = if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { status @@ -11297,13 +11326,13 @@ impl LspStore { pub fn pull_workspace_diagnostics(&mut self, server_id: LanguageServerId) { if let Some(LanguageServerState::Running { - workspace_refresh_task: Some((tx, _)), + workspace_refresh_task: Some(workspace_refresh_task), .. }) = self .as_local_mut() .and_then(|local| local.language_servers.get_mut(&server_id)) { - tx.try_send(()).ok(); + workspace_refresh_task.refresh_tx.try_send(()).ok(); } } @@ -11319,11 +11348,83 @@ impl LspStore { local.language_server_ids_for_buffer(buffer, cx) }) { if let Some(LanguageServerState::Running { - workspace_refresh_task: Some((tx, _)), + workspace_refresh_task: Some(workspace_refresh_task), .. }) = local.language_servers.get_mut(&server_id) { - tx.try_send(()).ok(); + workspace_refresh_task.refresh_tx.try_send(()).ok(); + } + } + } + + fn apply_workspace_diagnostic_report( + &mut self, + server_id: LanguageServerId, + report: lsp::WorkspaceDiagnosticReportResult, + cx: &mut Context<Self>, + ) { + 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( + 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(), + 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(); + } } } } @@ -11379,7 +11480,7 @@ fn subscribe_to_binary_statuses( fn lsp_workspace_diagnostics_refresh( server: Arc<LanguageServer>, cx: &mut Context<'_, LspStore>, -) -> Option<(mpsc::Sender<()>, Task<()>)> { +) -> Option<WorkspaceRefreshTask> { let identifier = match server.capabilities().diagnostic_provider? { lsp::DiagnosticServerCapabilities::Options(diagnostic_options) => { if !diagnostic_options.workspace_diagnostics { @@ -11396,19 +11497,22 @@ fn lsp_workspace_diagnostics_refresh( } }; - let (mut tx, mut rx) = mpsc::channel(1); - tx.try_send(()).ok(); + let (progress_tx, mut progress_rx) = mpsc::channel(1); + let (mut refresh_tx, mut refresh_rx) = mpsc::channel(1); + refresh_tx.try_send(()).ok(); let workspace_query_language_server = cx.spawn(async move |lsp_store, cx| { let mut attempts = 0; let max_attempts = 50; + let mut requests = 0; loop { - let Some(()) = rx.recv().await else { + let Some(()) = refresh_rx.recv().await else { return; }; 'request: loop { + requests += 1; if attempts > max_attempts { log::error!( "Failed to pull workspace diagnostics {max_attempts} times, aborting" @@ -11437,14 +11541,29 @@ fn lsp_workspace_diagnostics_refresh( return; }; + let token = format!("workspace/diagnostic-{}-{}", server.server_id(), requests); + + progress_rx.try_recv().ok(); + let timer = + LanguageServer::default_request_timer(cx.background_executor().clone()).fuse(); + let progress = pin!(progress_rx.recv().fuse()); let response_result = server - .request::<lsp::WorkspaceDiagnosticRequest>(lsp::WorkspaceDiagnosticParams { - previous_result_ids, - identifier: identifier.clone(), - work_done_progress_params: Default::default(), - partial_result_params: Default::default(), - }) + .request_with_timer::<lsp::WorkspaceDiagnosticRequest, _>( + lsp::WorkspaceDiagnosticParams { + previous_result_ids, + identifier: identifier.clone(), + work_done_progress_params: Default::default(), + partial_result_params: lsp::PartialResultParams { + partial_result_token: Some(lsp::ProgressToken::String(token)), + }, + }, + select(timer, progress).then(|either| match either { + Either::Left((message, ..)) => ready(message).left_future(), + Either::Right(..) => pending::<String>().right_future(), + }), + ) .await; + // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnostic_refresh // > If a server closes a workspace diagnostic pull request the client should re-trigger the request. match response_result { @@ -11464,72 +11583,11 @@ fn lsp_workspace_diagnostics_refresh( attempts = 0; if lsp_store .update(cx, |lsp_store, cx| { - let workspace_diagnostics = - GetDocumentDiagnostics::deserialize_workspace_diagnostics_report(pulled_diagnostics, server.server_id()); - for workspace_diagnostics in workspace_diagnostics { - let LspPullDiagnostics::Response { - server_id, - uri, - diagnostics, - } = workspace_diagnostics.diagnostics - 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 { - diagnostics, - result_id, - } => { - lsp_store - .merge_diagnostics( - server_id, - lsp::PublishDiagnosticsParams { - uri: uri.clone(), - 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(); - } - } - } + lsp_store.apply_workspace_diagnostic_report( + server.server_id(), + pulled_diagnostics, + cx, + ) }) .is_err() { @@ -11542,7 +11600,11 @@ fn lsp_workspace_diagnostics_refresh( } }); - Some((tx, workspace_query_language_server)) + Some(WorkspaceRefreshTask { + refresh_tx, + progress_tx, + task: workspace_query_language_server, + }) } fn resolve_word_completion(snapshot: &BufferSnapshot, completion: &mut Completion) { @@ -11912,6 +11974,13 @@ impl LanguageServerLogType { } } +pub struct WorkspaceRefreshTask { + refresh_tx: mpsc::Sender<()>, + progress_tx: mpsc::Sender<()>, + #[allow(dead_code)] + task: Task<()>, +} + pub enum LanguageServerState { Starting { startup: Task<Option<Arc<LanguageServer>>>, @@ -11923,7 +11992,7 @@ pub enum LanguageServerState { adapter: Arc<CachedLspAdapter>, server: Arc<LanguageServer>, simulate_disk_based_diagnostics_completion: Option<Task<()>>, - workspace_refresh_task: Option<(mpsc::Sender<()>, Task<()>)>, + workspace_refresh_task: Option<WorkspaceRefreshTask>, }, } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 8f4aa12354d34245003219b9fa385631a4838c25..e1d360cd976386d2e24e63b9eda05afe123dc411 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -108,6 +108,7 @@ pub struct ProjectPanel { hide_scrollbar_task: Option<Task<()>>, diagnostics: HashMap<(WorktreeId, PathBuf), DiagnosticSeverity>, max_width_item_index: Option<usize>, + diagnostic_summary_update: Task<()>, // We keep track of the mouse down state on entries so we don't flash the UI // in case a user clicks to open a file. mouse_down: bool, @@ -420,8 +421,16 @@ impl ProjectPanel { | project::Event::DiagnosticsUpdated { .. } => { if ProjectPanelSettings::get_global(cx).show_diagnostics != ShowDiagnostics::Off { - this.update_diagnostics(cx); - cx.notify(); + this.diagnostic_summary_update = cx.spawn(async move |this, cx| { + cx.background_executor() + .timer(Duration::from_millis(30)) + .await; + this.update(cx, |this, cx| { + this.update_diagnostics(cx); + cx.notify(); + }) + .log_err(); + }); } } project::Event::WorktreeRemoved(id) => { @@ -564,6 +573,7 @@ impl ProjectPanel { .parent_entity(&cx.entity()), max_width_item_index: None, diagnostics: Default::default(), + diagnostic_summary_update: Task::ready(()), scroll_handle, mouse_down: false, hover_expand_task: None, diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 4d3f6823b36e58c898f192ebb95e4ee274133580..19afd49848db3f63a227a2660486b4f0a9f19d1d 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -40,6 +40,7 @@ use std::{ Arc, atomic::{AtomicUsize, Ordering}, }, + time::Duration, }; use theme::ThemeSettings; use ui::{ @@ -364,6 +365,7 @@ pub struct Pane { pinned_tab_count: usize, diagnostics: HashMap<ProjectPath, DiagnosticSeverity>, zoom_out_on_close: bool, + diagnostic_summary_update: Task<()>, /// If a certain project item wants to get recreated with specific data, it can persist its data before the recreation here. pub project_item_restoration_data: HashMap<ProjectItemKind, Box<dyn Any + Send>>, } @@ -505,6 +507,7 @@ impl Pane { pinned_tab_count: 0, diagnostics: Default::default(), zoom_out_on_close: true, + diagnostic_summary_update: Task::ready(()), project_item_restoration_data: HashMap::default(), } } @@ -616,8 +619,16 @@ impl Pane { project::Event::DiskBasedDiagnosticsFinished { .. } | project::Event::DiagnosticsUpdated { .. } => { if ItemSettings::get_global(cx).show_diagnostics != ShowDiagnostics::Off { - self.update_diagnostics(cx); - cx.notify(); + self.diagnostic_summary_update = cx.spawn(async move |this, cx| { + cx.background_executor() + .timer(Duration::from_millis(30)) + .await; + this.update(cx, |this, cx| { + this.update_diagnostics(cx); + cx.notify(); + }) + .log_err(); + }); } } _ => {} From 95de2bfc74657241b209e42d7e73f3cdde5e6f23 Mon Sep 17 00:00:00 2001 From: Ben Kunkle <ben@zed.dev> Date: Tue, 15 Jul 2025 11:03:16 -0500 Subject: [PATCH 083/658] keymap_ui: Limit length of keystroke input and hook up actions (#34464) Closes #ISSUE Changes direction on the design of the keystroke input. Due to MacOS limitations, it was decided that the complex repeat keystroke logic could be avoided by limiting the number of keystrokes so that accidental repeats were less damaging to ux. This PR follows up on the design pass in #34437 that assumed these changes would be made, hooking up actions and greatly improving the keyboard navigability of the keystroke input. Release Notes: - N/A *or* Added/Fixed/Improved ... --- assets/keymaps/default-linux.json | 9 + assets/keymaps/default-macos.json | 9 + crates/settings_ui/src/keybindings.rs | 329 ++++++++++++++++++-------- crates/zed/src/zed.rs | 1 + 4 files changed, 247 insertions(+), 101 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 02d08347fee1c6d4c29db76f93206f7ed45b884f..562afea85454995a1e32ef46bf82fb46220b8e47 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -1120,5 +1120,14 @@ "alt-ctrl-f": "keymap_editor::ToggleKeystrokeSearch", "alt-c": "keymap_editor::ToggleConflictFilter" } + }, + { + "context": "KeystrokeInput", + "use_key_equivalents": true, + "bindings": { + "enter": "keystroke_input::StartRecording", + "escape escape escape": "keystroke_input::StopRecording", + "delete": "keystroke_input::ClearKeystrokes" + } } ] diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index ecb8648978bd677d526e5bdf921383ab6e4c8753..fa9fce4555319f62ff1e3ed359e5165acdcbdb49 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -1217,5 +1217,14 @@ "cmd-alt-f": "keymap_editor::ToggleKeystrokeSearch", "cmd-alt-c": "keymap_editor::ToggleConflictFilter" } + }, + { + "context": "KeystrokeInput", + "use_key_equivalents": true, + "bindings": { + "enter": "keystroke_input::StartRecording", + "escape escape escape": "keystroke_input::StopRecording", + "delete": "keystroke_input::ClearKeystrokes" + } } ] diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index a5008e17a0e9d892a8b9f8589dbb2a61063b4527..bf9e72297f2c3807f182e02e0f3de92701949064 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -12,7 +12,7 @@ use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ Action, Animation, AnimationExt, AppContext as _, AsyncApp, ClickEvent, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Global, IsZero, KeyContext, - KeyDownEvent, Keystroke, ModifiersChangedEvent, MouseButton, Point, ScrollStrategy, + Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, Point, ScrollStrategy, ScrollWheelEvent, StyledText, Subscription, WeakEntity, actions, anchored, deferred, div, }; use language::{Language, LanguageConfig, ToOffset as _}; @@ -68,6 +68,18 @@ actions!( ] ); +actions!( + keystroke_input, + [ + /// Starts recording keystrokes + StartRecording, + /// Stops recording keystrokes + StopRecording, + /// Clears the recorded keystrokes + ClearKeystrokes, + ] +); + pub fn init(cx: &mut App) { let keymap_event_channel = KeymapEventChannel::new(); cx.set_global(keymap_event_channel); @@ -1883,6 +1895,13 @@ async fn remove_keybinding( Ok(()) } +#[derive(PartialEq, Eq, Debug, Copy, Clone)] +enum CloseKeystrokeResult { + Partial, + Close, + None, +} + struct KeystrokeInput { keystrokes: Vec<Keystroke>, placeholder_keystrokes: Option<Vec<Keystroke>>, @@ -1892,9 +1911,13 @@ struct KeystrokeInput { intercept_subscription: Option<Subscription>, _focus_subscriptions: [Subscription; 2], search: bool, + close_keystrokes: Option<Vec<Keystroke>>, + close_keystrokes_start: Option<usize>, } impl KeystrokeInput { + const KEYSTROKE_COUNT_MAX: usize = 3; + fn new( placeholder_keystrokes: Option<Vec<Keystroke>>, window: &mut Window, @@ -1915,7 +1938,81 @@ impl KeystrokeInput { intercept_subscription: None, _focus_subscriptions, search: false, + close_keystrokes: None, + close_keystrokes_start: None, + } + } + + fn dummy(modifiers: Modifiers) -> Keystroke { + return Keystroke { + modifiers, + key: "".to_string(), + key_char: None, + }; + } + + fn keystrokes_changed(&self, cx: &mut Context<Self>) { + cx.emit(()); + cx.notify(); + } + + fn key_context() -> KeyContext { + let mut key_context = KeyContext::new_with_defaults(); + key_context.add("KeystrokeInput"); + key_context + } + + fn handle_possible_close_keystroke( + &mut self, + keystroke: &Keystroke, + window: &mut Window, + cx: &mut Context<Self>, + ) -> CloseKeystrokeResult { + let Some(keybind_for_close_action) = window + .highest_precedence_binding_for_action_in_context(&StopRecording, Self::key_context()) + else { + log::trace!("No keybinding to stop recording keystrokes in keystroke input"); + self.close_keystrokes.take(); + return CloseKeystrokeResult::None; + }; + let action_keystrokes = keybind_for_close_action.keystrokes(); + + if let Some(mut close_keystrokes) = self.close_keystrokes.take() { + let mut index = 0; + + while index < action_keystrokes.len() && index < close_keystrokes.len() { + if !close_keystrokes[index].should_match(&action_keystrokes[index]) { + break; + } + index += 1; + } + if index == close_keystrokes.len() { + if index >= action_keystrokes.len() { + self.close_keystrokes_start.take(); + return CloseKeystrokeResult::None; + } + if keystroke.should_match(&action_keystrokes[index]) { + if action_keystrokes.len() >= 1 && index == action_keystrokes.len() - 1 { + self.stop_recording(&StopRecording, window, cx); + return CloseKeystrokeResult::Close; + } else { + close_keystrokes.push(keystroke.clone()); + self.close_keystrokes = Some(close_keystrokes); + return CloseKeystrokeResult::Partial; + } + } else { + self.close_keystrokes_start.take(); + return CloseKeystrokeResult::None; + } + } + } else if let Some(first_action_keystroke) = action_keystrokes.first() + && keystroke.should_match(first_action_keystroke) + { + self.close_keystrokes = Some(vec![keystroke.clone()]); + return CloseKeystrokeResult::Partial; } + self.close_keystrokes_start.take(); + return CloseKeystrokeResult::None; } fn on_modifiers_changed( @@ -1924,65 +2021,60 @@ impl KeystrokeInput { _window: &mut Window, cx: &mut Context<Self>, ) { + let keystrokes_len = self.keystrokes.len(); + if let Some(last) = self.keystrokes.last_mut() && last.key.is_empty() + && keystrokes_len <= Self::KEYSTROKE_COUNT_MAX { if !event.modifiers.modified() { self.keystrokes.pop(); - cx.emit(()); } else { last.modifiers = event.modifiers; } - } else { - self.keystrokes.push(Keystroke { - modifiers: event.modifiers, - key: "".to_string(), - key_char: None, - }); - cx.emit(()); + self.keystrokes_changed(cx); + } else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX { + self.keystrokes.push(Self::dummy(event.modifiers)); + self.keystrokes_changed(cx); } cx.stop_propagation(); - cx.notify(); } - fn handle_keystroke(&mut self, keystroke: &Keystroke, cx: &mut Context<Self>) { - if let Some(last) = self.keystrokes.last_mut() - && last.key.is_empty() - { - *last = keystroke.clone(); - } else if Some(keystroke) != self.keystrokes.last() { - self.keystrokes.push(keystroke.clone()); - } - cx.emit(()); - cx.stop_propagation(); - cx.notify(); - } - - fn on_key_up( + fn handle_keystroke( &mut self, - event: &gpui::KeyUpEvent, - _window: &mut Window, + keystroke: &Keystroke, + window: &mut Window, cx: &mut Context<Self>, ) { - if let Some(last) = self.keystrokes.last_mut() - && !last.key.is_empty() - && last.modifiers == event.keystroke.modifiers + let close_keystroke_result = self.handle_possible_close_keystroke(keystroke, window, cx); + if close_keystroke_result == CloseKeystrokeResult::Close { + return; + } + if let Some(last) = self.keystrokes.last() + && last.key.is_empty() + && self.keystrokes.len() <= Self::KEYSTROKE_COUNT_MAX { - cx.emit(()); - self.keystrokes.push(Keystroke { - modifiers: event.keystroke.modifiers, - key: "".to_string(), - key_char: None, - }); + self.keystrokes.pop(); + } + if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX { + if close_keystroke_result == CloseKeystrokeResult::Partial + && self.close_keystrokes_start.is_none() + { + self.close_keystrokes_start = Some(self.keystrokes.len()); + } + self.keystrokes.push(keystroke.clone()); + if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX { + self.keystrokes.push(Self::dummy(keystroke.modifiers)); + } } + self.keystrokes_changed(cx); cx.stop_propagation(); - cx.notify(); } fn on_inner_focus_in(&mut self, _window: &mut Window, cx: &mut Context<Self>) { if self.intercept_subscription.is_none() { - let listener = cx.listener(|this, event: &gpui::KeystrokeEvent, _window, cx| { - this.handle_keystroke(&event.keystroke, cx); + let listener = cx.listener(|this, event: &gpui::KeystrokeEvent, window, cx| { + this.handle_keystroke(&event.keystroke, window, cx); }); self.intercept_subscription = Some(cx.intercept_keystrokes(listener)) } @@ -2014,18 +2106,22 @@ impl KeystrokeInput { return &self.keystrokes; } - fn render_keystrokes(&self) -> impl Iterator<Item = Div> { - let (keystrokes, color) = if let Some(placeholders) = self.placeholder_keystrokes.as_ref() + fn render_keystrokes(&self, is_recording: bool) -> impl Iterator<Item = Div> { + let keystrokes = if let Some(placeholders) = self.placeholder_keystrokes.as_ref() && self.keystrokes.is_empty() { - (placeholders, Color::Placeholder) + if is_recording { + &[] + } else { + placeholders.as_slice() + } } else { - (&self.keystrokes, Color::Default) + &self.keystrokes }; keystrokes.iter().map(move |keystroke| { h_flex().children(ui::render_keystroke( keystroke, - Some(color), + Some(Color::Default), Some(rems(0.875).into()), ui::PlatformStyle::platform(), false, @@ -2040,6 +2136,40 @@ impl KeystrokeInput { fn set_search_mode(&mut self, search: bool) { self.search = search; } + + fn start_recording(&mut self, _: &StartRecording, window: &mut Window, cx: &mut Context<Self>) { + if !self.outer_focus_handle.is_focused(window) { + return; + } + self.clear_keystrokes(&ClearKeystrokes, window, cx); + window.focus(&self.inner_focus_handle); + cx.notify(); + } + + fn stop_recording(&mut self, _: &StopRecording, window: &mut Window, cx: &mut Context<Self>) { + if !self.inner_focus_handle.is_focused(window) { + return; + } + window.focus(&self.outer_focus_handle); + if let Some(close_keystrokes_start) = self.close_keystrokes_start.take() { + self.keystrokes.drain(close_keystrokes_start..); + } + self.close_keystrokes.take(); + cx.notify(); + } + + fn clear_keystrokes( + &mut self, + _: &ClearKeystrokes, + window: &mut Window, + cx: &mut Context<Self>, + ) { + if !self.outer_focus_handle.is_focused(window) { + return; + } + self.keystrokes.clear(); + cx.notify(); + } } impl EventEmitter<()> for KeystrokeInput {} @@ -2062,6 +2192,22 @@ impl Render for KeystrokeInput { .editor_background .blend(colors.text_accent.opacity(0.1)); + let recording_pulse = || { + Icon::new(IconName::Circle) + .size(IconSize::Small) + .color(Color::Error) + .with_animation( + "recording-pulse", + Animation::new(std::time::Duration::from_secs(2)) + .repeat() + .with_easing(gpui::pulsating_between(0.4, 0.8)), + { + let color = Color::Error.color(cx); + move |this, delta| this.color(Color::Custom(color.opacity(delta))) + }, + ) + }; + let recording_indicator = h_flex() .h_4() .pr_1() @@ -2072,21 +2218,7 @@ impl Render for KeystrokeInput { .editor_background .blend(colors.text_accent.opacity(0.1))) .rounded_sm() - .child( - Icon::new(IconName::Circle) - .size(IconSize::Small) - .color(Color::Error) - .with_animation( - "recording-pulse", - Animation::new(std::time::Duration::from_secs(2)) - .repeat() - .with_easing(gpui::pulsating_between(0.4, 0.8)), - { - let color = Color::Error.color(cx); - move |this, delta| this.color(Color::Custom(color.opacity(delta))) - }, - ), - ) + .child(recording_pulse()) .child( Label::new("REC") .size(LabelSize::XSmall) @@ -2104,21 +2236,7 @@ impl Render for KeystrokeInput { .editor_background .blend(colors.text_accent.opacity(0.1))) .rounded_sm() - .child( - Icon::new(IconName::Circle) - .size(IconSize::Small) - .color(Color::Accent) - .with_animation( - "recording-pulse", - Animation::new(std::time::Duration::from_secs(2)) - .repeat() - .with_easing(gpui::pulsating_between(0.4, 0.8)), - { - let color = Color::Accent.color(cx); - move |this, delta| this.color(Color::Custom(color.opacity(delta))) - }, - ), - ) + .child(recording_pulse()) .child( Label::new("SEARCH") .size(LabelSize::XSmall) @@ -2156,13 +2274,9 @@ impl Render for KeystrokeInput { .when(is_focused, |parent| { parent.border_color(colors.border_focused) }) - .on_key_down(cx.listener(|this, event: &KeyDownEvent, window, cx| { - // TODO: replace with action - if !event.keystroke.modifiers.modified() && event.keystroke.key == "enter" { - window.focus(&this.inner_focus_handle); - cx.notify(); - } - })) + .key_context(Self::key_context()) + .on_action(cx.listener(Self::start_recording)) + .on_action(cx.listener(Self::stop_recording)) .child( h_flex() .w(horizontal_padding) @@ -2184,13 +2298,19 @@ impl Render for KeystrokeInput { .id("keystroke-input-inner") .track_focus(&self.inner_focus_handle) .on_modifiers_changed(cx.listener(Self::on_modifiers_changed)) - .on_key_up(cx.listener(Self::on_key_up)) .size_full() + .when(self.highlight_on_focus, |this| { + this.focus(|mut style| { + style.border_color = Some(colors.border_focused); + style + }) + }) + .w_full() .min_w_0() .justify_center() .flex_wrap() .gap(ui::DynamicSpacing::Base04.rems(cx)) - .children(self.render_keystrokes()), + .children(self.render_keystrokes(is_recording)), ) .child( h_flex() @@ -2204,15 +2324,18 @@ impl Render for KeystrokeInput { IconButton::new("stop-record-btn", IconName::StopFilled) .shape(ui::IconButtonShape::Square) .map(|this| { - if self.search { - this.tooltip(Tooltip::text("Stop Searching")) - } else { - this.tooltip(Tooltip::text("Stop Recording")) - } + this.tooltip(Tooltip::for_action_title( + if self.search { + "Stop Searching" + } else { + "Stop Recording" + }, + &StopRecording, + )) }) .icon_color(Color::Error) - .on_click(cx.listener(|this, _event, window, _cx| { - this.outer_focus_handle.focus(window); + .on_click(cx.listener(|this, _event, window, cx| { + this.stop_recording(&StopRecording, window, cx); })), ) } else { @@ -2220,15 +2343,18 @@ impl Render for KeystrokeInput { IconButton::new("record-btn", record_icon) .shape(ui::IconButtonShape::Square) .map(|this| { - if self.search { - this.tooltip(Tooltip::text("Start Searching")) - } else { - this.tooltip(Tooltip::text("Start Recording")) - } + this.tooltip(Tooltip::for_action_title( + if self.search { + "Start Searching" + } else { + "Start Recording" + }, + &StartRecording, + )) }) .when(!is_focused, |this| this.icon_color(Color::Muted)) - .on_click(cx.listener(|this, _event, window, _cx| { - this.inner_focus_handle.focus(window); + .on_click(cx.listener(|this, _event, window, cx| { + this.start_recording(&StartRecording, window, cx); })), ) } @@ -2236,14 +2362,15 @@ impl Render for KeystrokeInput { .child( IconButton::new("clear-btn", IconName::Delete) .shape(ui::IconButtonShape::Square) - .tooltip(Tooltip::text("Clear Keystrokes")) + .tooltip(Tooltip::for_action_title( + "Clear Keystrokes", + &ClearKeystrokes, + )) .when(!is_recording || !is_focused, |this| { this.icon_color(Color::Muted) }) - .on_click(cx.listener(|this, _event, _window, cx| { - this.keystrokes.clear(); - cx.emit(()); - cx.notify(); + .on_click(cx.listener(|this, _event, window, cx| { + this.clear_keystrokes(&ClearKeystrokes, window, cx); })), ), ); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index dc094a6c12fb1ba11642cc988f5d06d2cce01078..cc3906af4d957463f18e19a0e0756d21a2b1d022 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -4327,6 +4327,7 @@ mod tests { "jj", "journal", "keymap_editor", + "keystroke_input", "language_selector", "lsp_tool", "markdown", From b3747d9a216a6332754effd53c05ddac444f99d1 Mon Sep 17 00:00:00 2001 From: Finn Evers <finn@zed.dev> Date: Tue, 15 Jul 2025 18:52:21 +0200 Subject: [PATCH 084/658] keymap_ui: Add column for conflict indicator and edit button (#34423) This PR adds a column to the keymap editor to highlight warnings as well as add the possibility to click the edit icon there for editing the corresponding entry in the list. Release Notes: - N/A --------- Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Co-authored-by: Danilo Leal <daniloleal09@gmail.com> --- crates/settings_ui/src/keybindings.rs | 104 +++++++++++++++++++++----- 1 file changed, 87 insertions(+), 17 deletions(-) diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index bf9e72297f2c3807f182e02e0f3de92701949064..f246e9498c3503c9b95326a235b72708b51d24c3 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -22,8 +22,9 @@ use settings::{BaseKeymap, KeybindSource, KeymapFile, SettingsAssets}; use util::ResultExt; use ui::{ - ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, Modal, ModalFooter, ModalHeader, - ParentElement as _, Render, Section, SharedString, Styled as _, Tooltip, Window, prelude::*, + ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, IconButtonShape, Modal, + ModalFooter, ModalHeader, ParentElement as _, Render, Section, SharedString, Styled as _, + Tooltip, Window, prelude::*, }; use ui_input::SingleLineInput; use workspace::{ @@ -450,6 +451,13 @@ impl KeymapEditor { }) } + fn has_conflict(&self, row_index: usize) -> bool { + self.matches + .get(row_index) + .map(|candidate| candidate.candidate_id) + .is_some_and(|id| self.keybinding_conflict_state.has_conflict(&id)) + } + fn process_bindings( json_language: Arc<Language>, rust_language: Arc<Language>, @@ -847,8 +855,14 @@ impl KeymapEditor { _: &mut Window, cx: &mut Context<Self>, ) { - self.filter_state = self.filter_state.invert(); - self.update_matches(cx); + self.set_filter_state(self.filter_state.invert(), cx); + } + + fn set_filter_state(&mut self, filter_state: FilterState, cx: &mut Context<Self>) { + if self.filter_state != filter_state { + self.filter_state = filter_state; + self.update_matches(cx); + } } fn toggle_keystroke_search( @@ -1078,8 +1092,15 @@ impl Render for KeymapEditor { Table::new() .interactable(&self.table_interaction_state) .striped() - .column_widths([rems(16.), rems(16.), rems(16.), rems(32.), rems(8.)]) - .header(["Action", "Arguments", "Keystrokes", "Context", "Source"]) + .column_widths([ + rems(2.5), + rems(16.), + rems(16.), + rems(16.), + rems(32.), + rems(8.), + ]) + .header(["", "Action", "Arguments", "Keystrokes", "Context", "Source"]) .uniform_list( "keymap-editor-table", row_count, @@ -1091,6 +1112,49 @@ impl Render for KeymapEditor { let binding = &this.keybindings[candidate_id]; let action_name = binding.action_name.clone(); + let icon = (this.filter_state != FilterState::Conflicts + && this.has_conflict(index)) + .then(|| { + base_button_style(index, IconName::Warning) + .icon_color(Color::Warning) + .tooltip(|window, cx| { + Tooltip::with_meta( + "Edit Keybinding", + None, + "Use alt+click to show conflicts", + window, + cx, + ) + }) + .on_click(cx.listener( + move |this, click: &ClickEvent, window, cx| { + if click.modifiers().alt { + this.set_filter_state( + FilterState::Conflicts, + cx, + ); + } else { + this.select_index(index, cx); + this.open_edit_keybinding_modal( + false, window, cx, + ); + cx.stop_propagation(); + } + }, + )) + }) + .unwrap_or_else(|| { + base_button_style(index, IconName::Pencil) + .visible_on_hover(row_group_id(index)) + .tooltip(Tooltip::text("Edit Keybinding")) + .on_click(cx.listener(move |this, _, window, cx| { + this.select_index(index, cx); + this.open_edit_keybinding_modal(false, window, cx); + cx.stop_propagation(); + })) + }) + .into_any_element(); + let action = div() .id(("keymap action", index)) .child(command_palette::humanize_action_name(&action_name)) @@ -1148,32 +1212,26 @@ impl Render for KeymapEditor { .map(|(_source, name)| name) .unwrap_or_default() .into_any_element(); - Some([action, action_input, keystrokes, context, source]) + Some([icon, action, action_input, keystrokes, context, source]) }) .collect() }), ) .map_row( cx.processor(|this, (row_index, row): (usize, Div), _window, cx| { - let is_conflict = this - .matches - .get(row_index) - .map(|candidate| candidate.candidate_id) - .is_some_and(|id| this.keybinding_conflict_state.has_conflict(&id)); + let is_conflict = this.has_conflict(row_index); let is_selected = this.selected_index == Some(row_index); + let row_id = row_group_id(row_index); + let row = row - .id(("keymap-table-row", row_index)) + .id(row_id.clone()) .on_any_mouse_down(cx.listener( move |this, mouse_down_event: &gpui::MouseDownEvent, window, cx| { match mouse_down_event.button { - MouseButton::Left => { - this.select_index(row_index, cx); - } - MouseButton::Right => { this.select_index(row_index, cx); this.create_context_menu( @@ -1188,11 +1246,13 @@ impl Render for KeymapEditor { )) .on_click(cx.listener( move |this, event: &ClickEvent, window, cx| { + this.select_index(row_index, cx); if event.up.click_count == 2 { this.open_edit_keybinding_modal(false, window, cx); } }, )) + .group(row_id) .border_2() .when(is_conflict, |row| { row.bg(cx.theme().status().error_background) @@ -1225,6 +1285,16 @@ impl Render for KeymapEditor { } } +fn row_group_id(row_index: usize) -> SharedString { + SharedString::new(format!("keymap-table-row-{}", row_index)) +} + +fn base_button_style(row_index: usize, icon: IconName) -> IconButton { + IconButton::new(("keymap-icon", row_index), icon) + .shape(IconButtonShape::Square) + .size(ButtonSize::Compact) +} + #[derive(Debug, Clone, IntoElement)] struct SyntaxHighlightedText { text: SharedString, From f9561da673213ad24878460cfe22d984478243c7 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Tue, 15 Jul 2025 13:16:29 -0400 Subject: [PATCH 085/658] Maintain keymap editor position when deleting or modifying a binding (#34440) When a key binding is deleted we keep the exact same scroll bar position. When a keybinding is modified we select that keybinding in it's new position and scroll to it. I also changed save/modified keybinding to use fs.write istead of fs.atomic_write. Atomic write was creating two FS events that some scrollbar bugs when refreshing the keymap editor. Co-authored-by: Ben \<ben@zed.dev\> Release Notes: - N/A --- crates/settings_ui/src/keybindings.rs | 136 +++++++++++++++--- crates/settings_ui/src/ui_components/table.rs | 26 +++- 2 files changed, 137 insertions(+), 25 deletions(-) diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index f246e9498c3503c9b95326a235b72708b51d24c3..46a428038cce3e5b8498ec6215f764912aa44b47 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -10,9 +10,9 @@ use feature_flags::FeatureFlagViewExt; use fs::Fs; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - Action, Animation, AnimationExt, AppContext as _, AsyncApp, ClickEvent, Context, DismissEvent, - Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Global, IsZero, KeyContext, - Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, Point, ScrollStrategy, + Action, Animation, AnimationExt, AppContext as _, AsyncApp, Axis, ClickEvent, Context, + DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Global, IsZero, + KeyContext, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, Point, ScrollStrategy, ScrollWheelEvent, StyledText, Subscription, WeakEntity, actions, anchored, deferred, div, }; use language::{Language, LanguageConfig, ToOffset as _}; @@ -282,6 +282,25 @@ struct KeymapEditor { keystroke_editor: Entity<KeystrokeInput>, selected_index: Option<usize>, context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>, + previous_edit: Option<PreviousEdit>, +} + +enum PreviousEdit { + /// When deleting, we want to maintain the same scroll position + ScrollBarOffset(Point<Pixels>), + /// When editing or creating, because the new keybinding could be in a different position in the sort order + /// we store metadata about the new binding (either the modified version or newly created one) + /// and upon reload, we search for this binding in the list of keybindings, and if we find the one that matches + /// this metadata, we set the selected index to it and scroll to it, + /// and if we don't find it, we scroll to 0 and don't set a selected index + Keybinding { + action_mapping: ActionMapping, + action_name: SharedString, + /// The scrollbar position to fallback to if we don't find the keybinding during a refresh + /// this can happen if there's a filter applied to the search and the keybinding modification + /// filters the binding from the search results + fallback: Point<Pixels>, + }, } impl EventEmitter<()> for KeymapEditor {} @@ -294,8 +313,7 @@ impl Focusable for KeymapEditor { impl KeymapEditor { fn new(workspace: WeakEntity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self { - let _keymap_subscription = - cx.observe_global::<KeymapEventChannel>(Self::update_keybindings); + let _keymap_subscription = cx.observe_global::<KeymapEventChannel>(Self::on_keymap_changed); let table_interaction_state = TableInteractionState::new(window, cx); let keystroke_editor = cx.new(|cx| { @@ -315,7 +333,7 @@ impl KeymapEditor { return; } - this.update_matches(cx); + this.on_query_changed(cx); }) .detach(); @@ -324,7 +342,7 @@ impl KeymapEditor { return; } - this.update_matches(cx); + this.on_query_changed(cx); }) .detach(); @@ -343,9 +361,10 @@ impl KeymapEditor { keystroke_editor, selected_index: None, context_menu: None, + previous_edit: None, }; - this.update_keybindings(cx); + this.on_keymap_changed(cx); this } @@ -367,17 +386,20 @@ impl KeymapEditor { } } - fn update_matches(&self, cx: &mut Context<Self>) { + fn on_query_changed(&self, cx: &mut Context<Self>) { let action_query = self.current_action_query(cx); let keystroke_query = self.current_keystroke_query(cx); cx.spawn(async move |this, cx| { - Self::process_query(this, action_query, keystroke_query, cx).await + Self::update_matches(this.clone(), action_query, keystroke_query, cx).await?; + this.update(cx, |this, cx| { + this.scroll_to_item(0, ScrollStrategy::Top, cx) + }) }) .detach(); } - async fn process_query( + async fn update_matches( this: WeakEntity<Self>, action_query: String, keystroke_query: Vec<Keystroke>, @@ -445,7 +467,6 @@ impl KeymapEditor { }); } this.selected_index.take(); - this.scroll_to_item(0, ScrollStrategy::Top, cx); this.matches = matches; cx.notify(); }) @@ -539,7 +560,7 @@ impl KeymapEditor { (processed_bindings, string_match_candidates) } - fn update_keybindings(&mut self, cx: &mut Context<KeymapEditor>) { + fn on_keymap_changed(&mut self, cx: &mut Context<KeymapEditor>) { let workspace = self.workspace.clone(); cx.spawn(async move |this, cx| { let json_language = load_json_language(workspace.clone(), cx).await; @@ -574,7 +595,47 @@ impl KeymapEditor { ) })?; // calls cx.notify - Self::process_query(this, action_query, keystroke_query, cx).await + Self::update_matches(this.clone(), action_query, keystroke_query, cx).await?; + this.update(cx, |this, cx| { + if let Some(previous_edit) = this.previous_edit.take() { + match previous_edit { + // should remove scroll from process_query + PreviousEdit::ScrollBarOffset(offset) => { + this.table_interaction_state.update(cx, |table, _| { + table.set_scrollbar_offset(Axis::Vertical, offset) + }) + // set selected index and scroll + } + PreviousEdit::Keybinding { + action_mapping, + action_name, + fallback, + } => { + let scroll_position = + this.matches.iter().enumerate().find_map(|(index, item)| { + let binding = &this.keybindings[item.candidate_id]; + if binding.get_action_mapping() == action_mapping + && binding.action_name == action_name + { + Some(index) + } else { + None + } + }); + + if let Some(scroll_position) = scroll_position { + this.scroll_to_item(scroll_position, ScrollStrategy::Top, cx); + this.selected_index = Some(scroll_position); + } else { + this.table_interaction_state.update(cx, |table, _| { + table.set_scrollbar_offset(Axis::Vertical, fallback) + }); + } + cx.notify(); + } + } + } + }) }) .detach_and_log_err(cx); } @@ -806,6 +867,7 @@ impl KeymapEditor { let Some(to_remove) = self.selected_binding().cloned() else { return; }; + let Ok(fs) = self .workspace .read_with(cx, |workspace, _| workspace.app_state().fs.clone()) @@ -813,6 +875,11 @@ impl KeymapEditor { return; }; let tab_size = cx.global::<settings::SettingsStore>().json_tab_size(); + self.previous_edit = Some(PreviousEdit::ScrollBarOffset( + self.table_interaction_state + .read(cx) + .get_scrollbar_offset(Axis::Vertical), + )); cx.spawn(async move |_, _| remove_keybinding(to_remove, &fs, tab_size).await) .detach_and_notify_err(window, cx); } @@ -861,7 +928,7 @@ impl KeymapEditor { fn set_filter_state(&mut self, filter_state: FilterState, cx: &mut Context<Self>) { if self.filter_state != filter_state { self.filter_state = filter_state; - self.update_matches(cx); + self.on_query_changed(cx); } } @@ -872,7 +939,7 @@ impl KeymapEditor { cx: &mut Context<Self>, ) { self.search_mode = self.search_mode.invert(); - self.update_matches(cx); + self.on_query_changed(cx); // Update the keystroke editor to turn the `search` bool on self.keystroke_editor.update(cx, |keystroke_editor, cx| { @@ -1623,6 +1690,8 @@ impl KeybindingEditorModal { .log_err(); cx.spawn(async move |this, cx| { + let action_name = existing_keybind.action_name.clone(); + if let Err(err) = save_keybinding_update( create, existing_keybind, @@ -1639,7 +1708,22 @@ impl KeybindingEditorModal { }) .log_err(); } else { - this.update(cx, |_this, cx| { + this.update(cx, |this, cx| { + let action_mapping = ( + ui::text_for_keystrokes(new_keystrokes.as_slice(), cx).into(), + new_context.map(SharedString::from), + ); + + 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(); @@ -1917,9 +2001,12 @@ async fn save_keybinding_update( let updated_keymap_contents = settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size) .context("Failed to update keybinding")?; - fs.atomic_write(paths::keymap_file().clone(), updated_keymap_contents) - .await - .context("Failed to write keymap file")?; + fs.write( + paths::keymap_file().as_path(), + updated_keymap_contents.as_bytes(), + ) + .await + .context("Failed to write keymap file")?; Ok(()) } @@ -1959,9 +2046,12 @@ async fn remove_keybinding( let updated_keymap_contents = settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size) .context("Failed to update keybinding")?; - fs.atomic_write(paths::keymap_file().clone(), updated_keymap_contents) - .await - .context("Failed to write keymap file")?; + fs.write( + paths::keymap_file().as_path(), + updated_keymap_contents.as_bytes(), + ) + .await + .context("Failed to write keymap file")?; Ok(()) } diff --git a/crates/settings_ui/src/ui_components/table.rs b/crates/settings_ui/src/ui_components/table.rs index c3b70d7d4f166ff3b34cd2b52146e4dc7408badc..98dd7387659a0ed9afdff8a7b4280c2a03685174 100644 --- a/crates/settings_ui/src/ui_components/table.rs +++ b/crates/settings_ui/src/ui_components/table.rs @@ -3,8 +3,8 @@ use std::{ops::Range, rc::Rc, time::Duration}; use editor::{EditorSettings, ShowScrollbar, scroll::ScrollbarAutoHide}; use gpui::{ AppContext, Axis, Context, Entity, FocusHandle, Length, ListHorizontalSizingBehavior, - ListSizingBehavior, MouseButton, Task, UniformListScrollHandle, WeakEntity, transparent_black, - uniform_list, + ListSizingBehavior, MouseButton, Point, Task, UniformListScrollHandle, WeakEntity, + transparent_black, uniform_list, }; use settings::Settings as _; use ui::{ @@ -90,6 +90,28 @@ impl TableInteractionState { }) } + pub fn get_scrollbar_offset(&self, axis: Axis) -> Point<Pixels> { + match axis { + Axis::Vertical => self.vertical_scrollbar.state.scroll_handle().offset(), + Axis::Horizontal => self.horizontal_scrollbar.state.scroll_handle().offset(), + } + } + + pub fn set_scrollbar_offset(&self, axis: Axis, offset: Point<Pixels>) { + match axis { + Axis::Vertical => self + .vertical_scrollbar + .state + .scroll_handle() + .set_offset(offset), + Axis::Horizontal => self + .horizontal_scrollbar + .state + .scroll_handle() + .set_offset(offset), + } + } + fn update_scrollbar_visibility(&mut self, cx: &mut Context<Self>) { let show_setting = EditorSettings::get_global(cx).scrollbar.show; From 3ecdfc9b5adbe0932cc780fb1183f2de1eea911b Mon Sep 17 00:00:00 2001 From: Cole Miller <cole@zed.dev> Date: Tue, 15 Jul 2025 13:36:09 -0400 Subject: [PATCH 086/658] Remove auto-width editor type (#34438) Closes #34044 `EditorMode::SingleLine { auto_width: true }` was only used for the title editor in the rules library, and following https://github.com/zed-industries/zed/pull/31994 we can replace that with a normal single-line editor without problems. The auto-width editor was interacting badly with the recently-added newline visualization code, causing a panic during layout---by switching it to `Editor::single_line` the newline visualization works there too. Release Notes: - Fixed a panic that could occur when opening the rules library. --------- Co-authored-by: Finn <finn@zed.dev> --- crates/editor/src/editor.rs | 24 +----------- crates/editor/src/element.rs | 45 +++-------------------- crates/rules_library/src/rules_library.rs | 2 +- 3 files changed, 9 insertions(+), 62 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 72470c0a7d13cc75f83102d2c61c23c478063053..acd9c23c97682f648fe3389e8c0466be4d7b9667 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -482,9 +482,7 @@ pub enum SelectMode { #[derive(Clone, PartialEq, Eq, Debug)] pub enum EditorMode { - SingleLine { - auto_width: bool, - }, + SingleLine, AutoHeight { min_lines: usize, max_lines: Option<usize>, @@ -1662,13 +1660,7 @@ impl Editor { pub fn single_line(window: &mut Window, cx: &mut Context<Self>) -> Self { let buffer = cx.new(|cx| Buffer::local("", cx)); let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); - Self::new( - EditorMode::SingleLine { auto_width: false }, - buffer, - None, - window, - cx, - ) + Self::new(EditorMode::SingleLine, buffer, None, window, cx) } pub fn multi_line(window: &mut Window, cx: &mut Context<Self>) -> Self { @@ -1677,18 +1669,6 @@ impl Editor { Self::new(EditorMode::full(), buffer, None, window, cx) } - pub fn auto_width(window: &mut Window, cx: &mut Context<Self>) -> Self { - let buffer = cx.new(|cx| Buffer::local("", cx)); - let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); - Self::new( - EditorMode::SingleLine { auto_width: true }, - buffer, - None, - window, - cx, - ) - } - pub fn auto_height( min_lines: usize, max_lines: usize, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 06fb52cdb3a63baa16f932ae0062b290016657ef..e77be3398ca0fcef9edf65a0a318f94bd21a4fc8 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -7787,46 +7787,13 @@ impl Element for EditorElement { editor.set_style(self.style.clone(), window, cx); let layout_id = match editor.mode { - EditorMode::SingleLine { auto_width } => { + EditorMode::SingleLine => { let rem_size = window.rem_size(); - let height = self.style.text.line_height_in_pixels(rem_size); - if auto_width { - let editor_handle = cx.entity().clone(); - let style = self.style.clone(); - window.request_measured_layout( - Style::default(), - move |_, _, window, cx| { - let editor_snapshot = editor_handle - .update(cx, |editor, cx| editor.snapshot(window, cx)); - let line = Self::layout_lines( - DisplayRow(0)..DisplayRow(1), - &editor_snapshot, - &style, - px(f32::MAX), - |_| false, // Single lines never soft wrap - window, - cx, - ) - .pop() - .unwrap(); - - let font_id = - window.text_system().resolve_font(&style.text.font()); - let font_size = - style.text.font_size.to_pixels(window.rem_size()); - let em_width = - window.text_system().em_width(font_id, font_size).unwrap(); - - size(line.width + em_width, height) - }, - ) - } else { - let mut style = Style::default(); - style.size.height = height.into(); - style.size.width = relative(1.).into(); - window.request_layout(style, None, cx) - } + let mut style = Style::default(); + style.size.height = height.into(); + style.size.width = relative(1.).into(); + window.request_layout(style, None, cx) } EditorMode::AutoHeight { min_lines, @@ -10390,7 +10357,7 @@ mod tests { }); for editor_mode_without_invisibles in [ - EditorMode::SingleLine { auto_width: false }, + EditorMode::SingleLine, EditorMode::AutoHeight { min_lines: 1, max_lines: Some(100), diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs index f871416f391d844d324ee3a11d9c41465ea0dccd..be6a69c23bef20571fbdb54854f768c46f3678b7 100644 --- a/crates/rules_library/src/rules_library.rs +++ b/crates/rules_library/src/rules_library.rs @@ -611,7 +611,7 @@ impl RulesLibrary { this.update_in(cx, |this, window, cx| match rule { Ok(rule) => { let title_editor = cx.new(|cx| { - let mut editor = Editor::auto_width(window, cx); + let mut editor = Editor::single_line(window, cx); editor.set_placeholder_text("Untitled", cx); editor.set_text(rule_metadata.title.unwrap_or_default(), window, cx); if prompt_id.is_built_in() { From ebbf02e25b94b05e641ffe420a077ad3768d8107 Mon Sep 17 00:00:00 2001 From: Ben Kunkle <ben@zed.dev> Date: Tue, 15 Jul 2025 13:03:19 -0500 Subject: [PATCH 087/658] keymap_ui: Keyboard navigation for keybind edit modal (#34482) Adds keyboard navigation to the keybind edit modal. Using up/down arrows to select the previous/next input editor, and `cmd-enter` to save + `escape` to exit Release Notes: - N/A *or* Added/Fixed/Improved ... --- assets/keymaps/default-linux.json | 16 ++ assets/keymaps/default-macos.json | 16 ++ crates/settings_ui/src/keybindings.rs | 293 +++++++++++++++++--------- 3 files changed, 228 insertions(+), 97 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 562afea85454995a1e32ef46bf82fb46220b8e47..9ca7d8589a29b988a181f69f65310220d380d7e4 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -1129,5 +1129,21 @@ "escape escape escape": "keystroke_input::StopRecording", "delete": "keystroke_input::ClearKeystrokes" } + }, + { + "context": "KeybindEditorModal", + "use_key_equivalents": true, + "bindings": { + "ctrl-enter": "menu::Confirm", + "escape": "menu::Cancel" + } + }, + { + "context": "KeybindEditorModal > Editor", + "use_key_equivalents": true, + "bindings": { + "up": "menu::SelectPrevious", + "down": "menu::SelectNext" + } } ] diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index fa9fce4555319f62ff1e3ed359e5165acdcbdb49..7af79bdeea1b461e6b0f6fb665ccc9f8cef2138f 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -1226,5 +1226,21 @@ "escape escape escape": "keystroke_input::StopRecording", "delete": "keystroke_input::ClearKeystrokes" } + }, + { + "context": "KeybindEditorModal", + "use_key_equivalents": true, + "bindings": { + "cmd-enter": "menu::Confirm", + "escape": "menu::Cancel" + } + }, + { + "context": "KeybindEditorModal > Editor", + "use_key_equivalents": true, + "bindings": { + "up": "menu::SelectPrevious", + "down": "menu::SelectNext" + } } ] diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 46a428038cce3e5b8498ec6215f764912aa44b47..3567439d2b3c85202d5fe0ceea5a481bab6be482 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -1451,6 +1451,7 @@ struct KeybindingEditorModal { error: Option<InputError>, keymap_editor: Entity<KeymapEditor>, workspace: WeakEntity<Workspace>, + focus_state: KeybindingEditorModalFocusState, } impl ModalView for KeybindingEditorModal {} @@ -1539,6 +1540,14 @@ impl KeybindingEditorModal { }) }); + let focus_state = KeybindingEditorModalFocusState::new( + keybind_editor.read_with(cx, |keybind_editor, cx| keybind_editor.focus_handle(cx)), + input_editor.as_ref().map(|input_editor| { + input_editor.read_with(cx, |input_editor, cx| input_editor.focus_handle(cx)) + }), + context_editor.read_with(cx, |context_editor, cx| context_editor.focus_handle(cx)), + ); + Self { creating: create, editing_keybind, @@ -1550,6 +1559,7 @@ impl KeybindingEditorModal { error: None, keymap_editor, workspace, + focus_state, } } @@ -1731,6 +1741,33 @@ impl KeybindingEditorModal { }) .detach(); } + + fn key_context(&self) -> KeyContext { + let mut key_context = KeyContext::new_with_defaults(); + key_context.add("KeybindEditorModal"); + key_context + } + + fn focus_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) { + self.focus_state.focus_next(window, cx); + } + + fn focus_prev( + &mut self, + _: &menu::SelectPrevious, + window: &mut Window, + cx: &mut Context<Self>, + ) { + self.focus_state.focus_previous(window, cx); + } + + fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) { + self.save(cx); + } + + fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context<Self>) { + cx.emit(DismissEvent) + } } impl Render for KeybindingEditorModal { @@ -1739,93 +1776,156 @@ impl Render for KeybindingEditorModal { let action_name = command_palette::humanize_action_name(&self.editing_keybind.action_name).to_string(); - v_flex().w(rems(34.)).elevation_3(cx).child( - Modal::new("keybinding_editor_modal", None) - .header( - ModalHeader::new().child( - v_flex() - .pb_1p5() - .mb_1() - .gap_0p5() - .border_b_1() - .border_color(theme.border_variant) - .child(Label::new(action_name)) - .when_some(self.editing_keybind.action_docs, |this, docs| { - this.child( - Label::new(docs).size(LabelSize::Small).color(Color::Muted), - ) - }), - ), - ) - .section( - Section::new().child( - v_flex() - .gap_2() - .child( - v_flex() - .child(Label::new("Edit Keystroke")) - .gap_1() - .child(self.keybind_editor.clone()), - ) - .when_some(self.input_editor.clone(), |this, editor| { - this.child( + v_flex() + .w(rems(34.)) + .elevation_3(cx) + .key_context(self.key_context()) + .on_action(cx.listener(Self::focus_next)) + .on_action(cx.listener(Self::focus_prev)) + .on_action(cx.listener(Self::confirm)) + .on_action(cx.listener(Self::cancel)) + .child( + Modal::new("keybinding_editor_modal", None) + .header( + ModalHeader::new().child( + v_flex() + .pb_1p5() + .mb_1() + .gap_0p5() + .border_b_1() + .border_color(theme.border_variant) + .child(Label::new(action_name)) + .when_some(self.editing_keybind.action_docs, |this, docs| { + this.child( + Label::new(docs).size(LabelSize::Small).color(Color::Muted), + ) + }), + ), + ) + .section( + Section::new().child( + v_flex() + .gap_2() + .child( v_flex() - .mt_1p5() + .child(Label::new("Edit Keystroke")) .gap_1() - .child(Label::new("Edit Arguments")) - .child( - div() - .w_full() - .py_1() - .px_1p5() - .rounded_lg() - .bg(theme.editor_background) - .border_1() - .border_color(theme.border_variant) - .child(editor), - ), + .child(self.keybind_editor.clone()), ) - }) - .child(self.context_editor.clone()) - .when_some(self.error.as_ref(), |this, error| { - this.child( - Banner::new() - .map(|banner| match error { - InputError::Error(_) => { - banner.severity(ui::Severity::Error) - } - InputError::Warning(_) => { - banner.severity(ui::Severity::Warning) - } - }) - // 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())), - ), + .when_some(self.input_editor.clone(), |this, editor| { + this.child( + v_flex() + .mt_1p5() + .gap_1() + .child(Label::new("Edit Arguments")) + .child( + div() + .w_full() + .py_1() + .px_1p5() + .rounded_lg() + .bg(theme.editor_background) + .border_1() + .border_color(theme.border_variant) + .child(editor), + ), + ) + }) + .child(self.context_editor.clone()) + .when_some(self.error.as_ref(), |this, error| { + this.child( + Banner::new() + .map(|banner| match error { + InputError::Error(_) => { + banner.severity(ui::Severity::Error) + } + InputError::Warning(_) => { + banner.severity(ui::Severity::Warning) + } + }) + // 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())), + ), + ) + }), + ), + ) + .footer( + ModalFooter::new().end_slot( + h_flex() + .gap_1() + .child( + Button::new("cancel", "Cancel") + .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))), ) - }), - ), - ) - .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(cx); - }, - ))), + .child(Button::new("save-btn", "Save").on_click(cx.listener( + |this, _event, _window, cx| { + this.save(cx); + }, + ))), + ), ), - ), - ) + ) + } +} + +struct KeybindingEditorModalFocusState { + handles: Vec<FocusHandle>, +} + +impl KeybindingEditorModalFocusState { + fn new( + keystrokes: FocusHandle, + action_input: Option<FocusHandle>, + context: FocusHandle, + ) -> Self { + Self { + handles: Vec::from_iter( + [Some(keystrokes), action_input, Some(context)] + .into_iter() + .flatten(), + ), + } + } + + fn focused_index(&self, window: &Window, cx: &App) -> Option<i32> { + self.handles + .iter() + .position(|handle| handle.contains_focused(window, cx)) + .map(|i| i as i32) + } + + fn focus_index(&self, mut index: i32, window: &mut Window) { + if index < 0 { + index = self.handles.len() as i32 - 1; + } + if index >= self.handles.len() as i32 { + index = 0; + } + window.focus(&self.handles[index as usize]); + } + + fn focus_next(&self, window: &mut Window, cx: &App) { + let index_to_focus = if let Some(index) = self.focused_index(window, cx) { + index + 1 + } else { + 0 + }; + self.focus_index(index_to_focus, window); + } + + fn focus_previous(&self, window: &mut Window, cx: &App) { + let index_to_focus = if let Some(index) = self.focused_index(window, cx) { + index - 1 + } else { + self.handles.len() as i32 - 1 + }; + self.focus_index(index_to_focus, window); } } @@ -2207,24 +2307,23 @@ impl KeystrokeInput { cx: &mut Context<Self>, ) { let close_keystroke_result = self.handle_possible_close_keystroke(keystroke, window, cx); - if close_keystroke_result == CloseKeystrokeResult::Close { - return; - } - if let Some(last) = self.keystrokes.last() - && last.key.is_empty() - && self.keystrokes.len() <= Self::KEYSTROKE_COUNT_MAX - { - self.keystrokes.pop(); - } - if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX { - if close_keystroke_result == CloseKeystrokeResult::Partial - && self.close_keystrokes_start.is_none() + if close_keystroke_result != CloseKeystrokeResult::Close { + if let Some(last) = self.keystrokes.last() + && last.key.is_empty() + && self.keystrokes.len() <= Self::KEYSTROKE_COUNT_MAX { - self.close_keystrokes_start = Some(self.keystrokes.len()); + self.keystrokes.pop(); } - self.keystrokes.push(keystroke.clone()); if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX { - self.keystrokes.push(Self::dummy(keystroke.modifiers)); + if close_keystroke_result == CloseKeystrokeResult::Partial + && self.close_keystrokes_start.is_none() + { + self.close_keystrokes_start = Some(self.keystrokes.len()); + } + self.keystrokes.push(keystroke.clone()); + if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX { + self.keystrokes.push(Self::dummy(keystroke.modifiers)); + } } } self.keystrokes_changed(cx); From 729cde33f14b595856703603d016a75b1387dc91 Mon Sep 17 00:00:00 2001 From: Smit Barmase <heysmitbarmase@gmail.com> Date: Tue, 15 Jul 2025 23:41:53 +0530 Subject: [PATCH 088/658] project_panel: Add rename, delete and duplicate actions to workspace (#34478) Release Notes: - Added `project panel: rename`, `project panel: delete` and `project panel: duplicate` actions to workspace. Co-authored-by: Danilo Leal <daniloleal09@gmail.com> --- crates/project_panel/src/project_panel.rs | 27 +++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index e1d360cd976386d2e24e63b9eda05afe123dc411..b6fdcd6fa5bac837df5cab8aad3b9c69cd1613d8 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -320,6 +320,33 @@ pub fn init(cx: &mut App) { }); } }); + + workspace.register_action(|workspace, action: &Rename, window, cx| { + if let Some(panel) = workspace.panel::<ProjectPanel>(cx) { + panel.update(cx, |panel, cx| { + if let Some(first_marked) = panel.marked_entries.first() { + let first_marked = *first_marked; + panel.marked_entries.clear(); + panel.selection = Some(first_marked); + } + panel.rename(action, window, cx); + }); + } + }); + + workspace.register_action(|workspace, action: &Duplicate, window, cx| { + if let Some(panel) = workspace.panel::<ProjectPanel>(cx) { + panel.update(cx, |panel, cx| { + panel.duplicate(action, window, cx); + }); + } + }); + + workspace.register_action(|workspace, action: &Delete, window, cx| { + if let Some(panel) = workspace.panel::<ProjectPanel>(cx) { + panel.update(cx, |panel, cx| panel.delete(action, window, cx)); + } + }); }) .detach(); } From 57e8f5c5b9878c3a33e0e1b3452bb8a078ec794b Mon Sep 17 00:00:00 2001 From: Richard Feldman <oss@rtfeldman.com> Date: Tue, 15 Jul 2025 14:22:13 -0400 Subject: [PATCH 089/658] Automatically retry in more situations (#34473) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In #33275 I was very conservative about when to retry when there are errors in language completions in the Agent panel. Now we retry in more scenarios (e.g. HTTP 5xx and 4xx errors that aren't in the specific list of ones that we handle differently, such as 429s), and also we show a notification if the thread halts for any reason. <img width="441" height="68" alt="Screenshot 2025-07-15 at 12 51 30 PM" src="https://github.com/user-attachments/assets/433775d0-a8b2-403d-9427-1e296d164980" /> <img width="482" height="322" alt="Screenshot 2025-07-15 at 12 44 15 PM" src="https://github.com/user-attachments/assets/5a508224-0fe0-4d34-9768-25d95873eab8" /> Release Notes: - Automatic retry for more Agent errors - Whenever the Agent stops, play a sound (if configured) and show a notification (if configured) if the Zed window was in the background. --- crates/agent/src/thread.rs | 425 ++++++++++----------------- crates/agent_ui/src/active_thread.rs | 81 +++-- crates/agent_ui/src/agent_diff.rs | 1 - crates/eval/src/example.rs | 3 - 4 files changed, 215 insertions(+), 295 deletions(-) diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 6a20ad8f83dd984c74a001fb86ccd564b110ce24..8e66e526deedc6db1bec7912f48008bd6b36782c 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -21,6 +21,7 @@ use gpui::{ AnyWindowHandle, App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity, Window, }; +use http_client::StatusCode; use language_model::{ ConfiguredModel, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelExt as _, LanguageModelId, LanguageModelRegistry, LanguageModelRequest, @@ -51,7 +52,19 @@ use uuid::Uuid; use zed_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit}; const MAX_RETRY_ATTEMPTS: u8 = 3; -const BASE_RETRY_DELAY_SECS: u64 = 5; +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, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize, JsonSchema, @@ -1933,18 +1946,6 @@ impl Thread { project.set_agent_location(None, cx); }); - fn emit_generic_error(error: &anyhow::Error, cx: &mut Context<Thread>) { - let error_message = error - .chain() - .map(|err| err.to_string()) - .collect::<Vec<_>>() - .join("\n"); - cx.emit(ThreadEvent::ShowError(ThreadError::Message { - header: "Error interacting with language model".into(), - message: SharedString::from(error_message.clone()), - })); - } - if error.is::<PaymentRequiredError>() { cx.emit(ThreadEvent::ShowError(ThreadError::PaymentRequired)); } else if let Some(error) = @@ -1956,9 +1957,10 @@ impl Thread { } else if let Some(completion_error) = error.downcast_ref::<LanguageModelCompletionError>() { - use LanguageModelCompletionError::*; match &completion_error { - PromptTooLarge { tokens, .. } => { + LanguageModelCompletionError::PromptTooLarge { + tokens, .. + } => { let tokens = tokens.unwrap_or_else(|| { // We didn't get an exact token count from the API, so fall back on our estimate. thread @@ -1979,63 +1981,22 @@ impl Thread { }); cx.notify(); } - RateLimitExceeded { - retry_after: Some(retry_after), - .. - } - | ServerOverloaded { - retry_after: Some(retry_after), - .. - } => { - thread.handle_rate_limit_error( - &completion_error, - *retry_after, - model.clone(), - intent, - window, - cx, - ); - retry_scheduled = true; - } - RateLimitExceeded { .. } | ServerOverloaded { .. } => { - retry_scheduled = thread.handle_retryable_error( - &completion_error, - model.clone(), - intent, - window, - cx, - ); - if !retry_scheduled { - emit_generic_error(error, cx); - } - } - ApiInternalServerError { .. } - | ApiReadResponseError { .. } - | HttpSend { .. } => { - retry_scheduled = thread.handle_retryable_error( - &completion_error, - model.clone(), - intent, - window, - cx, - ); - if !retry_scheduled { - emit_generic_error(error, cx); + _ => { + if let Some(retry_strategy) = + Thread::get_retry_strategy(completion_error) + { + retry_scheduled = thread + .handle_retryable_error_with_delay( + &completion_error, + Some(retry_strategy), + model.clone(), + intent, + window, + cx, + ); } } - NoApiKey { .. } - | HttpResponseError { .. } - | BadRequestFormat { .. } - | AuthenticationError { .. } - | PermissionError { .. } - | ApiEndpointNotFound { .. } - | SerializeRequest { .. } - | BuildRequestBody { .. } - | DeserializeResponse { .. } - | Other { .. } => emit_generic_error(error, cx), } - } else { - emit_generic_error(error, cx); } if !retry_scheduled { @@ -2162,73 +2123,86 @@ impl Thread { }); } - fn handle_rate_limit_error( - &mut self, - error: &LanguageModelCompletionError, - retry_after: Duration, - model: Arc<dyn LanguageModel>, - intent: CompletionIntent, - window: Option<AnyWindowHandle>, - cx: &mut Context<Self>, - ) { - // For rate limit errors, we only retry once with the specified duration - let retry_message = format!("{error}. Retrying in {} seconds…", retry_after.as_secs()); - log::warn!( - "Retrying completion request in {} seconds: {error:?}", - retry_after.as_secs(), - ); - - // Add a UI-only message instead of a regular message - let id = self.next_message_id.post_inc(); - self.messages.push(Message { - id, - role: Role::System, - segments: vec![MessageSegment::Text(retry_message)], - loaded_context: LoadedContext::default(), - creases: Vec::new(), - is_hidden: false, - ui_only: true, - }); - cx.emit(ThreadEvent::MessageAdded(id)); - // Schedule the retry - let thread_handle = cx.entity().downgrade(); - - cx.spawn(async move |_thread, cx| { - cx.background_executor().timer(retry_after).await; + fn get_retry_strategy(error: &LanguageModelCompletionError) -> Option<RetryStrategy> { + use LanguageModelCompletionError::*; - thread_handle - .update(cx, |thread, cx| { - // Retry the completion - thread.send_to_model(model, intent, window, cx); + // 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), try multiple times with exponential backoff. + // - If it's an issue that *might* be fixed by retrying (e.g. internal server error), just retry once. + 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, }) - .log_err(); - }) - .detach(); - } - - fn handle_retryable_error( - &mut self, - error: &LanguageModelCompletionError, - model: Arc<dyn LanguageModel>, - intent: CompletionIntent, - window: Option<AnyWindowHandle>, - cx: &mut Context<Self>, - ) -> bool { - self.handle_retryable_error_with_delay(error, None, model, intent, window, cx) + } + ApiInternalServerError { .. } => Some(RetryStrategy::Fixed { + delay: BASE_RETRY_DELAY, + max_attempts: 1, + }), + ApiReadResponseError { .. } + | HttpSend { .. } + | DeserializeResponse { .. } + | BadRequestFormat { .. } => Some(RetryStrategy::Fixed { + delay: BASE_RETRY_DELAY, + max_attempts: 1, + }), + // Retrying these errors definitely shouldn't help. + HttpResponseError { + status_code: + StatusCode::PAYLOAD_TOO_LARGE | StatusCode::FORBIDDEN | StatusCode::UNAUTHORIZED, + .. + } + | SerializeRequest { .. } + | BuildRequestBody { .. } + | PromptTooLarge { .. } + | AuthenticationError { .. } + | PermissionError { .. } + | ApiEndpointNotFound { .. } + | NoApiKey { .. } => None, + // 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: 1, + }) + } + // Conservatively assume that any other errors are non-retryable + HttpResponseError { .. } | Other(..) => None, + } } fn handle_retryable_error_with_delay( &mut self, error: &LanguageModelCompletionError, - custom_delay: Option<Duration>, + strategy: Option<RetryStrategy>, model: Arc<dyn LanguageModel>, intent: CompletionIntent, window: Option<AnyWindowHandle>, cx: &mut Context<Self>, ) -> bool { + let Some(strategy) = strategy.or_else(|| Self::get_retry_strategy(error)) else { + return false; + }; + + let max_attempts = match &strategy { + RetryStrategy::ExponentialBackoff { max_attempts, .. } => *max_attempts, + RetryStrategy::Fixed { max_attempts, .. } => *max_attempts, + }; + let retry_state = self.retry_state.get_or_insert(RetryState { attempt: 0, - max_attempts: MAX_RETRY_ATTEMPTS, + max_attempts, intent, }); @@ -2238,20 +2212,24 @@ impl Thread { let intent = retry_state.intent; if attempt <= max_attempts { - // Use custom delay if provided (e.g., from rate limit), otherwise exponential backoff - let delay = if let Some(custom_delay) = custom_delay { - custom_delay - } else { - let delay_secs = BASE_RETRY_DELAY_SECS * 2u64.pow((attempt - 1) as u32); - Duration::from_secs(delay_secs) + 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, }; // Add a transient message to inform the user let delay_secs = delay.as_secs(); - let retry_message = format!( - "{error}. Retrying (attempt {attempt} of {max_attempts}) \ - in {delay_secs} seconds..." - ); + let retry_message = if max_attempts == 1 { + format!("{error}. Retrying in {delay_secs} seconds...") + } else { + format!( + "{error}. Retrying (attempt {attempt} of {max_attempts}) \ + in {delay_secs} seconds..." + ) + }; log::warn!( "Retrying completion request (attempt {attempt} of {max_attempts}) \ in {delay_secs} seconds: {error:?}", @@ -2290,19 +2268,9 @@ impl Thread { // Max retries exceeded self.retry_state = None; - let notification_text = if max_attempts == 1 { - "Failed after retrying.".into() - } else { - format!("Failed after retrying {} times.", max_attempts).into() - }; - // Stop generating since we're giving up on retrying. self.pending_completions.clear(); - cx.emit(ThreadEvent::RetriesFailed { - message: notification_text, - }); - false } } @@ -3258,9 +3226,6 @@ pub enum ThreadEvent { CancelEditing, CompletionCanceled, ProfileChanged, - RetriesFailed { - message: SharedString, - }, } impl EventEmitter<ThreadEvent> for Thread {} @@ -4192,7 +4157,7 @@ fn main() {{ assert_eq!(retry_state.attempt, 1, "Should be first retry attempt"); assert_eq!( retry_state.max_attempts, MAX_RETRY_ATTEMPTS, - "Should have default max attempts" + "Should retry MAX_RETRY_ATTEMPTS times for overloaded errors" ); }); @@ -4265,7 +4230,7 @@ fn main() {{ let retry_state = thread.retry_state.as_ref().unwrap(); assert_eq!(retry_state.attempt, 1, "Should be first retry attempt"); assert_eq!( - retry_state.max_attempts, MAX_RETRY_ATTEMPTS, + retry_state.max_attempts, 1, "Should have correct max attempts" ); }); @@ -4281,8 +4246,8 @@ fn main() {{ if let MessageSegment::Text(text) = seg { text.contains("internal") && text.contains("Fake") - && text - .contains(&format!("attempt 1 of {}", MAX_RETRY_ATTEMPTS)) + && text.contains("Retrying in") + && !text.contains("attempt") } else { false } @@ -4320,8 +4285,8 @@ fn main() {{ let project = create_test_project(cx, json!({})).await; let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await; - // Create model that returns overloaded error - let model = Arc::new(ErrorInjector::new(TestError::Overloaded)); + // Create model that returns internal server error + let model = Arc::new(ErrorInjector::new(TestError::InternalServerError)); // Insert a user message thread.update(cx, |thread, cx| { @@ -4371,50 +4336,17 @@ fn main() {{ assert!(thread.retry_state.is_some(), "Should have retry state"); let retry_state = thread.retry_state.as_ref().unwrap(); assert_eq!(retry_state.attempt, 1, "Should be first retry attempt"); - }); - - // Advance clock for first retry - cx.executor() - .advance_clock(Duration::from_secs(BASE_RETRY_DELAY_SECS)); - cx.run_until_parked(); - - // Should have scheduled second retry - count retry messages - let retry_count = thread.update(cx, |thread, _| { - thread - .messages - .iter() - .filter(|m| { - m.ui_only - && m.segments.iter().any(|s| { - if let MessageSegment::Text(text) = s { - text.contains("Retrying") && text.contains("seconds") - } else { - false - } - }) - }) - .count() - }); - assert_eq!(retry_count, 2, "Should have scheduled second retry"); - - // Check retry state updated - thread.read_with(cx, |thread, _| { - assert!(thread.retry_state.is_some(), "Should have retry state"); - let retry_state = thread.retry_state.as_ref().unwrap(); - assert_eq!(retry_state.attempt, 2, "Should be second retry attempt"); assert_eq!( - retry_state.max_attempts, MAX_RETRY_ATTEMPTS, - "Should have correct max attempts" + retry_state.max_attempts, 1, + "Internal server errors should only retry once" ); }); - // Advance clock for second retry (exponential backoff) - cx.executor() - .advance_clock(Duration::from_secs(BASE_RETRY_DELAY_SECS * 2)); + // Advance clock for first retry + cx.executor().advance_clock(BASE_RETRY_DELAY); cx.run_until_parked(); - // Should have scheduled third retry - // Count all retry messages now + // Should have scheduled second retry - count retry messages let retry_count = thread.update(cx, |thread, _| { thread .messages @@ -4432,56 +4364,24 @@ fn main() {{ .count() }); assert_eq!( - retry_count, MAX_RETRY_ATTEMPTS as usize, - "Should have scheduled third retry" + retry_count, 1, + "Should have only one retry for internal server errors" ); - // Check retry state updated + // For internal server errors, we only retry once and then give up + // Check that retry_state is cleared after the single retry thread.read_with(cx, |thread, _| { - assert!(thread.retry_state.is_some(), "Should have retry state"); - let retry_state = thread.retry_state.as_ref().unwrap(); - assert_eq!( - retry_state.attempt, MAX_RETRY_ATTEMPTS, - "Should be at max retry attempt" - ); - assert_eq!( - retry_state.max_attempts, MAX_RETRY_ATTEMPTS, - "Should have correct max attempts" + assert!( + thread.retry_state.is_none(), + "Retry state should be cleared after single retry" ); }); - // Advance clock for third retry (exponential backoff) - cx.executor() - .advance_clock(Duration::from_secs(BASE_RETRY_DELAY_SECS * 4)); - cx.run_until_parked(); - - // No more retries should be scheduled after clock was advanced. - let retry_count = thread.update(cx, |thread, _| { - thread - .messages - .iter() - .filter(|m| { - m.ui_only - && m.segments.iter().any(|s| { - if let MessageSegment::Text(text) = s { - text.contains("Retrying") && text.contains("seconds") - } else { - false - } - }) - }) - .count() - }); - assert_eq!( - retry_count, MAX_RETRY_ATTEMPTS as usize, - "Should not exceed max retries" - ); - - // Final completion count should be initial + max retries + // Verify total attempts (1 initial + 1 retry) assert_eq!( *completion_count.lock(), - (MAX_RETRY_ATTEMPTS + 1) as usize, - "Should have made initial + max retry attempts" + 2, + "Should have attempted once plus 1 retry" ); } @@ -4501,13 +4401,13 @@ fn main() {{ }); // Track events - let retries_failed = Arc::new(Mutex::new(false)); - let retries_failed_clone = retries_failed.clone(); + let stopped_with_error = Arc::new(Mutex::new(false)); + let stopped_with_error_clone = stopped_with_error.clone(); let _subscription = thread.update(cx, |_, cx| { cx.subscribe(&thread, move |_, _, event: &ThreadEvent, _| { - if let ThreadEvent::RetriesFailed { .. } = event { - *retries_failed_clone.lock() = true; + if let ThreadEvent::Stopped(Err(_)) = event { + *stopped_with_error_clone.lock() = true; } }) }); @@ -4519,23 +4419,11 @@ fn main() {{ cx.run_until_parked(); // Advance through all retries - for i in 0..MAX_RETRY_ATTEMPTS { - let delay = if i == 0 { - BASE_RETRY_DELAY_SECS - } else { - BASE_RETRY_DELAY_SECS * 2u64.pow(i as u32 - 1) - }; - cx.executor().advance_clock(Duration::from_secs(delay)); + for _ in 0..MAX_RETRY_ATTEMPTS { + cx.executor().advance_clock(BASE_RETRY_DELAY); cx.run_until_parked(); } - // After the 3rd retry is scheduled, we need to wait for it to execute and fail - // The 3rd retry has a delay of BASE_RETRY_DELAY_SECS * 4 (20 seconds) - let final_delay = BASE_RETRY_DELAY_SECS * 2u64.pow((MAX_RETRY_ATTEMPTS - 1) as u32); - cx.executor() - .advance_clock(Duration::from_secs(final_delay)); - cx.run_until_parked(); - let retry_count = thread.update(cx, |thread, _| { thread .messages @@ -4553,14 +4441,14 @@ fn main() {{ .count() }); - // After max retries, should emit RetriesFailed event + // After max retries, should emit Stopped(Err(...)) event assert_eq!( retry_count, MAX_RETRY_ATTEMPTS as usize, - "Should have attempted max retries" + "Should have attempted MAX_RETRY_ATTEMPTS retries for overloaded errors" ); assert!( - *retries_failed.lock(), - "Should emit RetriesFailed event after max retries exceeded" + *stopped_with_error.lock(), + "Should emit Stopped(Err(...)) event after max retries exceeded" ); // Retry state should be cleared @@ -4578,7 +4466,7 @@ fn main() {{ .count(); assert_eq!( retry_messages, MAX_RETRY_ATTEMPTS as usize, - "Should have one retry message per attempt" + "Should have MAX_RETRY_ATTEMPTS retry messages for overloaded errors" ); }); } @@ -4716,8 +4604,7 @@ fn main() {{ }); // Wait for retry - cx.executor() - .advance_clock(Duration::from_secs(BASE_RETRY_DELAY_SECS)); + cx.executor().advance_clock(BASE_RETRY_DELAY); cx.run_until_parked(); // Stream some successful content @@ -4879,8 +4766,7 @@ fn main() {{ }); // Wait for retry delay - cx.executor() - .advance_clock(Duration::from_secs(BASE_RETRY_DELAY_SECS)); + cx.executor().advance_clock(BASE_RETRY_DELAY); cx.run_until_parked(); // The retry should now use our FailOnceModel which should succeed @@ -5039,9 +4925,15 @@ fn main() {{ thread.read_with(cx, |thread, _| { assert!( - thread.retry_state.is_none(), - "Rate limit errors should not set retry_state" + thread.retry_state.is_some(), + "Rate limit errors should set retry_state" ); + if let Some(retry_state) = &thread.retry_state { + assert_eq!( + retry_state.max_attempts, MAX_RETRY_ATTEMPTS, + "Rate limit errors should use MAX_RETRY_ATTEMPTS" + ); + } }); // Verify we have one retry message @@ -5074,18 +4966,15 @@ fn main() {{ .find(|msg| msg.role == Role::System && msg.ui_only) .expect("Should have a retry message"); - // Check that the message doesn't contain attempt count + // Check that the message contains attempt count since we use retry_state if let Some(MessageSegment::Text(text)) = retry_message.segments.first() { assert!( - !text.contains("attempt"), - "Rate limit retry message should not contain attempt count" + text.contains(&format!("attempt 1 of {}", MAX_RETRY_ATTEMPTS)), + "Rate limit retry message should contain attempt count with MAX_RETRY_ATTEMPTS" ); assert!( - text.contains(&format!( - "Retrying in {} seconds", - TEST_RATE_LIMIT_RETRY_SECS - )), - "Rate limit retry message should contain retry delay" + text.contains("Retrying"), + "Rate limit retry message should contain retry text" ); } }); diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index 383729017a1635e4301fa50d587f70940543130f..3cf68b887ddf032b9b9f3ea8092bdbd97f31f90f 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -996,30 +996,57 @@ impl ActiveThread { | ThreadEvent::SummaryChanged => { self.save_thread(cx); } - ThreadEvent::Stopped(reason) => match reason { - Ok(StopReason::EndTurn | StopReason::MaxTokens) => { - let used_tools = self.thread.read(cx).used_tools_since_last_user_message(); - self.play_notification_sound(window, cx); - self.show_notification( - if used_tools { - "Finished running tools" - } else { - "New message" - }, - IconName::ZedAssistant, - window, - cx, - ); + ThreadEvent::Stopped(reason) => { + match reason { + Ok(StopReason::EndTurn | StopReason::MaxTokens) => { + let used_tools = self.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, + ); + } + Ok(StopReason::ToolUse) => { + // Don't notify for intermediate tool use + } + Ok(StopReason::Refusal) => { + self.notify_with_sound( + "Language model refused to respond", + IconName::Warning, + window, + cx, + ); + } + Err(error) => { + self.notify_with_sound( + "Agent stopped due to an error", + IconName::Warning, + window, + cx, + ); + + let error_message = error + .chain() + .map(|err| err.to_string()) + .collect::<Vec<_>>() + .join("\n"); + self.last_error = Some(ThreadError::Message { + header: "Error interacting with language model".into(), + message: error_message.into(), + }); + } } - _ => {} - }, + } ThreadEvent::ToolConfirmationNeeded => { - self.play_notification_sound(window, cx); - self.show_notification("Waiting for tool confirmation", IconName::Info, window, cx); + self.notify_with_sound("Waiting for tool confirmation", IconName::Info, window, cx); } ThreadEvent::ToolUseLimitReached => { - self.play_notification_sound(window, cx); - self.show_notification( + self.notify_with_sound( "Consecutive tool use limit reached.", IconName::Warning, window, @@ -1162,9 +1189,6 @@ impl ActiveThread { self.save_thread(cx); cx.notify(); } - ThreadEvent::RetriesFailed { message } => { - self.show_notification(message, ui::IconName::Warning, window, cx); - } } } @@ -1219,6 +1243,17 @@ impl ActiveThread { } } + fn notify_with_sound( + &mut self, + caption: impl Into<SharedString>, + icon: IconName, + window: &mut Window, + cx: &mut Context<ActiveThread>, + ) { + self.play_notification_sound(window, cx); + self.show_notification(caption, icon, window, cx); + } + fn pop_up( &mut self, icon: IconName, diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index 31fb0dd69fbbd133888eb26d14643d816c810554..000e27032202d56f1fa98cdb82d73ef757a84766 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -1488,7 +1488,6 @@ impl AgentDiff { | ThreadEvent::ToolConfirmationNeeded | ThreadEvent::ToolUseLimitReached | ThreadEvent::CancelEditing - | ThreadEvent::RetriesFailed { .. } | ThreadEvent::ProfileChanged => {} } } diff --git a/crates/eval/src/example.rs b/crates/eval/src/example.rs index 904eca83e609dc8766fb3a5a69ed9040c82f0168..09770364cb6b460a4ce8d61d76bcc833cb466129 100644 --- a/crates/eval/src/example.rs +++ b/crates/eval/src/example.rs @@ -221,9 +221,6 @@ impl ExampleContext { ThreadEvent::ShowError(thread_error) => { tx.try_send(Err(anyhow!(thread_error.clone()))).ok(); } - ThreadEvent::RetriesFailed { .. } => { - // Ignore retries failed events - } ThreadEvent::Stopped(reason) => match reason { Ok(StopReason::EndTurn) => { tx.close_channel(); From 78b77373685ed018f7f30fe7e4f1a805ded18f2a Mon Sep 17 00:00:00 2001 From: Michael Sloan <michael@zed.dev> Date: Tue, 15 Jul 2025 13:07:01 -0600 Subject: [PATCH 090/658] Remove scap from workspace-hack (#34490) Regression in #34251 which broke remote_server build Release Notes: - N/A --- .config/hakari.toml | 2 ++ Cargo.lock | 3 --- Cargo.toml | 1 + tooling/workspace-hack/Cargo.toml | 12 ------------ 4 files changed, 3 insertions(+), 15 deletions(-) diff --git a/.config/hakari.toml b/.config/hakari.toml index 982542ca397e072d83af67608ea31a3415360a8e..5168887581c8a1fdae0478e74b0f01225c7a1465 100644 --- a/.config/hakari.toml +++ b/.config/hakari.toml @@ -23,6 +23,8 @@ workspace-members = [ ] 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 = "270538dc780f5240723233ff901e1054641ed318" }, ] [final-excludes] diff --git a/Cargo.lock b/Cargo.lock index de808ff263088b952c60befb84d6c6ce786a19e0..e2d86576c372b538943b92cd548590ed655ede2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19687,7 +19687,6 @@ dependencies = [ "rustix 1.0.7", "rustls 0.23.26", "rustls-webpki 0.103.1", - "scap", "schemars", "scopeguard", "sea-orm", @@ -19735,9 +19734,7 @@ dependencies = [ "wasmtime-cranelift", "wasmtime-environ", "winapi", - "windows 0.61.1", "windows-core 0.61.0", - "windows-future", "windows-numerics", "windows-sys 0.48.0", "windows-sys 0.52.0", diff --git a/Cargo.toml b/Cargo.toml index 5403f279c80666f37e3d837e200ba0ba0b100a9f..0e4cd1504ff76a065e0ce557acc216b5c6c830bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -547,6 +547,7 @@ rustc-demangle = "0.1.23" rustc-hash = "2.1.0" rustls = { version = "0.23.26" } rustls-platform-verifier = "0.5.0" +# When updating scap rev, also update it in .config/hakari.toml scap = { git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318", default-features = false } schemars = { version = "1.0", features = ["indexmap2"] } semver = "1.0" diff --git a/tooling/workspace-hack/Cargo.toml b/tooling/workspace-hack/Cargo.toml index 530c2cf925017cb4a1d833290567e08b07a408ed..10264540262bfd021577a954bf8933a2554ca222 100644 --- a/tooling/workspace-hack/Cargo.toml +++ b/tooling/workspace-hack/Cargo.toml @@ -429,7 +429,6 @@ rand-274715c4dabd11b0 = { package = "rand", version = "0.9" } ring = { version = "0.17", features = ["std"] } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "param", "pipe", "process", "pty", "shm", "stdio", "system", "termios", "time"] } rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] } -scap = { git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318", default-features = false, features = ["x11"] } 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"] } @@ -469,7 +468,6 @@ rand-274715c4dabd11b0 = { package = "rand", version = "0.9" } ring = { version = "0.17", features = ["std"] } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "param", "pipe", "process", "pty", "shm", "stdio", "system", "termios", "time"] } rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] } -scap = { git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318", default-features = false, features = ["x11"] } scopeguard = { version = "1" } sync_wrapper = { version = "1", default-features = false, features = ["futures"] } tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] } @@ -509,7 +507,6 @@ rand-274715c4dabd11b0 = { package = "rand", version = "0.9" } ring = { version = "0.17", features = ["std"] } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "param", "pipe", "process", "pty", "shm", "stdio", "system", "termios", "time"] } rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] } -scap = { git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318", default-features = false, features = ["x11"] } 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"] } @@ -549,7 +546,6 @@ rand-274715c4dabd11b0 = { package = "rand", version = "0.9" } ring = { version = "0.17", features = ["std"] } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "param", "pipe", "process", "pty", "shm", "stdio", "system", "termios", "time"] } rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] } -scap = { git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318", default-features = false, features = ["x11"] } scopeguard = { version = "1" } sync_wrapper = { version = "1", default-features = false, features = ["futures"] } tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] } @@ -571,7 +567,6 @@ itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" } naga = { version = "25", features = ["spv-out", "wgsl-in"] } ring = { version = "0.17", features = ["std"] } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event"] } -scap = { git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318", default-features = false, features = ["x11"] } scopeguard = { version = "1" } sync_wrapper = { version = "1", default-features = false, features = ["futures"] } tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] } @@ -579,9 +574,7 @@ 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"] } winapi = { version = "0.3", default-features = false, features = ["cfg", "commapi", "consoleapi", "errhandlingapi", "evntrace", "fileapi", "handleapi", "impl-debug", "impl-default", "in6addr", "inaddr", "ioapiset", "knownfolders", "minwinbase", "minwindef", "namedpipeapi", "ntsecapi", "objbase", "processenv", "processthreadsapi", "shlobj", "std", "synchapi", "sysinfoapi", "timezoneapi", "winbase", "windef", "winerror", "winioctl", "winnt", "winreg", "winsock2", "winuser"] } -windows = { version = "0.61", features = ["Foundation_Metadata", "Foundation_Numerics", "Graphics_Capture", "Graphics_DirectX_Direct3D11", "Graphics_Imaging", "Media_Core", "Media_MediaProperties", "Media_Transcoding", "Security_Cryptography", "Storage_Search", "Storage_Streams", "System_Threading", "UI_ViewManagement", "Wdk_System_SystemServices", "Win32_Devices_Display", "Win32_Globalization", "Win32_Graphics_Direct2D_Common", "Win32_Graphics_Direct3D", "Win32_Graphics_Direct3D11", "Win32_Graphics_DirectWrite", "Win32_Graphics_Dwm", "Win32_Graphics_Dxgi_Common", "Win32_Graphics_Gdi", "Win32_Graphics_Imaging_D2D", "Win32_Networking_WinSock", "Win32_Security_Credentials", "Win32_Storage_FileSystem", "Win32_System_Com_StructuredStorage", "Win32_System_Console", "Win32_System_DataExchange", "Win32_System_IO", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Ole", "Win32_System_Pipes", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Variant", "Win32_System_WinRT_Direct3D11", "Win32_System_WinRT_Graphics_Capture", "Win32_UI_Controls", "Win32_UI_HiDpi", "Win32_UI_Input_Ime", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell_Common", "Win32_UI_Shell_PropertiesSystem", "Win32_UI_WindowsAndMessaging"] } windows-core = { version = "0.61" } -windows-future = { version = "0.2" } windows-numerics = { version = "0.2" } windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_Globalization", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] } windows-sys-b21d60becc0929df = { package = "windows-sys", version = "0.52", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Wdk_System_IO", "Win32_Foundation", "Win32_Networking_WinSock", "Win32_Security_Authorization", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_IO", "Win32_System_Memory", "Win32_System_Pipes", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_WindowsProgramming"] } @@ -599,7 +592,6 @@ naga = { version = "25", features = ["spv-out", "wgsl-in"] } proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] } ring = { version = "0.17", features = ["std"] } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event"] } -scap = { git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318", default-features = false, features = ["x11"] } scopeguard = { version = "1" } sync_wrapper = { version = "1", default-features = false, features = ["futures"] } tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] } @@ -607,9 +599,7 @@ 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"] } winapi = { version = "0.3", default-features = false, features = ["cfg", "commapi", "consoleapi", "errhandlingapi", "evntrace", "fileapi", "handleapi", "impl-debug", "impl-default", "in6addr", "inaddr", "ioapiset", "knownfolders", "minwinbase", "minwindef", "namedpipeapi", "ntsecapi", "objbase", "processenv", "processthreadsapi", "shlobj", "std", "synchapi", "sysinfoapi", "timezoneapi", "winbase", "windef", "winerror", "winioctl", "winnt", "winreg", "winsock2", "winuser"] } -windows = { version = "0.61", features = ["Foundation_Metadata", "Foundation_Numerics", "Graphics_Capture", "Graphics_DirectX_Direct3D11", "Graphics_Imaging", "Media_Core", "Media_MediaProperties", "Media_Transcoding", "Security_Cryptography", "Storage_Search", "Storage_Streams", "System_Threading", "UI_ViewManagement", "Wdk_System_SystemServices", "Win32_Devices_Display", "Win32_Globalization", "Win32_Graphics_Direct2D_Common", "Win32_Graphics_Direct3D", "Win32_Graphics_Direct3D11", "Win32_Graphics_DirectWrite", "Win32_Graphics_Dwm", "Win32_Graphics_Dxgi_Common", "Win32_Graphics_Gdi", "Win32_Graphics_Imaging_D2D", "Win32_Networking_WinSock", "Win32_Security_Credentials", "Win32_Storage_FileSystem", "Win32_System_Com_StructuredStorage", "Win32_System_Console", "Win32_System_DataExchange", "Win32_System_IO", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Ole", "Win32_System_Pipes", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Variant", "Win32_System_WinRT_Direct3D11", "Win32_System_WinRT_Graphics_Capture", "Win32_UI_Controls", "Win32_UI_HiDpi", "Win32_UI_Input_Ime", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell_Common", "Win32_UI_Shell_PropertiesSystem", "Win32_UI_WindowsAndMessaging"] } windows-core = { version = "0.61" } -windows-future = { version = "0.2" } windows-numerics = { version = "0.2" } windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_Globalization", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] } windows-sys-b21d60becc0929df = { package = "windows-sys", version = "0.52", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Wdk_System_IO", "Win32_Foundation", "Win32_Networking_WinSock", "Win32_Security_Authorization", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_IO", "Win32_System_Memory", "Win32_System_Pipes", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_WindowsProgramming"] } @@ -644,7 +634,6 @@ rand-274715c4dabd11b0 = { package = "rand", version = "0.9" } ring = { version = "0.17", features = ["std"] } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "param", "pipe", "process", "pty", "shm", "stdio", "system", "termios", "time"] } rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] } -scap = { git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318", default-features = false, features = ["x11"] } 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"] } @@ -684,7 +673,6 @@ rand-274715c4dabd11b0 = { package = "rand", version = "0.9" } ring = { version = "0.17", features = ["std"] } rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event", "mm", "param", "pipe", "process", "pty", "shm", "stdio", "system", "termios", "time"] } rustix-dff4ba8e3ae991db = { package = "rustix", version = "1", features = ["fs", "net", "process", "termios", "time"] } -scap = { git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318", default-features = false, features = ["x11"] } scopeguard = { version = "1" } sync_wrapper = { version = "1", default-features = false, features = ["futures"] } tokio-rustls = { version = "0.26", default-features = false, features = ["ring"] } From b39893508102653c7487a3d137235ef740435805 Mon Sep 17 00:00:00 2001 From: Ariel Rzezak <arzezak@users.noreply.github.com> Date: Tue, 15 Jul 2025 16:07:39 -0300 Subject: [PATCH 091/658] Fix comment in default.json (#34481) Update line to properly reference the intended setting. Release Notes: - N/A --- assets/settings/default.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index edf07fdbf98e745998f3fac553de2b0a5d78cefd..aa6e4399c387227dd557f9e30fb76006a75f4c2c 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -84,7 +84,7 @@ "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 horizontally. Defaults to "left" + // The direction that you want to split panes vertically. Defaults to "left" "pane_split_direction_vertical": "left", // Centered layout related settings. "centered_layout": { From af0031ae8bef3ecde88a2dd73aa2692a1ab06af2 Mon Sep 17 00:00:00 2001 From: Cole Miller <cole@zed.dev> Date: Tue, 15 Jul 2025 15:16:48 -0400 Subject: [PATCH 092/658] Fix positioning of terminal inline assist after clearing the screen (#34465) Closes #33945. Here's my attempt to describe what's going on in that issue and what this fix is doing: We always render the terminal inline assistant starting on the line after the cursor, with a height of 4 lines. When deploying it, we scroll the viewport to the bottom of the terminal so that the assistant will be in view. When scrolling while the assistant is deployed (including in that case), we need to make an adjustment that "pushes up" the terminal content by the height of the assistant, so that we can scroll to see all the normal content plus the assistant itself. That quantity is `scroll_top`, which represents _how much height in the current viewport is occupied by the assistant that would otherwise be occupied by terminal content_. So when you scroll up and a line of the assistant's height goes out of view, `scroll_top` decreases by 1, etc. When we scroll to the bottom after deploying the assistant, we set `scroll_top` to the result of calling `max_scroll_top`, which computes it this way: ``` block.height.saturating_sub(viewport_lines.saturating_sub(terminal_lines)) ``` Which, being interpreted, is "the height of the assistant, minus any viewport lines that are not occupied by terminal content", i.e. the assistant is allowed to eat up vertical space below the last line of terminal content without increasing `scroll_top`. The problem comes when we clear the screen---this adds a full screen to `terminal_lines`, but the cursor is positioned at the top of the viewport with blank lines below, just like at the beginning of a session when `terminal_lines == 1`. Those blank lines should be available to the assistant, but the `scroll_top` calculation doesn't reflect that. I've tried to fix this by basing the `max_scroll_top` calculation on the position of the cursor instead of the raw `terminal_lines` value. There was also a special case for `viewport_lines == terminal_lines` that I think can now be removed. Release Notes: - Fixed the positioning of the terminal inline assistant when it's deployed after clearing the terminal. --- crates/terminal_view/src/terminal_view.rs | 27 +++++++---------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index bad3ebd479fbf2deeaeeef08974a03d900c35389..1cc1fbcf6f671c8968975b807f080bfdce04317f 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -25,11 +25,11 @@ use terminal::{ TaskStatus, Terminal, TerminalBounds, ToggleViMode, alacritty_terminal::{ index::Point, - term::{TermMode, search::RegexSearch}, + term::{TermMode, point_to_viewport, search::RegexSearch}, }, terminal_settings::{self, CursorShape, TerminalBlink, TerminalSettings, WorkingDirectory}, }; -use terminal_element::{TerminalElement, is_blank}; +use terminal_element::TerminalElement; use terminal_panel::TerminalPanel; use terminal_scrollbar::TerminalScrollHandle; use terminal_slash_command::TerminalSlashCommand; @@ -497,25 +497,14 @@ impl TerminalView { }; let line_height = terminal.last_content().terminal_bounds.line_height; - let mut terminal_lines = terminal.total_lines(); let viewport_lines = terminal.viewport_lines(); - if terminal.total_lines() == terminal.viewport_lines() { - let mut last_line = None; - for cell in terminal.last_content.cells.iter().rev() { - if !is_blank(cell) { - break; - } - - let last_line = last_line.get_or_insert(cell.point.line); - if *last_line != cell.point.line { - terminal_lines -= 1; - } - *last_line = cell.point.line; - } - } - + let cursor = point_to_viewport( + terminal.last_content.display_offset, + terminal.last_content.cursor.point, + ) + .unwrap_or_default(); let max_scroll_top_in_lines = - (block.height as usize).saturating_sub(viewport_lines.saturating_sub(terminal_lines)); + (block.height as usize).saturating_sub(viewport_lines.saturating_sub(cursor.line + 1)); max_scroll_top_in_lines as f32 * line_height } From ec52e9281aacffc376b4747405a593b95b6851ca Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Wed, 16 Jul 2025 01:05:50 +0530 Subject: [PATCH 093/658] Add xAI language model provider (#33593) Closes #30010 Release Notes: - Add support for xAI language model provider --- Cargo.lock | 12 + Cargo.toml | 2 + assets/icons/ai_x_ai.svg | 3 + crates/icons/src/icons.rs | 1 + crates/language_models/Cargo.toml | 1 + crates/language_models/src/language_models.rs | 2 + crates/language_models/src/provider.rs | 1 + .../src/provider/open_router.rs | 2 +- crates/language_models/src/provider/x_ai.rs | 571 ++++++++++++++++++ crates/language_models/src/settings.rs | 47 +- crates/x_ai/Cargo.toml | 23 + crates/x_ai/LICENSE-GPL | 1 + crates/x_ai/src/x_ai.rs | 126 ++++ docs/src/ai/configuration.md | 74 ++- 14 files changed, 839 insertions(+), 27 deletions(-) create mode 100644 assets/icons/ai_x_ai.svg create mode 100644 crates/language_models/src/provider/x_ai.rs create mode 100644 crates/x_ai/Cargo.toml create mode 120000 crates/x_ai/LICENSE-GPL create mode 100644 crates/x_ai/src/x_ai.rs diff --git a/Cargo.lock b/Cargo.lock index e2d86576c372b538943b92cd548590ed655ede2d..15a28016c6d2f168168657a07443ea40080b07bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9094,6 +9094,7 @@ dependencies = [ "util", "vercel", "workspace-hack", + "x_ai", "zed_llm_client", ] @@ -19840,6 +19841,17 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" +[[package]] +name = "x_ai" +version = "0.1.0" +dependencies = [ + "anyhow", + "schemars", + "serde", + "strum 0.27.1", + "workspace-hack", +] + [[package]] name = "xattr" version = "0.2.3" diff --git a/Cargo.toml b/Cargo.toml index 0e4cd1504ff76a065e0ce557acc216b5c6c830bd..afb47c006e58d24fc9a6557ab437dfbf1db98e65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -179,6 +179,7 @@ members = [ "crates/welcome", "crates/workspace", "crates/worktree", + "crates/x_ai", "crates/zed", "crates/zed_actions", "crates/zeta", @@ -394,6 +395,7 @@ 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" } zed = { path = "crates/zed" } zed_actions = { path = "crates/zed_actions" } zeta = { path = "crates/zeta" } diff --git a/assets/icons/ai_x_ai.svg b/assets/icons/ai_x_ai.svg new file mode 100644 index 0000000000000000000000000000000000000000..289525c8ef72a26b92db3c6ee98b2717a679fbc7 --- /dev/null +++ b/assets/icons/ai_x_ai.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="m12.414 5.47.27 9.641h2.157l.27-13.15zM15.11.889h-3.293L6.651 7.613l1.647 2.142zM.889 15.11H4.18l1.647-2.142-1.647-2.143zm0-9.641 7.409 9.641h3.292L4.181 5.47z" fill="#000"/> +</svg> diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index 3c24ee59f6edf99b6f8ccf3093e83cf3ebea86c5..b2ec7684355c27280ea7d4a056bfb30ff31ea79b 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -21,6 +21,7 @@ pub enum IconName { AiOpenAi, AiOpenRouter, AiVZero, + AiXAi, AiZed, ArrowCircle, ArrowDown, diff --git a/crates/language_models/Cargo.toml b/crates/language_models/Cargo.toml index 0f248edd574819aee9ac1311ed23de30be48b21e..5d158e84f4fb072d40e43a43cd53b5b996274351 100644 --- a/crates/language_models/Cargo.toml +++ b/crates/language_models/Cargo.toml @@ -43,6 +43,7 @@ ollama = { workspace = true, features = ["schemars"] } open_ai = { workspace = true, features = ["schemars"] } open_router = { workspace = true, features = ["schemars"] } vercel = { workspace = true, features = ["schemars"] } +x_ai = { workspace = true, features = ["schemars"] } partial-json-fixer.workspace = true proto.workspace = true release_channel.workspace = true diff --git a/crates/language_models/src/language_models.rs b/crates/language_models/src/language_models.rs index c7324732c9bbf88698a1a7280ff80cea077a1d2f..192f5a5fae214693fab8b1b166e907478ce307f2 100644 --- a/crates/language_models/src/language_models.rs +++ b/crates/language_models/src/language_models.rs @@ -20,6 +20,7 @@ use crate::provider::ollama::OllamaLanguageModelProvider; use crate::provider::open_ai::OpenAiLanguageModelProvider; use crate::provider::open_router::OpenRouterLanguageModelProvider; use crate::provider::vercel::VercelLanguageModelProvider; +use crate::provider::x_ai::XAiLanguageModelProvider; pub use crate::settings::*; pub fn init(user_store: Entity<UserStore>, client: Arc<Client>, cx: &mut App) { @@ -81,5 +82,6 @@ fn register_language_model_providers( VercelLanguageModelProvider::new(client.http_client(), cx), cx, ); + registry.register_provider(XAiLanguageModelProvider::new(client.http_client(), cx), cx); registry.register_provider(CopilotChatLanguageModelProvider::new(cx), cx); } diff --git a/crates/language_models/src/provider.rs b/crates/language_models/src/provider.rs index 6bc93bd3661e86fc2c8f9bacafaf2d4121e0f7a6..c717be7c907cebf7427c67c748b689cb40b0ed9d 100644 --- a/crates/language_models/src/provider.rs +++ b/crates/language_models/src/provider.rs @@ -10,3 +10,4 @@ pub mod ollama; pub mod open_ai; pub mod open_router; pub mod vercel; +pub mod x_ai; diff --git a/crates/language_models/src/provider/open_router.rs b/crates/language_models/src/provider/open_router.rs index c46135ff3eae704f5d54027457d8f86fbef4820a..5a6acc432993d74ad4fc0077bc2550bda76e1171 100644 --- a/crates/language_models/src/provider/open_router.rs +++ b/crates/language_models/src/provider/open_router.rs @@ -376,7 +376,7 @@ impl LanguageModel for OpenRouterLanguageModel { fn tool_input_format(&self) -> LanguageModelToolSchemaFormat { let model_id = self.model.id().trim().to_lowercase(); - if model_id.contains("gemini") { + if model_id.contains("gemini") || model_id.contains("grok-4") { LanguageModelToolSchemaFormat::JsonSchemaSubset } else { LanguageModelToolSchemaFormat::JsonSchema diff --git a/crates/language_models/src/provider/x_ai.rs b/crates/language_models/src/provider/x_ai.rs new file mode 100644 index 0000000000000000000000000000000000000000..5f6034571b54fd30baa6769881f5d27bbcaf162f --- /dev/null +++ b/crates/language_models/src/provider/x_ai.rs @@ -0,0 +1,571 @@ +use anyhow::{Context as _, Result, anyhow}; +use collections::BTreeMap; +use credentials_provider::CredentialsProvider; +use futures::{FutureExt, StreamExt, future::BoxFuture}; +use gpui::{AnyView, App, AsyncApp, Context, Entity, Subscription, Task, Window}; +use http_client::HttpClient; +use language_model::{ + AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, + LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId, + LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, + LanguageModelToolChoice, LanguageModelToolSchemaFormat, RateLimiter, Role, +}; +use menu; +use open_ai::ResponseStreamEvent; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::{Settings, SettingsStore}; +use std::sync::Arc; +use strum::IntoEnumIterator; +use x_ai::Model; + +use ui::{ElevationIndex, List, Tooltip, prelude::*}; +use ui_input::SingleLineInput; +use util::ResultExt; + +use crate::{AllLanguageModelSettings, ui::InstructionListItem}; + +const PROVIDER_ID: &str = "x_ai"; +const PROVIDER_NAME: &str = "xAI"; + +#[derive(Default, Clone, Debug, PartialEq)] +pub struct XAiSettings { + pub api_url: String, + pub available_models: Vec<AvailableModel>, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct AvailableModel { + pub name: String, + pub display_name: Option<String>, + pub max_tokens: u64, + pub max_output_tokens: Option<u64>, + pub max_completion_tokens: Option<u64>, +} + +pub struct XAiLanguageModelProvider { + http_client: Arc<dyn HttpClient>, + state: gpui::Entity<State>, +} + +pub struct State { + api_key: Option<String>, + api_key_from_env: bool, + _subscription: Subscription, +} + +const XAI_API_KEY_VAR: &str = "XAI_API_KEY"; + +impl State { + fn is_authenticated(&self) -> bool { + self.api_key.is_some() + } + + fn reset_api_key(&self, cx: &mut Context<Self>) -> Task<Result<()>> { + let credentials_provider = <dyn CredentialsProvider>::global(cx); + let settings = &AllLanguageModelSettings::get_global(cx).x_ai; + let api_url = if settings.api_url.is_empty() { + x_ai::XAI_API_URL.to_string() + } else { + settings.api_url.clone() + }; + cx.spawn(async move |this, cx| { + credentials_provider + .delete_credentials(&api_url, &cx) + .await + .log_err(); + this.update(cx, |this, cx| { + this.api_key = None; + this.api_key_from_env = false; + cx.notify(); + }) + }) + } + + fn set_api_key(&mut self, api_key: String, cx: &mut Context<Self>) -> Task<Result<()>> { + let credentials_provider = <dyn CredentialsProvider>::global(cx); + let settings = &AllLanguageModelSettings::get_global(cx).x_ai; + let api_url = if settings.api_url.is_empty() { + x_ai::XAI_API_URL.to_string() + } else { + settings.api_url.clone() + }; + cx.spawn(async move |this, cx| { + credentials_provider + .write_credentials(&api_url, "Bearer", api_key.as_bytes(), &cx) + .await + .log_err(); + this.update(cx, |this, cx| { + this.api_key = Some(api_key); + cx.notify(); + }) + }) + } + + fn authenticate(&self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> { + if self.is_authenticated() { + return Task::ready(Ok(())); + } + + let credentials_provider = <dyn CredentialsProvider>::global(cx); + let settings = &AllLanguageModelSettings::get_global(cx).x_ai; + let api_url = if settings.api_url.is_empty() { + x_ai::XAI_API_URL.to_string() + } else { + settings.api_url.clone() + }; + cx.spawn(async move |this, cx| { + let (api_key, from_env) = if let Ok(api_key) = std::env::var(XAI_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, + ) + }; + this.update(cx, |this, cx| { + this.api_key = Some(api_key); + this.api_key_from_env = from_env; + cx.notify(); + })?; + + Ok(()) + }) + } +} + +impl XAiLanguageModelProvider { + pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut App) -> Self { + let state = cx.new(|cx| State { + api_key: None, + api_key_from_env: false, + _subscription: cx.observe_global::<SettingsStore>(|_this: &mut State, cx| { + cx.notify(); + }), + }); + + Self { http_client, state } + } + + fn create_language_model(&self, model: x_ai::Model) -> Arc<dyn LanguageModel> { + Arc::new(XAiLanguageModel { + id: LanguageModelId::from(model.id().to_string()), + model, + state: self.state.clone(), + http_client: self.http_client.clone(), + request_limiter: RateLimiter::new(4), + }) + } +} + +impl LanguageModelProviderState for XAiLanguageModelProvider { + type ObservableEntity = State; + + fn observable_entity(&self) -> Option<gpui::Entity<Self::ObservableEntity>> { + Some(self.state.clone()) + } +} + +impl LanguageModelProvider for XAiLanguageModelProvider { + fn id(&self) -> LanguageModelProviderId { + LanguageModelProviderId(PROVIDER_ID.into()) + } + + fn name(&self) -> LanguageModelProviderName { + LanguageModelProviderName(PROVIDER_NAME.into()) + } + + fn icon(&self) -> IconName { + IconName::AiXAi + } + + fn default_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> { + Some(self.create_language_model(x_ai::Model::default())) + } + + fn default_fast_model(&self, _cx: &App) -> Option<Arc<dyn LanguageModel>> { + Some(self.create_language_model(x_ai::Model::default_fast())) + } + + fn provided_models(&self, cx: &App) -> Vec<Arc<dyn LanguageModel>> { + let mut models = BTreeMap::default(); + + for model in x_ai::Model::iter() { + if !matches!(model, x_ai::Model::Custom { .. }) { + models.insert(model.id().to_string(), model); + } + } + + for model in &AllLanguageModelSettings::get_global(cx) + .x_ai + .available_models + { + models.insert( + model.name.clone(), + x_ai::Model::Custom { + name: model.name.clone(), + display_name: model.display_name.clone(), + max_tokens: model.max_tokens, + max_output_tokens: model.max_output_tokens, + max_completion_tokens: model.max_completion_tokens, + }, + ); + } + + models + .into_values() + .map(|model| self.create_language_model(model)) + .collect() + } + + fn is_authenticated(&self, cx: &App) -> bool { + self.state.read(cx).is_authenticated() + } + + fn authenticate(&self, cx: &mut App) -> Task<Result<(), AuthenticateError>> { + 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)) + .into() + } + + fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>> { + self.state.update(cx, |state, cx| state.reset_api_key(cx)) + } +} + +pub struct XAiLanguageModel { + id: LanguageModelId, + model: x_ai::Model, + state: gpui::Entity<State>, + http_client: Arc<dyn HttpClient>, + request_limiter: RateLimiter, +} + +impl XAiLanguageModel { + fn stream_completion( + &self, + request: open_ai::Request, + cx: &AsyncApp, + ) -> BoxFuture<'static, Result<futures::stream::BoxStream<'static, Result<ResponseStreamEvent>>>> + { + let http_client = self.http_client.clone(); + let Ok((api_key, api_url)) = cx.read_entity(&self.state, |state, cx| { + let settings = &AllLanguageModelSettings::get_global(cx).x_ai; + let api_url = if settings.api_url.is_empty() { + x_ai::XAI_API_URL.to_string() + } else { + settings.api_url.clone() + }; + (state.api_key.clone(), api_url) + }) else { + return futures::future::ready(Err(anyhow!("App state dropped"))).boxed(); + }; + + let future = self.request_limiter.stream(async move { + let api_key = api_key.context("Missing xAI API Key")?; + let request = + open_ai::stream_completion(http_client.as_ref(), &api_url, &api_key, request); + let response = request.await?; + Ok(response) + }); + + async move { Ok(future.await?.boxed()) }.boxed() + } +} + +impl LanguageModel for XAiLanguageModel { + fn id(&self) -> LanguageModelId { + self.id.clone() + } + + fn name(&self) -> LanguageModelName { + LanguageModelName::from(self.model.display_name().to_string()) + } + + fn provider_id(&self) -> LanguageModelProviderId { + LanguageModelProviderId(PROVIDER_ID.into()) + } + + fn provider_name(&self) -> LanguageModelProviderName { + LanguageModelProviderName(PROVIDER_NAME.into()) + } + + fn supports_tools(&self) -> bool { + self.model.supports_tool() + } + + fn supports_images(&self) -> bool { + self.model.supports_images() + } + + fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool { + match choice { + LanguageModelToolChoice::Auto + | LanguageModelToolChoice::Any + | LanguageModelToolChoice::None => true, + } + } + fn tool_input_format(&self) -> LanguageModelToolSchemaFormat { + let model_id = self.model.id().trim().to_lowercase(); + if model_id.eq(x_ai::Model::Grok4.id()) { + LanguageModelToolSchemaFormat::JsonSchemaSubset + } else { + LanguageModelToolSchemaFormat::JsonSchema + } + } + + fn telemetry_id(&self) -> String { + format!("x_ai/{}", self.model.id()) + } + + fn max_token_count(&self) -> u64 { + self.model.max_token_count() + } + + fn max_output_tokens(&self) -> Option<u64> { + self.model.max_output_tokens() + } + + fn count_tokens( + &self, + request: LanguageModelRequest, + cx: &App, + ) -> BoxFuture<'static, Result<u64>> { + count_xai_tokens(request, self.model.clone(), cx) + } + + fn stream_completion( + &self, + request: LanguageModelRequest, + cx: &AsyncApp, + ) -> BoxFuture< + 'static, + Result< + futures::stream::BoxStream< + 'static, + Result<LanguageModelCompletionEvent, LanguageModelCompletionError>, + >, + LanguageModelCompletionError, + >, + > { + let request = crate::provider::open_ai::into_open_ai( + request, + self.model.id(), + self.model.supports_parallel_tool_calls(), + self.max_output_tokens(), + ); + let completions = self.stream_completion(request, cx); + async move { + let mapper = crate::provider::open_ai::OpenAiEventMapper::new(); + Ok(mapper.map_stream(completions.await?).boxed()) + } + .boxed() + } +} + +pub fn count_xai_tokens( + request: LanguageModelRequest, + model: Model, + cx: &App, +) -> BoxFuture<'static, Result<u64>> { + cx.background_spawn(async move { + let messages = request + .messages + .into_iter() + .map(|message| tiktoken_rs::ChatCompletionRequestMessage { + role: match message.role { + Role::User => "user".into(), + Role::Assistant => "assistant".into(), + Role::System => "system".into(), + }, + content: Some(message.string_contents()), + name: None, + function_call: None, + }) + .collect::<Vec<_>>(); + + let model_name = if model.max_token_count() >= 100_000 { + "gpt-4o" + } else { + "gpt-4" + }; + tiktoken_rs::num_tokens_from_messages(model_name, &messages).map(|tokens| tokens as u64) + }) + .boxed() +} + +struct ConfigurationView { + api_key_editor: Entity<SingleLineInput>, + state: gpui::Entity<State>, + load_credentials_task: Option<Task<()>>, +} + +impl ConfigurationView { + fn new(state: gpui::Entity<State>, window: &mut Window, cx: &mut Context<Self>) -> Self { + let api_key_editor = cx.new(|cx| { + SingleLineInput::new( + window, + cx, + "xai-0000000000000000000000000000000000000000000000000", + ) + .label("API key") + }); + + cx.observe(&state, |_, _, cx| { + cx.notify(); + }) + .detach(); + + let load_credentials_task = Some(cx.spawn_in(window, { + let state = state.clone(); + async move |this, cx| { + if let Some(task) = state + .update(cx, |state, cx| state.authenticate(cx)) + .log_err() + { + // We don't log an error, because "not signed in" is also an error. + let _ = task.await; + } + this.update(cx, |this, cx| { + this.load_credentials_task = None; + cx.notify(); + }) + .log_err(); + } + })); + + Self { + api_key_editor, + state, + load_credentials_task, + } + } + + fn save_api_key(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) { + let api_key = self + .api_key_editor + .read(cx) + .editor() + .read(cx) + .text(cx) + .trim() + .to_string(); + + // Don't proceed if no API key is provided and we're not authenticated + if api_key.is_empty() && !self.state.read(cx).is_authenticated() { + return; + } + + let state = self.state.clone(); + cx.spawn_in(window, async move |_, cx| { + state + .update(cx, |state, cx| state.set_api_key(api_key, cx))? + .await + }) + .detach_and_log_err(cx); + + cx.notify(); + } + + fn reset_api_key(&mut self, window: &mut Window, cx: &mut Context<Self>) { + self.api_key_editor.update(cx, |input, cx| { + input.editor.update(cx, |editor, cx| { + editor.set_text("", window, cx); + }); + }); + + let state = self.state.clone(); + cx.spawn_in(window, async move |_, cx| { + state.update(cx, |state, cx| state.reset_api_key(cx))?.await + }) + .detach_and_log_err(cx); + + cx.notify(); + } + + fn should_render_editor(&self, cx: &mut Context<Self>) -> bool { + !self.state.read(cx).is_authenticated() + } +} + +impl Render for ConfigurationView { + fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { + let env_var_set = self.state.read(cx).api_key_from_env; + + 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 agent with xAI, you need to add an API key. Follow these steps:")) + .child( + List::new() + .child(InstructionListItem::new( + "Create one by visiting", + Some("xAI console"), + Some("https://console.x.ai/team/default/api-keys"), + )) + .child(InstructionListItem::text_only( + "Paste your API key below and hit enter to start using the agent", + )), + ) + .child(self.api_key_editor.clone()) + .child( + Label::new(format!( + "You can also assign the {XAI_API_KEY_VAR} environment variable and restart Zed." + )) + .size(LabelSize::Small) + .color(Color::Muted), + ) + .child( + Label::new("Note that xAI is a custom OpenAI-compatible provider.") + .size(LabelSize::Small) + .color(Color::Muted), + ) + .into_any() + } else { + h_flex() + .mt_1() + .p_1() + .justify_between() + .rounded_md() + .border_1() + .border_color(cx.theme().colors().border) + .bg(cx.theme().colors().background) + .child( + h_flex() + .gap_1() + .child(Icon::new(IconName::Check).color(Color::Success)) + .child(Label::new(if env_var_set { + format!("API key set in {XAI_API_KEY_VAR} environment variable.") + } else { + "API key configured.".to_string() + })), + ) + .child( + Button::new("reset-api-key", "Reset API Key") + .label_size(LabelSize::Small) + .icon(IconName::Undo) + .icon_size(IconSize::Small) + .icon_position(IconPosition::Start) + .layer(ElevationIndex::ModalSurface) + .when(env_var_set, |this| { + this.tooltip(Tooltip::text(format!("To reset your API key, unset the {XAI_API_KEY_VAR} environment variable."))) + }) + .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))), + ) + .into_any() + }; + + if self.load_credentials_task.is_some() { + div().child(Label::new("Loading credentials…")).into_any() + } else { + v_flex().size_full().child(api_key_section).into_any() + } + } +} diff --git a/crates/language_models/src/settings.rs b/crates/language_models/src/settings.rs index f96a2c0a66cfe698738deec177b5f82cde274df7..dafbb629100469e6a8dd77850eece139a3bed267 100644 --- a/crates/language_models/src/settings.rs +++ b/crates/language_models/src/settings.rs @@ -17,6 +17,7 @@ use crate::provider::{ open_ai::OpenAiSettings, open_router::OpenRouterSettings, vercel::VercelSettings, + x_ai::XAiSettings, }; /// Initializes the language model settings. @@ -28,33 +29,33 @@ pub fn init(cx: &mut App) { pub struct AllLanguageModelSettings { pub anthropic: AnthropicSettings, pub bedrock: AmazonBedrockSettings, - pub ollama: OllamaSettings, - pub openai: OpenAiSettings, - pub open_router: OpenRouterSettings, - pub zed_dot_dev: ZedDotDevSettings, + pub deepseek: DeepSeekSettings, pub google: GoogleSettings, - pub vercel: VercelSettings, - pub lmstudio: LmStudioSettings, - pub deepseek: DeepSeekSettings, pub mistral: MistralSettings, + pub ollama: OllamaSettings, + pub open_router: OpenRouterSettings, + pub openai: OpenAiSettings, + pub vercel: VercelSettings, + pub x_ai: XAiSettings, + pub zed_dot_dev: ZedDotDevSettings, } #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] pub struct AllLanguageModelSettingsContent { pub anthropic: Option<AnthropicSettingsContent>, pub bedrock: Option<AmazonBedrockSettingsContent>, - pub ollama: Option<OllamaSettingsContent>, + pub deepseek: Option<DeepseekSettingsContent>, + pub google: Option<GoogleSettingsContent>, pub lmstudio: Option<LmStudioSettingsContent>, - pub openai: Option<OpenAiSettingsContent>, + pub mistral: Option<MistralSettingsContent>, + pub ollama: Option<OllamaSettingsContent>, pub open_router: Option<OpenRouterSettingsContent>, + pub openai: Option<OpenAiSettingsContent>, + pub vercel: Option<VercelSettingsContent>, + pub x_ai: Option<XAiSettingsContent>, #[serde(rename = "zed.dev")] pub zed_dot_dev: Option<ZedDotDevSettingsContent>, - pub google: Option<GoogleSettingsContent>, - pub deepseek: Option<DeepseekSettingsContent>, - pub vercel: Option<VercelSettingsContent>, - - pub mistral: Option<MistralSettingsContent>, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] @@ -114,6 +115,12 @@ pub struct GoogleSettingsContent { pub available_models: Option<Vec<provider::google::AvailableModel>>, } +#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +pub struct XAiSettingsContent { + pub api_url: Option<String>, + pub available_models: Option<Vec<provider::x_ai::AvailableModel>>, +} + #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] pub struct ZedDotDevSettingsContent { available_models: Option<Vec<cloud::AvailableModel>>, @@ -230,6 +237,18 @@ impl settings::Settings for AllLanguageModelSettings { vercel.as_ref().and_then(|s| s.available_models.clone()), ); + // XAI + let x_ai = value.x_ai.clone(); + merge( + &mut settings.x_ai.api_url, + x_ai.as_ref().and_then(|s| s.api_url.clone()), + ); + merge( + &mut settings.x_ai.available_models, + x_ai.as_ref().and_then(|s| s.available_models.clone()), + ); + + // ZedDotDev merge( &mut settings.zed_dot_dev.available_models, value diff --git a/crates/x_ai/Cargo.toml b/crates/x_ai/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..7ca0ca09397111404a59dff85d1ccf0659c0ea45 --- /dev/null +++ b/crates/x_ai/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "x_ai" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/x_ai.rs" + +[features] +default = [] +schemars = ["dep:schemars"] + +[dependencies] +anyhow.workspace = true +schemars = { workspace = true, optional = true } +serde.workspace = true +strum.workspace = true +workspace-hack.workspace = true diff --git a/crates/x_ai/LICENSE-GPL b/crates/x_ai/LICENSE-GPL new file mode 120000 index 0000000000000000000000000000000000000000..89e542f750cd3860a0598eff0dc34b56d7336dc4 --- /dev/null +++ b/crates/x_ai/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/x_ai/src/x_ai.rs b/crates/x_ai/src/x_ai.rs new file mode 100644 index 0000000000000000000000000000000000000000..ac116b2f8f610614b4d1efd380169739bbdbc9f2 --- /dev/null +++ b/crates/x_ai/src/x_ai.rs @@ -0,0 +1,126 @@ +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use strum::EnumIter; + +pub const XAI_API_URL: &str = "https://api.x.ai/v1"; + +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)] +pub enum Model { + #[serde(rename = "grok-2-vision-latest")] + Grok2Vision, + #[default] + #[serde(rename = "grok-3-latest")] + Grok3, + #[serde(rename = "grok-3-mini-latest")] + Grok3Mini, + #[serde(rename = "grok-3-fast-latest")] + Grok3Fast, + #[serde(rename = "grok-3-mini-fast-latest")] + Grok3MiniFast, + #[serde(rename = "grok-4-latest")] + Grok4, + #[serde(rename = "custom")] + Custom { + name: String, + /// The name displayed in the UI, such as in the assistant panel model dropdown menu. + display_name: Option<String>, + max_tokens: u64, + max_output_tokens: Option<u64>, + max_completion_tokens: Option<u64>, + }, +} + +impl Model { + pub fn default_fast() -> Self { + Self::Grok3Fast + } + + pub fn from_id(id: &str) -> Result<Self> { + match id { + "grok-2-vision" => Ok(Self::Grok2Vision), + "grok-3" => Ok(Self::Grok3), + "grok-3-mini" => Ok(Self::Grok3Mini), + "grok-3-fast" => Ok(Self::Grok3Fast), + "grok-3-mini-fast" => Ok(Self::Grok3MiniFast), + _ => anyhow::bail!("invalid model id '{id}'"), + } + } + + pub fn id(&self) -> &str { + match self { + Self::Grok2Vision => "grok-2-vision", + Self::Grok3 => "grok-3", + Self::Grok3Mini => "grok-3-mini", + Self::Grok3Fast => "grok-3-fast", + Self::Grok3MiniFast => "grok-3-mini-fast", + Self::Grok4 => "grok-4", + Self::Custom { name, .. } => name, + } + } + + pub fn display_name(&self) -> &str { + match self { + Self::Grok2Vision => "Grok 2 Vision", + Self::Grok3 => "Grok 3", + Self::Grok3Mini => "Grok 3 Mini", + Self::Grok3Fast => "Grok 3 Fast", + Self::Grok3MiniFast => "Grok 3 Mini Fast", + Self::Grok4 => "Grok 4", + Self::Custom { + name, display_name, .. + } => display_name.as_ref().unwrap_or(name), + } + } + + pub fn max_token_count(&self) -> u64 { + match self { + Self::Grok3 | Self::Grok3Mini | Self::Grok3Fast | Self::Grok3MiniFast => 131_072, + Self::Grok4 => 256_000, + Self::Grok2Vision => 8_192, + Self::Custom { max_tokens, .. } => *max_tokens, + } + } + + pub fn max_output_tokens(&self) -> Option<u64> { + match self { + Self::Grok3 | Self::Grok3Mini | Self::Grok3Fast | Self::Grok3MiniFast => Some(8_192), + Self::Grok4 => Some(64_000), + Self::Grok2Vision => Some(4_096), + Self::Custom { + max_output_tokens, .. + } => *max_output_tokens, + } + } + + pub fn supports_parallel_tool_calls(&self) -> bool { + match self { + Self::Grok2Vision + | Self::Grok3 + | Self::Grok3Mini + | Self::Grok3Fast + | Self::Grok3MiniFast + | Self::Grok4 => true, + Model::Custom { .. } => false, + } + } + + pub fn supports_tool(&self) -> bool { + match self { + Self::Grok2Vision + | Self::Grok3 + | Self::Grok3Mini + | Self::Grok3Fast + | Self::Grok3MiniFast + | Self::Grok4 => true, + Model::Custom { .. } => false, + } + } + + pub fn supports_images(&self) -> bool { + match self { + Self::Grok2Vision => true, + _ => false, + } + } +} diff --git a/docs/src/ai/configuration.md b/docs/src/ai/configuration.md index ade1ae672f51944949c47e9f098c60a9a8198423..56eb4ab76cd990871d62ced2cccb019f2d607cd6 100644 --- a/docs/src/ai/configuration.md +++ b/docs/src/ai/configuration.md @@ -23,6 +23,8 @@ Here's an overview of the supported providers and tool call support: | [OpenAI](#openai) | ✅ | | [OpenAI API Compatible](#openai-api-compatible) | 🚫 | | [OpenRouter](#openrouter) | ✅ | +| [Vercel](#vercel-v0) | ✅ | +| [xAI](#xai) | ✅ | ## Use Your Own Keys {#use-your-own-keys} @@ -444,27 +446,30 @@ Custom models will be listed in the model dropdown in the Agent Panel. Zed supports using OpenAI compatible APIs by specifying a custom `endpoint` and `available_models` for the OpenAI provider. -You can add a custom API URL for OpenAI either via the UI or by editing your `settings.json`. -Here are a few model examples you can plug in by using this feature: +Zed supports using OpenAI compatible APIs 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. -#### X.ai Grok +To configure a compatible API, you can add a custom API URL for OpenAI either via the UI or by editing your `settings.json`. For example, to connect to [Together AI](https://www.together.ai/): -Example configuration for using X.ai Grok with Zed: +1. Get an API key from your [Together AI account](https://api.together.ai/settings/api-keys). +2. Add the following to your `settings.json`: ```json +{ "language_models": { "openai": { - "api_url": "https://api.x.ai/v1", + "api_url": "https://api.together.xyz/v1", + "api_key": "YOUR_TOGETHER_AI_API_KEY", "available_models": [ { - "name": "grok-beta", - "display_name": "X.ai Grok (Beta)", - "max_tokens": 131072 + "name": "mistralai/Mixtral-8x7B-Instruct-v0.1", + "display_name": "Together Mixtral 8x7B", + "max_tokens": 32768, + "supports_tools": true } - ], - "version": "1" - }, + ] + } } +} ``` ### OpenRouter {#openrouter} @@ -525,7 +530,9 @@ You can find available models and their specifications on the [OpenRouter models Custom models will be listed in the model dropdown in the Agent Panel. -### Vercel v0 +### 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. @@ -537,6 +544,49 @@ Once you have it, paste it directly into the Vercel provider section in the pane 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) +2. Open the settings view (`agent: open configuration`) and go to the **xAI** section +3. Enter your xAI API key + +The xAI API key will be saved in your keychain. Zed will also use the `XAI_API_KEY` environment variable if it's defined. + +> **Note:** While the xAI API is OpenAI-compatible, Zed has first-class support for it as a dedicated provider. For the best experience, we recommend using the dedicated `x_ai` provider configuration instead of the [OpenAI API Compatible](#openai-api-compatible) method. + +#### Custom Models {#xai-custom-models} + +The Zed agent comes pre-configured with common Grok models. If you wish to use alternate models or customize their parameters, you can do so by adding the following to your Zed `settings.json`: + +```json +{ + "language_models": { + "x_ai": { + "api_url": "https://api.x.ai/v1", + "available_models": [ + { + "name": "grok-1.5", + "display_name": "Grok 1.5", + "max_tokens": 131072, + "max_output_tokens": 8192 + }, + { + "name": "grok-1.5v", + "display_name": "Grok 1.5V (Vision)", + "max_tokens": 131072, + "max_output_tokens": 8192, + "supports_images": true + } + ] + } + } +} +``` + ## Advanced Configuration {#advanced-configuration} ### Custom Provider Endpoints {#custom-provider-endpoint} From 3751737621c7e0603344598fc3974637b0e30fbb Mon Sep 17 00:00:00 2001 From: Conrad Irwin <conrad.irwin@gmail.com> Date: Tue, 15 Jul 2025 13:42:25 -0600 Subject: [PATCH 094/658] Add zed://extension/{id} links (#34492) Release Notes: - Add zed://extension/{id} links to open the extensions UI with a specific extension --- crates/agent_ui/src/agent_configuration.rs | 1 + crates/agent_ui/src/agent_panel.rs | 1 + crates/debugger_ui/src/debugger_panel.rs | 1 + crates/extensions_ui/src/extensions_ui.rs | 54 +++++++++++++++---- .../theme_selector/src/icon_theme_selector.rs | 1 + crates/theme_selector/src/theme_selector.rs | 1 + crates/zed/src/main.rs | 17 ++++++ crates/zed/src/zed/open_listener.rs | 3 ++ crates/zed_actions/src/lib.rs | 3 ++ 9 files changed, 72 insertions(+), 10 deletions(-) diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 8bfdd507611112b2930fd07270667050796533e3..579331c9acd07d1e41a1ceaee7fe2452bb0c1591 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -491,6 +491,7 @@ impl AgentConfiguration { category_filter: Some( ExtensionCategoryFilter::ContextServers, ), + id: None, } .boxed_clone(), cx, diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 18e43dd51eaca2699bf6feeddd20386b145da628..ded26b189642c1eb9e6c79ec958a18ebb99ded68 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1921,6 +1921,7 @@ impl AgentPanel { category_filter: Some( zed_actions::ExtensionCategoryFilter::ContextServers, ), + id: None, }), ) .action("Add Custom Server…", Box::new(AddContextServer)) diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index bf5f31391885edf89beea3e8648df13f68258a77..d81c593484d7fbb46ca31fd1772117c1232c6752 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -1760,6 +1760,7 @@ impl Render for DebugPanel { category_filter: Some( zed_actions::ExtensionCategoryFilter::DebugAdapters, ), + id: None, } .boxed_clone(), cx, diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index 0d00deb10e64ec72e3bf64b1c8ce0929d944104a..b944b1ec505178b56d0894b9790040ac73ede639 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -6,6 +6,7 @@ use std::sync::OnceLock; use std::time::Duration; use std::{ops::Range, sync::Arc}; +use anyhow::Context as _; use client::{ExtensionMetadata, ExtensionProvides}; use collections::{BTreeMap, BTreeSet}; use editor::{Editor, EditorElement, EditorStyle}; @@ -80,16 +81,24 @@ pub fn init(cx: &mut App) { .find_map(|item| item.downcast::<ExtensionsPage>()); if let Some(existing) = existing { - if provides_filter.is_some() { - existing.update(cx, |extensions_page, cx| { + existing.update(cx, |extensions_page, cx| { + if provides_filter.is_some() { extensions_page.change_provides_filter(provides_filter, cx); - }); - } + } + if let Some(id) = action.id.as_ref() { + extensions_page.focus_extension(id, window, cx); + } + }); workspace.activate_item(&existing, true, true, window, cx); } else { - let extensions_page = - ExtensionsPage::new(workspace, provides_filter, window, cx); + let extensions_page = ExtensionsPage::new( + workspace, + provides_filter, + action.id.as_deref(), + window, + cx, + ); workspace.add_item_to_active_pane( Box::new(extensions_page), None, @@ -287,6 +296,7 @@ impl ExtensionsPage { pub fn new( workspace: &Workspace, provides_filter: Option<ExtensionProvides>, + focus_extension_id: Option<&str>, window: &mut Window, cx: &mut Context<Workspace>, ) -> Entity<Self> { @@ -317,6 +327,9 @@ impl ExtensionsPage { let query_editor = cx.new(|cx| { let mut input = Editor::single_line(window, cx); input.set_placeholder_text("Search extensions...", cx); + if let Some(id) = focus_extension_id { + input.set_text(format!("id:{id}"), window, cx); + } input }); cx.subscribe(&query_editor, Self::on_query_change).detach(); @@ -340,7 +353,7 @@ impl ExtensionsPage { scrollbar_state: ScrollbarState::new(scroll_handle), }; this.fetch_extensions( - None, + this.search_query(cx), Some(BTreeSet::from_iter(this.provides_filter)), None, cx, @@ -464,9 +477,23 @@ impl ExtensionsPage { .cloned() .collect::<Vec<_>>(); - let remote_extensions = extension_store.update(cx, |store, cx| { - store.fetch_extensions(search.as_deref(), provides_filter.as_ref(), cx) - }); + let remote_extensions = + if let Some(id) = search.as_ref().and_then(|s| s.strip_prefix("id:")) { + let versions = + extension_store.update(cx, |store, cx| store.fetch_extension_versions(id, cx)); + cx.foreground_executor().spawn(async move { + let versions = versions.await?; + let latest = versions + .into_iter() + .max_by_key(|v| v.published_at) + .context("no extension found")?; + Ok(vec![latest]) + }) + } else { + extension_store.update(cx, |store, cx| { + store.fetch_extensions(search.as_deref(), provides_filter.as_ref(), cx) + }) + }; cx.spawn(async move |this, cx| { let dev_extensions = if let Some(search) = search { @@ -1165,6 +1192,13 @@ impl ExtensionsPage { self.refresh_feature_upsells(cx); } + pub fn focus_extension(&mut self, id: &str, window: &mut Window, cx: &mut Context<Self>) { + self.query_editor.update(cx, |editor, cx| { + editor.set_text(format!("id:{id}"), window, cx) + }); + self.refresh_search(cx); + } + pub fn change_provides_filter( &mut self, provides_filter: Option<ExtensionProvides>, diff --git a/crates/theme_selector/src/icon_theme_selector.rs b/crates/theme_selector/src/icon_theme_selector.rs index 40ba7bd5a6e8381f3c11331d73aa9215f555ec8f..1adfc4b5d8183479c5c449d49596c397a6f02dfd 100644 --- a/crates/theme_selector/src/icon_theme_selector.rs +++ b/crates/theme_selector/src/icon_theme_selector.rs @@ -327,6 +327,7 @@ impl PickerDelegate for IconThemeSelectorDelegate { window.dispatch_action( Box::new(Extensions { category_filter: Some(ExtensionCategoryFilter::IconThemes), + id: None, }), cx, ); diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index 09d9877df874f192365a7bd595a62ee3cb108846..022daced7aaf02d095e4789e9caf690382e58753 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -385,6 +385,7 @@ impl PickerDelegate for ThemeSelectorDelegate { window.dispatch_action( Box::new(Extensions { category_filter: Some(ExtensionCategoryFilter::Themes), + id: None, }), cx, ); diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 6309c3a1373b2ce30db1f7eac0d1449f52ed4f7d..5eb96f21a4b13e5316863af0f7a20703f5506ee2 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -746,6 +746,23 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut return; } + if let Some(extension) = request.extension_id { + cx.spawn(async move |cx| { + let workspace = workspace::get_any_active_workspace(app_state, cx.clone()).await?; + workspace.update(cx, |_, window, cx| { + window.dispatch_action( + Box::new(zed_actions::Extensions { + category_filter: None, + id: Some(extension), + }), + cx, + ); + }) + }) + .detach_and_log_err(cx); + return; + } + if let Some(connection_options) = request.ssh_connection { cx.spawn(async move |mut cx| { let paths: Vec<PathBuf> = request.open_paths.into_iter().map(PathBuf::from).collect(); diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index 0fb08d1be5790557674ee91a08cd35d28ea0b062..42eb8198a4c091d0ce6dd4ecbae3f0ced7bdf7d3 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -37,6 +37,7 @@ pub struct OpenRequest { pub join_channel: Option<u64>, pub ssh_connection: Option<SshConnectionOptions>, pub dock_menu_action: Option<usize>, + pub extension_id: Option<String>, } impl OpenRequest { @@ -54,6 +55,8 @@ impl OpenRequest { } else if let Some(file) = url.strip_prefix("zed://ssh") { let ssh_url = "ssh:/".to_string() + file; this.parse_ssh_file_path(&ssh_url, cx)? + } else if let Some(file) = url.strip_prefix("zed://extension/") { + this.extension_id = Some(file.to_string()) } else if url.starts_with("ssh://") { this.parse_ssh_file_path(&url, cx)? } else if let Some(request_path) = parse_zed_link(&url, cx) { diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 06121a9de8e0b68316c8ffda1d4a393beedb217f..fc7d98178edfce397ae4600b17b7bbac4a1cb9c6 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -76,6 +76,9 @@ pub struct Extensions { /// Filters the extensions page down to extensions that are in the specified category. #[serde(default)] pub category_filter: Option<ExtensionCategoryFilter>, + /// Focuses just the extension with the specified ID. + #[serde(default)] + pub id: Option<String>, } /// Decreases the font size in the editor buffer. From 572d3d637a68281acf8e848ec86e008da3efb194 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Tue, 15 Jul 2025 16:06:50 -0400 Subject: [PATCH 095/658] Rename `action_input` to `action_arguments` in keybinding contexts (#34480) Release Notes: - N/A --- crates/settings/src/keymap_file.rs | 52 ++++++++--------- crates/settings_ui/src/keybindings.rs | 83 +++++++++++++++------------ 2 files changed, 71 insertions(+), 64 deletions(-) diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 78e306ed632dde43c3671ce3f746a9d198893f3c..470c5faf78d7e7b41d8c4895e471b82b557a5c3a 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -623,7 +623,7 @@ impl KeymapFile { target_keybind_source, } if target_keybind_source != KeybindSource::User => { target.action_name = gpui::NoAction.name(); - target.input.take(); + target.action_arguments.take(); operation = KeybindUpdateOperation::Add(target); } _ => {} @@ -848,17 +848,17 @@ pub struct KeybindUpdateTarget<'a> { pub keystrokes: &'a [Keystroke], pub action_name: &'a str, pub use_key_equivalents: bool, - pub input: Option<&'a str>, + pub action_arguments: Option<&'a str>, } impl<'a> KeybindUpdateTarget<'a> { fn action_value(&self) -> Result<Value> { let action_name: Value = self.action_name.into(); - let value = match self.input { - Some(input) => { - let input = serde_json::from_str::<Value>(input) - .context("Failed to parse action input as JSON")?; - serde_json::json!([action_name, input]) + let value = match self.action_arguments { + Some(args) => { + let args = serde_json::from_str::<Value>(args) + .context("Failed to parse action arguments as JSON")?; + serde_json::json!([action_name, args]) } None => action_name, }; @@ -986,7 +986,7 @@ mod tests { action_name: "zed::SomeAction", context: None, use_key_equivalents: false, - input: None, + action_arguments: None, }), r#"[ { @@ -1012,7 +1012,7 @@ mod tests { action_name: "zed::SomeOtherAction", context: None, use_key_equivalents: false, - input: None, + action_arguments: None, }), r#"[ { @@ -1043,7 +1043,7 @@ mod tests { action_name: "zed::SomeOtherAction", context: None, use_key_equivalents: false, - input: Some(r#"{"foo": "bar"}"#), + action_arguments: Some(r#"{"foo": "bar"}"#), }), r#"[ { @@ -1079,7 +1079,7 @@ mod tests { action_name: "zed::SomeOtherAction", context: Some("Zed > Editor && some_condition = true"), use_key_equivalents: true, - input: Some(r#"{"foo": "bar"}"#), + action_arguments: Some(r#"{"foo": "bar"}"#), }), r#"[ { @@ -1118,14 +1118,14 @@ mod tests { action_name: "zed::SomeAction", context: None, use_key_equivalents: false, - input: None, + action_arguments: None, }, source: KeybindUpdateTarget { keystrokes: &parse_keystrokes("ctrl-b"), action_name: "zed::SomeOtherAction", context: None, use_key_equivalents: false, - input: Some(r#"{"foo": "bar"}"#), + action_arguments: Some(r#"{"foo": "bar"}"#), }, target_keybind_source: KeybindSource::Base, }, @@ -1164,14 +1164,14 @@ mod tests { action_name: "zed::SomeAction", context: None, use_key_equivalents: false, - input: None, + action_arguments: None, }, source: KeybindUpdateTarget { keystrokes: &parse_keystrokes("ctrl-b"), action_name: "zed::SomeOtherAction", context: None, use_key_equivalents: false, - input: Some(r#"{"foo": "bar"}"#), + action_arguments: Some(r#"{"foo": "bar"}"#), }, target_keybind_source: KeybindSource::User, }, @@ -1205,14 +1205,14 @@ mod tests { action_name: "zed::SomeNonexistentAction", context: None, use_key_equivalents: false, - input: None, + action_arguments: None, }, source: KeybindUpdateTarget { keystrokes: &parse_keystrokes("ctrl-b"), action_name: "zed::SomeOtherAction", context: None, use_key_equivalents: false, - input: None, + action_arguments: None, }, target_keybind_source: KeybindSource::User, }, @@ -1248,14 +1248,14 @@ mod tests { action_name: "zed::SomeAction", context: None, use_key_equivalents: false, - input: None, + action_arguments: None, }, source: KeybindUpdateTarget { keystrokes: &parse_keystrokes("ctrl-b"), action_name: "zed::SomeOtherAction", context: None, use_key_equivalents: false, - input: Some(r#"{"foo": "bar"}"#), + action_arguments: Some(r#"{"foo": "bar"}"#), }, target_keybind_source: KeybindSource::User, }, @@ -1293,14 +1293,14 @@ mod tests { action_name: "foo::bar", context: Some("SomeContext"), use_key_equivalents: false, - input: None, + action_arguments: None, }, source: KeybindUpdateTarget { keystrokes: &parse_keystrokes("c"), action_name: "foo::baz", context: Some("SomeOtherContext"), use_key_equivalents: false, - input: None, + action_arguments: None, }, target_keybind_source: KeybindSource::User, }, @@ -1337,14 +1337,14 @@ mod tests { action_name: "foo::bar", context: Some("SomeContext"), use_key_equivalents: false, - input: None, + action_arguments: None, }, source: KeybindUpdateTarget { keystrokes: &parse_keystrokes("c"), action_name: "foo::baz", context: Some("SomeOtherContext"), use_key_equivalents: false, - input: None, + action_arguments: None, }, target_keybind_source: KeybindSource::User, }, @@ -1376,7 +1376,7 @@ mod tests { keystrokes: &parse_keystrokes("a"), action_name: "foo::bar", use_key_equivalents: false, - input: None, + action_arguments: None, }, target_keybind_source: KeybindSource::User, }, @@ -1408,7 +1408,7 @@ mod tests { keystrokes: &parse_keystrokes("a"), action_name: "foo::bar", use_key_equivalents: false, - input: Some("true"), + action_arguments: Some("true"), }, target_keybind_source: KeybindSource::User, }, @@ -1451,7 +1451,7 @@ mod tests { keystrokes: &parse_keystrokes("a"), action_name: "foo::bar", use_key_equivalents: false, - input: Some("true"), + action_arguments: Some("true"), }, target_keybind_source: KeybindSource::User, }, diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 3567439d2b3c85202d5fe0ceea5a481bab6be482..5b2cca92bb3f5c60d2e8386aabd3f41c44c85e32 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -520,9 +520,9 @@ impl KeymapEditor { let action_name = key_binding.action().name(); unmapped_action_names.remove(&action_name); - let action_input = key_binding + let action_arguments = key_binding .action_input() - .map(|input| SyntaxHighlightedText::new(input, json_language.clone())); + .map(|arguments| SyntaxHighlightedText::new(arguments, json_language.clone())); let action_docs = action_documentation.get(action_name).copied(); let index = processed_bindings.len(); @@ -531,7 +531,7 @@ impl KeymapEditor { keystroke_text: keystroke_text.into(), ui_key_binding, action_name: action_name.into(), - action_input, + action_arguments, action_docs, action_schema: action_schema.get(action_name).cloned(), context: Some(context), @@ -548,7 +548,7 @@ impl KeymapEditor { keystroke_text: empty.clone(), ui_key_binding: None, action_name: action_name.into(), - action_input: None, + action_arguments: None, action_docs: action_documentation.get(action_name).copied(), action_schema: action_schema.get(action_name).cloned(), context: None, @@ -961,7 +961,7 @@ struct ProcessedKeybinding { keystroke_text: SharedString, ui_key_binding: Option<ui::KeyBinding>, action_name: SharedString, - action_input: Option<SyntaxHighlightedText>, + action_arguments: Option<SyntaxHighlightedText>, action_docs: Option<&'static str>, action_schema: Option<schemars::Schema>, context: Option<KeybindContextString>, @@ -1244,8 +1244,8 @@ impl Render for KeymapEditor { binding.keystroke_text.clone().into_any_element(), IntoElement::into_any_element, ); - let action_input = match binding.action_input.clone() { - Some(input) => input.into_any_element(), + let action_arguments = match binding.action_arguments.clone() { + Some(arguments) => arguments.into_any_element(), None => { if binding.action_schema.is_some() { muted_styled_text(NO_ACTION_ARGUMENTS_TEXT, cx) @@ -1279,7 +1279,14 @@ impl Render for KeymapEditor { .map(|(_source, name)| name) .unwrap_or_default() .into_any_element(); - Some([icon, action, action_input, keystrokes, context, source]) + Some([ + icon, + action, + action_arguments, + keystrokes, + context, + source, + ]) }) .collect() }), @@ -1446,7 +1453,7 @@ struct KeybindingEditorModal { editing_keybind_idx: usize, keybind_editor: Entity<KeystrokeInput>, context_editor: Entity<SingleLineInput>, - input_editor: Option<Entity<Editor>>, + action_arguments_editor: Option<Entity<Editor>>, fs: Arc<dyn Fs>, error: Option<InputError>, keymap_editor: Entity<KeymapEditor>, @@ -1512,16 +1519,16 @@ impl KeybindingEditorModal { input }); - let input_editor = editing_keybind.action_schema.clone().map(|_schema| { + let action_arguments_editor = editing_keybind.action_schema.clone().map(|_schema| { cx.new(|cx| { let mut editor = Editor::auto_height_unbounded(1, window, cx); let workspace = workspace.clone(); - if let Some(input) = editing_keybind.action_input.clone() { - editor.set_text(input.text, window, cx); + if let Some(arguments) = editing_keybind.action_arguments.clone() { + editor.set_text(arguments.text, window, cx); } else { // TODO: default value from schema? - editor.set_placeholder_text("Action Input", cx); + editor.set_placeholder_text("Action Arguments", cx); } cx.spawn(async |editor, cx| { let json_language = load_json_language(workspace, cx).await; @@ -1533,7 +1540,7 @@ impl KeybindingEditorModal { }); } }) - .context("Failed to load JSON language for editing keybinding action input") + .context("Failed to load JSON language for editing keybinding action arguments input") }) .detach_and_log_err(cx); editor @@ -1542,8 +1549,8 @@ impl KeybindingEditorModal { let focus_state = KeybindingEditorModalFocusState::new( keybind_editor.read_with(cx, |keybind_editor, cx| keybind_editor.focus_handle(cx)), - input_editor.as_ref().map(|input_editor| { - input_editor.read_with(cx, |input_editor, cx| input_editor.focus_handle(cx)) + action_arguments_editor.as_ref().map(|args_editor| { + args_editor.read_with(cx, |args_editor, cx| args_editor.focus_handle(cx)) }), context_editor.read_with(cx, |context_editor, cx| context_editor.focus_handle(cx)), ); @@ -1555,7 +1562,7 @@ impl KeybindingEditorModal { fs, keybind_editor, context_editor, - input_editor, + action_arguments_editor, error: None, keymap_editor, workspace, @@ -1577,22 +1584,22 @@ impl KeybindingEditorModal { } } - fn validate_action_input(&self, cx: &App) -> anyhow::Result<Option<String>> { - let input = self - .input_editor + fn validate_action_arguments(&self, cx: &App) -> anyhow::Result<Option<String>> { + let action_arguments = self + .action_arguments_editor .as_ref() .map(|editor| editor.read(cx).text(cx)); - let value = input + let value = action_arguments .as_ref() - .map(|input| { - serde_json::from_str(input).context("Failed to parse action input as JSON") + .map(|args| { + serde_json::from_str(args).context("Failed to parse action arguments as JSON") }) .transpose()?; cx.build_action(&self.editing_keybind.action_name, value) - .context("Failed to validate action input")?; - Ok(input) + .context("Failed to validate action arguments")?; + Ok(action_arguments) } fn save(&mut self, cx: &mut Context<Self>) { @@ -1622,7 +1629,7 @@ impl KeybindingEditorModal { return; } - let new_input = match self.validate_action_input(cx) { + let new_action_args = match self.validate_action_arguments(cx) { Err(input_err) => { self.set_error(InputError::error(input_err.to_string()), cx); return; @@ -1707,7 +1714,7 @@ impl KeybindingEditorModal { existing_keybind, &new_keystrokes, new_context.as_deref(), - new_input.as_deref(), + new_action_args.as_deref(), &fs, tab_size, ) @@ -1812,7 +1819,7 @@ impl Render for KeybindingEditorModal { .gap_1() .child(self.keybind_editor.clone()), ) - .when_some(self.input_editor.clone(), |this, editor| { + .when_some(self.action_arguments_editor.clone(), |this, editor| { this.child( v_flex() .mt_1p5() @@ -2049,7 +2056,7 @@ async fn save_keybinding_update( existing: ProcessedKeybinding, new_keystrokes: &[Keystroke], new_context: Option<&str>, - new_input: Option<&str>, + new_args: Option<&str>, fs: &Arc<dyn Fs>, tab_size: usize, ) -> anyhow::Result<()> { @@ -2063,10 +2070,10 @@ async fn save_keybinding_update( .context .as_ref() .and_then(KeybindContextString::local_str); - let existing_input = existing - .action_input + let existing_args = existing + .action_arguments .as_ref() - .map(|input| input.text.as_ref()); + .map(|args| args.text.as_ref()); settings::KeybindUpdateOperation::Replace { target: settings::KeybindUpdateTarget { @@ -2074,7 +2081,7 @@ async fn save_keybinding_update( keystrokes: existing_keystrokes, action_name: &existing.action_name, use_key_equivalents: false, - input: existing_input, + action_arguments: existing_args, }, target_keybind_source: existing .source @@ -2086,7 +2093,7 @@ async fn save_keybinding_update( keystrokes: new_keystrokes, action_name: &existing.action_name, use_key_equivalents: false, - input: new_input, + action_arguments: new_args, }, } } else { @@ -2095,7 +2102,7 @@ async fn save_keybinding_update( keystrokes: new_keystrokes, action_name: &existing.action_name, use_key_equivalents: false, - input: new_input, + action_arguments: new_args, }) }; let updated_keymap_contents = @@ -2131,10 +2138,10 @@ async fn remove_keybinding( keystrokes, action_name: &existing.action_name, use_key_equivalents: false, - input: existing - .action_input + action_arguments: existing + .action_arguments .as_ref() - .map(|input| input.text.as_ref()), + .map(|arguments| arguments.text.as_ref()), }, target_keybind_source: existing .source From 0ada4ce900a13af97c732cba7d3eae240749e4b8 Mon Sep 17 00:00:00 2001 From: Smit Barmase <heysmitbarmase@gmail.com> Date: Wed, 16 Jul 2025 01:47:40 +0530 Subject: [PATCH 096/658] editor: Add ToggleFocus action (#34495) This PR adds action `editor: toggle focus` which focuses to last active editor pane item in workspace. Release Notes: - Added `editor: toggle focus` action, which focuses to last active editor pane item. --------- Co-authored-by: Danilo Leal <daniloleal09@gmail.com> --- crates/editor/src/actions.rs | 2 ++ crates/editor/src/editor.rs | 13 +++++++++++++ crates/workspace/src/workspace.rs | 21 +++++++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index c4866179c1d98bc1a36001b469cabe875ea42806..87463d246d2aba96485247467b15025c58f9d5d5 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -425,6 +425,8 @@ actions!( FoldRecursive, /// Folds the selected ranges. FoldSelectedRanges, + /// Toggles focus back to the last active buffer. + ToggleFocus, /// Toggles folding at the current position. ToggleFold, /// Toggles recursive folding at the current position. diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index acd9c23c97682f648fe3389e8c0466be4d7b9667..e5ff75561594746a17a228ee1c466f003097e37a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -356,6 +356,7 @@ pub fn init(cx: &mut App) { workspace.register_action(Editor::new_file_vertical); workspace.register_action(Editor::new_file_horizontal); workspace.register_action(Editor::cancel_language_server_work); + workspace.register_action(Editor::toggle_focus); }, ) .detach(); @@ -16954,6 +16955,18 @@ impl Editor { cx.notify(); } + pub fn toggle_focus( + workspace: &mut Workspace, + _: &actions::ToggleFocus, + window: &mut Window, + cx: &mut Context<Workspace>, + ) { + let Some(item) = workspace.recent_active_item_by_type::<Self>(cx) else { + return; + }; + workspace.activate_item(&item, true, true, window, cx); + } + pub fn toggle_fold( &mut self, _: &actions::ToggleFold, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index dc2c6516dd6892feb7749daf5bf654db94bfb11e..be5d693d356e3b328d1f3d0575a76df5c429f7dc 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1711,6 +1711,27 @@ impl Workspace { history } + pub fn recent_active_item_by_type<T: 'static>(&self, cx: &App) -> Option<Entity<T>> { + let mut recent_item: Option<Entity<T>> = None; + let mut recent_timestamp = 0; + for pane_handle in &self.panes { + let pane = pane_handle.read(cx); + 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); + } + } + } + } + } + recent_item + } + pub fn recent_navigation_history_iter( &self, cx: &App, From 0ebbeec11cadd93e0a2086fd3bfd4dfbfaaa20da Mon Sep 17 00:00:00 2001 From: Cole Miller <cole@zed.dev> Date: Tue, 15 Jul 2025 17:06:46 -0400 Subject: [PATCH 097/658] debugger: Remove `Start` button from the attach modal (#34496) Right now it doesn't work at all (the PID doesn't get set in the generated scenario), and it's sort of redundant with the picker functionality. Release Notes: - N/A --- crates/debugger_ui/src/new_process_modal.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/crates/debugger_ui/src/new_process_modal.rs b/crates/debugger_ui/src/new_process_modal.rs index 6d7fa244a2e2bfaaaa82f1321d446627e2b0c343..42f77ab056889653e108bfbda05f9fe7a0b270ad 100644 --- a/crates/debugger_ui/src/new_process_modal.rs +++ b/crates/debugger_ui/src/new_process_modal.rs @@ -766,14 +766,7 @@ impl Render for NewProcessModal { )) .child( h_flex() - .child(div().child(self.adapter_drop_down_menu(window, cx))) - .child( - Button::new("debugger-spawn", "Start") - .on_click(cx.listener(|this, _, window, cx| { - this.start_new_session(window, cx) - })) - .disabled(disabled), - ), + .child(div().child(self.adapter_drop_down_menu(window, cx))), ) }), NewProcessMode::Debug => el, From 0a3ef40c2fb7e4162c78f4014e953325dd61f29e Mon Sep 17 00:00:00 2001 From: Cole Miller <cole@zed.dev> Date: Tue, 15 Jul 2025 18:31:28 -0400 Subject: [PATCH 098/658] debugger: Interpret user-specified debug adapter binary paths in a more intuitive way for JS and Python (#33926) Previously we would append `js-debug/src/dapDebugServer.js` to the value of the `dap.JavaScript.binary` setting and `src/debugpy/adapter` to the value of the `dap.Debugpy.binary` setting, which isn't particularly intuitive. This PR fixes that. Release Notes: - debugger: Made the semantics of the `dap.$ADAPTER.binary` setting more intuitive for the `JavaScript` and `Debugpy` adapters. In the new semantics, this should be the path to `dapDebugServer.js` for `JavaScript` and the path to the `src/debugpy/adapter` directory for `Debugpy`. --------- Co-authored-by: Remco Smits <djsmits12@gmail.com> --- crates/dap_adapters/src/javascript.rs | 42 +++++++++++---------------- crates/dap_adapters/src/python.rs | 11 ++----- 2 files changed, 20 insertions(+), 33 deletions(-) diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index a51377cd76dd7ab1702c263378d0bf2904f27a6f..2d19921a0f0c979fe53ede5860ac0c4d26b510c3 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -54,20 +54,6 @@ impl JsDebugAdapter { user_args: Option<Vec<String>>, _: &mut AsyncApp, ) -> Result<DebugAdapterBinary> { - let adapter_path = if let Some(user_installed_path) = user_installed_path { - user_installed_path - } else { - let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref()); - - let file_name_prefix = format!("{}_", self.name()); - - util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { - file_name.starts_with(&file_name_prefix) - }) - .await - .context("Couldn't find JavaScript dap directory")? - }; - let tcp_connection = task_definition.tcp_connection.clone().unwrap_or_default(); let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?; @@ -136,21 +122,27 @@ impl JsDebugAdapter { .or_insert(true.into()); } + let adapter_path = if let Some(user_installed_path) = user_installed_path { + user_installed_path + } else { + let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref()); + + let file_name_prefix = format!("{}_", self.name()); + + util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { + file_name.starts_with(&file_name_prefix) + }) + .await + .context("Couldn't find JavaScript dap directory")? + .join(Self::ADAPTER_PATH) + }; + let arguments = if let Some(mut args) = user_args { - args.insert( - 0, - adapter_path - .join(Self::ADAPTER_PATH) - .to_string_lossy() - .to_string(), - ); + args.insert(0, adapter_path.to_string_lossy().to_string()); args } else { vec![ - adapter_path - .join(Self::ADAPTER_PATH) - .to_string_lossy() - .to_string(), + adapter_path.to_string_lossy().to_string(), port.to_string(), host.to_string(), ] diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index dc3d15e124578e183ba5ed09b80aee7d6dda54c8..eb541bde8e7df233c549656f7640ee45bb0f6c06 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -40,12 +40,7 @@ impl PythonDebugAdapter { "Using user-installed debugpy adapter from: {}", user_installed_path.display() ); - vec![ - user_installed_path - .join(Self::ADAPTER_PATH) - .to_string_lossy() - .to_string(), - ] + 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()] @@ -700,7 +695,7 @@ mod tests { let port = 5678; // Case 1: User-defined debugpy path (highest precedence) - let user_path = PathBuf::from("/custom/path/to/debugpy"); + let user_path = PathBuf::from("/custom/path/to/debugpy/src/debugpy/adapter"); let user_args = PythonDebugAdapter::generate_debugpy_arguments( &host, port, @@ -717,7 +712,7 @@ mod tests { .await .unwrap(); - assert!(user_args[0].ends_with("src/debugpy/adapter")); + 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"); From afbd2b760f6254e7de580f0923f65c128f74433b Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 15 Jul 2025 20:10:44 -0300 Subject: [PATCH 099/658] agent: Add plan chip in the Zed section within the settings view (#34503) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit | Free | Pro | |--------|--------| | <img width="1140" height="368" alt="CleanShot 2025-07-15 at 7  50 48@2x" src="https://github.com/user-attachments/assets/b54fd46d-d823-4689-b099-0a9aef8b1c9a" /> | <img width="1136" height="348" alt="CleanShot 2025-07-15 at 7  51 45@2x" src="https://github.com/user-attachments/assets/d291a1f5-511f-43df-9ce2-041c77d1cb86" /> | Release Notes: - agent: Added a chip communicating which Zed plan you're subscribed to in the agent panel settings view. --- crates/agent_ui/src/agent_configuration.rs | 83 ++++++++++++++++++++-- 1 file changed, 76 insertions(+), 7 deletions(-) diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 579331c9acd07d1e41a1ceaee7fe2452bb0c1591..699a776330b0fda13417e5ac3e5400345192fc94 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -24,6 +24,7 @@ use project::{ context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore}, project_settings::{ContextServerSettings, ProjectSettings}, }; +use proto::Plan; use settings::{Settings, update_settings_file}; use ui::{ ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu, @@ -171,6 +172,15 @@ impl AgentConfiguration { .copied() .unwrap_or(false); + let is_zed_provider = provider.id() == ZED_CLOUD_PROVIDER_ID; + let current_plan = if is_zed_provider { + self.workspace + .upgrade() + .and_then(|workspace| workspace.read(cx).user_store().read(cx).current_plan()) + } else { + None + }; + v_flex() .when(is_expanded, |this| this.mb_2()) .child( @@ -208,14 +218,31 @@ impl AgentConfiguration { .size(IconSize::Small) .color(Color::Muted), ) - .child(Label::new(provider_name.clone()).size(LabelSize::Large)) - .when( - provider.is_authenticated(cx) && !is_expanded, - |parent| { - parent.child( - Icon::new(IconName::Check).color(Color::Success), + .child( + h_flex() + .gap_1() + .child( + Label::new(provider_name.clone()) + .size(LabelSize::Large), ) - }, + .map(|this| { + if is_zed_provider { + this.child( + self.render_zed_plan_info(current_plan, cx), + ) + } else { + this.when( + provider.is_authenticated(cx) + && !is_expanded, + |parent| { + parent.child( + Icon::new(IconName::Check) + .color(Color::Success), + ) + }, + ) + } + }), ), ) .child( @@ -431,6 +458,48 @@ impl AgentConfiguration { .child(self.render_sound_notification(cx)) } + fn render_zed_plan_info(&self, plan: Option<Plan>, cx: &mut Context<Self>) -> impl IntoElement { + if let Some(plan) = plan { + let free_chip_bg = cx + .theme() + .colors() + .editor_background + .opacity(0.5) + .blend(cx.theme().colors().text_accent.opacity(0.05)); + + let pro_chip_bg = cx + .theme() + .colors() + .editor_background + .opacity(0.5) + .blend(cx.theme().colors().text_accent.opacity(0.2)); + + let (plan_name, plan_color, bg_color) = match plan { + Plan::Free => ("Free", Color::Default, free_chip_bg), + Plan::ZedProTrial => ("Pro Trial", Color::Accent, pro_chip_bg), + Plan::ZedPro => ("Pro", Color::Accent, pro_chip_bg), + }; + + h_flex() + .ml_1() + .px_1() + .rounded_sm() + .border_1() + .border_color(cx.theme().colors().border) + .bg(bg_color) + .overflow_hidden() + .child( + Label::new(plan_name.to_string()) + .color(plan_color) + .size(LabelSize::XSmall) + .buffer_font(cx), + ) + .into_any_element() + } else { + div().into_any_element() + } + } + fn render_context_servers_section( &mut self, window: &mut Window, From 7ca3d969e04c46d86b1263c428381e01b43c8c37 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 16 Jul 2025 01:32:48 +0200 Subject: [PATCH 100/658] debugger: Highlight the size of jumped-to memory (#34504) Closes #ISSUE Release Notes: - N/A --- .../src/session/running/memory_view.rs | 23 ++++++++++++++----- crates/project/src/debugger/session.rs | 6 +++-- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/crates/debugger_ui/src/session/running/memory_view.rs b/crates/debugger_ui/src/session/running/memory_view.rs index 9d946449544ecfd1a9c91c11918aaf1becb3d4d0..eb77604bee1cfa0ce48d6054c3c7231a4964d23f 100644 --- a/crates/debugger_ui/src/session/running/memory_view.rs +++ b/crates/debugger_ui/src/session/running/memory_view.rs @@ -159,6 +159,11 @@ impl MemoryView { open_context_menu: None, }; this.change_query_bar_mode(false, window, cx); + cx.on_focus_out(&this.focus_handle, window, |this, _, window, cx| { + this.change_query_bar_mode(false, window, cx); + cx.notify(); + }) + .detach(); this } fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) { @@ -583,16 +588,22 @@ impl MemoryView { else { return; }; + let expr = format!("?${{{expr}}}"); let reference = self.session.update(cx, |this, cx| { this.memory_reference_of_expr(selected_frame, expr, cx) }); cx.spawn(async move |this, cx| { - if let Some(reference) = reference.await { + if let Some((reference, typ)) = reference.await { _ = this.update(cx, |this, cx| { - let Ok(address) = parse_int::parse::<u64>(&reference) else { - return; + let sizeof_expr = if typ.as_ref().is_some_and(|t| { + t.chars() + .all(|c| c.is_whitespace() || c.is_alphabetic() || c == '*') + }) { + typ.as_deref() + } else { + None }; - this.jump_to_address(address, cx); + this.go_to_memory_reference(&reference, sizeof_expr, selected_frame, cx); }); } }) @@ -763,7 +774,7 @@ fn render_single_memory_view_line( this.when(selection.contains(base_address + cell_ix as u64), |this| { let weak = weak.clone(); - this.bg(Color::Accent.color(cx)).when( + this.bg(Color::Selected.color(cx).opacity(0.2)).when( !selection.is_dragging(), |this| { let selection = selection.drag().memory_range(); @@ -860,7 +871,7 @@ fn render_single_memory_view_line( .px_0p5() .when_some(view_state.selection.as_ref(), |this, selection| { this.when(selection.contains(base_address + ix as u64), |this| { - this.bg(Color::Accent.color(cx)) + this.bg(Color::Selected.color(cx).opacity(0.2)) }) }) .child( diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index cf157ce4f92173d3202be0470d01d049c1c5e87e..1e296ac2ac9b87a9fae4c0aaa8ae9fb474f64eb2 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -1787,7 +1787,7 @@ impl Session { frame_id: Option<u64>, expression: String, cx: &mut Context<Self>, - ) -> Task<Option<String>> { + ) -> Task<Option<(String, Option<String>)>> { let request = self.request( EvaluateCommand { expression, @@ -1801,7 +1801,9 @@ impl Session { ); cx.background_spawn(async move { let result = request.await?; - result.memory_reference + result + .memory_reference + .map(|reference| (reference, result.type_)) }) } From fc24102491c3a644e53792c1a318b00bfdfd6d6b Mon Sep 17 00:00:00 2001 From: Conrad Irwin <conrad.irwin@gmail.com> Date: Tue, 15 Jul 2025 17:52:50 -0600 Subject: [PATCH 101/658] Tweaks to ACP for the Gemini PR (#34506) - **Update to use --experimental-acp** - **Fix tool locations** Closes #ISSUE Release Notes: - N/A *or* Added/Fixed/Improved ... --------- Co-authored-by: mkorwel <matt.korwel@gmail.com> Co-authored-by: Agus Zubiaga <agus@zed.dev> --- crates/acp/src/acp.rs | 47 +++++++++++++++++++++-- crates/agent_servers/src/agent_servers.rs | 2 +- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/crates/acp/src/acp.rs b/crates/acp/src/acp.rs index 8351aeaee0ef1d12a6db938aa3949d7bd19ccb43..a7e72b0c2d59f8bfccb037b0e406308bcab947a0 100644 --- a/crates/acp/src/acp.rs +++ b/crates/acp/src/acp.rs @@ -1,10 +1,10 @@ pub use acp::ToolCallId; use agent_servers::AgentServer; -use agentic_coding_protocol::{self as acp, UserMessageChunk}; +use agentic_coding_protocol::{self as acp, ToolCallLocation, UserMessageChunk}; use anyhow::{Context as _, Result, anyhow}; use assistant_tool::ActionLog; use buffer_diff::BufferDiff; -use editor::{MultiBuffer, PathKey}; +use editor::{Bias, MultiBuffer, PathKey}; use futures::{FutureExt, channel::oneshot, future::BoxFuture}; use gpui::{AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity}; use itertools::Itertools; @@ -769,6 +769,11 @@ impl AcpThread { status, }; + let location = call.locations.last().cloned(); + if let Some(location) = location { + self.set_project_location(location, cx) + } + self.push_entry(AgentThreadEntry::ToolCall(call), cx); id @@ -831,6 +836,11 @@ impl AcpThread { } } + let location = call.locations.last().cloned(); + if let Some(location) = location { + self.set_project_location(location, cx) + } + cx.emit(AcpThreadEvent::EntryUpdated(ix)); Ok(()) } @@ -852,6 +862,37 @@ impl AcpThread { } } + pub fn set_project_location(&self, location: ToolCallLocation, cx: &mut Context<Self>) { + 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, + ); + }) + }) + .detach_and_log_err(cx); + }); + } + /// Returns true if the last turn is awaiting tool authorization pub fn waiting_for_tool_confirmation(&self) -> bool { for entry in self.entries.iter().rev() { @@ -1780,7 +1821,7 @@ mod tests { Ok(AgentServerCommand { path: "node".into(), - args: vec![cli_path, "--acp".into()], + args: vec![cli_path, "--experimental-acp".into()], env: None, }) } diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index 5d588cd4aea0f863203201de82b0614cc210e615..ba43122570323f43398802583ed9d60c4adadf7f 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -56,7 +56,7 @@ pub trait AgentServer: Send { ) -> impl Future<Output = Result<AgentServerVersion>> + Send; } -const GEMINI_ACP_ARG: &str = "--acp"; +const GEMINI_ACP_ARG: &str = "--experimental-acp"; impl AgentServer for Gemini { async fn command( From ae65ff95a6b59ae52f0b401b9827b998cd792220 Mon Sep 17 00:00:00 2001 From: Peter Tripp <peter@zed.dev> Date: Tue, 15 Jul 2025 21:24:35 -0400 Subject: [PATCH 102/658] ci: Disable FreeBSD builds (#34511) Recently FreeBSD zed-remote-server builds are failing 90%+ of the time for unknown reasons. Temporarily suspend them. Example failing builds: - [2025-07-15 16:15 Nightly Failure](https://github.com/zed-industries/zed/actions/runs/16302777887/job/46042358675) - [2025-07-15 12:20 Nightly Success](https://github.com/zed-industries/zed/actions/runs/16297907892/job/46025281518) - [2025-07-14 08:21 Nightly Failure](https://github.com/zed-industries/zed/actions/runs/16266193889/job/45923004940) - [2025-06-17 Nightly Failure](https://github.com/zed-industries/zed/actions/runs/15700462603/job/44234573761) Release Notes: - Temporarily disable FreeBSD zed-remote-server builds due to CI failures. --- .github/workflows/ci.yml | 4 +++- .github/workflows/release_nightly.yml | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ea352a9320827e25cfbf4f94dfcb28bdd9fba0d5..98b70ad834808e2814e3db359e0fe5e6458d2364 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -679,8 +679,10 @@ 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') + ) needs: [linux_tests] name: Build Zed on FreeBSD steps: @@ -798,7 +800,7 @@ jobs: if: | startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre') - needs: [bundle-mac, bundle-linux-x86_x64, bundle-linux-aarch64, bundle-windows-x64, freebsd] + needs: [bundle-mac, bundle-linux-x86_x64, bundle-linux-aarch64, bundle-windows-x64] runs-on: - self-hosted - bundle diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index 1b9669c5d527f568ea8cc6b3918feae92d8b44e0..4be20525f97039bfa55f362aeffe9863f378df8d 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -187,7 +187,7 @@ jobs: freebsd: timeout-minutes: 60 - if: github.repository_owner == 'zed-industries' + if: false && github.repository_owner == 'zed-industries' runs-on: github-8vcpu-ubuntu-2404 needs: tests name: Build Zed on FreeBSD From ee4b9a27a2b14936cb28f8ec4a4d842174b6951a Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 15 Jul 2025 23:51:12 -0300 Subject: [PATCH 103/658] ui: Fix wrapping in the banner component (#34516) Also removing the `icon` field as the banner component always renders with an icon anyway. Hopefully, this fixes any weird text wrapping that was happening before. Release Notes: - N/A --- crates/ui/src/components/banner.rs | 51 ++++++++++++++---------------- 1 file changed, 23 insertions(+), 28 deletions(-) diff --git a/crates/ui/src/components/banner.rs b/crates/ui/src/components/banner.rs index 043791cdd86ccf6a94fb469356bd2aca7abaddf4..b16ca795b4b0c6f0ef4332d54f3db75ae8e42103 100644 --- a/crates/ui/src/components/banner.rs +++ b/crates/ui/src/components/banner.rs @@ -19,8 +19,8 @@ pub enum Severity { /// use ui::{Banner}; /// /// Banner::new() -/// .severity(Severity::Info) -/// .children(Label::new("This is an informational message")) +/// .severity(Severity::Success) +/// .children(Label::new("This is a success message")) /// .action_slot( /// Button::new("learn-more", "Learn More") /// .icon(IconName::ArrowUpRight) @@ -32,7 +32,6 @@ pub enum Severity { pub struct Banner { severity: Severity, children: Vec<AnyElement>, - icon: Option<(IconName, Option<Color>)>, action_slot: Option<AnyElement>, } @@ -42,7 +41,6 @@ impl Banner { Self { severity: Severity::Info, children: Vec::new(), - icon: None, action_slot: None, } } @@ -53,12 +51,6 @@ impl Banner { self } - /// Sets an icon to display in the banner with an optional color. - pub fn icon(mut self, icon: IconName, color: Option<impl Into<Color>>) -> Self { - self.icon = Some((icon, color.map(|c| c.into()))); - self - } - /// A slot for actions, such as CTA or dismissal buttons. pub fn action_slot(mut self, element: impl IntoElement) -> Self { self.action_slot = Some(element.into_any_element()); @@ -73,12 +65,13 @@ impl ParentElement for Banner { } impl RenderOnce for Banner { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let base = h_flex() + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + let banner = h_flex() .py_0p5() - .rounded_sm() + .gap_1p5() .flex_wrap() .justify_between() + .rounded_sm() .border_1(); let (icon, icon_color, bg_color, border_color) = match self.severity { @@ -108,29 +101,31 @@ impl RenderOnce for Banner { ), }; - let mut container = base.bg(bg_color).border_color(border_color); - - let mut content_area = h_flex().id("content_area").gap_1p5().overflow_x_scroll(); - - if self.icon.is_none() { - content_area = - content_area.child(Icon::new(icon).size(IconSize::XSmall).color(icon_color)); - } + let mut banner = banner.bg(bg_color).border_color(border_color); - content_area = content_area.children(self.children); + let icon_and_child = h_flex() + .items_start() + .min_w_0() + .gap_1p5() + .child( + h_flex() + .h(window.line_height()) + .flex_shrink_0() + .child(Icon::new(icon).size(IconSize::XSmall).color(icon_color)), + ) + .child(div().min_w_0().children(self.children)); if let Some(action_slot) = self.action_slot { - container = container + banner = banner .pl_2() - .pr_0p5() - .gap_2() - .child(content_area) + .pr_1() + .child(icon_and_child) .child(action_slot); } else { - container = container.px_2().child(div().w_full().child(content_area)); + banner = banner.px_2().child(icon_and_child); } - container + banner } } From 59d524427e1a4bf437b05dc5212ec36b393dabf9 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 16 Jul 2025 01:15:45 -0300 Subject: [PATCH 104/658] ui: Add Chip component (#34521) Possibly the simplest component in our set, but a nice one to have so we can standardize how it looks across the app. Release Notes: - N/A --- crates/agent_ui/src/agent_configuration.rs | 23 ++--- crates/component/src/component_layout.rs | 12 +-- crates/extensions_ui/src/extensions_ui.rs | 17 +--- crates/ui/src/components.rs | 2 + crates/ui/src/components/chip.rs | 106 ++++++++++++++++++++ crates/ui/src/components/keybinding_hint.rs | 2 +- crates/ui/src/components/tooltip.rs | 2 +- 7 files changed, 124 insertions(+), 40 deletions(-) create mode 100644 crates/ui/src/components/chip.rs diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 699a776330b0fda13417e5ac3e5400345192fc94..0697f5dee758f0a5c4d4f530e45394fb79e14f3a 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -27,7 +27,7 @@ use project::{ use proto::Plan; use settings::{Settings, update_settings_file}; use ui::{ - ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu, + Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu, Scrollbar, ScrollbarState, Switch, SwitchColor, Tooltip, prelude::*, }; use util::ResultExt as _; @@ -227,7 +227,7 @@ impl AgentConfiguration { ) .map(|this| { if is_zed_provider { - this.child( + this.gap_2().child( self.render_zed_plan_info(current_plan, cx), ) } else { @@ -474,26 +474,15 @@ impl AgentConfiguration { .opacity(0.5) .blend(cx.theme().colors().text_accent.opacity(0.2)); - let (plan_name, plan_color, bg_color) = match plan { + let (plan_name, label_color, bg_color) = match plan { Plan::Free => ("Free", Color::Default, free_chip_bg), Plan::ZedProTrial => ("Pro Trial", Color::Accent, pro_chip_bg), Plan::ZedPro => ("Pro", Color::Accent, pro_chip_bg), }; - h_flex() - .ml_1() - .px_1() - .rounded_sm() - .border_1() - .border_color(cx.theme().colors().border) - .bg(bg_color) - .overflow_hidden() - .child( - Label::new(plan_name.to_string()) - .color(plan_color) - .size(LabelSize::XSmall) - .buffer_font(cx), - ) + Chip::new(plan_name.to_string()) + .bg_color(bg_color) + .label_color(label_color) .into_any_element() } else { div().into_any_element() diff --git a/crates/component/src/component_layout.rs b/crates/component/src/component_layout.rs index b749ea20eab8b347b83bf34e35c33ec4ef5c614f..9fe52507d8a27abd3ba739f89a375e455a80f520 100644 --- a/crates/component/src/component_layout.rs +++ b/crates/component/src/component_layout.rs @@ -48,20 +48,20 @@ impl RenderOnce for ComponentExample { ) .child( div() - .flex() - .w_full() - .rounded_xl() .min_h(px(100.)) - .justify_center() + .w_full() .p_8() + .flex() + .items_center() + .justify_center() + .rounded_xl() .border_1() .border_color(cx.theme().colors().border.opacity(0.5)) .bg(pattern_slash( - cx.theme().colors().surface_background.opacity(0.5), + cx.theme().colors().surface_background.opacity(0.25), 12.0, 12.0, )) - .shadow_xs() .child(self.element), ) .into_any_element() diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index b944b1ec505178b56d0894b9790040ac73ede639..fe3e94f5c20dc1a78ae01defc24e290c18a1a3e6 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -24,7 +24,7 @@ use settings::Settings; use strum::IntoEnumIterator as _; use theme::ThemeSettings; use ui::{ - CheckboxWithLabel, ContextMenu, PopoverMenu, ScrollableHandle, Scrollbar, ScrollbarState, + CheckboxWithLabel, Chip, ContextMenu, PopoverMenu, ScrollableHandle, Scrollbar, ScrollbarState, ToggleButton, Tooltip, prelude::*, }; use vim_mode_setting::VimModeSetting; @@ -759,20 +759,7 @@ impl ExtensionsPage { _ => {} } - Some( - div() - .px_1() - .border_1() - .rounded_sm() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().element_background) - .child( - Label::new(extension_provides_label( - *provides, - )) - .size(LabelSize::XSmall), - ), - ) + Some(Chip::new(extension_provides_label(*provides))) }) .collect::<Vec<_>>(), ), diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index 88676e8a2bbe383538e91499a71ca908b2057203..9c2961c55f234f821e947bf3ee3254b2e1fbecab 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -2,6 +2,7 @@ mod avatar; mod banner; mod button; mod callout; +mod chip; mod content_group; mod context_menu; mod disclosure; @@ -43,6 +44,7 @@ pub use avatar::*; pub use banner::*; pub use button::*; pub use callout::*; +pub use chip::*; pub use content_group::*; pub use context_menu::*; pub use disclosure::*; diff --git a/crates/ui/src/components/chip.rs b/crates/ui/src/components/chip.rs new file mode 100644 index 0000000000000000000000000000000000000000..e1262875feae77b69e660c0e9da17e1e669137b7 --- /dev/null +++ b/crates/ui/src/components/chip.rs @@ -0,0 +1,106 @@ +use crate::prelude::*; +use gpui::{AnyElement, Hsla, IntoElement, ParentElement, Styled}; + +/// Chips provide a container for an informative label. +/// +/// # Usage Example +/// +/// ``` +/// use ui::{Chip}; +/// +/// Chip::new("This Chip") +/// ``` +#[derive(IntoElement, RegisterComponent)] +pub struct Chip { + label: SharedString, + label_color: Color, + label_size: LabelSize, + bg_color: Option<Hsla>, +} + +impl Chip { + /// Creates a new `Chip` component with the specified label. + pub fn new(label: impl Into<SharedString>) -> Self { + Self { + label: label.into(), + label_color: Color::Default, + label_size: LabelSize::XSmall, + bg_color: None, + } + } + + /// Sets the color of the label. + pub fn label_color(mut self, color: Color) -> Self { + self.label_color = color; + self + } + + /// Sets the size of the label. + pub fn label_size(mut self, size: LabelSize) -> Self { + self.label_size = size; + self + } + + /// Sets a custom background color for the callout content. + pub fn bg_color(mut self, color: Hsla) -> Self { + self.bg_color = Some(color); + self + } +} + +impl RenderOnce for Chip { + fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { + let bg_color = self + .bg_color + .unwrap_or(cx.theme().colors().element_background); + + h_flex() + .min_w_0() + .flex_initial() + .px_1() + .border_1() + .rounded_sm() + .border_color(cx.theme().colors().border) + .bg(bg_color) + .overflow_hidden() + .child( + Label::new(self.label) + .size(self.label_size) + .color(self.label_color) + .buffer_font(cx), + ) + } +} + +impl Component for Chip { + fn scope() -> ComponentScope { + ComponentScope::DataDisplay + } + + fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> { + let chip_examples = vec![ + single_example("Default", Chip::new("Chip Example").into_any_element()), + single_example( + "Customized Label Color", + Chip::new("Chip Example") + .label_color(Color::Accent) + .into_any_element(), + ), + single_example( + "Customized Label Size", + Chip::new("Chip Example") + .label_size(LabelSize::Large) + .label_color(Color::Accent) + .into_any_element(), + ), + single_example( + "Customized Background Color", + Chip::new("Chip Example") + .bg_color(cx.theme().colors().text_accent.opacity(0.1)) + .into_any_element(), + ), + ]; + + Some(example_group(chip_examples).vertical().into_any_element()) + } +} diff --git a/crates/ui/src/components/keybinding_hint.rs b/crates/ui/src/components/keybinding_hint.rs index d6dc094d415bec9991b83dfc50a865a838c1bdf4..a34ca40ed8c413d2edd6278dd035b93329dc5339 100644 --- a/crates/ui/src/components/keybinding_hint.rs +++ b/crates/ui/src/components/keybinding_hint.rs @@ -206,7 +206,7 @@ impl RenderOnce for KeybindingHint { impl Component for KeybindingHint { fn scope() -> ComponentScope { - ComponentScope::None + ComponentScope::DataDisplay } fn description() -> Option<&'static str> { diff --git a/crates/ui/src/components/tooltip.rs b/crates/ui/src/components/tooltip.rs index 18c9decc59f6f68b92542fbd56b6fae916195bfd..ed0fdd0114137256273f420acd647228bf605218 100644 --- a/crates/ui/src/components/tooltip.rs +++ b/crates/ui/src/components/tooltip.rs @@ -274,7 +274,7 @@ impl Render for LinkPreview { impl Component for Tooltip { fn scope() -> ComponentScope { - ComponentScope::None + ComponentScope::DataDisplay } fn description() -> Option<&'static str> { From 1ed3f9eb42b37f8cb141d6d04813e6bad4ed2ab9 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 16 Jul 2025 01:48:01 -0300 Subject: [PATCH 105/658] Add user handle and plan chip to the user menu (#34522) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A nicer way to visualize in which plan you're in and a bit of personalization by adding the GitHub handle you're signed with in the user menu, as a complement to the avatar photo itself. Taking advantage of the newly added Chip component. <img width="320" height="476" alt="CleanShot 2025-07-16 at 1  33 08@2x" src="https://github.com/user-attachments/assets/36718a42-27d1-499e-ac81-1eef2cd00347" /> Release Notes: - N/A --- crates/title_bar/src/title_bar.rs | 60 ++++++++++++++++++++++++------- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 5c916254125672850b2d9a403554fcb8ff140567..4b8902d14e54bbfef86008d499caf9a5eb7e5027 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -34,7 +34,7 @@ use std::sync::Arc; use theme::ActiveTheme; use title_bar_settings::TitleBarSettings; use ui::{ - Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon, IconName, IconSize, + Avatar, Button, ButtonLike, ButtonStyle, Chip, ContextMenu, Icon, IconName, IconSize, IconWithIndicator, Indicator, PopoverMenu, Tooltip, h_flex, prelude::*, }; use util::ResultExt; @@ -631,21 +631,55 @@ impl TitleBar { // 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_uri.clone(); + let free_chip_bg = cx + .theme() + .colors() + .editor_background + .opacity(0.5) + .blend(cx.theme().colors().text_accent.opacity(0.05)); + + let pro_chip_bg = cx + .theme() + .colors() + .editor_background + .opacity(0.5) + .blend(cx.theme().colors().text_accent.opacity(0.2)); + PopoverMenu::new("user-menu") .anchor(Corner::TopRight) .menu(move |window, cx| { ContextMenu::build(window, cx, |menu, _, _cx| { - menu.link( - format!( - "Current Plan: {}", - match plan { - None => "None", - Some(proto::Plan::Free) => "Zed Free", - Some(proto::Plan::ZedPro) => "Zed Pro", - Some(proto::Plan::ZedProTrial) => "Zed Pro (Trial)", - } - ), - zed_actions::OpenAccountSettings.boxed_clone(), + let user_login = user.github_login.clone(); + + let (plan_name, label_color, bg_color) = match plan { + None => ("None", Color::Default, free_chip_bg), + Some(proto::Plan::Free) => ("Free", Color::Default, free_chip_bg), + Some(proto::Plan::ZedProTrial) => { + ("Pro Trial", Color::Accent, pro_chip_bg) + } + Some(proto::Plan::ZedPro) => ("Pro", Color::Accent, pro_chip_bg), + }; + + menu.custom_entry( + move |_window, _cx| { + let user_login = user_login.clone(); + + h_flex() + .w_full() + .justify_between() + .child(Label::new(user_login)) + .child( + Chip::new(plan_name.to_string()) + .bg_color(bg_color) + .label_color(label_color), + ) + .into_any_element() + }, + move |_, cx| { + cx.open_url("https://zed.dev/account"); + }, ) .separator() .action("Settings", zed_actions::OpenSettings.boxed_clone()) @@ -675,7 +709,7 @@ impl TitleBar { .children( TitleBarSettings::get_global(cx) .show_user_picture - .then(|| Avatar::new(user.avatar_uri.clone())), + .then(|| Avatar::new(user_avatar)), ) .child( Icon::new(IconName::ChevronDown) From a52910382522ade3e83554820d838aebb82a4615 Mon Sep 17 00:00:00 2001 From: someone13574 <81528246+someone13574@users.noreply.github.com> Date: Tue, 15 Jul 2025 23:08:16 -0700 Subject: [PATCH 106/658] Disable format-on-save for verilog (#34512) Disables format-on-save by default for the [verilog extension](https://github.com/someone13574/zed-verilog-extension), since there isn't a standard style. Release Notes: - N/A --- assets/settings/default.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/assets/settings/default.json b/assets/settings/default.json index aa6e4399c387227dd557f9e30fb76006a75f4c2c..32d4c496c10cf31d1ae4b5f43a2996cb00eea5d0 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1671,6 +1671,10 @@ "allowed": true } }, + "SystemVerilog": { + "format_on_save": "off", + "use_on_type_format": false + }, "Vue.js": { "language_servers": ["vue-language-server", "..."], "prettier": { From 42b2b65241feafa7bdccfdc81972fd9bd8ab2922 Mon Sep 17 00:00:00 2001 From: Stephen Samra <stephen@stephensamra.com> Date: Wed, 16 Jul 2025 07:14:18 +0100 Subject: [PATCH 107/658] Document alternative method to providing intelephense license key (#34502) This PR updates the [Intelephense section in the docs](https://zed.dev/docs/languages/php#intelephense) to include an alternative way to provide the premium license key. Release Notes: - N/A --- docs/src/languages/php.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/src/languages/php.md b/docs/src/languages/php.md index 2ddb93c8d5b9465f7a68fb4f59ce8cb8225410ac..9cb7c40762e7af0f0680cbbcf564d4d989e7f0e9 100644 --- a/docs/src/languages/php.md +++ b/docs/src/languages/php.md @@ -27,7 +27,7 @@ which php ## Intelephense -[Intelephense](https://intelephense.com/) is a [proprietary](https://github.com/bmewburn/vscode-intelephense/blob/master/LICENSE.txt#L29) language server for PHP operating under a freemium model. Certain features require purchase of a [premium license](https://intelephense.com/). To use these features you must place your [licence.txt file](https://intelephense.com/faq.html) at `~/intelephense/licence.txt` inside your home directory. +[Intelephense](https://intelephense.com/) is a [proprietary](https://github.com/bmewburn/vscode-intelephense/blob/master/LICENSE.txt#L29) language server for PHP operating under a freemium model. Certain features require purchase of a [premium license](https://intelephense.com/). To switch to `intelephense`, add the following to your `settings.json`: @@ -41,6 +41,20 @@ To switch to `intelephense`, add the following to your `settings.json`: } ``` +To use the premium features, you can place your [licence.txt file](https://intelephense.com/faq.html) at `~/intelephense/licence.txt` inside your home directory. Alternatively, you can pass the licence key or a path to a file containing the licence key as an initialization option for the `intelephense` language server. To do this, add the following to your `settings.json`: + +```json +{ + "lsp": { + "intelephense": { + "initialization_options": { + "licenceKey": "/path/to/licence.txt" + } + } + } +} +``` + ## PHPDoc Zed supports syntax highlighting for PHPDoc comments. From 312369c84f826643ff0e5ebb7dc66ff4fe367dce Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 16 Jul 2025 12:00:36 +0200 Subject: [PATCH 108/658] debugger: Improve drag-and-scroll in memory views (#34526) Closes #34508 Release Notes: - N/A --- .../src/session/running/memory_view.rs | 74 +++++++++++-------- 1 file changed, 42 insertions(+), 32 deletions(-) diff --git a/crates/debugger_ui/src/session/running/memory_view.rs b/crates/debugger_ui/src/session/running/memory_view.rs index eb77604bee1cfa0ce48d6054c3c7231a4964d23f..499091ca0fe687934d8c386577bf3157f68c96ff 100644 --- a/crates/debugger_ui/src/session/running/memory_view.rs +++ b/crates/debugger_ui/src/session/running/memory_view.rs @@ -8,10 +8,10 @@ use std::{ use editor::{Editor, EditorElement, EditorStyle}; use gpui::{ - Action, AppContext, DismissEvent, Empty, Entity, FocusHandle, Focusable, MouseButton, - MouseMoveEvent, Point, ScrollStrategy, ScrollWheelEvent, Stateful, Subscription, Task, - TextStyle, UniformList, UniformListScrollHandle, WeakEntity, actions, anchored, bounds, - deferred, point, size, uniform_list, + Action, AppContext, DismissEvent, DragMoveEvent, Empty, Entity, FocusHandle, Focusable, + MouseButton, Point, ScrollStrategy, ScrollWheelEvent, Stateful, Subscription, Task, TextStyle, + UniformList, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, point, + uniform_list, }; use notifications::status_toast::{StatusToast, ToastIcon}; use project::debugger::{MemoryCell, dap_command::DataBreakpointContext, session::Session}; @@ -126,6 +126,8 @@ impl ViewState { } } +struct ScrollbarDragging; + static HEX_BYTES_MEMOIZED: LazyLock<[SharedString; 256]> = LazyLock::new(|| std::array::from_fn(|byte| SharedString::from(format!("{byte:02X}")))); static UNKNOWN_BYTE: SharedString = SharedString::new_static("??"); @@ -189,11 +191,14 @@ impl MemoryView { div() .occlude() .id("memory-view-vertical-scrollbar") - .on_mouse_move(cx.listener(|this, evt, _, cx| { - this.handle_drag(evt); + .on_drag_move(cx.listener(|this, evt, _, cx| { + let did_handle = this.handle_scroll_drag(evt); cx.notify(); - cx.stop_propagation() + if did_handle { + cx.stop_propagation() + } })) + .on_drag(ScrollbarDragging, |_, _, _, cx| cx.new(|_| Empty)) .on_hover(|_, _, cx| { cx.stop_propagation(); }) @@ -307,16 +312,12 @@ impl MemoryView { .detach(); } - fn handle_drag(&mut self, evt: &MouseMoveEvent) { - if !evt.dragging() { - return; - } - if !self.scroll_state.is_dragging() - && !self - .view_state - .selection - .as_ref() - .is_some_and(|selection| selection.is_dragging()) + fn handle_memory_drag(&mut self, evt: &DragMoveEvent<Drag>) { + if !self + .view_state + .selection + .as_ref() + .is_some_and(|selection| selection.is_dragging()) { return; } @@ -324,22 +325,31 @@ impl MemoryView { debug_assert!(row_count > 1); let scroll_handle = self.scroll_state.scroll_handle(); let viewport = scroll_handle.viewport(); - let (top_area, bottom_area) = { - let size = size(viewport.size.width, viewport.size.height / 10.); - ( - bounds(viewport.origin, size), - bounds( - point(viewport.origin.x, viewport.origin.y + size.height * 2.), - size, - ), - ) - }; - if bottom_area.contains(&evt.position) { - //ix == row_count - 1 { + if viewport.bottom() < evt.event.position.y { + self.view_state.schedule_scroll_down(); + } else if viewport.top() > evt.event.position.y { + self.view_state.schedule_scroll_up(); + } + } + + fn handle_scroll_drag(&mut self, evt: &DragMoveEvent<ScrollbarDragging>) -> bool { + if !self.scroll_state.is_dragging() { + return false; + } + let row_count = self.view_state.row_count(); + debug_assert!(row_count > 1); + let scroll_handle = self.scroll_state.scroll_handle(); + let viewport = scroll_handle.viewport(); + + if viewport.bottom() < evt.event.position.y { self.view_state.schedule_scroll_down(); - } else if top_area.contains(&evt.position) { + true + } else if viewport.top() > evt.event.position.y { self.view_state.schedule_scroll_up(); + true + } else { + false } } @@ -955,8 +965,8 @@ impl Render for MemoryView { .child( v_flex() .size_full() - .on_mouse_move(cx.listener(|this, evt: &MouseMoveEvent, _, _| { - this.handle_drag(evt); + .on_drag_move(cx.listener(|this, evt, _, _| { + this.handle_memory_drag(&evt); })) .child(self.render_memory(cx).size_full()) .children(self.open_context_menu.as_ref().map(|(menu, position, _)| { From c29c46d3b604e00234b44b4fc5cc7feaf9f92d9f Mon Sep 17 00:00:00 2001 From: Ragul R <85612319+rv-ragul@users.noreply.github.com> Date: Wed, 16 Jul 2025 18:22:37 +0530 Subject: [PATCH 109/658] Appropriately pick venv activation script (#33205) when `terminal.detect_venv.activate_script` setting is default, pick the appropriate activate script as per the `terminal.shell` settings specified by the user. Previously when the activate_script setting is default, zed always try to use the `activate` script, which only works when the user shell is `bash or zsh`. But what if the user is using `fish` shell in zed? Release Notes: - python: value of `activate_script` setting is now automatically inferred based on the kind of shell the user is running with. --------- Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> --- crates/project/src/terminals.rs | 43 +++++++++++++++++++++--- crates/terminal/src/terminal_settings.rs | 2 +- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index d3aec588ec126f64557f064c9c3d0fe225e284e3..3d62b4156b7b96caade454d5d05a3c02c44dae8c 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -16,7 +16,7 @@ use std::{ use task::{DEFAULT_REMOTE_SHELL, Shell, ShellBuilder, SpawnInTerminal}; use terminal::{ TaskState, TaskStatus, Terminal, TerminalBuilder, - terminal_settings::{self, TerminalSettings, VenvSettings}, + terminal_settings::{self, ActivateScript, TerminalSettings, VenvSettings}, }; use util::{ ResultExt, @@ -256,8 +256,11 @@ impl Project { let (spawn_task, shell) = match kind { TerminalKind::Shell(_) => { if let Some(python_venv_directory) = &python_venv_directory { - python_venv_activate_command = - this.python_activate_command(python_venv_directory, &settings.detect_venv); + python_venv_activate_command = this.python_activate_command( + python_venv_directory, + &settings.detect_venv, + &settings.shell, + ); } match ssh_details { @@ -510,10 +513,27 @@ impl Project { }) } + fn activate_script_kind(shell: Option<&str>) -> ActivateScript { + let shell_env = std::env::var("SHELL").ok(); + let shell_path = shell.or_else(|| shell_env.as_deref()); + let shell = std::path::Path::new(shell_path.unwrap_or("")) + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(""); + match shell { + "fish" => ActivateScript::Fish, + "tcsh" => ActivateScript::Csh, + "nu" => ActivateScript::Nushell, + "powershell" | "pwsh" => ActivateScript::PowerShell, + _ => ActivateScript::Default, + } + } + fn python_activate_command( &self, venv_base_directory: &Path, venv_settings: &VenvSettings, + shell: &Shell, ) -> Option<String> { let venv_settings = venv_settings.as_option()?; let activate_keyword = match venv_settings.activate_script { @@ -526,7 +546,22 @@ impl Project { terminal_settings::ActivateScript::Pyenv => "pyenv", _ => "source", }; - let activate_script_name = match venv_settings.activate_script { + let script_kind = + if venv_settings.activate_script == terminal_settings::ActivateScript::Default { + match shell { + Shell::Program(program) => Self::activate_script_kind(Some(program)), + Shell::WithArguments { + program, + args: _, + title_override: _, + } => Self::activate_script_kind(Some(program)), + Shell::System => Self::activate_script_kind(None), + } + } else { + venv_settings.activate_script + }; + + let activate_script_name = match script_kind { terminal_settings::ActivateScript::Default | terminal_settings::ActivateScript::Pyenv => "activate", terminal_settings::ActivateScript::Csh => "activate.csh", diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs index f5d7d5b306fc428f9aa876effa54ae410b2c4a7f..a290ce9c81c18f5043fd45e9c1afdac52efa2061 100644 --- a/crates/terminal/src/terminal_settings.rs +++ b/crates/terminal/src/terminal_settings.rs @@ -123,7 +123,7 @@ impl VenvSettings { } } -#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum ActivateScript { #[default] From 3d160a6e263717728d4dc18f554b0b7c19cb4ae8 Mon Sep 17 00:00:00 2001 From: Peter Tripp <peter@zed.dev> Date: Wed, 16 Jul 2025 09:10:51 -0400 Subject: [PATCH 110/658] Don't highlight partial indent guide backgrounds (#34433) Closes https://github.com/zed-industries/zed/issues/33665 Previously if a line was indented something that was not a multiple of `tab_size` with `"ident_guides": { "background_coloring": "indent_aware" } }` the background of characters would be highlighted. E.g. indent of 6 with tab_size 4. | Before / After | | - | | <img width="497" height="77" alt="Screenshot 2025-07-14 at 14 43 46" src="https://github.com/user-attachments/assets/93923117-047d-4d21-9a4f-488345f1ab89" /> | <img width="481" height="84" alt="Screenshot 2025-07-14 at 14 43 09" src="https://github.com/user-attachments/assets/a5d383cb-50c3-4239-ae8c-f72765ae7287" /> | CC: @bennetbo Any idea why this partial indent was enabled in your initial implementation [here](https://github.com/zed-industries/zed/pull/11503/files#diff-1781b7848dd9630f3c4f62df322c08af9a2de74af736e7eba031ebaeb4a0e2f4R3156-R3160)? This looks to be intentional. Release Notes: - N/A --- crates/multi_buffer/src/multi_buffer.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index e22fdb1ed5a978211d4dc6fd071107600ccf789f..2cc8ea59abace61ea1cec23112524f9b3ec2dda8 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -5905,7 +5905,6 @@ impl MultiBufferSnapshot { let depth = if found_indent { line_indent.len(tab_size) / tab_size - + ((line_indent.len(tab_size) % tab_size) > 0) as u32 } else { 0 }; From d4110fd2ab680364b265704cba9165d29208c33b Mon Sep 17 00:00:00 2001 From: Smit Barmase <heysmitbarmase@gmail.com> Date: Wed, 16 Jul 2025 19:25:13 +0530 Subject: [PATCH 111/658] linux: Fix spacebar not working with multiple keyboard layouts (#34514) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #26468 #16667 This PR fixes the spacebar not working with multiple keyboard layouts on Linux X11. I have tested this with Czech, Russian, German, German Neo 2, etc. It seems to work correctly. `XkbStateNotify` events correctly update XKB state with complete modifier info (depressed/latched/locked), but `KeyPress/KeyRelease` events immediately overwrite that state using `update_mask()` with only raw X11 modifier bits. This breaks xkb state as we reset `latched_mods` and `locked_mods` to 0, as well as we might not correctly handle cases where this new xkb state needs to change. Previous logic is flawed because `KeyPress/KeyRelease` event only gives you depressed modifiers (`event.state`) and not others, which we try to fill in from `previous_xkb_state`. This patch was introduced to fix capitalization issue with Neo 2 (https://github.com/zed-industries/zed/pull/14466) and later to fix wrong keys with German layout (https://github.com/zed-industries/zed/pull/31193), both of which I have tested this PR with. Now, instead of manually managing XKB state, we use the `update_key` method, which internally handles modifier states and other cases we might have missed. From `update_key` docs: > Update the keyboard state to reflect a given key being pressed or released. > > This entry point is intended for programs which track the keyboard state explictly (like an evdev client). If the state is serialized to you by a master process (like a Wayland compositor) using functions like `xkb_state_serialize_mods()`, you should use `xkb_state_update_mask()` instead. **_The two functins should not generally be used together._** > > A series of calls to this function should be consistent; that is, a call with `xkb::KEY_DOWN` for a key should be matched by an `xkb::KEY_UP`; if a key is pressed twice, it should be released twice; etc. Otherwise (e.g. due to missed input events), situations like "stuck modifiers" may occur. > > This function is often used in conjunction with the function `xkb_state_key_get_syms()` (or `xkb_state_key_get_one_sym()`), for example, when handling a key event. In this case, you should prefer to get the keysyms *before* updating the key, such that the keysyms reported for the key event are not affected by the event itself. This is the conventional behavior. Release Notes: - Fix the issue where the spacebar doesn’t work with multiple keyboard layouts on Linux X11. --- crates/gpui/src/platform/linux/x11/client.rs | 70 +++++--------------- 1 file changed, 18 insertions(+), 52 deletions(-) diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index 6cff977128ec594d683085e5f2cc24683c9e9ba7..0606f619c6fb808e4be42abe07f51d1e124a69f4 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -1,23 +1,22 @@ use crate::{Capslock, xcb_flush}; -use core::str; -use std::{ - cell::RefCell, - collections::{BTreeMap, HashSet}, - ops::Deref, - path::PathBuf, - rc::{Rc, Weak}, - time::{Duration, Instant}, -}; - use anyhow::{Context as _, anyhow}; use calloop::{ EventLoop, LoopHandle, RegistrationToken, generic::{FdWrapper, Generic}, }; use collections::HashMap; +use core::str; use http_client::Url; use log::Level; use smallvec::SmallVec; +use std::{ + cell::RefCell, + collections::{BTreeMap, HashSet}, + ops::Deref, + path::PathBuf, + rc::{Rc, Weak}, + time::{Duration, Instant}, +}; use util::ResultExt; use x11rb::{ @@ -38,7 +37,7 @@ use x11rb::{ }; use xim::{AttributeName, Client, InputStyle, x11rb::X11rbClient}; use xkbc::x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION}; -use xkbcommon::xkb::{self as xkbc, LayoutIndex, ModMask, STATE_LAYOUT_EFFECTIVE}; +use xkbcommon::xkb::{self as xkbc, STATE_LAYOUT_EFFECTIVE}; use super::{ ButtonOrScroll, ScrollDirection, X11Display, X11WindowStatePtr, XcbAtoms, XimCallbackEvent, @@ -141,13 +140,6 @@ impl From<xim::ClientError> for EventHandlerError { } } -#[derive(Debug, Default, Clone)] -struct XKBStateNotiy { - depressed_layout: LayoutIndex, - latched_layout: LayoutIndex, - locked_layout: LayoutIndex, -} - #[derive(Debug, Default)] pub struct Xdnd { other_window: xproto::Window, @@ -200,7 +192,6 @@ pub struct X11ClientState { pub(crate) mouse_focused_window: Option<xproto::Window>, pub(crate) keyboard_focused_window: Option<xproto::Window>, pub(crate) xkb: xkbc::State, - previous_xkb_state: XKBStateNotiy, keyboard_layout: LinuxKeyboardLayout, pub(crate) ximc: Option<X11rbClient<Rc<XCBConnection>>>, pub(crate) xim_handler: Option<XimHandler>, @@ -507,7 +498,6 @@ impl X11Client { mouse_focused_window: None, keyboard_focused_window: None, xkb: xkb_state, - previous_xkb_state: XKBStateNotiy::default(), keyboard_layout, ximc, xim_handler, @@ -959,14 +949,6 @@ impl X11Client { state.xkb_device_id, ) }; - let depressed_layout = xkb_state.serialize_layout(xkbc::STATE_LAYOUT_DEPRESSED); - let latched_layout = xkb_state.serialize_layout(xkbc::STATE_LAYOUT_LATCHED); - let locked_layout = xkb_state.serialize_layout(xkbc::ffi::XKB_STATE_LAYOUT_LOCKED); - state.previous_xkb_state = XKBStateNotiy { - depressed_layout, - latched_layout, - locked_layout, - }; state.xkb = xkb_state; drop(state); self.handle_keyboard_layout_change(); @@ -983,12 +965,6 @@ impl X11Client { event.latched_group as u32, event.locked_group.into(), ); - state.previous_xkb_state = XKBStateNotiy { - depressed_layout: event.base_group as u32, - latched_layout: event.latched_group as u32, - locked_layout: event.locked_group.into(), - }; - let modifiers = Modifiers::from_xkb(&state.xkb); let capslock = Capslock::from_xkb(&state.xkb); if state.last_modifiers_changed_event == modifiers @@ -1025,17 +1001,12 @@ impl X11Client { state.pre_key_char_down.take(); let keystroke = { let code = event.detail.into(); - let xkb_state = state.previous_xkb_state.clone(); - state.xkb.update_mask( - event.state.bits() as ModMask, - 0, - 0, - xkb_state.depressed_layout, - xkb_state.latched_layout, - xkb_state.locked_layout, - ); let mut keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code); let keysym = state.xkb.key_get_one_sym(code); + + // should be called after key_get_one_sym + state.xkb.update_key(code, xkbc::KeyDirection::Down); + if keysym.is_modifier_key() { return Some(()); } @@ -1093,17 +1064,12 @@ impl X11Client { let keystroke = { let code = event.detail.into(); - let xkb_state = state.previous_xkb_state.clone(); - state.xkb.update_mask( - event.state.bits() as ModMask, - 0, - 0, - xkb_state.depressed_layout, - xkb_state.latched_layout, - xkb_state.locked_layout, - ); let keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code); let keysym = state.xkb.key_get_one_sym(code); + + // should be called after key_get_one_sym + state.xkb.update_key(code, xkbc::KeyDirection::Up); + if keysym.is_modifier_key() { return Some(()); } From 37927a5dc839df577c328f54ce3c3d0f51003880 Mon Sep 17 00:00:00 2001 From: Marshall Bowers <git@maxdeviant.com> Date: Wed, 16 Jul 2025 10:01:31 -0400 Subject: [PATCH 112/658] docs: Add some more redirects (#34537) This PR adds some more redirects for the docs. Release Notes: - N/A --- docs/book.toml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/book.toml b/docs/book.toml index d04447d90f846ff33b7437b22d4dc82bbf586c7e..1895a377a62a44de104900221dc551d6672eed18 100644 --- a/docs/book.toml +++ b/docs/book.toml @@ -37,7 +37,16 @@ enable = false "/assistant/model-context-protocol.html" = "/docs/ai/mcp.html" "/model-improvement.html" = "/docs/ai/ai-improvement.html" "/extensions/context-servers.html" = "/docs/extensions/mcp-extensions.html" - +"/assistant-panel" = "/docs/ai/agent-panel.html" +"/assistant/model-context-protocolCitedby" = "/docs/ai/mcp.html" +"/community/feedback" = "/community-links" +"/context-servers" = "/docs/ai/mcp.html" +"/contribute-to-zed" = "/docs/development.html#contributor-links" +"/contributing" = "/docs/development.html#contributor-links" +"/debuggers" = "/docs/debugger.html" +"/development/development/macos" = "/docs/development/macos.html" +"/development/development/linux" = "/docs/development/linux.html" +"/development/development/windows" = "/docs/development/windows.html" # Our custom preprocessor for expanding commands like `{#kb action::ActionName}`, # and other docs-related functions. From 257bedf09b1a3130531c20dcdf5b03c6f90f1f06 Mon Sep 17 00:00:00 2001 From: Marshall Bowers <git@maxdeviant.com> Date: Wed, 16 Jul 2025 10:15:33 -0400 Subject: [PATCH 113/658] docs: Add missing extensions to redirects (#34539) Fixes the redirects added in https://github.com/zed-industries/zed/pull/34537. Release Notes: - N/A --- docs/book.toml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/book.toml b/docs/book.toml index 1895a377a62a44de104900221dc551d6672eed18..98085c0cfa8fc45e2b182f6b9daacc6305922b4d 100644 --- a/docs/book.toml +++ b/docs/book.toml @@ -37,16 +37,16 @@ enable = false "/assistant/model-context-protocol.html" = "/docs/ai/mcp.html" "/model-improvement.html" = "/docs/ai/ai-improvement.html" "/extensions/context-servers.html" = "/docs/extensions/mcp-extensions.html" -"/assistant-panel" = "/docs/ai/agent-panel.html" -"/assistant/model-context-protocolCitedby" = "/docs/ai/mcp.html" -"/community/feedback" = "/community-links" -"/context-servers" = "/docs/ai/mcp.html" -"/contribute-to-zed" = "/docs/development.html#contributor-links" -"/contributing" = "/docs/development.html#contributor-links" -"/debuggers" = "/docs/debugger.html" -"/development/development/macos" = "/docs/development/macos.html" -"/development/development/linux" = "/docs/development/linux.html" -"/development/development/windows" = "/docs/development/windows.html" +"/assistant-panel.html" = "/docs/ai/agent-panel.html" +"/assistant/model-context-protocolCitedby.html" = "/docs/ai/mcp.html" +"/community/feedback.html" = "/community-links" +"/context-servers.html" = "/docs/ai/mcp.html" +"/contribute-to-zed.html" = "/docs/development.html#contributor-links" +"/contributing.html" = "/docs/development.html#contributor-links" +"/debuggers.html" = "/docs/debugger.html" +"/development/development/macos.html" = "/docs/development/macos.html" +"/development/development/linux.html" = "/docs/development/linux.html" +"/development/development/windows.html" = "/docs/development/windows.html" # Our custom preprocessor for expanding commands like `{#kb action::ActionName}`, # and other docs-related functions. From 406ffb1e20f2db900e6d6f1fb99d348d6f4dffac Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon <oleksiy@zed.dev> Date: Wed, 16 Jul 2025 17:38:58 +0300 Subject: [PATCH 114/658] agent: Push diffs of user edits to the agent (#34487) This change improves user/agent collaborative editing. When the user edits files that are used by the agent, the `project_notification` tool now pushes *diffs* of the changes, not just file names. This helps the agent to stay up to date without needing to re-read files. Release Notes: - Improved user/agent collaborative editing: agent now receives diffs of user edits --- Cargo.lock | 1 + crates/agent/src/thread.rs | 17 +- crates/assistant_tool/Cargo.toml | 1 + crates/assistant_tool/src/action_log.rs | 282 +++++++++++++++--- .../src/project_notifications_tool.rs | 50 ++-- 5 files changed, 274 insertions(+), 77 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 15a28016c6d2f168168657a07443ea40080b07bb..a2e9fc26ca417e9cf771cc5dd5e1a2e92d4e53e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -745,6 +745,7 @@ dependencies = [ "futures 0.3.31", "gpui", "icons", + "indoc", "language", "language_model", "log", diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 8e66e526deedc6db1bec7912f48008bd6b36782c..d46dada2703438686b9df0e452dfef28777ff715 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -1532,7 +1532,9 @@ impl Thread { ) -> Option<PendingToolUse> { let action_log = self.action_log.read(cx); - action_log.unnotified_stale_buffers(cx).next()?; + if !action_log.has_unnotified_user_edits() { + return None; + } // Represent notification as a simulated `project_notifications` tool call let tool_name = Arc::from("project_notifications"); @@ -3253,7 +3255,6 @@ mod tests { use futures::stream::BoxStream; use gpui::TestAppContext; use http_client; - use indoc::indoc; use language_model::fake_provider::{FakeLanguageModel, FakeLanguageModelProvider}; use language_model::{ LanguageModelCompletionError, LanguageModelName, LanguageModelProviderId, @@ -3614,6 +3615,7 @@ fn main() {{ cx, ); }); + cx.run_until_parked(); // We shouldn't have a stale buffer notification yet let notifications = thread.read_with(cx, |thread, _| { @@ -3643,11 +3645,13 @@ fn main() {{ cx, ) }); + cx.run_until_parked(); // Check for the stale buffer warning thread.update(cx, |thread, cx| { thread.flush_notifications(model.clone(), CompletionIntent::UserPrompt, cx) }); + cx.run_until_parked(); let notifications = thread.read_with(cx, |thread, _cx| { find_tool_uses(thread, "project_notifications") @@ -3661,12 +3665,8 @@ fn main() {{ panic!("`project_notifications` should return text"); }; - let expected_content = indoc! {"[The following is an auto-generated notification; do not reply] - - These files have changed since the last read: - - code.rs - "}; - assert_eq!(notification_content, expected_content); + assert!(notification_content.contains("These files have changed since the last read:")); + assert!(notification_content.contains("code.rs")); // Insert another user message and flush notifications again thread.update(cx, |thread, cx| { @@ -3682,6 +3682,7 @@ fn main() {{ thread.update(cx, |thread, cx| { thread.flush_notifications(model.clone(), CompletionIntent::UserPrompt, cx) }); + cx.run_until_parked(); // There should be no new notifications (we already flushed one) let notifications = thread.read_with(cx, |thread, _cx| { diff --git a/crates/assistant_tool/Cargo.toml b/crates/assistant_tool/Cargo.toml index 5a54e86eac15c2846e7e72ee45b47ab014cd69e6..acbe674b02cfe31a08f63e01f7dae1a2448c453e 100644 --- a/crates/assistant_tool/Cargo.toml +++ b/crates/assistant_tool/Cargo.toml @@ -40,6 +40,7 @@ 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"] } language_model = { workspace = true, features = ["test-support"] } log.workspace = true diff --git a/crates/assistant_tool/src/action_log.rs b/crates/assistant_tool/src/action_log.rs index e983075cd1e6db22af77856d50a43ebf812de825..dce1b0cdc1e4bfcf48d1387dd645d8b88a252060 100644 --- a/crates/assistant_tool/src/action_log.rs +++ b/crates/assistant_tool/src/action_log.rs @@ -8,7 +8,10 @@ use language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint}; use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle}; use std::{cmp, ops::Range, sync::Arc}; use text::{Edit, Patch, Rope}; -use util::{RangeExt, ResultExt as _}; +use util::{ + RangeExt, ResultExt as _, + paths::{PathStyle, RemotePathBuf}, +}; /// Tracks actions performed by tools in a thread pub struct ActionLog { @@ -18,8 +21,6 @@ pub struct ActionLog { edited_since_project_diagnostics_check: bool, /// The project this action log is associated with project: Entity<Project>, - /// Tracks which buffer versions have already been notified as changed externally - notified_versions: BTreeMap<Entity<Buffer>, clock::Global>, } impl ActionLog { @@ -29,7 +30,6 @@ impl ActionLog { tracked_buffers: BTreeMap::default(), edited_since_project_diagnostics_check: false, project, - notified_versions: BTreeMap::default(), } } @@ -51,6 +51,67 @@ impl ActionLog { Some(self.tracked_buffers.get(buffer)?.snapshot.clone()) } + pub fn has_unnotified_user_edits(&self) -> bool { + self.tracked_buffers + .values() + .any(|tracked| tracked.has_unnotified_user_edits) + } + + /// Return a unified diff patch with user edits made since last read or notification + pub fn unnotified_user_edits(&self, cx: &Context<Self>) -> Option<String> { + if !self.has_unnotified_user_edits() { + return None; + } + + let unified_diff = self + .tracked_buffers + .values() + .filter_map(|tracked| { + if !tracked.has_unnotified_user_edits { + return None; + } + + let text_with_latest_user_edits = tracked.diff_base.to_string(); + let text_with_last_seen_user_edits = tracked.last_seen_base.to_string(); + if text_with_latest_user_edits == text_with_last_seen_user_edits { + return None; + } + let patch = language::unified_diff( + &text_with_last_seen_user_edits, + &text_with_latest_user_edits, + ); + + let buffer = tracked.buffer.clone(); + let file_path = buffer + .read(cx) + .file() + .map(|file| RemotePathBuf::new(file.full_path(cx), PathStyle::Posix).to_proto()) + .unwrap_or_else(|| format!("buffer_{}", buffer.entity_id())); + + let mut result = String::new(); + result.push_str(&format!("--- a/{}\n", file_path)); + result.push_str(&format!("+++ b/{}\n", file_path)); + result.push_str(&patch); + + Some(result) + }) + .collect::<Vec<_>>() + .join("\n\n"); + + Some(unified_diff) + } + + /// Return a unified diff patch with user edits made since last read/notification + /// and mark them as notified + pub fn flush_unnotified_user_edits(&mut self, cx: &Context<Self>) -> Option<String> { + let patch = self.unnotified_user_edits(cx); + self.tracked_buffers.values_mut().for_each(|tracked| { + tracked.has_unnotified_user_edits = false; + tracked.last_seen_base = tracked.diff_base.clone(); + }); + patch + } + fn track_buffer_internal( &mut self, buffer: Entity<Buffer>, @@ -59,7 +120,6 @@ impl ActionLog { ) -> &mut TrackedBuffer { let status = if is_created { if let Some(tracked) = self.tracked_buffers.remove(&buffer) { - self.notified_versions.remove(&buffer); match tracked.status { TrackedBufferStatus::Created { existing_file_content, @@ -101,26 +161,31 @@ impl ActionLog { let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx)); let (diff_update_tx, diff_update_rx) = mpsc::unbounded(); let diff_base; + let last_seen_base; let unreviewed_edits; if is_created { diff_base = Rope::default(); + last_seen_base = Rope::default(); unreviewed_edits = Patch::new(vec![Edit { old: 0..1, new: 0..text_snapshot.max_point().row + 1, }]) } else { diff_base = buffer.read(cx).as_rope().clone(); + last_seen_base = diff_base.clone(); unreviewed_edits = Patch::default(); } TrackedBuffer { buffer: buffer.clone(), diff_base, + last_seen_base, unreviewed_edits, snapshot: text_snapshot.clone(), status, version: buffer.read(cx).version(), diff, diff_update: diff_update_tx, + has_unnotified_user_edits: false, _open_lsp_handle: open_lsp_handle, _maintain_diff: cx.spawn({ let buffer = buffer.clone(); @@ -174,7 +239,6 @@ impl ActionLog { // If the buffer had been edited by a tool, but it got // deleted externally, we want to stop tracking it. self.tracked_buffers.remove(&buffer); - self.notified_versions.remove(&buffer); } cx.notify(); } @@ -188,7 +252,6 @@ impl ActionLog { // resurrected externally, we want to clear the edits we // were tracking and reset the buffer's state. self.tracked_buffers.remove(&buffer); - self.notified_versions.remove(&buffer); self.track_buffer_internal(buffer, false, cx); } cx.notify(); @@ -262,19 +325,23 @@ impl ActionLog { buffer_snapshot: text::BufferSnapshot, cx: &mut AsyncApp, ) -> Result<()> { - let rebase = this.read_with(cx, |this, cx| { + let rebase = this.update(cx, |this, cx| { let tracked_buffer = this .tracked_buffers - .get(buffer) + .get_mut(buffer) .context("buffer not tracked")?; + if let ChangeAuthor::User = author { + tracked_buffer.has_unnotified_user_edits = true; + } + let rebase = cx.background_spawn({ let mut base_text = tracked_buffer.diff_base.clone(); let old_snapshot = tracked_buffer.snapshot.clone(); let new_snapshot = buffer_snapshot.clone(); let unreviewed_edits = tracked_buffer.unreviewed_edits.clone(); + let edits = diff_snapshots(&old_snapshot, &new_snapshot); async move { - let edits = diff_snapshots(&old_snapshot, &new_snapshot); if let ChangeAuthor::User = author { apply_non_conflicting_edits( &unreviewed_edits, @@ -494,7 +561,6 @@ impl ActionLog { match tracked_buffer.status { TrackedBufferStatus::Created { .. } => { self.tracked_buffers.remove(&buffer); - self.notified_versions.remove(&buffer); cx.notify(); } TrackedBufferStatus::Modified => { @@ -520,7 +586,6 @@ impl ActionLog { match tracked_buffer.status { TrackedBufferStatus::Deleted => { self.tracked_buffers.remove(&buffer); - self.notified_versions.remove(&buffer); cx.notify(); } _ => { @@ -629,7 +694,6 @@ impl ActionLog { }; self.tracked_buffers.remove(&buffer); - self.notified_versions.remove(&buffer); cx.notify(); task } @@ -643,7 +707,6 @@ impl ActionLog { // Clear all tracked edits for this buffer and start over as if we just read it. self.tracked_buffers.remove(&buffer); - self.notified_versions.remove(&buffer); self.buffer_read(buffer.clone(), cx); cx.notify(); save @@ -744,33 +807,6 @@ impl ActionLog { .collect() } - /// Returns stale buffers that haven't been notified yet - pub fn unnotified_stale_buffers<'a>( - &'a self, - cx: &'a App, - ) -> impl Iterator<Item = &'a Entity<Buffer>> { - self.stale_buffers(cx).filter(|buffer| { - let buffer_entity = buffer.read(cx); - self.notified_versions - .get(buffer) - .map_or(true, |notified_version| { - *notified_version != buffer_entity.version - }) - }) - } - - /// Marks the given buffers as notified at their current versions - pub fn mark_buffers_as_notified( - &mut self, - buffers: impl IntoIterator<Item = Entity<Buffer>>, - cx: &App, - ) { - for buffer in buffers { - let version = buffer.read(cx).version.clone(); - self.notified_versions.insert(buffer, version); - } - } - /// Iterate over buffers changed since last read or edited by the model pub fn stale_buffers<'a>(&'a self, cx: &'a App) -> impl Iterator<Item = &'a Entity<Buffer>> { self.tracked_buffers @@ -914,12 +950,14 @@ enum TrackedBufferStatus { struct TrackedBuffer { buffer: Entity<Buffer>, diff_base: Rope, + last_seen_base: Rope, unreviewed_edits: Patch<u32>, status: TrackedBufferStatus, version: clock::Global, diff: Entity<BufferDiff>, snapshot: text::BufferSnapshot, diff_update: mpsc::UnboundedSender<(ChangeAuthor, text::BufferSnapshot)>, + has_unnotified_user_edits: bool, _open_lsp_handle: OpenLspBufferHandle, _maintain_diff: Task<()>, _subscription: Subscription, @@ -950,6 +988,7 @@ mod tests { use super::*; use buffer_diff::DiffHunkStatusKind; use gpui::TestAppContext; + use indoc::indoc; use language::Point; use project::{FakeFs, Fs, Project, RemoveOptions}; use rand::prelude::*; @@ -1232,6 +1271,110 @@ mod tests { assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); } + #[gpui::test(iterations = 10)] + async fn test_user_edits_notifications(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/dir"), + json!({"file": indoc! {" + abc + def + ghi + jkl + mno"}}), + ) + .await; + 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/file", cx)) + .unwrap(); + let buffer = project + .update(cx, |project, cx| project.open_buffer(file_path, cx)) + .await + .unwrap(); + + // Agent edits + cx.update(|cx| { + action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); + buffer.update(cx, |buffer, cx| { + buffer + .edit([(Point::new(1, 2)..Point::new(2, 3), "F\nGHI")], None, cx) + .unwrap() + }); + action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); + }); + cx.run_until_parked(); + assert_eq!( + buffer.read_with(cx, |buffer, _| buffer.text()), + indoc! {" + abc + deF + GHI + jkl + mno"} + ); + assert_eq!( + unreviewed_hunks(&action_log, cx), + vec![( + buffer.clone(), + vec![HunkStatus { + range: Point::new(1, 0)..Point::new(3, 0), + diff_status: DiffHunkStatusKind::Modified, + old_text: "def\nghi\n".into(), + }], + )] + ); + + // User edits + buffer.update(cx, |buffer, cx| { + buffer.edit( + [ + (Point::new(0, 2)..Point::new(0, 2), "X"), + (Point::new(3, 0)..Point::new(3, 0), "Y"), + ], + None, + cx, + ) + }); + cx.run_until_parked(); + assert_eq!( + buffer.read_with(cx, |buffer, _| buffer.text()), + indoc! {" + abXc + deF + GHI + Yjkl + mno"} + ); + + // User edits should be stored separately from agent's + let user_edits = action_log.update(cx, |log, cx| log.unnotified_user_edits(cx)); + assert_eq!( + user_edits.expect("should have some user edits"), + indoc! {" + --- a/dir/file + +++ b/dir/file + @@ -1,5 +1,5 @@ + -abc + +abXc + def + ghi + -jkl + +Yjkl + mno + "} + ); + + action_log.update(cx, |log, cx| { + log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), cx) + }); + cx.run_until_parked(); + assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); + } + #[gpui::test(iterations = 10)] async fn test_creating_files(cx: &mut TestAppContext) { init_test(cx); @@ -2221,4 +2364,61 @@ mod tests { .collect() }) } + + #[gpui::test] + async fn test_format_patch(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/dir"), + json!({"test.txt": "line 1\nline 2\nline 3\n"}), + ) + .await; + 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/test.txt", cx) + }) + .unwrap(); + let buffer = project + .update(cx, |project, cx| project.open_buffer(file_path, cx)) + .await + .unwrap(); + + cx.update(|cx| { + // Track the buffer and mark it as read first + action_log.update(cx, |log, cx| { + log.buffer_read(buffer.clone(), cx); + }); + + // Make some edits to create a patch + buffer.update(cx, |buffer, cx| { + buffer + .edit([(Point::new(1, 0)..Point::new(1, 6), "CHANGED")], None, cx) + .unwrap(); // Replace "line2" with "CHANGED" + }); + }); + + cx.run_until_parked(); + + // Get the patch + let patch = action_log.update(cx, |log, cx| log.unnotified_user_edits(cx)); + + // Verify the patch format contains expected unified diff elements + assert_eq!( + patch.unwrap(), + indoc! {" + --- a/dir/test.txt + +++ b/dir/test.txt + @@ -1,3 +1,3 @@ + line 1 + -line 2 + +CHANGED + line 3 + "} + ); + } } diff --git a/crates/assistant_tools/src/project_notifications_tool.rs b/crates/assistant_tools/src/project_notifications_tool.rs index 168ec61ae98529e1c82dcbe1d4334436457bab44..1b926bb4469593689c4bdf797b055d71c60fca35 100644 --- a/crates/assistant_tools/src/project_notifications_tool.rs +++ b/crates/assistant_tools/src/project_notifications_tool.rs @@ -6,7 +6,6 @@ use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchem use project::Project; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use std::fmt::Write as _; use std::sync::Arc; use ui::IconName; @@ -52,34 +51,22 @@ impl Tool for ProjectNotificationsTool { _window: Option<AnyWindowHandle>, cx: &mut App, ) -> ToolResult { - let mut stale_files = String::new(); - let mut notified_buffers = Vec::new(); - - for stale_file in action_log.read(cx).unnotified_stale_buffers(cx) { - if let Some(file) = stale_file.read(cx).file() { - writeln!(&mut stale_files, "- {}", file.path().display()).ok(); - notified_buffers.push(stale_file.clone()); - } - } - - if !notified_buffers.is_empty() { - action_log.update(cx, |log, cx| { - log.mark_buffers_as_notified(notified_buffers, cx); - }); - } - - let response = if stale_files.is_empty() { - "No new notifications".to_string() - } else { - // NOTE: Changes to this prompt require a symmetric update in the LLM Worker - const HEADER: &str = include_str!("./project_notifications_tool/prompt_header.txt"); - format!("{HEADER}{stale_files}").replace("\r\n", "\n") + let Some(user_edits_diff) = + action_log.update(cx, |log, cx| log.flush_unnotified_user_edits(cx)) + else { + return result("No new notifications"); }; - Task::ready(Ok(response.into())).into() + // NOTE: Changes to this prompt require a symmetric update in the LLM Worker + const HEADER: &str = include_str!("./project_notifications_tool/prompt_header.txt"); + result(&format!("{HEADER}\n\n```diff\n{user_edits_diff}\n```\n").replace("\r\n", "\n")) } } +fn result(response: &str) -> ToolResult { + Task::ready(Ok(response.to_string().into())).into() +} + #[cfg(test)] mod tests { use super::*; @@ -123,6 +110,7 @@ mod tests { action_log.update(cx, |log, cx| { log.buffer_read(buffer.clone(), cx); }); + cx.run_until_parked(); // Run the tool before any changes let tool = Arc::new(ProjectNotificationsTool); @@ -142,6 +130,7 @@ mod tests { cx, ) }); + cx.run_until_parked(); let response = result.output.await.unwrap(); let response_text = match &response.content { @@ -158,6 +147,7 @@ mod tests { buffer.update(cx, |buffer, cx| { buffer.edit([(1..1, "\nChange!\n")], None, cx); }); + cx.run_until_parked(); // Run the tool again let result = cx.update(|cx| { @@ -171,6 +161,7 @@ mod tests { cx, ) }); + cx.run_until_parked(); // This time the buffer is stale, so the tool should return a notification let response = result.output.await.unwrap(); @@ -179,10 +170,12 @@ mod tests { _ => panic!("Expected text response"), }; - let expected_content = "[The following is an auto-generated notification; do not reply]\n\nThese files have changed since the last read:\n- code.rs\n"; - assert_eq!( - response_text.as_str(), - expected_content, + assert!( + response_text.contains("These files have changed"), + "Tool should return the stale buffer notification" + ); + assert!( + response_text.contains("test/code.rs"), "Tool should return the stale buffer notification" ); @@ -198,6 +191,7 @@ mod tests { cx, ) }); + cx.run_until_parked(); let response = result.output.await.unwrap(); let response_text = match &response.content { From 875c86e3ef30937f07b2db5324bf16331695f39b Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Wed, 16 Jul 2025 20:09:07 +0530 Subject: [PATCH 115/658] agent_ui: Fix token count not getting shown in the TextThread (#34485) Closes #34319 In this pr: https://github.com/zed-industries/zed/pull/33462 there was check added for early return for active_thread and message_editor as those are not present in the TextThread and only available in the Thread the token count was not getting triggered for TextThread this pr fixes that regression by moving the logic specific to Thread inside of thread view match. <img width="3024" height="1886" alt="CleanShot 2025-07-15 at 23 50 18@2x" src="https://github.com/user-attachments/assets/bd74ae8b-6c37-4cdd-ab95-d3c253b8a948" /> Release Notes: - Fix token count not getting shown in the TextThread --- crates/agent_ui/src/agent_panel.rs | 57 ++++++++++++++---------------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index ded26b189642c1eb9e6c79ec958a18ebb99ded68..2caa9dab42d374514324a2e97db3473db50fcbcf 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1975,48 +1975,45 @@ impl AgentPanel { } fn render_token_count(&self, cx: &App) -> Option<AnyElement> { - let (active_thread, message_editor) = match &self.active_view { + match &self.active_view { ActiveView::Thread { thread, message_editor, .. - } => (thread.read(cx), message_editor.read(cx)), - ActiveView::AcpThread { .. } => { - return None; - } - ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => { - return None; - } - }; + } => { + let active_thread = thread.read(cx); + let message_editor = message_editor.read(cx); - let editor_empty = message_editor.is_editor_fully_empty(cx); + let editor_empty = message_editor.is_editor_fully_empty(cx); - if active_thread.is_empty() && editor_empty { - return None; - } + if active_thread.is_empty() && editor_empty { + return None; + } - let thread = active_thread.thread().read(cx); - let is_generating = thread.is_generating(); - let conversation_token_usage = thread.total_token_usage()?; + let thread = active_thread.thread().read(cx); + let is_generating = thread.is_generating(); + let conversation_token_usage = thread.total_token_usage()?; - let (total_token_usage, is_estimating) = - if let Some((editing_message_id, unsent_tokens)) = active_thread.editing_message_id() { - let combined = thread - .token_usage_up_to_message(editing_message_id) - .add(unsent_tokens); + let (total_token_usage, is_estimating) = + if let Some((editing_message_id, unsent_tokens)) = + active_thread.editing_message_id() + { + let combined = thread + .token_usage_up_to_message(editing_message_id) + .add(unsent_tokens); - (combined, unsent_tokens > 0) - } else { - let unsent_tokens = message_editor.last_estimated_token_count().unwrap_or(0); - let combined = conversation_token_usage.add(unsent_tokens); + (combined, unsent_tokens > 0) + } else { + let unsent_tokens = + message_editor.last_estimated_token_count().unwrap_or(0); + let combined = conversation_token_usage.add(unsent_tokens); - (combined, unsent_tokens > 0) - }; + (combined, unsent_tokens > 0) + }; - let is_waiting_to_update_token_count = message_editor.is_waiting_to_update_token_count(); + let is_waiting_to_update_token_count = + message_editor.is_waiting_to_update_token_count(); - match &self.active_view { - ActiveView::Thread { .. } => { if total_token_usage.total == 0 { return None; } From 6e147b3b910c192c5e795c3a759d6c9427de4d4a Mon Sep 17 00:00:00 2001 From: Marshall Bowers <git@maxdeviant.com> Date: Wed, 16 Jul 2025 10:44:24 -0400 Subject: [PATCH 116/658] docs: Organize redirects (#34541) This PR organizes the docs redirects and adds some instructions for them. Release Notes: - N/A --- docs/book.toml | 56 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/docs/book.toml b/docs/book.toml index 98085c0cfa8fc45e2b182f6b9daacc6305922b4d..70e294c014fc4a89a6d1a1ec3c2b8a0eb4be3637 100644 --- a/docs/book.toml +++ b/docs/book.toml @@ -15,37 +15,57 @@ additional-js = ["theme/page-toc.js", "theme/plugins.js"] [output.html.print] enable = false +# Redirects for `/docs` pages. +# +# All of the source URLs are interpreted relative to mdBook, so they must: +# 1. Not start with `/docs` +# 2. End in `.html` +# +# The destination URLs are interpreted relative to `https://zed.dev`. +# - Redirects to other docs pages should end in `.html` +# - You can link to pages on the Zed site by omitting the `/docs` in front of it. [output.html.redirect] -"/elixir.html" = "/docs/languages/elixir.html" -"/javascript.html" = "/docs/languages/javascript.html" -"/ruby.html" = "/docs/languages/ruby.html" -"/python.html" = "/docs/languages/python.html" -"/adding-new-languages.html" = "/docs/extensions/languages.html" -"/language-model-integration.html" = "/docs/assistant/assistant.html" -"/assistant.html" = "/docs/assistant/assistant.html" -"/developing-zed.html" = "/docs/development.html" -"/conversations.html" = "/community-links" +# AI "/ai.html" = "/docs/ai/overview.html" +"/assistant-panel.html" = "/docs/ai/agent-panel.html" +"/assistant.html" = "/docs/assistant/assistant.html" +"/assistant/assistant-panel.html" = "/docs/ai/agent-panel.html" "/assistant/assistant.html" = "/docs/ai/overview.html" +"/assistant/commands.html" = "/docs/ai/text-threads.html" "/assistant/configuration.html" = "/docs/ai/configuration.html" -"/assistant/assistant-panel.html" = "/docs/ai/agent-panel.html" +"/assistant/context-servers.html" = "/docs/ai/mcp.html" "/assistant/contexts.html" = "/docs/ai/text-threads.html" "/assistant/inline-assistant.html" = "/docs/ai/inline-assistant.html" -"/assistant/commands.html" = "/docs/ai/text-threads.html" -"/assistant/prompting.html" = "/docs/ai/rules.html" -"/assistant/context-servers.html" = "/docs/ai/mcp.html" "/assistant/model-context-protocol.html" = "/docs/ai/mcp.html" +"/assistant/prompting.html" = "/docs/ai/rules.html" +"/language-model-integration.html" = "/docs/assistant/assistant.html" "/model-improvement.html" = "/docs/ai/ai-improvement.html" -"/extensions/context-servers.html" = "/docs/extensions/mcp-extensions.html" -"/assistant-panel.html" = "/docs/ai/agent-panel.html" -"/assistant/model-context-protocolCitedby.html" = "/docs/ai/mcp.html" + +# Community "/community/feedback.html" = "/community-links" +"/conversations.html" = "/community-links" + +# Debugger +"/debuggers.html" = "/docs/debugger.html" + +# MCP +"/assistant/model-context-protocolCitedby.html" = "/docs/ai/mcp.html" "/context-servers.html" = "/docs/ai/mcp.html" +"/extensions/context-servers.html" = "/docs/extensions/mcp-extensions.html" + +# Languages +"/adding-new-languages.html" = "/docs/extensions/languages.html" +"/elixir.html" = "/docs/languages/elixir.html" +"/javascript.html" = "/docs/languages/javascript.html" +"/python.html" = "/docs/languages/python.html" +"/ruby.html" = "/docs/languages/ruby.html" + +# Zed development "/contribute-to-zed.html" = "/docs/development.html#contributor-links" "/contributing.html" = "/docs/development.html#contributor-links" -"/debuggers.html" = "/docs/debugger.html" -"/development/development/macos.html" = "/docs/development/macos.html" +"/developing-zed.html" = "/docs/development.html" "/development/development/linux.html" = "/docs/development/linux.html" +"/development/development/macos.html" = "/docs/development/macos.html" "/development/development/windows.html" = "/docs/development/windows.html" # Our custom preprocessor for expanding commands like `{#kb action::ActionName}`, From 2a9a82d757480f5ec95b9fd86d1701ccf4e9922a Mon Sep 17 00:00:00 2001 From: Peter Tripp <peter@zed.dev> Date: Wed, 16 Jul 2025 10:45:34 -0400 Subject: [PATCH 117/658] macos: Add mappings for alt-delete and cmd-delete (#34493) Closes https://github.com/zed-industries/zed/issues/34484 Release Notes: - macos: Add default mappings for `alt-delete` and `cmd-delete` in Terminal (delete word to right; delete to end of line) --- assets/keymaps/default-macos.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 7af79bdeea1b461e6b0f6fb665ccc9f8cef2138f..1eece3169929f6289595fd29f903dfe0981eff63 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -1105,7 +1105,9 @@ "ctrl-enter": "assistant::InlineAssist", "ctrl-_": null, // emacs undo // Some nice conveniences - "cmd-backspace": ["terminal::SendText", "\u0015"], + "cmd-backspace": ["terminal::SendText", "\u0015"], // ctrl-u: clear line + "alt-delete": ["terminal::SendText", "\u001bd"], // alt-d: delete word forward + "cmd-delete": ["terminal::SendText", "\u000b"], // ctrl-k: delete to end of line "cmd-right": ["terminal::SendText", "\u0005"], "cmd-left": ["terminal::SendText", "\u0001"], // Terminal.app compatibility From 21b4a2ecdd9cae24fcc20b73c35eee278b51bbe3 Mon Sep 17 00:00:00 2001 From: Ben Kunkle <ben@zed.dev> Date: Wed, 16 Jul 2025 09:49:16 -0500 Subject: [PATCH 118/658] keymap_ui: Infer use key equivalents (#34498) Closes #ISSUE This PR attempts to add workarounds for `use_key_equivalents` in the keymap UI. First of all it makes it so that `use_key_equivalents` is ignored when searching for a binding to replace so that replacing a keybind with `use_key_equivalents` set to true does not result in a new binding. Second, it attempts to infer the value of `use_key_equivalents` off of a base binding when adding a binding by adding an optional `from` parameter to the `KeymapUpdateOperation::Add` variant. Neither workaround will work when the `from` binding for an add or the `target` binding for a replace are not in the user keymap. cc: @Anthony-Eid Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/settings/src/keymap_file.rs | 178 +++++++++++++++++--------- crates/settings/src/settings_json.rs | 25 +++- crates/settings_ui/src/keybindings.rs | 117 +++++++++-------- 3 files changed, 201 insertions(+), 119 deletions(-) diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 470c5faf78d7e7b41d8c4895e471b82b557a5c3a..b61d30e405471ce3d4ab7378f64610a1057ed439 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -10,6 +10,7 @@ use serde::Deserialize; use serde_json::{Value, json}; use std::borrow::Cow; use std::{any::TypeId, fmt::Write, rc::Rc, sync::Arc, sync::LazyLock}; +use util::ResultExt as _; use util::{ asset_str, markdown::{MarkdownEscaped, MarkdownInlineCode, MarkdownString}, @@ -612,19 +613,26 @@ impl KeymapFile { KeybindUpdateOperation::Replace { target_keybind_source: target_source, source, - .. + target, } if target_source != KeybindSource::User => { - operation = KeybindUpdateOperation::Add(source); + operation = KeybindUpdateOperation::Add { + source, + from: Some(target), + }; } // if trying to remove a keybinding that is not user-defined, treat it as creating a binding // that binds it to `zed::NoAction` KeybindUpdateOperation::Remove { - mut target, + target, target_keybind_source, } if target_keybind_source != KeybindSource::User => { - target.action_name = gpui::NoAction.name(); - target.action_arguments.take(); - operation = KeybindUpdateOperation::Add(target); + let mut source = target.clone(); + source.action_name = gpui::NoAction.name(); + source.action_arguments.take(); + operation = KeybindUpdateOperation::Add { + source, + from: Some(target), + }; } _ => {} } @@ -742,7 +750,10 @@ impl KeymapFile { ) .context("Failed to replace keybinding")?; keymap_contents.replace_range(replace_range, &replace_value); - operation = KeybindUpdateOperation::Add(source); + operation = KeybindUpdateOperation::Add { + source, + from: Some(target), + }; } } else { log::warn!( @@ -752,16 +763,28 @@ impl KeymapFile { source.keystrokes, source_action_value, ); - operation = KeybindUpdateOperation::Add(source); + operation = KeybindUpdateOperation::Add { + source, + from: Some(target), + }; } } - if let KeybindUpdateOperation::Add(keybinding) = operation { + if let KeybindUpdateOperation::Add { + source: keybinding, + from, + } = operation + { let mut value = serde_json::Map::with_capacity(4); if let Some(context) = keybinding.context { value.insert("context".to_string(), context.into()); } - if keybinding.use_key_equivalents { + let use_key_equivalents = from.and_then(|from| { + let action_value = from.action_value().context("Failed to serialize action value. `use_key_equivalents` on new keybinding may be incorrect.").log_err()?; + let (index, _) = find_binding(&keymap, &from, &action_value)?; + Some(keymap.0[index].use_key_equivalents) + }).unwrap_or(false); + if use_key_equivalents { value.insert("use_key_equivalents".to_string(), true.into()); } @@ -794,9 +817,6 @@ impl KeymapFile { if section_context_parsed != target_context_parsed { continue; } - if section.use_key_equivalents != target.use_key_equivalents { - continue; - } let Some(bindings) = §ion.bindings else { continue; }; @@ -835,19 +855,27 @@ pub enum KeybindUpdateOperation<'a> { target: KeybindUpdateTarget<'a>, target_keybind_source: KeybindSource, }, - Add(KeybindUpdateTarget<'a>), + Add { + source: KeybindUpdateTarget<'a>, + from: Option<KeybindUpdateTarget<'a>>, + }, Remove { target: KeybindUpdateTarget<'a>, target_keybind_source: KeybindSource, }, } -#[derive(Debug)] +impl<'a> KeybindUpdateOperation<'a> { + pub fn add(source: KeybindUpdateTarget<'a>) -> Self { + Self::Add { source, from: None } + } +} + +#[derive(Debug, Clone)] pub struct KeybindUpdateTarget<'a> { pub context: Option<&'a str>, pub keystrokes: &'a [Keystroke], pub action_name: &'a str, - pub use_key_equivalents: bool, pub action_arguments: Option<&'a str>, } @@ -933,6 +961,7 @@ impl From<KeybindSource> for KeyBindingMetaIndex { #[cfg(test)] mod tests { + use gpui::Keystroke; use unindent::Unindent; use crate::{ @@ -955,37 +984,35 @@ mod tests { KeymapFile::parse(json).unwrap(); } + #[track_caller] + fn check_keymap_update( + input: impl ToString, + operation: KeybindUpdateOperation, + expected: impl ToString, + ) { + let result = KeymapFile::update_keybinding(operation, input.to_string(), 4) + .expect("Update succeeded"); + pretty_assertions::assert_eq!(expected.to_string(), result); + } + + #[track_caller] + fn parse_keystrokes(keystrokes: &str) -> Vec<Keystroke> { + return keystrokes + .split(' ') + .map(|s| Keystroke::parse(s).expect("Keystrokes valid")) + .collect(); + } + #[test] fn keymap_update() { - use gpui::Keystroke; - zlog::init_test(); - #[track_caller] - fn check_keymap_update( - input: impl ToString, - operation: KeybindUpdateOperation, - expected: impl ToString, - ) { - let result = KeymapFile::update_keybinding(operation, input.to_string(), 4) - .expect("Update succeeded"); - pretty_assertions::assert_eq!(expected.to_string(), result); - } - - #[track_caller] - fn parse_keystrokes(keystrokes: &str) -> Vec<Keystroke> { - return keystrokes - .split(' ') - .map(|s| Keystroke::parse(s).expect("Keystrokes valid")) - .collect(); - } check_keymap_update( "[]", - KeybindUpdateOperation::Add(KeybindUpdateTarget { + KeybindUpdateOperation::add(KeybindUpdateTarget { keystrokes: &parse_keystrokes("ctrl-a"), action_name: "zed::SomeAction", context: None, - use_key_equivalents: false, action_arguments: None, }), r#"[ @@ -1007,11 +1034,10 @@ mod tests { } ]"# .unindent(), - KeybindUpdateOperation::Add(KeybindUpdateTarget { + KeybindUpdateOperation::add(KeybindUpdateTarget { keystrokes: &parse_keystrokes("ctrl-b"), action_name: "zed::SomeOtherAction", context: None, - use_key_equivalents: false, action_arguments: None, }), r#"[ @@ -1038,11 +1064,10 @@ mod tests { } ]"# .unindent(), - KeybindUpdateOperation::Add(KeybindUpdateTarget { + KeybindUpdateOperation::add(KeybindUpdateTarget { keystrokes: &parse_keystrokes("ctrl-b"), action_name: "zed::SomeOtherAction", context: None, - use_key_equivalents: false, action_arguments: Some(r#"{"foo": "bar"}"#), }), r#"[ @@ -1074,11 +1099,10 @@ mod tests { } ]"# .unindent(), - KeybindUpdateOperation::Add(KeybindUpdateTarget { + KeybindUpdateOperation::add(KeybindUpdateTarget { keystrokes: &parse_keystrokes("ctrl-b"), action_name: "zed::SomeOtherAction", context: Some("Zed > Editor && some_condition = true"), - use_key_equivalents: true, action_arguments: Some(r#"{"foo": "bar"}"#), }), r#"[ @@ -1089,7 +1113,6 @@ mod tests { }, { "context": "Zed > Editor && some_condition = true", - "use_key_equivalents": true, "bindings": { "ctrl-b": [ "zed::SomeOtherAction", @@ -1117,14 +1140,12 @@ mod tests { keystrokes: &parse_keystrokes("ctrl-a"), action_name: "zed::SomeAction", context: None, - use_key_equivalents: false, action_arguments: None, }, source: KeybindUpdateTarget { keystrokes: &parse_keystrokes("ctrl-b"), action_name: "zed::SomeOtherAction", context: None, - use_key_equivalents: false, action_arguments: Some(r#"{"foo": "bar"}"#), }, target_keybind_source: KeybindSource::Base, @@ -1163,14 +1184,12 @@ mod tests { keystrokes: &parse_keystrokes("a"), action_name: "zed::SomeAction", context: None, - use_key_equivalents: false, action_arguments: None, }, source: KeybindUpdateTarget { keystrokes: &parse_keystrokes("ctrl-b"), action_name: "zed::SomeOtherAction", context: None, - use_key_equivalents: false, action_arguments: Some(r#"{"foo": "bar"}"#), }, target_keybind_source: KeybindSource::User, @@ -1204,14 +1223,12 @@ mod tests { keystrokes: &parse_keystrokes("ctrl-a"), action_name: "zed::SomeNonexistentAction", context: None, - use_key_equivalents: false, action_arguments: None, }, source: KeybindUpdateTarget { keystrokes: &parse_keystrokes("ctrl-b"), action_name: "zed::SomeOtherAction", context: None, - use_key_equivalents: false, action_arguments: None, }, target_keybind_source: KeybindSource::User, @@ -1247,14 +1264,12 @@ mod tests { keystrokes: &parse_keystrokes("ctrl-a"), action_name: "zed::SomeAction", context: None, - use_key_equivalents: false, action_arguments: None, }, source: KeybindUpdateTarget { keystrokes: &parse_keystrokes("ctrl-b"), action_name: "zed::SomeOtherAction", context: None, - use_key_equivalents: false, action_arguments: Some(r#"{"foo": "bar"}"#), }, target_keybind_source: KeybindSource::User, @@ -1292,14 +1307,12 @@ mod tests { keystrokes: &parse_keystrokes("a"), action_name: "foo::bar", context: Some("SomeContext"), - use_key_equivalents: false, action_arguments: None, }, source: KeybindUpdateTarget { keystrokes: &parse_keystrokes("c"), action_name: "foo::baz", context: Some("SomeOtherContext"), - use_key_equivalents: false, action_arguments: None, }, target_keybind_source: KeybindSource::User, @@ -1336,14 +1349,12 @@ mod tests { keystrokes: &parse_keystrokes("a"), action_name: "foo::bar", context: Some("SomeContext"), - use_key_equivalents: false, action_arguments: None, }, source: KeybindUpdateTarget { keystrokes: &parse_keystrokes("c"), action_name: "foo::baz", context: Some("SomeOtherContext"), - use_key_equivalents: false, action_arguments: None, }, target_keybind_source: KeybindSource::User, @@ -1375,7 +1386,6 @@ mod tests { context: Some("SomeContext"), keystrokes: &parse_keystrokes("a"), action_name: "foo::bar", - use_key_equivalents: false, action_arguments: None, }, target_keybind_source: KeybindSource::User, @@ -1407,7 +1417,6 @@ mod tests { context: Some("SomeContext"), keystrokes: &parse_keystrokes("a"), action_name: "foo::bar", - use_key_equivalents: false, action_arguments: Some("true"), }, target_keybind_source: KeybindSource::User, @@ -1450,7 +1459,6 @@ mod tests { context: Some("SomeContext"), keystrokes: &parse_keystrokes("a"), action_name: "foo::bar", - use_key_equivalents: false, action_arguments: Some("true"), }, target_keybind_source: KeybindSource::User, @@ -1472,4 +1480,54 @@ mod tests { .unindent(), ); } + + #[test] + fn test_append() { + check_keymap_update( + r#"[ + { + "context": "SomeOtherContext", + "use_key_equivalents": true, + "bindings": { + "b": "foo::bar", + } + }, + ]"# + .unindent(), + KeybindUpdateOperation::Add { + source: KeybindUpdateTarget { + context: Some("SomeContext"), + keystrokes: &parse_keystrokes("a"), + action_name: "foo::baz", + action_arguments: Some("true"), + }, + from: Some(KeybindUpdateTarget { + context: Some("SomeOtherContext"), + keystrokes: &parse_keystrokes("b"), + action_name: "foo::bar", + action_arguments: None, + }), + }, + r#"[ + { + "context": "SomeOtherContext", + "use_key_equivalents": true, + "bindings": { + "b": "foo::bar", + } + }, + { + "context": "SomeContext", + "use_key_equivalents": true, + "bindings": { + "a": [ + "foo::baz", + true + ] + } + } + ]"# + .unindent(), + ); + } } diff --git a/crates/settings/src/settings_json.rs b/crates/settings/src/settings_json.rs index 1aed18b44ad46c78299e71314c62ebd17d4955cb..a448eb27375645b63247703affe25f2524164b8b 100644 --- a/crates/settings/src/settings_json.rs +++ b/crates/settings/src/settings_json.rs @@ -437,17 +437,19 @@ pub fn append_top_level_array_value_in_json_text( ); debug_assert_eq!(cursor.node().kind(), "]"); let close_bracket_start = cursor.node().start_byte(); - cursor.goto_previous_sibling(); - while (cursor.node().is_extra() || cursor.node().is_missing()) && cursor.goto_previous_sibling() - { - } + while cursor.goto_previous_sibling() + && (cursor.node().is_extra() || cursor.node().is_missing()) + && !cursor.node().is_error() + {} let mut comma_range = None; let mut prev_item_range = None; - if cursor.node().kind() == "," { + if cursor.node().kind() == "," || is_error_of_kind(&mut cursor, ",") { comma_range = Some(cursor.node().byte_range()); - while cursor.goto_previous_sibling() && cursor.node().is_extra() {} + while cursor.goto_previous_sibling() + && (cursor.node().is_extra() || cursor.node().is_missing()) + {} debug_assert_ne!(cursor.node().kind(), "["); prev_item_range = Some(cursor.node().range()); @@ -514,6 +516,17 @@ pub fn append_top_level_array_value_in_json_text( replace_value.push('\n'); } return Ok((replace_range, replace_value)); + + fn is_error_of_kind(cursor: &mut tree_sitter::TreeCursor<'_>, kind: &str) -> bool { + if cursor.node().kind() != "ERROR" { + return false; + } + + let descendant_index = cursor.descendant_index(); + let res = cursor.goto_first_child() && cursor.node().kind() == kind; + cursor.goto_descendant(descendant_index); + return res; + } } pub fn to_pretty_json( diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 5b2cca92bb3f5c60d2e8386aabd3f41c44c85e32..4526b7fcc8d8c598846dd3a230b53be913e7daa0 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -1,5 +1,5 @@ use std::{ - ops::{Not, Range}, + ops::{Not as _, Range}, sync::Arc, }; @@ -1602,32 +1602,45 @@ impl KeybindingEditorModal { Ok(action_arguments) } - fn save(&mut self, cx: &mut Context<Self>) { - let existing_keybind = self.editing_keybind.clone(); - let fs = self.fs.clone(); + fn validate_keystrokes(&self, cx: &App) -> anyhow::Result<Vec<Keystroke>> { let new_keystrokes = self .keybind_editor .read_with(cx, |editor, _| editor.keystrokes().to_vec()); - if new_keystrokes.is_empty() { - self.set_error(InputError::error("Keystrokes cannot be empty"), cx); - return; - } - let tab_size = cx.global::<settings::SettingsStore>().json_tab_size(); + anyhow::ensure!(!new_keystrokes.is_empty(), "Keystrokes cannot be empty"); + Ok(new_keystrokes) + } + + fn validate_context(&self, cx: &App) -> anyhow::Result<Option<String>> { let new_context = self .context_editor .read_with(cx, |input, cx| input.editor().read(cx).text(cx)); - let new_context = new_context.is_empty().not().then_some(new_context); - let new_context_err = new_context.as_deref().and_then(|context| { - gpui::KeyBindingContextPredicate::parse(context) - .context("Failed to parse key context") - .err() - }); - if let Some(err) = new_context_err { - // TODO: store and display as separate error - // TODO: also, should be validating on keystroke - self.set_error(InputError::error(err.to_string()), cx); - return; - } + let Some(context) = new_context.is_empty().not().then_some(new_context) else { + return Ok(None); + }; + gpui::KeyBindingContextPredicate::parse(&context).context("Failed to parse key context")?; + + Ok(Some(context)) + } + + fn save(&mut self, cx: &mut Context<Self>) { + let existing_keybind = self.editing_keybind.clone(); + let fs = self.fs.clone(); + let tab_size = cx.global::<settings::SettingsStore>().json_tab_size(); + let new_keystrokes = match self.validate_keystrokes(cx) { + Err(err) => { + self.set_error(InputError::error(err.to_string()), cx); + return; + } + Ok(keystrokes) => keystrokes, + }; + + let new_context = match self.validate_context(cx) { + Err(err) => { + self.set_error(InputError::error(err.to_string()), cx); + return; + } + Ok(context) => context, + }; let new_action_args = match self.validate_action_arguments(cx) { Err(input_err) => { @@ -2064,46 +2077,45 @@ async fn save_keybinding_update( .await .context("Failed to load keymap file")?; - let operation = if !create { - let existing_keystrokes = existing.keystrokes().unwrap_or_default(); - let existing_context = existing - .context - .as_ref() - .and_then(KeybindContextString::local_str); - let existing_args = existing - .action_arguments - .as_ref() - .map(|args| args.text.as_ref()); + let existing_keystrokes = existing.keystrokes().unwrap_or_default(); + let existing_context = existing + .context + .as_ref() + .and_then(KeybindContextString::local_str); + let existing_args = existing + .action_arguments + .as_ref() + .map(|args| args.text.as_ref()); + + let target = settings::KeybindUpdateTarget { + context: existing_context, + keystrokes: existing_keystrokes, + action_name: &existing.action_name, + action_arguments: existing_args, + }; + + let source = settings::KeybindUpdateTarget { + context: new_context, + keystrokes: new_keystrokes, + action_name: &existing.action_name, + action_arguments: new_args, + }; + let operation = if !create { settings::KeybindUpdateOperation::Replace { - target: settings::KeybindUpdateTarget { - context: existing_context, - keystrokes: existing_keystrokes, - action_name: &existing.action_name, - use_key_equivalents: false, - action_arguments: existing_args, - }, + target, target_keybind_source: existing .source .as_ref() .map(|(source, _name)| *source) .unwrap_or(KeybindSource::User), - source: settings::KeybindUpdateTarget { - context: new_context, - keystrokes: new_keystrokes, - action_name: &existing.action_name, - use_key_equivalents: false, - action_arguments: new_args, - }, + source, } } else { - settings::KeybindUpdateOperation::Add(settings::KeybindUpdateTarget { - context: new_context, - keystrokes: new_keystrokes, - action_name: &existing.action_name, - use_key_equivalents: false, - action_arguments: new_args, - }) + settings::KeybindUpdateOperation::Add { + source, + from: Some(target), + } }; let updated_keymap_contents = settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size) @@ -2137,7 +2149,6 @@ async fn remove_keybinding( .and_then(KeybindContextString::local_str), keystrokes, action_name: &existing.action_name, - use_key_equivalents: false, action_arguments: existing .action_arguments .as_ref() From 2a49f40cf53c23a674d9cd47070a7ad38ebc14be Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 16 Jul 2025 11:49:53 -0300 Subject: [PATCH 119/658] docs: Add some improvements to the agent panel page (#34543) Release Notes: - N/A --- docs/src/ai/agent-panel.md | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/docs/src/ai/agent-panel.md b/docs/src/ai/agent-panel.md index 3c04ae5c43f87ee54e96a253300aa20524d6d844..ca35e06e113c401876cc68de1a1cfa83846352c6 100644 --- a/docs/src/ai/agent-panel.md +++ b/docs/src/ai/agent-panel.md @@ -1,18 +1,21 @@ # Agent Panel -The Agent Panel provides you with a way to interact with LLMs. -You can use it for various tasks, such as generating code, asking questions about your code base, and general inquiries such as emails and documentation. +The Agent Panel provides you with a surface to interact with LLMs, enabling various types of tasks, such as generating code, asking questions about your codebase, and general inquiries like emails, documentation, and more. -To open the Agent Panel, use the `agent: new thread` action in [the Command Palette](../getting-started.md#command-palette) or click the ✨ (sparkles) icon in the status bar. +To open it, use the `agent: new thread` action in [the Command Palette](../getting-started.md#command-palette) or click the ✨ (sparkles) icon in the status bar. -If you're using the Agent Panel for the first time, you'll need to [configure at least one LLM provider](./configuration.md). +If you're using the Agent Panel for the first time, you need to have at least one LLM provider configured. +You can do that by: + +1. [subscribing to our Pro plan](https://zed.dev/pricing), so you have access to our hosted models +2. or by [bringing your own API keys](./configuration.md#use-your-own-keys) for your desired provider ## Overview {#overview} After you've configured one or more LLM providers, type at the message editor and hit `enter` to submit your prompt. If you need extra room to type, you can expand the message editor with {#kb agent::ExpandMessageEditor}. -You should start to see the responses stream in with indications of [which tools](./tools.md) the AI is using to fulfill your prompt. +You should start to see the responses stream in with indications of [which tools](./tools.md) the model is using to fulfill your prompt. ### Editing Messages {#editing-messages} @@ -21,13 +24,13 @@ You can click on the card that contains your message and re-submit it with an ad ### Checkpoints {#checkpoints} -Every time the AI performs an edit, you should see a "Restore Checkpoint" button to the top of your message, allowing you to return your codebase to the state it was in prior to that message. +Every time the AI performs an edit, you should see a "Restore Checkpoint" button to the top of your message, allowing you to return your code base to the state it was in prior to that message. The checkpoint button appears even if you interrupt the thread midway through an edit attempt, as this is likely a moment when you've identified that the agent is not heading in the right direction and you want to revert back. ### Navigating History {#navigating-history} -To quickly navigate through recently opened threads, use the {#kb agent::ToggleNavigationMenu} binding, when focused on the panel's editor, or click the hamburger icon button at the top left of the panel to open the dropdown that shows you the six most recent threads. +To quickly navigate through recently opened threads, use the {#kb agent::ToggleNavigationMenu} binding, when focused on the panel's editor, or click the menu icon button at the top left of the panel to open the dropdown that shows you the six most recent threads. The items in this menu function similarly to tabs, and closing them doesn’t delete the thread; instead, it simply removes them from the recent list. @@ -39,6 +42,8 @@ Zed is built with collaboration natively integrated. This approach extends to collaboration with AI as well. To follow the agent reading through your codebase and performing edits, click on the "crosshair" icon button at the bottom left of the panel. +You can also do that with the keyboard by pressing the `cmd`/`ctrl` modifier with `enter` when submitting a message. + ### Get Notified {#get-notified} If you send a prompt to the Agent and then move elsewhere, thus putting Zed in the background, you can be notified of whether its response is finished either via: @@ -63,12 +68,12 @@ So, if your active tab had edits made by the AI, you'll see diffs with the same ## Adding Context {#adding-context} -Although Zed's agent is very efficient at reading through your codebase to autonomously pick up relevant files, directories, and other context, manually adding context is still encouraged as a way to speed up and improve the AI's response quality. +Although Zed's agent is very efficient at reading through your code base to autonomously pick up relevant files, directories, and other context, manually adding context is still encouraged as a way to speed up and improve the AI's response quality. -If you have a tab open when opening the Agent Panel, that tab appears as a suggested context in form of a dashed button. +If you have a tab open while using the Agent Panel, that tab appears as a suggested context in form of a dashed button. You can also add other forms of context by either mentioning them with `@` or hitting the `+` icon button. -You can even add previous threads as context by mentioning them with `@thread`, or by selecting the "New From Summary" option from the top-right menu to continue a longer conversation, keeping it within the context window. +You can even add previous threads as context by mentioning them with `@thread`, or by selecting the "New From Summary" option from the `+` menu to continue a longer conversation, keeping it within the context window. Pasting images as context is also supported by the Agent Panel. @@ -141,24 +146,17 @@ You can remove and edit responses from the LLM, swap roles, and include more con For users who have been with us for some time, you'll notice that text threads are our original assistant panel—users love it for the control it offers. We do not plan to deprecate text threads, but it should be noted that if you want the AI to write to your code base autonomously, that's only available in the newer, and now default, "Threads". -### Text Thread History {#text-thread-history} - -Content from text thread are saved to your file system. -Visit [the dedicated docs](./text-threads.md#history) for more info. - ## Errors and Debugging {#errors-and-debugging} In case of any error or strange LLM response behavior, the best way to help the Zed team debug is by reaching for the `agent: open thread as markdown` action and attaching that data as part of your issue on GitHub. -This action exposes the entire thread in the form of Markdown and allows for deeper understanding of what each tool call was doing. - You can also open threads as Markdown by clicking on the file icon button, to the right of the thumbs down button, when focused on the panel's editor. ## Feedback {#feedback} -Every change we make to Zed's system prompt and tool set, needs to be backed by an eval with good scores. +Every change we make to Zed's system prompt and tool set, needs to be backed by a thorough eval with good scores. -Every time the LLM performs a weird change or investigates a certain topic in your codebase completely incorrectly, it's an indication that there's an improvement opportunity. +Every time the LLM performs a weird change or investigates a certain topic in your code base incorrectly, it's an indication that there's an improvement opportunity. > Note that rating responses will send your data related to that response to Zed's servers. > See [AI Improvement](./ai-improvement.md) and [Privacy and Security](./privacy-and-security.md) for more information about Zed's approach to AI improvement, privacy, and security. From b0e0485b32e34b8416c010a6f0c86ed4e46759a0 Mon Sep 17 00:00:00 2001 From: Marshall Bowers <git@maxdeviant.com> Date: Wed, 16 Jul 2025 10:50:54 -0400 Subject: [PATCH 120/658] docs: Add redirects for language pages (#34544) This PR adds some more docs redirects for language pages. Release Notes: - N/A --- docs/book.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/book.toml b/docs/book.toml index 70e294c014fc4a89a6d1a1ec3c2b8a0eb4be3637..f5d186f377698e21a928f2f978f87f895da8944d 100644 --- a/docs/book.toml +++ b/docs/book.toml @@ -57,6 +57,12 @@ enable = false "/adding-new-languages.html" = "/docs/extensions/languages.html" "/elixir.html" = "/docs/languages/elixir.html" "/javascript.html" = "/docs/languages/javascript.html" +"/languages/languages/html.html" = "/docs/languages/html.html" +"/languages/languages/javascript.html" = "/docs/languages/javascript.html" +"/languages/languages/makefile.html" = "/docs/languages/makefile.html" +"/languages/languages/nim.html" = "/docs/languages/nim.html" +"/languages/languages/ruby.html" = "/docs/languages/ruby.html" +"/languages/languages/scala.html" = "/docs/languages/scala.html" "/python.html" = "/docs/languages/python.html" "/ruby.html" = "/docs/languages/ruby.html" From 8ee5bf2c38528770620d33ead1d1042c6758287b Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Wed, 16 Jul 2025 21:14:08 +0530 Subject: [PATCH 121/658] open_router: Fix tool_choice getting serialized to null (#34532) Closes #34314 This PR resolves an issue where serde(untagged) caused Rust None values to serialize as null, which OpenRouter's Mistral API (when tool_choice is present) incorrectly interprets as a defined value, leading to a 400 error. By replacing serde(untagged) with serde(snake_case), None values are now correctly omitted from the serialized JSON, fixing the problem. P.S. A separate PR will address serde(untagged) usage for other providers, as null is not expected for them either. Release Notes: - Fix ToolChoice getting serialized to null on OpenRouter --- crates/open_router/src/open_router.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/open_router/src/open_router.rs b/crates/open_router/src/open_router.rs index 4128426a7fb429337028b03c12e90c6395651f0e..3e6e406d9842d5996f2e866d534094ded23fd61c 100644 --- a/crates/open_router/src/open_router.rs +++ b/crates/open_router/src/open_router.rs @@ -153,11 +153,12 @@ pub struct RequestUsage { } #[derive(Debug, Serialize, Deserialize)] -#[serde(untagged)] +#[serde(rename_all = "lowercase")] pub enum ToolChoice { Auto, Required, None, + #[serde(untagged)] Other(ToolDefinition), } From e339566dab4d64431a57bc828615cef581c707fe Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon <oleksiy@zed.dev> Date: Wed, 16 Jul 2025 18:46:13 +0300 Subject: [PATCH 122/658] agent: Limit the size of patches generated from user edits (#34548) Gradually remove details from a patch to keep it within the size limit. This helps avoid using too much context when the user pastes large files, generates files, or just makes many changes between agent notifications. Release Notes: - N/A --- Cargo.lock | 1 + crates/assistant_tools/Cargo.toml | 1 + .../src/project_notifications_tool.rs | 145 +++++++++++++++++- 3 files changed, 145 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a2e9fc26ca417e9cf771cc5dd5e1a2e92d4e53e7..395087168808d53ac5e508d7805e61bbae4cc932 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -779,6 +779,7 @@ dependencies = [ "collections", "component", "derive_more 0.99.19", + "diffy", "editor", "feature_flags", "fs", diff --git a/crates/assistant_tools/Cargo.toml b/crates/assistant_tools/Cargo.toml index 2b8958feb1bddc719bcd085058cdb5162fd777b1..e234b62b142c368ab8383df4eeff8848704a5b98 100644 --- a/crates/assistant_tools/Cargo.toml +++ b/crates/assistant_tools/Cargo.toml @@ -63,6 +63,7 @@ which.workspace = true workspace-hack.workspace = true workspace.workspace = true zed_llm_client.workspace = true +diffy = "0.4.2" [dev-dependencies] lsp = { workspace = true, features = ["test-support"] } diff --git a/crates/assistant_tools/src/project_notifications_tool.rs b/crates/assistant_tools/src/project_notifications_tool.rs index 1b926bb4469593689c4bdf797b055d71c60fca35..ec315d9ab15337ea78784df84e949090d2e0f1da 100644 --- a/crates/assistant_tools/src/project_notifications_tool.rs +++ b/crates/assistant_tools/src/project_notifications_tool.rs @@ -6,7 +6,7 @@ use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchem use project::Project; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use std::sync::Arc; +use std::{fmt::Write, sync::Arc}; use ui::IconName; #[derive(Debug, Serialize, Deserialize, JsonSchema)] @@ -59,7 +59,9 @@ impl Tool for ProjectNotificationsTool { // NOTE: Changes to this prompt require a symmetric update in the LLM Worker const HEADER: &str = include_str!("./project_notifications_tool/prompt_header.txt"); - result(&format!("{HEADER}\n\n```diff\n{user_edits_diff}\n```\n").replace("\r\n", "\n")) + const MAX_BYTES: usize = 8000; + let diff = fit_patch_to_size(&user_edits_diff, MAX_BYTES); + result(&format!("{HEADER}\n\n```diff\n{diff}\n```\n").replace("\r\n", "\n")) } } @@ -67,11 +69,95 @@ fn result(response: &str) -> ToolResult { Task::ready(Ok(response.to_string().into())).into() } +/// Make sure that the patch fits into the size limit (in bytes). +/// Compress the patch by omitting some parts if needed. +/// Unified diff format is assumed. +fn fit_patch_to_size(patch: &str, max_size: usize) -> String { + if patch.len() <= max_size { + return patch.to_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); + file_patches.sort_by_key(|patch| patch.len()); + let compressed_patches = file_patches + .iter() + .rev() + .map(|patch| { + if current_size > max_size { + let compressed = compress_patch(patch).unwrap_or_else(|_| patch.to_string()); + current_size -= patch.len() - compressed.len(); + compressed + } else { + patch.to_string() + } + }) + .collect::<Vec<_>>(); + + if current_size <= max_size { + return compressed_patches.join("\n\n"); + } + + // Compression level 2: list paths of the changed files only + let filenames = file_patches + .iter() + .map(|patch| { + let patch = diffy::Patch::from_str(patch).unwrap(); + let path = patch + .modified() + .and_then(|path| path.strip_prefix("b/")) + .unwrap_or_default(); + format!("- {path}\n") + }) + .collect::<Vec<_>>(); + + filenames.join("") +} + +/// Split a potentially multi-file patch into multiple single-file patches +fn split_patch(patch: &str) -> Vec<String> { + let mut result = Vec::new(); + let mut current_patch = String::new(); + + for line in patch.lines() { + if line.starts_with("---") && !current_patch.is_empty() { + result.push(current_patch.trim_end_matches('\n').into()); + current_patch = String::new(); + } + current_patch.push_str(line); + current_patch.push('\n'); + } + + if !current_patch.is_empty() { + result.push(current_patch.trim_end_matches('\n').into()); + } + + result +} + +fn compress_patch(patch: &str) -> anyhow::Result<String> { + let patch = diffy::Patch::from_str(patch)?; + let mut out = String::new(); + + writeln!(out, "--- {}", patch.original().unwrap_or("a"))?; + writeln!(out, "+++ {}", patch.modified().unwrap_or("b"))?; + + for hunk in patch.hunks() { + writeln!(out, "@@ -{} +{} @@", hunk.old_range(), hunk.new_range())?; + writeln!(out, "[...skipped...]")?; + } + + Ok(out) +} + #[cfg(test)] mod tests { use super::*; use assistant_tool::ToolResultContent; use gpui::{AppContext, TestAppContext}; + use indoc::indoc; use language_model::{LanguageModelRequest, fake_provider::FakeLanguageModelProvider}; use project::{FakeFs, Project}; use serde_json::json; @@ -206,6 +292,61 @@ mod tests { ); } + #[test] + fn test_patch_compression() { + // Given a patch that doesn't fit into the size budget + let patch = indoc! {" + --- a/dir/test.txt + +++ b/dir/test.txt + @@ -1,3 +1,3 @@ + line 1 + -line 2 + +CHANGED + line 3 + @@ -10,2 +10,2 @@ + line 10 + -line 11 + +line eleven + + + --- a/dir/another.txt + +++ b/dir/another.txt + @@ -100,1 +1,1 @@ + -before + +after + "}; + + // When the size deficit can be compensated by dropping the body, + // then the body should be trimmed for larger files first + let limit = patch.len() - 10; + let compressed = fit_patch_to_size(patch, limit); + let expected = indoc! {" + --- a/dir/test.txt + +++ b/dir/test.txt + @@ -1,3 +1,3 @@ + [...skipped...] + @@ -10,2 +10,2 @@ + [...skipped...] + + + --- a/dir/another.txt + +++ b/dir/another.txt + @@ -100,1 +1,1 @@ + -before + +after"}; + assert_eq!(compressed, expected); + + // When the size deficit is too large, then only file paths + // should be returned + let limit = 10; + let compressed = fit_patch_to_size(patch, limit); + let expected = indoc! {" + - dir/another.txt + - dir/test.txt + "}; + assert_eq!(compressed, expected); + } + fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { let settings_store = SettingsStore::test(cx); From 9ab3d55211daafe71f9141a9c1c542f5cec23f23 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Wed, 16 Jul 2025 12:14:09 -0400 Subject: [PATCH 123/658] Add exact matching option to keymap editor search (#34497) We know have the ability to filter matches in the keymap editor search by exact keystroke matches. This allows user's to have the same behavior as vscode when they toggle all actions with the same bindings We also fixed a bug where conflicts weren't counted correctly when saving a keymapping. This cause issues where warnings wouldn't appear when they were supposed to. Release Notes: - N/A --------- Co-authored-by: Ben Kunkle <ben@zed.dev> --- assets/icons/equal.svg | 1 + crates/icons/src/icons.rs | 1 + crates/settings_ui/src/keybindings.rs | 201 +++++++++++++++++++------- 3 files changed, 150 insertions(+), 53 deletions(-) create mode 100644 assets/icons/equal.svg diff --git a/assets/icons/equal.svg b/assets/icons/equal.svg new file mode 100644 index 0000000000000000000000000000000000000000..9b3a151a12fc3dea5f1eb295bf299e6360846ed2 --- /dev/null +++ b/assets/icons/equal.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-equal-icon lucide-equal"><line x1="5" x2="19" y1="9" y2="9"/><line x1="5" x2="19" y1="15" y2="15"/></svg> diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index b2ec7684355c27280ea7d4a056bfb30ff31ea79b..b29a8b78e679907d1077e015f3aae2528511267b 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -107,6 +107,7 @@ pub enum IconName { Ellipsis, EllipsisVertical, Envelope, + Equal, Eraser, Escape, Exit, diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 4526b7fcc8d8c598846dd3a230b53be913e7daa0..c83a4c2423a447129eaeebd9035f02363b6e2c1c 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -66,6 +66,8 @@ actions!( ToggleConflictFilter, /// Toggle Keystroke search ToggleKeystrokeSearch, + /// Toggles exact matching for keystroke search + ToggleExactKeystrokeMatching, ] ); @@ -176,14 +178,16 @@ impl KeymapEventChannel { enum SearchMode { #[default] Normal, - KeyStroke, + KeyStroke { + exact_match: bool, + }, } impl SearchMode { fn invert(&self) -> Self { match self { - SearchMode::Normal => SearchMode::KeyStroke, - SearchMode::KeyStroke => SearchMode::Normal, + SearchMode::Normal => SearchMode::KeyStroke { exact_match: false }, + SearchMode::KeyStroke { .. } => SearchMode::Normal, } } } @@ -204,7 +208,11 @@ impl FilterState { } } -type ActionMapping = (SharedString, Option<SharedString>); +#[derive(Debug, Default, PartialEq, Eq, Clone, Hash)] +struct ActionMapping { + keystroke_text: SharedString, + context: Option<SharedString>, +} #[derive(Default)] struct ConflictState { @@ -257,6 +265,12 @@ impl ConflictState { }) } + fn will_conflict(&self, action_mapping: ActionMapping) -> Option<Vec<usize>> { + self.action_keybind_mapping + .get(&action_mapping) + .and_then(|indices| indices.is_empty().not().then_some(indices.clone())) + } + fn has_conflict(&self, candidate_idx: &usize) -> bool { self.conflicts.contains(candidate_idx) } @@ -375,7 +389,7 @@ impl KeymapEditor { fn current_keystroke_query(&self, cx: &App) -> Vec<Keystroke> { match self.search_mode { - SearchMode::KeyStroke => self + SearchMode::KeyStroke { .. } => self .keystroke_editor .read(cx) .keystrokes() @@ -432,17 +446,27 @@ impl KeymapEditor { } match this.search_mode { - SearchMode::KeyStroke => { + SearchMode::KeyStroke { exact_match } => { matches.retain(|item| { this.keybindings[item.candidate_id] .keystrokes() .is_some_and(|keystrokes| { - keystroke_query.iter().all(|key| { - keystrokes.iter().any(|keystroke| { - keystroke.key == key.key - && keystroke.modifiers == key.modifiers + if exact_match { + keystroke_query.len() == keystrokes.len() + && keystroke_query.iter().zip(keystrokes).all( + |(query, keystroke)| { + query.key == keystroke.key + && query.modifiers == keystroke.modifiers + }, + ) + } else { + keystroke_query.iter().all(|key| { + keystrokes.iter().any(|keystroke| { + keystroke.key == key.key + && keystroke.modifiers == key.modifiers + }) }) - }) + } }) }); } @@ -699,7 +723,12 @@ impl KeymapEditor { window: &mut Window, cx: &mut Context<Self>, ) { + let weak = cx.weak_entity(); self.context_menu = self.selected_binding().map(|selected_binding| { + let key_strokes = selected_binding + .keystrokes() + .map(Vec::from) + .unwrap_or_default(); let selected_binding_has_no_context = selected_binding .context .as_ref() @@ -727,6 +756,22 @@ impl KeymapEditor { "Copy Context", Box::new(CopyContext), ) + .entry("Show matching keybindings", None, { + let weak = weak.clone(); + let key_strokes = key_strokes.clone(); + + move |_, cx| { + weak.update(cx, |this, cx| { + this.filter_state = FilterState::All; + this.search_mode = SearchMode::KeyStroke { exact_match: true }; + + this.keystroke_editor.update(cx, |editor, cx| { + editor.set_keystrokes(key_strokes.clone(), cx); + }); + }) + .ok(); + } + }) }); let context_menu_handle = context_menu.focus_handle(cx); @@ -943,17 +988,32 @@ impl KeymapEditor { // Update the keystroke editor to turn the `search` bool on self.keystroke_editor.update(cx, |keystroke_editor, cx| { - keystroke_editor.set_search_mode(self.search_mode == SearchMode::KeyStroke); + keystroke_editor + .set_search_mode(matches!(self.search_mode, SearchMode::KeyStroke { .. })); cx.notify(); }); match self.search_mode { - SearchMode::KeyStroke => { + SearchMode::KeyStroke { .. } => { window.focus(&self.keystroke_editor.read(cx).recording_focus_handle(cx)); } SearchMode::Normal => {} } } + + fn toggle_exact_keystroke_matching( + &mut self, + _: &ToggleExactKeystrokeMatching, + _: &mut Window, + cx: &mut Context<Self>, + ) { + let SearchMode::KeyStroke { exact_match } = &mut self.search_mode else { + return; + }; + + *exact_match = !(*exact_match); + self.on_query_changed(cx); + } } #[derive(Clone)] @@ -970,13 +1030,14 @@ struct ProcessedKeybinding { impl ProcessedKeybinding { fn get_action_mapping(&self) -> ActionMapping { - ( - self.keystroke_text.clone(), - self.context + ActionMapping { + keystroke_text: self.keystroke_text.clone(), + context: self + .context .as_ref() .and_then(|context| context.local()) .cloned(), - ) + } } fn keystrokes(&self) -> Option<&[Keystroke]> { @@ -1061,6 +1122,7 @@ impl Render for KeymapEditor { .on_action(cx.listener(Self::copy_context_to_clipboard)) .on_action(cx.listener(Self::toggle_conflict_filter)) .on_action(cx.listener(Self::toggle_keystroke_search)) + .on_action(cx.listener(Self::toggle_exact_keystroke_matching)) .size_full() .p_2() .gap_1() @@ -1103,7 +1165,10 @@ impl Render for KeymapEditor { cx, ) }) - .toggle_state(matches!(self.search_mode, SearchMode::KeyStroke)) + .toggle_state(matches!( + self.search_mode, + SearchMode::KeyStroke { .. } + )) .on_click(|_, window, cx| { window.dispatch_action(ToggleKeystrokeSearch.boxed_clone(), cx); }), @@ -1141,19 +1206,43 @@ impl Render for KeymapEditor { ) }), ) - .when(matches!(self.search_mode, SearchMode::KeyStroke), |this| { - this.child( - div() - .map(|this| { - if self.keybinding_conflict_state.any_conflicts() { - this.pr(rems_from_px(54.)) - } else { - this.pr_7() - } - }) - .child(self.keystroke_editor.clone()), - ) - }), + .when_some( + match self.search_mode { + SearchMode::Normal => None, + SearchMode::KeyStroke { exact_match } => Some(exact_match), + }, + |this, exact_match| { + this.child( + h_flex() + .map(|this| { + if self.keybinding_conflict_state.any_conflicts() { + this.pr(rems_from_px(54.)) + } else { + this.pr_7() + } + }) + .child(self.keystroke_editor.clone()) + .child( + div().p_1().child( + IconButton::new( + "keystrokes-exact-match", + IconName::Equal, + ) + .shape(IconButtonShape::Square) + .toggle_state(exact_match) + .on_click( + cx.listener(|_, _, window, cx| { + window.dispatch_action( + ToggleExactKeystrokeMatching.boxed_clone(), + cx, + ); + }), + ), + ), + ), + ) + }, + ), ) .child( Table::new() @@ -1650,20 +1739,23 @@ impl KeybindingEditorModal { Ok(input) => input, }; - let action_mapping: ActionMapping = ( - ui::text_for_keystrokes(&new_keystrokes, cx).into(), - new_context - .as_ref() - .map(Into::into) - .or_else(|| existing_keybind.get_action_mapping().1), - ); + let action_mapping = ActionMapping { + keystroke_text: ui::text_for_keystrokes(&new_keystrokes, cx).into(), + context: new_context.as_ref().map(Into::into), + }; - if let Some(conflicting_indices) = self - .keymap_editor - .read(cx) - .keybinding_conflict_state - .conflicting_indices_for_mapping(action_mapping, self.editing_keybind_idx) - { + let conflicting_indices = if self.creating { + self.keymap_editor + .read(cx) + .keybinding_conflict_state + .will_conflict(action_mapping) + } else { + self.keymap_editor + .read(cx) + .keybinding_conflict_state + .conflicting_indices_for_mapping(action_mapping, self.editing_keybind_idx) + }; + if let Some(conflicting_indices) = conflicting_indices { let first_conflicting_index = conflicting_indices[0]; let conflicting_action_name = self .keymap_editor @@ -1739,10 +1831,11 @@ impl KeybindingEditorModal { .log_err(); } else { this.update(cx, |this, cx| { - let action_mapping = ( - ui::text_for_keystrokes(new_keystrokes.as_slice(), cx).into(), - new_context.map(SharedString::from), - ); + let action_mapping = ActionMapping { + keystroke_text: ui::text_for_keystrokes(new_keystrokes.as_slice(), cx) + .into(), + context: new_context.map(SharedString::from), + }; this.keymap_editor.update(cx, |keymap, cx| { keymap.previous_edit = Some(PreviousEdit::Keybinding { @@ -2221,6 +2314,11 @@ impl KeystrokeInput { } } + fn set_keystrokes(&mut self, keystrokes: Vec<Keystroke>, cx: &mut Context<Self>) { + self.keystrokes = keystrokes; + self.keystrokes_changed(cx); + } + fn dummy(modifiers: Modifiers) -> Keystroke { return Keystroke { modifiers, @@ -2438,14 +2536,11 @@ impl KeystrokeInput { fn clear_keystrokes( &mut self, _: &ClearKeystrokes, - window: &mut Window, + _window: &mut Window, cx: &mut Context<Self>, ) { - if !self.outer_focus_handle.is_focused(window) { - return; - } self.keystrokes.clear(); - cx.notify(); + self.keystrokes_changed(cx); } } From 313f5968ebc26ff294f04d1714f0ac6fa2a0fb15 Mon Sep 17 00:00:00 2001 From: Adam <stepanek.ada@seznam.cz> Date: Wed, 16 Jul 2025 18:32:58 +0200 Subject: [PATCH 124/658] Improve the `read_file` tool prompt for long files (#34542) Closes [#ISSUE](https://github.com/zed-industries/zed/issues/31780) Release Notes: - Enhanced `read_file` tool call result message for long files. --- crates/assistant_tools/src/read_file_tool.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/assistant_tools/src/read_file_tool.rs b/crates/assistant_tools/src/read_file_tool.rs index 6bbc2fc0897fa92d676ab92392dbadac0447be33..dc504e2dc4adf5dbb155f03f9c92320fdedc15ae 100644 --- a/crates/assistant_tools/src/read_file_tool.rs +++ b/crates/assistant_tools/src/read_file_tool.rs @@ -285,7 +285,10 @@ 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." + implementations of symbols in the outline. + + Alternatively, you can fall back to the `grep` tool (if available) + to search the file for specific content." } .into()) } From 58807f0dd2e54a623c82a078023b04bd54ad265b Mon Sep 17 00:00:00 2001 From: Ben Kunkle <ben@zed.dev> Date: Wed, 16 Jul 2025 12:00:47 -0500 Subject: [PATCH 125/658] keymap_ui: Create language for Zed keybind context (#34558) Closes #ISSUE Creates a new language in the languages crate for the DSL used in Zed keybinding context. Previously, keybind context was highlighted as Rust in the keymap UI due to the expression syntax of Rust matching that of the context DSL, however, this had the side effect of highlighting upper case contexts (e.g. `Editor`) however Rust types would be highlighted based on the theme. By extracting only the necessary pieces of the Rust language `highlights.scm`, `brackets.scm`, and `config.toml`, and continuing to use the Rust grammar, we get a better result across different themes Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/languages/src/lib.rs | 4 ++ .../src/zed-keybind-context/brackets.scm | 1 + .../src/zed-keybind-context/config.toml | 6 +++ .../src/zed-keybind-context/highlights.scm | 23 ++++++++++ crates/settings_ui/src/keybindings.rs | 43 +++++++++++++------ crates/ui_input/src/ui_input.rs | 1 + 6 files changed, 65 insertions(+), 13 deletions(-) create mode 100644 crates/languages/src/zed-keybind-context/brackets.scm create mode 100644 crates/languages/src/zed-keybind-context/config.toml create mode 100644 crates/languages/src/zed-keybind-context/highlights.scm diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index 3db015a24182ff9f210958558c868da9e7168be6..431c05108184519bc110f03a801d224a3b4077d7 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -212,6 +212,10 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) { name: "gitcommit", ..Default::default() }, + LanguageInfo { + name: "zed-keybind-context", + ..Default::default() + }, ]; for registration in built_in_languages { diff --git a/crates/languages/src/zed-keybind-context/brackets.scm b/crates/languages/src/zed-keybind-context/brackets.scm new file mode 100644 index 0000000000000000000000000000000000000000..d086b2e98df0837208a13f6c6f79db84c204fb99 --- /dev/null +++ b/crates/languages/src/zed-keybind-context/brackets.scm @@ -0,0 +1 @@ +("(" @open ")" @close) diff --git a/crates/languages/src/zed-keybind-context/config.toml b/crates/languages/src/zed-keybind-context/config.toml new file mode 100644 index 0000000000000000000000000000000000000000..a999c70f6679843d07521c75a6a14bef26af67bb --- /dev/null +++ b/crates/languages/src/zed-keybind-context/config.toml @@ -0,0 +1,6 @@ +name = "Zed Keybind Context" +grammar = "rust" +autoclose_before = ")" +brackets = [ + { start = "(", end = ")", close = true, newline = false }, +] diff --git a/crates/languages/src/zed-keybind-context/highlights.scm b/crates/languages/src/zed-keybind-context/highlights.scm new file mode 100644 index 0000000000000000000000000000000000000000..9c5ec58eaeb7084bf79f31b280197b57bfe64b54 --- /dev/null +++ b/crates/languages/src/zed-keybind-context/highlights.scm @@ -0,0 +1,23 @@ +(identifier) @variable + +[ + "(" + ")" +] @punctuation.bracket + +[ + (integer_literal) + (float_literal) +] @number + +(boolean_literal) @boolean + +[ + "!=" + "==" + "=>" + ">" + "&&" + "||" + "!" +] @operator diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index c83a4c2423a447129eaeebd9035f02363b6e2c1c..2bfa6f820e9d63f3e0c9425b2caf3d7e432395b5 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -505,7 +505,7 @@ impl KeymapEditor { fn process_bindings( json_language: Arc<Language>, - rust_language: Arc<Language>, + zed_keybind_context_language: Arc<Language>, cx: &mut App, ) -> (Vec<ProcessedKeybinding>, Vec<StringMatchCandidate>) { let key_bindings_ptr = cx.key_bindings(); @@ -536,7 +536,10 @@ impl KeymapEditor { let context = key_binding .predicate() .map(|predicate| { - KeybindContextString::Local(predicate.to_string().into(), rust_language.clone()) + KeybindContextString::Local( + predicate.to_string().into(), + zed_keybind_context_language.clone(), + ) }) .unwrap_or(KeybindContextString::Global); @@ -588,11 +591,12 @@ impl KeymapEditor { let workspace = self.workspace.clone(); cx.spawn(async move |this, cx| { let json_language = load_json_language(workspace.clone(), cx).await; - let rust_language = load_rust_language(workspace.clone(), cx).await; + let zed_keybind_context_language = + load_keybind_context_language(workspace.clone(), cx).await; let (action_query, keystroke_query) = this.update(cx, |this, cx| { let (key_bindings, string_match_candidates) = - Self::process_bindings(json_language, rust_language, cx); + Self::process_bindings(json_language, zed_keybind_context_language, cx); this.keybinding_conflict_state = ConflictState::new(&key_bindings); @@ -1590,13 +1594,20 @@ impl KeybindingEditorModal { } let editor_entity = input.editor().clone(); + let workspace = workspace.clone(); cx.spawn(async move |_input_handle, cx| { let contexts = cx .background_spawn(async { collect_contexts_from_assets() }) .await; + let language = load_keybind_context_language(workspace, cx).await; editor_entity - .update(cx, |editor, _cx| { + .update(cx, |editor, cx| { + if let Some(buffer) = editor.buffer().read(cx).as_singleton() { + buffer.update(cx, |buffer, cx| { + buffer.set_language(Some(language), cx); + }); + } editor.set_completion_provider(Some(std::rc::Rc::new( KeyContextCompletionProvider { contexts }, ))); @@ -2131,25 +2142,31 @@ async fn load_json_language(workspace: WeakEntity<Workspace>, cx: &mut AsyncApp) }); } -async fn load_rust_language(workspace: WeakEntity<Workspace>, cx: &mut AsyncApp) -> Arc<Language> { - let rust_language_task = workspace +async fn load_keybind_context_language( + workspace: WeakEntity<Workspace>, + cx: &mut AsyncApp, +) -> Arc<Language> { + let language_task = workspace .read_with(cx, |workspace, cx| { workspace .project() .read(cx) .languages() - .language_for_name("Rust") + .language_for_name("Zed Keybind Context") }) - .context("Failed to load Rust language") + .context("Failed to load Zed Keybind Context language") .log_err(); - let rust_language = match rust_language_task { - Some(task) => task.await.context("Failed to load Rust language").log_err(), + let language = match language_task { + Some(task) => task + .await + .context("Failed to load Zed Keybind Context language") + .log_err(), None => None, }; - return rust_language.unwrap_or_else(|| { + return language.unwrap_or_else(|| { Arc::new(Language::new( LanguageConfig { - name: "Rust".into(), + name: "Zed Keybind Context".into(), ..Default::default() }, Some(tree_sitter_rust::LANGUAGE.into()), diff --git a/crates/ui_input/src/ui_input.rs b/crates/ui_input/src/ui_input.rs index ca2dea36df00ecb5f80fd535e98b80c1f0502141..18aa732e8153c15a064d74c88dfdb03d20bffedc 100644 --- a/crates/ui_input/src/ui_input.rs +++ b/crates/ui_input/src/ui_input.rs @@ -135,6 +135,7 @@ impl Render for SingleLineInput { let editor_style = EditorStyle { background: theme_color.ghost_element_background, local_player: cx.theme().players().local(), + syntax: cx.theme().syntax().clone(), text: text_style, ..Default::default() }; From dc8d0868ecc19f3a4436a8907206531406dbafaa Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 16 Jul 2025 19:24:34 +0200 Subject: [PATCH 126/658] project: Fix up documentation for Path Trie and add a test for having multiple present nodes (#34560) cc @cole-miller I was worried with https://github.com/zed-industries/zed/pull/34460#discussion_r2210814806 that PathTrie would not be able to support nested .git repositories, but it seems fine. Release Notes: - N/A --- crates/project/src/manifest_tree/path_trie.rs | 32 +++++++++++++++---- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/crates/project/src/manifest_tree/path_trie.rs b/crates/project/src/manifest_tree/path_trie.rs index 0f7575324b040bc951db730ee97f7a08350d571f..1a0736765a43b9e1365334de95eacbe9dbf64382 100644 --- a/crates/project/src/manifest_tree/path_trie.rs +++ b/crates/project/src/manifest_tree/path_trie.rs @@ -6,7 +6,7 @@ use std::{ sync::Arc, }; -/// [RootPathTrie] is a workhorse of [super::ManifestTree]. It is responsible for determining the closest known project root for a given path. +/// [RootPathTrie] is a workhorse of [super::ManifestTree]. It is responsible for determining the closest known entry for a given path. /// It also determines how much of a given path is unexplored, thus letting callers fill in that gap if needed. /// Conceptually, it allows one to annotate Worktree entries with arbitrary extra metadata and run closest-ancestor searches. /// @@ -20,19 +20,16 @@ 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 and it is the only label of that kind on the path to the root of a worktree -/// (none of it's ancestors or descendants can contain the same present label) +/// - 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!). -/// - Forbidden - we know there's definitely no project root at this node and none of it's ancestors or descendants can be Present. /// 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. When any of the ancestors is forbidden, we don't need to look at the node or its ancestors. -/// When there's a present labeled node on the path to the root, we don't need to ask the adapter to run the search at all. +/// 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). /// -/// Storing project absence allows us to recognize which paths have already been scanned for a project root unsuccessfully. This way we don't need to run +/// Storing absent nodes allows us to recognize which paths have already been scanned for a project root unsuccessfully. This way we don't need to run /// such scan more than once. #[derive(Clone, Copy, Debug, PartialOrd, PartialEq, Ord, Eq)] pub(super) enum LabelPresence { @@ -237,4 +234,25 @@ mod tests { Path::new("a/") ); } + + #[test] + fn path_to_a_root_can_contain_multiple_known_nodes() { + let mut trie = RootPathTrie::<()>::new(); + trie.insert( + &TriePath::from(Path::new("a/b")), + (), + LabelPresence::Present, + ); + trie.insert(&TriePath::from(Path::new("a")), (), LabelPresence::Present); + let mut visited_paths = BTreeSet::new(); + trie.walk(&TriePath::from(Path::new("a/b/c")), &mut |path, nodes| { + assert_eq!(nodes.get(&()), Some(&LabelPresence::Present)); + if path.as_ref() != Path::new("a") && path.as_ref() != Path::new("a/b") { + panic!("Unexpected path: {}", path.as_ref().display()); + } + assert!(visited_paths.insert(path.clone())); + ControlFlow::Continue(()) + }); + assert_eq!(visited_paths.len(), 2); + } } From ffc69b07e5878e376a7d31cff3e68b3075a46e0e Mon Sep 17 00:00:00 2001 From: Smit Barmase <heysmitbarmase@gmail.com> Date: Wed, 16 Jul 2025 23:24:02 +0530 Subject: [PATCH 127/658] editor: Fix sometimes green (+) cursor style appearing when cmd-clicking to navigate and back (#34557) Regressed in https://github.com/zed-industries/zed/pull/33928 This PR clears the selection drag state when the editor focus is out. To reproduce: 1. Select some item in buffer that has a go to definition. 2. Cmd+Click mouse down on it, but don't let go. 3. Wait for 300ms+. 4. Now cursor changed to green + (valid state, this is for selection drag-n-drop). 5. Now let go of your mouse down, we switched to a different file. Cursor looks normal. 6. Come back to the previous buffer, see green + cursor style (BUG!). Release Notes: - Fixed the issue where the green (+) cursor style sometimes appears when navigating to the definition and then back to the previous buffer. --- crates/editor/src/editor.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e5ff75561594746a17a228ee1c466f003097e37a..6ad4fc0318ac322c03d45d585891ca729ef06b83 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -20562,6 +20562,7 @@ impl Editor { if event.blurred != self.focus_handle { self.last_focused_descendant = Some(event.blurred); } + self.selection_drag_state = SelectionDragState::None; self.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(false), cx); } From 048dc47d87d3a88b25beaa1b7fcdcb45ab5a3766 Mon Sep 17 00:00:00 2001 From: Marshall Bowers <git@maxdeviant.com> Date: Wed, 16 Jul 2025 13:55:01 -0400 Subject: [PATCH 128/658] collab: Remove `GET /billing/preferences` endpoint (#34566) This PR removes the `GET /billing/preferences` endpoint, as it has been moved to `cloud.zed.dev`. Release Notes: - N/A --- crates/collab/src/api/billing.rs | 50 ++----------------- crates/collab/src/cents.rs | 83 -------------------------------- crates/collab/src/lib.rs | 2 - crates/collab/src/llm.rs | 8 --- 4 files changed, 3 insertions(+), 140 deletions(-) delete mode 100644 crates/collab/src/cents.rs diff --git a/crates/collab/src/api/billing.rs b/crates/collab/src/api/billing.rs index 00688a1e82be056a06e08a84013d4e95474bc971..72a9a1b46ba5fa4f5fe3e915c1ee3fa0fe37c5e2 100644 --- a/crates/collab/src/api/billing.rs +++ b/crates/collab/src/api/billing.rs @@ -1,4 +1,5 @@ use anyhow::{Context as _, bail}; +use axum::routing::put; use axum::{ Extension, Json, Router, extract::{self, Query}, @@ -27,8 +28,8 @@ use crate::api::events::SnowflakeRow; use crate::db::billing_subscription::{ StripeCancellationReason, StripeSubscriptionStatus, SubscriptionKind, }; +use crate::llm::AGENT_EXTENDED_TRIAL_FEATURE_FLAG; use crate::llm::db::subscription_usage_meter::{self, CompletionMode}; -use crate::llm::{AGENT_EXTENDED_TRIAL_FEATURE_FLAG, DEFAULT_MAX_MONTHLY_SPEND}; use crate::rpc::{ResultExt as _, Server}; use crate::stripe_client::{ StripeCancellationDetailsReason, StripeClient, StripeCustomerId, StripeSubscription, @@ -47,10 +48,7 @@ use crate::{ pub fn router() -> Router { Router::new() - .route( - "/billing/preferences", - get(get_billing_preferences).put(update_billing_preferences), - ) + .route("/billing/preferences", put(update_billing_preferences)) .route( "/billing/subscriptions", get(list_billing_subscriptions).post(create_billing_subscription), @@ -66,11 +64,6 @@ pub fn router() -> Router { .route("/billing/usage", get(get_current_usage)) } -#[derive(Debug, Deserialize)] -struct GetBillingPreferencesParams { - github_user_id: i32, -} - #[derive(Debug, Serialize)] struct BillingPreferencesResponse { trial_started_at: Option<String>, @@ -79,43 +72,6 @@ struct BillingPreferencesResponse { model_request_overages_spend_limit_in_cents: i32, } -async fn get_billing_preferences( - Extension(app): Extension<Arc<AppState>>, - Query(params): Query<GetBillingPreferencesParams>, -) -> Result<Json<BillingPreferencesResponse>> { - let user = app - .db - .get_user_by_github_user_id(params.github_user_id) - .await? - .context("user not found")?; - - let billing_customer = app.db.get_billing_customer_by_user_id(user.id).await?; - let preferences = app.db.get_billing_preferences(user.id).await?; - - Ok(Json(BillingPreferencesResponse { - trial_started_at: billing_customer - .and_then(|billing_customer| billing_customer.trial_started_at) - .map(|trial_started_at| { - trial_started_at - .and_utc() - .to_rfc3339_opts(SecondsFormat::Millis, true) - }), - max_monthly_llm_usage_spending_in_cents: preferences - .as_ref() - .map_or(DEFAULT_MAX_MONTHLY_SPEND.0 as i32, |preferences| { - preferences.max_monthly_llm_usage_spending_in_cents - }), - model_request_overages_enabled: preferences.as_ref().map_or(false, |preferences| { - preferences.model_request_overages_enabled - }), - model_request_overages_spend_limit_in_cents: preferences - .as_ref() - .map_or(0, |preferences| { - preferences.model_request_overages_spend_limit_in_cents - }), - })) -} - #[derive(Debug, Deserialize)] struct UpdateBillingPreferencesBody { github_user_id: i32, diff --git a/crates/collab/src/cents.rs b/crates/collab/src/cents.rs deleted file mode 100644 index a05971f1417339664d667665ddff63a13237f4dc..0000000000000000000000000000000000000000 --- a/crates/collab/src/cents.rs +++ /dev/null @@ -1,83 +0,0 @@ -use serde::Serialize; - -/// A number of cents. -#[derive( - Debug, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - Clone, - Copy, - derive_more::Add, - derive_more::AddAssign, - derive_more::Sub, - derive_more::SubAssign, - Serialize, -)] -pub struct Cents(pub u32); - -impl Cents { - pub const ZERO: Self = Self(0); - - pub const fn new(cents: u32) -> Self { - Self(cents) - } - - pub const fn from_dollars(dollars: u32) -> Self { - Self(dollars * 100) - } - - pub fn saturating_sub(self, other: Cents) -> Self { - Self(self.0.saturating_sub(other.0)) - } -} - -#[cfg(test)] -mod tests { - use pretty_assertions::assert_eq; - - use super::*; - - #[test] - fn test_cents_new() { - assert_eq!(Cents::new(50), Cents(50)); - } - - #[test] - fn test_cents_from_dollars() { - assert_eq!(Cents::from_dollars(1), Cents(100)); - assert_eq!(Cents::from_dollars(5), Cents(500)); - } - - #[test] - fn test_cents_zero() { - assert_eq!(Cents::ZERO, Cents(0)); - } - - #[test] - fn test_cents_add() { - assert_eq!(Cents(50) + Cents(30), Cents(80)); - } - - #[test] - fn test_cents_add_assign() { - let mut cents = Cents(50); - cents += Cents(30); - assert_eq!(cents, Cents(80)); - } - - #[test] - fn test_cents_saturating_sub() { - assert_eq!(Cents(50).saturating_sub(Cents(30)), Cents(20)); - assert_eq!(Cents(30).saturating_sub(Cents(50)), Cents(0)); - } - - #[test] - fn test_cents_ordering() { - assert!(Cents(50) > Cents(30)); - assert!(Cents(30) < Cents(50)); - assert_eq!(Cents(50), Cents(50)); - } -} diff --git a/crates/collab/src/lib.rs b/crates/collab/src/lib.rs index 2b20c8f080e5d55eab3e81d946d7a0aaf06cffd8..905859ca6996c3593e1f13fbcb0e723531595ff6 100644 --- a/crates/collab/src/lib.rs +++ b/crates/collab/src/lib.rs @@ -1,6 +1,5 @@ pub mod api; pub mod auth; -mod cents; pub mod db; pub mod env; pub mod executor; @@ -21,7 +20,6 @@ use axum::{ http::{HeaderMap, StatusCode}, response::IntoResponse, }; -pub use cents::*; use db::{ChannelId, Database}; use executor::Executor; use llm::db::LlmDatabase; diff --git a/crates/collab/src/llm.rs b/crates/collab/src/llm.rs index cf5dec6e282662a6766edd96f3669aa096206afc..de74858168fd94ab677cee03f721a1e3fbbdfd46 100644 --- a/crates/collab/src/llm.rs +++ b/crates/collab/src/llm.rs @@ -1,8 +1,6 @@ pub mod db; mod token; -use crate::Cents; - pub use token::*; pub const AGENT_EXTENDED_TRIAL_FEATURE_FLAG: &str = "agent-extended-trial"; @@ -12,9 +10,3 @@ pub const BYPASS_ACCOUNT_AGE_CHECK_FEATURE_FLAG: &str = "bypass-account-age-chec /// 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); - -/// The default value to use for maximum spend per month if the user did not -/// explicitly set a maximum spend. -/// -/// Used to prevent surprise bills. -pub const DEFAULT_MAX_MONTHLY_SPEND: Cents = Cents::from_dollars(10); From 573836a654ecf1642f56a0e40677b219116a6855 Mon Sep 17 00:00:00 2001 From: Ben Kunkle <ben@zed.dev> Date: Wed, 16 Jul 2025 12:55:58 -0500 Subject: [PATCH 129/658] keymap_ui: Replace `zed::NoAction` with `null` (#34562) Closes #ISSUE This change applies both to the UI (we render `<null>` as muted text instead of `zed::NoAction`) as well as how we update the keymap file (the duplicated binding is bound to `null` instead of `"zed::NoAction"`) Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/settings/src/keymap_file.rs | 45 ++++++++++++++++++++++++--- crates/settings_ui/src/keybindings.rs | 45 ++++++++++++++++++++------- 2 files changed, 75 insertions(+), 15 deletions(-) diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index b61d30e405471ce3d4ab7378f64610a1057ed439..d738e30c4fb2d3c8b9ff8ed91bb5b54accbd1eae 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -881,6 +881,9 @@ pub struct KeybindUpdateTarget<'a> { impl<'a> KeybindUpdateTarget<'a> { fn action_value(&self) -> Result<Value> { + if self.action_name == gpui::NoAction.name() { + return Ok(Value::Null); + } let action_name: Value = self.action_name.into(); let value = match self.action_arguments { Some(args) => { @@ -1479,10 +1482,6 @@ mod tests { ]"# .unindent(), ); - } - - #[test] - fn test_append() { check_keymap_update( r#"[ { @@ -1529,5 +1528,43 @@ mod tests { ]"# .unindent(), ); + + check_keymap_update( + r#"[ + { + "context": "SomeOtherContext", + "use_key_equivalents": true, + "bindings": { + "b": "foo::bar", + } + }, + ]"# + .unindent(), + KeybindUpdateOperation::Remove { + target: KeybindUpdateTarget { + context: Some("SomeContext"), + keystrokes: &parse_keystrokes("a"), + action_name: "foo::baz", + action_arguments: Some("true"), + }, + target_keybind_source: KeybindSource::Default, + }, + r#"[ + { + "context": "SomeOtherContext", + "use_key_equivalents": true, + "bindings": { + "b": "foo::bar", + } + }, + { + "context": "SomeContext", + "bindings": { + "a": null + } + } + ]"# + .unindent(), + ); } } diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 2bfa6f820e9d63f3e0c9425b2caf3d7e432395b5..281b01df27b09cd031b82d954876281ffe08fbae 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -297,6 +297,7 @@ struct KeymapEditor { selected_index: Option<usize>, context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>, previous_edit: Option<PreviousEdit>, + humanized_action_names: HashMap<&'static str, SharedString>, } enum PreviousEdit { @@ -309,7 +310,7 @@ enum PreviousEdit { /// and if we don't find it, we scroll to 0 and don't set a selected index Keybinding { action_mapping: ActionMapping, - action_name: SharedString, + action_name: &'static str, /// The scrollbar position to fallback to if we don't find the keybinding during a refresh /// this can happen if there's a filter applied to the search and the keybinding modification /// filters the binding from the search results @@ -360,6 +361,14 @@ impl KeymapEditor { }) .detach(); + let humanized_action_names = + HashMap::from_iter(cx.all_action_names().into_iter().map(|&action_name| { + ( + action_name, + command_palette::humanize_action_name(action_name).into(), + ) + })); + let mut this = Self { workspace, keybindings: vec![], @@ -376,6 +385,7 @@ impl KeymapEditor { selected_index: None, context_menu: None, previous_edit: None, + humanized_action_names, }; this.on_keymap_changed(cx); @@ -487,7 +497,7 @@ impl KeymapEditor { Some(Default) => 3, None => 4, }; - return (source_precedence, keybind.action_name.as_ref()); + return (source_precedence, keybind.action_name); }); } this.selected_index.take(); @@ -557,7 +567,7 @@ impl KeymapEditor { processed_bindings.push(ProcessedKeybinding { keystroke_text: keystroke_text.into(), ui_key_binding, - action_name: action_name.into(), + action_name, action_arguments, action_docs, action_schema: action_schema.get(action_name).cloned(), @@ -574,7 +584,7 @@ impl KeymapEditor { processed_bindings.push(ProcessedKeybinding { keystroke_text: empty.clone(), ui_key_binding: None, - action_name: action_name.into(), + action_name, action_arguments: None, action_docs: action_documentation.get(action_name).copied(), action_schema: action_schema.get(action_name).cloned(), @@ -1024,7 +1034,7 @@ impl KeymapEditor { struct ProcessedKeybinding { keystroke_text: SharedString, ui_key_binding: Option<ui::KeyBinding>, - action_name: SharedString, + action_name: &'static str, action_arguments: Option<SyntaxHighlightedText>, action_docs: Option<&'static str>, action_schema: Option<schemars::Schema>, @@ -1270,7 +1280,7 @@ impl Render for KeymapEditor { .filter_map(|index| { let candidate_id = this.matches.get(index)?.candidate_id; let binding = &this.keybindings[candidate_id]; - let action_name = binding.action_name.clone(); + let action_name = binding.action_name; let icon = (this.filter_state != FilterState::Conflicts && this.has_conflict(index)) @@ -1317,13 +1327,26 @@ impl Render for KeymapEditor { let action = div() .id(("keymap action", index)) - .child(command_palette::humanize_action_name(&action_name)) + .child({ + if action_name != gpui::NoAction.name() { + this.humanized_action_names + .get(action_name) + .cloned() + .unwrap_or(action_name.into()) + .into_any_element() + } else { + const NULL: SharedString = + SharedString::new_static("<null>"); + muted_styled_text(NULL.clone(), cx) + .into_any_element() + } + }) .when(!context_menu_deployed, |this| { this.tooltip({ - let action_name = binding.action_name.clone(); + let action_name = binding.action_name; let action_docs = binding.action_docs; move |_, cx| { - let action_tooltip = Tooltip::new(&action_name); + let action_tooltip = Tooltip::new(action_name); let action_tooltip = match action_docs { Some(docs) => action_tooltip.meta(docs), None => action_tooltip, @@ -1773,7 +1796,7 @@ impl KeybindingEditorModal { .read(cx) .keybindings .get(first_conflicting_index) - .map(|keybind| keybind.action_name.clone()); + .map(|keybind| keybind.action_name); let warning_message = match conflicting_action_name { Some(name) => { @@ -1823,7 +1846,7 @@ impl KeybindingEditorModal { .log_err(); cx.spawn(async move |this, cx| { - let action_name = existing_keybind.action_name.clone(); + let action_name = existing_keybind.action_name; if let Err(err) = save_keybinding_update( create, From 13f4a093c8dd834e50b55d7525ceec7d6066cc5f Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Wed, 16 Jul 2025 14:19:41 -0400 Subject: [PATCH 130/658] Improve keystroke search in keymap editor (#34567) This PR improves Keystroke search by: 1. Allow searching by modifiers without additional keys. 2. Take match count into consideration when deciding if we should show an action as a search match. 3. Take order into consideration as well. Release Notes: - N/A --------- Co-authored-by: Ben Kunkle <ben@zed.dev> --- crates/gpui/src/platform/keystroke.rs | 11 +++ crates/settings_ui/src/keybindings.rs | 129 +++++++++++++++++++++++--- 2 files changed, 127 insertions(+), 13 deletions(-) diff --git a/crates/gpui/src/platform/keystroke.rs b/crates/gpui/src/platform/keystroke.rs index 69d87ebdcb2510e31431409fb19172344d14c5bc..8b6e72d1508c829d15b9786dea69bef41cf4d7ef 100644 --- a/crates/gpui/src/platform/keystroke.rs +++ b/crates/gpui/src/platform/keystroke.rs @@ -417,6 +417,17 @@ impl Modifiers { self.control || self.alt || self.shift || self.platform || self.function } + /// Returns the XOR of two modifier sets + pub fn xor(&self, other: &Modifiers) -> Modifiers { + Modifiers { + control: self.control ^ other.control, + alt: self.alt ^ other.alt, + shift: self.shift ^ other.shift, + platform: self.platform ^ other.platform, + function: self.function ^ other.function, + } + } + /// Whether the semantically 'secondary' modifier key is pressed. /// /// On macOS, this is the command key. diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 281b01df27b09cd031b82d954876281ffe08fbae..1b97798328860c16403c8e2dfa28934dd81b2e3a 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -470,11 +470,22 @@ impl KeymapEditor { }, ) } else { - keystroke_query.iter().all(|key| { - keystrokes.iter().any(|keystroke| { - keystroke.key == key.key - && keystroke.modifiers == key.modifiers - }) + let key_press_query = + KeyPressIterator::new(keystroke_query.as_slice()); + let mut last_match_idx = 0; + + key_press_query.into_iter().all(|key| { + let key_presses = KeyPressIterator::new(keystrokes); + key_presses.into_iter().enumerate().any( + |(index, keystroke)| { + if last_match_idx > index || keystroke != key { + return false; + } + + last_match_idx = index; + true + }, + ) }) } }) @@ -2313,6 +2324,16 @@ enum CloseKeystrokeResult { None, } +#[derive(PartialEq, Eq, Debug, Clone)] +enum KeyPress<'a> { + Alt, + Control, + Function, + Shift, + Platform, + Key(&'a String), +} + struct KeystrokeInput { keystrokes: Vec<Keystroke>, placeholder_keystrokes: Option<Vec<Keystroke>>, @@ -2322,6 +2343,7 @@ struct KeystrokeInput { intercept_subscription: Option<Subscription>, _focus_subscriptions: [Subscription; 2], search: bool, + /// Handles tripe escape to stop recording close_keystrokes: Option<Vec<Keystroke>>, close_keystrokes_start: Option<usize>, } @@ -2443,11 +2465,14 @@ impl KeystrokeInput { && last.key.is_empty() && keystrokes_len <= Self::KEYSTROKE_COUNT_MAX { - if !event.modifiers.modified() { + if self.search { + last.modifiers = last.modifiers.xor(&event.modifiers); + } else if !event.modifiers.modified() { self.keystrokes.pop(); } else { last.modifiers = event.modifiers; } + self.keystrokes_changed(cx); } else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX { self.keystrokes.push(Self::dummy(event.modifiers)); @@ -2464,11 +2489,19 @@ impl KeystrokeInput { ) { let close_keystroke_result = self.handle_possible_close_keystroke(keystroke, window, cx); if close_keystroke_result != CloseKeystrokeResult::Close { - if let Some(last) = self.keystrokes.last() + let key_len = self.keystrokes.len(); + if let Some(last) = self.keystrokes.last_mut() && last.key.is_empty() - && self.keystrokes.len() <= Self::KEYSTROKE_COUNT_MAX + && key_len <= Self::KEYSTROKE_COUNT_MAX { - self.keystrokes.pop(); + if self.search { + last.key = keystroke.key.clone(); + self.keystrokes_changed(cx); + cx.stop_propagation(); + return; + } else { + self.keystrokes.pop(); + } } if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX { if close_keystroke_result == CloseKeystrokeResult::Partial @@ -2511,10 +2544,11 @@ impl KeystrokeInput { { return placeholders; } - if self - .keystrokes - .last() - .map_or(false, |last| last.key.is_empty()) + if !self.search + && self + .keystrokes + .last() + .map_or(false, |last| last.key.is_empty()) { return &self.keystrokes[..self.keystrokes.len() - 1]; } @@ -2957,3 +2991,72 @@ mod persistence { } } } + +/// Iterator that yields KeyPress values from a slice of Keystrokes +struct KeyPressIterator<'a> { + keystrokes: &'a [Keystroke], + current_keystroke_index: usize, + current_key_press_index: usize, +} + +impl<'a> KeyPressIterator<'a> { + fn new(keystrokes: &'a [Keystroke]) -> Self { + Self { + keystrokes, + current_keystroke_index: 0, + current_key_press_index: 0, + } + } +} + +impl<'a> Iterator for KeyPressIterator<'a> { + type Item = KeyPress<'a>; + + fn next(&mut self) -> Option<Self::Item> { + loop { + let keystroke = self.keystrokes.get(self.current_keystroke_index)?; + + match self.current_key_press_index { + 0 => { + self.current_key_press_index = 1; + if keystroke.modifiers.platform { + return Some(KeyPress::Platform); + } + } + 1 => { + self.current_key_press_index = 2; + if keystroke.modifiers.alt { + return Some(KeyPress::Alt); + } + } + 2 => { + self.current_key_press_index = 3; + if keystroke.modifiers.control { + return Some(KeyPress::Control); + } + } + 3 => { + self.current_key_press_index = 4; + if keystroke.modifiers.shift { + return Some(KeyPress::Shift); + } + } + 4 => { + self.current_key_press_index = 5; + if keystroke.modifiers.function { + return Some(KeyPress::Function); + } + } + _ => { + self.current_keystroke_index += 1; + self.current_key_press_index = 0; + + if keystroke.key.is_empty() { + continue; + } + return Some(KeyPress::Key(&keystroke.key)); + } + } + } + } +} From a6a7a1cc287adb90161b85d6c2873b749ae68b34 Mon Sep 17 00:00:00 2001 From: Ben Kunkle <ben@zed.dev> Date: Wed, 16 Jul 2025 13:28:44 -0500 Subject: [PATCH 131/658] keymap_ui: Remove feature flag (#34568) Closes #ISSUE Release Notes: - Rebound the keystroke to open the keymap file, to open the new keymap editor --- assets/keymaps/default-linux.json | 2 +- assets/keymaps/default-macos.json | 2 +- crates/settings_ui/src/keybindings.rs | 39 --------------------------- 3 files changed, 2 insertions(+), 41 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 9ca7d8589a29b988a181f69f65310220d380d7e4..da4d79eca111980263937eaac9387f0437e8ffc9 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -586,7 +586,7 @@ "ctrl-shift-f": "pane::DeploySearch", "ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }], "ctrl-shift-t": "pane::ReopenClosedItem", - "ctrl-k ctrl-s": "zed::OpenKeymap", + "ctrl-k ctrl-s": "zed::OpenKeymapEditor", "ctrl-k ctrl-t": "theme_selector::Toggle", "ctrl-t": "project_symbols::Toggle", "ctrl-p": "file_finder::Toggle", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 1eece3169929f6289595fd29f903dfe0981eff63..962760098b7969ae41d15c8d892c8a201d08c4a5 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -652,7 +652,7 @@ "cmd-shift-f": "pane::DeploySearch", "cmd-shift-h": ["pane::DeploySearch", { "replace_enabled": true }], "cmd-shift-t": "pane::ReopenClosedItem", - "cmd-k cmd-s": "zed::OpenKeymap", + "cmd-k cmd-s": "zed::OpenKeymapEditor", "cmd-k cmd-t": "theme_selector::Toggle", "cmd-t": "project_symbols::Toggle", "cmd-p": "file_finder::Toggle", diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 1b97798328860c16403c8e2dfa28934dd81b2e3a..78636f7023351243419397e157dfe4de5ebea17c 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -6,7 +6,6 @@ use std::{ use anyhow::{Context as _, anyhow}; use collections::{HashMap, HashSet}; use editor::{CompletionProvider, Editor, EditorEvent}; -use feature_flags::FeatureFlagViewExt; use fs::Fs; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ @@ -33,7 +32,6 @@ use workspace::{ }; use crate::{ - SettingsUiFeatureFlag, keybindings::persistence::KEYBINDING_EDITORS, ui_components::table::{Table, TableInteractionState}, }; @@ -48,7 +46,6 @@ actions!( ] ); -const KEYMAP_EDITOR_NAMESPACE: &'static str = "keymap_editor"; actions!( keymap_editor, [ @@ -115,42 +112,6 @@ pub fn init(cx: &mut App) { }) }); - cx.observe_new(|_workspace: &mut Workspace, window, cx| { - let Some(window) = window else { return }; - - let keymap_ui_actions = [std::any::TypeId::of::<OpenKeymapEditor>()]; - - command_palette_hooks::CommandPaletteFilter::update_global(cx, |filter, _cx| { - filter.hide_action_types(&keymap_ui_actions); - filter.hide_namespace(KEYMAP_EDITOR_NAMESPACE); - }); - - cx.observe_flag::<SettingsUiFeatureFlag, _>( - window, - move |is_enabled, _workspace, _, cx| { - if is_enabled { - command_palette_hooks::CommandPaletteFilter::update_global( - cx, - |filter, _cx| { - filter.show_action_types(keymap_ui_actions.iter()); - filter.show_namespace(KEYMAP_EDITOR_NAMESPACE); - }, - ); - } else { - command_palette_hooks::CommandPaletteFilter::update_global( - cx, - |filter, _cx| { - filter.hide_action_types(&keymap_ui_actions); - filter.hide_namespace(KEYMAP_EDITOR_NAMESPACE); - }, - ); - } - }, - ) - .detach(); - }) - .detach(); - register_serializable_item::<KeymapEditor>(cx); } From 6f60939d301ae08e0dd384154a0a325dfa409f43 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" <JosephTLyons@gmail.com> Date: Wed, 16 Jul 2025 14:48:50 -0400 Subject: [PATCH 132/658] Bump Zed to v0.197 (#34569) 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 395087168808d53ac5e508d7805e61bbae4cc932..b1b3ec32c823d62c5cbdb6d832bb69d2ef685d3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20095,7 +20095,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.196.0" +version = "0.197.0" dependencies = [ "activity_indicator", "agent", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 3af1709b74af3d65539d80d4e39a9978a8da86d5..ae96a48b5319231586f862eb5f88d41b48f37b51 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.196.0" +version = "0.197.0" publish.workspace = true license = "GPL-3.0-or-later" authors = ["Zed Team <hi@zed.dev>"] From 0bde929d548dc2f0733d8a33449067c642737d44 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Wed, 16 Jul 2025 15:50:53 -0400 Subject: [PATCH 133/658] Add keymap editor UI telemetry events (#34571) - Search queries - Keybinding update or removed - Copy action name - Copy context name cc @katie-z-geer Release Notes: - N/A Co-authored-by: Ben Kunkle <ben@zed.dev> --- Cargo.lock | 1 + crates/settings/src/keymap_file.rs | 52 +++++++++++++++++ crates/settings_ui/Cargo.toml | 1 + crates/settings_ui/src/keybindings.rs | 81 ++++++++++++++++++++++++++- 4 files changed, 132 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b1b3ec32c823d62c5cbdb6d832bb69d2ef685d3d..091b9441697a727cb7ea5af818aa1c6e06c74b27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14720,6 +14720,7 @@ dependencies = [ "serde", "serde_json", "settings", + "telemetry", "theme", "tree-sitter-json", "tree-sitter-rust", diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index d738e30c4fb2d3c8b9ff8ed91bb5b54accbd1eae..e6a32f731b173d26a998e6e1c7e57f43d39a3e6b 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -847,6 +847,7 @@ impl KeymapFile { } } +#[derive(Clone)] pub enum KeybindUpdateOperation<'a> { Replace { /// Describes the keybind to create @@ -865,6 +866,47 @@ pub enum KeybindUpdateOperation<'a> { }, } +impl KeybindUpdateOperation<'_> { + pub fn generate_telemetry( + &self, + ) -> ( + // The keybind that is created + String, + // The keybinding that was removed + String, + // The source of the keybinding + String, + ) { + let (new_binding, removed_binding, source) = match &self { + KeybindUpdateOperation::Replace { + source, + target, + target_keybind_source, + } => (Some(source), Some(target), Some(*target_keybind_source)), + KeybindUpdateOperation::Add { source, .. } => (Some(source), None, None), + KeybindUpdateOperation::Remove { + target, + target_keybind_source, + } => (None, Some(target), Some(*target_keybind_source)), + }; + + let new_binding = new_binding + .map(KeybindUpdateTarget::telemetry_string) + .unwrap_or("null".to_owned()); + let removed_binding = removed_binding + .map(KeybindUpdateTarget::telemetry_string) + .unwrap_or("null".to_owned()); + + let source = source + .as_ref() + .map(KeybindSource::name) + .map(ToOwned::to_owned) + .unwrap_or("null".to_owned()); + + (new_binding, removed_binding, source) + } +} + impl<'a> KeybindUpdateOperation<'a> { pub fn add(source: KeybindUpdateTarget<'a>) -> Self { Self::Add { source, from: None } @@ -905,6 +947,16 @@ impl<'a> KeybindUpdateTarget<'a> { keystrokes.pop(); keystrokes } + + fn telemetry_string(&self) -> String { + format!( + "action_name: {}, context: {}, action_arguments: {}, keystrokes: {}", + self.action_name, + self.context.unwrap_or("global"), + self.action_arguments.unwrap_or("none"), + self.keystrokes_unparsed() + ) + } } #[derive(Clone, Copy, PartialEq, Eq)] diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml index 4502d994e7e64583c10f06e3542c462f17757ff9..e512c4e4d4836ef9f690011bb84d5d0327bba640 100644 --- a/crates/settings_ui/Cargo.toml +++ b/crates/settings_ui/Cargo.toml @@ -34,6 +34,7 @@ search.workspace = true serde.workspace = true serde_json.workspace = true settings.workspace = true +telemetry.workspace = true theme.workspace = true tree-sitter-json.workspace = true tree-sitter-rust.workspace = true diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 78636f7023351243419397e157dfe4de5ebea17c..e0105fa87518f93382a47ef32e2b5d2d3320228f 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -1,6 +1,7 @@ use std::{ ops::{Not as _, Range}, sync::Arc, + time::Duration, }; use anyhow::{Context as _, anyhow}; @@ -12,7 +13,7 @@ use gpui::{ Action, Animation, AnimationExt, AppContext as _, AsyncApp, Axis, ClickEvent, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Global, IsZero, KeyContext, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, Point, ScrollStrategy, - ScrollWheelEvent, StyledText, Subscription, WeakEntity, actions, anchored, deferred, div, + ScrollWheelEvent, StyledText, Subscription, Task, WeakEntity, actions, anchored, deferred, div, }; use language::{Language, LanguageConfig, ToOffset as _}; use notifications::status_toast::{StatusToast, ToastIcon}; @@ -151,6 +152,13 @@ impl SearchMode { SearchMode::KeyStroke { .. } => SearchMode::Normal, } } + + fn exact_match(&self) -> bool { + match self { + SearchMode::Normal => false, + SearchMode::KeyStroke { exact_match } => *exact_match, + } + } } #[derive(Default, PartialEq, Copy, Clone)] @@ -249,6 +257,7 @@ struct KeymapEditor { keybinding_conflict_state: ConflictState, filter_state: FilterState, search_mode: SearchMode, + search_query_debounce: Option<Task<()>>, // corresponds 1 to 1 with keybindings string_match_candidates: Arc<Vec<StringMatchCandidate>>, matches: Vec<StringMatch>, @@ -347,6 +356,7 @@ impl KeymapEditor { context_menu: None, previous_edit: None, humanized_action_names, + search_query_debounce: None, }; this.on_keymap_changed(cx); @@ -371,10 +381,32 @@ impl KeymapEditor { } } - fn on_query_changed(&self, cx: &mut Context<Self>) { + fn on_query_changed(&mut self, cx: &mut Context<Self>) { let action_query = self.current_action_query(cx); let keystroke_query = self.current_keystroke_query(cx); + let exact_match = self.search_mode.exact_match(); + + let timer = cx.background_executor().timer(Duration::from_secs(1)); + self.search_query_debounce = Some(cx.background_spawn({ + let action_query = action_query.clone(); + let keystroke_query = keystroke_query.clone(); + async move { + timer.await; + let keystroke_query = keystroke_query + .into_iter() + .map(|keystroke| keystroke.unparse()) + .collect::<Vec<String>>() + .join(" "); + + telemetry::event!( + "Keystroke Search Completed", + action_query = action_query, + keystroke_query = keystroke_query, + keystroke_exact_match = exact_match + ) + } + })); cx.spawn(async move |this, cx| { Self::update_matches(this.clone(), action_query, keystroke_query, cx).await?; this.update(cx, |this, cx| { @@ -474,6 +506,7 @@ impl KeymapEditor { } this.selected_index.take(); this.matches = matches; + cx.notify(); }) } @@ -864,6 +897,26 @@ impl KeymapEditor { return; }; let keymap_editor = cx.entity(); + + let arguments = keybind + .action_arguments + .as_ref() + .map(|arguments| arguments.text.clone()); + let context = keybind + .context + .as_ref() + .map(|context| context.local_str().unwrap_or("global")); + let source = keybind.source.as_ref().map(|source| source.1.clone()); + + telemetry::event!( + "Edit Keybinding Modal Opened", + keystroke = keybind.keystroke_text, + action = keybind.action_name, + source = source, + context = context, + arguments = arguments, + ); + self.workspace .update(cx, |workspace, cx| { let fs = workspace.app_state().fs.clone(); @@ -899,7 +952,7 @@ impl KeymapEditor { return; }; - let Ok(fs) = self + let std::result::Result::Ok(fs) = self .workspace .read_with(cx, |workspace, _| workspace.app_state().fs.clone()) else { @@ -929,6 +982,8 @@ impl KeymapEditor { let Some(context) = context else { return; }; + + telemetry::event!("Keybinding Context Copied", context = context.clone()); cx.write_to_clipboard(gpui::ClipboardItem::new_string(context.clone())); } @@ -944,6 +999,8 @@ impl KeymapEditor { let Some(action) = action else { return; }; + + telemetry::event!("Keybinding Action Copied", action = action.clone()); cx.write_to_clipboard(gpui::ClipboardItem::new_string(action.clone())); } @@ -2222,6 +2279,9 @@ async fn save_keybinding_update( from: Some(target), } }; + + let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry(); + let updated_keymap_contents = settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size) .context("Failed to update keybinding")?; @@ -2231,6 +2291,13 @@ async fn save_keybinding_update( ) .await .context("Failed to write keymap file")?; + + telemetry::event!( + "Keybinding Updated", + new_keybinding = new_keybinding, + removed_keybinding = removed_keybinding, + source = source + ); Ok(()) } @@ -2266,6 +2333,7 @@ async fn remove_keybinding( .unwrap_or(KeybindSource::User), }; + let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry(); let updated_keymap_contents = settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size) .context("Failed to update keybinding")?; @@ -2275,6 +2343,13 @@ async fn remove_keybinding( ) .await .context("Failed to write keymap file")?; + + telemetry::event!( + "Keybinding Removed", + new_keybinding = new_keybinding, + removed_keybinding = removed_keybinding, + source = source + ); Ok(()) } From 0023773c68d8c065cfd99d2a6dad3f8c23bf856c Mon Sep 17 00:00:00 2001 From: Peter Tripp <peter@zed.dev> Date: Wed, 16 Jul 2025 15:57:02 -0400 Subject: [PATCH 134/658] docs: Add Zed as Git Editor example (#34572) Release Notes: - N/A --- docs/src/git.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/docs/src/git.md b/docs/src/git.md index 642861c7b0d7ae449f74b5891ff2f3d548635dc1..76db15a767b8dfb005035a75b377c01c4f1cb944 100644 --- a/docs/src/git.md +++ b/docs/src/git.md @@ -151,3 +151,17 @@ When viewing files with changes, Zed displays diff hunks that can be expanded or | {#action editor::ToggleSelectedDiffHunks} | {#kb editor::ToggleSelectedDiffHunks} | > Not all actions have default keybindings, but can be bound by [customizing your keymap](./key-bindings.md#user-keymaps). + +## Git CLI Configuration + +If you would like to also use Zed for your [git commit message editor](https://git-scm.com/book/en/v2/Customizing-Git-Git-Configuration#_core_editor) when committing from the command line you can use `zed --wait`: + +```sh +git config --global core.editor "zed --wait" +``` + +Or add the following to your shell environment (in `~/.zshrc`, `~/.bashrc`, etc): + +```sh +export GIT_EDITOR="zed --wait" +``` From b4c2ae5196910d204370731892863aa3f931c90d Mon Sep 17 00:00:00 2001 From: Richard Feldman <oss@rtfeldman.com> Date: Wed, 16 Jul 2025 16:31:31 -0400 Subject: [PATCH 135/658] Handle `upstream_http_error` completion responses (#34573) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses upstream errors such as: <img width="831" height="100" alt="Screenshot 2025-07-16 at 3 37 03 PM" src="https://github.com/user-attachments/assets/2aeb0257-6761-4148-b687-25fae93c68d8" /> These should now automatically retry like other upstream HTTP error codes. Release Notes: - N/A --- crates/language_model/src/language_model.rs | 128 ++++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index 81a0f7d8a1c5096989e8f7bf7ce140575950f281..8962e9d8d15bce91e8004a272a5a74773a430179 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -178,6 +178,21 @@ pub enum LanguageModelCompletionError { } impl LanguageModelCompletionError { + fn parse_upstream_error_json(message: &str) -> Option<(StatusCode, String)> { + let error_json = serde_json::from_str::<serde_json::Value>(message).ok()?; + let upstream_status = error_json + .get("upstream_status") + .and_then(|v| v.as_u64()) + .and_then(|status| u16::try_from(status).ok()) + .and_then(|status| StatusCode::from_u16(status).ok())?; + let inner_message = error_json + .get("message") + .and_then(|v| v.as_str()) + .unwrap_or(message) + .to_string(); + Some((upstream_status, inner_message)) + } + pub fn from_cloud_failure( upstream_provider: LanguageModelProviderName, code: String, @@ -191,6 +206,18 @@ impl LanguageModelCompletionError { Self::PromptTooLarge { tokens: Some(tokens), } + } else if code == "upstream_http_error" { + if let Some((upstream_status, inner_message)) = + Self::parse_upstream_error_json(&message) + { + return Self::from_http_status( + upstream_provider, + upstream_status, + inner_message, + retry_after, + ); + } + anyhow!("completion request failed, code: {code}, message: {message}").into() } else if let Some(status_code) = code .strip_prefix("upstream_http_") .and_then(|code| StatusCode::from_str(code).ok()) @@ -701,3 +728,104 @@ impl From<String> for LanguageModelProviderName { Self(SharedString::from(value)) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_from_cloud_failure_with_upstream_http_error() { + let error = LanguageModelCompletionError::from_cloud_failure( + String::from("anthropic").into(), + "upstream_http_error".to_string(), + r#"{"code":"upstream_http_error","message":"Received an error from the Anthropic API: upstream connect error or disconnect/reset before headers. reset reason: connection timeout","upstream_status":503}"#.to_string(), + None, + ); + + match error { + LanguageModelCompletionError::ServerOverloaded { provider, .. } => { + assert_eq!(provider.0, "anthropic"); + } + _ => panic!( + "Expected ServerOverloaded error for 503 status, got: {:?}", + error + ), + } + + let error = LanguageModelCompletionError::from_cloud_failure( + String::from("anthropic").into(), + "upstream_http_error".to_string(), + r#"{"code":"upstream_http_error","message":"Internal server error","upstream_status":500}"#.to_string(), + None, + ); + + match error { + LanguageModelCompletionError::ApiInternalServerError { provider, message } => { + assert_eq!(provider.0, "anthropic"); + assert_eq!(message, "Internal server error"); + } + _ => panic!( + "Expected ApiInternalServerError for 500 status, got: {:?}", + error + ), + } + } + + #[test] + fn test_from_cloud_failure_with_standard_format() { + let error = LanguageModelCompletionError::from_cloud_failure( + String::from("anthropic").into(), + "upstream_http_503".to_string(), + "Service unavailable".to_string(), + None, + ); + + match error { + LanguageModelCompletionError::ServerOverloaded { provider, .. } => { + assert_eq!(provider.0, "anthropic"); + } + _ => panic!("Expected ServerOverloaded error for upstream_http_503"), + } + } + + #[test] + fn test_upstream_http_error_connection_timeout() { + let error = LanguageModelCompletionError::from_cloud_failure( + String::from("anthropic").into(), + "upstream_http_error".to_string(), + r#"{"code":"upstream_http_error","message":"Received an error from the Anthropic API: upstream connect error or disconnect/reset before headers. reset reason: connection timeout","upstream_status":503}"#.to_string(), + None, + ); + + match error { + LanguageModelCompletionError::ServerOverloaded { provider, .. } => { + assert_eq!(provider.0, "anthropic"); + } + _ => panic!( + "Expected ServerOverloaded error for connection timeout with 503 status, got: {:?}", + error + ), + } + + let error = LanguageModelCompletionError::from_cloud_failure( + String::from("anthropic").into(), + "upstream_http_error".to_string(), + r#"{"code":"upstream_http_error","message":"Received an error from the Anthropic API: upstream connect error or disconnect/reset before headers. reset reason: connection timeout","upstream_status":500}"#.to_string(), + None, + ); + + match error { + LanguageModelCompletionError::ApiInternalServerError { provider, message } => { + assert_eq!(provider.0, "anthropic"); + assert_eq!( + message, + "Received an error from the Anthropic API: upstream connect error or disconnect/reset before headers. reset reason: connection timeout" + ); + } + _ => panic!( + "Expected ApiInternalServerError for connection timeout with 500 status, got: {:?}", + error + ), + } + } +} From f82ef1f76ffd06670ebd7a509dd34666346fb679 Mon Sep 17 00:00:00 2001 From: Peter Tripp <peter@zed.dev> Date: Wed, 16 Jul 2025 16:55:54 -0400 Subject: [PATCH 136/658] agent: Support GEMINI_API_KEY environment variable (#34574) Google Gemini Docs now recommend usage of `GEMINI_API_KEY` and the legacy `GOOGLE_AI_API_KEY` variable is no longer supported in the modern SDKs. Zed will now accept either. Release Notes: - N/A --- crates/language_models/src/provider/google.rs | 9 ++++++--- docs/src/ai/configuration.md | 2 +- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/language_models/src/provider/google.rs b/crates/language_models/src/provider/google.rs index d1539dd22cfb64b4ed194830f3f9c5babc2a6cea..bd8a09970a6f9ece98cac6273823948bcb9998b5 100644 --- a/crates/language_models/src/provider/google.rs +++ b/crates/language_models/src/provider/google.rs @@ -94,6 +94,7 @@ pub struct State { _subscription: Subscription, } +const GEMINI_API_KEY_VAR: &str = "GEMINI_API_KEY"; const GOOGLE_AI_API_KEY_VAR: &str = "GOOGLE_AI_API_KEY"; impl State { @@ -151,6 +152,8 @@ impl State { cx.spawn(async move |this, cx| { let (api_key, from_env) = if let Ok(api_key) = std::env::var(GOOGLE_AI_API_KEY_VAR) { (api_key, true) + } else if let Ok(api_key) = std::env::var(GEMINI_API_KEY_VAR) { + (api_key, true) } else { let (_, api_key) = credentials_provider .read_credentials(&api_url, &cx) @@ -903,7 +906,7 @@ impl Render for ConfigurationView { ) .child( Label::new( - format!("You can also assign the {GOOGLE_AI_API_KEY_VAR} environment variable and restart Zed."), + format!("You can also assign the {GEMINI_API_KEY_VAR} environment variable and restart Zed."), ) .size(LabelSize::Small).color(Color::Muted), ) @@ -922,7 +925,7 @@ impl Render for ConfigurationView { .gap_1() .child(Icon::new(IconName::Check).color(Color::Success)) .child(Label::new(if env_var_set { - format!("API key set in {GOOGLE_AI_API_KEY_VAR} environment variable.") + format!("API key set in {GEMINI_API_KEY_VAR} environment variable.") } else { "API key configured.".to_string() })), @@ -935,7 +938,7 @@ impl Render for ConfigurationView { .icon_position(IconPosition::Start) .disabled(env_var_set) .when(env_var_set, |this| { - this.tooltip(Tooltip::text(format!("To reset your API key, unset the {GOOGLE_AI_API_KEY_VAR} environment variable."))) + this.tooltip(Tooltip::text(format!("To reset your API key, make sure {GEMINI_API_KEY_VAR} and {GOOGLE_AI_API_KEY_VAR} environment variables are unset."))) }) .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))), ) diff --git a/docs/src/ai/configuration.md b/docs/src/ai/configuration.md index 56eb4ab76cd990871d62ced2cccb019f2d607cd6..1201fa217367a7e2ba48843b2a4fa0e815932e76 100644 --- a/docs/src/ai/configuration.md +++ b/docs/src/ai/configuration.md @@ -237,7 +237,7 @@ You can use Gemini models with the Zed agent by choosing it via the model dropdo The Google AI API key will be saved in your keychain. -Zed will also use the `GOOGLE_AI_API_KEY` environment variable if it's defined. +Zed will also use the `GEMINI_API_KEY` environment variable if it's defined. See [Using Gemini API keys](Using Gemini API keys) in the Gemini docs for more. #### Custom Models {#google-ai-custom-models} From e23a4564ccf9f12c79ea4d32c30c4055cbb754f3 Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Thu, 17 Jul 2025 03:00:08 +0530 Subject: [PATCH 137/658] keymap_ui: Open Keymap editor from settings dropdown (#34576) @probably-neb I guess we should be opening the keymap editor from title bar and menu as well. I believe this got missed in this: #34568. Release Notes: - Open Keymap editor from settings from menu and title bar. --- Cargo.lock | 1 + crates/title_bar/Cargo.toml | 1 + crates/title_bar/src/title_bar.rs | 5 +++-- crates/zed/src/zed/app_menus.rs | 3 ++- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 091b9441697a727cb7ea5af818aa1c6e06c74b27..59e444f1f86df7d4ee6a85b8062c28dcf55faa1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16452,6 +16452,7 @@ dependencies = [ "schemars", "serde", "settings", + "settings_ui", "smallvec", "story", "telemetry", diff --git a/crates/title_bar/Cargo.toml b/crates/title_bar/Cargo.toml index 123d0468ac86d6d37d428e73b4fd8de37dce429c..3c39e6b946a68824b8601a0978c702f51d06f5ec 100644 --- a/crates/title_bar/Cargo.toml +++ b/crates/title_bar/Cargo.toml @@ -40,6 +40,7 @@ rpc.workspace = true schemars.workspace = true serde.workspace = true settings.workspace = true +settings_ui.workspace = true smallvec.workspace = true story = { workspace = true, optional = true } telemetry.workspace = true diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 4b8902d14e54bbfef86008d499caf9a5eb7e5027..453bb54db8b1708219a8922bbadcb3d1c8be3be2 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -30,6 +30,7 @@ use onboarding_banner::OnboardingBanner; use project::Project; use rpc::proto; use settings::Settings as _; +use settings_ui::keybindings; use std::sync::Arc; use theme::ActiveTheme; use title_bar_settings::TitleBarSettings; @@ -683,7 +684,7 @@ impl TitleBar { ) .separator() .action("Settings", zed_actions::OpenSettings.boxed_clone()) - .action("Key Bindings", Box::new(zed_actions::OpenKeymap)) + .action("Key Bindings", Box::new(keybindings::OpenKeymapEditor)) .action( "Themes…", zed_actions::theme_selector::Toggle::default().boxed_clone(), @@ -727,7 +728,7 @@ impl TitleBar { .menu(|window, cx| { ContextMenu::build(window, cx, |menu, _, _| { menu.action("Settings", zed_actions::OpenSettings.boxed_clone()) - .action("Key Bindings", Box::new(zed_actions::OpenKeymap)) + .action("Key Bindings", Box::new(keybindings::OpenKeymapEditor)) .action( "Themes…", zed_actions::theme_selector::Toggle::default().boxed_clone(), diff --git a/crates/zed/src/zed/app_menus.rs b/crates/zed/src/zed/app_menus.rs index ddab724f4ad4e435d5e332dc55122f6677adfffa..c4131dbee9a6ed1c4723630ad60a55bedd2fa365 100644 --- a/crates/zed/src/zed/app_menus.rs +++ b/crates/zed/src/zed/app_menus.rs @@ -1,5 +1,6 @@ use collab_ui::collab_panel; use gpui::{Menu, MenuItem, OsAction}; +use settings_ui::keybindings; use terminal_view::terminal_panel; pub fn app_menus() -> Vec<Menu> { @@ -16,7 +17,7 @@ pub fn app_menus() -> Vec<Menu> { name: "Settings".into(), items: vec![ MenuItem::action("Open Settings", super::OpenSettings), - MenuItem::action("Open Key Bindings", zed_actions::OpenKeymap), + MenuItem::action("Open Key Bindings", keybindings::OpenKeymapEditor), MenuItem::action("Open Default Settings", super::OpenDefaultSettings), MenuItem::action( "Open Default Key Bindings", From f43bcc14927f68fa99d721bad41e52810cdc630b Mon Sep 17 00:00:00 2001 From: Marshall Bowers <git@maxdeviant.com> Date: Wed, 16 Jul 2025 18:04:53 -0400 Subject: [PATCH 138/658] collab: Remove `GET /billing/subscriptions` endpoint (#34580) This PR removes the `GET /billing/subscriptions` endpoint, as it has been moved to `cloud.zed.dev`. Release Notes: - N/A --- crates/collab/src/api/billing.rs | 89 +------------------------------- 1 file changed, 1 insertion(+), 88 deletions(-) diff --git a/crates/collab/src/api/billing.rs b/crates/collab/src/api/billing.rs index 72a9a1b46ba5fa4f5fe3e915c1ee3fa0fe37c5e2..1ca71726f928303bc8d6f99ca9c712d28fd9f70b 100644 --- a/crates/collab/src/api/billing.rs +++ b/crates/collab/src/api/billing.rs @@ -49,10 +49,7 @@ use crate::{ pub fn router() -> Router { Router::new() .route("/billing/preferences", put(update_billing_preferences)) - .route( - "/billing/subscriptions", - get(list_billing_subscriptions).post(create_billing_subscription), - ) + .route("/billing/subscriptions", post(create_billing_subscription)) .route( "/billing/subscriptions/manage", post(manage_billing_subscription), @@ -166,90 +163,6 @@ async fn update_billing_preferences( })) } -#[derive(Debug, Deserialize)] -struct ListBillingSubscriptionsParams { - github_user_id: i32, -} - -#[derive(Debug, Serialize)] -struct BillingSubscriptionJson { - id: BillingSubscriptionId, - name: String, - status: StripeSubscriptionStatus, - period: Option<BillingSubscriptionPeriodJson>, - trial_end_at: Option<String>, - cancel_at: Option<String>, - /// Whether this subscription can be canceled. - is_cancelable: bool, -} - -#[derive(Debug, Serialize)] -struct BillingSubscriptionPeriodJson { - start_at: String, - end_at: String, -} - -#[derive(Debug, Serialize)] -struct ListBillingSubscriptionsResponse { - subscriptions: Vec<BillingSubscriptionJson>, -} - -async fn list_billing_subscriptions( - Extension(app): Extension<Arc<AppState>>, - Query(params): Query<ListBillingSubscriptionsParams>, -) -> Result<Json<ListBillingSubscriptionsResponse>> { - let user = app - .db - .get_user_by_github_user_id(params.github_user_id) - .await? - .context("user not found")?; - - let subscriptions = app.db.get_billing_subscriptions(user.id).await?; - - Ok(Json(ListBillingSubscriptionsResponse { - subscriptions: subscriptions - .into_iter() - .map(|subscription| BillingSubscriptionJson { - id: subscription.id, - name: match subscription.kind { - Some(SubscriptionKind::ZedPro) => "Zed Pro".to_string(), - Some(SubscriptionKind::ZedProTrial) => "Zed Pro (Trial)".to_string(), - Some(SubscriptionKind::ZedFree) => "Zed Free".to_string(), - None => "Zed LLM Usage".to_string(), - }, - status: subscription.stripe_subscription_status, - period: maybe!({ - let start_at = subscription.current_period_start_at()?; - let end_at = subscription.current_period_end_at()?; - - Some(BillingSubscriptionPeriodJson { - start_at: start_at.to_rfc3339_opts(SecondsFormat::Millis, true), - end_at: end_at.to_rfc3339_opts(SecondsFormat::Millis, true), - }) - }), - trial_end_at: if subscription.kind == Some(SubscriptionKind::ZedProTrial) { - maybe!({ - let end_at = subscription.stripe_current_period_end?; - let end_at = DateTime::from_timestamp(end_at, 0)?; - - Some(end_at.to_rfc3339_opts(SecondsFormat::Millis, true)) - }) - } else { - None - }, - cancel_at: subscription.stripe_cancel_at.map(|cancel_at| { - cancel_at - .and_utc() - .to_rfc3339_opts(SecondsFormat::Millis, true) - }), - is_cancelable: subscription.kind != Some(SubscriptionKind::ZedFree) - && subscription.stripe_subscription_status.is_cancelable() - && subscription.stripe_cancel_at.is_none(), - }) - .collect(), - })) -} - #[derive(Debug, PartialEq, Clone, Copy, Deserialize)] #[serde(rename_all = "snake_case")] enum ProductCode { From c0261a1ea9c380b59c914c7dc6304add40496e24 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Wed, 16 Jul 2025 18:05:26 -0400 Subject: [PATCH 139/658] keymap ui: Fix keymap editor search bugs (#34579) Keystroke input now gets cleared when toggling to normal search mode Main search bar is focused when toggling to normal search mode This also gets rid of highlight on focus from keystroke_editor because it also matched the search bool field and was redundant Release Notes: - N/A --- crates/settings_ui/src/keybindings.rs | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index e0105fa87518f93382a47ef32e2b5d2d3320228f..21eec538b35b108c2d3884aca2edc6c5f4ac1cd7 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -303,7 +303,7 @@ impl KeymapEditor { let keystroke_editor = cx.new(|cx| { let mut keystroke_editor = KeystrokeInput::new(None, window, cx); - keystroke_editor.highlight_on_focus = false; + keystroke_editor.search = true; keystroke_editor }); @@ -1029,18 +1029,16 @@ impl KeymapEditor { self.search_mode = self.search_mode.invert(); self.on_query_changed(cx); - // Update the keystroke editor to turn the `search` bool on - self.keystroke_editor.update(cx, |keystroke_editor, cx| { - keystroke_editor - .set_search_mode(matches!(self.search_mode, SearchMode::KeyStroke { .. })); - cx.notify(); - }); - match self.search_mode { SearchMode::KeyStroke { .. } => { window.focus(&self.keystroke_editor.read(cx).recording_focus_handle(cx)); } - SearchMode::Normal => {} + SearchMode::Normal => { + self.keystroke_editor.update(cx, |editor, cx| { + editor.clear_keystrokes(&ClearKeystrokes, window, cx) + }); + window.focus(&self.filter_editor.focus_handle(cx)); + } } } @@ -2373,7 +2371,6 @@ enum KeyPress<'a> { struct KeystrokeInput { keystrokes: Vec<Keystroke>, placeholder_keystrokes: Option<Vec<Keystroke>>, - highlight_on_focus: bool, outer_focus_handle: FocusHandle, inner_focus_handle: FocusHandle, intercept_subscription: Option<Subscription>, @@ -2401,7 +2398,6 @@ impl KeystrokeInput { Self { keystrokes: Vec::new(), placeholder_keystrokes, - highlight_on_focus: true, inner_focus_handle, outer_focus_handle, intercept_subscription: None, @@ -2618,10 +2614,6 @@ impl KeystrokeInput { self.inner_focus_handle.clone() } - fn set_search_mode(&mut self, search: bool) { - self.search = search; - } - fn start_recording(&mut self, _: &StartRecording, window: &mut Window, cx: &mut Context<Self>) { if !self.outer_focus_handle.is_focused(window) { return; @@ -2781,7 +2773,7 @@ impl Render for KeystrokeInput { .track_focus(&self.inner_focus_handle) .on_modifiers_changed(cx.listener(Self::on_modifiers_changed)) .size_full() - .when(self.highlight_on_focus, |this| { + .when(!self.search, |this| { this.focus(|mut style| { style.border_color = Some(colors.border_focused); style From b9ff538747556f3f2f5ed4ee109654eb80cc9c9c Mon Sep 17 00:00:00 2001 From: Cole Miller <cole@zed.dev> Date: Wed, 16 Jul 2025 19:35:30 -0400 Subject: [PATCH 140/658] docs: Discuss `inlay_hints.show_value_hints` in debugger docs (#34581) This isn't under the `debugger` settings key, but it seems good to document on the debugger page anyway. Release Notes: - N/A --- docs/src/debugger.md | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/docs/src/debugger.md b/docs/src/debugger.md index 02c17c412785c2c05f4c4b8e83cc12f0df980e0d..7cfbf63cd8266f7865e948d7da1997c1d81a1f95 100644 --- a/docs/src/debugger.md +++ b/docs/src/debugger.md @@ -177,8 +177,8 @@ The debug adapter will then stop whenever an exception of a given kind occurs. W ### Stepping granularity - Description: The Step granularity that the debugger will use -- Default: line -- Setting: debugger.stepping_granularity +- Default: `line` +- Setting: `debugger.stepping_granularity` **Options** @@ -217,8 +217,8 @@ The debug adapter will then stop whenever an exception of a given kind occurs. W ### Save Breakpoints - Description: Whether the breakpoints should be saved across Zed sessions. -- Default: true -- Setting: debugger.save_breakpoints +- Default: `true` +- Setting: `debugger.save_breakpoints` **Options** @@ -235,8 +235,8 @@ The debug adapter will then stop whenever an exception of a given kind occurs. W ### Button - Description: Whether the button should be displayed in the debugger toolbar. -- Default: true -- Setting: debugger.show_button +- Default: `true` +- Setting: `debugger.show_button` **Options** @@ -253,8 +253,8 @@ The debug adapter will then stop whenever an exception of a given kind occurs. W ### Timeout - Description: Time in milliseconds until timeout error when connecting to a TCP debug adapter. -- Default: 2000 -- Setting: debugger.timeout +- Default: `2000` +- Setting: `debugger.timeout` **Options** @@ -268,6 +268,24 @@ The debug adapter will then stop whenever an exception of a given kind occurs. W } ``` +### Inline Values + +- Description: Whether to enable editor inlay hints showing the values of variables in your code during debugging sessions. +- Default: `true` +- Setting: `inlay_hints.show_value_hints` + +**Options** + +```json +{ + "inlay_hints": { + "show_value_hints": false + } +} +``` + +Inline value hints can also be toggled from the Editor Controls menu in the editor toolbar. + ### Log Dap Communications - Description: Whether to log messages between active debug adapters and Zed. (Used for DAP development) From ebad5ca50e11fc9d3fbbab397888d0944a825bb7 Mon Sep 17 00:00:00 2001 From: Smit Barmase <heysmitbarmase@gmail.com> Date: Thu, 17 Jul 2025 06:36:02 +0530 Subject: [PATCH 141/658] =?UTF-8?q?linux:=20Fix=20buttons=20clicks=20would?= =?UTF-8?q?n=E2=80=99t=20work=20on=20startup=20until=20clicked=20on=20cent?= =?UTF-8?q?er=20pane=20(#34590)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #31805 This is an issue with Linux currently that `window.focus` is `None` upon startup in both X11 and Wayland. Specifically, the order in which [this](https://github.com/zed-industries/zed/blob/8d05a3d389b6a1caa80bb18f26c3dac0c26debcb/crates/gpui/src/window.rs#L3116) and [this](https://github.com/zed-industries/zed/blob/8d05a3d389b6a1caa80bb18f26c3dac0c26debcb/crates/gpui/src/app.rs#L956) are executed varies between Linux and macOS. That is, one tries to remove (blur) focus from a window, while other checks window focus to put that focus id to a frame. In macOS, blur happens afterwards setting focus on a frame, but in Linux, the inverse of it happens, leading to `window.focus` to `None`. For the time being, we handle all visible buttons to take care of this **focus can be `None`** case, and make it work anyway. But, we should look at the deeper issue mentioned above with GPUI. Created new issue to track that https://github.com/zed-industries/zed/issues/34591. Release Notes: - Fixed an issue where button clicks wouldn’t work on startup until clicked on the center pane on Linux. --- crates/title_bar/src/title_bar.rs | 9 +++++---- crates/workspace/src/dock.rs | 3 +++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 453bb54db8b1708219a8922bbadcb3d1c8be3be2..977b5c3ecd0e5170a46941f2f3459e9d0d77f06e 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -22,9 +22,9 @@ use auto_update::AutoUpdateStatus; use call::ActiveCall; use client::{Client, UserStore}; use gpui::{ - Action, AnyElement, App, Context, Corner, Element, Entity, InteractiveElement, IntoElement, - MouseButton, ParentElement, Render, StatefulInteractiveElement, Styled, Subscription, - WeakEntity, Window, actions, div, + Action, AnyElement, App, Context, Corner, Element, Entity, Focusable, InteractiveElement, + IntoElement, MouseButton, ParentElement, Render, StatefulInteractiveElement, Styled, + Subscription, WeakEntity, Window, actions, div, }; use onboarding_banner::OnboardingBanner; use project::Project; @@ -504,7 +504,8 @@ impl TitleBar { ) }) .on_click(move |_, window, cx| { - let _ = workspace.update(cx, |_this, cx| { + let _ = workspace.update(cx, |this, cx| { + window.focus(&this.active_pane().focus_handle(cx)); window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx); }); }) diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index c8301dcf352f63db50586ee3a5e603a4583f6280..4e39c2d18287519e282b1812087ba72740080ec7 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -873,6 +873,8 @@ impl Render for PanelButtons { (action, icon_tooltip.into()) }; + let focus_handle = dock.focus_handle(cx); + Some( right_click_menu(name) .menu(move |window, cx| { @@ -909,6 +911,7 @@ impl Render for PanelButtons { .on_click({ let action = action.boxed_clone(); move |_, window, cx| { + window.focus(&focus_handle); window.dispatch_action(action.boxed_clone(), cx) } }) From 9f302df6d6977626e3ae0fe5d8df3fc5ebcb0c88 Mon Sep 17 00:00:00 2001 From: Conrad Irwin <conrad.irwin@gmail.com> Date: Wed, 16 Jul 2025 20:09:38 -0600 Subject: [PATCH 142/658] Don't override ascii graphical shortcuts (#34592) Closes #34536 Release Notes: - (preview only) Fix shortcuts on Extended Latin keyboards on Linux --- crates/gpui/src/platform/linux/platform.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index bab44e0069319030915814f2d9a6b471d1e56ebb..1e901387b02a6b32c061e7c97346d58b8ed3eb19 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -827,6 +827,9 @@ impl crate::Keystroke { let name = xkb::keysym_get_name(key_sym).to_lowercase(); if key_sym.is_keypad_key() { name.replace("kp_", "") + } else if key_utf8.len() == 1 && key_utf8.chars().next().unwrap().is_ascii_graphic() + { + key_utf8.clone() } else if let Some(key_en) = guess_ascii(keycode, modifiers.shift) { String::from(key_en) } else { From 1ce384bbda2afa90069eac4f9d36c342cd860f8c Mon Sep 17 00:00:00 2001 From: Conrad Irwin <conrad.irwin@gmail.com> Date: Wed, 16 Jul 2025 21:27:46 -0600 Subject: [PATCH 143/658] Fix ctrl-q on AZERTY on Linux (#34597) Closes #ISSUE Release Notes: - N/A --- crates/gpui/src/platform/linux/platform.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index 1e901387b02a6b32c061e7c97346d58b8ed3eb19..a24838339ed85bfef910aedcd47663aadf13f3a9 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -822,14 +822,28 @@ impl crate::Keystroke { Keysym::underscore => "_".to_owned(), Keysym::equal => "=".to_owned(), Keysym::plus => "+".to_owned(), + Keysym::space => "space".to_owned(), + Keysym::BackSpace => "backspace".to_owned(), + Keysym::Tab => "tab".to_owned(), + Keysym::Delete => "delete".to_owned(), + Keysym::Escape => "escape".to_owned(), _ => { let name = xkb::keysym_get_name(key_sym).to_lowercase(); if key_sym.is_keypad_key() { name.replace("kp_", "") - } else if key_utf8.len() == 1 && key_utf8.chars().next().unwrap().is_ascii_graphic() + } else if let Some(key) = key_utf8.chars().next() + && key_utf8.len() == 1 + && key.is_ascii() { - key_utf8.clone() + if key.is_ascii_graphic() { + key_utf8.clone() + // map ctrl-a to a + } else if key_utf32 <= 0x1f { + ((key_utf32 as u8 + 0x60) as char).to_string() + } else { + name + } } else if let Some(key_en) = guess_ascii(keycode, modifiers.shift) { String::from(key_en) } else { From 1d72fa8e9e4b358bf8a207dd73bf934b90d56cd9 Mon Sep 17 00:00:00 2001 From: Eric Cornelissen <ericornelissen@gmail.com> Date: Thu, 17 Jul 2025 05:39:54 +0200 Subject: [PATCH 144/658] git: Add ability to pass `--signoff` (#29874) This adds an option for `--signoff` to the git panel and commit modal. It allows users to enable the [`--signoff` flag](https://git-scm.com/docs/git-commit#Documentation/git-commit.txt-code--signoffcode) when committing through Zed. The option is added to the context menu of the commit button (following the style of the "Editor Controls"). To support this, the commit+amend experience was revamped (following the ideas of [this comment](https://github.com/zed-industries/zed/pull/29874#issuecomment-2950848000)). Amending is now also a toggle in the commit button's dropdown menu. I've kept some of the original experience such as the changed button text and ability to cancel outside the context menu. The tooltip of the commit buttons now also includes the flags that will be used based on the amending and signoff status (which I couldn't capture in screenshots unfortunately). So, by default the tooltip will say `git commit` and if you toggle, e.g., amending on it will say `git commit --amend`. | What | Panel | Modal | | --- | --- | --- | | Not amending, dropdown | ![git modal preview, not amending, dropdown](https://github.com/user-attachments/assets/82c2b338-b3b5-418c-97bf-98c33202d7dd) | ![commit modal preview, not amending, dropdown](https://github.com/user-attachments/assets/f7a6f2fb-902d-447d-a473-2efb4ba0f444) | | Amending, dropdown | ![git modal preview, amending, dropdown](https://github.com/user-attachments/assets/9e755975-4a27-43f0-aa62-be002ecd3a92) | ![commit modal preview, amending, dropdown](https://github.com/user-attachments/assets/cad03817-14e1-46f6-ba39-8ccc7dd12161) | | Amending | ![git modal preview, amending](https://github.com/user-attachments/assets/e1ec4eba-174e-4e5f-9659-5867d6b0fdc2) | - | The initial implementation was based on the changeset of https://github.com/zed-industries/zed/pull/28187. Closes https://github.com/zed-industries/zed/discussions/26114 Release Notes: - Added git `--signoff` support. - Update the git `--amend` experience. - Improved git panel to persist width as well as amend and signoff on a per-workspace basis. --- crates/git/src/git.rs | 2 + crates/git/src/repository.rs | 5 + crates/git_ui/src/commit_modal.rs | 253 +++++++++--------- crates/git_ui/src/git_panel.rs | 416 +++++++++++++++--------------- crates/project/src/git_store.rs | 2 + crates/proto/proto/git.proto | 1 + 6 files changed, 342 insertions(+), 337 deletions(-) diff --git a/crates/git/src/git.rs b/crates/git/src/git.rs index fccedaa80989a91dab1a9f53804cba7072ed65d4..3714086dd0a7696d232fb34e5b7f545617227add 100644 --- a/crates/git/src/git.rs +++ b/crates/git/src/git.rs @@ -77,6 +77,8 @@ actions!( Commit, /// Amends the last commit with staged changes. Amend, + /// Enable the --signoff option. + Signoff, /// Cancels the current git operation. Cancel, /// Expands the commit message editor. diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 2ecd4bb894348cf3fc532a8473e43f0712e61700..9cc3442392837d2c2e395133ff0d7dc0435a9cf2 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -96,6 +96,7 @@ impl Upstream { #[derive(Clone, Copy, Default)] pub struct CommitOptions { pub amend: bool, + pub signoff: bool, } #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] @@ -1209,6 +1210,10 @@ impl GitRepository for RealGitRepository { cmd.arg("--amend"); } + if options.signoff { + cmd.arg("--signoff"); + } + if let Some((name, email)) = name_and_email { cmd.arg("--author").arg(&format!("{name} <{email}>")); } diff --git a/crates/git_ui/src/commit_modal.rs b/crates/git_ui/src/commit_modal.rs index 15d0bec3139ca004e11982bc06b6f458b406be36..ac3d24e3eb791e9114844feccdb603475119303c 100644 --- a/crates/git_ui/src/commit_modal.rs +++ b/crates/git_ui/src/commit_modal.rs @@ -1,8 +1,8 @@ use crate::branch_picker::{self, BranchList}; use crate::git_panel::{GitPanel, commit_message_editor}; use git::repository::CommitOptions; -use git::{Amend, Commit, GenerateCommitMessage}; -use panel::{panel_button, panel_editor_style, panel_filled_button}; +use git::{Amend, Commit, GenerateCommitMessage, Signoff}; +use panel::{panel_button, panel_editor_style}; use ui::{ ContextMenu, KeybindingHint, PopoverMenu, PopoverMenuHandle, SplitButton, Tooltip, prelude::*, }; @@ -273,14 +273,51 @@ impl CommitModal { .child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)), ), ) - .menu(move |window, cx| { - Some(ContextMenu::build(window, cx, |context_menu, _, _| { - context_menu - .when_some(keybinding_target.clone(), |el, keybinding_target| { - el.context(keybinding_target.clone()) - }) - .action("Amend", Amend.boxed_clone()) - })) + .menu({ + let git_panel_entity = self.git_panel.clone(); + move |window, cx| { + let git_panel = git_panel_entity.read(cx); + let amend_enabled = git_panel.amend_pending(); + let signoff_enabled = git_panel.signoff_enabled(); + let has_previous_commit = git_panel.head_commit(cx).is_some(); + + Some(ContextMenu::build(window, cx, |context_menu, _, _| { + context_menu + .when_some(keybinding_target.clone(), |el, keybinding_target| { + el.context(keybinding_target.clone()) + }) + .when(has_previous_commit, |this| { + this.toggleable_entry( + "Amend", + amend_enabled, + IconPosition::Start, + Some(Box::new(Amend)), + { + let git_panel = git_panel_entity.clone(); + move |window, cx| { + git_panel.update(cx, |git_panel, cx| { + git_panel.toggle_amend_pending(&Amend, window, cx); + }) + } + }, + ) + }) + .toggleable_entry( + "Signoff", + signoff_enabled, + IconPosition::Start, + Some(Box::new(Signoff)), + { + let git_panel = git_panel_entity.clone(); + move |window, cx| { + git_panel.update(cx, |git_panel, cx| { + git_panel.toggle_signoff_enabled(&Signoff, window, cx); + }) + } + }, + ) + })) + } }) .with_handle(self.commit_menu_handle.clone()) .anchor(Corner::TopRight) @@ -295,7 +332,7 @@ impl CommitModal { generate_commit_message, active_repo, is_amend_pending, - has_previous_commit, + is_signoff_enabled, ) = self.git_panel.update(cx, |git_panel, cx| { let (can_commit, tooltip) = git_panel.configure_commit_button(cx); let title = git_panel.commit_button_title(); @@ -303,10 +340,7 @@ impl CommitModal { let generate_commit_message = git_panel.render_generate_commit_message_button(cx); let active_repo = git_panel.active_repository.clone(); let is_amend_pending = git_panel.amend_pending(); - let has_previous_commit = active_repo - .as_ref() - .and_then(|repo| repo.read(cx).head_commit.as_ref()) - .is_some(); + let is_signoff_enabled = git_panel.signoff_enabled(); ( can_commit, tooltip, @@ -315,7 +349,7 @@ impl CommitModal { generate_commit_message, active_repo, is_amend_pending, - has_previous_commit, + is_signoff_enabled, ) }); @@ -396,126 +430,59 @@ impl CommitModal { .px_1() .gap_4() .children(close_kb_hint) - .when(is_amend_pending, |this| { - let focus_handle = focus_handle.clone(); - this.child( - panel_filled_button(commit_label) - .tooltip(move |window, cx| { - if can_commit { - Tooltip::for_action_in( - tooltip, - &Amend, - &focus_handle, - window, - cx, - ) - } else { - Tooltip::simple(tooltip, cx) - } - }) - .disabled(!can_commit) - .on_click(cx.listener(move |this, _: &ClickEvent, window, cx| { - telemetry::event!("Git Amended", source = "Git Modal"); - this.git_panel.update(cx, |git_panel, cx| { - git_panel.set_amend_pending(false, cx); - git_panel.commit_changes( - CommitOptions { amend: true }, - window, - cx, - ); - }); - cx.emit(DismissEvent); - })), + .child(SplitButton::new( + ui::ButtonLike::new_rounded_left(ElementId::Name( + format!("split-button-left-{}", commit_label).into(), + )) + .layer(ui::ElevationIndex::ModalSurface) + .size(ui::ButtonSize::Compact) + .child( + div() + .child(Label::new(commit_label).size(LabelSize::Small)) + .mr_0p5(), ) - }) - .when(!is_amend_pending, |this| { - this.when(has_previous_commit, |this| { - this.child(SplitButton::new( - ui::ButtonLike::new_rounded_left(ElementId::Name( - format!("split-button-left-{}", commit_label).into(), - )) - .layer(ui::ElevationIndex::ModalSurface) - .size(ui::ButtonSize::Compact) - .child( - div() - .child(Label::new(commit_label).size(LabelSize::Small)) - .mr_0p5(), + .on_click(cx.listener(move |this, _: &ClickEvent, window, cx| { + telemetry::event!("Git Committed", source = "Git Modal"); + this.git_panel.update(cx, |git_panel, cx| { + git_panel.commit_changes( + CommitOptions { + amend: is_amend_pending, + signoff: is_signoff_enabled, + }, + window, + cx, ) - .on_click(cx.listener(move |this, _: &ClickEvent, window, cx| { - telemetry::event!("Git Committed", source = "Git Modal"); - this.git_panel.update(cx, |git_panel, cx| { - git_panel.commit_changes( - CommitOptions { amend: false }, - window, - cx, - ) - }); - cx.emit(DismissEvent); - })) - .disabled(!can_commit) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - if can_commit { - Tooltip::with_meta_in( - tooltip, - Some(&git::Commit), - "git commit", - &focus_handle.clone(), - window, - cx, - ) - } else { - Tooltip::simple(tooltip, cx) - } - } - }), - self.render_git_commit_menu( - ElementId::Name( - format!("split-button-right-{}", commit_label).into(), - ), - Some(focus_handle.clone()), - ) - .into_any_element(), - )) - }) - .when(!has_previous_commit, |this| { - this.child( - panel_filled_button(commit_label) - .tooltip(move |window, cx| { - if can_commit { - Tooltip::with_meta_in( - tooltip, - Some(&git::Commit), - "git commit", - &focus_handle, - window, - cx, - ) - } else { - Tooltip::simple(tooltip, cx) - } - }) - .disabled(!can_commit) - .on_click(cx.listener( - move |this, _: &ClickEvent, window, cx| { - telemetry::event!( - "Git Committed", - source = "Git Modal" - ); - this.git_panel.update(cx, |git_panel, cx| { - git_panel.commit_changes( - CommitOptions { amend: false }, - window, - cx, - ) - }); - cx.emit(DismissEvent); - }, - )), - ) - }) - }), + }); + cx.emit(DismissEvent); + })) + .disabled(!can_commit) + .tooltip({ + let focus_handle = focus_handle.clone(); + move |window, cx| { + if can_commit { + Tooltip::with_meta_in( + tooltip, + Some(&git::Commit), + format!( + "git commit{}{}", + if is_amend_pending { " --amend" } else { "" }, + if is_signoff_enabled { " --signoff" } else { "" } + ), + &focus_handle.clone(), + window, + cx, + ) + } else { + Tooltip::simple(tooltip, cx) + } + } + }), + self.render_git_commit_menu( + ElementId::Name(format!("split-button-right-{}", commit_label).into()), + Some(focus_handle.clone()), + ) + .into_any_element(), + )), ) } @@ -534,7 +501,14 @@ impl CommitModal { } telemetry::event!("Git Committed", source = "Git Modal"); self.git_panel.update(cx, |git_panel, cx| { - git_panel.commit_changes(CommitOptions { amend: false }, window, cx) + git_panel.commit_changes( + CommitOptions { + amend: false, + signoff: git_panel.signoff_enabled(), + }, + window, + cx, + ) }); cx.emit(DismissEvent); } @@ -559,7 +533,14 @@ impl CommitModal { telemetry::event!("Git Amended", source = "Git Modal"); self.git_panel.update(cx, |git_panel, cx| { git_panel.set_amend_pending(false, cx); - git_panel.commit_changes(CommitOptions { amend: true }, window, cx); + git_panel.commit_changes( + CommitOptions { + amend: true, + signoff: git_panel.signoff_enabled(), + }, + window, + cx, + ); }); cx.emit(DismissEvent); } diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 52bed2cc793944ac5f849786fa998c43fa420321..2397f51f82f4e29e8e967ba88fc3b62d28614b6e 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -25,7 +25,7 @@ use git::repository::{ UpstreamTrackingStatus, get_git_committer, }; use git::status::StageStatus; -use git::{Amend, ToggleStaged, repository::RepoPath, status::FileStatus}; +use git::{Amend, Signoff, ToggleStaged, repository::RepoPath, status::FileStatus}; use git::{ExpandCommitEditor, RestoreTrackedFiles, StageAll, TrashUntrackedFiles, UnstageAll}; use gpui::{ Action, Animation, AnimationExt as _, AsyncApp, AsyncWindowContext, Axis, ClickEvent, Corner, @@ -61,8 +61,8 @@ use std::{collections::HashSet, sync::Arc, time::Duration, usize}; use strum::{IntoEnumIterator, VariantNames}; use time::OffsetDateTime; use ui::{ - Checkbox, ContextMenu, ElevationIndex, PopoverMenu, Scrollbar, ScrollbarState, SplitButton, - Tooltip, prelude::*, + Checkbox, ContextMenu, ElevationIndex, IconPosition, Label, LabelSize, PopoverMenu, Scrollbar, + ScrollbarState, SplitButton, Tooltip, prelude::*, }; use util::{ResultExt, TryFutureExt, maybe}; @@ -174,6 +174,10 @@ pub enum Event { #[derive(Serialize, Deserialize)] struct SerializedGitPanel { width: Option<Pixels>, + #[serde(default)] + amend_pending: bool, + #[serde(default)] + signoff_enabled: bool, } #[derive(Debug, PartialEq, Eq, Clone, Copy)] @@ -337,6 +341,7 @@ pub struct GitPanel { pending: Vec<PendingOperation>, pending_commit: Option<Task<()>>, amend_pending: bool, + signoff_enabled: bool, pending_serialization: Task<Option<()>>, pub(crate) project: Entity<Project>, scroll_handle: UniformListScrollHandle, @@ -512,6 +517,7 @@ impl GitPanel { pending: Vec::new(), pending_commit: None, amend_pending: false, + signoff_enabled: false, pending_serialization: Task::ready(None), single_staged_entry: None, single_tracked_entry: None, @@ -690,14 +696,38 @@ impl GitPanel { cx.notify(); } + fn serialization_key(workspace: &Workspace) -> Option<String> { + workspace + .database_id() + .map(|id| i64::from(id).to_string()) + .or(workspace.session_id()) + .map(|id| format!("{}-{:?}", GIT_PANEL_KEY, id)) + } + fn serialize(&mut self, cx: &mut Context<Self>) { let width = self.width; + let amend_pending = self.amend_pending; + let signoff_enabled = self.signoff_enabled; + + let Some(serialization_key) = self + .workspace + .read_with(cx, |workspace, _| Self::serialization_key(workspace)) + .ok() + .flatten() + else { + return; + }; + self.pending_serialization = cx.background_spawn( async move { KEY_VALUE_STORE .write_kvp( - GIT_PANEL_KEY.into(), - serde_json::to_string(&SerializedGitPanel { width })?, + serialization_key, + serde_json::to_string(&SerializedGitPanel { + width, + amend_pending, + signoff_enabled, + })?, ) .await?; anyhow::Ok(()) @@ -1432,7 +1462,14 @@ impl GitPanel { .contains_focused(window, cx) { telemetry::event!("Git Committed", source = "Git Panel"); - self.commit_changes(CommitOptions { amend: false }, window, cx) + self.commit_changes( + CommitOptions { + amend: false, + signoff: self.signoff_enabled, + }, + window, + cx, + ) } else { cx.propagate(); } @@ -1444,19 +1481,21 @@ impl GitPanel { .focus_handle(cx) .contains_focused(window, cx) { - if self - .active_repository - .as_ref() - .and_then(|repo| repo.read(cx).head_commit.as_ref()) - .is_some() - { + if self.head_commit(cx).is_some() { if !self.amend_pending { self.set_amend_pending(true, cx); self.load_last_commit_message_if_empty(cx); } else { telemetry::event!("Git Amended", source = "Git Panel"); self.set_amend_pending(false, cx); - self.commit_changes(CommitOptions { amend: true }, window, cx); + self.commit_changes( + CommitOptions { + amend: true, + signoff: self.signoff_enabled, + }, + window, + cx, + ); } } } else { @@ -1464,21 +1503,21 @@ impl GitPanel { } } + pub fn head_commit(&self, cx: &App) -> Option<CommitDetails> { + self.active_repository + .as_ref() + .and_then(|repo| repo.read(cx).head_commit.as_ref()) + .cloned() + } + pub fn load_last_commit_message_if_empty(&mut self, cx: &mut Context<Self>) { if !self.commit_editor.read(cx).is_empty(cx) { return; } - let Some(active_repository) = self.active_repository.as_ref() else { - return; - }; - let Some(recent_sha) = active_repository - .read(cx) - .head_commit - .as_ref() - .map(|commit| commit.sha.to_string()) - else { + let Some(head_commit) = self.head_commit(cx) else { return; }; + let recent_sha = head_commit.sha.to_string(); let detail_task = self.load_commit_details(recent_sha, cx); cx.spawn(async move |this, cx| { if let Ok(message) = detail_task.await.map(|detail| detail.message) { @@ -1495,12 +1534,6 @@ impl GitPanel { .detach(); } - fn cancel(&mut self, _: &git::Cancel, _: &mut Window, cx: &mut Context<Self>) { - if self.amend_pending { - self.set_amend_pending(false, cx); - } - } - fn custom_or_suggested_commit_message( &self, window: &mut Window, @@ -3003,14 +3036,35 @@ impl GitPanel { .child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)), ), ) - .menu(move |window, cx| { - Some(ContextMenu::build(window, cx, |context_menu, _, _| { - context_menu - .when_some(keybinding_target.clone(), |el, keybinding_target| { - el.context(keybinding_target.clone()) - }) - .action("Amend", Amend.boxed_clone()) - })) + .menu({ + let has_previous_commit = self.head_commit(cx).is_some(); + let amend = self.amend_pending(); + let signoff = self.signoff_enabled; + + move |window, cx| { + Some(ContextMenu::build(window, cx, |context_menu, _, _| { + context_menu + .when_some(keybinding_target.clone(), |el, keybinding_target| { + el.context(keybinding_target.clone()) + }) + .when(has_previous_commit, |this| { + this.toggleable_entry( + "Amend", + amend, + IconPosition::Start, + Some(Box::new(Amend)), + move |window, cx| window.dispatch_action(Box::new(Amend), cx), + ) + }) + .toggleable_entry( + "Signoff", + signoff, + IconPosition::Start, + Some(Box::new(Signoff)), + move |window, cx| window.dispatch_action(Box::new(Signoff), cx), + ) + })) + } }) .anchor(Corner::TopRight) } @@ -3187,7 +3241,6 @@ impl GitPanel { let editor_is_long = self.commit_editor.update(cx, |editor, cx| { editor.max_point(cx).row().0 >= MAX_PANEL_EDITOR_LINES as u32 }); - let has_previous_commit = head_commit.is_some(); let footer = v_flex() .child(PanelRepoFooter::new( @@ -3231,7 +3284,7 @@ impl GitPanel { h_flex() .gap_0p5() .children(enable_coauthors) - .child(self.render_commit_button(has_previous_commit, cx)), + .child(self.render_commit_button(cx)), ), ) .child( @@ -3280,14 +3333,12 @@ impl GitPanel { Some(footer) } - fn render_commit_button( - &self, - has_previous_commit: bool, - cx: &mut Context<Self>, - ) -> impl IntoElement { + fn render_commit_button(&self, cx: &mut Context<Self>) -> impl IntoElement { let (can_commit, tooltip) = self.configure_commit_button(cx); let title = self.commit_button_title(); let commit_tooltip_focus_handle = self.commit_editor.focus_handle(cx); + let amend = self.amend_pending(); + let signoff = self.signoff_enabled; div() .id("commit-wrapper") @@ -3296,165 +3347,86 @@ impl GitPanel { *hovered && !this.has_staged_changes() && !this.has_unstaged_conflicts(); cx.notify() })) - .when(self.amend_pending, { - |this| { - this.h_flex() - .gap_1() - .child( - panel_filled_button("Cancel") - .tooltip({ - let handle = commit_tooltip_focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Cancel amend", - &git::Cancel, - &handle, - window, - cx, - ) - } - }) - .on_click(move |_, window, cx| { - window.dispatch_action(Box::new(git::Cancel), cx); - }), - ) - .child( - panel_filled_button(title) - .tooltip({ - let handle = commit_tooltip_focus_handle.clone(); - move |window, cx| { - if can_commit { - Tooltip::for_action_in( - tooltip, &Amend, &handle, window, cx, - ) - } else { - Tooltip::simple(tooltip, cx) - } - } - }) - .disabled(!can_commit || self.modal_open) - .on_click({ - let git_panel = cx.weak_entity(); - move |_, window, cx| { - telemetry::event!("Git Amended", source = "Git Panel"); - git_panel - .update(cx, |git_panel, cx| { - git_panel.set_amend_pending(false, cx); - git_panel.commit_changes( - CommitOptions { amend: true }, - window, - cx, - ); - }) - .ok(); - } - }), - ) - } - }) - .when(!self.amend_pending, |this| { - this.when(has_previous_commit, |this| { - this.child(SplitButton::new( - ui::ButtonLike::new_rounded_left(ElementId::Name( - format!("split-button-left-{}", title).into(), - )) - .layer(ui::ElevationIndex::ModalSurface) - .size(ui::ButtonSize::Compact) - .child( - div() - .child(Label::new(title).size(LabelSize::Small)) - .mr_0p5(), - ) - .on_click({ - let git_panel = cx.weak_entity(); - move |_, window, cx| { - telemetry::event!("Git Committed", source = "Git Panel"); - git_panel - .update(cx, |git_panel, cx| { - git_panel.commit_changes( - CommitOptions { amend: false }, - window, - cx, - ); - }) - .ok(); - } - }) - .disabled(!can_commit || self.modal_open) - .tooltip({ - let handle = commit_tooltip_focus_handle.clone(); - move |window, cx| { - if can_commit { - Tooltip::with_meta_in( - tooltip, - Some(&git::Commit), - "git commit", - &handle.clone(), - window, - cx, - ) - } else { - Tooltip::simple(tooltip, cx) - } - } - }), - self.render_git_commit_menu( - ElementId::Name(format!("split-button-right-{}", title).into()), - Some(commit_tooltip_focus_handle.clone()), - cx, - ) - .into_any_element(), - )) - }) - .when(!has_previous_commit, |this| { - this.child( - panel_filled_button(title) - .tooltip(move |window, cx| { - if can_commit { - Tooltip::with_meta_in( - tooltip, - Some(&git::Commit), - "git commit", - &commit_tooltip_focus_handle, - window, - cx, - ) - } else { - Tooltip::simple(tooltip, cx) - } + .child(SplitButton::new( + ui::ButtonLike::new_rounded_left(ElementId::Name( + format!("split-button-left-{}", title).into(), + )) + .layer(ui::ElevationIndex::ModalSurface) + .size(ui::ButtonSize::Compact) + .child( + div() + .child(Label::new(title).size(LabelSize::Small)) + .mr_0p5(), + ) + .on_click({ + let git_panel = cx.weak_entity(); + move |_, window, cx| { + telemetry::event!("Git Committed", source = "Git Panel"); + git_panel + .update(cx, |git_panel, cx| { + git_panel.set_amend_pending(false, cx); + git_panel.commit_changes( + CommitOptions { amend, signoff }, + window, + cx, + ); }) - .disabled(!can_commit || self.modal_open) - .on_click({ - let git_panel = cx.weak_entity(); - move |_, window, cx| { - telemetry::event!("Git Committed", source = "Git Panel"); - git_panel - .update(cx, |git_panel, cx| { - git_panel.commit_changes( - CommitOptions { amend: false }, - window, - cx, - ); - }) - .ok(); - } - }), - ) + .ok(); + } }) - }) + .disabled(!can_commit || self.modal_open) + .tooltip({ + let handle = commit_tooltip_focus_handle.clone(); + move |window, cx| { + if can_commit { + Tooltip::with_meta_in( + tooltip, + Some(&git::Commit), + format!( + "git commit{}{}", + if amend { " --amend" } else { "" }, + if signoff { " --signoff" } else { "" } + ), + &handle.clone(), + window, + cx, + ) + } else { + Tooltip::simple(tooltip, cx) + } + } + }), + self.render_git_commit_menu( + ElementId::Name(format!("split-button-right-{}", title).into()), + Some(commit_tooltip_focus_handle.clone()), + cx, + ) + .into_any_element(), + )) } fn render_pending_amend(&self, cx: &mut Context<Self>) -> impl IntoElement { - div() - .p_2() + h_flex() + .py_1p5() + .px_2() + .gap_1p5() + .justify_between() .border_t_1() - .border_color(cx.theme().colors().border) + .border_color(cx.theme().colors().border.opacity(0.8)) .child( - Label::new( - "This will update your most recent commit. Cancel to make a new one instead.", - ) - .size(LabelSize::Small), + div() + .flex_grow() + .overflow_hidden() + .max_w(relative(0.85)) + .child( + Label::new("This will update your most recent commit.") + .size(LabelSize::Small) + .truncate(), + ), ) + .child(panel_button("Cancel").size(ButtonSize::Default).on_click( + cx.listener(|this, _, window, cx| this.toggle_amend_pending(&Amend, window, cx)), + )) } fn render_previous_commit(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> { @@ -4218,17 +4190,56 @@ impl GitPanel { cx.notify(); } + pub fn toggle_amend_pending( + &mut self, + _: &Amend, + _window: &mut Window, + cx: &mut Context<Self>, + ) { + self.set_amend_pending(!self.amend_pending, cx); + self.serialize(cx); + } + + pub fn signoff_enabled(&self) -> bool { + self.signoff_enabled + } + + pub fn set_signoff_enabled(&mut self, value: bool, cx: &mut Context<Self>) { + self.signoff_enabled = value; + self.serialize(cx); + cx.notify(); + } + + pub fn toggle_signoff_enabled( + &mut self, + _: &Signoff, + _window: &mut Window, + cx: &mut Context<Self>, + ) { + self.set_signoff_enabled(!self.signoff_enabled, cx); + } + pub async fn load( workspace: WeakEntity<Workspace>, mut cx: AsyncWindowContext, ) -> anyhow::Result<Entity<Self>> { - let serialized_panel = cx - .background_spawn(async move { KEY_VALUE_STORE.read_kvp(&GIT_PANEL_KEY) }) - .await - .context("loading git panel") - .log_err() + let serialized_panel = match workspace + .read_with(&cx, |workspace, _| Self::serialization_key(workspace)) + .ok() .flatten() - .and_then(|panel| serde_json::from_str::<SerializedGitPanel>(&panel).log_err()); + { + Some(serialization_key) => cx + .background_spawn(async move { KEY_VALUE_STORE.read_kvp(&serialization_key) }) + .await + .context("loading git panel") + .log_err() + .flatten() + .map(|panel| serde_json::from_str::<SerializedGitPanel>(&panel)) + .transpose() + .log_err() + .flatten(), + None => None, + }; workspace.update_in(&mut cx, |workspace, window, cx| { let panel = GitPanel::new(workspace, window, cx); @@ -4236,6 +4247,8 @@ impl GitPanel { if let Some(serialized_panel) = serialized_panel { panel.update(cx, |panel, cx| { panel.width = serialized_panel.width; + panel.amend_pending = serialized_panel.amend_pending; + panel.signoff_enabled = serialized_panel.signoff_enabled; cx.notify(); }) } @@ -4320,7 +4333,8 @@ impl Render for GitPanel { .on_action(cx.listener(Self::stage_range)) .on_action(cx.listener(GitPanel::commit)) .on_action(cx.listener(GitPanel::amend)) - .on_action(cx.listener(GitPanel::cancel)) + .on_action(cx.listener(GitPanel::toggle_amend_pending)) + .on_action(cx.listener(GitPanel::toggle_signoff_enabled)) .on_action(cx.listener(Self::stage_all)) .on_action(cx.listener(Self::unstage_all)) .on_action(cx.listener(Self::stage_selected)) diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 9ff3823e0f13a87fdcff944db7ad2d52350a7cce..5f07bafe5bd9e99f723bd3932b0a985b457ae85b 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -1738,6 +1738,7 @@ impl GitStore { name.zip(email), CommitOptions { amend: options.amend, + signoff: options.signoff, }, cx, ) @@ -3488,6 +3489,7 @@ impl Repository { email: email.map(String::from), options: Some(proto::commit::CommitOptions { amend: options.amend, + signoff: options.signoff, }), }) .await diff --git a/crates/proto/proto/git.proto b/crates/proto/proto/git.proto index 6593062ed2b9c4e840fb60a033187c5fd22d0860..1d544b15ff77e9dc7e960b3b8519e870b4231309 100644 --- a/crates/proto/proto/git.proto +++ b/crates/proto/proto/git.proto @@ -298,6 +298,7 @@ message Commit { message CommitOptions { bool amend = 1; + bool signoff = 2; } } From ad2bfa3edd0991d9c15f349dab0e416a08210e9f Mon Sep 17 00:00:00 2001 From: Finn Evers <finn@zed.dev> Date: Thu, 17 Jul 2025 11:22:04 +0200 Subject: [PATCH 145/658] Disable minimap in the inspector (#34607) This disables the minimap in the inspector UI as it doesn't bring any value to it and just takes up unnecessary space. Release Notes: - N/A --- crates/inspector_ui/src/div_inspector.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/inspector_ui/src/div_inspector.rs b/crates/inspector_ui/src/div_inspector.rs index 7d162bcc355b1c29f55a6cb001638809a707599b..bd395aa01bca42ce923073ee6f80472abc7820eb 100644 --- a/crates/inspector_ui/src/div_inspector.rs +++ b/crates/inspector_ui/src/div_inspector.rs @@ -1,5 +1,8 @@ use anyhow::{Result, anyhow}; -use editor::{Bias, CompletionProvider, Editor, EditorEvent, EditorMode, ExcerptId, MultiBuffer}; +use editor::{ + Bias, CompletionProvider, Editor, EditorEvent, EditorMode, ExcerptId, MinimapVisibility, + MultiBuffer, +}; use fuzzy::StringMatch; use gpui::{ AsyncWindowContext, DivInspectorState, Entity, InspectorElementId, IntoElement, @@ -499,6 +502,7 @@ impl DivInspector { editor.set_show_git_diff_gutter(false, cx); editor.set_show_runnables(false, cx); editor.set_show_edit_predictions(Some(false), window, cx); + editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx); editor }) } From acb3ecef0c494fa8dbc346c08f722491182a192d Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon <oleksiy@zed.dev> Date: Thu, 17 Jul 2025 13:08:20 +0300 Subject: [PATCH 146/658] Do not send project notifications when agent creates a file (#34610) Release Notes: - N/A --- crates/assistant_tool/src/action_log.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/assistant_tool/src/action_log.rs b/crates/assistant_tool/src/action_log.rs index dce1b0cdc1e4bfcf48d1387dd645d8b88a252060..ecbbcc785e730e3a7ca60f014f641d555e696b9e 100644 --- a/crates/assistant_tool/src/action_log.rs +++ b/crates/assistant_tool/src/action_log.rs @@ -331,16 +331,17 @@ impl ActionLog { .get_mut(buffer) .context("buffer not tracked")?; - if let ChangeAuthor::User = author { - tracked_buffer.has_unnotified_user_edits = true; - } - let rebase = cx.background_spawn({ let mut base_text = tracked_buffer.diff_base.clone(); let old_snapshot = tracked_buffer.snapshot.clone(); let new_snapshot = buffer_snapshot.clone(); let unreviewed_edits = tracked_buffer.unreviewed_edits.clone(); let edits = diff_snapshots(&old_snapshot, &new_snapshot); + if let ChangeAuthor::User = author + && !edits.is_empty() + { + tracked_buffer.has_unnotified_user_edits = true; + } async move { if let ChangeAuthor::User = author { apply_non_conflicting_edits( From 758c5fb9551fb7a520da1b76b0d6357ea462fcb1 Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine <arseny.kapoulkine@gmail.com> Date: Thu, 17 Jul 2025 03:52:26 -0700 Subject: [PATCH 147/658] Allow disabling snippet completion by setting `snippet_sort_order` to `none` (#34565) This mirrors VSCode setting that inspired `snippet_sort_order` to begin with; VSCode supports inline/top/bottom/none, with none completely disabling snippet completion. See https://code.visualstudio.com/docs/editing/intellisense#_snippets-in-suggestions This is helpful for LSPs that do not allow configuring snippets via configuration such as clangd. Release Notes: - Added `none` as one of the values for `snippet_sort_order` to completely disable snippet completion. --- assets/settings/default.json | 2 ++ crates/editor/src/code_context_menus.rs | 15 +++++++++++++++ crates/editor/src/editor_settings.rs | 2 ++ docs/src/configuring-zed.md | 6 ++++++ docs/src/visual-customization.md | 2 +- 5 files changed, 26 insertions(+), 1 deletion(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 32d4c496c10cf31d1ae4b5f43a2996cb00eea5d0..2c980603bf3034d8460fdc6bde9096aadbb7fdf2 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -197,6 +197,8 @@ // "inline" // 3. Place snippets at the bottom of the completion list: // "bottom" + // 4. Do not show snippets in the completion list: + // "none" "snippet_sort_order": "inline", // How to highlight the current line in the editor. // diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 8fbae8d6052d89299b10f3cd0c971af79abd3c90..c7477837dd0b6cdf8e818f16491cda569ec7fc47 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -1074,6 +1074,20 @@ impl CompletionsMenu { .and_then(|q| q.chars().next()) .and_then(|c| c.to_lowercase().next()); + if snippet_sort_order == SnippetSortOrder::None { + matches.retain(|string_match| { + let completion = &completions[string_match.candidate_id]; + + let is_snippet = matches!( + &completion.source, + CompletionSource::Lsp { lsp_completion, .. } + if lsp_completion.kind == Some(CompletionItemKind::SNIPPET) + ); + + !is_snippet + }); + } + matches.sort_unstable_by_key(|string_match| { let completion = &completions[string_match.candidate_id]; @@ -1112,6 +1126,7 @@ impl CompletionsMenu { SnippetSortOrder::Top => Reverse(if is_snippet { 1 } else { 0 }), SnippetSortOrder::Bottom => Reverse(if is_snippet { 0 } else { 1 }), SnippetSortOrder::Inline => Reverse(0), + SnippetSortOrder::None => Reverse(0), }; let sort_positions = string_match.positions.clone(); let sort_exact = Reverse(if Some(completion.label.filter_text()) == query { diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 5d8379ddfb87600f7cd56d10f5684ed333589e78..14f46c0e60dfc3487430b22ea83913984bae3c24 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -395,6 +395,8 @@ pub enum SnippetSortOrder { Inline, /// Place snippets at the bottom of the completion list Bottom, + /// Do not show snippets in the completion list + None, } #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index eec9da60dd96d21652d78a8c4d0c0dfca17c207a..91a39d1ccd88f3decea5241008855368037b2cb8 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -639,6 +639,12 @@ List of `string` values "snippet_sort_order": "bottom" ``` +4. Do not show snippets in the completion list at all: + +```json +"snippet_sort_order": "none" +``` + ## Editor Scrollbar - Description: Whether or not to show the editor scrollbar and various elements in it. diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md index 636e0f9c4e242e57dd96304d877534ddcbd83632..ad38b86406bb885a66c12e91721b9ca853df81f0 100644 --- a/docs/src/visual-customization.md +++ b/docs/src/visual-customization.md @@ -317,7 +317,7 @@ TBD: Centered layout related settings ### Editor Completions, Snippets, Actions, Diagnostics {#editor-lsp} ```json - "snippet_sort_order": "inline", // Snippets completions: top, inline, bottom + "snippet_sort_order": "inline", // Snippets completions: top, inline, bottom, none "show_completions_on_input": true, // Show completions while typing "show_completion_documentation": true, // Show documentation in completions "auto_signature_help": false, // Show method signatures inside parentheses From 1e67e30034ca2b54bb3910825b045d038bfaa310 Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon <oleksiy@zed.dev> Date: Thu, 17 Jul 2025 14:21:20 +0300 Subject: [PATCH 148/658] Fix shortcuts with `Shift` (#34614) Closes #34605, #34606, #34609 Release Notes: - Fixed shortcuts involving Shift --- crates/gpui/src/platform/linux/platform.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index a24838339ed85bfef910aedcd47663aadf13f3a9..c4b90ccf084627640a6cda5806d1bcb63415feb3 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -837,7 +837,7 @@ impl crate::Keystroke { && key.is_ascii() { if key.is_ascii_graphic() { - key_utf8.clone() + key_utf8.to_lowercase() // map ctrl-a to a } else if key_utf32 <= 0x1f { ((key_utf32 as u8 + 0x60) as char).to_string() From 4df7f52bf3aa2e94dfbb4002a8881b08ee1ae8e1 Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon <oleksiy@zed.dev> Date: Thu, 17 Jul 2025 14:55:38 +0300 Subject: [PATCH 149/658] agent: Disable `project_notifications` by default (#34615) This tool needs more polishing before being generally available. Release Notes: - agent: Disabled `project_notifications` tool by default for the time being --- assets/settings/default.json | 4 ++-- crates/agent/src/thread.rs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 2c980603bf3034d8460fdc6bde9096aadbb7fdf2..358871650bbd39500912c4e7ffbc1dd57e1bc1be 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -819,7 +819,7 @@ "edit_file": true, "fetch": true, "list_directory": true, - "project_notifications": true, + "project_notifications": false, "move_path": true, "now": true, "find_path": true, @@ -839,7 +839,7 @@ "diagnostics": true, "fetch": true, "list_directory": true, - "project_notifications": true, + "project_notifications": false, "now": true, "find_path": true, "read_file": true, diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index d46dada2703438686b9df0e452dfef28777ff715..dca0f5a4304ff2da842bbe2ad0ff1939055535ca 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -3583,6 +3583,7 @@ fn main() {{ } #[gpui::test] + #[ignore] // turn this test on when project_notifications tool is re-enabled async fn test_stale_buffer_notification(cx: &mut TestAppContext) { init_test_settings(cx); From ceab139f54717aff87f2fc9175423303c61944bf Mon Sep 17 00:00:00 2001 From: Kirill Bulatov <kirill@zed.dev> Date: Thu, 17 Jul 2025 15:20:47 +0300 Subject: [PATCH 150/658] Rework extension-related errors (#34620) Before: <img width="1728" height="1079" alt="before" src="https://github.com/user-attachments/assets/4ab19211-8de4-458d-a835-52de859b7b20" /> After: <img width="1728" height="1079" alt="after" src="https://github.com/user-attachments/assets/231c9362-a0b0-47ae-b92e-de6742781d36" /> Makes clear which path is causing the FS error and removes backtraces from logging. Release Notes: - N/A --- crates/extension_host/src/extension_host.rs | 38 ++++++++++++++------- crates/extension_host/src/headless_host.rs | 5 ++- crates/extension_host/src/wasm_host.rs | 2 +- 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs index 075c68d51a1afe6bc67ae17ca88dd35b34191761..fd64d3fa59032444f827650eb1bcf4bb62321fa2 100644 --- a/crates/extension_host/src/extension_host.rs +++ b/crates/extension_host/src/extension_host.rs @@ -1313,10 +1313,17 @@ impl ExtensionStore { } for snippets_path in &snippets_to_add { - if let Some(snippets_contents) = fs.load(snippets_path).await.log_err() { - proxy - .register_snippet(snippets_path, &snippets_contents) - .log_err(); + match fs + .load(snippets_path) + .await + .with_context(|| format!("Loading snippets from {snippets_path:?}")) + { + Ok(snippets_contents) => { + proxy + .register_snippet(snippets_path, &snippets_contents) + .log_err(); + } + Err(e) => log::error!("Cannot load snippets: {e:#}"), } } } @@ -1331,20 +1338,25 @@ impl ExtensionStore { let extension_path = root_dir.join(extension.manifest.id.as_ref()); let wasm_extension = WasmExtension::load( - extension_path, + &extension_path, &extension.manifest, wasm_host.clone(), &cx, ) - .await; + .await + .with_context(|| format!("Loading extension from {extension_path:?}")); - if let Some(wasm_extension) = wasm_extension.log_err() { - wasm_extensions.push((extension.manifest.clone(), wasm_extension)); - } else { - this.update(cx, |_, cx| { - cx.emit(Event::ExtensionFailedToLoad(extension.manifest.id.clone())) - }) - .ok(); + match wasm_extension { + Ok(wasm_extension) => { + wasm_extensions.push((extension.manifest.clone(), wasm_extension)) + } + Err(e) => { + log::error!("Failed to load extension: {e:#}"); + this.update(cx, |_, cx| { + cx.emit(Event::ExtensionFailedToLoad(extension.manifest.id.clone())) + }) + .ok(); + } } } diff --git a/crates/extension_host/src/headless_host.rs b/crates/extension_host/src/headless_host.rs index dbc9bbfe1379a1766be5fc27b55f633d5b004e51..adc9638c2998eb1f122df5137577ca7e0cf4c975 100644 --- a/crates/extension_host/src/headless_host.rs +++ b/crates/extension_host/src/headless_host.rs @@ -173,9 +173,8 @@ impl HeadlessExtensionStore { return Ok(()); } - let wasm_extension: Arc<dyn Extension> = Arc::new( - WasmExtension::load(extension_dir.clone(), &manifest, wasm_host.clone(), &cx).await?, - ); + let wasm_extension: Arc<dyn Extension> = + 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/extension_host/src/wasm_host.rs b/crates/extension_host/src/wasm_host.rs index 3971fa426306a6f746bfe72de0ff96934205d4db..3e0f06fa38dfbe568da2349272dc0c7420fab66d 100644 --- a/crates/extension_host/src/wasm_host.rs +++ b/crates/extension_host/src/wasm_host.rs @@ -715,7 +715,7 @@ fn parse_wasm_extension_version_custom_section(data: &[u8]) -> Option<SemanticVe impl WasmExtension { pub async fn load( - extension_dir: PathBuf, + extension_dir: &Path, manifest: &Arc<ExtensionManifest>, wasm_host: Arc<WasmHost>, cx: &AsyncApp, From 5b97cd190002c798512fd18f92f26f96e13dbdbf Mon Sep 17 00:00:00 2001 From: Kirill Bulatov <kirill@zed.dev> Date: Thu, 17 Jul 2025 15:39:41 +0300 Subject: [PATCH 151/658] Better serialize the git panel (#34622) Follow-up of https://github.com/zed-industries/zed/pull/29874 Closes https://github.com/zed-industries/zed/issues/34618 Closes https://github.com/zed-industries/zed/issues/34611 Release Notes: - N/A --- crates/git_ui/src/git_panel.rs | 65 ++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 2397f51f82f4e29e8e967ba88fc3b62d28614b6e..e998586af4ad0f3553ed4c462a469eec1ab71124 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -65,6 +65,7 @@ use ui::{ ScrollbarState, SplitButton, Tooltip, prelude::*, }; use util::{ResultExt, TryFutureExt, maybe}; +use workspace::SERIALIZATION_THROTTLE_TIME; use workspace::{ Workspace, @@ -342,7 +343,7 @@ pub struct GitPanel { pending_commit: Option<Task<()>>, amend_pending: bool, signoff_enabled: bool, - pending_serialization: Task<Option<()>>, + pending_serialization: Task<()>, pub(crate) project: Entity<Project>, scroll_handle: UniformListScrollHandle, max_width_item_index: Option<usize>, @@ -518,7 +519,7 @@ impl GitPanel { pending_commit: None, amend_pending: false, signoff_enabled: false, - pending_serialization: Task::ready(None), + pending_serialization: Task::ready(()), single_staged_entry: None, single_tracked_entry: None, project, @@ -709,31 +710,41 @@ impl GitPanel { let amend_pending = self.amend_pending; let signoff_enabled = self.signoff_enabled; - let Some(serialization_key) = self - .workspace - .read_with(cx, |workspace, _| Self::serialization_key(workspace)) - .ok() - .flatten() - else { - return; - }; - - self.pending_serialization = cx.background_spawn( - async move { - KEY_VALUE_STORE - .write_kvp( - serialization_key, - serde_json::to_string(&SerializedGitPanel { - width, - amend_pending, - signoff_enabled, - })?, - ) - .await?; - anyhow::Ok(()) - } - .log_err(), - ); + self.pending_serialization = cx.spawn(async move |git_panel, cx| { + cx.background_executor() + .timer(SERIALIZATION_THROTTLE_TIME) + .await; + let Some(serialization_key) = git_panel + .update(cx, |git_panel, cx| { + git_panel + .workspace + .read_with(cx, |workspace, _| Self::serialization_key(workspace)) + .ok() + .flatten() + }) + .ok() + .flatten() + else { + return; + }; + cx.background_spawn( + async move { + KEY_VALUE_STORE + .write_kvp( + serialization_key, + serde_json::to_string(&SerializedGitPanel { + width, + amend_pending, + signoff_enabled, + })?, + ) + .await?; + anyhow::Ok(()) + } + .log_err(), + ) + .await; + }); } pub(crate) fn set_modal_open(&mut self, open: bool, cx: &mut Context<Self>) { From 8e4555455c6469b33803c8c653e38b2755ad9aad Mon Sep 17 00:00:00 2001 From: Agus Zubiaga <agus@zed.dev> Date: Thu, 17 Jul 2025 11:25:55 -0300 Subject: [PATCH 152/658] Claude experiment (#34577) Release Notes: - N/A --------- Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com> Co-authored-by: Anthony Eid <hello@anthonyeid.me> Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com> Co-authored-by: Nathan Sobo <nathan@zed.dev> Co-authored-by: Oleksiy Syvokon <oleksiy.syvokon@gmail.com> --- Cargo.lock | 33 +- Cargo.toml | 8 +- assets/icons/ai_claude.svg | 3 + assets/keymaps/default-linux.json | 4 +- assets/keymaps/default-macos.json | 4 +- crates/{acp => acp_thread}/Cargo.toml | 9 +- crates/{acp => acp_thread}/LICENSE-GPL | 0 .../acp.rs => acp_thread/src/acp_thread.rs} | 666 +++++------------ crates/acp_thread/src/connection.rs | 20 + crates/agent_servers/Cargo.toml | 21 + crates/agent_servers/src/agent_servers.rs | 213 +----- crates/agent_servers/src/claude.rs | 680 +++++++++++++++++ crates/agent_servers/src/claude/mcp_server.rs | 303 ++++++++ crates/agent_servers/src/claude/tools.rs | 670 +++++++++++++++++ crates/agent_servers/src/gemini.rs | 501 +++++++++++++ crates/agent_servers/src/settings.rs | 41 + .../agent_servers/src/stdio_agent_server.rs | 169 +++++ crates/agent_ui/Cargo.toml | 2 +- crates/agent_ui/src/acp/thread_view.rs | 704 +++++++----------- crates/agent_ui/src/agent_diff.rs | 4 +- crates/agent_ui/src/agent_panel.rs | 152 ++-- crates/agent_ui/src/agent_ui.rs | 31 +- crates/context_server/Cargo.toml | 2 + crates/context_server/src/client.rs | 29 +- crates/context_server/src/context_server.rs | 1 + crates/context_server/src/listener.rs | 236 ++++++ crates/context_server/src/types.rs | 2 +- crates/icons/src/icons.rs | 1 + crates/nc/Cargo.toml | 20 + crates/nc/LICENSE-GPL | 1 + crates/nc/src/nc.rs | 56 ++ crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 16 + 33 files changed, 3435 insertions(+), 1168 deletions(-) create mode 100644 assets/icons/ai_claude.svg rename crates/{acp => acp_thread}/Cargo.toml (92%) rename crates/{acp => acp_thread}/LICENSE-GPL (100%) rename crates/{acp/src/acp.rs => acp_thread/src/acp_thread.rs} (75%) create mode 100644 crates/acp_thread/src/connection.rs create mode 100644 crates/agent_servers/src/claude.rs create mode 100644 crates/agent_servers/src/claude/mcp_server.rs create mode 100644 crates/agent_servers/src/claude/tools.rs create mode 100644 crates/agent_servers/src/gemini.rs create mode 100644 crates/agent_servers/src/settings.rs create mode 100644 crates/agent_servers/src/stdio_agent_server.rs create mode 100644 crates/context_server/src/listener.rs create mode 100644 crates/nc/Cargo.toml create mode 120000 crates/nc/LICENSE-GPL create mode 100644 crates/nc/src/nc.rs diff --git a/Cargo.lock b/Cargo.lock index 59e444f1f86df7d4ee6a85b8062c28dcf55faa1f..540e3039ef042adcf67a35e543fb39b21fdb8e97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,10 +3,9 @@ version = 4 [[package]] -name = "acp" +name = "acp_thread" version = "0.1.0" dependencies = [ - "agent_servers", "agentic-coding-protocol", "anyhow", "assistant_tool", @@ -21,6 +20,7 @@ dependencies = [ "language", "markdown", "project", + "serde", "serde_json", "settings", "smol", @@ -139,16 +139,29 @@ dependencies = [ name = "agent_servers" version = "0.1.0" dependencies = [ + "acp_thread", + "agentic-coding-protocol", "anyhow", "collections", + "context_server", + "env_logger 0.11.8", "futures 0.3.31", "gpui", + "indoc", + "itertools 0.14.0", + "language", + "log", "paths", "project", "schemars", "serde", + "serde_json", "settings", + "smol", + "tempfile", + "ui", "util", + "watch", "which 6.0.3", "workspace-hack", ] @@ -176,7 +189,7 @@ dependencies = [ name = "agent_ui" version = "0.1.0" dependencies = [ - "acp", + "acp_thread", "agent", "agent_servers", "agent_settings", @@ -3411,12 +3424,14 @@ dependencies = [ "futures 0.3.31", "gpui", "log", + "net", "parking_lot", "postage", "schemars", "serde", "serde_json", "smol", + "tempfile", "url", "util", "workspace-hack", @@ -10288,6 +10303,17 @@ dependencies = [ "uuid", ] +[[package]] +name = "nc" +version = "0.1.0" +dependencies = [ + "anyhow", + "futures 0.3.31", + "net", + "smol", + "workspace-hack", +] + [[package]] name = "ndk" version = "0.8.0" @@ -20171,6 +20197,7 @@ dependencies = [ "menu", "migrator", "mimalloc", + "nc", "nix 0.29.0", "node_runtime", "notifications", diff --git a/Cargo.toml b/Cargo.toml index afb47c006e58d24fc9a6557ab437dfbf1db98e65..1c79f4c1c87c5ea12cc7fe4ff2ba10cb43459806 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ resolver = "2" members = [ "crates/activity_indicator", - "crates/acp", + "crates/acp_thread", "crates/agent_ui", "crates/agent", "crates/agent_settings", @@ -102,6 +102,7 @@ members = [ "crates/migrator", "crates/mistral", "crates/multi_buffer", + "crates/nc", "crates/net", "crates/node_runtime", "crates/notifications", @@ -219,7 +220,7 @@ edition = "2024" # Workspace member crates # -acp = { path = "crates/acp" } +acp_thread = { path = "crates/acp_thread" } agent = { path = "crates/agent" } activity_indicator = { path = "crates/activity_indicator" } agent_ui = { path = "crates/agent_ui" } @@ -317,6 +318,7 @@ menu = { path = "crates/menu" } migrator = { path = "crates/migrator" } mistral = { path = "crates/mistral" } multi_buffer = { path = "crates/multi_buffer" } +nc = { path = "crates/nc" } net = { path = "crates/net" } node_runtime = { path = "crates/node_runtime" } notifications = { path = "crates/notifications" } @@ -406,7 +408,7 @@ zlog_settings = { path = "crates/zlog_settings" } # External crates # -agentic-coding-protocol = { version = "0.0.9" } +agentic-coding-protocol = "0.0.9" 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/assets/icons/ai_claude.svg b/assets/icons/ai_claude.svg new file mode 100644 index 0000000000000000000000000000000000000000..423a963eba9b9492a9807082922a4c072786d843 --- /dev/null +++ b/assets/icons/ai_claude.svg @@ -0,0 +1,3 @@ +<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M7.8481 26.5925L15.7165 22.1806L15.8481 21.7961L15.7165 21.5836H15.3316L14.0152 21.5027L9.51899 21.3812L5.62025 21.2193L1.84304 21.0169L0.891139 20.8146L0 19.6408L0.0911392 19.0539L0.891139 18.5176L2.03544 18.6188L4.56709 18.7908L8.36456 19.0539L11.119 19.2158L15.2 19.6408H15.8481L15.9392 19.3777L15.7165 19.2158L15.5443 19.0539L11.6152 16.3926L7.36203 13.5796L5.13418 11.9605L3.92911 11.1409L3.32152 10.3719L3.05823 8.69213L4.1519 7.48798L5.62025 7.58917L5.99494 7.69036L7.48354 8.8338L10.6633 11.2927L14.8152 14.3486L15.4228 14.8545L15.6658 14.6825L15.6962 14.5611L15.4228 14.1057L13.1646 10.0278L10.7544 5.87908L9.68101 4.15887L9.39747 3.12674C9.2962 2.70175 9.22532 2.34758 9.22532 1.91247L10.4709 0.222616L11.1595 0L12.8203 0.222616L13.519 0.82975L14.5519 3.18745L16.2228 6.90109L18.8152 11.9504L19.5747 13.448L19.9797 14.8343L20.1316 15.2593H20.3949V15.0164L20.6076 12.173L21.0025 8.68201L21.3873 4.18922L21.519 2.92436L22.1468 1.40653L23.3924 0.586896L24.3646 1.05237L25.1646 2.1958L25.0532 2.93448L24.5772 6.02074L23.6456 10.8576L23.038 14.0956H23.3924L23.7975 13.6909L25.438 11.5153L28.1924 8.07488L29.4076 6.70883L30.8253 5.20111L31.7367 4.48267H33.4582L34.7241 6.36479L34.157 8.30761L32.3848 10.554L30.9165 12.4564L28.8101 15.2897L27.4937 17.5563L27.6152 17.7384L27.9291 17.7081L32.6886 16.6962L35.2608 16.2307L38.3291 15.7045L39.7165 16.3521L39.8684 17.0099L39.3215 18.3557L36.0405 19.1652L32.1924 19.9342L26.4608 21.2902L26.3899 21.3408L26.4709 21.4419L29.0532 21.6848L30.157 21.7455H32.8608L37.8937 22.1199L39.2101 22.9901L40 24.0526L39.8684 24.8621L37.843 25.8943L35.1089 25.2466L28.7291 23.7288L26.5418 23.1824H26.238V23.3645L28.0608 25.1455L31.4025 28.1609L35.5848 32.0465L35.7975 33.0078L35.2608 33.7668L34.6937 33.6858L31.0177 30.9233L29.6 29.6787L26.3899 26.977H26.1772V27.2603L26.9165 28.343L30.8253 34.212L31.0278 36.0132L30.7443 36.6L29.7316 36.9542L28.6177 36.7518L26.3291 33.5441L23.9696 29.9317L22.0658 26.6937L21.8329 26.8252L20.7089 38.9173L20.1823 39.5345L18.9671 40L17.9544 39.231L17.4177 37.9863L17.9544 35.5274L18.6025 32.3198L19.1291 29.7698L19.6051 26.6026L19.8886 25.5502L19.8684 25.4794L19.6354 25.5097L17.2456 28.7883L13.6101 33.6959L10.7342 36.7721L10.0456 37.0453L8.85063 36.428L8.96203 35.3251L9.63038 34.3435L13.6101 29.2841L16.0101 26.1472L17.5595 24.3359L17.5494 24.0729H17.4582L6.88608 30.9335L5.00253 31.1763L4.1924 30.4174L4.29367 29.1728L4.67848 28.768L7.85823 26.5823L7.8481 26.5925Z" fill="black"/> +</svg> diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index da4d79eca111980263937eaac9387f0437e8ffc9..b52b6c614d0643a1d8f7eb84251e5a0bf9a12132 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -269,10 +269,10 @@ } }, { - "context": "AgentPanel && acp_thread", + "context": "AgentPanel && external_agent_thread", "use_key_equivalents": true, "bindings": { - "ctrl-n": "agent::NewAcpThread", + "ctrl-n": "agent::NewExternalAgentThread", "ctrl-alt-t": "agent::NewThread" } }, diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 962760098b7969ae41d15c8d892c8a201d08c4a5..240b42fd1f23c38418427c01b1203847de388aac 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -310,10 +310,10 @@ } }, { - "context": "AgentPanel && acp_thread", + "context": "AgentPanel && external_agent_thread", "use_key_equivalents": true, "bindings": { - "cmd-n": "agent::NewAcpThread", + "cmd-n": "agent::NewExternalAgentThread", "cmd-alt-t": "agent::NewThread" } }, diff --git a/crates/acp/Cargo.toml b/crates/acp_thread/Cargo.toml similarity index 92% rename from crates/acp/Cargo.toml rename to crates/acp_thread/Cargo.toml index 1570aeaef083e692928d5bb3f6da8de2d79f5c0b..b44c25ccc998f5924277e6ca6ef393ca15e8e345 100644 --- a/crates/acp/Cargo.toml +++ b/crates/acp_thread/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "acp" +name = "acp_thread" version = "0.1.0" edition.workspace = true publish.workspace = true @@ -9,15 +9,13 @@ license = "GPL-3.0-or-later" workspace = true [lib] -path = "src/acp.rs" +path = "src/acp_thread.rs" doctest = false [features] test-support = ["gpui/test-support", "project/test-support"] -gemini = [] [dependencies] -agent_servers.workspace = true agentic-coding-protocol.workspace = true anyhow.workspace = true assistant_tool.workspace = true @@ -29,6 +27,8 @@ itertools.workspace = true language.workspace = true markdown.workspace = true project.workspace = true +serde.workspace = true +serde_json.workspace = true settings.workspace = true smol.workspace = true ui.workspace = true @@ -41,7 +41,6 @@ env_logger.workspace = true gpui = { workspace = true, "features" = ["test-support"] } indoc.workspace = true project = { workspace = true, "features" = ["test-support"] } -serde_json.workspace = true tempfile.workspace = true util.workspace = true settings.workspace = true diff --git a/crates/acp/LICENSE-GPL b/crates/acp_thread/LICENSE-GPL similarity index 100% rename from crates/acp/LICENSE-GPL rename to crates/acp_thread/LICENSE-GPL diff --git a/crates/acp/src/acp.rs b/crates/acp_thread/src/acp_thread.rs similarity index 75% rename from crates/acp/src/acp.rs rename to crates/acp_thread/src/acp_thread.rs index a7e72b0c2d59f8bfccb037b0e406308bcab947a0..1e3947351ab1700d94d353d1e848db403015eec0 100644 --- a/crates/acp/src/acp.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -1,7 +1,12 @@ +mod connection; +pub use connection::*; + pub use acp::ToolCallId; -use agent_servers::AgentServer; -use agentic_coding_protocol::{self as acp, ToolCallLocation, UserMessageChunk}; -use anyhow::{Context as _, Result, anyhow}; +use agentic_coding_protocol::{ + self as acp, AgentRequest, ProtocolVersion, ToolCallConfirmationOutcome, ToolCallLocation, + UserMessageChunk, +}; +use anyhow::{Context as _, Result}; use assistant_tool::ActionLog; use buffer_diff::BufferDiff; use editor::{Bias, MultiBuffer, PathKey}; @@ -97,7 +102,7 @@ pub struct AssistantMessage { } impl AssistantMessage { - fn to_markdown(&self, cx: &App) -> String { + pub fn to_markdown(&self, cx: &App) -> String { format!( "## Assistant\n\n{}\n\n", self.chunks @@ -455,9 +460,8 @@ pub struct AcpThread { action_log: Entity<ActionLog>, shared_buffers: HashMap<Entity<Buffer>, BufferSnapshot>, send_task: Option<Task<()>>, - connection: Arc<acp::AgentConnection>, + connection: Arc<dyn AgentConnection>, child_status: Option<Task<Result<()>>>, - _io_task: Task<()>, } pub enum AcpThreadEvent { @@ -476,7 +480,11 @@ pub enum ThreadStatus { #[derive(Debug, Clone)] pub enum LoadError { - Unsupported { current_version: SharedString }, + Unsupported { + error_message: SharedString, + upgrade_message: SharedString, + upgrade_command: String, + }, Exited(i32), Other(SharedString), } @@ -484,13 +492,7 @@ pub enum LoadError { impl Display for LoadError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - LoadError::Unsupported { current_version } => { - write!( - f, - "Your installed version of Gemini {} doesn't support the Agentic Coding Protocol (ACP).", - current_version - ) - } + LoadError::Unsupported { error_message, .. } => write!(f, "{}", error_message), LoadError::Exited(status) => write!(f, "Server exited with status {}", status), LoadError::Other(msg) => write!(f, "{}", msg), } @@ -500,124 +502,48 @@ impl Display for LoadError { impl Error for LoadError {} impl AcpThread { - pub async fn spawn( - server: impl AgentServer + 'static, - root_dir: &Path, - project: Entity<Project>, - cx: &mut AsyncApp, - ) -> Result<Entity<Self>> { - let command = match server.command(&project, cx).await { - Ok(command) => command, - Err(e) => return Err(anyhow!(LoadError::Other(format!("{e}").into()))), - }; - - 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(); - - cx.new(|cx| { - let foreground_executor = cx.foreground_executor().clone(); - - let (connection, io_fut) = acp::AgentConnection::connect_to_agent( - AcpClientDelegate::new(cx.entity().downgrade(), cx.to_async()), - 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 { - match child.status().await { - Err(e) => Err(anyhow!(e)), - Ok(result) if result.success() => Ok(()), - Ok(result) => { - if let Some(version) = server.version(&command).await.log_err() - && !version.supported - { - Err(anyhow!(LoadError::Unsupported { - current_version: version.current_version - })) - } else { - Err(anyhow!(LoadError::Exited(result.code().unwrap_or(-127)))) - } - } - } - }); - - let action_log = cx.new(|_| ActionLog::new(project.clone())); - - Self { - action_log, - shared_buffers: Default::default(), - entries: Default::default(), - title: "ACP Thread".into(), - project, - send_task: None, - connection: Arc::new(connection), - child_status: Some(child_status), - _io_task: io_task, - } - }) - } - - pub fn action_log(&self) -> &Entity<ActionLog> { - &self.action_log - } - - pub fn project(&self) -> &Entity<Project> { - &self.project - } - - #[cfg(test)] - pub fn fake( - stdin: async_pipe::PipeWriter, - stdout: async_pipe::PipeReader, + pub fn new( + connection: impl AgentConnection + 'static, + title: SharedString, + child_status: Option<Task<Result<()>>>, project: Entity<Project>, cx: &mut Context<Self>, ) -> Self { - let foreground_executor = cx.foreground_executor().clone(); - - let (connection, io_fut) = acp::AgentConnection::connect_to_agent( - AcpClientDelegate::new(cx.entity().downgrade(), cx.to_async()), - stdin, - stdout, - move |fut| { - foreground_executor.spawn(fut).detach(); - }, - ); - - let io_task = cx.background_spawn({ - async move { - io_fut.await.log_err(); - } - }); - let action_log = cx.new(|_| ActionLog::new(project.clone())); Self { action_log, shared_buffers: Default::default(), entries: Default::default(), - title: "ACP Thread".into(), + title, project, send_task: None, connection: Arc::new(connection), - child_status: None, - _io_task: io_task, + child_status, + } + } + + /// Send a request to the agent and wait for a response. + pub fn request<R: AgentRequest + 'static>( + &self, + params: R, + ) -> impl use<R> + Future<Output = Result<R::Response>> { + let params = params.into_any(); + let result = self.connection.request_any(params); + async move { + let result = result.await?; + Ok(R::response_from_any(result)?) } } + pub fn action_log(&self) -> &Entity<ActionLog> { + &self.action_log + } + + pub fn project(&self) -> &Entity<Project> { + &self.project + } + pub fn title(&self) -> SharedString { self.title.clone() } @@ -711,7 +637,7 @@ impl AcpThread { } } - pub fn request_tool_call( + pub fn request_new_tool_call( &mut self, tool_call: acp::RequestToolCallConfirmationParams, cx: &mut Context<Self>, @@ -731,6 +657,30 @@ impl AcpThread { ToolCallRequest { id, outcome: rx } } + pub fn request_tool_call_confirmation( + &mut self, + tool_call_id: ToolCallId, + confirmation: acp::ToolCallConfirmation, + cx: &mut Context<Self>, + ) -> Result<ToolCallRequest> { + let project = self.project.read(cx).languages().clone(); + let Some((_, call)) = self.tool_call_mut(tool_call_id) else { + anyhow::bail!("Tool call not found"); + }; + + let (tx, rx) = oneshot::channel(); + + call.status = ToolCallStatus::WaitingForConfirmation { + confirmation: ToolCallConfirmation::from_acp(confirmation, project, cx), + respond_tx: tx, + }; + + Ok(ToolCallRequest { + id: tool_call_id, + outcome: rx, + }) + } + pub fn push_tool_call( &mut self, request: acp::PushToolCallParams, @@ -912,19 +862,17 @@ impl AcpThread { false } - pub fn initialize( - &self, - ) -> impl use<> + Future<Output = Result<acp::InitializeResponse, acp::Error>> { - let connection = self.connection.clone(); - async move { connection.initialize().await } + pub fn initialize(&self) -> impl use<> + Future<Output = Result<acp::InitializeResponse>> { + self.request(acp::InitializeParams { + protocol_version: ProtocolVersion::latest(), + }) } - pub fn authenticate(&self) -> impl use<> + Future<Output = Result<(), acp::Error>> { - let connection = self.connection.clone(); - async move { connection.request(acp::AuthenticateParams).await } + pub fn authenticate(&self) -> impl use<> + Future<Output = Result<()>> { + self.request(acp::AuthenticateParams) } - #[cfg(test)] + #[cfg(any(test, feature = "test-support"))] pub fn send_raw( &mut self, message: &str, @@ -945,7 +893,6 @@ impl AcpThread { message: acp::SendUserMessageParams, cx: &mut Context<Self>, ) -> BoxFuture<'static, Result<(), acp::Error>> { - let agent = self.connection.clone(); self.push_entry( AgentThreadEntry::UserMessage(UserMessage::from_acp( &message, @@ -959,11 +906,16 @@ impl AcpThread { let cancel = self.cancel(cx); self.send_task = Some(cx.spawn(async move |this, cx| { - cancel.await.log_err(); + async { + cancel.await.log_err(); - let result = agent.request(message).await; - tx.send(result).log_err(); - this.update(cx, |this, _cx| this.send_task.take()).log_err(); + let result = this.update(cx, |this, _| this.request(message))?.await; + tx.send(result).log_err(); + this.update(cx, |this, _cx| this.send_task.take())?; + anyhow::Ok(()) + } + .await + .log_err(); })); async move { @@ -976,12 +928,10 @@ impl AcpThread { } pub fn cancel(&mut self, cx: &mut Context<Self>) -> Task<Result<(), acp::Error>> { - let agent = self.connection.clone(); - if self.send_task.take().is_some() { + let request = self.request(acp::CancelSendMessageParams); cx.spawn(async move |this, cx| { - agent.request(acp::CancelSendMessageParams).await?; - + request.await?; this.update(cx, |this, _cx| { for entry in this.entries.iter_mut() { if let AgentThreadEntry::ToolCall(call) = entry { @@ -1019,6 +969,7 @@ impl AcpThread { pub fn read_text_file( &self, request: acp::ReadTextFileParams, + reuse_shared_snapshot: bool, cx: &mut Context<Self>, ) -> Task<Result<String>> { let project = self.project.clone(); @@ -1032,28 +983,60 @@ impl AcpThread { }); let buffer = load??.await?; - action_log.update(cx, |action_log, cx| { - action_log.buffer_read(buffer.clone(), cx); - })?; - project.update(cx, |project, cx| { - let position = buffer - .read(cx) - .snapshot() - .anchor_before(Point::new(request.line.unwrap_or_default(), 0)); - project.set_agent_location( - Some(AgentLocation { - buffer: buffer.downgrade(), - position, - }), - cx, - ); - })?; - let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot())?; + let snapshot = if reuse_shared_snapshot { + this.read_with(cx, |this, _| { + this.shared_buffers.get(&buffer.clone()).cloned() + }) + .log_err() + .flatten() + } else { + None + }; + + let snapshot = if let Some(snapshot) = snapshot { + snapshot + } else { + action_log.update(cx, |action_log, cx| { + action_log.buffer_read(buffer.clone(), cx); + })?; + project.update(cx, |project, cx| { + let position = buffer + .read(cx) + .snapshot() + .anchor_before(Point::new(request.line.unwrap_or_default(), 0)); + project.set_agent_location( + Some(AgentLocation { + buffer: buffer.downgrade(), + position, + }), + cx, + ); + })?; + + buffer.update(cx, |buffer, _| buffer.snapshot())? + }; + this.update(cx, |this, _| { let text = snapshot.text(); this.shared_buffers.insert(buffer.clone(), snapshot); - text - }) + if request.line.is_none() && request.limit.is_none() { + return Ok(text); + } + let limit = request.limit.unwrap_or(u32::MAX) as usize; + let Some(line) = request.line else { + return Ok(text.lines().take(limit).collect::<String>()); + }; + + let count = text.lines().count(); + if count < line as usize { + anyhow::bail!("There are only {} lines", count); + } + Ok(text + .lines() + .skip(line as usize + 1) + .take(limit) + .collect::<String>()) + })? }) } @@ -1134,16 +1117,49 @@ impl AcpThread { } } -struct AcpClientDelegate { +#[derive(Clone)] +pub struct AcpClientDelegate { thread: WeakEntity<AcpThread>, cx: AsyncApp, // sent_buffer_versions: HashMap<Entity<Buffer>, HashMap<u64, BufferSnapshot>>, } impl AcpClientDelegate { - fn new(thread: WeakEntity<AcpThread>, cx: AsyncApp) -> Self { + pub fn new(thread: WeakEntity<AcpThread>, cx: AsyncApp) -> Self { Self { thread, cx } } + + pub async fn request_existing_tool_call_confirmation( + &self, + tool_call_id: ToolCallId, + confirmation: acp::ToolCallConfirmation, + ) -> Result<ToolCallConfirmationOutcome> { + let cx = &mut self.cx.clone(); + let ToolCallRequest { outcome, .. } = cx + .update(|cx| { + self.thread.update(cx, |thread, cx| { + thread.request_tool_call_confirmation(tool_call_id, confirmation, cx) + }) + })? + .context("Failed to update thread")??; + + Ok(outcome.await?) + } + + pub async fn read_text_file_reusing_snapshot( + &self, + request: acp::ReadTextFileParams, + ) -> Result<acp::ReadTextFileResponse, acp::Error> { + let content = self + .cx + .update(|cx| { + self.thread + .update(cx, |thread, cx| thread.read_text_file(request, true, cx)) + })? + .context("Failed to update thread")? + .await?; + Ok(acp::ReadTextFileResponse { content }) + } } impl acp::Client for AcpClientDelegate { @@ -1172,7 +1188,7 @@ impl acp::Client for AcpClientDelegate { let ToolCallRequest { id, outcome } = cx .update(|cx| { self.thread - .update(cx, |thread, cx| thread.request_tool_call(request, cx)) + .update(cx, |thread, cx| thread.request_new_tool_call(request, cx)) })? .context("Failed to update thread")?; @@ -1218,7 +1234,7 @@ impl acp::Client for AcpClientDelegate { .cx .update(|cx| { self.thread - .update(cx, |thread, cx| thread.read_text_file(request, cx)) + .update(cx, |thread, cx| thread.read_text_file(request, false, cx)) })? .context("Failed to update thread")? .await?; @@ -1260,7 +1276,7 @@ pub struct ToolCallRequest { #[cfg(test)] mod tests { use super::*; - use agent_servers::{AgentServerCommand, AgentServerVersion}; + use anyhow::anyhow; use async_pipe::{PipeReader, PipeWriter}; use futures::{channel::mpsc, future::LocalBoxFuture, select}; use gpui::{AsyncApp, TestAppContext}; @@ -1269,7 +1285,7 @@ mod tests { use serde_json::json; use settings::SettingsStore; use smol::{future::BoxedLocal, stream::StreamExt as _}; - use std::{cell::RefCell, env, path::Path, rc::Rc, time::Duration}; + use std::{cell::RefCell, rc::Rc, time::Duration}; use util::path; fn init_test(cx: &mut TestAppContext) { @@ -1515,265 +1531,6 @@ mod tests { }); } - #[gpui::test] - #[cfg_attr(not(feature = "gemini"), ignore)] - async fn test_gemini_basic(cx: &mut TestAppContext) { - init_test(cx); - - cx.executor().allow_parking(); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], cx).await; - let thread = gemini_acp_thread(project.clone(), "/private/tmp", cx).await; - thread - .update(cx, |thread, cx| thread.send_raw("Hello from Zed!", cx)) - .await - .unwrap(); - - thread.read_with(cx, |thread, _| { - assert_eq!(thread.entries.len(), 2); - assert!(matches!( - thread.entries[0], - AgentThreadEntry::UserMessage(_) - )); - assert!(matches!( - thread.entries[1], - AgentThreadEntry::AssistantMessage(_) - )); - }); - } - - #[gpui::test] - #[cfg_attr(not(feature = "gemini"), ignore)] - async fn test_gemini_path_mentions(cx: &mut TestAppContext) { - init_test(cx); - - cx.executor().allow_parking(); - let tempdir = tempfile::tempdir().unwrap(); - std::fs::write( - tempdir.path().join("foo.rs"), - indoc! {" - fn main() { - println!(\"Hello, world!\"); - } - "}, - ) - .expect("failed to write file"); - let project = Project::example([tempdir.path()], &mut cx.to_async()).await; - let thread = gemini_acp_thread(project.clone(), tempdir.path(), cx).await; - thread - .update(cx, |thread, cx| { - thread.send( - acp::SendUserMessageParams { - chunks: vec![ - acp::UserMessageChunk::Text { - text: "Read the file ".into(), - }, - acp::UserMessageChunk::Path { - path: Path::new("foo.rs").into(), - }, - acp::UserMessageChunk::Text { - text: " and tell me what the content of the println! is".into(), - }, - ], - }, - cx, - ) - }) - .await - .unwrap(); - - thread.read_with(cx, |thread, cx| { - assert_eq!(thread.entries.len(), 3); - assert!(matches!( - thread.entries[0], - AgentThreadEntry::UserMessage(_) - )); - assert!(matches!(thread.entries[1], AgentThreadEntry::ToolCall(_))); - let AgentThreadEntry::AssistantMessage(assistant_message) = &thread.entries[2] else { - panic!("Expected AssistantMessage") - }; - assert!( - assistant_message.to_markdown(cx).contains("Hello, world!"), - "unexpected assistant message: {:?}", - assistant_message.to_markdown(cx) - ); - }); - } - - #[gpui::test] - #[cfg_attr(not(feature = "gemini"), ignore)] - async fn test_gemini_tool_call(cx: &mut TestAppContext) { - init_test(cx); - - cx.executor().allow_parking(); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/private/tmp"), - json!({"foo": "Lorem ipsum dolor", "bar": "bar", "baz": "baz"}), - ) - .await; - let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await; - let thread = gemini_acp_thread(project.clone(), "/private/tmp", cx).await; - thread - .update(cx, |thread, cx| { - thread.send_raw( - "Read the '/private/tmp/foo' file and tell me what you see.", - cx, - ) - }) - .await - .unwrap(); - thread.read_with(cx, |thread, _cx| { - assert!(matches!( - &thread.entries()[2], - AgentThreadEntry::ToolCall(ToolCall { - status: ToolCallStatus::Allowed { .. }, - .. - }) - )); - - assert!(matches!( - thread.entries[3], - AgentThreadEntry::AssistantMessage(_) - )); - }); - } - - #[gpui::test] - #[cfg_attr(not(feature = "gemini"), ignore)] - async fn test_gemini_tool_call_with_confirmation(cx: &mut TestAppContext) { - init_test(cx); - - cx.executor().allow_parking(); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await; - let thread = gemini_acp_thread(project.clone(), "/private/tmp", cx).await; - let full_turn = thread.update(cx, |thread, cx| { - thread.send_raw(r#"Run `echo "Hello, world!"`"#, cx) - }); - - run_until_first_tool_call(&thread, cx).await; - - let tool_call_id = thread.read_with(cx, |thread, _cx| { - let AgentThreadEntry::ToolCall(ToolCall { - id, - status: - ToolCallStatus::WaitingForConfirmation { - confirmation: ToolCallConfirmation::Execute { root_command, .. }, - .. - }, - .. - }) = &thread.entries()[2] - else { - panic!(); - }; - - assert_eq!(root_command, "echo"); - - *id - }); - - thread.update(cx, |thread, cx| { - thread.authorize_tool_call(tool_call_id, acp::ToolCallConfirmationOutcome::Allow, cx); - - assert!(matches!( - &thread.entries()[2], - AgentThreadEntry::ToolCall(ToolCall { - status: ToolCallStatus::Allowed { .. }, - .. - }) - )); - }); - - full_turn.await.unwrap(); - - thread.read_with(cx, |thread, cx| { - let AgentThreadEntry::ToolCall(ToolCall { - content: Some(ToolCallContent::Markdown { markdown }), - status: ToolCallStatus::Allowed { .. }, - .. - }) = &thread.entries()[2] - else { - panic!(); - }; - - markdown.read_with(cx, |md, _cx| { - assert!( - md.source().contains("Hello, world!"), - r#"Expected '{}' to contain "Hello, world!""#, - md.source() - ); - }); - }); - } - - #[gpui::test] - #[cfg_attr(not(feature = "gemini"), ignore)] - async fn test_gemini_cancel(cx: &mut TestAppContext) { - init_test(cx); - - cx.executor().allow_parking(); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await; - let thread = gemini_acp_thread(project.clone(), "/private/tmp", cx).await; - let full_turn = thread.update(cx, |thread, cx| { - thread.send_raw(r#"Run `echo "Hello, world!"`"#, cx) - }); - - let first_tool_call_ix = run_until_first_tool_call(&thread, cx).await; - - thread.read_with(cx, |thread, _cx| { - let AgentThreadEntry::ToolCall(ToolCall { - id, - status: - ToolCallStatus::WaitingForConfirmation { - confirmation: ToolCallConfirmation::Execute { root_command, .. }, - .. - }, - .. - }) = &thread.entries()[first_tool_call_ix] - else { - panic!("{:?}", thread.entries()[1]); - }; - - assert_eq!(root_command, "echo"); - - *id - }); - - thread - .update(cx, |thread, cx| thread.cancel(cx)) - .await - .unwrap(); - full_turn.await.unwrap(); - thread.read_with(cx, |thread, _| { - let AgentThreadEntry::ToolCall(ToolCall { - status: ToolCallStatus::Canceled, - .. - }) = &thread.entries()[first_tool_call_ix] - else { - panic!(); - }; - }); - - thread - .update(cx, |thread, cx| { - thread.send_raw(r#"Stop running and say goodbye to me."#, cx) - }) - .await - .unwrap(); - thread.read_with(cx, |thread, _| { - assert!(matches!( - &thread.entries().last().unwrap(), - AgentThreadEntry::AssistantMessage(..), - )) - }); - } - async fn run_until_first_tool_call( thread: &Entity<AcpThread>, cx: &mut TestAppContext, @@ -1801,66 +1558,39 @@ mod tests { } } - pub async fn gemini_acp_thread( - project: Entity<Project>, - current_dir: impl AsRef<Path>, - cx: &mut TestAppContext, - ) -> Entity<AcpThread> { - struct DevGemini; - - impl agent_servers::AgentServer for DevGemini { - async fn command( - &self, - _project: &Entity<Project>, - _cx: &mut AsyncApp, - ) -> Result<agent_servers::AgentServerCommand> { - let cli_path = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../../gemini-cli/packages/cli") - .to_string_lossy() - .to_string(); - - Ok(AgentServerCommand { - path: "node".into(), - args: vec![cli_path, "--experimental-acp".into()], - env: None, - }) - } - - async fn version( - &self, - _command: &agent_servers::AgentServerCommand, - ) -> Result<AgentServerVersion> { - Ok(AgentServerVersion { - current_version: "0.1.0".into(), - supported: true, - }) - } - } - - let thread = AcpThread::spawn(DevGemini, current_dir.as_ref(), project, &mut cx.to_async()) - .await - .unwrap(); - - thread - .update(cx, |thread, _| thread.initialize()) - .await - .unwrap(); - thread - } - pub fn fake_acp_thread( project: Entity<Project>, cx: &mut TestAppContext, ) -> (Entity<AcpThread>, Entity<FakeAcpServer>) { let (stdin_tx, stdin_rx) = async_pipe::pipe(); let (stdout_tx, stdout_rx) = async_pipe::pipe(); - let thread = cx.update(|cx| cx.new(|cx| AcpThread::fake(stdin_tx, stdout_rx, project, cx))); + + let thread = cx.new(|cx| { + let foreground_executor = cx.foreground_executor().clone(); + let (connection, io_fut) = acp::AgentConnection::connect_to_agent( + AcpClientDelegate::new(cx.entity().downgrade(), 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(()) + } + }); + AcpThread::new(connection, "Test".into(), Some(io_task), project, cx) + }); let agent = cx.update(|cx| cx.new(|cx| FakeAcpServer::new(stdin_rx, stdout_tx, cx))); (thread, agent) } pub struct FakeAcpServer { connection: acp::ClientConnection, + _io_task: Task<()>, on_user_message: Option< Rc< diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs new file mode 100644 index 0000000000000000000000000000000000000000..7c0ba4f41c70c019dc42acc457c59e795679556f --- /dev/null +++ b/crates/acp_thread/src/connection.rs @@ -0,0 +1,20 @@ +use agentic_coding_protocol as acp; +use anyhow::Result; +use futures::future::{FutureExt as _, LocalBoxFuture}; + +pub trait AgentConnection { + fn request_any( + &self, + params: acp::AnyAgentRequest, + ) -> LocalBoxFuture<'static, Result<acp::AnyAgentResult>>; +} + +impl AgentConnection for acp::AgentConnection { + fn request_any( + &self, + params: acp::AnyAgentRequest, + ) -> LocalBoxFuture<'static, Result<acp::AnyAgentResult>> { + let task = self.request_any(params); + async move { Ok(task.await?) }.boxed_local() + } +} diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml index 549162c5dd16feeb1959ece447d79faa7b7073e4..d65235aee38a71ffd17cfb9d799cbf62c4fecc8c 100644 --- a/crates/agent_servers/Cargo.toml +++ b/crates/agent_servers/Cargo.toml @@ -5,6 +5,10 @@ edition.workspace = true publish.workspace = true license = "GPL-3.0-or-later" +[features] +test-support = ["acp_thread/test-support", "gpui/test-support", "project/test-support"] +gemini = [] + [lints] workspace = true @@ -13,15 +17,32 @@ path = "src/agent_servers.rs" doctest = false [dependencies] +acp_thread.workspace = true +agentic-coding-protocol.workspace = true anyhow.workspace = true collections.workspace = true +context_server.workspace = true futures.workspace = true gpui.workspace = true +itertools.workspace = true +log.workspace = true paths.workspace = true project.workspace = true schemars.workspace = true serde.workspace = true +serde_json.workspace = true settings.workspace = true +smol.workspace = true +tempfile.workspace = true +ui.workspace = true util.workspace = true +watch.workspace = true which.workspace = true workspace-hack.workspace = true + +[dev-dependencies] +env_logger.workspace = true +language.workspace = true +indoc.workspace = true +acp_thread = { workspace = true, features = ["test-support"] } +gpui = { workspace = true, features = ["test-support"] } diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index ba43122570323f43398802583ed9d60c4adadf7f..ebebeca5111d95ace8d0671c837b5287bb1e11a4 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -1,30 +1,24 @@ -use std::{ - path::{Path, PathBuf}, - sync::Arc, -}; - -use anyhow::{Context as _, Result}; +mod claude; +mod gemini; +mod settings; +mod stdio_agent_server; + +pub use claude::*; +pub use gemini::*; +pub use settings::*; +pub use stdio_agent_server::*; + +use acp_thread::AcpThread; +use anyhow::Result; use collections::HashMap; -use gpui::{App, AsyncApp, Entity, SharedString}; +use gpui::{App, Entity, SharedString, Task}; use project::Project; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources, SettingsStore}; -use util::{ResultExt, paths}; +use std::path::{Path, PathBuf}; pub fn init(cx: &mut App) { - AllAgentServersSettings::register(cx); -} - -#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug)] -pub struct AllAgentServersSettings { - gemini: Option<AgentServerSettings>, -} - -#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug)] -pub struct AgentServerSettings { - #[serde(flatten)] - command: AgentServerCommand, + settings::init(cx); } #[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)] @@ -36,153 +30,28 @@ pub struct AgentServerCommand { pub env: Option<HashMap<String, String>>, } -pub struct Gemini; - -pub struct AgentServerVersion { - pub current_version: SharedString, - pub supported: bool, +pub enum AgentServerVersion { + Supported, + Unsupported { + error_message: SharedString, + upgrade_message: SharedString, + upgrade_command: String, + }, } pub trait AgentServer: Send { - fn command( - &self, - project: &Entity<Project>, - cx: &mut AsyncApp, - ) -> impl Future<Output = Result<AgentServerCommand>>; - - fn version( - &self, - command: &AgentServerCommand, - ) -> impl Future<Output = Result<AgentServerVersion>> + Send; -} - -const GEMINI_ACP_ARG: &str = "--experimental-acp"; + 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 supports_always_allow(&self) -> bool; -impl AgentServer for Gemini { - async fn command( + fn new_thread( &self, + root_dir: &Path, project: &Entity<Project>, - cx: &mut AsyncApp, - ) -> Result<AgentServerCommand> { - let custom_command = cx.read_global(|settings: &SettingsStore, _| { - let settings = settings.get::<AllAgentServersSettings>(None); - settings - .gemini - .as_ref() - .map(|gemini_settings| AgentServerCommand { - path: gemini_settings.command.path.clone(), - args: gemini_settings - .command - .args - .iter() - .cloned() - .chain(std::iter::once(GEMINI_ACP_ARG.into())) - .collect(), - env: gemini_settings.command.env.clone(), - }) - })?; - - if let Some(custom_command) = custom_command { - return Ok(custom_command); - } - - if let Some(path) = find_bin_in_path("gemini", project, cx).await { - return Ok(AgentServerCommand { - path, - args: vec![GEMINI_ACP_ARG.into()], - env: None, - }); - } - - 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![GEMINI_ACP_ARG.into()], - env: None, - }) - } - - async fn version(&self, command: &AgentServerCommand) -> Result<AgentServerVersion> { - 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)?.into(); - let supported = String::from_utf8(help_output?.stdout)?.contains(GEMINI_ACP_ARG); - - Ok(AgentServerVersion { - current_version, - supported, - }) - } -} - -async fn find_bin_in_path( - bin_name: &'static str, - project: &Entity<Project>, - cx: &mut AsyncApp, -) -> Option<PathBuf> { - let (env_task, root_dir) = project - .update(cx, |project, cx| { - let worktree = project.visible_worktrees(cx).next(); - match worktree { - Some(worktree) => { - let env_task = project.environment().update(cx, |env, cx| { - env.get_worktree_environment(worktree.clone(), cx) - }); - - let path = worktree.read(cx).abs_path(); - (env_task, path) - } - None => { - let path: Arc<Path> = paths::home_dir().as_path().into(); - let env_task = project.environment().update(cx, |env, cx| { - env.get_directory_environment(path.clone(), cx) - }); - (env_task, path) - } - } - }) - .log_err()?; - - cx.background_executor() - .spawn(async move { - let which_result = if cfg!(windows) { - which::which(bin_name) - } else { - let env = env_task.await.unwrap_or_default(); - let shell_path = env.get("PATH").cloned(); - which::which_in(bin_name, shell_path.as_ref(), root_dir.as_ref()) - }; - - if let Err(which::Error::CannotFindBinaryPath) = which_result { - return None; - } - - which_result.log_err() - }) - .await + cx: &mut App, + ) -> Task<Result<Entity<AcpThread>>>; } impl std::fmt::Debug for AgentServerCommand { @@ -209,23 +78,3 @@ impl std::fmt::Debug for AgentServerCommand { .finish() } } - -impl settings::Settings for AllAgentServersSettings { - const KEY: Option<&'static str> = Some("agent_servers"); - - type FileContent = Self; - - fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> { - let mut settings = AllAgentServersSettings::default(); - - for value in sources.defaults_and_customizations() { - if value.gemini.is_some() { - settings.gemini = value.gemini.clone(); - } - } - - Ok(settings) - } - - fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} -} diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs new file mode 100644 index 0000000000000000000000000000000000000000..897158dc5705c839c3b93a3f139956f479555c57 --- /dev/null +++ b/crates/agent_servers/src/claude.rs @@ -0,0 +1,680 @@ +mod mcp_server; +mod tools; + +use collections::HashMap; +use project::Project; +use std::cell::RefCell; +use std::fmt::Display; +use std::path::Path; +use std::rc::Rc; + +use agentic_coding_protocol::{ + self as acp, AnyAgentRequest, AnyAgentResult, Client, ProtocolVersion, + StreamAssistantMessageChunkParams, ToolCallContent, UpdateToolCallParams, +}; +use anyhow::{Context as _, Result, anyhow}; +use futures::channel::oneshot; +use futures::future::LocalBoxFuture; +use futures::{AsyncBufReadExt, AsyncWriteExt}; +use futures::{ + AsyncRead, AsyncWrite, FutureExt, StreamExt, + channel::mpsc::{self, UnboundedReceiver, UnboundedSender}, + io::BufReader, + select_biased, +}; +use gpui::{App, AppContext, Entity, Task}; +use serde::{Deserialize, Serialize}; +use util::ResultExt; + +use crate::claude::mcp_server::ClaudeMcpServer; +use crate::claude::tools::ClaudeTool; +use crate::{AgentServer, find_bin_in_path}; +use acp_thread::{AcpClientDelegate, AcpThread, AgentConnection}; + +#[derive(Clone)] +pub struct ClaudeCode; + +impl AgentServer for ClaudeCode { + fn name(&self) -> &'static str { + "Claude Code" + } + + fn empty_state_headline(&self) -> &'static str { + self.name() + } + + fn empty_state_message(&self) -> &'static str { + "" + } + + fn logo(&self) -> ui::IconName { + ui::IconName::AiClaude + } + + fn supports_always_allow(&self) -> bool { + false + } + + fn new_thread( + &self, + root_dir: &Path, + project: &Entity<Project>, + cx: &mut App, + ) -> Task<Result<Entity<AcpThread>>> { + let project = project.clone(); + let root_dir = root_dir.to_path_buf(); + let title = self.name().into(); + cx.spawn(async move |cx| { + let (mut delegate_tx, delegate_rx) = watch::channel(None); + let tool_id_map = Rc::new(RefCell::new(HashMap::default())); + + let permission_mcp_server = + ClaudeMcpServer::new(delegate_rx, tool_id_map.clone(), cx).await?; + + let mut mcp_servers = HashMap::default(); + mcp_servers.insert( + mcp_server::SERVER_NAME.to_string(), + permission_mcp_server.server_config()?, + ); + let mcp_config = McpConfig { mcp_servers }; + + let mcp_config_file = tempfile::NamedTempFile::new()?; + let (mcp_config_file, mcp_config_path) = mcp_config_file.into_parts(); + + let mut mcp_config_file = smol::fs::File::from(mcp_config_file); + mcp_config_file + .write_all(serde_json::to_string(&mcp_config)?.as_bytes()) + .await?; + mcp_config_file.flush().await?; + + let command = find_bin_in_path("claude", &project, cx) + .await + .context("Failed to find claude binary")?; + + let mut child = util::command::new_smol_command(&command) + .args([ + "--input-format", + "stream-json", + "--output-format", + "stream-json", + "--print", + "--verbose", + "--mcp-config", + mcp_config_path.to_string_lossy().as_ref(), + "--permission-prompt-tool", + &format!( + "mcp__{}__{}", + mcp_server::SERVER_NAME, + mcp_server::PERMISSION_TOOL + ), + "--allowedTools", + "mcp__zed__Read,mcp__zed__Edit", + "--disallowedTools", + "Read,Edit", + ]) + .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 (incoming_message_tx, mut incoming_message_rx) = mpsc::unbounded(); + let (outgoing_tx, outgoing_rx) = mpsc::unbounded(); + + let io_task = + ClaudeAgentConnection::handle_io(outgoing_rx, incoming_message_tx, stdin, stdout); + cx.background_spawn(async move { + io_task.await.log_err(); + drop(mcp_config_path); + drop(child); + }) + .detach(); + + cx.new(|cx| { + let end_turn_tx = Rc::new(RefCell::new(None)); + let delegate = AcpClientDelegate::new(cx.entity().downgrade(), cx.to_async()); + delegate_tx.send(Some(delegate.clone())).log_err(); + + let handler_task = cx.foreground_executor().spawn({ + let end_turn_tx = end_turn_tx.clone(); + let tool_id_map = tool_id_map.clone(); + async move { + while let Some(message) = incoming_message_rx.next().await { + ClaudeAgentConnection::handle_message( + delegate.clone(), + message, + end_turn_tx.clone(), + tool_id_map.clone(), + ) + .await + } + } + }); + + let mut connection = ClaudeAgentConnection { + outgoing_tx, + end_turn_tx, + _handler_task: handler_task, + _mcp_server: None, + }; + + connection._mcp_server = Some(permission_mcp_server); + acp_thread::AcpThread::new(connection, title, None, project.clone(), cx) + }) + }) + } +} + +impl AgentConnection for ClaudeAgentConnection { + /// Send a request to the agent and wait for a response. + fn request_any( + &self, + params: AnyAgentRequest, + ) -> LocalBoxFuture<'static, Result<acp::AnyAgentResult>> { + let end_turn_tx = self.end_turn_tx.clone(); + let outgoing_tx = self.outgoing_tx.clone(); + async move { + match params { + // todo: consider sending an empty request so we get the init response? + AnyAgentRequest::InitializeParams(_) => Ok(AnyAgentResult::InitializeResponse( + acp::InitializeResponse { + is_authenticated: true, + protocol_version: ProtocolVersion::latest(), + }, + )), + AnyAgentRequest::AuthenticateParams(_) => { + Err(anyhow!("Authentication not supported")) + } + AnyAgentRequest::SendUserMessageParams(message) => { + let (tx, rx) = oneshot::channel(); + end_turn_tx.borrow_mut().replace(tx); + let mut content = String::new(); + for chunk in message.chunks { + match chunk { + agentic_coding_protocol::UserMessageChunk::Text { text } => { + content.push_str(&text) + } + agentic_coding_protocol::UserMessageChunk::Path { path } => { + content.push_str(&format!("@{path:?}")) + } + } + } + outgoing_tx.unbounded_send(SdkMessage::User { + message: Message { + role: Role::User, + content: Content::UntaggedText(content), + id: None, + model: None, + stop_reason: None, + stop_sequence: None, + usage: None, + }, + session_id: None, + })?; + rx.await??; + Ok(AnyAgentResult::SendUserMessageResponse( + acp::SendUserMessageResponse, + )) + } + AnyAgentRequest::CancelSendMessageParams(_) => Ok( + AnyAgentResult::CancelSendMessageResponse(acp::CancelSendMessageResponse), + ), + } + } + .boxed_local() + } +} + +struct ClaudeAgentConnection { + outgoing_tx: UnboundedSender<SdkMessage>, + end_turn_tx: Rc<RefCell<Option<oneshot::Sender<Result<()>>>>>, + _mcp_server: Option<ClaudeMcpServer>, + _handler_task: Task<()>, +} + +impl ClaudeAgentConnection { + async fn handle_message( + delegate: AcpClientDelegate, + message: SdkMessage, + end_turn_tx: Rc<RefCell<Option<oneshot::Sender<Result<()>>>>>, + tool_id_map: Rc<RefCell<HashMap<String, acp::ToolCallId>>>, + ) { + match message { + SdkMessage::Assistant { message, .. } | SdkMessage::User { message, .. } => { + for chunk in message.content.chunks() { + match chunk { + ContentChunk::Text { text } | ContentChunk::UntaggedText(text) => { + delegate + .stream_assistant_message_chunk(StreamAssistantMessageChunkParams { + chunk: acp::AssistantMessageChunk::Text { text }, + }) + .await + .log_err(); + } + ContentChunk::ToolUse { id, name, input } => { + if let Some(resp) = delegate + .push_tool_call(ClaudeTool::infer(&name, input).as_acp()) + .await + .log_err() + { + tool_id_map.borrow_mut().insert(id, resp.id); + } + } + ContentChunk::ToolResult { + content, + tool_use_id, + } => { + let id = tool_id_map.borrow_mut().remove(&tool_use_id); + if let Some(id) = id { + delegate + .update_tool_call(UpdateToolCallParams { + tool_call_id: id, + status: acp::ToolCallStatus::Finished, + content: Some(ToolCallContent::Markdown { + // For now we only include text content + markdown: content.to_string(), + }), + }) + .await + .log_err(); + } + } + ContentChunk::Image + | ContentChunk::Document + | ContentChunk::Thinking + | ContentChunk::RedactedThinking + | ContentChunk::WebSearchToolResult => { + delegate + .stream_assistant_message_chunk(StreamAssistantMessageChunkParams { + chunk: acp::AssistantMessageChunk::Text { + text: format!("Unsupported content: {:?}", chunk), + }, + }) + .await + .log_err(); + } + } + } + } + SdkMessage::Result { + is_error, subtype, .. + } => { + if let Some(end_turn_tx) = end_turn_tx.borrow_mut().take() { + if is_error { + end_turn_tx.send(Err(anyhow!("Error: {subtype}"))).ok(); + } else { + end_turn_tx.send(Ok(())).ok(); + } + } + } + SdkMessage::System { .. } => {} + } + } + + async fn handle_io( + mut outgoing_rx: UnboundedReceiver<SdkMessage>, + incoming_tx: UnboundedSender<SdkMessage>, + mut outgoing_bytes: impl Unpin + AsyncWrite, + incoming_bytes: impl Unpin + AsyncRead, + ) -> Result<()> { + let mut output_reader = BufReader::new(incoming_bytes); + let mut outgoing_line = Vec::new(); + let mut incoming_line = String::new(); + loop { + select_biased! { + message = outgoing_rx.next() => { + if let Some(message) = message { + outgoing_line.clear(); + serde_json::to_writer(&mut outgoing_line, &message)?; + log::trace!("send: {}", String::from_utf8_lossy(&outgoing_line)); + outgoing_line.push(b'\n'); + outgoing_bytes.write_all(&outgoing_line).await.ok(); + } else { + break; + } + } + bytes_read = output_reader.read_line(&mut incoming_line).fuse() => { + if bytes_read? == 0 { + break + } + log::trace!("recv: {}", &incoming_line); + match serde_json::from_str::<SdkMessage>(&incoming_line) { + Ok(message) => { + incoming_tx.unbounded_send(message).log_err(); + } + Err(error) => { + log::error!("failed to parse incoming message: {error}. Raw: {incoming_line}"); + } + } + incoming_line.clear(); + } + } + } + Ok(()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct Message { + role: Role, + content: Content, + #[serde(skip_serializing_if = "Option::is_none")] + id: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + model: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + stop_reason: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + stop_sequence: Option<String>, + #[serde(skip_serializing_if = "Option::is_none")] + usage: Option<Usage>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +enum Content { + UntaggedText(String), + Chunks(Vec<ContentChunk>), +} + +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(), + } + } +} + +impl Display for Content { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Content::UntaggedText(txt) => write!(f, "{}", txt), + Content::Chunks(chunks) => { + for chunk in chunks { + write!(f, "{}", chunk)?; + } + Ok(()) + } + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +enum ContentChunk { + Text { + text: String, + }, + ToolUse { + id: String, + name: String, + input: serde_json::Value, + }, + ToolResult { + content: Content, + tool_use_id: String, + }, + // TODO + Image, + Document, + Thinking, + RedactedThinking, + WebSearchToolResult, + #[serde(untagged)] + UntaggedText(String), +} + +impl Display for ContentChunk { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ContentChunk::Text { text } => write!(f, "{}", text), + 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) + } + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct Usage { + input_tokens: u32, + cache_creation_input_tokens: u32, + cache_read_input_tokens: u32, + output_tokens: u32, + service_tier: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +enum Role { + System, + Assistant, + User, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct MessageParam { + role: Role, + content: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +enum SdkMessage { + // An assistant message + Assistant { + message: Message, // from Anthropic SDK + #[serde(skip_serializing_if = "Option::is_none")] + session_id: Option<String>, + }, + + // A user message + User { + message: Message, // from Anthropic SDK + #[serde(skip_serializing_if = "Option::is_none")] + session_id: Option<String>, + }, + + // Emitted as the last message in a conversation + Result { + subtype: ResultErrorType, + duration_ms: f64, + duration_api_ms: f64, + is_error: bool, + num_turns: i32, + #[serde(skip_serializing_if = "Option::is_none")] + result: Option<String>, + session_id: String, + total_cost_usd: f64, + }, + // Emitted as the first message at the start of a conversation + System { + cwd: String, + session_id: String, + tools: Vec<String>, + model: String, + mcp_servers: Vec<McpServer>, + #[serde(rename = "apiKeySource")] + api_key_source: String, + #[serde(rename = "permissionMode")] + permission_mode: PermissionMode, + }, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +enum ResultErrorType { + Success, + ErrorMaxTurns, + ErrorDuringExecution, +} + +impl Display for ResultErrorType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ResultErrorType::Success => write!(f, "success"), + ResultErrorType::ErrorMaxTurns => write!(f, "error_max_turns"), + ResultErrorType::ErrorDuringExecution => write!(f, "error_during_execution"), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct McpServer { + name: String, + status: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +enum PermissionMode { + Default, + AcceptEdits, + BypassPermissions, + Plan, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct McpConfig { + mcp_servers: HashMap<String, McpServerConfig>, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct McpServerConfig { + command: String, + args: Vec<String>, + #[serde(skip_serializing_if = "Option::is_none")] + env: Option<HashMap<String, String>>, +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn test_deserialize_content_untagged_text() { + let json = json!("Hello, world!"); + let content: Content = serde_json::from_value(json).unwrap(); + match content { + Content::UntaggedText(text) => assert_eq!(text, "Hello, world!"), + _ => panic!("Expected UntaggedText variant"), + } + } + + #[test] + fn test_deserialize_content_chunks() { + let json = json!([ + { + "type": "text", + "text": "Hello" + }, + { + "type": "tool_use", + "id": "tool_123", + "name": "calculator", + "input": {"operation": "add", "a": 1, "b": 2} + } + ]); + let content: Content = serde_json::from_value(json).unwrap(); + match content { + Content::Chunks(chunks) => { + assert_eq!(chunks.len(), 2); + match &chunks[0] { + ContentChunk::Text { text } => assert_eq!(text, "Hello"), + _ => panic!("Expected Text chunk"), + } + match &chunks[1] { + ContentChunk::ToolUse { id, name, input } => { + assert_eq!(id, "tool_123"); + assert_eq!(name, "calculator"); + assert_eq!(input["operation"], "add"); + assert_eq!(input["a"], 1); + assert_eq!(input["b"], 2); + } + _ => panic!("Expected ToolUse chunk"), + } + } + _ => panic!("Expected Chunks variant"), + } + } + + #[test] + fn test_deserialize_tool_result_untagged_text() { + let json = json!({ + "type": "tool_result", + "content": "Result content", + "tool_use_id": "tool_456" + }); + let chunk: ContentChunk = serde_json::from_value(json).unwrap(); + match chunk { + ContentChunk::ToolResult { + content, + tool_use_id, + } => { + match content { + Content::UntaggedText(text) => assert_eq!(text, "Result content"), + _ => panic!("Expected UntaggedText content"), + } + assert_eq!(tool_use_id, "tool_456"); + } + _ => panic!("Expected ToolResult variant"), + } + } + + #[test] + fn test_deserialize_tool_result_chunks() { + let json = json!({ + "type": "tool_result", + "content": [ + { + "type": "text", + "text": "Processing complete" + }, + { + "type": "text", + "text": "Result: 42" + } + ], + "tool_use_id": "tool_789" + }); + let chunk: ContentChunk = serde_json::from_value(json).unwrap(); + match chunk { + ContentChunk::ToolResult { + content, + tool_use_id, + } => { + match content { + Content::Chunks(chunks) => { + assert_eq!(chunks.len(), 2); + match &chunks[0] { + ContentChunk::Text { text } => assert_eq!(text, "Processing complete"), + _ => panic!("Expected Text chunk"), + } + match &chunks[1] { + ContentChunk::Text { text } => assert_eq!(text, "Result: 42"), + _ => panic!("Expected Text chunk"), + } + } + _ => panic!("Expected Chunks content"), + } + assert_eq!(tool_use_id, "tool_789"); + } + _ => panic!("Expected ToolResult variant"), + } + } +} diff --git a/crates/agent_servers/src/claude/mcp_server.rs b/crates/agent_servers/src/claude/mcp_server.rs new file mode 100644 index 0000000000000000000000000000000000000000..fa61e671123b3ff186aa4db91de4408f4f0b4a9e --- /dev/null +++ b/crates/agent_servers/src/claude/mcp_server.rs @@ -0,0 +1,303 @@ +use std::{cell::RefCell, rc::Rc}; + +use acp_thread::AcpClientDelegate; +use agentic_coding_protocol::{self as acp, Client, ReadTextFileParams, WriteTextFileParams}; +use anyhow::{Context, Result}; +use collections::HashMap; +use context_server::{ + listener::McpServer, + types::{ + CallToolParams, CallToolResponse, Implementation, InitializeParams, InitializeResponse, + ListToolsResponse, ProtocolVersion, ServerCapabilities, Tool, ToolAnnotations, + ToolResponseContent, ToolsCapabilities, requests, + }, +}; +use gpui::{App, AsyncApp, Task}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use util::debug_panic; + +use crate::claude::{ + McpServerConfig, + tools::{ClaudeTool, EditToolParams, EditToolResponse, ReadToolParams, ReadToolResponse}, +}; + +pub struct ClaudeMcpServer { + server: McpServer, +} + +pub const SERVER_NAME: &str = "zed"; +pub const READ_TOOL: &str = "Read"; +pub const EDIT_TOOL: &str = "Edit"; +pub const PERMISSION_TOOL: &str = "Confirmation"; + +#[derive(Deserialize, JsonSchema, Debug)] +struct PermissionToolParams { + tool_name: String, + input: serde_json::Value, + tool_use_id: Option<String>, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct PermissionToolResponse { + behavior: PermissionToolBehavior, + updated_input: serde_json::Value, +} + +#[derive(Serialize)] +#[serde(rename_all = "snake_case")] +enum PermissionToolBehavior { + Allow, + Deny, +} + +impl ClaudeMcpServer { + pub async fn new( + delegate: watch::Receiver<Option<AcpClientDelegate>>, + tool_id_map: Rc<RefCell<HashMap<String, acp::ToolCallId>>>, + cx: &AsyncApp, + ) -> Result<Self> { + let mut mcp_server = McpServer::new(cx).await?; + mcp_server.handle_request::<requests::Initialize>(Self::handle_initialize); + mcp_server.handle_request::<requests::ListTools>(Self::handle_list_tools); + mcp_server.handle_request::<requests::CallTool>(move |request, cx| { + Self::handle_call_tool(request, delegate.clone(), tool_id_map.clone(), cx) + }); + + Ok(Self { server: mcp_server }) + } + + pub fn server_config(&self) -> Result<McpServerConfig> { + #[cfg(not(target_os = "windows"))] + let zed_path = util::get_shell_safe_zed_path()?; + #[cfg(target_os = "windows")] + let zed_path = std::env::current_exe() + .context("finding current executable path for use in mcp_server")? + .to_string_lossy() + .to_string(); + + Ok(McpServerConfig { + command: zed_path, + args: vec![ + "--nc".into(), + self.server.socket_path().display().to_string(), + ], + env: None, + }) + } + + fn handle_initialize(_: InitializeParams, cx: &App) -> Task<Result<InitializeResponse>> { + 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, + }) + }) + } + + fn handle_list_tools(_: (), cx: &App) -> Task<Result<ListToolsResponse>> { + cx.foreground_executor().spawn(async move { + Ok(ListToolsResponse { + tools: vec![ + Tool { + name: PERMISSION_TOOL.into(), + input_schema: schemars::schema_for!(PermissionToolParams).into(), + description: None, + annotations: None, + }, + Tool { + name: READ_TOOL.into(), + input_schema: schemars::schema_for!(ReadToolParams).into(), + description: Some("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.".to_string()), + annotations: Some(ToolAnnotations { + title: Some("Read file".to_string()), + read_only_hint: Some(true), + destructive_hint: Some(false), + open_world_hint: Some(false), + // if time passes the contents might change, but it's not going to do anything different + // true or false seem too strong, let's try a none. + idempotent_hint: None, + }), + }, + Tool { + name: EDIT_TOOL.into(), + input_schema: schemars::schema_for!(EditToolParams).into(), + description: Some("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.".to_string()), + annotations: Some(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), + }), + }, + ], + next_cursor: None, + meta: None, + }) + }) + } + + fn handle_call_tool( + request: CallToolParams, + mut delegate_watch: watch::Receiver<Option<AcpClientDelegate>>, + tool_id_map: Rc<RefCell<HashMap<String, acp::ToolCallId>>>, + cx: &App, + ) -> Task<Result<CallToolResponse>> { + cx.spawn(async move |cx| { + let Some(delegate) = delegate_watch.recv().await? else { + debug_panic!("Sent None delegate"); + anyhow::bail!("Server not available"); + }; + + if request.name.as_str() == PERMISSION_TOOL { + let input = + serde_json::from_value(request.arguments.context("Arguments required")?)?; + + let result = + Self::handle_permissions_tool_call(input, delegate, tool_id_map, cx).await?; + Ok(CallToolResponse { + content: vec![ToolResponseContent::Text { + text: serde_json::to_string(&result)?, + }], + is_error: None, + meta: None, + }) + } else if request.name.as_str() == READ_TOOL { + let input = + serde_json::from_value(request.arguments.context("Arguments required")?)?; + + let result = Self::handle_read_tool_call(input, delegate, cx).await?; + Ok(CallToolResponse { + content: vec![ToolResponseContent::Text { + text: serde_json::to_string(&result)?, + }], + is_error: None, + meta: None, + }) + } else if request.name.as_str() == EDIT_TOOL { + let input = + serde_json::from_value(request.arguments.context("Arguments required")?)?; + + let result = Self::handle_edit_tool_call(input, delegate, cx).await?; + Ok(CallToolResponse { + content: vec![ToolResponseContent::Text { + text: serde_json::to_string(&result)?, + }], + is_error: None, + meta: None, + }) + } else { + anyhow::bail!("Unsupported tool"); + } + }) + } + + fn handle_read_tool_call( + params: ReadToolParams, + delegate: AcpClientDelegate, + cx: &AsyncApp, + ) -> Task<Result<ReadToolResponse>> { + cx.foreground_executor().spawn(async move { + let response = delegate + .read_text_file(ReadTextFileParams { + path: params.abs_path, + line: params.offset, + limit: params.limit, + }) + .await?; + + Ok(ReadToolResponse { + content: response.content, + }) + }) + } + + fn handle_edit_tool_call( + params: EditToolParams, + delegate: AcpClientDelegate, + cx: &AsyncApp, + ) -> Task<Result<EditToolResponse>> { + cx.foreground_executor().spawn(async move { + let response = delegate + .read_text_file_reusing_snapshot(ReadTextFileParams { + path: params.abs_path.clone(), + line: None, + limit: None, + }) + .await?; + + let new_content = response.content.replace(¶ms.old_text, ¶ms.new_text); + if new_content == response.content { + return Err(anyhow::anyhow!("The old_text was not found in the content")); + } + + delegate + .write_text_file(WriteTextFileParams { + path: params.abs_path, + content: new_content, + }) + .await?; + + Ok(EditToolResponse) + }) + } + + fn handle_permissions_tool_call( + params: PermissionToolParams, + delegate: AcpClientDelegate, + tool_id_map: Rc<RefCell<HashMap<String, acp::ToolCallId>>>, + cx: &AsyncApp, + ) -> Task<Result<PermissionToolResponse>> { + cx.foreground_executor().spawn(async move { + let claude_tool = ClaudeTool::infer(¶ms.tool_name, params.input.clone()); + + let tool_call_id = match params.tool_use_id { + Some(tool_use_id) => tool_id_map + .borrow() + .get(&tool_use_id) + .cloned() + .context("Tool call ID not found")?, + + None => delegate.push_tool_call(claude_tool.as_acp()).await?.id, + }; + + let outcome = delegate + .request_existing_tool_call_confirmation( + tool_call_id, + claude_tool.confirmation(None), + ) + .await?; + + match outcome { + acp::ToolCallConfirmationOutcome::Allow + | acp::ToolCallConfirmationOutcome::AlwaysAllow + | acp::ToolCallConfirmationOutcome::AlwaysAllowMcpServer + | acp::ToolCallConfirmationOutcome::AlwaysAllowTool => Ok(PermissionToolResponse { + behavior: PermissionToolBehavior::Allow, + updated_input: params.input, + }), + acp::ToolCallConfirmationOutcome::Reject + | acp::ToolCallConfirmationOutcome::Cancel => Ok(PermissionToolResponse { + behavior: PermissionToolBehavior::Deny, + updated_input: params.input, + }), + } + }) + } +} diff --git a/crates/agent_servers/src/claude/tools.rs b/crates/agent_servers/src/claude/tools.rs new file mode 100644 index 0000000000000000000000000000000000000000..89d42c0daa229e60a9426b5d5101dd44dca06abc --- /dev/null +++ b/crates/agent_servers/src/claude/tools.rs @@ -0,0 +1,670 @@ +use std::path::PathBuf; + +use agentic_coding_protocol::{self as acp, PushToolCallParams, ToolCallLocation}; +use itertools::Itertools; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use util::ResultExt; + +pub enum ClaudeTool { + Task(Option<TaskToolParams>), + NotebookRead(Option<NotebookReadToolParams>), + NotebookEdit(Option<NotebookEditToolParams>), + Edit(Option<EditToolParams>), + MultiEdit(Option<MultiEditToolParams>), + ReadFile(Option<ReadToolParams>), + Write(Option<WriteToolParams>), + Ls(Option<LsToolParams>), + Glob(Option<GlobToolParams>), + Grep(Option<GrepToolParams>), + Terminal(Option<BashToolParams>), + WebFetch(Option<WebFetchToolParams>), + WebSearch(Option<WebSearchToolParams>), + TodoWrite(Option<TodoWriteToolParams>), + ExitPlanMode(Option<ExitPlanModeToolParams>), + Other { + name: String, + input: serde_json::Value, + }, +} + +impl ClaudeTool { + pub fn infer(tool_name: &str, input: serde_json::Value) -> Self { + match tool_name { + // 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()), + "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()), + "Glob" => Self::Glob(serde_json::from_value(input).log_err()), + "Grep" => Self::Grep(serde_json::from_value(input).log_err()), + "Bash" => Self::Terminal(serde_json::from_value(input).log_err()), + "WebFetch" => Self::WebFetch(serde_json::from_value(input).log_err()), + "WebSearch" => Self::WebSearch(serde_json::from_value(input).log_err()), + "TodoWrite" => Self::TodoWrite(serde_json::from_value(input).log_err()), + "exit_plan_mode" => Self::ExitPlanMode(serde_json::from_value(input).log_err()), + "Task" => Self::Task(serde_json::from_value(input).log_err()), + "NotebookRead" => Self::NotebookRead(serde_json::from_value(input).log_err()), + "NotebookEdit" => Self::NotebookEdit(serde_json::from_value(input).log_err()), + // Inferred from name + _ => { + let tool_name = tool_name.to_lowercase(); + + if tool_name.contains("edit") || tool_name.contains("write") { + Self::Edit(None) + } else if tool_name.contains("terminal") { + Self::Terminal(None) + } else { + Self::Other { + name: tool_name.to_string(), + input, + } + } + } + } + } + + pub fn label(&self) -> String { + match &self { + Self::Task(Some(params)) => params.description.clone(), + Self::Task(None) => "Task".into(), + Self::NotebookRead(Some(params)) => { + format!("Read Notebook {}", params.notebook_path.display()) + } + Self::NotebookRead(None) => "Read Notebook".into(), + Self::NotebookEdit(Some(params)) => { + format!("Edit Notebook {}", params.notebook_path.display()) + } + Self::NotebookEdit(None) => "Edit Notebook".into(), + Self::Terminal(Some(params)) => format!("`{}`", params.command), + Self::Terminal(None) => "Terminal".into(), + Self::ReadFile(_) => "Read File".into(), + Self::Ls(Some(params)) => { + format!("List Directory {}", params.path.display()) + } + Self::Ls(None) => "List Directory".into(), + Self::Edit(Some(params)) => { + format!("Edit {}", params.abs_path.display()) + } + Self::Edit(None) => "Edit".into(), + Self::MultiEdit(Some(params)) => { + format!("Multi Edit {}", params.file_path.display()) + } + Self::MultiEdit(None) => "Multi Edit".into(), + Self::Write(Some(params)) => { + format!("Write {}", params.file_path.display()) + } + Self::Write(None) => "Write".into(), + Self::Glob(Some(params)) => { + format!("Glob {params}") + } + Self::Glob(None) => "Glob".into(), + Self::Grep(Some(params)) => params.to_string(), + Self::Grep(None) => "Grep".into(), + Self::WebFetch(Some(params)) => format!("Fetch {}", params.url), + Self::WebFetch(None) => "Fetch".into(), + Self::WebSearch(Some(params)) => format!("Web Search: {}", params), + Self::WebSearch(None) => "Web Search".into(), + Self::TodoWrite(Some(params)) => format!( + "Update TODOs: {}", + params.todos.iter().map(|todo| &todo.content).join(", ") + ), + Self::TodoWrite(None) => "Update TODOs".into(), + Self::ExitPlanMode(_) => "Exit Plan Mode".into(), + Self::Other { name, .. } => name.clone(), + } + } + + pub fn content(&self) -> Option<acp::ToolCallContent> { + match &self { + ClaudeTool::Other { input, .. } => Some(acp::ToolCallContent::Markdown { + markdown: format!( + "```json\n{}```", + serde_json::to_string_pretty(&input).unwrap_or("{}".to_string()) + ), + }), + _ => None, + } + } + + pub fn icon(&self) -> acp::Icon { + match self { + Self::Task(_) => acp::Icon::Hammer, + Self::NotebookRead(_) => acp::Icon::FileSearch, + Self::NotebookEdit(_) => acp::Icon::Pencil, + Self::Edit(_) => acp::Icon::Pencil, + Self::MultiEdit(_) => acp::Icon::Pencil, + Self::Write(_) => acp::Icon::Pencil, + Self::ReadFile(_) => acp::Icon::FileSearch, + Self::Ls(_) => acp::Icon::Folder, + Self::Glob(_) => acp::Icon::FileSearch, + Self::Grep(_) => acp::Icon::Regex, + Self::Terminal(_) => acp::Icon::Terminal, + Self::WebSearch(_) => acp::Icon::Globe, + Self::WebFetch(_) => acp::Icon::Globe, + Self::TodoWrite(_) => acp::Icon::LightBulb, + Self::ExitPlanMode(_) => acp::Icon::Hammer, + Self::Other { .. } => acp::Icon::Hammer, + } + } + + pub fn confirmation(&self, description: Option<String>) -> acp::ToolCallConfirmation { + match &self { + Self::Edit(_) | Self::Write(_) | Self::NotebookEdit(_) | Self::MultiEdit(_) => { + acp::ToolCallConfirmation::Edit { description } + } + Self::WebFetch(params) => acp::ToolCallConfirmation::Fetch { + urls: params + .as_ref() + .map(|p| vec![p.url.clone()]) + .unwrap_or_default(), + description, + }, + Self::Terminal(Some(BashToolParams { + description, + command, + .. + })) => acp::ToolCallConfirmation::Execute { + command: command.clone(), + root_command: command.clone(), + description: description.clone(), + }, + Self::ExitPlanMode(Some(params)) => acp::ToolCallConfirmation::Other { + description: if let Some(description) = description { + format!("{description} {}", params.plan) + } else { + params.plan.clone() + }, + }, + Self::Task(Some(params)) => acp::ToolCallConfirmation::Other { + description: if let Some(description) = description { + format!("{description} {}", params.description) + } else { + params.description.clone() + }, + }, + Self::Ls(Some(LsToolParams { path, .. })) + | Self::ReadFile(Some(ReadToolParams { abs_path: path, .. })) => { + let path = path.display(); + acp::ToolCallConfirmation::Other { + description: if let Some(description) = description { + format!("{description} {path}") + } else { + path.to_string() + }, + } + } + Self::NotebookRead(Some(NotebookReadToolParams { notebook_path, .. })) => { + let path = notebook_path.display(); + acp::ToolCallConfirmation::Other { + description: if let Some(description) = description { + format!("{description} {path}") + } else { + path.to_string() + }, + } + } + Self::Glob(Some(params)) => acp::ToolCallConfirmation::Other { + description: if let Some(description) = description { + format!("{description} {params}") + } else { + params.to_string() + }, + }, + Self::Grep(Some(params)) => acp::ToolCallConfirmation::Other { + description: if let Some(description) = description { + format!("{description} {params}") + } else { + params.to_string() + }, + }, + Self::WebSearch(Some(params)) => acp::ToolCallConfirmation::Other { + description: if let Some(description) = description { + format!("{description} {params}") + } else { + params.to_string() + }, + }, + Self::TodoWrite(Some(params)) => { + let params = params.todos.iter().map(|todo| &todo.content).join(", "); + acp::ToolCallConfirmation::Other { + description: if let Some(description) = description { + format!("{description} {params}") + } else { + params + }, + } + } + Self::Terminal(None) + | Self::Task(None) + | Self::NotebookRead(None) + | Self::ExitPlanMode(None) + | Self::Ls(None) + | Self::Glob(None) + | Self::Grep(None) + | Self::ReadFile(None) + | Self::WebSearch(None) + | Self::TodoWrite(None) + | Self::Other { .. } => acp::ToolCallConfirmation::Other { + description: description.unwrap_or("".to_string()), + }, + } + } + + pub fn locations(&self) -> Vec<acp::ToolCallLocation> { + match &self { + Self::Edit(Some(EditToolParams { abs_path, .. })) => vec![ToolCallLocation { + path: abs_path.clone(), + line: None, + }], + Self::MultiEdit(Some(MultiEditToolParams { file_path, .. })) => { + vec![ToolCallLocation { + path: file_path.clone(), + line: None, + }] + } + Self::Write(Some(WriteToolParams { file_path, .. })) => vec![ToolCallLocation { + path: file_path.clone(), + line: None, + }], + Self::ReadFile(Some(ReadToolParams { + abs_path, offset, .. + })) => vec![ToolCallLocation { + path: abs_path.clone(), + line: *offset, + }], + Self::NotebookRead(Some(NotebookReadToolParams { notebook_path, .. })) => { + vec![ToolCallLocation { + path: notebook_path.clone(), + line: None, + }] + } + Self::NotebookEdit(Some(NotebookEditToolParams { notebook_path, .. })) => { + vec![ToolCallLocation { + path: notebook_path.clone(), + line: None, + }] + } + Self::Glob(Some(GlobToolParams { + path: Some(path), .. + })) => vec![ToolCallLocation { + path: path.clone(), + line: None, + }], + Self::Ls(Some(LsToolParams { path, .. })) => vec![ToolCallLocation { + path: path.clone(), + line: None, + }], + Self::Grep(Some(GrepToolParams { + path: Some(path), .. + })) => vec![ToolCallLocation { + path: PathBuf::from(path), + line: None, + }], + Self::Task(_) + | Self::NotebookRead(None) + | Self::NotebookEdit(None) + | Self::Edit(None) + | Self::MultiEdit(None) + | Self::Write(None) + | Self::ReadFile(None) + | Self::Ls(None) + | Self::Glob(_) + | Self::Grep(_) + | Self::Terminal(_) + | Self::WebFetch(_) + | Self::WebSearch(_) + | Self::TodoWrite(_) + | Self::ExitPlanMode(_) + | Self::Other { .. } => vec![], + } + } + + pub fn as_acp(&self) -> PushToolCallParams { + PushToolCallParams { + label: self.label(), + content: self.content(), + icon: self.icon(), + locations: self.locations(), + } + } +} + +#[derive(Deserialize, JsonSchema, Debug)] +pub struct EditToolParams { + /// The absolute path to the file to read. + pub abs_path: PathBuf, + /// The old text to replace (must be unique in the file) + pub old_text: String, + /// The new text. + pub new_text: String, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct EditToolResponse; + +#[derive(Deserialize, JsonSchema, Debug)] +pub struct ReadToolParams { + /// The absolute path to the file to read. + pub abs_path: PathBuf, + /// Which line to start reading from. Omit to start from the beginning. + #[serde(skip_serializing_if = "Option::is_none")] + pub offset: Option<u32>, + /// How many lines to read. Omit for the whole file. + #[serde(skip_serializing_if = "Option::is_none")] + pub limit: Option<u32>, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ReadToolResponse { + pub content: String, +} + +#[derive(Deserialize, JsonSchema, Debug)] +pub struct WriteToolParams { + /// Absolute path for new file + pub file_path: PathBuf, + /// File content + pub content: String, +} + +#[derive(Deserialize, JsonSchema, Debug)] +pub struct BashToolParams { + /// Shell command to execute + pub command: String, + /// 5-10 word description of what command does + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option<String>, + /// Timeout in ms (max 600000ms/10min, default 120000ms) + #[serde(skip_serializing_if = "Option::is_none")] + pub timeout: Option<u32>, +} + +#[derive(Deserialize, JsonSchema, Debug)] +pub struct GlobToolParams { + /// Glob pattern like **/*.js or src/**/*.ts + pub pattern: String, + /// Directory to search in (omit for current directory) + #[serde(skip_serializing_if = "Option::is_none")] + pub path: Option<PathBuf>, +} + +impl std::fmt::Display for GlobToolParams { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(path) = &self.path { + write!(f, "{}", path.display())?; + } + write!(f, "{}", self.pattern) + } +} + +#[derive(Deserialize, JsonSchema, Debug)] +pub struct LsToolParams { + /// Absolute path to directory + pub path: PathBuf, + /// Array of glob patterns to ignore + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub ignore: Vec<String>, +} + +#[derive(Deserialize, JsonSchema, Debug)] +pub struct GrepToolParams { + /// Regex pattern to search for + pub pattern: String, + /// File/directory to search (defaults to current directory) + #[serde(skip_serializing_if = "Option::is_none")] + pub path: Option<String>, + /// "content" (shows lines), "files_with_matches" (default), "count" + #[serde(skip_serializing_if = "Option::is_none")] + pub output_mode: Option<GrepOutputMode>, + /// Filter files with glob pattern like "*.js" + #[serde(skip_serializing_if = "Option::is_none")] + pub glob: Option<String>, + /// File type filter like "js", "py", "rust" + #[serde(rename = "type", skip_serializing_if = "Option::is_none")] + pub file_type: Option<String>, + /// Case insensitive search + #[serde(rename = "-i", default, skip_serializing_if = "is_false")] + pub case_insensitive: bool, + /// Show line numbers (content mode only) + #[serde(rename = "-n", default, skip_serializing_if = "is_false")] + pub line_numbers: bool, + /// Lines after match (content mode only) + #[serde(rename = "-A", skip_serializing_if = "Option::is_none")] + pub after_context: Option<u32>, + /// Lines before match (content mode only) + #[serde(rename = "-B", skip_serializing_if = "Option::is_none")] + pub before_context: Option<u32>, + /// Lines before and after match (content mode only) + #[serde(rename = "-C", skip_serializing_if = "Option::is_none")] + pub context: Option<u32>, + /// Enable multiline/cross-line matching + #[serde(default, skip_serializing_if = "is_false")] + pub multiline: bool, + /// Limit output to first N results + #[serde(skip_serializing_if = "Option::is_none")] + pub head_limit: Option<u32>, +} + +impl std::fmt::Display for GrepToolParams { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "grep")?; + + // Boolean flags + if self.case_insensitive { + write!(f, " -i")?; + } + if self.line_numbers { + write!(f, " -n")?; + } + + // Context options + if let Some(after) = self.after_context { + write!(f, " -A {}", after)?; + } + if let Some(before) = self.before_context { + write!(f, " -B {}", before)?; + } + if let Some(context) = self.context { + write!(f, " -C {}", context)?; + } + + // Output mode + if let Some(mode) = &self.output_mode { + match mode { + GrepOutputMode::FilesWithMatches => write!(f, " -l")?, + GrepOutputMode::Count => write!(f, " -c")?, + GrepOutputMode::Content => {} // Default mode + } + } + + // Head limit + if let Some(limit) = self.head_limit { + write!(f, " | head -{}", limit)?; + } + + // Glob pattern + if let Some(glob) = &self.glob { + write!(f, " --include=\"{}\"", glob)?; + } + + // File type + if let Some(file_type) = &self.file_type { + write!(f, " --type={}", file_type)?; + } + + // Multiline + if self.multiline { + write!(f, " -P")?; // Perl-compatible regex for multiline + } + + // Pattern (escaped if contains special characters) + write!(f, " \"{}\"", self.pattern)?; + + // Path + if let Some(path) = &self.path { + write!(f, " {}", path)?; + } + + Ok(()) + } +} + +#[derive(Deserialize, Serialize, JsonSchema, Debug)] +#[serde(rename_all = "snake_case")] +pub enum TodoPriority { + High, + Medium, + Low, +} + +#[derive(Deserialize, Serialize, JsonSchema, Debug)] +#[serde(rename_all = "snake_case")] +pub enum TodoStatus { + Pending, + InProgress, + Completed, +} + +#[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, +} + +#[derive(Deserialize, JsonSchema, Debug)] +pub struct TodoWriteToolParams { + pub todos: Vec<Todo>, +} + +#[derive(Deserialize, JsonSchema, Debug)] +pub struct ExitPlanModeToolParams { + /// Implementation plan in markdown format + pub plan: String, +} + +#[derive(Deserialize, JsonSchema, Debug)] +pub struct TaskToolParams { + /// Short 3-5 word description of task + pub description: String, + /// Detailed task for agent to perform + pub prompt: String, +} + +#[derive(Deserialize, JsonSchema, Debug)] +pub struct NotebookReadToolParams { + /// Absolute path to .ipynb file + pub notebook_path: PathBuf, + /// Specific cell ID to read + #[serde(skip_serializing_if = "Option::is_none")] + pub cell_id: Option<String>, +} + +#[derive(Deserialize, Serialize, JsonSchema, Debug)] +#[serde(rename_all = "snake_case")] +pub enum CellType { + Code, + Markdown, +} + +#[derive(Deserialize, Serialize, JsonSchema, Debug)] +#[serde(rename_all = "snake_case")] +pub enum EditMode { + Replace, + Insert, + Delete, +} + +#[derive(Deserialize, JsonSchema, Debug)] +pub struct NotebookEditToolParams { + /// Absolute path to .ipynb file + pub notebook_path: PathBuf, + /// New cell content + pub new_source: String, + /// Cell ID to edit + #[serde(skip_serializing_if = "Option::is_none")] + pub cell_id: Option<String>, + /// Type of cell (code or markdown) + #[serde(skip_serializing_if = "Option::is_none")] + pub cell_type: Option<CellType>, + /// Edit operation mode + #[serde(skip_serializing_if = "Option::is_none")] + pub edit_mode: Option<EditMode>, +} + +#[derive(Deserialize, Serialize, JsonSchema, Debug)] +pub struct MultiEditItem { + /// The text to search for and replace + pub old_string: String, + /// The replacement text + pub new_string: String, + /// Whether to replace all occurrences or just the first + #[serde(default, skip_serializing_if = "is_false")] + pub replace_all: bool, +} + +#[derive(Deserialize, JsonSchema, Debug)] +pub struct MultiEditToolParams { + /// Absolute path to file + pub file_path: PathBuf, + /// List of edits to apply + pub edits: Vec<MultiEditItem>, +} + +fn is_false(v: &bool) -> bool { + !*v +} + +#[derive(Deserialize, JsonSchema, Debug)] +#[serde(rename_all = "snake_case")] +pub enum GrepOutputMode { + Content, + FilesWithMatches, + Count, +} + +#[derive(Deserialize, JsonSchema, Debug)] +pub struct WebFetchToolParams { + /// Valid URL to fetch + #[serde(rename = "url")] + pub url: String, + /// What to extract from content + pub prompt: String, +} + +#[derive(Deserialize, JsonSchema, Debug)] +pub struct WebSearchToolParams { + /// Search query (min 2 chars) + pub query: String, + /// Only include these domains + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub allowed_domains: Vec<String>, + /// Exclude these domains + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub blocked_domains: Vec<String>, +} + +impl std::fmt::Display for WebSearchToolParams { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "\"{}\"", self.query)?; + + if !self.allowed_domains.is_empty() { + write!(f, " (allowed: {})", self.allowed_domains.join(", "))?; + } + + if !self.blocked_domains.is_empty() { + write!(f, " (blocked: {})", self.blocked_domains.join(", "))?; + } + + Ok(()) + } +} diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs new file mode 100644 index 0000000000000000000000000000000000000000..bf1d13429e51660c68770af66f2eb44e601c3330 --- /dev/null +++ b/crates/agent_servers/src/gemini.rs @@ -0,0 +1,501 @@ +use crate::stdio_agent_server::{StdioAgentServer, find_bin_in_path}; +use crate::{AgentServerCommand, AgentServerVersion}; +use anyhow::{Context as _, Result}; +use gpui::{AsyncApp, Entity}; +use project::Project; +use settings::SettingsStore; + +use crate::AllAgentServersSettings; + +#[derive(Clone)] +pub struct Gemini; + +const ACP_ARG: &str = "--experimental-acp"; + +impl StdioAgentServer for Gemini { + fn name(&self) -> &'static str { + "Gemini" + } + + fn empty_state_headline(&self) -> &'static str { + "Welcome to Gemini" + } + + fn empty_state_message(&self) -> &'static str { + "Ask questions, edit files, run commands.\nBe specific for the best results." + } + + fn supports_always_allow(&self) -> bool { + true + } + + fn logo(&self) -> ui::IconName { + ui::IconName::AiGemini + } + + async fn command( + &self, + project: &Entity<Project>, + cx: &mut AsyncApp, + ) -> Result<AgentServerCommand> { + let custom_command = cx.read_global(|settings: &SettingsStore, _| { + let settings = settings.get::<AllAgentServersSettings>(None); + settings + .gemini + .as_ref() + .map(|gemini_settings| AgentServerCommand { + path: gemini_settings.command.path.clone(), + args: gemini_settings + .command + .args + .iter() + .cloned() + .chain(std::iter::once(ACP_ARG.into())) + .collect(), + env: gemini_settings.command.env.clone(), + }) + })?; + + if let Some(custom_command) = custom_command { + return Ok(custom_command); + } + + if let Some(path) = find_bin_in_path("gemini", project, cx).await { + return Ok(AgentServerCommand { + path, + args: vec![ACP_ARG.into()], + env: None, + }); + } + + 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<AgentServerVersion> { + 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)] +mod test { + use std::{path::Path, time::Duration}; + + use acp_thread::{ + AcpThread, AgentThreadEntry, ToolCall, ToolCallConfirmation, ToolCallContent, + ToolCallStatus, + }; + use agentic_coding_protocol as acp; + use anyhow::Result; + use futures::{FutureExt, StreamExt, channel::mpsc, select}; + use gpui::{AsyncApp, Entity, TestAppContext}; + use indoc::indoc; + use project::{FakeFs, Project}; + use serde_json::json; + use settings::SettingsStore; + use util::path; + + use crate::{AgentServer, AgentServerCommand, AgentServerVersion, StdioAgentServer}; + + pub async fn gemini_acp_thread( + project: Entity<Project>, + current_dir: impl AsRef<Path>, + cx: &mut TestAppContext, + ) -> Entity<AcpThread> { + #[derive(Clone)] + struct DevGemini; + + impl StdioAgentServer for DevGemini { + async fn command( + &self, + _project: &Entity<Project>, + _cx: &mut AsyncApp, + ) -> Result<AgentServerCommand> { + let cli_path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../../gemini-cli/packages/cli") + .to_string_lossy() + .to_string(); + + Ok(AgentServerCommand { + path: "node".into(), + args: vec![cli_path, "--experimental-acp".into()], + env: None, + }) + } + + async fn version(&self, _command: &AgentServerCommand) -> Result<AgentServerVersion> { + Ok(AgentServerVersion::Supported) + } + + fn logo(&self) -> ui::IconName { + ui::IconName::AiGemini + } + + fn name(&self) -> &'static str { + "test" + } + + fn empty_state_headline(&self) -> &'static str { + "test" + } + + fn empty_state_message(&self) -> &'static str { + "test" + } + + fn supports_always_allow(&self) -> bool { + true + } + } + + let thread = cx + .update(|cx| AgentServer::new_thread(&DevGemini, current_dir.as_ref(), &project, cx)) + .await + .unwrap(); + + thread + .update(cx, |thread, _| thread.initialize()) + .await + .unwrap(); + thread + } + + 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); + }); + } + + #[gpui::test] + #[cfg_attr(not(feature = "gemini"), ignore)] + async fn test_gemini_basic(cx: &mut TestAppContext) { + init_test(cx); + + cx.executor().allow_parking(); + + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs, [], cx).await; + let thread = gemini_acp_thread(project.clone(), "/private/tmp", cx).await; + thread + .update(cx, |thread, cx| thread.send_raw("Hello from Zed!", cx)) + .await + .unwrap(); + + thread.read_with(cx, |thread, _| { + assert_eq!(thread.entries().len(), 2); + assert!(matches!( + thread.entries()[0], + AgentThreadEntry::UserMessage(_) + )); + assert!(matches!( + thread.entries()[1], + AgentThreadEntry::AssistantMessage(_) + )); + }); + } + + #[gpui::test] + #[cfg_attr(not(feature = "gemini"), ignore)] + async fn test_gemini_path_mentions(cx: &mut TestAppContext) { + init_test(cx); + + cx.executor().allow_parking(); + let tempdir = tempfile::tempdir().unwrap(); + std::fs::write( + tempdir.path().join("foo.rs"), + indoc! {" + fn main() { + println!(\"Hello, world!\"); + } + "}, + ) + .expect("failed to write file"); + let project = Project::example([tempdir.path()], &mut cx.to_async()).await; + let thread = gemini_acp_thread(project.clone(), tempdir.path(), cx).await; + thread + .update(cx, |thread, cx| { + thread.send( + acp::SendUserMessageParams { + chunks: vec![ + acp::UserMessageChunk::Text { + text: "Read the file ".into(), + }, + acp::UserMessageChunk::Path { + path: Path::new("foo.rs").into(), + }, + acp::UserMessageChunk::Text { + text: " and tell me what the content of the println! is".into(), + }, + ], + }, + cx, + ) + }) + .await + .unwrap(); + + thread.read_with(cx, |thread, cx| { + assert_eq!(thread.entries().len(), 3); + assert!(matches!( + thread.entries()[0], + AgentThreadEntry::UserMessage(_) + )); + assert!(matches!(thread.entries()[1], AgentThreadEntry::ToolCall(_))); + let AgentThreadEntry::AssistantMessage(assistant_message) = &thread.entries()[2] else { + panic!("Expected AssistantMessage") + }; + assert!( + assistant_message.to_markdown(cx).contains("Hello, world!"), + "unexpected assistant message: {:?}", + assistant_message.to_markdown(cx) + ); + }); + } + + #[gpui::test] + #[cfg_attr(not(feature = "gemini"), ignore)] + async fn test_gemini_tool_call(cx: &mut TestAppContext) { + init_test(cx); + + cx.executor().allow_parking(); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/private/tmp"), + json!({"foo": "Lorem ipsum dolor", "bar": "bar", "baz": "baz"}), + ) + .await; + let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await; + let thread = gemini_acp_thread(project.clone(), "/private/tmp", cx).await; + thread + .update(cx, |thread, cx| { + thread.send_raw( + "Read the '/private/tmp/foo' file and tell me what you see.", + cx, + ) + }) + .await + .unwrap(); + thread.read_with(cx, |thread, _cx| { + assert!(matches!( + &thread.entries()[2], + AgentThreadEntry::ToolCall(ToolCall { + status: ToolCallStatus::Allowed { .. }, + .. + }) + )); + + assert!(matches!( + thread.entries()[3], + AgentThreadEntry::AssistantMessage(_) + )); + }); + } + + #[gpui::test] + #[cfg_attr(not(feature = "gemini"), ignore)] + async fn test_gemini_tool_call_with_confirmation(cx: &mut TestAppContext) { + init_test(cx); + + cx.executor().allow_parking(); + + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await; + let thread = gemini_acp_thread(project.clone(), "/private/tmp", cx).await; + let full_turn = thread.update(cx, |thread, cx| { + thread.send_raw(r#"Run `echo "Hello, world!"`"#, cx) + }); + + run_until_first_tool_call(&thread, cx).await; + + let tool_call_id = thread.read_with(cx, |thread, _cx| { + let AgentThreadEntry::ToolCall(ToolCall { + id, + status: + ToolCallStatus::WaitingForConfirmation { + confirmation: ToolCallConfirmation::Execute { root_command, .. }, + .. + }, + .. + }) = &thread.entries()[2] + else { + panic!(); + }; + + assert_eq!(root_command, "echo"); + + *id + }); + + thread.update(cx, |thread, cx| { + thread.authorize_tool_call(tool_call_id, acp::ToolCallConfirmationOutcome::Allow, cx); + + assert!(matches!( + &thread.entries()[2], + AgentThreadEntry::ToolCall(ToolCall { + status: ToolCallStatus::Allowed { .. }, + .. + }) + )); + }); + + full_turn.await.unwrap(); + + thread.read_with(cx, |thread, cx| { + let AgentThreadEntry::ToolCall(ToolCall { + content: Some(ToolCallContent::Markdown { markdown }), + status: ToolCallStatus::Allowed { .. }, + .. + }) = &thread.entries()[2] + else { + panic!(); + }; + + markdown.read_with(cx, |md, _cx| { + assert!( + md.source().contains("Hello, world!"), + r#"Expected '{}' to contain "Hello, world!""#, + md.source() + ); + }); + }); + } + + #[gpui::test] + #[cfg_attr(not(feature = "gemini"), ignore)] + async fn test_gemini_cancel(cx: &mut TestAppContext) { + init_test(cx); + + cx.executor().allow_parking(); + + let fs = FakeFs::new(cx.executor()); + let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await; + let thread = gemini_acp_thread(project.clone(), "/private/tmp", cx).await; + let full_turn = thread.update(cx, |thread, cx| { + thread.send_raw(r#"Run `echo "Hello, world!"`"#, cx) + }); + + let first_tool_call_ix = run_until_first_tool_call(&thread, cx).await; + + thread.read_with(cx, |thread, _cx| { + let AgentThreadEntry::ToolCall(ToolCall { + id, + status: + ToolCallStatus::WaitingForConfirmation { + confirmation: ToolCallConfirmation::Execute { root_command, .. }, + .. + }, + .. + }) = &thread.entries()[first_tool_call_ix] + else { + panic!("{:?}", thread.entries()[1]); + }; + + assert_eq!(root_command, "echo"); + + *id + }); + + thread + .update(cx, |thread, cx| thread.cancel(cx)) + .await + .unwrap(); + full_turn.await.unwrap(); + thread.read_with(cx, |thread, _| { + let AgentThreadEntry::ToolCall(ToolCall { + status: ToolCallStatus::Canceled, + .. + }) = &thread.entries()[first_tool_call_ix] + else { + panic!(); + }; + }); + + thread + .update(cx, |thread, cx| { + thread.send_raw(r#"Stop running and say goodbye to me."#, cx) + }) + .await + .unwrap(); + thread.read_with(cx, |thread, _| { + assert!(matches!( + &thread.entries().last().unwrap(), + AgentThreadEntry::AssistantMessage(..), + )) + }); + } + + async fn run_until_first_tool_call( + thread: &Entity<AcpThread>, + cx: &mut TestAppContext, + ) -> usize { + let (mut tx, mut rx) = mpsc::channel::<usize>(1); + + let subscription = cx.update(|cx| { + cx.subscribe(thread, move |thread, _, cx| { + for (ix, entry) in thread.read(cx).entries().iter().enumerate() { + if matches!(entry, AgentThreadEntry::ToolCall(_)) { + return tx.try_send(ix).unwrap(); + } + } + }) + }); + + select! { + _ = cx.executor().timer(Duration::from_secs(10)).fuse() => { + panic!("Timeout waiting for tool call") + } + ix = rx.next().fuse() => { + drop(subscription); + ix.unwrap() + } + } + } +} diff --git a/crates/agent_servers/src/settings.rs b/crates/agent_servers/src/settings.rs new file mode 100644 index 0000000000000000000000000000000000000000..8e6914352b58033814be3be7b622f0c609c9d50f --- /dev/null +++ b/crates/agent_servers/src/settings.rs @@ -0,0 +1,41 @@ +use crate::AgentServerCommand; +use anyhow::Result; +use gpui::App; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::{Settings, SettingsSources}; + +pub fn init(cx: &mut App) { + AllAgentServersSettings::register(cx); +} + +#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug)] +pub struct AllAgentServersSettings { + pub gemini: Option<AgentServerSettings>, +} + +#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug)] +pub struct AgentServerSettings { + #[serde(flatten)] + pub command: AgentServerCommand, +} + +impl settings::Settings for AllAgentServersSettings { + const KEY: Option<&'static str> = Some("agent_servers"); + + type FileContent = Self; + + fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> { + let mut settings = AllAgentServersSettings::default(); + + for value in sources.defaults_and_customizations() { + if value.gemini.is_some() { + settings.gemini = value.gemini.clone(); + } + } + + Ok(settings) + } + + fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {} +} diff --git a/crates/agent_servers/src/stdio_agent_server.rs b/crates/agent_servers/src/stdio_agent_server.rs new file mode 100644 index 0000000000000000000000000000000000000000..d78506022dc8e3c2a853ebb2b9966bd8a228e05a --- /dev/null +++ b/crates/agent_servers/src/stdio_agent_server.rs @@ -0,0 +1,169 @@ +use crate::{AgentServer, AgentServerCommand, AgentServerVersion}; +use acp_thread::{AcpClientDelegate, AcpThread, LoadError}; +use agentic_coding_protocol as acp; +use anyhow::{Result, anyhow}; +use gpui::{App, AsyncApp, Entity, Task, prelude::*}; +use project::Project; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; +use util::{ResultExt, paths}; + +pub trait StdioAgentServer: Send + Clone { + 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 supports_always_allow(&self) -> bool; + + fn command( + &self, + project: &Entity<Project>, + cx: &mut AsyncApp, + ) -> impl Future<Output = Result<AgentServerCommand>>; + + fn version( + &self, + command: &AgentServerCommand, + ) -> impl Future<Output = Result<AgentServerVersion>> + Send; +} + +impl<T: StdioAgentServer + 'static> AgentServer for T { + fn name(&self) -> &'static str { + self.name() + } + + fn empty_state_headline(&self) -> &'static str { + self.empty_state_headline() + } + + fn empty_state_message(&self) -> &'static str { + self.empty_state_message() + } + + fn logo(&self) -> ui::IconName { + self.logo() + } + + fn supports_always_allow(&self) -> bool { + self.supports_always_allow() + } + + fn new_thread( + &self, + root_dir: &Path, + project: &Entity<Project>, + cx: &mut App, + ) -> Task<Result<Entity<AcpThread>>> { + let root_dir = root_dir.to_path_buf(); + let project = project.clone(); + let this = self.clone(); + let title = self.name().into(); + + cx.spawn(async move |cx| { + let command = this.command(&project, cx).await?; + + 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(); + + cx.new(|cx| { + let foreground_executor = cx.foreground_executor().clone(); + + let (connection, io_fut) = acp::AgentConnection::connect_to_agent( + AcpClientDelegate::new(cx.entity().downgrade(), cx.to_async()), + 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 + }); + + AcpThread::new(connection, title, Some(child_status), project.clone(), cx) + }) + }) + } +} + +pub async fn find_bin_in_path( + bin_name: &'static str, + project: &Entity<Project>, + cx: &mut AsyncApp, +) -> Option<PathBuf> { + let (env_task, root_dir) = project + .update(cx, |project, cx| { + let worktree = project.visible_worktrees(cx).next(); + match worktree { + Some(worktree) => { + let env_task = project.environment().update(cx, |env, cx| { + env.get_worktree_environment(worktree.clone(), cx) + }); + + let path = worktree.read(cx).abs_path(); + (env_task, path) + } + None => { + let path: Arc<Path> = paths::home_dir().as_path().into(); + let env_task = project.environment().update(cx, |env, cx| { + env.get_directory_environment(path.clone(), cx) + }); + (env_task, path) + } + } + }) + .log_err()?; + + cx.background_executor() + .spawn(async move { + let which_result = if cfg!(windows) { + which::which(bin_name) + } else { + let env = env_task.await.unwrap_or_default(); + let shell_path = env.get("PATH").cloned(); + which::which_in(bin_name, shell_path.as_ref(), root_dir.as_ref()) + }; + + if let Err(which::Error::CannotFindBinaryPath) = which_result { + return None; + } + + which_result.log_err() + }) + .await +} diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index 72466fe8e7a05f52ac69d79e64a1af3452df089f..d4feceb0b67628052887e805d9b04eeb11c40040 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -16,7 +16,7 @@ doctest = false test-support = ["gpui/test-support", "language/test-support"] [dependencies] -acp.workspace = true +acp_thread.workspace = true agent.workspace = true agentic-coding-protocol.workspace = true agent_settings.workspace = true diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 7ab395815f734d8f5d0b20eb5b49419361b4627d..765f4fe6c0f270ee88b17d140169095a34dc3a3c 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1,3 +1,4 @@ +use agent_servers::AgentServer; use std::cell::RefCell; use std::collections::BTreeMap; use std::path::Path; @@ -35,7 +36,7 @@ use util::ResultExt; use workspace::{CollaboratorId, Workspace}; use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage}; -use ::acp::{ +use ::acp_thread::{ AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk, Diff, LoadError, MentionPath, ThreadStatus, ToolCall, ToolCallConfirmation, ToolCallContent, ToolCallId, ToolCallStatus, @@ -49,6 +50,7 @@ use crate::{AgentDiffPane, Follow, KeepAll, OpenAgentDiff, RejectAll}; const RESPONSE_PADDING_X: Pixels = px(19.); pub struct AcpThreadView { + agent: Rc<dyn AgentServer>, workspace: WeakEntity<Workspace>, project: Entity<Project>, thread_state: ThreadState, @@ -80,8 +82,15 @@ enum ThreadState { }, } +struct AlwaysAllowOption { + id: &'static str, + label: SharedString, + outcome: acp::ToolCallConfirmationOutcome, +} + impl AcpThreadView { pub fn new( + agent: Rc<dyn AgentServer>, workspace: WeakEntity<Workspace>, project: Entity<Project>, message_history: Rc<RefCell<MessageHistory<acp::SendUserMessageParams>>>, @@ -158,9 +167,10 @@ impl AcpThreadView { ); Self { + agent: agent.clone(), workspace: workspace.clone(), project: project.clone(), - thread_state: Self::initial_state(workspace, project, window, cx), + thread_state: Self::initial_state(agent, workspace, project, window, cx), message_editor, message_set_from_history: false, _message_editor_subscription: message_editor_subscription, @@ -177,6 +187,7 @@ impl AcpThreadView { } fn initial_state( + agent: Rc<dyn AgentServer>, workspace: WeakEntity<Workspace>, project: Entity<Project>, window: &mut Window, @@ -189,9 +200,9 @@ impl AcpThreadView { .map(|worktree| worktree.read(cx).abs_path()) .unwrap_or_else(|| paths::home_dir().as_path().into()); + let task = agent.new_thread(&root_dir, &project, cx); let load_task = cx.spawn_in(window, async move |this, cx| { - let thread = match AcpThread::spawn(agent_servers::Gemini, &root_dir, project, cx).await - { + let thread = match task.await { Ok(thread) => thread, Err(err) => { this.update(cx, |this, cx| { @@ -410,6 +421,33 @@ impl AcpThreadView { ); } + fn open_agent_diff(&mut self, _: &OpenAgentDiff, window: &mut Window, cx: &mut Context<Self>) { + if let Some(thread) = self.thread() { + AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err(); + } + } + + fn open_edited_buffer( + &mut self, + buffer: &Entity<Buffer>, + window: &mut Window, + cx: &mut Context<Self>, + ) { + let Some(thread) = self.thread() else { + return; + }; + + let Some(diff) = + AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err() + else { + return; + }; + + diff.update(cx, |diff, cx| { + diff.move_to_path(PathKey::for_buffer(&buffer, cx), window, cx) + }) + } + fn set_draft_message( message_editor: Entity<Editor>, mention_set: Arc<Mutex<MentionSet>>, @@ -485,33 +523,6 @@ impl AcpThreadView { true } - fn open_agent_diff(&mut self, _: &OpenAgentDiff, window: &mut Window, cx: &mut Context<Self>) { - if let Some(thread) = self.thread() { - AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err(); - } - } - - fn open_edited_buffer( - &mut self, - buffer: &Entity<Buffer>, - window: &mut Window, - cx: &mut Context<Self>, - ) { - let Some(thread) = self.thread() else { - return; - }; - - let Some(diff) = - AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err() - else { - return; - }; - - diff.update(cx, |diff, cx| { - diff.move_to_path(PathKey::for_buffer(&buffer, cx), window, cx) - }) - } - fn handle_thread_event( &mut self, thread: &Entity<AcpThread>, @@ -608,6 +619,7 @@ impl AcpThreadView { let authenticate = thread.read(cx).authenticate(); 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; @@ -617,8 +629,13 @@ impl AcpThreadView { Markdown::new(format!("Error: {err}").into(), None, None, cx) })) } else { - this.thread_state = - Self::initial_state(this.workspace.clone(), project.clone(), window, cx) + this.thread_state = Self::initial_state( + agent, + this.workspace.clone(), + project.clone(), + window, + cx, + ) } this.auth_task.take() }) @@ -1047,14 +1064,6 @@ impl AcpThreadView { ) -> AnyElement { let confirmation_container = v_flex().mt_1().py_1p5(); - let button_container = h_flex() - .pt_1p5() - .px_1p5() - .gap_1() - .justify_end() - .border_t_1() - .border_color(self.tool_card_border_color(cx)); - match confirmation { ToolCallConfirmation::Edit { description } => confirmation_container .child( @@ -1068,60 +1077,15 @@ impl AcpThreadView { })), ) .children(content.map(|content| self.render_tool_call_content(content, window, cx))) - .child( - button_container - .child( - Button::new(("always_allow", tool_call_id.0), "Always Allow Edits") - .icon(IconName::CheckDouble) - .icon_position(IconPosition::Start) - .icon_size(IconSize::XSmall) - .icon_color(Color::Success) - .on_click(cx.listener({ - let id = tool_call_id; - move |this, _, _, cx| { - this.authorize_tool_call( - id, - acp::ToolCallConfirmationOutcome::AlwaysAllow, - cx, - ); - } - })), - ) - .child( - Button::new(("allow", tool_call_id.0), "Allow") - .icon(IconName::Check) - .icon_position(IconPosition::Start) - .icon_size(IconSize::XSmall) - .icon_color(Color::Success) - .on_click(cx.listener({ - let id = tool_call_id; - move |this, _, _, cx| { - this.authorize_tool_call( - id, - acp::ToolCallConfirmationOutcome::Allow, - cx, - ); - } - })), - ) - .child( - Button::new(("reject", tool_call_id.0), "Reject") - .icon(IconName::X) - .icon_position(IconPosition::Start) - .icon_size(IconSize::XSmall) - .icon_color(Color::Error) - .on_click(cx.listener({ - let id = tool_call_id; - move |this, _, _, cx| { - this.authorize_tool_call( - id, - acp::ToolCallConfirmationOutcome::Reject, - cx, - ); - } - })), - ), - ) + .child(self.render_confirmation_buttons( + &[AlwaysAllowOption { + id: "always_allow", + label: "Always Allow Edits".into(), + outcome: acp::ToolCallConfirmationOutcome::AlwaysAllow, + }], + tool_call_id, + cx, + )) .into_any(), ToolCallConfirmation::Execute { command, @@ -1140,66 +1104,15 @@ impl AcpThreadView { }), )) .children(content.map(|content| self.render_tool_call_content(content, window, cx))) - .child( - button_container - .child( - Button::new( - ("always_allow", tool_call_id.0), - format!("Always Allow {root_command}"), - ) - .icon(IconName::CheckDouble) - .icon_position(IconPosition::Start) - .icon_size(IconSize::XSmall) - .icon_color(Color::Success) - .label_size(LabelSize::Small) - .on_click(cx.listener({ - let id = tool_call_id; - move |this, _, _, cx| { - this.authorize_tool_call( - id, - acp::ToolCallConfirmationOutcome::AlwaysAllow, - cx, - ); - } - })), - ) - .child( - Button::new(("allow", tool_call_id.0), "Allow") - .icon(IconName::Check) - .icon_position(IconPosition::Start) - .icon_size(IconSize::XSmall) - .icon_color(Color::Success) - .label_size(LabelSize::Small) - .on_click(cx.listener({ - let id = tool_call_id; - move |this, _, _, cx| { - this.authorize_tool_call( - id, - acp::ToolCallConfirmationOutcome::Allow, - cx, - ); - } - })), - ) - .child( - Button::new(("reject", tool_call_id.0), "Reject") - .icon(IconName::X) - .icon_position(IconPosition::Start) - .icon_size(IconSize::XSmall) - .icon_color(Color::Error) - .label_size(LabelSize::Small) - .on_click(cx.listener({ - let id = tool_call_id; - move |this, _, _, cx| { - this.authorize_tool_call( - id, - acp::ToolCallConfirmationOutcome::Reject, - cx, - ); - } - })), - ), - ) + .child(self.render_confirmation_buttons( + &[AlwaysAllowOption { + id: "always_allow", + label: format!("Always Allow {root_command}").into(), + outcome: acp::ToolCallConfirmationOutcome::AlwaysAllow, + }], + tool_call_id, + cx, + )) .into_any(), ToolCallConfirmation::Mcp { server_name, @@ -1220,87 +1133,22 @@ impl AcpThreadView { })), ) .children(content.map(|content| self.render_tool_call_content(content, window, cx))) - .child( - button_container - .child( - Button::new( - ("always_allow_server", tool_call_id.0), - format!("Always Allow {server_name}"), - ) - .icon(IconName::CheckDouble) - .icon_position(IconPosition::Start) - .icon_size(IconSize::XSmall) - .icon_color(Color::Success) - .label_size(LabelSize::Small) - .on_click(cx.listener({ - let id = tool_call_id; - move |this, _, _, cx| { - this.authorize_tool_call( - id, - acp::ToolCallConfirmationOutcome::AlwaysAllowMcpServer, - cx, - ); - } - })), - ) - .child( - Button::new( - ("always_allow_tool", tool_call_id.0), - format!("Always Allow {tool_display_name}"), - ) - .icon(IconName::CheckDouble) - .icon_position(IconPosition::Start) - .icon_size(IconSize::XSmall) - .icon_color(Color::Success) - .label_size(LabelSize::Small) - .on_click(cx.listener({ - let id = tool_call_id; - move |this, _, _, cx| { - this.authorize_tool_call( - id, - acp::ToolCallConfirmationOutcome::AlwaysAllowTool, - cx, - ); - } - })), - ) - .child( - Button::new(("allow", tool_call_id.0), "Allow") - .icon(IconName::Check) - .icon_position(IconPosition::Start) - .icon_size(IconSize::XSmall) - .icon_color(Color::Success) - .label_size(LabelSize::Small) - .on_click(cx.listener({ - let id = tool_call_id; - move |this, _, _, cx| { - this.authorize_tool_call( - id, - acp::ToolCallConfirmationOutcome::Allow, - cx, - ); - } - })), - ) - .child( - Button::new(("reject", tool_call_id.0), "Reject") - .icon(IconName::X) - .icon_position(IconPosition::Start) - .icon_size(IconSize::XSmall) - .icon_color(Color::Error) - .label_size(LabelSize::Small) - .on_click(cx.listener({ - let id = tool_call_id; - move |this, _, _, cx| { - this.authorize_tool_call( - id, - acp::ToolCallConfirmationOutcome::Reject, - cx, - ); - } - })), - ), - ) + .child(self.render_confirmation_buttons( + &[ + AlwaysAllowOption { + id: "always_allow_server", + label: format!("Always Allow {server_name}").into(), + outcome: acp::ToolCallConfirmationOutcome::AlwaysAllowMcpServer, + }, + AlwaysAllowOption { + id: "always_allow_tool", + label: format!("Always Allow {tool_display_name}").into(), + outcome: acp::ToolCallConfirmationOutcome::AlwaysAllowTool, + }, + ], + tool_call_id, + cx, + )) .into_any(), ToolCallConfirmation::Fetch { description, urls } => confirmation_container .child( @@ -1328,63 +1176,15 @@ impl AcpThreadView { })), ) .children(content.map(|content| self.render_tool_call_content(content, window, cx))) - .child( - button_container - .child( - Button::new(("always_allow", tool_call_id.0), "Always Allow") - .icon(IconName::CheckDouble) - .icon_position(IconPosition::Start) - .icon_size(IconSize::XSmall) - .icon_color(Color::Success) - .label_size(LabelSize::Small) - .on_click(cx.listener({ - let id = tool_call_id; - move |this, _, _, cx| { - this.authorize_tool_call( - id, - acp::ToolCallConfirmationOutcome::AlwaysAllow, - cx, - ); - } - })), - ) - .child( - Button::new(("allow", tool_call_id.0), "Allow") - .icon(IconName::Check) - .icon_position(IconPosition::Start) - .icon_size(IconSize::XSmall) - .icon_color(Color::Success) - .label_size(LabelSize::Small) - .on_click(cx.listener({ - let id = tool_call_id; - move |this, _, _, cx| { - this.authorize_tool_call( - id, - acp::ToolCallConfirmationOutcome::Allow, - cx, - ); - } - })), - ) - .child( - Button::new(("reject", tool_call_id.0), "Reject") - .icon(IconName::X) - .icon_position(IconPosition::Start) - .icon_size(IconSize::XSmall) - .icon_color(Color::Error) - .label_size(LabelSize::Small) - .on_click(cx.listener({ - let id = tool_call_id; - move |this, _, _, cx| { - this.authorize_tool_call( - id, - acp::ToolCallConfirmationOutcome::Reject, - cx, - ); - } - })), - ), - ) + .child(self.render_confirmation_buttons( + &[AlwaysAllowOption { + id: "always_allow", + label: "Always Allow".into(), + outcome: acp::ToolCallConfirmationOutcome::AlwaysAllow, + }], + tool_call_id, + cx, + )) .into_any(), ToolCallConfirmation::Other { description } => confirmation_container .child(v_flex().px_2().pb_1p5().child(self.render_markdown( @@ -1392,67 +1192,87 @@ impl AcpThreadView { default_markdown_style(false, window, cx), ))) .children(content.map(|content| self.render_tool_call_content(content, window, cx))) - .child( - button_container - .child( - Button::new(("always_allow", tool_call_id.0), "Always Allow") - .icon(IconName::CheckDouble) - .icon_position(IconPosition::Start) - .icon_size(IconSize::XSmall) - .icon_color(Color::Success) - .label_size(LabelSize::Small) - .on_click(cx.listener({ - let id = tool_call_id; - move |this, _, _, cx| { - this.authorize_tool_call( - id, - acp::ToolCallConfirmationOutcome::AlwaysAllow, - cx, - ); - } - })), - ) - .child( - Button::new(("allow", tool_call_id.0), "Allow") - .icon(IconName::Check) - .icon_position(IconPosition::Start) - .icon_size(IconSize::XSmall) - .icon_color(Color::Success) - .label_size(LabelSize::Small) - .on_click(cx.listener({ - let id = tool_call_id; - move |this, _, _, cx| { - this.authorize_tool_call( - id, - acp::ToolCallConfirmationOutcome::Allow, - cx, - ); - } - })), - ) - .child( - Button::new(("reject", tool_call_id.0), "Reject") - .icon(IconName::X) - .icon_position(IconPosition::Start) - .icon_size(IconSize::XSmall) - .icon_color(Color::Error) - .label_size(LabelSize::Small) - .on_click(cx.listener({ - let id = tool_call_id; - move |this, _, _, cx| { - this.authorize_tool_call( - id, - acp::ToolCallConfirmationOutcome::Reject, - cx, - ); - } - })), - ), - ) + .child(self.render_confirmation_buttons( + &[AlwaysAllowOption { + id: "always_allow", + label: "Always Allow".into(), + outcome: acp::ToolCallConfirmationOutcome::AlwaysAllow, + }], + tool_call_id, + cx, + )) .into_any(), } } + fn render_confirmation_buttons( + &self, + always_allow_options: &[AlwaysAllowOption], + tool_call_id: ToolCallId, + cx: &Context<Self>, + ) -> Div { + h_flex() + .pt_1p5() + .px_1p5() + .gap_1() + .justify_end() + .border_t_1() + .border_color(self.tool_card_border_color(cx)) + .when(self.agent.supports_always_allow(), |this| { + this.children(always_allow_options.into_iter().map(|always_allow_option| { + let outcome = always_allow_option.outcome; + Button::new( + (always_allow_option.id, tool_call_id.0), + always_allow_option.label.clone(), + ) + .icon(IconName::CheckDouble) + .icon_position(IconPosition::Start) + .icon_size(IconSize::XSmall) + .icon_color(Color::Success) + .on_click(cx.listener({ + let id = tool_call_id; + move |this, _, _, cx| { + this.authorize_tool_call(id, outcome, cx); + } + })) + })) + }) + .child( + Button::new(("allow", tool_call_id.0), "Allow") + .icon(IconName::Check) + .icon_position(IconPosition::Start) + .icon_size(IconSize::XSmall) + .icon_color(Color::Success) + .on_click(cx.listener({ + let id = tool_call_id; + move |this, _, _, cx| { + this.authorize_tool_call( + id, + acp::ToolCallConfirmationOutcome::Allow, + cx, + ); + } + })), + ) + .child( + Button::new(("reject", tool_call_id.0), "Reject") + .icon(IconName::X) + .icon_position(IconPosition::Start) + .icon_size(IconSize::XSmall) + .icon_color(Color::Error) + .on_click(cx.listener({ + let id = tool_call_id; + move |this, _, _, cx| { + this.authorize_tool_call( + id, + acp::ToolCallConfirmationOutcome::Reject, + cx, + ); + } + })), + ) + } + fn render_diff_editor(&self, multibuffer: &Entity<MultiBuffer>) -> AnyElement { v_flex() .h_full() @@ -1466,15 +1286,15 @@ impl AcpThreadView { .into_any() } - fn render_gemini_logo(&self) -> AnyElement { - Icon::new(IconName::AiGemini) + fn render_agent_logo(&self) -> AnyElement { + Icon::new(self.agent.logo()) .color(Color::Muted) .size(IconSize::XLarge) .into_any_element() } - fn render_error_gemini_logo(&self) -> AnyElement { - let logo = Icon::new(IconName::AiGemini) + fn render_error_agent_logo(&self) -> AnyElement { + let logo = Icon::new(self.agent.logo()) .color(Color::Muted) .size(IconSize::XLarge) .into_any_element(); @@ -1493,49 +1313,50 @@ impl AcpThreadView { .into_any_element() } - fn render_empty_state(&self, loading: bool, cx: &App) -> AnyElement { + fn render_empty_state(&self, cx: &App) -> AnyElement { + let loading = matches!(&self.thread_state, ThreadState::Loading { .. }); + v_flex() .size_full() .items_center() .justify_center() - .child( - if loading { - h_flex() - .justify_center() - .child(self.render_gemini_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_gemini_logo().into_any_element() - } - ) - .child( + .child(if loading { h_flex() - .mt_4() - .mb_1() .justify_center() - .child(Headline::new(if loading { - "Connecting to Gemini…" - } else { - "Welcome to Gemini" - }).size(HeadlineSize::Medium)), - ) + .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() - } else { - this.text_color(cx.theme().colors().text_muted) + .map(|this| { + if loading { + this.invisible() + } else { + this.text_color(cx.theme().colors().text_muted) + } }) - .child("Ask questions, edit files, run commands.\nBe specific for the best results.") + .child(self.agent.empty_state_message()), ) .into_any() } @@ -1544,7 +1365,7 @@ impl AcpThreadView { v_flex() .items_center() .justify_center() - .child(self.render_error_gemini_logo()) + .child(self.render_error_agent_logo()) .child( h_flex() .mt_4() @@ -1559,7 +1380,7 @@ impl AcpThreadView { let mut container = v_flex() .items_center() .justify_center() - .child(self.render_error_gemini_logo()) + .child(self.render_error_agent_logo()) .child( v_flex() .mt_4() @@ -1575,43 +1396,47 @@ impl AcpThreadView { ), ); - if matches!(e, LoadError::Unsupported { .. }) { - container = - container.child(Button::new("upgrade", "Upgrade Gemini to Latest").on_click( - cx.listener(|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 command = - "npm install -g @google/gemini-cli@latest".to_string(); - let spawn_in_terminal = task::SpawnInTerminal { - id: task::TaskId("install".to_string()), - full_label: command.clone(), - label: command.clone(), - command: Some(command.clone()), - args: Vec::new(), - command_label: 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(); - }) - .ok(); - }), - )); + 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).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(); + }) + .ok(); + }), + )); } container.into_any() @@ -2267,20 +2092,23 @@ 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", "Sign in to Gemini").on_click( - cx.listener(|this, _, window, cx| this.authenticate(window, cx)), - ), - )), - ThreadState::Loading { .. } => { - v_flex().flex_1().child(self.render_empty_state(true, cx)) + 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::Loading { .. } => v_flex().flex_1().child(self.render_empty_state(cx)), ThreadState::LoadError(e) => v_flex() .p_2() .flex_1() @@ -2321,7 +2149,7 @@ impl Render for AcpThreadView { }) .children(self.render_edits_bar(&thread, window, cx)) } else { - this.child(self.render_empty_state(false, cx)) + this.child(self.render_empty_state(cx)) } }), }) diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index 000e27032202d56f1fa98cdb82d73ef757a84766..e69664ce882df70915a84d2ba38cf7ec8521fd4b 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -1,5 +1,5 @@ use crate::{Keep, KeepAll, OpenAgentDiff, Reject, RejectAll}; -use acp::{AcpThread, AcpThreadEvent}; +use acp_thread::{AcpThread, AcpThreadEvent}; use agent::{Thread, ThreadEvent, ThreadSummary}; use agent_settings::AgentSettings; use anyhow::Result; @@ -81,7 +81,7 @@ impl AgentDiffThread { match self { AgentDiffThread::Native(thread) => thread.read(cx).is_generating(), AgentDiffThread::AcpThread(thread) => { - thread.read(cx).status() == acp::ThreadStatus::Generating + thread.read(cx).status() == acp_thread::ThreadStatus::Generating } } } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 2caa9dab42d374514324a2e97db3473db50fcbcf..895a49950241e0072ac3926b2ed78e9248918442 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -5,10 +5,11 @@ 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}; -use crate::NewAcpThread; +use crate::NewExternalAgentThread; use crate::agent_diff::AgentDiffThread; use crate::language_model_selector::ToggleModelSelector; use crate::{ @@ -114,10 +115,12 @@ pub fn init(cx: &mut App) { panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx)); } }) - .register_action(|workspace, _: &NewAcpThread, window, cx| { + .register_action(|workspace, action: &NewExternalAgentThread, window, cx| { if let Some(panel) = workspace.panel::<AgentPanel>(cx) { workspace.focus_panel::<AgentPanel>(window, cx); - panel.update(cx, |panel, cx| panel.new_gemini_thread(window, cx)); + panel.update(cx, |panel, cx| { + panel.new_external_thread(action.agent, window, cx) + }); } }) .register_action(|workspace, action: &OpenRulesLibrary, window, cx| { @@ -136,7 +139,7 @@ pub fn init(cx: &mut App) { let thread = thread.read(cx).thread().clone(); AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx); } - ActiveView::AcpThread { .. } + ActiveView::ExternalAgentThread { .. } | ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {} @@ -200,7 +203,7 @@ enum ActiveView { message_editor: Entity<MessageEditor>, _subscriptions: Vec<gpui::Subscription>, }, - AcpThread { + ExternalAgentThread { thread_view: Entity<AcpThreadView>, }, TextThread { @@ -222,9 +225,9 @@ enum WhichFontSize { impl ActiveView { pub fn which_font_size_used(&self) -> WhichFontSize { match self { - ActiveView::Thread { .. } | ActiveView::AcpThread { .. } | ActiveView::History => { - WhichFontSize::AgentFont - } + ActiveView::Thread { .. } + | ActiveView::ExternalAgentThread { .. } + | ActiveView::History => WhichFontSize::AgentFont, ActiveView::TextThread { .. } => WhichFontSize::BufferFont, ActiveView::Configuration => WhichFontSize::None, } @@ -255,7 +258,7 @@ impl ActiveView { thread.scroll_to_bottom(cx); }); } - ActiveView::AcpThread { .. } => {} + ActiveView::ExternalAgentThread { .. } => {} ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {} @@ -674,7 +677,7 @@ impl AgentPanel { .clone() .update(cx, |thread, cx| thread.get_or_init_configured_model(cx)); } - ActiveView::AcpThread { .. } + ActiveView::ExternalAgentThread { .. } | ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {} @@ -757,7 +760,7 @@ impl AgentPanel { ActiveView::Thread { thread, .. } => { thread.update(cx, |thread, cx| thread.cancel_last_completion(window, cx)); } - ActiveView::AcpThread { thread_view, .. } => { + ActiveView::ExternalAgentThread { thread_view, .. } => { thread_view.update(cx, |thread_element, cx| thread_element.cancel(cx)); } ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {} @@ -767,7 +770,7 @@ impl AgentPanel { fn active_message_editor(&self) -> Option<&Entity<MessageEditor>> { match &self.active_view { ActiveView::Thread { message_editor, .. } => Some(message_editor), - ActiveView::AcpThread { .. } + ActiveView::ExternalAgentThread { .. } | ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None, @@ -889,35 +892,77 @@ impl AgentPanel { context_editor.focus_handle(cx).focus(window); } - fn new_gemini_thread(&mut self, window: &mut Window, cx: &mut Context<Self>) { + fn new_external_thread( + &mut self, + agent_choice: Option<crate::ExternalAgent>, + window: &mut Window, + cx: &mut Context<Self>, + ) { let workspace = self.workspace.clone(); let project = self.project.clone(); let message_history = self.acp_message_history.clone(); + const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent"; + + #[derive(Default, Serialize, Deserialize)] + struct LastUsedExternalAgent { + agent: crate::ExternalAgent, + } + cx.spawn_in(window, async move |this, cx| { - let thread_view = cx.new_window_entity(|window, cx| { - crate::acp::AcpThreadView::new( - workspace.clone(), - project, - message_history, - window, - cx, - ) - })?; + let server: Rc<dyn AgentServer> = 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(); + } + }) + .detach(); + + agent.server() + } + None => cx + .background_spawn(async move { + KEY_VALUE_STORE.read_kvp(LAST_USED_EXTERNAL_AGENT_KEY) + }) + .await + .log_err() + .flatten() + .and_then(|value| { + serde_json::from_str::<LastUsedExternalAgent>(&value).log_err() + }) + .unwrap_or_default() + .agent + .server(), + }; + this.update_in(cx, |this, window, cx| { + let thread_view = cx.new(|cx| { + crate::acp::AcpThreadView::new( + server, + workspace.clone(), + project, + message_history, + window, + cx, + ) + }); + this.set_active_view( - ActiveView::AcpThread { + ActiveView::ExternalAgentThread { thread_view: thread_view.clone(), }, window, cx, ); }) - .log_err(); - - anyhow::Ok(()) }) - .detach(); + .detach_and_log_err(cx); } fn deploy_rules_library( @@ -1084,7 +1129,7 @@ impl AgentPanel { ActiveView::Thread { message_editor, .. } => { message_editor.focus_handle(cx).focus(window); } - ActiveView::AcpThread { thread_view } => { + ActiveView::ExternalAgentThread { thread_view } => { thread_view.focus_handle(cx).focus(window); } ActiveView::TextThread { context_editor, .. } => { @@ -1211,7 +1256,7 @@ impl AgentPanel { }) .log_err(); } - ActiveView::AcpThread { .. } + ActiveView::ExternalAgentThread { .. } | ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {} @@ -1267,7 +1312,7 @@ impl AgentPanel { ) .detach_and_log_err(cx); } - ActiveView::AcpThread { thread_view } => { + ActiveView::ExternalAgentThread { thread_view } => { thread_view .update(cx, |thread_view, cx| { thread_view.open_thread_as_markdown(workspace, window, cx) @@ -1428,7 +1473,7 @@ impl AgentPanel { } }) } - ActiveView::AcpThread { .. } => {} + ActiveView::ExternalAgentThread { .. } => {} ActiveView::History | ActiveView::Configuration => {} } @@ -1517,7 +1562,7 @@ impl Focusable for AgentPanel { fn focus_handle(&self, cx: &App) -> FocusHandle { match &self.active_view { ActiveView::Thread { message_editor, .. } => message_editor.focus_handle(cx), - ActiveView::AcpThread { thread_view, .. } => thread_view.focus_handle(cx), + ActiveView::ExternalAgentThread { thread_view, .. } => thread_view.focus_handle(cx), ActiveView::History => self.history.focus_handle(cx), ActiveView::TextThread { context_editor, .. } => context_editor.focus_handle(cx), ActiveView::Configuration => { @@ -1674,9 +1719,11 @@ impl AgentPanel { .into_any_element(), } } - ActiveView::AcpThread { thread_view } => Label::new(thread_view.read(cx).title(cx)) - .truncate() - .into_any_element(), + ActiveView::ExternalAgentThread { thread_view } => { + Label::new(thread_view.read(cx).title(cx)) + .truncate() + .into_any_element() + } ActiveView::TextThread { title_editor, context_editor, @@ -1811,7 +1858,7 @@ impl AgentPanel { let active_thread = match &self.active_view { ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()), - ActiveView::AcpThread { .. } + ActiveView::ExternalAgentThread { .. } | ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None, @@ -1849,7 +1896,20 @@ impl AgentPanel { .when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| { this.separator() .header("External Agents") - .action("New Gemini Thread", NewAcpThread.boxed_clone()) + .action( + "New Gemini Thread", + NewExternalAgentThread { + agent: Some(crate::ExternalAgent::Gemini), + } + .boxed_clone(), + ) + .action( + "New Claude Code Thread", + NewExternalAgentThread { + agent: Some(crate::ExternalAgent::ClaudeCode), + } + .boxed_clone(), + ) }); menu })) @@ -2090,7 +2150,11 @@ impl AgentPanel { Some(element.into_any_element()) } - _ => None, + ActiveView::ExternalAgentThread { .. } + | ActiveView::History + | ActiveView::Configuration => { + return None; + } } } @@ -2119,7 +2183,7 @@ impl AgentPanel { return false; } } - ActiveView::AcpThread { .. } => { + ActiveView::ExternalAgentThread { .. } => { return false; } ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => { @@ -2706,7 +2770,7 @@ impl AgentPanel { ) -> Option<AnyElement> { let active_thread = match &self.active_view { ActiveView::Thread { thread, .. } => thread, - ActiveView::AcpThread { .. } => { + ActiveView::ExternalAgentThread { .. } => { return None; } ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => { @@ -3055,7 +3119,7 @@ impl AgentPanel { .detach(); }); } - ActiveView::AcpThread { .. } => { + ActiveView::ExternalAgentThread { .. } => { unimplemented!() } ActiveView::TextThread { context_editor, .. } => { @@ -3077,7 +3141,7 @@ impl AgentPanel { let mut key_context = KeyContext::new_with_defaults(); key_context.add("AgentPanel"); match &self.active_view { - ActiveView::AcpThread { .. } => key_context.add("acp_thread"), + ActiveView::ExternalAgentThread { .. } => key_context.add("external_agent_thread"), ActiveView::TextThread { .. } => key_context.add("prompt_editor"), ActiveView::Thread { .. } | ActiveView::History | ActiveView::Configuration => {} } @@ -3133,7 +3197,7 @@ impl Render for AgentPanel { }); this.continue_conversation(window, cx); } - ActiveView::AcpThread { .. } => {} + ActiveView::ExternalAgentThread { .. } => {} ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {} @@ -3175,7 +3239,7 @@ impl Render for AgentPanel { }) .child(h_flex().child(message_editor.clone())) .child(self.render_drag_target(cx)), - ActiveView::AcpThread { thread_view, .. } => parent + ActiveView::ExternalAgentThread { thread_view, .. } => parent .relative() .child(thread_view.clone()) .child(self.render_drag_target(cx)), diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 3170ec4a267d76791968b410e9426079a6ae1f2d..7f69e8f66e3bcf37fb56c0384c0b8bf17a37d0f4 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -25,6 +25,7 @@ mod thread_history; mod tool_compatibility; mod ui; +use std::rc::Rc; use std::sync::Arc; use agent::{Thread, ThreadId}; @@ -40,7 +41,7 @@ use language_model::{ }; use prompt_store::PromptBuilder; use schemars::JsonSchema; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use settings::{Settings as _, SettingsStore}; pub use crate::active_thread::ActiveThread; @@ -57,8 +58,6 @@ actions!( [ /// Creates a new text-based conversation thread. NewTextThread, - /// Creates a new external agent conversation thread. - NewAcpThread, /// Toggles the context picker interface for adding files, symbols, or other context. ToggleContextPicker, /// Toggles the navigation menu for switching between threads and views. @@ -133,6 +132,32 @@ pub struct NewThread { from_thread_id: Option<ThreadId>, } +/// Creates a new external agent conversation thread. +#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)] +#[action(namespace = agent)] +#[serde(deny_unknown_fields)] +pub struct NewExternalAgentThread { + /// Which agent to use for the conversation. + agent: Option<ExternalAgent>, +} + +#[derive(Default, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +enum ExternalAgent { + #[default] + Gemini, + ClaudeCode, +} + +impl ExternalAgent { + pub fn server(&self) -> Rc<dyn agent_servers::AgentServer> { + match self { + ExternalAgent::Gemini => Rc::new(agent_servers::Gemini), + ExternalAgent::ClaudeCode => Rc::new(agent_servers::ClaudeCode), + } + } +} + /// Opens the profile management interface for configuring agent tools and settings. #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] #[action(namespace = agent)] diff --git a/crates/context_server/Cargo.toml b/crates/context_server/Cargo.toml index 96bb9e071f42dd1f6f7fa0782ed8ca425e1cd379..5e4f8369c45f0edb58efda1618bf8fe0aad55749 100644 --- a/crates/context_server/Cargo.toml +++ b/crates/context_server/Cargo.toml @@ -21,12 +21,14 @@ collections.workspace = true futures.workspace = true gpui.workspace = true log.workspace = true +net.workspace = true parking_lot.workspace = true postage.workspace = true schemars.workspace = true serde.workspace = true serde_json.workspace = true smol.workspace = true +tempfile.workspace = true url = { workspace = true, features = ["serde"] } util.workspace = true workspace-hack.workspace = true diff --git a/crates/context_server/src/client.rs b/crates/context_server/src/client.rs index 83d815432da6bba4ac6e077e0378a31739655548..6b24d9b136efc2d9cc99843e54027058e1602861 100644 --- a/crates/context_server/src/client.rs +++ b/crates/context_server/src/client.rs @@ -70,12 +70,12 @@ fn is_null_value<T: Serialize>(value: &T) -> bool { } #[derive(Serialize, Deserialize)] -struct Request<'a, T> { - jsonrpc: &'static str, - id: RequestId, - method: &'a str, +pub struct Request<'a, T> { + pub jsonrpc: &'static str, + pub id: RequestId, + pub method: &'a str, #[serde(skip_serializing_if = "is_null_value")] - params: T, + pub params: T, } #[derive(Serialize, Deserialize)] @@ -88,18 +88,18 @@ struct AnyResponse<'a> { result: Option<&'a RawValue>, } -#[derive(Deserialize)] +#[derive(Serialize, Deserialize)] #[allow(dead_code)] -struct Response<T> { - jsonrpc: &'static str, - id: RequestId, +pub(crate) struct Response<T> { + pub jsonrpc: &'static str, + pub id: RequestId, #[serde(flatten)] - value: CspResult<T>, + pub value: CspResult<T>, } -#[derive(Deserialize)] +#[derive(Serialize, Deserialize)] #[serde(rename_all = "snake_case")] -enum CspResult<T> { +pub(crate) enum CspResult<T> { #[serde(rename = "result")] Ok(Option<T>), #[allow(dead_code)] @@ -123,8 +123,9 @@ struct AnyNotification<'a> { } #[derive(Debug, Serialize, Deserialize)] -struct Error { - message: String, +pub(crate) struct Error { + pub message: String, + pub code: i32, } #[derive(Debug, Clone, Deserialize)] diff --git a/crates/context_server/src/context_server.rs b/crates/context_server/src/context_server.rs index 905435fcce57dc8ce8719e5056b28118168e9a04..807b17f1ca64fcc253084d553b8ec700c60fb74e 100644 --- a/crates/context_server/src/context_server.rs +++ b/crates/context_server/src/context_server.rs @@ -1,4 +1,5 @@ pub mod client; +pub mod listener; pub mod protocol; #[cfg(any(test, feature = "test-support"))] pub mod test; diff --git a/crates/context_server/src/listener.rs b/crates/context_server/src/listener.rs new file mode 100644 index 0000000000000000000000000000000000000000..9295ad979c99ee9c1a16a844edc1df456cea61d7 --- /dev/null +++ b/crates/context_server/src/listener.rs @@ -0,0 +1,236 @@ +use ::serde::{Deserialize, Serialize}; +use anyhow::{Context as _, Result}; +use collections::HashMap; +use futures::{ + AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, FutureExt, + channel::mpsc::{UnboundedReceiver, UnboundedSender, unbounded}, + io::BufReader, + select_biased, +}; +use gpui::{App, AppContext, AsyncApp, Task}; +use net::async_net::{UnixListener, UnixStream}; +use serde_json::{json, value::RawValue}; +use smol::stream::StreamExt; +use std::{ + cell::RefCell, + path::{Path, PathBuf}, + rc::Rc, +}; +use util::ResultExt; + +use crate::{ + client::{CspResult, RequestId, Response}, + types::Request, +}; + +pub struct McpServer { + socket_path: PathBuf, + handlers: Rc<RefCell<HashMap<&'static str, McpHandler>>>, + _server_task: Task<()>, +} + +type McpHandler = Box<dyn Fn(RequestId, Option<Box<RawValue>>, &App) -> Task<String>>; + +impl McpServer { + pub fn new(cx: &AsyncApp) -> Task<Result<Self>> { + let task = cx.background_spawn(async move { + let temp_dir = tempfile::Builder::new().prefix("zed-mcp").tempdir()?; + let socket_path = temp_dir.path().join("mcp.sock"); + let listener = UnixListener::bind(&socket_path).context("creating mcp socket")?; + + anyhow::Ok((temp_dir, socket_path, listener)) + }); + + cx.spawn(async move |cx| { + let (temp_dir, socket_path, listener) = task.await?; + let handlers = Rc::new(RefCell::new(HashMap::default())); + let server_task = cx.spawn({ + let handlers = handlers.clone(); + async move |cx| { + while let Ok((stream, _)) = listener.accept().await { + Self::serve_connection(stream, handlers.clone(), cx); + } + drop(temp_dir) + } + }); + Ok(Self { + socket_path, + _server_task: server_task, + handlers: handlers.clone(), + }) + }) + } + + pub fn handle_request<R: Request>( + &mut self, + f: impl Fn(R::Params, &App) -> Task<Result<R::Response>> + 'static, + ) { + let f = Box::new(f); + self.handlers.borrow_mut().insert( + R::METHOD, + Box::new(move |req_id, opt_params, cx| { + let result = match opt_params { + Some(params) => serde_json::from_str(params.get()), + None => serde_json::from_value(serde_json::Value::Null), + }; + + let params: R::Params = match result { + Ok(params) => params, + Err(e) => { + return Task::ready( + serde_json::to_string(&Response::<R::Response> { + jsonrpc: "2.0", + id: req_id, + value: CspResult::Error(Some(crate::client::Error { + message: format!("{e}"), + code: -32700, + })), + }) + .unwrap(), + ); + } + }; + let task = f(params, cx); + cx.background_spawn(async move { + match task.await { + Ok(result) => serde_json::to_string(&Response { + jsonrpc: "2.0", + id: req_id, + value: CspResult::Ok(Some(result)), + }) + .unwrap(), + Err(e) => serde_json::to_string(&Response { + jsonrpc: "2.0", + id: req_id, + value: CspResult::Error::<R::Response>(Some(crate::client::Error { + message: format!("{e}"), + code: -32603, + })), + }) + .unwrap(), + } + }) + }), + ); + } + + pub fn socket_path(&self) -> &Path { + &self.socket_path + } + + fn serve_connection( + stream: UnixStream, + handlers: Rc<RefCell<HashMap<&'static str, McpHandler>>>, + cx: &mut AsyncApp, + ) { + let (read, write) = smol::io::split(stream); + let (incoming_tx, mut incoming_rx) = unbounded(); + let (outgoing_tx, outgoing_rx) = unbounded(); + + cx.background_spawn(Self::handle_io(outgoing_rx, incoming_tx, write, read)) + .detach(); + + cx.spawn(async move |cx| { + while let Some(request) = incoming_rx.next().await { + let Some(request_id) = request.id.clone() else { + continue; + }; + if let Some(handler) = handlers.borrow().get(&request.method.as_ref()) { + let outgoing_tx = outgoing_tx.clone(); + + if let Some(task) = cx + .update(|cx| handler(request_id, request.params, cx)) + .log_err() + { + cx.spawn(async move |_| { + let response = task.await; + outgoing_tx.unbounded_send(response).ok(); + }) + .detach(); + } + } else { + outgoing_tx + .unbounded_send( + serde_json::to_string(&Response::<()> { + jsonrpc: "2.0", + id: request.id.unwrap(), + value: CspResult::Error(Some(crate::client::Error { + message: format!("unhandled method {}", request.method), + code: -32601, + })), + }) + .unwrap(), + ) + .ok(); + } + } + }) + .detach(); + } + + async fn handle_io( + mut outgoing_rx: UnboundedReceiver<String>, + incoming_tx: UnboundedSender<RawRequest>, + mut outgoing_bytes: impl Unpin + AsyncWrite, + incoming_bytes: impl Unpin + AsyncRead, + ) -> Result<()> { + let mut output_reader = BufReader::new(incoming_bytes); + let mut incoming_line = String::new(); + loop { + select_biased! { + message = outgoing_rx.next().fuse() => { + if let Some(message) = message { + log::trace!("send: {}", &message); + outgoing_bytes.write_all(message.as_bytes()).await?; + outgoing_bytes.write_all(&[b'\n']).await?; + } else { + break; + } + } + bytes_read = output_reader.read_line(&mut incoming_line).fuse() => { + if bytes_read? == 0 { + break + } + log::trace!("recv: {}", &incoming_line); + match serde_json::from_str(&incoming_line) { + Ok(message) => { + incoming_tx.unbounded_send(message).log_err(); + } + Err(error) => { + outgoing_bytes.write_all(serde_json::to_string(&json!({ + "jsonrpc": "2.0", + "error": json!({ + "code": -32603, + "message": format!("Failed to parse: {error}"), + }), + }))?.as_bytes()).await?; + outgoing_bytes.write_all(&[b'\n']).await?; + log::error!("failed to parse incoming message: {error}. Raw: {incoming_line}"); + } + } + incoming_line.clear(); + } + } + } + Ok(()) + } +} + +#[derive(Serialize, Deserialize)] +struct RawRequest { + #[serde(skip_serializing_if = "Option::is_none")] + id: Option<RequestId>, + method: String, + #[serde(skip_serializing_if = "Option::is_none")] + params: Option<Box<serde_json::value::RawValue>>, +} + +#[derive(Serialize, Deserialize)] +struct RawResponse { + jsonrpc: &'static str, + id: RequestId, + #[serde(skip_serializing_if = "Option::is_none")] + error: Option<crate::client::Error>, + #[serde(skip_serializing_if = "Option::is_none")] + result: Option<Box<serde_json::value::RawValue>>, +} diff --git a/crates/context_server/src/types.rs b/crates/context_server/src/types.rs index 8e3daf9e222a29cf373ba7a3bb37d83c2950acf7..4a6fdcabd3421e14cab3ff89ce6962023935059a 100644 --- a/crates/context_server/src/types.rs +++ b/crates/context_server/src/types.rs @@ -153,7 +153,7 @@ pub struct InitializeParams { pub struct CallToolParams { pub name: String, #[serde(skip_serializing_if = "Option::is_none")] - pub arguments: Option<HashMap<String, serde_json::Value>>, + pub arguments: Option<serde_json::Value>, #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] pub meta: Option<HashMap<String, serde_json::Value>>, } diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index b29a8b78e679907d1077e015f3aae2528511267b..6834d56215a86e3373f11d1b21736c8350e79666 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -11,6 +11,7 @@ pub enum IconName { Ai, AiAnthropic, AiBedrock, + AiClaude, AiDeepSeek, AiEdit, AiGemini, diff --git a/crates/nc/Cargo.toml b/crates/nc/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..46ef2d3c62e233cc8693b3fdb3082749c05d9ed5 --- /dev/null +++ b/crates/nc/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "nc" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/nc.rs" +doctest = false + +[dependencies] +anyhow.workspace = true +futures.workspace = true +net.workspace = true +smol.workspace = true +workspace-hack.workspace = true diff --git a/crates/nc/LICENSE-GPL b/crates/nc/LICENSE-GPL new file mode 120000 index 0000000000000000000000000000000000000000..89e542f750cd3860a0598eff0dc34b56d7336dc4 --- /dev/null +++ b/crates/nc/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/nc/src/nc.rs b/crates/nc/src/nc.rs new file mode 100644 index 0000000000000000000000000000000000000000..fccb4d726c49258d75323bf59389bfeb5baafa6a --- /dev/null +++ b/crates/nc/src/nc.rs @@ -0,0 +1,56 @@ +use anyhow::Result; + +#[cfg(windows)] +pub fn main(_socket: &str) -> Result<()> { + // It looks like we can't get an async stdio stream on Windows from smol. + // + // We decided to merge this with a panic on Windows since this is only used + // by the experimental Claude Code Agent Server. + // + // We're tracking this internally, and we will address it before shipping the integration. + panic!("--nc isn't yet supported on Windows"); +} + +/// The main function for when Zed is running in netcat mode +#[cfg(not(windows))] +pub fn main(socket: &str) -> Result<()> { + use futures::{AsyncReadExt as _, AsyncWriteExt as _, FutureExt as _, io::BufReader, select}; + use net::async_net::UnixStream; + use smol::{Async, io::AsyncBufReadExt}; + + smol::block_on(async { + let socket_stream = UnixStream::connect(socket).await?; + let (socket_read, mut socket_write) = socket_stream.split(); + let mut socket_reader = BufReader::new(socket_read); + + let mut stdout = Async::new(std::io::stdout())?; + let stdin = Async::new(std::io::stdin())?; + let mut stdin_reader = BufReader::new(stdin); + + let mut socket_line = Vec::new(); + let mut stdin_line = Vec::new(); + + loop { + select! { + bytes_read = socket_reader.read_until(b'\n', &mut socket_line).fuse() => { + if bytes_read? == 0 { + break + } + stdout.write_all(&socket_line).await?; + stdout.flush().await?; + socket_line.clear(); + } + bytes_read = stdin_reader.read_until(b'\n', &mut stdin_line).fuse() => { + if bytes_read? == 0 { + break + } + socket_write.write_all(&stdin_line).await?; + socket_write.flush().await?; + stdin_line.clear(); + } + } + } + + anyhow::Ok(()) + }) +} diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index ae96a48b5319231586f862eb5f88d41b48f37b51..bbceb3f1019fe3a86ba2c853cbd6a364594574e2 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -95,6 +95,7 @@ svg_preview.workspace = true menu.workspace = true migrator.workspace = true mimalloc = { version = "0.1", optional = true } +nc.workspace = true nix = { workspace = true, features = ["pthread", "signal"] } node_runtime.workspace = true notifications.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 5eb96f21a4b13e5316863af0f7a20703f5506ee2..89b9fad6bf12c3529d4c695df6592d1d906fac8b 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -175,6 +175,17 @@ pub fn main() { return; } + // `zed --nc` Makes zed operate in nc/netcat mode for use with MCP + if let Some(socket) = &args.nc { + match nc::main(socket) { + Ok(()) => return, + Err(err) => { + eprintln!("Error: {}", err); + process::exit(1); + } + } + } + // `zed --printenv` Outputs environment variables as JSON to stdout if args.printenv { util::shell_env::print_env(); @@ -1168,6 +1179,11 @@ struct Args { #[arg(long, hide = true)] askpass: Option<String>, + /// Used for the MCP Server, to remove the need for netcat as a dependency, + /// by having Zed act like netcat communicating over a Unix socket. + #[arg(long, hide = true)] + nc: Option<String>, + /// Run zed in the foreground, only used on Windows, to match the behavior on macOS. #[arg(long)] #[cfg(target_os = "windows")] From b0eac4267d3f0d69657593dd04fa970e1128b00f Mon Sep 17 00:00:00 2001 From: Ben Brandt <benjamin.j.brandt@gmail.com> Date: Thu, 17 Jul 2025 17:01:02 +0200 Subject: [PATCH 153/658] Mark glob/grep as code blocks (#34628) Release Notes: - N/A --- crates/agent_servers/src/claude/tools.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/agent_servers/src/claude/tools.rs b/crates/agent_servers/src/claude/tools.rs index 89d42c0daa229e60a9426b5d5101dd44dca06abc..e3ac6c14e229d81d3d26bae3fc5e2d1bb8f6a189 100644 --- a/crates/agent_servers/src/claude/tools.rs +++ b/crates/agent_servers/src/claude/tools.rs @@ -97,10 +97,10 @@ impl ClaudeTool { } Self::Write(None) => "Write".into(), Self::Glob(Some(params)) => { - format!("Glob {params}") + format!("Glob `{params}`") } Self::Glob(None) => "Glob".into(), - Self::Grep(Some(params)) => params.to_string(), + Self::Grep(Some(params)) => format!("`{params}`"), Self::Grep(None) => "Grep".into(), Self::WebFetch(Some(params)) => format!("Fetch {}", params.url), Self::WebFetch(None) => "Fetch".into(), From 948c1f22bb67152306b137dd8c4b31c5bbad076b Mon Sep 17 00:00:00 2001 From: Ben Kunkle <ben@zed.dev> Date: Thu, 17 Jul 2025 10:10:07 -0500 Subject: [PATCH 154/658] keymap_ui: Improve keybind display in menus (#34587) Closes #ISSUE Defines keybindings for `keymap_editor::EditBinding` and `keymap_editor::CreateBinding`, making sure those actions are used in tooltips. Release Notes: - N/A *or* Added/Fixed/Improved ... --------- Co-authored-by: Finn <dev@bahn.sh> --- assets/keymaps/default-linux.json | 4 +- assets/keymaps/default-macos.json | 4 +- crates/settings_ui/src/keybindings.rs | 118 +++++++++++++---------- crates/ui/src/components/context_menu.rs | 6 +- 4 files changed, 75 insertions(+), 57 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index b52b6c614d0643a1d8f7eb84251e5a0bf9a12132..ebc88ec135e53d6fee8ff3668f400d34f993abe9 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -1118,7 +1118,9 @@ "ctrl-f": "search::FocusSearch", "alt-find": "keymap_editor::ToggleKeystrokeSearch", "alt-ctrl-f": "keymap_editor::ToggleKeystrokeSearch", - "alt-c": "keymap_editor::ToggleConflictFilter" + "alt-c": "keymap_editor::ToggleConflictFilter", + "enter": "keymap_editor::EditBinding", + "alt-enter": "keymap_editor::CreateBinding" } }, { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 240b42fd1f23c38418427c01b1203847de388aac..cec485ce883d43404acfe62f7974ff3759cad207 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -1217,7 +1217,9 @@ "use_key_equivalents": true, "bindings": { "cmd-alt-f": "keymap_editor::ToggleKeystrokeSearch", - "cmd-alt-c": "keymap_editor::ToggleConflictFilter" + "cmd-alt-c": "keymap_editor::ToggleConflictFilter", + "enter": "keymap_editor::EditBinding", + "alt-enter": "keymap_editor::CreateBinding" } }, { diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 21eec538b35b108c2d3884aca2edc6c5f4ac1cd7..6b438ede02bb11673a887355504c354acb709c00 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -683,7 +683,7 @@ impl KeymapEditor { .detach_and_log_err(cx); } - fn dispatch_context(&self, _window: &Window, _cx: &Context<Self>) -> KeyContext { + fn key_context(&self) -> KeyContext { let mut dispatch_context = KeyContext::new_with_defaults(); dispatch_context.add("KeymapEditor"); dispatch_context.add("menu"); @@ -718,14 +718,19 @@ impl KeymapEditor { self.selected_index.take(); } - fn selected_keybind_idx(&self) -> Option<usize> { + fn selected_keybind_index(&self) -> Option<usize> { self.selected_index .and_then(|match_index| self.matches.get(match_index)) .map(|r#match| r#match.candidate_id) } + fn selected_keybind_and_index(&self) -> Option<(&ProcessedKeybinding, usize)> { + self.selected_keybind_index() + .map(|keybind_index| (&self.keybindings[keybind_index], keybind_index)) + } + fn selected_binding(&self) -> Option<&ProcessedKeybinding> { - self.selected_keybind_idx() + self.selected_keybind_index() .and_then(|keybind_index| self.keybindings.get(keybind_index)) } @@ -757,40 +762,41 @@ impl KeymapEditor { let selected_binding_is_unbound = selected_binding.keystrokes().is_none(); let context_menu = ContextMenu::build(window, cx, |menu, _window, _cx| { - menu.action_disabled_when( - selected_binding_is_unbound, - "Edit", - Box::new(EditBinding), - ) - .action("Create", Box::new(CreateBinding)) - .action_disabled_when( - selected_binding_is_unbound, - "Delete", - Box::new(DeleteBinding), - ) - .separator() - .action("Copy Action", Box::new(CopyAction)) - .action_disabled_when( - selected_binding_has_no_context, - "Copy Context", - Box::new(CopyContext), - ) - .entry("Show matching keybindings", None, { - let weak = weak.clone(); - let key_strokes = key_strokes.clone(); + menu.context(self.focus_handle.clone()) + .action_disabled_when( + selected_binding_is_unbound, + "Edit", + Box::new(EditBinding), + ) + .action("Create", Box::new(CreateBinding)) + .action_disabled_when( + selected_binding_is_unbound, + "Delete", + Box::new(DeleteBinding), + ) + .separator() + .action("Copy Action", Box::new(CopyAction)) + .action_disabled_when( + selected_binding_has_no_context, + "Copy Context", + Box::new(CopyContext), + ) + .entry("Show matching keybindings", None, { + let weak = weak.clone(); + let key_strokes = key_strokes.clone(); - move |_, cx| { - weak.update(cx, |this, cx| { - this.filter_state = FilterState::All; - this.search_mode = SearchMode::KeyStroke { exact_match: true }; + move |_, cx| { + weak.update(cx, |this, cx| { + this.filter_state = FilterState::All; + this.search_mode = SearchMode::KeyStroke { exact_match: true }; - this.keystroke_editor.update(cx, |editor, cx| { - editor.set_keystrokes(key_strokes.clone(), cx); - }); - }) - .ok(); - } - }) + this.keystroke_editor.update(cx, |editor, cx| { + editor.set_keystrokes(key_strokes.clone(), cx); + }); + }) + .ok(); + } + }) }); let context_menu_handle = context_menu.focus_handle(cx); @@ -880,22 +886,16 @@ impl KeymapEditor { } } - fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) { - self.open_edit_keybinding_modal(false, window, cx); - } - fn open_edit_keybinding_modal( &mut self, create: bool, window: &mut Window, cx: &mut Context<Self>, ) { - let Some((keybind_idx, keybind)) = self - .selected_keybind_idx() - .zip(self.selected_binding().cloned()) - else { + let Some((keybind, keybind_index)) = self.selected_keybind_and_index() else { return; }; + let keybind = keybind.clone(); let keymap_editor = cx.entity(); let arguments = keybind @@ -925,7 +925,7 @@ impl KeymapEditor { let modal = KeybindingEditorModal::new( create, keybind, - keybind_idx, + keybind_index, keymap_editor, workspace_weak, fs, @@ -1142,20 +1142,19 @@ impl Item for KeymapEditor { } impl Render for KeymapEditor { - fn render(&mut self, window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement { + fn render(&mut self, _window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement { let row_count = self.matches.len(); let theme = cx.theme(); v_flex() .id("keymap-editor") .track_focus(&self.focus_handle) - .key_context(self.dispatch_context(window, cx)) + .key_context(self.key_context()) .on_action(cx.listener(Self::select_next)) .on_action(cx.listener(Self::select_previous)) .on_action(cx.listener(Self::select_first)) .on_action(cx.listener(Self::select_last)) .on_action(cx.listener(Self::focus_search)) - .on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::edit_binding)) .on_action(cx.listener(Self::create_binding)) .on_action(cx.listener(Self::delete_binding)) @@ -1269,6 +1268,18 @@ impl Render for KeymapEditor { "keystrokes-exact-match", IconName::Equal, ) + .tooltip(move |window, cx| { + Tooltip::for_action( + if exact_match { + "Partial match mode" + } else { + "Exact match mode" + }, + &ToggleExactKeystrokeMatching, + window, + cx, + ) + }) .shape(IconButtonShape::Square) .toggle_state(exact_match) .on_click( @@ -1316,9 +1327,9 @@ impl Render for KeymapEditor { .icon_color(Color::Warning) .tooltip(|window, cx| { Tooltip::with_meta( - "Edit Keybinding", - None, - "Use alt+click to show conflicts", + "View conflicts", + Some(&ToggleConflictFilter), + "Use alt+click to show all conflicts", window, cx, ) @@ -1343,7 +1354,10 @@ impl Render for KeymapEditor { .unwrap_or_else(|| { base_button_style(index, IconName::Pencil) .visible_on_hover(row_group_id(index)) - .tooltip(Tooltip::text("Edit Keybinding")) + .tooltip(Tooltip::for_action_title( + "Edit Keybinding", + &EditBinding, + )) .on_click(cx.listener(move |this, _, window, cx| { this.select_index(index, cx); this.open_edit_keybinding_modal(false, window, cx); @@ -2545,6 +2559,8 @@ impl KeystrokeInput { if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX { self.keystrokes.push(Self::dummy(keystroke.modifiers)); } + } else if close_keystroke_result != CloseKeystrokeResult::Partial { + self.clear_keystrokes(&ClearKeystrokes, window, cx); } } self.keystrokes_changed(cx); diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 3ba73f6dff54d919622d7961bdd8adbb8f9df8b6..467dd226fbffb4fe54d7e7564c736431b8be094b 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -972,12 +972,10 @@ impl ContextMenu { .children(action.as_ref().and_then(|action| { self.action_context .as_ref() - .map(|focus| { + .and_then(|focus| { KeyBinding::for_action_in(&**action, focus, window, cx) }) - .unwrap_or_else(|| { - KeyBinding::for_action(&**action, window, cx) - }) + .or_else(|| KeyBinding::for_action(&**action, window, cx)) .map(|binding| { div().ml_4().child(binding.disabled(*disabled)).when( *disabled && documentation_aside.is_some(), From b94649ce1eca2a524852eeda0d955468158f54f2 Mon Sep 17 00:00:00 2001 From: Ben Kunkle <ben@zed.dev> Date: Thu, 17 Jul 2025 10:34:34 -0500 Subject: [PATCH 155/658] keymap_ui: Show edit icon on hovered and selected row (#34630) Closes #ISSUE Improves the behavior of the edit icon in the far left column of the keymap UI table. It is now shown in both the selected and the hovered row as an indicator that the row is editable in this configuration. When hovered a row can be double clicked or the edit icon can be clicked, and when selected it can be edited via keyboard shortcuts. Additionally, the edit icon and all other hover tooltips will now disappear when the table is navigated via keyboard shortcuts. <details><summary>Video</summary> https://github.com/user-attachments/assets/6584810f-4c6d-4e6f-bdca-25b16c920cfc </details> Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/settings_ui/src/keybindings.rs | 98 ++++++++++++++++++--------- 1 file changed, 65 insertions(+), 33 deletions(-) diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 6b438ede02bb11673a887355504c354acb709c00..af096f4ce1cc1d110d4775e62ab78e963a5dcaa6 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -268,6 +268,7 @@ struct KeymapEditor { context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>, previous_edit: Option<PreviousEdit>, humanized_action_names: HashMap<&'static str, SharedString>, + show_hover_menus: bool, } enum PreviousEdit { @@ -357,6 +358,7 @@ impl KeymapEditor { previous_edit: None, humanized_action_names, search_query_debounce: None, + show_hover_menus: true, }; this.on_keymap_changed(cx); @@ -825,6 +827,7 @@ impl KeymapEditor { } fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) { + self.show_hover_menus = false; if let Some(selected) = self.selected_index { let selected = selected + 1; if selected >= self.matches.len() { @@ -845,6 +848,7 @@ impl KeymapEditor { window: &mut Window, cx: &mut Context<Self>, ) { + self.show_hover_menus = false; if let Some(selected) = self.selected_index { if selected == 0 { return; @@ -870,6 +874,7 @@ impl KeymapEditor { _window: &mut Window, cx: &mut Context<Self>, ) { + self.show_hover_menus = false; if self.matches.get(0).is_some() { self.selected_index = Some(0); self.scroll_to_item(0, ScrollStrategy::Center, cx); @@ -878,6 +883,7 @@ impl KeymapEditor { } fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) { + self.show_hover_menus = false; if self.matches.last().is_some() { let index = self.matches.len() - 1; self.selected_index = Some(index); @@ -892,6 +898,7 @@ impl KeymapEditor { window: &mut Window, cx: &mut Context<Self>, ) { + self.show_hover_menus = false; let Some((keybind, keybind_index)) = self.selected_keybind_and_index() else { return; }; @@ -1167,6 +1174,9 @@ impl Render for KeymapEditor { .p_2() .gap_1() .bg(theme.colors().editor_background) + .on_mouse_move(cx.listener(|this, _, _window, _cx| { + this.show_hover_menus = true; + })) .child( v_flex() .p_2() @@ -1320,9 +1330,9 @@ impl Render for KeymapEditor { let binding = &this.keybindings[candidate_id]; let action_name = binding.action_name; - let icon = (this.filter_state != FilterState::Conflicts - && this.has_conflict(index)) - .then(|| { + let icon = if this.filter_state != FilterState::Conflicts + && this.has_conflict(index) + { base_button_style(index, IconName::Warning) .icon_color(Color::Warning) .tooltip(|window, cx| { @@ -1350,21 +1360,34 @@ impl Render for KeymapEditor { } }, )) - }) - .unwrap_or_else(|| { + .into_any_element() + } else { base_button_style(index, IconName::Pencil) - .visible_on_hover(row_group_id(index)) - .tooltip(Tooltip::for_action_title( - "Edit Keybinding", - &EditBinding, - )) + .visible_on_hover( + if this.selected_index == Some(index) { + "".into() + } else if this.show_hover_menus { + row_group_id(index) + } else { + "never-show".into() + }, + ) + .when( + this.show_hover_menus && !context_menu_deployed, + |this| { + this.tooltip(Tooltip::for_action_title( + "Edit Keybinding", + &EditBinding, + )) + }, + ) .on_click(cx.listener(move |this, _, window, cx| { this.select_index(index, cx); this.open_edit_keybinding_modal(false, window, cx); cx.stop_propagation(); })) - }) - .into_any_element(); + .into_any_element() + }; let action = div() .id(("keymap action", index)) @@ -1382,20 +1405,24 @@ impl Render for KeymapEditor { .into_any_element() } }) - .when(!context_menu_deployed, |this| { - this.tooltip({ - let action_name = binding.action_name; - let action_docs = binding.action_docs; - move |_, cx| { - let action_tooltip = Tooltip::new(action_name); - let action_tooltip = match action_docs { - Some(docs) => action_tooltip.meta(docs), - None => action_tooltip, - }; - cx.new(|_| action_tooltip).into() - } - }) - }) + .when( + !context_menu_deployed && this.show_hover_menus, + |this| { + this.tooltip({ + let action_name = binding.action_name; + let action_docs = binding.action_docs; + move |_, cx| { + let action_tooltip = + Tooltip::new(action_name); + let action_tooltip = match action_docs { + Some(docs) => action_tooltip.meta(docs), + None => action_tooltip, + }; + cx.new(|_| action_tooltip).into() + } + }) + }, + ) .into_any_element(); let keystrokes = binding.ui_key_binding.clone().map_or( binding.keystroke_text.clone().into_any_element(), @@ -1420,13 +1447,18 @@ impl Render for KeymapEditor { div() .id(("keymap context", index)) .child(context.clone()) - .when(is_local && !context_menu_deployed, |this| { - this.tooltip(Tooltip::element({ - move |_, _| { - context.clone().into_any_element() - } - })) - }) + .when( + is_local + && !context_menu_deployed + && this.show_hover_menus, + |this| { + this.tooltip(Tooltip::element({ + move |_, _| { + context.clone().into_any_element() + } + })) + }, + ) .into_any_element() }, ); From b4dc7f8a8aadd0a2f12650a0d393f7f9ae3ed29a Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Thu, 17 Jul 2025 21:14:40 +0530 Subject: [PATCH 156/658] debugger: Add support for running test methods with function receiver in Go (#34613) ![CleanShot 2025-07-17 at 16 35 10](https://github.com/user-attachments/assets/bad794fb-198e-40a1-958c-6ff30a0a4e53) Closes #33759 Release Notes: - debugger: Add support for running test methods with function receiver in Go Signed-off-by: Umesh Yadav <git@umesh.dev> --- crates/languages/src/go/runnables.scm | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/crates/languages/src/go/runnables.scm b/crates/languages/src/go/runnables.scm index 8d5f4375c137e8e804f4af8647eccddef09bdb42..bdeb77b46c83fce079ea43d413c3a9ef231a5a8c 100644 --- a/crates/languages/src/go/runnables.scm +++ b/crates/languages/src/go/runnables.scm @@ -1,9 +1,21 @@ ; Functions names start with `Test` ( - ( + [ (function_declaration name: (_) @run (#match? @run "^Test.*")) - ) @_ + (method_declaration + receiver: (parameter_list + (parameter_declaration + name: (identifier) @_receiver_name + type: [ + (pointer_type (type_identifier) @_receiver_type) + (type_identifier) @_receiver_type + ] + ) + ) + name: (field_identifier) @run @_method_name + (#match? @_method_name "^Test.*")) + ] @_ (#set! tag go-test) ) From 8980526a859cc7ee3d9b120a6df6f966c8fef165 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 17 Jul 2025 17:58:54 +0200 Subject: [PATCH 157/658] chore: Bump lsp-types rev (#34345) Closes #ISSUE Release Notes: - N/A --- Cargo.lock | 2 +- Cargo.toml | 2 +- crates/lsp/src/lsp.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 540e3039ef042adcf67a35e543fb39b21fdb8e97..ea7bfa0a3821d8e6e462f53a4a65c7cf40d6263c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9704,7 +9704,7 @@ dependencies = [ [[package]] name = "lsp-types" version = "0.95.1" -source = "git+https://github.com/zed-industries/lsp-types?rev=6add7052b598ea1f40f7e8913622c3958b009b60#6add7052b598ea1f40f7e8913622c3958b009b60" +source = "git+https://github.com/zed-industries/lsp-types?rev=39f629bdd03d59abd786ed9fc27e8bca02c0c0ec#39f629bdd03d59abd786ed9fc27e8bca02c0c0ec" dependencies = [ "bitflags 1.3.2", "serde", diff --git a/Cargo.toml b/Cargo.toml index 1c79f4c1c87c5ea12cc7fe4ff2ba10cb43459806..1be2eb8d773d4168a3eec9f2c7d35e884699aa93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -496,7 +496,7 @@ libc = "0.2" 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 = "6add7052b598ea1f40f7e8913622c3958b009b60" } +lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "39f629bdd03d59abd786ed9fc27e8bca02c0c0ec" } markup5ever_rcdom = "0.3.0" metal = "0.29" moka = { version = "0.12.10", features = ["sync"] } diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 4248f910eedd2b9a242365569318ad0d9b32510b..7dcfa61f471680b6a0753f3002d723b7b8194935 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -633,7 +633,7 @@ impl LanguageServer { inlay_hint: Some(InlayHintWorkspaceClientCapabilities { refresh_support: Some(true), }), - diagnostic: Some(DiagnosticWorkspaceClientCapabilities { + diagnostics: Some(DiagnosticWorkspaceClientCapabilities { refresh_support: Some(true), }) .filter(|_| pull_diagnostics), From ae0d4f6a0d58397355896fde3b01b0e3d6a50fd3 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Thu, 17 Jul 2025 13:05:58 -0400 Subject: [PATCH 158/658] debugger: Add data breakpoint access type support (#34639) Release Notes: - Support specifying a data breakpoint's access type - Read, Write, Read & Write --- Cargo.lock | 1 + crates/debugger_ui/Cargo.toml | 11 ++-- crates/debugger_ui/src/debugger_ui.rs | 20 +++++- .../src/session/running/memory_view.rs | 2 +- .../src/session/running/variable_list.rs | 61 ++++++++++++++++--- 5 files changed, 76 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ea7bfa0a3821d8e6e462f53a4a65c7cf40d6263c..a8a8d12e37a08a2db3ce53525041366834772e5c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4427,6 +4427,7 @@ dependencies = [ "pretty_assertions", "project", "rpc", + "schemars", "serde", "serde_json", "serde_json_lenient", diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index ebb135c1d9fc56e21b40bd0a4f9850d72286d866..df4125860f4ab79ce3a55d6b5b4fbb8f8fc64e5e 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -35,6 +35,7 @@ command_palette_hooks.workspace = true dap.workspace = true dap_adapters = { workspace = true, optional = true } db.workspace = true +debugger_tools.workspace = true editor.workspace = true file_icons.workspace = true futures.workspace = true @@ -54,6 +55,7 @@ picker.workspace = true pretty_assertions.workspace = true project.workspace = true rpc.workspace = true +schemars.workspace = true serde.workspace = true serde_json.workspace = true serde_json_lenient.workspace = true @@ -66,14 +68,13 @@ telemetry.workspace = true terminal_view.workspace = true text.workspace = true theme.workspace = true -tree-sitter.workspace = true tree-sitter-json.workspace = true +tree-sitter.workspace = true ui.workspace = true +unindent = { workspace = true, optional = true } util.workspace = true -workspace.workspace = true workspace-hack.workspace = true -debugger_tools.workspace = true -unindent = { workspace = true, optional = true } +workspace.workspace = true zed_actions.workspace = true [dev-dependencies] @@ -83,8 +84,8 @@ debugger_tools = { workspace = true, features = ["test-support"] } editor = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } project = { workspace = true, features = ["test-support"] } +tree-sitter-go.workspace = true unindent.workspace = true util = { workspace = true, features = ["test-support"] } workspace = { workspace = true, features = ["test-support"] } zlog.workspace = true -tree-sitter-go.workspace = true diff --git a/crates/debugger_ui/src/debugger_ui.rs b/crates/debugger_ui/src/debugger_ui.rs index c932f1b600effa424db6b995d8128dca5c29594f..9eac59af83d6a9255fd11202fc5d30969919fd01 100644 --- a/crates/debugger_ui/src/debugger_ui.rs +++ b/crates/debugger_ui/src/debugger_ui.rs @@ -3,10 +3,12 @@ use std::any::TypeId; use dap::debugger_settings::DebuggerSettings; use debugger_panel::DebugPanel; use editor::Editor; -use gpui::{App, DispatchPhase, EntityInputHandler, actions}; +use gpui::{Action, App, DispatchPhase, EntityInputHandler, actions}; use new_process_modal::{NewProcessModal, NewProcessMode}; use onboarding_modal::DebuggerOnboardingModal; use project::debugger::{self, breakpoint_store::SourceBreakpoint, session::ThreadStatus}; +use schemars::JsonSchema; +use serde::Deserialize; use session::DebugSession; use settings::Settings; use stack_trace_view::StackTraceView; @@ -83,11 +85,23 @@ actions!( Rerun, /// Toggles expansion of the selected item in the debugger UI. ToggleExpandItem, - /// Set a data breakpoint on the selected variable or memory region. - ToggleDataBreakpoint, ] ); +/// Extends selection down by a specified number of lines. +#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] +#[action(namespace = debugger)] +#[serde(deny_unknown_fields)] +/// Set a data breakpoint on the selected variable or memory region. +pub struct ToggleDataBreakpoint { + /// The type of data breakpoint + /// Read & Write + /// Read + /// Write + #[serde(default)] + pub access_type: Option<dap::DataBreakpointAccessType>, +} + actions!( dev, [ diff --git a/crates/debugger_ui/src/session/running/memory_view.rs b/crates/debugger_ui/src/session/running/memory_view.rs index 499091ca0fe687934d8c386577bf3157f68c96ff..7b62a1d55d7403befac35521ab56f5b8dc845aa8 100644 --- a/crates/debugger_ui/src/session/running/memory_view.rs +++ b/crates/debugger_ui/src/session/running/memory_view.rs @@ -688,7 +688,7 @@ impl MemoryView { menu = menu.action_disabled_when( *memory_unreadable, "Set Data Breakpoint", - ToggleDataBreakpoint.boxed_clone(), + ToggleDataBreakpoint { access_type: None }.boxed_clone(), ); } menu.context(self.focus_handle.clone()) diff --git a/crates/debugger_ui/src/session/running/variable_list.rs b/crates/debugger_ui/src/session/running/variable_list.rs index b158314b507317227a6b94c8916c7a2c125f2380..906e482687db5f03fea3803bcb56567c6b68c024 100644 --- a/crates/debugger_ui/src/session/running/variable_list.rs +++ b/crates/debugger_ui/src/session/running/variable_list.rs @@ -670,9 +670,9 @@ impl VariableList { let focus_handle = self.focus_handle.clone(); cx.spawn_in(window, async move |this, cx| { let can_toggle_data_breakpoint = if let Some(task) = can_toggle_data_breakpoint { - task.await.is_some() + task.await } else { - true + None }; cx.update(|window, cx| { let context_menu = ContextMenu::build(window, cx, |menu, _, _| { @@ -686,11 +686,35 @@ impl VariableList { menu.action("Go To Memory", GoToMemory.boxed_clone()) }) .action("Watch Variable", AddWatch.boxed_clone()) - .when(can_toggle_data_breakpoint, |menu| { - menu.action( - "Toggle Data Breakpoint", - crate::ToggleDataBreakpoint.boxed_clone(), - ) + .when_some(can_toggle_data_breakpoint, |mut menu, data_info| { + menu = menu.separator(); + if let Some(access_types) = data_info.access_types { + for access in access_types { + menu = menu.action( + format!( + "Toggle {} Data Breakpoint", + match access { + dap::DataBreakpointAccessType::Read => "Read", + dap::DataBreakpointAccessType::Write => "Write", + dap::DataBreakpointAccessType::ReadWrite => + "Read/Write", + } + ), + crate::ToggleDataBreakpoint { + access_type: Some(access), + } + .boxed_clone(), + ); + } + + menu + } else { + menu.action( + "Toggle Data Breakpoint", + crate::ToggleDataBreakpoint { access_type: None } + .boxed_clone(), + ) + } }) }) .when(entry.as_watcher().is_some(), |menu| { @@ -729,7 +753,7 @@ impl VariableList { fn toggle_data_breakpoint( &mut self, - _: &crate::ToggleDataBreakpoint, + data_info: &crate::ToggleDataBreakpoint, _window: &mut Window, cx: &mut Context<Self>, ) { @@ -759,17 +783,34 @@ impl VariableList { }); let session = self.session.downgrade(); + let access_type = data_info.access_type; cx.spawn(async move |_, cx| { - let Some(data_id) = data_breakpoint.await.and_then(|info| info.data_id) else { + let Some((data_id, access_types)) = data_breakpoint + .await + .and_then(|info| Some((info.data_id?, info.access_types))) + else { return; }; + + // Because user's can manually add this action to the keymap + // we check if access type is supported + let access_type = match access_types { + None => None, + Some(access_types) => { + if access_type.is_some_and(|access_type| access_types.contains(&access_type)) { + access_type + } else { + None + } + } + }; _ = session.update(cx, |session, cx| { session.create_data_breakpoint( context, data_id.clone(), dap::DataBreakpoint { data_id, - access_type: None, + access_type, condition: None, hit_condition: None, }, From 1ceda2babd71a07049dfce85dc64efe779bb5935 Mon Sep 17 00:00:00 2001 From: Peter Tripp <peter@zed.dev> Date: Thu, 17 Jul 2025 13:14:24 -0400 Subject: [PATCH 159/658] JetBrains keymap improvements (July 2025) (#34641) Closes: https://github.com/zed-industries/zed/issues/14639 Closes: https://github.com/zed-industries/zed/issues/33020 If would have ideas for future enhancements, please see: - https://github.com/zed-industries/zed/discussions/34643 Various Jetbrains keymaps improvements for macOS and Linux/Windows: | Area | Action | macOS | Linux | | ------------- | -------------------------- | --------------------------------- | --------------------------------- | | Workspace | Toggle Git Panel | `cmd-0` | `ctrl-0` | | Workspace | Toggle Project Panel | `cmd-1` | `alt-0` | | Workspace | Toggle Debug Panel | `cmd-5` | `alt-1` | | Workspace | Toggle Diagnostics | `cmd-6` | `alt-6` | | Workspace | Toggle Outline Panel | `cmd-7` | `alt-7` | | Workspace | Toggle Terminal Panel | `alt-f12` | `alt-f12` | | Workspace | File Finder | `cmd-e` | `ctrl-e` | | Workspace | Task Spawn | `ctrl-alt-r` | `alt-shift-f10` | | Workspace | Close All Docks | `ctrl-shift-f12` | `ctrl-shift-f12` | | Project Panel | Search in Directory | `cmd-shift-f` | `ctrl-shift-f` | | Search | Replace in Files | `cmd-shift-r` | `ctrl-shift-r` | | Search | Replace in Buffer | `cmd-r` | `ctrl-r` | | Search | Toggle Case Sensitive | `ctrl-alt-c` / `alt-c` | `ctrl-alt-c` | | Search | Toggle Search in Selection | `ctrl-alt-s` / `alt-s` | `ctrl-alt-s` | | Search | Toggle Regex | `ctrl-alt-x` / `alt-x` | `ctrl-alt-x` | | Search | Toggle Whole Word | `ctrl-alt-w` / `alt-w` | `ctrl-alt-w` | | Terminal | New Terminal Tab | `cmd-t` | `ctrl-shift-t` | | Terminal | Scroll Line | `cmd-up` / `cmd-down` | `ctrl-up` / `ctrl-down` | | Terminal | Scroll Page | `shift-pageup` / `shift-pagedown` | `shift-pageup` / `shift-pagedown` | | Git | Git Panel | `cmd-k` | `ctrl-k` | | Git | Git Push | `cmd-shift-k` | `ctrl-shift-k` | In addition, with the help of the recently merged https://github.com/zed-industries/zed/pull/34495, no matter where you are mashing `escape` will refocus you back to your most recent editor buffer similar to the behavior of JetBrains. Release Notes: - jetbrains: Added 25+ keybinds to the macOS and Linux/Windows JetBrains compatibility keymaps --- assets/keymaps/linux/jetbrains.json | 53 +++++++++++++++++++++++++++-- assets/keymaps/macos/jetbrains.json | 53 ++++++++++++++++++++++++++--- 2 files changed, 98 insertions(+), 8 deletions(-) diff --git a/assets/keymaps/linux/jetbrains.json b/assets/keymaps/linux/jetbrains.json index dbf50b0fcefa99063f0c8f535bd453aade4d7e56..7266318a9ea82b420b4c04e66f97e2cb006094c0 100644 --- a/assets/keymaps/linux/jetbrains.json +++ b/assets/keymaps/linux/jetbrains.json @@ -66,22 +66,46 @@ "context": "Editor && mode == full", "bindings": { "ctrl-f12": "outline::Toggle", - "alt-7": "outline::Toggle", + "ctrl-r": ["buffer_search::Deploy", { "replace_enabled": true }], "ctrl-shift-n": "file_finder::Toggle", "ctrl-g": "go_to_line::Toggle", "alt-enter": "editor::ToggleCodeActions" } }, + { + "context": "BufferSearchBar || ProjectSearchBar", + "bindings": { + "shift-enter": "search::SelectPreviousMatch", + "ctrl-alt-c": "search::ToggleCaseSensitive", + "ctrl-alt-e": "search::ToggleSelection", + "ctrl-alt-w": "search::ToggleWholeWord", + "ctrl-alt-x": "search::ToggleRegex" + } + }, { "context": "Workspace", "bindings": { + "ctrl-shift-f12": "workspace::CloseAllDocks", + "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-shift-n": "file_finder::Toggle", "ctrl-shift-a": "command_palette::Toggle", "shift shift": "command_palette::Toggle", "ctrl-alt-shift-n": "project_symbols::Toggle", + "alt-0": "git_panel::ToggleFocus", "alt-1": "workspace::ToggleLeftDock", - "ctrl-e": "tab_switcher::Toggle", - "alt-6": "diagnostics::Deploy" + "alt-5": "debug_panel::ToggleFocus", + "alt-6": "diagnostics::Deploy", + "alt-7": "outline_panel::ToggleFocus" + } + }, + { + "context": "Workspace || Editor", + "bindings": { + "alt-f12": "terminal_panel::ToggleFocus", + "ctrl-shift-k": "git::Push" } }, { @@ -95,10 +119,33 @@ "context": "ProjectPanel", "bindings": { "enter": "project_panel::Open", + "ctrl-shift-f": "project_panel::NewSearchInDirectory", "backspace": ["project_panel::Trash", { "skip_prompt": false }], "delete": ["project_panel::Trash", { "skip_prompt": false }], "shift-delete": ["project_panel::Delete", { "skip_prompt": false }], "shift-f6": "project_panel::Rename" } + }, + { + "context": "Terminal", + "bindings": { + "ctrl-shift-t": "workspace::NewTerminal", + "alt-f12": "workspace::CloseActiveDock", + "alt-left": "pane::ActivatePreviousItem", + "alt-right": "pane::ActivateNextItem", + "ctrl-up": "terminal::ScrollLineUp", + "ctrl-down": "terminal::ScrollLineDown", + "shift-pageup": "terminal::ScrollPageUp", + "shift-pagedown": "terminal::ScrollPageDown" + } + }, + { "context": "GitPanel", "bindings": { "alt-0": "workspace::CloseActiveDock" } }, + { "context": "ProjectPanel", "bindings": { "alt-1": "workspace::CloseActiveDock" } }, + { "context": "DebugPanel", "bindings": { "alt-5": "workspace::CloseActiveDock" } }, + { "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)", + "bindings": { "escape": "editor::ToggleFocus" } } ] diff --git a/assets/keymaps/macos/jetbrains.json b/assets/keymaps/macos/jetbrains.json index 22c6f18383a32f5def1869b953e31baf665404b9..a41e39932feed0cbf4f280f9a3666f5e501cde39 100644 --- a/assets/keymaps/macos/jetbrains.json +++ b/assets/keymaps/macos/jetbrains.json @@ -3,6 +3,7 @@ "bindings": { "cmd-{": "pane::ActivatePreviousItem", "cmd-}": "pane::ActivateNextItem", + "cmd-0": "git_panel::ToggleFocus", // overrides `cmd-0` zoom reset "ctrl-f2": "debugger::Stop", "f6": "debugger::Pause", "f7": "debugger::StepInto", @@ -63,28 +64,50 @@ "context": "Editor && mode == full", "bindings": { "cmd-f12": "outline::Toggle", - "cmd-7": "outline::Toggle", + "cmd-r": ["buffer_search::Deploy", { "replace_enabled": true }], "cmd-shift-o": "file_finder::Toggle", "cmd-l": "go_to_line::Toggle", "alt-enter": "editor::ToggleCodeActions" } }, { - "context": "BufferSearchBar > Editor", + "context": "BufferSearchBar || ProjectSearchBar", "bindings": { - "shift-enter": "search::SelectPreviousMatch" + "shift-enter": "search::SelectPreviousMatch", + "alt-c": "search::ToggleCaseSensitive", + "alt-e": "search::ToggleSelection", + "alt-x": "search::ToggleRegex", + "alt-w": "search::ToggleWholeWord", + "ctrl-alt-c": "search::ToggleCaseSensitive", + "ctrl-alt-e": "search::ToggleSelection", + "ctrl-alt-w": "search::ToggleWholeWord", + "ctrl-alt-x": "search::ToggleRegex" } }, { "context": "Workspace", "bindings": { + "cmd-shift-f12": "workspace::CloseAllDocks", + "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-shift-o": "file_finder::Toggle", "cmd-shift-a": "command_palette::Toggle", "shift shift": "command_palette::Toggle", "cmd-alt-o": "project_symbols::Toggle", // JetBrains: Go to Symbol "cmd-o": "project_symbols::Toggle", // JetBrains: Go to Class - "cmd-1": "workspace::ToggleLeftDock", - "cmd-6": "diagnostics::Deploy" + "cmd-1": "project_panel::ToggleFocus", + "cmd-5": "debug_panel::ToggleFocus", + "cmd-6": "diagnostics::Deploy", + "cmd-7": "outline_panel::ToggleFocus" + } + }, + { + "context": "Workspace || Editor", + "bindings": { + "alt-f12": "terminal_panel::ToggleFocus", + "cmd-shift-k": "git::Push" } }, { @@ -98,11 +121,31 @@ "context": "ProjectPanel", "bindings": { "enter": "project_panel::Open", + "cmd-shift-f": "project_panel::NewSearchInDirectory", "cmd-backspace": ["project_panel::Trash", { "skip_prompt": false }], "backspace": ["project_panel::Trash", { "skip_prompt": false }], "delete": ["project_panel::Trash", { "skip_prompt": false }], "shift-delete": ["project_panel::Delete", { "skip_prompt": false }], "shift-f6": "project_panel::Rename" } + }, + { + "context": "Terminal", + "bindings": { + "cmd-t": "workspace::NewTerminal", + "alt-f12": "workspace::CloseActiveDock", + "cmd-up": "terminal::ScrollLineUp", + "cmd-down": "terminal::ScrollLineDown", + "shift-pageup": "terminal::ScrollPageUp", + "shift-pagedown": "terminal::ScrollPageDown" + } + }, + { "context": "GitPanel", "bindings": { "cmd-0": "workspace::CloseActiveDock" } }, + { "context": "DebugPanel", "bindings": { "cmd-5": "workspace::CloseActiveDock" } }, + { "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)", + "bindings": { "escape": "editor::ToggleFocus" } } ] From 0f72d7ed52bad0c8f6a5e504a2adf9670d726814 Mon Sep 17 00:00:00 2001 From: Smit Barmase <heysmitbarmase@gmail.com> Date: Thu, 17 Jul 2025 22:46:44 +0530 Subject: [PATCH 160/658] editor: Fix sometimes green (+) cursor style appearing when cmd-clicking in same buffer (#34638) Follow-up for https://github.com/zed-industries/zed/pull/34557 This PR clears the selection drag state on click, because mouse up doesn't trigger on click event because of `cx.stop_propagation`. The issue occurs with similar repro steps as mentioned in the attached PR. Release Notes: - Fixed the issue where the green (+) cursor style sometimes appears when navigating to the definition in buffer. --- 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 e77be3398ca0fcef9edf65a0a318f94bd21a4fc8..fef185bb156085655c8a144cb3d06c70d8558f2c 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -949,6 +949,7 @@ impl EditorElement { if !pending_nonempty_selections && hovered_link_modifier && text_hitbox.is_hovered(window) { let point = position_map.point_for_position(event.up.position); editor.handle_click_hovered_link(point, event.modifiers(), window, cx); + editor.selection_drag_state = SelectionDragState::None; cx.stop_propagation(); } From dab0b3509d2394cd073cbd8b4260454c1f1d5278 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga <agus@zed.dev> Date: Thu, 17 Jul 2025 14:34:38 -0300 Subject: [PATCH 161/658] Unify agent server settings and extract e2e tests out (#34642) Release Notes: - N/A --- crates/agent_servers/Cargo.toml | 2 +- crates/agent_servers/src/agent_servers.rs | 125 ++++- crates/agent_servers/src/claude.rs | 77 ++-- crates/agent_servers/src/e2e_tests.rs | 368 +++++++++++++++ crates/agent_servers/src/gemini.rs | 428 +----------------- crates/agent_servers/src/settings.rs | 1 + .../agent_servers/src/stdio_agent_server.rs | 54 +-- 7 files changed, 551 insertions(+), 504 deletions(-) create mode 100644 crates/agent_servers/src/e2e_tests.rs diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml index d65235aee38a71ffd17cfb9d799cbf62c4fecc8c..2d68148264c687444cb67129d502bda3c025f0fd 100644 --- a/crates/agent_servers/Cargo.toml +++ b/crates/agent_servers/Cargo.toml @@ -7,7 +7,7 @@ license = "GPL-3.0-or-later" [features] test-support = ["acp_thread/test-support", "gpui/test-support", "project/test-support"] -gemini = [] +e2e = [] [lints] workspace = true diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index ebebeca5111d95ace8d0671c837b5287bb1e11a4..6d9c77f2968d7b39302391829d2d14b6d4493a91 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -3,6 +3,9 @@ mod gemini; mod settings; mod stdio_agent_server; +#[cfg(test)] +mod e2e_tests; + pub use claude::*; pub use gemini::*; pub use settings::*; @@ -11,34 +14,20 @@ pub use stdio_agent_server::*; use acp_thread::AcpThread; use anyhow::Result; use collections::HashMap; -use gpui::{App, Entity, SharedString, Task}; +use gpui::{App, AsyncApp, Entity, SharedString, Task}; use project::Project; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use std::path::{Path, PathBuf}; +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; +use util::ResultExt as _; pub fn init(cx: &mut App) { settings::init(cx); } -#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)] -pub struct AgentServerCommand { - #[serde(rename = "command")] - pub path: PathBuf, - #[serde(default)] - pub args: Vec<String>, - pub env: Option<HashMap<String, String>>, -} - -pub enum AgentServerVersion { - Supported, - Unsupported { - error_message: SharedString, - upgrade_message: SharedString, - upgrade_command: String, - }, -} - pub trait AgentServer: Send { fn logo(&self) -> ui::IconName; fn name(&self) -> &'static str; @@ -78,3 +67,99 @@ impl std::fmt::Debug for AgentServerCommand { .finish() } } + +pub enum AgentServerVersion { + Supported, + Unsupported { + error_message: SharedString, + upgrade_message: SharedString, + upgrade_command: String, + }, +} + +#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)] +pub struct AgentServerCommand { + #[serde(rename = "command")] + pub path: PathBuf, + #[serde(default)] + pub args: Vec<String>, + pub env: Option<HashMap<String, String>>, +} + +impl AgentServerCommand { + pub(crate) async fn resolve( + path_bin_name: &'static str, + extra_args: &[&'static str], + settings: Option<AgentServerSettings>, + project: &Entity<Project>, + cx: &mut AsyncApp, + ) -> Option<Self> { + if let Some(agent_settings) = settings { + return Some(Self { + path: agent_settings.command.path, + args: agent_settings + .command + .args + .into_iter() + .chain(extra_args.iter().map(|arg| arg.to_string())) + .collect(), + env: agent_settings.command.env, + }); + } else { + find_bin_in_path(path_bin_name, project, cx) + .await + .map(|path| Self { + path, + args: extra_args.iter().map(|arg| arg.to_string()).collect(), + env: None, + }) + } + } +} + +async fn find_bin_in_path( + bin_name: &'static str, + project: &Entity<Project>, + cx: &mut AsyncApp, +) -> Option<PathBuf> { + let (env_task, root_dir) = project + .update(cx, |project, cx| { + let worktree = project.visible_worktrees(cx).next(); + match worktree { + Some(worktree) => { + let env_task = project.environment().update(cx, |env, cx| { + env.get_worktree_environment(worktree.clone(), cx) + }); + + let path = worktree.read(cx).abs_path(); + (env_task, path) + } + None => { + let path: Arc<Path> = paths::home_dir().as_path().into(); + let env_task = project.environment().update(cx, |env, cx| { + env.get_directory_environment(path.clone(), cx) + }); + (env_task, path) + } + } + }) + .log_err()?; + + cx.background_executor() + .spawn(async move { + let which_result = if cfg!(windows) { + which::which(bin_name) + } else { + let env = env_task.await.unwrap_or_default(); + let shell_path = env.get("PATH").cloned(); + which::which_in(bin_name, shell_path.as_ref(), root_dir.as_ref()) + }; + + if let Err(which::Error::CannotFindBinaryPath) = which_result { + return None; + } + + which_result.log_err() + }) + .await +} diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 897158dc5705c839c3b93a3f139956f479555c57..5760a96d8cf900d37aa249ffa05006df19690a34 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -3,6 +3,7 @@ mod tools; use collections::HashMap; use project::Project; +use settings::SettingsStore; use std::cell::RefCell; use std::fmt::Display; use std::path::Path; @@ -12,7 +13,7 @@ use agentic_coding_protocol::{ self as acp, AnyAgentRequest, AnyAgentResult, Client, ProtocolVersion, StreamAssistantMessageChunkParams, ToolCallContent, UpdateToolCallParams, }; -use anyhow::{Context as _, Result, anyhow}; +use anyhow::{Result, anyhow}; use futures::channel::oneshot; use futures::future::LocalBoxFuture; use futures::{AsyncBufReadExt, AsyncWriteExt}; @@ -28,7 +29,7 @@ use util::ResultExt; use crate::claude::mcp_server::ClaudeMcpServer; use crate::claude::tools::ClaudeTool; -use crate::{AgentServer, find_bin_in_path}; +use crate::{AgentServer, AgentServerCommand, AllAgentServersSettings}; use acp_thread::{AcpClientDelegate, AcpThread, AgentConnection}; #[derive(Clone)] @@ -87,31 +88,41 @@ impl AgentServer for ClaudeCode { .await?; mcp_config_file.flush().await?; - let command = find_bin_in_path("claude", &project, cx) - .await - .context("Failed to find claude binary")?; - - let mut child = util::command::new_smol_command(&command) - .args([ - "--input-format", - "stream-json", - "--output-format", - "stream-json", - "--print", - "--verbose", - "--mcp-config", - mcp_config_path.to_string_lossy().as_ref(), - "--permission-prompt-tool", - &format!( - "mcp__{}__{}", - mcp_server::SERVER_NAME, - mcp_server::PERMISSION_TOOL - ), - "--allowedTools", - "mcp__zed__Read,mcp__zed__Edit", - "--disallowedTools", - "Read,Edit", - ]) + let settings = cx.read_global(|settings: &SettingsStore, _| { + settings.get::<AllAgentServersSettings>(None).claude.clone() + })?; + + let Some(command) = + AgentServerCommand::resolve("claude", &[], settings, &project, cx).await + else { + anyhow::bail!("Failed to find claude binary"); + }; + + let mut child = util::command::new_smol_command(&command.path) + .args( + [ + "--input-format", + "stream-json", + "--output-format", + "stream-json", + "--print", + "--verbose", + "--mcp-config", + mcp_config_path.to_string_lossy().as_ref(), + "--permission-prompt-tool", + &format!( + "mcp__{}__{}", + mcp_server::SERVER_NAME, + mcp_server::PERMISSION_TOOL + ), + "--allowedTools", + "mcp__zed__Read,mcp__zed__Edit", + "--disallowedTools", + "Read,Edit", + ] + .into_iter() + .chain(command.args.iter().map(|arg| arg.as_str())), + ) .current_dir(root_dir) .stdin(std::process::Stdio::piped()) .stdout(std::process::Stdio::piped()) @@ -562,10 +573,20 @@ struct McpServerConfig { } #[cfg(test)] -mod tests { +pub(crate) mod tests { use super::*; use serde_json::json; + // crate::common_e2e_tests!(ClaudeCode); + + pub fn local_command() -> AgentServerCommand { + AgentServerCommand { + path: "claude".into(), + args: vec![], + env: None, + } + } + #[test] fn test_deserialize_content_untagged_text() { let json = json!("Hello, world!"); diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..923c6cdd6f1112233b45386c8a6a4db0ab380d65 --- /dev/null +++ b/crates/agent_servers/src/e2e_tests.rs @@ -0,0 +1,368 @@ +use std::{path::Path, sync::Arc, time::Duration}; + +use crate::{AgentServer, AgentServerSettings, AllAgentServersSettings}; +use acp_thread::{ + AcpThread, AgentThreadEntry, ToolCall, ToolCallConfirmation, ToolCallContent, ToolCallStatus, +}; +use agentic_coding_protocol as acp; +use futures::{FutureExt, StreamExt, channel::mpsc, select}; +use gpui::{Entity, TestAppContext}; +use indoc::indoc; +use project::{FakeFs, Project}; +use serde_json::json; +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; + + thread + .update(cx, |thread, cx| thread.send_raw("Hello from Zed!", cx)) + .await + .unwrap(); + + thread.read_with(cx, |thread, _| { + assert_eq!(thread.entries().len(), 2); + assert!(matches!( + thread.entries()[0], + AgentThreadEntry::UserMessage(_) + )); + assert!(matches!( + thread.entries()[1], + AgentThreadEntry::AssistantMessage(_) + )); + }); +} + +pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut TestAppContext) { + let _fs = init_test(cx).await; + + let tempdir = tempfile::tempdir().unwrap(); + std::fs::write( + tempdir.path().join("foo.rs"), + indoc! {" + fn main() { + println!(\"Hello, world!\"); + } + "}, + ) + .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; + thread + .update(cx, |thread, cx| { + thread.send( + acp::SendUserMessageParams { + chunks: vec![ + acp::UserMessageChunk::Text { + text: "Read the file ".into(), + }, + acp::UserMessageChunk::Path { + path: Path::new("foo.rs").into(), + }, + acp::UserMessageChunk::Text { + text: " and tell me what the content of the println! is".into(), + }, + ], + }, + cx, + ) + }) + .await + .unwrap(); + + thread.read_with(cx, |thread, cx| { + assert_eq!(thread.entries().len(), 3); + assert!(matches!( + thread.entries()[0], + AgentThreadEntry::UserMessage(_) + )); + assert!(matches!(thread.entries()[1], AgentThreadEntry::ToolCall(_))); + let AgentThreadEntry::AssistantMessage(assistant_message) = &thread.entries()[2] else { + panic!("Expected AssistantMessage") + }; + assert!( + assistant_message.to_markdown(cx).contains("Hello, world!"), + "unexpected assistant message: {:?}", + assistant_message.to_markdown(cx) + ); + }); +} + +pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestAppContext) { + let fs = init_test(cx).await; + fs.insert_tree( + path!("/private/tmp"), + json!({"foo": "Lorem ipsum dolor", "bar": "bar", "baz": "baz"}), + ) + .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; + + thread + .update(cx, |thread, cx| { + thread.send_raw( + "Read the '/private/tmp/foo' file and tell me what you see.", + cx, + ) + }) + .await + .unwrap(); + thread.read_with(cx, |thread, _cx| { + assert!(matches!( + &thread.entries()[2], + AgentThreadEntry::ToolCall(ToolCall { + status: ToolCallStatus::Allowed { .. }, + .. + }) + )); + + assert!(matches!( + thread.entries()[3], + AgentThreadEntry::AssistantMessage(_) + )); + }); +} + +pub async fn test_tool_call_with_confirmation( + server: impl AgentServer + 'static, + 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; + let full_turn = thread.update(cx, |thread, cx| { + thread.send_raw(r#"Run `echo "Hello, world!"`"#, cx) + }); + + run_until_first_tool_call(&thread, cx).await; + + let tool_call_id = thread.read_with(cx, |thread, _cx| { + let AgentThreadEntry::ToolCall(ToolCall { + id, + status: + ToolCallStatus::WaitingForConfirmation { + confirmation: ToolCallConfirmation::Execute { root_command, .. }, + .. + }, + .. + }) = &thread.entries()[2] + else { + panic!(); + }; + + assert_eq!(root_command, "echo"); + + *id + }); + + thread.update(cx, |thread, cx| { + thread.authorize_tool_call(tool_call_id, acp::ToolCallConfirmationOutcome::Allow, cx); + + assert!(matches!( + &thread.entries()[2], + AgentThreadEntry::ToolCall(ToolCall { + status: ToolCallStatus::Allowed { .. }, + .. + }) + )); + }); + + full_turn.await.unwrap(); + + thread.read_with(cx, |thread, cx| { + let AgentThreadEntry::ToolCall(ToolCall { + content: Some(ToolCallContent::Markdown { markdown }), + status: ToolCallStatus::Allowed { .. }, + .. + }) = &thread.entries()[2] + else { + panic!(); + }; + + markdown.read_with(cx, |md, _cx| { + assert!( + md.source().contains("Hello, world!"), + r#"Expected '{}' to contain "Hello, world!""#, + md.source() + ); + }); + }); +} + +pub async fn test_cancel(server: impl AgentServer + 'static, 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; + let full_turn = thread.update(cx, |thread, cx| { + thread.send_raw(r#"Run `echo "Hello, world!"`"#, cx) + }); + + let first_tool_call_ix = run_until_first_tool_call(&thread, cx).await; + + thread.read_with(cx, |thread, _cx| { + let AgentThreadEntry::ToolCall(ToolCall { + id, + status: + ToolCallStatus::WaitingForConfirmation { + confirmation: ToolCallConfirmation::Execute { root_command, .. }, + .. + }, + .. + }) = &thread.entries()[first_tool_call_ix] + else { + panic!("{:?}", thread.entries()[1]); + }; + + assert_eq!(root_command, "echo"); + + *id + }); + + thread + .update(cx, |thread, cx| thread.cancel(cx)) + .await + .unwrap(); + full_turn.await.unwrap(); + thread.read_with(cx, |thread, _| { + let AgentThreadEntry::ToolCall(ToolCall { + status: ToolCallStatus::Canceled, + .. + }) = &thread.entries()[first_tool_call_ix] + else { + panic!(); + }; + }); + + thread + .update(cx, |thread, cx| { + thread.send_raw(r#"Stop running and say goodbye to me."#, cx) + }) + .await + .unwrap(); + thread.read_with(cx, |thread, _| { + assert!(matches!( + &thread.entries().last().unwrap(), + AgentThreadEntry::AssistantMessage(..), + )) + }); +} + +#[macro_export] +macro_rules! common_e2e_tests { + ($server:expr) => { + mod common_e2e { + use super::*; + + #[::gpui::test] + #[cfg_attr(not(feature = "e2e"), ignore)] + async fn basic(cx: &mut ::gpui::TestAppContext) { + $crate::e2e_tests::test_basic($server, cx).await; + } + + #[::gpui::test] + #[cfg_attr(not(feature = "e2e"), ignore)] + async fn path_mentions(cx: &mut ::gpui::TestAppContext) { + $crate::e2e_tests::test_path_mentions($server, cx).await; + } + + #[::gpui::test] + #[cfg_attr(not(feature = "e2e"), ignore)] + async fn tool_call(cx: &mut ::gpui::TestAppContext) { + $crate::e2e_tests::test_tool_call($server, cx).await; + } + + #[::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($server, cx).await; + } + + #[::gpui::test] + #[cfg_attr(not(feature = "e2e"), ignore)] + async fn cancel(cx: &mut ::gpui::TestAppContext) { + $crate::e2e_tests::test_cancel($server, cx).await; + } + } + }; +} + +// Helpers + +pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> { + 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); + crate::settings::init(cx); + + crate::AllAgentServersSettings::override_global( + AllAgentServersSettings { + claude: Some(AgentServerSettings { + command: crate::claude::tests::local_command(), + }), + gemini: Some(AgentServerSettings { + command: crate::gemini::tests::local_command(), + }), + }, + cx, + ); + }); + + cx.executor().allow_parking(); + + FakeFs::new(cx.executor()) +} + +pub async fn new_test_thread( + server: impl AgentServer + 'static, + project: Entity<Project>, + current_dir: impl AsRef<Path>, + cx: &mut TestAppContext, +) -> Entity<AcpThread> { + let thread = cx + .update(|cx| server.new_thread(current_dir.as_ref(), &project, cx)) + .await + .unwrap(); + + thread + .update(cx, |thread, _| thread.initialize()) + .await + .unwrap(); + thread +} + +pub async fn run_until_first_tool_call( + thread: &Entity<AcpThread>, + cx: &mut TestAppContext, +) -> usize { + let (mut tx, mut rx) = mpsc::channel::<usize>(1); + + let subscription = cx.update(|cx| { + cx.subscribe(thread, move |thread, _, cx| { + for (ix, entry) in thread.read(cx).entries().iter().enumerate() { + if matches!(entry, AgentThreadEntry::ToolCall(_)) { + return tx.try_send(ix).unwrap(); + } + } + }) + }); + + select! { + // We have to use a smol timer here because + // cx.background_executor().timer isn't real in the test context + _ = futures::FutureExt::fuse(smol::Timer::after(Duration::from_secs(10))) => { + panic!("Timeout waiting for tool call") + } + ix = rx.next().fuse() => { + drop(subscription); + ix.unwrap() + } + } +} diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index bf1d13429e51660c68770af66f2eb44e601c3330..8ad147cbffb2ce0a1881ccbb52aad135d4d79dc6 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -1,4 +1,4 @@ -use crate::stdio_agent_server::{StdioAgentServer, find_bin_in_path}; +use crate::stdio_agent_server::StdioAgentServer; use crate::{AgentServerCommand, AgentServerVersion}; use anyhow::{Context as _, Result}; use gpui::{AsyncApp, Entity}; @@ -38,35 +38,15 @@ impl StdioAgentServer for Gemini { project: &Entity<Project>, cx: &mut AsyncApp, ) -> Result<AgentServerCommand> { - let custom_command = cx.read_global(|settings: &SettingsStore, _| { - let settings = settings.get::<AllAgentServersSettings>(None); - settings - .gemini - .as_ref() - .map(|gemini_settings| AgentServerCommand { - path: gemini_settings.command.path.clone(), - args: gemini_settings - .command - .args - .iter() - .cloned() - .chain(std::iter::once(ACP_ARG.into())) - .collect(), - env: gemini_settings.command.env.clone(), - }) + let settings = cx.read_global(|settings: &SettingsStore, _| { + settings.get::<AllAgentServersSettings>(None).gemini.clone() })?; - if let Some(custom_command) = custom_command { - return Ok(custom_command); - } - - if let Some(path) = find_bin_in_path("gemini", project, cx).await { - return Ok(AgentServerCommand { - path, - args: vec![ACP_ARG.into()], - env: None, - }); - } + 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()) @@ -121,381 +101,23 @@ impl StdioAgentServer for Gemini { } #[cfg(test)] -mod test { - use std::{path::Path, time::Duration}; - - use acp_thread::{ - AcpThread, AgentThreadEntry, ToolCall, ToolCallConfirmation, ToolCallContent, - ToolCallStatus, - }; - use agentic_coding_protocol as acp; - use anyhow::Result; - use futures::{FutureExt, StreamExt, channel::mpsc, select}; - use gpui::{AsyncApp, Entity, TestAppContext}; - use indoc::indoc; - use project::{FakeFs, Project}; - use serde_json::json; - use settings::SettingsStore; - use util::path; - - use crate::{AgentServer, AgentServerCommand, AgentServerVersion, StdioAgentServer}; - - pub async fn gemini_acp_thread( - project: Entity<Project>, - current_dir: impl AsRef<Path>, - cx: &mut TestAppContext, - ) -> Entity<AcpThread> { - #[derive(Clone)] - struct DevGemini; - - impl StdioAgentServer for DevGemini { - async fn command( - &self, - _project: &Entity<Project>, - _cx: &mut AsyncApp, - ) -> Result<AgentServerCommand> { - let cli_path = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../../gemini-cli/packages/cli") - .to_string_lossy() - .to_string(); - - Ok(AgentServerCommand { - path: "node".into(), - args: vec![cli_path, "--experimental-acp".into()], - env: None, - }) - } - - async fn version(&self, _command: &AgentServerCommand) -> Result<AgentServerVersion> { - Ok(AgentServerVersion::Supported) - } - - fn logo(&self) -> ui::IconName { - ui::IconName::AiGemini - } - - fn name(&self) -> &'static str { - "test" - } - - fn empty_state_headline(&self) -> &'static str { - "test" - } - - fn empty_state_message(&self) -> &'static str { - "test" - } - - fn supports_always_allow(&self) -> bool { - true - } - } - - let thread = cx - .update(|cx| AgentServer::new_thread(&DevGemini, current_dir.as_ref(), &project, cx)) - .await - .unwrap(); - - thread - .update(cx, |thread, _| thread.initialize()) - .await - .unwrap(); - thread - } - - 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); - }); - } - - #[gpui::test] - #[cfg_attr(not(feature = "gemini"), ignore)] - async fn test_gemini_basic(cx: &mut TestAppContext) { - init_test(cx); - - cx.executor().allow_parking(); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], cx).await; - let thread = gemini_acp_thread(project.clone(), "/private/tmp", cx).await; - thread - .update(cx, |thread, cx| thread.send_raw("Hello from Zed!", cx)) - .await - .unwrap(); - - thread.read_with(cx, |thread, _| { - assert_eq!(thread.entries().len(), 2); - assert!(matches!( - thread.entries()[0], - AgentThreadEntry::UserMessage(_) - )); - assert!(matches!( - thread.entries()[1], - AgentThreadEntry::AssistantMessage(_) - )); - }); - } - - #[gpui::test] - #[cfg_attr(not(feature = "gemini"), ignore)] - async fn test_gemini_path_mentions(cx: &mut TestAppContext) { - init_test(cx); - - cx.executor().allow_parking(); - let tempdir = tempfile::tempdir().unwrap(); - std::fs::write( - tempdir.path().join("foo.rs"), - indoc! {" - fn main() { - println!(\"Hello, world!\"); - } - "}, - ) - .expect("failed to write file"); - let project = Project::example([tempdir.path()], &mut cx.to_async()).await; - let thread = gemini_acp_thread(project.clone(), tempdir.path(), cx).await; - thread - .update(cx, |thread, cx| { - thread.send( - acp::SendUserMessageParams { - chunks: vec![ - acp::UserMessageChunk::Text { - text: "Read the file ".into(), - }, - acp::UserMessageChunk::Path { - path: Path::new("foo.rs").into(), - }, - acp::UserMessageChunk::Text { - text: " and tell me what the content of the println! is".into(), - }, - ], - }, - cx, - ) - }) - .await - .unwrap(); - - thread.read_with(cx, |thread, cx| { - assert_eq!(thread.entries().len(), 3); - assert!(matches!( - thread.entries()[0], - AgentThreadEntry::UserMessage(_) - )); - assert!(matches!(thread.entries()[1], AgentThreadEntry::ToolCall(_))); - let AgentThreadEntry::AssistantMessage(assistant_message) = &thread.entries()[2] else { - panic!("Expected AssistantMessage") - }; - assert!( - assistant_message.to_markdown(cx).contains("Hello, world!"), - "unexpected assistant message: {:?}", - assistant_message.to_markdown(cx) - ); - }); - } - - #[gpui::test] - #[cfg_attr(not(feature = "gemini"), ignore)] - async fn test_gemini_tool_call(cx: &mut TestAppContext) { - init_test(cx); - - cx.executor().allow_parking(); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/private/tmp"), - json!({"foo": "Lorem ipsum dolor", "bar": "bar", "baz": "baz"}), - ) - .await; - let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await; - let thread = gemini_acp_thread(project.clone(), "/private/tmp", cx).await; - thread - .update(cx, |thread, cx| { - thread.send_raw( - "Read the '/private/tmp/foo' file and tell me what you see.", - cx, - ) - }) - .await - .unwrap(); - thread.read_with(cx, |thread, _cx| { - assert!(matches!( - &thread.entries()[2], - AgentThreadEntry::ToolCall(ToolCall { - status: ToolCallStatus::Allowed { .. }, - .. - }) - )); - - assert!(matches!( - thread.entries()[3], - AgentThreadEntry::AssistantMessage(_) - )); - }); - } - - #[gpui::test] - #[cfg_attr(not(feature = "gemini"), ignore)] - async fn test_gemini_tool_call_with_confirmation(cx: &mut TestAppContext) { - init_test(cx); - - cx.executor().allow_parking(); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await; - let thread = gemini_acp_thread(project.clone(), "/private/tmp", cx).await; - let full_turn = thread.update(cx, |thread, cx| { - thread.send_raw(r#"Run `echo "Hello, world!"`"#, cx) - }); - - run_until_first_tool_call(&thread, cx).await; - - let tool_call_id = thread.read_with(cx, |thread, _cx| { - let AgentThreadEntry::ToolCall(ToolCall { - id, - status: - ToolCallStatus::WaitingForConfirmation { - confirmation: ToolCallConfirmation::Execute { root_command, .. }, - .. - }, - .. - }) = &thread.entries()[2] - else { - panic!(); - }; - - assert_eq!(root_command, "echo"); - - *id - }); - - thread.update(cx, |thread, cx| { - thread.authorize_tool_call(tool_call_id, acp::ToolCallConfirmationOutcome::Allow, cx); - - assert!(matches!( - &thread.entries()[2], - AgentThreadEntry::ToolCall(ToolCall { - status: ToolCallStatus::Allowed { .. }, - .. - }) - )); - }); - - full_turn.await.unwrap(); - - thread.read_with(cx, |thread, cx| { - let AgentThreadEntry::ToolCall(ToolCall { - content: Some(ToolCallContent::Markdown { markdown }), - status: ToolCallStatus::Allowed { .. }, - .. - }) = &thread.entries()[2] - else { - panic!(); - }; - - markdown.read_with(cx, |md, _cx| { - assert!( - md.source().contains("Hello, world!"), - r#"Expected '{}' to contain "Hello, world!""#, - md.source() - ); - }); - }); - } - - #[gpui::test] - #[cfg_attr(not(feature = "gemini"), ignore)] - async fn test_gemini_cancel(cx: &mut TestAppContext) { - init_test(cx); - - cx.executor().allow_parking(); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await; - let thread = gemini_acp_thread(project.clone(), "/private/tmp", cx).await; - let full_turn = thread.update(cx, |thread, cx| { - thread.send_raw(r#"Run `echo "Hello, world!"`"#, cx) - }); - - let first_tool_call_ix = run_until_first_tool_call(&thread, cx).await; - - thread.read_with(cx, |thread, _cx| { - let AgentThreadEntry::ToolCall(ToolCall { - id, - status: - ToolCallStatus::WaitingForConfirmation { - confirmation: ToolCallConfirmation::Execute { root_command, .. }, - .. - }, - .. - }) = &thread.entries()[first_tool_call_ix] - else { - panic!("{:?}", thread.entries()[1]); - }; - - assert_eq!(root_command, "echo"); - - *id - }); - - thread - .update(cx, |thread, cx| thread.cancel(cx)) - .await - .unwrap(); - full_turn.await.unwrap(); - thread.read_with(cx, |thread, _| { - let AgentThreadEntry::ToolCall(ToolCall { - status: ToolCallStatus::Canceled, - .. - }) = &thread.entries()[first_tool_call_ix] - else { - panic!(); - }; - }); - - thread - .update(cx, |thread, cx| { - thread.send_raw(r#"Stop running and say goodbye to me."#, cx) - }) - .await - .unwrap(); - thread.read_with(cx, |thread, _| { - assert!(matches!( - &thread.entries().last().unwrap(), - AgentThreadEntry::AssistantMessage(..), - )) - }); - } - - async fn run_until_first_tool_call( - thread: &Entity<AcpThread>, - cx: &mut TestAppContext, - ) -> usize { - let (mut tx, mut rx) = mpsc::channel::<usize>(1); - - let subscription = cx.update(|cx| { - cx.subscribe(thread, move |thread, _, cx| { - for (ix, entry) in thread.read(cx).entries().iter().enumerate() { - if matches!(entry, AgentThreadEntry::ToolCall(_)) { - return tx.try_send(ix).unwrap(); - } - } - }) - }); - - select! { - _ = cx.executor().timer(Duration::from_secs(10)).fuse() => { - panic!("Timeout waiting for tool call") - } - ix = rx.next().fuse() => { - drop(subscription); - ix.unwrap() - } +pub(crate) mod tests { + use super::*; + use crate::AgentServerCommand; + use std::path::Path; + + crate::common_e2e_tests!(Gemini); + + pub fn local_command() -> AgentServerCommand { + let cli_path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../../gemini-cli/packages/cli") + .to_string_lossy() + .to_string(); + + AgentServerCommand { + path: "node".into(), + args: vec![cli_path, ACP_ARG.into()], + env: None, } } } diff --git a/crates/agent_servers/src/settings.rs b/crates/agent_servers/src/settings.rs index 8e6914352b58033814be3be7b622f0c609c9d50f..29dcf5eb8c9b3e1738fd5c24b18e309d897b0c6f 100644 --- a/crates/agent_servers/src/settings.rs +++ b/crates/agent_servers/src/settings.rs @@ -12,6 +12,7 @@ pub fn init(cx: &mut App) { #[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug)] pub struct AllAgentServersSettings { pub gemini: Option<AgentServerSettings>, + pub claude: Option<AgentServerSettings>, } #[derive(Deserialize, Serialize, Clone, JsonSchema, Debug)] diff --git a/crates/agent_servers/src/stdio_agent_server.rs b/crates/agent_servers/src/stdio_agent_server.rs index d78506022dc8e3c2a853ebb2b9966bd8a228e05a..e60dd39de45925223196f43fcb6025c49281c4c9 100644 --- a/crates/agent_servers/src/stdio_agent_server.rs +++ b/crates/agent_servers/src/stdio_agent_server.rs @@ -4,11 +4,8 @@ use agentic_coding_protocol as acp; use anyhow::{Result, anyhow}; use gpui::{App, AsyncApp, Entity, Task, prelude::*}; use project::Project; -use std::{ - path::{Path, PathBuf}, - sync::Arc, -}; -use util::{ResultExt, paths}; +use std::path::Path; +use util::ResultExt; pub trait StdioAgentServer: Send + Clone { fn logo(&self) -> ui::IconName; @@ -120,50 +117,3 @@ impl<T: StdioAgentServer + 'static> AgentServer for T { }) } } - -pub async fn find_bin_in_path( - bin_name: &'static str, - project: &Entity<Project>, - cx: &mut AsyncApp, -) -> Option<PathBuf> { - let (env_task, root_dir) = project - .update(cx, |project, cx| { - let worktree = project.visible_worktrees(cx).next(); - match worktree { - Some(worktree) => { - let env_task = project.environment().update(cx, |env, cx| { - env.get_worktree_environment(worktree.clone(), cx) - }); - - let path = worktree.read(cx).abs_path(); - (env_task, path) - } - None => { - let path: Arc<Path> = paths::home_dir().as_path().into(); - let env_task = project.environment().update(cx, |env, cx| { - env.get_directory_environment(path.clone(), cx) - }); - (env_task, path) - } - } - }) - .log_err()?; - - cx.background_executor() - .spawn(async move { - let which_result = if cfg!(windows) { - which::which(bin_name) - } else { - let env = env_task.await.unwrap_or_default(); - let shell_path = env.get("PATH").cloned(); - which::which_in(bin_name, shell_path.as_ref(), root_dir.as_ref()) - }; - - if let Err(which::Error::CannotFindBinaryPath) = which_result { - return None; - } - - which_result.log_err() - }) - .await -} From 9efe9df80080fe09e8bfe679cdb5804611a3a8b9 Mon Sep 17 00:00:00 2001 From: Peter Tripp <peter@zed.dev> Date: Thu, 17 Jul 2025 14:32:04 -0400 Subject: [PATCH 162/658] Harmonize `buffer_font_size` between default.json and initial_settings.json (#34650) Release Notes: - N/A --- assets/settings/initial_user_settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/settings/initial_user_settings.json b/assets/settings/initial_user_settings.json index 71f3beb1d6076ed5a41064291a83662ee7023f03..5ac2063bdb481e057a2d124c1e72f998390b066b 100644 --- a/assets/settings/initial_user_settings.json +++ b/assets/settings/initial_user_settings.json @@ -8,7 +8,7 @@ // command palette (cmd-shift-p / ctrl-shift-p) { "ui_font_size": 16, - "buffer_font_size": 16, + "buffer_font_size": 15, "theme": { "mode": "system", "light": "One Light", From 1e60ebb2c6364279be3d08ed20ff4399fe61197c Mon Sep 17 00:00:00 2001 From: Marshall Bowers <git@maxdeviant.com> Date: Thu, 17 Jul 2025 14:34:04 -0400 Subject: [PATCH 163/658] collab: Remove `GET /billing/usage` endpoint (#34651) This PR removes the `GET /billing/usage` endpoint, as it has been moved to `cloud.zed.dev`. Release Notes: - N/A --- crates/collab/src/api/billing.rs | 159 +------------------------------ 1 file changed, 1 insertion(+), 158 deletions(-) diff --git a/crates/collab/src/api/billing.rs b/crates/collab/src/api/billing.rs index 1ca71726f928303bc8d6f99ca9c712d28fd9f70b..09f307c6727b9b23c800aae684e3f813d92375ad 100644 --- a/crates/collab/src/api/billing.rs +++ b/crates/collab/src/api/billing.rs @@ -1,10 +1,6 @@ use anyhow::{Context as _, bail}; use axum::routing::put; -use axum::{ - Extension, Json, Router, - extract::{self, Query}, - routing::{get, post}, -}; +use axum::{Extension, Json, Router, extract, routing::post}; use chrono::{DateTime, SecondsFormat, Utc}; use collections::{HashMap, HashSet}; use reqwest::StatusCode; @@ -28,7 +24,6 @@ use crate::api::events::SnowflakeRow; use crate::db::billing_subscription::{ StripeCancellationReason, StripeSubscriptionStatus, SubscriptionKind, }; -use crate::llm::AGENT_EXTENDED_TRIAL_FEATURE_FLAG; use crate::llm::db::subscription_usage_meter::{self, CompletionMode}; use crate::rpc::{ResultExt as _, Server}; use crate::stripe_client::{ @@ -58,7 +53,6 @@ pub fn router() -> Router { "/billing/subscriptions/sync", post(sync_billing_subscription), ) - .route("/billing/usage", get(get_current_usage)) } #[derive(Debug, Serialize)] @@ -1027,157 +1021,6 @@ async fn handle_customer_subscription_event( Ok(()) } -#[derive(Debug, Deserialize)] -struct GetCurrentUsageParams { - github_user_id: i32, -} - -#[derive(Debug, Serialize)] -struct UsageCounts { - pub used: i32, - pub limit: Option<i32>, - pub remaining: Option<i32>, -} - -#[derive(Debug, Serialize)] -struct ModelRequestUsage { - pub model: String, - pub mode: CompletionMode, - pub requests: i32, -} - -#[derive(Debug, Serialize)] -struct CurrentUsage { - pub model_requests: UsageCounts, - pub model_request_usage: Vec<ModelRequestUsage>, - pub edit_predictions: UsageCounts, -} - -#[derive(Debug, Default, Serialize)] -struct GetCurrentUsageResponse { - pub plan: String, - pub current_usage: Option<CurrentUsage>, -} - -async fn get_current_usage( - Extension(app): Extension<Arc<AppState>>, - Query(params): Query<GetCurrentUsageParams>, -) -> Result<Json<GetCurrentUsageResponse>> { - let user = app - .db - .get_user_by_github_user_id(params.github_user_id) - .await? - .context("user not found")?; - - let feature_flags = app.db.get_user_flags(user.id).await?; - let has_extended_trial = feature_flags - .iter() - .any(|flag| flag == AGENT_EXTENDED_TRIAL_FEATURE_FLAG); - - let Some(llm_db) = app.llm_db.clone() else { - return Err(Error::http( - StatusCode::NOT_IMPLEMENTED, - "LLM database not available".into(), - )); - }; - - let Some(subscription) = app.db.get_active_billing_subscription(user.id).await? else { - return Ok(Json(GetCurrentUsageResponse::default())); - }; - - let subscription_period = maybe!({ - 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)) - }); - - let Some((period_start_at, period_end_at)) = subscription_period else { - return Ok(Json(GetCurrentUsageResponse::default())); - }; - - let usage = llm_db - .get_subscription_usage_for_period(user.id, period_start_at, period_end_at) - .await?; - - let plan = subscription - .kind - .map(Into::into) - .unwrap_or(zed_llm_client::Plan::ZedFree); - - let model_requests_limit = match plan.model_requests_limit() { - zed_llm_client::UsageLimit::Limited(limit) => { - let limit = if plan == zed_llm_client::Plan::ZedProTrial && has_extended_trial { - 1_000 - } else { - limit - }; - - Some(limit) - } - zed_llm_client::UsageLimit::Unlimited => None, - }; - - let edit_predictions_limit = match plan.edit_predictions_limit() { - zed_llm_client::UsageLimit::Limited(limit) => Some(limit), - zed_llm_client::UsageLimit::Unlimited => None, - }; - - let Some(usage) = usage else { - return Ok(Json(GetCurrentUsageResponse { - plan: plan.as_str().to_string(), - current_usage: Some(CurrentUsage { - model_requests: UsageCounts { - used: 0, - limit: model_requests_limit, - remaining: model_requests_limit, - }, - model_request_usage: Vec::new(), - edit_predictions: UsageCounts { - used: 0, - limit: edit_predictions_limit, - remaining: edit_predictions_limit, - }, - }), - })); - }; - - let subscription_usage_meters = llm_db - .get_current_subscription_usage_meters_for_user(user.id, Utc::now()) - .await?; - - let model_request_usage = subscription_usage_meters - .into_iter() - .filter_map(|(usage_meter, _usage)| { - let model = llm_db.model_by_id(usage_meter.model_id).ok()?; - - Some(ModelRequestUsage { - model: model.name.clone(), - mode: usage_meter.mode, - requests: usage_meter.requests, - }) - }) - .collect::<Vec<_>>(); - - Ok(Json(GetCurrentUsageResponse { - plan: plan.as_str().to_string(), - current_usage: Some(CurrentUsage { - model_requests: UsageCounts { - used: usage.model_requests, - limit: model_requests_limit, - remaining: model_requests_limit.map(|limit| (limit - usage.model_requests).max(0)), - }, - model_request_usage, - edit_predictions: UsageCounts { - used: usage.edit_predictions, - limit: edit_predictions_limit, - remaining: edit_predictions_limit - .map(|limit| (limit - usage.edit_predictions).max(0)), - }, - }), - })) -} - impl From<SubscriptionStatus> for StripeSubscriptionStatus { fn from(value: SubscriptionStatus) -> Self { match value { From 0c88189aab0bee0661ee8bcfebdcceccf47e5003 Mon Sep 17 00:00:00 2001 From: Peter Tripp <peter@zed.dev> Date: Thu, 17 Jul 2025 15:23:00 -0400 Subject: [PATCH 164/658] Refine JetBrains keymaps (#34658) Follow-up to: https://github.com/zed-industries/zed/pull/34641 Release Notes: - N/A --- assets/keymaps/linux/jetbrains.json | 15 ++++++++++----- assets/keymaps/macos/jetbrains.json | 7 ++++++- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/assets/keymaps/linux/jetbrains.json b/assets/keymaps/linux/jetbrains.json index 7266318a9ea82b420b4c04e66f97e2cb006094c0..629333663d25a3c8215a3b792d72427aa78b3fde 100644 --- a/assets/keymaps/linux/jetbrains.json +++ b/assets/keymaps/linux/jetbrains.json @@ -72,14 +72,19 @@ "alt-enter": "editor::ToggleCodeActions" } }, + { + "context": "BufferSearchBar", + "bindings": { + "shift-enter": "search::SelectPreviousMatch" + } + }, { "context": "BufferSearchBar || ProjectSearchBar", "bindings": { - "shift-enter": "search::SelectPreviousMatch", - "ctrl-alt-c": "search::ToggleCaseSensitive", - "ctrl-alt-e": "search::ToggleSelection", - "ctrl-alt-w": "search::ToggleWholeWord", - "ctrl-alt-x": "search::ToggleRegex" + "alt-c": "search::ToggleCaseSensitive", + "alt-e": "search::ToggleSelection", + "alt-x": "search::ToggleRegex", + "alt-w": "search::ToggleWholeWord" } }, { diff --git a/assets/keymaps/macos/jetbrains.json b/assets/keymaps/macos/jetbrains.json index a41e39932feed0cbf4f280f9a3666f5e501cde39..e8b796f534aa133b517f163779d012dddfb8161f 100644 --- a/assets/keymaps/macos/jetbrains.json +++ b/assets/keymaps/macos/jetbrains.json @@ -70,10 +70,15 @@ "alt-enter": "editor::ToggleCodeActions" } }, + { + "context": "BufferSearchBar", + "bindings": { + "shift-enter": "search::SelectPreviousMatch" + } + }, { "context": "BufferSearchBar || ProjectSearchBar", "bindings": { - "shift-enter": "search::SelectPreviousMatch", "alt-c": "search::ToggleCaseSensitive", "alt-e": "search::ToggleSelection", "alt-x": "search::ToggleRegex", From 237ce5c8e8e3107981c6922a883527afa2ea2c83 Mon Sep 17 00:00:00 2001 From: Cole Miller <cole@zed.dev> Date: Thu, 17 Jul 2025 17:38:45 -0400 Subject: [PATCH 165/658] Report build SHA to Slack for Zed Nightly panics (#34665) There can be a bunch of nightlies with the same version number and it's helpful to know exactly which one reported a panic. Release Notes: - N/A --- crates/collab/src/api/events.rs | 83 +++++++++++++++++---------------- 1 file changed, 44 insertions(+), 39 deletions(-) diff --git a/crates/collab/src/api/events.rs b/crates/collab/src/api/events.rs index 6ccc86c520082998c10e37a5cc4bea339a5d3a8d..bc7dd152b02d91e4902addef09dfe6b817e7789f 100644 --- a/crates/collab/src/api/events.rs +++ b/crates/collab/src/api/events.rs @@ -389,53 +389,58 @@ pub async fn post_panic( } } - let backtrace = if panic.backtrace.len() > 25 { - let total = panic.backtrace.len(); - format!( - "{}\n and {} more", - panic - .backtrace - .iter() - .take(20) - .cloned() - .collect::<Vec<_>>() - .join("\n"), - total - 20 - ) - } else { - panic.backtrace.join("\n") - }; - if !report_to_slack(&panic) { return Ok(()); } - let backtrace_with_summary = panic.payload + "\n" + &backtrace; - if let Some(slack_panics_webhook) = app.config.slack_panics_webhook.clone() { + let backtrace = if panic.backtrace.len() > 25 { + let total = panic.backtrace.len(); + format!( + "{}\n and {} more", + panic + .backtrace + .iter() + .take(20) + .cloned() + .collect::<Vec<_>>() + .join("\n"), + total - 20 + ) + } else { + panic.backtrace.join("\n") + }; + let backtrace_with_summary = panic.payload + "\n" + &backtrace; + + let version = if panic.release_channel == "nightly" + && !panic.app_version.contains("remote-server") + && let Some(sha) = panic.app_commit_sha + { + format!("Zed Nightly {}", sha.chars().take(7).collect::<String>()) + } else { + panic.app_version + }; + let payload = slack::WebhookBody::new(|w| { w.add_section(|s| s.text(slack::Text::markdown("Panic request".to_string()))) .add_section(|s| { - s.add_field(slack::Text::markdown(format!( - "*Version:*\n {} ", - panic.app_version - ))) - .add_field({ - let hostname = app.config.blob_store_url.clone().unwrap_or_default(); - let hostname = hostname.strip_prefix("https://").unwrap_or_else(|| { - hostname.strip_prefix("http://").unwrap_or_default() - }); - - slack::Text::markdown(format!( - "*{} {}:*\n<https://{}.{}/{}.json|{}…>", - panic.os_name, - panic.os_version.unwrap_or_default(), - CRASH_REPORTS_BUCKET, - hostname, - incident_id, - incident_id.chars().take(8).collect::<String>(), - )) - }) + s.add_field(slack::Text::markdown(format!("*Version:*\n {version} ",))) + .add_field({ + let hostname = app.config.blob_store_url.clone().unwrap_or_default(); + let hostname = hostname.strip_prefix("https://").unwrap_or_else(|| { + hostname.strip_prefix("http://").unwrap_or_default() + }); + + slack::Text::markdown(format!( + "*{} {}:*\n<https://{}.{}/{}.json|{}…>", + panic.os_name, + panic.os_version.unwrap_or_default(), + CRASH_REPORTS_BUCKET, + hostname, + incident_id, + incident_id.chars().take(8).collect::<String>(), + )) + }) }) .add_rich_text(|r| r.add_preformatted(|p| p.add_text(backtrace_with_summary))) }); From 29030a243ca9cf3b99e6746f9addcaf318da5112 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 17 Jul 2025 18:49:44 -0300 Subject: [PATCH 166/658] docs: Add instructions about how to use MCP servers (#34656) Release Notes: - N/A --- docs/src/ai/agent-panel.md | 6 ++ docs/src/ai/mcp.md | 109 +++++++++++++++++++++++++++++++------ 2 files changed, 97 insertions(+), 18 deletions(-) diff --git a/docs/src/ai/agent-panel.md b/docs/src/ai/agent-panel.md index ca35e06e113c401876cc68de1a1cfa83846352c6..97568d66431613d550d62799a866373f157cd80e 100644 --- a/docs/src/ai/agent-panel.md +++ b/docs/src/ai/agent-panel.md @@ -121,6 +121,12 @@ Zed will store this profile in your settings using the same profile name as the All custom profiles can be edited via the UI or by hand under the `assistant.profiles` key in your `settings.json` file. +### Tool Approval + +Zed's Agent Panel surfaces the `agent.always_allow_tool_actions` setting that, if turned to `false`, will require you to give permission to any editing attempt as well as tool calls coming from MCP servers. + +You can change that by setting this key to `true` in either your `settings.json` or via the Agent Panel's settings view. + ### Model Support {#model-support} Tool calling needs to be individually supported by each model and model provider. diff --git a/docs/src/ai/mcp.md b/docs/src/ai/mcp.md index 202b14102209ae8d3dbf338ff11bb8b443432cf9..95929b2d7e259ad2f6192c260f2e717ddf8b51ba 100644 --- a/docs/src/ai/mcp.md +++ b/docs/src/ai/mcp.md @@ -4,35 +4,35 @@ Zed uses the [Model Context Protocol](https://modelcontextprotocol.io/) to inter > The Model Context Protocol (MCP) is an open protocol that enables seamless integration between LLM applications and external data sources and tools. Whether you're building an AI-powered IDE, enhancing a chat interface, or creating custom AI workflows, MCP provides a standardized way to connect LLMs with the context they need. -Check out the [Anthropic news post](https://www.anthropic.com/news/model-context-protocol) and the [Zed blog post](https://zed.dev/blog/mcp) for an introduction to MCP. +Check out the [Anthropic news post](https://www.anthropic.com/news/model-context-protocol) and the [Zed blog post](https://zed.dev/blog/mcp) for a general intro to MCP. -## MCP Servers as Extensions +## Installing MCP Servers -One of the ways you can use MCP servers in Zed is by exposing them as an extension. -To learn how to do that, check out the [MCP Server Extensions](../extensions/mcp-extensions.md) page for more details. +### As Extensions -### Available extensions +One of the ways you can use MCP servers in Zed is by exposing them as an extension. +To learn how to create your own, check out the [MCP Server Extensions](../extensions/mcp-extensions.md) page for more details. -Many MCP servers have been exposed as extensions already, thanks to Zed's awesome community. -Check which ones are already available in Zed's extension store via any of these routes: +Thanks to our awesome community, many MCP servers have already been added as extensions. +You can check which ones are available via any of these routes: 1. [the Zed website](https://zed.dev/extensions?filter=context-servers) -2. in the app, run the `zed: extensions` action +2. in the app, open the Command Palette and run the `zed: extensions` action 3. in the app, go to the Agent Panel's top-right menu and look for the "View Server Extensions" menu item In any case, here are some of the ones available: -- [Postgres](https://github.com/zed-extensions/postgres-context-server) -- [GitHub](https://github.com/LoamStudios/zed-mcp-server-github) -- [Puppeteer](https://github.com/zed-extensions/mcp-server-puppeteer) -- [BrowserTools](https://github.com/mirageN1349/browser-tools-context-server) -- [Brave Search](https://github.com/zed-extensions/mcp-server-brave-search) +- [Context7](https://zed.dev/extensions/context7-mcp-server) +- [GitHub](https://zed.dev/extensions/github-mcp-server) +- [Puppeteer](https://zed.dev/extensions/puppeteer-mcp-server) +- [Gem](https://zed.dev/extensions/gem) +- [Brave Search](https://zed.dev/extensions/brave-search-mcp-server) - [Prisma](https://github.com/aqrln/prisma-mcp-zed) -- [Framelink Figma](https://github.com/LoamStudios/zed-mcp-server-figma) -- [Linear](https://github.com/LoamStudios/zed-mcp-server-linear) -- [Resend](https://github.com/danilo-leal/zed-resend-mcp-server) +- [Framelink Figma](https://zed.dev/extensions/framelink-figma-mcp-server) +- [Linear](https://zed.dev/extensions/linear-mcp-server) +- [Resend](https://zed.dev/extensions/resend-mcp-server) -## Add your own MCP server +### As Custom Servers Creating an extension is not the only way to use MCP servers in Zed. You can connect them by adding their commands directly to your `settings.json`, like so: @@ -51,4 +51,77 @@ You can connect them by adding their commands directly to your `settings.json`, ``` Alternatively, you can also add a custom server by accessing the Agent Panel's Settings view (also accessible via the `agent: open configuration` action). -From there, you can add it through the modal that appears when clicking the "Add Custom Server" button. +From there, you can add it through the modal that appears when you click the "Add Custom Server" button. + +## Using MCP Servers + +### Installation Check + +Regardless of whether you're using MCP servers as an extension or adding them directly, most servers out there need some sort of configuration as part of the set up process. + +In the case of extensions, Zed will show a modal displaying what is required for you to properly set up a given server. +For example, the GitHub MCP extension requires you to add a [Personal Access Token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens). + +In the case of custom servers, make sure you check the provider documentation to determine what type of command, arguments, and environment variables need to be added to the JSON. + +To check whether your MCP server is properly installed, go to the Agent Panel's settings view and watch the indicator dot next to its name. +If they're running correctly, the indicator will be green and its tooltip will say "Server is active". +If not, other colors and tooltip messages will indicate what is happening. + +### Using in the Agent Panel + +Once installation is complete, you can return to the Agent Panel and start prompting. +Mentioning your MCP server by name helps the agent pick it up. + +If you want to ensure a given server will be used, you can create [a custom profile](./agent-panel.md#custom-profiles) by turning off the built-in tools (either all of them or the ones that would cause conflicts) and turning on only the tools coming from the MCP server. + +As an example, [the Dagger team suggests](https://container-use.com/agent-integrations#add-container-use-agent-profile-optional) doing that with their [Container Use MCP server](https://zed.dev/extensions/container-use-mcp-server): + +```json +"agent": { + "profiles": { + "container-use": { + "name": "Container Use", + "tools": { + "fetch": true, + "thinking": true, + "copy_path": false, + "find_path": false, + "delete_path": false, + "create_directory": false, + "list_directory": false, + "diagnostics": false, + "read_file": false, + "open": false, + "move_path": false, + "grep": false, + "edit_file": false, + "terminal": false + }, + "enable_all_context_servers": false, + "context_servers": { + "container-use": { + "tools": { + "environment_create": true, + "environment_add_service": true, + "environment_update": true, + "environment_run_cmd": true, + "environment_open": true, + "environment_file_write": true, + "environment_file_read": true, + "environment_file_list": true, + "environment_file_delete": true, + "environment_checkpoint": true + } + } + } + } + } +} +``` + +### Tool Approval + +Zed's Agent Panel includes the `agent.always_allow_tool_actions` setting that, if set to `false`, will require you to give permission for any editing attempt as well as tool calls coming from MCP servers. + +You can change this by setting this key to `true` in either your `settings.json` or through the Agent Panel's settings view. From 6c741292dfc538b10fcb753a1c35e631611b386a Mon Sep 17 00:00:00 2001 From: Finn Evers <finn@zed.dev> Date: Fri, 18 Jul 2025 00:12:10 +0200 Subject: [PATCH 167/658] keymap_ui: Fix various keymap editor issues (#34647) This PR tackles miscellaneous nits for the new keymap editor UI. Release Notes: - N/A --------- Co-authored-by: Ben Kunkle <ben@zed.dev> --- crates/settings_ui/src/keybindings.rs | 375 ++++++++++-------- crates/settings_ui/src/ui_components/table.rs | 31 ++ 2 files changed, 247 insertions(+), 159 deletions(-) diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index af096f4ce1cc1d110d4775e62ab78e963a5dcaa6..d5ac253fb8c071ccab2adc6ef3379ba94cd509c6 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -22,9 +22,9 @@ use settings::{BaseKeymap, KeybindSource, KeymapFile, SettingsAssets}; use util::ResultExt; use ui::{ - ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, IconButtonShape, Modal, - ModalFooter, ModalHeader, ParentElement as _, Render, Section, SharedString, Styled as _, - Tooltip, Window, prelude::*, + ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, IconButtonShape, Indicator, + Modal, ModalFooter, ModalHeader, ParentElement as _, Render, Section, SharedString, + Styled as _, Tooltip, Window, prelude::*, }; use ui_input::SingleLineInput; use workspace::{ @@ -179,14 +179,29 @@ impl FilterState { #[derive(Debug, Default, PartialEq, Eq, Clone, Hash)] struct ActionMapping { - keystroke_text: SharedString, + keystrokes: Vec<Keystroke>, context: Option<SharedString>, } +#[derive(Debug)] +struct KeybindConflict { + first_conflict_index: usize, + remaining_conflict_amount: usize, +} + +impl KeybindConflict { + fn from_iter<'a>(mut indices: impl Iterator<Item = &'a usize>) -> Option<Self> { + indices.next().map(|index| Self { + first_conflict_index: *index, + remaining_conflict_amount: indices.count(), + }) + } +} + #[derive(Default)] struct ConflictState { conflicts: Vec<usize>, - action_keybind_mapping: HashMap<ActionMapping, Vec<usize>>, + keybind_mapping: HashMap<ActionMapping, Vec<usize>>, } impl ConflictState { @@ -197,7 +212,7 @@ impl ConflictState { .iter() .enumerate() .filter(|(_, binding)| { - !binding.keystroke_text.is_empty() + binding.keystrokes().is_some() && binding .source .as_ref() @@ -217,27 +232,26 @@ impl ConflictState { .flatten() .copied() .collect(), - action_keybind_mapping, + keybind_mapping: action_keybind_mapping, } } fn conflicting_indices_for_mapping( &self, - action_mapping: ActionMapping, + action_mapping: &ActionMapping, keybind_idx: usize, - ) -> Option<Vec<usize>> { - self.action_keybind_mapping - .get(&action_mapping) + ) -> Option<KeybindConflict> { + self.keybind_mapping + .get(action_mapping) .and_then(|indices| { - let mut indices = indices.iter().filter(|&idx| *idx != keybind_idx).peekable(); - indices.peek().is_some().then(|| indices.copied().collect()) + KeybindConflict::from_iter(indices.iter().filter(|&idx| *idx != keybind_idx)) }) } - fn will_conflict(&self, action_mapping: ActionMapping) -> Option<Vec<usize>> { - self.action_keybind_mapping - .get(&action_mapping) - .and_then(|indices| indices.is_empty().not().then_some(indices.clone())) + fn will_conflict(&self, action_mapping: &ActionMapping) -> Option<KeybindConflict> { + self.keybind_mapping + .get(action_mapping) + .and_then(|indices| KeybindConflict::from_iter(indices.iter())) } fn has_conflict(&self, candidate_idx: &usize) -> bool { @@ -267,7 +281,7 @@ struct KeymapEditor { selected_index: Option<usize>, context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>, previous_edit: Option<PreviousEdit>, - humanized_action_names: HashMap<&'static str, SharedString>, + humanized_action_names: HumanizedActionNameCache, show_hover_menus: bool, } @@ -332,14 +346,6 @@ impl KeymapEditor { }) .detach(); - let humanized_action_names = - HashMap::from_iter(cx.all_action_names().into_iter().map(|&action_name| { - ( - action_name, - command_palette::humanize_action_name(action_name).into(), - ) - })); - let mut this = Self { workspace, keybindings: vec![], @@ -356,8 +362,8 @@ impl KeymapEditor { selected_index: None, context_menu: None, previous_edit: None, - humanized_action_names, search_query_debounce: None, + humanized_action_names: HumanizedActionNameCache::new(cx), show_hover_menus: true, }; @@ -383,6 +389,24 @@ impl KeymapEditor { } } + fn filter_on_selected_binding_keystrokes(&mut self, cx: &mut Context<Self>) { + let Some(selected_binding) = self.selected_binding() else { + return; + }; + + let keystrokes = selected_binding + .keystrokes() + .map(Vec::from) + .unwrap_or_default(); + + self.filter_state = FilterState::All; + self.search_mode = SearchMode::KeyStroke { exact_match: true }; + + self.keystroke_editor.update(cx, |editor, cx| { + editor.set_keystrokes(keystrokes, cx); + }); + } + fn on_query_changed(&mut self, cx: &mut Context<Self>) { let action_query = self.current_action_query(cx); let keystroke_query = self.current_keystroke_query(cx); @@ -523,6 +547,7 @@ impl KeymapEditor { fn process_bindings( json_language: Arc<Language>, zed_keybind_context_language: Arc<Language>, + humanized_action_names: &HumanizedActionNameCache, cx: &mut App, ) -> (Vec<ProcessedKeybinding>, Vec<StringMatchCandidate>) { let key_bindings_ptr = cx.key_bindings(); @@ -570,12 +595,14 @@ impl KeymapEditor { let action_docs = action_documentation.get(action_name).copied(); let index = processed_bindings.len(); - let string_match_candidate = StringMatchCandidate::new(index, &action_name); + let humanized_action_name = humanized_action_names.get(action_name); + let string_match_candidate = StringMatchCandidate::new(index, &humanized_action_name); processed_bindings.push(ProcessedKeybinding { keystroke_text: keystroke_text.into(), ui_key_binding, action_name, action_arguments, + humanized_action_name, action_docs, action_schema: action_schema.get(action_name).cloned(), context: Some(context), @@ -587,12 +614,14 @@ impl KeymapEditor { let empty = SharedString::new_static(""); for action_name in unmapped_action_names.into_iter() { let index = processed_bindings.len(); - let string_match_candidate = StringMatchCandidate::new(index, &action_name); + let humanized_action_name = humanized_action_names.get(action_name); + let string_match_candidate = StringMatchCandidate::new(index, &humanized_action_name); processed_bindings.push(ProcessedKeybinding { keystroke_text: empty.clone(), ui_key_binding: None, action_name, action_arguments: None, + humanized_action_name, action_docs: action_documentation.get(action_name).copied(), action_schema: action_schema.get(action_name).cloned(), context: None, @@ -612,15 +641,15 @@ impl KeymapEditor { load_keybind_context_language(workspace.clone(), cx).await; let (action_query, keystroke_query) = this.update(cx, |this, cx| { - let (key_bindings, string_match_candidates) = - Self::process_bindings(json_language, zed_keybind_context_language, cx); + let (key_bindings, string_match_candidates) = Self::process_bindings( + json_language, + zed_keybind_context_language, + &this.humanized_action_names, + cx, + ); this.keybinding_conflict_state = ConflictState::new(&key_bindings); - if !this.keybinding_conflict_state.any_conflicts() { - this.filter_state = FilterState::All; - } - this.keybindings = key_bindings; this.string_match_candidates = Arc::new(string_match_candidates); this.matches = this @@ -751,10 +780,6 @@ impl KeymapEditor { ) { let weak = cx.weak_entity(); self.context_menu = self.selected_binding().map(|selected_binding| { - let key_strokes = selected_binding - .keystrokes() - .map(Vec::from) - .unwrap_or_default(); let selected_binding_has_no_context = selected_binding .context .as_ref() @@ -784,17 +809,9 @@ impl KeymapEditor { Box::new(CopyContext), ) .entry("Show matching keybindings", None, { - let weak = weak.clone(); - let key_strokes = key_strokes.clone(); - move |_, cx| { weak.update(cx, |this, cx| { - this.filter_state = FilterState::All; - this.search_mode = SearchMode::KeyStroke { exact_match: true }; - - this.keystroke_editor.update(cx, |editor, cx| { - editor.set_keystrokes(key_strokes.clone(), cx); - }); + this.filter_on_selected_binding_keystrokes(cx); }) .ok(); } @@ -826,6 +843,24 @@ impl KeymapEditor { self.context_menu.is_some() } + fn render_no_matches_hint(&self, _window: &mut Window, _cx: &App) -> AnyElement { + let hint = match (self.filter_state, &self.search_mode) { + (FilterState::Conflicts, _) => { + if self.keybinding_conflict_state.any_conflicts() { + "No conflicting keybinds found that match the provided query" + } else { + "No conflicting keybinds found" + } + } + (FilterState::All, SearchMode::KeyStroke { .. }) => { + "No keybinds found matching the entered keystrokes" + } + (FilterState::All, SearchMode::Normal) => "No matches found for the provided query", + }; + + Label::new(hint).color(Color::Muted).into_any_element() + } + fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) { self.show_hover_menus = false; if let Some(selected) = self.selected_index { @@ -1064,11 +1099,35 @@ impl KeymapEditor { } } +struct HumanizedActionNameCache { + cache: HashMap<&'static str, SharedString>, +} + +impl HumanizedActionNameCache { + fn new(cx: &App) -> Self { + let cache = HashMap::from_iter(cx.all_action_names().into_iter().map(|&action_name| { + ( + action_name, + command_palette::humanize_action_name(action_name).into(), + ) + })); + Self { cache } + } + + fn get(&self, action_name: &'static str) -> SharedString { + match self.cache.get(action_name) { + Some(name) => name.clone(), + None => action_name.into(), + } + } +} + #[derive(Clone)] struct ProcessedKeybinding { keystroke_text: SharedString, ui_key_binding: Option<ui::KeyBinding>, action_name: &'static str, + humanized_action_name: SharedString, action_arguments: Option<SyntaxHighlightedText>, action_docs: Option<&'static str>, action_schema: Option<schemars::Schema>, @@ -1079,7 +1138,7 @@ struct ProcessedKeybinding { impl ProcessedKeybinding { fn get_action_mapping(&self) -> ActionMapping { ActionMapping { - keystroke_text: self.keystroke_text.clone(), + keystrokes: self.keystrokes().map(Vec::from).unwrap_or_default(), context: self .context .as_ref() @@ -1223,38 +1282,39 @@ impl Render for KeymapEditor { window.dispatch_action(ToggleKeystrokeSearch.boxed_clone(), cx); }), ) - .when(self.keybinding_conflict_state.any_conflicts(), |this| { - this.child( - IconButton::new("KeymapEditorConflictIcon", IconName::Warning) - .shape(ui::IconButtonShape::Square) - .tooltip({ - let filter_state = self.filter_state; - - move |window, cx| { - Tooltip::for_action( - match filter_state { - FilterState::All => "Show Conflicts", - FilterState::Conflicts => "Hide Conflicts", - }, - &ToggleConflictFilter, - window, - cx, - ) - } - }) - .selected_icon_color(Color::Warning) - .toggle_state(matches!( - self.filter_state, - FilterState::Conflicts - )) - .on_click(|_, window, cx| { - window.dispatch_action( - ToggleConflictFilter.boxed_clone(), + .child( + IconButton::new("KeymapEditorConflictIcon", IconName::Warning) + .shape(ui::IconButtonShape::Square) + .when(self.keybinding_conflict_state.any_conflicts(), |this| { + this.indicator(Indicator::dot().color(Color::Warning)) + }) + .tooltip({ + let filter_state = self.filter_state; + + move |window, cx| { + Tooltip::for_action( + match filter_state { + FilterState::All => "Show Conflicts", + FilterState::Conflicts => "Hide Conflicts", + }, + &ToggleConflictFilter, + window, cx, - ); - }), - ) - }), + ) + } + }) + .selected_icon_color(Color::Warning) + .toggle_state(matches!( + self.filter_state, + FilterState::Conflicts + )) + .on_click(|_, window, cx| { + window.dispatch_action( + ToggleConflictFilter.boxed_clone(), + cx, + ); + }), + ), ) .when_some( match self.search_mode { @@ -1310,13 +1370,17 @@ impl Render for KeymapEditor { Table::new() .interactable(&self.table_interaction_state) .striped() + .empty_table_callback({ + let this = cx.entity(); + move |window, cx| this.read(cx).render_no_matches_hint(window, cx) + }) .column_widths([ - rems(2.5), - rems(16.), - rems(16.), - rems(16.), - rems(32.), - rems(8.), + DefiniteLength::Absolute(AbsoluteLength::Pixels(px(40.))), + DefiniteLength::Fraction(0.25), + DefiniteLength::Fraction(0.20), + DefiniteLength::Fraction(0.14), + DefiniteLength::Fraction(0.45), + DefiniteLength::Fraction(0.08), ]) .header(["", "Action", "Arguments", "Keystrokes", "Context", "Source"]) .uniform_list( @@ -1393,10 +1457,9 @@ impl Render for KeymapEditor { .id(("keymap action", index)) .child({ if action_name != gpui::NoAction.name() { - this.humanized_action_names - .get(action_name) - .cloned() - .unwrap_or(action_name.into()) + binding + .humanized_action_name + .clone() .into_any_element() } else { const NULL: SharedString = @@ -1606,7 +1669,7 @@ impl RenderOnce for SyntaxHighlightedText { runs.push(text_style.to_run(text.len() - offset)); } - return StyledText::new(text).with_runs(runs); + StyledText::new(text).with_runs(runs) } } @@ -1621,8 +1684,8 @@ impl InputError { Self::Warning(message.into()) } - fn error(message: impl Into<SharedString>) -> Self { - Self::Error(message.into()) + fn error(error: anyhow::Error) -> Self { + Self::Error(error.to_string().into()) } fn content(&self) -> &SharedString { @@ -1630,10 +1693,6 @@ impl InputError { InputError::Warning(content) | InputError::Error(content) => content, } } - - fn is_warning(&self) -> bool { - matches!(self, InputError::Warning(_)) - } } struct KeybindingEditorModal { @@ -1766,17 +1825,14 @@ impl KeybindingEditorModal { } } - fn set_error(&mut self, error: InputError, cx: &mut Context<Self>) -> bool { + fn set_error(&mut self, error: InputError, cx: &mut Context<Self>) { if self .error .as_ref() - .is_some_and(|old_error| old_error.is_warning() && *old_error == error) + .is_none_or(|old_error| *old_error != error) { - false - } else { self.error = Some(error); cx.notify(); - true } } @@ -1818,66 +1874,62 @@ impl KeybindingEditorModal { Ok(Some(context)) } - fn save(&mut self, cx: &mut Context<Self>) { + fn save_or_display_error(&mut self, cx: &mut Context<Self>) { + self.save(cx).map_err(|err| self.set_error(err, cx)).ok(); + } + + fn save(&mut self, cx: &mut Context<Self>) -> Result<(), InputError> { let existing_keybind = self.editing_keybind.clone(); let fs = self.fs.clone(); let tab_size = cx.global::<settings::SettingsStore>().json_tab_size(); - let new_keystrokes = match self.validate_keystrokes(cx) { - Err(err) => { - self.set_error(InputError::error(err.to_string()), cx); - return; - } - Ok(keystrokes) => keystrokes, - }; - let new_context = match self.validate_context(cx) { - Err(err) => { - self.set_error(InputError::error(err.to_string()), cx); - return; - } - Ok(context) => context, - }; + let new_keystrokes = self + .validate_keystrokes(cx) + .map_err(InputError::error)? + .into_iter() + .map(remove_key_char) + .collect::<Vec<_>>(); - let new_action_args = match self.validate_action_arguments(cx) { - Err(input_err) => { - self.set_error(InputError::error(input_err.to_string()), cx); - return; - } - Ok(input) => input, - }; + let new_context = self.validate_context(cx).map_err(InputError::error)?; + let new_action_args = self + .validate_action_arguments(cx) + .map_err(InputError::error)?; let action_mapping = ActionMapping { - keystroke_text: ui::text_for_keystrokes(&new_keystrokes, cx).into(), - context: new_context.as_ref().map(Into::into), + keystrokes: new_keystrokes, + context: new_context.map(SharedString::from), }; let conflicting_indices = if self.creating { self.keymap_editor .read(cx) .keybinding_conflict_state - .will_conflict(action_mapping) + .will_conflict(&action_mapping) } else { self.keymap_editor .read(cx) .keybinding_conflict_state - .conflicting_indices_for_mapping(action_mapping, self.editing_keybind_idx) + .conflicting_indices_for_mapping(&action_mapping, self.editing_keybind_idx) }; - if let Some(conflicting_indices) = conflicting_indices { - let first_conflicting_index = conflicting_indices[0]; + + conflicting_indices.map(|KeybindConflict { + first_conflict_index, + remaining_conflict_amount, + }| + { let conflicting_action_name = self .keymap_editor .read(cx) .keybindings - .get(first_conflicting_index) + .get(first_conflict_index) .map(|keybind| keybind.action_name); let warning_message = match conflicting_action_name { Some(name) => { - let confliction_action_amount = conflicting_indices.len() - 1; - if confliction_action_amount > 0 { + if remaining_conflict_amount > 0 { format!( "Your keybind would conflict with the \"{}\" action and {} other bindings", - name, confliction_action_amount + name, remaining_conflict_amount ) } else { format!("Your keybind would conflict with the \"{}\" action", name) @@ -1886,23 +1938,26 @@ impl KeybindingEditorModal { None => { log::info!( "Could not find action in keybindings with index {}", - first_conflicting_index + first_conflict_index ); "Your keybind would conflict with other actions".to_string() } }; - if self.set_error(InputError::warning(warning_message), cx) { - return; + let warning = InputError::warning(warning_message); + if self.error.as_ref().is_some_and(|old_error| *old_error == warning) { + Ok(()) + } else { + Err(warning) } - } + }).unwrap_or(Ok(()))?; let create = self.creating; let status_toast = StatusToast::new( format!( "Saved edits to the {} action.", - command_palette::humanize_action_name(&self.editing_keybind.action_name) + &self.editing_keybind.humanized_action_name ), cx, move |this, _cx| { @@ -1924,8 +1979,7 @@ impl KeybindingEditorModal { if let Err(err) = save_keybinding_update( create, existing_keybind, - &new_keystrokes, - new_context.as_deref(), + &action_mapping, new_action_args.as_deref(), &fs, tab_size, @@ -1933,17 +1987,11 @@ impl KeybindingEditorModal { .await { this.update(cx, |this, cx| { - this.set_error(InputError::error(err.to_string()), cx); + this.set_error(InputError::error(err), cx); }) .log_err(); } else { this.update(cx, |this, cx| { - let action_mapping = ActionMapping { - keystroke_text: ui::text_for_keystrokes(new_keystrokes.as_slice(), cx) - .into(), - context: new_context.map(SharedString::from), - }; - this.keymap_editor.update(cx, |keymap, cx| { keymap.previous_edit = Some(PreviousEdit::Keybinding { action_mapping, @@ -1960,6 +2008,8 @@ impl KeybindingEditorModal { } }) .detach(); + + Ok(()) } fn key_context(&self) -> KeyContext { @@ -1982,7 +2032,7 @@ impl KeybindingEditorModal { } fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) { - self.save(cx); + self.save_or_display_error(cx); } fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context<Self>) { @@ -1990,11 +2040,17 @@ impl KeybindingEditorModal { } } +fn remove_key_char(Keystroke { modifiers, key, .. }: Keystroke) -> Keystroke { + Keystroke { + modifiers, + key, + ..Default::default() + } +} + impl Render for KeybindingEditorModal { fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let theme = cx.theme().colors(); - let action_name = - command_palette::humanize_action_name(&self.editing_keybind.action_name).to_string(); v_flex() .w(rems(34.)) @@ -2014,7 +2070,9 @@ impl Render for KeybindingEditorModal { .gap_0p5() .border_b_1() .border_color(theme.border_variant) - .child(Label::new(action_name)) + .child(Label::new( + self.editing_keybind.humanized_action_name.clone(), + )) .when_some(self.editing_keybind.action_docs, |this, docs| { this.child( Label::new(docs).size(LabelSize::Small).color(Color::Muted), @@ -2085,7 +2143,7 @@ impl Render for KeybindingEditorModal { ) .child(Button::new("save-btn", "Save").on_click(cx.listener( |this, _event, _window, cx| { - this.save(cx); + this.save_or_display_error(cx); }, ))), ), @@ -2273,8 +2331,7 @@ async fn load_keybind_context_language( async fn save_keybinding_update( create: bool, existing: ProcessedKeybinding, - new_keystrokes: &[Keystroke], - new_context: Option<&str>, + action_mapping: &ActionMapping, new_args: Option<&str>, fs: &Arc<dyn Fs>, tab_size: usize, @@ -2301,8 +2358,8 @@ async fn save_keybinding_update( }; let source = settings::KeybindUpdateTarget { - context: new_context, - keystrokes: new_keystrokes, + context: action_mapping.context.as_ref().map(|a| &***a), + keystrokes: &action_mapping.keystrokes, action_name: &existing.action_name, action_arguments: new_args, }; @@ -2772,7 +2829,7 @@ impl Render for KeystrokeInput { IconName::PlayFilled }; - return h_flex() + h_flex() .id("keystroke-input") .track_focus(&self.outer_focus_handle) .py_2() @@ -2895,7 +2952,7 @@ impl Render for KeystrokeInput { this.clear_keystrokes(&ClearKeystrokes, window, cx); })), ), - ); + ) } } diff --git a/crates/settings_ui/src/ui_components/table.rs b/crates/settings_ui/src/ui_components/table.rs index 98dd7387659a0ed9afdff8a7b4280c2a03685174..6ea59cd2f42eb570237465430ccffbf8f753b16f 100644 --- a/crates/settings_ui/src/ui_components/table.rs +++ b/crates/settings_ui/src/ui_components/table.rs @@ -40,6 +40,10 @@ impl<const COLS: usize> TableContents<COLS> { TableContents::UniformList(data) => data.row_count, } } + + fn is_empty(&self) -> bool { + self.len() == 0 + } } pub struct TableInteractionState { @@ -375,6 +379,7 @@ pub struct Table<const COLS: usize = 3> { interaction_state: Option<WeakEntity<TableInteractionState>>, column_widths: Option<[Length; COLS]>, map_row: Option<Rc<dyn Fn((usize, Div), &mut Window, &mut App) -> AnyElement>>, + empty_table_callback: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyElement>>, } impl<const COLS: usize> Table<COLS> { @@ -388,6 +393,7 @@ impl<const COLS: usize> Table<COLS> { interaction_state: None, column_widths: None, map_row: None, + empty_table_callback: None, } } @@ -460,6 +466,15 @@ impl<const COLS: usize> Table<COLS> { self.map_row = Some(Rc::new(callback)); self } + + /// Provide a callback that is invoked when the table is rendered without any rows + pub fn empty_table_callback( + mut self, + callback: impl Fn(&mut Window, &mut App) -> AnyElement + 'static, + ) -> Self { + self.empty_table_callback = Some(Rc::new(callback)); + self + } } fn base_cell_style(width: Option<Length>, cx: &App) -> Div { @@ -582,6 +597,7 @@ impl<const COLS: usize> RenderOnce for Table<COLS> { }; let width = self.width; + let no_rows_rendered = self.rows.is_empty(); let table = div() .when_some(width, |this, width| this.w(width)) @@ -662,6 +678,21 @@ impl<const COLS: usize> RenderOnce for Table<COLS> { }) }), ) + .when_some( + no_rows_rendered + .then_some(self.empty_table_callback) + .flatten(), + |this, callback| { + this.child( + h_flex() + .size_full() + .p_3() + .items_start() + .justify_center() + .child(callback(window, cx)), + ) + }, + ) .when_some( width.and(interaction_state.as_ref()), |this, interaction_state| { From d470411725d8de6f1129d3b9ddd322b6eaf50e42 Mon Sep 17 00:00:00 2001 From: Richard Feldman <oss@rtfeldman.com> Date: Thu, 17 Jul 2025 18:12:48 -0400 Subject: [PATCH 168/658] Improve upstream error reporting (#34668) Now we handle more upstream error cases using the same auto-retry logic. Release Notes: - N/A --- crates/agent/src/thread.rs | 29 +++ crates/agent_ui/src/active_thread.rs | 2 +- .../assistant_tools/src/edit_agent/evals.rs | 25 +++ crates/language_model/src/language_model.rs | 6 + crates/language_models/src/provider/cloud.rs | 206 ++++++++++++++++++ 5 files changed, 267 insertions(+), 1 deletion(-) diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index dca0f5a4304ff2da842bbe2ad0ff1939055535ca..54cc6296d5152752ed36b9b8c3b47fed94665abd 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -2146,6 +2146,35 @@ impl Thread { 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, so only retry once. + max_attempts: 1, + }), + 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 { + None + } + } + }, ApiInternalServerError { .. } => Some(RetryStrategy::Fixed { delay: BASE_RETRY_DELAY, max_attempts: 1, diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index 3cf68b887ddf032b9b9f3ea8092bdbd97f31f90f..f5f6952519e61938aa60d2d023313fcf32a53f3f 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -1036,7 +1036,7 @@ impl ActiveThread { .collect::<Vec<_>>() .join("\n"); self.last_error = Some(ThreadError::Message { - header: "Error interacting with language model".into(), + header: "Error".into(), message: error_message.into(), }); } diff --git a/crates/assistant_tools/src/edit_agent/evals.rs b/crates/assistant_tools/src/edit_agent/evals.rs index c7af7dc64e4507dda9e7140e22652c50501e3e75..eda7eee0e34be6cc31e1dcc9b50c48a6be8b5c57 100644 --- a/crates/assistant_tools/src/edit_agent/evals.rs +++ b/crates/assistant_tools/src/edit_agent/evals.rs @@ -12,6 +12,7 @@ use collections::HashMap; use fs::FakeFs; use futures::{FutureExt, future::LocalBoxFuture}; use gpui::{AppContext, TestAppContext, Timer}; +use http_client::StatusCode; use indoc::{formatdoc, indoc}; use language_model::{ LanguageModelRegistry, LanguageModelRequestTool, LanguageModelToolResult, @@ -1675,6 +1676,30 @@ async fn retry_on_rate_limit<R>(mut request: impl AsyncFnMut() -> Result<R>) -> Timer::after(retry_after + jitter).await; continue; } + LanguageModelCompletionError::UpstreamProviderError { + status, + retry_after, + .. + } => { + // Only retry for specific status codes + let should_retry = matches!( + *status, + StatusCode::TOO_MANY_REQUESTS | StatusCode::SERVICE_UNAVAILABLE + ) || status.as_u16() == 529; + + if !should_retry { + return Err(err.into()); + } + + // 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()), }, Err(err) => return Err(err), diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index 8962e9d8d15bce91e8004a272a5a74773a430179..6bd33fcdf508b33fb397ccc602de2b719d4906a2 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -116,6 +116,12 @@ pub enum LanguageModelCompletionError { provider: LanguageModelProviderName, message: String, }, + #[error("{message}")] + UpstreamProviderError { + message: String, + status: StatusCode, + retry_after: Option<Duration>, + }, #[error("HTTP response error from {provider}'s API: status {status_code} - {message:?}")] HttpResponseError { provider: LanguageModelProviderName, diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index c044a318b878d49f2d37acd60fed29d565ae14a4..6aea576258e6e46f5d1b9355a12007852296724a 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -644,8 +644,62 @@ struct ApiError { headers: HeaderMap<HeaderValue>, } +/// Represents error responses from Zed's cloud API. +/// +/// Example JSON for an upstream HTTP error: +/// ```json +/// { +/// "code": "upstream_http_error", +/// "message": "Received an error from the Anthropic API: upstream connect error or disconnect/reset before headers, reset reason: connection timeout", +/// "upstream_status": 503 +/// } +/// ``` +#[derive(Debug, serde::Deserialize)] +struct CloudApiError { + code: String, + message: String, + #[serde(default)] + #[serde(deserialize_with = "deserialize_optional_status_code")] + upstream_status: Option<StatusCode>, + #[serde(default)] + retry_after: Option<f64>, +} + +fn deserialize_optional_status_code<'de, D>(deserializer: D) -> Result<Option<StatusCode>, D::Error> +where + D: serde::Deserializer<'de>, +{ + let opt: Option<u16> = Option::deserialize(deserializer)?; + Ok(opt.and_then(|code| StatusCode::from_u16(code).ok())) +} + 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) + }; + + return LanguageModelCompletionError::UpstreamProviderError { + message: cloud_error.message, + status, + retry_after: cloud_error.retry_after.map(Duration::from_secs_f64), + }; + } + } + let retry_after = None; LanguageModelCompletionError::from_http_status( PROVIDER_NAME, @@ -1279,3 +1333,155 @@ impl Component for ZedAiConfiguration { ) } } + +#[cfg(test)] +mod tests { + use super::*; + use http_client::http::{HeaderMap, StatusCode}; + use language_model::LanguageModelCompletionError; + + #[test] + fn test_api_error_conversion_with_upstream_http_error() { + // upstream_http_error with 503 status should become ServerOverloaded + let error_body = r#"{"code":"upstream_http_error","message":"Received an error from the Anthropic API: upstream connect error or disconnect/reset before headers, reset reason: connection timeout","upstream_status":503}"#; + + let api_error = ApiError { + status: StatusCode::INTERNAL_SERVER_ERROR, + body: error_body.to_string(), + headers: HeaderMap::new(), + }; + + let completion_error: LanguageModelCompletionError = api_error.into(); + + match completion_error { + LanguageModelCompletionError::UpstreamProviderError { message, .. } => { + assert_eq!( + message, + "Received an error from the Anthropic API: upstream connect error or disconnect/reset before headers, reset reason: connection timeout" + ); + } + _ => panic!( + "Expected UpstreamProviderError for upstream 503, got: {:?}", + completion_error + ), + } + + // upstream_http_error with 500 status should become ApiInternalServerError + let error_body = r#"{"code":"upstream_http_error","message":"Received an error from the OpenAI API: internal server error","upstream_status":500}"#; + + let api_error = ApiError { + status: StatusCode::INTERNAL_SERVER_ERROR, + body: error_body.to_string(), + headers: HeaderMap::new(), + }; + + let completion_error: LanguageModelCompletionError = api_error.into(); + + match completion_error { + LanguageModelCompletionError::UpstreamProviderError { message, .. } => { + assert_eq!( + message, + "Received an error from the OpenAI API: internal server error" + ); + } + _ => panic!( + "Expected UpstreamProviderError for upstream 500, got: {:?}", + completion_error + ), + } + + // upstream_http_error with 429 status should become RateLimitExceeded + let error_body = r#"{"code":"upstream_http_error","message":"Received an error from the Google API: rate limit exceeded","upstream_status":429}"#; + + let api_error = ApiError { + status: StatusCode::INTERNAL_SERVER_ERROR, + body: error_body.to_string(), + headers: HeaderMap::new(), + }; + + let completion_error: LanguageModelCompletionError = api_error.into(); + + match completion_error { + LanguageModelCompletionError::UpstreamProviderError { message, .. } => { + assert_eq!( + message, + "Received an error from the Google API: rate limit exceeded" + ); + } + _ => panic!( + "Expected UpstreamProviderError for upstream 429, got: {:?}", + completion_error + ), + } + + // Regular 500 error without upstream_http_error should remain ApiInternalServerError for Zed + let error_body = "Regular internal server error"; + + let api_error = ApiError { + status: StatusCode::INTERNAL_SERVER_ERROR, + body: error_body.to_string(), + headers: HeaderMap::new(), + }; + + let completion_error: LanguageModelCompletionError = api_error.into(); + + match completion_error { + LanguageModelCompletionError::ApiInternalServerError { provider, message } => { + assert_eq!(provider, PROVIDER_NAME); + assert_eq!(message, "Regular internal server error"); + } + _ => panic!( + "Expected ApiInternalServerError for regular 500, got: {:?}", + completion_error + ), + } + + // upstream_http_429 format should be converted to UpstreamProviderError + let error_body = r#"{"code":"upstream_http_429","message":"Upstream Anthropic rate limit exceeded.","retry_after":30.5}"#; + + let api_error = ApiError { + status: StatusCode::INTERNAL_SERVER_ERROR, + body: error_body.to_string(), + headers: HeaderMap::new(), + }; + + let completion_error: LanguageModelCompletionError = api_error.into(); + + match completion_error { + LanguageModelCompletionError::UpstreamProviderError { + message, + status, + retry_after, + } => { + assert_eq!(message, "Upstream Anthropic rate limit exceeded."); + assert_eq!(status, StatusCode::TOO_MANY_REQUESTS); + assert_eq!(retry_after, Some(Duration::from_secs_f64(30.5))); + } + _ => panic!( + "Expected UpstreamProviderError for upstream_http_429, got: {:?}", + completion_error + ), + } + + // Invalid JSON in error body should fall back to regular error handling + let error_body = "Not JSON at all"; + + let api_error = ApiError { + status: StatusCode::INTERNAL_SERVER_ERROR, + body: error_body.to_string(), + headers: HeaderMap::new(), + }; + + let completion_error: LanguageModelCompletionError = api_error.into(); + + match completion_error { + LanguageModelCompletionError::ApiInternalServerError { provider, .. } => { + assert_eq!(provider, PROVIDER_NAME); + } + _ => panic!( + "Expected ApiInternalServerError for invalid JSON, got: {:?}", + completion_error + ), + } + } +} From 1ab659c71fc8b12ab37c96dafec546d3afb744ea Mon Sep 17 00:00:00 2001 From: Richard Feldman <oss@rtfeldman.com> Date: Thu, 17 Jul 2025 19:04:03 -0400 Subject: [PATCH 169/658] Retry on burn mode (#34669) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now we only auto-retry if burn mode is enabled. We also show a "Retry" button (so you don't have to type "continue") if you think that's the right remedy, and additionally we show a "Retry and Enable Burn Mode" button if you don't have it enabled. <img width="484" height="260" alt="Screenshot 2025-07-17 at 6 25 27 PM" src="https://github.com/user-attachments/assets/dc5bf1f6-8b11-4041-87aa-4f37c95ea9f0" /> <img width="478" height="307" alt="Screenshot 2025-07-17 at 6 22 36 PM" src="https://github.com/user-attachments/assets/1ed6578a-1696-449d-96d1-e447d11959fa" /> Release Notes: - Only auto-retry Agent requests when Burn Mode is enabled --- crates/agent/src/thread.rs | 200 ++++++++++++++++++++++++++++- crates/agent_ui/src/agent_panel.rs | 92 ++++++++++++- 2 files changed, 285 insertions(+), 7 deletions(-) diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 54cc6296d5152752ed36b9b8c3b47fed94665abd..180cc88390eb21d96c5109fac5802f42e425f83a 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -396,6 +396,7 @@ pub struct Thread { remaining_turns: u32, configured_model: Option<ConfiguredModel>, profile: AgentProfile, + last_error_context: Option<(Arc<dyn LanguageModel>, CompletionIntent)>, } #[derive(Clone, Debug)] @@ -489,10 +490,11 @@ impl Thread { retry_state: None, message_feedback: HashMap::default(), last_auto_capture_at: None, + last_error_context: None, last_received_chunk_at: None, request_callback: None, remaining_turns: u32::MAX, - configured_model, + configured_model: configured_model.clone(), profile: AgentProfile::new(profile_id, tools), } } @@ -613,6 +615,7 @@ impl Thread { feedback: None, message_feedback: HashMap::default(), last_auto_capture_at: None, + last_error_context: None, last_received_chunk_at: None, request_callback: None, remaining_turns: u32::MAX, @@ -1264,9 +1267,58 @@ impl Thread { self.flush_notifications(model.clone(), intent, cx); - let request = self.to_completion_request(model.clone(), intent, cx); + let _checkpoint = self.finalize_pending_checkpoint(cx); + self.stream_completion( + self.to_completion_request(model.clone(), intent, cx), + model, + intent, + window, + cx, + ); + } + + pub fn retry_last_completion( + &mut self, + window: Option<AnyWindowHandle>, + cx: &mut Context<Self>, + ) { + // Clear any existing error state + self.retry_state = None; + + // Use the last error context if available, otherwise fall back to configured model + let (model, intent) = if let Some((model, intent)) = self.last_error_context.take() { + (model, intent) + } else if let Some(configured_model) = self.configured_model.as_ref() { + let model = configured_model.model.clone(); + let intent = if self.has_pending_tool_uses() { + CompletionIntent::ToolResults + } else { + CompletionIntent::UserPrompt + }; + (model, intent) + } else if let Some(configured_model) = self.get_or_init_configured_model(cx) { + let model = configured_model.model.clone(); + let intent = if self.has_pending_tool_uses() { + CompletionIntent::ToolResults + } else { + CompletionIntent::UserPrompt + }; + (model, intent) + } else { + return; + }; - self.stream_completion(request, model, intent, window, cx); + self.send_to_model(model, intent, window, cx); + } + + pub fn enable_burn_mode_and_retry( + &mut self, + window: Option<AnyWindowHandle>, + cx: &mut Context<Self>, + ) { + self.completion_mode = CompletionMode::Burn; + cx.emit(ThreadEvent::ProfileChanged); + self.retry_last_completion(window, cx); } pub fn used_tools_since_last_user_message(&self) -> bool { @@ -2222,6 +2274,23 @@ impl Thread { window: Option<AnyWindowHandle>, cx: &mut Context<Self>, ) -> bool { + // Store context for the Retry button + self.last_error_context = Some((model.clone(), intent)); + + // Only auto-retry if Burn Mode is enabled + if self.completion_mode != CompletionMode::Burn { + // Show error with retry options + cx.emit(ThreadEvent::ShowError(ThreadError::RetryableError { + message: format!( + "{}\n\nTo automatically retry when similar errors happen, enable Burn Mode.", + error + ) + .into(), + can_enable_burn_mode: true, + })); + return false; + } + let Some(strategy) = strategy.or_else(|| Self::get_retry_strategy(error)) else { return false; }; @@ -2302,6 +2371,13 @@ impl Thread { // Stop generating since we're giving up on retrying. self.pending_completions.clear(); + // Show error alongside a Retry button, but no + // Enable Burn Mode button (since it's already enabled) + cx.emit(ThreadEvent::ShowError(ThreadError::RetryableError { + message: format!("Failed after retrying: {}", error).into(), + can_enable_burn_mode: false, + })); + false } } @@ -3212,6 +3288,11 @@ pub enum ThreadError { header: SharedString, message: SharedString, }, + #[error("Retryable error: {message}")] + RetryableError { + message: SharedString, + can_enable_burn_mode: bool, + }, } #[derive(Debug, Clone)] @@ -4167,6 +4248,11 @@ fn main() {{ let project = create_test_project(cx, json!({})).await; let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await; + // Enable Burn Mode to allow retries + thread.update(cx, |thread, _| { + thread.set_completion_mode(CompletionMode::Burn); + }); + // Create model that returns overloaded error let model = Arc::new(ErrorInjector::new(TestError::Overloaded)); @@ -4240,6 +4326,11 @@ fn main() {{ let project = create_test_project(cx, json!({})).await; let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await; + // Enable Burn Mode to allow retries + thread.update(cx, |thread, _| { + thread.set_completion_mode(CompletionMode::Burn); + }); + // Create model that returns internal server error let model = Arc::new(ErrorInjector::new(TestError::InternalServerError)); @@ -4316,6 +4407,11 @@ fn main() {{ let project = create_test_project(cx, json!({})).await; let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await; + // Enable Burn Mode to allow retries + thread.update(cx, |thread, _| { + thread.set_completion_mode(CompletionMode::Burn); + }); + // Create model that returns internal server error let model = Arc::new(ErrorInjector::new(TestError::InternalServerError)); @@ -4423,6 +4519,11 @@ fn main() {{ let project = create_test_project(cx, json!({})).await; let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await; + // Enable Burn Mode to allow retries + thread.update(cx, |thread, _| { + thread.set_completion_mode(CompletionMode::Burn); + }); + // Create model that returns overloaded error let model = Arc::new(ErrorInjector::new(TestError::Overloaded)); @@ -4509,6 +4610,11 @@ fn main() {{ let project = create_test_project(cx, json!({})).await; let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await; + // Enable Burn Mode to allow retries + thread.update(cx, |thread, _| { + thread.set_completion_mode(CompletionMode::Burn); + }); + // We'll use a wrapper to switch behavior after first failure struct RetryTestModel { inner: Arc<FakeLanguageModel>, @@ -4677,6 +4783,11 @@ fn main() {{ let project = create_test_project(cx, json!({})).await; let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await; + // Enable Burn Mode to allow retries + thread.update(cx, |thread, _| { + thread.set_completion_mode(CompletionMode::Burn); + }); + // Create a model that fails once then succeeds struct FailOnceModel { inner: Arc<FakeLanguageModel>, @@ -4838,6 +4949,11 @@ fn main() {{ let project = create_test_project(cx, json!({})).await; let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await; + // Enable Burn Mode to allow retries + thread.update(cx, |thread, _| { + thread.set_completion_mode(CompletionMode::Burn); + }); + // Create a model that returns rate limit error with retry_after struct RateLimitModel { inner: Arc<FakeLanguageModel>, @@ -5111,6 +5227,79 @@ fn main() {{ ); } + #[gpui::test] + async fn test_no_retry_without_burn_mode(cx: &mut TestAppContext) { + init_test_settings(cx); + + let project = create_test_project(cx, json!({})).await; + let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await; + + // Ensure we're in Normal mode (not Burn mode) + thread.update(cx, |thread, _| { + thread.set_completion_mode(CompletionMode::Normal); + }); + + // Track error events + let error_events = Arc::new(Mutex::new(Vec::new())); + let error_events_clone = error_events.clone(); + + let _subscription = thread.update(cx, |_, cx| { + cx.subscribe(&thread, move |_, _, event: &ThreadEvent, _| { + if let ThreadEvent::ShowError(error) = event { + error_events_clone.lock().push(error.clone()); + } + }) + }); + + // Create model that returns overloaded error + let model = Arc::new(ErrorInjector::new(TestError::Overloaded)); + + // Insert a user message + thread.update(cx, |thread, cx| { + thread.insert_user_message("Hello!", ContextLoadResult::default(), None, vec![], cx); + }); + + // Start completion + thread.update(cx, |thread, cx| { + thread.send_to_model(model.clone(), CompletionIntent::UserPrompt, None, cx); + }); + + cx.run_until_parked(); + + // Verify no retry state was created + thread.read_with(cx, |thread, _| { + assert!( + thread.retry_state.is_none(), + "Should not have retry state in Normal mode" + ); + }); + + // Check that a retryable error was reported + let errors = error_events.lock(); + assert!(!errors.is_empty(), "Should have received an error event"); + + if let ThreadError::RetryableError { + message: _, + can_enable_burn_mode, + } = &errors[0] + { + assert!( + *can_enable_burn_mode, + "Error should indicate burn mode can be enabled" + ); + } else { + panic!("Expected RetryableError, got {:?}", errors[0]); + } + + // Verify the thread is no longer generating + thread.read_with(cx, |thread, _| { + assert!( + !thread.is_generating(), + "Should not be generating after error without retry" + ); + }); + } + #[gpui::test] async fn test_retry_cancelled_on_stop(cx: &mut TestAppContext) { init_test_settings(cx); @@ -5118,6 +5307,11 @@ fn main() {{ let project = create_test_project(cx, json!({})).await; let (_, _, thread, _, _base_model) = setup_test_environment(cx, project.clone()).await; + // Enable Burn Mode to allow retries + thread.update(cx, |thread, _| { + thread.set_completion_mode(CompletionMode::Burn); + }); + // Create model that returns overloaded error let model = Arc::new(ErrorInjector::new(TestError::Overloaded)); diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 895a49950241e0072ac3926b2ed78e9248918442..139e32f835602e5bb72b6610b9519fe37ab0633f 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -65,8 +65,9 @@ use theme::ThemeSettings; use time::UtcOffset; use ui::utils::WithRemSize; use ui::{ - Banner, Callout, CheckboxWithLabel, ContextMenu, ElevationIndex, KeyBinding, PopoverMenu, - PopoverMenuHandle, ProgressBar, Tab, Tooltip, Vector, VectorName, prelude::*, + Banner, Button, Callout, CheckboxWithLabel, ContextMenu, ElevationIndex, IconPosition, + KeyBinding, PopoverMenu, PopoverMenuHandle, ProgressBar, Tab, Tooltip, Vector, VectorName, + prelude::*, }; use util::ResultExt as _; use workspace::{ @@ -2977,6 +2978,21 @@ impl AgentPanel { .size(IconSize::Small) .color(Color::Error); + let retry_button = Button::new("retry", "Retry") + .icon(IconName::RotateCw) + .icon_position(IconPosition::Start) + .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) @@ -2985,13 +3001,72 @@ impl AgentPanel { .icon(icon) .title(header) .description(message.clone()) - .primary_action(self.dismiss_error_button(thread, cx)) - .secondary_action(self.create_copy_button(message_with_header)) + .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)), ) .into_any_element() } + fn render_retryable_error( + &self, + 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) + .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) + .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) + .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) + .into_any_element() + } + fn render_prompt_editor( &self, context_editor: &Entity<TextThreadEditor>, @@ -3233,6 +3308,15 @@ impl Render for AgentPanel { ThreadError::Message { header, message } => { self.render_error_message(header, message, thread, cx) } + ThreadError::RetryableError { + message, + can_enable_burn_mode, + } => self.render_retryable_error( + message, + can_enable_burn_mode, + thread, + cx, + ), }) .into_any(), ) From f0a91502a925d74491083bd126690b6f730fbdf1 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 17 Jul 2025 20:22:21 -0300 Subject: [PATCH 170/658] keymap_ui: Add some design refinements (#34673) Mostly small stuff over here. Release Notes: - N/A --- crates/settings_ui/src/keybindings.rs | 91 +++++++++++++++------------ 1 file changed, 50 insertions(+), 41 deletions(-) diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index d5ac253fb8c071ccab2adc6ef3379ba94cd509c6..2e00426c1bf124ae1b7520c4889889d06ca712ff 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -808,7 +808,8 @@ impl KeymapEditor { "Copy Context", Box::new(CopyContext), ) - .entry("Show matching keybindings", None, { + .separator() + .entry("Show Matching Keybindings", None, { move |_, cx| { weak.update(cx, |this, cx| { this.filter_on_selected_binding_keystrokes(cx); @@ -1211,10 +1212,11 @@ impl Render for KeymapEditor { fn render(&mut self, _window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement { let row_count = self.matches.len(); let theme = cx.theme(); + let focus_handle = &self.focus_handle; v_flex() .id("keymap-editor") - .track_focus(&self.focus_handle) + .track_focus(focus_handle) .key_context(self.key_context()) .on_action(cx.listener(Self::select_next)) .on_action(cx.listener(Self::select_previous)) @@ -1229,16 +1231,15 @@ impl Render for KeymapEditor { .on_action(cx.listener(Self::toggle_conflict_filter)) .on_action(cx.listener(Self::toggle_keystroke_search)) .on_action(cx.listener(Self::toggle_exact_keystroke_matching)) + .on_mouse_move(cx.listener(|this, _, _window, _cx| { + this.show_hover_menus = true; + })) .size_full() .p_2() .gap_1() .bg(theme.colors().editor_background) - .on_mouse_move(cx.listener(|this, _, _window, _cx| { - this.show_hover_menus = true; - })) .child( v_flex() - .p_2() .gap_2() .child( h_flex() @@ -1266,13 +1267,18 @@ impl Render for KeymapEditor { IconName::Keyboard, ) .shape(ui::IconButtonShape::Square) - .tooltip(|window, cx| { - Tooltip::for_action( - "Search by Keystroke", - &ToggleKeystrokeSearch, - window, - cx, - ) + .tooltip({ + let focus_handle = focus_handle.clone(); + + move |window, cx| { + Tooltip::for_action_in( + "Search by Keystroke", + &ToggleKeystrokeSearch, + &focus_handle.clone(), + window, + cx, + ) + } }) .toggle_state(matches!( self.search_mode, @@ -1290,14 +1296,16 @@ impl Render for KeymapEditor { }) .tooltip({ let filter_state = self.filter_state; + let focus_handle = focus_handle.clone(); move |window, cx| { - Tooltip::for_action( + Tooltip::for_action_in( match filter_state { FilterState::All => "Show Conflicts", FilterState::Conflicts => "Hide Conflicts", }, &ToggleConflictFilter, + &focus_handle.clone(), window, cx, ) @@ -1331,35 +1339,36 @@ impl Render for KeymapEditor { this.pr_7() } }) + .gap_2() .child(self.keystroke_editor.clone()) .child( - div().p_1().child( - IconButton::new( - "keystrokes-exact-match", - IconName::Equal, - ) - .tooltip(move |window, cx| { - Tooltip::for_action( - if exact_match { - "Partial match mode" - } else { - "Exact match mode" - }, + IconButton::new( + "keystrokes-exact-match", + IconName::CaseSensitive, + ) + .tooltip({ + let keystroke_focus_handle = + self.keystroke_editor.read(cx).focus_handle(cx); + + move |window, cx| { + Tooltip::for_action_in( + "Toggle Exact Match Mode", &ToggleExactKeystrokeMatching, + &keystroke_focus_handle, window, cx, ) - }) - .shape(IconButtonShape::Square) - .toggle_state(exact_match) - .on_click( - cx.listener(|_, _, window, cx| { - window.dispatch_action( - ToggleExactKeystrokeMatching.boxed_clone(), - cx, - ); - }), - ), + } + }) + .shape(IconButtonShape::Square) + .toggle_state(exact_match) + .on_click( + cx.listener(|_, _, window, cx| { + window.dispatch_action( + ToggleExactKeystrokeMatching.boxed_clone(), + cx, + ); + }), ), ), ) @@ -2771,7 +2780,7 @@ impl Render for KeystrokeInput { .editor_background .blend(colors.text_accent.opacity(0.1)); - let recording_pulse = || { + let recording_pulse = |color: Color| { Icon::new(IconName::Circle) .size(IconSize::Small) .color(Color::Error) @@ -2781,7 +2790,7 @@ impl Render for KeystrokeInput { .repeat() .with_easing(gpui::pulsating_between(0.4, 0.8)), { - let color = Color::Error.color(cx); + let color = color.color(cx); move |this, delta| this.color(Color::Custom(color.opacity(delta))) }, ) @@ -2797,7 +2806,7 @@ impl Render for KeystrokeInput { .editor_background .blend(colors.text_accent.opacity(0.1))) .rounded_sm() - .child(recording_pulse()) + .child(recording_pulse(Color::Error)) .child( Label::new("REC") .size(LabelSize::XSmall) @@ -2815,7 +2824,7 @@ impl Render for KeystrokeInput { .editor_background .blend(colors.text_accent.opacity(0.1))) .rounded_sm() - .child(recording_pulse()) + .child(recording_pulse(Color::Accent)) .child( Label::new("SEARCH") .size(LabelSize::XSmall) From ed4deaa7389e6d740c32b9a3a9c0dd69b7bececf Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 17 Jul 2025 20:46:16 -0300 Subject: [PATCH 171/658] agent: Remove layout shift due to the "waiting for confirmation" label (#34674) Take 2 on https://github.com/zed-industries/zed/pull/33046. Release Notes: - N/A --- crates/agent/src/context_server_tool.rs | 2 +- crates/agent_ui/src/active_thread.rs | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/agent/src/context_server_tool.rs b/crates/agent/src/context_server_tool.rs index da7de1e312cea24c1be63568cc796a49ddfa178c..4c6d2b2b0bcc528df364b3122eacc8350d6a99be 100644 --- a/crates/agent/src/context_server_tool.rs +++ b/crates/agent/src/context_server_tool.rs @@ -38,7 +38,7 @@ impl Tool for ContextServerTool { } fn icon(&self) -> IconName { - IconName::Cog + IconName::ToolHammer } fn source(&self) -> ToolSource { diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index f5f6952519e61938aa60d2d023313fcf32a53f3f..14e7cf05b51b8f302d58ee928c7c0bbde0d4fc31 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -3202,7 +3202,10 @@ impl ActiveThread { .border_color(self.tool_card_border_color(cx)) .rounded_b_lg() .child( - LoadingLabel::new("Waiting for Confirmation").size(LabelSize::Small) + div() + .min_w(rems_from_px(145.)) + .child(LoadingLabel::new("Waiting for Confirmation").size(LabelSize::Small) + ) ) .child( h_flex() @@ -3247,7 +3250,6 @@ impl ActiveThread { }, )) }) - .child(ui::Divider::vertical()) .child({ let tool_id = tool_use.id.clone(); Button::new("allow-tool-action", "Allow") From 4314b3528898050c119075f0adf98e8b0c680371 Mon Sep 17 00:00:00 2001 From: Ben Kunkle <ben@zed.dev> Date: Thu, 17 Jul 2025 19:03:10 -0500 Subject: [PATCH 172/658] keymap_ui: Don't panic on `KeybindSource::from_meta` (#34652) Closes #ISSUE Log error instead of panicking when `from_meta` is passed an invalid value Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/settings/src/keymap_file.rs | 10 +++++++--- crates/settings_ui/src/keybindings.rs | 5 ++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index e6a32f731b173d26a998e6e1c7e57f43d39a3e6b..3a01d889c47f88216f2271025bcf782828accae5 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -992,13 +992,17 @@ impl KeybindSource { } pub fn from_meta(index: KeyBindingMetaIndex) -> Self { - match index { + Self::try_from_meta(index).unwrap() + } + + pub fn try_from_meta(index: KeyBindingMetaIndex) -> Result<Self> { + Ok(match index { Self::USER => KeybindSource::User, Self::BASE => KeybindSource::Base, Self::DEFAULT => KeybindSource::Default, Self::VIM => KeybindSource::Vim, - _ => unreachable!(), - } + _ => anyhow::bail!("Invalid keybind source {:?}", index), + }) } } diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 2e00426c1bf124ae1b7520c4889889d06ca712ff..29927c419676c034d52b292703774d80619dc717 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -567,7 +567,10 @@ impl KeymapEditor { let mut string_match_candidates = Vec::new(); for key_binding in key_bindings { - let source = key_binding.meta().map(settings::KeybindSource::from_meta); + let source = key_binding + .meta() + .map(settings::KeybindSource::try_from_meta) + .and_then(|source| source.log_err()); let keystroke_text = ui::text_for_keystrokes(key_binding.keystrokes(), cx); let ui_key_binding = Some( From a7284adafab23d96330584bb0db38c85b0f171e5 Mon Sep 17 00:00:00 2001 From: Smit Barmase <heysmitbarmase@gmail.com> Date: Fri, 18 Jul 2025 06:07:52 +0530 Subject: [PATCH 173/658] =?UTF-8?q?editor:=20Fix=20cursor=20doesn=E2=80=99?= =?UTF-8?q?t=20move=20up=20and=20down=20on=20arrow=20keys=20when=20no=20co?= =?UTF-8?q?mpletions=20are=20shown=20(#34678)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #34338 After https://github.com/zed-industries/zed/pull/31872, to avoid re-querying language servers, we keep the context menu around, which stores initial query, completions items, etc., even though it may not contain any items and hence not be rendered on screen. In this state, up/down arrows try to switch focus in the context menu instead of propagating it to the editor. Hence blocking buffer movement. This PR fixes it by changing the context for `menu`, `showing_completions`, and `showing_code_actions` to only be added when the menu is actually being rendered (i.e., not empty). Release Notes: - Fix an issue where the cursor doesn’t move up and down on arrow keys when no completions are shown. --- crates/editor/src/code_context_menus.rs | 2 +- crates/editor/src/editor.rs | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index c7477837dd0b6cdf8e818f16491cda569ec7fc47..9f842836ed20bb960c1e112398b8939d6f77e6cc 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -1384,7 +1384,7 @@ impl CodeActionsMenu { } } - fn visible(&self) -> bool { + pub fn visible(&self) -> bool { !self.actions.is_empty() } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6ad4fc0318ac322c03d45d585891ca729ef06b83..c3187f6b51188c946ac07b3c40f2a235e554c623 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2382,13 +2382,17 @@ impl Editor { } match self.context_menu.borrow().as_ref() { - Some(CodeContextMenu::Completions(_)) => { - key_context.add("menu"); - key_context.add("showing_completions"); + Some(CodeContextMenu::Completions(menu)) => { + if menu.visible() { + key_context.add("menu"); + key_context.add("showing_completions"); + } } - Some(CodeContextMenu::CodeActions(_)) => { - key_context.add("menu"); - key_context.add("showing_code_actions") + Some(CodeContextMenu::CodeActions(menu)) => { + if menu.visible() { + key_context.add("menu"); + key_context.add("showing_code_actions") + } } None => {} } From c287397a18e3f1385d634daf00f88776ca688533 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 17 Jul 2025 21:40:02 -0300 Subject: [PATCH 174/658] Rename "CloseInactiveItems" action to "CloseOtherItems" (#34676) This is following feedback from folks that were searching the "close others" action, available in the tab's context menu, and not finding it because it was actually named "close inactive", which was confusing. So, this PR makes sure the tab's menu item and the action have consistent naming. Release Notes: - Rename "CloseInactiveItems" action to "CloseOtherItems" for naming consistency. --- assets/keymaps/default-linux.json | 2 +- assets/keymaps/default-macos.json | 2 +- assets/keymaps/linux/emacs.json | 2 +- assets/keymaps/macos/emacs.json | 2 +- crates/collab/src/tests/following_tests.rs | 2 +- crates/editor/src/editor_tests.rs | 6 ++--- crates/vim/src/command.rs | 4 ++-- crates/workspace/src/pane.rs | 27 +++++++++++----------- crates/workspace/src/workspace.rs | 8 +++---- 9 files changed, 28 insertions(+), 27 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index ebc88ec135e53d6fee8ff3668f400d34f993abe9..b859d2d84c98f721c51f37302a7123a222796dfa 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -419,7 +419,7 @@ "ctrl-shift-pagedown": "pane::SwapItemRight", "ctrl-f4": ["pane::CloseActiveItem", { "close_pinned": false }], "ctrl-w": ["pane::CloseActiveItem", { "close_pinned": false }], - "alt-ctrl-t": ["pane::CloseInactiveItems", { "close_pinned": false }], + "alt-ctrl-t": ["pane::CloseOtherItems", { "close_pinned": false }], "alt-ctrl-shift-w": "workspace::CloseInactiveTabsAndPanes", "ctrl-k e": ["pane::CloseItemsToTheLeft", { "close_pinned": false }], "ctrl-k t": ["pane::CloseItemsToTheRight", { "close_pinned": false }], diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index cec485ce883d43404acfe62f7974ff3759cad207..748deaa05d5be88c9a1e4f79d6b4eb03dedbd028 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -477,7 +477,7 @@ "ctrl-shift-pageup": "pane::SwapItemLeft", "ctrl-shift-pagedown": "pane::SwapItemRight", "cmd-w": ["pane::CloseActiveItem", { "close_pinned": false }], - "alt-cmd-t": ["pane::CloseInactiveItems", { "close_pinned": false }], + "alt-cmd-t": ["pane::CloseOtherItems", { "close_pinned": false }], "ctrl-alt-cmd-w": "workspace::CloseInactiveTabsAndPanes", "cmd-k e": ["pane::CloseItemsToTheLeft", { "close_pinned": false }], "cmd-k t": ["pane::CloseItemsToTheRight", { "close_pinned": false }], diff --git a/assets/keymaps/linux/emacs.json b/assets/keymaps/linux/emacs.json index 0c633efabee89e5756b36e2ea5e5f31d02a5819d..0ff3796f03d85affdae88d009e88e73516ba385a 100755 --- a/assets/keymaps/linux/emacs.json +++ b/assets/keymaps/linux/emacs.json @@ -114,7 +114,7 @@ "ctrl-x o": "workspace::ActivateNextPane", // other-window "ctrl-x k": "pane::CloseActiveItem", // kill-buffer "ctrl-x 0": "pane::CloseActiveItem", // delete-window - "ctrl-x 1": "pane::CloseInactiveItems", // delete-other-windows + "ctrl-x 1": "pane::CloseOtherItems", // delete-other-windows "ctrl-x 2": "pane::SplitDown", // split-window-below "ctrl-x 3": "pane::SplitRight", // split-window-right "ctrl-x ctrl-f": "file_finder::Toggle", // find-file diff --git a/assets/keymaps/macos/emacs.json b/assets/keymaps/macos/emacs.json index 0c633efabee89e5756b36e2ea5e5f31d02a5819d..0ff3796f03d85affdae88d009e88e73516ba385a 100755 --- a/assets/keymaps/macos/emacs.json +++ b/assets/keymaps/macos/emacs.json @@ -114,7 +114,7 @@ "ctrl-x o": "workspace::ActivateNextPane", // other-window "ctrl-x k": "pane::CloseActiveItem", // kill-buffer "ctrl-x 0": "pane::CloseActiveItem", // delete-window - "ctrl-x 1": "pane::CloseInactiveItems", // delete-other-windows + "ctrl-x 1": "pane::CloseOtherItems", // delete-other-windows "ctrl-x 2": "pane::SplitDown", // split-window-below "ctrl-x 3": "pane::SplitRight", // split-window-right "ctrl-x ctrl-f": "file_finder::Toggle", // find-file diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index 3aa86a434dac611be260eb7f281d9067812c15ac..1a4c3a70a4c6cb622cb90dcd636a845c77c756c6 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -1013,7 +1013,7 @@ async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut T // and some of which were originally opened by client B. workspace_b.update_in(cx_b, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { - pane.close_inactive_items(&Default::default(), None, window, cx) + pane.close_other_items(&Default::default(), None, window, cx) .detach(); }); }); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 43c9c0db659210f68c59e225f1103405c2f14dc1..4efb052c71d284373981157c3ddc0cee8db276c6 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -55,7 +55,7 @@ use util::{ uri, }; use workspace::{ - CloseActiveItem, CloseAllItems, CloseInactiveItems, MoveItemToPaneInDirection, NavigationEntry, + CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry, OpenOptions, ViewId, item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions}, }; @@ -21463,7 +21463,7 @@ println!("5"); .unwrap(); pane_1 .update_in(cx, |pane, window, cx| { - pane.close_inactive_items(&CloseInactiveItems::default(), None, window, cx) + pane.close_other_items(&CloseOtherItems::default(), None, window, cx) }) .await .unwrap(); @@ -21499,7 +21499,7 @@ println!("5"); .unwrap(); pane_2 .update_in(cx, |pane, window, cx| { - pane.close_inactive_items(&CloseInactiveItems::default(), None, window, cx) + pane.close_other_items(&CloseOtherItems::default(), None, window, cx) }) .await .unwrap(); diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 74aed815a2aae5cffaa9e4c7a33b028305658258..23e04cae2c1efda237caf93414d16256f11eff04 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -1085,12 +1085,12 @@ fn generate_commands(_: &App) -> Vec<VimCommand> { ), VimCommand::new( ("tabo", "nly"), - workspace::CloseInactiveItems { + workspace::CloseOtherItems { save_intent: Some(SaveIntent::Close), close_pinned: false, }, ) - .bang(workspace::CloseInactiveItems { + .bang(workspace::CloseOtherItems { save_intent: Some(SaveIntent::Skip), close_pinned: false, }), diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 19afd49848db3f63a227a2660486b4f0a9f19d1d..7cc10c27f714bec6480c44cb241d8012eda138d6 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -116,7 +116,8 @@ pub struct CloseActiveItem { #[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] #[action(namespace = pane)] #[serde(deny_unknown_fields)] -pub struct CloseInactiveItems { +#[action(deprecated_aliases = ["pane::CloseInactiveItems"])] +pub struct CloseOtherItems { #[serde(default)] pub save_intent: Option<SaveIntent>, #[serde(default)] @@ -1354,9 +1355,9 @@ impl Pane { }) } - pub fn close_inactive_items( + pub fn close_other_items( &mut self, - action: &CloseInactiveItems, + action: &CloseOtherItems, target_item_id: Option<EntityId>, window: &mut Window, cx: &mut Context<Self>, @@ -2578,7 +2579,7 @@ impl Pane { save_intent: None, close_pinned: true, }; - let close_inactive_items_action = CloseInactiveItems { + let close_inactive_items_action = CloseOtherItems { save_intent: None, close_pinned: false, }; @@ -2610,7 +2611,7 @@ impl Pane { .action(Box::new(close_inactive_items_action.clone())) .disabled(total_items == 1) .handler(window.handler_for(&pane, move |pane, window, cx| { - pane.close_inactive_items( + pane.close_other_items( &close_inactive_items_action, Some(item_id), window, @@ -3521,8 +3522,8 @@ impl Render for Pane { }), ) .on_action( - cx.listener(|pane: &mut Self, action: &CloseInactiveItems, window, cx| { - pane.close_inactive_items(action, None, window, cx) + cx.listener(|pane: &mut Self, action: &CloseOtherItems, window, cx| { + pane.close_other_items(action, None, window, cx) .detach_and_log_err(cx); }), ) @@ -5853,8 +5854,8 @@ mod tests { assert_item_labels(&pane, ["A!", "B!", "C", "D", "E*"], cx); pane.update_in(cx, |pane, window, cx| { - pane.close_inactive_items( - &CloseInactiveItems { + pane.close_other_items( + &CloseOtherItems { save_intent: None, close_pinned: false, }, @@ -5890,8 +5891,8 @@ mod tests { assert_item_labels(&pane, ["A", "B", "C", "D", "E*"], cx); pane.update_in(cx, |pane, window, cx| { - pane.close_inactive_items( - &CloseInactiveItems { + pane.close_other_items( + &CloseOtherItems { save_intent: None, close_pinned: false, }, @@ -6256,8 +6257,8 @@ mod tests { .unwrap(); pane.update_in(cx, |pane, window, cx| { - pane.close_inactive_items( - &CloseInactiveItems { + pane.close_other_items( + &CloseOtherItems { save_intent: None, close_pinned: false, }, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index be5d693d356e3b328d1f3d0575a76df5c429f7dc..f37abe59e24b64cc8a7e4f699afb1e3cc569a42f 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2793,8 +2793,8 @@ impl Workspace { if retain_active_pane { let current_pane_close = current_pane.update(cx, |pane, cx| { - pane.close_inactive_items( - &CloseInactiveItems { + pane.close_other_items( + &CloseOtherItems { save_intent: None, close_pinned: false, }, @@ -9471,8 +9471,8 @@ mod tests { ); }); let close_all_but_multi_buffer_task = pane.update_in(cx, |pane, window, cx| { - pane.close_inactive_items( - &CloseInactiveItems { + pane.close_other_items( + &CloseOtherItems { save_intent: Some(SaveIntent::Save), close_pinned: true, }, From 8a7bd5f47b69741a66b8929f5937175cee4bc544 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 17 Jul 2025 22:19:27 -0300 Subject: [PATCH 175/658] agent: Adjust retry on Burn Mode layout (#34680) Quick follow-up to https://github.com/zed-industries/zed/pull/34669 so that the buttons don't look so big in comparison to the callout. Release Notes: - N/A --- crates/agent_ui/src/agent_panel.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 139e32f835602e5bb72b6610b9519fe37ab0633f..087eec5efb08de000f95be74226e120fde56654d 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -2981,6 +2981,8 @@ impl AgentPanel { 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| { @@ -3023,6 +3025,8 @@ impl AgentPanel { 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| { @@ -3046,6 +3050,8 @@ impl AgentPanel { 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| { From c1307cead48ba96c663d9d074ebeb21a1c90d96d Mon Sep 17 00:00:00 2001 From: Daniel Sauble <djsauble@gmail.com> Date: Thu, 17 Jul 2025 21:30:01 -0700 Subject: [PATCH 176/658] Add `;` key binding for Helix mode (#34315) Closes #34111 In Helix mode, the `;` key should collapse the current selection without moving the cursor. I've added a new action `vim::HelixCollapseSelection` to support this behavior. https://github.com/user-attachments/assets/1a40821a-f56f-456e-9d37-532500bef17b Release Notes: - Added `;` key binding to collapse the current text selection in Helix mode --- assets/keymaps/vim.json | 1 + crates/vim/src/normal.rs | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index dcb52e5250335531ebfa6e4146614ee8b9adf73a..89a71e59e6af2ffb51c88a17fc6bdda491760111 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -377,6 +377,7 @@ "context": "vim_mode == helix_normal && !menu", "bindings": { "ctrl-[": "editor::Cancel", + ";": "vim::HelixCollapseSelection", ":": "command_palette::Toggle", "left": "vim::WrappingLeft", "right": "vim::WrappingRight", diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 6131032f4fab7b6ac7f3d2965413464317e55490..13128e7b403ab0e921e7f323d03d83e187d1556d 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -64,6 +64,8 @@ actions!( DeleteRight, /// Deletes using Helix-style behavior. HelixDelete, + /// Collapse the current selection + HelixCollapseSelection, /// Changes from cursor to end of line. ChangeToEndOfLine, /// Deletes from cursor to end of line. @@ -143,6 +145,20 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) { vim.switch_mode(Mode::HelixNormal, true, window, cx); }); + Vim::action(editor, cx, |vim, _: &HelixCollapseSelection, window, cx| { + vim.update_editor(window, cx, |_, editor, window, cx| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.move_with(|map, selection| { + let mut point = selection.head(); + if !selection.reversed && !selection.is_empty() { + point = movement::left(map, selection.head()); + } + selection.collapse_to(point, selection.goal) + }); + }); + }); + }); + Vim::action(editor, cx, |vim, _: &ChangeToEndOfLine, window, cx| { vim.start_recording(cx); let times = Vim::take_count(cx); From c13322397eaf81735a97d93ba236702c218dc27b Mon Sep 17 00:00:00 2001 From: Andy Waite <github.aw@andywaite.com> Date: Fri, 18 Jul 2025 05:46:36 -0400 Subject: [PATCH 177/658] docs: Document pull diagnostics support for Ruby (#34028) This is now supported. Release Notes: - N/A --- docs/src/languages/ruby.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/src/languages/ruby.md b/docs/src/languages/ruby.md index b7856b2cd07ab15bb1b5fd402908c162a543f86d..6f530433bd0e15d2ed659dc2e1f0055ad5711cb5 100644 --- a/docs/src/languages/ruby.md +++ b/docs/src/languages/ruby.md @@ -127,7 +127,7 @@ Solargraph reads its configuration from a file called `.solargraph.yml` in the r ## Setting up `ruby-lsp` -Ruby LSP uses pull-based diagnostics which Zed doesn't support yet. We can tell Zed to disable it by adding the following to your `settings.json`: +You can pass Ruby LSP configuration to `initialization_options`, e.g. ```json { @@ -140,8 +140,7 @@ Ruby LSP uses pull-based diagnostics which Zed doesn't support yet. We can tell "ruby-lsp": { "initialization_options": { "enabledFeatures": { - // This disables diagnostics - "diagnostics": false + // "someFeature": false } } } From 00097df0d585c65103e1c1fd170cce3a28c631aa Mon Sep 17 00:00:00 2001 From: Arseny Kapoulkine <arseny.kapoulkine@gmail.com> Date: Fri, 18 Jul 2025 03:24:20 -0700 Subject: [PATCH 178/658] Improve C/C++ indentation flow for single statement blocks (#34549) Before this, indentation did not automatically increase after if/for/while/do/else statements in C++, and only increased after if/for in C. This led to Zed using last line logic when inserting lines *after* the indented statement, as well as not indenting the statement itself, resulting in irregular indentation during typing. Just adding indentation (similar to C) creates a new problem: now if a scope is started with a brace on a new line, that brace is indented. Thus we need to deindent it. Using else_clause in the indent guide results in the else statement being indented forward as well, so we need to deindent that too. Note: the most significant issue for me is the one where indentation jumps forward when inserting lines after indented lines. Unfortunately, it appears that fixing that issue requires all of these other changes. I would have preferred a simpler fix, but I'm not sure if disabling last line behavior for C/C++ is appropriate as it probably breaks something else, like cases where the file is incomplete and the statements can't be parsed properly. Editing flow before this change: [Screencast From 2025-07-16 08-31-36.webm](https://github.com/user-attachments/assets/3dea86c5-47bd-47c2-aee8-b0aa613948e6) Editing flow after this change: [Screencast From 2025-07-16 08-35-36.webm](https://github.com/user-attachments/assets/7ef23e60-1ee3-49fd-90f9-d53f909ca674) (note: the "else" snippet is completely breaking the flow here, but I think that comes from clangd by default? Unfortunately I haven't found a way to disable it cleanly but that is a separate problem that happens right now too.) Release Notes: - Improve indentation during typing for C/C++ around if/for/while/do blocks --- crates/languages/src/c/config.toml | 4 ++++ crates/languages/src/c/indents.scm | 10 ++++++++++ crates/languages/src/cpp/config.toml | 4 ++++ crates/languages/src/cpp/indents.scm | 12 ++++++++++++ 4 files changed, 30 insertions(+) diff --git a/crates/languages/src/c/config.toml b/crates/languages/src/c/config.toml index 08cd100f4d4dcb7c00eee33a2491864283986a82..78213da5be43da1ba13e1566a72f552f7db3986c 100644 --- a/crates/languages/src/c/config.toml +++ b/crates/languages/src/c/config.toml @@ -2,6 +2,10 @@ name = "C" grammar = "c" path_suffixes = ["c"] line_comments = ["// "] +decrease_indent_patterns = [ + { pattern = "^\\s*\\{.*\\}?\\s*$", valid_after = ["if", "for", "while", "do", "switch", "else"] }, + { pattern = "^\\s*else\\s*$", valid_after = ["if"] } +] autoclose_before = ";:.,=}])>" brackets = [ { start = "{", end = "}", close = true, newline = true }, diff --git a/crates/languages/src/c/indents.scm b/crates/languages/src/c/indents.scm index fa40ce215e358a067f48997ab4d870174f1ea479..3b6d5135abe593656d4134b309bf5d43f54a8f59 100644 --- a/crates/languages/src/c/indents.scm +++ b/crates/languages/src/c/indents.scm @@ -3,7 +3,17 @@ (assignment_expression) (if_statement) (for_statement) + (while_statement) + (do_statement) + (else_clause) ] @indent (_ "{" "}" @end) @indent (_ "(" ")" @end) @indent + +(if_statement) @start.if +(for_statement) @start.for +(while_statement) @start.while +(do_statement) @start.do +(switch_statement) @start.switch +(else_clause) @start.else diff --git a/crates/languages/src/cpp/config.toml b/crates/languages/src/cpp/config.toml index a81cbe09cde970398719eef8af75864635b3e43b..1e283816053f27fa39d985e79bc4e7d89db4477a 100644 --- a/crates/languages/src/cpp/config.toml +++ b/crates/languages/src/cpp/config.toml @@ -2,6 +2,10 @@ name = "C++" grammar = "cpp" path_suffixes = ["cc", "hh", "cpp", "h", "hpp", "cxx", "hxx", "c++", "ipp", "inl", "ixx", "cu", "cuh", "C", "H"] line_comments = ["// ", "/// ", "//! "] +decrease_indent_patterns = [ + { pattern = "^\\s*\\{.*\\}?\\s*$", valid_after = ["if", "for", "while", "do", "switch", "else"] }, + { pattern = "^\\s*else\\s*$", valid_after = ["if"] } +] autoclose_before = ";:.,=}])>" brackets = [ { start = "{", end = "}", close = true, newline = true }, diff --git a/crates/languages/src/cpp/indents.scm b/crates/languages/src/cpp/indents.scm index a17f4c4821e1ff096e9977c28801eeba581d8b17..d95dfe178cbada6836cb14bca997619fe2a319b3 100644 --- a/crates/languages/src/cpp/indents.scm +++ b/crates/languages/src/cpp/indents.scm @@ -1,7 +1,19 @@ [ (field_expression) (assignment_expression) + (if_statement) + (for_statement) + (while_statement) + (do_statement) + (else_clause) ] @indent (_ "{" "}" @end) @indent (_ "(" ")" @end) @indent + +(if_statement) @start.if +(for_statement) @start.for +(while_statement) @start.while +(do_statement) @start.do +(switch_statement) @start.switch +(else_clause) @start.else From 7e3fd7bb020077383947950f4e899c258d48c2aa Mon Sep 17 00:00:00 2001 From: tidely <43219534+tidely@users.noreply.github.com> Date: Fri, 18 Jul 2025 13:35:38 +0300 Subject: [PATCH 179/658] gpui: Use static keyword with `LazyLock` when loading system fonts (#34555) Use the `static` keyword to actually make the `LazyLock` static, which previously would reinitialize on every call to `SvgRenderer::new`. Related: #26335 Release Notes: - N/A --- crates/gpui/src/svg_renderer.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/gpui/src/svg_renderer.rs b/crates/gpui/src/svg_renderer.rs index 08d281b850ca80a370130e9f364d6ecb5334a1ce..0107624bc8d0e6a26c6acc4a085cbddc7e14c4c5 100644 --- a/crates/gpui/src/svg_renderer.rs +++ b/crates/gpui/src/svg_renderer.rs @@ -27,7 +27,7 @@ pub enum SvgSize { impl SvgRenderer { pub fn new(asset_source: Arc<dyn AssetSource>) -> Self { - let font_db = LazyLock::new(|| { + static FONT_DB: LazyLock<Arc<usvg::fontdb::Database>> = LazyLock::new(|| { let mut db = usvg::fontdb::Database::new(); db.load_system_fonts(); Arc::new(db) @@ -36,7 +36,7 @@ impl SvgRenderer { let font_resolver = Box::new( move |font: &usvg::Font, db: &mut Arc<usvg::fontdb::Database>| { if db.is_empty() { - *db = font_db.clone(); + *db = FONT_DB.clone(); } default_font_resolver(font, db) }, From fd05f17fa7e0253bc98698e84a1f8939bcb6616f Mon Sep 17 00:00:00 2001 From: Lukas Spiss <35728419+Spissable@users.noreply.github.com> Date: Fri, 18 Jul 2025 12:38:18 +0100 Subject: [PATCH 180/658] go: Support raw string subtest names (#34636) Currently, we're not able to run Go sub-tests that have a raw string (e.g. we're using multi-line names a lot) via the UI. I added the changes that are needed, plus a handful of tests to cover the basics. Quick comparison: Before: <img width="901" height="370" alt="before" src="https://github.com/user-attachments/assets/4e5cadeb-9a0c-49e2-b976-2223e1010f85" /> After: <img width="901" height="505" alt="after" src="https://github.com/user-attachments/assets/994fc69b-f720-488c-a14b-853a3ca2f53c" /> Release Notes: - Added support for Go subtest runner with raw string names --- crates/languages/src/go.rs | 121 +++++++++++++++++++++++++- crates/languages/src/go/runnables.scm | 5 +- 2 files changed, 121 insertions(+), 5 deletions(-) diff --git a/crates/languages/src/go.rs b/crates/languages/src/go.rs index 25aa5a67b909ab9898cc449f6132a0b2f077d707..16c1b67203e673ddb8c20c110d46ea7bf062ea43 100644 --- a/crates/languages/src/go.rs +++ b/crates/languages/src/go.rs @@ -41,7 +41,7 @@ static VERSION_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\d+\.\d+\.\d+").expect("Failed to create VERSION_REGEX")); static GO_ESCAPE_SUBTEST_NAME_REGEX: LazyLock<Regex> = LazyLock::new(|| { - Regex::new(r#"[.*+?^${}()|\[\]\\]"#).expect("Failed to create GO_ESCAPE_SUBTEST_NAME_REGEX") + Regex::new(r#"[.*+?^${}()|\[\]\\"']"#).expect("Failed to create GO_ESCAPE_SUBTEST_NAME_REGEX") }); const BINARY: &str = if cfg!(target_os = "windows") { @@ -685,11 +685,20 @@ impl ContextProvider for GoContextProvider { } fn extract_subtest_name(input: &str) -> Option<String> { - let replaced_spaces = input.trim_matches('"').replace(' ', "_"); + let content = if input.starts_with('`') && input.ends_with('`') { + input.trim_matches('`') + } else { + input.trim_matches('"') + }; + + let processed = content + .chars() + .map(|c| if c.is_whitespace() { '_' } else { c }) + .collect::<String>(); Some( GO_ESCAPE_SUBTEST_NAME_REGEX - .replace_all(&replaced_spaces, |caps: ®ex::Captures| { + .replace_all(&processed, |caps: ®ex::Captures| { format!("\\{}", &caps[0]) }) .to_string(), @@ -700,7 +709,7 @@ fn extract_subtest_name(input: &str) -> Option<String> { mod tests { use super::*; use crate::language; - use gpui::Hsla; + use gpui::{AppContext, Hsla, TestAppContext}; use theme::SyntaxTheme; #[gpui::test] @@ -790,4 +799,108 @@ mod tests { }) ); } + + #[gpui::test] + fn test_go_runnable_detection(cx: &mut TestAppContext) { + let language = language("go", tree_sitter_go::LANGUAGE.into()); + + let interpreted_string_subtest = r#" + package main + + import "testing" + + func TestExample(t *testing.T) { + t.Run("subtest with double quotes", func(t *testing.T) { + // test code + }) + } + "#; + + let raw_string_subtest = r#" + package main + + import "testing" + + func TestExample(t *testing.T) { + t.Run(`subtest with + multiline + backticks`, func(t *testing.T) { + // test code + }) + } + "#; + + let buffer = cx.new(|cx| { + crate::Buffer::local(interpreted_string_subtest, 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..interpreted_string_subtest.len()) + .collect() + }); + + assert!( + runnables.len() == 2, + "Should find test function and subtest with double quotes, found: {}", + runnables.len() + ); + + let buffer = cx.new(|cx| { + crate::Buffer::local(raw_string_subtest, 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..raw_string_subtest.len()) + .collect() + }); + + assert!( + runnables.len() == 2, + "Should find test function and subtest with backticks, found: {}", + runnables.len() + ); + } + + #[test] + fn test_extract_subtest_name() { + // Interpreted string literal + let input_double_quoted = r#""subtest with double quotes""#; + let result = extract_subtest_name(input_double_quoted); + assert_eq!(result, Some(r#"subtest_with_double_quotes"#.to_string())); + + let input_double_quoted_with_backticks = r#""test with `backticks` inside""#; + let result = extract_subtest_name(input_double_quoted_with_backticks); + assert_eq!(result, Some(r#"test_with_`backticks`_inside"#.to_string())); + + // Raw string literal + let input_with_backticks = r#"`subtest with backticks`"#; + let result = extract_subtest_name(input_with_backticks); + assert_eq!(result, Some(r#"subtest_with_backticks"#.to_string())); + + let input_raw_with_quotes = r#"`test with "quotes" and other chars`"#; + let result = extract_subtest_name(input_raw_with_quotes); + assert_eq!( + result, + Some(r#"test_with_\"quotes\"_and_other_chars"#.to_string()) + ); + + let input_multiline = r#"`subtest with + multiline + backticks`"#; + let result = extract_subtest_name(input_multiline); + assert_eq!( + result, + Some(r#"subtest_with_________multiline_________backticks"#.to_string()) + ); + + let input_with_double_quotes = r#"`test with "double quotes"`"#; + let result = extract_subtest_name(input_with_double_quotes); + assert_eq!(result, Some(r#"test_with_\"double_quotes\""#.to_string())); + } } diff --git a/crates/languages/src/go/runnables.scm b/crates/languages/src/go/runnables.scm index bdeb77b46c83fce079ea43d413c3a9ef231a5a8c..49e112b860d331f4399d53cff8bd849bdc559f42 100644 --- a/crates/languages/src/go/runnables.scm +++ b/crates/languages/src/go/runnables.scm @@ -38,7 +38,10 @@ arguments: ( argument_list . - (interpreted_string_literal) @_subtest_name + [ + (interpreted_string_literal) + (raw_string_literal) + ] @_subtest_name . (func_literal parameters: ( From cfe1adc7922e2f4b337a5613a3bbe33e742a9904 Mon Sep 17 00:00:00 2001 From: Ben Brandt <benjamin.j.brandt@gmail.com> Date: Fri, 18 Jul 2025 15:17:41 +0200 Subject: [PATCH 181/658] E2E Claude tests (#34702) - **Fix cancellation of tool calls** - **Make tool_call test more resilient** - **Fix tool call confirmation test** Release Notes: - N/A --- Cargo.lock | 1 + crates/acp_thread/src/acp_thread.rs | 13 +++- crates/agent_servers/Cargo.toml | 1 + crates/agent_servers/src/claude.rs | 14 ++-- crates/agent_servers/src/claude/tools.rs | 99 +++++++++++++++++++++++- crates/agent_servers/src/e2e_tests.rs | 98 ++++++++++++++++------- 6 files changed, 188 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a8a8d12e37a08a2db3ce53525041366834772e5c..4d84249b0064386aa20072d97ccb61660d3f56f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -158,6 +158,7 @@ dependencies = [ "serde_json", "settings", "smol", + "strum 0.27.1", "tempfile", "ui", "util", diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 1e3947351ab1700d94d353d1e848db403015eec0..ae22725d5eac36a326718a66d5944c9fcfb44a4b 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -664,7 +664,7 @@ impl AcpThread { cx: &mut Context<Self>, ) -> Result<ToolCallRequest> { let project = self.project.read(cx).languages().clone(); - let Some((_, call)) = self.tool_call_mut(tool_call_id) else { + let Some((idx, call)) = self.tool_call_mut(tool_call_id) else { anyhow::bail!("Tool call not found"); }; @@ -675,6 +675,8 @@ impl AcpThread { respond_tx: tx, }; + cx.emit(AcpThreadEvent::EntryUpdated(idx)); + Ok(ToolCallRequest { id: tool_call_id, outcome: rx, @@ -768,8 +770,13 @@ impl AcpThread { let language_registry = self.project.read(cx).languages().clone(); let (ix, call) = self.tool_call_mut(id).context("Entry not found")?; - call.content = new_content - .map(|new_content| ToolCallContent::from_acp(new_content, language_registry, cx)); + if let Some(new_content) = new_content { + call.content = Some(ToolCallContent::from_acp( + new_content, + language_registry, + cx, + )); + } match &mut call.status { ToolCallStatus::Allowed { status } => { diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml index 2d68148264c687444cb67129d502bda3c025f0fd..f3df25f70914e95c63265e4c711ca76fd66050b1 100644 --- a/crates/agent_servers/Cargo.toml +++ b/crates/agent_servers/Cargo.toml @@ -33,6 +33,7 @@ serde.workspace = true serde_json.workspace = true settings.workspace = true smol.workspace = true +strum.workspace = true tempfile.workspace = true ui.workspace = true util.workspace = true diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 5760a96d8cf900d37aa249ffa05006df19690a34..52c601226705e8253147feb4c45c62f86265058a 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -281,14 +281,18 @@ impl ClaudeAgentConnection { } => { let id = tool_id_map.borrow_mut().remove(&tool_use_id); if let Some(id) = id { + let content = content.to_string(); delegate .update_tool_call(UpdateToolCallParams { tool_call_id: id, status: acp::ToolCallStatus::Finished, - content: Some(ToolCallContent::Markdown { - // For now we only include text content - markdown: content.to_string(), - }), + // Don't unset existing content + content: (!content.is_empty()).then_some( + ToolCallContent::Markdown { + // For now we only include text content + markdown: content, + }, + ), }) .await .log_err(); @@ -577,7 +581,7 @@ pub(crate) mod tests { use super::*; use serde_json::json; - // crate::common_e2e_tests!(ClaudeCode); + crate::common_e2e_tests!(ClaudeCode); pub fn local_command() -> AgentServerCommand { AgentServerCommand { diff --git a/crates/agent_servers/src/claude/tools.rs b/crates/agent_servers/src/claude/tools.rs index e3ac6c14e229d81d3d26bae3fc5e2d1bb8f6a189..a2d6b487b2fb7aa4baceadf02f1b8f81b0e7b29f 100644 --- a/crates/agent_servers/src/claude/tools.rs +++ b/crates/agent_servers/src/claude/tools.rs @@ -118,13 +118,106 @@ impl ClaudeTool { pub fn content(&self) -> Option<acp::ToolCallContent> { match &self { - ClaudeTool::Other { input, .. } => Some(acp::ToolCallContent::Markdown { + Self::Other { input, .. } => Some(acp::ToolCallContent::Markdown { markdown: format!( "```json\n{}```", serde_json::to_string_pretty(&input).unwrap_or("{}".to_string()) ), }), - _ => None, + Self::Task(Some(params)) => Some(acp::ToolCallContent::Markdown { + markdown: params.prompt.clone(), + }), + Self::NotebookRead(Some(params)) => Some(acp::ToolCallContent::Markdown { + markdown: params.notebook_path.display().to_string(), + }), + Self::NotebookEdit(Some(params)) => Some(acp::ToolCallContent::Markdown { + markdown: params.new_source.clone(), + }), + Self::Terminal(Some(params)) => Some(acp::ToolCallContent::Markdown { + markdown: format!( + "`{}`\n\n{}", + params.command, + params.description.as_deref().unwrap_or_default() + ), + }), + Self::ReadFile(Some(params)) => Some(acp::ToolCallContent::Markdown { + markdown: params.abs_path.display().to_string(), + }), + Self::Ls(Some(params)) => Some(acp::ToolCallContent::Markdown { + markdown: params.path.display().to_string(), + }), + Self::Glob(Some(params)) => Some(acp::ToolCallContent::Markdown { + markdown: params.to_string(), + }), + Self::Grep(Some(params)) => Some(acp::ToolCallContent::Markdown { + markdown: format!("`{params}`"), + }), + Self::WebFetch(Some(params)) => Some(acp::ToolCallContent::Markdown { + markdown: params.prompt.clone(), + }), + Self::WebSearch(Some(params)) => Some(acp::ToolCallContent::Markdown { + markdown: params.to_string(), + }), + Self::TodoWrite(Some(params)) => Some(acp::ToolCallContent::Markdown { + markdown: params + .todos + .iter() + .map(|todo| { + format!( + "- {} {}: {}", + match todo.status { + TodoStatus::Completed => "✅", + TodoStatus::InProgress => "🚧", + TodoStatus::Pending => "⬜", + }, + todo.priority, + todo.content + ) + }) + .join("\n"), + }), + Self::ExitPlanMode(Some(params)) => Some(acp::ToolCallContent::Markdown { + markdown: params.plan.clone(), + }), + Self::Edit(Some(params)) => Some(acp::ToolCallContent::Diff { + diff: acp::Diff { + path: params.abs_path.clone(), + old_text: Some(params.old_text.clone()), + new_text: params.new_text.clone(), + }, + }), + Self::Write(Some(params)) => Some(acp::ToolCallContent::Diff { + diff: acp::Diff { + path: params.file_path.clone(), + old_text: None, + new_text: params.content.clone(), + }, + }), + Self::MultiEdit(Some(params)) => { + // todo: show multiple edits in a multibuffer? + params.edits.first().map(|edit| acp::ToolCallContent::Diff { + diff: acp::Diff { + path: params.file_path.clone(), + old_text: Some(edit.old_string.clone()), + new_text: edit.new_string.clone(), + }, + }) + } + Self::Task(None) + | Self::NotebookRead(None) + | Self::NotebookEdit(None) + | Self::Terminal(None) + | Self::ReadFile(None) + | Self::Ls(None) + | Self::Glob(None) + | Self::Grep(None) + | Self::WebFetch(None) + | Self::WebSearch(None) + | Self::TodoWrite(None) + | Self::ExitPlanMode(None) + | Self::Edit(None) + | Self::Write(None) + | Self::MultiEdit(None) => None, } } @@ -513,7 +606,7 @@ impl std::fmt::Display for GrepToolParams { } } -#[derive(Deserialize, Serialize, JsonSchema, Debug)] +#[derive(Deserialize, Serialize, JsonSchema, strum::Display, Debug)] #[serde(rename_all = "snake_case")] pub enum TodoPriority { High, diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index 923c6cdd6f1112233b45386c8a6a4db0ab380d65..12f74cb13e41fea5c313729edf857173af94f74e 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -111,18 +111,21 @@ pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestApp .await .unwrap(); thread.read_with(cx, |thread, _cx| { - assert!(matches!( - &thread.entries()[2], - AgentThreadEntry::ToolCall(ToolCall { - status: ToolCallStatus::Allowed { .. }, - .. - }) - )); - - assert!(matches!( - thread.entries()[3], - AgentThreadEntry::AssistantMessage(_) - )); + assert!(thread.entries().iter().any(|entry| { + matches!( + entry, + AgentThreadEntry::ToolCall(ToolCall { + status: ToolCallStatus::Allowed { .. }, + .. + }) + ) + })); + assert!( + thread + .entries() + .iter() + .any(|entry| { matches!(entry, AgentThreadEntry::AssistantMessage(_)) }) + ); }); } @@ -134,10 +137,26 @@ pub async fn test_tool_call_with_confirmation( 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| { - thread.send_raw(r#"Run `echo "Hello, world!"`"#, cx) + thread.send_raw( + r#"Run `touch hello.txt && echo "Hello, world!" | tee hello.txt`"#, + cx, + ) }); - run_until_first_tool_call(&thread, cx).await; + run_until_first_tool_call( + &thread, + |entry| { + matches!( + entry, + AgentThreadEntry::ToolCall(ToolCall { + status: ToolCallStatus::WaitingForConfirmation { .. }, + .. + }) + ) + }, + cx, + ) + .await; let tool_call_id = thread.read_with(cx, |thread, _cx| { let AgentThreadEntry::ToolCall(ToolCall { @@ -148,12 +167,16 @@ pub async fn test_tool_call_with_confirmation( .. }, .. - }) = &thread.entries()[2] + }) = &thread + .entries() + .iter() + .find(|entry| matches!(entry, AgentThreadEntry::ToolCall(_))) + .unwrap() else { panic!(); }; - assert_eq!(root_command, "echo"); + assert!(root_command.contains("touch")); *id }); @@ -161,13 +184,13 @@ pub async fn test_tool_call_with_confirmation( thread.update(cx, |thread, cx| { thread.authorize_tool_call(tool_call_id, acp::ToolCallConfirmationOutcome::Allow, cx); - assert!(matches!( - &thread.entries()[2], + assert!(thread.entries().iter().any(|entry| matches!( + entry, AgentThreadEntry::ToolCall(ToolCall { status: ToolCallStatus::Allowed { .. }, .. }) - )); + ))); }); full_turn.await.unwrap(); @@ -177,15 +200,19 @@ pub async fn test_tool_call_with_confirmation( content: Some(ToolCallContent::Markdown { markdown }), status: ToolCallStatus::Allowed { .. }, .. - }) = &thread.entries()[2] + }) = thread + .entries() + .iter() + .find(|entry| matches!(entry, AgentThreadEntry::ToolCall(_))) + .unwrap() else { panic!(); }; markdown.read_with(cx, |md, _cx| { assert!( - md.source().contains("Hello, world!"), - r#"Expected '{}' to contain "Hello, world!""#, + md.source().contains("Hello"), + r#"Expected '{}' to contain "Hello""#, md.source() ); }); @@ -198,10 +225,26 @@ 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| { - thread.send_raw(r#"Run `echo "Hello, world!"`"#, cx) + thread.send_raw( + r#"Run `touch hello.txt && echo "Hello, world!" >> hello.txt`"#, + cx, + ) }); - let first_tool_call_ix = run_until_first_tool_call(&thread, cx).await; + let first_tool_call_ix = run_until_first_tool_call( + &thread, + |entry| { + matches!( + entry, + AgentThreadEntry::ToolCall(ToolCall { + status: ToolCallStatus::WaitingForConfirmation { .. }, + .. + }) + ) + }, + cx, + ) + .await; thread.read_with(cx, |thread, _cx| { let AgentThreadEntry::ToolCall(ToolCall { @@ -217,7 +260,7 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon panic!("{:?}", thread.entries()[1]); }; - assert_eq!(root_command, "echo"); + assert!(root_command.contains("touch")); *id }); @@ -340,6 +383,7 @@ pub async fn new_test_thread( pub async fn run_until_first_tool_call( thread: &Entity<AcpThread>, + wait_until: impl Fn(&AgentThreadEntry) -> bool + 'static, cx: &mut TestAppContext, ) -> usize { let (mut tx, mut rx) = mpsc::channel::<usize>(1); @@ -347,7 +391,7 @@ pub async fn run_until_first_tool_call( let subscription = cx.update(|cx| { cx.subscribe(thread, move |thread, _, cx| { for (ix, entry) in thread.read(cx).entries().iter().enumerate() { - if matches!(entry, AgentThreadEntry::ToolCall(_)) { + if wait_until(entry) { return tx.try_send(ix).unwrap(); } } @@ -357,7 +401,7 @@ pub async fn run_until_first_tool_call( select! { // We have to use a smol timer here because // cx.background_executor().timer isn't real in the test context - _ = futures::FutureExt::fuse(smol::Timer::after(Duration::from_secs(10))) => { + _ = futures::FutureExt::fuse(smol::Timer::after(Duration::from_secs(20))) => { panic!("Timeout waiting for tool call") } ix = rx.next().fuse() => { From 1070de47ec2ac32e17ca756492cc11620cda8320 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Fri, 18 Jul 2025 10:24:57 -0300 Subject: [PATCH 182/658] component preview: Add separators between sections in sidebar (#34701) Small improvement for navigating inside the component preview. Release Notes: - N/A --- crates/component/src/component_layout.rs | 4 ++-- crates/zed/src/zed/component_preview.rs | 26 ++++++++---------------- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/crates/component/src/component_layout.rs b/crates/component/src/component_layout.rs index 9fe52507d8a27abd3ba739f89a375e455a80f520..58bf1d8f0c85533a4a06bd38c07f840c08cc6de3 100644 --- a/crates/component/src/component_layout.rs +++ b/crates/component/src/component_layout.rs @@ -118,8 +118,8 @@ impl RenderOnce for ComponentExampleGroup { .flex() .items_center() .gap_3() - .pb_1() - .child(div().h_px().w_4().bg(cx.theme().colors().border)) + .mt_4() + .mb_1() .child( div() .flex_none() diff --git a/crates/zed/src/zed/component_preview.rs b/crates/zed/src/zed/component_preview.rs index c32248cbe00f08d5982dbf394b806f3226c814ae..670793cff3816231d8f7a2e5c946643dbeaa3453 100644 --- a/crates/zed/src/zed/component_preview.rs +++ b/crates/zed/src/zed/component_preview.rs @@ -369,7 +369,6 @@ impl ComponentPreview { // Always show all components first entries.push(PreviewEntry::AllComponents); entries.push(PreviewEntry::ActiveThread); - entries.push(PreviewEntry::Separator); let mut scopes: Vec<_> = scope_groups .keys() @@ -382,7 +381,9 @@ impl ComponentPreview { 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())); + let mut sorted_components = components; sorted_components.sort_by_key(|(component, _)| component.sort_name()); @@ -515,16 +516,12 @@ impl ComponentPreview { Vec::new() }; if valid_positions.is_empty() { - Label::new(name.clone()) - .color(Color::Default) - .into_any_element() + Label::new(name.clone()).into_any_element() } else { HighlightedLabel::new(name.clone(), valid_positions).into_any_element() } } else { - Label::new(name.clone()) - .color(Color::Default) - .into_any_element() + Label::new(name.clone()).into_any_element() }) .selectable(true) .toggle_state(selected) @@ -542,7 +539,7 @@ impl ComponentPreview { let selected = self.active_page == PreviewPage::AllComponents; ListItem::new(ix) - .child(Label::new("All Components").color(Color::Default)) + .child(Label::new("All Components")) .selectable(true) .toggle_state(selected) .inset(true) @@ -555,7 +552,7 @@ impl ComponentPreview { let selected = self.active_page == PreviewPage::ActiveThread; ListItem::new(ix) - .child(Label::new("Active Thread").color(Color::Default)) + .child(Label::new("Active Thread")) .selectable(true) .toggle_state(selected) .inset(true) @@ -565,12 +562,8 @@ impl ComponentPreview { .into_any_element() } PreviewEntry::Separator => ListItem::new(ix) - .child( - h_flex() - .occlude() - .pt_3() - .child(Divider::horizontal_dashed()), - ) + .disabled(true) + .child(div().w_full().py_2().child(Divider::horizontal())) .into_any_element(), } } @@ -585,7 +578,6 @@ impl ComponentPreview { h_flex() .w_full() .h_10() - .items_center() .child(Headline::new(title).size(HeadlineSize::XSmall)) .child(Divider::horizontal()) } @@ -798,7 +790,7 @@ impl Render for ComponentPreview { ) .track_scroll(self.nav_scroll_handle.clone()) .p_2p5() - .w(px(240.)) + .w(px(229.)) .h_full() .flex_1(), ) From fd8480a9dc6cf07668b1e70f6b24b31268a88e15 Mon Sep 17 00:00:00 2001 From: Peter Tripp <peter@zed.dev> Date: Fri, 18 Jul 2025 09:35:36 -0400 Subject: [PATCH 183/658] Document terminal.cursor_shape (#34707) Release Notes: - N/A --- docs/src/configuring-zed.md | 48 ++++++++++++++++++++++++++++++++ docs/src/visual-customization.md | 2 +- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/docs/src/configuring-zed.md b/docs/src/configuring-zed.md index 91a39d1ccd88f3decea5241008855368037b2cb8..cc4800fd6d08832709a2562f6a3b3d9b5e2f322d 100644 --- a/docs/src/configuring-zed.md +++ b/docs/src/configuring-zed.md @@ -2698,6 +2698,54 @@ List of `integer` column numbers } ``` +### Terminal: Cursor Shape + +- Description: Whether or not selecting text in the terminal will automatically copy to the system clipboard. +- Setting: `cursor_shape` +- Default: `null` (defaults to block) + +**Options** + +1. A block that surrounds the following character + +```json +{ + "terminal": { + "cursor_shape": "block" + } +} +``` + +2. A vertical bar + +```json +{ + "terminal": { + "cursor_shape": "bar" + } +} +``` + +3. An underline / underscore that runs along the following character + +```json +{ + "terminal": { + "cursor_shape": "underline" + } +} +``` + +4. A box drawn around the following character + +```json +{ + "terminal": { + "cursor_shape": "hollow" + } +} +``` + ### Terminal: Keep Selection On Copy - Description: Whether or not to keep the selection in the terminal after copying text. diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md index ad38b86406bb885a66c12e91721b9ca853df81f0..197c9b80f858fc8d0dcb4adfc1aa8787754072fa 100644 --- a/docs/src/visual-customization.md +++ b/docs/src/visual-customization.md @@ -448,7 +448,7 @@ See [Zed AI Documentation](./ai/overview.md) for additional non-visual AI settin // Set the cursor blinking behavior in the terminal (on, off, terminal_controlled) "blinking": "terminal_controlled", - // Default cursor shape for the terminal (block, bar, underline, hollow) + // Default cursor shape for the terminal cursor (block, bar, underline, hollow) "cursor_shape": "block", // Environment variables to add to terminal's process environment From 40028010347f658b4576348d8ff70cf68873213b Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <bennet@zed.dev> Date: Fri, 18 Jul 2025 15:58:44 +0200 Subject: [PATCH 184/658] agent: Fix new thread model selection when starting new thread (#34708) Release Notes: - agent: Fixed an issue where clicking on "Start New Thread" in the agent configuration would not always switch to the correct provider/model --- crates/agent_ui/src/agent_panel.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 087eec5efb08de000f95be74226e120fde56654d..103e4396154e3073daed1063cb0239ba18ddb4cc 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -53,7 +53,8 @@ use gpui::{ }; use language::LanguageRegistry; use language_model::{ - ConfigurationError, LanguageModelProviderTosView, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID, + ConfigurationError, ConfiguredModel, LanguageModelProviderTosView, LanguageModelRegistry, + ZED_CLOUD_PROVIDER_ID, }; use project::{Project, ProjectPath, Worktree}; use prompt_store::{PromptBuilder, PromptStore, UserPromptId}; @@ -1347,6 +1348,19 @@ impl AgentPanel { } self.new_thread(&NewThread::default(), window, cx); + if let Some((thread, model)) = + self.active_thread(cx).zip(provider.default_model(cx)) + { + thread.update(cx, |thread, cx| { + thread.set_configured_model( + Some(ConfiguredModel { + provider: provider.clone(), + model, + }), + cx, + ); + }); + } } } } From d604b3b29183a09579f408059e7ca0f6bf6fbc62 Mon Sep 17 00:00:00 2001 From: Peter Tripp <peter@zed.dev> Date: Fri, 18 Jul 2025 10:01:09 -0400 Subject: [PATCH 185/658] Fix incorrect minimum_contrast comment (#34710) Actual default is 45 https://github.com/zed-industries/zed/blob/fd05f17fa7e0253bc98698e84a1f8939bcb6616f/assets/settings/default.json#L1402 Release Notes: - N/A --- crates/terminal/src/terminal_settings.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs index a290ce9c81c18f5043fd45e9c1afdac52efa2061..3f89afffab766126d5f1ef33f7d12b109d0198ca 100644 --- a/crates/terminal/src/terminal_settings.rs +++ b/crates/terminal/src/terminal_settings.rs @@ -248,7 +248,7 @@ pub struct TerminalSettingsContent { /// - 75: Minimum for body text /// - 90: Preferred for body text /// - /// Default: 0 (no adjustment) + /// Default: 45 pub minimum_contrast: Option<f32>, } From 2ac99e7a1158e1e6d3bfd44016fa01af000d5ad7 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 18 Jul 2025 16:28:03 +0200 Subject: [PATCH 186/658] debugger: Fix attaching with DebugPy (#34706) @cole-miller found a root cause of our struggles with attach scenarios; we did not fetch .so files necessary for attaching to work, as we were downloading DebugPy source tarballs from GitHub. This PR does away with it by setting up a virtualenv instead that has debugpy installed. Closes #34660 Closes #34575 Release Notes: - debugger: Fixed attaching with DebugPy. DebugPy is now installed automatically from pip (instead of GitHub), unless it is present in active virtual environment. Additionally this should resolve any startup issues with missing .so on Linux. --- Cargo.lock | 1 + crates/dap_adapters/Cargo.toml | 1 + crates/dap_adapters/src/dap_adapters.rs | 1 - crates/dap_adapters/src/python.rs | 189 ++++++++++++------------ 4 files changed, 98 insertions(+), 94 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4d84249b0064386aa20072d97ccb61660d3f56f5..aad5349a87b31b8dc577ecbc634d8d6af32cd55b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4248,6 +4248,7 @@ dependencies = [ "serde", "serde_json", "shlex", + "smol", "task", "util", "workspace-hack", diff --git a/crates/dap_adapters/Cargo.toml b/crates/dap_adapters/Cargo.toml index 65544fbb6a1b7565c4fe641058e4e6c725b21016..e7366785c810077ef2bdc3669dd5b340859c97a6 100644 --- a/crates/dap_adapters/Cargo.toml +++ b/crates/dap_adapters/Cargo.toml @@ -36,6 +36,7 @@ paths.workspace = true serde.workspace = true serde_json.workspace = true shlex.workspace = true +smol.workspace = true task.workspace = true util.workspace = true workspace-hack.workspace = true diff --git a/crates/dap_adapters/src/dap_adapters.rs b/crates/dap_adapters/src/dap_adapters.rs index a147861f8dc965c7924a70d884004d594d59a949..a4e6beb2495ebe1eec9f08ddb8394b498c0ae410 100644 --- a/crates/dap_adapters/src/dap_adapters.rs +++ b/crates/dap_adapters/src/dap_adapters.rs @@ -13,7 +13,6 @@ use dap::{ DapRegistry, adapters::{ self, AdapterVersion, DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, - GithubRepo, }, configure_tcp_connection, }; diff --git a/crates/dap_adapters/src/python.rs b/crates/dap_adapters/src/python.rs index eb541bde8e7df233c549656f7640ee45bb0f6c06..aa64fea6eda15e3033cdfbd11d8187188385f195 100644 --- a/crates/dap_adapters/src/python.rs +++ b/crates/dap_adapters/src/python.rs @@ -1,31 +1,39 @@ use crate::*; use anyhow::Context as _; -use dap::adapters::latest_github_release; use dap::{DebugRequest, StartDebuggingRequestArguments, adapters::DebugTaskDefinition}; -use gpui::{AppContext, AsyncApp, SharedString}; +use gpui::{AsyncApp, SharedString}; use json_dotpath::DotPaths; -use language::{LanguageName, Toolchain}; +use language::LanguageName; +use paths::debug_adapters_dir; use serde_json::Value; +use smol::lock::OnceCell; use std::net::Ipv4Addr; use std::{ collections::HashMap, ffi::OsStr, path::{Path, PathBuf}, - sync::OnceLock, }; -use util::ResultExt; #[derive(Default)] pub(crate) struct PythonDebugAdapter { - checked: OnceLock<()>, + python_venv_base: OnceCell<Result<Arc<Path>, String>>, } impl PythonDebugAdapter { const ADAPTER_NAME: &'static str = "Debugpy"; const DEBUG_ADAPTER_NAME: DebugAdapterName = DebugAdapterName(SharedString::new_static(Self::ADAPTER_NAME)); - const ADAPTER_PACKAGE_NAME: &'static str = "debugpy"; - const ADAPTER_PATH: &'static str = "src/debugpy/adapter"; + 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"; async fn generate_debugpy_arguments( @@ -46,25 +54,12 @@ impl PythonDebugAdapter { vec!["-m".to_string(), "debugpy.adapter".to_string()] } else { let adapter_path = paths::debug_adapters_dir().join(Self::DEBUG_ADAPTER_NAME.as_ref()); - let file_name_prefix = format!("{}_", Self::ADAPTER_NAME); - - let debugpy_dir = - util::fs::find_file_name_in_dir(adapter_path.as_path(), |file_name| { - file_name.starts_with(&file_name_prefix) - }) - .await - .context("Debugpy directory not found")?; - - log::debug!( - "Using GitHub-downloaded debugpy adapter from: {}", - debugpy_dir.display() - ); - vec![ - debugpy_dir - .join(Self::ADAPTER_PATH) - .to_string_lossy() - .to_string(), - ] + let path = adapter_path + .join(Self::ADAPTER_PATH) + .to_string_lossy() + .into_owned(); + log::debug!("Using pip debugpy adapter from: {path}"); + vec![path] }; args.extend(if let Some(args) = user_args { @@ -100,44 +95,67 @@ impl PythonDebugAdapter { request, }) } - async fn fetch_latest_adapter_version( - &self, - delegate: &Arc<dyn DapDelegate>, - ) -> Result<AdapterVersion> { - let github_repo = GithubRepo { - repo_name: Self::ADAPTER_PACKAGE_NAME.into(), - repo_owner: "microsoft".into(), - }; - fetch_latest_adapter_version_from_github(github_repo, delegate.as_ref()).await - } - - async fn install_binary( - adapter_name: DebugAdapterName, - version: AdapterVersion, - delegate: Arc<dyn DapDelegate>, - ) -> Result<()> { - let version_path = adapters::download_adapter_from_github( - adapter_name, - version, - adapters::DownloadedFileType::GzipTar, - delegate.as_ref(), - ) - .await?; - // only needed when you install the latest version for the first time - if let Some(debugpy_dir) = - util::fs::find_file_name_in_dir(version_path.as_path(), |file_name| { - file_name.starts_with("microsoft-debugpy-") - }) + async fn ensure_venv(delegate: &dyn DapDelegate) -> Result<Arc<Path>> { + let python_path = Self::find_base_python(delegate) .await - { - // TODO Debugger: Rename folder instead of moving all files to another folder - // We're doing unnecessary IO work right now - util::fs::move_folder_files_to_folder(debugpy_dir.as_path(), version_path.as_path()) + .context("Could not find Python installation for DebugPy")?; + let work_dir = debug_adapters_dir().join(Self::ADAPTER_NAME); + 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(()) + 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<PathBuf> { + for path in ["python3", "python"] { + if let Some(path) = delegate.which(path.as_ref()).await { + return Some(path); + } + } + None + } + + async fn base_venv(&self, delegate: &dyn DapDelegate) -> Result<Arc<Path>, 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()); + } + + Ok(venv_base) + }) + .await + .clone() } async fn get_installed_binary( @@ -146,15 +164,15 @@ impl PythonDebugAdapter { config: &DebugTaskDefinition, user_installed_path: Option<PathBuf>, user_args: Option<Vec<String>>, - toolchain: Option<Toolchain>, + python_from_toolchain: Option<String>, installed_in_venv: bool, ) -> Result<DebugAdapterBinary> { 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) = toolchain { - Some(toolchain.path.to_string()) + let python_path = if let Some(toolchain) = python_from_toolchain { + Some(toolchain) } else { let mut name = None; @@ -635,25 +653,28 @@ impl DebugAdapter for PythonDebugAdapter { &config, None, user_args, - Some(toolchain.clone()), + Some(toolchain.path.to_string()), true, ) .await; } } } - - if self.checked.set(()).is_ok() { - delegate.output_to_console(format!("Checking latest version of {}...", self.name())); - if let Some(version) = self.fetch_latest_adapter_version(delegate).await.log_err() { - cx.background_spawn(Self::install_binary(self.name(), version, delegate.clone())) - .await - .context("Failed to install debugpy")?; - } - } - - self.get_installed_binary(delegate, &config, None, user_args, toolchain, false) + let toolchain = self + .base_venv(&**delegate) .await + .map_err(|e| anyhow::anyhow!(e))? + .join(Self::PYTHON_ADAPTER_IN_VENV); + + self.get_installed_binary( + delegate, + &config, + None, + user_args, + Some(toolchain.to_string_lossy().into_owned()), + false, + ) + .await } fn label_for_child_session(&self, args: &StartDebuggingRequestArguments) -> Option<String> { @@ -666,24 +687,6 @@ impl DebugAdapter for PythonDebugAdapter { } } -async fn fetch_latest_adapter_version_from_github( - github_repo: GithubRepo, - delegate: &dyn DapDelegate, -) -> Result<AdapterVersion> { - let release = latest_github_release( - &format!("{}/{}", github_repo.repo_owner, github_repo.repo_name), - false, - false, - delegate.http_client(), - ) - .await?; - - Ok(AdapterVersion { - tag_name: release.tag_name, - url: release.tarball_url, - }) -} - #[cfg(test)] mod tests { use super::*; From 6a24b2479cddc96133b9878fd560478004ead1ef Mon Sep 17 00:00:00 2001 From: Peter Tripp <peter@zed.dev> Date: Fri, 18 Jul 2025 10:28:20 -0400 Subject: [PATCH 187/658] Redact license keys in environment variables from log output (#34711) Release Notes: - N/A --- crates/util/src/redact.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/util/src/redact.rs b/crates/util/src/redact.rs index 0b979fb4132f3e39381b75ddcaa85505c62de530..6b297dfb58bb0b4537d4032d8f9cf4db845f9d78 100644 --- a/crates/util/src/redact.rs +++ b/crates/util/src/redact.rs @@ -1,7 +1,14 @@ /// Whether a given environment variable name should have its value redacted pub fn should_redact(env_var_name: &str) -> bool { - const REDACTED_SUFFIXES: &[&str] = - &["KEY", "TOKEN", "PASSWORD", "SECRET", "PASS", "CREDENTIALS"]; + const REDACTED_SUFFIXES: &[&str] = &[ + "KEY", + "TOKEN", + "PASSWORD", + "SECRET", + "PASS", + "CREDENTIALS", + "LICENSE", + ]; REDACTED_SUFFIXES .iter() .any(|suffix| env_var_name.ends_with(suffix)) From f461290ac352de2cf0a5a00f8a1f9264ad5b29da Mon Sep 17 00:00:00 2001 From: Marshall Bowers <git@maxdeviant.com> Date: Fri, 18 Jul 2025 10:52:42 -0400 Subject: [PATCH 188/658] collab: Add `POST /users/:id/refresh_llm_tokens` endpoint (#34714) This PR adds a new `POST /users/:id/refresh_llm_tokens` endpoint to Collab so that we can refresh LLM tokens from Cloud. Release Notes: - N/A --- crates/collab/src/api.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/collab/src/api.rs b/crates/collab/src/api.rs index 7fca27c5c2b9580b7ef6546e4188c0aac7f73e3c..8f1433a26f1a09fd820e8272684b08ff1b6d9581 100644 --- a/crates/collab/src/api.rs +++ b/crates/collab/src/api.rs @@ -100,6 +100,7 @@ pub fn routes(rpc_server: Arc<rpc::Server>) -> Router<(), Body> { .route("/user", get(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)) .route("/rpc_server_snapshot", get(get_rpc_server_snapshot)) .merge(billing::router()) .merge(contributors::router()) @@ -334,3 +335,15 @@ async fn create_access_token( encrypted_access_token, })) } + +#[derive(Serialize)] +struct RefreshLlmTokensResponse {} + +async fn refresh_llm_tokens( + Path(user_id): Path<UserId>, + Extension(rpc_server): Extension<Arc<rpc::Server>>, +) -> Result<Json<RefreshLlmTokensResponse>> { + rpc_server.refresh_llm_tokens_for_user(user_id).await; + + Ok(Json(RefreshLlmTokensResponse {})) +} From e421fc7a2dfb044fc118926fcab4aa7e13a9dab5 Mon Sep 17 00:00:00 2001 From: Conrad Irwin <conrad.irwin@gmail.com> Date: Fri, 18 Jul 2025 09:25:18 -0600 Subject: [PATCH 189/658] Update keymap context binding behavior of > and ! (#34664) Now ! means "no ancestors matches this", and > means "any descendent" not "any child". Updates #34570 Co-authored-by: Ben Kunkle <ben@zed.dev> Release Notes: - *Breaking change*. The context predicates in the keymap file now handle ! and > differently. Before this change ! meant "this node does not match", now it means "none of these nodes match". Before this change > meant "child of", now it means "descendent of". We do not expect these changes to break many keymaps, but they may cause subtle changes for complex context queries. --------- Co-authored-by: Ben Kunkle <ben@zed.dev> --- assets/keymaps/vim.json | 4 +- crates/gpui/src/keymap.rs | 140 +++++++------- crates/gpui/src/keymap/context.rs | 181 +++++++++++++++++- crates/language_tools/src/key_context_view.rs | 9 +- crates/settings_ui/src/keybindings.rs | 2 +- docs/src/key-bindings.md | 19 +- 6 files changed, 263 insertions(+), 92 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 89a71e59e6af2ffb51c88a17fc6bdda491760111..2ef282c21edc87fe5f062f8083296b43cc0a571d 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -724,7 +724,7 @@ } }, { - "context": "AgentPanel || GitPanel || ProjectPanel || CollabPanel || OutlinePanel || ChatPanel || VimControl || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView || DebugPanel", + "context": "VimControl || !Editor && !Terminal", "bindings": { // window related commands (ctrl-w X) "ctrl-w": null, @@ -782,7 +782,7 @@ } }, { - "context": "ChangesList || EmptyPane || SharedScreen || MarkdownPreview || KeyContextView || Welcome", + "context": "!Editor && !Terminal", "bindings": { ":": "command_palette::Toggle", "g /": "pane::DeploySearch" diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index b5dbab15c77a0cfff96885e5835f602197e408e6..174dbc80f02e9c1a16d563bfceff65c2bbb34e64 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -24,7 +24,7 @@ pub struct Keymap { } /// Index of a binding within a keymap. -#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] pub struct BindingIndex(usize); impl Keymap { @@ -167,65 +167,60 @@ impl Keymap { input: &[Keystroke], context_stack: &[KeyContext], ) -> (SmallVec<[(BindingIndex, KeyBinding); 1]>, bool) { - let possibilities = self + let mut possibilities = self .bindings() .enumerate() .rev() .filter_map(|(ix, binding)| { - binding - .match_keystrokes(input) - .map(|pending| (BindingIndex(ix), binding, pending)) - }); + let depth = self.binding_enabled(binding, &context_stack)?; + let pending = binding.match_keystrokes(input)?; + Some((depth, BindingIndex(ix), binding, pending)) + }) + .collect::<Vec<_>>(); + possibilities.sort_by(|(depth_a, ix_a, _, _), (depth_b, ix_b, _, _)| { + depth_b.cmp(depth_a).then(ix_b.cmp(ix_a)) + }); let mut bindings: SmallVec<[(BindingIndex, KeyBinding, usize); 1]> = SmallVec::new(); // (pending, is_no_action, depth, keystrokes) let mut pending_info_opt: Option<(bool, bool, usize, &[Keystroke])> = None; - 'outer: for (binding_index, binding, pending) in possibilities { - for depth in (0..=context_stack.len()).rev() { - if self.binding_enabled(binding, &context_stack[0..depth]) { - let is_no_action = is_no_action(&*binding.action); - // We only want to consider a binding pending if it has an action - // This, however, means that if we have both a NoAction binding and a binding - // with an action at the same depth, we should still set is_pending to true. - if let Some(pending_info) = pending_info_opt.as_mut() { - let ( - already_pending, - pending_is_no_action, - pending_depth, - pending_keystrokes, - ) = *pending_info; - - // We only want to change the pending status if it's not already pending AND if - // the existing pending status was set by a NoAction binding. This avoids a NoAction - // binding erroneously setting the pending status to true when a binding with an action - // already set it to false - // - // We also want to change the pending status if the keystrokes don't match, - // meaning it's different keystrokes than the NoAction that set pending to false - if pending - && !already_pending - && pending_is_no_action - && (pending_depth == depth - || pending_keystrokes != binding.keystrokes()) - { - pending_info.0 = !is_no_action; - } - } else { - pending_info_opt = Some(( - pending && !is_no_action, - is_no_action, - depth, - binding.keystrokes(), - )); - } - - if !pending { - bindings.push((binding_index, binding.clone(), depth)); - continue 'outer; - } + 'outer: for (depth, binding_index, binding, pending) in possibilities { + let is_no_action = is_no_action(&*binding.action); + // We only want to consider a binding pending if it has an action + // This, however, means that if we have both a NoAction binding and a binding + // with an action at the same depth, we should still set is_pending to true. + if let Some(pending_info) = pending_info_opt.as_mut() { + let (already_pending, pending_is_no_action, pending_depth, pending_keystrokes) = + *pending_info; + + // We only want to change the pending status if it's not already pending AND if + // the existing pending status was set by a NoAction binding. This avoids a NoAction + // binding erroneously setting the pending status to true when a binding with an action + // already set it to false + // + // We also want to change the pending status if the keystrokes don't match, + // meaning it's different keystrokes than the NoAction that set pending to false + if pending + && !already_pending + && pending_is_no_action + && (pending_depth == depth || pending_keystrokes != binding.keystrokes()) + { + pending_info.0 = !is_no_action; } + } else { + pending_info_opt = Some(( + pending && !is_no_action, + is_no_action, + depth, + binding.keystrokes(), + )); + } + + if !pending { + bindings.push((binding_index, binding.clone(), depth)); + continue 'outer; } } // sort by descending depth @@ -245,15 +240,13 @@ impl Keymap { } /// Check if the given binding is enabled, given a certain key context. - fn binding_enabled(&self, binding: &KeyBinding, context: &[KeyContext]) -> bool { - // If binding has a context predicate, it must match the current context, + /// Returns the deepest depth at which the binding matches, or None if it doesn't match. + fn binding_enabled(&self, binding: &KeyBinding, contexts: &[KeyContext]) -> Option<usize> { if let Some(predicate) = &binding.context_predicate { - if !predicate.eval(context) { - return false; - } + predicate.depth_of(contexts) + } else { + Some(contexts.len()) } - - true } } @@ -280,18 +273,33 @@ mod tests { keymap.add_bindings(bindings.clone()); // global bindings are enabled in all contexts - assert!(keymap.binding_enabled(&bindings[0], &[])); - assert!(keymap.binding_enabled(&bindings[0], &[KeyContext::parse("terminal").unwrap()])); + assert_eq!(keymap.binding_enabled(&bindings[0], &[]), Some(0)); + assert_eq!( + keymap.binding_enabled(&bindings[0], &[KeyContext::parse("terminal").unwrap()]), + Some(1) + ); // contextual bindings are enabled in contexts that match their predicate - assert!(!keymap.binding_enabled(&bindings[1], &[KeyContext::parse("barf x=y").unwrap()])); - assert!(keymap.binding_enabled(&bindings[1], &[KeyContext::parse("pane x=y").unwrap()])); - - assert!(!keymap.binding_enabled(&bindings[2], &[KeyContext::parse("editor").unwrap()])); - assert!(keymap.binding_enabled( - &bindings[2], - &[KeyContext::parse("editor mode=full").unwrap()] - )); + assert_eq!( + keymap.binding_enabled(&bindings[1], &[KeyContext::parse("barf x=y").unwrap()]), + None + ); + assert_eq!( + keymap.binding_enabled(&bindings[1], &[KeyContext::parse("pane x=y").unwrap()]), + Some(1) + ); + + assert_eq!( + keymap.binding_enabled(&bindings[2], &[KeyContext::parse("editor").unwrap()]), + None + ); + assert_eq!( + keymap.binding_enabled( + &bindings[2], + &[KeyContext::parse("editor mode=full").unwrap()] + ), + Some(1) + ); } #[test] diff --git a/crates/gpui/src/keymap/context.rs b/crates/gpui/src/keymap/context.rs index eaad06098218275ab37c9078c358cab019e90761..f4b878ae772ece481f9e7e7e2241c476dd8e4153 100644 --- a/crates/gpui/src/keymap/context.rs +++ b/crates/gpui/src/keymap/context.rs @@ -178,7 +178,7 @@ pub enum KeyBindingContextPredicate { NotEqual(SharedString, SharedString), /// A predicate that will match a given predicate appearing below another predicate. /// in the element tree - Child( + Descendant( Box<KeyBindingContextPredicate>, Box<KeyBindingContextPredicate>, ), @@ -203,7 +203,7 @@ impl fmt::Display for KeyBindingContextPredicate { Self::Equal(left, right) => write!(f, "{} == {}", left, right), Self::NotEqual(left, right) => write!(f, "{} != {}", left, right), Self::Not(pred) => write!(f, "!{}", pred), - Self::Child(parent, child) => write!(f, "{} > {}", parent, child), + Self::Descendant(parent, child) => write!(f, "{} > {}", parent, child), Self::And(left, right) => write!(f, "({} && {})", left, right), Self::Or(left, right) => write!(f, "({} || {})", left, right), } @@ -249,8 +249,25 @@ impl KeyBindingContextPredicate { } } + /// Find the deepest depth at which the predicate matches. + pub fn depth_of(&self, contexts: &[KeyContext]) -> Option<usize> { + for depth in (0..=contexts.len()).rev() { + let context_slice = &contexts[0..depth]; + if self.eval_inner(context_slice, contexts) { + return Some(depth); + } + } + None + } + + /// Eval a predicate against a set of contexts, arranged from lowest to highest. + #[allow(unused)] + pub(crate) fn eval(&self, contexts: &[KeyContext]) -> bool { + self.eval_inner(contexts, contexts) + } + /// Eval a predicate against a set of contexts, arranged from lowest to highest. - pub fn eval(&self, contexts: &[KeyContext]) -> bool { + pub fn eval_inner(&self, contexts: &[KeyContext], all_contexts: &[KeyContext]) -> bool { let Some(context) = contexts.last() else { return false; }; @@ -264,12 +281,38 @@ impl KeyBindingContextPredicate { .get(left) .map(|value| value != right) .unwrap_or(true), - Self::Not(pred) => !pred.eval(contexts), - Self::Child(parent, child) => { - parent.eval(&contexts[..contexts.len() - 1]) && child.eval(contexts) + Self::Not(pred) => { + for i in 0..all_contexts.len() { + if pred.eval_inner(&all_contexts[..=i], all_contexts) { + return false; + } + } + return true; + } + // Workspace > Pane > Editor + // + // Pane > (Pane > Editor) // should match? + // (Pane > Pane) > Editor // should not match? + // Pane > !Workspace <-- should match? + // !Workspace <-- shouldn't match? + Self::Descendant(parent, child) => { + for i in 0..contexts.len() - 1 { + // [Workspace > Pane], [Editor] + if parent.eval_inner(&contexts[..=i], all_contexts) { + if !child.eval_inner(&contexts[i + 1..], &contexts[i + 1..]) { + return false; + } + return true; + } + } + return false; + } + Self::And(left, right) => { + left.eval_inner(contexts, all_contexts) && right.eval_inner(contexts, all_contexts) + } + Self::Or(left, right) => { + left.eval_inner(contexts, all_contexts) || right.eval_inner(contexts, all_contexts) } - Self::And(left, right) => left.eval(contexts) && right.eval(contexts), - Self::Or(left, right) => left.eval(contexts) || right.eval(contexts), } } @@ -285,7 +328,7 @@ impl KeyBindingContextPredicate { } match other { - KeyBindingContextPredicate::Child(_, child) => self.is_superset(child), + KeyBindingContextPredicate::Descendant(_, child) => self.is_superset(child), KeyBindingContextPredicate::And(left, right) => { self.is_superset(left) || self.is_superset(right) } @@ -375,7 +418,7 @@ impl KeyBindingContextPredicate { } fn new_child(self, other: Self) -> Result<Self> { - Ok(Self::Child(Box::new(self), Box::new(other))) + Ok(Self::Descendant(Box::new(self), Box::new(other))) } fn new_eq(self, other: Self) -> Result<Self> { @@ -598,4 +641,122 @@ mod tests { assert_eq!(a.is_superset(&b), result, "({a:?}).is_superset({b:?})"); } } + + #[test] + fn test_child_operator() { + let predicate = KeyBindingContextPredicate::parse("parent > child").unwrap(); + + let parent_context = KeyContext::try_from("parent").unwrap(); + let child_context = KeyContext::try_from("child").unwrap(); + + let contexts = vec![parent_context.clone(), child_context.clone()]; + assert!(predicate.eval(&contexts)); + + let grandparent_context = KeyContext::try_from("grandparent").unwrap(); + + let contexts = vec![ + grandparent_context, + parent_context.clone(), + child_context.clone(), + ]; + assert!(predicate.eval(&contexts)); + + let other_context = KeyContext::try_from("other").unwrap(); + + 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(), + ]; + assert!(predicate.eval(&contexts)); + + assert!(!predicate.eval(&[])); + assert!(!predicate.eval(&[child_context.clone()])); + assert!(!predicate.eval(&[parent_context])); + + let zany_predicate = KeyBindingContextPredicate::parse("child > child").unwrap(); + assert!(!zany_predicate.eval(&[child_context.clone()])); + assert!(zany_predicate.eval(&[child_context.clone(), child_context.clone()])); + } + + #[test] + fn test_not_operator() { + let not_predicate = KeyBindingContextPredicate::parse("!editor").unwrap(); + let editor_context = KeyContext::try_from("editor").unwrap(); + let workspace_context = KeyContext::try_from("workspace").unwrap(); + 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(&[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(&[editor_context.clone(), workspace_context.clone()])); + + let not_mode_predicate = KeyBindingContextPredicate::parse("!(mode == full)").unwrap(); + let mut mode_context = KeyContext::default(); + mode_context.set("mode", "full"); + assert!(!not_mode_predicate.eval(&[mode_context.clone()])); + + let mut other_mode_context = KeyContext::default(); + other_mode_context.set("mode", "partial"); + 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(&[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(&[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()])); + + // Test complex descendant cases + let workspace_context = KeyContext::try_from("Workspace").unwrap(); + let pane_context = KeyContext::try_from("Pane").unwrap(); + let editor_context = KeyContext::try_from("Editor").unwrap(); + + // Workspace > Pane > Editor + let workspace_pane_editor = vec![ + workspace_context.clone(), + pane_context.clone(), + editor_context.clone(), + ]; + + // Pane > (Pane > Editor) - should not match + let pane_pane_editor = KeyBindingContextPredicate::parse("Pane > (Pane > Editor)").unwrap(); + assert!(!pane_pane_editor.eval(&workspace_pane_editor)); + + let workspace_pane_editor_predicate = + KeyBindingContextPredicate::parse("Workspace > Pane > Editor").unwrap(); + assert!(workspace_pane_editor_predicate.eval(&workspace_pane_editor)); + + // (Pane > Pane) > Editor - should not match + let pane_pane_then_editor = + KeyBindingContextPredicate::parse("(Pane > Pane) > Editor").unwrap(); + assert!(!pane_pane_then_editor.eval(&workspace_pane_editor)); + + // Pane > !Workspace - should match + let pane_not_workspace = KeyBindingContextPredicate::parse("Pane > !Workspace").unwrap(); + assert!(pane_not_workspace.eval(&[pane_context.clone(), editor_context.clone()])); + assert!(!pane_not_workspace.eval(&[pane_context.clone(), workspace_context.clone()])); + + // !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(&workspace_pane_editor)); + } } diff --git a/crates/language_tools/src/key_context_view.rs b/crates/language_tools/src/key_context_view.rs index c933872d8c513c21c2095b6b32d7a316fcb7f92f..88131781ec3af336d3ae793cf1820e5bcf731605 100644 --- a/crates/language_tools/src/key_context_view.rs +++ b/crates/language_tools/src/key_context_view.rs @@ -132,14 +132,7 @@ impl KeyContextView { } fn matches(&self, predicate: &KeyBindingContextPredicate) -> bool { - let mut stack = self.context_stack.clone(); - while !stack.is_empty() { - if predicate.eval(&stack) { - return true; - } - stack.pop(); - } - false + predicate.depth_of(&self.context_stack).is_some() } fn action_matches(&self, a: &Option<Box<dyn Action>>, b: &dyn Action) -> bool { diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 29927c419676c034d52b292703774d80619dc717..1a5bb7b45943113653a43b3ecf728a904286674f 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -3008,7 +3008,7 @@ fn collect_contexts_from_assets() -> Vec<SharedString> { contexts.insert(ident_a); contexts.insert(ident_b); } - gpui::KeyBindingContextPredicate::Child(ctx_a, ctx_b) => { + gpui::KeyBindingContextPredicate::Descendant(ctx_a, ctx_b) => { queue.push(*ctx_a); queue.push(*ctx_b); } diff --git a/docs/src/key-bindings.md b/docs/src/key-bindings.md index b2beaf9ffbcdd3c3635b5aec028767eb10b41a28..90aa400bb443b710fd4ef0bf01543f5b01bc8174 100644 --- a/docs/src/key-bindings.md +++ b/docs/src/key-bindings.md @@ -24,7 +24,7 @@ The file contains a JSON array of objects with `"bindings"`. If no `"context"` i Within each binding section a [key sequence](#keybinding-syntax) is mapped to an [action](#actions). If conflicts are detected they are resolved as [described below](#precedence). -If you are using a non-QWERTY, Latin-character keyboard, you may want to set `use_layout_keys` to `true`. See [Non-QWERTY keyboards](#non-qwerty-keyboards) for more information. +If you are using a non-QWERTY, Latin-character keyboard, you may want to set `use_key_equivalents` to `true`. See [Non-QWERTY keyboards](#non-qwerty-keyboards) for more information. For example: @@ -87,8 +87,6 @@ If a binding group has a `"context"` key it will be matched against the currentl Zed's contexts make up a tree, with the root being `Workspace`. Workspaces contain Panes and Panels, and Panes contain Editors, etc. The easiest way to see what contexts are active at a given moment is the key context view, which you can get to with `dev: Open Key Context View` in the command palette. -Contexts can contain extra attributes in addition to the name, so that you can (for example) match only in markdown files with `"context": "Editor && extension==md"`. It's worth noting that you can only use attributes at the level they are defined. - For example: ``` @@ -106,9 +104,20 @@ Workspace os=macos Context expressions can contain the following syntax: - `X && Y`, `X || Y` to and/or two conditions -- `!X` to negate a condition +- `!X` to check that a condition is false - `(X)` for grouping -- `X > Y` to match if a parent in the tree matches X and this layer matches Y. +- `X > Y` to match if an ancestor in the tree matches X and this layer matches Y. + +For example: + +- `"context": "Editor"` - matches any editor (including inline inputs) +- `"context": "Editor && mode=full"` - matches the main editors used for editing code +- `"context": "!Editor && !Terminal"` - matches anywhere except where an Editor or Terminal is focused +- `"context": "os=macos > Editor"` - matches any editor on macOS. + +It's worth noting that attributes are only available on the node they are defined on. This means that if you want to (for example) only enable a keybinding when the debugger is stopped in vim normal mode, you need to do `debugger_stopped > vim_mode == normal`. + +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) From 9a20843ba251324150cb072e7dcbd337a40dc7d5 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" <JosephTLyons@gmail.com> Date: Fri, 18 Jul 2025 12:03:08 -0400 Subject: [PATCH 190/658] Revert "gpui: Improve path rendering & global multisample anti-aliasing" (#34722) Reverts zed-industries/zed#29718 We've noticed some issues with Zed on Intel-based Macs where typing has become sluggish, and git bisect has seemed to point towards this PR. Reverting for now, until we can understand why it is causing this issue. --- Cargo.lock | 6 +- Cargo.toml | 12 +- crates/gpui/build.rs | 2 +- crates/gpui/examples/painting.rs | 23 +- crates/gpui/src/path_builder.rs | 5 +- crates/gpui/src/platform.rs | 1 + crates/gpui/src/platform/blade/blade_atlas.rs | 80 +++- .../gpui/src/platform/blade/blade_renderer.rs | 374 +++++++-------- crates/gpui/src/platform/blade/shaders.wgsl | 57 ++- crates/gpui/src/platform/mac/metal_atlas.rs | 35 +- .../gpui/src/platform/mac/metal_renderer.rs | 433 +++++++++++------- crates/gpui/src/platform/mac/shaders.metal | 103 +++-- crates/gpui/src/platform/test/window.rs | 2 +- crates/gpui/src/scene.rs | 61 ++- crates/gpui/src/window.rs | 2 +- docs/src/linux.md | 2 +- 16 files changed, 719 insertions(+), 479 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aad5349a87b31b8dc577ecbc634d8d6af32cd55b..8bf26543705042204358c61e6844aefabb8ede04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2162,7 +2162,7 @@ dependencies = [ [[package]] name = "blade-graphics" version = "0.6.0" -source = "git+https://github.com/kvark/blade?rev=416375211bb0b5826b3584dccdb6a43369e499ad#416375211bb0b5826b3584dccdb6a43369e499ad" +source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5" dependencies = [ "ash", "ash-window", @@ -2195,7 +2195,7 @@ dependencies = [ [[package]] name = "blade-macros" version = "0.3.0" -source = "git+https://github.com/kvark/blade?rev=416375211bb0b5826b3584dccdb6a43369e499ad#416375211bb0b5826b3584dccdb6a43369e499ad" +source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5" dependencies = [ "proc-macro2", "quote", @@ -2205,7 +2205,7 @@ dependencies = [ [[package]] name = "blade-util" version = "0.2.0" -source = "git+https://github.com/kvark/blade?rev=416375211bb0b5826b3584dccdb6a43369e499ad#416375211bb0b5826b3584dccdb6a43369e499ad" +source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5" dependencies = [ "blade-graphics", "bytemuck", diff --git a/Cargo.toml b/Cargo.toml index 1be2eb8d773d4168a3eec9f2c7d35e884699aa93..8d942a4c73c5ae1306e4e81a6a064759b0f1c782 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -436,9 +436,9 @@ 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" bitflags = "2.6.0" -blade-graphics = { git = "https://github.com/kvark/blade", rev = "416375211bb0b5826b3584dccdb6a43369e499ad" } -blade-macros = { git = "https://github.com/kvark/blade", rev = "416375211bb0b5826b3584dccdb6a43369e499ad" } -blade-util = { git = "https://github.com/kvark/blade", rev = "416375211bb0b5826b3584dccdb6a43369e499ad" } +blade-graphics = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" } +blade-macros = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" } +blade-util = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" } blake3 = "1.5.3" bytes = "1.0" cargo_metadata = "0.19" @@ -491,7 +491,7 @@ json_dotpath = "1.1" jsonschema = "0.30.0" jsonwebtoken = "9.3" jupyter-protocol = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" } -jupyter-websocket-client = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" } +jupyter-websocket-client = { git = "https://github.com/ConradIrwin/runtimed" ,rev = "7130c804216b6914355d15d0b91ea91f6babd734" } libc = "0.2" libsqlite3-sys = { version = "0.30.1", features = ["bundled"] } linkify = "0.10.0" @@ -502,7 +502,7 @@ metal = "0.29" moka = { version = "0.12.10", features = ["sync"] } naga = { version = "25.0", features = ["wgsl-in"] } nanoid = "0.4" -nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" } +nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" } nix = "0.29" num-format = "0.4.4" objc = "0.2" @@ -543,7 +543,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "951c77 "stream", ] } rsa = "0.9.6" -runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [ +runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [ "async-dispatcher-runtime", ] } rust-embed = { version = "8.4", features = ["include-exclude"] } diff --git a/crates/gpui/build.rs b/crates/gpui/build.rs index b9496cc01426485cbef625c7e697bbf6082d1a67..aed439744044574c87e8873e0d06f1c5cc68ec26 100644 --- a/crates/gpui/build.rs +++ b/crates/gpui/build.rs @@ -126,7 +126,7 @@ mod macos { "ContentMask".into(), "Uniforms".into(), "AtlasTile".into(), - "PathInputIndex".into(), + "PathRasterizationInputIndex".into(), "PathVertex_ScaledPixels".into(), "ShadowInputIndex".into(), "Shadow".into(), diff --git a/crates/gpui/examples/painting.rs b/crates/gpui/examples/painting.rs index 9ab58cffc9d417d181634e9958bb64ea5dace478..ff4b64cbda124733bc9f2a93c350ec3134759a5e 100644 --- a/crates/gpui/examples/painting.rs +++ b/crates/gpui/examples/painting.rs @@ -1,13 +1,9 @@ use gpui::{ Application, Background, Bounds, ColorSpace, Context, MouseDownEvent, Path, PathBuilder, - PathStyle, Pixels, Point, Render, SharedString, StrokeOptions, Window, WindowBounds, - WindowOptions, canvas, div, linear_color_stop, linear_gradient, point, prelude::*, px, rgb, - size, + PathStyle, Pixels, Point, Render, SharedString, StrokeOptions, Window, WindowOptions, canvas, + div, linear_color_stop, linear_gradient, point, prelude::*, px, rgb, size, }; -const DEFAULT_WINDOW_WIDTH: Pixels = px(1024.0); -const DEFAULT_WINDOW_HEIGHT: Pixels = px(768.0); - struct PaintingViewer { default_lines: Vec<(Path<Pixels>, Background)>, lines: Vec<Vec<Point<Pixels>>>, @@ -151,6 +147,8 @@ impl PaintingViewer { px(320.0 + (i as f32 * 10.0).sin() * 40.0), )); } + let path = builder.build().unwrap(); + lines.push((path, gpui::green().into())); Self { default_lines: lines.clone(), @@ -185,13 +183,9 @@ fn button( } impl Render for PaintingViewer { - fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { - window.request_animation_frame(); - + fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let default_lines = self.default_lines.clone(); let lines = self.lines.clone(); - let window_size = window.bounds().size; - let scale = window_size.width / DEFAULT_WINDOW_WIDTH; let dashed = self.dashed; div() @@ -228,7 +222,7 @@ impl Render for PaintingViewer { move |_, _, _| {}, move |_, _, window, _| { for (path, color) in default_lines { - window.paint_path(path.clone().scale(scale), color); + window.paint_path(path, color); } for points in lines { @@ -304,11 +298,6 @@ fn main() { cx.open_window( WindowOptions { focus: true, - window_bounds: Some(WindowBounds::Windowed(Bounds::centered( - None, - size(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT), - cx, - ))), ..Default::default() }, |window, cx| cx.new(|cx| PaintingViewer::new(window, cx)), diff --git a/crates/gpui/src/path_builder.rs b/crates/gpui/src/path_builder.rs index 13c168b0bb90f7d209ce02cbab798faf48ae1d2f..6c8cfddd523c4d56c81ebcbbf1437b5cc418d73c 100644 --- a/crates/gpui/src/path_builder.rs +++ b/crates/gpui/src/path_builder.rs @@ -336,7 +336,10 @@ impl PathBuilder { let v1 = buf.vertices[i1]; let v2 = buf.vertices[i2]; - path.push_triangle((v0.into(), v1.into(), v2.into())); + path.push_triangle( + (v0.into(), v1.into(), v2.into()), + (point(0., 1.), point(0., 1.), point(0., 1.)), + ); } path diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 0250e59a9bbc363a61377dc8c0ab01bccd820df3..8918fdd28bda083abd0335f6f500b35d01895c58 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -794,6 +794,7 @@ pub(crate) struct AtlasTextureId { pub(crate) enum AtlasTextureKind { Monochrome = 0, Polychrome = 1, + Path = 2, } #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] diff --git a/crates/gpui/src/platform/blade/blade_atlas.rs b/crates/gpui/src/platform/blade/blade_atlas.rs index 0b119c39101ff36199d41d7905fb6e9e25db4a68..78ba52056a9dce1fb4a497ac257d96f6e1e2bd5c 100644 --- a/crates/gpui/src/platform/blade/blade_atlas.rs +++ b/crates/gpui/src/platform/blade/blade_atlas.rs @@ -10,6 +10,8 @@ use etagere::BucketedAtlasAllocator; use parking_lot::Mutex; use std::{borrow::Cow, ops, sync::Arc}; +pub(crate) const PATH_TEXTURE_FORMAT: gpu::TextureFormat = gpu::TextureFormat::R16Float; + pub(crate) struct BladeAtlas(Mutex<BladeAtlasState>); struct PendingUpload { @@ -25,6 +27,7 @@ struct BladeAtlasState { tiles_by_key: FxHashMap<AtlasKey, AtlasTile>, initializations: Vec<AtlasTextureId>, uploads: Vec<PendingUpload>, + path_sample_count: u32, } #[cfg(gles)] @@ -38,13 +41,13 @@ impl BladeAtlasState { } pub struct BladeTextureInfo { - #[allow(dead_code)] pub size: gpu::Extent, pub raw_view: gpu::TextureView, + pub msaa_view: Option<gpu::TextureView>, } impl BladeAtlas { - pub(crate) fn new(gpu: &Arc<gpu::Context>) -> Self { + pub(crate) fn new(gpu: &Arc<gpu::Context>, path_sample_count: u32) -> Self { BladeAtlas(Mutex::new(BladeAtlasState { gpu: Arc::clone(gpu), upload_belt: BufferBelt::new(BufferBeltDescriptor { @@ -56,6 +59,7 @@ impl BladeAtlas { tiles_by_key: Default::default(), initializations: Vec::new(), uploads: Vec::new(), + path_sample_count, })) } @@ -63,7 +67,6 @@ impl BladeAtlas { self.0.lock().destroy(); } - #[allow(dead_code)] pub(crate) fn clear_textures(&self, texture_kind: AtlasTextureKind) { let mut lock = self.0.lock(); let textures = &mut lock.storage[texture_kind]; @@ -72,6 +75,19 @@ impl BladeAtlas { } } + /// Allocate a rectangle and make it available for rendering immediately (without waiting for `before_frame`) + pub fn allocate_for_rendering( + &self, + size: Size<DevicePixels>, + texture_kind: AtlasTextureKind, + gpu_encoder: &mut gpu::CommandEncoder, + ) -> AtlasTile { + let mut lock = self.0.lock(); + let tile = lock.allocate(size, texture_kind); + lock.flush_initializations(gpu_encoder); + tile + } + pub fn before_frame(&self, gpu_encoder: &mut gpu::CommandEncoder) { let mut lock = self.0.lock(); lock.flush(gpu_encoder); @@ -93,6 +109,7 @@ impl BladeAtlas { depth: 1, }, raw_view: texture.raw_view, + msaa_view: texture.msaa_view, } } } @@ -183,8 +200,48 @@ impl BladeAtlasState { format = gpu::TextureFormat::Bgra8UnormSrgb; usage = gpu::TextureUsage::COPY | gpu::TextureUsage::RESOURCE; } + AtlasTextureKind::Path => { + format = PATH_TEXTURE_FORMAT; + usage = gpu::TextureUsage::COPY + | gpu::TextureUsage::RESOURCE + | gpu::TextureUsage::TARGET; + } } + // We currently only enable MSAA for path textures. + let (msaa, msaa_view) = if self.path_sample_count > 1 && kind == AtlasTextureKind::Path { + let msaa = self.gpu.create_texture(gpu::TextureDesc { + name: "msaa path texture", + format, + size: gpu::Extent { + width: size.width.into(), + height: size.height.into(), + depth: 1, + }, + array_layer_count: 1, + mip_level_count: 1, + sample_count: self.path_sample_count, + dimension: gpu::TextureDimension::D2, + usage: gpu::TextureUsage::TARGET, + external: None, + }); + + ( + Some(msaa), + Some(self.gpu.create_texture_view( + msaa, + gpu::TextureViewDesc { + name: "msaa texture view", + format, + dimension: gpu::ViewDimension::D2, + subresources: &Default::default(), + }, + )), + ) + } else { + (None, None) + }; + let raw = self.gpu.create_texture(gpu::TextureDesc { name: "atlas", format, @@ -222,6 +279,8 @@ impl BladeAtlasState { format, raw, raw_view, + msaa, + msaa_view, live_atlas_keys: 0, }; @@ -281,6 +340,7 @@ impl BladeAtlasState { struct BladeAtlasStorage { monochrome_textures: AtlasTextureList<BladeAtlasTexture>, polychrome_textures: AtlasTextureList<BladeAtlasTexture>, + path_textures: AtlasTextureList<BladeAtlasTexture>, } impl ops::Index<AtlasTextureKind> for BladeAtlasStorage { @@ -289,6 +349,7 @@ impl ops::Index<AtlasTextureKind> for BladeAtlasStorage { match kind { crate::AtlasTextureKind::Monochrome => &self.monochrome_textures, crate::AtlasTextureKind::Polychrome => &self.polychrome_textures, + crate::AtlasTextureKind::Path => &self.path_textures, } } } @@ -298,6 +359,7 @@ impl ops::IndexMut<AtlasTextureKind> for BladeAtlasStorage { match kind { crate::AtlasTextureKind::Monochrome => &mut self.monochrome_textures, crate::AtlasTextureKind::Polychrome => &mut self.polychrome_textures, + crate::AtlasTextureKind::Path => &mut self.path_textures, } } } @@ -308,6 +370,7 @@ impl ops::Index<AtlasTextureId> for BladeAtlasStorage { let textures = match id.kind { crate::AtlasTextureKind::Monochrome => &self.monochrome_textures, crate::AtlasTextureKind::Polychrome => &self.polychrome_textures, + crate::AtlasTextureKind::Path => &self.path_textures, }; textures[id.index as usize].as_ref().unwrap() } @@ -321,6 +384,9 @@ impl BladeAtlasStorage { for mut texture in self.polychrome_textures.drain().flatten() { texture.destroy(gpu); } + for mut texture in self.path_textures.drain().flatten() { + texture.destroy(gpu); + } } } @@ -329,6 +395,8 @@ struct BladeAtlasTexture { allocator: BucketedAtlasAllocator, raw: gpu::Texture, raw_view: gpu::TextureView, + msaa: Option<gpu::Texture>, + msaa_view: Option<gpu::TextureView>, format: gpu::TextureFormat, live_atlas_keys: u32, } @@ -356,6 +424,12 @@ impl BladeAtlasTexture { fn destroy(&mut self, gpu: &gpu::Context) { gpu.destroy_texture(self.raw); gpu.destroy_texture_view(self.raw_view); + if let Some(msaa) = self.msaa { + gpu.destroy_texture(msaa); + } + if let Some(msaa_view) = self.msaa_view { + gpu.destroy_texture_view(msaa_view); + } } fn bytes_per_pixel(&self) -> u8 { diff --git a/crates/gpui/src/platform/blade/blade_renderer.rs b/crates/gpui/src/platform/blade/blade_renderer.rs index 1b9f111b0d44f2182e5e76b17228b41b66baa32b..cac47434ae308f7de7123baf26527ccb0da3321d 100644 --- a/crates/gpui/src/platform/blade/blade_renderer.rs +++ b/crates/gpui/src/platform/blade/blade_renderer.rs @@ -1,19 +1,24 @@ // Doing `if let` gives you nice scoping with passes/encoders #![allow(irrefutable_let_patterns)] -use super::{BladeAtlas, BladeContext}; +use super::{BladeAtlas, BladeContext, PATH_TEXTURE_FORMAT}; use crate::{ - Background, Bounds, ContentMask, DevicePixels, GpuSpecs, MonochromeSprite, PathVertex, - PolychromeSprite, PrimitiveBatch, Quad, ScaledPixels, Scene, Shadow, Size, Underline, + AtlasTextureKind, AtlasTile, Background, Bounds, ContentMask, DevicePixels, GpuSpecs, + MonochromeSprite, Path, PathId, PathVertex, PolychromeSprite, PrimitiveBatch, Quad, + ScaledPixels, Scene, Shadow, Size, Underline, }; -use blade_graphics::{self as gpu}; +use blade_graphics as gpu; use blade_util::{BufferBelt, BufferBeltDescriptor}; use bytemuck::{Pod, Zeroable}; +use collections::HashMap; #[cfg(target_os = "macos")] use media::core_video::CVMetalTextureCache; use std::{mem, sync::Arc}; const MAX_FRAME_TIME_MS: u32 = 10000; +// Use 4x MSAA, all devices support it. +// https://developer.apple.com/documentation/metal/mtldevice/1433355-supportstexturesamplecount +const DEFAULT_PATH_SAMPLE_COUNT: u32 = 4; #[repr(C)] #[derive(Clone, Copy, Pod, Zeroable)] @@ -61,9 +66,16 @@ struct ShaderShadowsData { } #[derive(blade_macros::ShaderData)] -struct ShaderPathsData { +struct ShaderPathRasterizationData { globals: GlobalParams, b_path_vertices: gpu::BufferPiece, +} + +#[derive(blade_macros::ShaderData)] +struct ShaderPathsData { + globals: GlobalParams, + t_sprite: gpu::TextureView, + s_sprite: gpu::Sampler, b_path_sprites: gpu::BufferPiece, } @@ -103,27 +115,13 @@ struct ShaderSurfacesData { struct PathSprite { bounds: Bounds<ScaledPixels>, color: Background, -} - -/// Argument buffer layout for `draw_indirect` commands. -#[repr(C)] -#[derive(Copy, Clone, Debug, Default, Pod, Zeroable)] -pub struct DrawIndirectArgs { - /// The number of vertices to draw. - pub vertex_count: u32, - /// The number of instances to draw. - pub instance_count: u32, - /// The Index of the first vertex to draw. - pub first_vertex: u32, - /// The instance ID of the first instance to draw. - /// - /// Has to be 0, unless [`Features::INDIRECT_FIRST_INSTANCE`](crate::Features::INDIRECT_FIRST_INSTANCE) is enabled. - pub first_instance: u32, + tile: AtlasTile, } struct BladePipelines { quads: gpu::RenderPipeline, shadows: gpu::RenderPipeline, + path_rasterization: gpu::RenderPipeline, paths: gpu::RenderPipeline, underlines: gpu::RenderPipeline, mono_sprites: gpu::RenderPipeline, @@ -132,7 +130,7 @@ struct BladePipelines { } impl BladePipelines { - fn new(gpu: &gpu::Context, surface_info: gpu::SurfaceInfo, sample_count: u32) -> Self { + fn new(gpu: &gpu::Context, surface_info: gpu::SurfaceInfo, path_sample_count: u32) -> Self { use gpu::ShaderData as _; log::info!( @@ -180,10 +178,7 @@ impl BladePipelines { depth_stencil: None, fragment: Some(shader.at("fs_quad")), color_targets, - multisample_state: gpu::MultisampleState { - sample_count, - ..Default::default() - }, + multisample_state: gpu::MultisampleState::default(), }), shadows: gpu.create_render_pipeline(gpu::RenderPipelineDesc { name: "shadows", @@ -197,8 +192,26 @@ impl BladePipelines { depth_stencil: None, fragment: Some(shader.at("fs_shadow")), color_targets, + multisample_state: gpu::MultisampleState::default(), + }), + path_rasterization: gpu.create_render_pipeline(gpu::RenderPipelineDesc { + name: "path_rasterization", + data_layouts: &[&ShaderPathRasterizationData::layout()], + vertex: shader.at("vs_path_rasterization"), + vertex_fetches: &[], + primitive: gpu::PrimitiveState { + topology: gpu::PrimitiveTopology::TriangleList, + ..Default::default() + }, + depth_stencil: None, + fragment: Some(shader.at("fs_path_rasterization")), + color_targets: &[gpu::ColorTargetState { + format: PATH_TEXTURE_FORMAT, + blend: Some(gpu::BlendState::ADDITIVE), + write_mask: gpu::ColorWrites::default(), + }], multisample_state: gpu::MultisampleState { - sample_count, + sample_count: path_sample_count, ..Default::default() }, }), @@ -208,16 +221,13 @@ impl BladePipelines { vertex: shader.at("vs_path"), vertex_fetches: &[], primitive: gpu::PrimitiveState { - topology: gpu::PrimitiveTopology::TriangleList, + topology: gpu::PrimitiveTopology::TriangleStrip, ..Default::default() }, depth_stencil: None, fragment: Some(shader.at("fs_path")), color_targets, - multisample_state: gpu::MultisampleState { - sample_count, - ..Default::default() - }, + multisample_state: gpu::MultisampleState::default(), }), underlines: gpu.create_render_pipeline(gpu::RenderPipelineDesc { name: "underlines", @@ -231,10 +241,7 @@ impl BladePipelines { depth_stencil: None, fragment: Some(shader.at("fs_underline")), color_targets, - multisample_state: gpu::MultisampleState { - sample_count, - ..Default::default() - }, + multisample_state: gpu::MultisampleState::default(), }), mono_sprites: gpu.create_render_pipeline(gpu::RenderPipelineDesc { name: "mono-sprites", @@ -248,10 +255,7 @@ impl BladePipelines { depth_stencil: None, fragment: Some(shader.at("fs_mono_sprite")), color_targets, - multisample_state: gpu::MultisampleState { - sample_count, - ..Default::default() - }, + multisample_state: gpu::MultisampleState::default(), }), poly_sprites: gpu.create_render_pipeline(gpu::RenderPipelineDesc { name: "poly-sprites", @@ -265,10 +269,7 @@ impl BladePipelines { depth_stencil: None, fragment: Some(shader.at("fs_poly_sprite")), color_targets, - multisample_state: gpu::MultisampleState { - sample_count, - ..Default::default() - }, + multisample_state: gpu::MultisampleState::default(), }), surfaces: gpu.create_render_pipeline(gpu::RenderPipelineDesc { name: "surfaces", @@ -282,10 +283,7 @@ impl BladePipelines { depth_stencil: None, fragment: Some(shader.at("fs_surface")), color_targets, - multisample_state: gpu::MultisampleState { - sample_count, - ..Default::default() - }, + multisample_state: gpu::MultisampleState::default(), }), } } @@ -293,6 +291,7 @@ impl BladePipelines { fn destroy(&mut self, gpu: &gpu::Context) { gpu.destroy_render_pipeline(&mut self.quads); gpu.destroy_render_pipeline(&mut self.shadows); + gpu.destroy_render_pipeline(&mut self.path_rasterization); gpu.destroy_render_pipeline(&mut self.paths); gpu.destroy_render_pipeline(&mut self.underlines); gpu.destroy_render_pipeline(&mut self.mono_sprites); @@ -318,13 +317,12 @@ pub struct BladeRenderer { last_sync_point: Option<gpu::SyncPoint>, pipelines: BladePipelines, instance_belt: BufferBelt, + path_tiles: HashMap<PathId, AtlasTile>, atlas: Arc<BladeAtlas>, atlas_sampler: gpu::Sampler, #[cfg(target_os = "macos")] core_video_texture_cache: CVMetalTextureCache, - sample_count: u32, - texture_msaa: Option<gpu::Texture>, - texture_view_msaa: Option<gpu::TextureView>, + path_sample_count: u32, } impl BladeRenderer { @@ -333,18 +331,6 @@ impl BladeRenderer { window: &I, config: BladeSurfaceConfig, ) -> anyhow::Result<Self> { - // workaround for https://github.com/zed-industries/zed/issues/26143 - let sample_count = std::env::var("ZED_SAMPLE_COUNT") - .ok() - .or_else(|| std::env::var("ZED_PATH_SAMPLE_COUNT").ok()) - .and_then(|v| v.parse().ok()) - .or_else(|| { - [4, 2, 1] - .into_iter() - .find(|count| context.gpu.supports_texture_sample_count(*count)) - }) - .unwrap_or(1); - let surface_config = gpu::SurfaceConfig { size: config.size, usage: gpu::TextureUsage::TARGET, @@ -358,27 +344,22 @@ impl BladeRenderer { .create_surface_configured(window, surface_config) .map_err(|err| anyhow::anyhow!("Failed to create surface: {err:?}"))?; - let (texture_msaa, texture_view_msaa) = create_msaa_texture_if_needed( - &context.gpu, - surface.info().format, - config.size.width, - config.size.height, - sample_count, - ) - .unzip(); - let command_encoder = context.gpu.create_command_encoder(gpu::CommandEncoderDesc { name: "main", buffer_count: 2, }); - - let pipelines = BladePipelines::new(&context.gpu, surface.info(), sample_count); + // workaround for https://github.com/zed-industries/zed/issues/26143 + let path_sample_count = std::env::var("ZED_PATH_SAMPLE_COUNT") + .ok() + .and_then(|v| v.parse().ok()) + .unwrap_or(DEFAULT_PATH_SAMPLE_COUNT); + let pipelines = BladePipelines::new(&context.gpu, surface.info(), path_sample_count); let instance_belt = BufferBelt::new(BufferBeltDescriptor { memory: gpu::Memory::Shared, min_chunk_size: 0x1000, alignment: 0x40, // Vulkan `minStorageBufferOffsetAlignment` on Intel Xe }); - let atlas = Arc::new(BladeAtlas::new(&context.gpu)); + let atlas = Arc::new(BladeAtlas::new(&context.gpu, path_sample_count)); let atlas_sampler = context.gpu.create_sampler(gpu::SamplerDesc { name: "atlas", mag_filter: gpu::FilterMode::Linear, @@ -402,13 +383,12 @@ impl BladeRenderer { last_sync_point: None, pipelines, instance_belt, + path_tiles: HashMap::default(), atlas, atlas_sampler, #[cfg(target_os = "macos")] core_video_texture_cache, - sample_count, - texture_msaa, - texture_view_msaa, + path_sample_count, }) } @@ -461,24 +441,6 @@ impl BladeRenderer { self.surface_config.size = gpu_size; self.gpu .reconfigure_surface(&mut self.surface, self.surface_config); - - if let Some(texture_msaa) = self.texture_msaa { - self.gpu.destroy_texture(texture_msaa); - } - if let Some(texture_view_msaa) = self.texture_view_msaa { - self.gpu.destroy_texture_view(texture_view_msaa); - } - - let (texture_msaa, texture_view_msaa) = create_msaa_texture_if_needed( - &self.gpu, - self.surface.info().format, - gpu_size.width, - gpu_size.height, - self.sample_count, - ) - .unzip(); - self.texture_msaa = texture_msaa; - self.texture_view_msaa = texture_view_msaa; } } @@ -489,7 +451,8 @@ impl BladeRenderer { self.gpu .reconfigure_surface(&mut self.surface, self.surface_config); self.pipelines.destroy(&self.gpu); - self.pipelines = BladePipelines::new(&self.gpu, self.surface.info(), self.sample_count); + self.pipelines = + BladePipelines::new(&self.gpu, self.surface.info(), self.path_sample_count); } } @@ -527,6 +490,80 @@ impl BladeRenderer { objc2::rc::Retained::as_ptr(&self.surface.metal_layer()) as *mut _ } + #[profiling::function] + fn rasterize_paths(&mut self, paths: &[Path<ScaledPixels>]) { + self.path_tiles.clear(); + let mut vertices_by_texture_id = HashMap::default(); + + for path in paths { + let clipped_bounds = path + .bounds + .intersect(&path.content_mask.bounds) + .map_origin(|origin| origin.floor()) + .map_size(|size| size.ceil()); + let tile = self.atlas.allocate_for_rendering( + clipped_bounds.size.map(Into::into), + AtlasTextureKind::Path, + &mut self.command_encoder, + ); + vertices_by_texture_id + .entry(tile.texture_id) + .or_insert(Vec::new()) + .extend(path.vertices.iter().map(|vertex| PathVertex { + xy_position: vertex.xy_position - clipped_bounds.origin + + tile.bounds.origin.map(Into::into), + st_position: vertex.st_position, + content_mask: ContentMask { + bounds: tile.bounds.map(Into::into), + }, + })); + self.path_tiles.insert(path.id, tile); + } + + for (texture_id, vertices) in vertices_by_texture_id { + let tex_info = self.atlas.get_texture_info(texture_id); + let globals = GlobalParams { + viewport_size: [tex_info.size.width as f32, tex_info.size.height as f32], + premultiplied_alpha: 0, + pad: 0, + }; + + let vertex_buf = unsafe { self.instance_belt.alloc_typed(&vertices, &self.gpu) }; + let frame_view = tex_info.raw_view; + let color_target = if let Some(msaa_view) = tex_info.msaa_view { + gpu::RenderTarget { + view: msaa_view, + init_op: gpu::InitOp::Clear(gpu::TextureColor::OpaqueBlack), + finish_op: gpu::FinishOp::ResolveTo(frame_view), + } + } else { + gpu::RenderTarget { + view: frame_view, + init_op: gpu::InitOp::Clear(gpu::TextureColor::OpaqueBlack), + finish_op: gpu::FinishOp::Store, + } + }; + + if let mut pass = self.command_encoder.render( + "paths", + gpu::RenderTargetSet { + colors: &[color_target], + depth_stencil: None, + }, + ) { + let mut encoder = pass.with(&self.pipelines.path_rasterization); + encoder.bind( + 0, + &ShaderPathRasterizationData { + globals, + b_path_vertices: vertex_buf, + }, + ); + encoder.draw(0, vertices.len() as u32, 0, 1); + } + } + } + pub fn destroy(&mut self) { self.wait_for_gpu(); self.atlas.destroy(); @@ -535,26 +572,17 @@ impl BladeRenderer { self.gpu.destroy_command_encoder(&mut self.command_encoder); self.pipelines.destroy(&self.gpu); self.gpu.destroy_surface(&mut self.surface); - if let Some(texture_msaa) = self.texture_msaa { - self.gpu.destroy_texture(texture_msaa); - } - if let Some(texture_view_msaa) = self.texture_view_msaa { - self.gpu.destroy_texture_view(texture_view_msaa); - } } pub fn draw(&mut self, scene: &Scene) { self.command_encoder.start(); self.atlas.before_frame(&mut self.command_encoder); + self.rasterize_paths(scene.paths()); let frame = { profiling::scope!("acquire frame"); self.surface.acquire_frame() }; - let frame_view = frame.texture_view(); - if let Some(texture_msaa) = self.texture_msaa { - self.command_encoder.init_texture(texture_msaa); - } self.command_encoder.init_texture(frame.texture()); let globals = GlobalParams { @@ -569,25 +597,14 @@ impl BladeRenderer { pad: 0, }; - let target = if let Some(texture_view_msaa) = self.texture_view_msaa { - gpu::RenderTarget { - view: texture_view_msaa, - init_op: gpu::InitOp::Clear(gpu::TextureColor::TransparentBlack), - finish_op: gpu::FinishOp::ResolveTo(frame_view), - } - } else { - gpu::RenderTarget { - view: frame_view, - init_op: gpu::InitOp::Clear(gpu::TextureColor::TransparentBlack), - finish_op: gpu::FinishOp::Store, - } - }; - - // draw to the target texture if let mut pass = self.command_encoder.render( "main", gpu::RenderTargetSet { - colors: &[target], + colors: &[gpu::RenderTarget { + view: frame.texture_view(), + init_op: gpu::InitOp::Clear(gpu::TextureColor::TransparentBlack), + finish_op: gpu::FinishOp::Store, + }], depth_stencil: None, }, ) { @@ -622,55 +639,32 @@ impl BladeRenderer { } PrimitiveBatch::Paths(paths) => { let mut encoder = pass.with(&self.pipelines.paths); - - let mut vertices = Vec::new(); - let mut sprites = Vec::with_capacity(paths.len()); - let mut draw_indirect_commands = Vec::with_capacity(paths.len()); - let mut first_vertex = 0; - - for (i, path) in paths.iter().enumerate() { - draw_indirect_commands.push(DrawIndirectArgs { - vertex_count: path.vertices.len() as u32, - instance_count: 1, - first_vertex, - first_instance: i as u32, - }); - first_vertex += path.vertices.len() as u32; - - vertices.extend(path.vertices.iter().map(|v| PathVertex { - xy_position: v.xy_position, - content_mask: ContentMask { - bounds: path.content_mask.bounds, + // todo(linux): group by texture ID + for path in paths { + let tile = &self.path_tiles[&path.id]; + let tex_info = self.atlas.get_texture_info(tile.texture_id); + let origin = path.bounds.intersect(&path.content_mask.bounds).origin; + let sprites = [PathSprite { + bounds: Bounds { + origin: origin.map(|p| p.floor()), + size: tile.bounds.size.map(Into::into), }, - })); - - sprites.push(PathSprite { - bounds: path.bounds, color: path.color, - }); - } - - let b_path_vertices = - unsafe { self.instance_belt.alloc_typed(&vertices, &self.gpu) }; - let instance_buf = - unsafe { self.instance_belt.alloc_typed(&sprites, &self.gpu) }; - let indirect_buf = unsafe { - self.instance_belt - .alloc_typed(&draw_indirect_commands, &self.gpu) - }; - - encoder.bind( - 0, - &ShaderPathsData { - globals, - b_path_vertices, - b_path_sprites: instance_buf, - }, - ); - - for i in 0..paths.len() { - encoder.draw_indirect(indirect_buf.buffer.at(indirect_buf.offset - + (i * mem::size_of::<DrawIndirectArgs>()) as u64)); + tile: (*tile).clone(), + }]; + + let instance_buf = + unsafe { self.instance_belt.alloc_typed(&sprites, &self.gpu) }; + encoder.bind( + 0, + &ShaderPathsData { + globals, + t_sprite: tex_info.raw_view, + s_sprite: self.atlas_sampler, + b_path_sprites: instance_buf, + }, + ); + encoder.draw(0, 4, 0, sprites.len() as u32); } } PrimitiveBatch::Underlines(underlines) => { @@ -823,47 +817,9 @@ impl BladeRenderer { profiling::scope!("finish"); self.instance_belt.flush(&sync_point); self.atlas.after_frame(&sync_point); + self.atlas.clear_textures(AtlasTextureKind::Path); self.wait_for_gpu(); self.last_sync_point = Some(sync_point); } } - -fn create_msaa_texture_if_needed( - gpu: &gpu::Context, - format: gpu::TextureFormat, - width: u32, - height: u32, - sample_count: u32, -) -> Option<(gpu::Texture, gpu::TextureView)> { - if sample_count <= 1 { - return None; - } - - let texture_msaa = gpu.create_texture(gpu::TextureDesc { - name: "msaa", - format, - size: gpu::Extent { - width, - height, - depth: 1, - }, - array_layer_count: 1, - mip_level_count: 1, - sample_count, - dimension: gpu::TextureDimension::D2, - usage: gpu::TextureUsage::TARGET, - external: None, - }); - let texture_view_msaa = gpu.create_texture_view( - texture_msaa, - gpu::TextureViewDesc { - name: "msaa view", - format, - dimension: gpu::ViewDimension::D2, - subresources: &Default::default(), - }, - ); - - Some((texture_msaa, texture_view_msaa)) -} diff --git a/crates/gpui/src/platform/blade/shaders.wgsl b/crates/gpui/src/platform/blade/shaders.wgsl index 00c9d07af7d670a8bc00f4374c143bb28ff2b6d6..0b34a0eea32fd492b5a82055e591bf22d593f136 100644 --- a/crates/gpui/src/platform/blade/shaders.wgsl +++ b/crates/gpui/src/platform/blade/shaders.wgsl @@ -922,23 +922,59 @@ fn fs_shadow(input: ShadowVarying) -> @location(0) vec4<f32> { return blend_color(input.color, alpha); } -// --- paths --- // +// --- path rasterization --- // struct PathVertex { xy_position: vec2<f32>, + st_position: vec2<f32>, content_mask: Bounds, } +var<storage, read> b_path_vertices: array<PathVertex>; + +struct PathRasterizationVarying { + @builtin(position) position: vec4<f32>, + @location(0) st_position: vec2<f32>, + //TODO: use `clip_distance` once Naga supports it + @location(3) clip_distances: vec4<f32>, +} + +@vertex +fn vs_path_rasterization(@builtin(vertex_index) vertex_id: u32) -> PathRasterizationVarying { + let v = b_path_vertices[vertex_id]; + + var out = PathRasterizationVarying(); + out.position = to_device_position_impl(v.xy_position); + out.st_position = v.st_position; + out.clip_distances = distance_from_clip_rect_impl(v.xy_position, v.content_mask); + return out; +} + +@fragment +fn fs_path_rasterization(input: PathRasterizationVarying) -> @location(0) f32 { + let dx = dpdx(input.st_position); + let dy = dpdy(input.st_position); + if (any(input.clip_distances < vec4<f32>(0.0))) { + return 0.0; + } + + let gradient = 2.0 * input.st_position.xx * vec2<f32>(dx.x, dy.x) - vec2<f32>(dx.y, dy.y); + let f = input.st_position.x * input.st_position.x - input.st_position.y; + let distance = f / length(gradient); + return saturate(0.5 - distance); +} + +// --- paths --- // struct PathSprite { bounds: Bounds, color: Background, + tile: AtlasTile, } -var<storage, read> b_path_vertices: array<PathVertex>; var<storage, read> b_path_sprites: array<PathSprite>; struct PathVarying { @builtin(position) position: vec4<f32>, - @location(0) clip_distances: vec4<f32>, + @location(0) tile_position: vec2<f32>, @location(1) @interpolate(flat) instance_id: u32, @location(2) @interpolate(flat) color_solid: vec4<f32>, @location(3) @interpolate(flat) color0: vec4<f32>, @@ -947,12 +983,13 @@ struct PathVarying { @vertex fn vs_path(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> PathVarying { - let v = b_path_vertices[vertex_id]; + let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u)); let sprite = b_path_sprites[instance_id]; + // Don't apply content mask because it was already accounted for when rasterizing the path. var out = PathVarying(); - out.position = to_device_position_impl(v.xy_position); - out.clip_distances = distance_from_clip_rect_impl(v.xy_position, v.content_mask); + out.position = to_device_position(unit_vertex, sprite.bounds); + out.tile_position = to_tile_position(unit_vertex, sprite.tile); out.instance_id = instance_id; let gradient = prepare_gradient_color( @@ -969,15 +1006,13 @@ fn vs_path(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) insta @fragment fn fs_path(input: PathVarying) -> @location(0) vec4<f32> { - if any(input.clip_distances < vec4<f32>(0.0)) { - return vec4<f32>(0.0); - } - + let sample = textureSample(t_sprite, s_sprite, input.tile_position).r; + let mask = 1.0 - abs(1.0 - sample % 2.0); let sprite = b_path_sprites[input.instance_id]; let background = sprite.color; let color = gradient_color(background, input.position.xy, sprite.bounds, input.color_solid, input.color0, input.color1); - return blend_color(color, 1.0); + return blend_color(color, mask); } // --- underlines --- // diff --git a/crates/gpui/src/platform/mac/metal_atlas.rs b/crates/gpui/src/platform/mac/metal_atlas.rs index 0c8e1d37032f48994bbf41fb44a77efe991e47bf..366f2dcc3ca5b0227a790ef7c25375891ab62504 100644 --- a/crates/gpui/src/platform/mac/metal_atlas.rs +++ b/crates/gpui/src/platform/mac/metal_atlas.rs @@ -13,12 +13,14 @@ use std::borrow::Cow; pub(crate) struct MetalAtlas(Mutex<MetalAtlasState>); impl MetalAtlas { - pub(crate) fn new(device: Device) -> Self { + pub(crate) fn new(device: Device, path_sample_count: u32) -> Self { MetalAtlas(Mutex::new(MetalAtlasState { device: AssertSend(device), monochrome_textures: Default::default(), polychrome_textures: Default::default(), + path_textures: Default::default(), tiles_by_key: Default::default(), + path_sample_count, })) } @@ -26,7 +28,10 @@ impl MetalAtlas { self.0.lock().texture(id).metal_texture.clone() } - #[allow(dead_code)] + pub(crate) fn msaa_texture(&self, id: AtlasTextureId) -> Option<metal::Texture> { + self.0.lock().texture(id).msaa_texture.clone() + } + pub(crate) fn allocate( &self, size: Size<DevicePixels>, @@ -35,12 +40,12 @@ impl MetalAtlas { self.0.lock().allocate(size, texture_kind) } - #[allow(dead_code)] pub(crate) fn clear_textures(&self, texture_kind: AtlasTextureKind) { let mut lock = self.0.lock(); let textures = match texture_kind { AtlasTextureKind::Monochrome => &mut lock.monochrome_textures, AtlasTextureKind::Polychrome => &mut lock.polychrome_textures, + AtlasTextureKind::Path => &mut lock.path_textures, }; for texture in textures.iter_mut() { texture.clear(); @@ -52,7 +57,9 @@ struct MetalAtlasState { device: AssertSend<Device>, monochrome_textures: AtlasTextureList<MetalAtlasTexture>, polychrome_textures: AtlasTextureList<MetalAtlasTexture>, + path_textures: AtlasTextureList<MetalAtlasTexture>, tiles_by_key: FxHashMap<AtlasKey, AtlasTile>, + path_sample_count: u32, } impl PlatformAtlas for MetalAtlas { @@ -87,6 +94,7 @@ impl PlatformAtlas for MetalAtlas { let textures = match id.kind { AtlasTextureKind::Monochrome => &mut lock.monochrome_textures, AtlasTextureKind::Polychrome => &mut lock.polychrome_textures, + AtlasTextureKind::Path => &mut lock.polychrome_textures, }; let Some(texture_slot) = textures @@ -120,6 +128,7 @@ impl MetalAtlasState { let textures = match texture_kind { AtlasTextureKind::Monochrome => &mut self.monochrome_textures, AtlasTextureKind::Polychrome => &mut self.polychrome_textures, + AtlasTextureKind::Path => &mut self.path_textures, }; if let Some(tile) = textures @@ -164,14 +173,31 @@ impl MetalAtlasState { pixel_format = metal::MTLPixelFormat::BGRA8Unorm; usage = metal::MTLTextureUsage::ShaderRead; } + AtlasTextureKind::Path => { + pixel_format = metal::MTLPixelFormat::R16Float; + usage = metal::MTLTextureUsage::RenderTarget | metal::MTLTextureUsage::ShaderRead; + } } texture_descriptor.set_pixel_format(pixel_format); texture_descriptor.set_usage(usage); let metal_texture = self.device.new_texture(&texture_descriptor); + // We currently only enable MSAA for path textures. + let msaa_texture = if self.path_sample_count > 1 && kind == AtlasTextureKind::Path { + let mut descriptor = texture_descriptor.clone(); + descriptor.set_texture_type(metal::MTLTextureType::D2Multisample); + descriptor.set_storage_mode(metal::MTLStorageMode::Private); + descriptor.set_sample_count(self.path_sample_count as _); + let msaa_texture = self.device.new_texture(&descriptor); + Some(msaa_texture) + } else { + None + }; + let texture_list = match kind { AtlasTextureKind::Monochrome => &mut self.monochrome_textures, AtlasTextureKind::Polychrome => &mut self.polychrome_textures, + AtlasTextureKind::Path => &mut self.path_textures, }; let index = texture_list.free_list.pop(); @@ -183,6 +209,7 @@ impl MetalAtlasState { }, allocator: etagere::BucketedAtlasAllocator::new(size.into()), metal_texture: AssertSend(metal_texture), + msaa_texture: AssertSend(msaa_texture), live_atlas_keys: 0, }; @@ -199,6 +226,7 @@ impl MetalAtlasState { let textures = match id.kind { crate::AtlasTextureKind::Monochrome => &self.monochrome_textures, crate::AtlasTextureKind::Polychrome => &self.polychrome_textures, + crate::AtlasTextureKind::Path => &self.path_textures, }; textures[id.index as usize].as_ref().unwrap() } @@ -208,6 +236,7 @@ struct MetalAtlasTexture { id: AtlasTextureId, allocator: BucketedAtlasAllocator, metal_texture: AssertSend<metal::Texture>, + msaa_texture: AssertSend<Option<metal::Texture>>, live_atlas_keys: u32, } diff --git a/crates/gpui/src/platform/mac/metal_renderer.rs b/crates/gpui/src/platform/mac/metal_renderer.rs index 8936cf242cf3d997495d486d471a14285ae7caa0..3cdc2dd2cf42ea7c2a92152893679aa930466869 100644 --- a/crates/gpui/src/platform/mac/metal_renderer.rs +++ b/crates/gpui/src/platform/mac/metal_renderer.rs @@ -1,28 +1,27 @@ use super::metal_atlas::MetalAtlas; use crate::{ - AtlasTextureId, Background, Bounds, ContentMask, DevicePixels, MonochromeSprite, PaintSurface, - Path, PathVertex, PolychromeSprite, PrimitiveBatch, Quad, ScaledPixels, Scene, Shadow, Size, - Surface, Underline, point, size, + AtlasTextureId, AtlasTextureKind, AtlasTile, Background, Bounds, ContentMask, DevicePixels, + MonochromeSprite, PaintSurface, Path, PathId, PathVertex, PolychromeSprite, PrimitiveBatch, + Quad, ScaledPixels, Scene, Shadow, Size, Surface, Underline, point, size, }; -use anyhow::Result; +use anyhow::{Context as _, Result}; use block::ConcreteBlock; use cocoa::{ base::{NO, YES}, foundation::{NSSize, NSUInteger}, quartzcore::AutoresizingMask, }; +use collections::HashMap; use core_foundation::base::TCFType; use core_video::{ metal_texture::CVMetalTextureGetTexture, metal_texture_cache::CVMetalTextureCache, pixel_buffer::kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, }; use foreign_types::{ForeignType, ForeignTypeRef}; -use metal::{ - CAMetalLayer, CommandQueue, MTLDrawPrimitivesIndirectArguments, MTLPixelFormat, - MTLResourceOptions, NSRange, -}; +use metal::{CAMetalLayer, CommandQueue, MTLPixelFormat, MTLResourceOptions, NSRange}; use objc::{self, msg_send, sel, sel_impl}; use parking_lot::Mutex; +use smallvec::SmallVec; use std::{cell::Cell, ffi::c_void, mem, ptr, sync::Arc}; // Exported to metal @@ -32,6 +31,9 @@ pub(crate) type PointF = crate::Point<f32>; const SHADERS_METALLIB: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/shaders.metallib")); #[cfg(feature = "runtime_shaders")] const SHADERS_SOURCE_FILE: &str = include_str!(concat!(env!("OUT_DIR"), "/stitched_shaders.metal")); +// Use 4x MSAA, all devices support it. +// https://developer.apple.com/documentation/metal/mtldevice/1433355-supportstexturesamplecount +const PATH_SAMPLE_COUNT: u32 = 4; pub type Context = Arc<Mutex<InstanceBufferPool>>; pub type Renderer = MetalRenderer; @@ -96,7 +98,8 @@ pub(crate) struct MetalRenderer { layer: metal::MetalLayer, presents_with_transaction: bool, command_queue: CommandQueue, - path_pipeline_state: metal::RenderPipelineState, + paths_rasterization_pipeline_state: metal::RenderPipelineState, + path_sprites_pipeline_state: metal::RenderPipelineState, shadows_pipeline_state: metal::RenderPipelineState, quads_pipeline_state: metal::RenderPipelineState, underlines_pipeline_state: metal::RenderPipelineState, @@ -108,8 +111,6 @@ pub(crate) struct MetalRenderer { instance_buffer_pool: Arc<Mutex<InstanceBufferPool>>, sprite_atlas: Arc<MetalAtlas>, core_video_texture_cache: core_video::metal_texture_cache::CVMetalTextureCache, - sample_count: u64, - msaa_texture: Option<metal::Texture>, } impl MetalRenderer { @@ -168,19 +169,22 @@ impl MetalRenderer { MTLResourceOptions::StorageModeManaged, ); - let sample_count = [4, 2, 1] - .into_iter() - .find(|count| device.supports_texture_sample_count(*count)) - .unwrap_or(1); - - let path_pipeline_state = build_pipeline_state( + let paths_rasterization_pipeline_state = build_path_rasterization_pipeline_state( + &device, + &library, + "paths_rasterization", + "path_rasterization_vertex", + "path_rasterization_fragment", + MTLPixelFormat::R16Float, + PATH_SAMPLE_COUNT, + ); + let path_sprites_pipeline_state = build_pipeline_state( &device, &library, - "paths", - "path_vertex", - "path_fragment", + "path_sprites", + "path_sprite_vertex", + "path_sprite_fragment", MTLPixelFormat::BGRA8Unorm, - sample_count, ); let shadows_pipeline_state = build_pipeline_state( &device, @@ -189,7 +193,6 @@ impl MetalRenderer { "shadow_vertex", "shadow_fragment", MTLPixelFormat::BGRA8Unorm, - sample_count, ); let quads_pipeline_state = build_pipeline_state( &device, @@ -198,7 +201,6 @@ impl MetalRenderer { "quad_vertex", "quad_fragment", MTLPixelFormat::BGRA8Unorm, - sample_count, ); let underlines_pipeline_state = build_pipeline_state( &device, @@ -207,7 +209,6 @@ impl MetalRenderer { "underline_vertex", "underline_fragment", MTLPixelFormat::BGRA8Unorm, - sample_count, ); let monochrome_sprites_pipeline_state = build_pipeline_state( &device, @@ -216,7 +217,6 @@ impl MetalRenderer { "monochrome_sprite_vertex", "monochrome_sprite_fragment", MTLPixelFormat::BGRA8Unorm, - sample_count, ); let polychrome_sprites_pipeline_state = build_pipeline_state( &device, @@ -225,7 +225,6 @@ impl MetalRenderer { "polychrome_sprite_vertex", "polychrome_sprite_fragment", MTLPixelFormat::BGRA8Unorm, - sample_count, ); let surfaces_pipeline_state = build_pipeline_state( &device, @@ -234,21 +233,20 @@ impl MetalRenderer { "surface_vertex", "surface_fragment", MTLPixelFormat::BGRA8Unorm, - sample_count, ); let command_queue = device.new_command_queue(); - let sprite_atlas = Arc::new(MetalAtlas::new(device.clone())); + let sprite_atlas = Arc::new(MetalAtlas::new(device.clone(), PATH_SAMPLE_COUNT)); let core_video_texture_cache = CVMetalTextureCache::new(None, device.clone(), None).unwrap(); - let msaa_texture = create_msaa_texture(&device, &layer, sample_count); Self { device, layer, presents_with_transaction: false, command_queue, - path_pipeline_state, + paths_rasterization_pipeline_state, + path_sprites_pipeline_state, shadows_pipeline_state, quads_pipeline_state, underlines_pipeline_state, @@ -259,8 +257,6 @@ impl MetalRenderer { instance_buffer_pool, sprite_atlas, core_video_texture_cache, - sample_count, - msaa_texture, } } @@ -293,8 +289,6 @@ impl MetalRenderer { setDrawableSize: size ]; } - - self.msaa_texture = create_msaa_texture(&self.device, &self.layer, self.sample_count); } pub fn update_transparency(&self, _transparent: bool) { @@ -381,23 +375,25 @@ impl MetalRenderer { let command_queue = self.command_queue.clone(); let command_buffer = command_queue.new_command_buffer(); let mut instance_offset = 0; + + let path_tiles = self + .rasterize_paths( + scene.paths(), + instance_buffer, + &mut instance_offset, + command_buffer, + ) + .with_context(|| format!("rasterizing {} paths", scene.paths().len()))?; + let render_pass_descriptor = metal::RenderPassDescriptor::new(); let color_attachment = render_pass_descriptor .color_attachments() .object_at(0) .unwrap(); - if let Some(msaa_texture_ref) = self.msaa_texture.as_deref() { - color_attachment.set_texture(Some(msaa_texture_ref)); - color_attachment.set_load_action(metal::MTLLoadAction::Clear); - color_attachment.set_store_action(metal::MTLStoreAction::MultisampleResolve); - color_attachment.set_resolve_texture(Some(drawable.texture())); - } else { - color_attachment.set_load_action(metal::MTLLoadAction::Clear); - color_attachment.set_texture(Some(drawable.texture())); - color_attachment.set_store_action(metal::MTLStoreAction::Store); - } - + color_attachment.set_texture(Some(drawable.texture())); + color_attachment.set_load_action(metal::MTLLoadAction::Clear); + color_attachment.set_store_action(metal::MTLStoreAction::Store); let alpha = if self.layer.is_opaque() { 1. } else { 0. }; color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., alpha)); let command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor); @@ -429,6 +425,7 @@ impl MetalRenderer { ), PrimitiveBatch::Paths(paths) => self.draw_paths( paths, + &path_tiles, instance_buffer, &mut instance_offset, viewport_size, @@ -496,6 +493,106 @@ impl MetalRenderer { Ok(command_buffer.to_owned()) } + fn rasterize_paths( + &self, + paths: &[Path<ScaledPixels>], + instance_buffer: &mut InstanceBuffer, + instance_offset: &mut usize, + command_buffer: &metal::CommandBufferRef, + ) -> Option<HashMap<PathId, AtlasTile>> { + self.sprite_atlas.clear_textures(AtlasTextureKind::Path); + + let mut tiles = HashMap::default(); + let mut vertices_by_texture_id = HashMap::default(); + for path in paths { + let clipped_bounds = path.bounds.intersect(&path.content_mask.bounds); + + let tile = self + .sprite_atlas + .allocate(clipped_bounds.size.map(Into::into), AtlasTextureKind::Path)?; + vertices_by_texture_id + .entry(tile.texture_id) + .or_insert(Vec::new()) + .extend(path.vertices.iter().map(|vertex| PathVertex { + xy_position: vertex.xy_position - clipped_bounds.origin + + tile.bounds.origin.map(Into::into), + st_position: vertex.st_position, + content_mask: ContentMask { + bounds: tile.bounds.map(Into::into), + }, + })); + tiles.insert(path.id, tile); + } + + for (texture_id, vertices) in vertices_by_texture_id { + align_offset(instance_offset); + let vertices_bytes_len = mem::size_of_val(vertices.as_slice()); + let next_offset = *instance_offset + vertices_bytes_len; + if next_offset > instance_buffer.size { + return None; + } + + let render_pass_descriptor = metal::RenderPassDescriptor::new(); + let color_attachment = render_pass_descriptor + .color_attachments() + .object_at(0) + .unwrap(); + + let texture = self.sprite_atlas.metal_texture(texture_id); + let msaa_texture = self.sprite_atlas.msaa_texture(texture_id); + + if let Some(msaa_texture) = msaa_texture { + color_attachment.set_texture(Some(&msaa_texture)); + color_attachment.set_resolve_texture(Some(&texture)); + color_attachment.set_load_action(metal::MTLLoadAction::Clear); + color_attachment.set_store_action(metal::MTLStoreAction::MultisampleResolve); + } else { + color_attachment.set_texture(Some(&texture)); + color_attachment.set_load_action(metal::MTLLoadAction::Clear); + color_attachment.set_store_action(metal::MTLStoreAction::Store); + } + color_attachment.set_clear_color(metal::MTLClearColor::new(0., 0., 0., 1.)); + + let command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor); + command_encoder.set_render_pipeline_state(&self.paths_rasterization_pipeline_state); + command_encoder.set_vertex_buffer( + PathRasterizationInputIndex::Vertices as u64, + Some(&instance_buffer.metal_buffer), + *instance_offset as u64, + ); + let texture_size = Size { + width: DevicePixels::from(texture.width()), + height: DevicePixels::from(texture.height()), + }; + command_encoder.set_vertex_bytes( + PathRasterizationInputIndex::AtlasTextureSize as u64, + mem::size_of_val(&texture_size) as u64, + &texture_size as *const Size<DevicePixels> as *const _, + ); + + let buffer_contents = unsafe { + (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) + }; + unsafe { + ptr::copy_nonoverlapping( + vertices.as_ptr() as *const u8, + buffer_contents, + vertices_bytes_len, + ); + } + + command_encoder.draw_primitives( + metal::MTLPrimitiveType::Triangle, + 0, + vertices.len() as u64, + ); + command_encoder.end_encoding(); + *instance_offset = next_offset; + } + + Some(tiles) + } + fn draw_shadows( &self, shadows: &[Shadow], @@ -621,6 +718,7 @@ impl MetalRenderer { fn draw_paths( &self, paths: &[Path<ScaledPixels>], + tiles_by_path_id: &HashMap<PathId, AtlasTile>, instance_buffer: &mut InstanceBuffer, instance_offset: &mut usize, viewport_size: Size<DevicePixels>, @@ -630,108 +728,100 @@ impl MetalRenderer { return true; } - command_encoder.set_render_pipeline_state(&self.path_pipeline_state); + command_encoder.set_render_pipeline_state(&self.path_sprites_pipeline_state); + command_encoder.set_vertex_buffer( + SpriteInputIndex::Vertices as u64, + Some(&self.unit_vertices), + 0, + ); + command_encoder.set_vertex_bytes( + SpriteInputIndex::ViewportSize as u64, + mem::size_of_val(&viewport_size) as u64, + &viewport_size as *const Size<DevicePixels> as *const _, + ); - unsafe { - let base_addr = instance_buffer.metal_buffer.contents(); - let mut p = (base_addr as *mut u8).add(*instance_offset); - let mut draw_indirect_commands = Vec::with_capacity(paths.len()); - - // copy vertices - let vertices_offset = (p as usize) - (base_addr as usize); - let mut first_vertex = 0; - for (i, path) in paths.iter().enumerate() { - if (p as usize) - (base_addr as usize) - + (mem::size_of::<PathVertex<ScaledPixels>>() * path.vertices.len()) - > instance_buffer.size - { - return false; - } + let mut prev_texture_id = None; + let mut sprites = SmallVec::<[_; 1]>::new(); + let mut paths_and_tiles = paths + .iter() + .map(|path| (path, tiles_by_path_id.get(&path.id).unwrap())) + .peekable(); - for v in &path.vertices { - *(p as *mut PathVertex<ScaledPixels>) = PathVertex { - xy_position: v.xy_position, - content_mask: ContentMask { - bounds: path.content_mask.bounds, + loop { + if let Some((path, tile)) = paths_and_tiles.peek() { + if prev_texture_id.map_or(true, |texture_id| texture_id == tile.texture_id) { + prev_texture_id = Some(tile.texture_id); + let origin = path.bounds.intersect(&path.content_mask.bounds).origin; + sprites.push(PathSprite { + bounds: Bounds { + origin: origin.map(|p| p.floor()), + size: tile.bounds.size.map(Into::into), }, - }; - p = p.add(mem::size_of::<PathVertex<ScaledPixels>>()); + color: path.color, + tile: (*tile).clone(), + }); + paths_and_tiles.next(); + continue; } - - draw_indirect_commands.push(MTLDrawPrimitivesIndirectArguments { - vertexCount: path.vertices.len() as u32, - instanceCount: 1, - vertexStart: first_vertex, - baseInstance: i as u32, - }); - first_vertex += path.vertices.len() as u32; } - // copy sprites - let sprites_offset = (p as u64) - (base_addr as u64); - if (p as usize) - (base_addr as usize) + (mem::size_of::<PathSprite>() * paths.len()) - > instance_buffer.size - { - return false; - } - for path in paths { - *(p as *mut PathSprite) = PathSprite { - bounds: path.bounds, - color: path.color, - }; - p = p.add(mem::size_of::<PathSprite>()); - } - - // copy indirect commands - let icb_bytes_len = mem::size_of_val(draw_indirect_commands.as_slice()); - let icb_offset = (p as u64) - (base_addr as u64); - if (p as usize) - (base_addr as usize) + icb_bytes_len > instance_buffer.size { - return false; - } - ptr::copy_nonoverlapping( - draw_indirect_commands.as_ptr() as *const u8, - p, - icb_bytes_len, - ); - p = p.add(icb_bytes_len); + if sprites.is_empty() { + break; + } else { + align_offset(instance_offset); + let texture_id = prev_texture_id.take().unwrap(); + let texture: metal::Texture = self.sprite_atlas.metal_texture(texture_id); + let texture_size = size( + DevicePixels(texture.width() as i32), + DevicePixels(texture.height() as i32), + ); - // draw path - command_encoder.set_vertex_buffer( - PathInputIndex::Vertices as u64, - Some(&instance_buffer.metal_buffer), - vertices_offset as u64, - ); + command_encoder.set_vertex_buffer( + SpriteInputIndex::Sprites as u64, + Some(&instance_buffer.metal_buffer), + *instance_offset as u64, + ); + command_encoder.set_vertex_bytes( + SpriteInputIndex::AtlasTextureSize as u64, + mem::size_of_val(&texture_size) as u64, + &texture_size as *const Size<DevicePixels> as *const _, + ); + command_encoder.set_fragment_buffer( + SpriteInputIndex::Sprites as u64, + Some(&instance_buffer.metal_buffer), + *instance_offset as u64, + ); + command_encoder + .set_fragment_texture(SpriteInputIndex::AtlasTexture as u64, Some(&texture)); - command_encoder.set_vertex_bytes( - PathInputIndex::ViewportSize as u64, - mem::size_of_val(&viewport_size) as u64, - &viewport_size as *const Size<DevicePixels> as *const _, - ); + let sprite_bytes_len = mem::size_of_val(sprites.as_slice()); + let next_offset = *instance_offset + sprite_bytes_len; + if next_offset > instance_buffer.size { + return false; + } - command_encoder.set_vertex_buffer( - PathInputIndex::Sprites as u64, - Some(&instance_buffer.metal_buffer), - sprites_offset, - ); + let buffer_contents = unsafe { + (instance_buffer.metal_buffer.contents() as *mut u8).add(*instance_offset) + }; - command_encoder.set_fragment_buffer( - PathInputIndex::Sprites as u64, - Some(&instance_buffer.metal_buffer), - sprites_offset, - ); + unsafe { + ptr::copy_nonoverlapping( + sprites.as_ptr() as *const u8, + buffer_contents, + sprite_bytes_len, + ); + } - for i in 0..paths.len() { - command_encoder.draw_primitives_indirect( + command_encoder.draw_primitives_instanced( metal::MTLPrimitiveType::Triangle, - &instance_buffer.metal_buffer, - icb_offset - + (i * std::mem::size_of::<MTLDrawPrimitivesIndirectArguments>()) as u64, + 0, + 6, + sprites.len() as u64, ); + *instance_offset = next_offset; + sprites.clear(); } - - *instance_offset = (p as usize) - (base_addr as usize); } - true } @@ -1053,7 +1143,6 @@ fn build_pipeline_state( vertex_fn_name: &str, fragment_fn_name: &str, pixel_format: metal::MTLPixelFormat, - sample_count: u64, ) -> metal::RenderPipelineState { let vertex_fn = library .get_function(vertex_fn_name, None) @@ -1066,7 +1155,6 @@ fn build_pipeline_state( descriptor.set_label(label); descriptor.set_vertex_function(Some(vertex_fn.as_ref())); descriptor.set_fragment_function(Some(fragment_fn.as_ref())); - descriptor.set_sample_count(sample_count); let color_attachment = descriptor.color_attachments().object_at(0).unwrap(); color_attachment.set_pixel_format(pixel_format); color_attachment.set_blending_enabled(true); @@ -1082,43 +1170,48 @@ fn build_pipeline_state( .expect("could not create render pipeline state") } -// Align to multiples of 256 make Metal happy. -fn align_offset(offset: &mut usize) { - *offset = (*offset).div_ceil(256) * 256; -} - -fn create_msaa_texture( - device: &metal::Device, - layer: &metal::MetalLayer, - sample_count: u64, -) -> Option<metal::Texture> { - let viewport_size = layer.drawable_size(); - let width = viewport_size.width.ceil() as u64; - let height = viewport_size.height.ceil() as u64; - - if width == 0 || height == 0 { - return None; - } +fn build_path_rasterization_pipeline_state( + device: &metal::DeviceRef, + library: &metal::LibraryRef, + label: &str, + vertex_fn_name: &str, + fragment_fn_name: &str, + pixel_format: metal::MTLPixelFormat, + path_sample_count: u32, +) -> metal::RenderPipelineState { + let vertex_fn = library + .get_function(vertex_fn_name, None) + .expect("error locating vertex function"); + let fragment_fn = library + .get_function(fragment_fn_name, None) + .expect("error locating fragment function"); - if sample_count <= 1 { - return None; + let descriptor = metal::RenderPipelineDescriptor::new(); + descriptor.set_label(label); + descriptor.set_vertex_function(Some(vertex_fn.as_ref())); + descriptor.set_fragment_function(Some(fragment_fn.as_ref())); + if path_sample_count > 1 { + descriptor.set_raster_sample_count(path_sample_count as _); + descriptor.set_alpha_to_coverage_enabled(true); } + let color_attachment = descriptor.color_attachments().object_at(0).unwrap(); + color_attachment.set_pixel_format(pixel_format); + color_attachment.set_blending_enabled(true); + color_attachment.set_rgb_blend_operation(metal::MTLBlendOperation::Add); + color_attachment.set_alpha_blend_operation(metal::MTLBlendOperation::Add); + color_attachment.set_source_rgb_blend_factor(metal::MTLBlendFactor::One); + color_attachment.set_source_alpha_blend_factor(metal::MTLBlendFactor::One); + color_attachment.set_destination_rgb_blend_factor(metal::MTLBlendFactor::One); + color_attachment.set_destination_alpha_blend_factor(metal::MTLBlendFactor::One); - let texture_descriptor = metal::TextureDescriptor::new(); - texture_descriptor.set_texture_type(metal::MTLTextureType::D2Multisample); - - // MTLStorageMode default is `shared` only for Apple silicon GPUs. Use `private` for Apple and Intel GPUs both. - // Reference: https://developer.apple.com/documentation/metal/choosing-a-resource-storage-mode-for-apple-gpus - texture_descriptor.set_storage_mode(metal::MTLStorageMode::Private); - - texture_descriptor.set_width(width); - texture_descriptor.set_height(height); - texture_descriptor.set_pixel_format(layer.pixel_format()); - texture_descriptor.set_usage(metal::MTLTextureUsage::RenderTarget); - texture_descriptor.set_sample_count(sample_count); + device + .new_render_pipeline_state(&descriptor) + .expect("could not create render pipeline state") +} - let metal_texture = device.new_texture(&texture_descriptor); - Some(metal_texture) +// Align to multiples of 256 make Metal happy. +fn align_offset(offset: &mut usize) { + *offset = (*offset).div_ceil(256) * 256; } #[repr(C)] @@ -1162,10 +1255,9 @@ enum SurfaceInputIndex { } #[repr(C)] -enum PathInputIndex { +enum PathRasterizationInputIndex { Vertices = 0, - ViewportSize = 1, - Sprites = 2, + AtlasTextureSize = 1, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -1173,6 +1265,7 @@ enum PathInputIndex { pub struct PathSprite { pub bounds: Bounds<ScaledPixels>, pub color: Background, + pub tile: AtlasTile, } #[derive(Clone, Debug, Eq, PartialEq)] diff --git a/crates/gpui/src/platform/mac/shaders.metal b/crates/gpui/src/platform/mac/shaders.metal index 5f0dc3323d4b4cec77a8c25fc9b008ea9da0a578..64ebb1e22b3b2645f61af308dd832a80ef4eda52 100644 --- a/crates/gpui/src/platform/mac/shaders.metal +++ b/crates/gpui/src/platform/mac/shaders.metal @@ -698,27 +698,76 @@ fragment float4 polychrome_sprite_fragment( return color; } -struct PathVertexOutput { +struct PathRasterizationVertexOutput { float4 position [[position]]; + float2 st_position; + float clip_rect_distance [[clip_distance]][4]; +}; + +struct PathRasterizationFragmentInput { + float4 position [[position]]; + float2 st_position; +}; + +vertex PathRasterizationVertexOutput path_rasterization_vertex( + uint vertex_id [[vertex_id]], + constant PathVertex_ScaledPixels *vertices + [[buffer(PathRasterizationInputIndex_Vertices)]], + constant Size_DevicePixels *atlas_size + [[buffer(PathRasterizationInputIndex_AtlasTextureSize)]]) { + PathVertex_ScaledPixels v = vertices[vertex_id]; + float2 vertex_position = float2(v.xy_position.x, v.xy_position.y); + float2 viewport_size = float2(atlas_size->width, atlas_size->height); + return PathRasterizationVertexOutput{ + float4(vertex_position / viewport_size * float2(2., -2.) + + float2(-1., 1.), + 0., 1.), + float2(v.st_position.x, v.st_position.y), + {v.xy_position.x - v.content_mask.bounds.origin.x, + v.content_mask.bounds.origin.x + v.content_mask.bounds.size.width - + v.xy_position.x, + v.xy_position.y - v.content_mask.bounds.origin.y, + v.content_mask.bounds.origin.y + v.content_mask.bounds.size.height - + v.xy_position.y}}; +} + +fragment float4 path_rasterization_fragment(PathRasterizationFragmentInput input + [[stage_in]]) { + float2 dx = dfdx(input.st_position); + float2 dy = dfdy(input.st_position); + float2 gradient = float2((2. * input.st_position.x) * dx.x - dx.y, + (2. * input.st_position.x) * dy.x - dy.y); + float f = (input.st_position.x * input.st_position.x) - input.st_position.y; + float distance = f / length(gradient); + float alpha = saturate(0.5 - distance); + return float4(alpha, 0., 0., 1.); +} + +struct PathSpriteVertexOutput { + float4 position [[position]]; + float2 tile_position; uint sprite_id [[flat]]; float4 solid_color [[flat]]; float4 color0 [[flat]]; float4 color1 [[flat]]; - float4 clip_distance; }; -vertex PathVertexOutput path_vertex( - uint vertex_id [[vertex_id]], - constant PathVertex_ScaledPixels *vertices [[buffer(PathInputIndex_Vertices)]], - uint sprite_id [[instance_id]], - constant PathSprite *sprites [[buffer(PathInputIndex_Sprites)]], - constant Size_DevicePixels *input_viewport_size [[buffer(PathInputIndex_ViewportSize)]]) { - PathVertex_ScaledPixels v = vertices[vertex_id]; - float2 vertex_position = float2(v.xy_position.x, v.xy_position.y); - float2 viewport_size = float2((float)input_viewport_size->width, - (float)input_viewport_size->height); +vertex PathSpriteVertexOutput path_sprite_vertex( + uint unit_vertex_id [[vertex_id]], uint sprite_id [[instance_id]], + constant float2 *unit_vertices [[buffer(SpriteInputIndex_Vertices)]], + constant PathSprite *sprites [[buffer(SpriteInputIndex_Sprites)]], + constant Size_DevicePixels *viewport_size + [[buffer(SpriteInputIndex_ViewportSize)]], + constant Size_DevicePixels *atlas_size + [[buffer(SpriteInputIndex_AtlasTextureSize)]]) { + + float2 unit_vertex = unit_vertices[unit_vertex_id]; PathSprite sprite = sprites[sprite_id]; - float4 device_position = float4(vertex_position / viewport_size * float2(2., -2.) + float2(-1., 1.), 0., 1.); + // Don't apply content mask because it was already accounted for when + // rasterizing the path. + float4 device_position = + to_device_position(unit_vertex, sprite.bounds, viewport_size); + float2 tile_position = to_tile_position(unit_vertex, sprite.tile, atlas_size); GradientColor gradient = prepare_fill_color( sprite.color.tag, @@ -728,32 +777,30 @@ vertex PathVertexOutput path_vertex( sprite.color.colors[1].color ); - return PathVertexOutput{ + return PathSpriteVertexOutput{ device_position, + tile_position, sprite_id, gradient.solid, gradient.color0, - gradient.color1, - {v.xy_position.x - v.content_mask.bounds.origin.x, - v.content_mask.bounds.origin.x + v.content_mask.bounds.size.width - - v.xy_position.x, - v.xy_position.y - v.content_mask.bounds.origin.y, - v.content_mask.bounds.origin.y + v.content_mask.bounds.size.height - - v.xy_position.y} + gradient.color1 }; } -fragment float4 path_fragment( - PathVertexOutput input [[stage_in]], - constant PathSprite *sprites [[buffer(PathInputIndex_Sprites)]]) { - if (any(input.clip_distance < float4(0.0))) { - return float4(0.0); - } - +fragment float4 path_sprite_fragment( + PathSpriteVertexOutput input [[stage_in]], + constant PathSprite *sprites [[buffer(SpriteInputIndex_Sprites)]], + texture2d<float> atlas_texture [[texture(SpriteInputIndex_AtlasTexture)]]) { + constexpr sampler atlas_texture_sampler(mag_filter::linear, + min_filter::linear); + float4 sample = + atlas_texture.sample(atlas_texture_sampler, input.tile_position); + float mask = 1. - abs(1. - fmod(sample.r, 2.)); PathSprite sprite = sprites[input.sprite_id]; Background background = sprite.color; float4 color = fill_color(background, input.position.xy, sprite.bounds, input.solid_color, input.color0, input.color1); + color.a *= mask; return color; } diff --git a/crates/gpui/src/platform/test/window.rs b/crates/gpui/src/platform/test/window.rs index 65ee10a13ffa8a73f377ae4ac4e5f7a4381519ec..1b88415d3b6f57f90643a54742f5312e9fa2ec97 100644 --- a/crates/gpui/src/platform/test/window.rs +++ b/crates/gpui/src/platform/test/window.rs @@ -341,7 +341,7 @@ impl PlatformAtlas for TestAtlas { crate::AtlasTile { texture_id: AtlasTextureId { index: texture_id, - kind: crate::AtlasTextureKind::Polychrome, + kind: crate::AtlasTextureKind::Path, }, tile_id: TileId(tile_id), padding: 0, diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index 681444a4737867bb78ac8081958a4cf1af4f6771..4eaef64afa1d0d888d93dceca07569136edb0d8e 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use crate::{ AtlasTextureId, AtlasTile, Background, Bounds, ContentMask, Corners, Edges, Hsla, Pixels, - Point, Radians, ScaledPixels, Size, bounds_tree::BoundsTree, + Point, Radians, ScaledPixels, Size, bounds_tree::BoundsTree, point, }; use std::{fmt::Debug, iter::Peekable, ops::Range, slice}; @@ -43,7 +43,13 @@ impl Scene { self.surfaces.clear(); } - #[allow(dead_code)] + #[cfg_attr( + all( + any(target_os = "linux", target_os = "freebsd"), + not(any(feature = "x11", feature = "wayland")) + ), + allow(dead_code) + )] pub fn paths(&self) -> &[Path<ScaledPixels>] { &self.paths } @@ -683,7 +689,6 @@ pub struct Path<P: Clone + Debug + Default + PartialEq> { start: Point<P>, current: Point<P>, contour_count: usize, - base_scale: f32, } impl Path<Pixels> { @@ -702,35 +707,25 @@ impl Path<Pixels> { content_mask: Default::default(), color: Default::default(), contour_count: 0, - base_scale: 1.0, } } - /// Set the base scale of the path. - pub fn scale(mut self, factor: f32) -> Self { - self.base_scale = factor; - self - } - - /// Apply a scale to the path. - pub(crate) fn apply_scale(&self, factor: f32) -> Path<ScaledPixels> { + /// Scale this path by the given factor. + pub fn scale(&self, factor: f32) -> Path<ScaledPixels> { Path { id: self.id, order: self.order, - bounds: self.bounds.scale(self.base_scale * factor), - content_mask: self.content_mask.scale(self.base_scale * factor), + bounds: self.bounds.scale(factor), + content_mask: self.content_mask.scale(factor), vertices: self .vertices .iter() - .map(|vertex| vertex.scale(self.base_scale * factor)) + .map(|vertex| vertex.scale(factor)) .collect(), - start: self - .start - .map(|start| start.scale(self.base_scale * factor)), - current: self.current.scale(self.base_scale * factor), + start: self.start.map(|start| start.scale(factor)), + current: self.current.scale(factor), contour_count: self.contour_count, color: self.color, - base_scale: 1.0, } } @@ -745,7 +740,10 @@ impl Path<Pixels> { pub fn line_to(&mut self, to: Point<Pixels>) { self.contour_count += 1; if self.contour_count > 1 { - self.push_triangle((self.start, self.current, to)); + self.push_triangle( + (self.start, self.current, to), + (point(0., 1.), point(0., 1.), point(0., 1.)), + ); } self.current = to; } @@ -754,15 +752,25 @@ impl Path<Pixels> { pub fn curve_to(&mut self, to: Point<Pixels>, ctrl: Point<Pixels>) { self.contour_count += 1; if self.contour_count > 1 { - self.push_triangle((self.start, self.current, to)); + self.push_triangle( + (self.start, self.current, to), + (point(0., 1.), point(0., 1.), point(0., 1.)), + ); } - self.push_triangle((self.current, ctrl, to)); + self.push_triangle( + (self.current, ctrl, to), + (point(0., 0.), point(0.5, 0.), point(1., 1.)), + ); self.current = to; } /// Push a triangle to the Path. - pub fn push_triangle(&mut self, xy: (Point<Pixels>, Point<Pixels>, Point<Pixels>)) { + pub fn push_triangle( + &mut self, + xy: (Point<Pixels>, Point<Pixels>, Point<Pixels>), + st: (Point<f32>, Point<f32>, Point<f32>), + ) { self.bounds = self .bounds .union(&Bounds { @@ -780,14 +788,17 @@ impl Path<Pixels> { self.vertices.push(PathVertex { xy_position: xy.0, + st_position: st.0, content_mask: Default::default(), }); self.vertices.push(PathVertex { xy_position: xy.1, + st_position: st.1, content_mask: Default::default(), }); self.vertices.push(PathVertex { xy_position: xy.2, + st_position: st.2, content_mask: Default::default(), }); } @@ -803,6 +814,7 @@ impl From<Path<ScaledPixels>> for Primitive { #[repr(C)] pub(crate) struct PathVertex<P: Clone + Debug + Default + PartialEq> { pub(crate) xy_position: Point<P>, + pub(crate) st_position: Point<f32>, pub(crate) content_mask: ContentMask<P>, } @@ -810,6 +822,7 @@ impl PathVertex<Pixels> { pub fn scale(&self, factor: f32) -> PathVertex<ScaledPixels> { PathVertex { xy_position: self.xy_position.scale(factor), + st_position: self.st_position, content_mask: self.content_mask.scale(factor), } } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index e9145bd9f5181da662a882f0f12e340e34d4822f..94f1b39ba20982cefd50ad149fbc50b40bc80cbf 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -2658,7 +2658,7 @@ impl Window { path.color = color.opacity(opacity); self.next_frame .scene - .insert_primitive(path.apply_scale(scale_factor)); + .insert_primitive(path.scale(scale_factor)); } /// Paint an underline into the scene for the next frame at the current z-index. diff --git a/docs/src/linux.md b/docs/src/linux.md index 896bfdaf3ff9e525896576717f215df7e54ba0de..ca65da29695c71659650edad8fde60523b4fd029 100644 --- a/docs/src/linux.md +++ b/docs/src/linux.md @@ -148,7 +148,7 @@ On some systems the file `/etc/prime-discrete` can be used to enforce the use of On others, you may be able to the environment variable `DRI_PRIME=1` when running Zed to force the use of the discrete GPU. -If you're using an AMD GPU and Zed crashes when selecting long lines, try setting the `ZED_SAMPLE_COUNT=0` environment variable. (See [#26143](https://github.com/zed-industries/zed/issues/26143)) +If you're using an AMD GPU and Zed crashes when selecting long lines, try setting the `ZED_PATH_SAMPLE_COUNT=0` environment variable. (See [#26143](https://github.com/zed-industries/zed/issues/26143)) If you're using an AMD GPU, you might get a 'Broken Pipe' error. Try using the RADV or Mesa drivers. (See [#13880](https://github.com/zed-industries/zed/issues/13880)) From 44768606641ba3c9d40de8b38d96569d0ed69661 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Fri, 18 Jul 2025 13:25:36 -0300 Subject: [PATCH 191/658] Add refinements to the AI onboarding flow (#33738) This includes making sure that both the agent panel and Zed's edit prediction have a consistent narrative when it comes to onboarding users into the AI features, considering the possible different plans and conditions (such as being signed in/out, account age, etc.) Release Notes: - N/A --------- Co-authored-by: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com> Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de> --- Cargo.lock | 22 +- Cargo.toml | 2 + crates/agent_ui/Cargo.toml | 1 + crates/agent_ui/src/agent_model_selector.rs | 21 +- crates/agent_ui/src/agent_panel.rs | 729 ++++++------------ crates/agent_ui/src/inline_prompt_editor.rs | 2 +- .../agent_ui/src/language_model_selector.rs | 13 +- crates/agent_ui/src/message_editor.rs | 11 +- crates/agent_ui/src/text_thread_editor.rs | 144 +--- crates/agent_ui/src/ui.rs | 2 + crates/agent_ui/src/ui/end_trial_upsell.rs | 112 +++ crates/ai_onboarding/Cargo.toml | 27 + crates/ai_onboarding/LICENSE-GPL | 1 + .../src/agent_panel_onboarding_card.rs | 81 ++ .../src/agent_panel_onboarding_content.rs | 145 ++++ crates/ai_onboarding/src/ai_onboarding.rs | 397 ++++++++++ .../src/edit_prediction_onboarding_content.rs | 73 ++ .../ai_onboarding/src/young_account_banner.rs | 21 + crates/client/src/client.rs | 7 + crates/client/src/user.rs | 10 + crates/client/src/zed_urls.rs | 5 + crates/copilot/src/copilot.rs | 10 +- .../src/inline_completion_button.rs | 93 +-- crates/language_model/src/language_model.rs | 2 +- crates/language_models/Cargo.toml | 1 + crates/language_models/src/provider/cloud.rs | 136 ++-- crates/title_bar/src/title_bar.rs | 9 +- .../ui/src/components/button/button_like.rs | 42 + crates/zed_actions/src/lib.rs | 5 +- crates/zeta/Cargo.toml | 4 +- crates/zeta/src/init.rs | 1 - crates/zeta/src/onboarding_modal.rs | 499 ++---------- crates/zeta/src/zeta.rs | 34 +- 33 files changed, 1456 insertions(+), 1206 deletions(-) create mode 100644 crates/agent_ui/src/ui/end_trial_upsell.rs create mode 100644 crates/ai_onboarding/Cargo.toml create mode 120000 crates/ai_onboarding/LICENSE-GPL create mode 100644 crates/ai_onboarding/src/agent_panel_onboarding_card.rs create mode 100644 crates/ai_onboarding/src/agent_panel_onboarding_content.rs create mode 100644 crates/ai_onboarding/src/ai_onboarding.rs create mode 100644 crates/ai_onboarding/src/edit_prediction_onboarding_content.rs create mode 100644 crates/ai_onboarding/src/young_account_banner.rs diff --git a/Cargo.lock b/Cargo.lock index 8bf26543705042204358c61e6844aefabb8ede04..cbed9f5988b1ec96308f89c3c37309bab0b13bb1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -195,6 +195,7 @@ dependencies = [ "agent_servers", "agent_settings", "agentic-coding-protocol", + "ai_onboarding", "anyhow", "assistant_context", "assistant_slash_command", @@ -329,6 +330,22 @@ dependencies = [ "memchr", ] +[[package]] +name = "ai_onboarding" +version = "0.1.0" +dependencies = [ + "client", + "component", + "gpui", + "language_model", + "proto", + "serde", + "smallvec", + "ui", + "workspace-hack", + "zed_actions", +] + [[package]] name = "alacritty_terminal" version = "0.25.1-dev" @@ -9066,6 +9083,7 @@ dependencies = [ name = "language_models" version = "0.1.0" dependencies = [ + "ai_onboarding", "anthropic", "anyhow", "aws-config", @@ -20510,6 +20528,7 @@ dependencies = [ name = "zeta" version = "0.1.0" dependencies = [ + "ai_onboarding", "anyhow", "arrayvec", "call", @@ -20517,6 +20536,7 @@ dependencies = [ "clock", "collections", "command_palette_hooks", + "copilot", "ctor", "db", "editor", @@ -20531,8 +20551,6 @@ dependencies = [ "language_model", "log", "menu", - "migrator", - "paths", "postage", "project", "proto", diff --git a/Cargo.toml b/Cargo.toml index 8d942a4c73c5ae1306e4e81a6a064759b0f1c782..aa9af9a423eb0d283df821a46424a4702154bce5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "crates/agent_ui", "crates/agent", "crates/agent_settings", + "crates/ai_onboarding", "crates/agent_servers", "crates/anthropic", "crates/askpass", @@ -227,6 +228,7 @@ agent_ui = { path = "crates/agent_ui" } agent_settings = { path = "crates/agent_settings" } agent_servers = { path = "crates/agent_servers" } ai = { path = "crates/ai" } +ai_onboarding = { path = "crates/ai_onboarding" } anthropic = { path = "crates/anthropic" } askpass = { path = "crates/askpass" } assets = { path = "crates/assets" } diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index d4feceb0b67628052887e805d9b04eeb11c40040..e55ae86fb726f6aeeacb822b62720365d64514b1 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -21,6 +21,7 @@ agent.workspace = true agentic-coding-protocol.workspace = true agent_settings.workspace = true agent_servers.workspace = true +ai_onboarding.workspace = true anyhow.workspace = true assistant_context.workspace = true assistant_slash_command.workspace = true diff --git a/crates/agent_ui/src/agent_model_selector.rs b/crates/agent_ui/src/agent_model_selector.rs index f7b9157bbb9c07abac6a80dddfc014443165a712..b989e7bf1e9147c7f6beb90b5054120cef7b818f 100644 --- a/crates/agent_ui/src/agent_model_selector.rs +++ b/crates/agent_ui/src/agent_model_selector.rs @@ -1,8 +1,6 @@ use crate::{ ModelUsageContext, - language_model_selector::{ - LanguageModelSelector, ToggleModelSelector, language_model_selector, - }, + language_model_selector::{LanguageModelSelector, language_model_selector}, }; use agent_settings::AgentSettings; use fs::Fs; @@ -12,6 +10,7 @@ use picker::popover_menu::PickerPopoverMenu; use settings::update_settings_file; use std::sync::Arc; use ui::{ButtonLike, PopoverMenuHandle, Tooltip, prelude::*}; +use zed_actions::agent::ToggleModelSelector; pub struct AgentModelSelector { selector: Entity<LanguageModelSelector>, @@ -96,22 +95,18 @@ impl Render for AgentModelSelector { let model_name = model .as_ref() .map(|model| model.model.name().0) - .unwrap_or_else(|| SharedString::from("No model selected")); - let provider_icon = model - .as_ref() - .map(|model| model.provider.icon()) - .unwrap_or_else(|| IconName::Ai); + .unwrap_or_else(|| SharedString::from("Select a Model")); + + let provider_icon = model.as_ref().map(|model| model.provider.icon()); let focus_handle = self.focus_handle.clone(); PickerPopoverMenu::new( self.selector.clone(), ButtonLike::new("active-model") - .child( - Icon::new(provider_icon) - .color(Color::Muted) - .size(IconSize::XSmall), - ) + .when_some(provider_icon, |this, icon| { + this.child(Icon::new(icon).color(Color::Muted).size(IconSize::XSmall)) + }) .child( Label::new(model_name) .color(Color::Muted) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 103e4396154e3073daed1063cb0239ba18ddb4cc..7f2fbce189280887ff8a7cbbcfa719542a0598d6 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -11,7 +11,6 @@ use serde::{Deserialize, Serialize}; use crate::NewExternalAgentThread; use crate::agent_diff::AgentDiffThread; -use crate::language_model_selector::ToggleModelSelector; use crate::{ AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode, DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread, @@ -28,7 +27,7 @@ use crate::{ render_remaining_tokens, }, thread_history::{HistoryEntryElement, ThreadHistory}, - ui::AgentOnboardingModal, + ui::{AgentOnboardingModal, EndTrialUpsell}, }; use agent::{ Thread, ThreadError, ThreadEvent, ThreadId, ThreadSummary, TokenUsageRatio, @@ -37,6 +36,7 @@ use agent::{ thread_store::{TextThreadStore, ThreadStore}, }; use agent_settings::{AgentDockPosition, AgentSettings, CompletionMode, DefaultView}; +use ai_onboarding::AgentPanelOnboarding; use anyhow::{Result, anyhow}; use assistant_context::{AssistantContext, ContextEvent, ContextSummary}; use assistant_slash_command::SlashCommandWorkingSet; @@ -48,13 +48,12 @@ 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, linear_color_stop, - linear_gradient, prelude::*, pulsating_between, + KeyContext, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, + pulsating_between, }; use language::LanguageRegistry; use language_model::{ ConfigurationError, ConfiguredModel, LanguageModelProviderTosView, LanguageModelRegistry, - ZED_CLOUD_PROVIDER_ID, }; use project::{Project, ProjectPath, Worktree}; use prompt_store::{PromptBuilder, PromptStore, UserPromptId}; @@ -66,9 +65,8 @@ use theme::ThemeSettings; use time::UtcOffset; use ui::utils::WithRemSize; use ui::{ - Banner, Button, Callout, CheckboxWithLabel, ContextMenu, ElevationIndex, IconPosition, - KeyBinding, PopoverMenu, PopoverMenuHandle, ProgressBar, Tab, Tooltip, Vector, VectorName, - prelude::*, + Banner, Callout, ContextMenu, ElevationIndex, KeyBinding, PopoverMenu, PopoverMenuHandle, + ProgressBar, Tab, Tooltip, prelude::*, }; use util::ResultExt as _; use workspace::{ @@ -77,7 +75,7 @@ use workspace::{ }; use zed_actions::{ DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize, - agent::{OpenConfiguration, OpenOnboardingModal, ResetOnboarding}, + agent::{OpenConfiguration, OpenOnboardingModal, ResetOnboarding, ToggleModelSelector}, assistant::{OpenRulesLibrary, ToggleFocus}, }; use zed_llm_client::{CompletionIntent, UsageLimit}; @@ -188,7 +186,7 @@ pub fn init(cx: &mut App) { window.refresh(); }) .register_action(|_workspace, _: &ResetTrialUpsell, _window, cx| { - Upsell::set_dismissed(false, cx); + OnboardingUpsell::set_dismissed(false, cx); }) .register_action(|_workspace, _: &ResetTrialEndUpsell, _window, cx| { TrialEndUpsell::set_dismissed(false, cx); @@ -453,7 +451,7 @@ pub struct AgentPanel { height: Option<Pixels>, zoomed: bool, pending_serialization: Option<Task<Result<()>>>, - hide_upsell: bool, + onboarding: Entity<AgentPanelOnboarding>, } impl AgentPanel { @@ -555,6 +553,7 @@ impl AgentPanel { let user_store = workspace.app_state().user_store.clone(); let project = workspace.project(); let language_registry = project.read(cx).languages().clone(); + let client = workspace.client().clone(); let workspace = workspace.weak_handle(); let weak_self = cx.entity().downgrade(); @@ -688,6 +687,17 @@ impl AgentPanel { }, ); + let onboarding = cx.new(|cx| { + AgentPanelOnboarding::new( + user_store.clone(), + client, + |_window, cx| { + OnboardingUpsell::set_dismissed(true, cx); + }, + cx, + ) + }); + Self { active_view, workspace, @@ -719,7 +729,7 @@ impl AgentPanel { height: None, zoomed: false, pending_serialization: None, - hide_upsell: false, + onboarding, } } @@ -2178,191 +2188,78 @@ impl AgentPanel { return false; } - let plan = self.user_store.read(cx).current_plan(); - let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some(); - - matches!(plan, Some(Plan::Free)) && has_previous_trial - } - - fn should_render_upsell(&self, cx: &mut Context<Self>) -> bool { match &self.active_view { ActiveView::Thread { thread, .. } => { - let is_using_zed_provider = thread + if thread .read(cx) .thread() .read(cx) .configured_model() - .map_or(false, |model| model.provider.id() == ZED_CLOUD_PROVIDER_ID); - - if !is_using_zed_provider { + .map_or(false, |model| { + model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID + }) + { return false; } } - ActiveView::ExternalAgentThread { .. } => { - return false; - } - ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => { - return false; + ActiveView::TextThread { .. } => { + if LanguageModelRegistry::global(cx) + .read(cx) + .default_model() + .map_or(false, |model| { + model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID + }) + { + return false; + } } - }; - - if self.hide_upsell || Upsell::dismissed() { - return false; + ActiveView::ExternalAgentThread { .. } + | ActiveView::History + | ActiveView::Configuration => return false, } let plan = self.user_store.read(cx).current_plan(); - if matches!(plan, Some(Plan::ZedPro | Plan::ZedProTrial)) { - return false; - } - let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some(); - if has_previous_trial { + + matches!(plan, Some(Plan::Free)) && has_previous_trial + } + + fn should_render_onboarding(&self, cx: &mut Context<Self>) -> bool { + if OnboardingUpsell::dismissed() { return false; } - true + match &self.active_view { + ActiveView::Thread { thread, .. } => thread + .read(cx) + .thread() + .read(cx) + .configured_model() + .map_or(true, |model| { + model.provider.id() == language_model::ZED_CLOUD_PROVIDER_ID + }), + ActiveView::TextThread { .. } => LanguageModelRegistry::global(cx) + .read(cx) + .default_model() + .map_or(true, |model| { + model.provider.id() == language_model::ZED_CLOUD_PROVIDER_ID + }), + ActiveView::ExternalAgentThread { .. } + | ActiveView::History + | ActiveView::Configuration => false, + } } - fn render_upsell( + fn render_onboarding( &self, _window: &mut Window, cx: &mut Context<Self>, ) -> Option<impl IntoElement> { - if !self.should_render_upsell(cx) { + if !self.should_render_onboarding(cx) { return None; } - if self.user_store.read(cx).account_too_young() { - Some(self.render_young_account_upsell(cx).into_any_element()) - } else { - Some(self.render_trial_upsell(cx).into_any_element()) - } - } - - fn render_young_account_upsell(&self, cx: &mut Context<Self>) -> impl IntoElement { - let checkbox = CheckboxWithLabel::new( - "dont-show-again", - Label::new("Don't show again").color(Color::Muted), - ToggleState::Unselected, - move |toggle_state, _window, cx| { - let toggle_state_bool = toggle_state.selected(); - - Upsell::set_dismissed(toggle_state_bool, cx); - }, - ); - - let contents = div() - .size_full() - .gap_2() - .flex() - .flex_col() - .child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small)) - .child( - Label::new("Your GitHub account was created less than 30 days ago, so we can't offer you a free trial.") - .size(LabelSize::Small), - ) - .child( - Label::new( - "Use your own API keys, upgrade to Zed Pro or send an email to billing-support@zed.dev.", - ) - .color(Color::Muted), - ) - .child( - h_flex() - .w_full() - .px_neg_1() - .justify_between() - .items_center() - .child(h_flex().items_center().gap_1().child(checkbox)) - .child( - h_flex() - .gap_2() - .child( - Button::new("dismiss-button", "Not Now") - .style(ButtonStyle::Transparent) - .color(Color::Muted) - .on_click({ - let agent_panel = cx.entity(); - move |_, _, cx| { - agent_panel.update(cx, |this, cx| { - this.hide_upsell = true; - cx.notify(); - }); - } - }), - ) - .child( - Button::new("cta-button", "Upgrade to Zed Pro") - .style(ButtonStyle::Transparent) - .on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))), - ), - ), - ); - - self.render_upsell_container(cx, contents) - } - - fn render_trial_upsell(&self, cx: &mut Context<Self>) -> impl IntoElement { - let checkbox = CheckboxWithLabel::new( - "dont-show-again", - Label::new("Don't show again").color(Color::Muted), - ToggleState::Unselected, - move |toggle_state, _window, cx| { - let toggle_state_bool = toggle_state.selected(); - - Upsell::set_dismissed(toggle_state_bool, cx); - }, - ); - - let contents = div() - .size_full() - .gap_2() - .flex() - .flex_col() - .child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small)) - .child( - Label::new("Try Zed Pro for free for 14 days - no credit card required.") - .size(LabelSize::Small), - ) - .child( - Label::new( - "Use your own API keys or enable usage-based billing once you hit the cap.", - ) - .color(Color::Muted), - ) - .child( - h_flex() - .w_full() - .px_neg_1() - .justify_between() - .items_center() - .child(h_flex().items_center().gap_1().child(checkbox)) - .child( - h_flex() - .gap_2() - .child( - Button::new("dismiss-button", "Not Now") - .style(ButtonStyle::Transparent) - .color(Color::Muted) - .on_click({ - let agent_panel = cx.entity(); - move |_, _, cx| { - agent_panel.update(cx, |this, cx| { - this.hide_upsell = true; - cx.notify(); - }); - } - }), - ) - .child( - Button::new("cta-button", "Start Trial") - .style(ButtonStyle::Transparent) - .on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))), - ), - ), - ); - - self.render_upsell_container(cx, contents) + Some(div().size_full().child(self.onboarding.clone())) } fn render_trial_end_upsell( @@ -2374,141 +2271,15 @@ impl AgentPanel { return None; } - Some( - self.render_upsell_container( - cx, - div() - .size_full() - .gap_2() - .flex() - .flex_col() - .child( - Headline::new("Your Zed Pro trial has expired.").size(HeadlineSize::Small), - ) - .child( - Label::new("You've been automatically reset to the free plan.") - .size(LabelSize::Small), - ) - .child( - h_flex() - .w_full() - .px_neg_1() - .justify_between() - .items_center() - .child(div()) - .child( - h_flex() - .gap_2() - .child( - Button::new("dismiss-button", "Stay on Free") - .style(ButtonStyle::Transparent) - .color(Color::Muted) - .on_click({ - let agent_panel = cx.entity(); - move |_, _, cx| { - agent_panel.update(cx, |_this, cx| { - TrialEndUpsell::set_dismissed(true, cx); - cx.notify(); - }); - } - }), - ) - .child( - Button::new("cta-button", "Upgrade to Zed Pro") - .style(ButtonStyle::Transparent) - .on_click(|_, _, cx| { - cx.open_url(&zed_urls::account_url(cx)) - }), - ), - ), - ), - ), - ) - } - - fn render_upsell_container(&self, cx: &mut Context<Self>, content: Div) -> Div { - div().p_2().child( - v_flex() - .w_full() - .elevation_2(cx) - .rounded(px(8.)) - .bg(cx.theme().colors().background.alpha(0.5)) - .p(px(3.)) - .child( - div() - .gap_2() - .flex() - .flex_col() - .size_full() - .border_1() - .rounded(px(5.)) - .border_color(cx.theme().colors().text.alpha(0.1)) - .overflow_hidden() - .relative() - .bg(cx.theme().colors().panel_background) - .px_4() - .py_3() - .child( - div() - .absolute() - .top_0() - .right(px(-1.0)) - .w(px(441.)) - .h(px(167.)) - .child( - Vector::new( - VectorName::Grid, - rems_from_px(441.), - rems_from_px(167.), - ) - .color(ui::Color::Custom(cx.theme().colors().text.alpha(0.1))), - ), - ) - .child( - div() - .absolute() - .top(px(-8.0)) - .right_0() - .w(px(400.)) - .h(px(92.)) - .child( - Vector::new( - VectorName::AiGrid, - rems_from_px(400.), - rems_from_px(92.), - ) - .color(ui::Color::Custom(cx.theme().colors().text.alpha(0.32))), - ), - ) - // .child( - // div() - // .absolute() - // .top_0() - // .right(px(360.)) - // .size(px(401.)) - // .overflow_hidden() - // .bg(cx.theme().colors().panel_background) - // ) - .child( - div() - .absolute() - .top_0() - .right_0() - .w(px(660.)) - .h(px(401.)) - .overflow_hidden() - .bg(linear_gradient( - 75., - linear_color_stop( - cx.theme().colors().panel_background.alpha(0.01), - 1.0, - ), - linear_color_stop(cx.theme().colors().panel_background, 0.45), - )), - ) - .child(content), - ), - ) + Some(EndTrialUpsell::new(Arc::new({ + let this = cx.entity(); + move |_, cx| { + this.update(cx, |_this, cx| { + TrialEndUpsell::set_dismissed(true, cx); + cx.notify(); + }); + } + }))) } fn render_thread_empty_state( @@ -2521,8 +2292,10 @@ impl AgentPanel { .update(cx, |this, cx| this.recent_entries(6, cx)); let model_registry = LanguageModelRegistry::read_global(cx); + let configuration_error = model_registry.configuration_error(model_registry.default_model(), cx); + let no_error = configuration_error.is_none(); let focus_handle = self.focus_handle(cx); @@ -2530,11 +2303,9 @@ impl AgentPanel { .size_full() .bg(cx.theme().colors().panel_background) .when(recent_history.is_empty(), |this| { - let configuration_error_ref = &configuration_error; this.child( v_flex() .size_full() - .max_w_80() .mx_auto() .justify_center() .items_center() @@ -2542,137 +2313,91 @@ impl AgentPanel { .child(h_flex().child(Headline::new("Welcome to the Agent Panel"))) .when(no_error, |parent| { parent - .child( - h_flex().child( - Label::new("Ask and build anything.") - .color(Color::Muted) - .mb_2p5(), - ), - ) - .child( - Button::new("new-thread", "Start New Thread") - .icon(IconName::Plus) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .full_width() - .key_binding(KeyBinding::for_action_in( - &NewThread::default(), - &focus_handle, - window, - cx, - )) - .on_click(|_event, window, cx| { - window.dispatch_action( - NewThread::default().boxed_clone(), - cx, - ) - }), - ) - .child( - Button::new("context", "Add Context") - .icon(IconName::FileCode) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .full_width() - .key_binding(KeyBinding::for_action_in( - &ToggleContextPicker, - &focus_handle, - window, - cx, - )) - .on_click(|_event, window, cx| { - window.dispatch_action( - ToggleContextPicker.boxed_clone(), - cx, - ) - }), - ) - .child( - Button::new("mode", "Switch Model") - .icon(IconName::DatabaseZap) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .full_width() - .key_binding(KeyBinding::for_action_in( - &ToggleModelSelector, - &focus_handle, - window, - cx, - )) - .on_click(|_event, window, cx| { - window.dispatch_action( - ToggleModelSelector.boxed_clone(), - cx, - ) - }), - ) - .child( - Button::new("settings", "View Settings") - .icon(IconName::Settings) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .full_width() - .key_binding(KeyBinding::for_action_in( - &OpenConfiguration, - &focus_handle, - window, - cx, - )) - .on_click(|_event, window, cx| { - window.dispatch_action( - OpenConfiguration.boxed_clone(), - cx, - ) - }), - ) - }) - .map(|parent| match configuration_error_ref { - Some( - err @ (ConfigurationError::ModelNotFound - | ConfigurationError::ProviderNotAuthenticated(_) - | ConfigurationError::NoProvider), - ) => parent .child(h_flex().child( - Label::new(err.to_string()).color(Color::Muted).mb_2p5(), + Label::new("Ask and build anything.").color(Color::Muted), )) .child( - Button::new("settings", "Configure a Provider") - .icon(IconName::Settings) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .full_width() - .key_binding(KeyBinding::for_action_in( - &OpenConfiguration, - &focus_handle, - window, - cx, - )) - .on_click(|_event, window, cx| { - window.dispatch_action( - OpenConfiguration.boxed_clone(), - cx, - ) - }), - ), - Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => { - parent.children(provider.render_accept_terms( - LanguageModelProviderTosView::ThreadFreshStart, - cx, - )) - } - None => parent, + v_flex() + .mt_2() + .gap_1() + .max_w_48() + .child( + Button::new("context", "Add Context") + .label_size(LabelSize::Small) + .icon(IconName::FileCode) + .icon_position(IconPosition::Start) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .full_width() + .key_binding(KeyBinding::for_action_in( + &ToggleContextPicker, + &focus_handle, + window, + cx, + )) + .on_click(|_event, window, cx| { + window.dispatch_action( + ToggleContextPicker.boxed_clone(), + cx, + ) + }), + ) + .child( + Button::new("mode", "Switch Model") + .label_size(LabelSize::Small) + .icon(IconName::DatabaseZap) + .icon_position(IconPosition::Start) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .full_width() + .key_binding(KeyBinding::for_action_in( + &ToggleModelSelector, + &focus_handle, + window, + cx, + )) + .on_click(|_event, window, cx| { + window.dispatch_action( + ToggleModelSelector.boxed_clone(), + cx, + ) + }), + ) + .child( + Button::new("settings", "View Settings") + .label_size(LabelSize::Small) + .icon(IconName::Settings) + .icon_position(IconPosition::Start) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .full_width() + .key_binding(KeyBinding::for_action_in( + &OpenConfiguration, + &focus_handle, + window, + cx, + )) + .on_click(|_event, window, cx| { + window.dispatch_action( + OpenConfiguration.boxed_clone(), + cx, + ) + }), + ), + ) + }) + .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(); - let configuration_error_ref = &configuration_error; - parent .overflow_hidden() .p_1p5() @@ -2735,49 +2460,55 @@ impl AgentPanel { }, )), ) - .map(|parent| match configuration_error_ref { - Some( - err @ (ConfigurationError::ModelNotFound - | ConfigurationError::ProviderNotAuthenticated(_) - | ConfigurationError::NoProvider), - ) => parent.child( - Banner::new() - .severity(ui::Severity::Warning) - .child(Label::new(err.to_string()).size(LabelSize::Small)) - .action_slot( - Button::new("settings", "Configure Provider") - .style(ButtonStyle::Tinted(ui::TintColor::Warning)) - .label_size(LabelSize::Small) - .key_binding( - KeyBinding::for_action_in( - &OpenConfiguration, - &focus_handle, - window, - cx, - ) - .map(|kb| kb.size(rems_from_px(12.))), - ) - .on_click(|_event, window, cx| { - window.dispatch_action( - OpenConfiguration.boxed_clone(), - cx, - ) - }), - ), - ), - Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => { - parent.child(Banner::new().severity(ui::Severity::Warning).child( - h_flex().w_full().children(provider.render_accept_terms( - LanguageModelProviderTosView::ThreadEmptyState, - cx, - )), - )) - } - None => parent, + .when_some(configuration_error.as_ref(), |this, err| { + this.child(self.render_configuration_error(err, &focus_handle, window, cx)) }) }) } + fn render_configuration_error( + &self, + 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") + .style(ButtonStyle::Tinted(ui::TintColor::Warning)) + .label_size(LabelSize::Small) + .key_binding( + KeyBinding::for_action_in( + &OpenConfiguration, + &focus_handle, + window, + cx, + ) + .map(|kb| kb.size(rems_from_px(12.))), + ) + .on_click(|_event, window, cx| { + window.dispatch_action(OpenConfiguration.boxed_clone(), cx) + }), + ), + ConfigurationError::ProviderPendingTermsAcceptance(provider) => { + Banner::new().severity(ui::Severity::Warning).child( + h_flex().w_full().children( + provider.render_accept_terms( + LanguageModelProviderTosView::ThreadEmptyState, + cx, + ), + ), + ) + } + } + } + fn render_tool_use_limit_reached( &self, window: &mut Window, @@ -2910,7 +2641,7 @@ impl AgentPanel { this.clear_last_error(); }); - cx.open_url(&zed_urls::account_url(cx)); + cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx)); cx.notify(); } })) @@ -3300,7 +3031,7 @@ impl Render for AgentPanel { })) .on_action(cx.listener(Self::toggle_burn_mode)) .child(self.render_toolbar(window, cx)) - .children(self.render_upsell(window, cx)) + .children(self.render_onboarding(window, cx)) .children(self.render_trial_end_upsell(window, cx)) .map(|parent| match &self.active_view { ActiveView::Thread { @@ -3309,12 +3040,14 @@ impl Render for AgentPanel { .. } => parent .relative() - .child(if thread.read(cx).is_empty() { - self.render_thread_empty_state(window, cx) - .into_any_element() - } else { - thread.clone().into_any_element() - }) + .child( + if thread.read(cx).is_empty() && !self.should_render_onboarding(cx) { + self.render_thread_empty_state(window, cx) + .into_any_element() + } else { + thread.clone().into_any_element() + }, + ) .children(self.render_tool_use_limit_reached(window, cx)) .when_some(thread.read(cx).last_error(), |this, last_error| { this.child( @@ -3352,12 +3085,36 @@ impl Render for AgentPanel { context_editor, buffer_search_bar, .. - } => parent.child(self.render_prompt_editor( - context_editor, - buffer_search_bar, - window, - cx, - )), + } => { + let model_registry = LanguageModelRegistry::read_global(cx); + let configuration_error = + model_registry.configuration_error(model_registry.default_model(), cx); + parent + .map(|this| { + 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, + ), + ), + ) + } else { + this + } + }) + .child(self.render_prompt_editor( + context_editor, + buffer_search_bar, + window, + cx, + )) + } ActiveView::Configuration => parent.children(self.configuration.clone()), }); @@ -3526,9 +3283,9 @@ impl AgentPanelDelegate for ConcreteAssistantPanelDelegate { } } -struct Upsell; +struct OnboardingUpsell; -impl Dismissable for Upsell { +impl Dismissable for OnboardingUpsell { const KEY: &'static str = "dismissed-trial-upsell"; } diff --git a/crates/agent_ui/src/inline_prompt_editor.rs b/crates/agent_ui/src/inline_prompt_editor.rs index 7a61eef7486de92bc181a3f28e032865a4452fe2..ade7a5e13deb08f2de6b044683f7038395e0f5b5 100644 --- a/crates/agent_ui/src/inline_prompt_editor.rs +++ b/crates/agent_ui/src/inline_prompt_editor.rs @@ -2,7 +2,6 @@ use crate::agent_model_selector::AgentModelSelector; use crate::buffer_codegen::BufferCodegen; use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider}; use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind}; -use crate::language_model_selector::ToggleModelSelector; use crate::message_editor::{ContextCreasesAddon, extract_message_creases, insert_message_creases}; use crate::terminal_codegen::TerminalCodegen; use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext}; @@ -38,6 +37,7 @@ use ui::{ CheckboxWithLabel, IconButtonShape, KeyBinding, Popover, PopoverMenuHandle, Tooltip, prelude::*, }; use workspace::Workspace; +use zed_actions::agent::ToggleModelSelector; pub struct PromptEditor<T> { pub editor: Entity<Editor>, diff --git a/crates/agent_ui/src/language_model_selector.rs b/crates/agent_ui/src/language_model_selector.rs index ff18a95f3f8b84eb0876a099cb664aa0908bed8f..655e87d7cdc394e182aa01089c081673991660b5 100644 --- a/crates/agent_ui/src/language_model_selector.rs +++ b/crates/agent_ui/src/language_model_selector.rs @@ -3,9 +3,7 @@ use std::{cmp::Reverse, sync::Arc}; use collections::{HashSet, IndexMap}; use feature_flags::ZedProFeatureFlag; use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; -use gpui::{ - Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task, actions, -}; +use gpui::{Action, AnyElement, App, BackgroundExecutor, DismissEvent, Subscription, Task}; use language_model::{ AuthenticateError, ConfiguredModel, LanguageModel, LanguageModelProviderId, LanguageModelRegistry, @@ -15,15 +13,6 @@ use picker::{Picker, PickerDelegate}; use proto::Plan; use ui::{ListItem, ListItemSpacing, prelude::*}; -actions!( - agent, - [ - /// Toggles the language model selector dropdown. - #[action(deprecated_aliases = ["assistant::ToggleModelSelector", "assistant2::ToggleModelSelector"])] - ToggleModelSelector - ] -); - const TRY_ZED_PRO_URL: &str = "https://zed.dev/pro"; type OnModelChanged = Arc<dyn Fn(Arc<dyn LanguageModel>, &mut App) + 'static>; diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index d2b136f274f98842ee248016b400883083ab62d5..6967c8ab3ee0d928cd094c26b844c66069755243 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -4,7 +4,6 @@ use std::sync::Arc; use crate::agent_diff::AgentDiffThread; use crate::agent_model_selector::AgentModelSelector; -use crate::language_model_selector::ToggleModelSelector; use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip}; use crate::ui::{ MaxModeTooltip, @@ -49,6 +48,7 @@ use ui::{ use util::ResultExt as _; use workspace::{CollaboratorId, Workspace}; use zed_actions::agent::Chat; +use zed_actions::agent::ToggleModelSelector; use zed_llm_client::CompletionIntent; use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention}; @@ -609,7 +609,11 @@ impl MessageEditor { ) } - fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement { + fn render_follow_toggle( + &self, + is_model_selected: bool, + cx: &mut Context<Self>, + ) -> impl IntoElement { let following = self .workspace .read_with(cx, |workspace, _| { @@ -618,6 +622,7 @@ impl MessageEditor { .unwrap_or(false); IconButton::new("follow-agent", IconName::Crosshair) + .disabled(is_model_selected) .icon_size(IconSize::Small) .icon_color(Color::Muted) .toggle_state(following) @@ -786,7 +791,7 @@ impl MessageEditor { .justify_between() .child( h_flex() - .child(self.render_follow_toggle(cx)) + .child(self.render_follow_toggle(is_model_selected, cx)) .children(self.render_burn_mode_toggle(cx)), ) .child( diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index 2941da19653fa6ebbd581663ed675af6b57a2d30..3df0a48aa418fcf7078a52972e3ea0659376e0ea 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -1,8 +1,6 @@ use crate::{ burn_mode_tooltip::BurnModeTooltip, - language_model_selector::{ - LanguageModelSelector, ToggleModelSelector, language_model_selector, - }, + language_model_selector::{LanguageModelSelector, language_model_selector}, }; use agent_settings::{AgentSettings, CompletionMode}; use anyhow::Result; @@ -38,8 +36,7 @@ use language::{ language_settings::{SoftWrap, all_language_settings}, }; use language_model::{ - ConfigurationError, LanguageModelExt, LanguageModelImage, LanguageModelProviderTosView, - LanguageModelRegistry, Role, + ConfigurationError, LanguageModelExt, LanguageModelImage, LanguageModelRegistry, Role, }; use multi_buffer::MultiBufferRow; use picker::{Picker, popover_menu::PickerPopoverMenu}; @@ -74,6 +71,7 @@ use workspace::{ pane, searchable::{SearchEvent, SearchableItem}, }; +use zed_actions::agent::ToggleModelSelector; use crate::{slash_command::SlashCommandCompletionProvider, slash_command_picker}; use assistant_context::{ @@ -1895,108 +1893,6 @@ impl TextThreadEditor { .update(cx, |context, cx| context.summarize(true, cx)); } - fn render_notice(&self, cx: &mut Context<Self>) -> Option<AnyElement> { - // This was previously gated behind the `zed-pro` feature flag. Since we - // aren't planning to ship that right now, we're just hard-coding this - // value to not show the nudge. - let nudge = Some(false); - - let model_registry = LanguageModelRegistry::read_global(cx); - - if nudge.map_or(false, |value| value) { - Some( - h_flex() - .p_3() - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .bg(cx.theme().colors().editor_background) - .justify_between() - .child( - h_flex() - .gap_3() - .child(Icon::new(IconName::ZedAssistant).color(Color::Accent)) - .child(Label::new("Zed AI is here! Get started by signing in →")), - ) - .child( - Button::new("sign-in", "Sign in") - .size(ButtonSize::Compact) - .style(ButtonStyle::Filled) - .on_click(cx.listener(|this, _event, _window, cx| { - let client = this - .workspace - .read_with(cx, |workspace, _| workspace.client().clone()) - .log_err(); - - if let Some(client) = client { - cx.spawn(async move |context_editor, cx| { - match client.authenticate_and_connect(true, cx).await { - util::ConnectionResult::Timeout => { - log::error!("Authentication timeout") - } - util::ConnectionResult::ConnectionReset => { - log::error!("Connection reset") - } - util::ConnectionResult::Result(r) => { - if r.log_err().is_some() { - context_editor - .update(cx, |_, cx| cx.notify()) - .ok(); - } - } - } - }) - .detach() - } - })), - ) - .into_any_element(), - ) - } else if let Some(configuration_error) = - model_registry.configuration_error(model_registry.default_model(), cx) - { - Some( - h_flex() - .px_3() - .py_2() - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .bg(cx.theme().colors().editor_background) - .justify_between() - .child( - h_flex() - .gap_3() - .child( - Icon::new(IconName::Warning) - .size(IconSize::Small) - .color(Color::Warning), - ) - .child(Label::new(configuration_error.to_string())), - ) - .child( - Button::new("open-configuration", "Configure Providers") - .size(ButtonSize::Compact) - .icon(Some(IconName::SlidersVertical)) - .icon_size(IconSize::Small) - .icon_position(IconPosition::Start) - .style(ButtonStyle::Filled) - .on_click({ - let focus_handle = self.focus_handle(cx).clone(); - move |_event, window, cx| { - focus_handle.dispatch_action( - &zed_actions::agent::OpenConfiguration, - window, - cx, - ); - } - }), - ) - .into_any_element(), - ) - } else { - None - } - } - fn render_send_button(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let focus_handle = self.focus_handle(cx).clone(); @@ -2128,12 +2024,13 @@ impl TextThreadEditor { .map(|default| default.model); let model_name = match active_model { Some(model) => model.name().0, - None => SharedString::from("No model selected"), + None => SharedString::from("Select Model"), }; let active_provider = LanguageModelRegistry::read_global(cx) .default_model() .map(|default| default.provider); + let provider_icon = match active_provider { Some(provider) => provider.icon(), None => IconName::Ai, @@ -2581,20 +2478,7 @@ impl EventEmitter<SearchEvent> for TextThreadEditor {} impl Render for TextThreadEditor { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { - let provider = LanguageModelRegistry::read_global(cx) - .default_model() - .map(|default| default.provider); - - let accept_terms = if self.show_accept_terms { - provider.as_ref().and_then(|provider| { - provider.render_accept_terms(LanguageModelProviderTosView::PromptEditorPopup, cx) - }) - } else { - None - }; - let language_model_selector = self.language_model_selector_menu_handle.clone(); - let burn_mode_toggle = self.render_burn_mode_toggle(cx); v_flex() .key_context("ContextEditor") @@ -2611,28 +2495,12 @@ impl Render for TextThreadEditor { language_model_selector.toggle(window, cx); }) .size_full() - .children(self.render_notice(cx)) .child( div() .flex_grow() .bg(cx.theme().colors().editor_background) .child(self.editor.clone()), ) - .when_some(accept_terms, |this, element| { - this.child( - div() - .absolute() - .right_3() - .bottom_12() - .max_w_96() - .py_2() - .px_3() - .elevation_2(cx) - .bg(cx.theme().colors().surface_background) - .occlude() - .child(element), - ) - }) .children(self.render_last_error(cx)) .child( h_flex() @@ -2649,7 +2517,7 @@ impl Render for TextThreadEditor { h_flex() .gap_0p5() .child(self.render_inject_context_menu(cx)) - .when_some(burn_mode_toggle, |this, element| this.child(element)), + .children(self.render_burn_mode_toggle(cx)), ) .child( h_flex() diff --git a/crates/agent_ui/src/ui.rs b/crates/agent_ui/src/ui.rs index 43cd0f5e8937d860ce0f453d40ece8d230f7d16d..6398f64abb65bb6c9639c71c59e31e1d1a214bba 100644 --- a/crates/agent_ui/src/ui.rs +++ b/crates/agent_ui/src/ui.rs @@ -1,6 +1,7 @@ mod agent_notification; mod burn_mode_tooltip; mod context_pill; +mod end_trial_upsell; mod onboarding_modal; pub mod preview; mod upsell; @@ -8,4 +9,5 @@ mod upsell; pub use agent_notification::*; pub use burn_mode_tooltip::*; pub use context_pill::*; +pub use end_trial_upsell::*; pub use onboarding_modal::*; diff --git a/crates/agent_ui/src/ui/end_trial_upsell.rs b/crates/agent_ui/src/ui/end_trial_upsell.rs new file mode 100644 index 0000000000000000000000000000000000000000..9c2dd98d2000d83733ad41147c3fa4486240de55 --- /dev/null +++ b/crates/agent_ui/src/ui/end_trial_upsell.rs @@ -0,0 +1,112 @@ +use std::sync::Arc; + +use ai_onboarding::{AgentPanelOnboardingCard, BulletItem}; +use client::zed_urls; +use gpui::{AnyElement, App, IntoElement, RenderOnce, Window}; +use ui::{Divider, List, prelude::*}; + +#[derive(IntoElement, RegisterComponent)] +pub struct EndTrialUpsell { + dismiss_upsell: Arc<dyn Fn(&mut Window, &mut App)>, +} + +impl EndTrialUpsell { + pub fn new(dismiss_upsell: Arc<dyn Fn(&mut Window, &mut App)>) -> Self { + Self { dismiss_upsell } + } +} + +impl RenderOnce for EndTrialUpsell { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + let pro_section = v_flex() + .gap_1() + .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 edit predictions")), + ) + .child( + Button::new("cta-button", "Upgrade to Zed Pro") + .full_width() + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .on_click(|_, _, cx| cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))), + ); + + let free_section = v_flex() + .mt_1p5() + .gap_1() + .child( + h_flex() + .gap_2() + .child( + Label::new("Free") + .size(LabelSize::Small) + .color(Color::Muted) + .buffer_font(cx), + ) + .child(Divider::horizontal()), + ) + .child( + List::new() + .child(BulletItem::new( + "50 prompts per month with the Claude models", + )) + .child(BulletItem::new( + "2000 accepted edit predictions using our open-source Zeta model", + )), + ) + .child( + Button::new("dismiss-button", "Stay on Free") + .full_width() + .style(ButtonStyle::Outlined) + .on_click({ + let callback = self.dismiss_upsell.clone(); + move |_, window, cx| callback(window, cx) + }), + ); + + AgentPanelOnboardingCard::new() + .child(Headline::new("Your Zed Pro trial has expired.")) + .child( + Label::new("You've been automatically reset to the Free plan.") + .size(LabelSize::Small) + .color(Color::Muted) + .mb_1(), + ) + .child(pro_section) + .child(free_section) + } +} + +impl Component for EndTrialUpsell { + fn scope() -> ComponentScope { + ComponentScope::Agent + } + + fn sort_name() -> &'static str { + "AgentEndTrialUpsell" + } + + fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> { + Some( + v_flex() + .p_4() + .gap_4() + .child(EndTrialUpsell { + dismiss_upsell: Arc::new(|_, _| {}), + }) + .into_any_element(), + ) + } +} diff --git a/crates/ai_onboarding/Cargo.toml b/crates/ai_onboarding/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..e9208a724865e2d0d5288f493925f5a944d67642 --- /dev/null +++ b/crates/ai_onboarding/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "ai_onboarding" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/ai_onboarding.rs" + +[features] +default = [] + +[dependencies] +client.workspace = true +component.workspace = true +gpui.workspace = true +language_model.workspace = true +proto.workspace = true +serde.workspace = true +smallvec.workspace = true +ui.workspace = true +workspace-hack.workspace = true +zed_actions.workspace = true diff --git a/crates/ai_onboarding/LICENSE-GPL b/crates/ai_onboarding/LICENSE-GPL new file mode 120000 index 0000000000000000000000000000000000000000..89e542f750cd3860a0598eff0dc34b56d7336dc4 --- /dev/null +++ b/crates/ai_onboarding/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/ai_onboarding/src/agent_panel_onboarding_card.rs b/crates/ai_onboarding/src/agent_panel_onboarding_card.rs new file mode 100644 index 0000000000000000000000000000000000000000..8ec9ccfe2230cedd921d3a18d0cb6236a043c716 --- /dev/null +++ b/crates/ai_onboarding/src/agent_panel_onboarding_card.rs @@ -0,0 +1,81 @@ +use gpui::{AnyElement, IntoElement, ParentElement, linear_color_stop, linear_gradient}; +use smallvec::SmallVec; +use ui::{Vector, VectorName, prelude::*}; + +#[derive(IntoElement)] +pub struct AgentPanelOnboardingCard { + children: SmallVec<[AnyElement; 2]>, +} + +impl AgentPanelOnboardingCard { + pub fn new() -> Self { + Self { + children: SmallVec::new(), + } + } +} + +impl ParentElement for AgentPanelOnboardingCard { + fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) { + self.children.extend(elements) + } +} + +impl RenderOnce for AgentPanelOnboardingCard { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + div() + .m_4() + .p(px(3.)) + .elevation_2(cx) + .rounded_lg() + .bg(cx.theme().colors().background.alpha(0.5)) + .child( + v_flex() + .relative() + .size_full() + .px_4() + .py_3() + .gap_2() + .border_1() + .rounded(px(5.)) + .border_color(cx.theme().colors().text.alpha(0.1)) + .overflow_hidden() + .bg(cx.theme().colors().panel_background) + .child( + div() + .opacity(0.5) + .absolute() + .top(px(-8.0)) + .right_0() + .w(px(400.)) + .h(px(92.)) + .child( + Vector::new( + VectorName::AiGrid, + rems_from_px(400.), + rems_from_px(92.), + ) + .color(Color::Custom(cx.theme().colors().text.alpha(0.32))), + ), + ) + .child( + div() + .absolute() + .top_0() + .right_0() + .w(px(660.)) + .h(px(401.)) + .overflow_hidden() + .bg(linear_gradient( + 75., + linear_color_stop( + cx.theme().colors().panel_background.alpha(0.01), + 1.0, + ), + linear_color_stop(cx.theme().colors().panel_background, 0.45), + )), + ) + .children(self.children), + ) + } +} diff --git a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs new file mode 100644 index 0000000000000000000000000000000000000000..f3f7d6c3d7e152ee8e46c6cf28b1d0bc0322c057 --- /dev/null +++ b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs @@ -0,0 +1,145 @@ +use std::sync::Arc; + +use client::{Client, UserStore}; +use gpui::{Action, ClickEvent, Entity, IntoElement, ParentElement}; +use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID}; +use ui::{Divider, List, prelude::*}; +use zed_actions::agent::{OpenConfiguration, ToggleModelSelector}; + +use crate::{AgentPanelOnboardingCard, BulletItem, ZedAiOnboarding}; + +pub struct AgentPanelOnboarding { + user_store: Entity<UserStore>, + client: Arc<Client>, + configured_providers: Vec<(IconName, SharedString)>, + continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>, +} + +impl AgentPanelOnboarding { + pub fn new( + user_store: Entity<UserStore>, + client: Arc<Client>, + continue_with_zed_ai: impl Fn(&mut Window, &mut App) + 'static, + cx: &mut Context<Self>, + ) -> Self { + cx.subscribe( + &LanguageModelRegistry::global(cx), + |this: &mut Self, _registry, event: &language_model::Event, cx| match event { + language_model::Event::ProviderStateChanged + | language_model::Event::AddedProvider(_) + | language_model::Event::RemovedProvider(_) => { + this.configured_providers = Self::compute_available_providers(cx) + } + _ => {} + }, + ) + .detach(); + + Self { + user_store, + client, + configured_providers: Self::compute_available_providers(cx), + continue_with_zed_ai: Arc::new(continue_with_zed_ai), + } + } + + fn compute_available_providers(cx: &App) -> Vec<(IconName, SharedString)> { + LanguageModelRegistry::read_global(cx) + .providers() + .iter() + .filter(|provider| { + provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID + }) + .map(|provider| (provider.icon(), provider.name().0.clone())) + .collect() + } + + fn configure_providers(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) { + window.dispatch_action(OpenConfiguration.boxed_clone(), cx); + cx.notify(); + } + + fn render_api_keys_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement { + let has_existing_providers = self.configured_providers.len() > 0; + let configure_provider_label = if has_existing_providers { + "Configure Other Provider" + } else { + "Configure Providers" + }; + + let content = if has_existing_providers { + List::new() + .child(BulletItem::new( + "Or start now using API keys from your environment for the following providers:" + )) + .child( + h_flex() + .px_5() + .gap_2() + .flex_wrap() + .children(self.configured_providers.iter().cloned().map(|(icon, name)| + h_flex() + .gap_1p5() + .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted)) + .child(Label::new(name)) + )) + ) + .child(BulletItem::new( + "No need for any of the plans or even to sign in", + )) + } else { + List::new() + .child(BulletItem::new( + "You can also use AI in Zed by bringing your own API keys", + )) + .child(BulletItem::new( + "No need for any of the plans or even to sign in", + )) + }; + + v_flex() + .mt_2() + .gap_1() + .child( + h_flex() + .gap_2() + .child( + Label::new("API Keys") + .size(LabelSize::Small) + .color(Color::Muted) + .buffer_font(cx), + ) + .child(Divider::horizontal()), + ) + .child(content) + .when(has_existing_providers, |this| { + this.child( + Button::new("pick-model", "Choose Model") + .full_width() + .style(ButtonStyle::Outlined) + .on_click(|_event, window, cx| { + window.dispatch_action(ToggleModelSelector.boxed_clone(), cx) + }), + ) + }) + .child( + Button::new("configure-providers", configure_provider_label) + .full_width() + .style(ButtonStyle::Outlined) + .on_click(cx.listener(Self::configure_providers)), + ) + } +} + +impl Render for AgentPanelOnboarding { + fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { + AgentPanelOnboardingCard::new() + .child(ZedAiOnboarding::new( + self.client.clone(), + &self.user_store, + self.continue_with_zed_ai.clone(), + cx, + )) + .child(self.render_api_keys_section(cx)) + } +} diff --git a/crates/ai_onboarding/src/ai_onboarding.rs b/crates/ai_onboarding/src/ai_onboarding.rs new file mode 100644 index 0000000000000000000000000000000000000000..131d385e7891644ea512676d49cc2ec9206c7784 --- /dev/null +++ b/crates/ai_onboarding/src/ai_onboarding.rs @@ -0,0 +1,397 @@ +mod agent_panel_onboarding_card; +mod agent_panel_onboarding_content; +mod edit_prediction_onboarding_content; +mod young_account_banner; + +pub use agent_panel_onboarding_card::AgentPanelOnboardingCard; +pub use agent_panel_onboarding_content::AgentPanelOnboarding; +pub use edit_prediction_onboarding_content::EditPredictionOnboarding; +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, prelude::*}; + +pub struct BulletItem { + label: SharedString, +} + +impl BulletItem { + pub fn new(label: impl Into<SharedString>) -> Self { + Self { + label: label.into(), + } + } +} + +impl IntoElement for BulletItem { + type Element = AnyElement; + + fn into_element(self) -> Self::Element { + ListItem::new("list-item") + .selectable(false) + .start_slot( + Icon::new(IconName::Dash) + .size(IconSize::XSmall) + .color(Color::Hidden), + ) + .child(div().w_full().child(Label::new(self.label))) + .into_any_element() + } +} + +pub enum SignInStatus { + SignedIn, + SigningIn, + SignedOut, +} + +impl From<client::Status> for SignInStatus { + fn from(status: client::Status) -> Self { + if status.is_signing_in() { + Self::SigningIn + } else if status.is_signed_out() { + Self::SignedOut + } else { + Self::SignedIn + } + } +} + +#[derive(RegisterComponent, IntoElement)] +pub struct ZedAiOnboarding { + pub sign_in_status: SignInStatus, + pub has_accepted_terms_of_service: bool, + pub plan: Option<proto::Plan>, + pub account_too_young: bool, + pub continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>, + pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>, + pub accept_terms_of_service: Arc<dyn Fn(&mut Window, &mut App)>, +} + +impl ZedAiOnboarding { + pub fn new( + client: Arc<Client>, + user_store: &Entity<UserStore>, + continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>, + cx: &mut App, + ) -> Self { + let store = user_store.read(cx); + let status = *client.status().borrow(); + Self { + sign_in_status: status.into(), + has_accepted_terms_of_service: store.current_user_has_accepted_terms().unwrap_or(false), + plan: store.current_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(); + async move |cx| { + client.authenticate_and_connect(true, cx).await; + } + }) + .detach(); + }), + } + } + + fn render_free_plan_section(&self, cx: &mut App) -> impl IntoElement { + v_flex() + .mt_2() + .gap_1() + .when(self.account_too_young, |this| this.opacity(0.4)) + .child( + h_flex() + .gap_2() + .child( + Label::new("Free") + .size(LabelSize::Small) + .color(Color::Muted) + .buffer_font(cx), + ) + .child(Divider::horizontal()), + ) + .child( + List::new() + .child(BulletItem::new( + "50 prompts per month with the Claude models", + )) + .child(BulletItem::new( + "2000 accepted edit predictions using our open-source Zeta model", + )), + ) + .child( + Button::new("continue", "Continue Free") + .disabled(self.account_too_young) + .full_width() + .style(ButtonStyle::Outlined) + .on_click({ + let callback = self.continue_with_zed_ai.clone(); + move |_, window, cx| callback(window, cx) + }), + ) + } + + fn render_pro_plan_section(&self, cx: &mut App) -> impl IntoElement { + let (button_label, button_url) = if self.account_too_young { + ("Start with Pro", zed_urls::upgrade_to_zed_pro_url(cx)) + } else { + ("Start Pro Trial", zed_urls::account_url(cx)) + }; + + 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( + List::new() + .child(BulletItem::new("500 prompts per month with Claude models")) + .child(BulletItem::new("Unlimited edit predictions")) + .when(!self.account_too_young, |this| { + this.child(BulletItem::new( + "Try it out for 14 days with no charge, no credit card required", + )) + }), + ) + .child( + Button::new("pro", button_label) + .full_width() + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .on_click(move |_, _window, cx| cx.open_url(&button_url)), + ) + } + + fn render_accept_terms_of_service(&self) -> Div { + v_flex() + .w_full() + .gap_1() + .child(Headline::new("Before starting…")) + .child(Label::new( + "Make sure you have read and accepted Zed AI's terms of service.", + )) + .child( + Button::new("terms_of_service", "View and Read the Terms of Service") + .full_width() + .style(ButtonStyle::Outlined) + .icon(IconName::ArrowUpRight) + .icon_color(Color::Muted) + .icon_size(IconSize::XSmall) + .on_click(move |_, _window, cx| { + cx.open_url("https://zed.dev/terms-of-service") + }), + ) + .child( + Button::new("accept_terms", "I've read it and accept it") + .full_width() + .style(ButtonStyle::Tinted(TintColor::Accent)) + .on_click({ + let callback = self.accept_terms_of_service.clone(); + move |_, window, cx| (callback)(window, cx) + }), + ) + } + + fn render_sign_in_disclaimer(&self, _cx: &mut App) -> Div { + const SIGN_IN_DISCLAIMER: &str = + "To start using AI in Zed with our hosted models, sign in and subscribe to a plan."; + let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn); + + v_flex() + .gap_2() + .child(Headline::new("Welcome to Zed AI")) + .child(div().w_full().child(Label::new(SIGN_IN_DISCLAIMER))) + .child( + Button::new("sign_in", "Sign In with GitHub") + .icon(IconName::Github) + .icon_position(IconPosition::Start) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .disabled(signing_in) + .full_width() + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .on_click({ + let callback = self.sign_in.clone(); + move |_, window, cx| callback(window, cx) + }), + ) + } + + fn render_free_plan_onboarding(&self, cx: &mut App) -> Div { + const PLANS_DESCRIPTION: &str = "Choose how you want to start."; + let young_account_banner = YoungAccountBanner; + + v_flex() + .child(Headline::new("Welcome to Zed AI")) + .child( + Label::new(PLANS_DESCRIPTION) + .size(LabelSize::Small) + .color(Color::Muted) + .mt_1() + .mb_3(), + ) + .when(self.account_too_young, |this| { + this.child(young_account_banner) + }) + .child(self.render_free_plan_section(cx)) + .child(self.render_pro_plan_section(cx)) + } + + fn render_trial_onboarding(&self, _cx: &mut App) -> Div { + v_flex() + .child(Headline::new("Welcome to the trial of Zed Pro")) + .child( + Label::new("Here's what you get for the next 14 days:") + .size(LabelSize::Small) + .color(Color::Muted) + .mt_1(), + ) + .child( + List::new() + .child(BulletItem::new("150 prompts with Claude models")) + .child(BulletItem::new( + "Unlimited edit predictions with Zeta, our open-source model", + )), + ) + .child( + Button::new("trial", "Start Trial") + .full_width() + .style(ButtonStyle::Outlined) + .on_click({ + let callback = self.continue_with_zed_ai.clone(); + move |_, window, cx| callback(window, cx) + }), + ) + } + + fn render_pro_plan_onboarding(&self, _cx: &mut App) -> Div { + v_flex() + .child(Headline::new("Welcome to Zed Pro")) + .child( + Label::new("Here's what you get:") + .size(LabelSize::Small) + .color(Color::Muted) + .mt_1(), + ) + .child( + List::new() + .child(BulletItem::new("500 prompts with Claude models")) + .child(BulletItem::new("Unlimited edit predictions")), + ) + .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| callback(window, cx) + }), + ) + } +} + +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(proto::Plan::Free) => self.render_free_plan_onboarding(cx), + Some(proto::Plan::ZedProTrial) => self.render_trial_onboarding(cx), + Some(proto::Plan::ZedPro) => self.render_pro_plan_onboarding(cx), + } + } else { + self.render_accept_terms_of_service() + } + } else { + self.render_sign_in_disclaimer(cx) + } + } +} + +impl Component for ZedAiOnboarding { + fn scope() -> ComponentScope { + ComponentScope::Agent + } + + fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> { + fn onboarding( + sign_in_status: SignInStatus, + has_accepted_terms_of_service: bool, + plan: Option<proto::Plan>, + 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(|_, _| {}), + } + .into_any_element() + } + + Some( + v_flex() + .p_4() + .gap_4() + .children(vec![ + single_example( + "Not Signed-in", + onboarding(SignInStatus::SignedOut, false, None, false), + ), + single_example( + "Not Accepted ToS", + onboarding(SignInStatus::SignedIn, false, None, false), + ), + single_example( + "Account too young", + onboarding(SignInStatus::SignedIn, false, None, true), + ), + single_example( + "Free Plan", + onboarding(SignInStatus::SignedIn, true, Some(proto::Plan::Free), false), + ), + single_example( + "Pro Trial", + onboarding( + SignInStatus::SignedIn, + true, + Some(proto::Plan::ZedProTrial), + false, + ), + ), + single_example( + "Pro Plan", + onboarding( + SignInStatus::SignedIn, + true, + Some(proto::Plan::ZedPro), + false, + ), + ), + ]) + .into_any_element(), + ) + } +} diff --git a/crates/ai_onboarding/src/edit_prediction_onboarding_content.rs b/crates/ai_onboarding/src/edit_prediction_onboarding_content.rs new file mode 100644 index 0000000000000000000000000000000000000000..e883d8da8ce01bfea3f08676666c308a90f6d650 --- /dev/null +++ b/crates/ai_onboarding/src/edit_prediction_onboarding_content.rs @@ -0,0 +1,73 @@ +use std::sync::Arc; + +use client::{Client, UserStore}; +use gpui::{Entity, IntoElement, ParentElement}; +use ui::prelude::*; + +use crate::ZedAiOnboarding; + +pub struct EditPredictionOnboarding { + user_store: Entity<UserStore>, + client: Arc<Client>, + copilot_is_configured: bool, + continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>, + continue_with_copilot: Arc<dyn Fn(&mut Window, &mut App)>, +} + +impl EditPredictionOnboarding { + pub fn new( + user_store: Entity<UserStore>, + client: Arc<Client>, + copilot_is_configured: bool, + continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>, + continue_with_copilot: Arc<dyn Fn(&mut Window, &mut App)>, + _cx: &mut Context<Self>, + ) -> Self { + Self { + user_store, + copilot_is_configured, + client, + continue_with_zed_ai, + continue_with_copilot, + } + } +} + +impl Render for EditPredictionOnboarding { + fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { + let github_copilot = v_flex() + .gap_1() + .child(Label::new(if self.copilot_is_configured { + "Alternatively, you can continue to use GitHub Copilot as that's already set up." + } else { + "Alternatively, you can use GitHub Copilot as your edit prediction provider." + })) + .child( + Button::new( + "configure-copilot", + if self.copilot_is_configured { + "Use Copilot" + } else { + "Configure Copilot" + }, + ) + .full_width() + .style(ButtonStyle::Outlined) + .on_click({ + let callback = self.continue_with_copilot.clone(); + move |_, window, cx| callback(window, cx) + }), + ); + + v_flex() + .gap_2() + .child(ZedAiOnboarding::new( + self.client.clone(), + &self.user_store, + self.continue_with_zed_ai.clone(), + cx, + )) + .child(ui::Divider::horizontal()) + .child(github_copilot) + } +} diff --git a/crates/ai_onboarding/src/young_account_banner.rs b/crates/ai_onboarding/src/young_account_banner.rs new file mode 100644 index 0000000000000000000000000000000000000000..f6e1446fd05cc719e8a6674ae9246084185162c7 --- /dev/null +++ b/crates/ai_onboarding/src/young_account_banner.rs @@ -0,0 +1,21 @@ +use gpui::{IntoElement, ParentElement}; +use ui::{Banner, prelude::*}; + +#[derive(IntoElement)] +pub struct YoungAccountBanner; + +impl RenderOnce for YoungAccountBanner { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + const YOUNG_ACCOUNT_DISCLAIMER: &str = "Given your GitHub account was created less than 30 days ago, we cannot put you in the Free plan or offer you a free trial of the Pro plan. We hope you'll understand, as this is unfortunately required to prevent abuse of our service. To continue, upgrade to Pro or use your own API keys for other providers."; + + let label = div() + .w_full() + .text_sm() + .text_color(cx.theme().colors().text_muted) + .child(YOUNG_ACCOUNT_DISCLAIMER); + + div() + .my_1() + .child(Banner::new().severity(ui::Severity::Warning).child(label)) + } +} diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index c4211f72c819cfed5c0ee2f555356aa970968bc5..1be8ffdb55b088ba7e9b8e0b1525b89e7dd0a48a 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -301,6 +301,13 @@ impl Status { matches!(self, Self::Connected { .. }) } + pub fn is_signing_in(&self) -> bool { + matches!( + self, + Self::Authenticating | Self::Reauthenticating | Self::Connecting | Self::Reconnecting + ) + } + pub fn is_signed_out(&self) -> bool { matches!(self, Self::SignedOut | Self::UpgradeRequired) } diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 61e3064eb496b59910ce8ab25797b9b4b4848201..f5213fbcb6c42db9d6a63ab312d024ca0e909f3f 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -764,6 +764,16 @@ impl UserStore { } pub fn current_plan(&self) -> Option<proto::Plan> { + #[cfg(debug_assertions)] + if let Ok(plan) = std::env::var("ZED_SIMULATE_ZED_PRO_PLAN").as_ref() { + return match plan.as_str() { + "free" => Some(proto::Plan::Free), + "trial" => Some(proto::Plan::ZedProTrial), + "pro" => Some(proto::Plan::ZedPro), + _ => None, + }; + } + self.current_plan } diff --git a/crates/client/src/zed_urls.rs b/crates/client/src/zed_urls.rs index bfdae468fbb6cc9d829d820a7d9cb0828a8763dd..442875b45132c1d7990f82ac93248ebd0477362c 100644 --- a/crates/client/src/zed_urls.rs +++ b/crates/client/src/zed_urls.rs @@ -17,3 +17,8 @@ fn server_url(cx: &App) -> &str { pub fn account_url(cx: &App) -> String { format!("{server_url}/account", server_url = server_url(cx)) } + +/// Returns the URL to the upgrade page on zed.dev. +pub fn upgrade_to_zed_pro_url(cx: &App) -> String { + format!("{server_url}/account/upgrade", server_url = server_url(cx)) +} diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index e4370d2e67cef9c5c4db68123edfb7dca5d7fa00..1966d1a3890157e76a44bcddce89d225af8ea923 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -209,8 +209,14 @@ impl Status { matches!(self, Status::Authorized) } - pub fn is_disabled(&self) -> bool { - matches!(self, Status::Disabled) + pub fn is_configured(&self) -> bool { + matches!( + self, + Status::Starting { .. } + | Status::Error(_) + | Status::SigningIn { .. } + | Status::Authorized + ) } } diff --git a/crates/inline_completion_button/src/inline_completion_button.rs b/crates/inline_completion_button/src/inline_completion_button.rs index 7e6b77b93deafbb971980d8b2d19f33f2fa348b4..8a8eacdc6a5855db435dd4d5e476f67fbe207910 100644 --- a/crates/inline_completion_button/src/inline_completion_button.rs +++ b/crates/inline_completion_button/src/inline_completion_button.rs @@ -46,6 +46,7 @@ actions!( ); const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot"; +const PRIVACY_DOCS: &str = "https://zed.dev/docs/ai/privacy-and-security"; struct CopilotErrorToast; @@ -193,13 +194,13 @@ impl Render for InlineCompletionButton { cx.open_url(activate_url.as_str()) }) .entry( - "Use Copilot", + "Use Zed AI", None, move |_, cx| { set_completion_provider( fs.clone(), cx, - EditPredictionProvider::Copilot, + EditPredictionProvider::Zed, ) }, ) @@ -239,22 +240,13 @@ impl Render for InlineCompletionButton { IconName::ZedPredictDisabled }; - let current_user_terms_accepted = - self.user_store.read(cx).current_user_has_accepted_terms(); - let has_subscription = self.user_store.read(cx).current_plan().is_some() - && self.user_store.read(cx).subscription_period().is_some(); - - if !has_subscription || !current_user_terms_accepted.unwrap_or(false) { - let signed_in = current_user_terms_accepted.is_some(); - let tooltip_meta = if signed_in { - if has_subscription { - "Read Terms of Service" - } else { - "Choose a Plan" - } - } else { - "Sign in to use" - }; + 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", + }; return div().child( IconButton::new("zed-predict-pending-button", zeta_icon) @@ -403,15 +395,16 @@ impl InlineCompletionButton { ) -> Entity<ContextMenu> { let fs = self.fs.clone(); ContextMenu::build(window, cx, |menu, _, _| { - menu.entry("Sign In", None, copilot::initiate_sign_in) + menu.entry("Sign In to Copilot", None, copilot::initiate_sign_in) .entry("Disable Copilot", None, { let fs = fs.clone(); move |_window, cx| hide_copilot(fs.clone(), cx) }) - .entry("Use Supermaven", None, { + .separator() + .entry("Use Zed AI", None, { let fs = fs.clone(); move |_window, cx| { - set_completion_provider(fs.clone(), cx, EditPredictionProvider::Supermaven) + set_completion_provider(fs.clone(), cx, EditPredictionProvider::Zed) } }) }) @@ -518,7 +511,7 @@ impl InlineCompletionButton { ); } - menu = menu.separator().header("Privacy Settings"); + menu = menu.separator().header("Privacy"); if let Some(provider) = &self.edit_prediction_provider { let data_collection = provider.data_collection_state(cx); if data_collection.is_supported() { @@ -569,13 +562,15 @@ impl InlineCompletionButton { .child( Label::new(indoc!{ "Help us improve our open dataset model by sharing data from open source repositories. \ - Zed must detect a license file in your repo for this setting to take effect." + Zed must detect a license file in your repo for this setting to take effect. \ + Files with sensitive data and secrets are excluded by default." }) ) .child( h_flex() .items_start() .pt_2() + .pr_1() .flex_1() .gap_1p5() .border_t_1() @@ -635,6 +630,13 @@ impl InlineCompletionButton { .detach_and_log_err(cx); } }), + ).item( + ContextMenuEntry::new("View Documentation") + .icon(IconName::FileGeneric) + .icon_color(Color::Muted) + .handler(move |_, cx| { + cx.open_url(PRIVACY_DOCS); + }) ); if !self.editor_enabled.unwrap_or(true) { @@ -672,6 +674,13 @@ impl InlineCompletionButton { ) -> Entity<ContextMenu> { ContextMenu::build(window, cx, |menu, window, cx| { self.build_language_settings_menu(menu, window, cx) + .separator() + .entry("Use Zed AI instead", None, { + let fs = self.fs.clone(); + move |_window, cx| { + set_completion_provider(fs.clone(), cx, EditPredictionProvider::Zed) + } + }) .separator() .link( "Go to Copilot Settings", @@ -750,44 +759,24 @@ impl InlineCompletionButton { menu = menu .custom_entry( |_window, _cx| { - h_flex() - .gap_1() - .child( - Icon::new(IconName::Warning) - .size(IconSize::Small) - .color(Color::Warning), - ) - .child( - Label::new("Your GitHub account is less than 30 days old") - .size(LabelSize::Small) - .color(Color::Warning), - ) + Label::new("Your GitHub account is less than 30 days old.") + .size(LabelSize::Small) + .color(Color::Warning) .into_any_element() }, |_window, cx| cx.open_url(&zed_urls::account_url(cx)), ) - .entry( - "You need to upgrade to Zed Pro or contact us.", - None, - |_window, cx| cx.open_url(&zed_urls::account_url(cx)), - ) + .entry("Upgrade to Zed Pro or contact us.", None, |_window, cx| { + cx.open_url(&zed_urls::account_url(cx)) + }) .separator(); } else if self.user_store.read(cx).has_overdue_invoices() { menu = menu .custom_entry( |_window, _cx| { - h_flex() - .gap_1() - .child( - Icon::new(IconName::Warning) - .size(IconSize::Small) - .color(Color::Warning), - ) - .child( - Label::new("You have an outstanding invoice") - .size(LabelSize::Small) - .color(Color::Warning), - ) + Label::new("You have an outstanding invoice") + .size(LabelSize::Small) + .color(Color::Warning) .into_any_element() }, |_window, cx| { diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index 6bd33fcdf508b33fb397ccc602de2b719d4906a2..72455b382199fc503256e33706045baef2c1b1ec 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -654,7 +654,7 @@ pub enum LanguageModelProviderTosView { ThreadEmptyState, /// When there are no past interactions in the Agent Panel. ThreadFreshStart, - PromptEditorPopup, + TextThreadPopup, Configuration, } diff --git a/crates/language_models/Cargo.toml b/crates/language_models/Cargo.toml index 5d158e84f4fb072d40e43a43cd53b5b996274351..ed38ac76605e5b9554f3c5cd2a91a6650c20393d 100644 --- a/crates/language_models/Cargo.toml +++ b/crates/language_models/Cargo.toml @@ -12,6 +12,7 @@ workspace = true path = "src/language_models.rs" [dependencies] +ai_onboarding.workspace = true anthropic = { workspace = true, features = ["schemars"] } anyhow.workspace = true aws-config = { workspace = true, features = ["behavior-version-latest"] } diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index 6aea576258e6e46f5d1b9355a12007852296724a..736107570b395c3014e25dce1cbe21737de9e96b 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -1,3 +1,4 @@ +use ai_onboarding::YoungAccountBanner; use anthropic::AnthropicModelMode; use anyhow::{Context as _, Result, anyhow}; use chrono::{DateTime, Utc}; @@ -500,7 +501,7 @@ fn render_accept_terms( ) .child({ match view_kind { - LanguageModelProviderTosView::PromptEditorPopup => { + LanguageModelProviderTosView::TextThreadPopup => { button_container.w_full().justify_end() } LanguageModelProviderTosView::Configuration => { @@ -1126,6 +1127,7 @@ struct ZedAiConfiguration { subscription_period: Option<(DateTime<Utc>, DateTime<Utc>)>, 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<dyn Fn(&mut Window, &mut App) + Send + Sync>, sign_in_callback: Arc<dyn Fn(&mut Window, &mut App) + Send + Sync>, @@ -1133,18 +1135,18 @@ struct ZedAiConfiguration { impl RenderOnce for ZedAiConfiguration { fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - const ZED_PRICING_URL: &str = "https://zed.dev/pricing"; + let young_account_banner = YoungAccountBanner; let is_pro = self.plan == Some(proto::Plan::ZedPro); let subscription_text = match (self.plan, self.subscription_period) { (Some(proto::Plan::ZedPro), Some(_)) => { - "You have access to Zed's hosted LLMs through your Zed Pro subscription." + "You have access to Zed's hosted LLMs through your Pro subscription." } (Some(proto::Plan::ZedProTrial), Some(_)) => { - "You have access to Zed's hosted LLMs through your Zed Pro trial." + "You have access to Zed's hosted LLMs through your Pro trial." } (Some(proto::Plan::Free), Some(_)) => { - "You have basic access to Zed's hosted LLMs through your Zed Free subscription." + "You have basic access to Zed's hosted LLMs through the Free plan." } _ => { if self.eligible_for_trial { @@ -1154,68 +1156,76 @@ impl RenderOnce for ZedAiConfiguration { } } }; + let manage_subscription_buttons = if is_pro { - h_flex().child( - Button::new("manage_settings", "Manage Subscription") - .style(ButtonStyle::Tinted(TintColor::Accent)) - .on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))), - ) + Button::new("manage_settings", "Manage Subscription") + .style(ButtonStyle::Tinted(TintColor::Accent)) + .on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))) + .into_any_element() + } else if self.plan.is_none() || self.eligible_for_trial { + Button::new("start_trial", "Start 14-day Free Pro Trial") + .style(ui::ButtonStyle::Tinted(ui::TintColor::Accent)) + .full_width() + .on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))) + .into_any_element() } else { - h_flex() - .gap_2() - .child( - Button::new("learn_more", "Learn more") - .style(ButtonStyle::Subtle) - .on_click(|_, _, cx| cx.open_url(ZED_PRICING_URL)), - ) - .child( - Button::new( - "upgrade", - if self.plan.is_none() && self.eligible_for_trial { - "Start Trial" - } else { - "Upgrade" - }, - ) - .style(ButtonStyle::Subtle) - .color(Color::Accent) - .on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))), - ) + 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))) + .into_any_element() }; - if self.is_connected { - v_flex() - .gap_3() - .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) - }, - )) - }) - .when(self.has_accepted_terms_of_service, |this| { - this.child(subscription_text) - .child(manage_subscription_buttons) - }) - } else { - v_flex() + if !self.is_connected { + return v_flex() .gap_2() - .child(Label::new("Use Zed AI to access hosted language models.")) + .child(Label::new("Sign in to have access to Zed's complete agentic experience with hosted models.")) .child( - Button::new("sign_in", "Sign In") + Button::new("sign_in", "Sign In to use Zed AI") .icon_color(Color::Muted) .icon(IconName::Github) + .icon_size(IconSize::Small) .icon_position(IconPosition::Start) + .full_width() .on_click({ let callback = self.sign_in_callback.clone(); move |_, window, cx| (callback)(window, cx) }), - ) + ); } + + 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) } } @@ -1264,6 +1274,7 @@ impl Render for ConfigurationView { 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(), @@ -1281,6 +1292,7 @@ impl Component for ZedAiConfiguration { is_connected: bool, plan: Option<proto::Plan>, eligible_for_trial: bool, + account_too_young: bool, has_accepted_terms_of_service: bool, ) -> AnyElement { ZedAiConfiguration { @@ -1291,6 +1303,7 @@ impl Component for ZedAiConfiguration { .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(|_, _| {}), @@ -1303,30 +1316,33 @@ impl Component for ZedAiConfiguration { .p_4() .gap_4() .children(vec![ - single_example("Not connected", configuration(false, None, false, true)), + single_example( + "Not connected", + configuration(false, None, false, false, true), + ), single_example( "Accept Terms of Service", - configuration(true, None, true, false), + configuration(true, None, true, false, false), ), single_example( "No Plan - Not eligible for trial", - configuration(true, None, false, true), + configuration(true, None, false, false, true), ), single_example( "No Plan - Eligible for trial", - configuration(true, None, true, true), + configuration(true, None, true, false, true), ), single_example( "Free Plan", - configuration(true, Some(proto::Plan::Free), true, true), + configuration(true, Some(proto::Plan::Free), true, false, true), ), single_example( "Zed Pro Trial Plan", - configuration(true, Some(proto::Plan::ZedProTrial), true, true), + configuration(true, Some(proto::Plan::ZedProTrial), true, false, true), ), single_example( "Zed Pro Plan", - configuration(true, Some(proto::Plan::ZedPro), true, true), + configuration(true, Some(proto::Plan::ZedPro), true, false, true), ), ]) .into_any_element(), diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 977b5c3ecd0e5170a46941f2f3459e9d0d77f06e..c4fdb16f4f5d8b18b7e2b536198cc1ba61ec04d8 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, UserStore}; +use client::{Client, UserStore, zed_urls}; use gpui::{ Action, AnyElement, App, Context, Corner, Element, Entity, Focusable, InteractiveElement, IntoElement, MouseButton, ParentElement, Render, StatefulInteractiveElement, Styled, @@ -656,8 +656,9 @@ impl TitleBar { let user_login = user.github_login.clone(); let (plan_name, label_color, bg_color) = match plan { - None => ("None", Color::Default, free_chip_bg), - Some(proto::Plan::Free) => ("Free", Color::Default, free_chip_bg), + None | Some(proto::Plan::Free) => { + ("Free", Color::Default, free_chip_bg) + } Some(proto::Plan::ZedProTrial) => { ("Pro Trial", Color::Accent, pro_chip_bg) } @@ -680,7 +681,7 @@ impl TitleBar { .into_any_element() }, move |_, cx| { - cx.open_url("https://zed.dev/account"); + cx.open_url(&zed_urls::account_url(cx)); }, ) .separator() diff --git a/crates/ui/src/components/button/button_like.rs b/crates/ui/src/components/button/button_like.rs index a0158b2fe745f383be179594c49ce1874b181176..135ecdfe62a632909ac36d05ffaa157824e220f6 100644 --- a/crates/ui/src/components/button/button_like.rs +++ b/crates/ui/src/components/button/button_like.rs @@ -126,6 +126,10 @@ pub enum ButtonStyle { /// coloring like an error or success button. Tinted(TintColor), + /// Usually used as a secondary action that should have more emphasis than + /// a fully transparent button. + Outlined, + /// The default button style, used for most buttons. Has a transparent background, /// but has a background color to indicate states like hover and active. #[default] @@ -180,6 +184,12 @@ impl ButtonStyle { icon_color: Color::Default.color(cx), }, ButtonStyle::Tinted(tint) => tint.button_like_style(cx), + ButtonStyle::Outlined => ButtonLikeStyles { + background: element_bg_from_elevation(elevation, cx), + border_color: cx.theme().colors().border_variant, + label_color: Color::Default.color(cx), + icon_color: Color::Default.color(cx), + }, ButtonStyle::Subtle => ButtonLikeStyles { background: cx.theme().colors().ghost_element_background, border_color: transparent_black(), @@ -219,6 +229,12 @@ impl ButtonStyle { styles.background = theme.darken(styles.background, 0.05, 0.2); styles } + ButtonStyle::Outlined => ButtonLikeStyles { + background: cx.theme().colors().ghost_element_hover, + border_color: cx.theme().colors().border, + label_color: Color::Default.color(cx), + icon_color: Color::Default.color(cx), + }, ButtonStyle::Subtle => ButtonLikeStyles { background: cx.theme().colors().ghost_element_hover, border_color: transparent_black(), @@ -251,6 +267,12 @@ impl ButtonStyle { label_color: Color::Default.color(cx), icon_color: Color::Default.color(cx), }, + ButtonStyle::Outlined => ButtonLikeStyles { + background: cx.theme().colors().element_active, + border_color: cx.theme().colors().border_variant, + label_color: Color::Default.color(cx), + icon_color: Color::Default.color(cx), + }, ButtonStyle::Transparent => ButtonLikeStyles { background: transparent_black(), border_color: transparent_black(), @@ -278,6 +300,12 @@ impl ButtonStyle { label_color: Color::Default.color(cx), icon_color: Color::Default.color(cx), }, + ButtonStyle::Outlined => ButtonLikeStyles { + background: cx.theme().colors().ghost_element_background, + border_color: cx.theme().colors().border, + label_color: Color::Default.color(cx), + icon_color: Color::Default.color(cx), + }, ButtonStyle::Transparent => ButtonLikeStyles { background: transparent_black(), border_color: cx.theme().colors().border_focused, @@ -308,6 +336,12 @@ impl ButtonStyle { label_color: Color::Disabled.color(cx), icon_color: Color::Disabled.color(cx), }, + ButtonStyle::Outlined => ButtonLikeStyles { + background: cx.theme().colors().element_disabled, + border_color: cx.theme().colors().border_disabled, + label_color: Color::Default.color(cx), + icon_color: Color::Default.color(cx), + }, ButtonStyle::Transparent => ButtonLikeStyles { background: transparent_black(), border_color: transparent_black(), @@ -525,6 +559,13 @@ 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_some(self.rounding, |this, rounding| match rounding { ButtonLikeRounding::All => this.rounded_sm(), ButtonLikeRounding::Left => this.rounded_l_sm(), @@ -538,6 +579,7 @@ impl RenderOnce for ButtonLike { } ButtonSize::None => this, }) + .border_color(style.enabled(self.layer, cx).border_color) .bg(style.enabled(self.layer, cx).background) .when(self.disabled, |this| { if self.cursor_style == CursorStyle::PointingHand { diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index fc7d98178edfce397ae4600b17b7bbac4a1cb9c6..4b4bf016c4b06293bda3769a5545a2fa1bd6195b 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -277,7 +277,10 @@ pub mod agent { /// Displays the previous message in the history. PreviousHistoryMessage, /// Displays the next message in the history. - NextHistoryMessage + NextHistoryMessage, + /// Toggles the language model selector dropdown. + #[action(deprecated_aliases = ["assistant::ToggleModelSelector", "assistant2::ToggleModelSelector"])] + ToggleModelSelector ] ); } diff --git a/crates/zeta/Cargo.toml b/crates/zeta/Cargo.toml index 1609773339a57df929ce317ce2a793fb8b067bca..c2b1de08aea4d096dea25b50d54077306489482d 100644 --- a/crates/zeta/Cargo.toml +++ b/crates/zeta/Cargo.toml @@ -17,11 +17,13 @@ doctest = false test-support = [] [dependencies] +ai_onboarding.workspace = true anyhow.workspace = true arrayvec.workspace = true client.workspace = true collections.workspace = true command_palette_hooks.workspace = true +copilot.workspace = true db.workspace = true editor.workspace = true feature_flags.workspace = true @@ -35,8 +37,6 @@ language.workspace = true language_model.workspace = true log.workspace = true menu.workspace = true -migrator.workspace = true -paths.workspace = true postage.workspace = true project.workspace = true proto.workspace = true diff --git a/crates/zeta/src/init.rs b/crates/zeta/src/init.rs index 6411e423a4d2e0b0f8b9e8b6e2e745a11e7864e6..4bcd50df885a43ade7bb04bbde8b8c3d3a1f54d1 100644 --- a/crates/zeta/src/init.rs +++ b/crates/zeta/src/init.rs @@ -34,7 +34,6 @@ pub fn init(cx: &mut App) { workspace, workspace.user_store().clone(), workspace.client().clone(), - workspace.app_state().fs.clone(), window, cx, ) diff --git a/crates/zeta/src/onboarding_modal.rs b/crates/zeta/src/onboarding_modal.rs index c123d76c53c801fb8eb7eb95416b8f53fc3f58f6..1d59f36b0532429f8cc24f3fc6adcdd468279d33 100644 --- a/crates/zeta/src/onboarding_modal.rs +++ b/crates/zeta/src/onboarding_modal.rs @@ -1,40 +1,33 @@ -use std::{sync::Arc, time::Duration}; +use std::sync::Arc; -use crate::{ZED_PREDICT_DATA_COLLECTION_CHOICE, onboarding_event}; -use anyhow::Context as _; +use crate::{ZedPredictUpsell, onboarding_event}; +use ai_onboarding::EditPredictionOnboarding; use client::{Client, UserStore}; -use db::kvp::KEY_VALUE_STORE; +use db::kvp::Dismissable; use fs::Fs; use gpui::{ - Animation, AnimationExt as _, ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, - Focusable, MouseDownEvent, Render, ease_in_out, svg, + ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render, + linear_color_stop, linear_gradient, }; use language::language_settings::{AllLanguageSettings, EditPredictionProvider}; -use settings::{Settings, update_settings_file}; -use ui::{Checkbox, TintColor, prelude::*}; -use util::ResultExt; -use workspace::{ModalView, Workspace, notifications::NotifyTaskExt}; +use settings::update_settings_file; +use ui::{Vector, VectorName, prelude::*}; +use workspace::{ModalView, Workspace}; /// Introduces user to Zed's Edit Prediction feature and terms of service pub struct ZedPredictModal { - user_store: Entity<UserStore>, - client: Arc<Client>, - fs: Arc<dyn Fs>, + onboarding: Entity<EditPredictionOnboarding>, focus_handle: FocusHandle, - sign_in_status: SignInStatus, - terms_of_service: bool, - data_collection_expanded: bool, - data_collection_opted_in: bool, } -#[derive(PartialEq, Eq)] -enum SignInStatus { - /// Signed out or signed in but not from this modal - Idle, - /// Authentication triggered from this modal - Waiting, - /// Signed in after authentication from this modal - SignedIn, +pub(crate) fn set_edit_prediction_provider(provider: EditPredictionProvider, cx: &mut App) { + let fs = <dyn Fs>::global(cx); + update_settings_file::<AllLanguageSettings>(fs, cx, move |settings, _| { + settings + .features + .get_or_insert(Default::default()) + .edit_prediction_provider = Some(provider); + }); } impl ZedPredictModal { @@ -42,127 +35,45 @@ impl ZedPredictModal { workspace: &mut Workspace, user_store: Entity<UserStore>, client: Arc<Client>, - fs: Arc<dyn Fs>, window: &mut Window, cx: &mut Context<Workspace>, ) { - workspace.toggle_modal(window, cx, |_window, cx| Self { - user_store, - client, - fs, - focus_handle: cx.focus_handle(), - sign_in_status: SignInStatus::Idle, - terms_of_service: false, - data_collection_expanded: false, - data_collection_opted_in: false, - }); - } - - fn view_terms(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) { - cx.open_url("https://zed.dev/terms-of-service"); - cx.notify(); - - onboarding_event!("ToS Link Clicked"); - } - - fn view_blog(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) { - cx.open_url("https://zed.dev/blog/edit-prediction"); - cx.notify(); - - onboarding_event!("Blog Link clicked"); - } - - fn inline_completions_doc(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) { - cx.open_url("https://zed.dev/docs/configuring-zed#disabled-globs"); - cx.notify(); - - onboarding_event!("Docs Link Clicked"); - } - - fn accept_and_enable(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) { - let task = self - .user_store - .update(cx, |this, cx| this.accept_terms_of_service(cx)); - let fs = self.fs.clone(); - - cx.spawn(async move |this, cx| { - task.await?; - - let mut data_collection_opted_in = false; - this.update(cx, |this, _cx| { - data_collection_opted_in = this.data_collection_opted_in; - }) - .ok(); - - KEY_VALUE_STORE - .write_kvp( - ZED_PREDICT_DATA_COLLECTION_CHOICE.into(), - data_collection_opted_in.to_string(), - ) - .await - .log_err(); - - // Make sure edit prediction provider setting is using the new key - let settings_path = paths::settings_file().as_path(); - let settings_path = fs.canonicalize(settings_path).await.with_context(|| { - format!("Failed to canonicalize settings path {:?}", settings_path) - })?; - - if let Some(settings) = fs.load(&settings_path).await.log_err() { - if let Some(new_settings) = - migrator::migrate_edit_prediction_provider_settings(&settings)? - { - fs.atomic_write(settings_path, new_settings).await?; - } + workspace.toggle_modal(window, cx, |_window, cx| { + let weak_entity = cx.weak_entity(); + Self { + onboarding: cx.new(|cx| { + EditPredictionOnboarding::new( + user_store.clone(), + client.clone(), + copilot::Copilot::global(cx) + .map_or(false, |copilot| copilot.read(cx).status().is_configured()), + Arc::new({ + let this = weak_entity.clone(); + move |_window, cx| { + ZedPredictUpsell::set_dismissed(true, cx); + set_edit_prediction_provider(EditPredictionProvider::Zed, cx); + this.update(cx, |_, cx| cx.emit(DismissEvent)).ok(); + } + }), + Arc::new({ + let this = weak_entity.clone(); + move |window, cx| { + ZedPredictUpsell::set_dismissed(true, cx); + set_edit_prediction_provider(EditPredictionProvider::Copilot, cx); + this.update(cx, |_, cx| cx.emit(DismissEvent)).ok(); + copilot::initiate_sign_in(window, cx); + } + }), + cx, + ) + }), + focus_handle: cx.focus_handle(), } - - this.update(cx, |this, cx| { - update_settings_file::<AllLanguageSettings>(this.fs.clone(), cx, move |file, _| { - file.features - .get_or_insert(Default::default()) - .edit_prediction_provider = Some(EditPredictionProvider::Zed); - }); - - cx.emit(DismissEvent); - }) - }) - .detach_and_notify_err(window, cx); - - onboarding_event!( - "Enable Clicked", - data_collection_opted_in = self.data_collection_opted_in, - ); - } - - fn sign_in(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) { - let client = self.client.clone(); - self.sign_in_status = SignInStatus::Waiting; - - cx.spawn(async move |this, cx| { - let result = client - .authenticate_and_connect(true, &cx) - .await - .into_response(); - - let status = match result { - Ok(_) => SignInStatus::SignedIn, - Err(_) => SignInStatus::Idle, - }; - - this.update(cx, |this, cx| { - this.sign_in_status = status; - onboarding_event!("Signed In"); - cx.notify() - })?; - - result - }) - .detach_and_notify_err(window, cx); - - onboarding_event!("Sign In Clicked"); + }); } fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) { + ZedPredictUpsell::set_dismissed(true, cx); cx.emit(DismissEvent); } } @@ -177,85 +88,12 @@ impl Focusable for ZedPredictModal { impl ModalView for ZedPredictModal {} -impl ZedPredictModal { - fn render_data_collection_explanation(&self, cx: &Context<Self>) -> impl IntoElement { - fn label_item(label_text: impl Into<SharedString>) -> impl Element { - Label::new(label_text).color(Color::Muted).into_element() - } - - fn info_item(label_text: impl Into<SharedString>) -> impl Element { - h_flex() - .items_start() - .gap_2() - .child( - div() - .mt_1p5() - .child(Icon::new(IconName::Check).size(IconSize::XSmall)), - ) - .child(div().w_full().child(label_item(label_text))) - } - - fn multiline_info_item<E1: Into<SharedString>, E2: IntoElement>( - first_line: E1, - second_line: E2, - ) -> impl Element { - v_flex() - .child(info_item(first_line)) - .child(div().pl_5().child(second_line)) - } - - v_flex() - .mt_2() - .p_2() - .rounded_sm() - .bg(cx.theme().colors().editor_background.opacity(0.5)) - .border_1() - .border_color(cx.theme().colors().border_variant) - .child( - div().child( - Label::new("To improve edit predictions, please consider contributing to our open dataset based on your interactions within open source repositories.") - .mb_1() - ) - ) - .child(info_item( - "We collect data exclusively from open source projects.", - )) - .child(info_item( - "Zed automatically detects if your project is open source.", - )) - .child(info_item("Toggle participation at any time via the status bar menu.")) - .child(multiline_info_item( - "If turned on, this setting applies for all open source repositories", - label_item("you open in Zed.") - )) - .child(multiline_info_item( - "Files with sensitive data, like `.env`, are excluded by default", - h_flex() - .w_full() - .flex_wrap() - .child(label_item("via the")) - .child( - Button::new("doc-link", "disabled_globs").on_click( - cx.listener(Self::inline_completions_doc), - ), - ) - .child(label_item("setting.")), - )) - } -} - impl Render for ZedPredictModal { fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { let window_height = window.viewport_size().height; let max_height = window_height - px(200.); - let has_subscription_period = self.user_store.read(cx).subscription_period().is_some(); - let plan = self.user_store.read(cx).current_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 base = v_flex() + v_flex() .id("edit-prediction-onboarding") .key_context("ZedPredictModal") .relative() @@ -264,14 +102,9 @@ impl Render for ZedPredictModal { .max_h(max_height) .p_4() .gap_2() - .when(self.data_collection_expanded, |element| { - element.overflow_y_scroll() - }) - .when(!self.data_collection_expanded, |element| { - element.overflow_hidden() - }) .elevation_3(cx) .track_focus(&self.focus_handle(cx)) + .overflow_hidden() .on_action(cx.listener(Self::cancel)) .on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| { onboarding_event!("Cancelled", trigger = "Action"); @@ -282,77 +115,30 @@ impl Render for ZedPredictModal { })) .child( div() - .p_1p5() + .opacity(0.5) .absolute() - .top_1() - .left_1() + .top(px(-8.0)) .right_0() - .h(px(200.)) + .w(px(400.)) + .h(px(92.)) .child( - svg() - .path("icons/zed_predict_bg.svg") - .text_color(cx.theme().colors().icon_disabled) - .w(px(530.)) - .h(px(128.)) - .overflow_hidden(), + Vector::new(VectorName::AiGrid, rems_from_px(400.), rems_from_px(92.)) + .color(Color::Custom(cx.theme().colors().text.alpha(0.32))), ), ) .child( - h_flex() - .w_full() - .mb_2() - .justify_between() - .child( - v_flex() - .gap_1() - .child( - Label::new("Introducing Zed AI's") - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child(Headline::new("Edit Prediction").size(HeadlineSize::Large)), - ) - .child({ - let tab = |n: usize| { - let text_color = cx.theme().colors().text; - let border_color = cx.theme().colors().text_accent.opacity(0.4); - - h_flex().child( - h_flex() - .px_4() - .py_0p5() - .bg(cx.theme().colors().editor_background) - .border_1() - .border_color(border_color) - .rounded_sm() - .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) - .text_size(TextSize::XSmall.rems(cx)) - .text_color(text_color) - .child("tab") - .with_animation( - 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.border_color(border_color.opacity(delta)) - .text_color(text_color.opacity(delta)) - }, - ), - ) - }; - - v_flex() - .gap_2() - .items_center() - .pr_2p5() - .child(tab(0).ml_neg_20()) - .child(tab(1)) - .child(tab(2).ml_20()) - }), + div() + .absolute() + .top_0() + .right_0() + .w(px(660.)) + .h(px(401.)) + .overflow_hidden() + .bg(linear_gradient( + 75., + linear_color_stop(cx.theme().colors().panel_background.alpha(0.01), 1.0), + linear_color_stop(cx.theme().colors().panel_background, 0.45), + )), ) .child(h_flex().absolute().top_2().right_2().child( IconButton::new("cancel", IconName::X).on_click(cx.listener( @@ -361,148 +147,7 @@ impl Render for ZedPredictModal { cx.emit(DismissEvent); }, )), - )); - - let blog_post_button = Button::new("view-blog", "Read the Blog Post") - .full_width() - .icon(IconName::ArrowUpRight) - .icon_size(IconSize::Indicator) - .icon_color(Color::Muted) - .on_click(cx.listener(Self::view_blog)); - - if self.user_store.read(cx).current_user().is_some() { - let copy = match self.sign_in_status { - SignInStatus::Idle => { - "Zed can now predict your next edit on every keystroke. Powered by Zeta, our open-source, open-dataset language model." - } - SignInStatus::SignedIn => "Almost there! Ensure you:", - SignInStatus::Waiting => unreachable!(), - }; - - let accordion_icons = if self.data_collection_expanded { - (IconName::ChevronUp, IconName::ChevronDown) - } else { - (IconName::ChevronDown, IconName::ChevronUp) - }; - let plan = plan.unwrap_or(proto::Plan::Free); - - base.child(Label::new(copy).color(Color::Muted)) - .child( - h_flex().child( - Checkbox::new("plan", ToggleState::Selected) - .fill() - .disabled(true) - .label(format!( - "You get {} edit predictions through your {}.", - if plan == proto::Plan::Free { - "2,000" - } else { - "unlimited" - }, - match plan { - proto::Plan::Free => "Zed Free plan", - proto::Plan::ZedPro => "Zed Pro plan", - proto::Plan::ZedProTrial => "Zed Pro trial", - } - )), - ), - ) - .child( - h_flex() - .child( - Checkbox::new("tos-checkbox", self.terms_of_service.into()) - .fill() - .label("I have read and accept the") - .on_click(cx.listener(move |this, state, _window, cx| { - this.terms_of_service = *state == ToggleState::Selected; - cx.notify(); - })), - ) - .child( - Button::new("view-tos", "Terms of Service") - .icon(IconName::ArrowUpRight) - .icon_size(IconSize::Indicator) - .icon_color(Color::Muted) - .on_click(cx.listener(Self::view_terms)), - ), - ) - .child( - v_flex() - .child( - h_flex() - .flex_wrap() - .child( - Checkbox::new( - "training-data-checkbox", - self.data_collection_opted_in.into(), - ) - .label( - "Contribute to the open dataset when editing open source.", - ) - .fill() - .on_click(cx.listener( - move |this, state, _window, cx| { - this.data_collection_opted_in = - *state == ToggleState::Selected; - cx.notify() - }, - )), - ) - .child( - Button::new("learn-more", "Learn More") - .icon(accordion_icons.0) - .icon_size(IconSize::Indicator) - .icon_color(Color::Muted) - .on_click(cx.listener(|this, _, _, cx| { - this.data_collection_expanded = - !this.data_collection_expanded; - cx.notify(); - - if this.data_collection_expanded { - onboarding_event!( - "Data Collection Learn More Clicked" - ); - } - })), - ), - ) - .when(self.data_collection_expanded, |element| { - element.child(self.render_data_collection_explanation(cx)) - }), - ) - .child( - v_flex() - .mt_2() - .gap_2() - .w_full() - .child( - Button::new("accept-tos", "Enable Edit Prediction") - .disabled(!self.terms_of_service) - .style(ButtonStyle::Tinted(TintColor::Accent)) - .full_width() - .on_click(cx.listener(Self::accept_and_enable)), - ) - .child(blog_post_button), - ) - } else { - base.child( - Label::new("To set Zed as your edit prediction provider, please sign in.") - .color(Color::Muted), - ) - .child( - v_flex() - .mt_2() - .gap_2() - .w_full() - .child( - Button::new("accept-tos", "Sign in with GitHub") - .disabled(self.sign_in_status == SignInStatus::Waiting) - .style(ButtonStyle::Tinted(TintColor::Accent)) - .full_width() - .on_click(cx.listener(Self::sign_in)), - ) - .child(blog_post_button), - ) - } + )) + .child(self.onboarding.clone()) } } diff --git a/crates/zeta/src/zeta.rs b/crates/zeta/src/zeta.rs index 87cd1e604c3fd422c2ea9c218cbed755e72925cf..d6f033899de8443fa736ec92774b9363e6da459b 100644 --- a/crates/zeta/src/zeta.rs +++ b/crates/zeta/src/zeta.rs @@ -7,7 +7,7 @@ mod onboarding_telemetry; mod rate_completion_modal; pub(crate) use completion_diff_element::*; -use db::kvp::KEY_VALUE_STORE; +use db::kvp::{Dismissable, KEY_VALUE_STORE}; pub use init::*; use inline_completion::DataCollectionState; use license_detection::LICENSE_FILES_TO_CHECK; @@ -95,6 +95,38 @@ impl std::fmt::Display for InlineCompletionId { } } +struct ZedPredictUpsell; + +impl Dismissable for ZedPredictUpsell { + const KEY: &'static str = "dismissed-edit-predict-upsell"; + + fn dismissed() -> bool { + // To make this backwards compatible with older versions of Zed, we + // check if the user has seen the previous Edit Prediction Onboarding + // before, by checking the data collection choice which was written to + // the database once the user clicked on "Accept and Enable" + if KEY_VALUE_STORE + .read_kvp(ZED_PREDICT_DATA_COLLECTION_CHOICE) + .log_err() + .map_or(false, |s| s.is_some()) + { + return true; + } + + KEY_VALUE_STORE + .read_kvp(Self::KEY) + .log_err() + .map_or(false, |s| s.is_some()) + } +} + +pub fn should_show_upsell_modal(user_store: &Entity<UserStore>, cx: &App) -> bool { + match user_store.read(cx).current_user_has_accepted_terms() { + Some(true) => !ZedPredictUpsell::dismissed(), + Some(false) | None => true, + } +} + #[derive(Clone)] struct ZetaGlobal(Entity<Zeta>); From 750ceeb760fb6e74d6c5b0503c2b8b34ab911efa Mon Sep 17 00:00:00 2001 From: Marshall Bowers <git@maxdeviant.com> Date: Fri, 18 Jul 2025 12:28:58 -0400 Subject: [PATCH 192/658] collab: Don't use `screen-capture` feature from `gpui` (#34725) This PR removes the `screen-capture` feature from `gpui` when depending on it in `collab`. Release Notes: - N/A --- crates/collab/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 242694d96365d218060601df7030b18552ee1e9b..d3b504828350e8cf6be5b44f1b0e1a5361006eb7 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -35,7 +35,7 @@ dashmap.workspace = true derive_more.workspace = true envy = "0.4.2" futures.workspace = true -gpui = { workspace = true, features = ["screen-capture"] } +gpui.workspace = true hex.workspace = true http_client.workspace = true jsonwebtoken.workspace = true From 8bc8d61fa6991cf0f1a675088a2a5e6356d16a6a Mon Sep 17 00:00:00 2001 From: Finn Evers <finn@zed.dev> Date: Fri, 18 Jul 2025 18:55:03 +0200 Subject: [PATCH 193/658] theme_importer: Add missing color imports for the minimap thumb (#34724) These should have been part of https://github.com/zed-industries/zed/pull/30785 but I forgot to add them there. Release Notes: - N/A --- crates/theme_importer/src/vscode/converter.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/theme_importer/src/vscode/converter.rs b/crates/theme_importer/src/vscode/converter.rs index 9a17a4cdd2b13e116b81c86c753ccab83a965c79..0249bdc7c94a5008240bde25153203c10d247a82 100644 --- a/crates/theme_importer/src/vscode/converter.rs +++ b/crates/theme_importer/src/vscode/converter.rs @@ -175,6 +175,8 @@ impl VsCodeThemeConverter { 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()), From 1dd470ca48fef83e920972f175f232f9ea252b44 Mon Sep 17 00:00:00 2001 From: Smit Barmase <heysmitbarmase@gmail.com> Date: Fri, 18 Jul 2025 22:39:00 +0530 Subject: [PATCH 194/658] editor: Fix double $ sign on completion accept in PHP (#34726) Closes #33510 https://github.com/zed-extensions/php/issues/29 If certain language servers do not provide an insert/replace range, we use `surrounding_word` as a fallback for that range, which internally uses `word_characters`. It makes sense to use `completion_query_characters` instead of `word_characters` to get that range, because we use `completion_query_characters` to query completions in the first place. That means, for some hypothetical reason (e.g., if the Tailwind server stops providing insert/replace ranges), we would correctly fall back to the range "bg-blue-200^" instead of "200^", because `completion_query_characters` includes "-" in this case. For this particular fix, right now the default PHP language server `phpactor` does not provide an insert/replace range, and hence completion query character is used, which is `$` in this case. Note that `$` isn't in word characters for reasons mentioned here: https://github.com/zed-extensions/php/issues/14 Release Notes: - Fixed an issue where accepting variable completion in PHP would result in a double $ sign in the prefix. --- crates/editor/src/editor.rs | 8 ++++---- crates/language/src/buffer.rs | 10 ++++++++-- crates/project/src/lsp_command.rs | 4 ++-- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c3187f6b51188c946ac07b3c40f2a235e554c623..b8dcdd35e07102afd613e99c22a60df6f4699604 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5451,7 +5451,7 @@ impl Editor { }; let (word_replace_range, word_to_exclude) = if let (word_range, Some(CharKind::Word)) = - buffer_snapshot.surrounding_word(buffer_position) + buffer_snapshot.surrounding_word(buffer_position, false) { let word_to_exclude = buffer_snapshot .text_for_range(word_range.clone()) @@ -6605,8 +6605,8 @@ impl Editor { } let snapshot = cursor_buffer.read(cx).snapshot(); - let (start_word_range, _) = snapshot.surrounding_word(cursor_buffer_position); - let (end_word_range, _) = snapshot.surrounding_word(tail_buffer_position); + let (start_word_range, _) = snapshot.surrounding_word(cursor_buffer_position, false); + let (end_word_range, _) = snapshot.surrounding_word(tail_buffer_position, false); if start_word_range != end_word_range { self.document_highlights_task.take(); self.clear_background_highlights::<DocumentHighlightRead>(cx); @@ -22137,7 +22137,7 @@ impl SemanticsProvider for Entity<Project> { // Fallback on using TreeSitter info to determine identifier range buffer.read_with(cx, |buffer, _| { let snapshot = buffer.snapshot(); - let (range, kind) = snapshot.surrounding_word(position); + let (range, kind) = snapshot.surrounding_word(position, false); if kind != Some(CharKind::Word) { return None; } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index ae0184b22a97acfb2adf1080a352479fca2ab82e..59aa63ff3806d0a56965dce229112c948d073c7b 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -3364,13 +3364,19 @@ impl BufferSnapshot { /// Returns a tuple of the range and character kind of the word /// surrounding the given position. - pub fn surrounding_word<T: ToOffset>(&self, start: T) -> (Range<usize>, Option<CharKind>) { + pub fn surrounding_word<T: ToOffset>( + &self, + start: T, + for_completion: bool, + ) -> (Range<usize>, Option<CharKind>) { let mut start = start.to_offset(self); let mut end = start; let mut next_chars = self.chars_at(start).take(128).peekable(); let mut prev_chars = self.reversed_chars_at(start).take(128).peekable(); - let classifier = self.char_classifier_at(start); + let classifier = self + .char_classifier_at(start) + .for_completion(for_completion); let word_kind = cmp::max( prev_chars.peek().copied().map(|c| classifier.kind(c)), next_chars.peek().copied().map(|c| classifier.kind(c)), diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 4538dc4cda59a5478f8fb68cae2fd6862310382b..a2f6de44c9c6a478879cf81ad48acea0e485dbbd 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -350,7 +350,7 @@ impl LspCommand for PrepareRename { } Some(lsp::PrepareRenameResponse::DefaultBehavior { .. }) => { let snapshot = buffer.snapshot(); - let (range, _) = snapshot.surrounding_word(self.position); + let (range, _) = snapshot.surrounding_word(self.position, false); let range = snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end); Ok(PrepareRenameResponse::Success(range)) } @@ -2297,7 +2297,7 @@ impl LspCommand for GetCompletions { range_for_token .get_or_insert_with(|| { let offset = self.position.to_offset(&snapshot); - let (range, kind) = snapshot.surrounding_word(offset); + let (range, kind) = snapshot.surrounding_word(offset, true); let range = if kind == Some(CharKind::Word) { range } else { From 5b18ce79abb0242ed91573994414160c44ed716c Mon Sep 17 00:00:00 2001 From: Finn Evers <finn@zed.dev> Date: Fri, 18 Jul 2025 19:16:31 +0200 Subject: [PATCH 195/658] editor: Ensure topmost buffer header can be properly folded (#34721) This PR fixes an issue where the topmost header in a multibuffer would jump when the corresponding buffer was folded. The issue arose because for the topmost header, the offset within the scroll anchor is negative, as the corresponding buffer only starts below the header itself and thus the offset for the scroll position has to be negative. However, upon collapsing that buffer, we end up with a negative vertical scroll position, which causes all kinds of different problems. The issue has been present for a long time, but became more visible after https://github.com/zed-industries/zed/pull/34295 landed, as that change removed the case distinction for buffers scrolled all the way to the top. This PR fixes this by clamping just the vertical scroll position upon return, which ensures the negative offset works as expected when the buffer is expanded, but the vertical scroll position does not turn negative once the buffer is folded. Release Notes: - Fixed an issue where folding the topmost buffer in a multibuffer would cause the header to jump slightly. --- crates/editor/src/scroll.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index 7310d6d3c05f4532f0a9ff5d27b86f0efdf24791..ecaf7c11e41373c96547e13f3d4b83757e2501a8 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -12,7 +12,7 @@ use crate::{ }; pub use autoscroll::{Autoscroll, AutoscrollStrategy}; use core::fmt::Debug; -use gpui::{App, Axis, Context, Global, Pixels, Task, Window, point, px}; +use gpui::{Along, App, Axis, Context, Global, Pixels, Task, Window, point, px}; use language::language_settings::{AllLanguageSettings, SoftWrap}; use language::{Bias, Point}; pub use scroll_amount::ScrollAmount; @@ -49,14 +49,14 @@ impl ScrollAnchor { } pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> gpui::Point<f32> { - let mut scroll_position = self.offset; - if self.anchor == Anchor::min() { - scroll_position.y = 0.; - } else { - let scroll_top = self.anchor.to_display_point(snapshot).row().as_f32(); - scroll_position.y += scroll_top; - } - scroll_position + self.offset.apply_along(Axis::Vertical, |offset| { + if self.anchor == Anchor::min() { + 0. + } else { + let scroll_top = self.anchor.to_display_point(snapshot).row().as_f32(); + (offset + scroll_top).max(0.) + } + }) } pub fn top_row(&self, buffer: &MultiBufferSnapshot) -> u32 { From 87555d3f0bb4e19895ad68bb3538978007f6d9e8 Mon Sep 17 00:00:00 2001 From: tidely <43219534+tidely@users.noreply.github.com> Date: Fri, 18 Jul 2025 20:41:12 +0300 Subject: [PATCH 196/658] project: Remove clones from git blame serialization (#34727) Release Notes: - N/A --- crates/project/src/git_store.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 5f07bafe5bd9e99f723bd3932b0a985b457ae85b..6e3d27deffd8f470b327462aefd8c8bc3b81ad65 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -4397,17 +4397,17 @@ fn serialize_blame_buffer_response(blame: Option<git::blame::Blame>) -> proto::B start_line: entry.range.start, end_line: entry.range.end, original_line_number: entry.original_line_number, - author: entry.author.clone(), - author_mail: entry.author_mail.clone(), + author: entry.author, + author_mail: entry.author_mail, author_time: entry.author_time, - author_tz: entry.author_tz.clone(), - committer: entry.committer_name.clone(), - committer_mail: entry.committer_email.clone(), + author_tz: entry.author_tz, + committer: entry.committer_name, + committer_mail: entry.committer_email, committer_time: entry.committer_time, - committer_tz: entry.committer_tz.clone(), - summary: entry.summary.clone(), - previous: entry.previous.clone(), - filename: entry.filename.clone(), + committer_tz: entry.committer_tz, + summary: entry.summary, + previous: entry.previous, + filename: entry.filename, }) .collect::<Vec<_>>(); From 64ce696aae27ff5afc44301035a21f40c8ca634f Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Fri, 18 Jul 2025 15:03:14 -0300 Subject: [PATCH 197/658] ui: Add the `SwitchField` component (#34713) This will be useful for both the current agent panel and some other onboarding stuff we're working on. Also ended up removing the `SwitchWithLabel` as it was unused. Release Notes: - N/A --- crates/ui/src/components/toggle.rs | 166 ++++++++++++++++++++++++----- 1 file changed, 142 insertions(+), 24 deletions(-) diff --git a/crates/ui/src/components/toggle.rs b/crates/ui/src/components/toggle.rs index 4672a0cfc2d78e50a2b15e1225f388ca99d0d3a9..759b22543408676e239b55303d929e4d2f08dfe3 100644 --- a/crates/ui/src/components/toggle.rs +++ b/crates/ui/src/components/toggle.rs @@ -543,28 +543,48 @@ impl RenderOnce for Switch { } } -/// A [`Switch`] that has a [`Label`]. -#[derive(IntoElement)] -pub struct SwitchWithLabel { +/// # SwitchField +/// +/// A field component that combines a label, description, and switch into one reusable component. +/// +/// # Examples +/// +/// ``` +/// use ui::prelude::*; +/// +/// SwitchField::new( +/// "feature-toggle", +/// "Enable feature", +/// "This feature adds new functionality to the app.", +/// ToggleState::Unselected, +/// |state, window, cx| { +/// // Logic here +/// } +/// ); +/// ``` +#[derive(IntoElement, RegisterComponent)] +pub struct SwitchField { id: ElementId, - label: Label, + label: SharedString, + description: SharedString, toggle_state: ToggleState, on_click: Arc<dyn Fn(&ToggleState, &mut Window, &mut App) + 'static>, disabled: bool, color: SwitchColor, } -impl SwitchWithLabel { - /// Creates a switch with an attached label. +impl SwitchField { pub fn new( id: impl Into<ElementId>, - label: Label, + label: impl Into<SharedString>, + description: impl Into<SharedString>, toggle_state: impl Into<ToggleState>, on_click: impl Fn(&ToggleState, &mut Window, &mut App) + 'static, ) -> Self { Self { id: id.into(), - label, + label: label.into(), + description: description.into(), toggle_state: toggle_state.into(), on_click: Arc::new(on_click), disabled: false, @@ -572,43 +592,141 @@ impl SwitchWithLabel { } } - /// Sets the disabled state of the [`SwitchWithLabel`]. pub fn disabled(mut self, disabled: bool) -> Self { self.disabled = disabled; self } /// Sets the color of the switch using the specified [`SwitchColor`]. + /// This changes the color scheme of the switch when it's in the "on" state. pub fn color(mut self, color: SwitchColor) -> Self { self.color = color; self } } -impl RenderOnce for SwitchWithLabel { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { +impl RenderOnce for SwitchField { + fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { h_flex() .id(SharedString::from(format!("{}-container", self.id))) - .gap(DynamicSpacing::Base08.rems(cx)) + .w_full() + .gap_4() + .justify_between() + .flex_wrap() .child( - Switch::new(self.id.clone(), self.toggle_state) - .disabled(self.disabled) - .color(self.color) - .on_click({ - let on_click = self.on_click.clone(); - move |checked, window, cx| { - (on_click)(checked, window, cx); - } - }), + v_flex() + .gap_0p5() + .max_w_5_6() + .child(Label::new(self.label)) + .child(Label::new(self.description).color(Color::Muted)), ) .child( - div() - .id(SharedString::from(format!("{}-label", self.id))) - .child(self.label), + 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); + } + }), ) } } +impl Component for SwitchField { + fn scope() -> ComponentScope { + ComponentScope::Input + } + + fn description() -> Option<&'static str> { + Some("A field component that combines a label, description, and switch") + } + + fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> { + Some( + v_flex() + .gap_6() + .children(vec![ + example_group_with_title( + "States", + vec![ + single_example( + "Unselected", + SwitchField::new( + "switch_field_unselected", + "Enable notifications", + "Receive notifications when new messages arrive.", + ToggleState::Unselected, + |_, _, _| {}, + ) + .into_any_element(), + ), + single_example( + "Selected", + SwitchField::new( + "switch_field_selected", + "Enable notifications", + "Receive notifications when new messages arrive.", + ToggleState::Selected, + |_, _, _| {}, + ) + .into_any_element(), + ), + ], + ), + example_group_with_title( + "Colors", + vec![ + single_example( + "Default", + SwitchField::new( + "switch_field_default", + "Default color", + "This uses the default switch color.", + ToggleState::Selected, + |_, _, _| {}, + ) + .into_any_element(), + ), + single_example( + "Accent", + SwitchField::new( + "switch_field_accent", + "Accent color", + "This uses the accent color scheme.", + ToggleState::Selected, + |_, _, _| {}, + ) + .color(SwitchColor::Accent) + .into_any_element(), + ), + ], + ), + example_group_with_title( + "Disabled", + vec![single_example( + "Disabled", + SwitchField::new( + "switch_field_disabled", + "Disabled field", + "This field is disabled and cannot be toggled.", + ToggleState::Selected, + |_, _, _| {}, + ) + .disabled(true) + .into_any_element(), + )], + ), + ]) + .into_any_element(), + ) + } +} + impl Component for Checkbox { fn scope() -> ComponentScope { ComponentScope::Input From e1d28ff957dbc16c39e4a8e061e79526f181849a Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Fri, 18 Jul 2025 15:03:31 -0300 Subject: [PATCH 198/658] agent: Add `use_modifier_to_send` setting (#34709) When `use_modifier_to_send` is turned on, holding `cmd`/`ctrl` is necessary to send a message in the agent panel. Text threads already use `cmd-enter` by default to submit a message, and it was done this way to have the usual text editing bindings not taken over when writing a prompt, sort of stimulating more thoughtful writing. While `enter` to send is still somewhat a huge pattern in chat-like LLM UIs, it still makes sense to allow this for the new agent panel... hence the existence of this setting now! Release Notes: - agent: Added the `use_modifier_to_send` setting, which makes holding a modifier (`cmd`/`ctrl`), together with `enter`, required to send a new message. --- assets/keymaps/default-linux.json | 13 ++++++++- assets/keymaps/default-macos.json | 14 +++++++++- crates/agent_settings/src/agent_settings.rs | 13 +++++++++ crates/agent_ui/src/message_editor.rs | 30 +++++++++++++++++++-- 4 files changed, 66 insertions(+), 4 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index b859d2d84c98f721c51f37302a7123a222796dfa..8e670314853bb341612bf92a0e256024f2815b27 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -277,7 +277,7 @@ } }, { - "context": "MessageEditor > Editor", + "context": "MessageEditor > Editor && !use_modifier_to_send", "bindings": { "enter": "agent::Chat", "ctrl-enter": "agent::ChatWithFollow", @@ -287,6 +287,17 @@ "ctrl-shift-n": "agent::RejectAll" } }, + { + "context": "MessageEditor > Editor && use_modifier_to_send", + "bindings": { + "ctrl-enter": "agent::Chat", + "enter": "editor::Newline", + "ctrl-i": "agent::ToggleProfileSelector", + "shift-ctrl-r": "agent::OpenAgentDiff", + "ctrl-shift-y": "agent::KeepAll", + "ctrl-shift-n": "agent::RejectAll" + } + }, { "context": "EditMessageEditor > Editor", "bindings": { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 748deaa05d5be88c9a1e4f79d6b4eb03dedbd028..70edee8534bbaa3619d4bee83761bab3669d926d 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -318,7 +318,7 @@ } }, { - "context": "MessageEditor > Editor", + "context": "MessageEditor > Editor && !use_modifier_to_send", "use_key_equivalents": true, "bindings": { "enter": "agent::Chat", @@ -329,6 +329,18 @@ "cmd-shift-n": "agent::RejectAll" } }, + { + "context": "MessageEditor > Editor && use_modifier_to_send", + "use_key_equivalents": true, + "bindings": { + "cmd-enter": "agent::Chat", + "enter": "editor::Newline", + "cmd-i": "agent::ToggleProfileSelector", + "shift-ctrl-r": "agent::OpenAgentDiff", + "cmd-shift-y": "agent::KeepAll", + "cmd-shift-n": "agent::RejectAll" + } + }, { "context": "EditMessageEditor > Editor", "use_key_equivalents": true, diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index 131cd2dc3f3e4e8967c03cbf1e808ebdeee306cf..13b966608c096ef7048c442096e55c36b64553b6 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -69,6 +69,7 @@ pub struct AgentSettings { pub enable_feedback: bool, pub expand_edit_card: bool, pub expand_terminal_card: bool, + pub use_modifier_to_send: bool, } impl AgentSettings { @@ -174,6 +175,10 @@ impl AgentSettingsContent { self.single_file_review = Some(allow); } + pub fn set_use_modifier_to_send(&mut self, always_use: bool) { + self.use_modifier_to_send = Some(always_use); + } + pub fn set_profile(&mut self, profile_id: AgentProfileId) { self.default_profile = Some(profile_id); } @@ -301,6 +306,10 @@ pub struct AgentSettingsContent { /// /// Default: true expand_terminal_card: Option<bool>, + /// Whether to always use cmd-enter (or ctrl-enter on Linux) to send messages in the agent panel. + /// + /// Default: false + use_modifier_to_send: Option<bool>, } #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)] @@ -456,6 +465,10 @@ impl Settings for AgentSettings { &mut settings.expand_terminal_card, value.expand_terminal_card, ); + merge( + &mut settings.use_modifier_to_send, + value.use_modifier_to_send, + ); settings .model_parameters diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index 6967c8ab3ee0d928cd094c26b844c66069755243..ce9cc87fe3f0f3434fac744ec4b9caca8fef11b7 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -28,8 +28,8 @@ use fs::Fs; use futures::future::Shared; use futures::{FutureExt as _, future}; use gpui::{ - Animation, AnimationExt, App, Entity, EventEmitter, Focusable, Subscription, Task, TextStyle, - WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between, + Animation, AnimationExt, App, Entity, EventEmitter, Focusable, KeyContext, Subscription, Task, + TextStyle, WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between, }; use language::{Buffer, Language, Point}; use language_model::{ @@ -132,6 +132,7 @@ pub(crate) fn create_editor( placement: Some(ContextMenuPlacement::Above), }); editor.register_addon(ContextCreasesAddon::new()); + editor.register_addon(MessageEditorAddon::new()); editor }); @@ -1494,6 +1495,31 @@ pub struct ContextCreasesAddon { _subscription: Option<Subscription>, } +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"); + } + } +} + impl Addon for ContextCreasesAddon { fn to_any(&self) -> &dyn std::any::Any { self From fd64ee1bb6756cc41967a053d83ec0ca3519adcd Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Fri, 18 Jul 2025 14:09:28 -0400 Subject: [PATCH 199/658] keymap ui: Fix remove key mapping bug (#34683) Release Notes: - N/A --------- Co-authored-by: Ben Kunkle <ben@zed.dev> --- crates/settings/src/keymap_file.rs | 40 ++++++++++++++++++++++++++++ crates/settings/src/settings_json.rs | 7 +++-- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 3a01d889c47f88216f2271025bcf782828accae5..67e8f7e7b2a503ce037c75745b2656c968f9b897 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -1623,4 +1623,44 @@ mod tests { .unindent(), ); } + + #[test] + fn test_keymap_remove() { + zlog::init_test(); + + check_keymap_update( + r#" + [ + { + "context": "Editor", + "bindings": { + "cmd-k cmd-u": "editor::ConvertToUpperCase", + "cmd-k cmd-l": "editor::ConvertToLowerCase", + "cmd-[": "pane::GoBack", + } + }, + ] + "#, + KeybindUpdateOperation::Remove { + target: KeybindUpdateTarget { + context: Some("Editor"), + keystrokes: &parse_keystrokes("cmd-k cmd-l"), + action_name: "editor::ConvertToLowerCase", + action_arguments: None, + }, + target_keybind_source: KeybindSource::User, + }, + r#" + [ + { + "context": "Editor", + "bindings": { + "cmd-k cmd-u": "editor::ConvertToUpperCase", + "cmd-[": "pane::GoBack", + } + }, + ] + "#, + ); + } } diff --git a/crates/settings/src/settings_json.rs b/crates/settings/src/settings_json.rs index a448eb27375645b63247703affe25f2524164b8b..e6683857e778e0c9cd052fc9f72407ee5d7787be 100644 --- a/crates/settings/src/settings_json.rs +++ b/crates/settings/src/settings_json.rs @@ -190,6 +190,7 @@ fn replace_value_in_json_text( } } + let mut removed_comma = false; // Look backward for a preceding comma first let preceding_text = text.get(0..removal_start).unwrap_or(""); if let Some(comma_pos) = preceding_text.rfind(',') { @@ -197,10 +198,12 @@ fn replace_value_in_json_text( let between_comma_and_key = text.get(comma_pos + 1..removal_start).unwrap_or(""); if between_comma_and_key.trim().is_empty() { removal_start = comma_pos; + removed_comma = true; } } - - if let Some(remaining_text) = text.get(existing_value_range.end..) { + if let Some(remaining_text) = text.get(existing_value_range.end..) + && !removed_comma + { let mut chars = remaining_text.char_indices(); while let Some((offset, ch)) = chars.next() { if ch == ',' { From 7b6b75b63fbcdd42e42394732e0fa2e1962512ea Mon Sep 17 00:00:00 2001 From: Peter Tripp <peter@zed.dev> Date: Fri, 18 Jul 2025 14:31:08 -0400 Subject: [PATCH 200/658] ci: Skip generating Windows release artifacts (#34704) Release Notes: - N/A --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 98b70ad834808e2814e3db359e0fe5e6458d2364..6987022f7a14ca39fcd188d5ad7b56469bf1d121 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -748,7 +748,7 @@ jobs: timeout-minutes: 120 name: Create a Windows installer runs-on: [self-hosted, Windows, X64] - if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') }} + if: false && (startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling')) needs: [windows_tests] env: AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }} @@ -787,7 +787,7 @@ jobs: - name: Upload Artifacts to release uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1 # Re-enable when we are ready to publish windows preview releases - if: false && ${{ !(contains(github.event.pull_request.labels.*.name, 'run-bundling')) && env.RELEASE_CHANNEL == 'preview' }} # upload only preview + if: ${{ !(contains(github.event.pull_request.labels.*.name, 'run-bundling')) && env.RELEASE_CHANNEL == 'preview' }} # upload only preview with: draft: true prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }} From d197c96cdc83d2e3b163784944296b377d3c9f42 Mon Sep 17 00:00:00 2001 From: Michael Sloan <michael@zed.dev> Date: Fri, 18 Jul 2025 12:58:55 -0600 Subject: [PATCH 201/658] Add stripe-mock to docker compose configuration (#34732) Release Notes: - N/A --- compose.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/compose.yml b/compose.yml index 4cd4c86df646fdd142df1206d18d78f7a4267083..06e8806d200edf31ae1a3b045112377866a6257a 100644 --- a/compose.yml +++ b/compose.yml @@ -59,5 +59,11 @@ services: depends_on: - postgres + stripe-mock: + image: stripe/stripe-mock:v0.184.0 + ports: + - 12111:12111 + - 12112:12112 + volumes: postgres_data: From 43486c416c2be9a25322c52c71b2ed802593bcd4 Mon Sep 17 00:00:00 2001 From: Conrad Irwin <conrad.irwin@gmail.com> Date: Fri, 18 Jul 2025 13:04:05 -0600 Subject: [PATCH 202/658] Fix enter in branch view (#34731) Broken by #34664 Release Notes: - N/A --- assets/keymaps/default-linux.json | 2 +- assets/keymaps/default-macos.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 8e670314853bb341612bf92a0e256024f2815b27..6aba27fec8fee9ed601e6bae43d575a5d050f95b 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -928,7 +928,7 @@ } }, { - "context": "GitPanel > Editor", + "context": "CommitEditor > Editor", "bindings": { "escape": "git_panel::FocusChanges", "tab": "git_panel::FocusChanges", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 70edee8534bbaa3619d4bee83761bab3669d926d..6bce3b0f28ce1a0b8ee42a32df521a042b880701 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -975,7 +975,7 @@ } }, { - "context": "GitPanel > Editor", + "context": "CommitEditor > Editor", "use_key_equivalents": true, "bindings": { "enter": "editor::Newline", From 70bde54a2c861310eeb005e177019c96947dff09 Mon Sep 17 00:00:00 2001 From: Peter Tripp <peter@zed.dev> Date: Fri, 18 Jul 2025 15:15:07 -0400 Subject: [PATCH 203/658] ci: Lint GitHub Actions workflows with actionlint (#34729) Added [rhysd/actionlint](https://github.com/rhysd/actionlint/) a static checker for GitHub Actions workflow files. Install locally with `brew install actionlint` the run with `actionlint`. Inspired by: https://github.com/zed-industries/zed/pull/34704 which yielded this observation: > In github actions: > 1. strings are truthy > 2. `${{ }}` will become a string if it doesn't wrap the whole value. > > So `if: false && true` becomes `false` > and `if: ${{ false && true }}` becomes `false` > but `if: false && ${{ true }}` becomes `"false && true"` which evaluates true > The reason you sometimes need `${{ }}` is because YAML doesn't like `!` > so `if: !false` is invalid yaml > and `if: ${{ !false }}` works just fine. Changes: - Add `actionlint` job - Refactor `job_spec` job to be more readable - Fix all `actionlint` and `shellcheck` errors in Actions workflows (62 in all) - Add `self-mini-macos` and `self-32vcpu-windows-2022` labels to self-hosted runners. Not strictly related, but useful if you need to take a runner out of the rotation (since `macOS`, `self-hosted`, and `ARM64` are auto-set and cannot be added/removed). - Change ci.yml macos_relase to target `self-mini-macos` instead of `bundle` which was previously deprecated. This would've caught the error fixed in https://github.com/zed-industries/zed/pull/34704. Here's what that [job failure](https://github.com/zed-industries/zed/actions/runs/16376993944/job/46279281842?pr=34729) would've looked like. Release Notes: - N/A --- .github/actionlint.yml | 30 ++++++ .github/workflows/bump_patch_version.yml | 8 +- .github/workflows/ci.yml | 100 +++++++++++------- .../workflows/community_release_actions.yml | 6 +- .github/workflows/deploy_collab.yml | 9 +- .github/workflows/eval.yml | 2 +- .github/workflows/nix.yml | 10 +- .github/workflows/release_nightly.yml | 5 +- .github/workflows/unit_evals.yml | 2 +- 9 files changed, 111 insertions(+), 61 deletions(-) create mode 100644 .github/actionlint.yml diff --git a/.github/actionlint.yml b/.github/actionlint.yml new file mode 100644 index 0000000000000000000000000000000000000000..d93ec5b15efb56b58efacc950032ca7e1003d8dd --- /dev/null +++ b/.github/actionlint.yml @@ -0,0 +1,30 @@ +# Configuration related to self-hosted runner. +self-hosted-runner: + # Labels of self-hosted runner in array of strings. + labels: + # GitHub-hosted Runners + - github-8vcpu-ubuntu-2404 + - github-16vcpu-ubuntu-2404 + - 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 + - buildjet-64vcpu-ubuntu-2204-arm + # 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 02857a9151cc3ea88914113f9c792bb2d6b7a811..8a48ff96f1a0e7b0926d8f528bc465d3fb315379 100644 --- a/.github/workflows/bump_patch_version.yml +++ b/.github/workflows/bump_patch_version.yml @@ -28,7 +28,7 @@ jobs: run: | set -eux - channel=$(cat crates/zed/RELEASE_CHANNEL) + channel="$(cat crates/zed/RELEASE_CHANNEL)" tag_suffix="" case $channel in @@ -43,9 +43,9 @@ jobs: ;; esac which cargo-set-version > /dev/null || cargo install cargo-edit - output=$(cargo set-version -p zed --bump patch 2>&1 | sed 's/.* //') + output="$(cargo set-version -p zed --bump patch 2>&1 | sed 's/.* //')" export GIT_COMMITTER_NAME="Zed Bot" export GIT_COMMITTER_EMAIL="hi@zed.dev" git commit -am "Bump to $output for @$GITHUB_ACTOR" --author "Zed Bot <hi@zed.dev>" - git tag v${output}${tag_suffix} - git push origin HEAD v${output}${tag_suffix} + git tag "v${output}${tag_suffix}" + git push origin HEAD "v${output}${tag_suffix}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6987022f7a14ca39fcd188d5ad7b56469bf1d121..a4da5e99ba214980a678efef0c6d6d2e48b0f499 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,6 +34,7 @@ jobs: run_license: ${{ steps.filter.outputs.run_license }} run_docs: ${{ steps.filter.outputs.run_docs }} run_nix: ${{ steps.filter.outputs.run_nix }} + run_actionlint: ${{ steps.filter.outputs.run_actionlint }} runs-on: - ubuntu-latest steps: @@ -47,39 +48,40 @@ jobs: run: | if [ -z "$GITHUB_BASE_REF" ]; then echo "Not in a PR context (i.e., push to main/stable/preview)" - COMPARE_REV=$(git rev-parse HEAD~1) + COMPARE_REV="$(git rev-parse HEAD~1)" else echo "In a PR context comparing to pull_request.base.ref" git fetch origin "$GITHUB_BASE_REF" --depth=350 - COMPARE_REV=$(git merge-base "origin/${GITHUB_BASE_REF}" HEAD) + COMPARE_REV="$(git merge-base "origin/${GITHUB_BASE_REF}" HEAD)" fi - # Specify anything which should skip full CI in this regex: + CHANGED_FILES="$(git diff --name-only "$COMPARE_REV" ${{ github.sha }})" + + # Specify anything which should potentially skip full test suite in this regex: # - docs/ # - script/update_top_ranking_issues/ # - .github/ISSUE_TEMPLATE/ # - .github/workflows/ (except .github/workflows/ci.yml) SKIP_REGEX='^(docs/|script/update_top_ranking_issues/|\.github/(ISSUE_TEMPLATE|workflows/(?!ci)))' - if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep -vP "$SKIP_REGEX") ]]; then - echo "run_tests=true" >> $GITHUB_OUTPUT - else - echo "run_tests=false" >> $GITHUB_OUTPUT - fi - if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep '^docs/') ]]; then - echo "run_docs=true" >> $GITHUB_OUTPUT - else - echo "run_docs=false" >> $GITHUB_OUTPUT - fi - if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep -P '^(Cargo.lock|script/.*licenses)') ]]; then - echo "run_license=true" >> $GITHUB_OUTPUT - else - echo "run_license=false" >> $GITHUB_OUTPUT - fi - NIX_REGEX='^(nix/|flake\.|Cargo\.|rust-toolchain.toml|\.cargo/config.toml)' - if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep -P "$NIX_REGEX") ]]; then - echo "run_nix=true" >> $GITHUB_OUTPUT - else - echo "run_nix=false" >> $GITHUB_OUTPUT - fi + + echo "$CHANGED_FILES" | grep -qvP "$SKIP_REGEX" && \ + echo "run_tests=true" >> "$GITHUB_OUTPUT" || \ + echo "run_tests=false" >> "$GITHUB_OUTPUT" + + echo "$CHANGED_FILES" | grep -qP '^docs/' && \ + echo "run_docs=true" >> "$GITHUB_OUTPUT" || \ + echo "run_docs=false" >> "$GITHUB_OUTPUT" + + echo "$CHANGED_FILES" | grep -qP '^\.github/(workflows/|actions/|actionlint.yml)' && \ + echo "run_actionlint=true" >> "$GITHUB_OUTPUT" || \ + echo "run_actionlint=false" >> "$GITHUB_OUTPUT" + + echo "$CHANGED_FILES" | grep -qP '^(Cargo.lock|script/.*licenses)' && \ + echo "run_license=true" >> "$GITHUB_OUTPUT" || \ + echo "run_license=false" >> "$GITHUB_OUTPUT" + + echo "$CHANGED_FILES" | grep -qP '^(nix/|flake\.|Cargo\.|rust-toolchain.toml|\.cargo/config.toml)' && \ + echo "run_nix=true" >> "$GITHUB_OUTPUT" || \ + echo "run_nix=false" >> "$GITHUB_OUTPUT" migration_checks: name: Check Postgres and Protobuf migrations, mergability @@ -89,8 +91,7 @@ jobs: needs.job_spec.outputs.run_tests == 'true' timeout-minutes: 60 runs-on: - - self-hosted - - macOS + - self-mini-macos steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -112,11 +113,11 @@ jobs: run: | if [ -z "$GITHUB_BASE_REF" ]; then - echo "BUF_BASE_BRANCH=$(git merge-base origin/main HEAD)" >> $GITHUB_ENV + echo "BUF_BASE_BRANCH=$(git merge-base origin/main HEAD)" >> "$GITHUB_ENV" else git checkout -B temp - git merge -q origin/$GITHUB_BASE_REF -m "merge main into temp" - echo "BUF_BASE_BRANCH=$GITHUB_BASE_REF" >> $GITHUB_ENV + git merge -q "origin/$GITHUB_BASE_REF" -m "merge main into temp" + echo "BUF_BASE_BRANCH=$GITHUB_BASE_REF" >> "$GITHUB_ENV" fi - uses: bufbuild/buf-setup-action@v1 @@ -140,7 +141,7 @@ jobs: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - name: Add Rust to the PATH - run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH + run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" - name: Install cargo-hakari uses: clechasseur/rs-cargo@8435b10f6e71c2e3d4d3b7573003a8ce4bfc6386 # v2 with: @@ -178,7 +179,7 @@ jobs: - name: Prettier Check on /docs working-directory: ./docs run: | - pnpm dlx prettier@${PRETTIER_VERSION} . --check || { + pnpm dlx "prettier@${PRETTIER_VERSION}" . --check || { echo "To fix, run from the root of the Zed repo:" echo " cd docs && pnpm dlx prettier@${PRETTIER_VERSION} . --write && cd .." false @@ -188,7 +189,7 @@ jobs: - name: Prettier Check on default.json run: | - pnpm dlx prettier@${PRETTIER_VERSION} assets/settings/default.json --check || { + pnpm dlx "prettier@${PRETTIER_VERSION}" assets/settings/default.json --check || { echo "To fix, run from the root of the Zed repo:" echo " pnpm dlx prettier@${PRETTIER_VERSION} assets/settings/default.json --write" false @@ -234,6 +235,20 @@ jobs: - name: Build docs uses: ./.github/actions/build_docs + actionlint: + runs-on: ubuntu-latest + if: github.repository_owner == 'zed-industries' && needs.job_spec.outputs.run_actionlint == 'true' + needs: [job_spec] + steps: + - uses: actions/checkout@v4 + - name: Download actionlint + id: get_actionlint + run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) + shell: bash + - name: Check workflow files + run: ${{ steps.get_actionlint.outputs.executable }} -color + shell: bash + macos_tests: timeout-minutes: 60 name: (macOS) Run Clippy and tests @@ -242,8 +257,7 @@ jobs: github.repository_owner == 'zed-industries' && needs.job_spec.outputs.run_tests == 'true' runs-on: - - self-hosted - - macOS + - self-mini-macos steps: - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -312,7 +326,7 @@ jobs: - buildjet-16vcpu-ubuntu-2204 steps: - name: Add Rust to the PATH - run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH + run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -364,7 +378,7 @@ jobs: - buildjet-8vcpu-ubuntu-2204 steps: - name: Add Rust to the PATH - run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH + run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 @@ -444,6 +458,7 @@ jobs: - job_spec - style - check_docs + - actionlint - migration_checks # run_tests: If adding required tests, add them here and to script below. - workspace_hack @@ -465,6 +480,11 @@ jobs: if [[ "${{ needs.job_spec.outputs.run_docs }}" == "true" ]]; then [[ "${{ needs.check_docs.result }}" != 'success' ]] && { RET_CODE=1; echo "docs checks failed"; } fi + + if [[ "${{ needs.job_spec.outputs.run_actionlint }}" == "true" ]]; then + [[ "${{ needs.actionlint.result }}" != 'success' ]] && { RET_CODE=1; echo "actionlint checks failed"; } + fi + # Only check test jobs if they were supposed to run if [[ "${{ needs.job_spec.outputs.run_tests }}" == "true" ]]; then [[ "${{ needs.workspace_hack.result }}" != 'success' ]] && { RET_CODE=1; echo "Workspace Hack failed"; } @@ -484,8 +504,7 @@ jobs: timeout-minutes: 120 name: Create a macOS bundle runs-on: - - self-hosted - - bundle + - self-mini-macos if: | startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling') @@ -802,10 +821,9 @@ jobs: && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre') needs: [bundle-mac, bundle-linux-x86_x64, bundle-linux-aarch64, bundle-windows-x64] runs-on: - - self-hosted - - bundle + - self-mini-macos steps: - name: gh release - run: gh release edit $GITHUB_REF_NAME --draft=false + run: gh release edit "$GITHUB_REF_NAME" --draft=false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/community_release_actions.yml b/.github/workflows/community_release_actions.yml index 3e253978b7a54c2a12ea281f84f0d3dbf45a8561..31dda1fa6d005ee16eb9d13aec6277ebf9a3ab94 100644 --- a/.github/workflows/community_release_actions.yml +++ b/.github/workflows/community_release_actions.yml @@ -18,7 +18,7 @@ jobs: URL="https://zed.dev/releases/stable/latest" fi - echo "URL=$URL" >> $GITHUB_OUTPUT + echo "URL=$URL" >> "$GITHUB_OUTPUT" - name: Get content uses: 2428392/gh-truncate-string-action@b3ff790d21cf42af3ca7579146eedb93c8fb0757 # v1.4.1 id: get-content @@ -50,9 +50,9 @@ jobs: PREVIEW_TAG="${VERSION}-pre" if git rev-parse "$PREVIEW_TAG" > /dev/null 2>&1; then - echo "was_promoted_from_preview=true" >> $GITHUB_OUTPUT + echo "was_promoted_from_preview=true" >> "$GITHUB_OUTPUT" else - echo "was_promoted_from_preview=false" >> $GITHUB_OUTPUT + echo "was_promoted_from_preview=false" >> "$GITHUB_OUTPUT" fi - name: Send release notes email diff --git a/.github/workflows/deploy_collab.yml b/.github/workflows/deploy_collab.yml index cfd455f92092d773dc68fccf08c39fe7d5147c0f..f7348a10693ffd5cc1ea94f8b6bb26e430d2f59f 100644 --- a/.github/workflows/deploy_collab.yml +++ b/.github/workflows/deploy_collab.yml @@ -79,12 +79,12 @@ jobs: - name: Build docker image run: | docker build -f Dockerfile-collab \ - --build-arg GITHUB_SHA=$GITHUB_SHA \ - --tag registry.digitalocean.com/zed/collab:$GITHUB_SHA \ + --build-arg "GITHUB_SHA=$GITHUB_SHA" \ + --tag "registry.digitalocean.com/zed/collab:$GITHUB_SHA" \ . - name: Publish docker image - run: docker push registry.digitalocean.com/zed/collab:${GITHUB_SHA} + run: docker push "registry.digitalocean.com/zed/collab:${GITHUB_SHA}" - name: Prune Docker system run: docker system prune --filter 'until=72h' -f @@ -131,7 +131,8 @@ jobs: source script/lib/deploy-helpers.sh export_vars_for_environment $ZED_KUBE_NAMESPACE - export ZED_DO_CERTIFICATE_ID=$(doctl compute certificate list --format ID --no-header) + ZED_DO_CERTIFICATE_ID="$(doctl compute certificate list --format ID --no-header)" + export ZED_DO_CERTIFICATE_ID export ZED_IMAGE_ID="registry.digitalocean.com/zed/collab:${GITHUB_SHA}" export ZED_SERVICE_NAME=collab diff --git a/.github/workflows/eval.yml b/.github/workflows/eval.yml index 6eefdfea954c58919850baabe013d3d8676b54f9..2ad302a602200587e245511841e4b59ee89b6b5d 100644 --- a/.github/workflows/eval.yml +++ b/.github/workflows/eval.yml @@ -35,7 +35,7 @@ jobs: - buildjet-16vcpu-ubuntu-2204 steps: - name: Add Rust to the PATH - run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH + run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 155fc484f57b593dbdca1811f571d97384ceb3c0..beacd277743f963ce9acdf1edcde774738c4e909 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -43,8 +43,8 @@ jobs: - name: Set path if: ${{ ! matrix.system.install_nix }} run: | - echo "/nix/var/nix/profiles/default/bin" >> $GITHUB_PATH - echo "/Users/administrator/.nix-profile/bin" >> $GITHUB_PATH + echo "/nix/var/nix/profiles/default/bin" >> "$GITHUB_PATH" + echo "/Users/administrator/.nix-profile/bin" >> "$GITHUB_PATH" - uses: cachix/install-nix-action@02a151ada4993995686f9ed4f1be7cfbb229e56f # v31 if: ${{ matrix.system.install_nix }} @@ -56,11 +56,13 @@ jobs: name: zed authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" pushFilter: "${{ inputs.cachix-filter }}" - cachixArgs: '-v' + cachixArgs: "-v" - run: nix build .#${{ inputs.flake-output }} -L --accept-flake-config - name: Limit /nix/store to 50GB on macs if: ${{ ! matrix.system.install_nix }} run: | - [ $(du -sm /nix/store | cut -f1) -gt 50000 ] && nix-collect-garbage -d || : + if [ "$(du -sm /nix/store | cut -f1)" -gt 50000 ]; then + nix-collect-garbage -d || true + fi diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index 4be20525f97039bfa55f362aeffe9863f378df8d..f799133ea700af96f61a8301a30ab2ac4b77fe8b 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -85,8 +85,7 @@ jobs: name: Create a macOS bundle if: github.repository_owner == 'zed-industries' runs-on: - - self-hosted - - bundle + - self-mini-macos needs: tests env: MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} @@ -132,7 +131,7 @@ jobs: clean: false - name: Add Rust to the PATH - run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH + run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" - name: Install Linux dependencies run: ./script/linux && ./script/install-mold 2.34.0 diff --git a/.github/workflows/unit_evals.yml b/.github/workflows/unit_evals.yml index 705caff37afcba6cfb1f303b28559d1425147437..cb4e39d151971c839242adfab7b47bd9971096ef 100644 --- a/.github/workflows/unit_evals.yml +++ b/.github/workflows/unit_evals.yml @@ -26,7 +26,7 @@ jobs: - buildjet-16vcpu-ubuntu-2204 steps: - name: Add Rust to the PATH - run: echo "$HOME/.cargo/bin" >> $GITHUB_PATH + run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 From 4bdac8026c6334f88777ab1f69145e124292a5d0 Mon Sep 17 00:00:00 2001 From: morgankrey <morgan@zed.dev> Date: Fri, 18 Jul 2025 16:42:48 -0500 Subject: [PATCH 204/658] collab: Enable automatic tax calculation for all new subscriptions (#34720) Release Notes: - N/A --------- Co-authored-by: Marshall Bowers <git@maxdeviant.com> --- crates/collab/src/stripe_billing.rs | 5 ++-- crates/collab/src/stripe_client.rs | 6 +++++ .../src/stripe_client/real_stripe_client.rs | 23 ++++++++++++++----- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/crates/collab/src/stripe_billing.rs b/crates/collab/src/stripe_billing.rs index fdd9653d7cd4cbfeb63e65892c7ccce312c97d97..3d52dea0e3b08b42f97fcf1017f03503deb21e7a 100644 --- a/crates/collab/src/stripe_billing.rs +++ b/crates/collab/src/stripe_billing.rs @@ -11,8 +11,8 @@ use crate::Result; use crate::db::billing_subscription::SubscriptionKind; use crate::llm::AGENT_EXTENDED_TRIAL_FEATURE_FLAG; use crate::stripe_client::{ - RealStripeClient, StripeBillingAddressCollection, StripeCheckoutSessionMode, - StripeCheckoutSessionPaymentMethodCollection, StripeClient, + RealStripeClient, StripeAutomaticTax, StripeBillingAddressCollection, + StripeCheckoutSessionMode, StripeCheckoutSessionPaymentMethodCollection, StripeClient, StripeCreateCheckoutSessionLineItems, StripeCreateCheckoutSessionParams, StripeCreateCheckoutSessionSubscriptionData, StripeCreateMeterEventParams, StripeCreateMeterEventPayload, StripeCreateSubscriptionItems, StripeCreateSubscriptionParams, @@ -344,6 +344,7 @@ impl StripeBilling { price: Some(zed_free_price_id), quantity: Some(1), }], + automatic_tax: Some(StripeAutomaticTax { enabled: true }), }; let subscription = self.client.create_subscription(params).await?; diff --git a/crates/collab/src/stripe_client.rs b/crates/collab/src/stripe_client.rs index ec947e12f792661578f9a8a675a0017f321e8fc4..6e75a4d874bf41e7cb4418d4b56cfeb6040e5ff8 100644 --- a/crates/collab/src/stripe_client.rs +++ b/crates/collab/src/stripe_client.rs @@ -73,6 +73,7 @@ pub enum StripeCancellationDetailsReason { pub struct StripeCreateSubscriptionParams { pub customer: StripeCustomerId, pub items: Vec<StripeCreateSubscriptionItems>, + pub automatic_tax: Option<StripeAutomaticTax>, } #[derive(Debug)] @@ -224,6 +225,11 @@ pub struct StripeTaxIdCollection { pub enabled: bool, } +#[derive(Debug, Clone)] +pub struct StripeAutomaticTax { + pub enabled: bool, +} + #[derive(Debug)] pub struct StripeCheckoutSession { pub url: Option<String>, diff --git a/crates/collab/src/stripe_client/real_stripe_client.rs b/crates/collab/src/stripe_client/real_stripe_client.rs index 07dde68d179d8650ef902ae07b05015bf5aa2633..07c191ff30400ccbf4b73c4c84f09aa47e0fd9aa 100644 --- a/crates/collab/src/stripe_client/real_stripe_client.rs +++ b/crates/collab/src/stripe_client/real_stripe_client.rs @@ -10,16 +10,17 @@ use stripe::{ CreateCheckoutSessionSubscriptionData, CreateCheckoutSessionSubscriptionDataTrialSettings, CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehavior, CreateCheckoutSessionSubscriptionDataTrialSettingsEndBehaviorMissingPaymentMethod, - CreateCustomer, Customer, CustomerId, ListCustomers, Price, PriceId, Recurring, Subscription, - SubscriptionId, SubscriptionItem, SubscriptionItemId, UpdateCustomer, UpdateSubscriptionItems, - UpdateSubscriptionTrialSettings, UpdateSubscriptionTrialSettingsEndBehavior, + CreateCustomer, CreateSubscriptionAutomaticTax, Customer, CustomerId, ListCustomers, Price, + PriceId, Recurring, Subscription, SubscriptionId, SubscriptionItem, SubscriptionItemId, + UpdateCustomer, UpdateSubscriptionItems, UpdateSubscriptionTrialSettings, + UpdateSubscriptionTrialSettingsEndBehavior, UpdateSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, }; use crate::stripe_client::{ - CreateCustomerParams, StripeBillingAddressCollection, StripeCancellationDetails, - StripeCancellationDetailsReason, StripeCheckoutSession, StripeCheckoutSessionMode, - StripeCheckoutSessionPaymentMethodCollection, StripeClient, + CreateCustomerParams, StripeAutomaticTax, StripeBillingAddressCollection, + StripeCancellationDetails, StripeCancellationDetailsReason, StripeCheckoutSession, + StripeCheckoutSessionMode, StripeCheckoutSessionPaymentMethodCollection, StripeClient, StripeCreateCheckoutSessionLineItems, StripeCreateCheckoutSessionParams, StripeCreateCheckoutSessionSubscriptionData, StripeCreateMeterEventParams, StripeCreateSubscriptionParams, StripeCustomer, StripeCustomerId, StripeCustomerUpdate, @@ -151,6 +152,7 @@ impl StripeClient for RealStripeClient { }) .collect(), ); + create_subscription.automatic_tax = params.automatic_tax.map(Into::into); let subscription = Subscription::create(&self.client, create_subscription).await?; @@ -366,6 +368,15 @@ impl From<SubscriptionItem> for StripeSubscriptionItem { } } +impl From<StripeAutomaticTax> for CreateSubscriptionAutomaticTax { + fn from(value: StripeAutomaticTax) -> Self { + Self { + enabled: value.enabled, + liability: None, + } + } +} + impl From<StripeSubscriptionTrialSettings> for UpdateSubscriptionTrialSettings { fn from(value: StripeSubscriptionTrialSettings) -> Self { Self { From 2da2ae65a07dbace600e301b63640ac07ab61289 Mon Sep 17 00:00:00 2001 From: Mikayla Maki <mikayla@zed.dev> Date: Fri, 18 Jul 2025 18:27:54 -0700 Subject: [PATCH 205/658] gpui: Add use state APIs (#34741) This PR adds a component level state API to GPUI, as well as a few utilities for simplified interactions with entities Release Notes: - N/A --- crates/eval/src/example.rs | 7 ++ crates/gpui/src/app.rs | 96 +++++++++++++++++++- crates/gpui/src/app/async_context.rs | 20 +++- crates/gpui/src/app/context.rs | 7 ++ crates/gpui/src/app/entity_map.rs | 32 +++++-- crates/gpui/src/app/test_context.rs | 21 +++++ crates/gpui/src/element.rs | 29 +++--- crates/gpui/src/gpui.rs | 5 + crates/gpui/src/window.rs | 50 ++++++++++ crates/gpui_macros/src/derive_app_context.rs | 10 ++ 10 files changed, 252 insertions(+), 25 deletions(-) diff --git a/crates/eval/src/example.rs b/crates/eval/src/example.rs index 09770364cb6b460a4ce8d61d76bcc833cb466129..7ce3b1fdf101e8f5c792f62e15b272b38477b2cf 100644 --- a/crates/eval/src/example.rs +++ b/crates/eval/src/example.rs @@ -422,6 +422,13 @@ impl AppContext for ExampleContext { self.app.update_entity(handle, update) } + fn as_mut<'a, T>(&'a mut self, handle: &Entity<T>) -> Self::Result<gpui::GpuiBorrow<'a, T>> + where + T: 'static, + { + self.app.as_mut(handle) + } + fn read_entity<T, R>( &self, handle: &Entity<T>, diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 957c7c4be6e2b818c9a750f5b0e0037a29607eda..70e1d1e4cd0b0303bf487848fa308b29d3b69910 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -448,15 +448,23 @@ impl App { } pub(crate) fn update<R>(&mut self, update: impl FnOnce(&mut Self) -> R) -> R { - self.pending_updates += 1; + self.start_update(); let result = update(self); + self.finish_update(); + result + } + + pub(crate) fn start_update(&mut self) { + self.pending_updates += 1; + } + + pub(crate) fn finish_update(&mut self) { if !self.flushing_effects && self.pending_updates == 1 { self.flushing_effects = true; self.flush_effects(); self.flushing_effects = false; } self.pending_updates -= 1; - result } /// Arrange a callback to be invoked when the given entity calls `notify` on its respective context. @@ -868,7 +876,6 @@ impl App { loop { self.release_dropped_entities(); self.release_dropped_focus_handles(); - if let Some(effect) = self.pending_effects.pop_front() { match effect { Effect::Notify { emitter } => { @@ -1819,6 +1826,13 @@ impl AppContext for App { }) } + fn as_mut<'a, T>(&'a mut self, handle: &Entity<T>) -> GpuiBorrow<'a, T> + where + T: 'static, + { + GpuiBorrow::new(handle.clone(), self) + } + fn read_entity<T, R>( &self, handle: &Entity<T>, @@ -2015,3 +2029,79 @@ impl HttpClient for NullHttpClient { type_name::<Self>() } } + +/// A mutable reference to an entity owned by GPUI +pub struct GpuiBorrow<'a, T> { + inner: Option<Lease<T>>, + app: &'a mut App, +} + +impl<'a, T: 'static> GpuiBorrow<'a, T> { + fn new(inner: Entity<T>, app: &'a mut App) -> Self { + app.start_update(); + let lease = app.entities.lease(&inner); + Self { + inner: Some(lease), + app, + } + } +} + +impl<'a, T: 'static> std::borrow::Borrow<T> for GpuiBorrow<'a, T> { + fn borrow(&self) -> &T { + self.inner.as_ref().unwrap().borrow() + } +} + +impl<'a, T: 'static> std::borrow::BorrowMut<T> for GpuiBorrow<'a, T> { + fn borrow_mut(&mut self) -> &mut T { + self.inner.as_mut().unwrap().borrow_mut() + } +} + +impl<'a, T> Drop for GpuiBorrow<'a, T> { + fn drop(&mut self) { + let lease = self.inner.take().unwrap(); + self.app.notify(lease.id); + self.app.entities.end_lease(lease); + self.app.finish_update(); + } +} + +#[cfg(test)] +mod test { + use std::{cell::RefCell, rc::Rc}; + + use crate::{AppContext, TestAppContext}; + + #[test] + fn test_gpui_borrow() { + let cx = TestAppContext::single(); + let observation_count = Rc::new(RefCell::new(0)); + + let state = cx.update(|cx| { + let state = cx.new(|_| false); + cx.observe(&state, { + let observation_count = observation_count.clone(); + move |_, _| { + let mut count = observation_count.borrow_mut(); + *count += 1; + } + }) + .detach(); + + state + }); + + cx.update(|cx| { + // Calling this like this so that we don't clobber the borrow_mut above + *std::borrow::BorrowMut::borrow_mut(&mut state.as_mut(cx)) = true; + }); + + cx.update(|cx| { + state.write(cx, false); + }); + + assert_eq!(*observation_count.borrow(), 2); + } +} diff --git a/crates/gpui/src/app/async_context.rs b/crates/gpui/src/app/async_context.rs index c3b60dd580483771f683b6d76fd76e52b3f531ad..d9d21c024461cab68d62d685a40b61c9c74d46dd 100644 --- a/crates/gpui/src/app/async_context.rs +++ b/crates/gpui/src/app/async_context.rs @@ -3,7 +3,7 @@ use crate::{ Entity, EventEmitter, Focusable, ForegroundExecutor, Global, PromptButton, PromptLevel, Render, Reservation, Result, Subscription, Task, VisualContext, Window, WindowHandle, }; -use anyhow::Context as _; +use anyhow::{Context as _, anyhow}; use derive_more::{Deref, DerefMut}; use futures::channel::oneshot; use std::{future::Future, rc::Weak}; @@ -58,6 +58,15 @@ impl AppContext for AsyncApp { Ok(app.update_entity(handle, update)) } + fn as_mut<'a, T>(&'a mut self, _handle: &Entity<T>) -> Self::Result<super::GpuiBorrow<'a, T>> + where + T: 'static, + { + Err(anyhow!( + "Cannot as_mut with an async context. Try calling update() first" + )) + } + fn read_entity<T, R>( &self, handle: &Entity<T>, @@ -364,6 +373,15 @@ impl AppContext for AsyncWindowContext { .update(self, |_, _, cx| cx.update_entity(handle, update)) } + fn as_mut<'a, T>(&'a mut self, _: &Entity<T>) -> Self::Result<super::GpuiBorrow<'a, T>> + where + T: 'static, + { + Err(anyhow!( + "Cannot use as_mut() from an async context, call `update`" + )) + } + fn read_entity<T, R>( &self, handle: &Entity<T>, diff --git a/crates/gpui/src/app/context.rs b/crates/gpui/src/app/context.rs index 2d90ff35b1b47c44e19de15adad64b7b569a0ec1..392be2ffe9ce4eed9397a11770b7133db145a7a8 100644 --- a/crates/gpui/src/app/context.rs +++ b/crates/gpui/src/app/context.rs @@ -726,6 +726,13 @@ impl<T> AppContext for Context<'_, T> { self.app.update_entity(handle, update) } + fn as_mut<'a, E>(&'a mut self, handle: &Entity<E>) -> Self::Result<super::GpuiBorrow<'a, E>> + where + E: 'static, + { + self.app.as_mut(handle) + } + fn read_entity<U, R>( &self, handle: &Entity<U>, diff --git a/crates/gpui/src/app/entity_map.rs b/crates/gpui/src/app/entity_map.rs index f1aafa55e871567a58fe0696a4e84287e82bd437..d4e5b2570ed5851b47c8557638de47760bedba2f 100644 --- a/crates/gpui/src/app/entity_map.rs +++ b/crates/gpui/src/app/entity_map.rs @@ -1,4 +1,4 @@ -use crate::{App, AppContext, VisualContext, Window, seal::Sealed}; +use crate::{App, AppContext, GpuiBorrow, VisualContext, Window, seal::Sealed}; use anyhow::{Context as _, Result}; use collections::FxHashSet; use derive_more::{Deref, DerefMut}; @@ -105,7 +105,7 @@ impl EntityMap { /// Move an entity to the stack. #[track_caller] - pub fn lease<'a, T>(&mut self, pointer: &'a Entity<T>) -> Lease<'a, T> { + pub fn lease<T>(&mut self, pointer: &Entity<T>) -> Lease<T> { self.assert_valid_context(pointer); let mut accessed_entities = self.accessed_entities.borrow_mut(); accessed_entities.insert(pointer.entity_id); @@ -117,15 +117,14 @@ impl EntityMap { ); Lease { entity, - pointer, + id: pointer.entity_id, entity_type: PhantomData, } } /// Returns an entity after moving it to the stack. pub fn end_lease<T>(&mut self, mut lease: Lease<T>) { - self.entities - .insert(lease.pointer.entity_id, lease.entity.take().unwrap()); + self.entities.insert(lease.id, lease.entity.take().unwrap()); } pub fn read<T: 'static>(&self, entity: &Entity<T>) -> &T { @@ -187,13 +186,13 @@ fn double_lease_panic<T>(operation: &str) -> ! { ) } -pub(crate) struct Lease<'a, T> { +pub(crate) struct Lease<T> { entity: Option<Box<dyn Any>>, - pub pointer: &'a Entity<T>, + pub id: EntityId, entity_type: PhantomData<T>, } -impl<T: 'static> core::ops::Deref for Lease<'_, T> { +impl<T: 'static> core::ops::Deref for Lease<T> { type Target = T; fn deref(&self) -> &Self::Target { @@ -201,13 +200,13 @@ impl<T: 'static> core::ops::Deref for Lease<'_, T> { } } -impl<T: 'static> core::ops::DerefMut for Lease<'_, T> { +impl<T: 'static> core::ops::DerefMut for Lease<T> { fn deref_mut(&mut self) -> &mut Self::Target { self.entity.as_mut().unwrap().downcast_mut().unwrap() } } -impl<T> Drop for Lease<'_, T> { +impl<T> Drop for Lease<T> { fn drop(&mut self) { if self.entity.is_some() && !panicking() { panic!("Leases must be ended with EntityMap::end_lease") @@ -437,6 +436,19 @@ impl<T: 'static> Entity<T> { cx.update_entity(self, update) } + /// Updates the entity referenced by this handle with the given function. + pub fn as_mut<'a, C: AppContext>(&self, cx: &'a mut C) -> C::Result<GpuiBorrow<'a, T>> { + cx.as_mut(self) + } + + /// Updates the entity referenced by this handle with the given function. + pub fn write<C: AppContext>(&self, cx: &mut C, value: T) -> C::Result<()> { + self.update(cx, |entity, cx| { + *entity = value; + cx.notify(); + }) + } + /// Updates the entity referenced by this handle with the given function if /// the referenced entity still exists, within a visual context that has a window. /// Returns an error if the entity has been released. diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index dfc7af0d9c02ae08f7ff46b2400e3ebecc48f8ec..35e60326714f049faeaac54e8d979a91f9d97bbc 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -9,6 +9,7 @@ use crate::{ }; use anyhow::{anyhow, bail}; use futures::{Stream, StreamExt, channel::oneshot}; +use rand::{SeedableRng, rngs::StdRng}; use std::{cell::RefCell, future::Future, ops::Deref, rc::Rc, sync::Arc, time::Duration}; /// A TestAppContext is provided to tests created with `#[gpui::test]`, it provides @@ -63,6 +64,13 @@ impl AppContext for TestAppContext { app.update_entity(handle, update) } + fn as_mut<'a, T>(&'a mut self, _: &Entity<T>) -> Self::Result<super::GpuiBorrow<'a, T>> + where + T: 'static, + { + panic!("Cannot use as_mut with a test app context. Try calling update() first") + } + fn read_entity<T, R>( &self, handle: &Entity<T>, @@ -134,6 +142,12 @@ impl TestAppContext { } } + /// Create a single TestAppContext, for non-multi-client tests + pub fn single() -> Self { + let dispatcher = TestDispatcher::new(StdRng::from_entropy()); + Self::build(dispatcher, None) + } + /// The name of the test function that created this `TestAppContext` pub fn test_function_name(&self) -> Option<&'static str> { self.fn_name @@ -914,6 +928,13 @@ impl AppContext for VisualTestContext { self.cx.update_entity(handle, update) } + fn as_mut<'a, T>(&'a mut self, handle: &Entity<T>) -> Self::Result<super::GpuiBorrow<'a, T>> + where + T: 'static, + { + self.cx.as_mut(handle) + } + fn read_entity<T, R>( &self, handle: &Entity<T>, diff --git a/crates/gpui/src/element.rs b/crates/gpui/src/element.rs index 2852841b2c2b42ceceddaeebcf0b3abfa2684808..e5f49c7be141a3620e52599bcc2b151acc1f7319 100644 --- a/crates/gpui/src/element.rs +++ b/crates/gpui/src/element.rs @@ -39,7 +39,7 @@ use crate::{ use derive_more::{Deref, DerefMut}; pub(crate) use smallvec::SmallVec; use std::{ - any::Any, + any::{Any, type_name}, fmt::{self, Debug, Display}, mem, panic, }; @@ -220,14 +220,17 @@ impl<C: RenderOnce> Element for Component<C> { window: &mut Window, cx: &mut App, ) -> (LayoutId, Self::RequestLayoutState) { - let mut element = self - .component - .take() - .unwrap() - .render(window, cx) - .into_any_element(); - let layout_id = element.request_layout(window, cx); - (layout_id, element) + window.with_global_id(ElementId::Name(type_name::<C>().into()), |_, window| { + let mut element = self + .component + .take() + .unwrap() + .render(window, cx) + .into_any_element(); + + let layout_id = element.request_layout(window, cx); + (layout_id, element) + }) } fn prepaint( @@ -239,7 +242,9 @@ impl<C: RenderOnce> Element for Component<C> { window: &mut Window, cx: &mut App, ) { - element.prepaint(window, cx); + window.with_global_id(ElementId::Name(type_name::<C>().into()), |_, window| { + element.prepaint(window, cx); + }) } fn paint( @@ -252,7 +257,9 @@ impl<C: RenderOnce> Element for Component<C> { window: &mut Window, cx: &mut App, ) { - element.paint(window, cx); + window.with_global_id(ElementId::Name(type_name::<C>().into()), |_, window| { + element.paint(window, cx); + }) } } diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 91461a4d2c8f1bbf1504a36429064a038bedec21..4eb6fa8dabeb1476c779d36cbd61257faf431413 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -197,6 +197,11 @@ pub trait AppContext { where T: 'static; + /// Update a entity in the app context. + fn as_mut<'a, T>(&'a mut self, handle: &Entity<T>) -> Self::Result<GpuiBorrow<'a, T>> + where + T: 'static; + /// Read a entity from the app context. fn read_entity<T, R>( &self, diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 94f1b39ba20982cefd50ad149fbc50b40bc80cbf..b6601829c74e6e267c48fe1c5aa9f9ca681d2855 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -2424,6 +2424,53 @@ impl Window { result } + /// Use a piece of state that exists as long this element is being rendered in consecutive frames. + pub fn use_keyed_state<S: 'static>( + &mut self, + key: impl Into<ElementId>, + cx: &mut App, + init: impl FnOnce(&mut Self, &mut App) -> S, + ) -> Entity<S> { + let current_view = self.current_view(); + self.with_global_id(key.into(), |global_id, window| { + window.with_element_state(global_id, |state: Option<Entity<S>>, window| { + if let Some(state) = state { + (state.clone(), state) + } else { + let new_state = cx.new(|cx| init(window, cx)); + cx.observe(&new_state, move |_, cx| { + cx.notify(current_view); + }) + .detach(); + (new_state.clone(), new_state) + } + }) + }) + } + + /// Immediately push an element ID onto the stack. Useful for simplifying IDs in lists + pub fn with_id<R>(&mut self, id: impl Into<ElementId>, f: impl FnOnce(&mut Self) -> R) -> R { + self.with_global_id(id.into(), |_, window| f(window)) + } + + /// Use a piece of state that exists as long this element is being rendered in consecutive frames, without needing to specify a key + /// + /// NOTE: This method uses the location of the caller to generate an ID for this state. + /// If this is not sufficient to identify your state (e.g. you're rendering a list item), + /// you can provide a custom ElementID using the `use_keyed_state` method. + #[track_caller] + pub fn use_state<S: 'static>( + &mut self, + cx: &mut App, + init: impl FnOnce(&mut Self, &mut App) -> S, + ) -> Entity<S> { + self.use_keyed_state( + ElementId::CodeLocation(*core::panic::Location::caller()), + cx, + init, + ) + } + /// Updates or initializes state for an element with the given id that lives across multiple /// frames. If an element with this ID existed in the rendered frame, its state will be passed /// to the given closure. The state returned by the closure will be stored so it can be referenced @@ -4577,6 +4624,8 @@ pub enum ElementId { NamedInteger(SharedString, u64), /// A path. Path(Arc<std::path::Path>), + /// A code location. + CodeLocation(core::panic::Location<'static>), } impl ElementId { @@ -4596,6 +4645,7 @@ impl Display for ElementId { ElementId::NamedInteger(s, i) => write!(f, "{}-{}", s, i)?, ElementId::Uuid(uuid) => write!(f, "{}", uuid)?, ElementId::Path(path) => write!(f, "{}", path.display())?, + ElementId::CodeLocation(location) => write!(f, "{}", location)?, } Ok(()) diff --git a/crates/gpui_macros/src/derive_app_context.rs b/crates/gpui_macros/src/derive_app_context.rs index bca015b8dc5ab43c0a6873f03251d68e2d0b592b..d2dc250d0239769f6834860a128c2653546a926e 100644 --- a/crates/gpui_macros/src/derive_app_context.rs +++ b/crates/gpui_macros/src/derive_app_context.rs @@ -53,6 +53,16 @@ pub fn derive_app_context(input: TokenStream) -> TokenStream { self.#app_variable.update_entity(handle, update) } + fn as_mut<'y, 'z, T>( + &'y mut self, + handle: &'z gpui::Entity<T>, + ) -> Self::Result<gpui::GpuiBorrow<'y, T>> + where + T: 'static, + { + self.#app_variable.as_mut(handle) + } + fn read_entity<T, R>( &self, handle: &gpui::Entity<T>, From 0ffd93774cb9b21850e8480a2e57f69763923a0e Mon Sep 17 00:00:00 2001 From: Vitaly Slobodin <vitaliy.slobodin@gmail.com> Date: Sat, 19 Jul 2025 17:06:35 +0200 Subject: [PATCH 206/658] Fix Tailwind support for HTML/ERB files (#34743) Closes #27118 Closes #34165 Fix a small issue after we landed https://github.com/zed-extensions/ruby/pull/113+ where we introduced `HTML/ERB` and `YAML/ERB` language IDs to improve user experience. Sorry about that. Thanks! Release Notes: - N/A --- crates/languages/src/lib.rs | 1 + crates/languages/src/tailwind.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index 431c05108184519bc110f03a801d224a3b4077d7..a224111002b25e2c056406f1064e0d985135a1b7 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -273,6 +273,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) { "Astro", "CSS", "ERB", + "HTML/ERB", "HEEX", "HTML", "JavaScript", diff --git a/crates/languages/src/tailwind.rs b/crates/languages/src/tailwind.rs index 04f30b624615da9432719e5236833b5277ff1ef2..cb4e939083e07961e3f7e3d7b664c2a18b338c1d 100644 --- a/crates/languages/src/tailwind.rs +++ b/crates/languages/src/tailwind.rs @@ -179,6 +179,7 @@ impl LspAdapter for TailwindLspAdapter { ("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()), ]) From 29111304dde348a00fbceca71183bd30703175b8 Mon Sep 17 00:00:00 2001 From: Oleksandr Mykhailenko <58030797+armyhaylenko@users.noreply.github.com> Date: Sat, 19 Jul 2025 18:59:57 +0300 Subject: [PATCH 207/658] agent: Fix Mistral tool use error message (#34692) Closes #32675 Exactly the same changes as in #33640 by @sviande The PR has been in WIP state for 3 weeks with no activity, and the issue basically makes Mistral models unusable. I have tested the changes locally, and it does indeed work. Full credit goes to @sviande, I just want this feature to be finished. Release Notes: - agent: Fixed an issue with tool calling with the Mistral provider (thanks [@sviande](https://github.com/sviande) and [@armyhaylenko](https://github.com/armyhaylenko)) Co-authored-by: sviande <sviande@gmail.com> --- .../language_models/src/provider/mistral.rs | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/crates/language_models/src/provider/mistral.rs b/crates/language_models/src/provider/mistral.rs index 11497fda350a02ec9433cb2311a28e1901dfeb4f..fb385308fac98240ae0a9aaf64aa1206fd8f826b 100644 --- a/crates/language_models/src/provider/mistral.rs +++ b/crates/language_models/src/provider/mistral.rs @@ -410,8 +410,20 @@ pub fn into_mistral( .push_part(mistral::MessagePart::Text { text: text.clone() }); } MessageContent::RedactedThinking(_) => {} - MessageContent::ToolUse(_) | MessageContent::ToolResult(_) => { - // Tool content is not supported in User messages for Mistral + MessageContent::ToolUse(_) => { + // Tool use is not supported in User messages for Mistral + } + MessageContent::ToolResult(tool_result) => { + let tool_content = match &tool_result.content { + LanguageModelToolResultContent::Text(text) => text.to_string(), + LanguageModelToolResultContent::Image(_) => { + "[Tool responded with an image, but Zed doesn't support these in Mistral models yet]".to_string() + } + }; + messages.push(mistral::RequestMessage::Tool { + content: tool_content, + tool_call_id: tool_result.tool_use_id.to_string(), + }); } } } @@ -482,24 +494,6 @@ pub fn into_mistral( } } - for message in &request.messages { - for content in &message.content { - if let MessageContent::ToolResult(tool_result) = content { - let content = match &tool_result.content { - LanguageModelToolResultContent::Text(text) => text.to_string(), - LanguageModelToolResultContent::Image(_) => { - "[Tool responded with an image, but Zed doesn't support these in Mistral models yet]".to_string() - } - }; - - messages.push(mistral::RequestMessage::Tool { - content, - tool_call_id: tool_result.tool_use_id.to_string(), - }); - } - } - } - // 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 From fb88de92234fd494bafb13c694569015bcb2356c Mon Sep 17 00:00:00 2001 From: Peter Tripp <peter@zed.dev> Date: Sat, 19 Jul 2025 12:01:18 -0400 Subject: [PATCH 208/658] Fix error in OpenRouter svg logo (#34764) Fix a spurious error in Zed logs from the OpenRouter svg Logo introduced in https://github.com/zed-industries/zed/pull/29496: ```log WARN [usvg::parser::svgtree] Failed to parse clip-path value: 'url(#clip0_205_3)'. ``` Release Notes: - N/A --- assets/icons/ai_open_router.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/icons/ai_open_router.svg b/assets/icons/ai_open_router.svg index cc8597729a8ac4011d5ef8937fdb4f0ddaff7839..94f284914608956090858803612eb971cc72d2fa 100644 --- a/assets/icons/ai_open_router.svg +++ b/assets/icons/ai_open_router.svg @@ -1,5 +1,5 @@ <svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor" stroke="currentColor"> - <g clip-path="url(#clip0_205_3)"> + <g> <path d="M0.094 7.78c0.469 0 2.281 -0.405 3.219 -0.936s0.938 -0.531 2.875 -1.906c2.453 -1.741 4.188 -1.158 7.031 -1.158" stroke-width="2.8125" /> <path d="m15.969 3.797 -4.805 2.774V1.023z" /> <path d="M0 7.781c0.469 0 2.281 0.405 3.219 0.936s0.938 0.531 2.875 1.906C8.547 12.364 10.281 11.781 13.125 11.781" stroke-width="2.8125" /> From 5f92ac25a7690fdfa6864e30d4528c2a5b5e56f8 Mon Sep 17 00:00:00 2001 From: Peter Tripp <peter@zed.dev> Date: Sat, 19 Jul 2025 12:01:33 -0400 Subject: [PATCH 209/658] docs: Consolidate backend setup docs into local-collaboration.md (#34653) Simplify docs for mac/linux/windows by consolidating the backend dependencies (collaboration) docs into local-collaboration.md. Most users building zed will not need to do this -- streamline them into getting setup to build the zed client app first. Release Notes: - N/A --- docs/src/development/linux.md | 15 +---- docs/src/development/local-collaboration.md | 66 ++++++++++++++++++++- docs/src/development/macos.md | 23 ++----- docs/src/development/windows.md | 15 +---- 4 files changed, 73 insertions(+), 46 deletions(-) diff --git a/docs/src/development/linux.md b/docs/src/development/linux.md index 08ac7f116bdcb0000bcc734fe8c6a170992af1cf..6fff25f6c114a946c4ac8c8fc0441d0dde9a9475 100644 --- a/docs/src/development/linux.md +++ b/docs/src/development/linux.md @@ -16,20 +16,9 @@ Clone down the [Zed repository](https://github.com/zed-industries/zed). If you prefer to install the system libraries manually, you can find the list of required packages in the `script/linux` file. -## Backend dependencies +### Backend Dependencies (optional) {#backend-dependencies} -> This section is still in development. The instructions are not yet complete. - -If you are developing collaborative features of Zed, you'll need to install the dependencies of zed's `collab` server: - -- Install [Postgres](https://www.postgresql.org/download/linux/) -- Install [Livekit](https://github.com/livekit/livekit-cli) and [Foreman](https://theforeman.org/manuals/3.9/quickstart_guide.html) - -Alternatively, if you have [Docker](https://www.docker.com/) installed you can bring up all the `collab` dependencies using Docker Compose: - -```sh -docker compose up -d -``` +If you are looking to develop Zed collaboration features using a local collabortation server, please see: [Local Collaboration](./local-collaboration.md) docs. ## Building from source diff --git a/docs/src/development/local-collaboration.md b/docs/src/development/local-collaboration.md index 6c96c342a872868a0c86495d5772168cc8772f40..9f0e3ef1912780bb6b554e6bebf53a07cf24f85c 100644 --- a/docs/src/development/local-collaboration.md +++ b/docs/src/development/local-collaboration.md @@ -1,6 +1,6 @@ # Local Collaboration -First, make sure you've installed Zed's backend dependencies for your platform: +First, make sure you've installed Zed's dependencies for your platform: - [macOS](./macos.md#backend-dependencies) - [Linux](./linux.md#backend-dependencies) @@ -8,6 +8,70 @@ First, make sure you've installed Zed's backend dependencies for your platform: Note that `collab` can be compiled only with MSVC toolchain on Windows +## Backend Dependencies + +If you are developing collaborative features of Zed, you'll need to install the dependencies of zed's `collab` server: + +- PostgreSQL +- LiveKit +- Foreman + +You can install these dependencies natively or run them under Docker. + +### MacOS + +1. Install [Postgres.app](https://postgresapp.com) or [postgresql via homebrew](https://formulae.brew.sh/formula/postgresql@15): + + ```sh + brew install postgresql@15 + ``` + +2. Install [Livekit](https://formulae.brew.sh/formula/livekit) and [Foreman](https://formulae.brew.sh/formula/foreman) + + ```sh + brew install livekit foreman + ``` + +- Follow the steps in the [collab README](https://github.com/zed-industries/zed/blob/main/crates/collab/README.md) to configure the Postgres database for integration tests + +Alternatively, if you have [Docker](https://www.docker.com/) installed you can bring up all the `collab` dependencies using Docker Compose: + +### Linux + +1. Install [Postgres](https://www.postgresql.org/download/linux/) + + ```sh + sudo apt-get install postgresql postgresql # Ubuntu/Debian + sudo pacman -S postgresql # Arch Linux + sudo dnf install postgresql postgresql-server # RHEL/Fedora + sudo zypper install postgresql postgresql-server # OpenSUSE + ``` + +2. Install [Livekit](https://github.com/livekit/livekit-cli) + + ```sh + curl -sSL https://get.livekit.io/cli | bash + ``` + +3. Install [Foreman](https://theforeman.org/manuals/3.15/quickstart_guide.html) + +### Windows {#backend-windows} + +> This section is still in development. The instructions are not yet complete. + +- Install [Postgres](https://www.postgresql.org/download/windows/) +- Install [Livekit](https://github.com/livekit/livekit), optionally you can add the `livekit-server` binary to your `PATH`. + +Alternatively, if you have [Docker](https://www.docker.com/) installed you can bring up all the `collab` dependencies using Docker Compose. + +### Docker {#Docker} + +If you have docker or podman available, you can run the backend dependencies inside containers with Docker Compose: + +```sh +docker compose up -d +``` + ## Database setup Before you can run the `collab` server locally, you'll need to set up a `zed` Postgres database. diff --git a/docs/src/development/macos.md b/docs/src/development/macos.md index 91adf7819386b8e60306a692fed38bf142ccc26c..f081f0b5f12b11e57ee9c82e38be03c16292311e 100644 --- a/docs/src/development/macos.md +++ b/docs/src/development/macos.md @@ -31,6 +31,10 @@ Clone down the [Zed repository](https://github.com/zed-industries/zed). brew install cmake ``` +### Backend Dependencies (optional) {#backend-dependencies} + +If you are looking to develop Zed collaboration features using a local collabortation server, please see: [Local Collaboration](./local-collaboration.md) docs. + ## Building Zed from Source Once you have the dependencies installed, you can build Zed using [Cargo](https://doc.rust-lang.org/cargo/). @@ -53,25 +57,6 @@ And to run the tests: cargo test --workspace ``` -## Backend Dependencies - -If you are developing collaborative features of Zed, you'll need to install the dependencies of zed's `collab` server: - -- Install [Postgres](https://postgresapp.com) -- Install [Livekit](https://formulae.brew.sh/formula/livekit) and [Foreman](https://formulae.brew.sh/formula/foreman) - - ```sh - brew install livekit foreman - ``` - -- Follow the steps in the [collab README](https://github.com/zed-industries/zed/blob/main/crates/collab/README.md) to configure the Postgres database for integration tests - -Alternatively, if you have [Docker](https://www.docker.com/) installed you can bring up all the `collab` dependencies using Docker Compose: - -```sh -docker compose up -d -``` - ## Troubleshooting ### Error compiling metal shaders diff --git a/docs/src/development/windows.md b/docs/src/development/windows.md index 6d67500aab9f7c65f5a746c1eedc6a004fd3d1aa..ac38e4d7d699b55c5722d2e9c56e527eea0e3620 100644 --- a/docs/src/development/windows.md +++ b/docs/src/development/windows.md @@ -66,20 +66,9 @@ The list can be obtained as follows: - Click on `More` in the `Installed` tab - Click on `Export configuration` -## Backend dependencies +### Backend Dependencies (optional) {#backend-dependencies} -> This section is still in development. The instructions are not yet complete. - -If you are developing collaborative features of Zed, you'll need to install the dependencies of zed's `collab` server: - -- Install [Postgres](https://www.postgresql.org/download/windows/) -- Install [Livekit](https://github.com/livekit/livekit), optionally you can add the `livekit-server` binary to your `PATH`. - -Alternatively, if you have [Docker](https://www.docker.com/) installed you can bring up all the `collab` dependencies using Docker Compose: - -```sh -docker compose up -d -``` +If you are looking to develop Zed collaboration features using a local collabortation server, please see: [Local Collaboration](./local-collaboration.md) docs. ### Notes From 2e41e312ad73cb138475c79b5cf8ab3e4a887866 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <bennet@zed.dev> Date: Sun, 20 Jul 2025 18:27:18 +0200 Subject: [PATCH 210/658] component preview: Fix Zed AI onboarding young account preview (#34783) Release Notes: - N/A --- crates/ai_onboarding/src/ai_onboarding.rs | 24 ++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/crates/ai_onboarding/src/ai_onboarding.rs b/crates/ai_onboarding/src/ai_onboarding.rs index 131d385e7891644ea512676d49cc2ec9206c7784..f19b8821fa2cbcda063bc9a47f9b7736ef639d8e 100644 --- a/crates/ai_onboarding/src/ai_onboarding.rs +++ b/crates/ai_onboarding/src/ai_onboarding.rs @@ -339,16 +339,18 @@ impl Component for ZedAiOnboarding { plan: Option<proto::Plan>, 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(|_, _| {}), - } - .into_any_element() + div() + .w(px(800.)) + .child(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(|_, _| {}), + }) + .into_any_element() } Some( @@ -366,7 +368,7 @@ impl Component for ZedAiOnboarding { ), single_example( "Account too young", - onboarding(SignInStatus::SignedIn, false, None, true), + onboarding(SignInStatus::SignedIn, true, None, true), ), single_example( "Free Plan", From ff79b29f3812e8d39763e51af17c9c13e3ebf8f5 Mon Sep 17 00:00:00 2001 From: Michael Sloan <michael@zed.dev> Date: Sun, 20 Jul 2025 13:39:04 -0600 Subject: [PATCH 211/658] Set stripe-mock version to `0.178.0` to match stripe API version used (#34786) Release Notes: - N/A --- compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose.yml b/compose.yml index 06e8806d200edf31ae1a3b045112377866a6257a..d0d9bac425356687bfb33efab9ee24e76d1b30a0 100644 --- a/compose.yml +++ b/compose.yml @@ -60,7 +60,7 @@ services: - postgres stripe-mock: - image: stripe/stripe-mock:v0.184.0 + image: stripe/stripe-mock:v0.178.0 ports: - 12111:12111 - 12112:12112 From 7c1040bc938df6926a88fc5bce139c4a1ec7e3d0 Mon Sep 17 00:00:00 2001 From: Ben Kunkle <ben@zed.dev> Date: Sun, 20 Jul 2025 15:24:17 -0500 Subject: [PATCH 212/658] keymap_ui: Auto complete action arguments (#34785) Supersedes: #34242 Creates an `ActionArgumentsEditor` that implements the required logic to have a JSON language server run when editing keybinds so that there is auto-complete for action arguments. This is the first time action argument schemas are required by themselves rather than inlined in the keymap schema. Rather than add all action schemas to the configuration options we send to the JSON LSP on startup, this PR implements support for the `vscode-json-language-server` extension to the LSP whereby the server will request the client (Zed) to resolve URLs with URI schemes it does not recognize, in our case `zed://`. This limits the impact on the size of the configuration options to ~1KB as we send URLs for the language server to resolve on demand rather than the schema itself. My understanding is that this is how VSCode handles JSON schemas as well. I plan to investigate converting the rest of our schema generation logic to this method in a follow up PR. Co-Authored-By: Cole <cole@zed.dev> Release Notes: - N/A *or* Added/Fixed/Improved ... --- Cargo.lock | 1 + assets/keymaps/default-macos.json | 1 + crates/languages/src/json.rs | 7 + crates/project/src/lsp_store.rs | 2 + .../src/lsp_store/json_language_server_ext.rs | 101 ++++ crates/settings_ui/Cargo.toml | 1 + crates/settings_ui/src/keybindings.rs | 449 +++++++++++++----- 7 files changed, 453 insertions(+), 109 deletions(-) create mode 100644 crates/project/src/lsp_store/json_language_server_ext.rs diff --git a/Cargo.lock b/Cargo.lock index cbed9f5988b1ec96308f89c3c37309bab0b13bb1..a5ea621cd14662cba0838558b70d3e13b51c7840 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14768,6 +14768,7 @@ dependencies = [ "serde_json", "settings", "telemetry", + "tempfile", "theme", "tree-sitter-json", "tree-sitter-rust", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 6bce3b0f28ce1a0b8ee42a32df521a042b880701..ba903c07821fec6fad805e3a4b1cac81831216ed 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -1228,6 +1228,7 @@ "context": "KeymapEditor", "use_key_equivalents": true, "bindings": { + "cmd-f": "search::FocusSearch", "cmd-alt-f": "keymap_editor::ToggleKeystrokeSearch", "cmd-alt-c": "keymap_editor::ToggleConflictFilter", "enter": "keymap_editor::EditBinding", diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 7a3300eb010d9da30111023e660ef56a2070ea9e..15818730b8a09be94a826caf2e8c6f0bd2bfbadc 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -231,6 +231,13 @@ impl JsonLspAdapter { )) } + schemas + .as_array_mut() + .unwrap() + .extend(cx.all_action_names().into_iter().map(|&name| { + project::lsp_store::json_language_server_ext::url_schema_for_action(name) + })); + // This can be viewed via `dev: open language server logs` -> `json-language-server` -> // `Server Info` serde_json::json!({ diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index e4078393ee20fb906d6501bd6820e73a46bf9c39..28cbfcdd1842b8c861d2777c3858efc8752ac75b 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -1,4 +1,5 @@ pub mod clangd_ext; +pub mod json_language_server_ext; pub mod lsp_ext_command; pub mod rust_analyzer_ext; @@ -1034,6 +1035,7 @@ impl LocalLspStore { }) .detach(); + json_language_server_ext::register_requests(this.clone(), language_server); rust_analyzer_ext::register_notifications(this.clone(), language_server); clangd_ext::register_notifications(this, language_server, adapter); } diff --git a/crates/project/src/lsp_store/json_language_server_ext.rs b/crates/project/src/lsp_store/json_language_server_ext.rs new file mode 100644 index 0000000000000000000000000000000000000000..3eb93386a99bf40dffc5f6de75d56248936b38e3 --- /dev/null +++ b/crates/project/src/lsp_store/json_language_server_ext.rs @@ -0,0 +1,101 @@ +use anyhow::Context as _; +use collections::HashMap; +use gpui::WeakEntity; +use lsp::LanguageServer; + +use crate::LspStore; +/// https://github.com/Microsoft/vscode/blob/main/extensions/json-language-features/server/README.md#schema-content-request +/// +/// Represents a "JSON language server-specific, non-standardized, extension to the LSP" with which the vscode-json-language-server +/// can request the contents of a schema that is associated with a uri scheme it does not support. +/// In our case, we provide the uris for actions on server startup under the `zed://schemas/action/{normalize_action_name}` scheme. +/// We can then respond to this request with the schema content on demand, thereby greatly reducing the total size of the JSON we send to the server on startup +struct SchemaContentRequest {} + +impl lsp::request::Request for SchemaContentRequest { + type Params = Vec<String>; + + type Result = String; + + const METHOD: &'static str = "vscode/content"; +} + +pub fn register_requests(_lsp_store: WeakEntity<LspStore>, language_server: &LanguageServer) { + language_server + .on_request::<SchemaContentRequest, _, _>(|params, cx| { + // PERF: Use a cache (`OnceLock`?) to avoid recomputing the action schemas + let mut generator = settings::KeymapFile::action_schema_generator(); + let all_schemas = cx.update(|cx| HashMap::from_iter(cx.action_schemas(&mut generator))); + async move { + let all_schemas = all_schemas?; + let Some(uri) = params.get(0) else { + anyhow::bail!("No URI"); + }; + let normalized_action_name = uri + .strip_prefix("zed://schemas/action/") + .context("Invalid URI")?; + let action_name = denormalize_action_name(normalized_action_name); + let schema = root_schema_from_action_schema( + all_schemas + .get(action_name.as_str()) + .and_then(Option::as_ref), + &mut generator, + ) + .to_value(); + + serde_json::to_string(&schema).context("Failed to serialize schema") + } + }) + .detach(); +} + +pub fn normalize_action_name(action_name: &str) -> String { + action_name.replace("::", "__") +} + +pub fn denormalize_action_name(action_name: &str) -> String { + action_name.replace("__", "::") +} + +pub fn normalized_action_file_name(action_name: &str) -> String { + normalized_action_name_to_file_name(normalize_action_name(action_name)) +} + +pub fn normalized_action_name_to_file_name(mut normalized_action_name: String) -> String { + normalized_action_name.push_str(".json"); + normalized_action_name +} + +pub fn url_schema_for_action(action_name: &str) -> serde_json::Value { + let normalized_name = normalize_action_name(action_name); + let file_name = normalized_action_name_to_file_name(normalized_name.clone()); + serde_json::json!({ + "fileMatch": [file_name], + "url": format!("zed://schemas/action/{}", normalized_name) + }) +} + +fn root_schema_from_action_schema( + action_schema: Option<&schemars::Schema>, + generator: &mut schemars::SchemaGenerator, +) -> schemars::Schema { + let Some(action_schema) = action_schema else { + return schemars::json_schema!(false); + }; + let meta_schema = generator + .settings() + .meta_schema + .as_ref() + .expect("meta_schema should be present in schemars settings") + .to_string(); + let defs = generator.definitions(); + let mut schema = schemars::json_schema!({ + "$schema": meta_schema, + "allowTrailingCommas": true, + "$defs": defs, + }); + schema + .ensure_object() + .extend(std::mem::take(action_schema.clone().ensure_object())); + schema +} diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml index e512c4e4d4836ef9f690011bb84d5d0327bba640..651397dd51b1b2406cc4149f0951d3f506b73689 100644 --- a/crates/settings_ui/Cargo.toml +++ b/crates/settings_ui/Cargo.toml @@ -35,6 +35,7 @@ serde.workspace = true serde_json.workspace = true settings.workspace = true telemetry.workspace = true +tempfile.workspace = true theme.workspace = true tree-sitter-json.workspace = true tree-sitter-rust.workspace = true diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 1a5bb7b45943113653a43b3ecf728a904286674f..5f940e8a25cc784a5ad0bd529e213c2e0b69a03d 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -13,11 +13,13 @@ use gpui::{ Action, Animation, AnimationExt, AppContext as _, AsyncApp, Axis, ClickEvent, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Global, IsZero, KeyContext, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, Point, ScrollStrategy, - ScrollWheelEvent, StyledText, Subscription, Task, WeakEntity, actions, anchored, deferred, div, + ScrollWheelEvent, StyledText, Subscription, Task, TextStyleRefinement, WeakEntity, actions, + anchored, deferred, div, }; use language::{Language, LanguageConfig, ToOffset as _}; use notifications::status_toast::{StatusToast, ToastIcon}; -use settings::{BaseKeymap, KeybindSource, KeymapFile, SettingsAssets}; +use project::Project; +use settings::{BaseKeymap, KeybindSource, KeymapFile, Settings as _, SettingsAssets}; use util::ResultExt; @@ -283,6 +285,14 @@ struct KeymapEditor { previous_edit: Option<PreviousEdit>, humanized_action_names: HumanizedActionNameCache, show_hover_menus: bool, + /// In order for the JSON LSP to run in the actions arguments editor, we + /// require a backing file In order to avoid issues (primarily log spam) + /// with drop order between the buffer, file, worktree, etc, we create a + /// temporary directory for these backing files in the keymap editor struct + /// instead of here. This has the added benefit of only having to create a + /// worktree and directory once, although the perf improvement is negligible. + action_args_temp_dir_worktree: Option<Entity<project::Worktree>>, + action_args_temp_dir: Option<tempfile::TempDir>, } enum PreviousEdit { @@ -307,13 +317,18 @@ impl EventEmitter<()> for KeymapEditor {} impl Focusable for KeymapEditor { fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { - return self.filter_editor.focus_handle(cx); + if self.selected_index.is_some() { + self.focus_handle.clone() + } else { + self.filter_editor.focus_handle(cx) + } } } impl KeymapEditor { fn new(workspace: WeakEntity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self { - let _keymap_subscription = cx.observe_global::<KeymapEventChannel>(Self::on_keymap_changed); + let _keymap_subscription = + cx.observe_global_in::<KeymapEventChannel>(window, Self::on_keymap_changed); let table_interaction_state = TableInteractionState::new(window, cx); let keystroke_editor = cx.new(|cx| { @@ -346,6 +361,24 @@ impl KeymapEditor { }) .detach(); + cx.spawn({ + let workspace = workspace.clone(); + async move |this, cx| { + let temp_dir = tempfile::tempdir_in(paths::temp_dir())?; + let worktree = workspace + .update(cx, |ws, cx| { + ws.project() + .update(cx, |p, cx| p.create_worktree(temp_dir.path(), false, cx)) + })? + .await?; + this.update(cx, |this, _| { + this.action_args_temp_dir = Some(temp_dir); + this.action_args_temp_dir_worktree = Some(worktree); + }) + } + }) + .detach(); + let mut this = Self { workspace, keybindings: vec![], @@ -365,9 +398,11 @@ impl KeymapEditor { search_query_debounce: None, humanized_action_names: HumanizedActionNameCache::new(cx), show_hover_menus: true, + action_args_temp_dir: None, + action_args_temp_dir_worktree: None, }; - this.on_keymap_changed(cx); + this.on_keymap_changed(window, cx); this } @@ -557,10 +592,10 @@ impl KeymapEditor { HashSet::from_iter(cx.all_action_names().into_iter().copied()); let action_documentation = cx.action_documentation(); let mut generator = KeymapFile::action_schema_generator(); - let action_schema = HashMap::from_iter( + let actions_with_schemas = HashSet::from_iter( cx.action_schemas(&mut generator) .into_iter() - .filter_map(|(name, schema)| schema.map(|schema| (name, schema))), + .filter_map(|(name, schema)| schema.is_some().then_some(name)), ); let mut processed_bindings = Vec::new(); @@ -607,7 +642,7 @@ impl KeymapEditor { action_arguments, humanized_action_name, action_docs, - action_schema: action_schema.get(action_name).cloned(), + has_schema: actions_with_schemas.contains(action_name), context: Some(context), source, }); @@ -626,7 +661,7 @@ impl KeymapEditor { action_arguments: None, humanized_action_name, action_docs: action_documentation.get(action_name).copied(), - action_schema: action_schema.get(action_name).cloned(), + has_schema: actions_with_schemas.contains(action_name), context: None, source: None, }); @@ -636,9 +671,9 @@ impl KeymapEditor { (processed_bindings, string_match_candidates) } - fn on_keymap_changed(&mut self, cx: &mut Context<KeymapEditor>) { + fn on_keymap_changed(&mut self, window: &mut Window, cx: &mut Context<KeymapEditor>) { let workspace = self.workspace.clone(); - cx.spawn(async move |this, cx| { + cx.spawn_in(window, async move |this, cx| { let json_language = load_json_language(workspace.clone(), cx).await; let zed_keybind_context_language = load_keybind_context_language(workspace.clone(), cx).await; @@ -673,7 +708,7 @@ impl KeymapEditor { })?; // calls cx.notify Self::update_matches(this.clone(), action_query, keystroke_query, cx).await?; - this.update(cx, |this, cx| { + this.update_in(cx, |this, window, cx| { if let Some(previous_edit) = this.previous_edit.take() { match previous_edit { // should remove scroll from process_query @@ -701,8 +736,12 @@ impl KeymapEditor { }); if let Some(scroll_position) = scroll_position { - this.scroll_to_item(scroll_position, ScrollStrategy::Top, cx); - this.selected_index = Some(scroll_position); + this.select_index( + scroll_position, + Some(ScrollStrategy::Top), + window, + cx, + ); } else { this.table_interaction_state.update(cx, |table, _| { table.set_scrollbar_offset(Axis::Vertical, fallback) @@ -768,9 +807,19 @@ impl KeymapEditor { .and_then(|keybind_index| self.keybindings.get(keybind_index)) } - fn select_index(&mut self, index: usize, cx: &mut Context<Self>) { + fn select_index( + &mut self, + index: usize, + scroll: Option<ScrollStrategy>, + window: &mut Window, + cx: &mut Context<Self>, + ) { if self.selected_index != Some(index) { self.selected_index = Some(index); + if let Some(scroll_strategy) = scroll { + self.scroll_to_item(index, scroll_strategy, cx); + } + window.focus(&self.focus_handle); cx.notify(); } } @@ -872,9 +921,7 @@ impl KeymapEditor { if selected >= self.matches.len() { self.select_last(&Default::default(), window, cx); } else { - self.selected_index = Some(selected); - self.scroll_to_item(selected, ScrollStrategy::Center, cx); - cx.notify(); + self.select_index(selected, Some(ScrollStrategy::Center), window, cx); } } else { self.select_first(&Default::default(), window, cx); @@ -898,36 +945,25 @@ impl KeymapEditor { if selected >= self.matches.len() { self.select_last(&Default::default(), window, cx); } else { - self.selected_index = Some(selected); - self.scroll_to_item(selected, ScrollStrategy::Center, cx); - cx.notify(); + self.select_index(selected, Some(ScrollStrategy::Center), window, cx); } } else { self.select_last(&Default::default(), window, cx); } } - fn select_first( - &mut self, - _: &menu::SelectFirst, - _window: &mut Window, - cx: &mut Context<Self>, - ) { + fn select_first(&mut self, _: &menu::SelectFirst, window: &mut Window, cx: &mut Context<Self>) { self.show_hover_menus = false; if self.matches.get(0).is_some() { - self.selected_index = Some(0); - self.scroll_to_item(0, ScrollStrategy::Center, cx); - cx.notify(); + self.select_index(0, Some(ScrollStrategy::Center), window, cx); } } - fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) { + fn select_last(&mut self, _: &menu::SelectLast, window: &mut Window, cx: &mut Context<Self>) { self.show_hover_menus = false; if self.matches.last().is_some() { let index = self.matches.len() - 1; - self.selected_index = Some(index); - self.scroll_to_item(index, ScrollStrategy::Center, cx); - cx.notify(); + self.select_index(index, Some(ScrollStrategy::Center), window, cx); } } @@ -963,6 +999,8 @@ impl KeymapEditor { arguments = arguments, ); + let temp_dir = self.action_args_temp_dir.as_ref().map(|dir| dir.path()); + self.workspace .update(cx, |workspace, cx| { let fs = workspace.app_state().fs.clone(); @@ -973,6 +1011,7 @@ impl KeymapEditor { keybind, keybind_index, keymap_editor, + temp_dir, workspace_weak, fs, window, @@ -1134,7 +1173,7 @@ struct ProcessedKeybinding { humanized_action_name: SharedString, action_arguments: Option<SyntaxHighlightedText>, action_docs: Option<&'static str>, - action_schema: Option<schemars::Schema>, + has_schema: bool, context: Option<KeybindContextString>, source: Option<(KeybindSource, SharedString)>, } @@ -1428,7 +1467,7 @@ impl Render for KeymapEditor { cx, ); } else { - this.select_index(index, cx); + this.select_index(index, None, window, cx); this.open_edit_keybinding_modal( false, window, cx, ); @@ -1458,7 +1497,7 @@ impl Render for KeymapEditor { }, ) .on_click(cx.listener(move |this, _, window, cx| { - this.select_index(index, cx); + this.select_index(index, None, window, cx); this.open_edit_keybinding_modal(false, window, cx); cx.stop_propagation(); })) @@ -1506,7 +1545,7 @@ impl Render for KeymapEditor { let action_arguments = match binding.action_arguments.clone() { Some(arguments) => arguments.into_any_element(), None => { - if binding.action_schema.is_some() { + if binding.has_schema { muted_styled_text(NO_ACTION_ARGUMENTS_TEXT, cx) .into_any_element() } else { @@ -1571,7 +1610,7 @@ impl Render for KeymapEditor { cx| { match mouse_down_event.button { MouseButton::Right => { - this.select_index(row_index, cx); + this.select_index(row_index, None, window, cx); this.create_context_menu( mouse_down_event.position, window, @@ -1584,7 +1623,7 @@ impl Render for KeymapEditor { )) .on_click(cx.listener( move |this, event: &ClickEvent, window, cx| { - this.select_index(row_index, cx); + this.select_index(row_index, None, window, cx); if event.up.click_count == 2 { this.open_edit_keybinding_modal(false, window, cx); } @@ -1686,23 +1725,23 @@ impl RenderOnce for SyntaxHighlightedText { } #[derive(PartialEq)] -enum InputError { - Warning(SharedString), - Error(SharedString), +struct InputError { + severity: ui::Severity, + content: SharedString, } impl InputError { fn warning(message: impl Into<SharedString>) -> Self { - Self::Warning(message.into()) - } - - fn error(error: anyhow::Error) -> Self { - Self::Error(error.to_string().into()) + Self { + severity: ui::Severity::Warning, + content: message.into(), + } } - fn content(&self) -> &SharedString { - match self { - InputError::Warning(content) | InputError::Error(content) => content, + fn error(message: anyhow::Error) -> Self { + Self { + severity: ui::Severity::Error, + content: message.to_string().into(), } } } @@ -1713,7 +1752,7 @@ struct KeybindingEditorModal { editing_keybind_idx: usize, keybind_editor: Entity<KeystrokeInput>, context_editor: Entity<SingleLineInput>, - action_arguments_editor: Option<Entity<Editor>>, + action_arguments_editor: Option<Entity<ActionArgumentsEditor>>, fs: Arc<dyn Fs>, error: Option<InputError>, keymap_editor: Entity<KeymapEditor>, @@ -1737,6 +1776,7 @@ impl KeybindingEditorModal { editing_keybind: ProcessedKeybinding, editing_keybind_idx: usize, keymap_editor: Entity<KeymapEditor>, + action_args_temp_dir: Option<&std::path::Path>, workspace: WeakEntity<Workspace>, fs: Arc<dyn Fs>, window: &mut Window, @@ -1786,40 +1826,29 @@ impl KeybindingEditorModal { input }); - let action_arguments_editor = editing_keybind.action_schema.clone().map(|_schema| { + let action_arguments_editor = editing_keybind.has_schema.then(|| { + let arguments = editing_keybind + .action_arguments + .as_ref() + .map(|args| args.text.clone()); cx.new(|cx| { - let mut editor = Editor::auto_height_unbounded(1, window, cx); - let workspace = workspace.clone(); - - if let Some(arguments) = editing_keybind.action_arguments.clone() { - editor.set_text(arguments.text, window, cx); - } else { - // TODO: default value from schema? - editor.set_placeholder_text("Action Arguments", cx); - } - cx.spawn(async |editor, cx| { - let json_language = load_json_language(workspace, cx).await; - editor - .update(cx, |editor, cx| { - if let Some(buffer) = editor.buffer().read(cx).as_singleton() { - buffer.update(cx, |buffer, cx| { - buffer.set_language(Some(json_language), cx) - }); - } - }) - .context("Failed to load JSON language for editing keybinding action arguments input") - }) - .detach_and_log_err(cx); - editor + ActionArgumentsEditor::new( + editing_keybind.action_name, + arguments, + action_args_temp_dir, + workspace.clone(), + window, + cx, + ) }) }); let focus_state = KeybindingEditorModalFocusState::new( - keybind_editor.read_with(cx, |keybind_editor, cx| keybind_editor.focus_handle(cx)), - action_arguments_editor.as_ref().map(|args_editor| { - args_editor.read_with(cx, |args_editor, cx| args_editor.focus_handle(cx)) - }), - context_editor.read_with(cx, |context_editor, cx| context_editor.focus_handle(cx)), + keybind_editor.focus_handle(cx), + action_arguments_editor + .as_ref() + .map(|args_editor| args_editor.focus_handle(cx)), + context_editor.focus_handle(cx), ); Self { @@ -1837,14 +1866,15 @@ impl KeybindingEditorModal { } } - fn set_error(&mut self, error: InputError, cx: &mut Context<Self>) { - if self - .error - .as_ref() - .is_none_or(|old_error| *old_error != error) - { + 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 + }) { + false + } else { self.error = Some(error); cx.notify(); + true } } @@ -1852,7 +1882,7 @@ impl KeybindingEditorModal { let action_arguments = self .action_arguments_editor .as_ref() - .map(|editor| editor.read(cx).text(cx)); + .map(|editor| editor.read(cx).editor.read(cx).text(cx)); let value = action_arguments .as_ref() @@ -1938,7 +1968,7 @@ impl KeybindingEditorModal { let warning_message = match conflicting_action_name { Some(name) => { - if remaining_conflict_amount > 0 { + if remaining_conflict_amount > 0 { format!( "Your keybind would conflict with the \"{}\" action and {} other bindings", name, remaining_conflict_amount @@ -2108,38 +2138,21 @@ impl Render for KeybindingEditorModal { .mt_1p5() .gap_1() .child(Label::new("Edit Arguments")) - .child( - div() - .w_full() - .py_1() - .px_1p5() - .rounded_lg() - .bg(theme.editor_background) - .border_1() - .border_color(theme.border_variant) - .child(editor), - ), + .child(editor), ) }) .child(self.context_editor.clone()) .when_some(self.error.as_ref(), |this, error| { this.child( Banner::new() - .map(|banner| match error { - InputError::Error(_) => { - banner.severity(ui::Severity::Error) - } - InputError::Warning(_) => { - banner.severity(ui::Severity::Warning) - } - }) + .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())), + .child(Label::new(error.content.clone())), ), ) }), @@ -2219,6 +2232,219 @@ impl KeybindingEditorModalFocusState { } } +struct ActionArgumentsEditor { + editor: Entity<Editor>, + focus_handle: FocusHandle, + is_loading: bool, + /// See documentation in `KeymapEditor` for why a temp dir is needed. + /// This field exists because the keymap editor temp dir creation may fail, + /// and rather than implement a complicated retry mechanism, we simply + /// fallback to trying to create a temporary directory in this editor on + /// demand. Of note is that the TempDir struct will remove the directory + /// when dropped. + backup_temp_dir: Option<tempfile::TempDir>, +} + +impl Focusable for ActionArgumentsEditor { + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl ActionArgumentsEditor { + fn new( + action_name: &'static str, + arguments: Option<SharedString>, + temp_dir: Option<&std::path::Path>, + workspace: WeakEntity<Workspace>, + window: &mut Window, + cx: &mut Context<Self>, + ) -> Self { + let focus_handle = cx.focus_handle(); + cx.on_focus_in(&focus_handle, window, |this, window, cx| { + this.editor.focus_handle(cx).focus(window); + }) + .detach(); + let editor = cx.new(|cx| { + let mut editor = Editor::auto_height_unbounded(1, window, cx); + Self::set_editor_text(&mut editor, arguments.clone(), window, cx); + editor.set_read_only(true); + editor + }); + + let temp_dir = temp_dir.map(|path| path.to_owned()); + cx.spawn_in(window, async move |this, cx| { + let result = async { + let (project, fs) = workspace.read_with(cx, |workspace, _cx| { + ( + workspace.project().downgrade(), + workspace.app_state().fs.clone(), + ) + })?; + + let file_name = project::lsp_store::json_language_server_ext::normalized_action_file_name(action_name); + + let (buffer, backup_temp_dir) = Self::create_temp_buffer(temp_dir, file_name.clone(), project.clone(), fs, cx).await.context("Failed to create temporary buffer for action arguments. Auto-complete will not work") + ?; + + let editor = cx.new_window_entity(|window, cx| { + let multi_buffer = cx.new(|cx| editor::MultiBuffer::singleton(buffer, cx)); + let mut editor = Editor::new(editor::EditorMode::Full { scale_ui_elements_with_buffer_font_size: true, show_active_line_background: false, sized_by_content: true },multi_buffer, project.upgrade(), window, cx); + editor.set_searchable(false); + editor.disable_scrollbars_and_minimap(window, cx); + editor.set_show_edit_predictions(Some(false), window, cx); + editor.set_show_gutter(false, cx); + Self::set_editor_text(&mut editor, arguments, window, cx); + editor + })?; + + this.update_in(cx, |this, window, cx| { + if this.editor.focus_handle(cx).is_focused(window) { + editor.focus_handle(cx).focus(window); + } + this.editor = editor; + this.backup_temp_dir = backup_temp_dir; + this.is_loading = false; + })?; + + anyhow::Ok(()) + }.await; + if result.is_err() { + let json_language = load_json_language(workspace.clone(), cx).await; + this.update(cx, |this, cx| { + this.editor.update(cx, |editor, cx| { + if let Some(buffer) = editor.buffer().read(cx).as_singleton() { + buffer.update(cx, |buffer, cx| { + buffer.set_language(Some(json_language.clone()), cx) + }); + } + }) + // .context("Failed to load JSON language for editing keybinding action arguments input") + }).ok(); + this.update(cx, |this, _cx| { + this.is_loading = false; + }).ok(); + } + return result; + }) + .detach_and_log_err(cx); + Self { + editor, + focus_handle, + is_loading: true, + backup_temp_dir: None, + } + } + + fn set_editor_text( + editor: &mut Editor, + arguments: Option<SharedString>, + window: &mut Window, + cx: &mut Context<Editor>, + ) { + if let Some(arguments) = arguments { + editor.set_text(arguments, window, cx); + } else { + // TODO: default value from schema? + editor.set_placeholder_text("Action Arguments", cx); + } + } + + async fn create_temp_buffer( + temp_dir: Option<std::path::PathBuf>, + file_name: String, + project: WeakEntity<Project>, + fs: Arc<dyn Fs>, + cx: &mut AsyncApp, + ) -> anyhow::Result<(Entity<language::Buffer>, Option<tempfile::TempDir>)> { + let (temp_file_path, temp_dir) = { + let file_name = file_name.clone(); + async move { + let temp_dir_backup = match temp_dir.as_ref() { + Some(_) => None, + None => { + let temp_dir = paths::temp_dir(); + let sub_temp_dir = tempfile::Builder::new() + .tempdir_in(temp_dir) + .context("Failed to create temporary directory")?; + Some(sub_temp_dir) + } + }; + let dir_path = temp_dir.as_deref().unwrap_or_else(|| { + temp_dir_backup + .as_ref() + .expect("created backup tempdir") + .path() + }); + let path = dir_path.join(file_name); + fs.create_file( + &path, + fs::CreateOptions { + ignore_if_exists: true, + overwrite: true, + }, + ) + .await + .context("Failed to create temporary file")?; + anyhow::Ok((path, temp_dir_backup)) + } + } + .await + .context("Failed to create backing file")?; + + project + .update(cx, |project, cx| { + project.open_local_buffer(temp_file_path, cx) + })? + .await + .context("Failed to create buffer") + .map(|buffer| (buffer, temp_dir)) + } +} + +impl Render for ActionArgumentsEditor { + fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { + let background_color; + let border_color; + let text_style = { + let colors = cx.theme().colors(); + let settings = theme::ThemeSettings::get_global(cx); + background_color = colors.editor_background; + border_color = if self.is_loading { + colors.border_disabled + } else { + colors.border_variant + }; + TextStyleRefinement { + font_size: Some(rems(0.875).into()), + font_weight: Some(settings.buffer_font.weight), + line_height: Some(relative(1.2)), + font_style: Some(gpui::FontStyle::Normal), + color: self.is_loading.then_some(colors.text_disabled), + ..Default::default() + } + }; + + self.editor + .update(cx, |editor, _| editor.set_text_style_refinement(text_style)); + + return v_flex().w_full().child( + h_flex() + .min_h_8() + .min_w_48() + .px_2() + .py_1p5() + .flex_grow() + .rounded_lg() + .bg(background_color) + .border_1() + .border_color(border_color) + .track_focus(&self.focus_handle) + .child(self.editor.clone()), + ); + } +} + struct KeyContextCompletionProvider { contexts: Vec<SharedString>, } @@ -2643,6 +2869,11 @@ impl KeystrokeInput { { if self.search { last.key = keystroke.key.clone(); + if close_keystroke_result == CloseKeystrokeResult::Partial + && self.close_keystrokes_start.is_none() + { + self.close_keystrokes_start = Some(self.keystrokes.len() - 1); + } self.keystrokes_changed(cx); cx.stop_propagation(); return; From 137081f050c766cc2f5a3fbf913a12453912e35d Mon Sep 17 00:00:00 2001 From: Michael Sloan <michael@zed.dev> Date: Sun, 20 Jul 2025 17:22:13 -0600 Subject: [PATCH 213/658] Misc code cleanups accumulated while working on other changes (#34787) Release Notes: - N/A --- crates/command_palette/src/command_palette.rs | 2 +- crates/dap/src/registry.rs | 1 + crates/gpui/examples/set_menus.rs | 2 +- crates/gpui/src/app.rs | 4 +++- .../src/migrations/m_2025_04_15/keymap.rs | 2 +- crates/theme/src/schema.rs | 18 ++---------------- crates/util/src/schemars.rs | 1 - 7 files changed, 9 insertions(+), 21 deletions(-) diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index abb8978d5a103fb66f862af6c5ee69beee0f6251..dfaede0dc4c5d2d2ab9f45c1e73713e7d56f189d 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -242,7 +242,7 @@ impl CommandPaletteDelegate { self.selected_ix = cmp::min(self.selected_ix, self.matches.len() - 1); } } - /// + /// Hit count for each command in the palette. /// We only account for commands triggered directly via command palette and not by e.g. keystrokes because /// if a user already knows a keystroke for a command, they are unlikely to use a command palette to look for it. diff --git a/crates/dap/src/registry.rs b/crates/dap/src/registry.rs index 9435b16b924e43406d5ed99c864df78c179f27b1..d56e2f8f34e70dafa0126f793b2f755d09a6854d 100644 --- a/crates/dap/src/registry.rs +++ b/crates/dap/src/registry.rs @@ -46,6 +46,7 @@ impl DapRegistry { let name = adapter.name(); let _previous_value = self.0.write().adapters.insert(name, adapter); } + pub fn add_locator(&self, locator: Arc<dyn DapLocator>) { self.0.write().locators.insert(locator.name(), locator); } diff --git a/crates/gpui/examples/set_menus.rs b/crates/gpui/examples/set_menus.rs index 2b302f78f273449b9afeac8cb272a1a8148aaf56..f53fff7c7f7dfca1d2e44faf39347d1716ddad1c 100644 --- a/crates/gpui/examples/set_menus.rs +++ b/crates/gpui/examples/set_menus.rs @@ -34,7 +34,7 @@ fn main() { }); } -// Associate actions using the `actions!` macro (or `impl_actions!` macro) +// Associate actions using the `actions!` macro (or `Action` derive macro) actions!(set_menus, [Quit]); // Define the quit function that is registered with the App diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 70e1d1e4cd0b0303bf487848fa308b29d3b69910..de7ba782b2039c00c729343daa92d094b59ad248 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1370,7 +1370,9 @@ impl App { self.keymap.clone() } - /// Register a global listener for actions invoked via the keyboard. + /// Register a global handler for actions invoked via the keyboard. These handlers are run at + /// the end of the bubble phase for actions, and so will only be invoked if there are no other + /// handlers or if they called `cx.propagate()`. pub fn on_action<A: Action>(&mut self, listener: impl Fn(&A, &mut Self) + 'static) { self.global_action_listeners .entry(TypeId::of::<A>()) diff --git a/crates/migrator/src/migrations/m_2025_04_15/keymap.rs b/crates/migrator/src/migrations/m_2025_04_15/keymap.rs index d1443a922afc52a37912d8aa78b5c9f0d4b4e017..efbdc6b1c64443c4733c73568a13a70cc3fa1f97 100644 --- a/crates/migrator/src/migrations/m_2025_04_15/keymap.rs +++ b/crates/migrator/src/migrations/m_2025_04_15/keymap.rs @@ -25,7 +25,7 @@ fn replace_string_action( None } -/// "ctrl-k ctrl-1": "inline_completion::ToggleMenu" -> "edit_prediction::ToggleMenu" +/// "space": "outline_panel::Open" -> "outline_panel::OpenSelectedEntry" static STRING_REPLACE: LazyLock<HashMap<&str, &str>> = LazyLock::new(|| { HashMap::from_iter([("outline_panel::Open", "outline_panel::OpenSelectedEntry")]) }); diff --git a/crates/theme/src/schema.rs b/crates/theme/src/schema.rs index b2a13b54b662f106018667de9635a4c896e1993c..bed25d0c054fc4e1767cc852597db13dc2cb434c 100644 --- a/crates/theme/src/schema.rs +++ b/crates/theme/src/schema.rs @@ -4,11 +4,10 @@ use anyhow::Result; use gpui::{FontStyle, FontWeight, HighlightStyle, Hsla, WindowBackgroundAppearance}; use indexmap::IndexMap; use palette::FromColor; -use schemars::{JsonSchema, json_schema}; +use schemars::{JsonSchema, JsonSchema_repr}; use serde::{Deserialize, Deserializer, Serialize}; use serde_json::Value; use serde_repr::{Deserialize_repr, Serialize_repr}; -use std::borrow::Cow; use crate::{StatusColorsRefinement, ThemeColorsRefinement}; @@ -1486,7 +1485,7 @@ impl From<FontStyleContent> for FontStyle { } } -#[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, PartialEq)] +#[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr, JsonSchema_repr, PartialEq)] #[repr(u16)] pub enum FontWeightContent { Thin = 100, @@ -1500,19 +1499,6 @@ pub enum FontWeightContent { Black = 900, } -impl JsonSchema for FontWeightContent { - fn schema_name() -> Cow<'static, str> { - "FontWeightContent".into() - } - - fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { - json_schema!({ - "type": "integer", - "enum": [100, 200, 300, 400, 500, 600, 700, 800, 900] - }) - } -} - impl From<FontWeightContent> for FontWeight { fn from(value: FontWeightContent) -> Self { match value { diff --git a/crates/util/src/schemars.rs b/crates/util/src/schemars.rs index 4d8ab530dd6beb3cf3c448256ff4bde89f9de8f7..e162b41933117eb603d36601aaf4b87b0e3d1d85 100644 --- a/crates/util/src/schemars.rs +++ b/crates/util/src/schemars.rs @@ -15,7 +15,6 @@ pub fn replace_subschema<T: JsonSchema>( generator: &mut schemars::SchemaGenerator, schema: impl Fn() -> schemars::Schema, ) -> schemars::Schema { - // fallback on just using the schema name, which could collide. let schema_name = T::schema_name(); let definitions = generator.definitions_mut(); assert!(!definitions.contains_key(&format!("{schema_name}2"))); From caa4b529e4b130006990a31b4908e4252aa96fe4 Mon Sep 17 00:00:00 2001 From: Jason Lee <huacnlee@gmail.com> Date: Mon, 21 Jul 2025 07:38:54 +0800 Subject: [PATCH 214/658] gpui: Add tab focus support (#33008) Release Notes: - N/A With a `tab_index` and `tab_stop` option to `FocusHandle` to us can switch focus by `Tab`, `Shift-Tab`. The `tab_index` is from [WinUI](https://learn.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.control.tabindex?view=winrt-26100) and [HTML tabindex](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/tabindex), only the `tab_stop` is enabled that can be added into the `tab_handles` list. - Added `window.focus_next()` and `window.focus_previous()` method to switch focus. - Added `tab_index` to `InteractiveElement`. ```bash cargo run -p gpui --example tab_stop ``` https://github.com/user-attachments/assets/ac4e3e49-8359-436c-9a6e-badba2225211 --- crates/gpui/examples/tab_stop.rs | 130 +++++++++++++++++++++++++ crates/gpui/src/app.rs | 4 +- crates/gpui/src/elements/div.rs | 29 ++++-- crates/gpui/src/gpui.rs | 2 + crates/gpui/src/tab_stop.rs | 157 +++++++++++++++++++++++++++++++ crates/gpui/src/window.rs | 81 ++++++++++++++-- 6 files changed, 387 insertions(+), 16 deletions(-) create mode 100644 crates/gpui/examples/tab_stop.rs create mode 100644 crates/gpui/src/tab_stop.rs diff --git a/crates/gpui/examples/tab_stop.rs b/crates/gpui/examples/tab_stop.rs new file mode 100644 index 0000000000000000000000000000000000000000..9c58b52a5e93b154237f8822e6abc86a237c2d02 --- /dev/null +++ b/crates/gpui/examples/tab_stop.rs @@ -0,0 +1,130 @@ +use gpui::{ + App, Application, Bounds, Context, Div, ElementId, FocusHandle, KeyBinding, SharedString, + Stateful, Window, WindowBounds, WindowOptions, actions, div, prelude::*, px, size, +}; + +actions!(example, [Tab, TabPrev]); + +struct Example { + items: Vec<FocusHandle>, + message: SharedString, +} + +impl Example { + fn new(window: &mut Window, cx: &mut Context<Self>) -> Self { + let items = vec![ + cx.focus_handle().tab_index(1).tab_stop(true), + cx.focus_handle().tab_index(2).tab_stop(true), + cx.focus_handle().tab_index(3).tab_stop(true), + cx.focus_handle(), + cx.focus_handle().tab_index(2).tab_stop(true), + ]; + + window.focus(items.first().unwrap()); + Self { + items, + message: SharedString::from("Press `Tab`, `Shift-Tab` to switch focus."), + } + } + + fn on_tab(&mut self, _: &Tab, window: &mut Window, _: &mut Context<Self>) { + window.focus_next(); + self.message = SharedString::from("You have pressed `Tab`."); + } + + fn on_tab_prev(&mut self, _: &TabPrev, window: &mut Window, _: &mut Context<Self>) { + window.focus_prev(); + self.message = SharedString::from("You have pressed `Shift-Tab`."); + } +} + +impl Render for Example { + fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { + fn button(id: impl Into<ElementId>) -> Stateful<Div> { + div() + .id(id) + .h_10() + .flex_1() + .flex() + .justify_center() + .items_center() + .border_1() + .border_color(gpui::black()) + .bg(gpui::black()) + .text_color(gpui::white()) + .focus(|this| this.border_color(gpui::blue())) + .shadow_sm() + } + + div() + .id("app") + .on_action(cx.listener(Self::on_tab)) + .on_action(cx.listener(Self::on_tab_prev)) + .size_full() + .flex() + .flex_col() + .p_4() + .gap_3() + .bg(gpui::white()) + .text_color(gpui::black()) + .child(self.message.clone()) + .children( + self.items + .clone() + .into_iter() + .enumerate() + .map(|(ix, item_handle)| { + div() + .id(("item", ix)) + .track_focus(&item_handle) + .h_10() + .w_full() + .flex() + .justify_center() + .items_center() + .border_1() + .border_color(gpui::black()) + .when( + item_handle.tab_stop && item_handle.is_focused(window), + |this| this.border_color(gpui::blue()), + ) + .map(|this| match item_handle.tab_stop { + true => this + .hover(|this| this.bg(gpui::black().opacity(0.1))) + .child(format!("tab_index: {}", item_handle.tab_index)), + false => this.opacity(0.4).child("tab_stop: false"), + }) + }), + ) + .child( + div() + .flex() + .flex_row() + .gap_3() + .items_center() + .child(button("el1").tab_index(4).child("Button 1")) + .child(button("el2").tab_index(5).child("Button 2")), + ) + } +} + +fn main() { + Application::new().run(|cx: &mut App| { + cx.bind_keys([ + KeyBinding::new("tab", Tab, None), + KeyBinding::new("shift-tab", TabPrev, None), + ]); + + let bounds = Bounds::centered(None, size(px(800.), px(600.0)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |window, cx| cx.new(|cx| Example::new(window, cx)), + ) + .unwrap(); + + cx.activate(true); + }); +} diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index de7ba782b2039c00c729343daa92d094b59ad248..2771de9aac2bc721126091826aa672d194589e61 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -954,8 +954,8 @@ impl App { self.focus_handles .clone() .write() - .retain(|handle_id, count| { - if count.load(SeqCst) == 0 { + .retain(|handle_id, focus| { + if focus.ref_count.load(SeqCst) == 0 { for window_handle in self.windows() { window_handle .update(self, |_, window, _| { diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index cb53276bc2a879168e718e210b03b7af2061ad52..ed1666c53060dfdf3ed4c10a85a730d69f87986d 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -619,6 +619,13 @@ pub trait InteractiveElement: Sized { self } + /// Set index of the tab stop order. + fn tab_index(mut self, index: isize) -> Self { + self.interactivity().focusable = true; + self.interactivity().tab_index = Some(index); + self + } + /// Set the keymap context for this element. This will be used to determine /// which action to dispatch from the keymap. fn key_context<C, E>(mut self, key_context: C) -> Self @@ -1462,6 +1469,7 @@ pub struct Interactivity { pub(crate) tooltip_builder: Option<TooltipBuilder>, pub(crate) window_control: Option<WindowControlArea>, pub(crate) hitbox_behavior: HitboxBehavior, + pub(crate) tab_index: Option<isize>, #[cfg(any(feature = "inspector", debug_assertions))] pub(crate) source_location: Option<&'static core::panic::Location<'static>>, @@ -1521,12 +1529,17 @@ impl Interactivity { // 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() { - self.tracked_focus_handle = Some( - element_state - .focus_handle - .get_or_insert_with(|| cx.focus_handle()) - .clone(), - ); + 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); } } @@ -1729,6 +1742,10 @@ impl Interactivity { return ((), element_state); } + if let Some(focus_handle) = &self.tracked_focus_handle { + window.next_frame.tab_handles.insert(focus_handle); + } + window.with_element_opacity(style.opacity, |window| { style.paint(bounds, window, cx, |window: &mut Window, cx: &mut App| { window.with_text_style(style.text_style().cloned(), |window| { diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 4eb6fa8dabeb1476c779d36cbd61257faf431413..09799eb910f0eeece17fd9975c3c13f6accd2df6 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -95,6 +95,7 @@ mod style; mod styled; mod subscription; mod svg_renderer; +mod tab_stop; mod taffy; #[cfg(any(test, feature = "test-support"))] pub mod test; @@ -151,6 +152,7 @@ pub use style::*; pub use styled::*; pub use subscription::*; use svg_renderer::*; +pub(crate) use tab_stop::*; pub use taffy::{AvailableSpace, LayoutId}; #[cfg(any(test, feature = "test-support"))] pub use test::*; diff --git a/crates/gpui/src/tab_stop.rs b/crates/gpui/src/tab_stop.rs new file mode 100644 index 0000000000000000000000000000000000000000..2ec3f560e8be80d486f0920b4d41d1964c7645da --- /dev/null +++ b/crates/gpui/src/tab_stop.rs @@ -0,0 +1,157 @@ +use crate::{FocusHandle, FocusId}; + +/// Represents a collection of tab handles. +/// +/// Used to manage the `Tab` event to switch between focus handles. +#[derive(Default)] +pub(crate) struct TabHandles { + handles: Vec<FocusHandle>, +} + +impl TabHandles { + pub(crate) fn insert(&mut self, focus_handle: &FocusHandle) { + if !focus_handle.tab_stop { + return; + } + + let focus_handle = focus_handle.clone(); + + // Insert handle with same tab_index last + if let Some(ix) = self + .handles + .iter() + .position(|tab| tab.tab_index > focus_handle.tab_index) + { + self.handles.insert(ix, focus_handle); + } else { + self.handles.push(focus_handle); + } + } + + pub(crate) fn clear(&mut self) { + self.handles.clear(); + } + + fn current_index(&self, focused_id: Option<&FocusId>) -> usize { + self.handles + .iter() + .position(|h| Some(&h.id) == focused_id) + .unwrap_or_default() + } + + pub(crate) fn next(&self, focused_id: Option<&FocusId>) -> Option<FocusHandle> { + let ix = self.current_index(focused_id); + + let mut next_ix = ix + 1; + if next_ix + 1 > self.handles.len() { + next_ix = 0; + } + + if let Some(next_handle) = self.handles.get(next_ix) { + Some(next_handle.clone()) + } else { + None + } + } + + pub(crate) fn prev(&self, focused_id: Option<&FocusId>) -> Option<FocusHandle> { + let ix = self.current_index(focused_id); + let prev_ix; + if ix == 0 { + prev_ix = self.handles.len().saturating_sub(1); + } else { + prev_ix = ix.saturating_sub(1); + } + + if let Some(prev_handle) = self.handles.get(prev_ix) { + Some(prev_handle.clone()) + } else { + None + } + } +} + +#[cfg(test)] +mod tests { + use crate::{FocusHandle, FocusMap, TabHandles}; + use std::sync::Arc; + + #[test] + fn test_tab_handles() { + let focus_map = Arc::new(FocusMap::default()); + let mut tab = TabHandles::default(); + + let focus_handles = vec![ + FocusHandle::new(&focus_map).tab_stop(true).tab_index(0), + FocusHandle::new(&focus_map).tab_stop(true).tab_index(1), + FocusHandle::new(&focus_map).tab_stop(true).tab_index(1), + FocusHandle::new(&focus_map), + FocusHandle::new(&focus_map).tab_index(2), + FocusHandle::new(&focus_map).tab_stop(true).tab_index(0), + FocusHandle::new(&focus_map).tab_stop(true).tab_index(2), + ]; + + for handle in focus_handles.iter() { + tab.insert(&handle); + } + assert_eq!( + tab.handles + .iter() + .map(|handle| handle.id) + .collect::<Vec<_>>(), + vec![ + focus_handles[0].id, + focus_handles[5].id, + focus_handles[1].id, + focus_handles[2].id, + focus_handles[6].id, + ] + ); + + // next + assert_eq!(tab.next(None), Some(tab.handles[1].clone())); + assert_eq!( + tab.next(Some(&tab.handles[0].id)), + Some(tab.handles[1].clone()) + ); + assert_eq!( + tab.next(Some(&tab.handles[1].id)), + Some(tab.handles[2].clone()) + ); + assert_eq!( + tab.next(Some(&tab.handles[2].id)), + Some(tab.handles[3].clone()) + ); + assert_eq!( + tab.next(Some(&tab.handles[3].id)), + Some(tab.handles[4].clone()) + ); + assert_eq!( + tab.next(Some(&tab.handles[4].id)), + Some(tab.handles[0].clone()) + ); + + // prev + assert_eq!(tab.prev(None), Some(tab.handles[4].clone())); + assert_eq!( + tab.prev(Some(&tab.handles[0].id)), + Some(tab.handles[4].clone()) + ); + assert_eq!( + tab.prev(Some(&tab.handles[1].id)), + Some(tab.handles[0].clone()) + ); + assert_eq!( + tab.prev(Some(&tab.handles[2].id)), + Some(tab.handles[1].clone()) + ); + assert_eq!( + tab.prev(Some(&tab.handles[3].id)), + Some(tab.handles[2].clone()) + ); + assert_eq!( + tab.prev(Some(&tab.handles[4].id)), + Some(tab.handles[3].clone()) + ); + } +} diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index b6601829c74e6e267c48fe1c5aa9f9ca681d2855..963d2bb45c437e98d7a56587dedd7ff56827a56f 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -12,10 +12,11 @@ use crate::{ PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptButton, PromptLevel, Quad, Render, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Replay, ResizeEdge, SMOOTH_SVG_SCALE_FACTOR, SUBPIXEL_VARIANTS, ScaledPixels, Scene, Shadow, SharedString, Size, - StrikethroughStyle, Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, TextStyle, - TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, WindowAppearance, - WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations, WindowOptions, - WindowParams, WindowTextSystem, point, prelude::*, px, rems, size, transparent_black, + StrikethroughStyle, Style, SubscriberSet, Subscription, TabHandles, TaffyLayoutEngine, Task, + TextStyle, TextStyleRefinement, TransformationMatrix, Underline, UnderlineStyle, + WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControls, WindowDecorations, + WindowOptions, WindowParams, WindowTextSystem, point, prelude::*, px, rems, size, + transparent_black, }; use anyhow::{Context as _, Result, anyhow}; use collections::{FxHashMap, FxHashSet}; @@ -222,7 +223,12 @@ impl ArenaClearNeeded { } } -pub(crate) type FocusMap = RwLock<SlotMap<FocusId, AtomicUsize>>; +pub(crate) type FocusMap = RwLock<SlotMap<FocusId, FocusRef>>; +pub(crate) struct FocusRef { + pub(crate) ref_count: AtomicUsize, + pub(crate) tab_index: isize, + pub(crate) tab_stop: bool, +} impl FocusId { /// Obtains whether the element associated with this handle is currently focused. @@ -258,6 +264,10 @@ impl FocusId { pub struct FocusHandle { pub(crate) id: FocusId, handles: Arc<FocusMap>, + /// The index of this element in the tab order. + pub tab_index: isize, + /// Whether this element can be focused by tab navigation. + pub tab_stop: bool, } impl std::fmt::Debug for FocusHandle { @@ -268,25 +278,54 @@ impl std::fmt::Debug for FocusHandle { impl FocusHandle { pub(crate) fn new(handles: &Arc<FocusMap>) -> Self { - let id = handles.write().insert(AtomicUsize::new(1)); + let id = handles.write().insert(FocusRef { + ref_count: AtomicUsize::new(1), + tab_index: 0, + tab_stop: false, + }); + Self { id, + tab_index: 0, + tab_stop: false, handles: handles.clone(), } } pub(crate) fn for_id(id: FocusId, handles: &Arc<FocusMap>) -> Option<Self> { let lock = handles.read(); - let ref_count = lock.get(id)?; - if atomic_incr_if_not_zero(ref_count) == 0 { + let focus = lock.get(id)?; + if atomic_incr_if_not_zero(&focus.ref_count) == 0 { return None; } Some(Self { id, + tab_index: focus.tab_index, + tab_stop: focus.tab_stop, handles: handles.clone(), }) } + /// Sets the tab index of the element associated with this handle. + pub fn tab_index(mut self, index: isize) -> Self { + self.tab_index = index; + if let Some(focus) = self.handles.write().get_mut(self.id) { + focus.tab_index = index; + } + self + } + + /// Sets whether the element associated with this handle is a tab stop. + /// + /// When `false`, the element will not be included in the tab order. + pub fn tab_stop(mut self, tab_stop: bool) -> Self { + self.tab_stop = tab_stop; + if let Some(focus) = self.handles.write().get_mut(self.id) { + focus.tab_stop = tab_stop; + } + self + } + /// Converts this focus handle into a weak variant, which does not prevent it from being released. pub fn downgrade(&self) -> WeakFocusHandle { WeakFocusHandle { @@ -354,6 +393,7 @@ impl Drop for FocusHandle { .read() .get(self.id) .unwrap() + .ref_count .fetch_sub(1, SeqCst); } } @@ -642,6 +682,7 @@ pub(crate) struct Frame { pub(crate) next_inspector_instance_ids: FxHashMap<Rc<crate::InspectorElementPath>, usize>, #[cfg(any(feature = "inspector", debug_assertions))] pub(crate) inspector_hitboxes: FxHashMap<HitboxId, crate::InspectorElementId>, + pub(crate) tab_handles: TabHandles, } #[derive(Clone, Default)] @@ -689,6 +730,7 @@ impl Frame { #[cfg(any(feature = "inspector", debug_assertions))] inspector_hitboxes: FxHashMap::default(), + tab_handles: TabHandles::default(), } } @@ -704,6 +746,7 @@ impl Frame { self.hitboxes.clear(); self.window_control_hitboxes.clear(); self.deferred_draws.clear(); + self.tab_handles.clear(); self.focus = None; #[cfg(any(feature = "inspector", debug_assertions))] @@ -1289,6 +1332,28 @@ impl Window { self.focus_enabled = false; } + /// Move focus to next tab stop. + pub fn focus_next(&mut self) { + if !self.focus_enabled { + return; + } + + if let Some(handle) = self.rendered_frame.tab_handles.next(self.focus.as_ref()) { + self.focus(&handle) + } + } + + /// Move focus to previous tab stop. + pub fn focus_prev(&mut self) { + if !self.focus_enabled { + return; + } + + if let Some(handle) = self.rendered_frame.tab_handles.prev(self.focus.as_ref()) { + self.focus(&handle) + } + } + /// Accessor for the text system. pub fn text_system(&self) -> &Arc<WindowTextSystem> { &self.text_system From 57ab09c2dabe5282926593360e0d865b19fe8e36 Mon Sep 17 00:00:00 2001 From: Ben Brandt <benjamin.j.brandt@gmail.com> Date: Mon, 21 Jul 2025 11:53:05 +0200 Subject: [PATCH 215/658] claude: Don't quote executable path in mcp configuration (#34805) This was generating an invalid string for the configuration, removing the extra quotes makes it work. This affected the versions of Zed that have a space in their name. Release Notes: - N/A --- crates/agent_servers/src/claude/mcp_server.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/agent_servers/src/claude/mcp_server.rs b/crates/agent_servers/src/claude/mcp_server.rs index fa61e671123b3ff186aa4db91de4408f4f0b4a9e..468027c4c3dd4c3391dc8196e54a840ce01a965b 100644 --- a/crates/agent_servers/src/claude/mcp_server.rs +++ b/crates/agent_servers/src/claude/mcp_server.rs @@ -69,9 +69,6 @@ impl ClaudeMcpServer { } pub fn server_config(&self) -> Result<McpServerConfig> { - #[cfg(not(target_os = "windows"))] - let zed_path = util::get_shell_safe_zed_path()?; - #[cfg(target_os = "windows")] let zed_path = std::env::current_exe() .context("finding current executable path for use in mcp_server")? .to_string_lossy() From 88af35fe4727f5187608cfb396def24f4c60c004 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 21 Jul 2025 13:44:51 +0200 Subject: [PATCH 216/658] collab: Add screen selector (#31506) Instead of selecting a screen to share arbitrarily, we'll now allow user to select the screen to share. Note that sharing multiple screens at the time is still not supported (though prolly not too far-fetched). Related to #4666 ![image](https://github.com/user-attachments/assets/1afb664f-3cdb-4e0a-bb29-9d7093d87fa5) Release Notes: - Added screen selector dropdown to screen share button --------- Co-authored-by: Kirill Bulatov <kirill@zed.dev> Co-authored-by: Cole Miller <cole@zed.dev> --- .config/hakari.toml | 2 +- Cargo.lock | 6 +- Cargo.toml | 4 +- crates/call/src/call_impl/room.rs | 61 +++--- crates/collab/src/tests/following_tests.rs | 12 +- crates/collab/src/tests/integration_tests.rs | 21 +- crates/collab_ui/src/collab_panel.rs | 22 +- crates/gpui/src/app.rs | 2 +- crates/gpui/src/platform.rs | 31 ++- .../src/platform/linux/headless/client.rs | 2 +- crates/gpui/src/platform/linux/platform.rs | 4 +- .../gpui/src/platform/linux/wayland/client.rs | 2 +- crates/gpui/src/platform/linux/x11/client.rs | 2 +- crates/gpui/src/platform/mac/platform.rs | 2 +- .../gpui/src/platform/mac/screen_capture.rs | 102 ++++++++-- .../gpui/src/platform/scap_screen_capture.rs | 99 ++++++--- crates/gpui/src/platform/test.rs | 2 +- crates/gpui/src/platform/test/platform.rs | 22 +- crates/gpui/src/platform/windows/platform.rs | 2 +- .../src/livekit_client/playback.rs | 6 +- .../src/mock_client/participant.rs | 6 +- crates/title_bar/Cargo.toml | 1 + crates/title_bar/src/collab.rs | 192 +++++++++++++++--- crates/title_bar/src/title_bar.rs | 4 +- .../ui/src/components/button/icon_button.rs | 3 +- crates/ui/src/components/context_menu.rs | 6 + 26 files changed, 473 insertions(+), 145 deletions(-) diff --git a/.config/hakari.toml b/.config/hakari.toml index 5168887581c8a1fdae0478e74b0f01225c7a1465..2050065cc2d6be2a27ec012dcd125af992793eeb 100644 --- a/.config/hakari.toml +++ b/.config/hakari.toml @@ -24,7 +24,7 @@ workspace-members = [ 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 = "270538dc780f5240723233ff901e1054641ed318" }, + { name = "scap", git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7" }, ] [final-excludes] diff --git a/Cargo.lock b/Cargo.lock index a5ea621cd14662cba0838558b70d3e13b51c7840..bc69de7a7cd551590252a0a06f3dca927197e8d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14185,7 +14185,7 @@ dependencies = [ [[package]] name = "scap" version = "0.0.8" -source = "git+https://github.com/zed-industries/scap?rev=270538dc780f5240723233ff901e1054641ed318#270538dc780f5240723233ff901e1054641ed318" +source = "git+https://github.com/zed-industries/scap?rev=808aa5c45b41e8f44729d02e38fd00a2fe2722e7#808aa5c45b41e8f44729d02e38fd00a2fe2722e7" dependencies = [ "anyhow", "cocoa 0.25.0", @@ -16484,6 +16484,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" name = "title_bar" version = "0.1.0" dependencies = [ + "anyhow", "auto_update", "call", "chrono", @@ -18729,8 +18730,7 @@ dependencies = [ [[package]] name = "windows-capture" version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59d10b4be8b907c7055bc7270dd68d2b920978ffacc1599dcb563a79f0e68d16" +source = "git+https://github.com/zed-industries/windows-capture.git?rev=f0d6c1b6691db75461b732f6d5ff56eed002eeb9#f0d6c1b6691db75461b732f6d5ff56eed002eeb9" dependencies = [ "clap", "ctrlc", diff --git a/Cargo.toml b/Cargo.toml index aa9af9a423eb0d283df821a46424a4702154bce5..0169d32eb6a671dad002e8d520e54a38a5901f8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -553,8 +553,7 @@ rustc-demangle = "0.1.23" rustc-hash = "2.1.0" rustls = { version = "0.23.26" } rustls-platform-verifier = "0.5.0" -# When updating scap rev, also update it in .config/hakari.toml -scap = { git = "https://github.com/zed-industries/scap", rev = "270538dc780f5240723233ff901e1054641ed318", default-features = false } +scap = { git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7", default-features = false } schemars = { version = "1.0", features = ["indexmap2"] } semver = "1.0" serde = { version = "1.0", features = ["derive", "rc"] } @@ -708,6 +707,7 @@ features = [ [patch.crates-io] notify = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" } notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" } +windows-capture = { git = "https://github.com/zed-industries/windows-capture.git", rev = "f0d6c1b6691db75461b732f6d5ff56eed002eeb9" } # Makes the workspace hack crate refer to the local one, but only when you're building locally workspace-hack = { path = "tooling/workspace-hack" } diff --git a/crates/call/src/call_impl/room.rs b/crates/call/src/call_impl/room.rs index 7aac72ed46e777a1c70a194cf79f9bad160d1028..afeee4c924feb2990668f953d5b2f7dfcff26f34 100644 --- a/crates/call/src/call_impl/room.rs +++ b/crates/call/src/call_impl/room.rs @@ -11,15 +11,18 @@ use client::{ use collections::{BTreeMap, HashMap, HashSet}; use fs::Fs; use futures::{FutureExt, StreamExt}; -use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Task, WeakEntity}; +use gpui::{ + App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, ScreenCaptureSource, + ScreenCaptureStream, Task, WeakEntity, +}; use gpui_tokio::Tokio; use language::LanguageRegistry; use livekit::{LocalTrackPublication, ParticipantIdentity, RoomEvent}; -use livekit_client::{self as livekit, TrackSid}; +use livekit_client::{self as livekit, AudioStream, TrackSid}; use postage::{sink::Sink, stream::Stream, watch}; use project::Project; use settings::Settings as _; -use std::{any::Any, future::Future, mem, rc::Rc, sync::Arc, time::Duration}; +use std::{future::Future, mem, rc::Rc, sync::Arc, time::Duration}; use util::{ResultExt, TryFutureExt, post_inc}; pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); @@ -1251,12 +1254,21 @@ impl Room { }) } - pub fn is_screen_sharing(&self) -> bool { + pub fn is_sharing_screen(&self) -> bool { self.live_kit.as_ref().map_or(false, |live_kit| { !matches!(live_kit.screen_track, LocalTrack::None) }) } + pub fn shared_screen_id(&self) -> Option<u64> { + self.live_kit.as_ref().and_then(|lk| match lk.screen_track { + LocalTrack::Published { ref _stream, .. } => { + _stream.metadata().ok().map(|meta| meta.id) + } + _ => None, + }) + } + pub fn is_sharing_mic(&self) -> bool { self.live_kit.as_ref().map_or(false, |live_kit| { !matches!(live_kit.microphone_track, LocalTrack::None) @@ -1369,11 +1381,15 @@ impl Room { }) } - pub fn share_screen(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> { + pub fn share_screen( + &mut self, + source: Rc<dyn ScreenCaptureSource>, + cx: &mut Context<Self>, + ) -> Task<Result<()>> { if self.status.is_offline() { return Task::ready(Err(anyhow!("room is offline"))); } - if self.is_screen_sharing() { + if self.is_sharing_screen() { return Task::ready(Err(anyhow!("screen was already shared"))); } @@ -1386,20 +1402,8 @@ impl Room { return Task::ready(Err(anyhow!("live-kit was not initialized"))); }; - let sources = cx.screen_capture_sources(); - cx.spawn(async move |this, cx| { - let sources = sources - .await - .map_err(|error| error.into()) - .and_then(|sources| sources); - let source = - sources.and_then(|sources| sources.into_iter().next().context("no display found")); - - let publication = match source { - Ok(source) => participant.publish_screenshare_track(&*source, cx).await, - Err(error) => Err(error), - }; + let publication = participant.publish_screenshare_track(&*source, cx).await; this.update(cx, |this, cx| { let live_kit = this @@ -1426,7 +1430,7 @@ impl Room { } else { live_kit.screen_track = LocalTrack::Published { track_publication: publication, - _stream: Box::new(stream), + _stream: stream, }; cx.notify(); } @@ -1492,7 +1496,7 @@ impl Room { } } - pub fn unshare_screen(&mut self, cx: &mut Context<Self>) -> Result<()> { + pub fn unshare_screen(&mut self, play_sound: bool, cx: &mut Context<Self>) -> Result<()> { anyhow::ensure!(!self.status.is_offline(), "room is offline"); let live_kit = self @@ -1516,7 +1520,10 @@ impl Room { cx.notify(); } - Audio::play_sound(Sound::StopScreenshare, cx); + if play_sound { + Audio::play_sound(Sound::StopScreenshare, cx); + } + Ok(()) } } @@ -1624,8 +1631,8 @@ fn spawn_room_connection( struct LiveKitRoom { room: Rc<livekit::Room>, - screen_track: LocalTrack, - microphone_track: LocalTrack, + screen_track: LocalTrack<dyn ScreenCaptureStream>, + microphone_track: LocalTrack<AudioStream>, /// Tracks whether we're currently in a muted state due to auto-mute from deafening or manual mute performed by user. muted_by_user: bool, deafened: bool, @@ -1663,18 +1670,18 @@ impl LiveKitRoom { } } -enum LocalTrack { +enum LocalTrack<Stream: ?Sized> { None, Pending { publish_id: usize, }, Published { track_publication: LocalTrackPublication, - _stream: Box<dyn Any>, + _stream: Box<Stream>, }, } -impl Default for LocalTrack { +impl<T: ?Sized> Default for LocalTrack<T> { fn default() -> Self { Self::None } diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index 1a4c3a70a4c6cb622cb90dcd636a845c77c756c6..d9fd8ffeb2a6c693c3570409070f7a0fbfe33ea2 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -439,7 +439,7 @@ async fn test_basic_following( editor_a1.item_id() ); - #[cfg(all(not(target_os = "macos"), not(target_os = "windows")))] + // #[cfg(all(not(target_os = "macos"), not(target_os = "windows")))] { use crate::rpc::RECONNECT_TIMEOUT; use gpui::TestScreenCaptureSource; @@ -456,11 +456,19 @@ async fn test_basic_following( .await .unwrap(); cx_b.set_screen_capture_sources(vec![display]); + let source = cx_b + .read(|cx| cx.screen_capture_sources()) + .await + .unwrap() + .unwrap() + .into_iter() + .next() + .unwrap(); active_call_b .update(cx_b, |call, cx| { call.room() .unwrap() - .update(cx, |room, cx| room.share_screen(cx)) + .update(cx, |room, cx| room.share_screen(source, cx)) }) .await .unwrap(); diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index d1099a327a4d090dcd26fff8d5308e36922a49b6..9795c27574d1b744e02064683925366012b3defa 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -277,11 +277,19 @@ async fn test_basic_calls( let events_b = active_call_events(cx_b); let events_c = active_call_events(cx_c); cx_a.set_screen_capture_sources(vec![display]); + let screen_a = cx_a + .update(|cx| cx.screen_capture_sources()) + .await + .unwrap() + .unwrap() + .into_iter() + .next() + .unwrap(); active_call_a .update(cx_a, |call, cx| { call.room() .unwrap() - .update(cx, |room, cx| room.share_screen(cx)) + .update(cx, |room, cx| room.share_screen(screen_a, cx)) }) .await .unwrap(); @@ -6312,11 +6320,20 @@ async fn test_join_call_after_screen_was_shared( // User A shares their screen let display = gpui::TestScreenCaptureSource::new(); cx_a.set_screen_capture_sources(vec![display]); + let screen_a = cx_a + .update(|cx| cx.screen_capture_sources()) + .await + .unwrap() + .unwrap() + .into_iter() + .next() + .unwrap(); + active_call_a .update(cx_a, |call, cx| { call.room() .unwrap() - .update(cx, |room, cx| room.share_screen(cx)) + .update(cx, |room, cx| room.share_screen(screen_a, cx)) }) .await .unwrap(); diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index ec23e2c3f536dc38db05f448f0d239d243a15756..4d5973481e6cf776355c2e0d2a6cbc6d9f02d1b4 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -144,10 +144,22 @@ pub fn init(cx: &mut App) { if let Some(room) = room { window.defer(cx, move |_window, cx| { room.update(cx, |room, cx| { - if room.is_screen_sharing() { - room.unshare_screen(cx).ok(); + if room.is_sharing_screen() { + room.unshare_screen(true, cx).ok(); } else { - room.share_screen(cx).detach_and_log_err(cx); + let sources = cx.screen_capture_sources(); + + cx.spawn(async move |room, cx| { + let sources = sources.await??; + let first = sources.into_iter().next(); + if let Some(first) = first { + room.update(cx, |room, cx| room.share_screen(first, cx))? + .await + } else { + Ok(()) + } + }) + .detach_and_log_err(cx); }; }); }); @@ -528,10 +540,10 @@ impl CollabPanel { project_id: project.id, worktree_root_names: project.worktree_root_names.clone(), host_user_id: user_id, - is_last: projects.peek().is_none() && !room.is_screen_sharing(), + is_last: projects.peek().is_none() && !room.is_sharing_screen(), }); } - if room.is_screen_sharing() { + if room.is_sharing_screen() { self.entries.push(ListEntry::ParticipantScreen { peer_id: None, is_last: true, diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 2771de9aac2bc721126091826aa672d194589e61..759d33563e0af1be038a98f78712f7b3f18ef327 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -696,7 +696,7 @@ impl App { /// Returns a list of available screen capture sources. pub fn screen_capture_sources( &self, - ) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> { + ) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> { self.platform.screen_capture_sources() } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 8918fdd28bda083abd0335f6f500b35d01895c58..6f227f1d077e96337c82ad7eba9b1d0fd9c7dfc0 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -85,7 +85,7 @@ pub(crate) use test::*; pub(crate) use windows::*; #[cfg(any(test, feature = "test-support"))] -pub use test::{TestDispatcher, TestScreenCaptureSource}; +pub use test::{TestDispatcher, TestScreenCaptureSource, TestScreenCaptureStream}; /// Returns a background executor for the current platform. pub fn background_executor() -> BackgroundExecutor { @@ -189,13 +189,12 @@ pub(crate) trait Platform: 'static { false } #[cfg(feature = "screen-capture")] - fn screen_capture_sources( - &self, - ) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>>; + fn screen_capture_sources(&self) + -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>>; #[cfg(not(feature = "screen-capture"))] fn screen_capture_sources( &self, - ) -> oneshot::Receiver<anyhow::Result<Vec<Box<dyn ScreenCaptureSource>>>> { + ) -> oneshot::Receiver<anyhow::Result<Vec<Rc<dyn ScreenCaptureSource>>>> { let (sources_tx, sources_rx) = oneshot::channel(); sources_tx .send(Err(anyhow::anyhow!( @@ -293,10 +292,23 @@ pub trait PlatformDisplay: Send + Sync + Debug { } } +/// Metadata for a given [ScreenCaptureSource] +#[derive(Clone)] +pub struct SourceMetadata { + /// Opaque identifier of this screen. + pub id: u64, + /// Human-readable label for this source. + pub label: Option<SharedString>, + /// Whether this source is the main display. + pub is_main: Option<bool>, + /// Video resolution of this source. + pub resolution: Size<DevicePixels>, +} + /// A source of on-screen video content that can be captured. pub trait ScreenCaptureSource { - /// Returns the video resolution of this source. - fn resolution(&self) -> Result<Size<DevicePixels>>; + /// Returns metadata for this source. + fn metadata(&self) -> Result<SourceMetadata>; /// Start capture video from this source, invoking the given callback /// with each frame. @@ -308,7 +320,10 @@ pub trait ScreenCaptureSource { } /// A video stream captured from a screen. -pub trait ScreenCaptureStream {} +pub trait ScreenCaptureStream { + /// Returns metadata for this source. + fn metadata(&self) -> Result<SourceMetadata>; +} /// A frame of video captured from a screen. pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame); diff --git a/crates/gpui/src/platform/linux/headless/client.rs b/crates/gpui/src/platform/linux/headless/client.rs index 663a740389e68c0505a4b3f1f55a3b4681aacfa6..da54db371033bac53e2ac3324306fa86eb57fb57 100644 --- a/crates/gpui/src/platform/linux/headless/client.rs +++ b/crates/gpui/src/platform/linux/headless/client.rs @@ -73,7 +73,7 @@ impl LinuxClient for HeadlessClient { #[cfg(feature = "screen-capture")] fn screen_capture_sources( &self, - ) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Box<dyn crate::ScreenCaptureSource>>>> + ) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>> { let (mut tx, rx) = futures::channel::oneshot::channel(); tx.send(Err(anyhow::anyhow!( diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index c4b90ccf084627640a6cda5806d1bcb63415feb3..a52841e1afe4b0a396c68ef72587777edd5eb14e 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -56,7 +56,7 @@ pub trait LinuxClient { #[cfg(feature = "screen-capture")] fn screen_capture_sources( &self, - ) -> oneshot::Receiver<Result<Vec<Box<dyn crate::ScreenCaptureSource>>>>; + ) -> oneshot::Receiver<Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>>; fn open_window( &self, @@ -245,7 +245,7 @@ impl<P: LinuxClient + 'static> Platform for P { #[cfg(feature = "screen-capture")] fn screen_capture_sources( &self, - ) -> oneshot::Receiver<Result<Vec<Box<dyn crate::ScreenCaptureSource>>>> { + ) -> oneshot::Receiver<Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>> { self.screen_capture_sources() } diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 57d1dcec04ee6aa1828c98286c9115df4ccb6d44..72e4477ecf697a9f6443dffb80e0637202d3b848 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -672,7 +672,7 @@ impl LinuxClient for WaylandClient { #[cfg(feature = "screen-capture")] fn screen_capture_sources( &self, - ) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Box<dyn crate::ScreenCaptureSource>>>> + ) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>> { // TODO: Get screen capture working on wayland. Be sure to try window resizing as that may // be tricky. diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index 0606f619c6fb808e4be42abe07f51d1e124a69f4..d1cb7d00cc7468f7b9bc02b10dfde04e195b8950 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -1448,7 +1448,7 @@ impl LinuxClient for X11Client { #[cfg(feature = "screen-capture")] fn screen_capture_sources( &self, - ) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Box<dyn crate::ScreenCaptureSource>>>> + ) -> futures::channel::oneshot::Receiver<anyhow::Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>> { crate::platform::scap_screen_capture::scap_screen_sources( &self.0.borrow().common.foreground_executor, diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index d9bb665469002bd89e248a8593f56b12cfebcca1..1d2146cf73562beed6c26754396dc2c4c0c915f9 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -583,7 +583,7 @@ impl Platform for MacPlatform { #[cfg(feature = "screen-capture")] fn screen_capture_sources( &self, - ) -> oneshot::Receiver<Result<Vec<Box<dyn crate::ScreenCaptureSource>>>> { + ) -> oneshot::Receiver<Result<Vec<Rc<dyn crate::ScreenCaptureSource>>>> { super::screen_capture::get_sources() } diff --git a/crates/gpui/src/platform/mac/screen_capture.rs b/crates/gpui/src/platform/mac/screen_capture.rs index af5e02fc06cbd6a82c4502a5f20e54237b5dc64d..4d4ffa6896520e465dfeb7b1ccc06e1149f9e25d 100644 --- a/crates/gpui/src/platform/mac/screen_capture.rs +++ b/crates/gpui/src/platform/mac/screen_capture.rs @@ -1,5 +1,5 @@ use crate::{ - DevicePixels, ForegroundExecutor, Size, + DevicePixels, ForegroundExecutor, SharedString, SourceMetadata, platform::{ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream}, size, }; @@ -7,8 +7,9 @@ use anyhow::{Result, anyhow}; use block::ConcreteBlock; use cocoa::{ base::{YES, id, nil}, - foundation::NSArray, + foundation::{NSArray, NSString}, }; +use collections::HashMap; use core_foundation::base::TCFType; use core_graphics::display::{ CGDirectDisplayID, CGDisplayCopyDisplayMode, CGDisplayModeGetPixelHeight, @@ -32,11 +33,13 @@ use super::NSStringExt; #[derive(Clone)] pub struct MacScreenCaptureSource { sc_display: id, + meta: Option<ScreenMeta>, } pub struct MacScreenCaptureStream { sc_stream: id, sc_stream_output: id, + meta: SourceMetadata, } static mut DELEGATE_CLASS: *const Class = ptr::null(); @@ -47,19 +50,31 @@ const FRAME_CALLBACK_IVAR: &str = "frame_callback"; const SCStreamOutputTypeScreen: NSInteger = 0; impl ScreenCaptureSource for MacScreenCaptureSource { - fn resolution(&self) -> Result<Size<DevicePixels>> { - unsafe { + fn metadata(&self) -> Result<SourceMetadata> { + let (display_id, size) = unsafe { let display_id: CGDirectDisplayID = msg_send![self.sc_display, displayID]; let display_mode_ref = CGDisplayCopyDisplayMode(display_id); let width = CGDisplayModeGetPixelWidth(display_mode_ref); let height = CGDisplayModeGetPixelHeight(display_mode_ref); CGDisplayModeRelease(display_mode_ref); - Ok(size( - DevicePixels(width as i32), - DevicePixels(height as i32), - )) - } + ( + display_id, + size(DevicePixels(width as i32), DevicePixels(height as i32)), + ) + }; + let (label, is_main) = self + .meta + .clone() + .map(|meta| (meta.label, meta.is_main)) + .unzip(); + + Ok(SourceMetadata { + id: display_id as u64, + label, + is_main, + resolution: size, + }) } fn stream( @@ -89,9 +104,9 @@ impl ScreenCaptureSource for MacScreenCaptureSource { Box::into_raw(Box::new(frame_callback)) as *mut c_void, ); - let resolution = self.resolution().unwrap(); - let _: id = msg_send![configuration, setWidth: resolution.width.0 as i64]; - let _: id = msg_send![configuration, setHeight: resolution.height.0 as i64]; + let meta = self.metadata().unwrap(); + let _: id = msg_send![configuration, setWidth: meta.resolution.width.0 as i64]; + let _: id = msg_send![configuration, setHeight: meta.resolution.height.0 as i64]; let stream: id = msg_send![stream, initWithFilter:filter configuration:configuration delegate:delegate]; let (mut tx, rx) = oneshot::channel(); @@ -110,6 +125,7 @@ impl ScreenCaptureSource for MacScreenCaptureSource { move |error: id| { let result = if error == nil { let stream = MacScreenCaptureStream { + meta: meta.clone(), sc_stream: stream, sc_stream_output: output, }; @@ -138,7 +154,11 @@ impl Drop for MacScreenCaptureSource { } } -impl ScreenCaptureStream for MacScreenCaptureStream {} +impl ScreenCaptureStream for MacScreenCaptureStream { + fn metadata(&self) -> Result<SourceMetadata> { + Ok(self.meta.clone()) + } +} impl Drop for MacScreenCaptureStream { fn drop(&mut self) { @@ -164,24 +184,74 @@ impl Drop for MacScreenCaptureStream { } } -pub(crate) fn get_sources() -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> { +#[derive(Clone)] +struct ScreenMeta { + label: SharedString, + // Is this the screen with menu bar? + is_main: bool, +} + +unsafe fn screen_id_to_human_label() -> HashMap<CGDirectDisplayID, ScreenMeta> { + let screens: id = msg_send![class!(NSScreen), screens]; + let count: usize = msg_send![screens, count]; + let mut map = HashMap::default(); + let screen_number_key = unsafe { NSString::alloc(nil).init_str("NSScreenNumber") }; + for i in 0..count { + let screen: id = msg_send![screens, objectAtIndex: i]; + let device_desc: id = msg_send![screen, deviceDescription]; + if device_desc == nil { + continue; + } + + let nsnumber: id = msg_send![device_desc, objectForKey: screen_number_key]; + if nsnumber == nil { + continue; + } + + let screen_id: u32 = msg_send![nsnumber, unsignedIntValue]; + + let name: id = msg_send![screen, localizedName]; + if name != nil { + let cstr: *const std::os::raw::c_char = msg_send![name, UTF8String]; + let rust_str = unsafe { + std::ffi::CStr::from_ptr(cstr) + .to_string_lossy() + .into_owned() + }; + map.insert( + screen_id, + ScreenMeta { + label: rust_str.into(), + is_main: i == 0, + }, + ); + } + } + map +} + +pub(crate) fn get_sources() -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> { unsafe { let (mut tx, rx) = oneshot::channel(); let tx = Rc::new(RefCell::new(Some(tx))); - + let screen_id_to_label = screen_id_to_human_label(); let block = ConcreteBlock::new(move |shareable_content: id, error: id| { let Some(mut tx) = tx.borrow_mut().take() else { return; }; + let result = if error == nil { let displays: id = msg_send![shareable_content, displays]; let mut result = Vec::new(); for i in 0..displays.count() { let display = displays.objectAtIndex(i); + let id: CGDirectDisplayID = msg_send![display, displayID]; + let meta = screen_id_to_label.get(&id).cloned(); let source = MacScreenCaptureSource { sc_display: msg_send![display, retain], + meta, }; - result.push(Box::new(source) as Box<dyn ScreenCaptureSource>); + result.push(Rc::new(source) as Rc<dyn ScreenCaptureSource>); } Ok(result) } else { diff --git a/crates/gpui/src/platform/scap_screen_capture.rs b/crates/gpui/src/platform/scap_screen_capture.rs index c5e2267a37c794aeab70bc06d88d849b64be1c6f..32041b655fdc20b046717291c623dcb5c4d5146c 100644 --- a/crates/gpui/src/platform/scap_screen_capture.rs +++ b/crates/gpui/src/platform/scap_screen_capture.rs @@ -1,10 +1,12 @@ //! Screen capture for Linux and Windows use crate::{ DevicePixels, ForegroundExecutor, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, - Size, size, + Size, SourceMetadata, size, }; use anyhow::{Context as _, Result, anyhow}; use futures::channel::oneshot; +use scap::Target; +use std::rc::Rc; use std::sync::Arc; use std::sync::atomic::{self, AtomicBool}; @@ -15,7 +17,7 @@ use std::sync::atomic::{self, AtomicBool}; #[allow(dead_code)] pub(crate) fn scap_screen_sources( foreground_executor: &ForegroundExecutor, -) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> { +) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> { let (sources_tx, sources_rx) = oneshot::channel(); get_screen_targets(sources_tx); to_dyn_screen_capture_sources(sources_rx, foreground_executor) @@ -29,14 +31,14 @@ pub(crate) fn scap_screen_sources( #[allow(dead_code)] pub(crate) fn start_scap_default_target_source( foreground_executor: &ForegroundExecutor, -) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> { +) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> { let (sources_tx, sources_rx) = oneshot::channel(); start_default_target_screen_capture(sources_tx); to_dyn_screen_capture_sources(sources_rx, foreground_executor) } struct ScapCaptureSource { - target: scap::Target, + target: scap::Display, size: Size<DevicePixels>, } @@ -52,7 +54,7 @@ fn get_screen_targets(sources_tx: oneshot::Sender<Result<Vec<ScapCaptureSource>> } }; let sources = targets - .iter() + .into_iter() .filter_map(|target| match target { scap::Target::Display(display) => { let size = Size { @@ -60,7 +62,7 @@ fn get_screen_targets(sources_tx: oneshot::Sender<Result<Vec<ScapCaptureSource>> height: DevicePixels(display.height as i32), }; Some(ScapCaptureSource { - target: target.clone(), + target: display, size, }) } @@ -72,8 +74,13 @@ fn get_screen_targets(sources_tx: oneshot::Sender<Result<Vec<ScapCaptureSource>> } impl ScreenCaptureSource for ScapCaptureSource { - fn resolution(&self) -> Result<Size<DevicePixels>> { - Ok(self.size) + fn metadata(&self) -> Result<SourceMetadata> { + Ok(SourceMetadata { + resolution: self.size, + label: Some(self.target.title.clone().into()), + is_main: None, + id: self.target.id as u64, + }) } fn stream( @@ -85,13 +92,15 @@ impl ScreenCaptureSource for ScapCaptureSource { let target = self.target.clone(); // Due to use of blocking APIs, a dedicated thread is used. - std::thread::spawn(move || match new_scap_capturer(Some(target)) { - Ok(mut capturer) => { - capturer.start_capture(); - run_capture(capturer, frame_callback, stream_tx); - } - Err(e) => { - stream_tx.send(Err(e)).ok(); + std::thread::spawn(move || { + match new_scap_capturer(Some(scap::Target::Display(target.clone()))) { + Ok(mut capturer) => { + capturer.start_capture(); + run_capture(capturer, target.clone(), frame_callback, stream_tx); + } + Err(e) => { + stream_tx.send(Err(e)).ok(); + } } }); @@ -107,6 +116,7 @@ struct ScapDefaultTargetCaptureSource { // Callback for frames. Box<dyn Fn(ScreenCaptureFrame) + Send>, )>, + target: scap::Display, size: Size<DevicePixels>, } @@ -123,33 +133,48 @@ fn start_default_target_screen_capture( .get_next_frame() .context("Failed to get first frame of screenshare to get the size.")?; let size = frame_size(&first_frame); - Ok((capturer, size)) + let target = capturer + .target() + .context("Unable to determine the target display.")?; + let target = target.clone(); + Ok((capturer, size, target)) }); match start_result { - Err(e) => { - sources_tx.send(Err(e)).ok(); - } - Ok((capturer, size)) => { + Ok((capturer, size, Target::Display(display))) => { let (stream_call_tx, stream_rx) = std::sync::mpsc::sync_channel(1); sources_tx .send(Ok(vec![ScapDefaultTargetCaptureSource { stream_call_tx, size, + target: display.clone(), }])) .ok(); let Ok((stream_tx, frame_callback)) = stream_rx.recv() else { return; }; - run_capture(capturer, frame_callback, stream_tx); + run_capture(capturer, display, frame_callback, stream_tx); + } + Err(e) => { + sources_tx.send(Err(e)).ok(); + } + _ => { + sources_tx + .send(Err(anyhow!("The screen capture source is not a display"))) + .ok(); } } }); } impl ScreenCaptureSource for ScapDefaultTargetCaptureSource { - fn resolution(&self) -> Result<Size<DevicePixels>> { - Ok(self.size) + fn metadata(&self) -> Result<SourceMetadata> { + Ok(SourceMetadata { + resolution: self.size, + label: None, + is_main: None, + id: self.target.id as u64, + }) } fn stream( @@ -189,12 +214,19 @@ fn new_scap_capturer(target: Option<scap::Target>) -> Result<scap::capturer::Cap fn run_capture( mut capturer: scap::capturer::Capturer, + display: scap::Display, frame_callback: Box<dyn Fn(ScreenCaptureFrame) + Send>, stream_tx: oneshot::Sender<Result<ScapStream>>, ) { let cancel_stream = Arc::new(AtomicBool::new(false)); + let size = Size { + width: DevicePixels(display.width as i32), + height: DevicePixels(display.height as i32), + }; let stream_send_result = stream_tx.send(Ok(ScapStream { cancel_stream: cancel_stream.clone(), + display, + size, })); if let Err(_) = stream_send_result { return; @@ -213,9 +245,20 @@ fn run_capture( struct ScapStream { cancel_stream: Arc<AtomicBool>, + display: scap::Display, + size: Size<DevicePixels>, } -impl ScreenCaptureStream for ScapStream {} +impl ScreenCaptureStream for ScapStream { + fn metadata(&self) -> Result<SourceMetadata> { + Ok(SourceMetadata { + resolution: self.size, + label: Some(self.display.title.clone().into()), + is_main: None, + id: self.display.id as u64, + }) + } +} impl Drop for ScapStream { fn drop(&mut self) { @@ -237,12 +280,12 @@ fn frame_size(frame: &scap::frame::Frame) -> Size<DevicePixels> { } /// This is used by `get_screen_targets` and `start_default_target_screen_capture` to turn their -/// results into `Box<dyn ScreenCaptureSource>`. They need to `Send` their capture source, and so -/// the capture source structs are used as `Box<dyn ScreenCaptureSource>` is not `Send`. +/// results into `Rc<dyn ScreenCaptureSource>`. They need to `Send` their capture source, and so +/// the capture source structs are used as `Rc<dyn ScreenCaptureSource>` is not `Send`. fn to_dyn_screen_capture_sources<T: ScreenCaptureSource + 'static>( sources_rx: oneshot::Receiver<Result<Vec<T>>>, foreground_executor: &ForegroundExecutor, -) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> { +) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> { let (dyn_sources_tx, dyn_sources_rx) = oneshot::channel(); foreground_executor .spawn(async move { @@ -250,7 +293,7 @@ fn to_dyn_screen_capture_sources<T: ScreenCaptureSource + 'static>( Ok(Ok(results)) => dyn_sources_tx .send(Ok(results .into_iter() - .map(|source| Box::new(source) as Box<dyn ScreenCaptureSource>) + .map(|source| Rc::new(source) as Rc<dyn ScreenCaptureSource>) .collect::<Vec<_>>())) .ok(), Ok(Err(err)) => dyn_sources_tx.send(Err(err)).ok(), diff --git a/crates/gpui/src/platform/test.rs b/crates/gpui/src/platform/test.rs index e4173b7c6ba2011bdea80514ac12fa862cade1e4..9227df5b63314b44a3c641835d00ba340aa909e8 100644 --- a/crates/gpui/src/platform/test.rs +++ b/crates/gpui/src/platform/test.rs @@ -8,4 +8,4 @@ pub(crate) use display::*; pub(crate) use platform::*; pub(crate) use window::*; -pub use platform::TestScreenCaptureSource; +pub use platform::{TestScreenCaptureSource, TestScreenCaptureStream}; diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index bef05399e52a6eb1a05552bb8693f5850274e98a..a26b65576cc49e290494762eed597d5bd8d0af26 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -2,7 +2,7 @@ use crate::{ AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, - Size, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size, + SourceMetadata, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size, }; use anyhow::Result; use collections::VecDeque; @@ -44,11 +44,17 @@ pub(crate) struct TestPlatform { /// A fake screen capture source, used for testing. pub struct TestScreenCaptureSource {} +/// A fake screen capture stream, used for testing. pub struct TestScreenCaptureStream {} impl ScreenCaptureSource for TestScreenCaptureSource { - fn resolution(&self) -> Result<Size<DevicePixels>> { - Ok(size(DevicePixels(1), DevicePixels(1))) + fn metadata(&self) -> Result<SourceMetadata> { + Ok(SourceMetadata { + id: 0, + is_main: None, + label: None, + resolution: size(DevicePixels(1), DevicePixels(1)), + }) } fn stream( @@ -64,7 +70,11 @@ impl ScreenCaptureSource for TestScreenCaptureSource { } } -impl ScreenCaptureStream for TestScreenCaptureStream {} +impl ScreenCaptureStream for TestScreenCaptureStream { + fn metadata(&self) -> Result<SourceMetadata> { + TestScreenCaptureSource {}.metadata() + } +} struct TestPrompt { msg: String, @@ -271,13 +281,13 @@ impl Platform for TestPlatform { #[cfg(feature = "screen-capture")] fn screen_capture_sources( &self, - ) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> { + ) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> { let (mut tx, rx) = oneshot::channel(); tx.send(Ok(self .screen_capture_sources .borrow() .iter() - .map(|source| Box::new(source.clone()) as Box<dyn ScreenCaptureSource>) + .map(|source| Rc::new(source.clone()) as Rc<dyn ScreenCaptureSource>) .collect())) .ok(); rx diff --git a/crates/gpui/src/platform/windows/platform.rs b/crates/gpui/src/platform/windows/platform.rs index f69a802da07fab1636404e3aae0dfd8487d69479..401ecdeffecc1aefdab85ec1728aa6918a0e0857 100644 --- a/crates/gpui/src/platform/windows/platform.rs +++ b/crates/gpui/src/platform/windows/platform.rs @@ -440,7 +440,7 @@ impl Platform for WindowsPlatform { #[cfg(feature = "screen-capture")] fn screen_capture_sources( &self, - ) -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptureSource>>>> { + ) -> oneshot::Receiver<Result<Vec<Rc<dyn ScreenCaptureSource>>>> { crate::platform::scap_screen_capture::scap_screen_sources(&self.foreground_executor) } diff --git a/crates/livekit_client/src/livekit_client/playback.rs b/crates/livekit_client/src/livekit_client/playback.rs index 7e36314c12f24fcc696cb5d66f57717ed052a81b..c62b8853b4782055b919f6f46e95cc0c82693b33 100644 --- a/crates/livekit_client/src/livekit_client/playback.rs +++ b/crates/livekit_client/src/livekit_client/playback.rs @@ -326,11 +326,11 @@ pub(crate) async fn capture_local_video_track( capture_source: &dyn ScreenCaptureSource, cx: &mut gpui::AsyncApp, ) -> Result<(crate::LocalVideoTrack, Box<dyn ScreenCaptureStream>)> { - let resolution = capture_source.resolution()?; + let metadata = capture_source.metadata()?; let track_source = gpui_tokio::Tokio::spawn(cx, async move { NativeVideoSource::new(VideoResolution { - width: resolution.width.0 as u32, - height: resolution.height.0 as u32, + width: metadata.resolution.width.0 as u32, + height: metadata.resolution.height.0 as u32, }) })? .await?; diff --git a/crates/livekit_client/src/mock_client/participant.rs b/crates/livekit_client/src/mock_client/participant.rs index 1f4168b8e04058f00af3b3117ba17dfa90947736..991d10bd5057014de3726ae4d1d0bc2c5b1b4661 100644 --- a/crates/livekit_client/src/mock_client/participant.rs +++ b/crates/livekit_client/src/mock_client/participant.rs @@ -5,7 +5,7 @@ use crate::{ }; use anyhow::Result; use collections::HashMap; -use gpui::{AsyncApp, ScreenCaptureSource, ScreenCaptureStream}; +use gpui::{AsyncApp, ScreenCaptureSource, ScreenCaptureStream, TestScreenCaptureStream}; #[derive(Clone, Debug)] pub struct LocalParticipant { @@ -119,7 +119,3 @@ impl RemoteParticipant { self.identity.clone() } } - -struct TestScreenCaptureStream; - -impl gpui::ScreenCaptureStream for TestScreenCaptureStream {} diff --git a/crates/title_bar/Cargo.toml b/crates/title_bar/Cargo.toml index 3c39e6b946a68824b8601a0978c702f51d06f5ec..8e95c6f79ff158a2f1dc4f03152a18d606b21727 100644 --- a/crates/title_bar/Cargo.toml +++ b/crates/title_bar/Cargo.toml @@ -27,6 +27,7 @@ test-support = [ ] [dependencies] +anyhow.workspace = true auto_update.workspace = true call.workspace = true chrono.workspace = true diff --git a/crates/title_bar/src/collab.rs b/crates/title_bar/src/collab.rs index b2a37a4f1c11c00139abe5c555f7ef254cc69f4c..1eebc0de0c7012231933619a432263ef911a8b7e 100644 --- a/crates/title_bar/src/collab.rs +++ b/crates/title_bar/src/collab.rs @@ -1,12 +1,20 @@ +use std::rc::Rc; use std::sync::Arc; use call::{ActiveCall, ParticipantLocation, Room}; use client::{User, proto::PeerId}; -use gpui::{AnyElement, Hsla, IntoElement, MouseButton, Path, Styled, canvas, point}; +use gpui::{ + AnyElement, Hsla, IntoElement, MouseButton, Path, ScreenCaptureSource, Styled, WeakEntity, + canvas, point, +}; use gpui::{App, Task, Window, actions}; use rpc::proto::{self}; use theme::ActiveTheme; -use ui::{Avatar, AvatarAudioStatusIndicator, Facepile, TintColor, Tooltip, prelude::*}; +use ui::{ + Avatar, AvatarAudioStatusIndicator, ContextMenu, ContextMenuItem, Facepile, PopoverMenu, + SplitButton, TintColor, Tooltip, prelude::*, +}; +use util::maybe; use workspace::notifications::DetachAndPromptErr; use crate::TitleBar; @@ -23,24 +31,49 @@ actions!( ] ); -fn toggle_screen_sharing(_: &ToggleScreenSharing, window: &mut Window, cx: &mut App) { +fn toggle_screen_sharing( + screen: Option<Rc<dyn ScreenCaptureSource>>, + 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| { - if room.is_screen_sharing() { + 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(), ); - Task::ready(room.unshare_screen(cx)) + 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(), + ); + } + 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 { - telemetry::event!( - "Screen Share Enabled", - room_id = room.id(), - channel_id = room.channel_id(), - ); - room.share_screen(cx) + 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))); @@ -303,7 +336,7 @@ impl TitleBar { let is_muted = room.is_muted(); let muted_by_user = room.muted_by_user(); let is_deafened = room.is_deafened().unwrap_or(false); - let is_screen_sharing = room.is_screen_sharing(); + let is_screen_sharing = room.is_sharing_screen(); let can_use_microphone = room.can_use_microphone(); let can_share_projects = room.can_share_projects(); let screen_sharing_supported = cx.is_screen_capture_supported(); @@ -428,21 +461,43 @@ impl TitleBar { ); if can_use_microphone && screen_sharing_supported { + let trigger = IconButton::new("screen-share", ui::IconName::Screen) + .style(ButtonStyle::Subtle) + .icon_size(IconSize::Small) + .toggle_state(is_screen_sharing) + .selected_style(ButtonStyle::Tinted(TintColor::Accent)) + .tooltip(Tooltip::text(if is_screen_sharing { + "Stop Sharing Screen" + } else { + "Share Screen" + })) + .on_click(move |_, window, cx| { + let should_share = ActiveCall::global(cx) + .read(cx) + .room() + .is_some_and(|room| !room.read(cx).is_sharing_screen()); + + window + .spawn(cx, async move |cx| { + let screen = if should_share { + cx.update(|_, cx| pick_default_screen(cx))?.await + } else { + None + }; + + cx.update(|window, cx| toggle_screen_sharing(screen, window, cx))?; + + Result::<_, anyhow::Error>::Ok(()) + }) + .detach(); + }); + children.push( - IconButton::new("screen-share", ui::IconName::Screen) - .style(ButtonStyle::Subtle) - .icon_size(IconSize::Small) - .toggle_state(is_screen_sharing) - .selected_style(ButtonStyle::Tinted(TintColor::Accent)) - .tooltip(Tooltip::text(if is_screen_sharing { - "Stop Sharing Screen" - } else { - "Share Screen" - })) - .on_click(move |_, window, cx| { - toggle_screen_sharing(&Default::default(), window, cx) - }) - .into_any_element(), + SplitButton::new( + trigger.render(window, cx), + self.render_screen_list().into_any_element(), + ) + .into_any_element(), ); } @@ -450,4 +505,89 @@ impl TitleBar { children } + + fn render_screen_list(&self) -> impl IntoElement { + PopoverMenu::new("screen-share-screen-list") + .with_handle(self.screen_share_popover_handle.clone()) + .trigger( + ui::ButtonLike::new_rounded_right("screen-share-screen-list-trigger") + .layer(ui::ElevationIndex::ModalSurface) + .size(ui::ButtonSize::None) + .child( + div() + .px_1() + .child(Icon::new(IconName::ChevronDownSmall).size(IconSize::XSmall)), + ) + .toggle_state(self.screen_share_popover_handle.is_deployed()), + ) + .menu(|window, cx| { + let screens = cx.screen_capture_sources(); + Some(ContextMenu::build(window, cx, |context_menu, _, cx| { + cx.spawn(async move |this: WeakEntity<ContextMenu>, cx| { + let screens = screens.await??; + this.update(cx, |this, cx| { + let active_screenshare_id = ActiveCall::global(cx) + .read(cx) + .room() + .and_then(|room| room.read(cx).shared_screen_id()); + for screen in screens { + let Ok(meta) = screen.metadata() else { + continue; + }; + + let label = meta + .label + .clone() + .unwrap_or_else(|| SharedString::from("Unknown screen")); + let resolution = SharedString::from(format!( + "{} × {}", + meta.resolution.width.0, meta.resolution.height.0 + )); + this.push_item(ContextMenuItem::CustomEntry { + entry_render: Box::new(move |_, _| { + h_flex() + .gap_2() + .child(Icon::new(IconName::Screen).when( + active_screenshare_id == Some(meta.id), + |this| this.color(Color::Accent), + )) + .child(Label::new(label.clone())) + .child( + Label::new(resolution.clone()) + .color(Color::Muted) + .size(LabelSize::Small), + ) + .into_any() + }), + selectable: true, + documentation_aside: None, + handler: Rc::new(move |_, window, cx| { + toggle_screen_sharing(Some(screen.clone()), window, cx); + }), + }); + } + }) + }) + .detach_and_log_err(cx); + context_menu + })) + }) + } +} + +/// Picks the screen to share when clicking on the main screen sharing button. +fn pick_default_screen(cx: &App) -> Task<Option<Rc<dyn ScreenCaptureSource>>> { + let source = cx.screen_capture_sources(); + cx.spawn(async move |_| { + let available_sources = maybe!(async move { source.await? }).await.ok()?; + available_sources + .iter() + .find(|it| { + it.as_ref() + .metadata() + .is_ok_and(|meta| meta.is_main.unwrap_or_default()) + }) + .or_else(|| available_sources.iter().next()) + .cloned() + }) } diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index c4fdb16f4f5d8b18b7e2b536198cc1ba61ec04d8..17c4c85b6d7ebfa4515748c89e7896a008d1c839 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -36,7 +36,7 @@ use theme::ActiveTheme; use title_bar_settings::TitleBarSettings; use ui::{ Avatar, Button, ButtonLike, ButtonStyle, Chip, ContextMenu, Icon, IconName, IconSize, - IconWithIndicator, Indicator, PopoverMenu, Tooltip, h_flex, prelude::*, + IconWithIndicator, Indicator, PopoverMenu, PopoverMenuHandle, Tooltip, h_flex, prelude::*, }; use util::ResultExt; use workspace::{Workspace, notifications::NotifyResultExt}; @@ -131,6 +131,7 @@ pub struct TitleBar { application_menu: Option<Entity<ApplicationMenu>>, _subscriptions: Vec<Subscription>, banner: Entity<OnboardingBanner>, + screen_share_popover_handle: PopoverMenuHandle<ContextMenu>, } impl Render for TitleBar { @@ -295,6 +296,7 @@ impl TitleBar { client, _subscriptions: subscriptions, banner, + screen_share_popover_handle: Default::default(), } } diff --git a/crates/ui/src/components/button/icon_button.rs b/crates/ui/src/components/button/icon_button.rs index 050db6addd2ba32535edffdd6fde066ac57ec644..e5d13e09cd804cdf50970c082d8b944e890f74db 100644 --- a/crates/ui/src/components/button/icon_button.rs +++ b/crates/ui/src/components/button/icon_button.rs @@ -178,7 +178,8 @@ impl VisibleOnHover for IconButton { } impl RenderOnce for IconButton { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + #[allow(refining_impl_trait)] + fn render(self, window: &mut Window, cx: &mut App) -> ButtonLike { let is_disabled = self.base.disabled; let is_selected = self.base.selected; let selected_style = self.base.selected_style; diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs index 467dd226fbffb4fe54d7e7564c736431b8be094b..77468fd29596a2aae015e73ad2d618c82031128c 100644 --- a/crates/ui/src/components/context_menu.rs +++ b/crates/ui/src/components/context_menu.rs @@ -139,6 +139,8 @@ impl ContextMenuEntry { } } +impl FluentBuilder for ContextMenuEntry {} + impl From<ContextMenuEntry> for ContextMenuItem { fn from(entry: ContextMenuEntry) -> Self { ContextMenuItem::Entry(entry) @@ -353,6 +355,10 @@ impl ContextMenu { self } + pub fn push_item(&mut self, item: impl Into<ContextMenuItem>) { + self.items.push(item.into()); + } + pub fn entry( mut self, label: impl Into<SharedString>, From 56fd950d940f13bcef6b93f9c4e2bb472acb707b Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 21 Jul 2025 09:26:31 -0300 Subject: [PATCH 217/658] thread view: Add ability to expand message editor and fix scroll (#34766) Release Notes: - N/A --- crates/agent_ui/src/acp/thread_view.rs | 173 ++++++++++++++++++------- crates/agent_ui/src/agent_panel.rs | 3 + crates/agent_ui/src/message_editor.rs | 10 +- 3 files changed, 135 insertions(+), 51 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 765f4fe6c0f270ee88b17d140169095a34dc3a3c..d2903ab6ebc88c0b31d582d277b3d4c03c3e55e0 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -45,7 +45,8 @@ use ::acp_thread::{ use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet}; use crate::acp::message_history::MessageHistory; use crate::agent_diff::AgentDiff; -use crate::{AgentDiffPane, Follow, KeepAll, OpenAgentDiff, RejectAll}; +use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES}; +use crate::{AgentDiffPane, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, RejectAll}; const RESPONSE_PADDING_X: Pixels = px(19.); @@ -65,6 +66,7 @@ pub struct AcpThreadView { expanded_tool_calls: HashSet<ToolCallId>, expanded_thinking_blocks: HashSet<(usize, usize)>, edits_expanded: bool, + editor_is_expanded: bool, message_history: Rc<RefCell<MessageHistory<acp::SendUserMessageParams>>>, } @@ -94,6 +96,8 @@ impl AcpThreadView { workspace: WeakEntity<Workspace>, project: Entity<Project>, message_history: Rc<RefCell<MessageHistory<acp::SendUserMessageParams>>>, + min_lines: usize, + max_lines: Option<usize>, window: &mut Window, cx: &mut Context<Self>, ) -> Self { @@ -113,8 +117,8 @@ impl AcpThreadView { let mut editor = Editor::new( editor::EditorMode::AutoHeight { - min_lines: 4, - max_lines: None, + min_lines, + max_lines: max_lines, }, buffer, None, @@ -182,6 +186,7 @@ impl AcpThreadView { expanded_tool_calls: HashSet::default(), expanded_thinking_blocks: HashSet::default(), edits_expanded: false, + editor_is_expanded: false, message_history, } } @@ -321,6 +326,35 @@ impl AcpThreadView { } } + pub fn expand_message_editor( + &mut self, + _: &ExpandMessageEditor, + _window: &mut Window, + cx: &mut Context<Self>, + ) { + self.set_editor_is_expanded(!self.editor_is_expanded, cx); + cx.notify(); + } + + fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context<Self>) { + self.editor_is_expanded = is_expanded; + self.message_editor.update(cx, |editor, _| { + if self.editor_is_expanded { + editor.set_mode(EditorMode::Full { + scale_ui_elements_with_buffer_font_size: false, + show_active_line_background: false, + sized_by_content: false, + }) + } else { + editor.set_mode(EditorMode::AutoHeight { + min_lines: MIN_EDITOR_LINES, + max_lines: Some(MAX_EDITOR_LINES), + }) + } + }); + cx.notify(); + } + fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context<Self>) { self.last_error.take(); @@ -381,6 +415,7 @@ 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) @@ -1793,34 +1828,96 @@ impl AcpThreadView { )) } - fn render_message_editor(&mut self, cx: &mut Context<Self>) -> AnyElement { - 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() + fn render_message_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement { + let focus_handle = self.message_editor.focus_handle(cx); + let editor_bg_color = cx.theme().colors().editor_background; + let (expand_icon, expand_tooltip) = if self.editor_is_expanded { + (IconName::Minimize, "Minimize Message Editor") + } else { + (IconName::Maximize, "Expand Message Editor") }; - EditorElement::new( - &self.message_editor, - EditorStyle { - background: cx.theme().colors().editor_background, - local_player: cx.theme().players().local(), - text: text_style, - syntax: cx.theme().syntax().clone(), - ..Default::default() - }, - ) - .into_any() + v_flex() + .on_action(cx.listener(Self::expand_message_editor)) + .p_2() + .gap_2() + .border_t_1() + .border_color(cx.theme().colors().border) + .bg(editor_bg_color) + .when(self.editor_is_expanded, |this| { + this.h(vh(0.8, window)).size_full().justify_between() + }) + .child( + v_flex() + .relative() + .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( + h_flex() + .absolute() + .top_0() + .right_0() + .opacity(0.5) + .hover(|this| this.opacity(1.0)) + .child( + IconButton::new("toggle-height", expand_icon) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .tooltip({ + let focus_handle = focus_handle.clone(); + move |window, cx| { + Tooltip::for_action_in( + expand_tooltip, + &ExpandMessageEditor, + &focus_handle, + window, + cx, + ) + } + }) + .on_click(cx.listener(|_, _, window, cx| { + window.dispatch_action(Box::new(ExpandMessageEditor), cx); + })), + ), + ), + ) + .child( + h_flex() + .flex_none() + .justify_between() + .child(self.render_follow_toggle(cx)) + .child(self.render_send_button(cx)), + ) + .into_any() } fn render_send_button(&self, cx: &mut Context<Self>) -> AnyElement { @@ -2132,7 +2229,6 @@ impl Render for AcpThreadView { .px(RESPONSE_PADDING_X) .opacity(0.4) .hover(|style| style.opacity(1.)) - .gap_1() .flex_wrap() .justify_end() .child(open_as_markdown) @@ -2166,22 +2262,7 @@ impl Render for AcpThreadView { ), ) }) - .child( - v_flex() - .p_2() - .pt_3() - .gap_1() - .bg(cx.theme().colors().editor_background) - .border_t_1() - .border_color(cx.theme().colors().border) - .child(self.render_message_editor(cx)) - .child( - h_flex() - .justify_between() - .child(self.render_follow_toggle(cx)) - .child(self.render_send_button(cx)), - ), - ) + .child(self.render_message_editor(window, cx)) } } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 7f2fbce189280887ff8a7cbbcfa719542a0598d6..36851e44bac6e2455c19a403e417f9722db6b7c6 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -11,6 +11,7 @@ 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, @@ -960,6 +961,8 @@ impl AgentPanel { workspace.clone(), project, message_history, + MIN_EDITOR_LINES, + Some(MAX_EDITOR_LINES), window, cx, ) diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index ce9cc87fe3f0f3434fac744ec4b9caca8fef11b7..a2cf4aac48a0eb6e368596bc7458615ea1f008a1 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -65,6 +65,9 @@ use agent::{ thread_store::{TextThreadStore, ThreadStore}, }; +pub const MIN_EDITOR_LINES: usize = 4; +pub const MAX_EDITOR_LINES: usize = 8; + #[derive(RegisterComponent)] pub struct MessageEditor { thread: Entity<Thread>, @@ -88,9 +91,6 @@ pub struct MessageEditor { _subscriptions: Vec<Subscription>, } -const MIN_EDITOR_LINES: usize = 4; -const MAX_EDITOR_LINES: usize = 8; - pub(crate) fn create_editor( workspace: WeakEntity<Workspace>, context_store: WeakEntity<ContextStore>, @@ -711,11 +711,11 @@ impl MessageEditor { cx.listener(|this, _: &RejectAll, window, cx| this.handle_reject_all(window, cx)), ) .capture_action(cx.listener(Self::paste)) - .gap_2() .p_2() - .bg(editor_bg_color) + .gap_2() .border_t_1() .border_color(cx.theme().colors().border) + .bg(editor_bg_color) .child( h_flex() .justify_between() From 35b4a918c91bedac4968dde188929bb59523e769 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:43:51 +0200 Subject: [PATCH 218/658] chore: Bump taffy to 0.5.1 (#34817) We've had problems in the past with bumping past 0.5.2 due to perf regressions reported by @huacnlee; 0.5.1 was fine though. Hence, let's bump taffy to 0.5.1 as a safe bet and then try to push past 0.5.2 (after we identify the root cause of regression Related to #19189 Release Notes: - N/A --- Cargo.lock | 8 ++++---- crates/gpui/Cargo.toml | 2 +- crates/gpui/src/taffy.rs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bc69de7a7cd551590252a0a06f3dca927197e8d0..5ceed10b199ab68a671a7f2a76e86874bccabf5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7396,9 +7396,9 @@ dependencies = [ [[package]] name = "grid" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d196ffc1627db18a531359249b2bf8416178d84b729f3cebeb278f285fb9b58c" +checksum = "be136d9dacc2a13cc70bb6c8f902b414fb2641f8db1314637c6b7933411a8f82" [[package]] name = "group" @@ -15936,9 +15936,9 @@ dependencies = [ [[package]] name = "taffy" -version = "0.4.4" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ec17858c2d465b2f734b798b920818a974faf0babb15d7fef81818a4b2d16f1" +checksum = "9cb893bff0f80ae17d3a57e030622a967b8dbc90e38284d9b4b1442e23873c94" dependencies = [ "arrayvec", "grid", diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index d3462e9e9c1a0ad98a9e16787ee2281d5ff6f028..9da01c5ff3ac44214ff56327bbc81f89da647397 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.4.3" +taffy = "0.5.1" thiserror.workspace = true util.workspace = true uuid.workspace = true diff --git a/crates/gpui/src/taffy.rs b/crates/gpui/src/taffy.rs index f12c62d504395a2afbf698685a4eb3cc5f0e4e1f..6228a604904f6aa40d6d15fb7f9c5ff19b29f6a1 100644 --- a/crates/gpui/src/taffy.rs +++ b/crates/gpui/src/taffy.rs @@ -182,7 +182,7 @@ impl TaffyLayoutEngine { .compute_layout_with_measure( id.into(), available_space.into(), - |known_dimensions, available_space, _id, node_context| { + |known_dimensions, available_space, _id, node_context, _style| { let Some(node_context) = node_context else { return taffy::geometry::Size::default(); }; From 405244d422e3e2baf3660749e21e4cb5fcc5e82d Mon Sep 17 00:00:00 2001 From: Agus Zubiaga <agus@zed.dev> Date: Mon, 21 Jul 2025 11:11:37 -0300 Subject: [PATCH 219/658] Display ACP plans (#34816) Release Notes: - N/A --------- Co-authored-by: Danilo Leal <daniloleal09@gmail.com> --- Cargo.lock | 4 +- Cargo.toml | 2 +- assets/icons/todo_complete.svg | 4 + assets/icons/todo_pending.svg | 10 + assets/icons/todo_progress.svg | 11 ++ crates/acp_thread/src/acp_thread.rs | 109 ++++++++++- crates/agent_servers/src/claude.rs | 19 +- crates/agent_servers/src/claude/tools.rs | 30 +++ crates/agent_ui/src/acp/thread_view.rs | 221 ++++++++++++++++++++--- crates/icons/src/icons.rs | 3 + 10 files changed, 379 insertions(+), 34 deletions(-) create mode 100644 assets/icons/todo_complete.svg create mode 100644 assets/icons/todo_pending.svg create mode 100644 assets/icons/todo_progress.svg diff --git a/Cargo.lock b/Cargo.lock index 5ceed10b199ab68a671a7f2a76e86874bccabf5a..a323fb70afaa85656aae2dbce2d672fcb1a21808 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -279,9 +279,9 @@ dependencies = [ [[package]] name = "agentic-coding-protocol" -version = "0.0.9" +version = "0.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e276b798eddd02562a339340a96919d90bbfcf78de118fdddc932524646fac7" +checksum = "a3e6ae951b36fa2f8d9dd6e1af6da2fcaba13d7c866cf6a9e65deda9dc6c5fe4" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 0169d32eb6a671dad002e8d520e54a38a5901f8d..c99ba3953dbdd909cda61b4e118a4ef9f38d1f59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -410,7 +410,7 @@ zlog_settings = { path = "crates/zlog_settings" } # External crates # -agentic-coding-protocol = "0.0.9" +agentic-coding-protocol = "0.0.10" 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/assets/icons/todo_complete.svg b/assets/icons/todo_complete.svg new file mode 100644 index 0000000000000000000000000000000000000000..9fa2e818bb61137de35d260f4384a0db545d4125 --- /dev/null +++ b/assets/icons/todo_complete.svg @@ -0,0 +1,4 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M8 13C10.7614 13 13 10.7614 13 8C13 5.23858 10.7614 3 8 3C5.23858 3 3 5.23858 3 8C3 10.7614 5.23858 13 8 13Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M6 8L7.33333 9L10 7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/assets/icons/todo_pending.svg b/assets/icons/todo_pending.svg new file mode 100644 index 0000000000000000000000000000000000000000..dfb013b52b987a3f99e1b8304418b847ff1ccf2b --- /dev/null +++ b/assets/icons/todo_pending.svg @@ -0,0 +1,10 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M7 3C7.66045 3 8.33955 3 9 3" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M11 4C11.3949 4.26602 11.7345 4.60558 12 5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M13 7C13 7.66045 13 8.33955 13 9" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M12 11C11.734 11.3949 11.3944 11.7345 11 12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M9 13C8.33954 13 7.66046 13 7 13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M5 12C4.6051 11.734 4.26554 11.3944 4 11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M3 9C3 8.33955 3 7.66045 3 7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M4 5C4.26602 4.6051 4.60558 4.26554 5 4" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/assets/icons/todo_progress.svg b/assets/icons/todo_progress.svg new file mode 100644 index 0000000000000000000000000000000000000000..9b2ed7375d9807139261a2d81f7f1f168470d0f4 --- /dev/null +++ b/assets/icons/todo_progress.svg @@ -0,0 +1,11 @@ +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M7 3C7.66045 3 8.33955 3 9 3" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M11 4C11.3949 4.26602 11.7345 4.60558 12 5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M13 7C13 7.66045 13 8.33955 13 9" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M12 11C11.734 11.3949 11.3944 11.7345 11 12" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M9 13C8.33954 13 7.66046 13 7 13" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M5 12C4.6051 11.734 4.26554 11.3944 4 11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M3 9C3 8.33955 3 7.66045 3 7" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M4 5C4.26602 4.6051 4.60558 4.26554 5 4" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +<path d="M8.00016 8.66665C8.36835 8.66665 8.66683 8.36817 8.66683 7.99998C8.66683 7.63179 8.36835 7.33331 8.00016 7.33331C7.63197 7.33331 7.3335 7.63179 7.3335 7.99998C7.3335 8.36817 7.63197 8.66665 8.00016 8.66665Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index ae22725d5eac36a326718a66d5944c9fcfb44a4b..9af1eeb1872fb9c44e3159a3d1772b68c98e67d7 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -453,9 +453,69 @@ impl Diff { } } +#[derive(Debug, Default)] +pub struct Plan { + pub entries: Vec<PlanEntry>, +} + +#[derive(Debug)] +pub struct PlanStats<'a> { + pub in_progress_entry: Option<&'a PlanEntry>, + pub pending: u32, + pub completed: u32, +} + +impl Plan { + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } + + pub fn stats(&self) -> PlanStats<'_> { + let mut stats = PlanStats { + in_progress_entry: None, + pending: 0, + completed: 0, + }; + + for entry in &self.entries { + match &entry.status { + acp::PlanEntryStatus::Pending => { + stats.pending += 1; + } + acp::PlanEntryStatus::InProgress => { + stats.in_progress_entry = stats.in_progress_entry.or(Some(entry)); + } + acp::PlanEntryStatus::Completed => { + stats.completed += 1; + } + } + } + + stats + } +} + +#[derive(Debug)] +pub struct PlanEntry { + pub content: Entity<Markdown>, + pub priority: acp::PlanEntryPriority, + pub status: acp::PlanEntryStatus, +} + +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)), + priority: entry.priority, + status: entry.status, + } + } +} + pub struct AcpThread { - entries: Vec<AgentThreadEntry>, title: SharedString, + entries: Vec<AgentThreadEntry>, + plan: Plan, project: Entity<Project>, action_log: Entity<ActionLog>, shared_buffers: HashMap<Entity<Buffer>, BufferSnapshot>, @@ -515,6 +575,7 @@ impl AcpThread { action_log, shared_buffers: Default::default(), entries: Default::default(), + plan: Default::default(), title, project, send_task: None, @@ -819,6 +880,29 @@ impl AcpThread { } } + pub fn plan(&self) -> &Plan { + &self.plan + } + + pub fn update_plan(&mut self, request: acp::UpdatePlanParams, cx: &mut Context<Self>) { + self.plan = Plan { + entries: request + .entries + .into_iter() + .map(|entry| PlanEntry::from_acp(entry, cx)) + .collect(), + }; + + cx.notify(); + } + + pub fn clear_completed_plan_entries(&mut self, cx: &mut Context<Self>) { + self.plan + .entries + .retain(|entry| !matches!(entry.status, acp::PlanEntryStatus::Completed)); + cx.notify(); + } + pub fn set_project_location(&self, location: ToolCallLocation, cx: &mut Context<Self>) { self.project.update(cx, |project, cx| { let Some(path) = project.project_path_for_absolute_path(&location.path, cx) else { @@ -1136,6 +1220,17 @@ impl AcpClientDelegate { Self { thread, cx } } + pub async fn clear_completed_plan_entries(&self) -> Result<()> { + let cx = &mut self.cx.clone(); + cx.update(|cx| { + self.thread + .update(cx, |thread, cx| thread.clear_completed_plan_entries(cx)) + })? + .context("Failed to update thread")?; + + Ok(()) + } + pub async fn request_existing_tool_call_confirmation( &self, tool_call_id: ToolCallId, @@ -1233,6 +1328,18 @@ impl acp::Client for AcpClientDelegate { Ok(()) } + async fn update_plan(&self, request: acp::UpdatePlanParams) -> Result<(), acp::Error> { + let cx = &mut self.cx.clone(); + + cx.update(|cx| { + self.thread + .update(cx, |thread, cx| thread.update_plan(request, cx)) + })? + .context("Failed to update thread")?; + + Ok(()) + } + async fn read_text_file( &self, request: acp::ReadTextFileParams, diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 52c601226705e8253147feb4c45c62f86265058a..8b3d93a122d07448ddbfb9daf2dfc9226fb11545 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -153,6 +153,7 @@ impl AgentServer for ClaudeCode { let handler_task = cx.foreground_executor().spawn({ let end_turn_tx = end_turn_tx.clone(); let tool_id_map = tool_id_map.clone(); + let delegate = delegate.clone(); async move { while let Some(message) = incoming_message_rx.next().await { ClaudeAgentConnection::handle_message( @@ -167,6 +168,7 @@ impl AgentServer for ClaudeCode { }); let mut connection = ClaudeAgentConnection { + delegate, outgoing_tx, end_turn_tx, _handler_task: handler_task, @@ -186,6 +188,7 @@ impl AgentConnection for ClaudeAgentConnection { &self, params: AnyAgentRequest, ) -> LocalBoxFuture<'static, Result<acp::AnyAgentResult>> { + let delegate = self.delegate.clone(); let end_turn_tx = self.end_turn_tx.clone(); let outgoing_tx = self.outgoing_tx.clone(); async move { @@ -201,6 +204,8 @@ impl AgentConnection for ClaudeAgentConnection { Err(anyhow!("Authentication not supported")) } AnyAgentRequest::SendUserMessageParams(message) => { + delegate.clear_completed_plan_entries().await?; + let (tx, rx) = oneshot::channel(); end_turn_tx.borrow_mut().replace(tx); let mut content = String::new(); @@ -241,6 +246,7 @@ impl AgentConnection for ClaudeAgentConnection { } struct ClaudeAgentConnection { + delegate: AcpClientDelegate, outgoing_tx: UnboundedSender<SdkMessage>, end_turn_tx: Rc<RefCell<Option<oneshot::Sender<Result<()>>>>>, _mcp_server: Option<ClaudeMcpServer>, @@ -267,8 +273,17 @@ impl ClaudeAgentConnection { .log_err(); } ContentChunk::ToolUse { id, name, input } => { - if let Some(resp) = delegate - .push_tool_call(ClaudeTool::infer(&name, input).as_acp()) + let claude_tool = ClaudeTool::infer(&name, input); + + if let ClaudeTool::TodoWrite(Some(params)) = claude_tool { + delegate + .update_plan(acp::UpdatePlanParams { + entries: params.todos.into_iter().map(Into::into).collect(), + }) + .await + .log_err(); + } else if let Some(resp) = delegate + .push_tool_call(claude_tool.as_acp()) .await .log_err() { diff --git a/crates/agent_servers/src/claude/tools.rs b/crates/agent_servers/src/claude/tools.rs index a2d6b487b2fb7aa4baceadf02f1b8f81b0e7b29f..9c82139a07f38d9f78df8fa4719ecba420cd8838 100644 --- a/crates/agent_servers/src/claude/tools.rs +++ b/crates/agent_servers/src/claude/tools.rs @@ -614,6 +614,16 @@ pub enum TodoPriority { Low, } +impl Into<acp::PlanEntryPriority> for TodoPriority { + fn into(self) -> acp::PlanEntryPriority { + match self { + TodoPriority::High => acp::PlanEntryPriority::High, + TodoPriority::Medium => acp::PlanEntryPriority::Medium, + TodoPriority::Low => acp::PlanEntryPriority::Low, + } + } +} + #[derive(Deserialize, Serialize, JsonSchema, Debug)] #[serde(rename_all = "snake_case")] pub enum TodoStatus { @@ -622,6 +632,16 @@ pub enum TodoStatus { Completed, } +impl Into<acp::PlanEntryStatus> for TodoStatus { + fn into(self) -> acp::PlanEntryStatus { + match self { + TodoStatus::Pending => acp::PlanEntryStatus::Pending, + TodoStatus::InProgress => acp::PlanEntryStatus::InProgress, + TodoStatus::Completed => acp::PlanEntryStatus::Completed, + } + } +} + #[derive(Deserialize, Serialize, JsonSchema, Debug)] pub struct Todo { /// Unique identifier @@ -634,6 +654,16 @@ pub struct Todo { pub status: TodoStatus, } +impl Into<acp::PlanEntry> for Todo { + fn into(self) -> acp::PlanEntry { + acp::PlanEntry { + content: self.content, + priority: self.priority.into(), + status: self.status.into(), + } + } +} + #[derive(Deserialize, JsonSchema, Debug)] pub struct TodoWriteToolParams { pub todos: Vec<Todo>, diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index d2903ab6ebc88c0b31d582d277b3d4c03c3e55e0..95f4f81205f198f270eb5d4dec6597f9b04efd2f 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1,3 +1,4 @@ +use acp_thread::Plan; use agent_servers::AgentServer; use std::cell::RefCell; use std::collections::BTreeMap; @@ -66,7 +67,8 @@ pub struct AcpThreadView { expanded_tool_calls: HashSet<ToolCallId>, expanded_thinking_blocks: HashSet<(usize, usize)>, edits_expanded: bool, - editor_is_expanded: bool, + plan_expanded: bool, + editor_expanded: bool, message_history: Rc<RefCell<MessageHistory<acp::SendUserMessageParams>>>, } @@ -186,7 +188,8 @@ impl AcpThreadView { expanded_tool_calls: HashSet::default(), expanded_thinking_blocks: HashSet::default(), edits_expanded: false, - editor_is_expanded: false, + plan_expanded: false, + editor_expanded: false, message_history, } } @@ -332,14 +335,14 @@ impl AcpThreadView { _window: &mut Window, cx: &mut Context<Self>, ) { - self.set_editor_is_expanded(!self.editor_is_expanded, cx); + self.set_editor_is_expanded(!self.editor_expanded, cx); cx.notify(); } fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context<Self>) { - self.editor_is_expanded = is_expanded; + self.editor_expanded = is_expanded; self.message_editor.update(cx, |editor, _| { - if self.editor_is_expanded { + if self.editor_expanded { editor.set_mode(EditorMode::Full { scale_ui_elements_with_buffer_font_size: false, show_active_line_background: false, @@ -1477,7 +1480,7 @@ impl AcpThreadView { container.into_any() } - fn render_edits_bar( + fn render_activity_bar( &self, thread_entity: &Entity<AcpThread>, window: &mut Window, @@ -1486,8 +1489,9 @@ impl AcpThreadView { let thread = thread_entity.read(cx); let action_log = thread.action_log(); let changed_buffers = action_log.read(cx).changed_buffers(cx); + let plan = thread.plan(); - if changed_buffers.is_empty() { + if changed_buffers.is_empty() && plan.is_empty() { return None; } @@ -1496,7 +1500,6 @@ impl AcpThreadView { let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3)); let pending_edits = thread.has_pending_edit_tool_calls(); - let expanded = self.edits_expanded; v_flex() .mt_1() @@ -1512,27 +1515,165 @@ impl AcpThreadView { blur_radius: px(3.), spread_radius: px(0.), }]) - .child(self.render_edits_bar_summary( - action_log, - &changed_buffers, - expanded, - pending_edits, - window, - cx, - )) - .when(expanded, |parent| { - parent.child(self.render_edits_bar_files( - action_log, - &changed_buffers, - pending_edits, - cx, - )) + .when(!plan.is_empty(), |this| { + this.child(self.render_plan_summary(plan, window, cx)) + .when(self.plan_expanded, |parent| { + parent.child(self.render_plan_entries(plan, window, cx)) + }) + }) + .when(!changed_buffers.is_empty(), |this| { + this.child(Divider::horizontal()) + .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, + pending_edits, + cx, + )) + }) }) .into_any() .into() } - fn render_edits_bar_summary( + fn render_plan_summary(&self, plan: &Plan, window: &mut Window, cx: &Context<Self>) -> Div { + let stats = plan.stats(); + + let title = if let Some(entry) = stats.in_progress_entry + && !self.plan_expanded + { + h_flex() + .w_full() + .gap_1() + .text_xs() + .text_color(cx.theme().colors().text_muted) + .justify_between() + .child( + h_flex() + .gap_1() + .child( + Label::new("Current:") + .size(LabelSize::Small) + .color(Color::Muted), + ) + .child(MarkdownElement::new( + entry.content.clone(), + plan_label_markdown_style(&entry.status, window, cx), + )), + ) + .when(stats.pending > 0, |this| { + this.child( + Label::new(format!("{} left", stats.pending)) + .size(LabelSize::Small) + .color(Color::Muted) + .mr_1(), + ) + }) + } else { + let status_label = if stats.pending == 0 { + "All Done".to_string() + } else if stats.completed == 0 { + format!("{}", plan.entries.len()) + } else { + format!("{}/{}", stats.completed, plan.entries.len()) + }; + + h_flex() + .w_full() + .gap_1() + .justify_between() + .child( + Label::new("Plan") + .size(LabelSize::Small) + .color(Color::Muted), + ) + .child( + Label::new(status_label) + .size(LabelSize::Small) + .color(Color::Muted) + .mr_1(), + ) + }; + + h_flex() + .p_1() + .justify_between() + .when(self.plan_expanded, |this| { + this.border_b_1().border_color(cx.theme().colors().border) + }) + .child( + h_flex() + .id("plan_summary") + .w_full() + .gap_1() + .child(Disclosure::new("plan_disclosure", self.plan_expanded)) + .child(title) + .on_click(cx.listener(|this, _, _, cx| { + this.plan_expanded = !this.plan_expanded; + cx.notify(); + })), + ) + } + + fn render_plan_entries(&self, plan: &Plan, window: &mut Window, cx: &Context<Self>) -> Div { + v_flex().children(plan.entries.iter().enumerate().flat_map(|(index, entry)| { + let element = h_flex() + .py_1() + .px_2() + .gap_2() + .justify_between() + .bg(cx.theme().colors().editor_background) + .when(index < plan.entries.len() - 1, |parent| { + parent.border_color(cx.theme().colors().border).border_b_1() + }) + .child( + h_flex() + .id(("plan_entry", index)) + .gap_1p5() + .max_w_full() + .overflow_x_scroll() + .text_xs() + .text_color(cx.theme().colors().text_muted) + .child(match entry.status { + acp::PlanEntryStatus::Pending => Icon::new(IconName::TodoPending) + .size(IconSize::Small) + .color(Color::Muted) + .into_any_element(), + acp::PlanEntryStatus::InProgress => Icon::new(IconName::TodoProgress) + .size(IconSize::Small) + .color(Color::Accent) + .with_animation( + "running", + Animation::new(Duration::from_secs(2)).repeat(), + |icon, delta| { + icon.transform(Transformation::rotate(percentage(delta))) + }, + ) + .into_any_element(), + acp::PlanEntryStatus::Completed => Icon::new(IconName::TodoComplete) + .size(IconSize::Small) + .color(Color::Success) + .into_any_element(), + }) + .child(MarkdownElement::new( + entry.content.clone(), + plan_label_markdown_style(&entry.status, window, cx), + )), + ); + + Some(element) + })) + } + + fn render_edits_summary( &self, action_log: &Entity<ActionLog>, changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>, @@ -1678,7 +1819,7 @@ impl AcpThreadView { ) } - fn render_edits_bar_files( + fn render_edited_files( &self, action_log: &Entity<ActionLog>, changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>, @@ -1831,7 +1972,7 @@ impl AcpThreadView { fn render_message_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement { let focus_handle = self.message_editor.focus_handle(cx); let editor_bg_color = cx.theme().colors().editor_background; - let (expand_icon, expand_tooltip) = if self.editor_is_expanded { + let (expand_icon, expand_tooltip) = if self.editor_expanded { (IconName::Minimize, "Minimize Message Editor") } else { (IconName::Maximize, "Expand Message Editor") @@ -1844,7 +1985,7 @@ impl AcpThreadView { .border_t_1() .border_color(cx.theme().colors().border) .bg(editor_bg_color) - .when(self.editor_is_expanded, |this| { + .when(self.editor_expanded, |this| { this.h(vh(0.8, window)).size_full().justify_between() }) .child( @@ -2243,7 +2384,7 @@ impl Render for AcpThreadView { .child(LoadingLabel::new("").size(LabelSize::Small)) .into(), }) - .children(self.render_edits_bar(&thread, window, cx)) + .children(self.render_activity_bar(&thread, window, cx)) } else { this.child(self.render_empty_state(cx)) } @@ -2409,3 +2550,27 @@ fn default_markdown_style(buffer_font: bool, window: &Window, cx: &App) -> Markd ..Default::default() } } + +fn plan_label_markdown_style( + status: &acp::PlanEntryStatus, + window: &Window, + cx: &App, +) -> MarkdownStyle { + let default_md_style = default_markdown_style(false, window, cx); + + MarkdownStyle { + base_text_style: TextStyle { + color: cx.theme().colors().text_muted, + strikethrough: if matches!(status, acp::PlanEntryStatus::Completed) { + Some(gpui::StrikethroughStyle { + thickness: px(1.), + color: Some(cx.theme().colors().text_muted.opacity(0.8)), + }) + } else { + None + }, + ..default_md_style.base_text_style + }, + ..default_md_style + } +} diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index 6834d56215a86e3373f11d1b21736c8350e79666..631ccc1af3123defdc07c3e5dfb9756c0f235ec1 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -256,6 +256,9 @@ pub enum IconName { TextSnippet, ThumbsDown, ThumbsUp, + TodoComplete, + TodoPending, + TodoProgress, ToolBulb, ToolCopy, ToolDeleteFile, From cc561961525b5e47e2a6eba52b3382233b6d49a6 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga <agus@zed.dev> Date: Mon, 21 Jul 2025 11:26:00 -0300 Subject: [PATCH 220/658] Fix loading agent server settings (#34662) Release Notes: - N/A --- crates/agent_servers/src/settings.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/agent_servers/src/settings.rs b/crates/agent_servers/src/settings.rs index 29dcf5eb8c9b3e1738fd5c24b18e309d897b0c6f..645674b5f15087250c2364fb9a8a846e163ad54c 100644 --- a/crates/agent_servers/src/settings.rs +++ b/crates/agent_servers/src/settings.rs @@ -29,9 +29,12 @@ impl settings::Settings for AllAgentServersSettings { fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> { let mut settings = AllAgentServersSettings::default(); - for value in sources.defaults_and_customizations() { - if value.gemini.is_some() { - settings.gemini = value.gemini.clone(); + 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(); } } From c251f2a2d4f655b3380b532b74231e214398230f Mon Sep 17 00:00:00 2001 From: Kamal Ahmad <kamalahmad22@pm.me> Date: Mon, 21 Jul 2025 19:48:01 +0500 Subject: [PATCH 221/658] gpui: Throttle interactive resize events on Wayland (#34760) Wayland compositors can potentially generate thousands of resize requests when drag-resizing a window, which Zed is unable to keep up with. This commit changes the behavior to only resize once per vblank cycle, which helps significantly when resizing the window with a high poll-rate mouse (1000Hz is common these days) Here is an example of the behavior pre and post this commit, with some print-debugging added for tracking resize calls. I have a wireless mouse with a 2000Hz polling rate and a 165Hz display: Before: https://github.com/user-attachments/assets/4c657f90-5fd2-4809-97ef-363fd48e81b8 After: https://github.com/user-attachments/assets/4a0f5fbd-c3c4-40a1-9f71-3b4358c827cf Closes #20660 Release Notes: - Improved: Make resizing smoother on Wayland/Linux --- crates/gpui/src/platform/linux/wayland/window.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index 36e070b0b0fc03d1dd6cd3402eedd228dbc909e3..255ae9c3721ec43d47f6a603a7de75a6d13ef5d4 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/crates/gpui/src/platform/linux/wayland/window.rs @@ -76,6 +76,7 @@ struct InProgressConfigure { size: Option<Size<Pixels>>, fullscreen: bool, maximized: bool, + resizing: bool, tiling: Tiling, } @@ -107,6 +108,7 @@ pub struct WaylandWindowState { active: bool, hovered: bool, in_progress_configure: Option<InProgressConfigure>, + resize_throttle: bool, in_progress_window_controls: Option<WindowControls>, window_controls: WindowControls, inset: Option<Pixels>, @@ -176,6 +178,7 @@ impl WaylandWindowState { tiling: Tiling::default(), window_bounds: options.bounds, in_progress_configure: None, + resize_throttle: false, client, appearance, handle, @@ -335,6 +338,7 @@ impl WaylandWindowStatePtr { pub fn frame(&self) { let mut state = self.state.borrow_mut(); state.surface.frame(&state.globals.qh, state.surface.id()); + state.resize_throttle = false; drop(state); let mut cb = self.callbacks.borrow_mut(); @@ -366,6 +370,12 @@ impl WaylandWindowStatePtr { 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) @@ -472,6 +482,7 @@ impl WaylandWindowStatePtr { let mut tiling = Tiling::default(); let mut fullscreen = false; let mut maximized = false; + let mut resizing = false; for state in states { match state { @@ -481,6 +492,7 @@ impl WaylandWindowStatePtr { xdg_toplevel::State::Fullscreen => { fullscreen = true; } + xdg_toplevel::State::Resizing => resizing = true, xdg_toplevel::State::TiledTop => { tiling.top = true; } @@ -508,6 +520,7 @@ impl WaylandWindowStatePtr { size, fullscreen, maximized, + resizing, tiling, }); From 97af7e1bd92eb5c3bdd038e596378dc4054902f6 Mon Sep 17 00:00:00 2001 From: Marshall Bowers <git@maxdeviant.com> Date: Mon, 21 Jul 2025 11:29:16 -0400 Subject: [PATCH 222/658] collab: Remove `PUT /billing/preferences` endpoint (#34825) This PR removes the `PUT /billing/preferences` endpoint, as it has been moved to `cloud.zed.dev`. Release Notes: - N/A --- crates/collab/src/api/billing.rs | 110 +------------------------------ 1 file changed, 2 insertions(+), 108 deletions(-) diff --git a/crates/collab/src/api/billing.rs b/crates/collab/src/api/billing.rs index 09f307c6727b9b23c800aae684e3f813d92375ad..9aa6578b2a54cb8806d819c32e0112dd397bdd23 100644 --- a/crates/collab/src/api/billing.rs +++ b/crates/collab/src/api/billing.rs @@ -1,12 +1,10 @@ use anyhow::{Context as _, bail}; -use axum::routing::put; use axum::{Extension, Json, Router, extract, routing::post}; -use chrono::{DateTime, SecondsFormat, Utc}; +use chrono::{DateTime, Utc}; use collections::{HashMap, HashSet}; use reqwest::StatusCode; use sea_orm::ActiveValue; use serde::{Deserialize, Serialize}; -use serde_json::json; use std::{str::FromStr, sync::Arc, time::Duration}; use stripe::{ BillingPortalSession, CancellationDetailsReason, CreateBillingPortalSession, @@ -20,7 +18,6 @@ use stripe::{ use util::{ResultExt, maybe}; use zed_llm_client::LanguageModelProvider; -use crate::api::events::SnowflakeRow; use crate::db::billing_subscription::{ StripeCancellationReason, StripeSubscriptionStatus, SubscriptionKind, }; @@ -36,14 +33,13 @@ use crate::{ db::{ BillingSubscriptionId, CreateBillingCustomerParams, CreateBillingSubscriptionParams, CreateProcessedStripeEventParams, UpdateBillingCustomerParams, - UpdateBillingPreferencesParams, UpdateBillingSubscriptionParams, billing_customer, + UpdateBillingSubscriptionParams, billing_customer, }, stripe_billing::StripeBilling, }; pub fn router() -> Router { Router::new() - .route("/billing/preferences", put(update_billing_preferences)) .route("/billing/subscriptions", post(create_billing_subscription)) .route( "/billing/subscriptions/manage", @@ -55,108 +51,6 @@ pub fn router() -> Router { ) } -#[derive(Debug, Serialize)] -struct BillingPreferencesResponse { - trial_started_at: Option<String>, - max_monthly_llm_usage_spending_in_cents: i32, - model_request_overages_enabled: bool, - model_request_overages_spend_limit_in_cents: i32, -} - -#[derive(Debug, Deserialize)] -struct UpdateBillingPreferencesBody { - github_user_id: i32, - #[serde(default)] - max_monthly_llm_usage_spending_in_cents: i32, - #[serde(default)] - model_request_overages_enabled: bool, - #[serde(default)] - model_request_overages_spend_limit_in_cents: i32, -} - -async fn update_billing_preferences( - Extension(app): Extension<Arc<AppState>>, - Extension(rpc_server): Extension<Arc<crate::rpc::Server>>, - extract::Json(body): extract::Json<UpdateBillingPreferencesBody>, -) -> Result<Json<BillingPreferencesResponse>> { - let user = app - .db - .get_user_by_github_user_id(body.github_user_id) - .await? - .context("user not found")?; - - let billing_customer = app.db.get_billing_customer_by_user_id(user.id).await?; - - let max_monthly_llm_usage_spending_in_cents = - body.max_monthly_llm_usage_spending_in_cents.max(0); - let model_request_overages_spend_limit_in_cents = - body.model_request_overages_spend_limit_in_cents.max(0); - - let billing_preferences = - if let Some(_billing_preferences) = app.db.get_billing_preferences(user.id).await? { - app.db - .update_billing_preferences( - user.id, - &UpdateBillingPreferencesParams { - max_monthly_llm_usage_spending_in_cents: ActiveValue::set( - max_monthly_llm_usage_spending_in_cents, - ), - model_request_overages_enabled: ActiveValue::set( - body.model_request_overages_enabled, - ), - model_request_overages_spend_limit_in_cents: ActiveValue::set( - model_request_overages_spend_limit_in_cents, - ), - }, - ) - .await? - } else { - app.db - .create_billing_preferences( - user.id, - &crate::db::CreateBillingPreferencesParams { - max_monthly_llm_usage_spending_in_cents, - model_request_overages_enabled: body.model_request_overages_enabled, - model_request_overages_spend_limit_in_cents, - }, - ) - .await? - }; - - SnowflakeRow::new( - "Billing Preferences Updated", - Some(user.metrics_id), - user.admin, - None, - json!({ - "user_id": user.id, - "model_request_overages_enabled": billing_preferences.model_request_overages_enabled, - "model_request_overages_spend_limit_in_cents": billing_preferences.model_request_overages_spend_limit_in_cents, - "max_monthly_llm_usage_spending_in_cents": billing_preferences.max_monthly_llm_usage_spending_in_cents, - }), - ) - .write(&app.kinesis_client, &app.config.kinesis_stream) - .await - .log_err(); - - rpc_server.refresh_llm_tokens_for_user(user.id).await; - - Ok(Json(BillingPreferencesResponse { - trial_started_at: billing_customer - .and_then(|billing_customer| billing_customer.trial_started_at) - .map(|trial_started_at| { - trial_started_at - .and_utc() - .to_rfc3339_opts(SecondsFormat::Millis, true) - }), - max_monthly_llm_usage_spending_in_cents: billing_preferences - .max_monthly_llm_usage_spending_in_cents, - model_request_overages_enabled: billing_preferences.model_request_overages_enabled, - model_request_overages_spend_limit_in_cents: billing_preferences - .model_request_overages_spend_limit_in_cents, - })) -} - #[derive(Debug, PartialEq, Clone, Copy, Deserialize)] #[serde(rename_all = "snake_case")] enum ProductCode { From e14c9479e40e3ce83c4222b974fc22c0fb81c5e5 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 21 Jul 2025 17:36:52 +0200 Subject: [PATCH 223/658] chore: Pin taffy version (#34827) Follow-up to 34817 Closes #ISSUE Release Notes: - N/A *or* Added/Fixed/Improved ... --- Cargo.lock | 4 ++-- crates/gpui/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a323fb70afaa85656aae2dbce2d672fcb1a21808..c0d8dabf090c964e6e03b4008278447e13c776f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -15936,9 +15936,9 @@ dependencies = [ [[package]] name = "taffy" -version = "0.5.2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cb893bff0f80ae17d3a57e030622a967b8dbc90e38284d9b4b1442e23873c94" +checksum = "e8b61630cba2afd2c851821add2e1bb1b7851a2436e839ab73b56558b009035e" dependencies = [ "arrayvec", "grid", diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 9da01c5ff3ac44214ff56327bbc81f89da647397..878794647a4633b63eedd36ec07d18ec67ee1ef8 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.5.1" +taffy = "=0.5.1" thiserror.workspace = true util.workspace = true uuid.workspace = true From bf8aba566ce81a5bb1dfc4c5c70810695076f339 Mon Sep 17 00:00:00 2001 From: Marshall Bowers <git@maxdeviant.com> Date: Mon, 21 Jul 2025 11:47:40 -0400 Subject: [PATCH 224/658] collab: Remove unused billing preferences queries (#34830) This PR removes some billing preferences queries that are no longer in use. Release Notes: - N/A --- crates/collab/src/db.rs | 3 - .../src/db/queries/billing_preferences.rs | 74 ------------------- 2 files changed, 77 deletions(-) diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index cc2924569776f7be5bb2be546fa67413bbf75d4c..8cd1e3ea83d270effc0a0aa2425a0cc94f99ec11 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -42,9 +42,6 @@ pub use tests::TestDb; pub use ids::*; pub use queries::billing_customers::{CreateBillingCustomerParams, UpdateBillingCustomerParams}; -pub use queries::billing_preferences::{ - CreateBillingPreferencesParams, UpdateBillingPreferencesParams, -}; pub use queries::billing_subscriptions::{ CreateBillingSubscriptionParams, UpdateBillingSubscriptionParams, }; diff --git a/crates/collab/src/db/queries/billing_preferences.rs b/crates/collab/src/db/queries/billing_preferences.rs index 1a6fbe946a47e5c47e5ad5c4c41db32ab25e4e7c..f370964ecd7d5c762c88e5fb572fde84ce81935d 100644 --- a/crates/collab/src/db/queries/billing_preferences.rs +++ b/crates/collab/src/db/queries/billing_preferences.rs @@ -1,21 +1,5 @@ -use anyhow::Context as _; - use super::*; -#[derive(Debug)] -pub struct CreateBillingPreferencesParams { - 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(Debug, Default)] -pub struct UpdateBillingPreferencesParams { - pub max_monthly_llm_usage_spending_in_cents: ActiveValue<i32>, - pub model_request_overages_enabled: ActiveValue<bool>, - pub model_request_overages_spend_limit_in_cents: ActiveValue<i32>, -} - impl Database { /// Returns the billing preferences for the given user, if they exist. pub async fn get_billing_preferences( @@ -30,62 +14,4 @@ impl Database { }) .await } - - /// Creates new billing preferences for the given user. - pub async fn create_billing_preferences( - &self, - user_id: UserId, - params: &CreateBillingPreferencesParams, - ) -> Result<billing_preference::Model> { - self.transaction(|tx| async move { - let preferences = billing_preference::Entity::insert(billing_preference::ActiveModel { - user_id: ActiveValue::set(user_id), - max_monthly_llm_usage_spending_in_cents: ActiveValue::set( - params.max_monthly_llm_usage_spending_in_cents, - ), - model_request_overages_enabled: ActiveValue::set( - params.model_request_overages_enabled, - ), - model_request_overages_spend_limit_in_cents: ActiveValue::set( - params.model_request_overages_spend_limit_in_cents, - ), - ..Default::default() - }) - .exec_with_returning(&*tx) - .await?; - - Ok(preferences) - }) - .await - } - - /// Updates the billing preferences for the given user. - pub async fn update_billing_preferences( - &self, - user_id: UserId, - params: &UpdateBillingPreferencesParams, - ) -> Result<billing_preference::Model> { - self.transaction(|tx| async move { - let preferences = billing_preference::Entity::update_many() - .set(billing_preference::ActiveModel { - max_monthly_llm_usage_spending_in_cents: params - .max_monthly_llm_usage_spending_in_cents - .clone(), - model_request_overages_enabled: params.model_request_overages_enabled.clone(), - model_request_overages_spend_limit_in_cents: params - .model_request_overages_spend_limit_in_cents - .clone(), - ..Default::default() - }) - .filter(billing_preference::Column::UserId.eq(user_id)) - .exec_with_returning(&*tx) - .await?; - - Ok(preferences - .into_iter() - .next() - .context("billing preferences not found")?) - }) - .await - } } From bc5c5cf5d65654d3001bc27c43a8f511dee77952 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Mon, 21 Jul 2025 12:20:38 -0400 Subject: [PATCH 225/658] onboarding: Create basic onboarding page (#34723) Release Notes: - N/A --------- Co-authored-by: Ben Kunkle <ben@zed.dev> --- Cargo.lock | 18 ++ Cargo.toml | 2 + crates/onboarding/Cargo.toml | 28 +++ crates/onboarding/LICENSE-GPL | 1 + crates/onboarding/src/onboarding.rs | 352 ++++++++++++++++++++++++++++ crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 6 + crates/zed/src/zed.rs | 1 + 8 files changed, 409 insertions(+) create mode 100644 crates/onboarding/Cargo.toml create mode 120000 crates/onboarding/LICENSE-GPL create mode 100644 crates/onboarding/src/onboarding.rs diff --git a/Cargo.lock b/Cargo.lock index c0d8dabf090c964e6e03b4008278447e13c776f6..1dcfb877562cd86a9edf8a4aa58138b6cbce5181 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10983,6 +10983,23 @@ dependencies = [ "workspace-hack", ] +[[package]] +name = "onboarding" +version = "0.1.0" +dependencies = [ + "anyhow", + "command_palette_hooks", + "db", + "feature_flags", + "fs", + "gpui", + "settings", + "theme", + "ui", + "workspace", + "workspace-hack", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -20223,6 +20240,7 @@ dependencies = [ "nix 0.29.0", "node_runtime", "notifications", + "onboarding", "outline", "outline_panel", "parking_lot", diff --git a/Cargo.toml b/Cargo.toml index c99ba3953dbdd909cda61b4e118a4ef9f38d1f59..ea8690f2b3af444900d8760c7a678124702c7f93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -108,6 +108,7 @@ members = [ "crates/node_runtime", "crates/notifications", "crates/ollama", + "crates/onboarding", "crates/open_ai", "crates/open_router", "crates/outline", @@ -325,6 +326,7 @@ net = { path = "crates/net" } node_runtime = { path = "crates/node_runtime" } notifications = { path = "crates/notifications" } ollama = { path = "crates/ollama" } +onboarding = { path = "crates/onboarding" } open_ai = { path = "crates/open_ai" } open_router = { path = "crates/open_router", features = ["schemars"] } outline = { path = "crates/outline" } diff --git a/crates/onboarding/Cargo.toml b/crates/onboarding/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..693e39d4ca052dc3ed91376622235db2bd92f6ca --- /dev/null +++ b/crates/onboarding/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "onboarding" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/onboarding.rs" + +[features] +default = [] + +[dependencies] +anyhow.workspace = true +command_palette_hooks.workspace = true +db.workspace = true +feature_flags.workspace = true +fs.workspace = true +gpui.workspace = true +settings.workspace = true +theme.workspace = true +ui.workspace = true +workspace.workspace = true +workspace-hack.workspace = true diff --git a/crates/onboarding/LICENSE-GPL b/crates/onboarding/LICENSE-GPL new file mode 120000 index 0000000000000000000000000000000000000000..dd648cce4f61e4a93871ac05a9f381e485f80319 --- /dev/null +++ b/crates/onboarding/LICENSE-GPL @@ -0,0 +1 @@ +../../../LICENSE-GPL \ No newline at end of file diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs new file mode 100644 index 0000000000000000000000000000000000000000..1ce236f94180845906057bb546377742a8a2c0ca --- /dev/null +++ b/crates/onboarding/src/onboarding.rs @@ -0,0 +1,352 @@ +use command_palette_hooks::CommandPaletteFilter; +use db::kvp::KEY_VALUE_STORE; +use feature_flags::{FeatureFlag, FeatureFlagViewExt as _}; +use fs::Fs; +use gpui::{ + AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, + IntoElement, Render, SharedString, Subscription, Task, WeakEntity, Window, actions, +}; +use settings::{Settings, SettingsStore, update_settings_file}; +use std::sync::Arc; +use theme::{ThemeMode, ThemeSettings}; +use ui::{ + ButtonCommon as _, ButtonSize, ButtonStyle, Clickable as _, Color, Divider, FluentBuilder, + Headline, InteractiveElement, KeyBinding, Label, LabelCommon, ParentElement as _, + StatefulInteractiveElement, Styled, ToggleButton, Toggleable as _, Vector, VectorName, div, + h_flex, rems, v_container, v_flex, +}; +use workspace::{ + AppState, Workspace, WorkspaceId, + dock::DockPosition, + item::{Item, ItemEvent}, + open_new, with_active_or_new_workspace, +}; + +pub struct OnBoardingFeatureFlag {} + +impl FeatureFlag for OnBoardingFeatureFlag { + const NAME: &'static str = "onboarding"; +} + +pub const FIRST_OPEN: &str = "first_open"; + +actions!( + zed, + [ + /// Opens the onboarding view. + OpenOnboarding + ] +); + +pub fn init(cx: &mut App) { + cx.on_action(|_: &OpenOnboarding, cx| { + with_active_or_new_workspace(cx, |workspace, window, cx| { + workspace + .with_local_workspace(window, cx, |workspace, window, cx| { + let existing = workspace + .active_pane() + .read(cx) + .items() + .find_map(|item| item.downcast::<Onboarding>()); + + if let Some(existing) = existing { + workspace.activate_item(&existing, true, true, window, cx); + } else { + let settings_page = Onboarding::new(workspace.weak_handle(), cx); + workspace.add_item_to_active_pane( + Box::new(settings_page), + None, + true, + window, + cx, + ) + } + }) + .detach(); + }); + }); + cx.observe_new::<Workspace>(|_, window, cx| { + let Some(window) = window else { + return; + }; + + let onboarding_actions = [std::any::TypeId::of::<OpenOnboarding>()]; + + CommandPaletteFilter::update_global(cx, |filter, _cx| { + filter.hide_action_types(&onboarding_actions); + }); + + cx.observe_flag::<OnBoardingFeatureFlag, _>(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(); +} + +pub fn show_onboarding_view(app_state: Arc<AppState>, cx: &mut App) -> Task<anyhow::Result<()>> { + open_new( + Default::default(), + app_state, + cx, + |workspace, window, cx| { + { + workspace.toggle_dock(DockPosition::Left, window, cx); + let onboarding_page = Onboarding::new(workspace.weak_handle(), cx); + workspace.add_item_to_center(Box::new(onboarding_page.clone()), window, cx); + + window.focus(&onboarding_page.focus_handle(cx)); + + cx.notify(); + }; + db::write_and_log(cx, || { + KEY_VALUE_STORE.write_kvp(FIRST_OPEN.to_string(), "false".to_string()) + }); + }, + ) +} + +fn read_theme_selection(cx: &App) -> ThemeMode { + let settings = ThemeSettings::get_global(cx); + settings + .theme_selection + .as_ref() + .and_then(|selection| selection.mode()) + .unwrap_or_default() +} + +fn write_theme_selection(theme_mode: ThemeMode, cx: &App) { + let fs = <dyn Fs>::global(cx); + + update_settings_file::<ThemeSettings>(fs, cx, move |settings, _| { + settings.set_mode(theme_mode); + }); +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum SelectedPage { + Basics, + Editing, + AiSetup, +} + +struct Onboarding { + workspace: WeakEntity<Workspace>, + focus_handle: FocusHandle, + selected_page: SelectedPage, + _settings_subscription: Subscription, +} + +impl Onboarding { + fn new(workspace: WeakEntity<Workspace>, cx: &mut App) -> Entity<Self> { + cx.new(|cx| Self { + workspace, + focus_handle: cx.focus_handle(), + selected_page: SelectedPage::Basics, + _settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()), + }) + } + + fn render_page_nav( + &mut self, + page: SelectedPage, + _: &mut Window, + cx: &mut Context<Self>, + ) -> impl IntoElement { + let text = match page { + SelectedPage::Basics => "Basics", + SelectedPage::Editing => "Editing", + SelectedPage::AiSetup => "AI Setup", + }; + let binding = match page { + SelectedPage::Basics => { + KeyBinding::new(vec![gpui::Keystroke::parse("cmd-1").unwrap()], cx) + } + SelectedPage::Editing => { + KeyBinding::new(vec![gpui::Keystroke::parse("cmd-2").unwrap()], cx) + } + SelectedPage::AiSetup => { + KeyBinding::new(vec![gpui::Keystroke::parse("cmd-3").unwrap()], cx) + } + }; + let selected = self.selected_page == page; + h_flex() + .id(text) + .rounded_sm() + .child(text) + .child(binding) + .h_8() + .gap_2() + .px_2() + .py_0p5() + .w_full() + .justify_between() + .map(|this| { + if selected { + this.bg(Color::Selected.color(cx)) + .border_l_1() + .border_color(Color::Accent.color(cx)) + } else { + this.text_color(Color::Muted.color(cx)) + } + }) + .hover(|style| { + if selected { + style.bg(Color::Selected.color(cx).opacity(0.6)) + } else { + style.bg(Color::Selected.color(cx).opacity(0.3)) + } + }) + .on_click(cx.listener(move |this, _, _, cx| { + this.selected_page = page; + cx.notify(); + })) + } + + fn render_page(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement { + match self.selected_page { + SelectedPage::Basics => self.render_basics_page(window, cx).into_any_element(), + SelectedPage::Editing => self.render_editing_page(window, cx).into_any_element(), + SelectedPage::AiSetup => self.render_ai_setup_page(window, cx).into_any_element(), + } + } + + fn render_basics_page(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { + let theme_mode = read_theme_selection(cx); + + v_container().child( + h_flex() + .items_center() + .justify_between() + .child(Label::new("Theme")) + .child( + h_flex() + .rounded_md() + .child( + ToggleButton::new("light", "Light") + .style(ButtonStyle::Filled) + .size(ButtonSize::Large) + .toggle_state(theme_mode == ThemeMode::Light) + .on_click(|_, _, cx| write_theme_selection(ThemeMode::Light, cx)) + .first(), + ) + .child( + ToggleButton::new("dark", "Dark") + .style(ButtonStyle::Filled) + .size(ButtonSize::Large) + .toggle_state(theme_mode == ThemeMode::Dark) + .on_click(|_, _, cx| write_theme_selection(ThemeMode::Dark, cx)) + .last(), + ) + .child( + ToggleButton::new("system", "System") + .style(ButtonStyle::Filled) + .size(ButtonSize::Large) + .toggle_state(theme_mode == ThemeMode::System) + .on_click(|_, _, cx| write_theme_selection(ThemeMode::System, cx)) + .middle(), + ), + ), + ) + } + + fn render_editing_page(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement { + // div().child("editing page") + "Right" + } + + fn render_ai_setup_page(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement { + div().child("ai setup page") + } +} + +impl Render for Onboarding { + fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { + h_flex() + .image_cache(gpui::retain_all("onboarding-page")) + .key_context("onboarding-page") + .px_24() + .py_12() + .items_start() + .child( + v_flex() + .w_1_3() + .h_full() + .child( + h_flex() + .pt_0p5() + .child(Vector::square(VectorName::ZedLogo, rems(2.))) + .child( + v_flex() + .left_1() + .items_center() + .child(Headline::new("Welcome to Zed")) + .child( + Label::new("The editor for what's next") + .color(Color::Muted) + .italic(), + ), + ), + ) + .p_1() + .child(Divider::horizontal_dashed()) + .child( + v_flex().gap_1().children([ + self.render_page_nav(SelectedPage::Basics, window, cx) + .into_element(), + self.render_page_nav(SelectedPage::Editing, window, cx) + .into_element(), + self.render_page_nav(SelectedPage::AiSetup, window, cx) + .into_element(), + ]), + ), + ) + // .child(Divider::vertical_dashed()) + .child(div().w_2_3().h_full().child(self.render_page(window, cx))) + } +} + +impl EventEmitter<ItemEvent> for Onboarding {} + +impl Focusable for Onboarding { + fn focus_handle(&self, _: &App) -> gpui::FocusHandle { + self.focus_handle.clone() + } +} + +impl Item for Onboarding { + type Event = ItemEvent; + + fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { + "Onboarding".into() + } + + fn telemetry_event_text(&self) -> Option<&'static str> { + Some("Onboarding Page Opened") + } + + fn show_toolbar(&self) -> bool { + false + } + + fn clone_on_split( + &self, + _workspace_id: Option<WorkspaceId>, + _: &mut Window, + cx: &mut Context<Self>, + ) -> Option<Entity<Self>> { + Some(Onboarding::new(self.workspace.clone(), cx)) + } + + fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) { + f(*event) + } +} diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index bbceb3f1019fe3a86ba2c853cbd6a364594574e2..e565aba26b4caae298a063b7cd2036f5a7ee648d 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -99,6 +99,7 @@ nc.workspace = true nix = { workspace = true, features = ["pthread", "signal"] } node_runtime.workspace = true notifications.workspace = true +onboarding.workspace = true outline.workspace = true outline_panel.workspace = true parking_lot.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 89b9fad6bf12c3529d4c695df6592d1d906fac8b..9d85923ca2f8e837c2e1f280c145947e43404594 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -24,6 +24,7 @@ use reqwest_client::ReqwestClient; use assets::Assets; use node_runtime::{NodeBinaryOptions, NodeRuntime}; +use onboarding::show_onboarding_view; use parking_lot::Mutex; use project::project_settings::ProjectSettings; use recent_projects::{SshSettings, open_ssh_project}; @@ -619,6 +620,7 @@ pub fn main() { markdown_preview::init(cx); svg_preview::init(cx); welcome::init(cx); + onboarding::init(cx); settings_ui::init(cx); extensions_ui::init(cx); zeta::init(cx); @@ -1039,6 +1041,10 @@ async fn restore_or_create_workspace(app_state: Arc<AppState>, cx: &mut AsyncApp ); } } + } else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) { + let state = app_state.clone(); + cx.update(|cx| show_onboarding_view(state, cx))?.await?; + // cx.update(|cx| show_welcome_view(app_state, cx))?.await?; } else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) { cx.update(|cx| show_welcome_view(app_state, cx))?.await?; } else { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index cc3906af4d957463f18e19a0e0756d21a2b1d022..24c7ab5ba278b2beff9c61202216aaf1876cf945 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -3957,6 +3957,7 @@ mod tests { language::init(cx); workspace::init(app_state.clone(), cx); welcome::init(cx); + onboarding::init(cx); Project::init_settings(cx); app_state }) From b6cf398eab34bbc515a94f77ec01d0cd44570c61 Mon Sep 17 00:00:00 2001 From: Balboa Codes <20909423+balboacodes@users.noreply.github.com> Date: Mon, 21 Jul 2025 13:25:59 -0400 Subject: [PATCH 226/658] docs: Fix PHP docs typo (#34836) This fixes a minor typo in the PHP docs. Release Notes: - N/A --- docs/src/languages/php.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/languages/php.md b/docs/src/languages/php.md index 9cb7c40762e7af0f0680cbbcf564d4d989e7f0e9..4e94c134467c5a3484ede7a2146f2f09c172e859 100644 --- a/docs/src/languages/php.md +++ b/docs/src/languages/php.md @@ -15,7 +15,7 @@ The PHP extension offers both `phpactor` and `intelephense` language server supp ## Phpactor -The Zed PHP Extension can install `phpactor` automatically but requires `php` to installed and available in your path: +The Zed PHP Extension can install `phpactor` automatically but requires `php` to be installed and available in your path: ```sh # brew install php # macOS From 254c7a330a9c95d4ca2d7251cf601a5dac165768 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov <kirill@zed.dev> Date: Mon, 21 Jul 2025 20:48:07 +0300 Subject: [PATCH 227/658] Regroup LSP context menu items by the worktree name (#34838) Also * remove the feature gate * open buffers with an error when no logs are present * adjust the hover text to indicate that difference <img width="480" height="380" alt="image" src="https://github.com/user-attachments/assets/6b2350fc-5121-4b1e-bc22-503d964531a2" /> Release Notes: - N/A --- Cargo.lock | 1 - .../src/activity_indicator.rs | 4 +- crates/language_tools/Cargo.toml | 1 - crates/language_tools/src/lsp_tool.rs | 592 +++++++++++------- 4 files changed, 357 insertions(+), 241 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1dcfb877562cd86a9edf8a4aa58138b6cbce5181..4537d440ccd97b4934164eee3f0bdf0230edd4ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9165,7 +9165,6 @@ dependencies = [ "collections", "copilot", "editor", - "feature_flags", "futures 0.3.31", "gpui", "itertools 0.14.0", diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index aee25fc9e39d533409b980782fa8f0cac3977935..f8ea7173d8afecb14a8e8ed9e1de6a87660ddc1a 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -231,7 +231,6 @@ impl ActivityIndicator { status, } => { let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx)); - let project = project.clone(); let status = status.clone(); let server_name = server_name.clone(); cx.spawn_in(window, async move |workspace, cx| { @@ -247,8 +246,7 @@ impl ActivityIndicator { workspace.update_in(cx, |workspace, window, cx| { workspace.add_item_to_active_pane( Box::new(cx.new(|cx| { - let mut editor = - Editor::for_buffer(buffer, Some(project.clone()), window, cx); + let mut editor = Editor::for_buffer(buffer, None, window, cx); editor.set_read_only(true); editor })), diff --git a/crates/language_tools/Cargo.toml b/crates/language_tools/Cargo.toml index 45af7518d589166e26788203c919d2267b544756..5aa914311a6eccc1cb68efa37e878ad12249d6fd 100644 --- a/crates/language_tools/Cargo.toml +++ b/crates/language_tools/Cargo.toml @@ -18,7 +18,6 @@ client.workspace = true collections.workspace = true copilot.workspace = true editor.workspace = true -feature_flags.workspace = true futures.workspace = true gpui.workspace = true itertools.workspace = true diff --git a/crates/language_tools/src/lsp_tool.rs b/crates/language_tools/src/lsp_tool.rs index fd843916800a552692f53404a5165b85f48d172e..9e95ed46734940f3f8de429ad6a581dc092b4614 100644 --- a/crates/language_tools/src/lsp_tool.rs +++ b/crates/language_tools/src/lsp_tool.rs @@ -1,13 +1,17 @@ -use std::{collections::hash_map, path::PathBuf, rc::Rc, time::Duration}; +use std::{ + collections::{BTreeMap, HashMap}, + path::{Path, PathBuf}, + rc::Rc, + time::Duration, +}; use client::proto; -use collections::{HashMap, HashSet}; +use collections::HashSet; use editor::{Editor, EditorEvent}; -use feature_flags::FeatureFlagAppExt as _; use gpui::{Corner, Entity, Subscription, Task, WeakEntity, actions}; -use language::{BinaryStatus, BufferId, LocalFile, ServerHealth}; +use language::{BinaryStatus, BufferId, ServerHealth}; use lsp::{LanguageServerId, LanguageServerName, LanguageServerSelector}; -use project::{LspStore, LspStoreEvent, project_settings::ProjectSettings}; +use project::{LspStore, LspStoreEvent, Worktree, project_settings::ProjectSettings}; use settings::{Settings as _, SettingsStore}; use ui::{ Context, ContextMenu, ContextMenuEntry, ContextMenuItem, DocumentationAside, DocumentationSide, @@ -36,8 +40,7 @@ pub struct LspTool { #[derive(Debug)] struct LanguageServerState { - items: Vec<LspItem>, - other_servers_start_index: Option<usize>, + items: Vec<LspMenuItem>, workspace: WeakEntity<Workspace>, lsp_store: WeakEntity<LspStore>, active_editor: Option<ActiveEditor>, @@ -63,8 +66,13 @@ impl std::fmt::Debug for ActiveEditor { struct LanguageServers { health_statuses: HashMap<LanguageServerId, LanguageServerHealthStatus>, binary_statuses: HashMap<LanguageServerName, LanguageServerBinaryStatus>, - servers_per_buffer_abs_path: - HashMap<PathBuf, HashMap<LanguageServerId, Option<LanguageServerName>>>, + servers_per_buffer_abs_path: HashMap<PathBuf, ServersForPath>, +} + +#[derive(Debug, Clone)] +struct ServersForPath { + servers: HashMap<LanguageServerId, Option<LanguageServerName>>, + worktree: Option<WeakEntity<Worktree>>, } #[derive(Debug, Clone)] @@ -120,8 +128,8 @@ impl LanguageServerState { }; let mut first_button_encountered = false; - for (i, item) in self.items.iter().enumerate() { - if let LspItem::ToggleServersButton { restart } = item { + for item in &self.items { + if let LspMenuItem::ToggleServersButton { restart } = item { let label = if *restart { "Restart All Servers" } else { @@ -140,22 +148,19 @@ impl LanguageServerState { }; let project = workspace.read(cx).project().clone(); let buffer_store = project.read(cx).buffer_store().clone(); - let worktree_store = project.read(cx).worktree_store(); - let buffers = state .read(cx) .language_servers .servers_per_buffer_abs_path - .keys() - .filter_map(|abs_path| { - worktree_store.read(cx).find_worktree(abs_path, cx) - }) - .filter_map(|(worktree, relative_path)| { - let entry = - worktree.read(cx).entry_for_path(&relative_path)?; - project.read(cx).path_for_entry(entry.id, cx) - }) - .filter_map(|project_path| { + .iter() + .filter_map(|(abs_path, servers)| { + let worktree = + servers.worktree.as_ref()?.upgrade()?.read(cx); + let relative_path = + abs_path.strip_prefix(&worktree.abs_path()).ok()?; + let entry = worktree.entry_for_path(&relative_path)?; + let project_path = + project.read(cx).path_for_entry(entry.id, cx)?; buffer_store.read(cx).get_by_path(&project_path) }) .collect(); @@ -165,13 +170,16 @@ impl LanguageServerState { .iter() // Do not try to use IDs as we have stopped all servers already, when allowing to restart them all .flat_map(|item| match item { - LspItem::ToggleServersButton { .. } => None, - LspItem::WithHealthCheck(_, status, ..) => Some( - LanguageServerSelector::Name(status.name.clone()), - ), - LspItem::WithBinaryStatus(_, server_name, ..) => Some( - LanguageServerSelector::Name(server_name.clone()), + LspMenuItem::Header { .. } => None, + LspMenuItem::ToggleServersButton { .. } => None, + LspMenuItem::WithHealthCheck { health, .. } => Some( + LanguageServerSelector::Name(health.name.clone()), ), + LspMenuItem::WithBinaryStatus { + server_name, .. + } => Some(LanguageServerSelector::Name( + server_name.clone(), + )), }) .collect(); lsp_store.restart_language_servers_for_buffers( @@ -190,13 +198,17 @@ impl LanguageServerState { } menu = menu.item(button); continue; - }; + } else if let LspMenuItem::Header { header, separator } = item { + menu = menu + .when(*separator, |menu| menu.separator()) + .when_some(header.as_ref(), |menu, header| menu.header(header)); + continue; + } let Some(server_info) = item.server_info() else { continue; }; - let workspace = self.workspace.clone(); let server_selector = server_info.server_selector(); // TODO currently, Zed remote does not work well with the LSP logs // https://github.com/zed-industries/zed/issues/28557 @@ -205,6 +217,7 @@ impl LanguageServerState { let status_color = server_info .binary_status + .as_ref() .and_then(|binary_status| match binary_status.status { BinaryStatus::None => None, BinaryStatus::CheckingForUpdate @@ -223,17 +236,20 @@ impl LanguageServerState { }) .unwrap_or(Color::Success); - if self - .other_servers_start_index - .is_some_and(|index| index == i) - { - menu = menu.separator().header("Other Buffers"); - } - - if i == 0 && self.other_servers_start_index.is_some() { - menu = menu.header("Current Buffer"); - } + let message = server_info + .message + .as_ref() + .or_else(|| server_info.binary_status.as_ref()?.message.as_ref()) + .cloned(); + let hover_label = if has_logs { + Some("View Logs") + } else if message.is_some() { + Some("View Message") + } else { + None + }; + let server_name = server_info.name.clone(); menu = menu.item(ContextMenuItem::custom_entry( move |_, _| { h_flex() @@ -245,42 +261,99 @@ impl LanguageServerState { h_flex() .gap_2() .child(Indicator::dot().color(status_color)) - .child(Label::new(server_info.name.0.clone())), - ) - .child( - h_flex() - .visible_on_hover("menu_item") - .child( - Label::new("View Logs") - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child( - Icon::new(IconName::ChevronRight) - .size(IconSize::Small) - .color(Color::Muted), - ), + .child(Label::new(server_name.0.clone())), ) + .when_some(hover_label, |div, hover_label| { + div.child( + h_flex() + .visible_on_hover("menu_item") + .child( + Label::new(hover_label) + .size(LabelSize::Small) + .color(Color::Muted), + ) + .child( + Icon::new(IconName::ChevronRight) + .size(IconSize::Small) + .color(Color::Muted), + ), + ) + }) .into_any_element() }, { let lsp_logs = lsp_logs.clone(); + let message = message.clone(); + let server_selector = server_selector.clone(); + let server_name = server_info.name.clone(); + let workspace = self.workspace.clone(); move |window, cx| { - if !has_logs { + if has_logs { + lsp_logs.update(cx, |lsp_logs, cx| { + lsp_logs.open_server_trace( + workspace.clone(), + server_selector.clone(), + window, + cx, + ); + }); + } else if let Some(message) = &message { + let Some(create_buffer) = workspace + .update(cx, |workspace, cx| { + workspace + .project() + .update(cx, |project, cx| project.create_buffer(cx)) + }) + .ok() + else { + return; + }; + + let window = window.window_handle(); + let workspace = workspace.clone(); + let message = message.clone(); + let server_name = server_name.clone(); + cx.spawn(async move |cx| { + let buffer = create_buffer.await?; + buffer.update(cx, |buffer, cx| { + buffer.edit( + [( + 0..0, + format!("Language server {server_name}:\n\n{message}"), + )], + None, + cx, + ); + buffer.set_capability(language::Capability::ReadOnly, cx); + })?; + + workspace.update(cx, |workspace, cx| { + window.update(cx, |_, window, cx| { + workspace.add_item_to_active_pane( + Box::new(cx.new(|cx| { + let mut editor = + Editor::for_buffer(buffer, None, window, cx); + editor.set_read_only(true); + editor + })), + None, + true, + window, + cx, + ); + }) + })??; + + anyhow::Ok(()) + }) + .detach(); + } else { cx.propagate(); return; } - lsp_logs.update(cx, |lsp_logs, cx| { - lsp_logs.open_server_trace( - workspace.clone(), - server_selector.clone(), - window, - cx, - ); - }); } }, - server_info.message.map(|server_message| { + message.map(|server_message| { DocumentationAside::new( DocumentationSide::Right, Rc::new(move |_| Label::new(server_message.clone()).into_any_element()), @@ -345,81 +418,95 @@ impl LanguageServers { #[derive(Debug)] enum ServerData<'a> { - WithHealthCheck( - LanguageServerId, - &'a LanguageServerHealthStatus, - Option<&'a LanguageServerBinaryStatus>, - ), - WithBinaryStatus( - Option<LanguageServerId>, - &'a LanguageServerName, - &'a LanguageServerBinaryStatus, - ), + WithHealthCheck { + server_id: LanguageServerId, + health: &'a LanguageServerHealthStatus, + binary_status: Option<&'a LanguageServerBinaryStatus>, + }, + WithBinaryStatus { + server_id: Option<LanguageServerId>, + server_name: &'a LanguageServerName, + binary_status: &'a LanguageServerBinaryStatus, + }, } #[derive(Debug)] -enum LspItem { - WithHealthCheck( - LanguageServerId, - LanguageServerHealthStatus, - Option<LanguageServerBinaryStatus>, - ), - WithBinaryStatus( - Option<LanguageServerId>, - LanguageServerName, - LanguageServerBinaryStatus, - ), +enum LspMenuItem { + WithHealthCheck { + server_id: LanguageServerId, + health: LanguageServerHealthStatus, + binary_status: Option<LanguageServerBinaryStatus>, + }, + WithBinaryStatus { + server_id: Option<LanguageServerId>, + server_name: LanguageServerName, + binary_status: LanguageServerBinaryStatus, + }, ToggleServersButton { restart: bool, }, + Header { + header: Option<SharedString>, + separator: bool, + }, } -impl LspItem { +impl LspMenuItem { fn server_info(&self) -> Option<ServerInfo> { match self { - LspItem::ToggleServersButton { .. } => None, - LspItem::WithHealthCheck( - language_server_id, - language_server_health_status, - language_server_binary_status, - ) => Some(ServerInfo { - name: language_server_health_status.name.clone(), - id: Some(*language_server_id), - health: language_server_health_status.health(), - binary_status: language_server_binary_status.clone(), - message: language_server_health_status.message(), + Self::Header { .. } => None, + Self::ToggleServersButton { .. } => None, + Self::WithHealthCheck { + server_id, + health, + binary_status, + .. + } => Some(ServerInfo { + name: health.name.clone(), + id: Some(*server_id), + health: health.health(), + binary_status: binary_status.clone(), + message: health.message(), }), - LspItem::WithBinaryStatus( + Self::WithBinaryStatus { server_id, - language_server_name, - language_server_binary_status, - ) => Some(ServerInfo { - name: language_server_name.clone(), + server_name, + binary_status, + .. + } => Some(ServerInfo { + name: server_name.clone(), id: *server_id, health: None, - binary_status: Some(language_server_binary_status.clone()), - message: language_server_binary_status.message.clone(), + binary_status: Some(binary_status.clone()), + message: binary_status.message.clone(), }), } } } impl ServerData<'_> { - fn name(&self) -> &LanguageServerName { - match self { - Self::WithHealthCheck(_, state, _) => &state.name, - Self::WithBinaryStatus(_, name, ..) => name, - } - } - - fn into_lsp_item(self) -> LspItem { + fn into_lsp_item(self) -> LspMenuItem { match self { - Self::WithHealthCheck(id, name, status) => { - LspItem::WithHealthCheck(id, name.clone(), status.cloned()) - } - Self::WithBinaryStatus(server_id, name, status) => { - LspItem::WithBinaryStatus(server_id, name.clone(), status.clone()) - } + Self::WithHealthCheck { + server_id, + health, + binary_status, + .. + } => LspMenuItem::WithHealthCheck { + server_id, + health: health.clone(), + binary_status: binary_status.cloned(), + }, + Self::WithBinaryStatus { + server_id, + server_name, + binary_status, + .. + } => LspMenuItem::WithBinaryStatus { + server_id, + server_name: server_name.clone(), + binary_status: binary_status.clone(), + }, } } } @@ -452,7 +539,6 @@ impl LspTool { let state = cx.new(|_| LanguageServerState { workspace: workspace.weak_handle(), items: Vec::new(), - other_servers_start_index: None, lsp_store: lsp_store.downgrade(), active_editor: None, language_servers: LanguageServers::default(), @@ -542,13 +628,28 @@ impl LspTool { message: proto::update_language_server::Variant::RegisteredForBuffer(update), .. } => { - self.server_state.update(cx, |state, _| { - state + self.server_state.update(cx, |state, cx| { + let Ok(worktree) = state.workspace.update(cx, |workspace, cx| { + workspace + .project() + .read(cx) + .find_worktree(Path::new(&update.buffer_abs_path), cx) + .map(|(worktree, _)| worktree.downgrade()) + }) else { + return; + }; + let entry = state .language_servers .servers_per_buffer_abs_path .entry(PathBuf::from(&update.buffer_abs_path)) - .or_default() - .insert(*language_server_id, name.clone()); + .or_insert_with(|| ServersForPath { + servers: HashMap::default(), + worktree: worktree.clone(), + }); + entry.servers.insert(*language_server_id, name.clone()); + if worktree.is_some() { + entry.worktree = worktree; + } }); updated = true; } @@ -562,94 +663,95 @@ impl LspTool { fn regenerate_items(&mut self, cx: &mut App) { self.server_state.update(cx, |state, cx| { - let editor_buffers = state + let active_worktrees = state .active_editor .as_ref() - .map(|active_editor| active_editor.editor_buffers.clone()) - .unwrap_or_default(); - let editor_buffer_paths = editor_buffers - .iter() - .filter_map(|buffer_id| { - let buffer_path = state - .lsp_store - .update(cx, |lsp_store, cx| { - Some( - project::File::from_dyn( - lsp_store - .buffer_store() - .read(cx) - .get(*buffer_id)? - .read(cx) - .file(), - )? - .abs_path(cx), - ) + .into_iter() + .flat_map(|active_editor| { + active_editor + .editor + .upgrade() + .into_iter() + .flat_map(|active_editor| { + active_editor + .read(cx) + .buffer() + .read(cx) + .all_buffers() + .into_iter() + .filter_map(|buffer| { + project::File::from_dyn(buffer.read(cx).file()) + }) + .map(|buffer_file| buffer_file.worktree.clone()) }) - .ok()??; - Some(buffer_path) }) - .collect::<Vec<_>>(); + .collect::<HashSet<_>>(); - let mut servers_with_health_checks = HashSet::default(); - let mut server_ids_with_health_checks = HashSet::default(); - let mut buffer_servers = - Vec::with_capacity(state.language_servers.health_statuses.len()); - let mut other_servers = - Vec::with_capacity(state.language_servers.health_statuses.len()); - let buffer_server_ids = editor_buffer_paths - .iter() - .filter_map(|buffer_path| { - state - .language_servers - .servers_per_buffer_abs_path - .get(buffer_path) - }) - .flatten() - .fold(HashMap::default(), |mut acc, (server_id, name)| { - match acc.entry(*server_id) { - hash_map::Entry::Occupied(mut o) => { - let old_name: &mut Option<&LanguageServerName> = o.get_mut(); - if old_name.is_none() { - *old_name = name.as_ref(); - } - } - hash_map::Entry::Vacant(v) => { - v.insert(name.as_ref()); + let mut server_ids_to_worktrees = + HashMap::<LanguageServerId, Entity<Worktree>>::default(); + let mut server_names_to_worktrees = HashMap::< + LanguageServerName, + HashSet<(Entity<Worktree>, LanguageServerId)>, + >::default(); + for servers_for_path in state.language_servers.servers_per_buffer_abs_path.values() { + if let Some(worktree) = servers_for_path + .worktree + .as_ref() + .and_then(|worktree| worktree.upgrade()) + { + for (server_id, server_name) in &servers_for_path.servers { + server_ids_to_worktrees.insert(*server_id, worktree.clone()); + if let Some(server_name) = server_name { + server_names_to_worktrees + .entry(server_name.clone()) + .or_default() + .insert((worktree.clone(), *server_id)); } } - acc + } + } + + let mut servers_per_worktree = BTreeMap::<SharedString, Vec<ServerData>>::new(); + let mut servers_without_worktree = Vec::<ServerData>::new(); + let mut servers_with_health_checks = HashSet::default(); + + for (server_id, health) in &state.language_servers.health_statuses { + let worktree = server_ids_to_worktrees.get(server_id).or_else(|| { + let worktrees = server_names_to_worktrees.get(&health.name)?; + worktrees + .iter() + .find(|(worktree, _)| active_worktrees.contains(worktree)) + .or_else(|| worktrees.iter().next()) + .map(|(worktree, _)| worktree) }); - for (server_id, server_state) in &state.language_servers.health_statuses { - let binary_status = state - .language_servers - .binary_statuses - .get(&server_state.name); - servers_with_health_checks.insert(&server_state.name); - server_ids_with_health_checks.insert(*server_id); - if buffer_server_ids.contains_key(server_id) { - buffer_servers.push(ServerData::WithHealthCheck( - *server_id, - server_state, - binary_status, - )); - } else { - other_servers.push(ServerData::WithHealthCheck( - *server_id, - server_state, - binary_status, - )); + servers_with_health_checks.insert(&health.name); + let worktree_name = + worktree.map(|worktree| SharedString::new(worktree.read(cx).root_name())); + + let binary_status = state.language_servers.binary_statuses.get(&health.name); + let server_data = ServerData::WithHealthCheck { + server_id: *server_id, + health, + binary_status, + }; + match worktree_name { + Some(worktree_name) => servers_per_worktree + .entry(worktree_name.clone()) + .or_default() + .push(server_data), + None => servers_without_worktree.push(server_data), } } let mut can_stop_all = !state.language_servers.health_statuses.is_empty(); let mut can_restart_all = state.language_servers.health_statuses.is_empty(); - for (server_name, status) in state + for (server_name, binary_status) in state .language_servers .binary_statuses .iter() .filter(|(name, _)| !servers_with_health_checks.contains(name)) { - match status.status { + match binary_status.status { BinaryStatus::None => { can_restart_all = false; can_stop_all |= true; @@ -674,52 +776,73 @@ impl LspTool { BinaryStatus::Failed { .. } => {} } - let matching_server_id = state - .language_servers - .servers_per_buffer_abs_path - .iter() - .filter(|(path, _)| editor_buffer_paths.contains(path)) - .flat_map(|(_, server_associations)| server_associations.iter()) - .find_map(|(id, name)| { - if name.as_ref() == Some(server_name) { - Some(*id) - } else { - None + match server_names_to_worktrees.get(server_name) { + Some(worktrees_for_name) => { + match worktrees_for_name + .iter() + .find(|(worktree, _)| active_worktrees.contains(worktree)) + .or_else(|| worktrees_for_name.iter().next()) + { + Some((worktree, server_id)) => { + let worktree_name = + SharedString::new(worktree.read(cx).root_name()); + servers_per_worktree + .entry(worktree_name.clone()) + .or_default() + .push(ServerData::WithBinaryStatus { + server_name, + binary_status, + server_id: Some(*server_id), + }); + } + None => servers_without_worktree.push(ServerData::WithBinaryStatus { + server_name, + binary_status, + server_id: None, + }), } - }); - if let Some(server_id) = matching_server_id { - buffer_servers.push(ServerData::WithBinaryStatus( - Some(server_id), + } + None => servers_without_worktree.push(ServerData::WithBinaryStatus { server_name, - status, - )); - } else { - other_servers.push(ServerData::WithBinaryStatus(None, server_name, status)); + binary_status, + server_id: None, + }), } } - buffer_servers.sort_by_key(|data| data.name().clone()); - other_servers.sort_by_key(|data| data.name().clone()); - - let mut other_servers_start_index = None; let mut new_lsp_items = - Vec::with_capacity(buffer_servers.len() + other_servers.len() + 1); - new_lsp_items.extend(buffer_servers.into_iter().map(ServerData::into_lsp_item)); - if !new_lsp_items.is_empty() { - other_servers_start_index = Some(new_lsp_items.len()); + Vec::with_capacity(servers_per_worktree.len() + servers_without_worktree.len() + 2); + for (worktree_name, worktree_servers) in servers_per_worktree { + if worktree_servers.is_empty() { + continue; + } + new_lsp_items.push(LspMenuItem::Header { + header: Some(worktree_name), + separator: false, + }); + new_lsp_items.extend(worktree_servers.into_iter().map(ServerData::into_lsp_item)); + } + if !servers_without_worktree.is_empty() { + new_lsp_items.push(LspMenuItem::Header { + header: Some(SharedString::from("Unknown worktree")), + separator: false, + }); + new_lsp_items.extend( + servers_without_worktree + .into_iter() + .map(ServerData::into_lsp_item), + ); } - new_lsp_items.extend(other_servers.into_iter().map(ServerData::into_lsp_item)); if !new_lsp_items.is_empty() { if can_stop_all { - new_lsp_items.push(LspItem::ToggleServersButton { restart: true }); - new_lsp_items.push(LspItem::ToggleServersButton { restart: false }); + new_lsp_items.push(LspMenuItem::ToggleServersButton { restart: true }); + new_lsp_items.push(LspMenuItem::ToggleServersButton { restart: false }); } else if can_restart_all { - new_lsp_items.push(LspItem::ToggleServersButton { restart: true }); + new_lsp_items.push(LspMenuItem::ToggleServersButton { restart: true }); } } state.items = new_lsp_items; - state.other_servers_start_index = other_servers_start_index; }); } @@ -841,10 +964,7 @@ impl StatusItemView for LspTool { impl Render for LspTool { fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement { - if !cx.is_staff() - || self.server_state.read(cx).language_servers.is_empty() - || self.lsp_menu.is_none() - { + if self.server_state.read(cx).language_servers.is_empty() || self.lsp_menu.is_none() { return div(); } @@ -852,12 +972,12 @@ impl Render for LspTool { let mut has_warnings = false; let mut has_other_notifications = false; let state = self.server_state.read(cx); - for server in state.language_servers.health_statuses.values() { - if let Some(binary_status) = &state.language_servers.binary_statuses.get(&server.name) { - has_errors |= matches!(binary_status.status, BinaryStatus::Failed { .. }); - has_other_notifications |= binary_status.message.is_some(); - } + for binary_status in state.language_servers.binary_statuses.values() { + has_errors |= matches!(binary_status.status, BinaryStatus::Failed { .. }); + has_other_notifications |= binary_status.message.is_some(); + } + for server in state.language_servers.health_statuses.values() { if let Some((message, health)) = &server.health { has_other_notifications |= message.is_some(); match health { From 589af59dfe6fe5f54e37c4b99f9aa944ff864f30 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner <bennet@zed.dev> Date: Mon, 21 Jul 2025 20:02:28 +0200 Subject: [PATCH 228/658] collab: Refresh the LLM token once the terms of service have been accepted (#34833) Release Notes: - N/A --- crates/collab/src/rpc.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 7a454e11cfced2fa7f9f1dc8c0263934830c7cad..924784109b1de0a56abca60c4866ab137d14e7c3 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -4167,6 +4167,13 @@ async fn accept_terms_of_service( response.send(proto::AcceptTermsOfServiceResponse { 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(()) } From da8bf9ad795312fb83eb030586c9e369852d1f38 Mon Sep 17 00:00:00 2001 From: Richard Feldman <oss@rtfeldman.com> Date: Mon, 21 Jul 2025 14:32:22 -0400 Subject: [PATCH 229/658] Auto-retry agent errors by default (#34842) Now we explicitly carve out exceptions for which HTTP responses we do *not* retry for, and retry at least once on all others. Release Notes: - The Agent panel now automatically retries failed requests under more circumstances. --- crates/agent/src/thread.rs | 75 ++++++++++++++++++++++++-------------- 1 file changed, 47 insertions(+), 28 deletions(-) diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 180cc88390eb21d96c5109fac5802f42e425f83a..e50763535a461bef6769b4f7c1aadeb2f219b904 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -51,7 +51,7 @@ use util::{ResultExt as _, debug_panic, post_inc}; use uuid::Uuid; use zed_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit}; -const MAX_RETRY_ATTEMPTS: u8 = 3; +const MAX_RETRY_ATTEMPTS: u8 = 4; const BASE_RETRY_DELAY: Duration = Duration::from_secs(5); #[derive(Debug, Clone)] @@ -2182,8 +2182,8 @@ impl Thread { // 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), try multiple times with exponential backoff. - // - If it's an issue that *might* be fixed by retrying (e.g. internal server error), just retry once. + // - 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, @@ -2211,8 +2211,8 @@ impl Thread { } StatusCode::INTERNAL_SERVER_ERROR => Some(RetryStrategy::Fixed { delay: retry_after.unwrap_or(BASE_RETRY_DELAY), - // Internal Server Error could be anything, so only retry once. - max_attempts: 1, + // 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"), @@ -2223,20 +2223,23 @@ impl Thread { max_attempts: MAX_RETRY_ATTEMPTS, }) } else { - None + Some(RetryStrategy::Fixed { + delay: retry_after.unwrap_or(BASE_RETRY_DELAY), + max_attempts: 2, + }) } } }, ApiInternalServerError { .. } => Some(RetryStrategy::Fixed { delay: BASE_RETRY_DELAY, - max_attempts: 1, + max_attempts: 3, }), ApiReadResponseError { .. } | HttpSend { .. } | DeserializeResponse { .. } | BadRequestFormat { .. } => Some(RetryStrategy::Fixed { delay: BASE_RETRY_DELAY, - max_attempts: 1, + max_attempts: 3, }), // Retrying these errors definitely shouldn't help. HttpResponseError { @@ -2244,24 +2247,31 @@ impl Thread { StatusCode::PAYLOAD_TOO_LARGE | StatusCode::FORBIDDEN | StatusCode::UNAUTHORIZED, .. } - | SerializeRequest { .. } + | AuthenticationError { .. } + | PermissionError { .. } => None, + // These errors might be transient, so retry them + SerializeRequest { .. } | BuildRequestBody { .. } | PromptTooLarge { .. } - | AuthenticationError { .. } - | PermissionError { .. } | ApiEndpointNotFound { .. } - | NoApiKey { .. } => None, + | NoApiKey { .. } => Some(RetryStrategy::Fixed { + delay: BASE_RETRY_DELAY, + max_attempts: 2, + }), // 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: 1, + max_attempts: 3, }) } // Conservatively assume that any other errors are non-retryable - HttpResponseError { .. } | Other(..) => None, + HttpResponseError { .. } | Other(..) => Some(RetryStrategy::Fixed { + delay: BASE_RETRY_DELAY, + max_attempts: 2, + }), } } @@ -4352,7 +4362,7 @@ fn main() {{ let retry_state = thread.retry_state.as_ref().unwrap(); assert_eq!(retry_state.attempt, 1, "Should be first retry attempt"); assert_eq!( - retry_state.max_attempts, 1, + retry_state.max_attempts, 3, "Should have correct max attempts" ); }); @@ -4368,8 +4378,9 @@ fn main() {{ if let MessageSegment::Text(text) = seg { text.contains("internal") && text.contains("Fake") - && text.contains("Retrying in") - && !text.contains("attempt") + && text.contains("Retrying") + && text.contains("attempt 1 of 3") + && text.contains("seconds") } else { false } @@ -4464,8 +4475,8 @@ fn main() {{ let retry_state = thread.retry_state.as_ref().unwrap(); assert_eq!(retry_state.attempt, 1, "Should be first retry attempt"); assert_eq!( - retry_state.max_attempts, 1, - "Internal server errors should only retry once" + retry_state.max_attempts, 3, + "Internal server errors should retry up to 3 times" ); }); @@ -4473,7 +4484,15 @@ fn main() {{ cx.executor().advance_clock(BASE_RETRY_DELAY); cx.run_until_parked(); - // Should have scheduled second retry - count retry messages + // Advance clock for second retry + cx.executor().advance_clock(BASE_RETRY_DELAY); + cx.run_until_parked(); + + // Advance clock for third retry + cx.executor().advance_clock(BASE_RETRY_DELAY); + cx.run_until_parked(); + + // Should have completed all retries - count retry messages let retry_count = thread.update(cx, |thread, _| { thread .messages @@ -4491,24 +4510,24 @@ fn main() {{ .count() }); assert_eq!( - retry_count, 1, - "Should have only one retry for internal server errors" + retry_count, 3, + "Should have 3 retries for internal server errors" ); - // For internal server errors, we only retry once and then give up - // Check that retry_state is cleared after the single retry + // For internal server errors, we retry 3 times and then give up + // Check that retry_state is cleared after all retries thread.read_with(cx, |thread, _| { assert!( thread.retry_state.is_none(), - "Retry state should be cleared after single retry" + "Retry state should be cleared after all retries" ); }); - // Verify total attempts (1 initial + 1 retry) + // Verify total attempts (1 initial + 3 retries) assert_eq!( *completion_count.lock(), - 2, - "Should have attempted once plus 1 retry" + 4, + "Should have attempted once plus 3 retries" ); } From 3e50d997ddfb5e0fa483c4010cf1b1f4de6fc93c Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 21 Jul 2025 20:44:45 +0200 Subject: [PATCH 230/658] agent: Fix double-lease panic when clicking on thread to jump (#34843) Closes #ISSUE Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/agent_ui/src/active_thread.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index 14e7cf05b51b8f302d58ee928c7c0bbde0d4fc31..bfed81f5b7e07baf7b2f4db489742ce0944cf538 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -3724,8 +3724,11 @@ pub(crate) fn open_context( AgentContextHandle::Thread(thread_context) => workspace.update(cx, |workspace, cx| { if let Some(panel) = workspace.panel::<AgentPanel>(cx) { - panel.update(cx, |panel, cx| { - panel.open_thread(thread_context.thread.clone(), window, cx); + let thread = thread_context.thread.clone(); + window.defer(cx, move |window, cx| { + panel.update(cx, |panel, cx| { + panel.open_thread(thread, window, cx); + }); }); } }), @@ -3733,8 +3736,11 @@ pub(crate) fn open_context( AgentContextHandle::TextThread(text_thread_context) => { workspace.update(cx, |workspace, cx| { if let Some(panel) = workspace.panel::<AgentPanel>(cx) { - panel.update(cx, |panel, cx| { - panel.open_prompt_editor(text_thread_context.context.clone(), window, cx) + let context = text_thread_context.context.clone(); + window.defer(cx, move |window, cx| { + panel.update(cx, |panel, cx| { + panel.open_prompt_editor(context, window, cx) + }); }); } }) From 6ea09beea8b5038c6101f0fed2715acb80faa735 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 21 Jul 2025 20:51:27 +0200 Subject: [PATCH 231/658] terminal: Handle spaces in cwds of remote terminals (#34844) Closes #34807 Release Notes: - Fixed "Open in terminal" action not working with paths that contain spaces in SSH projects. --- crates/project/src/terminals.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 3d62b4156b7b96caade454d5d05a3c02c44dae8c..8cfbdff31183cc6763455e4cbe5acf635440343e 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -662,7 +662,7 @@ pub fn wrap_for_ssh( format!("cd \"$HOME/{trimmed_path}\"; {env_changes} {to_run}") } else { - format!("cd {path}; {env_changes} {to_run}") + format!("cd \"{path}\"; {env_changes} {to_run}") } } else { format!("cd; {env_changes} {to_run}") From 241acbe4be9dff7320a10817faafb9fd2518049d Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Mon, 21 Jul 2025 15:02:02 -0400 Subject: [PATCH 232/658] Stop onboarding page from showing up instead of welcome page (#34845) This is from PR #34723 where I was working on developing the onboarding page, but I forgot to switch the first page back to our current version. Release Notes: - N/A --- crates/zed/src/main.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 9d85923ca2f8e837c2e1f280c145947e43404594..a9d3d63381dfbb24bbc9e7882e1e035887dccecb 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -24,7 +24,6 @@ use reqwest_client::ReqwestClient; use assets::Assets; use node_runtime::{NodeBinaryOptions, NodeRuntime}; -use onboarding::show_onboarding_view; use parking_lot::Mutex; use project::project_settings::ProjectSettings; use recent_projects::{SshSettings, open_ssh_project}; @@ -1041,10 +1040,6 @@ async fn restore_or_create_workspace(app_state: Arc<AppState>, cx: &mut AsyncApp ); } } - } else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) { - let state = app_state.clone(); - cx.update(|cx| show_onboarding_view(state, cx))?.await?; - // cx.update(|cx| show_welcome_view(app_state, cx))?.await?; } else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) { cx.update(|cx| show_welcome_view(app_state, cx))?.await?; } else { From 1a1715766f0f5af9d1ae5df23d58cb1763bd886d Mon Sep 17 00:00:00 2001 From: Ben Kunkle <ben@zed.dev> Date: Mon, 21 Jul 2025 14:16:47 -0500 Subject: [PATCH 233/658] Fix enter to select model in agent panel (#34846) Broken by #34664 Release Notes: - N/A *or* Added/Fixed/Improved ... --- assets/keymaps/default-linux.json | 4 ++-- assets/keymaps/default-macos.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 6aba27fec8fee9ed601e6bae43d575a5d050f95b..9585bcdecd297ba7ef9c14d237804fd53d45849e 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -277,7 +277,7 @@ } }, { - "context": "MessageEditor > Editor && !use_modifier_to_send", + "context": "MessageEditor && !Picker > Editor && !use_modifier_to_send", "bindings": { "enter": "agent::Chat", "ctrl-enter": "agent::ChatWithFollow", @@ -288,7 +288,7 @@ } }, { - "context": "MessageEditor > Editor && use_modifier_to_send", + "context": "MessageEditor && !Picker > Editor && use_modifier_to_send", "bindings": { "ctrl-enter": "agent::Chat", "enter": "editor::Newline", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index ba903c07821fec6fad805e3a4b1cac81831216ed..9a72bc912cfbb11ecaf7fee3d71bbd0d5fd8d602 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -318,7 +318,7 @@ } }, { - "context": "MessageEditor > Editor && !use_modifier_to_send", + "context": "MessageEditor && !Picker > Editor && !use_modifier_to_send", "use_key_equivalents": true, "bindings": { "enter": "agent::Chat", @@ -330,7 +330,7 @@ } }, { - "context": "MessageEditor > Editor && use_modifier_to_send", + "context": "MessageEditor && !Picker > Editor && use_modifier_to_send", "use_key_equivalents": true, "bindings": { "cmd-enter": "agent::Chat", From 8eca7f32e2041c648c3c9b0ac1e8b686fec4e5df Mon Sep 17 00:00:00 2001 From: Peter Tripp <peter@zed.dev> Date: Mon, 21 Jul 2025 16:09:02 -0400 Subject: [PATCH 234/658] Fix for vim bindings in Pickers on Linux (#34840) Closes: https://github.com/zed-industries/zed/issues/34780 Also relocated undo/redo selection in the keymap (no-op) as they are from Sublime, not VSCode. Release Notes: - vim: Fixed an issue so `ctrl-w` / `ctrl-h` and `ctrl-u` work in pickers on Linux when Vim mode is enabled. --- assets/keymaps/default-linux.json | 4 ++-- assets/keymaps/default-macos.json | 4 ++-- assets/keymaps/vim.json | 8 ++++++++ 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 9585bcdecd297ba7ef9c14d237804fd53d45849e..377a26242b995a3db4d67a9c9dea082c9c11b93d 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -484,8 +484,6 @@ "ctrl-k ctrl-shift-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch "ctrl-k ctrl-i": "editor::Hover", "ctrl-/": ["editor::ToggleComments", { "advance_downwards": false }], - "ctrl-u": "editor::UndoSelection", - "ctrl-shift-u": "editor::RedoSelection", "f8": ["editor::GoToDiagnostic", { "severity": { "min": "hint", "max": "error" } }], "shift-f8": ["editor::GoToPreviousDiagnostic", { "severity": { "min": "hint", "max": "error" } }], "f2": "editor::Rename", @@ -663,6 +661,8 @@ { "context": "Editor", "bindings": { + "ctrl-u": "editor::UndoSelection", + "ctrl-shift-u": "editor::RedoSelection", "ctrl-shift-j": "editor::JoinLines", "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart", "ctrl-alt-h": "editor::DeleteToPreviousSubwordStart", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 9a72bc912cfbb11ecaf7fee3d71bbd0d5fd8d602..712d73b8ec5d6dfd3aa6abb5c1420f3e81c2ee0a 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -538,8 +538,6 @@ "cmd-k ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch "cmd-k cmd-i": "editor::Hover", "cmd-/": ["editor::ToggleComments", { "advance_downwards": false }], - "cmd-u": "editor::UndoSelection", - "cmd-shift-u": "editor::RedoSelection", "f8": ["editor::GoToDiagnostic", { "severity": { "min": "hint", "max": "error" } }], "shift-f8": ["editor::GoToPreviousDiagnostic", { "severity": { "min": "hint", "max": "error" } }], "f2": "editor::Rename", @@ -726,6 +724,8 @@ "context": "Editor", "use_key_equivalents": true, "bindings": { + "cmd-u": "editor::UndoSelection", + "cmd-shift-u": "editor::RedoSelection", "ctrl-j": "editor::JoinLines", "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart", "ctrl-alt-h": "editor::DeleteToPreviousSubwordStart", diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 2ef282c21edc87fe5f062f8083296b43cc0a571d..04e6b0bcd40720067dea4b8ecaf2bdb72adcdc2d 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -858,6 +858,14 @@ "shift-n": null } }, + { + "context": "Picker > Editor", + "bindings": { + "ctrl-h": "editor::Backspace", + "ctrl-u": "editor::DeleteToBeginningOfLine", + "ctrl-w": "editor::DeleteToPreviousWordStart" + } + }, { "context": "GitCommit > Editor && VimControl && vim_mode == normal", "bindings": { From 5b3e37181240d8ed1ef8e04aa0593c7ca5f46a6c Mon Sep 17 00:00:00 2001 From: Finn Evers <finn@zed.dev> Date: Mon, 21 Jul 2025 22:24:33 +0200 Subject: [PATCH 235/658] gpui: Round `scroll_max` to two decimal places (#34832) Follow up to #31836 After enabling rounding in the Taffy layout engine, we frequently run into cases where the bounds produced by Taffy and ours slightly differ after 5 or more decimal places. This leads to cases where containers become scrollable for less than 0.0000x Pixels. In case this happens for e.g. hover popovers, we render a scrollbar due to the container being technically scrollable, even though the scroll amount here will in practice never be visible. This change fixes this by rounding the `scroll_max` by which we clamp the current scroll position to two decimal places. We don't benefit from the additional floating point precision here at all and it stops such containers from becoming scrollable altogether. Furthermore, we now store the `scroll_max` instead of the `padded_content_size` as the former gives a much better idea on whether the corresponding container is scrollable or not. | `main` | After these changes | | -- | -- | | <img width="610" height="316" alt="main" src="https://github.com/user-attachments/assets/ffcc0322-6d6e-4f79-a916-bd3c57fe4211" /> | <img width="610" height="316" alt="scroll_max_rounded" src="https://github.com/user-attachments/assets/5fe530f5-2e21-4aaa-81f4-e5c53ab73e4f" /> | Release Notes: - Fixed an issue where scrollbars would appear in containers where no scrolling was possible. --- crates/gpui/src/elements/div.rs | 29 ++++++++++++----- crates/gpui/src/elements/list.rs | 6 ++-- .../terminal_view/src/terminal_scrollbar.rs | 11 +++++-- crates/ui/src/components/scrollbar.rs | 31 ++++++++++--------- crates/workspace/src/pane.rs | 11 +++---- 5 files changed, 55 insertions(+), 33 deletions(-) diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index ed1666c53060dfdf3ed4c10a85a730d69f87986d..4655c92409d3f21fd8a2a919154368a56da9567e 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -1664,6 +1664,11 @@ impl Interactivity { window: &mut Window, _cx: &mut App, ) -> Point<Pixels> { + fn round_to_two_decimals(pixels: Pixels) -> Pixels { + const ROUNDING_FACTOR: f32 = 100.0; + (pixels * ROUNDING_FACTOR).round() / ROUNDING_FACTOR + } + if let Some(scroll_offset) = self.scroll_offset.as_ref() { let mut scroll_to_bottom = false; let mut tracked_scroll_handle = self @@ -1678,8 +1683,16 @@ impl Interactivity { let rem_size = window.rem_size(); let padding = style.padding.to_pixels(bounds.size.into(), rem_size); let padding_size = size(padding.left + padding.right, padding.top + padding.bottom); + // The floating point values produced by Taffy and ours often vary + // slightly after ~5 decimal places. This can lead to cases where after + // subtracting these, the container becomes scrollable for less than + // 0.00000x pixels. As we generally don't benefit from a precision that + // high for the maximum scroll, we round the scroll max to 2 decimal + // places here. let padded_content_size = self.content_size + padding_size; - let scroll_max = (padded_content_size - bounds.size).max(&Size::default()); + let scroll_max = (padded_content_size - bounds.size) + .map(round_to_two_decimals) + .max(&Default::default()); // Clamp scroll offset in case scroll max is smaller now (e.g., if children // were removed or the bounds became larger). let mut scroll_offset = scroll_offset.borrow_mut(); @@ -1692,7 +1705,7 @@ impl Interactivity { } if let Some(mut scroll_handle_state) = tracked_scroll_handle { - scroll_handle_state.padded_content_size = padded_content_size; + scroll_handle_state.max_offset = scroll_max; } *scroll_offset @@ -2936,7 +2949,7 @@ impl ScrollAnchor { struct ScrollHandleState { offset: Rc<RefCell<Point<Pixels>>>, bounds: Bounds<Pixels>, - padded_content_size: Size<Pixels>, + max_offset: Size<Pixels>, child_bounds: Vec<Bounds<Pixels>>, scroll_to_bottom: bool, overflow: Point<Overflow>, @@ -2965,6 +2978,11 @@ impl ScrollHandle { *self.0.borrow().offset.borrow() } + /// Get the maximum scroll offset. + pub fn max_offset(&self) -> Size<Pixels> { + self.0.borrow().max_offset + } + /// Get the top child that's scrolled into view. pub fn top_item(&self) -> usize { let state = self.0.borrow(); @@ -2999,11 +3017,6 @@ impl ScrollHandle { self.0.borrow().child_bounds.get(ix).cloned() } - /// Get the size of the content with padding of the container. - pub fn padded_content_size(&self) -> Size<Pixels> { - self.0.borrow().padded_content_size - } - /// scroll_to_item scrolls the minimal amount to ensure that the child is /// fully visible pub fn scroll_to_item(&self, ix: usize) { diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 35a3b622b2e53028218ce0c42ab0a5ad7f1a4ec3..f24d38794f7611ee173dbe913ed51a53bdef73ed 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -411,9 +411,9 @@ impl ListState { self.0.borrow_mut().set_offset_from_scrollbar(point); } - /// Returns the size of items we have measured. + /// Returns the maximum scroll offset according to the items we have measured. /// This value remains constant while dragging to prevent the scrollbar from moving away unexpectedly. - pub fn content_size_for_scrollbar(&self) -> Size<Pixels> { + pub fn max_offset_for_scrollbar(&self) -> Size<Pixels> { let state = self.0.borrow(); let bounds = state.last_layout_bounds.unwrap_or_default(); @@ -421,7 +421,7 @@ impl ListState { .scrollbar_drag_start_height .unwrap_or_else(|| state.items.summary().height); - Size::new(bounds.size.width, height) + Size::new(Pixels::ZERO, Pixels::ZERO.max(height - bounds.size.height)) } /// Returns the current scroll offset adjusted for the scrollbar diff --git a/crates/terminal_view/src/terminal_scrollbar.rs b/crates/terminal_view/src/terminal_scrollbar.rs index 18e135be2eef3b8e7ec71c070f2a60a46792a271..c8565a42bee0858e0928e557b9fae590dba319fb 100644 --- a/crates/terminal_view/src/terminal_scrollbar.rs +++ b/crates/terminal_view/src/terminal_scrollbar.rs @@ -46,9 +46,16 @@ impl TerminalScrollHandle { } impl ScrollableHandle for TerminalScrollHandle { - fn content_size(&self) -> Size<Pixels> { + fn max_offset(&self) -> Size<Pixels> { let state = self.state.borrow(); - size(Pixels::ZERO, state.total_lines as f32 * state.line_height) + size( + Pixels::ZERO, + state + .total_lines + .checked_sub(state.viewport_lines) + .unwrap_or(0) as f32 + * state.line_height, + ) } fn offset(&self) -> Point<Pixels> { diff --git a/crates/ui/src/components/scrollbar.rs b/crates/ui/src/components/scrollbar.rs index 2a8c4885acff5f3b5e75c7e2f6ae62335f9b8ebe..17ab2e788f3d7ef37fe99136b99397f200512124 100644 --- a/crates/ui/src/components/scrollbar.rs +++ b/crates/ui/src/components/scrollbar.rs @@ -29,8 +29,8 @@ impl ThumbState { } impl ScrollableHandle for UniformListScrollHandle { - fn content_size(&self) -> Size<Pixels> { - self.0.borrow().base_handle.content_size() + fn max_offset(&self) -> Size<Pixels> { + self.0.borrow().base_handle.max_offset() } fn set_offset(&self, point: Point<Pixels>) { @@ -47,8 +47,8 @@ impl ScrollableHandle for UniformListScrollHandle { } impl ScrollableHandle for ListState { - fn content_size(&self) -> Size<Pixels> { - self.content_size_for_scrollbar() + fn max_offset(&self) -> Size<Pixels> { + self.max_offset_for_scrollbar() } fn set_offset(&self, point: Point<Pixels>) { @@ -73,8 +73,8 @@ impl ScrollableHandle for ListState { } impl ScrollableHandle for ScrollHandle { - fn content_size(&self) -> Size<Pixels> { - self.padded_content_size() + fn max_offset(&self) -> Size<Pixels> { + self.max_offset() } fn set_offset(&self, point: Point<Pixels>) { @@ -91,7 +91,10 @@ impl ScrollableHandle for ScrollHandle { } pub trait ScrollableHandle: Any + Debug { - fn content_size(&self) -> Size<Pixels>; + fn content_size(&self) -> Size<Pixels> { + self.viewport().size + self.max_offset() + } + fn max_offset(&self) -> Size<Pixels>; fn set_offset(&self, point: Point<Pixels>); fn offset(&self) -> Point<Pixels>; fn viewport(&self) -> Bounds<Pixels>; @@ -149,17 +152,17 @@ impl ScrollbarState { fn thumb_range(&self, axis: ScrollbarAxis) -> Option<Range<f32>> { const MINIMUM_THUMB_SIZE: Pixels = px(25.); - let content_size = self.scroll_handle.content_size().along(axis); + let max_offset = self.scroll_handle.max_offset().along(axis); let viewport_size = self.scroll_handle.viewport().size.along(axis); - if content_size.is_zero() || viewport_size.is_zero() || content_size <= viewport_size { + if max_offset.is_zero() || viewport_size.is_zero() { return None; } + let content_size = viewport_size + max_offset; let visible_percentage = viewport_size / content_size; let thumb_size = MINIMUM_THUMB_SIZE.max(viewport_size * visible_percentage); if thumb_size > viewport_size { return None; } - let max_offset = content_size - viewport_size; let current_offset = self .scroll_handle .offset() @@ -307,7 +310,7 @@ impl Element for Scrollbar { let compute_click_offset = move |event_position: Point<Pixels>, - item_size: Size<Pixels>, + max_offset: Size<Pixels>, event_type: ScrollbarMouseEvent| { let viewport_size = padded_bounds.size.along(axis); @@ -323,7 +326,7 @@ impl Element for Scrollbar { - thumb_offset) .clamp(px(0.), viewport_size - thumb_size); - let max_offset = (item_size.along(axis) - viewport_size).max(px(0.)); + let max_offset = max_offset.along(axis); let percentage = if viewport_size > thumb_size { thumb_start / (viewport_size - thumb_size) } else { @@ -347,7 +350,7 @@ impl Element for Scrollbar { } else { let click_offset = compute_click_offset( event.position, - scroll.content_size(), + scroll.max_offset(), ScrollbarMouseEvent::GutterClick, ); scroll.set_offset(scroll.offset().apply_along(axis, |_| click_offset)); @@ -373,7 +376,7 @@ impl Element for Scrollbar { ThumbState::Dragging(drag_state) if event.dragging() => { let drag_offset = compute_click_offset( event.position, - scroll.content_size(), + scroll.max_offset(), ScrollbarMouseEvent::ThumbDrag(drag_state), ); scroll.set_offset(scroll.offset().apply_along(axis, |_| drag_offset)); diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 7cc10c27f714bec6480c44cb241d8012eda138d6..e57b103c61988c4a48f2078cdeb600cc3bd34978 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -18,7 +18,7 @@ use futures::{StreamExt, stream::FuturesUnordered}; use gpui::{ Action, AnyElement, App, AsyncWindowContext, ClickEvent, ClipboardItem, Context, Corner, Div, DragMoveEvent, Entity, EntityId, EventEmitter, ExternalPaths, FocusHandle, FocusOutEvent, - Focusable, KeyContext, MouseButton, MouseDownEvent, NavigationDirection, Pixels, Point, + Focusable, IsZero, KeyContext, MouseButton, MouseDownEvent, NavigationDirection, Pixels, Point, PromptLevel, Render, ScrollHandle, Subscription, Task, WeakEntity, WeakFocusHandle, Window, actions, anchored, deferred, prelude::*, }; @@ -46,8 +46,8 @@ use theme::ThemeSettings; use ui::{ ButtonSize, Color, ContextMenu, ContextMenuEntry, ContextMenuItem, DecoratedIcon, IconButton, IconButtonShape, IconDecoration, IconDecorationKind, IconName, IconSize, Indicator, Label, - PopoverMenu, PopoverMenuHandle, ScrollableHandle, Tab, TabBar, TabPosition, Tooltip, - prelude::*, right_click_menu, + PopoverMenu, PopoverMenuHandle, Tab, TabBar, TabPosition, Tooltip, prelude::*, + right_click_menu, }; use util::{ResultExt, debug_panic, maybe, truncate_and_remove_front}; @@ -2865,10 +2865,9 @@ impl Pane { } }) .children(pinned_tabs.len().ne(&0).then(|| { - let content_width = self.tab_bar_scroll_handle.content_size().width; - let viewport_width = self.tab_bar_scroll_handle.viewport().size.width; + let max_scroll = self.tab_bar_scroll_handle.max_offset().width; // We need to check both because offset returns delta values even when the scroll handle is not scrollable - let is_scrollable = content_width > viewport_width; + let is_scrollable = !max_scroll.is_zero(); let is_scrolled = self.tab_bar_scroll_handle.offset().x < px(0.); let has_active_unpinned_tab = self.active_item_index >= self.pinned_tab_count; h_flex() From 722a05bc21aaa451908e208c674a45f0b1139b1c Mon Sep 17 00:00:00 2001 From: Agus Zubiaga <agus@zed.dev> Date: Mon, 21 Jul 2025 17:33:59 -0300 Subject: [PATCH 236/658] Wire up stop button in claude threads (#34839) Release Notes: - N/A --------- Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com> --- Cargo.lock | 3 + crates/agent_servers/Cargo.toml | 5 + crates/agent_servers/src/claude.rs | 189 +++++++++++++++++++++-------- 3 files changed, 145 insertions(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4537d440ccd97b4934164eee3f0bdf0230edd4ec..ad6c40bcf20ccc8cf770313d19deb831d864be3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -150,7 +150,9 @@ dependencies = [ "indoc", "itertools 0.14.0", "language", + "libc", "log", + "nix 0.29.0", "paths", "project", "schemars", @@ -162,6 +164,7 @@ dependencies = [ "tempfile", "ui", "util", + "uuid", "watch", "which 6.0.3", "workspace-hack", diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml index f3df25f70914e95c63265e4c711ca76fd66050b1..4714245b94fd9b519cfe1987817d51ff6ecbc7fd 100644 --- a/crates/agent_servers/Cargo.toml +++ b/crates/agent_servers/Cargo.toml @@ -37,10 +37,15 @@ strum.workspace = true tempfile.workspace = true ui.workspace = true util.workspace = true +uuid.workspace = true watch.workspace = true which.workspace = true workspace-hack.workspace = true +[target.'cfg(unix)'.dependencies] +libc.workspace = true +nix.workspace = true + [dev-dependencies] env_logger.workspace = true language.workspace = true diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 8b3d93a122d07448ddbfb9daf2dfc9226fb11545..835efbd6552423e7e5bcd1d321ca193581e5ab0a 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -4,10 +4,13 @@ mod tools; use collections::HashMap; use project::Project; use settings::SettingsStore; +use smol::process::Child; use std::cell::RefCell; use std::fmt::Display; use std::path::Path; +use std::pin::pin; use std::rc::Rc; +use uuid::Uuid; use agentic_coding_protocol::{ self as acp, AnyAgentRequest, AnyAgentResult, Client, ProtocolVersion, @@ -16,7 +19,7 @@ use agentic_coding_protocol::{ use anyhow::{Result, anyhow}; use futures::channel::oneshot; use futures::future::LocalBoxFuture; -use futures::{AsyncBufReadExt, AsyncWriteExt}; +use futures::{AsyncBufReadExt, AsyncWriteExt, SinkExt}; use futures::{ AsyncRead, AsyncWrite, FutureExt, StreamExt, channel::mpsc::{self, UnboundedReceiver, UnboundedSender}, @@ -69,13 +72,12 @@ impl AgentServer for ClaudeCode { let (mut delegate_tx, delegate_rx) = watch::channel(None); let tool_id_map = Rc::new(RefCell::new(HashMap::default())); - let permission_mcp_server = - ClaudeMcpServer::new(delegate_rx, tool_id_map.clone(), cx).await?; + let mcp_server = ClaudeMcpServer::new(delegate_rx, tool_id_map.clone(), cx).await?; let mut mcp_servers = HashMap::default(); mcp_servers.insert( mcp_server::SERVER_NAME.to_string(), - permission_mcp_server.server_config()?, + mcp_server.server_config()?, ); let mcp_config = McpConfig { mcp_servers }; @@ -98,50 +100,58 @@ impl AgentServer for ClaudeCode { anyhow::bail!("Failed to find claude binary"); }; - let mut child = util::command::new_smol_command(&command.path) - .args( - [ - "--input-format", - "stream-json", - "--output-format", - "stream-json", - "--print", - "--verbose", - "--mcp-config", - mcp_config_path.to_string_lossy().as_ref(), - "--permission-prompt-tool", - &format!( - "mcp__{}__{}", - mcp_server::SERVER_NAME, - mcp_server::PERMISSION_TOOL - ), - "--allowedTools", - "mcp__zed__Read,mcp__zed__Edit", - "--disallowedTools", - "Read,Edit", - ] - .into_iter() - .chain(command.args.iter().map(|arg| arg.as_str())), - ) - .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 (incoming_message_tx, mut incoming_message_rx) = mpsc::unbounded(); let (outgoing_tx, outgoing_rx) = mpsc::unbounded(); + let (cancel_tx, mut cancel_rx) = mpsc::unbounded::<oneshot::Sender<Result<()>>>(); + + let session_id = Uuid::new_v4(); + + log::trace!("Starting session with id: {}", session_id); - let io_task = - ClaudeAgentConnection::handle_io(outgoing_rx, incoming_message_tx, stdin, stdout); cx.background_spawn(async move { - io_task.await.log_err(); + let mut outgoing_rx = Some(outgoing_rx); + let mut mode = ClaudeSessionMode::Start; + + loop { + let mut child = + spawn_claude(&command, mode, session_id, &mcp_config_path, &root_dir) + .await?; + mode = ClaudeSessionMode::Resume; + + let pid = child.id(); + log::trace!("Spawned (pid: {})", pid); + + let mut io_fut = pin!( + ClaudeAgentConnection::handle_io( + outgoing_rx.take().unwrap(), + incoming_message_tx.clone(), + child.stdin.take().unwrap(), + child.stdout.take().unwrap(), + ) + .fuse() + ); + + select_biased! { + done_tx = cancel_rx.next() => { + if let Some(done_tx) = done_tx { + log::trace!("Interrupted (pid: {})", pid); + let result = send_interrupt(pid as i32); + outgoing_rx.replace(io_fut.await?); + done_tx.send(result).log_err(); + continue; + } + } + result = io_fut => { + result?; + } + } + + log::trace!("Stopped (pid: {})", pid); + break; + } + drop(mcp_config_path); - drop(child); + anyhow::Ok(()) }) .detach(); @@ -171,17 +181,32 @@ impl AgentServer for ClaudeCode { delegate, outgoing_tx, end_turn_tx, + cancel_tx, + session_id, _handler_task: handler_task, _mcp_server: None, }; - connection._mcp_server = Some(permission_mcp_server); + connection._mcp_server = Some(mcp_server); acp_thread::AcpThread::new(connection, title, None, project.clone(), cx) }) }) } } +#[cfg(unix)] +fn send_interrupt(pid: libc::pid_t) -> anyhow::Result<()> { + let pid = nix::unistd::Pid::from_raw(pid); + + nix::sys::signal::kill(pid, nix::sys::signal::SIGINT) + .map_err(|e| anyhow!("Failed to interrupt process: {}", e)) +} + +#[cfg(windows)] +fn send_interrupt(_pid: i32) -> anyhow::Result<()> { + panic!("Cancel not implemented on Windows") +} + impl AgentConnection for ClaudeAgentConnection { /// Send a request to the agent and wait for a response. fn request_any( @@ -191,6 +216,8 @@ impl AgentConnection for ClaudeAgentConnection { let delegate = self.delegate.clone(); let end_turn_tx = self.end_turn_tx.clone(); let outgoing_tx = self.outgoing_tx.clone(); + let mut cancel_tx = self.cancel_tx.clone(); + let session_id = self.session_id; async move { match params { // todo: consider sending an empty request so we get the init response? @@ -229,26 +256,83 @@ impl AgentConnection for ClaudeAgentConnection { stop_sequence: None, usage: None, }, - session_id: None, + session_id: Some(session_id), })?; rx.await??; Ok(AnyAgentResult::SendUserMessageResponse( acp::SendUserMessageResponse, )) } - AnyAgentRequest::CancelSendMessageParams(_) => Ok( - AnyAgentResult::CancelSendMessageResponse(acp::CancelSendMessageResponse), - ), + AnyAgentRequest::CancelSendMessageParams(_) => { + let (done_tx, done_rx) = oneshot::channel(); + cancel_tx.send(done_tx).await?; + done_rx.await??; + + Ok(AnyAgentResult::CancelSendMessageResponse( + acp::CancelSendMessageResponse, + )) + } } } .boxed_local() } } +#[derive(Clone, Copy)] +enum ClaudeSessionMode { + Start, + Resume, +} + +async fn spawn_claude( + command: &AgentServerCommand, + mode: ClaudeSessionMode, + session_id: Uuid, + mcp_config_path: &Path, + root_dir: &Path, +) -> Result<Child> { + let child = util::command::new_smol_command(&command.path) + .args([ + "--input-format", + "stream-json", + "--output-format", + "stream-json", + "--print", + "--verbose", + "--mcp-config", + mcp_config_path.to_string_lossy().as_ref(), + "--permission-prompt-tool", + &format!( + "mcp__{}__{}", + mcp_server::SERVER_NAME, + mcp_server::PERMISSION_TOOL + ), + "--allowedTools", + "mcp__zed__Read,mcp__zed__Edit", + "--disallowedTools", + "Read,Edit", + ]) + .args(match mode { + ClaudeSessionMode::Start => ["--session-id".to_string(), session_id.to_string()], + ClaudeSessionMode::Resume => ["--resume".to_string(), session_id.to_string()], + }) + .args(command.args.iter().map(|arg| arg.as_str())) + .current_dir(root_dir) + .stdin(std::process::Stdio::piped()) + .stdout(std::process::Stdio::piped()) + .stderr(std::process::Stdio::inherit()) + .kill_on_drop(true) + .spawn()?; + + Ok(child) +} + struct ClaudeAgentConnection { delegate: AcpClientDelegate, + session_id: Uuid, outgoing_tx: UnboundedSender<SdkMessage>, end_turn_tx: Rc<RefCell<Option<oneshot::Sender<Result<()>>>>>, + cancel_tx: UnboundedSender<oneshot::Sender<Result<()>>>, _mcp_server: Option<ClaudeMcpServer>, _handler_task: Task<()>, } @@ -350,7 +434,7 @@ impl ClaudeAgentConnection { incoming_tx: UnboundedSender<SdkMessage>, mut outgoing_bytes: impl Unpin + AsyncWrite, incoming_bytes: impl Unpin + AsyncRead, - ) -> Result<()> { + ) -> Result<UnboundedReceiver<SdkMessage>> { let mut output_reader = BufReader::new(incoming_bytes); let mut outgoing_line = Vec::new(); let mut incoming_line = String::new(); @@ -384,7 +468,8 @@ impl ClaudeAgentConnection { } } } - Ok(()) + + Ok(outgoing_rx) } } @@ -507,14 +592,14 @@ enum SdkMessage { Assistant { message: Message, // from Anthropic SDK #[serde(skip_serializing_if = "Option::is_none")] - session_id: Option<String>, + session_id: Option<Uuid>, }, // A user message User { message: Message, // from Anthropic SDK #[serde(skip_serializing_if = "Option::is_none")] - session_id: Option<String>, + session_id: Option<Uuid>, }, // Emitted as the last message in a conversation From 19ab1eb79258759e3ee0fef7017d259345b912e6 Mon Sep 17 00:00:00 2001 From: Sergei Surovtsev <97428129+stillonearth@users.noreply.github.com> Date: Tue, 22 Jul 2025 02:16:44 +0300 Subject: [PATCH 237/658] Fix an issue where xkb defined hotkeys for arrows would not work (#34823) Addresses https://github.com/zed-industries/zed/pull/34053#issuecomment-3096447601 where custom-defined arrows would stop working in Zed. How to reproduce: 1. Define custom keyboard layout ```bash cd /usr/share/X11/xkb/symbols/ sudo nano mykbd ``` ``` default partial alphanumeric_keys xkb_symbols "custom" { name[Group1]= "Custom Layout"; key <AD01> { [ q, Q, Escape, Escape ] }; key <AD02> { [ w, W, Home, Home ] }; key <AD03> { [ e, E, Up, Up ] }; key <AD04> { [ r, R, End, End ] }; key <AD05> { [ t, T, Tab, Tab ] }; key <AC01> { [ a, A, Return, Return ] }; key <AC02> { [ s, S, Left, Left ] }; key <AC03> { [ d, D, Down, Down ] }; key <AC04> { [ f, F, Right, Right ] }; key <AC05> { [ g, G, BackSpace, BackSpace ] }; // include a base layout to inherit the rest include "us(basic)" }; ``` 2. Activate custom layout with win-key as AltGr ```bash setxkbmap mykbd -variant custom -option lv3:win_switch ``` 3. Now Win-S should produce left arrow, Win-F right arrow 4. Test whether it works in Zed Release Notes: - linux: xkb-defined hotkeys for arrow keys should behave as expected. --------- Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com> --- crates/gpui/src/platform/linux/platform.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index a52841e1afe4b0a396c68ef72587777edd5eb14e..d65118e994e4cd488c23436bea8d3666495881ec 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -828,6 +828,13 @@ impl crate::Keystroke { Keysym::Delete => "delete".to_owned(), Keysym::Escape => "escape".to_owned(), + Keysym::Left => "left".to_owned(), + Keysym::Right => "right".to_owned(), + Keysym::Up => "up".to_owned(), + Keysym::Down => "down".to_owned(), + Keysym::Home => "home".to_owned(), + Keysym::End => "end".to_owned(), + _ => { let name = xkb::keysym_get_name(key_sym).to_lowercase(); if key_sym.is_keypad_key() { From 8515487bbce339414940dd2deeac8e962f14118d Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 21 Jul 2025 20:39:29 -0300 Subject: [PATCH 238/658] agent: Add new thread start buttons to the empty state (#34829) Release Notes: - N/A --- assets/icons/ai_claude.svg | 4 +- assets/icons/ai_gemini.svg | 4 +- assets/icons/new_from_summary.svg | 7 + assets/icons/new_text_thread.svg | 7 + assets/icons/new_thread.svg | 3 + crates/agent_ui/src/agent_panel.rs | 230 +++++++++++++++++--- crates/agent_ui/src/ui.rs | 2 + crates/agent_ui/src/ui/new_thread_button.rs | 75 +++++++ crates/icons/src/icons.rs | 3 + 9 files changed, 297 insertions(+), 38 deletions(-) create mode 100644 assets/icons/new_from_summary.svg create mode 100644 assets/icons/new_text_thread.svg create mode 100644 assets/icons/new_thread.svg create mode 100644 crates/agent_ui/src/ui/new_thread_button.rs diff --git a/assets/icons/ai_claude.svg b/assets/icons/ai_claude.svg index 423a963eba9b9492a9807082922a4c072786d843..a3e3e1f4cd7bcc4924ed3f8164c35c5c8e2a9c4c 100644 --- a/assets/icons/ai_claude.svg +++ b/assets/icons/ai_claude.svg @@ -1,3 +1,3 @@ -<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M7.8481 26.5925L15.7165 22.1806L15.8481 21.7961L15.7165 21.5836H15.3316L14.0152 21.5027L9.51899 21.3812L5.62025 21.2193L1.84304 21.0169L0.891139 20.8146L0 19.6408L0.0911392 19.0539L0.891139 18.5176L2.03544 18.6188L4.56709 18.7908L8.36456 19.0539L11.119 19.2158L15.2 19.6408H15.8481L15.9392 19.3777L15.7165 19.2158L15.5443 19.0539L11.6152 16.3926L7.36203 13.5796L5.13418 11.9605L3.92911 11.1409L3.32152 10.3719L3.05823 8.69213L4.1519 7.48798L5.62025 7.58917L5.99494 7.69036L7.48354 8.8338L10.6633 11.2927L14.8152 14.3486L15.4228 14.8545L15.6658 14.6825L15.6962 14.5611L15.4228 14.1057L13.1646 10.0278L10.7544 5.87908L9.68101 4.15887L9.39747 3.12674C9.2962 2.70175 9.22532 2.34758 9.22532 1.91247L10.4709 0.222616L11.1595 0L12.8203 0.222616L13.519 0.82975L14.5519 3.18745L16.2228 6.90109L18.8152 11.9504L19.5747 13.448L19.9797 14.8343L20.1316 15.2593H20.3949V15.0164L20.6076 12.173L21.0025 8.68201L21.3873 4.18922L21.519 2.92436L22.1468 1.40653L23.3924 0.586896L24.3646 1.05237L25.1646 2.1958L25.0532 2.93448L24.5772 6.02074L23.6456 10.8576L23.038 14.0956H23.3924L23.7975 13.6909L25.438 11.5153L28.1924 8.07488L29.4076 6.70883L30.8253 5.20111L31.7367 4.48267H33.4582L34.7241 6.36479L34.157 8.30761L32.3848 10.554L30.9165 12.4564L28.8101 15.2897L27.4937 17.5563L27.6152 17.7384L27.9291 17.7081L32.6886 16.6962L35.2608 16.2307L38.3291 15.7045L39.7165 16.3521L39.8684 17.0099L39.3215 18.3557L36.0405 19.1652L32.1924 19.9342L26.4608 21.2902L26.3899 21.3408L26.4709 21.4419L29.0532 21.6848L30.157 21.7455H32.8608L37.8937 22.1199L39.2101 22.9901L40 24.0526L39.8684 24.8621L37.843 25.8943L35.1089 25.2466L28.7291 23.7288L26.5418 23.1824H26.238V23.3645L28.0608 25.1455L31.4025 28.1609L35.5848 32.0465L35.7975 33.0078L35.2608 33.7668L34.6937 33.6858L31.0177 30.9233L29.6 29.6787L26.3899 26.977H26.1772V27.2603L26.9165 28.343L30.8253 34.212L31.0278 36.0132L30.7443 36.6L29.7316 36.9542L28.6177 36.7518L26.3291 33.5441L23.9696 29.9317L22.0658 26.6937L21.8329 26.8252L20.7089 38.9173L20.1823 39.5345L18.9671 40L17.9544 39.231L17.4177 37.9863L17.9544 35.5274L18.6025 32.3198L19.1291 29.7698L19.6051 26.6026L19.8886 25.5502L19.8684 25.4794L19.6354 25.5097L17.2456 28.7883L13.6101 33.6959L10.7342 36.7721L10.0456 37.0453L8.85063 36.428L8.96203 35.3251L9.63038 34.3435L13.6101 29.2841L16.0101 26.1472L17.5595 24.3359L17.5494 24.0729H17.4582L6.88608 30.9335L5.00253 31.1763L4.1924 30.4174L4.29367 29.1728L4.67848 28.768L7.85823 26.5823L7.8481 26.5925Z" fill="black"/> +<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M4.35443 9.97775L6.71495 8.65418L6.75443 8.53883L6.71495 8.47508H6.59948L6.20456 8.45081L4.8557 8.41436L3.68608 8.36579L2.55291 8.30507L2.26734 8.24438L2 7.89224L2.02734 7.71617L2.26734 7.55528L2.61063 7.58564L3.37013 7.63724L4.50937 7.71617L5.3357 7.76474L6.56 7.89224H6.75443L6.78176 7.81331L6.71495 7.76474L6.66329 7.71617L5.48456 6.91778L4.20861 6.07388L3.54025 5.58815L3.17873 5.34227L2.99646 5.11157L2.91747 4.60764L3.24557 4.24639L3.68608 4.27675L3.79848 4.30711L4.24506 4.65014L5.19899 5.38781L6.44456 6.30458L6.62684 6.45635L6.69974 6.40475L6.70886 6.36833L6.62684 6.23171L5.94938 5.00834L5.22632 3.76372L4.9043 3.24766L4.81924 2.93802C4.78886 2.81053 4.7676 2.70427 4.7676 2.57374L5.14127 2.06678L5.34785 2L5.84609 2.06678L6.0557 2.24893L6.36557 2.95624L6.86684 4.07033L7.64456 5.58512L7.87241 6.0344L7.99391 6.45029L8.03948 6.57779H8.11847V6.50492L8.18228 5.6519L8.30075 4.6046L8.41619 3.25677L8.4557 2.87731L8.64404 2.42196L9.01772 2.17607L9.30938 2.31571L9.54938 2.65874L9.51596 2.88034L9.37316 3.80622L9.09368 5.25728L8.9114 6.22868H9.01772L9.13925 6.10727L9.6314 5.45459L10.4577 4.42246L10.8223 4.01265L11.2476 3.56033L11.521 3.3448H12.0375L12.4172 3.90944L12.2471 4.49228L11.7154 5.1662L11.275 5.73692L10.643 6.58691L10.2481 7.26689L10.2846 7.32152L10.3787 7.31243L11.8066 7.00886L12.5782 6.86921L13.4987 6.71135L13.915 6.90563L13.9605 7.10297L13.7965 7.50671L12.8122 7.74956L11.6577 7.98026L9.93824 8.38706L9.91697 8.40224L9.94127 8.43257L10.716 8.50544L11.0471 8.52365H11.8582L13.3681 8.63597L13.763 8.89703L14 9.21578L13.9605 9.45863L13.3529 9.76829L12.5327 9.57398L10.6187 9.11864L9.96254 8.95472H9.8714V9.00935L10.4182 9.54365L11.4208 10.4483L12.6754 11.614L12.7393 11.9023L12.5782 12.13L12.4081 12.1057L11.3053 11.277L10.88 10.9036L9.91697 10.0931H9.85316V10.1781L10.075 10.5029L11.2476 12.2636L11.3083 12.804L11.2233 12.98L10.9195 13.0863L10.5853 13.0255L9.89873 12.0632L9.19088 10.9795L8.61974 10.0081L8.54987 10.0476L8.21267 13.6752L8.05469 13.8604L7.69013 14L7.38632 13.7693L7.22531 13.3959L7.38632 12.6582L7.58075 11.6959L7.73873 10.9309L7.88153 9.98078L7.96658 9.66506L7.96052 9.64382L7.89062 9.65291L7.17368 10.6365L6.08303 12.1088L5.22026 13.0316L5.01368 13.1136L4.65519 12.9284L4.68861 12.5975L4.88911 12.303L6.08303 10.7852L6.80303 9.84416L7.26785 9.30077L7.26482 9.22187H7.23746L4.06582 11.2801L3.50076 11.3529L3.25772 11.1252L3.2881 10.7518L3.40354 10.6304L4.35747 9.97469L4.35443 9.97775Z" fill="black"/> </svg> diff --git a/assets/icons/ai_gemini.svg b/assets/icons/ai_gemini.svg index 60197dc4adcf912128756b32ead43b8b1da61222..bdde44ed2475313f0dfd418a496f372ca61db22d 100644 --- a/assets/icons/ai_gemini.svg +++ b/assets/icons/ai_gemini.svg @@ -1 +1,3 @@ -<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Google Gemini + + + diff --git a/assets/icons/new_from_summary.svg b/assets/icons/new_from_summary.svg new file mode 100644 index 0000000000000000000000000000000000000000..3b61ca51a08ca8901333d8beb172147b1f1cfcd0 --- /dev/null +++ b/assets/icons/new_from_summary.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/new_text_thread.svg b/assets/icons/new_text_thread.svg new file mode 100644 index 0000000000000000000000000000000000000000..75afa934a028f1bddd104effe536db70ad4f241c --- /dev/null +++ b/assets/icons/new_text_thread.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/new_thread.svg b/assets/icons/new_thread.svg new file mode 100644 index 0000000000000000000000000000000000000000..8c2596a4c9fca9f75a122dc85225f33696320030 --- /dev/null +++ b/assets/icons/new_thread.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 36851e44bac6e2455c19a403e417f9722db6b7c6..57d16d6e59ec9ef83831cfe6ff3ab5eeebc4a354 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -12,6 +12,7 @@ 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, @@ -66,8 +67,8 @@ use theme::ThemeSettings; use time::UtcOffset; use ui::utils::WithRemSize; use ui::{ - Banner, Callout, ContextMenu, 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::{ @@ -1906,16 +1907,39 @@ impl AgentPanel { .when(cx.has_flag::(), |this| { this.header("Zed Agent") }) - .action("New Thread", NewThread::default().boxed_clone()) - .action("New Text Thread", NewTextThread.boxed_clone()) + .item( + ContextMenuEntry::new("New Thread") + .icon(IconName::NewThread) + .icon_color(Color::Muted) + .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) + .handler(move |window, cx| { + window.dispatch_action(NewTextThread.boxed_clone(), cx); + }), + ) .when_some(active_thread, |this, active_thread| { let thread = active_thread.read(cx); + if !thread.is_empty() { - this.action( - "New From Summary", - Box::new(NewThread { - from_thread_id: Some(thread.id().clone()), - }), + let thread_id = thread.id().clone(); + this.item( + ContextMenuEntry::new("New From Summary") + .icon(IconName::NewFromSummary) + .icon_color(Color::Muted) + .handler(move |window, cx| { + window.dispatch_action( + Box::new(NewThread { + from_thread_id: Some(thread_id.clone()), + }), + cx, + ); + }), ) } else { this @@ -1924,19 +1948,33 @@ impl AgentPanel { .when(cx.has_flag::(), |this| { this.separator() .header("External Agents") - .action( - "New Gemini Thread", - NewExternalAgentThread { - agent: Some(crate::ExternalAgent::Gemini), - } - .boxed_clone(), + .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, + ); + }), ) - .action( - "New Claude Code Thread", - NewExternalAgentThread { - agent: Some(crate::ExternalAgent::ClaudeCode), - } - .boxed_clone(), + .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, + ); + }), ) }); menu @@ -2285,6 +2323,28 @@ impl AgentPanel { }))) } + fn render_empty_state_section_header( + &self, + label: impl Into, + action_slot: Option, + cx: &mut Context, + ) -> 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) + } + fn render_thread_empty_state( &self, window: &mut Window, @@ -2407,19 +2467,9 @@ impl AgentPanel { .justify_end() .gap_1() .child( - h_flex() - .pl_1p5() - .pb_1() - .w_full() - .justify_between() - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .child( - Label::new("Recent") - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child( + self.render_empty_state_section_header( + "Recent", + Some( Button::new("view-history", "View All") .style(ButtonStyle::Subtle) .label_size(LabelSize::Small) @@ -2434,8 +2484,11 @@ impl AgentPanel { ) .on_click(move |_event, window, cx| { window.dispatch_action(OpenHistory.boxed_clone(), cx); - }), + }) + .into_any_element(), ), + cx, + ), ) .child( v_flex() @@ -2463,6 +2516,113 @@ 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::NewThread, + ) + .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::NewTextThread, + ) + .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, + ) + }, + ), + ), + ) + }), + ) .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/ui.rs b/crates/agent_ui/src/ui.rs index 6398f64abb65bb6c9639c71c59e31e1d1a214bba..15f2e28e5824242c3a6da258c15810263c0d9b83 100644 --- a/crates/agent_ui/src/ui.rs +++ b/crates/agent_ui/src/ui.rs @@ -2,6 +2,7 @@ mod agent_notification; mod burn_mode_tooltip; mod context_pill; mod end_trial_upsell; +mod new_thread_button; mod onboarding_modal; pub mod preview; mod upsell; @@ -10,4 +11,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 onboarding_modal::*; diff --git a/crates/agent_ui/src/ui/new_thread_button.rs b/crates/agent_ui/src/ui/new_thread_button.rs new file mode 100644 index 0000000000000000000000000000000000000000..7764144150762f9b828ea98f1c917332759bd5ad --- /dev/null +++ b/crates/agent_ui/src/ui/new_thread_button.rs @@ -0,0 +1,75 @@ +use gpui::{ClickEvent, ElementId, IntoElement, ParentElement, Styled}; +use ui::prelude::*; + +#[derive(IntoElement)] +pub struct NewThreadButton { + id: ElementId, + label: SharedString, + icon: IconName, + keybinding: Option, + on_click: Option>, +} + +impl NewThreadButton { + pub fn new(id: impl Into, label: impl Into, icon: IconName) -> Self { + Self { + id: id.into(), + label: label.into(), + icon, + keybinding: None, + on_click: None, + } + } + + pub fn keybinding(mut self, keybinding: Option) -> Self { + self.keybinding = keybinding; + self + } + + pub fn on_click(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)) + }) + } +} diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index 631ccc1af3123defdc07c3e5dfb9756c0f235ec1..b85e5b517d6587ffdc39abb2295a2bcf6381fc19 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -181,6 +181,9 @@ pub enum IconName { MicMute, Microscope, Minimize, + NewFromSummary, + NewTextThread, + NewThread, Option, PageDown, PageUp, From 5289b815fe8b386a737b21ec837fb4827381de07 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 21 Jul 2025 19:58:16 -0400 Subject: [PATCH 239/658] ai_onboarding: Send users directly into the trial checkout flow when starting the trial (#34859) This PR makes it so users will be sent immediately into the trial checkout flow (by hitting zed.dev/account/start-trial) when they click the "Start Pro Trial" button. Release Notes: - N/A --- crates/ai_onboarding/src/ai_onboarding.rs | 2 +- crates/client/src/zed_urls.rs | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/ai_onboarding/src/ai_onboarding.rs b/crates/ai_onboarding/src/ai_onboarding.rs index f19b8821fa2cbcda063bc9a47f9b7736ef639d8e..9c53078e5a32ee421b88b680c0c7854b34859f42 100644 --- a/crates/ai_onboarding/src/ai_onboarding.rs +++ b/crates/ai_onboarding/src/ai_onboarding.rs @@ -146,7 +146,7 @@ impl ZedAiOnboarding { let (button_label, button_url) = if self.account_too_young { ("Start with Pro", zed_urls::upgrade_to_zed_pro_url(cx)) } else { - ("Start Pro Trial", zed_urls::account_url(cx)) + ("Start Pro Trial", zed_urls::start_trial_url(cx)) }; v_flex() diff --git a/crates/client/src/zed_urls.rs b/crates/client/src/zed_urls.rs index 442875b45132c1d7990f82ac93248ebd0477362c..e36f5f65dae646bae361eea34e582c308ea64dae 100644 --- a/crates/client/src/zed_urls.rs +++ b/crates/client/src/zed_urls.rs @@ -18,6 +18,14 @@ pub fn account_url(cx: &App) -> String { format!("{server_url}/account", server_url = server_url(cx)) } +/// Returns the URL to the start trial page on zed.dev. +pub fn start_trial_url(cx: &App) -> String { + format!( + "{server_url}/account/start-trial", + server_url = server_url(cx) + ) +} + /// Returns the URL to the upgrade page on zed.dev. pub fn upgrade_to_zed_pro_url(cx: &App) -> String { format!("{server_url}/account/upgrade", server_url = server_url(cx)) From 15353630e4f8f1f3db8f230e078de2f42bdbfb7b Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 21 Jul 2025 21:11:11 -0400 Subject: [PATCH 240/658] zed: Add `OpenRequestKind` (#34860) This PR refactors the `OpenRequest` to introduce an `OpenRequestKind` enum. It seems most of the fields on `OpenRequest` are mutually-exclusive, so it is better to model it as an enum rather than using a bunch of `Option`s. There are likely more of the existing fields that can be converted into `OpenRequestKind` variants, but I'm being conservative for this first pass. Release Notes: - N/A --- crates/zed/src/main.rs | 54 ++++++++++++++++------------- crates/zed/src/zed/open_listener.rs | 23 ++++++++---- 2 files changed, 45 insertions(+), 32 deletions(-) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index a9d3d63381dfbb24bbc9e7882e1e035887dccecb..c7856931ef0716e80ab34ffeb799eea2399cac15 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -55,6 +55,8 @@ use zed::{ inline_completion_registry, open_paths_with_positions, }; +use crate::zed::OpenRequestKind; + #[cfg(feature = "mimalloc")] #[global_allocator] static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; @@ -746,32 +748,34 @@ pub fn main() { } fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut App) { - if let Some(connection) = request.cli_connection { - let app_state = app_state.clone(); - cx.spawn(async move |cx| handle_cli_connection(connection, app_state, cx).await) - .detach(); - return; - } - - if let Some(action_index) = request.dock_menu_action { - cx.perform_dock_menu_action(action_index); - return; - } + 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(); + } + OpenRequestKind::Extension { extension_id } => { + cx.spawn(async move |cx| { + let workspace = + workspace::get_any_active_workspace(app_state, cx.clone()).await?; + workspace.update(cx, |_, window, cx| { + window.dispatch_action( + Box::new(zed_actions::Extensions { + category_filter: None, + id: Some(extension_id), + }), + cx, + ); + }) + }) + .detach_and_log_err(cx); + } + OpenRequestKind::DockMenuAction { index } => { + cx.perform_dock_menu_action(index); + } + } - if let Some(extension) = request.extension_id { - cx.spawn(async move |cx| { - let workspace = workspace::get_any_active_workspace(app_state, cx.clone()).await?; - workspace.update(cx, |_, window, cx| { - window.dispatch_action( - Box::new(zed_actions::Extensions { - category_filter: None, - id: Some(extension), - }), - cx, - ); - }) - }) - .detach_and_log_err(cx); return; } diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index 42eb8198a4c091d0ce6dd4ecbae3f0ced7bdf7d3..af646465be2ca6bb33128b1685f4067063ce177f 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -30,14 +30,19 @@ use workspace::{AppState, OpenOptions, SerializedWorkspaceLocation, Workspace}; #[derive(Default, Debug)] pub struct OpenRequest { - pub cli_connection: Option<(mpsc::Receiver, IpcSender)>, + pub kind: Option, pub open_paths: Vec, pub diff_paths: Vec<[String; 2]>, pub open_channel_notes: Vec<(u64, Option)>, pub join_channel: Option, pub ssh_connection: Option, - pub dock_menu_action: Option, - pub extension_id: Option, +} + +#[derive(Debug)] +pub enum OpenRequestKind { + CliConnection((mpsc::Receiver, IpcSender)), + Extension { extension_id: String }, + DockMenuAction { index: usize }, } impl OpenRequest { @@ -45,9 +50,11 @@ impl OpenRequest { let mut this = Self::default(); for url in request.urls { if let Some(server_name) = url.strip_prefix("zed-cli://") { - this.cli_connection = Some(connect_to_cli(server_name)?); + this.kind = Some(OpenRequestKind::CliConnection(connect_to_cli(server_name)?)); } else if let Some(action_index) = url.strip_prefix("zed-dock-action://") { - this.dock_menu_action = Some(action_index.parse()?); + this.kind = Some(OpenRequestKind::DockMenuAction { + index: action_index.parse()?, + }); } else if let Some(file) = url.strip_prefix("file://") { this.parse_file_path(file) } else if let Some(file) = url.strip_prefix("zed://file") { @@ -55,8 +62,10 @@ impl OpenRequest { } else if let Some(file) = url.strip_prefix("zed://ssh") { let ssh_url = "ssh:/".to_string() + file; this.parse_ssh_file_path(&ssh_url, cx)? - } else if let Some(file) = url.strip_prefix("zed://extension/") { - this.extension_id = Some(file.to_string()) + } else if let Some(extension_id) = url.strip_prefix("zed://extension/") { + this.kind = Some(OpenRequestKind::Extension { + extension_id: extension_id.to_string(), + }); } else if url.starts_with("ssh://") { this.parse_ssh_file_path(&url, cx)? } else if let Some(request_path) = parse_zed_link(&url, cx) { From 233e66d35f888a84f76317f5e23ab14a279ae806 Mon Sep 17 00:00:00 2001 From: Daste Date: Tue, 22 Jul 2025 03:30:23 +0200 Subject: [PATCH 241/658] Add `editor::BlameHover` action for triggering the blame popover via keyboard (#32096) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the git blame popover available via the keymap by making it an action. The blame popover stays open after being shown via the action, similar to the `editor::Hover` action. I added a default vim-mode key binding for `g b`, which goes in hand with `g h` for hover. I'm not sure what the keybind would be for regular layouts, if any would be set by default. I'm opening this as a draft because I coludn't figure out a way to position the popover correctly above/under the cursor head. I saw some uses of `content_origin` in other places for calculating absolute pixel positions, but I'm not sure how to make use of it here without doing a big refactor of the blame popover code 🤔. I would appreciate some help/tips with positioning, because it seems like the last thing to implement here. Opening as a draft for now because I think without the correct positioning this feature is not complete. Closes https://github.com/zed-industries/zed/discussions/26447 Release Notes: - Added `editor::BlameHover` action for showing the git blame popover under the cursor. By default bound to `ctrl-k ctrl-b` and to `g h` in vim mode. --- assets/keymaps/default-linux.json | 1 + assets/keymaps/default-macos.json | 1 + assets/keymaps/vim.json | 1 + crates/editor/src/actions.rs | 2 ++ crates/editor/src/editor.rs | 44 ++++++++++++++++++++++++++++--- crates/editor/src/element.rs | 9 +++++-- 6 files changed, 52 insertions(+), 6 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 377a26242b995a3db4d67a9c9dea082c9c11b93d..4918e654fc50e7282cf5ee99228c77381d6997ee 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -483,6 +483,7 @@ "ctrl-k ctrl-d": ["editor::SelectNext", { "replace_newest": true }], // editor.action.moveSelectionToNextFindMatch / find_under_expand_skip "ctrl-k ctrl-shift-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch "ctrl-k ctrl-i": "editor::Hover", + "ctrl-k ctrl-b": "editor::BlameHover", "ctrl-/": ["editor::ToggleComments", { "advance_downwards": false }], "f8": ["editor::GoToDiagnostic", { "severity": { "min": "hint", "max": "error" } }], "shift-f8": ["editor::GoToPreviousDiagnostic", { "severity": { "min": "hint", "max": "error" } }], diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 712d73b8ec5d6dfd3aa6abb5c1420f3e81c2ee0a..60f29b1da148e26d72744f42252f6086894cd5db 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -537,6 +537,7 @@ "ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": false }], // editor.action.addSelectionToPreviousFindMatch "cmd-k ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch "cmd-k cmd-i": "editor::Hover", + "cmd-k cmd-b": "editor::BlameHover", "cmd-/": ["editor::ToggleComments", { "advance_downwards": false }], "f8": ["editor::GoToDiagnostic", { "severity": { "min": "hint", "max": "error" } }], "shift-f8": ["editor::GoToPreviousDiagnostic", { "severity": { "min": "hint", "max": "error" } }], diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 04e6b0bcd40720067dea4b8ecaf2bdb72adcdc2d..d0cf4621a59d8614022f2a4ba176a79b40f8d331 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -124,6 +124,7 @@ "g r a": "editor::ToggleCodeActions", "g g": "vim::StartOfDocument", "g h": "editor::Hover", + "g B": "editor::BlameHover", "g t": "pane::ActivateNextItem", "g shift-t": "pane::ActivatePreviousItem", "g d": "editor::GoToDefinition", diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 87463d246d2aba96485247467b15025c58f9d5d5..8557b57f4602e35174cc711aa62e2bebf8c8a140 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -322,6 +322,8 @@ actions!( ApplyDiffHunk, /// Deletes the character before the cursor. Backspace, + /// Shows git blame information for the current line. + BlameHover, /// Cancels the current operation. Cancel, /// Cancels the running flycheck operation. diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b8dcdd35e07102afd613e99c22a60df6f4699604..1f985eeb7c225be0778cf13de925276583063e16 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -950,6 +950,7 @@ struct InlineBlamePopover { hide_task: Option>, popover_bounds: Option>, popover_state: InlineBlamePopoverState, + keyboard_grace: bool, } enum SelectionDragState { @@ -6517,21 +6518,55 @@ impl Editor { } } + pub fn blame_hover(&mut self, _: &BlameHover, window: &mut Window, cx: &mut Context) { + let snapshot = self.snapshot(window, cx); + let cursor = self.selections.newest::(cx).head(); + let Some((buffer, point, _)) = snapshot.buffer_snapshot.point_to_buffer_point(cursor) + else { + return; + }; + + let Some(blame) = self.blame.as_ref() else { + return; + }; + + let row_info = RowInfo { + buffer_id: Some(buffer.remote_id()), + buffer_row: Some(point.row), + ..Default::default() + }; + let Some(blame_entry) = blame + .update(cx, |blame, cx| blame.blame_for_rows(&[row_info], cx).next()) + .flatten() + else { + return; + }; + + let anchor = self.selections.newest_anchor().head(); + let position = self.to_pixel_point(anchor, &snapshot, window); + if let (Some(position), Some(last_bounds)) = (position, self.last_bounds) { + self.show_blame_popover(&blame_entry, position + last_bounds.origin, true, cx); + }; + } + fn show_blame_popover( &mut self, blame_entry: &BlameEntry, position: gpui::Point, + ignore_timeout: bool, cx: &mut Context, ) { if let Some(state) = &mut self.inline_blame_popover { state.hide_task.take(); } else { - let delay = EditorSettings::get_global(cx).hover_popover_delay; + let blame_popover_delay = EditorSettings::get_global(cx).hover_popover_delay; let blame_entry = blame_entry.clone(); let show_task = cx.spawn(async move |editor, cx| { - cx.background_executor() - .timer(std::time::Duration::from_millis(delay)) - .await; + if !ignore_timeout { + cx.background_executor() + .timer(std::time::Duration::from_millis(blame_popover_delay)) + .await; + } editor .update(cx, |editor, cx| { editor.inline_blame_popover_show_task.take(); @@ -6560,6 +6595,7 @@ impl Editor { commit_message: details, markdown, }, + keyboard_grace: ignore_timeout, }); cx.notify(); }) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index fef185bb156085655c8a144cb3d06c70d8558f2c..cbff544c7e2e4159ae290e37b3fbe8b631696184 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -216,6 +216,7 @@ impl EditorElement { register_action(editor, window, Editor::newline_above); register_action(editor, window, Editor::newline_below); register_action(editor, window, Editor::backspace); + register_action(editor, window, Editor::blame_hover); register_action(editor, window, Editor::delete); register_action(editor, window, Editor::tab); register_action(editor, window, Editor::backtab); @@ -1143,10 +1144,14 @@ impl EditorElement { .as_ref() .and_then(|state| state.popover_bounds) .map_or(false, |bounds| bounds.contains(&event.position)); + let keyboard_grace = editor + .inline_blame_popover + .as_ref() + .map_or(false, |state| state.keyboard_grace); if mouse_over_inline_blame || mouse_over_popover { - editor.show_blame_popover(&blame_entry, event.position, cx); - } else { + editor.show_blame_popover(&blame_entry, event.position, false, cx); + } else if !keyboard_grace { editor.hide_blame_popover(cx); } } else { From 5a530ecd39f8be386a98cabef45c13163ff1da19 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Mon, 21 Jul 2025 21:40:33 -0400 Subject: [PATCH 242/658] zed: Add support for `zed://agent` links (#34862) This PR adds support for `zed://agent` links for opening the Agent Panel. Release Notes: - N/A --- crates/zed/src/main.rs | 15 ++++++++++++++- crates/zed/src/zed/open_listener.rs | 3 +++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index c7856931ef0716e80ab34ffeb799eea2399cac15..c9b8eebff6a9001c2350a2e1bc2e56ad6708c5ee 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -1,6 +1,7 @@ mod reliability; mod zed; +use agent_ui::AgentPanel; use anyhow::{Context as _, Result}; use clap::{Parser, command}; use cli::FORCE_CLI_MODE_ENV_VAR_NAME; @@ -14,7 +15,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, UpdateGlobal as _}; +use gpui::{App, AppContext as _, Application, AsyncApp, Focusable as _, UpdateGlobal as _}; use gpui_tokio::Tokio; use http_client::{Url, read_proxy_from_env}; @@ -771,6 +772,18 @@ fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut }) .detach_and_log_err(cx); } + OpenRequestKind::AgentPanel => { + cx.spawn(async move |cx| { + let workspace = + workspace::get_any_active_workspace(app_state, cx.clone()).await?; + workspace.update(cx, |workspace, window, cx| { + if let Some(panel) = workspace.panel::(cx) { + panel.focus_handle(cx).focus(window); + } + }) + }) + .detach_and_log_err(cx); + } OpenRequestKind::DockMenuAction { index } => { cx.perform_dock_menu_action(index); } diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index af646465be2ca6bb33128b1685f4067063ce177f..b6feb0073e3d46fb43c45e0f17069eef730f2481 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -42,6 +42,7 @@ pub struct OpenRequest { pub enum OpenRequestKind { CliConnection((mpsc::Receiver, IpcSender)), Extension { extension_id: String }, + AgentPanel, DockMenuAction { index: usize }, } @@ -66,6 +67,8 @@ impl OpenRequest { this.kind = Some(OpenRequestKind::Extension { extension_id: extension_id.to_string(), }); + } else if url == "zed://agent" { + this.kind = Some(OpenRequestKind::AgentPanel); } else if url.starts_with("ssh://") { this.parse_ssh_file_path(&url, cx)? } else if let Some(request_path) = parse_zed_link(&url, cx) { From eaccd542fd5e03a93fd1b2f7d02c874f3d491abf Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 22 Jul 2025 02:09:05 -0300 Subject: [PATCH 243/658] Add fast-follows to the AI onboarding flow (#34737) Follow-up to https://github.com/zed-industries/zed/pull/33738. Release Notes: - N/A --------- Co-authored-by: Bennet Bo Fenner --- crates/agent_ui/src/agent_panel.rs | 30 +- crates/agent_ui/src/message_editor.rs | 23 +- .../src/agent_api_keys_onboarding.rs | 135 +++++++++ .../src/agent_panel_onboarding_card.rs | 8 +- .../src/agent_panel_onboarding_content.rs | 114 ++----- crates/ai_onboarding/src/ai_onboarding.rs | 279 +++++++++++------- .../ai_onboarding/src/young_account_banner.rs | 2 +- crates/client/src/zed_urls.rs | 5 + crates/language_model/src/registry.rs | 4 +- crates/language_models/src/provider/cloud.rs | 12 +- 10 files changed, 403 insertions(+), 209 deletions(-) create mode 100644 crates/ai_onboarding/src/agent_api_keys_onboarding.rs diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 57d16d6e59ec9ef83831cfe6ff3ab5eeebc4a354..fc803c730eab7741ba33c9423e6558e54c8cba8d 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -2300,7 +2300,20 @@ impl AgentPanel { return None; } - Some(div().size_full().child(self.onboarding.clone())) + let thread_view = matches!(&self.active_view, ActiveView::Thread { .. }); + let text_thread_view = matches!(&self.active_view, ActiveView::TextThread { .. }); + + Some( + div() + .size_full() + .when(thread_view, |this| { + this.bg(cx.theme().colors().panel_background) + }) + .when(text_thread_view, |this| { + this.bg(cx.theme().colors().editor_background) + }) + .child(self.onboarding.clone()), + ) } fn render_trial_end_upsell( @@ -3237,7 +3250,20 @@ impl Render for AgentPanel { .into_any(), ) }) - .child(h_flex().child(message_editor.clone())) + .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(), + ) + }, + )) .child(self.render_drag_target(cx)), ActiveView::ExternalAgentThread { thread_view, .. } => parent .relative() diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index a2cf4aac48a0eb6e368596bc7458615ea1f008a1..69eae982f8008849d02e4b695c4b00c08cff0b46 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -14,6 +14,7 @@ use agent::{ context_store::ContextStoreEvent, }; use agent_settings::{AgentSettings, CompletionMode}; +use ai_onboarding::ApiKeysWithProviders; use buffer_diff::BufferDiff; use client::UserStore; use collections::{HashMap, HashSet}; @@ -33,7 +34,8 @@ use gpui::{ }; use language::{Buffer, Language, Point}; use language_model::{ - ConfiguredModel, LanguageModelRequestMessage, MessageContent, ZED_CLOUD_PROVIDER_ID, + ConfiguredModel, LanguageModelRegistry, LanguageModelRequestMessage, MessageContent, + ZED_CLOUD_PROVIDER_ID, }; use multi_buffer; use project::Project; @@ -1655,9 +1657,28 @@ impl Render for MessageEditor { let line_height = TextSize::Small.rems(cx).to_pixels(window.rem_size()) * 1.5; + let enrolled_in_trial = matches!( + self.user_store.read(cx).current_plan(), + Some(proto::Plan::ZedProTrial) + ); + + let configured_providers: Vec<(IconName, SharedString)> = + LanguageModelRegistry::read_global(cx) + .providers() + .iter() + .filter(|provider| { + provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID + }) + .map(|provider| (provider.icon(), provider.name().0.clone())) + .collect(); + let has_existing_providers = configured_providers.len() > 0; + v_flex() .size_full() .bg(cx.theme().colors().panel_background) + .when(has_existing_providers && !enrolled_in_trial, |this| { + this.child(cx.new(ApiKeysWithProviders::new)) + }) .when(changed_buffers.len() > 0, |parent| { parent.child(self.render_edits_bar(&changed_buffers, window, cx)) }) diff --git a/crates/ai_onboarding/src/agent_api_keys_onboarding.rs b/crates/ai_onboarding/src/agent_api_keys_onboarding.rs new file mode 100644 index 0000000000000000000000000000000000000000..4f9e20cf77ed2685241cd72e5971df26cd918563 --- /dev/null +++ b/crates/ai_onboarding/src/agent_api_keys_onboarding.rs @@ -0,0 +1,135 @@ +use gpui::{Action, IntoElement, ParentElement, RenderOnce, point}; +use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID}; +use ui::{Divider, List, prelude::*}; + +use crate::BulletItem; + +pub struct ApiKeysWithProviders { + configured_providers: Vec<(IconName, SharedString)>, +} + +impl ApiKeysWithProviders { + pub fn new(cx: &mut Context) -> Self { + cx.subscribe( + &LanguageModelRegistry::global(cx), + |this: &mut Self, _registry, event: &language_model::Event, cx| match event { + language_model::Event::ProviderStateChanged + | language_model::Event::AddedProvider(_) + | language_model::Event::RemovedProvider(_) => { + this.configured_providers = Self::compute_configured_providers(cx) + } + _ => {} + }, + ) + .detach(); + + Self { + configured_providers: Self::compute_configured_providers(cx), + } + } + + fn compute_configured_providers(cx: &App) -> Vec<(IconName, SharedString)> { + LanguageModelRegistry::read_global(cx) + .providers() + .iter() + .filter(|provider| { + provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID + }) + .map(|provider| (provider.icon(), provider.name().0.clone())) + .collect() + } + + pub fn has_providers(&self) -> bool { + !self.configured_providers.is_empty() + } +} + +impl Render for ApiKeysWithProviders { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let configured_providers_list = + self.configured_providers + .iter() + .cloned() + .map(|(icon, name)| { + h_flex() + .gap_1p5() + .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted)) + .child(Label::new(name)) + }); + + h_flex() + .mx_2p5() + .p_1() + .pb_0() + .gap_2() + .rounded_t_lg() + .border_t_1() + .border_x_1() + .border_color(cx.theme().colors().border.opacity(0.5)) + .bg(cx.theme().colors().background.alpha(0.5)) + .shadow(vec![gpui::BoxShadow { + color: gpui::black().opacity(0.15), + offset: point(px(1.), px(-1.)), + blur_radius: px(3.), + spread_radius: px(0.), + }]) + .child( + h_flex() + .px_2p5() + .py_1p5() + .gap_2() + .flex_wrap() + .rounded_t(px(5.)) + .overflow_hidden() + .border_t_1() + .border_x_1() + .border_color(cx.theme().colors().border) + .bg(cx.theme().colors().panel_background) + .child(Icon::new(IconName::Info).size(IconSize::XSmall).color(Color::Muted)) + .child(Label::new("Or start now using API keys from your environment for the following providers:").color(Color::Muted)) + .children(configured_providers_list) + ) + } +} + +#[derive(IntoElement)] +pub struct ApiKeysWithoutProviders; + +impl ApiKeysWithoutProviders { + pub fn new() -> Self { + Self + } +} + +impl RenderOnce for ApiKeysWithoutProviders { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + v_flex() + .mt_2() + .gap_1() + .child( + h_flex() + .gap_2() + .child( + Label::new("API Keys") + .size(LabelSize::Small) + .color(Color::Muted) + .buffer_font(cx), + ) + .child(Divider::horizontal()), + ) + .child(List::new().child(BulletItem::new( + "You can also use AI in Zed by bringing your own API keys", + ))) + .child( + Button::new("configure-providers", "Configure Providers") + .full_width() + .style(ButtonStyle::Outlined) + .on_click(move |_, window, cx| { + window.dispatch_action( + zed_actions::agent::OpenConfiguration.boxed_clone(), + cx, + ); + }), + ) + } +} diff --git a/crates/ai_onboarding/src/agent_panel_onboarding_card.rs b/crates/ai_onboarding/src/agent_panel_onboarding_card.rs index 8ec9ccfe2230cedd921d3a18d0cb6236a043c716..c63c5926428ab47f80afd2e157f90f8852dbf4ee 100644 --- a/crates/ai_onboarding/src/agent_panel_onboarding_card.rs +++ b/crates/ai_onboarding/src/agent_panel_onboarding_card.rs @@ -24,7 +24,7 @@ impl ParentElement for AgentPanelOnboardingCard { impl RenderOnce for AgentPanelOnboardingCard { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { div() - .m_4() + .m_2p5() .p(px(3.)) .elevation_2(cx) .rounded_lg() @@ -49,6 +49,7 @@ impl RenderOnce for AgentPanelOnboardingCard { .right_0() .w(px(400.)) .h(px(92.)) + .rounded_md() .child( Vector::new( VectorName::AiGrid, @@ -61,11 +62,12 @@ impl RenderOnce for AgentPanelOnboardingCard { .child( div() .absolute() - .top_0() - .right_0() + .top_0p5() + .right_0p5() .w(px(660.)) .h(px(401.)) .overflow_hidden() + .rounded_md() .bg(linear_gradient( 75., linear_color_stop( diff --git a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs index f3f7d6c3d7e152ee8e46c6cf28b1d0bc0322c057..771482abf3f5ba871f2955d8579514013c6704f0 100644 --- a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs +++ b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs @@ -1,12 +1,11 @@ use std::sync::Arc; use client::{Client, UserStore}; -use gpui::{Action, ClickEvent, Entity, IntoElement, ParentElement}; +use gpui::{Entity, IntoElement, ParentElement}; use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID}; -use ui::{Divider, List, prelude::*}; -use zed_actions::agent::{OpenConfiguration, ToggleModelSelector}; +use ui::prelude::*; -use crate::{AgentPanelOnboardingCard, BulletItem, ZedAiOnboarding}; +use crate::{AgentPanelOnboardingCard, ApiKeysWithoutProviders, ZedAiOnboarding}; pub struct AgentPanelOnboarding { user_store: Entity, @@ -53,93 +52,34 @@ impl AgentPanelOnboarding { .map(|provider| (provider.icon(), provider.name().0.clone())) .collect() } - - fn configure_providers(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { - window.dispatch_action(OpenConfiguration.boxed_clone(), cx); - cx.notify(); - } - - fn render_api_keys_section(&mut self, cx: &mut Context) -> impl IntoElement { - let has_existing_providers = self.configured_providers.len() > 0; - let configure_provider_label = if has_existing_providers { - "Configure Other Provider" - } else { - "Configure Providers" - }; - - let content = if has_existing_providers { - List::new() - .child(BulletItem::new( - "Or start now using API keys from your environment for the following providers:" - )) - .child( - h_flex() - .px_5() - .gap_2() - .flex_wrap() - .children(self.configured_providers.iter().cloned().map(|(icon, name)| - h_flex() - .gap_1p5() - .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted)) - .child(Label::new(name)) - )) - ) - .child(BulletItem::new( - "No need for any of the plans or even to sign in", - )) - } else { - List::new() - .child(BulletItem::new( - "You can also use AI in Zed by bringing your own API keys", - )) - .child(BulletItem::new( - "No need for any of the plans or even to sign in", - )) - }; - - v_flex() - .mt_2() - .gap_1() - .child( - h_flex() - .gap_2() - .child( - Label::new("API Keys") - .size(LabelSize::Small) - .color(Color::Muted) - .buffer_font(cx), - ) - .child(Divider::horizontal()), - ) - .child(content) - .when(has_existing_providers, |this| { - this.child( - Button::new("pick-model", "Choose Model") - .full_width() - .style(ButtonStyle::Outlined) - .on_click(|_event, window, cx| { - window.dispatch_action(ToggleModelSelector.boxed_clone(), cx) - }), - ) - }) - .child( - Button::new("configure-providers", configure_provider_label) - .full_width() - .style(ButtonStyle::Outlined) - .on_click(cx.listener(Self::configure_providers)), - ) - } } 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) + ); + AgentPanelOnboardingCard::new() - .child(ZedAiOnboarding::new( - self.client.clone(), - &self.user_store, - self.continue_with_zed_ai.clone(), - cx, - )) - .child(self.render_api_keys_section(cx)) + .child( + ZedAiOnboarding::new( + self.client.clone(), + &self.user_store, + self.continue_with_zed_ai.clone(), + cx, + ) + .with_dismiss({ + let callback = self.continue_with_zed_ai.clone(); + move |window, cx| callback(window, cx) + }), + ) + .map(|this| { + if enrolled_in_trial || self.configured_providers.len() >= 1 { + this + } else { + this.child(ApiKeysWithoutProviders::new()) + } + }) } } diff --git a/crates/ai_onboarding/src/ai_onboarding.rs b/crates/ai_onboarding/src/ai_onboarding.rs index 9c53078e5a32ee421b88b680c0c7854b34859f42..88c962c1ba5271c8bf713af72a7aa2492d10c84d 100644 --- a/crates/ai_onboarding/src/ai_onboarding.rs +++ b/crates/ai_onboarding/src/ai_onboarding.rs @@ -1,8 +1,10 @@ +mod agent_api_keys_onboarding; mod agent_panel_onboarding_card; mod agent_panel_onboarding_content; mod edit_prediction_onboarding_content; mod young_account_banner; +pub use agent_api_keys_onboarding::{ApiKeysWithProviders, ApiKeysWithoutProviders}; pub use agent_panel_onboarding_card::AgentPanelOnboardingCard; pub use agent_panel_onboarding_content::AgentPanelOnboarding; pub use edit_prediction_onboarding_content::EditPredictionOnboarding; @@ -12,7 +14,7 @@ use std::sync::Arc; use client::{Client, UserStore, zed_urls}; use gpui::{AnyElement, Entity, IntoElement, ParentElement, SharedString}; -use ui::{Divider, List, ListItem, RegisterComponent, TintColor, prelude::*}; +use ui::{Divider, List, ListItem, RegisterComponent, TintColor, Tooltip, prelude::*}; pub struct BulletItem { label: SharedString, @@ -69,6 +71,7 @@ pub struct ZedAiOnboarding { pub continue_with_zed_ai: Arc, pub sign_in: Arc, pub accept_terms_of_service: Arc, + pub dismiss_onboarding: Option>, } impl ZedAiOnboarding { @@ -80,6 +83,7 @@ impl ZedAiOnboarding { ) -> Self { let store = user_store.read(cx); let status = *client.status().borrow(); + Self { sign_in_status: status.into(), has_accepted_terms_of_service: store.current_user_has_accepted_terms().unwrap_or(false), @@ -102,14 +106,22 @@ impl ZedAiOnboarding { }) .detach(); }), + dismiss_onboarding: None, } } - fn render_free_plan_section(&self, cx: &mut App) -> impl IntoElement { + pub fn with_dismiss( + mut self, + dismiss_callback: impl Fn(&mut Window, &mut App) + 'static, + ) -> Self { + self.dismiss_onboarding = Some(Arc::new(dismiss_callback)); + self + } + + fn free_plan_definition(&self, cx: &mut App) -> impl IntoElement { v_flex() .mt_2() .gap_1() - .when(self.account_too_young, |this| this.opacity(0.4)) .child( h_flex() .gap_2() @@ -119,6 +131,12 @@ impl ZedAiOnboarding { .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( @@ -130,65 +148,89 @@ impl ZedAiOnboarding { "2000 accepted edit predictions using our open-source Zeta model", )), ) - .child( - Button::new("continue", "Continue Free") - .disabled(self.account_too_young) - .full_width() - .style(ButtonStyle::Outlined) - .on_click({ - let callback = self.continue_with_zed_ai.clone(); - move |_, window, cx| callback(window, cx) - }), - ) } - fn render_pro_plan_section(&self, cx: &mut App) -> impl IntoElement { - let (button_label, button_url) = if self.account_too_young { - ("Start with Pro", zed_urls::upgrade_to_zed_pro_url(cx)) - } else { - ("Start Pro Trial", zed_urls::start_trial_url(cx)) - }; + fn pro_trial_definition(&self) -> impl IntoElement { + List::new() + .child(BulletItem::new( + "150 prompts per month with the Claude models", + )) + .child(BulletItem::new( + "Unlimited accepted edit predictions using our open-source Zeta model", + )) + } - 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( - List::new() - .child(BulletItem::new("500 prompts per month with Claude models")) - .child(BulletItem::new("Unlimited edit predictions")) - .when(!self.account_too_young, |this| { - this.child(BulletItem::new( - "Try it out for 14 days with no charge, no credit card required", + 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 using our open-source Zeta model", )) - }), - ) - .child( - Button::new("pro", button_label) - .full_width() - .style(ButtonStyle::Tinted(ui::TintColor::Accent)) - .on_click(move |_, _window, cx| cx.open_url(&button_url)), - ) + .child(BulletItem::new("USD $20 per month")), + ) + .child( + Button::new("pro", "Start with Pro") + .full_width() + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .on_click(move |_, _window, cx| { + 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 with no charge and no credit card required", + )), + ) + .child( + Button::new("pro", "Start Pro Trial") + .full_width() + .style(ButtonStyle::Tinted(ui::TintColor::Accent)) + .on_click(move |_, _window, cx| { + cx.open_url(&zed_urls::start_trial_url(cx)) + }), + ) + } + }) } - fn render_accept_terms_of_service(&self) -> Div { + fn render_accept_terms_of_service(&self) -> AnyElement { v_flex() - .w_full() .gap_1() + .w_full() .child(Headline::new("Before starting…")) - .child(Label::new( - "Make sure you have read and accepted Zed AI's terms of service.", - )) + .child( + Label::new("Make sure you have read and accepted Zed AI's terms of service.") + .color(Color::Muted) + .mb_2(), + ) .child( Button::new("terms_of_service", "View and Read the Terms of Service") .full_width() @@ -196,9 +238,7 @@ impl ZedAiOnboarding { .icon(IconName::ArrowUpRight) .icon_color(Color::Muted) .icon_size(IconSize::XSmall) - .on_click(move |_, _window, cx| { - cx.open_url("https://zed.dev/terms-of-service") - }), + .on_click(move |_, _window, cx| cx.open_url(&zed_urls::terms_of_service(cx))), ) .child( Button::new("accept_terms", "I've read it and accept it") @@ -209,23 +249,23 @@ impl ZedAiOnboarding { move |_, window, cx| (callback)(window, cx) }), ) + .into_any_element() } - fn render_sign_in_disclaimer(&self, _cx: &mut App) -> Div { - const SIGN_IN_DISCLAIMER: &str = - "To start using AI in Zed with our hosted models, sign in and subscribe to a plan."; + fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement { let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn); v_flex() - .gap_2() + .gap_1() .child(Headline::new("Welcome to Zed AI")) - .child(div().w_full().child(Label::new(SIGN_IN_DISCLAIMER))) .child( - Button::new("sign_in", "Sign In with GitHub") - .icon(IconName::Github) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) + Label::new("Sign in to start using AI in Zed with a free trial of the Pro plan, which includes:") + .color(Color::Muted) + .mb_2(), + ) + .child(self.pro_trial_definition()) + .child( + Button::new("sign_in", "Sign in to Start Trial") .disabled(signing_in) .full_width() .style(ButtonStyle::Tinted(ui::TintColor::Accent)) @@ -234,36 +274,55 @@ impl ZedAiOnboarding { move |_, window, cx| callback(window, cx) }), ) + .into_any_element() } - fn render_free_plan_onboarding(&self, cx: &mut App) -> Div { - const PLANS_DESCRIPTION: &str = "Choose how you want to start."; + fn render_free_plan_state(&self, cx: &mut App) -> AnyElement { let young_account_banner = YoungAccountBanner; v_flex() + .relative() + .gap_1() .child(Headline::new("Welcome to Zed AI")) .child( - Label::new(PLANS_DESCRIPTION) - .size(LabelSize::Small) + Label::new("Choose how you want to start.") .color(Color::Muted) - .mt_1() - .mb_3(), + .mb_2(), ) - .when(self.account_too_young, |this| { - this.child(young_account_banner) + .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(); + + 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| callback(window, cx)), + ), + ) + }, + ) + } }) - .child(self.render_free_plan_section(cx)) - .child(self.render_pro_plan_section(cx)) + .child(self.pro_plan_definition(cx)) + .into_any_element() } - fn render_trial_onboarding(&self, _cx: &mut App) -> Div { + fn render_trial_state(&self, _cx: &mut App) -> AnyElement { v_flex() - .child(Headline::new("Welcome to the trial of Zed Pro")) + .relative() + .gap_1() + .child(Headline::new("Welcome to the Zed Pro free trial")) .child( Label::new("Here's what you get for the next 14 days:") - .size(LabelSize::Small) .color(Color::Muted) - .mt_1(), + .mb_2(), ) .child( List::new() @@ -272,25 +331,31 @@ impl ZedAiOnboarding { "Unlimited edit predictions with Zeta, our open-source model", )), ) - .child( - Button::new("trial", "Start Trial") - .full_width() - .style(ButtonStyle::Outlined) - .on_click({ - let callback = self.continue_with_zed_ai.clone(); - move |_, window, cx| 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| callback(window, cx)), + ), + ) + }, ) + .into_any_element() } - fn render_pro_plan_onboarding(&self, _cx: &mut App) -> Div { + fn render_pro_plan_state(&self, _cx: &mut App) -> AnyElement { v_flex() + .gap_1() .child(Headline::new("Welcome to Zed Pro")) .child( Label::new("Here's what you get:") - .size(LabelSize::Small) .color(Color::Muted) - .mt_1(), + .mb_2(), ) .child( List::new() @@ -306,6 +371,7 @@ impl ZedAiOnboarding { move |_, window, cx| callback(window, cx) }), ) + .into_any_element() } } @@ -314,9 +380,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_onboarding(cx), - Some(proto::Plan::ZedProTrial) => self.render_trial_onboarding(cx), - Some(proto::Plan::ZedPro) => self.render_pro_plan_onboarding(cx), + 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), } } else { self.render_accept_terms_of_service() @@ -339,18 +405,17 @@ impl Component for ZedAiOnboarding { plan: Option, account_too_young: bool, ) -> AnyElement { - div() - .w(px(800.)) - .child(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(|_, _| {}), - }) - .into_any_element() + 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() } Some( @@ -368,7 +433,7 @@ impl Component for ZedAiOnboarding { ), single_example( "Account too young", - onboarding(SignInStatus::SignedIn, true, None, true), + onboarding(SignInStatus::SignedIn, false, None, true), ), single_example( "Free Plan", diff --git a/crates/ai_onboarding/src/young_account_banner.rs b/crates/ai_onboarding/src/young_account_banner.rs index f6e1446fd05cc719e8a6674ae9246084185162c7..1e1ed3a8653d0cb39955fb54b10dd1dc3937ceb3 100644 --- a/crates/ai_onboarding/src/young_account_banner.rs +++ b/crates/ai_onboarding/src/young_account_banner.rs @@ -6,7 +6,7 @@ pub struct YoungAccountBanner; impl RenderOnce for YoungAccountBanner { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - const YOUNG_ACCOUNT_DISCLAIMER: &str = "Given your GitHub account was created less than 30 days ago, we cannot put you in the Free plan or offer you a free trial of the Pro plan. We hope you'll understand, as this is unfortunately required to prevent abuse of our service. To continue, upgrade to Pro or use your own API keys for other providers."; + const YOUNG_ACCOUNT_DISCLAIMER: &str = "To prevent abuse of our service, we cannot offer plans to GitHub accounts created fewer than 30 days ago. To request an exception, reach out to billing@zed.dev."; let label = div() .w_full() diff --git a/crates/client/src/zed_urls.rs b/crates/client/src/zed_urls.rs index e36f5f65dae646bae361eea34e582c308ea64dae..693c7bf836330fc8c6cd36ca72ee862a9e2b865b 100644 --- a/crates/client/src/zed_urls.rs +++ b/crates/client/src/zed_urls.rs @@ -30,3 +30,8 @@ pub fn start_trial_url(cx: &App) -> String { pub fn upgrade_to_zed_pro_url(cx: &App) -> String { format!("{server_url}/account/upgrade", server_url = server_url(cx)) } + +/// Returns the URL to Zed's terms of service. +pub fn terms_of_service(cx: &App) -> String { + format!("{server_url}/terms-of-service", server_url = server_url(cx)) +} diff --git a/crates/language_model/src/registry.rs b/crates/language_model/src/registry.rs index 840fda38dec4714a32f3397a28dd2d116bb67f5d..6e8e8e91088bf3197c523c75209c56f9edda82be 100644 --- a/crates/language_model/src/registry.rs +++ b/crates/language_model/src/registry.rs @@ -206,8 +206,8 @@ impl LanguageModelRegistry { None } - /// Check that we have at least one provider that is authenticated. - fn has_authenticated_provider(&self, cx: &App) -> bool { + /// Returns `true` if at least one provider that is authenticated. + pub fn has_authenticated_provider(&self, cx: &App) -> bool { self.providers.values().any(|p| p.is_authenticated(cx)) } diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index 736107570b395c3014e25dce1cbe21737de9e96b..fac88107143919437c1851b8417210343bb44b0c 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -1140,19 +1140,19 @@ impl RenderOnce for ZedAiConfiguration { let is_pro = self.plan == Some(proto::Plan::ZedPro); let subscription_text = match (self.plan, self.subscription_period) { (Some(proto::Plan::ZedPro), Some(_)) => { - "You have access to Zed's hosted LLMs through your Pro subscription." + "You have access to Zed's hosted models through your Pro subscription." } (Some(proto::Plan::ZedProTrial), Some(_)) => { - "You have access to Zed's hosted LLMs through your Pro trial." + "You have access to Zed's hosted models through your Pro trial." } (Some(proto::Plan::Free), Some(_)) => { - "You have basic access to Zed's hosted LLMs through the Free plan." + "You have basic access to Zed's hosted models through the Free plan." } _ => { if self.eligible_for_trial { - "Subscribe for access to Zed's hosted LLMs. Start with a 14 day free trial." + "Subscribe for access to Zed's hosted models. Start with a 14 day free trial." } else { - "Subscribe for access to Zed's hosted LLMs." + "Subscribe for access to Zed's hosted models." } } }; @@ -1166,7 +1166,7 @@ impl RenderOnce for ZedAiConfiguration { Button::new("start_trial", "Start 14-day Free Pro Trial") .style(ui::ButtonStyle::Tinted(ui::TintColor::Accent)) .full_width() - .on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))) + .on_click(|_, _, cx| cx.open_url(&zed_urls::start_trial_url(cx))) .into_any_element() } else { Button::new("upgrade", "Upgrade to Pro") From 2b671a46f23e2f5d1393e58973f7305e6b78494d Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 22 Jul 2025 02:39:22 -0300 Subject: [PATCH 244/658] ai onboarding: Don't show API keys section if user is already in Pro (#34867) Release Notes: - N/A --- crates/agent_ui/src/message_editor.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index 69eae982f8008849d02e4b695c4b00c08cff0b46..78037532925d8214b3f6fe8c780039e3e590a7f7 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -1657,11 +1657,16 @@ impl Render for MessageEditor { let line_height = TextSize::Small.rems(cx).to_pixels(window.rem_size()) * 1.5; - let enrolled_in_trial = matches!( + let in_pro_trial = matches!( self.user_store.read(cx).current_plan(), Some(proto::Plan::ZedProTrial) ); + let pro_user = matches!( + self.user_store.read(cx).current_plan(), + Some(proto::Plan::ZedPro) + ); + let configured_providers: Vec<(IconName, SharedString)> = LanguageModelRegistry::read_global(cx) .providers() @@ -1676,9 +1681,10 @@ impl Render for MessageEditor { v_flex() .size_full() .bg(cx.theme().colors().panel_background) - .when(has_existing_providers && !enrolled_in_trial, |this| { - this.child(cx.new(ApiKeysWithProviders::new)) - }) + .when( + has_existing_providers && !in_pro_trial && !pro_user, + |this| this.child(cx.new(ApiKeysWithProviders::new)), + ) .when(changed_buffers.len() > 0, |parent| { parent.child(self.render_edits_bar(&changed_buffers, window, cx)) }) From 87014cec71335c6ae20fbe3fe6299f95542103c4 Mon Sep 17 00:00:00 2001 From: Bret Comnes <166301+bcomnes@users.noreply.github.com> Date: Tue, 22 Jul 2025 02:50:26 -0700 Subject: [PATCH 245/658] theme: Add `panel.overlay_background` and `panel.overlay_hover` (#34655) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In https://github.com/zed-industries/zed/pull/33994 sticky scroll was added to project_panel. I love this feature! This introduces a new element layering not seen before. On themes that use transparency, the overlapping elements can make it difficult to read project panel entries. This PR introduces a new selector: ~~panel.sticky_entry.background~~ `panel.overlay_background` This selector lets you set the background of entries when they become sticky. Closes https://github.com/zed-industries/zed/issues/34654 Before: Screenshot 2025-07-17 at 10 19 11 AM After: Screenshot 2025-07-17 at 11 46 57 AM Screenshot 2025-07-17 at 11 39 57 AM Screenshot 2025-07-17 at 11 39 29 AM Release Notes: - Add `panel.sticky_entry.background` theme selector for modifying project panel entries when they become sticky when scrolling and overlap with entries below them. --------- Co-authored-by: Smit Barmase --- crates/project_panel/src/project_panel.rs | 16 ++++++++--- crates/theme/src/default_colors.rs | 4 +++ crates/theme/src/fallback_themes.rs | 9 ++++-- crates/theme/src/schema.rs | 34 +++++++++++++++++------ crates/theme/src/styles/colors.rs | 10 +++++++ 5 files changed, 58 insertions(+), 15 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index b6fdcd6fa5bac837df5cab8aad3b9c69cd1613d8..44f4e8985ad90462f3c68b21e7f12274725e3673 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -384,12 +384,20 @@ struct ItemColors { focused: Hsla, } -fn get_item_color(cx: &App) -> ItemColors { +fn get_item_color(is_sticky: bool, cx: &App) -> ItemColors { let colors = cx.theme().colors(); ItemColors { - default: colors.panel_background, - hover: colors.element_hover, + default: if is_sticky { + colors.panel_overlay_background + } else { + colors.panel_background + }, + hover: if is_sticky { + colors.panel_overlay_hover + } else { + colors.element_hover + }, marked: colors.element_selected, focused: colors.panel_focused_border, drag_over: colors.drop_target_background, @@ -3903,7 +3911,7 @@ impl ProjectPanel { let filename_text_color = details.filename_text_color; let diagnostic_severity = details.diagnostic_severity; - let item_colors = get_item_color(cx); + let item_colors = get_item_color(is_sticky, cx); let canonical_path = details .canonical_path diff --git a/crates/theme/src/default_colors.rs b/crates/theme/src/default_colors.rs index 3424e0fe04cdbc11544fa81018edba4ff2b357c1..1c3f48b548d3fdd4a2a554b476afaa08dcbae150 100644 --- a/crates/theme/src/default_colors.rs +++ b/crates/theme/src/default_colors.rs @@ -83,6 +83,8 @@ impl ThemeColors { panel_indent_guide: neutral().light_alpha().step_5(), panel_indent_guide_hover: neutral().light_alpha().step_6(), panel_indent_guide_active: neutral().light_alpha().step_6(), + panel_overlay_background: neutral().light().step_2(), + panel_overlay_hover: neutral().light_alpha().step_4(), pane_focused_border: blue().light().step_5(), pane_group_border: neutral().light().step_6(), scrollbar_thumb_background: neutral().light_alpha().step_3(), @@ -206,6 +208,8 @@ impl ThemeColors { panel_indent_guide: neutral().dark_alpha().step_4(), panel_indent_guide_hover: neutral().dark_alpha().step_6(), panel_indent_guide_active: neutral().dark_alpha().step_6(), + panel_overlay_background: neutral().dark().step_2(), + panel_overlay_hover: neutral().dark_alpha().step_4(), pane_focused_border: blue().dark().step_5(), pane_group_border: neutral().dark().step_6(), scrollbar_thumb_background: neutral().dark_alpha().step_3(), diff --git a/crates/theme/src/fallback_themes.rs b/crates/theme/src/fallback_themes.rs index 5e9967d4603a5bac8c9f1a7e461c7319f52f82d7..4d77dd5d81dfc45427bda4034ff7a2085dbcb489 100644 --- a/crates/theme/src/fallback_themes.rs +++ b/crates/theme/src/fallback_themes.rs @@ -59,6 +59,7 @@ pub(crate) fn zed_default_dark() -> Theme { let bg = hsla(215. / 360., 12. / 100., 15. / 100., 1.); let editor = hsla(220. / 360., 12. / 100., 18. / 100., 1.); let elevated_surface = hsla(225. / 360., 12. / 100., 17. / 100., 1.); + let hover = hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 1.0); let blue = hsla(207.8 / 360., 81. / 100., 66. / 100., 1.0); let gray = hsla(218.8 / 360., 10. / 100., 40. / 100., 1.0); @@ -108,14 +109,14 @@ pub(crate) fn zed_default_dark() -> Theme { surface_background: bg, background: bg, element_background: hsla(223.0 / 360., 13. / 100., 21. / 100., 1.0), - element_hover: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 1.0), + element_hover: hover, element_active: hsla(220.0 / 360., 11.8 / 100., 20.0 / 100., 1.0), element_selected: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0), 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), ghost_element_background: SystemColors::default().transparent, - ghost_element_hover: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 1.0), + ghost_element_hover: hover, ghost_element_active: hsla(220.0 / 360., 11.8 / 100., 20.0 / 100., 1.0), ghost_element_selected: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0), ghost_element_disabled: SystemColors::default().transparent, @@ -202,10 +203,12 @@ pub(crate) fn zed_default_dark() -> Theme { panel_indent_guide: hsla(228. / 360., 8. / 100., 25. / 100., 1.), panel_indent_guide_hover: hsla(225. / 360., 13. / 100., 12. / 100., 1.), panel_indent_guide_active: hsla(225. / 360., 13. / 100., 12. / 100., 1.), + panel_overlay_background: bg, + panel_overlay_hover: hover, pane_focused_border: blue, pane_group_border: hsla(225. / 360., 13. / 100., 12. / 100., 1.), scrollbar_thumb_background: gpui::transparent_black(), - scrollbar_thumb_hover_background: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 1.0), + scrollbar_thumb_hover_background: hover, scrollbar_thumb_active_background: hsla( 225.0 / 360., 11.8 / 100., diff --git a/crates/theme/src/schema.rs b/crates/theme/src/schema.rs index bed25d0c054fc4e1767cc852597db13dc2cb434c..bfa2adcedf73ec9d51c25d30785b1e81cd83173e 100644 --- a/crates/theme/src/schema.rs +++ b/crates/theme/src/schema.rs @@ -351,6 +351,12 @@ pub struct ThemeColorsContent { #[serde(rename = "panel.indent_guide_active")] pub panel_indent_guide_active: Option, + #[serde(rename = "panel.overlay_background")] + pub panel_overlay_background: Option, + + #[serde(rename = "panel.overlay_hover")] + pub panel_overlay_hover: Option, + #[serde(rename = "pane.focused_border")] pub pane_focused_border: Option, @@ -674,6 +680,14 @@ impl ThemeColorsContent { .scrollbar_thumb_border .as_ref() .and_then(|color| try_parse_color(color).ok()); + let element_hover = self + .element_hover + .as_ref() + .and_then(|color| try_parse_color(color).ok()); + let panel_background = self + .panel_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()); ThemeColorsRefinement { border, border_variant: self @@ -712,10 +726,7 @@ impl ThemeColorsContent { .element_background .as_ref() .and_then(|color| try_parse_color(color).ok()), - element_hover: self - .element_hover - .as_ref() - .and_then(|color| try_parse_color(color).ok()), + element_hover, element_active: self .element_active .as_ref() @@ -832,10 +843,7 @@ impl ThemeColorsContent { .search_match_background .as_ref() .and_then(|color| try_parse_color(color).ok()), - panel_background: self - .panel_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), + panel_background, panel_focused_border: self .panel_focused_border .as_ref() @@ -852,6 +860,16 @@ impl ThemeColorsContent { .panel_indent_guide_active .as_ref() .and_then(|color| try_parse_color(color).ok()), + panel_overlay_background: self + .panel_overlay_background + .as_ref() + .and_then(|color| try_parse_color(color).ok()) + .or(panel_background), + panel_overlay_hover: self + .panel_overlay_hover + .as_ref() + .and_then(|color| try_parse_color(color).ok()) + .or(element_hover), pane_focused_border: self .pane_focused_border .as_ref() diff --git a/crates/theme/src/styles/colors.rs b/crates/theme/src/styles/colors.rs index 7c5270e3612dfbe1fb6b1ec45dc4787dac0e9463..aab11803f4d810453f5bfc286624ea8e4efb4a61 100644 --- a/crates/theme/src/styles/colors.rs +++ b/crates/theme/src/styles/colors.rs @@ -131,6 +131,12 @@ pub struct ThemeColors { pub panel_indent_guide: Hsla, pub panel_indent_guide_hover: Hsla, pub panel_indent_guide_active: Hsla, + + /// The color of the overlay surface on top of panel. + pub panel_overlay_background: Hsla, + /// The color of the overlay surface on top of panel when hovered over. + pub panel_overlay_hover: Hsla, + pub pane_focused_border: Hsla, pub pane_group_border: Hsla, /// The color of the scrollbar thumb. @@ -326,6 +332,8 @@ pub enum ThemeColorField { PanelIndentGuide, PanelIndentGuideHover, PanelIndentGuideActive, + PanelOverlayBackground, + PanelOverlayHover, PaneFocusedBorder, PaneGroupBorder, ScrollbarThumbBackground, @@ -438,6 +446,8 @@ impl ThemeColors { ThemeColorField::PanelIndentGuide => self.panel_indent_guide, ThemeColorField::PanelIndentGuideHover => self.panel_indent_guide_hover, ThemeColorField::PanelIndentGuideActive => self.panel_indent_guide_active, + ThemeColorField::PanelOverlayBackground => self.panel_overlay_background, + ThemeColorField::PanelOverlayHover => self.panel_overlay_hover, ThemeColorField::PaneFocusedBorder => self.pane_focused_border, ThemeColorField::PaneGroupBorder => self.pane_group_border, ThemeColorField::ScrollbarThumbBackground => self.scrollbar_thumb_background, From 3a651c546b13c7b9a2a25a551975f5313b7e3793 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Tue, 22 Jul 2025 12:12:07 +0200 Subject: [PATCH 246/658] context_server: Change command string field to PathBuf (#34873) Release Notes: - N/A --- .../configure_context_server_modal.rs | 8 +++++--- crates/context_server/src/context_server.rs | 4 ++-- crates/extension/src/types.rs | 4 ++-- .../src/wasm_host/wit/since_v0_6_0.rs | 4 ++-- crates/project/src/context_server_store.rs | 18 +++++++++--------- .../src/context_server_store/extension.rs | 5 +---- crates/project/src/project_settings.rs | 2 +- 7 files changed, 22 insertions(+), 23 deletions(-) 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 9e5f6e09c82489dd4ccdc89f188e962ceeec596d..06d035d836853068c8ed402ee0e85ff85d9af6b2 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 @@ -1,4 +1,5 @@ use std::{ + path::PathBuf, sync::{Arc, Mutex}, time::Duration, }; @@ -188,7 +189,7 @@ fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand) } None => ( "some-mcp-server".to_string(), - "".to_string(), + PathBuf::new(), "[]".to_string(), "{}".to_string(), ), @@ -199,13 +200,14 @@ fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand) /// The name of your MCP server "{name}": {{ /// The command which runs the MCP server - "command": "{command}", + "command": "{}", /// The arguments to pass to the MCP server "args": {args}, /// The environment variables to set "env": {env} }} -}}"# +}}"#, + command.display() ) } diff --git a/crates/context_server/src/context_server.rs b/crates/context_server/src/context_server.rs index 807b17f1ca64fcc253084d553b8ec700c60fb74e..f2517feb27e9ceab2187e0f86bc752e14de5d63f 100644 --- a/crates/context_server/src/context_server.rs +++ b/crates/context_server/src/context_server.rs @@ -6,9 +6,9 @@ pub mod test; pub mod transport; pub mod types; -use std::fmt::Display; use std::path::Path; use std::sync::Arc; +use std::{fmt::Display, path::PathBuf}; use anyhow::Result; use client::Client; @@ -31,7 +31,7 @@ impl Display for ContextServerId { #[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)] pub struct ContextServerCommand { #[serde(rename = "command")] - pub path: String, + pub path: PathBuf, pub args: Vec, pub env: Option>, } diff --git a/crates/extension/src/types.rs b/crates/extension/src/types.rs index cb24e5077b839a0c5ded24c084fbdd7c1cbeab7c..ed9eb2ec2fb96a3b19125355be90e6ba7a5a6e90 100644 --- a/crates/extension/src/types.rs +++ b/crates/extension/src/types.rs @@ -3,7 +3,7 @@ mod dap; mod lsp; mod slash_command; -use std::ops::Range; +use std::{ops::Range, path::PathBuf}; use util::redact::should_redact; @@ -18,7 +18,7 @@ pub type EnvVars = Vec<(String, String)>; /// A command. pub struct Command { /// The command to execute. - pub command: String, + pub command: PathBuf, /// The arguments to pass to the command. pub args: Vec, /// The environment variables to set for the command. 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 ced2ea9c677022e95f106ac6ba0543303fe5a372..d25328ee7f6744528e606e5043ff51fbc0896aee 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 @@ -75,7 +75,7 @@ impl From for std::ops::Range { impl From for extension::Command { fn from(value: Command) -> Self { Self { - command: value.command, + command: value.command.into(), args: value.args, env: value.env, } @@ -958,7 +958,7 @@ impl ExtensionImports for WasmState { command, } => Ok(serde_json::to_string(&settings::ContextServerSettings { command: Some(settings::CommandSettings { - path: Some(command.path), + path: command.path.to_str().map(|path| path.to_string()), arguments: Some(command.args), env: command.env.map(|env| env.into_iter().collect()), }), diff --git a/crates/project/src/context_server_store.rs b/crates/project/src/context_server_store.rs index fd31e638d4bf7774af83d430dca232d1ade74f01..ceec0c0a52b70cb68f0fb41d8c415a13e39e8b85 100644 --- a/crates/project/src/context_server_store.rs +++ b/crates/project/src/context_server_store.rs @@ -610,7 +610,7 @@ mod tests { use context_server::test::create_fake_transport; use gpui::{AppContext, TestAppContext, UpdateGlobal as _}; use serde_json::json; - use std::{cell::RefCell, rc::Rc}; + use std::{cell::RefCell, path::PathBuf, rc::Rc}; use util::path; #[gpui::test] @@ -931,7 +931,7 @@ mod tests { ContextServerSettings::Custom { enabled: true, command: ContextServerCommand { - path: "somebinary".to_string(), + path: "somebinary".into(), args: vec!["arg".to_string()], env: None, }, @@ -971,7 +971,7 @@ mod tests { ContextServerSettings::Custom { enabled: true, command: ContextServerCommand { - path: "somebinary".to_string(), + path: "somebinary".into(), args: vec!["anotherArg".to_string()], env: None, }, @@ -1053,7 +1053,7 @@ mod tests { ContextServerSettings::Custom { enabled: true, command: ContextServerCommand { - path: "somebinary".to_string(), + path: "somebinary".into(), args: vec!["arg".to_string()], env: None, }, @@ -1104,7 +1104,7 @@ mod tests { ContextServerSettings::Custom { enabled: false, command: ContextServerCommand { - path: "somebinary".to_string(), + path: "somebinary".into(), args: vec!["arg".to_string()], env: None, }, @@ -1132,7 +1132,7 @@ mod tests { ContextServerSettings::Custom { enabled: true, command: ContextServerCommand { - path: "somebinary".to_string(), + path: "somebinary".into(), args: vec!["arg".to_string()], env: None, }, @@ -1184,7 +1184,7 @@ mod tests { ContextServerSettings::Custom { enabled: true, command: ContextServerCommand { - path: "somebinary".to_string(), + path: "somebinary".into(), args: vec!["arg".to_string()], env: None, }, @@ -1256,11 +1256,11 @@ mod tests { } struct FakeContextServerDescriptor { - path: String, + path: PathBuf, } impl FakeContextServerDescriptor { - fn new(path: impl Into) -> Self { + fn new(path: impl Into) -> Self { Self { path: path.into() } } } diff --git a/crates/project/src/context_server_store/extension.rs b/crates/project/src/context_server_store/extension.rs index 1eaecd987dd51158fc2f505c1ae9b0c8fcc076a3..1eb0fe7da129ba9dbd3ee640cb6e02474a3990b6 100644 --- a/crates/project/src/context_server_store/extension.rs +++ b/crates/project/src/context_server_store/extension.rs @@ -61,10 +61,7 @@ impl registry::ContextServerDescriptor for ContextServerDescriptor { let mut command = extension .context_server_command(id.clone(), extension_project.clone()) .await?; - command.command = extension - .path_from_extension(command.command.as_ref()) - .to_string_lossy() - .to_string(); + command.command = extension.path_from_extension(&command.command); log::info!("loaded command for context server {id}: {command:?}"); diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index a85d90fe33575ecd15fd7f55166b35e2489e222b..20be7fef85c79910904fe577f0691fba57424d45 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -581,7 +581,7 @@ impl Settings for ProjectSettings { #[derive(Deserialize)] struct VsCodeContextServerCommand { - command: String, + command: PathBuf, args: Option>, env: Option>, // note: we don't support envFile and type From c7158f0bd7ec3324fe299b4cf3e6b4e34a492436 Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon Date: Tue, 22 Jul 2025 14:23:50 +0300 Subject: [PATCH 247/658] Improve distinguishing user from agent edits (#34716) We no longer rely on the `author` field to tell if a change was made by the user or the agent. The `author` can be set to `User` in many situations that are not really user-made edits, such as saving a file, accepting a change, auto-formatting, and more. I started tracking and fixing some of these cases, but found that inspecting changes in `diff_base` is a more reliable method. Also, we no longer show empty diffs. For example, if the user adds a line and then removes the same line, the final diff is empty, even though the buffer is marked as user-changed. Now we won't show such edit. There are still some issues to address: - When a user edits within an unaccepted agent-written block, this change becomes a part of the agent's edit. Rejecting this block will lose user edits. It won't be displayed in project notifications, either. - Accepting an agent block counts as a user-made edit. - Agent start to call `project_notifications` tool after seeing enough auto-calls. Release Notes: - N/A --- crates/agent/src/thread.rs | 20 ++++--- crates/assistant_tool/src/action_log.rs | 56 +++++++++++--------- crates/assistant_tools/src/edit_file_tool.rs | 3 ++ 3 files changed, 42 insertions(+), 37 deletions(-) diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index e50763535a461bef6769b4f7c1aadeb2f219b904..f8a78276154f3eec89e30acdb808b7c41242a1b1 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -47,7 +47,7 @@ use std::{ time::{Duration, Instant}, }; use thiserror::Error; -use util::{ResultExt as _, debug_panic, post_inc}; +use util::{ResultExt as _, post_inc}; use uuid::Uuid; use zed_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit}; @@ -1582,23 +1582,21 @@ impl Thread { model: Arc, cx: &mut App, ) -> Option { - let action_log = self.action_log.read(cx); - - if !action_log.has_unnotified_user_edits() { - return None; - } - // Represent notification as a simulated `project_notifications` tool call let tool_name = Arc::from("project_notifications"); - let Some(tool) = self.tools.read(cx).tool(&tool_name, cx) else { - debug_panic!("`project_notifications` tool not found"); - return None; - }; + let tool = self.tools.read(cx).tool(&tool_name, cx)?; if !self.profile.is_tool_enabled(tool.source(), tool.name(), cx) { return None; } + if self + .action_log + .update(cx, |log, cx| log.unnotified_user_edits(cx).is_none()) + { + return None; + } + let input = serde_json::json!({}); let request = Arc::new(LanguageModelRequest::default()); // unused let window = None; diff --git a/crates/assistant_tool/src/action_log.rs b/crates/assistant_tool/src/action_log.rs index ecbbcc785e730e3a7ca60f014f641d555e696b9e..672c048872c954da977ce9a1c64d50835f662150 100644 --- a/crates/assistant_tool/src/action_log.rs +++ b/crates/assistant_tool/src/action_log.rs @@ -51,23 +51,13 @@ impl ActionLog { Some(self.tracked_buffers.get(buffer)?.snapshot.clone()) } - pub fn has_unnotified_user_edits(&self) -> bool { - self.tracked_buffers - .values() - .any(|tracked| tracked.has_unnotified_user_edits) - } - /// Return a unified diff patch with user edits made since last read or notification pub fn unnotified_user_edits(&self, cx: &Context) -> Option { - if !self.has_unnotified_user_edits() { - return None; - } - - let unified_diff = self + let diffs = self .tracked_buffers .values() .filter_map(|tracked| { - if !tracked.has_unnotified_user_edits { + if !tracked.may_have_unnotified_user_edits { return None; } @@ -95,9 +85,13 @@ impl ActionLog { Some(result) }) - .collect::>() - .join("\n\n"); + .collect::>(); + + if diffs.is_empty() { + return None; + } + let unified_diff = diffs.join("\n\n"); Some(unified_diff) } @@ -106,7 +100,7 @@ impl ActionLog { pub fn flush_unnotified_user_edits(&mut self, cx: &Context) -> Option { let patch = self.unnotified_user_edits(cx); self.tracked_buffers.values_mut().for_each(|tracked| { - tracked.has_unnotified_user_edits = false; + tracked.may_have_unnotified_user_edits = false; tracked.last_seen_base = tracked.diff_base.clone(); }); patch @@ -185,7 +179,7 @@ impl ActionLog { version: buffer.read(cx).version(), diff, diff_update: diff_update_tx, - has_unnotified_user_edits: false, + may_have_unnotified_user_edits: false, _open_lsp_handle: open_lsp_handle, _maintain_diff: cx.spawn({ let buffer = buffer.clone(); @@ -337,27 +331,34 @@ impl ActionLog { let new_snapshot = buffer_snapshot.clone(); let unreviewed_edits = tracked_buffer.unreviewed_edits.clone(); let edits = diff_snapshots(&old_snapshot, &new_snapshot); - if let ChangeAuthor::User = author - && !edits.is_empty() - { - tracked_buffer.has_unnotified_user_edits = true; - } + let mut has_user_changes = false; async move { if let ChangeAuthor::User = author { - apply_non_conflicting_edits( + has_user_changes = apply_non_conflicting_edits( &unreviewed_edits, edits, &mut base_text, new_snapshot.as_rope(), ); } - (Arc::new(base_text.to_string()), base_text) + + (Arc::new(base_text.to_string()), base_text, has_user_changes) } }); anyhow::Ok(rebase) })??; - let (new_base_text, new_diff_base) = rebase.await; + let (new_base_text, new_diff_base, has_user_changes) = rebase.await; + + this.update(cx, |this, _| { + let tracked_buffer = this + .tracked_buffers + .get_mut(buffer) + .context("buffer not tracked") + .unwrap(); + tracked_buffer.may_have_unnotified_user_edits |= has_user_changes; + })?; + Self::update_diff( this, buffer, @@ -829,11 +830,12 @@ fn apply_non_conflicting_edits( edits: Vec>, old_text: &mut Rope, new_text: &Rope, -) { +) -> bool { let mut old_edits = patch.edits().iter().cloned().peekable(); let mut new_edits = edits.into_iter().peekable(); let mut applied_delta = 0i32; let mut rebased_delta = 0i32; + let mut has_made_changes = false; while let Some(mut new_edit) = new_edits.next() { let mut conflict = false; @@ -883,8 +885,10 @@ fn apply_non_conflicting_edits( &new_text.chunks_in_range(new_bytes).collect::(), ); applied_delta += new_edit.new_len() as i32 - new_edit.old_len() as i32; + has_made_changes = true; } } + has_made_changes } fn diff_snapshots( @@ -958,7 +962,7 @@ struct TrackedBuffer { diff: Entity, snapshot: text::BufferSnapshot, diff_update: mpsc::UnboundedSender<(ChangeAuthor, text::BufferSnapshot)>, - has_unnotified_user_edits: bool, + may_have_unnotified_user_edits: bool, _open_lsp_handle: OpenLspBufferHandle, _maintain_diff: Task<()>, _subscription: Subscription, diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index 0423f56145bc484108ff958419353e2378a3779a..6413677bd9178c40259ca56fa00491478efda30c 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -278,6 +278,9 @@ impl Tool for EditFileTool { .unwrap_or(false); 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()]), From a3850b3d38928e87f01b67d43755b5a5cc410605 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 22 Jul 2025 09:42:34 -0300 Subject: [PATCH 248/658] agent: Add `use_modifier_to_send` section in the settings view (#34866) This PR also converts all of these switch-based settings to use the new `SwitchField` component, introduced in https://github.com/zed-industries/zed/pull/34713. Release Notes: - agent: Added the ability to change the `use_modifier_to_send` setting from the agent panel settings UI. --- crates/agent_ui/src/agent_configuration.rs | 162 ++++++++------------- crates/ui/src/components/toggle.rs | 11 +- 2 files changed, 69 insertions(+), 104 deletions(-) diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 0697f5dee758f0a5c4d4f530e45394fb79e14f3a..b5ad6ba37c8625e3b8714b1a59fa8a1332871cd6 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -28,7 +28,7 @@ use proto::Plan; use settings::{Settings, update_settings_file}; use ui::{ Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu, - Scrollbar, ScrollbarState, Switch, SwitchColor, Tooltip, prelude::*, + Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*, }; use util::ResultExt as _; use workspace::Workspace; @@ -330,119 +330,74 @@ impl AgentConfiguration { fn render_command_permission(&mut self, cx: &mut Context) -> impl IntoElement { let always_allow_tool_actions = AgentSettings::get_global(cx).always_allow_tool_actions; + let fs = self.fs.clone(); - h_flex() - .gap_4() - .justify_between() - .flex_wrap() - .child( - v_flex() - .gap_0p5() - .max_w_5_6() - .child(Label::new("Allow running editing tools without asking for confirmation")) - .child( - Label::new( - "The agent can perform potentially destructive actions without asking for your confirmation.", - ) - .color(Color::Muted), - ), - ) - .child( - Switch::new( - "always-allow-tool-actions-switch", - always_allow_tool_actions.into(), - ) - .color(SwitchColor::Accent) - .on_click({ - let fs = self.fs.clone(); - move |state, _window, cx| { - let allow = state == &ToggleState::Selected; - update_settings_file::( - fs.clone(), - cx, - move |settings, _| { - settings.set_always_allow_tool_actions(allow); - }, - ); - } - }), - ) + SwitchField::new( + "single-file-review", + "Enable single-file agent reviews", + "Agent edits are also displayed in single-file editors for review.", + always_allow_tool_actions, + move |state, _window, cx| { + let allow = state == &ToggleState::Selected; + update_settings_file::(fs.clone(), cx, move |settings, _| { + settings.set_always_allow_tool_actions(allow); + }); + }, + ) } fn render_single_file_review(&mut self, cx: &mut Context) -> impl IntoElement { let single_file_review = AgentSettings::get_global(cx).single_file_review; + let fs = self.fs.clone(); - h_flex() - .gap_4() - .justify_between() - .flex_wrap() - .child( - v_flex() - .gap_0p5() - .max_w_5_6() - .child(Label::new("Enable single-file agent reviews")) - .child( - Label::new( - "Agent edits are also displayed in single-file editors for review.", - ) - .color(Color::Muted), - ), - ) - .child( - Switch::new("single-file-review-switch", single_file_review.into()) - .color(SwitchColor::Accent) - .on_click({ - let fs = self.fs.clone(); - move |state, _window, cx| { - let allow = state == &ToggleState::Selected; - update_settings_file::( - fs.clone(), - cx, - move |settings, _| { - settings.set_single_file_review(allow); - }, - ); - } - }), - ) + SwitchField::new( + "single-file-review", + "Enable single-file agent reviews", + "Agent edits are also displayed in single-file editors for review.", + single_file_review, + move |state, _window, cx| { + let allow = state == &ToggleState::Selected; + update_settings_file::(fs.clone(), cx, move |settings, _| { + settings.set_single_file_review(allow); + }); + }, + ) } fn render_sound_notification(&mut self, cx: &mut Context) -> impl IntoElement { let play_sound_when_agent_done = AgentSettings::get_global(cx).play_sound_when_agent_done; + let fs = self.fs.clone(); - h_flex() - .gap_4() - .justify_between() - .flex_wrap() - .child( - v_flex() - .gap_0p5() - .max_w_5_6() - .child(Label::new("Play sound when finished generating")) - .child( - Label::new( - "Hear a notification sound when the agent is done generating changes or needs your input.", - ) - .color(Color::Muted), - ), - ) - .child( - Switch::new("play-sound-notification-switch", play_sound_when_agent_done.into()) - .color(SwitchColor::Accent) - .on_click({ - let fs = self.fs.clone(); - move |state, _window, cx| { - let allow = state == &ToggleState::Selected; - update_settings_file::( - fs.clone(), - cx, - move |settings, _| { - settings.set_play_sound_when_agent_done(allow); - }, - ); - } - }), - ) + SwitchField::new( + "sound-notification", + "Play sound when finished generating", + "Hear a notification sound when the agent is done generating changes or needs your input.", + play_sound_when_agent_done, + move |state, _window, cx| { + let allow = state == &ToggleState::Selected; + update_settings_file::(fs.clone(), cx, move |settings, _| { + settings.set_play_sound_when_agent_done(allow); + }); + }, + ) + } + + fn render_modifier_to_send(&mut self, cx: &mut Context) -> impl IntoElement { + let use_modifier_to_send = AgentSettings::get_global(cx).use_modifier_to_send; + let fs = self.fs.clone(); + + 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.", + use_modifier_to_send, + move |state, _window, cx| { + let allow = state == &ToggleState::Selected; + update_settings_file::(fs.clone(), cx, move |settings, _| { + settings.set_use_modifier_to_send(allow); + }); + }, + ) } fn render_general_settings_section(&mut self, cx: &mut Context) -> impl IntoElement { @@ -456,6 +411,7 @@ impl AgentConfiguration { .child(self.render_command_permission(cx)) .child(self.render_single_file_review(cx)) .child(self.render_sound_notification(cx)) + .child(self.render_modifier_to_send(cx)) } fn render_zed_plan_info(&self, plan: Option, cx: &mut Context) -> impl IntoElement { diff --git a/crates/ui/src/components/toggle.rs b/crates/ui/src/components/toggle.rs index 759b22543408676e239b55303d929e4d2f08dfe3..cf2a56b1c96ee9bd4864ef4264f12b7b68c6592c 100644 --- a/crates/ui/src/components/toggle.rs +++ b/crates/ui/src/components/toggle.rs @@ -588,7 +588,7 @@ impl SwitchField { toggle_state: toggle_state.into(), on_click: Arc::new(on_click), disabled: false, - color: SwitchColor::default(), + color: SwitchColor::Accent, } } @@ -634,6 +634,15 @@ impl RenderOnce for SwitchField { } }), ) + .when(!self.disabled, |this| { + this.on_click({ + let on_click = self.on_click.clone(); + let toggle_state = self.toggle_state; + move |_click, window, cx| { + (on_click)(&toggle_state.inverse(), window, cx); + } + }) + }) } } From 31aab89ab0343b9b6388224f4e8ec2fa3f91fe2e Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Tue, 22 Jul 2025 18:24:06 +0530 Subject: [PATCH 249/658] ai_onboarding: Fix API key onboarding callout not showing properly (#34880) The current onboarding callout for ApiKeysWithProviders is broken. | Before | After | |--------|--------| | CleanShot 2025-07-22 at 16 21
53@2x | CleanShot 2025-07-22 at 16 22
38@2x | cc @danilo-leal Release Notes: - N/A --------- Co-authored-by: Danilo Leal --- .../src/agent_api_keys_onboarding.rs | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/crates/ai_onboarding/src/agent_api_keys_onboarding.rs b/crates/ai_onboarding/src/agent_api_keys_onboarding.rs index 4f9e20cf77ed2685241cd72e5971df26cd918563..883317e5666bb07cc17471cd64764a859ba0ec6b 100644 --- a/crates/ai_onboarding/src/agent_api_keys_onboarding.rs +++ b/crates/ai_onboarding/src/agent_api_keys_onboarding.rs @@ -38,10 +38,6 @@ impl ApiKeysWithProviders { .map(|provider| (provider.icon(), provider.name().0.clone())) .collect() } - - pub fn has_providers(&self) -> bool { - !self.configured_providers.is_empty() - } } impl Render for ApiKeysWithProviders { @@ -53,11 +49,10 @@ impl Render for ApiKeysWithProviders { .map(|(icon, name)| { h_flex() .gap_1p5() - .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted)) + .child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted)) .child(Label::new(name)) }); - - h_flex() + div() .mx_2p5() .p_1() .pb_0() @@ -85,8 +80,24 @@ impl Render for ApiKeysWithProviders { .border_x_1() .border_color(cx.theme().colors().border) .bg(cx.theme().colors().panel_background) - .child(Icon::new(IconName::Info).size(IconSize::XSmall).color(Color::Muted)) - .child(Label::new("Or start now using API keys from your environment for the following providers:").color(Color::Muted)) + .child( + h_flex() + .min_w_0() + .gap_2() + .child( + Icon::new(IconName::Info) + .size(IconSize::XSmall) + .color(Color::Muted) + ) + .child( + div() + .w_full() + .child( + Label::new("Or start now using API keys from your environment for the following providers:") + .color(Color::Muted) + ) + ) + ) .children(configured_providers_list) ) } From 30177b87d6c1fb76f809f470f1ea42249f7ab662 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Tue, 22 Jul 2025 08:51:30 -0500 Subject: [PATCH 250/658] Fix detection of pending bindings when binding in parent context matches (#34856) Broke in #34664 Release Notes: - N/A *or* Added/Fixed/Improved ... Co-authored-by: Conrad --- crates/gpui/src/keymap.rs | 310 ++++++++++++++++++++++++++++++-------- 1 file changed, 247 insertions(+), 63 deletions(-) diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index 174dbc80f02e9c1a16d563bfceff65c2bbb34e64..69700e64dc879fe73f7f79c73a5aa6f5fad2f46f 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -5,7 +5,7 @@ pub use binding::*; pub use context::*; use crate::{Action, Keystroke, is_no_action}; -use collections::HashMap; +use collections::{HashMap, HashSet}; use smallvec::SmallVec; use std::any::TypeId; @@ -167,76 +167,49 @@ impl Keymap { input: &[Keystroke], context_stack: &[KeyContext], ) -> (SmallVec<[(BindingIndex, KeyBinding); 1]>, bool) { - let mut possibilities = self - .bindings() - .enumerate() - .rev() - .filter_map(|(ix, binding)| { - let depth = self.binding_enabled(binding, &context_stack)?; - let pending = binding.match_keystrokes(input)?; - Some((depth, BindingIndex(ix), binding, pending)) - }) - .collect::>(); - possibilities.sort_by(|(depth_a, ix_a, _, _), (depth_b, ix_b, _, _)| { - depth_b.cmp(depth_a).then(ix_b.cmp(ix_a)) - }); + let mut bindings: SmallVec<[(usize, BindingIndex, &KeyBinding); 1]> = SmallVec::new(); + let mut pending_bindings: SmallVec<[(BindingIndex, &KeyBinding); 1]> = SmallVec::new(); - let mut bindings: SmallVec<[(BindingIndex, KeyBinding, usize); 1]> = SmallVec::new(); - - // (pending, is_no_action, depth, keystrokes) - let mut pending_info_opt: Option<(bool, bool, usize, &[Keystroke])> = None; - - 'outer: for (depth, binding_index, binding, pending) in possibilities { - let is_no_action = is_no_action(&*binding.action); - // We only want to consider a binding pending if it has an action - // This, however, means that if we have both a NoAction binding and a binding - // with an action at the same depth, we should still set is_pending to true. - if let Some(pending_info) = pending_info_opt.as_mut() { - let (already_pending, pending_is_no_action, pending_depth, pending_keystrokes) = - *pending_info; - - // We only want to change the pending status if it's not already pending AND if - // the existing pending status was set by a NoAction binding. This avoids a NoAction - // binding erroneously setting the pending status to true when a binding with an action - // already set it to false - // - // We also want to change the pending status if the keystrokes don't match, - // meaning it's different keystrokes than the NoAction that set pending to false - if pending - && !already_pending - && pending_is_no_action - && (pending_depth == depth || pending_keystrokes != binding.keystrokes()) - { - pending_info.0 = !is_no_action; - } - } else { - pending_info_opt = Some(( - pending && !is_no_action, - is_no_action, - depth, - binding.keystrokes(), - )); - } + for (ix, binding) in self.bindings().enumerate().rev() { + let Some(depth) = self.binding_enabled(binding, &context_stack) else { + continue; + }; + let Some(pending) = binding.match_keystrokes(input) else { + continue; + }; if !pending { - bindings.push((binding_index, binding.clone(), depth)); - continue 'outer; + bindings.push((depth, BindingIndex(ix), binding)) + } else { + pending_bindings.push((BindingIndex(ix), binding)) } } - // sort by descending depth - bindings.sort_by(|a, b| a.2.cmp(&b.2).reverse()); - let bindings = bindings + + bindings.sort_by(|(depth_a, ix_a, _), (depth_b, ix_b, _)| { + depth_b.cmp(depth_a).then(ix_b.cmp(ix_a)) + }); + + let bindings: SmallVec<[_; 1]> = bindings .into_iter() - .map_while(|(binding_index, binding, _)| { - if is_no_action(&*binding.action) { - None - } else { - Some((binding_index, binding)) - } - }) + .take_while(|(_, _, binding)| !is_no_action(&*binding.action)) + .map(|(_, ix, binding)| (ix, binding.clone())) .collect(); - (bindings, pending_info_opt.unwrap_or_default().0) + let mut pending = HashSet::default(); + for (ix, binding) in pending_bindings.into_iter().rev() { + if let Some((binding_ix, _)) = bindings.first() + && *binding_ix > ix + { + continue; + } + if is_no_action(&*binding.action) { + pending.remove(&&binding.keystrokes); + continue; + } + pending.insert(&binding.keystrokes); + } + + (bindings, !pending.is_empty()) } /// Check if the given binding is enabled, given a certain key context. @@ -302,6 +275,30 @@ mod tests { ); } + #[test] + fn test_depth_precedence() { + let bindings = [ + KeyBinding::new("ctrl-a", ActionBeta {}, Some("pane")), + KeyBinding::new("ctrl-a", ActionGamma {}, Some("editor")), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings.clone()); + + let (result, pending) = keymap.bindings_for_input( + &[Keystroke::parse("ctrl-a").unwrap()], + &[ + KeyContext::parse("pane").unwrap(), + KeyContext::parse("editor").unwrap(), + ], + ); + + assert!(!pending); + assert_eq!(result.len(), 2); + assert!(result[0].action.partial_eq(&ActionGamma {})); + assert!(result[1].action.partial_eq(&ActionBeta {})); + } + #[test] fn test_keymap_disabled() { let bindings = [ @@ -453,6 +450,193 @@ mod tests { assert_eq!(space_editor.1, true); } + #[test] + fn test_override_multikey() { + let bindings = [ + KeyBinding::new("ctrl-w left", ActionAlpha {}, Some("editor")), + KeyBinding::new("ctrl-w", NoAction {}, Some("editor")), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings.clone()); + + // Ensure `space` results in pending input on the workspace, but not editor + let (result, pending) = keymap.bindings_for_input( + &[Keystroke::parse("ctrl-w").unwrap()], + &[KeyContext::parse("editor").unwrap()], + ); + assert!(result.is_empty()); + assert_eq!(pending, true); + + let bindings = [ + KeyBinding::new("ctrl-w left", ActionAlpha {}, Some("editor")), + KeyBinding::new("ctrl-w", ActionBeta {}, Some("editor")), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings.clone()); + + // Ensure `space` results in pending input on the workspace, but not editor + let (result, pending) = keymap.bindings_for_input( + &[Keystroke::parse("ctrl-w").unwrap()], + &[KeyContext::parse("editor").unwrap()], + ); + assert_eq!(result.len(), 1); + assert_eq!(pending, false); + } + + #[test] + fn test_simple_disable() { + let bindings = [ + KeyBinding::new("ctrl-x", ActionAlpha {}, Some("editor")), + KeyBinding::new("ctrl-x", NoAction {}, Some("editor")), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings.clone()); + + // Ensure `space` results in pending input on the workspace, but not editor + let (result, pending) = keymap.bindings_for_input( + &[Keystroke::parse("ctrl-x").unwrap()], + &[KeyContext::parse("editor").unwrap()], + ); + assert!(result.is_empty()); + assert_eq!(pending, false); + } + + #[test] + fn test_fail_to_disable() { + // disabled at the wrong level + let bindings = [ + KeyBinding::new("ctrl-x", ActionAlpha {}, Some("editor")), + KeyBinding::new("ctrl-x", NoAction {}, Some("workspace")), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings.clone()); + + // Ensure `space` results in pending input on the workspace, but not editor + let (result, pending) = keymap.bindings_for_input( + &[Keystroke::parse("ctrl-x").unwrap()], + &[ + KeyContext::parse("workspace").unwrap(), + KeyContext::parse("editor").unwrap(), + ], + ); + assert_eq!(result.len(), 1); + assert_eq!(pending, false); + } + + #[test] + fn test_disable_deeper() { + let bindings = [ + KeyBinding::new("ctrl-x", ActionAlpha {}, Some("workspace")), + KeyBinding::new("ctrl-x", NoAction {}, Some("editor")), + ]; + + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings.clone()); + + // Ensure `space` results in pending input on the workspace, but not editor + let (result, pending) = keymap.bindings_for_input( + &[Keystroke::parse("ctrl-x").unwrap()], + &[ + KeyContext::parse("workspace").unwrap(), + KeyContext::parse("editor").unwrap(), + ], + ); + assert_eq!(result.len(), 0); + assert_eq!(pending, false); + } + + #[test] + fn test_pending_match_enabled() { + let bindings = [ + KeyBinding::new("ctrl-x", ActionBeta, Some("vim_mode == normal")), + KeyBinding::new("ctrl-x 0", ActionAlpha, Some("Workspace")), + ]; + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings.clone()); + + let matched = keymap.bindings_for_input( + &[Keystroke::parse("ctrl-x")].map(Result::unwrap), + &[ + KeyContext::parse("Workspace"), + KeyContext::parse("Pane"), + KeyContext::parse("Editor vim_mode=normal"), + ] + .map(Result::unwrap), + ); + assert_eq!(matched.0.len(), 1); + assert!(matched.0[0].action.partial_eq(&ActionBeta)); + assert!(matched.1); + } + + #[test] + fn test_pending_match_enabled_extended() { + let bindings = [ + KeyBinding::new("ctrl-x", ActionBeta, Some("vim_mode == normal")), + KeyBinding::new("ctrl-x 0", NoAction, Some("Workspace")), + ]; + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings.clone()); + + let matched = keymap.bindings_for_input( + &[Keystroke::parse("ctrl-x")].map(Result::unwrap), + &[ + KeyContext::parse("Workspace"), + KeyContext::parse("Pane"), + KeyContext::parse("Editor vim_mode=normal"), + ] + .map(Result::unwrap), + ); + assert_eq!(matched.0.len(), 1); + assert!(matched.0[0].action.partial_eq(&ActionBeta)); + assert!(!matched.1); + let bindings = [ + KeyBinding::new("ctrl-x", ActionBeta, Some("Workspace")), + KeyBinding::new("ctrl-x 0", NoAction, Some("vim_mode == normal")), + ]; + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings.clone()); + + let matched = keymap.bindings_for_input( + &[Keystroke::parse("ctrl-x")].map(Result::unwrap), + &[ + KeyContext::parse("Workspace"), + KeyContext::parse("Pane"), + KeyContext::parse("Editor vim_mode=normal"), + ] + .map(Result::unwrap), + ); + assert_eq!(matched.0.len(), 1); + assert!(matched.0[0].action.partial_eq(&ActionBeta)); + assert!(!matched.1); + } + + #[test] + fn test_overriding_prefix() { + let bindings = [ + KeyBinding::new("ctrl-x 0", ActionAlpha, Some("Workspace")), + KeyBinding::new("ctrl-x", ActionBeta, Some("vim_mode == normal")), + ]; + let mut keymap = Keymap::default(); + keymap.add_bindings(bindings.clone()); + + let matched = keymap.bindings_for_input( + &[Keystroke::parse("ctrl-x")].map(Result::unwrap), + &[ + KeyContext::parse("Workspace"), + KeyContext::parse("Pane"), + KeyContext::parse("Editor vim_mode=normal"), + ] + .map(Result::unwrap), + ); + assert_eq!(matched.0.len(), 1); + assert!(matched.0[0].action.partial_eq(&ActionBeta)); + assert!(!matched.1); + } + #[test] fn test_bindings_for_action() { let bindings = [ From 2eeab5b0bf9921814c9d8a1824a3d09d3f5397f4 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Tue, 22 Jul 2025 10:52:04 -0400 Subject: [PATCH 251/658] textmate: Correct context for 'Editor && mode == full' keybinds (#34895) Closes https://github.com/zed-industries/zed/issues/34891 Release Notes: - Fixed textmate keymap misbehaving in certain contexts --- assets/keymaps/macos/textmate.json | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/assets/keymaps/macos/textmate.json b/assets/keymaps/macos/textmate.json index dccb675f6c9734c6da3b797cae199037591085fc..0bd8873b1749d2423d97df480b1aadeb28fe9bab 100644 --- a/assets/keymaps/macos/textmate.json +++ b/assets/keymaps/macos/textmate.json @@ -6,7 +6,7 @@ } }, { - "context": "Editor", + "context": "Editor && mode == full", "bindings": { "cmd-l": "go_to_line::Toggle", "ctrl-shift-d": "editor::DuplicateLineDown", @@ -15,7 +15,12 @@ "cmd-enter": "editor::NewlineBelow", "cmd-alt-enter": "editor::NewlineAbove", "cmd-shift-l": "editor::SelectLine", - "cmd-shift-t": "outline::Toggle", + "cmd-shift-t": "outline::Toggle" + } + }, + { + "context": "Editor", + "bindings": { "alt-backspace": "editor::DeleteToPreviousWordStart", "alt-shift-backspace": "editor::DeleteToNextWordEnd", "alt-delete": "editor::DeleteToNextWordEnd", @@ -39,10 +44,6 @@ "ctrl-_": "editor::ConvertToSnakeCase" } }, - { - "context": "Editor && mode == full", - "bindings": {} - }, { "context": "BufferSearchBar", "bindings": { From 1a76a6b0bfc5145cf51355ff7777b0413063a868 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Tue, 22 Jul 2025 09:59:51 -0500 Subject: [PATCH 252/658] gpui: Simplify `bindings_for_action` API (#34857) Closes #ISSUE Simplifies the API to no longer have a variant that returns indices. The downside is that a few places that used to call `bindings_for_action_with_indices` now compare `Box` instead of indices, however the result is the removal of wrapper code and index handling that is largely unnecessary Release Notes: - N/A *or* Added/Fixed/Improved ... Co-authored-by: Conrad --- crates/gpui/src/key_dispatch.rs | 39 +++++++++--------------- crates/gpui/src/keymap.rs | 53 +++++++++++---------------------- 2 files changed, 31 insertions(+), 61 deletions(-) diff --git a/crates/gpui/src/key_dispatch.rs b/crates/gpui/src/key_dispatch.rs index a290a132c3b5f9fa42e338c28b86de7ded5b10ac..cc6ebb9b08db114c1aabbb2a58296fc3aa8a9949 100644 --- a/crates/gpui/src/key_dispatch.rs +++ b/crates/gpui/src/key_dispatch.rs @@ -50,8 +50,8 @@ /// KeyBinding::new("cmd-k left", pane::SplitLeft, Some("Pane")) /// use crate::{ - Action, ActionRegistry, App, BindingIndex, DispatchPhase, EntityId, FocusId, KeyBinding, - KeyContext, Keymap, Keystroke, ModifiersChangedEvent, Window, + Action, ActionRegistry, App, DispatchPhase, EntityId, FocusId, KeyBinding, KeyContext, Keymap, + Keystroke, ModifiersChangedEvent, Window, }; use collections::FxHashMap; use smallvec::SmallVec; @@ -406,16 +406,11 @@ impl DispatchTree { // methods, but this can't be done very cleanly since keymap must be borrowed. let keymap = self.keymap.borrow(); keymap - .bindings_for_action_with_indices(action) - .filter(|(binding_index, binding)| { - Self::binding_matches_predicate_and_not_shadowed( - &keymap, - *binding_index, - &binding.keystrokes, - context_stack, - ) + .bindings_for_action(action) + .filter(|binding| { + Self::binding_matches_predicate_and_not_shadowed(&keymap, &binding, context_stack) }) - .map(|(_, binding)| binding.clone()) + .cloned() .collect() } @@ -428,28 +423,22 @@ impl DispatchTree { ) -> Option { let keymap = self.keymap.borrow(); keymap - .bindings_for_action_with_indices(action) + .bindings_for_action(action) .rev() - .find_map(|(binding_index, binding)| { - let found = Self::binding_matches_predicate_and_not_shadowed( - &keymap, - binding_index, - &binding.keystrokes, - context_stack, - ); - if found { Some(binding.clone()) } else { None } + .find(|binding| { + Self::binding_matches_predicate_and_not_shadowed(&keymap, &binding, context_stack) }) + .cloned() } fn binding_matches_predicate_and_not_shadowed( keymap: &Keymap, - binding_index: BindingIndex, - keystrokes: &[Keystroke], + binding: &KeyBinding, context_stack: &[KeyContext], ) -> bool { - let (bindings, _) = keymap.bindings_for_input_with_indices(&keystrokes, context_stack); - if let Some((highest_precedence_index, _)) = bindings.iter().next() { - binding_index == *highest_precedence_index + let (bindings, _) = keymap.bindings_for_input(&binding.keystrokes, context_stack); + if let Some(found) = bindings.iter().next() { + found.action.partial_eq(binding.action.as_ref()) } else { false } diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index 69700e64dc879fe73f7f79c73a5aa6f5fad2f46f..83d7479a04423d249a2be69c69756211eb9d485d 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -77,15 +77,6 @@ impl Keymap { &'a self, action: &'a dyn Action, ) -> impl 'a + DoubleEndedIterator { - self.bindings_for_action_with_indices(action) - .map(|(_, binding)| binding) - } - - /// Like `bindings_for_action_with_indices`, but also returns the binding indices. - pub fn bindings_for_action_with_indices<'a>( - &'a self, - action: &'a dyn Action, - ) -> impl 'a + DoubleEndedIterator { let action_id = action.type_id(); let binding_indices = self .binding_indices_by_action_id @@ -118,7 +109,7 @@ impl Keymap { } } - Some((BindingIndex(*ix), binding)) + Some(binding) }) } @@ -153,22 +144,8 @@ impl Keymap { input: &[Keystroke], context_stack: &[KeyContext], ) -> (SmallVec<[KeyBinding; 1]>, bool) { - let (bindings, pending) = self.bindings_for_input_with_indices(input, context_stack); - let bindings = bindings - .into_iter() - .map(|(_, binding)| binding) - .collect::>(); - (bindings, pending) - } - - /// Like `bindings_for_input`, but also returns the binding indices. - pub fn bindings_for_input_with_indices( - &self, - input: &[Keystroke], - context_stack: &[KeyContext], - ) -> (SmallVec<[(BindingIndex, KeyBinding); 1]>, bool) { - let mut bindings: SmallVec<[(usize, BindingIndex, &KeyBinding); 1]> = SmallVec::new(); - let mut pending_bindings: SmallVec<[(BindingIndex, &KeyBinding); 1]> = SmallVec::new(); + let mut matched_bindings = SmallVec::<[(usize, BindingIndex, &KeyBinding); 1]>::new(); + 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 { @@ -179,26 +156,30 @@ impl Keymap { }; if !pending { - bindings.push((depth, BindingIndex(ix), binding)) + matched_bindings.push((depth, BindingIndex(ix), binding)); } else { - pending_bindings.push((BindingIndex(ix), binding)) + pending_bindings.push((BindingIndex(ix), binding)); } } - bindings.sort_by(|(depth_a, ix_a, _), (depth_b, ix_b, _)| { + matched_bindings.sort_by(|(depth_a, ix_a, _), (depth_b, ix_b, _)| { depth_b.cmp(depth_a).then(ix_b.cmp(ix_a)) }); - let bindings: SmallVec<[_; 1]> = bindings - .into_iter() - .take_while(|(_, _, binding)| !is_no_action(&*binding.action)) - .map(|(_, ix, binding)| (ix, binding.clone())) - .collect(); + let mut bindings: SmallVec<[_; 1]> = SmallVec::new(); + let mut first_binding_index = None; + for (_, ix, binding) in matched_bindings { + if is_no_action(&*binding.action) { + break; + } + bindings.push(binding.clone()); + first_binding_index.get_or_insert(ix); + } let mut pending = HashSet::default(); for (ix, binding) in pending_bindings.into_iter().rev() { - if let Some((binding_ix, _)) = bindings.first() - && *binding_ix > ix + if let Some(binding_ix) = first_binding_index + && binding_ix > ix { continue; } From 230061a6cb79c93faf3e3a9ff880207dc6668e37 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Tue, 22 Jul 2025 17:20:07 +0200 Subject: [PATCH 253/658] Support multiple OpenAI compatible providers (#34212) TODO - [x] OpenAI Compatible API Icon - [x] Docs - [x] Link to docs in OpenAI provider section about configuring OpenAI API compatible providers Closes #33992 Related to #30010 Release Notes: - agent: Add support for adding multiple OpenAI API compatible providers --------- Co-authored-by: MrSubidubi Co-authored-by: Danilo Leal --- Cargo.lock | 4 +- assets/icons/ai_open_ai_compat.svg | 4 + assets/settings/default.json | 1 + crates/agent/src/thread.rs | 2 +- crates/agent_ui/Cargo.toml | 2 + crates/agent_ui/src/active_thread.rs | 4 +- crates/agent_ui/src/agent_configuration.rs | 55 +- .../add_llm_provider_modal.rs | 639 ++++++++++++++++++ .../src/assistant_context_tests.rs | 2 +- .../src/project_notifications_tool.rs | 2 +- crates/icons/src/icons.rs | 1 + crates/language_model/src/fake_provider.rs | 55 +- crates/language_model/src/language_model.rs | 12 + crates/language_model/src/registry.rs | 9 +- crates/language_models/Cargo.toml | 2 +- crates/language_models/src/language_models.rs | 72 +- crates/language_models/src/provider.rs | 1 + .../language_models/src/provider/open_ai.rs | 192 ++---- .../src/provider/open_ai_compatible.rs | 522 ++++++++++++++ crates/language_models/src/settings.rs | 27 +- crates/ui/src/components/modal.rs | 16 +- crates/ui_input/src/ui_input.rs | 4 + docs/src/ai/configuration.md | 13 +- 23 files changed, 1450 insertions(+), 191 deletions(-) create mode 100644 assets/icons/ai_open_ai_compat.svg create mode 100644 crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs create mode 100644 crates/language_models/src/provider/open_ai_compatible.rs diff --git a/Cargo.lock b/Cargo.lock index ad6c40bcf20ccc8cf770313d19deb831d864be3f..c7297e6d597f3ef15c92fab59ea7b0aa584c6cea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -231,6 +231,7 @@ dependencies = [ "jsonschema", "language", "language_model", + "language_models", "languages", "log", "lsp", @@ -269,6 +270,7 @@ dependencies = [ "time_format", "tree-sitter-md", "ui", + "ui_input", "unindent", "urlencoding", "util", @@ -9097,11 +9099,11 @@ dependencies = [ "client", "collections", "component", + "convert_case 0.8.0", "copilot", "credentials_provider", "deepseek", "editor", - "fs", "futures 0.3.31", "google_ai", "gpui", diff --git a/assets/icons/ai_open_ai_compat.svg b/assets/icons/ai_open_ai_compat.svg new file mode 100644 index 0000000000000000000000000000000000000000..f6557caac3304821b051fa6375c7ef32b225f70e --- /dev/null +++ b/assets/icons/ai_open_ai_compat.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/settings/default.json b/assets/settings/default.json index 358871650bbd39500912c4e7ffbc1dd57e1bc1be..309afaccf500034b621a3d3be8fd8a6421783c59 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1712,6 +1712,7 @@ "openai": { "api_url": "https://api.openai.com/v1" }, + "openai_compatible": {}, "open_router": { "api_url": "https://openrouter.ai/api/v1" }, diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index f8a78276154f3eec89e30acdb808b7c41242a1b1..1b3b022ab23cf026c8298555dd53075f74d4f23c 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -5490,7 +5490,7 @@ fn main() {{ let thread = thread_store.update(cx, |store, cx| store.create_thread(cx)); let context_store = cx.new(|_cx| ContextStore::new(project.downgrade(), None)); - let provider = Arc::new(FakeLanguageModelProvider); + let provider = Arc::new(FakeLanguageModelProvider::default()); let model = provider.test_model(); let model: Arc = Arc::new(model); diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index e55ae86fb726f6aeeacb822b62720365d64514b1..33042c0ebd306992c0ed01bc4611ccc0dcfee3b3 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -53,6 +53,7 @@ itertools.workspace = true jsonschema.workspace = true language.workspace = true language_model.workspace = true +language_models.workspace = true log.workspace = true lsp.workspace = true markdown.workspace = true @@ -87,6 +88,7 @@ theme.workspace = true time.workspace = true time_format.workspace = true ui.workspace = true +ui_input.workspace = true urlencoding.workspace = true util.workspace = true uuid.workspace = true diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index bfed81f5b7e07baf7b2f4db489742ce0944cf538..e27c3182213dda590a10ade56907afb8c509721f 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -3895,7 +3895,7 @@ mod tests { LanguageModelRegistry::global(cx).update(cx, |registry, cx| { registry.set_default_model( Some(ConfiguredModel { - provider: Arc::new(FakeLanguageModelProvider), + provider: Arc::new(FakeLanguageModelProvider::default()), model, }), cx, @@ -3979,7 +3979,7 @@ mod tests { LanguageModelRegistry::global(cx).update(cx, |registry, cx| { registry.set_default_model( Some(ConfiguredModel { - provider: Arc::new(FakeLanguageModelProvider), + provider: Arc::new(FakeLanguageModelProvider::default()), model: model.clone(), }), cx, diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index b5ad6ba37c8625e3b8714b1a59fa8a1332871cd6..334c5ee6dc297d7e4bedba0c417a22a7d960c84d 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -1,3 +1,4 @@ +mod add_llm_provider_modal; mod configure_context_server_modal; mod manage_profiles_modal; mod tool_picker; @@ -37,7 +38,10 @@ use zed_actions::ExtensionCategoryFilter; pub(crate) use configure_context_server_modal::ConfigureContextServerModal; pub(crate) use manage_profiles_modal::ManageProfilesModal; -use crate::AddContextServer; +use crate::{ + AddContextServer, + agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider}, +}; pub struct AgentConfiguration { fs: Arc, @@ -304,16 +308,55 @@ impl AgentConfiguration { v_flex() .child( - v_flex() + h_flex() .p(DynamicSpacing::Base16.rems(cx)) .pr(DynamicSpacing::Base20.rems(cx)) .pb_0() .mb_2p5() - .gap_0p5() - .child(Headline::new("LLM Providers")) + .items_start() + .justify_between() .child( - Label::new("Add at least one provider to use AI-powered features.") - .color(Color::Muted), + v_flex() + .gap_0p5() + .child(Headline::new("LLM Providers")) + .child( + Label::new("Add at least one provider to use AI-powered features.") + .color(Color::Muted), + ), + ) + .child( + PopoverMenu::new("add-provider-popover") + .trigger( + Button::new("add-provider", "Add Provider") + .icon_position(IconPosition::Start) + .icon(IconName::Plus) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .label_size(LabelSize::Small), + ) + .anchor(gpui::Corner::TopRight) + .menu({ + let workspace = self.workspace.clone(); + move |window, cx| { + Some(ContextMenu::build(window, cx, |menu, _window, _cx| { + menu.header("Compatible APIs").entry("OpenAI", None, { + let workspace = workspace.clone(); + move |window, cx| { + workspace + .update(cx, |workspace, cx| { + AddLlmProviderModal::toggle( + LlmCompatibleProvider::OpenAi, + workspace, + window, + cx, + ); + }) + .log_err(); + } + }) + })) + } + }), ), ) .child( 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 new file mode 100644 index 0000000000000000000000000000000000000000..94b32d156bd1fc214aecf9726343bd50c8420f90 --- /dev/null +++ b/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs @@ -0,0 +1,639 @@ +use std::sync::Arc; + +use anyhow::Result; +use collections::HashSet; +use fs::Fs; +use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, Task}; +use language_model::LanguageModelRegistry; +use language_models::{ + AllLanguageModelSettings, OpenAiCompatibleSettingsContent, + provider::open_ai_compatible::AvailableModel, +}; +use settings::update_settings_file; +use ui::{Banner, KeyBinding, Modal, ModalFooter, ModalHeader, Section, prelude::*}; +use ui_input::SingleLineInput; +use workspace::{ModalView, Workspace}; + +#[derive(Clone, Copy)] +pub enum LlmCompatibleProvider { + OpenAi, +} + +impl LlmCompatibleProvider { + fn name(&self) -> &'static str { + match self { + LlmCompatibleProvider::OpenAi => "OpenAI", + } + } + + fn api_url(&self) -> &'static str { + match self { + LlmCompatibleProvider::OpenAi => "https://api.openai.com/v1", + } + } +} + +struct AddLlmProviderInput { + provider_name: Entity, + api_url: Entity, + api_key: Entity, + models: Vec, +} + +impl AddLlmProviderInput { + fn new(provider: LlmCompatibleProvider, window: &mut Window, cx: &mut App) -> Self { + let provider_name = single_line_input("Provider Name", provider.name(), None, window, cx); + let api_url = single_line_input("API URL", provider.api_url(), None, window, cx); + let api_key = single_line_input( + "API Key", + "000000000000000000000000000000000000000000000000", + None, + window, + cx, + ); + + Self { + provider_name, + api_url, + api_key, + models: vec![ModelInput::new(window, cx)], + } + } + + fn add_model(&mut self, window: &mut Window, cx: &mut App) { + self.models.push(ModelInput::new(window, cx)); + } + + fn remove_model(&mut self, index: usize) { + self.models.remove(index); + } +} + +struct ModelInput { + name: Entity, + max_completion_tokens: Entity, + max_output_tokens: Entity, + max_tokens: Entity, +} + +impl ModelInput { + fn new(window: &mut Window, cx: &mut App) -> Self { + let model_name = single_line_input( + "Model Name", + "e.g. gpt-4o, claude-opus-4, gemini-2.5-pro", + None, + window, + cx, + ); + let max_completion_tokens = single_line_input( + "Max Completion Tokens", + "200000", + Some("200000"), + window, + cx, + ); + let max_output_tokens = single_line_input( + "Max Output Tokens", + "Max Output Tokens", + Some("32000"), + window, + cx, + ); + let max_tokens = single_line_input("Max Tokens", "Max Tokens", Some("200000"), window, cx); + Self { + name: model_name, + max_completion_tokens, + max_output_tokens, + max_tokens, + } + } + + fn parse(&self, cx: &App) -> Result { + let name = self.name.read(cx).text(cx); + if name.is_empty() { + return Err(SharedString::from("Model Name cannot be empty")); + } + Ok(AvailableModel { + name, + display_name: None, + max_completion_tokens: Some( + self.max_completion_tokens + .read(cx) + .text(cx) + .parse::() + .map_err(|_| SharedString::from("Max Completion Tokens must be a number"))?, + ), + max_output_tokens: Some( + self.max_output_tokens + .read(cx) + .text(cx) + .parse::() + .map_err(|_| SharedString::from("Max Output Tokens must be a number"))?, + ), + max_tokens: self + .max_tokens + .read(cx) + .text(cx) + .parse::() + .map_err(|_| SharedString::from("Max Tokens must be a number"))?, + }) + } +} + +fn single_line_input( + label: impl Into, + placeholder: impl Into, + text: Option<&str>, + window: &mut Window, + cx: &mut App, +) -> Entity { + cx.new(|cx| { + let input = SingleLineInput::new(window, cx, placeholder).label(label); + if let Some(text) = text { + input + .editor() + .update(cx, |editor, cx| editor.set_text(text, window, cx)); + } + input + }) +} + +fn save_provider_to_settings( + input: &AddLlmProviderInput, + cx: &mut App, +) -> Task> { + let provider_name: Arc = input.provider_name.read(cx).text(cx).into(); + if provider_name.is_empty() { + return Task::ready(Err("Provider Name cannot be empty".into())); + } + + if LanguageModelRegistry::read_global(cx) + .providers() + .iter() + .any(|provider| { + provider.id().0.as_ref() == provider_name.as_ref() + || provider.name().0.as_ref() == provider_name.as_ref() + }) + { + return Task::ready(Err( + "Provider Name is already taken by another provider".into() + )); + } + + let api_url = input.api_url.read(cx).text(cx); + if api_url.is_empty() { + return Task::ready(Err("API URL cannot be empty".into())); + } + + let api_key = input.api_key.read(cx).text(cx); + if api_key.is_empty() { + return Task::ready(Err("API Key cannot be empty".into())); + } + + let mut models = Vec::new(); + let mut model_names: HashSet = HashSet::default(); + for model in &input.models { + match model.parse(cx) { + Ok(model) => { + if !model_names.insert(model.name.clone()) { + return Task::ready(Err("Model Names must be unique".into())); + } + models.push(model) + } + Err(err) => return Task::ready(Err(err)), + } + } + + let fs = ::global(cx); + let task = cx.write_credentials(&api_url, "Bearer", api_key.as_bytes()); + cx.spawn(async move |cx| { + task.await + .map_err(|_| "Failed to write API key to keychain")?; + cx.update(|cx| { + update_settings_file::(fs, cx, |settings, _cx| { + settings.openai_compatible.get_or_insert_default().insert( + provider_name, + OpenAiCompatibleSettingsContent { + api_url, + available_models: models, + }, + ); + }); + }) + .ok(); + Ok(()) + }) +} + +pub struct AddLlmProviderModal { + provider: LlmCompatibleProvider, + input: AddLlmProviderInput, + focus_handle: FocusHandle, + last_error: Option, +} + +impl AddLlmProviderModal { + pub fn toggle( + provider: LlmCompatibleProvider, + workspace: &mut Workspace, + window: &mut Window, + cx: &mut Context, + ) { + workspace.toggle_modal(window, cx, |window, cx| Self::new(provider, window, cx)); + } + + fn new(provider: LlmCompatibleProvider, window: &mut Window, cx: &mut Context) -> Self { + Self { + input: AddLlmProviderInput::new(provider, window, cx), + provider, + last_error: None, + focus_handle: cx.focus_handle(), + } + } + + fn confirm(&mut self, _: &menu::Confirm, _: &mut Window, cx: &mut Context) { + let task = save_provider_to_settings(&self.input, cx); + cx.spawn(async move |this, cx| { + let result = task.await; + this.update(cx, |this, cx| match result { + Ok(_) => { + cx.emit(DismissEvent); + } + Err(error) => { + this.last_error = Some(error); + cx.notify(); + } + }) + }) + .detach_and_log_err(cx); + } + + fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) { + 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(&self, ix: usize, cx: &mut Context) -> impl IntoElement + use<> { + let has_more_than_one_model = self.input.models.len() > 1; + let model = &self.input.models[ix]; + + v_flex() + .p_2() + .gap_2() + .rounded_sm() + .border_1() + .border_dashed() + .border_color(cx.theme().colors().border.opacity(0.6)) + .bg(cx.theme().colors().element_active.opacity(0.15)) + .child(model.name.clone()) + .child( + h_flex() + .gap_2() + .child(model.max_completion_tokens.clone()) + .child(model.max_output_tokens.clone()), + ) + .child(model.max_tokens.clone()) + .when(has_more_than_one_model, |this| { + this.child( + Button::new(("remove-model", ix), "Remove Model") + .icon(IconName::Trash) + .icon_position(IconPosition::Start) + .icon_size(IconSize::XSmall) + .icon_color(Color::Muted) + .label_size(LabelSize::Small) + .style(ButtonStyle::Outlined) + .full_width() + .on_click(cx.listener(move |this, _, _window, cx| { + this.input.remove_model(ix); + cx.notify(); + })), + ) + }) + } +} + +impl EventEmitter for AddLlmProviderModal {} + +impl Focusable for AddLlmProviderModal { + fn focus_handle(&self, _cx: &App) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl ModalView for AddLlmProviderModal {} + +impl Render for AddLlmProviderModal { + fn render(&mut self, window: &mut ui::Window, cx: &mut ui::Context) -> impl IntoElement { + let focus_handle = self.focus_handle(cx); + + div() + .id("add-llm-provider-modal") + .key_context("AddLlmProviderModal") + .w(rems(34.)) + .elevation_3(cx) + .on_action(cx.listener(Self::cancel)) + .capture_any_mouse_down(cx.listener(|this, _, window, cx| { + this.focus_handle(cx).focus(window); + })) + .child( + Modal::new("configure-context-server", None) + .header(ModalHeader::new().headline("Add LLM Provider").description( + match self.provider { + LlmCompatibleProvider::OpenAi => { + "This provider will use an OpenAI compatible API." + } + }, + )) + .when_some(self.last_error.clone(), |this, error| { + this.section( + Section::new().child( + Banner::new() + .severity(ui::Severity::Warning) + .child(div().text_xs().child(error)), + ), + ) + }) + .child( + v_flex() + .id("modal_content") + .max_h_128() + .overflow_y_scroll() + .gap_2() + .child(self.render_section()) + .child(self.render_model_section(cx)), + ) + .footer( + ModalFooter::new().end_slot( + h_flex() + .gap_1() + .child( + Button::new("cancel", "Cancel") + .key_binding( + KeyBinding::for_action_in( + &menu::Cancel, + &focus_handle, + window, + cx, + ) + .map(|kb| kb.size(rems_from_px(12.))), + ) + .on_click(cx.listener(|this, _event, window, cx| { + this.cancel(&menu::Cancel, window, cx) + })), + ) + .child( + Button::new("save-server", "Save Provider") + .key_binding( + KeyBinding::for_action_in( + &menu::Confirm, + &focus_handle, + window, + cx, + ) + .map(|kb| kb.size(rems_from_px(12.))), + ) + .on_click(cx.listener(|this, _event, window, cx| { + this.confirm(&menu::Confirm, window, cx) + })), + ), + ), + ), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use editor::EditorSettings; + use fs::FakeFs; + use gpui::{TestAppContext, VisualTestContext}; + use language::language_settings; + use language_model::{ + LanguageModelProviderId, LanguageModelProviderName, + fake_provider::FakeLanguageModelProvider, + }; + use project::Project; + use settings::{Settings as _, SettingsStore}; + use util::path; + + #[gpui::test] + async fn test_save_provider_invalid_inputs(cx: &mut TestAppContext) { + let cx = setup_test(cx).await; + + assert_eq!( + save_provider_validation_errors("", "someurl", "somekey", vec![], cx,).await, + Some("Provider Name cannot be empty".into()) + ); + + assert_eq!( + save_provider_validation_errors("someprovider", "", "somekey", vec![], cx,).await, + Some("API URL cannot be empty".into()) + ); + + assert_eq!( + save_provider_validation_errors("someprovider", "someurl", "", vec![], cx,).await, + Some("API Key cannot be empty".into()) + ); + + assert_eq!( + save_provider_validation_errors( + "someprovider", + "someurl", + "somekey", + vec![("", "200000", "200000", "32000")], + cx, + ) + .await, + Some("Model Name cannot be empty".into()) + ); + + assert_eq!( + save_provider_validation_errors( + "someprovider", + "someurl", + "somekey", + vec![("somemodel", "abc", "200000", "32000")], + cx, + ) + .await, + Some("Max Tokens must be a number".into()) + ); + + assert_eq!( + save_provider_validation_errors( + "someprovider", + "someurl", + "somekey", + vec![("somemodel", "200000", "abc", "32000")], + cx, + ) + .await, + Some("Max Completion Tokens must be a number".into()) + ); + + assert_eq!( + save_provider_validation_errors( + "someprovider", + "someurl", + "somekey", + vec![("somemodel", "200000", "200000", "abc")], + cx, + ) + .await, + Some("Max Output Tokens must be a number".into()) + ); + + assert_eq!( + save_provider_validation_errors( + "someprovider", + "someurl", + "somekey", + vec![ + ("somemodel", "200000", "200000", "32000"), + ("somemodel", "200000", "200000", "32000"), + ], + cx, + ) + .await, + Some("Model Names must be unique".into()) + ); + } + + #[gpui::test] + async fn test_save_provider_name_conflict(cx: &mut TestAppContext) { + let cx = setup_test(cx).await; + + cx.update(|_window, cx| { + LanguageModelRegistry::global(cx).update(cx, |registry, cx| { + registry.register_provider( + FakeLanguageModelProvider::new( + LanguageModelProviderId::new("someprovider"), + LanguageModelProviderName::new("Some Provider"), + ), + cx, + ); + }); + }); + + assert_eq!( + save_provider_validation_errors( + "someprovider", + "someurl", + "someapikey", + vec![("somemodel", "200000", "200000", "32000")], + cx, + ) + .await, + Some("Provider Name is already taken by another provider".into()) + ); + } + + async fn setup_test(cx: &mut TestAppContext) -> &mut VisualTestContext { + cx.update(|cx| { + let store = SettingsStore::test(cx); + cx.set_global(store); + workspace::init_settings(cx); + Project::init_settings(cx); + theme::init(theme::LoadThemes::JustBase, cx); + language_settings::init(cx); + EditorSettings::register(cx); + language_model::init_settings(cx); + language_models::init_settings(cx); + }); + + let fs = FakeFs::new(cx.executor()); + cx.update(|cx| ::set_global(fs.clone(), cx)); + let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; + let (_, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + + cx + } + + async fn save_provider_validation_errors( + provider_name: &str, + api_url: &str, + api_key: &str, + models: Vec<(&str, &str, &str, &str)>, + cx: &mut VisualTestContext, + ) -> Option { + fn set_text( + input: &Entity, + text: &str, + window: &mut Window, + cx: &mut App, + ) { + input.update(cx, |input, cx| { + input.editor().update(cx, |editor, cx| { + editor.set_text(text, window, cx); + }); + }); + } + + let task = cx.update(|window, cx| { + let mut input = AddLlmProviderInput::new(LlmCompatibleProvider::OpenAi, window, cx); + set_text(&input.provider_name, provider_name, window, cx); + set_text(&input.api_url, api_url, window, cx); + set_text(&input.api_key, api_key, window, cx); + + for (i, (name, max_tokens, max_completion_tokens, max_output_tokens)) in + models.iter().enumerate() + { + if i >= input.models.len() { + input.models.push(ModelInput::new(window, cx)); + } + let model = &mut input.models[i]; + set_text(&model.name, name, window, cx); + set_text(&model.max_tokens, max_tokens, window, cx); + set_text( + &model.max_completion_tokens, + max_completion_tokens, + window, + cx, + ); + set_text(&model.max_output_tokens, max_output_tokens, window, cx); + } + save_provider_to_settings(&input, cx) + }); + + task.await.err() + } +} diff --git a/crates/assistant_context/src/assistant_context_tests.rs b/crates/assistant_context/src/assistant_context_tests.rs index dba3bfde61bb997a25d25f29651ad0a7aa2c2708..f139d525d34376837418ff6055bb0a1eada8b878 100644 --- a/crates/assistant_context/src/assistant_context_tests.rs +++ b/crates/assistant_context/src/assistant_context_tests.rs @@ -1323,7 +1323,7 @@ fn setup_context_editor_with_fake_model( ) -> (Entity, Arc) { let registry = Arc::new(LanguageRegistry::test(cx.executor().clone())); - let fake_provider = Arc::new(FakeLanguageModelProvider); + let fake_provider = Arc::new(FakeLanguageModelProvider::default()); let fake_model = Arc::new(fake_provider.test_model()); cx.update(|cx| { diff --git a/crates/assistant_tools/src/project_notifications_tool.rs b/crates/assistant_tools/src/project_notifications_tool.rs index ec315d9ab15337ea78784df84e949090d2e0f1da..7567926dcaa38aca7740818a82dcac93b8410c1e 100644 --- a/crates/assistant_tools/src/project_notifications_tool.rs +++ b/crates/assistant_tools/src/project_notifications_tool.rs @@ -200,7 +200,7 @@ mod tests { // Run the tool before any changes let tool = Arc::new(ProjectNotificationsTool); - let provider = Arc::new(FakeLanguageModelProvider); + let provider = Arc::new(FakeLanguageModelProvider::default()); let model: Arc = Arc::new(provider.test_model()); let request = Arc::new(LanguageModelRequest::default()); let tool_input = json!({}); diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index b85e5b517d6587ffdc39abb2295a2bcf6381fc19..e7066ae151664a196d78547ae62dcd39d1ba0653 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -20,6 +20,7 @@ pub enum IconName { AiMistral, AiOllama, AiOpenAi, + AiOpenAiCompat, AiOpenRouter, AiVZero, AiXAi, diff --git a/crates/language_model/src/fake_provider.rs b/crates/language_model/src/fake_provider.rs index f5191016d89d1418922865bc2eaddada945d072a..d54db7554a522f7f9b280732c69c9c14dda1cdb2 100644 --- a/crates/language_model/src/fake_provider.rs +++ b/crates/language_model/src/fake_provider.rs @@ -10,25 +10,21 @@ use http_client::Result; use parking_lot::Mutex; use std::sync::Arc; -pub fn language_model_id() -> LanguageModelId { - LanguageModelId::from("fake".to_string()) +#[derive(Clone)] +pub struct FakeLanguageModelProvider { + id: LanguageModelProviderId, + name: LanguageModelProviderName, } -pub fn language_model_name() -> LanguageModelName { - LanguageModelName::from("Fake".to_string()) -} - -pub fn provider_id() -> LanguageModelProviderId { - LanguageModelProviderId::from("fake".to_string()) -} - -pub fn provider_name() -> LanguageModelProviderName { - LanguageModelProviderName::from("Fake".to_string()) +impl Default for FakeLanguageModelProvider { + fn default() -> Self { + Self { + id: LanguageModelProviderId::from("fake".to_string()), + name: LanguageModelProviderName::from("Fake".to_string()), + } + } } -#[derive(Clone, Default)] -pub struct FakeLanguageModelProvider; - impl LanguageModelProviderState for FakeLanguageModelProvider { type ObservableEntity = (); @@ -39,11 +35,11 @@ impl LanguageModelProviderState for FakeLanguageModelProvider { impl LanguageModelProvider for FakeLanguageModelProvider { fn id(&self) -> LanguageModelProviderId { - provider_id() + self.id.clone() } fn name(&self) -> LanguageModelProviderName { - provider_name() + self.name.clone() } fn default_model(&self, _cx: &App) -> Option> { @@ -76,6 +72,10 @@ impl LanguageModelProvider for FakeLanguageModelProvider { } impl FakeLanguageModelProvider { + pub fn new(id: LanguageModelProviderId, name: LanguageModelProviderName) -> Self { + Self { id, name } + } + pub fn test_model(&self) -> FakeLanguageModel { FakeLanguageModel::default() } @@ -89,11 +89,22 @@ pub struct ToolUseRequest { pub schema: serde_json::Value, } -#[derive(Default)] pub struct FakeLanguageModel { + provider_id: LanguageModelProviderId, + provider_name: LanguageModelProviderName, current_completion_txs: Mutex)>>, } +impl Default for FakeLanguageModel { + fn default() -> Self { + Self { + provider_id: LanguageModelProviderId::from("fake".to_string()), + provider_name: LanguageModelProviderName::from("Fake".to_string()), + current_completion_txs: Mutex::new(Vec::new()), + } + } +} + impl FakeLanguageModel { pub fn pending_completions(&self) -> Vec { self.current_completion_txs @@ -138,19 +149,19 @@ impl FakeLanguageModel { impl LanguageModel for FakeLanguageModel { fn id(&self) -> LanguageModelId { - language_model_id() + LanguageModelId::from("fake".to_string()) } fn name(&self) -> LanguageModelName { - language_model_name() + LanguageModelName::from("Fake".to_string()) } fn provider_id(&self) -> LanguageModelProviderId { - provider_id() + self.provider_id.clone() } fn provider_name(&self) -> LanguageModelProviderName { - provider_name() + self.provider_name.clone() } fn supports_tools(&self) -> bool { diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index 72455b382199fc503256e33706045baef2c1b1ec..54640419b674e8680e5b266b5f6eaf2a3d365f76 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -735,6 +735,18 @@ impl From for LanguageModelProviderName { } } +impl From> for LanguageModelProviderId { + fn from(value: Arc) -> Self { + Self(SharedString::from(value)) + } +} + +impl From> for LanguageModelProviderName { + fn from(value: Arc) -> Self { + Self(SharedString::from(value)) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/language_model/src/registry.rs b/crates/language_model/src/registry.rs index 6e8e8e91088bf3197c523c75209c56f9edda82be..7cf071808a2c0d95bf9aa5a41eaa260cff533d57 100644 --- a/crates/language_model/src/registry.rs +++ b/crates/language_model/src/registry.rs @@ -125,7 +125,7 @@ impl LanguageModelRegistry { #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &mut App) -> crate::fake_provider::FakeLanguageModelProvider { - let fake_provider = crate::fake_provider::FakeLanguageModelProvider; + let fake_provider = crate::fake_provider::FakeLanguageModelProvider::default(); let registry = cx.new(|cx| { let mut registry = Self::default(); registry.register_provider(fake_provider.clone(), cx); @@ -403,16 +403,17 @@ mod tests { fn test_register_providers(cx: &mut App) { let registry = cx.new(|_| LanguageModelRegistry::default()); + let provider = FakeLanguageModelProvider::default(); registry.update(cx, |registry, cx| { - registry.register_provider(FakeLanguageModelProvider, cx); + registry.register_provider(provider.clone(), cx); }); let providers = registry.read(cx).providers(); assert_eq!(providers.len(), 1); - assert_eq!(providers[0].id(), crate::fake_provider::provider_id()); + assert_eq!(providers[0].id(), provider.id()); registry.update(cx, |registry, cx| { - registry.unregister_provider(crate::fake_provider::provider_id(), cx); + registry.unregister_provider(provider.id(), cx); }); let providers = registry.read(cx).providers(); diff --git a/crates/language_models/Cargo.toml b/crates/language_models/Cargo.toml index ed38ac76605e5b9554f3c5cd2a91a6650c20393d..574579aaa762248133c3027fac70efe255ac6997 100644 --- a/crates/language_models/Cargo.toml +++ b/crates/language_models/Cargo.toml @@ -26,10 +26,10 @@ client.workspace = true collections.workspace = true component.workspace = true credentials_provider.workspace = true +convert_case.workspace = true copilot.workspace = true deepseek = { workspace = true, features = ["schemars"] } editor.workspace = true -fs.workspace = true futures.workspace = true google_ai = { workspace = true, features = ["schemars"] } gpui.workspace = true diff --git a/crates/language_models/src/language_models.rs b/crates/language_models/src/language_models.rs index 192f5a5fae214693fab8b1b166e907478ce307f2..18e6f47ed0591256591df578f98dcaf988ed6444 100644 --- a/crates/language_models/src/language_models.rs +++ b/crates/language_models/src/language_models.rs @@ -1,8 +1,10 @@ use std::sync::Arc; +use ::settings::{Settings, SettingsStore}; use client::{Client, UserStore}; +use collections::HashSet; use gpui::{App, Context, Entity}; -use language_model::LanguageModelRegistry; +use language_model::{LanguageModelProviderId, LanguageModelRegistry}; use provider::deepseek::DeepSeekLanguageModelProvider; pub mod provider; @@ -18,17 +20,81 @@ use crate::provider::lmstudio::LmStudioLanguageModelProvider; use crate::provider::mistral::MistralLanguageModelProvider; use crate::provider::ollama::OllamaLanguageModelProvider; use crate::provider::open_ai::OpenAiLanguageModelProvider; +use crate::provider::open_ai_compatible::OpenAiCompatibleLanguageModelProvider; use crate::provider::open_router::OpenRouterLanguageModelProvider; 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) { - crate::settings::init(cx); + crate::settings::init_settings(cx); let registry = LanguageModelRegistry::global(cx); registry.update(cx, |registry, cx| { - register_language_model_providers(registry, user_store, client, cx); + register_language_model_providers(registry, user_store, client.clone(), cx); }); + + let mut openai_compatible_providers = AllLanguageModelSettings::get_global(cx) + .openai_compatible + .keys() + .cloned() + .collect::>(); + + registry.update(cx, |registry, cx| { + register_openai_compatible_providers( + registry, + &HashSet::default(), + &openai_compatible_providers, + client.clone(), + cx, + ); + }); + cx.observe_global::(move |cx| { + let openai_compatible_providers_new = AllLanguageModelSettings::get_global(cx) + .openai_compatible + .keys() + .cloned() + .collect::>(); + if openai_compatible_providers_new != openai_compatible_providers { + registry.update(cx, |registry, cx| { + register_openai_compatible_providers( + registry, + &openai_compatible_providers, + &openai_compatible_providers_new, + client.clone(), + cx, + ); + }); + openai_compatible_providers = openai_compatible_providers_new; + } + }) + .detach(); +} + +fn register_openai_compatible_providers( + registry: &mut LanguageModelRegistry, + old: &HashSet>, + new: &HashSet>, + client: Arc, + cx: &mut Context, +) { + for provider_id in old { + if !new.contains(provider_id) { + registry.unregister_provider(LanguageModelProviderId::from(provider_id.clone()), cx); + } + } + + for provider_id in new { + if !old.contains(provider_id) { + registry.register_provider( + OpenAiCompatibleLanguageModelProvider::new( + provider_id.clone(), + client.http_client(), + cx, + ), + cx, + ); + } + } } fn register_language_model_providers( diff --git a/crates/language_models/src/provider.rs b/crates/language_models/src/provider.rs index c717be7c907cebf7427c67c748b689cb40b0ed9d..d780195c66ec0d19c2b7d53e62b5e3629baa8a43 100644 --- a/crates/language_models/src/provider.rs +++ b/crates/language_models/src/provider.rs @@ -8,6 +8,7 @@ pub mod lmstudio; pub mod mistral; pub mod ollama; pub mod open_ai; +pub mod open_ai_compatible; pub mod open_router; pub mod vercel; pub mod x_ai; diff --git a/crates/language_models/src/provider/open_ai.rs b/crates/language_models/src/provider/open_ai.rs index 76f2fbe303c4bed0cfeefbfca6358667420aed51..6c4d4c9b3edd2bdf3085701a5379745200a907ed 100644 --- a/crates/language_models/src/provider/open_ai.rs +++ b/crates/language_models/src/provider/open_ai.rs @@ -2,7 +2,6 @@ use anyhow::{Context as _, Result, anyhow}; use collections::{BTreeMap, HashMap}; use credentials_provider::CredentialsProvider; -use fs::Fs; use futures::Stream; use futures::{FutureExt, StreamExt, future::BoxFuture}; use gpui::{AnyView, App, AsyncApp, Context, Entity, Subscription, Task, Window}; @@ -18,7 +17,7 @@ use menu; use open_ai::{ImageUrl, Model, ResponseStreamEvent, stream_completion}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsStore, update_settings_file}; +use settings::{Settings, SettingsStore}; use std::pin::Pin; use std::str::FromStr as _; use std::sync::Arc; @@ -28,7 +27,6 @@ use ui::{ElevationIndex, List, Tooltip, prelude::*}; use ui_input::SingleLineInput; use util::ResultExt; -use crate::OpenAiSettingsContent; use crate::{AllLanguageModelSettings, ui::InstructionListItem}; const PROVIDER_ID: LanguageModelProviderId = language_model::OPEN_AI_PROVIDER_ID; @@ -621,26 +619,32 @@ struct RawToolCall { arguments: String, } +pub(crate) fn collect_tiktoken_messages( + request: LanguageModelRequest, +) -> Vec { + request + .messages + .into_iter() + .map(|message| tiktoken_rs::ChatCompletionRequestMessage { + role: match message.role { + Role::User => "user".into(), + Role::Assistant => "assistant".into(), + Role::System => "system".into(), + }, + content: Some(message.string_contents()), + name: None, + function_call: None, + }) + .collect::>() +} + pub fn count_open_ai_tokens( request: LanguageModelRequest, model: Model, cx: &App, ) -> BoxFuture<'static, Result> { cx.background_spawn(async move { - let messages = request - .messages - .into_iter() - .map(|message| tiktoken_rs::ChatCompletionRequestMessage { - role: match message.role { - Role::User => "user".into(), - Role::Assistant => "assistant".into(), - Role::System => "system".into(), - }, - content: Some(message.string_contents()), - name: None, - function_call: None, - }) - .collect::>(); + let messages = collect_tiktoken_messages(request); match model { Model::Custom { max_tokens, .. } => { @@ -678,7 +682,6 @@ pub fn count_open_ai_tokens( struct ConfigurationView { api_key_editor: Entity, - api_url_editor: Entity, state: gpui::Entity, load_credentials_task: Option>, } @@ -691,23 +694,6 @@ impl ConfigurationView { cx, "sk-000000000000000000000000000000000000000000000000", ) - .label("API key") - }); - - let api_url = AllLanguageModelSettings::get_global(cx) - .openai - .api_url - .clone(); - - let api_url_editor = cx.new(|cx| { - let input = SingleLineInput::new(window, cx, open_ai::OPEN_AI_API_URL).label("API URL"); - - if !api_url.is_empty() { - input.editor.update(cx, |editor, cx| { - editor.set_text(&*api_url, window, cx); - }); - } - input }); cx.observe(&state, |_, _, cx| { @@ -735,7 +721,6 @@ impl ConfigurationView { Self { api_key_editor, - api_url_editor, state, load_credentials_task, } @@ -783,57 +768,6 @@ impl ConfigurationView { cx.notify(); } - fn save_api_url(&mut self, cx: &mut Context) { - let api_url = self - .api_url_editor - .read(cx) - .editor() - .read(cx) - .text(cx) - .trim() - .to_string(); - - let current_url = AllLanguageModelSettings::get_global(cx) - .openai - .api_url - .clone(); - - let effective_current_url = if current_url.is_empty() { - open_ai::OPEN_AI_API_URL - } else { - ¤t_url - }; - - if !api_url.is_empty() && api_url != effective_current_url { - let fs = ::global(cx); - update_settings_file::(fs, cx, move |settings, _| { - if let Some(settings) = settings.openai.as_mut() { - settings.api_url = Some(api_url.clone()); - } else { - settings.openai = Some(OpenAiSettingsContent { - api_url: Some(api_url.clone()), - available_models: None, - }); - } - }); - } - } - - fn reset_api_url(&mut self, window: &mut Window, cx: &mut Context) { - self.api_url_editor.update(cx, |input, cx| { - input.editor.update(cx, |editor, cx| { - editor.set_text("", window, cx); - }); - }); - let fs = ::global(cx); - update_settings_file::(fs, cx, |settings, _cx| { - if let Some(settings) = settings.openai.as_mut() { - settings.api_url = None; - } - }); - cx.notify(); - } - fn should_render_editor(&self, cx: &mut Context) -> bool { !self.state.read(cx).is_authenticated() } @@ -846,7 +780,6 @@ 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( List::new() @@ -910,59 +843,34 @@ impl Render for ConfigurationView { .into_any() }; - let custom_api_url_set = - AllLanguageModelSettings::get_global(cx).openai.api_url != open_ai::OPEN_AI_API_URL; - - let api_url_section = if custom_api_url_set { - h_flex() - .mt_1() - .p_1() - .justify_between() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().background) - .child( - h_flex() - .gap_1() - .child(Icon::new(IconName::Check).color(Color::Success)) - .child(Label::new("Custom API URL configured.")), - ) - .child( - Button::new("reset-api-url", "Reset API URL") - .label_size(LabelSize::Small) - .icon(IconName::Undo) - .icon_size(IconSize::Small) - .icon_position(IconPosition::Start) - .layer(ElevationIndex::ModalSurface) - .on_click( - cx.listener(|this, _, window, cx| this.reset_api_url(window, cx)), - ), - ) - .into_any() - } else { - v_flex() - .on_action(cx.listener(|this, _: &menu::Confirm, _window, cx| { - this.save_api_url(cx); - cx.notify(); - })) - .mt_2() - .pt_2() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .gap_1() - .child( - List::new() - .child(InstructionListItem::text_only( - "Optionally, you can change the base URL for the OpenAI API request.", - )) - .child(InstructionListItem::text_only( - "Paste the new API endpoint below and hit enter", - )), - ) - .child(self.api_url_editor.clone()) - .into_any() - }; + let compatible_api_section = h_flex() + .mt_1p5() + .gap_0p5() + .flex_wrap() + .when(self.should_render_editor(cx), |this| { + this.pt_1p5() + .border_t_1() + .border_color(cx.theme().colors().border_variant) + }) + .child( + h_flex() + .gap_2() + .child( + Icon::new(IconName::Info) + .size(IconSize::XSmall) + .color(Color::Muted), + ) + .child(Label::new("Zed also supports OpenAI-compatible models.")), + ) + .child( + Button::new("docs", "Learn More") + .icon(IconName::ArrowUpRight) + .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") + }), + ); if self.load_credentials_task.is_some() { div().child(Label::new("Loading credentials…")).into_any() @@ -970,7 +878,7 @@ impl Render for ConfigurationView { v_flex() .size_full() .child(api_key_section) - .child(api_url_section) + .child(compatible_api_section) .into_any() } } diff --git a/crates/language_models/src/provider/open_ai_compatible.rs b/crates/language_models/src/provider/open_ai_compatible.rs new file mode 100644 index 0000000000000000000000000000000000000000..64add5483d9c01b09873711060a9533e06e3d1b7 --- /dev/null +++ b/crates/language_models/src/provider/open_ai_compatible.rs @@ -0,0 +1,522 @@ +use anyhow::{Context as _, Result, anyhow}; +use credentials_provider::CredentialsProvider; + +use convert_case::{Case, Casing}; +use futures::{FutureExt, StreamExt, future::BoxFuture}; +use gpui::{AnyView, App, AsyncApp, Context, Entity, Subscription, Task, Window}; +use http_client::HttpClient; +use language_model::{ + AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, + LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId, + LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest, + LanguageModelToolChoice, RateLimiter, +}; +use menu; +use open_ai::{ResponseStreamEvent, stream_completion}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::{Settings, SettingsStore}; +use std::sync::Arc; + +use ui::{ElevationIndex, Tooltip, prelude::*}; +use ui_input::SingleLineInput; +use util::ResultExt; + +use crate::AllLanguageModelSettings; +use crate::provider::open_ai::{OpenAiEventMapper, into_open_ai}; + +#[derive(Default, Clone, Debug, PartialEq)] +pub struct OpenAiCompatibleSettings { + pub api_url: String, + pub available_models: Vec, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema)] +pub struct AvailableModel { + pub name: String, + pub display_name: Option, + pub max_tokens: u64, + pub max_output_tokens: Option, + pub max_completion_tokens: Option, +} + +pub struct OpenAiCompatibleLanguageModelProvider { + id: LanguageModelProviderId, + name: LanguageModelProviderName, + http_client: Arc, + state: gpui::Entity, +} + +pub struct State { + id: Arc, + env_var_name: Arc, + api_key: Option, + api_key_from_env: bool, + settings: OpenAiCompatibleSettings, + _subscription: Subscription, +} + +impl State { + fn is_authenticated(&self) -> bool { + self.api_key.is_some() + } + + fn reset_api_key(&self, cx: &mut Context) -> Task> { + let credentials_provider = ::global(cx); + let api_url = self.settings.api_url.clone(); + cx.spawn(async move |this, cx| { + credentials_provider + .delete_credentials(&api_url, &cx) + .await + .log_err(); + this.update(cx, |this, cx| { + this.api_key = None; + this.api_key_from_env = false; + cx.notify(); + }) + }) + } + + fn set_api_key(&mut self, api_key: String, cx: &mut Context) -> Task> { + let credentials_provider = ::global(cx); + 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) + .await + .log_err(); + this.update(cx, |this, cx| { + this.api_key = Some(api_key); + cx.notify(); + }) + }) + } + + fn authenticate(&self, cx: &mut Context) -> Task> { + if self.is_authenticated() { + return Task::ready(Ok(())); + } + + let credentials_provider = ::global(cx); + let env_var_name = self.env_var_name.clone(); + let api_url = self.settings.api_url.clone(); + cx.spawn(async move |this, cx| { + let (api_key, from_env) = if let Ok(api_key) = std::env::var(env_var_name.as_ref()) { + (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, + ) + }; + this.update(cx, |this, cx| { + this.api_key = Some(api_key); + this.api_key_from_env = from_env; + cx.notify(); + })?; + + Ok(()) + }) + } +} + +impl OpenAiCompatibleLanguageModelProvider { + pub fn new(id: Arc, http_client: Arc, cx: &mut App) -> Self { + fn resolve_settings<'a>(id: &'a str, cx: &'a App) -> Option<&'a OpenAiCompatibleSettings> { + AllLanguageModelSettings::get_global(cx) + .openai_compatible + .get(id) + } + + let state = cx.new(|cx| State { + id: id.clone(), + env_var_name: format!("{}_API_KEY", id).to_case(Case::Constant).into(), + settings: resolve_settings(&id, cx).cloned().unwrap_or_default(), + api_key: None, + api_key_from_env: false, + _subscription: cx.observe_global::(|this: &mut State, cx| { + let Some(settings) = resolve_settings(&this.id, cx) else { + return; + }; + if &this.settings != settings { + this.settings = settings.clone(); + cx.notify(); + } + }), + }); + + Self { + id: id.clone().into(), + name: id.into(), + http_client, + state, + } + } + + fn create_language_model(&self, model: AvailableModel) -> Arc { + Arc::new(OpenAiCompatibleLanguageModel { + id: LanguageModelId::from(model.name.clone()), + provider_id: self.id.clone(), + provider_name: self.name.clone(), + model, + state: self.state.clone(), + http_client: self.http_client.clone(), + request_limiter: RateLimiter::new(4), + }) + } +} + +impl LanguageModelProviderState for OpenAiCompatibleLanguageModelProvider { + type ObservableEntity = State; + + fn observable_entity(&self) -> Option> { + Some(self.state.clone()) + } +} + +impl LanguageModelProvider for OpenAiCompatibleLanguageModelProvider { + fn id(&self) -> LanguageModelProviderId { + self.id.clone() + } + + fn name(&self) -> LanguageModelProviderName { + self.name.clone() + } + + fn icon(&self) -> IconName { + IconName::AiOpenAiCompat + } + + fn default_model(&self, cx: &App) -> Option> { + self.state + .read(cx) + .settings + .available_models + .first() + .map(|model| self.create_language_model(model.clone())) + } + + fn default_fast_model(&self, _cx: &App) -> Option> { + None + } + + fn provided_models(&self, cx: &App) -> Vec> { + self.state + .read(cx) + .settings + .available_models + .iter() + .map(|model| self.create_language_model(model.clone())) + .collect() + } + + fn is_authenticated(&self, cx: &App) -> bool { + self.state.read(cx).is_authenticated() + } + + fn authenticate(&self, cx: &mut App) -> Task> { + 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)) + .into() + } + + fn reset_credentials(&self, cx: &mut App) -> Task> { + self.state.update(cx, |state, cx| state.reset_api_key(cx)) + } +} + +pub struct OpenAiCompatibleLanguageModel { + id: LanguageModelId, + provider_id: LanguageModelProviderId, + provider_name: LanguageModelProviderName, + model: AvailableModel, + state: gpui::Entity, + http_client: Arc, + request_limiter: RateLimiter, +} + +impl OpenAiCompatibleLanguageModel { + fn stream_completion( + &self, + request: open_ai::Request, + cx: &AsyncApp, + ) -> BoxFuture<'static, Result>>> + { + let http_client = self.http_client.clone(); + let Ok((api_key, api_url)) = cx.read_entity(&self.state, |state, _| { + (state.api_key.clone(), state.settings.api_url.clone()) + }) else { + return futures::future::ready(Err(anyhow!("App state dropped"))).boxed(); + }; + + let provider = self.provider_name.clone(); + let future = self.request_limiter.stream(async move { + let Some(api_key) = api_key else { + return Err(LanguageModelCompletionError::NoApiKey { provider }); + }; + let request = stream_completion(http_client.as_ref(), &api_url, &api_key, request); + let response = request.await?; + Ok(response) + }); + + async move { Ok(future.await?.boxed()) }.boxed() + } +} + +impl LanguageModel for OpenAiCompatibleLanguageModel { + fn id(&self) -> LanguageModelId { + self.id.clone() + } + + fn name(&self) -> LanguageModelName { + LanguageModelName::from( + self.model + .display_name + .clone() + .unwrap_or_else(|| self.model.name.clone()), + ) + } + + fn provider_id(&self) -> LanguageModelProviderId { + self.provider_id.clone() + } + + fn provider_name(&self) -> LanguageModelProviderName { + self.provider_name.clone() + } + + fn supports_tools(&self) -> bool { + true + } + + fn supports_images(&self) -> bool { + false + } + + fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool { + match choice { + LanguageModelToolChoice::Auto => true, + LanguageModelToolChoice::Any => true, + LanguageModelToolChoice::None => true, + } + } + + fn telemetry_id(&self) -> String { + format!("openai/{}", self.model.name) + } + + fn max_token_count(&self) -> u64 { + self.model.max_tokens + } + + fn max_output_tokens(&self) -> Option { + self.model.max_output_tokens + } + + fn count_tokens( + &self, + request: LanguageModelRequest, + cx: &App, + ) -> BoxFuture<'static, Result> { + let max_token_count = self.max_token_count(); + cx.background_spawn(async move { + let messages = super::open_ai::collect_tiktoken_messages(request); + let model = if max_token_count >= 100_000 { + // If the max tokens is 100k or more, it is likely the o200k_base tokenizer from gpt4o + "gpt-4o" + } else { + // Otherwise fallback to gpt-4, since only cl100k_base and o200k_base are + // supported with this tiktoken method + "gpt-4" + }; + tiktoken_rs::num_tokens_from_messages(model, &messages).map(|tokens| tokens as u64) + }) + .boxed() + } + + fn stream_completion( + &self, + request: LanguageModelRequest, + cx: &AsyncApp, + ) -> BoxFuture< + 'static, + Result< + futures::stream::BoxStream< + 'static, + Result, + >, + LanguageModelCompletionError, + >, + > { + let request = into_open_ai(request, &self.model.name, true, self.max_output_tokens()); + let completions = self.stream_completion(request, cx); + async move { + let mapper = OpenAiEventMapper::new(); + Ok(mapper.map_stream(completions.await?).boxed()) + } + .boxed() + } +} + +struct ConfigurationView { + api_key_editor: Entity, + state: gpui::Entity, + load_credentials_task: Option>, +} + +impl ConfigurationView { + fn new(state: gpui::Entity, window: &mut Window, cx: &mut Context) -> Self { + let api_key_editor = cx.new(|cx| { + SingleLineInput::new( + window, + cx, + "000000000000000000000000000000000000000000000000000", + ) + }); + + cx.observe(&state, |_, _, cx| { + cx.notify(); + }) + .detach(); + + let load_credentials_task = Some(cx.spawn_in(window, { + let state = state.clone(); + async move |this, cx| { + if let Some(task) = state + .update(cx, |state, cx| state.authenticate(cx)) + .log_err() + { + // We don't log an error, because "not signed in" is also an error. + let _ = task.await; + } + this.update(cx, |this, cx| { + this.load_credentials_task = None; + cx.notify(); + }) + .log_err(); + } + })); + + Self { + api_key_editor, + state, + load_credentials_task, + } + } + + fn save_api_key(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) { + let api_key = self + .api_key_editor + .read(cx) + .editor() + .read(cx) + .text(cx) + .trim() + .to_string(); + + // Don't proceed if no API key is provided and we're not authenticated + if api_key.is_empty() && !self.state.read(cx).is_authenticated() { + return; + } + + let state = self.state.clone(); + cx.spawn_in(window, async move |_, cx| { + state + .update(cx, |state, cx| state.set_api_key(api_key, cx))? + .await + }) + .detach_and_log_err(cx); + + cx.notify(); + } + + fn reset_api_key(&mut self, window: &mut Window, cx: &mut Context) { + self.api_key_editor.update(cx, |input, cx| { + input.editor.update(cx, |editor, cx| { + editor.set_text("", window, cx); + }); + }); + + let state = self.state.clone(); + cx.spawn_in(window, async move |_, cx| { + state.update(cx, |state, cx| state.reset_api_key(cx))?.await + }) + .detach_and_log_err(cx); + + cx.notify(); + } + + fn should_render_editor(&self, cx: &mut Context) -> bool { + !self.state.read(cx).is_authenticated() + } +} + +impl Render for ConfigurationView { + fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let env_var_set = self.state.read(cx).api_key_from_env; + let env_var_name = self.state.read(cx).env_var_name.clone(); + + 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( + div() + .pt(DynamicSpacing::Base04.rems(cx)) + .child(self.api_key_editor.clone()) + ) + .child( + Label::new( + format!("You can also assign the {env_var_name} environment variable and restart Zed."), + ) + .size(LabelSize::Small).color(Color::Muted), + ) + .into_any() + } else { + h_flex() + .mt_1() + .p_1() + .justify_between() + .rounded_md() + .border_1() + .border_color(cx.theme().colors().border) + .bg(cx.theme().colors().background) + .child( + h_flex() + .gap_1() + .child(Icon::new(IconName::Check).color(Color::Success)) + .child(Label::new(if env_var_set { + format!("API key set in {env_var_name} environment variable.") + } else { + "API key configured.".to_string() + })), + ) + .child( + Button::new("reset-api-key", "Reset API Key") + .label_size(LabelSize::Small) + .icon(IconName::Undo) + .icon_size(IconSize::Small) + .icon_position(IconPosition::Start) + .layer(ElevationIndex::ModalSurface) + .when(env_var_set, |this| { + this.tooltip(Tooltip::text(format!("To reset your API key, unset the {env_var_name} environment variable."))) + }) + .on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))), + ) + .into_any() + }; + + if self.load_credentials_task.is_some() { + div().child(Label::new("Loading credentials…")).into_any() + } else { + v_flex().size_full().child(api_key_section).into_any() + } + } +} diff --git a/crates/language_models/src/settings.rs b/crates/language_models/src/settings.rs index dafbb629100469e6a8dd77850eece139a3bed267..b163585aa7b745447381aa62f710e8c5dbdf469c 100644 --- a/crates/language_models/src/settings.rs +++ b/crates/language_models/src/settings.rs @@ -1,4 +1,7 @@ +use std::sync::Arc; + use anyhow::Result; +use collections::HashMap; use gpui::App; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -15,13 +18,14 @@ use crate::provider::{ mistral::MistralSettings, ollama::OllamaSettings, open_ai::OpenAiSettings, + open_ai_compatible::OpenAiCompatibleSettings, open_router::OpenRouterSettings, vercel::VercelSettings, x_ai::XAiSettings, }; /// Initializes the language model settings. -pub fn init(cx: &mut App) { +pub fn init_settings(cx: &mut App) { AllLanguageModelSettings::register(cx); } @@ -36,6 +40,7 @@ pub struct AllLanguageModelSettings { pub ollama: OllamaSettings, pub open_router: OpenRouterSettings, pub openai: OpenAiSettings, + pub openai_compatible: HashMap, OpenAiCompatibleSettings>, pub vercel: VercelSettings, pub x_ai: XAiSettings, pub zed_dot_dev: ZedDotDevSettings, @@ -52,6 +57,7 @@ pub struct AllLanguageModelSettingsContent { pub ollama: Option, pub open_router: Option, pub openai: Option, + pub openai_compatible: Option, OpenAiCompatibleSettingsContent>>, pub vercel: Option, pub x_ai: Option, #[serde(rename = "zed.dev")] @@ -103,6 +109,12 @@ pub struct OpenAiSettingsContent { pub available_models: Option>, } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] +pub struct OpenAiCompatibleSettingsContent { + pub api_url: String, + pub available_models: Vec, +} + #[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema)] pub struct VercelSettingsContent { pub api_url: Option, @@ -226,6 +238,19 @@ impl settings::Settings for AllLanguageModelSettings { openai.as_ref().and_then(|s| s.available_models.clone()), ); + // OpenAI Compatible + if let Some(openai_compatible) = value.openai_compatible.clone() { + for (id, openai_compatible_settings) in openai_compatible { + settings.openai_compatible.insert( + id, + OpenAiCompatibleSettings { + api_url: openai_compatible_settings.api_url, + available_models: openai_compatible_settings.available_models, + }, + ); + } + } + // Vercel let vercel = value.vercel.clone(); merge( diff --git a/crates/ui/src/components/modal.rs b/crates/ui/src/components/modal.rs index 2e926b7593808070ab65be36902b01483945e2ac..2145b34ef26614754c6ce0c3a674cae32f8638d3 100644 --- a/crates/ui/src/components/modal.rs +++ b/crates/ui/src/components/modal.rs @@ -93,6 +93,7 @@ impl RenderOnce for Modal { #[derive(IntoElement)] pub struct ModalHeader { headline: Option, + description: Option, children: SmallVec<[AnyElement; 2]>, show_dismiss_button: bool, show_back_button: bool, @@ -108,6 +109,7 @@ impl ModalHeader { pub fn new() -> Self { Self { headline: None, + description: None, children: SmallVec::new(), show_dismiss_button: false, show_back_button: false, @@ -123,6 +125,11 @@ impl ModalHeader { self } + pub fn description(mut self, description: impl Into) -> Self { + self.description = Some(description.into()); + self + } + pub fn show_dismiss_button(mut self, show: bool) -> Self { self.show_dismiss_button = show; self @@ -171,7 +178,14 @@ impl RenderOnce for ModalHeader { }), ) }) - .child(div().flex_1().children(children)) + .child( + v_flex().flex_1().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( IconButton::new("dismiss", IconName::Close) diff --git a/crates/ui_input/src/ui_input.rs b/crates/ui_input/src/ui_input.rs index 18aa732e8153c15a064d74c88dfdb03d20bffedc..309b3f62f6cb44435e0fba25f7f617afcb392b2a 100644 --- a/crates/ui_input/src/ui_input.rs +++ b/crates/ui_input/src/ui_input.rs @@ -97,6 +97,10 @@ impl SingleLineInput { pub fn editor(&self) -> &Entity { &self.editor } + + pub fn text(&self, cx: &App) -> String { + self.editor().read(cx).text(cx) + } } impl Render for SingleLineInput { diff --git a/docs/src/ai/configuration.md b/docs/src/ai/configuration.md index 1201fa217367a7e2ba48843b2a4fa0e815932e76..414da2206fff90c8ed560c8596b4ab3216311a82 100644 --- a/docs/src/ai/configuration.md +++ b/docs/src/ai/configuration.md @@ -444,14 +444,17 @@ 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 by specifying a custom `endpoint` and `available_models` for the OpenAI provider. +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 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`. -To configure a compatible API, you can add a custom API URL for OpenAI either via the UI or by editing your `settings.json`. For example, to connect to [Together AI](https://www.together.ai/): +For example, to connect to [Together AI](https://www.together.ai/) via the UI: -1. Get an API key from your [Together AI account](https://api.together.ai/settings/api-keys). -2. Add the following to your `settings.json`: +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`: ```json { From 939f9fffa3f7d305280957dcf1573989ab429a7b Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 22 Jul 2025 11:27:58 -0400 Subject: [PATCH 254/658] collab: Remove unneeded caching of Stripe meters (#34900) This PR removes the caching of Stripe meters on the `StripeBilling` object, as we weren't actually reading them anywhere. Release Notes: - N/A --- crates/collab/src/stripe_billing.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/crates/collab/src/stripe_billing.rs b/crates/collab/src/stripe_billing.rs index 3d52dea0e3b08b42f97fcf1017f03503deb21e7a..707928d5cd8952cda154531fd9c4d2e21d661f0e 100644 --- a/crates/collab/src/stripe_billing.rs +++ b/crates/collab/src/stripe_billing.rs @@ -17,7 +17,7 @@ use crate::stripe_client::{ StripeCreateCheckoutSessionSubscriptionData, StripeCreateMeterEventParams, StripeCreateMeterEventPayload, StripeCreateSubscriptionItems, StripeCreateSubscriptionParams, StripeCustomerId, StripeCustomerUpdate, StripeCustomerUpdateAddress, StripeCustomerUpdateName, - StripeMeter, StripePrice, StripePriceId, StripeSubscription, StripeSubscriptionId, + StripePrice, StripePriceId, StripeSubscription, StripeSubscriptionId, StripeSubscriptionTrialSettings, StripeSubscriptionTrialSettingsEndBehavior, StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, StripeTaxIdCollection, UpdateSubscriptionItems, UpdateSubscriptionParams, @@ -30,7 +30,6 @@ pub struct StripeBilling { #[derive(Default)] struct StripeBillingState { - meters_by_event_name: HashMap, price_ids_by_meter_id: HashMap, prices_by_lookup_key: HashMap, } @@ -60,14 +59,7 @@ impl StripeBilling { let mut state = self.state.write().await; - let (meters, prices) = - futures::try_join!(self.client.list_meters(), self.client.list_prices())?; - - for meter in meters { - state - .meters_by_event_name - .insert(meter.event_name.clone(), meter); - } + let prices = self.client.list_prices().await?; for price in prices { if let Some(lookup_key) = price.lookup_key.clone() { From 96f994279167569a4458bb9f5cd636bfb82c0fac Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Tue, 22 Jul 2025 11:32:39 -0400 Subject: [PATCH 255/658] Add setting to disable all AI features (#34896) https://github.com/user-attachments/assets/674bba41-40ac-4a98-99e4-0b47f9097b6a Release Notes: - Added setting to disable all AI features --- Cargo.lock | 2 + assets/settings/default.json | 4 ++ crates/agent_ui/Cargo.toml | 1 + crates/agent_ui/src/agent_panel.rs | 8 ++- crates/agent_ui/src/agent_ui.rs | 65 ++++++++++++++++++- crates/agent_ui/src/inline_assistant.rs | 44 ++++++++++++- crates/assistant_tools/Cargo.toml | 1 + crates/assistant_tools/src/assistant_tools.rs | 3 +- crates/client/src/client.rs | 28 ++++++++ crates/copilot/src/copilot.rs | 46 ++++++++----- crates/git_ui/Cargo.toml | 1 + crates/git_ui/src/commit_modal.rs | 14 ++-- crates/git_ui/src/git_panel.rs | 19 ++++-- .../src/inline_completion_button.rs | 7 +- crates/welcome/src/welcome.rs | 36 +++++----- crates/workspace/src/dock.rs | 13 +++- crates/zed/src/main.rs | 1 + crates/zed/src/zed/quick_action_bar.rs | 26 +++++++- crates/zeta/src/init.rs | 57 ++++++++++++---- 19 files changed, 308 insertions(+), 68 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c7297e6d597f3ef15c92fab59ea7b0aa584c6cea..6237bac204f160f54c7fd5829e4c3bdbe3400fc8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -210,6 +210,7 @@ dependencies = [ "chrono", "client", "collections", + "command_palette_hooks", "component", "context_server", "db", @@ -6360,6 +6361,7 @@ dependencies = [ "buffer_diff", "call", "chrono", + "client", "collections", "command_palette_hooks", "component", diff --git a/assets/settings/default.json b/assets/settings/default.json index 309afaccf500034b621a3d3be8fd8a6421783c59..dab1684aef4412bf2f297787e6443dba6ce01f67 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -1076,6 +1076,10 @@ // Send anonymized usage data like what languages you're using Zed with. "metrics": true }, + // Whether to disable all AI features in Zed. + // + // Default: false + "disable_ai": false, // Automatically update Zed. This setting may be ignored on Linux if // installed through a package manager. "auto_update": true, diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index 33042c0ebd306992c0ed01bc4611ccc0dcfee3b3..7d3b84e42e682cd1aa824f99adf9255df5f75124 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -32,6 +32,7 @@ buffer_diff.workspace = true chrono.workspace = true client.workspace = true collections.workspace = true +command_palette_hooks.workspace = true component.workspace = true context_server.workspace = true db.workspace = true diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index fc803c730eab7741ba33c9423e6558e54c8cba8d..7e9360a0cbf65bdae58b0dc68ffe0f4526ab4926 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::{UserStore, zed_urls}; +use client::{DisableAiSettings, UserStore, zed_urls}; use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer}; use feature_flags::{self, FeatureFlagAppExt}; use fs::Fs; @@ -744,6 +744,7 @@ impl AgentPanel { if workspace .panel::(cx) .is_some_and(|panel| panel.read(cx).enabled(cx)) + && !DisableAiSettings::get_global(cx).disable_ai { workspace.toggle_panel_focus::(window, cx); } @@ -1665,7 +1666,10 @@ impl Panel for AgentPanel { } fn icon(&self, _window: &Window, cx: &App) -> Option { - (self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAssistant) + (self.enabled(cx) + && AgentSettings::get_global(cx).button + && !DisableAiSettings::get_global(cx).disable_ai) + .then_some(IconName::ZedAssistant) } fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> { diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 7f69e8f66e3bcf37fb56c0384c0b8bf17a37d0f4..cac0f1adace1113dea78537ee000fb951f54d74a 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -31,7 +31,8 @@ use std::sync::Arc; use agent::{Thread, ThreadId}; use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection}; use assistant_slash_command::SlashCommandRegistry; -use client::Client; +use client::{Client, DisableAiSettings}; +use command_palette_hooks::CommandPaletteFilter; use feature_flags::FeatureFlagAppExt as _; use fs::Fs; use gpui::{Action, App, Entity, actions}; @@ -43,6 +44,7 @@ use prompt_store::PromptBuilder; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{Settings as _, SettingsStore}; +use std::any::TypeId; pub use crate::active_thread::ActiveThread; use crate::agent_configuration::{ConfigureContextServerModal, ManageProfilesModal}; @@ -52,6 +54,7 @@ use crate::slash_command_settings::SlashCommandSettings; pub use agent_diff::{AgentDiffPane, AgentDiffToolbar}; pub use text_thread_editor::{AgentPanelDelegate, TextThreadEditor}; pub use ui::preview::{all_agent_previews, get_agent_preview}; +use zed_actions; actions!( agent, @@ -241,6 +244,66 @@ pub fn init( }) .detach(); cx.observe_new(ManageProfilesModal::register).detach(); + + // Update command palette filter based on AI settings + update_command_palette_filter(cx); + + // Watch for settings changes + cx.observe_global::(|app_cx| { + // When settings change, update the command palette filter + update_command_palette_filter(app_cx); + }) + .detach(); +} + +fn update_command_palette_filter(cx: &mut App) { + let disable_ai = DisableAiSettings::get_global(cx).disable_ai; + CommandPaletteFilter::update_global(cx, |filter, _| { + if disable_ai { + filter.hide_namespace("agent"); + filter.hide_namespace("assistant"); + filter.hide_namespace("zed_predict_onboarding"); + filter.hide_namespace("edit_prediction"); + + use editor::actions::{ + AcceptEditPrediction, AcceptPartialEditPrediction, NextEditPrediction, + PreviousEditPrediction, ShowEditPrediction, ToggleEditPrediction, + }; + let edit_prediction_actions = [ + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + ]; + filter.hide_action_types(&edit_prediction_actions); + filter.hide_action_types(&[TypeId::of::()]); + } else { + filter.show_namespace("agent"); + filter.show_namespace("assistant"); + filter.show_namespace("zed_predict_onboarding"); + + filter.show_namespace("edit_prediction"); + + use editor::actions::{ + AcceptEditPrediction, AcceptPartialEditPrediction, NextEditPrediction, + PreviousEditPrediction, ShowEditPrediction, ToggleEditPrediction, + }; + let edit_prediction_actions = [ + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + ]; + filter.show_action_types(edit_prediction_actions.iter()); + + filter + .show_action_types([TypeId::of::()].iter()); + } + }); } fn init_language_model_settings(cx: &mut App) { diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs index 65b72cbba5f15aa3f527b77939a80abff7a05c05..44ec050ae2b9533c9b3e25b007189759fe0d0e3a 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::telemetry::Telemetry; +use client::{DisableAiSettings, telemetry::Telemetry}; use collections::{HashMap, HashSet, VecDeque, hash_map}; use editor::SelectionEffects; use editor::{ @@ -57,6 +57,17 @@ pub fn init( cx: &mut App, ) { cx.set_global(InlineAssistant::new(fs, prompt_builder, telemetry)); + + cx.observe_global::(|cx| { + if DisableAiSettings::get_global(cx).disable_ai { + // Hide any active inline assist UI when AI is disabled + InlineAssistant::update_global(cx, |assistant, cx| { + assistant.cancel_all_active_completions(cx); + }); + } + }) + .detach(); + cx.observe_new(|_workspace: &mut Workspace, window, cx| { let Some(window) = window else { return; @@ -141,6 +152,26 @@ impl InlineAssistant { .detach(); } + /// Hides all active inline assists when AI is disabled + pub fn cancel_all_active_completions(&mut self, cx: &mut App) { + // Cancel all active completions in editors + for (editor_handle, _) in self.assists_by_editor.iter() { + if let Some(editor) = editor_handle.upgrade() { + let windows = cx.windows(); + if !windows.is_empty() { + let window = windows[0]; + let _ = window.update(cx, |_, window, cx| { + editor.update(cx, |editor, cx| { + if editor.has_active_inline_completion() { + editor.cancel(&Default::default(), window, cx); + } + }); + }); + } + } + } + } + fn handle_workspace_event( &mut self, workspace: Entity, @@ -176,7 +207,7 @@ impl InlineAssistant { window: &mut Window, cx: &mut App, ) { - let is_assistant2_enabled = true; + let is_assistant2_enabled = !DisableAiSettings::get_global(cx).disable_ai; if let Some(editor) = item.act_as::(cx) { editor.update(cx, |editor, cx| { @@ -199,6 +230,13 @@ impl InlineAssistant { cx, ); + if DisableAiSettings::get_global(cx).disable_ai { + // Cancel any active completions + if editor.has_active_inline_completion() { + editor.cancel(&Default::default(), window, cx); + } + } + // Remove the Assistant1 code action provider, as it still might be registered. editor.remove_code_action_provider("assistant".into(), window, cx); } else { @@ -219,7 +257,7 @@ impl InlineAssistant { cx: &mut Context, ) { let settings = AgentSettings::get_global(cx); - if !settings.enabled { + if !settings.enabled || DisableAiSettings::get_global(cx).disable_ai { return; } diff --git a/crates/assistant_tools/Cargo.toml b/crates/assistant_tools/Cargo.toml index e234b62b142c368ab8383df4eeff8848704a5b98..146800e094a25baf3901134bdd5a7fb4f9330214 100644 --- a/crates/assistant_tools/Cargo.toml +++ b/crates/assistant_tools/Cargo.toml @@ -20,6 +20,7 @@ anyhow.workspace = true assistant_tool.workspace = true buffer_diff.workspace = true chrono.workspace = true +client.workspace = true collections.workspace = true component.workspace = true derive_more.workspace = true diff --git a/crates/assistant_tools/src/assistant_tools.rs b/crates/assistant_tools/src/assistant_tools.rs index eef792f526fb684e83752241194d293064a9f4f7..57fdc51336275f5344c40e564b853dc1db8b0f64 100644 --- a/crates/assistant_tools/src/assistant_tools.rs +++ b/crates/assistant_tools/src/assistant_tools.rs @@ -20,14 +20,13 @@ mod thinking_tool; mod ui; mod web_search_tool; -use std::sync::Arc; - use assistant_tool::ToolRegistry; use copy_path_tool::CopyPathTool; use gpui::{App, Entity}; use http_client::HttpClientWithUrl; use language_model::LanguageModelRegistry; use move_path_tool::MovePathTool; +use std::sync::Arc; use web_search_tool::WebSearchTool; pub(crate) use templates::*; diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 1be8ffdb55b088ba7e9b8e0b1525b89e7dd0a48a..81bb95b5143b94c0fe71963ae00d7ac3edf93b29 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -151,6 +151,7 @@ impl Settings for ProxySettings { pub fn init_settings(cx: &mut App) { TelemetrySettings::register(cx); + DisableAiSettings::register(cx); ClientSettings::register(cx); ProxySettings::register(cx); } @@ -548,6 +549,33 @@ 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 1966d1a3890157e76a44bcddce89d225af8ea923..e11242cb15347d6ec4f8982a774164db66ffe028 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -6,6 +6,7 @@ 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}; @@ -25,6 +26,7 @@ use node_runtime::NodeRuntime; use parking_lot::Mutex; use request::StatusNotification; use serde_json::json; +use settings::Settings; use settings::SettingsStore; use sign_in::{reinstall_and_sign_in_within_workspace, sign_out_within_workspace}; use std::collections::hash_map::Entry; @@ -93,26 +95,34 @@ pub fn init( 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); - 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()); + 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()); + } } } }) diff --git a/crates/git_ui/Cargo.toml b/crates/git_ui/Cargo.toml index 6e04dcb656636e9323b6171c6b95012154f52a52..2fb80b7e7366a103fc3012041f5b35952819547d 100644 --- a/crates/git_ui/Cargo.toml +++ b/crates/git_ui/Cargo.toml @@ -23,6 +23,7 @@ askpass.workspace = true buffer_diff.workspace = true call.workspace = true chrono.workspace = true +client.workspace = true collections.workspace = true command_palette_hooks.workspace = true component.workspace = true diff --git a/crates/git_ui/src/commit_modal.rs b/crates/git_ui/src/commit_modal.rs index ac3d24e3eb791e9114844feccdb603475119303c..b99f628806f51d424304a746571731ad36a7765f 100644 --- a/crates/git_ui/src/commit_modal.rs +++ b/crates/git_ui/src/commit_modal.rs @@ -1,8 +1,10 @@ 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 settings::Settings; use ui::{ ContextMenu, KeybindingHint, PopoverMenu, PopoverMenuHandle, SplitButton, Tooltip, prelude::*, }; @@ -569,11 +571,13 @@ impl Render for CommitModal { .on_action(cx.listener(Self::dismiss)) .on_action(cx.listener(Self::commit)) .on_action(cx.listener(Self::amend)) - .on_action(cx.listener(|this, _: &GenerateCommitMessage, _, cx| { - this.git_panel.update(cx, |panel, cx| { - panel.generate_commit_message(cx); - }) - })) + .when(!DisableAiSettings::get_global(cx).disable_ai, |this| { + this.on_action(cx.listener(|this, _: &GenerateCommitMessage, _, cx| { + this.git_panel.update(cx, |panel, cx| { + panel.generate_commit_message(cx); + }) + })) + }) .on_action( cx.listener(|this, _: &zed_actions::git::Branch, window, cx| { this.toggle_branch_selector(window, cx); diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index e998586af4ad0f3553ed4c462a469eec1ab71124..061833a6c72cee8110ebf6bb8a3d7398bf5c6cde 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -12,6 +12,7 @@ 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, @@ -53,7 +54,7 @@ use project::{ git_store::{GitStoreEvent, Repository}, }; use serde::{Deserialize, Serialize}; -use settings::{Settings as _, SettingsStore}; +use settings::{Settings, SettingsStore}; use std::future::Future; use std::ops::Range; use std::path::{Path, PathBuf}; @@ -464,9 +465,14 @@ impl GitPanel { }; let mut assistant_enabled = AgentSettings::get_global(cx).enabled; + let mut was_ai_disabled = DisableAiSettings::get_global(cx).disable_ai; let _settings_subscription = cx.observe_global::(move |_, cx| { - if assistant_enabled != AgentSettings::get_global(cx).enabled { + let is_ai_disabled = DisableAiSettings::get_global(cx).disable_ai; + if assistant_enabled != AgentSettings::get_global(cx).enabled + || was_ai_disabled != is_ai_disabled + { assistant_enabled = AgentSettings::get_global(cx).enabled; + was_ai_disabled = is_ai_disabled; cx.notify(); } }); @@ -1806,7 +1812,7 @@ impl GitPanel { /// Generates a commit message using an LLM. pub fn generate_commit_message(&mut self, cx: &mut Context) { - if !self.can_commit() { + if !self.can_commit() || DisableAiSettings::get_global(cx).disable_ai { return; } @@ -4305,8 +4311,10 @@ impl GitPanel { } fn current_language_model(cx: &Context<'_, GitPanel>) -> Option> { - agent_settings::AgentSettings::get_global(cx) - .enabled + let is_enabled = agent_settings::AgentSettings::get_global(cx).enabled + && !DisableAiSettings::get_global(cx).disable_ai; + + is_enabled .then(|| { let ConfiguredModel { provider, model } = LanguageModelRegistry::read_global(cx).commit_message_model()?; @@ -5037,6 +5045,7 @@ 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/src/inline_completion_button.rs b/crates/inline_completion_button/src/inline_completion_button.rs index 8a8eacdc6a5855db435dd4d5e476f67fbe207910..2615a8beef7bdcd4e458a215c70da657070d9311 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::{UserStore, zed_urls}; +use client::{DisableAiSettings, UserStore, zed_urls}; use copilot::{Copilot, Status}; use editor::{ Editor, SelectionEffects, @@ -72,6 +72,11 @@ enum SupermavenButtonStatus { impl Render for InlineCompletionButton { 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 { + return div(); + } + let all_language_settings = all_language_settings(None, cx); match all_language_settings.edit_predictions.provider { diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index ea4ac13de7f41f4b46da6b465f1e22076a5bae7b..49bf2031ab1fb4f324ff17461a3f6d1e00953560 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -1,4 +1,4 @@ -use client::{TelemetrySettings, telemetry::Telemetry}; +use client::{DisableAiSettings, TelemetrySettings, telemetry::Telemetry}; use db::kvp::KEY_VALUE_STORE; use gpui::{ Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, @@ -174,23 +174,25 @@ impl Render for WelcomePage { .ok(); })), ) - .child( - Button::new( - "try-zed-edit-prediction", - edit_prediction_label, + .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); + }), + ), ) - .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) diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 4e39c2d18287519e282b1812087ba72740080ec7..3f047e2f114f972380eba43d9478adac71cc7257 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -242,6 +242,7 @@ struct PanelEntry { pub struct PanelButtons { dock: Entity, + _settings_subscription: Subscription, } impl Dock { @@ -373,6 +374,12 @@ impl Dock { }) } + pub fn first_enabled_panel_idx_excluding(&self, exclude_name: &str, cx: &App) -> Option { + self.panel_entries.iter().position(|entry| { + entry.panel.persistent_name() != exclude_name && entry.panel.enabled(cx) + }) + } + fn active_panel_entry(&self) -> Option<&PanelEntry> { self.active_panel_index .and_then(|index| self.panel_entries.get(index)) @@ -833,7 +840,11 @@ impl Render for Dock { impl PanelButtons { pub fn new(dock: Entity, cx: &mut Context) -> Self { cx.observe(&dock, |_, _, cx| cx.notify()).detach(); - Self { dock } + let settings_subscription = cx.observe_global::(|_, cx| cx.notify()); + Self { + dock, + _settings_subscription: settings_subscription, + } } } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index c9b8eebff6a9001c2350a2e1bc2e56ad6708c5ee..d0b9c53397d8470ce2b7b1c1223dc9d7df4d1fa0 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -554,6 +554,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.client.clone(), cx); + agent_settings::init(cx); agent_servers::init(cx); web_search::init(cx); web_search_providers::init(app_state.client.clone(), cx); diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index c95d86c84f72a77010e06e9fec5b96d0c060fee4..aff124a0bc0d1ad71b81a07f4fdf33149cbedbd6 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -2,6 +2,7 @@ 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, @@ -32,6 +33,7 @@ const MAX_CODE_ACTION_MENU_LINES: u32 = 16; pub struct QuickActionBar { _inlay_hints_enabled_subscription: Option, + _ai_settings_subscription: Subscription, active_item: Option>, buffer_search_bar: Entity, show: bool, @@ -46,8 +48,28 @@ impl QuickActionBar { workspace: &Workspace, cx: &mut Context, ) -> Self { + let mut was_ai_disabled = DisableAiSettings::get_global(cx).disable_ai; + let mut was_agent_enabled = AgentSettings::get_global(cx).enabled; + let mut was_agent_button = AgentSettings::get_global(cx).button; + + let ai_settings_subscription = cx.observe_global::(move |_, cx| { + let is_ai_disabled = DisableAiSettings::get_global(cx).disable_ai; + let agent_settings = AgentSettings::get_global(cx); + + if was_ai_disabled != is_ai_disabled + || was_agent_enabled != agent_settings.enabled + || was_agent_button != agent_settings.button + { + was_ai_disabled = is_ai_disabled; + was_agent_enabled = agent_settings.enabled; + was_agent_button = agent_settings.button; + cx.notify(); + } + }); + let mut this = Self { _inlay_hints_enabled_subscription: None, + _ai_settings_subscription: ai_settings_subscription, active_item: None, buffer_search_bar, show: true, @@ -575,7 +597,9 @@ impl Render for QuickActionBar { .children(self.render_preview_button(self.workspace.clone(), cx)) .children(search_button) .when( - AgentSettings::get_global(cx).enabled && AgentSettings::get_global(cx).button, + AgentSettings::get_global(cx).enabled + && AgentSettings::get_global(cx).button + && !DisableAiSettings::get_global(cx).disable_ai, |bar| bar.child(assistant_button), ) .children(code_actions_dropdown) diff --git a/crates/zeta/src/init.rs b/crates/zeta/src/init.rs index 4bcd50df885a43ade7bb04bbde8b8c3d3a1f54d1..4a65771223815fa88d092be09cf6f7e265ba367b 100644 --- a/crates/zeta/src/init.rs +++ b/crates/zeta/src/init.rs @@ -1,10 +1,11 @@ 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 settings::update_settings_file; +use settings::{Settings, SettingsStore, update_settings_file}; use ui::App; use workspace::Workspace; @@ -21,6 +22,8 @@ actions!( ); pub fn init(cx: &mut App) { + feature_gate_predict_edits_actions(cx); + cx.observe_new(move |workspace: &mut Workspace, _, _cx| { workspace.register_action(|workspace, _: &RateCompletions, window, cx| { if cx.has_flag::() { @@ -53,27 +56,57 @@ pub fn init(cx: &mut App) { }); }) .detach(); - - feature_gate_predict_edits_rating_actions(cx); } -fn feature_gate_predict_edits_rating_actions(cx: &mut App) { +fn feature_gate_predict_edits_actions(cx: &mut App) { let rate_completion_action_types = [TypeId::of::()]; + let reset_onboarding_action_types = [TypeId::of::()]; + let zeta_all_action_types = [ + TypeId::of::(), + TypeId::of::(), + zed_actions::OpenZedPredictOnboarding.type_id(), + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + ]; CommandPaletteFilter::update_global(cx, |filter, _cx| { filter.hide_action_types(&rate_completion_action_types); + filter.hide_action_types(&reset_onboarding_action_types); filter.hide_action_types(&[zed_actions::OpenZedPredictOnboarding.type_id()]); }); + cx.observe_global::(move |cx| { + let is_ai_disabled = DisableAiSettings::get_global(cx).disable_ai; + let has_feature_flag = cx.has_flag::(); + + 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 { + filter.hide_action_types(&rate_completion_action_types); + } + } + }); + }) + .detach(); + cx.observe_flag::(move |is_enabled, cx| { - if is_enabled { - CommandPaletteFilter::update_global(cx, |filter, _cx| { - filter.show_action_types(rate_completion_action_types.iter()); - }); - } else { - CommandPaletteFilter::update_global(cx, |filter, _cx| { - filter.hide_action_types(&rate_completion_action_types); - }); + if !DisableAiSettings::get_global(cx).disable_ai { + if is_enabled { + CommandPaletteFilter::update_global(cx, |filter, _cx| { + filter.show_action_types(rate_completion_action_types.iter()); + }); + } else { + CommandPaletteFilter::update_global(cx, |filter, _cx| { + filter.hide_action_types(&rate_completion_action_types); + }); + } } }) .detach(); From 2b888e1d30c5f1876cedd1ddac18bb050a568ae2 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Tue, 22 Jul 2025 10:45:42 -0500 Subject: [PATCH 256/658] Fix redo after noop format (#34898) Closes #31917 Previously, as of #28457 we used a hack, creating an empty transaction in the history that we then merged formatting changes into in order to correctly identify concurrent edits to the buffer while formatting was happening. This caused issues with noop formatting however, as using the normal API of the buffer history (in an albeit weird way) resulted in the redo stack being cleared, regardless of whether the formatting transaction included edits or not, which is the correct behavior in all other contexts. This PR fixes the redo issue by codifying the behavior formatting wants, that being the ability to push an empty transaction to the history with no other side-effects (i.e. clearing the redo stack) to detect concurrent edits, with the tradeoff being that it must then manually remove the transaction later if no changes occurred from the formatting. The redo stack is still cleared when there are formatting edits, as the individual format steps use the normal `{start,end}_transaction` methods which clear the redo stack if the finished transaction isn't empty. Release Notes: - Fixed an issue where redo would not work after buffer formatting (including formatting on save) when the formatting did not result in any changes --- crates/editor/src/editor_tests.rs | 70 ++++++++++++++++++++++++++++++- crates/language/src/buffer.rs | 15 +++++++ crates/project/src/lsp_store.rs | 8 +--- crates/text/src/text.rs | 52 ++++++++++++++++++++++- 4 files changed, 137 insertions(+), 8 deletions(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 4efb052c71d284373981157c3ddc0cee8db276c6..fbb877796cbdb066179f104d862905d0ce71c25c 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -9570,6 +9570,74 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) { } } +#[gpui::test] +async fn test_redo_after_noop_format(cx: &mut TestAppContext) { + init_test(cx, |settings| { + settings.defaults.ensure_final_newline_on_save = Some(false); + }); + + let fs = FakeFs::new(cx.executor()); + fs.insert_file(path!("/file.txt"), "foo".into()).await; + + let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await; + + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer(path!("/file.txt"), cx) + }) + .await + .unwrap(); + + let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); + let (editor, cx) = cx.add_window_view(|window, cx| { + build_editor_with_project(project.clone(), buffer, window, cx) + }); + editor.update_in(cx, |editor, window, cx| { + editor.change_selections(SelectionEffects::default(), window, cx, |s| { + s.select_ranges([0..0]) + }); + }); + assert!(!cx.read(|cx| editor.is_dirty(cx))); + + editor.update_in(cx, |editor, window, cx| { + editor.handle_input("\n", window, cx) + }); + cx.run_until_parked(); + save(&editor, &project, cx).await; + assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx))); + + editor.update_in(cx, |editor, window, cx| { + editor.undo(&Default::default(), window, cx); + }); + save(&editor, &project, cx).await; + assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx))); + + editor.update_in(cx, |editor, window, cx| { + editor.redo(&Default::default(), window, cx); + }); + cx.run_until_parked(); + assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx))); + + async fn save(editor: &Entity, project: &Entity, cx: &mut VisualTestContext) { + let save = editor + .update_in(cx, |editor, window, cx| { + editor.save( + SaveOptions { + format: true, + autosave: false, + }, + project.clone(), + window, + cx, + ) + }) + .unwrap(); + cx.executor().start_waiting(); + save.await; + assert!(!cx.read(|cx| editor.is_dirty(cx))); + } +} + #[gpui::test] async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) { init_test(cx, |_| {}); @@ -22708,7 +22776,7 @@ pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsC workspace::init_settings(cx); crate::init(cx); }); - + zlog::init_test(); update_test_language_settings(cx, f); } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 59aa63ff3806d0a56965dce229112c948d073c7b..83517accc239ecf9d2196f124fc5695a8545ef17 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2072,6 +2072,21 @@ impl Buffer { self.text.push_transaction(transaction, now); } + /// Differs from `push_transaction` in that it does not clear the redo + /// stack. Intended to be used to create a parent transaction to merge + /// potential child transactions into. + /// + /// The caller is responsible for removing it from the undo history using + /// `forget_transaction` if no edits are merged into it. Otherwise, if edits + /// are merged into this transaction, the caller is responsible for ensuring + /// the redo stack is cleared. The easiest way to ensure the redo stack is + /// cleared is to create transactions with the usual `start_transaction` and + /// `end_transaction` methods and merging the resulting transactions into + /// the transaction created by this method + pub fn push_empty_transaction(&mut self, now: Instant) -> TransactionId { + self.text.push_empty_transaction(now) + } + /// Prevent the last transaction from being grouped with any subsequent transactions, /// even if they occur with the buffer's undo grouping duration. pub fn finalize_last_transaction(&mut self) -> Option<&Transaction> { diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 28cbfcdd1842b8c861d2777c3858efc8752ac75b..0cd375e0c5497f02003d9d3b2edb76aa550f2041 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -1274,15 +1274,11 @@ impl LocalLspStore { // grouped with the previous transaction in the history // based on the transaction group interval buffer.finalize_last_transaction(); - let transaction_id = buffer + buffer .start_transaction() .context("transaction already open")?; - let transaction = buffer - .get_transaction(transaction_id) - .expect("transaction started") - .clone(); buffer.end_transaction(cx); - buffer.push_transaction(transaction, cx.background_executor().now()); + let transaction_id = buffer.push_empty_transaction(cx.background_executor().now()); buffer.finalize_last_transaction(); anyhow::Ok(transaction_id) })??; diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index a2742081f4b79eeff92cd2fb8a02890d1523fa5a..aa9682029efb2fbfd1436561eed281432b325162 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -320,7 +320,39 @@ impl History { last_edit_at: now, suppress_grouping: false, }); - self.redo_stack.clear(); + } + + /// Differs from `push_transaction` in that it does not clear the redo + /// stack. Intended to be used to create a parent transaction to merge + /// potential child transactions into. + /// + /// The caller is responsible for removing it from the undo history using + /// `forget_transaction` if no edits are merged into it. Otherwise, if edits + /// are merged into this transaction, the caller is responsible for ensuring + /// the redo stack is cleared. The easiest way to ensure the redo stack is + /// cleared is to create transactions with the usual `start_transaction` and + /// `end_transaction` methods and merging the resulting transactions into + /// the transaction created by this method + fn push_empty_transaction( + &mut self, + start: clock::Global, + now: Instant, + clock: &mut clock::Lamport, + ) -> TransactionId { + assert_eq!(self.transaction_depth, 0); + let id = clock.tick(); + let transaction = Transaction { + id, + start, + edit_ids: Vec::new(), + }; + self.undo_stack.push(HistoryEntry { + transaction, + first_edit_at: now, + last_edit_at: now, + suppress_grouping: false, + }); + id } fn push_undo(&mut self, op_id: clock::Lamport) { @@ -1495,6 +1527,24 @@ impl Buffer { self.history.push_transaction(transaction, now); } + /// Differs from `push_transaction` in that it does not clear the redo stack. + /// The caller responsible for + /// Differs from `push_transaction` in that it does not clear the redo + /// stack. Intended to be used to create a parent transaction to merge + /// potential child transactions into. + /// + /// The caller is responsible for removing it from the undo history using + /// `forget_transaction` if no edits are merged into it. Otherwise, if edits + /// are merged into this transaction, the caller is responsible for ensuring + /// the redo stack is cleared. The easiest way to ensure the redo stack is + /// cleared is to create transactions with the usual `start_transaction` and + /// `end_transaction` methods and merging the resulting transactions into + /// the transaction created by this method + pub fn push_empty_transaction(&mut self, now: Instant) -> TransactionId { + self.history + .push_empty_transaction(self.version.clone(), now, &mut self.lamport_clock) + } + pub fn edited_ranges_for_transaction_id( &self, transaction_id: TransactionId, From 56b99f49fdbba2fe87e4693a646e88a613fdadc0 Mon Sep 17 00:00:00 2001 From: tiagoq <47694386+tiagoq@users.noreply.github.com> Date: Tue, 22 Jul 2025 11:55:24 -0400 Subject: [PATCH 257/658] bedrock: Fix remaining streaming delays (#33931) Closes #26030 *Note: This is my first contribution to Zed* This addresses a second streaming bottleneck in Bedrock that remained after the initial fix in #28281 (released in preview 194). The issue is in the mechanism used to convert Zed's internal `AsyncBody` into the `SdkBody` expected by the Bedrock language provider. We are using a non-streaming converter that buffers responses. **How the fix works:** The AWS SDK provides streaming-compatible converters to create `SdkBody` instances, but these require the input body to implement the `Body` trait from the `http-body` crate. This PR enables streaming by implementing the required trait and switching to the streaming-compatible converter. **Changes (2 commits):** * 1st Commit - **Implement http-body Body trait for AsyncBody:** - Add `http-body = 1.0` dependency (already an indirect dependency) - Implement the `Body` trait for our existing `AsyncBody` type - Uses `poll_frame` to read data chunks asynchronously, preserving streaming behavior * 2nd Commit - **Use streaming-compatible AWS SDK converter:** - Create `SdkBody` using `SdkBody::from_body_1_x()` with the new `Body` trait implementation **Details/FAQ:** **Q: Why add another dependency?** A: We tried to avoid adding a dependency, but the AWS SDK requires the `Body` trait and `http-body` is where it's defined. The crate is already an indirect dependency, making this a reasonable solution. **Q: Why modify the shared `http_client` crate instead of just `aws_bedrock_client`?** A: We considered implementing the `Body` trait on a wrapper in `aws_bedrock_client`, but since `AsyncBody` already uses `http` crate types, extending support to the companion `http-body` crate seems reasonable and may benefit other integrations. **Q: How was this bottleneck discovered?** A: After @5herlocked's initial streaming fix in #28281, I tested preview 194 and noticed streaming still had issues. I found a way to reproduce the problem and chatted with @5herlocked about it. He immediately pinpointed the exact location where the issue was occurring, his diagnosis made this fix possible. **Q: How does this relate to the previous fix?** A: #28281 fixed buffering issues higher in the stack, but unfortunately there was another bottleneck lower-down in the aws-http-client. This PR addresses that separate buffering issue. **Q: Does this use zero-copy or one-copy?** A: The `Body` implementation includes one copy. Someone more knowledgeable might be able to achieve a zero-copy approach, but we opted for a conservative approach. The performance impact should not be perceptible in typical usage. **Testing:** Confirmed that Bedrock streaming now works without buffering delays in a local build. Release Notes: - Improved Bedrock streaming by eliminating response buffering delays --------- Co-authored-by: Marshall Bowers --- Cargo.lock | 3 +- Cargo.toml | 1 + crates/aws_http_client/Cargo.toml | 2 - crates/aws_http_client/src/aws_http_client.rs | 39 +++++-------------- crates/http_client/Cargo.toml | 1 + crates/http_client/src/async_body.rs | 22 +++++++++++ .../language_models/src/provider/bedrock.rs | 8 +--- 7 files changed, 36 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6237bac204f160f54c7fd5829e4c3bdbe3400fc8..c64995b01b7bdd0c511ed1b94000c9e647fcd976 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1872,9 +1872,7 @@ version = "0.1.0" dependencies = [ "aws-smithy-runtime-api", "aws-smithy-types", - "futures 0.3.31", "http_client", - "tokio", "workspace-hack", ] @@ -7857,6 +7855,7 @@ dependencies = [ "derive_more 0.99.19", "futures 0.3.31", "http 1.3.1", + "http-body 1.0.1", "log", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index ea8690f2b3af444900d8760c7a678124702c7f93..ec793a7429a1e786ef8f7620a65169283354ff05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -482,6 +482,7 @@ heed = { version = "0.21.0", features = ["read-txn-no-tls"] } hex = "0.4.3" html5ever = "0.27.0" http = "1.1" +http-body = "1.0" hyper = "0.14" ignore = "0.4.22" image = "0.25.1" diff --git a/crates/aws_http_client/Cargo.toml b/crates/aws_http_client/Cargo.toml index 3760f70fe02973fb9f83ea401f4f9807309cf2d8..2749286d4c1361d9dbdb50d6566e3b4043f97b2e 100644 --- a/crates/aws_http_client/Cargo.toml +++ b/crates/aws_http_client/Cargo.toml @@ -17,7 +17,5 @@ default = [] [dependencies] aws-smithy-runtime-api.workspace = true aws-smithy-types.workspace = true -futures.workspace = true http_client.workspace = true -tokio = { workspace = true, features = ["rt", "rt-multi-thread"] } workspace-hack.workspace = true diff --git a/crates/aws_http_client/src/aws_http_client.rs b/crates/aws_http_client/src/aws_http_client.rs index 6adb995747317c33a3e3a177d0240b45ce03b8f9..d08c8e64a792a06126ecdd8e3833a87bb0ace7ab 100644 --- a/crates/aws_http_client/src/aws_http_client.rs +++ b/crates/aws_http_client/src/aws_http_client.rs @@ -11,14 +11,11 @@ use aws_smithy_runtime_api::client::result::ConnectorError; use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents; use aws_smithy_runtime_api::http::{Headers, StatusCode}; use aws_smithy_types::body::SdkBody; -use futures::AsyncReadExt; -use http_client::{AsyncBody, Inner}; +use http_client::AsyncBody; use http_client::{HttpClient, Request}; -use tokio::runtime::Handle; struct AwsHttpConnector { client: Arc, - handle: Handle, } impl std::fmt::Debug for AwsHttpConnector { @@ -42,18 +39,17 @@ impl AwsConnector for AwsHttpConnector { .client .send(Request::from_parts(parts, convert_to_async_body(body))); - let handle = self.handle.clone(); - HttpConnectorFuture::new(async move { let response = match response.await { Ok(response) => response, Err(err) => return Err(ConnectorError::other(err.into(), None)), }; let (parts, body) = response.into_parts(); - let body = convert_to_sdk_body(body, handle).await; - let mut response = - HttpResponse::new(StatusCode::try_from(parts.status.as_u16()).unwrap(), body); + let mut response = HttpResponse::new( + StatusCode::try_from(parts.status.as_u16()).unwrap(), + convert_to_sdk_body(body), + ); let headers = match Headers::try_from(parts.headers) { Ok(headers) => headers, @@ -70,7 +66,6 @@ impl AwsConnector for AwsHttpConnector { #[derive(Clone)] pub struct AwsHttpClient { client: Arc, - handler: Handle, } impl std::fmt::Debug for AwsHttpClient { @@ -80,11 +75,8 @@ impl std::fmt::Debug for AwsHttpClient { } impl AwsHttpClient { - pub fn new(client: Arc, handle: Handle) -> Self { - Self { - client, - handler: handle, - } + pub fn new(client: Arc) -> Self { + Self { client } } } @@ -96,25 +88,12 @@ impl AwsClient for AwsHttpClient { ) -> SharedHttpConnector { SharedHttpConnector::new(AwsHttpConnector { client: self.client.clone(), - handle: self.handler.clone(), }) } } -pub async fn convert_to_sdk_body(body: AsyncBody, handle: Handle) -> SdkBody { - match body.0 { - Inner::Empty => SdkBody::empty(), - Inner::Bytes(bytes) => SdkBody::from(bytes.into_inner()), - Inner::AsyncReader(mut reader) => { - let buffer = handle.spawn(async move { - let mut buffer = Vec::new(); - let _ = reader.read_to_end(&mut buffer).await; - buffer - }); - - SdkBody::from(buffer.await.unwrap_or_default()) - } - } +pub fn convert_to_sdk_body(body: AsyncBody) -> SdkBody { + SdkBody::from_body_1_x(body) } pub fn convert_to_async_body(body: SdkBody) -> AsyncBody { diff --git a/crates/http_client/Cargo.toml b/crates/http_client/Cargo.toml index 2b114f240acc32f146f5d0fc4b70bbe150f655da..2045708ff24a4dc7bcbcdadec1b54f3f95b3801e 100644 --- a/crates/http_client/Cargo.toml +++ b/crates/http_client/Cargo.toml @@ -21,6 +21,7 @@ anyhow.workspace = true derive_more.workspace = true futures.workspace = true http.workspace = true +http-body.workspace = true log.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/http_client/src/async_body.rs b/crates/http_client/src/async_body.rs index caf8089d0f15d0ce818839bd0672e1a2d1419fc7..88972d279cc05cd6b4d9f531a6c7b2f876dbcf4c 100644 --- a/crates/http_client/src/async_body.rs +++ b/crates/http_client/src/async_body.rs @@ -6,6 +6,7 @@ use std::{ use bytes::Bytes; use futures::AsyncRead; +use http_body::{Body, Frame}; /// Based on the implementation of AsyncBody in /// . @@ -114,3 +115,24 @@ impl futures::AsyncRead for AsyncBody { } } } + +impl Body for AsyncBody { + type Data = Bytes; + type Error = std::io::Error; + + fn poll_frame( + mut self: Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> Poll, Self::Error>>> { + let mut buffer = vec![0; 8192]; + match AsyncRead::poll_read(self.as_mut(), cx, &mut buffer) { + Poll::Ready(Ok(0)) => Poll::Ready(None), + Poll::Ready(Ok(n)) => { + let data = Bytes::copy_from_slice(&buffer[..n]); + Poll::Ready(Some(Ok(Frame::data(data)))) + } + Poll::Ready(Err(e)) => Poll::Ready(Some(Err(e))), + Poll::Pending => Poll::Pending, + } + } +} diff --git a/crates/language_models/src/provider/bedrock.rs b/crates/language_models/src/provider/bedrock.rs index 65ce1dbc4b61cb1d6432fa6e6011aadc4479613f..a022511b11e84719043b4d436a60669590afc625 100644 --- a/crates/language_models/src/provider/bedrock.rs +++ b/crates/language_models/src/provider/bedrock.rs @@ -258,13 +258,9 @@ impl BedrockLanguageModelProvider { }), }); - let tokio_handle = Tokio::handle(cx); - - let coerced_client = AwsHttpClient::new(http_client.clone(), tokio_handle.clone()); - Self { - http_client: coerced_client, - handler: tokio_handle.clone(), + http_client: AwsHttpClient::new(http_client.clone()), + handler: Tokio::handle(cx), state, } } From fa3e1ccc37373777756283829d180e41e63f8d1a Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 22 Jul 2025 18:19:51 +0200 Subject: [PATCH 258/658] chore: Bump taffy to 0.8.3 (#34876) That's the latest release. Note that we have an opportunity to simplify our size types per https://github.com/DioxusLabs/taffy/blob/main/CHANGELOG.md#highlights-1 (though that's left out of this PR) image Release Notes: - N/A --- Cargo.lock | 9 ++++----- crates/gpui/Cargo.toml | 2 +- crates/gpui/src/taffy.rs | 26 +++++++++++++------------- 3 files changed, 18 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c64995b01b7bdd0c511ed1b94000c9e647fcd976..08d29cdc8000bd0a6136d0d7f9237f79f15f830c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7401,9 +7401,9 @@ dependencies = [ [[package]] name = "grid" -version = "0.14.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be136d9dacc2a13cc70bb6c8f902b414fb2641f8db1314637c6b7933411a8f82" +checksum = "71b01d27060ad58be4663b9e4ac9e2d4806918e8876af8912afbddd1a91d5eaa" [[package]] name = "group" @@ -15958,13 +15958,12 @@ dependencies = [ [[package]] name = "taffy" -version = "0.5.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8b61630cba2afd2c851821add2e1bb1b7851a2436e839ab73b56558b009035e" +checksum = "7aaef0ac998e6527d6d0d5582f7e43953bb17221ac75bb8eb2fcc2db3396db1c" dependencies = [ "arrayvec", "grid", - "num-traits", "serde", "slotmap", ] diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 878794647a4633b63eedd36ec07d18ec67ee1ef8..68c0ea89c70f10c634ac3026b9b6997108d33996 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.5.1" +taffy = "=0.8.3" thiserror.workspace = true util.workspace = true uuid.workspace = true diff --git a/crates/gpui/src/taffy.rs b/crates/gpui/src/taffy.rs index 6228a604904f6aa40d6d15fb7f9c5ff19b29f6a1..f7fa54256df20b38170ecb4d3e48c22913e44ae6 100644 --- a/crates/gpui/src/taffy.rs +++ b/crates/gpui/src/taffy.rs @@ -283,7 +283,7 @@ impl ToTaffy for Length { fn to_taffy(&self, rem_size: Pixels) -> taffy::prelude::LengthPercentageAuto { match self { Length::Definite(length) => length.to_taffy(rem_size), - Length::Auto => taffy::prelude::LengthPercentageAuto::Auto, + Length::Auto => taffy::prelude::LengthPercentageAuto::auto(), } } } @@ -292,7 +292,7 @@ impl ToTaffy for Length { fn to_taffy(&self, rem_size: Pixels) -> taffy::prelude::Dimension { match self { Length::Definite(length) => length.to_taffy(rem_size), - Length::Auto => taffy::prelude::Dimension::Auto, + Length::Auto => taffy::prelude::Dimension::auto(), } } } @@ -302,14 +302,14 @@ impl ToTaffy for DefiniteLength { match self { DefiniteLength::Absolute(length) => match length { AbsoluteLength::Pixels(pixels) => { - taffy::style::LengthPercentage::Length(pixels.into()) + taffy::style::LengthPercentage::length(pixels.into()) } AbsoluteLength::Rems(rems) => { - taffy::style::LengthPercentage::Length((*rems * rem_size).into()) + taffy::style::LengthPercentage::length((*rems * rem_size).into()) } }, DefiniteLength::Fraction(fraction) => { - taffy::style::LengthPercentage::Percent(*fraction) + taffy::style::LengthPercentage::percent(*fraction) } } } @@ -320,14 +320,14 @@ impl ToTaffy for DefiniteLength { match self { DefiniteLength::Absolute(length) => match length { AbsoluteLength::Pixels(pixels) => { - taffy::style::LengthPercentageAuto::Length(pixels.into()) + taffy::style::LengthPercentageAuto::length(pixels.into()) } AbsoluteLength::Rems(rems) => { - taffy::style::LengthPercentageAuto::Length((*rems * rem_size).into()) + taffy::style::LengthPercentageAuto::length((*rems * rem_size).into()) } }, DefiniteLength::Fraction(fraction) => { - taffy::style::LengthPercentageAuto::Percent(*fraction) + taffy::style::LengthPercentageAuto::percent(*fraction) } } } @@ -337,12 +337,12 @@ impl ToTaffy for DefiniteLength { fn to_taffy(&self, rem_size: Pixels) -> taffy::style::Dimension { match self { DefiniteLength::Absolute(length) => match length { - AbsoluteLength::Pixels(pixels) => taffy::style::Dimension::Length(pixels.into()), + AbsoluteLength::Pixels(pixels) => taffy::style::Dimension::length(pixels.into()), AbsoluteLength::Rems(rems) => { - taffy::style::Dimension::Length((*rems * rem_size).into()) + taffy::style::Dimension::length((*rems * rem_size).into()) } }, - DefiniteLength::Fraction(fraction) => taffy::style::Dimension::Percent(*fraction), + DefiniteLength::Fraction(fraction) => taffy::style::Dimension::percent(*fraction), } } } @@ -350,9 +350,9 @@ impl ToTaffy for DefiniteLength { impl ToTaffy for AbsoluteLength { fn to_taffy(&self, rem_size: Pixels) -> taffy::style::LengthPercentage { match self { - AbsoluteLength::Pixels(pixels) => taffy::style::LengthPercentage::Length(pixels.into()), + AbsoluteLength::Pixels(pixels) => taffy::style::LengthPercentage::length(pixels.into()), AbsoluteLength::Rems(rems) => { - taffy::style::LengthPercentage::Length((*rems * rem_size).into()) + taffy::style::LengthPercentage::length((*rems * rem_size).into()) } } } From 64d0fec699607f4cef8b2625fef62d1ea105a2ed Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 22 Jul 2025 18:20:48 +0200 Subject: [PATCH 259/658] sum_tree: Store context on cursor (#34904) This gets rid of the need to pass context to all cursor functions. In practice context is always immutable when interacting with cursors. A nicety of this is in the follow-up PR we will be able to implement Iterator for all Cursors/filter cursors (hell, we may be able to get rid of filter cursor altogether, as it is just a custom `filter` impl on iterator trait). Release Notes: - N/A --- crates/buffer_diff/src/buffer_diff.rs | 42 ++- crates/channel/src/channel_chat.rs | 30 +- crates/editor/src/display_map/block_map.rs | 96 +++--- crates/editor/src/display_map/crease_map.rs | 24 +- crates/editor/src/display_map/fold_map.rs | 119 ++++---- crates/editor/src/display_map/inlay_map.rs | 94 +++--- crates/editor/src/display_map/wrap_map.rs | 106 +++---- crates/editor/src/git/blame.rs | 14 +- crates/gpui/src/elements/list.rs | 54 ++-- crates/language/src/diagnostic_set.rs | 8 +- crates/language/src/syntax_map.rs | 35 ++- crates/multi_buffer/src/multi_buffer.rs | 286 +++++++++--------- .../notifications/src/notification_store.rs | 10 +- crates/project/src/git_store.rs | 4 +- crates/project/src/git_store/git_traversal.rs | 7 +- crates/rope/src/rope.rs | 104 +++---- crates/sum_tree/src/cursor.rs | 173 ++++++----- crates/sum_tree/src/sum_tree.rs | 222 +++++--------- crates/sum_tree/src/tree_map.rs | 34 +-- crates/text/src/anchor.rs | 2 +- crates/text/src/text.rs | 115 ++++--- crates/text/src/undo_map.rs | 2 - crates/worktree/src/worktree.rs | 44 ++- 23 files changed, 749 insertions(+), 876 deletions(-) diff --git a/crates/buffer_diff/src/buffer_diff.rs b/crates/buffer_diff/src/buffer_diff.rs index ee09fda46e008c903120eb0430ff18fae57dc3da..97f529fe377c0eaa3d74dca1600e1b3f0c3499db 100644 --- a/crates/buffer_diff/src/buffer_diff.rs +++ b/crates/buffer_diff/src/buffer_diff.rs @@ -343,8 +343,7 @@ impl BufferDiffInner { .. } in hunks.iter().cloned() { - let preceding_pending_hunks = - old_pending_hunks.slice(&buffer_range.start, Bias::Left, buffer); + let preceding_pending_hunks = old_pending_hunks.slice(&buffer_range.start, Bias::Left); pending_hunks.append(preceding_pending_hunks, buffer); // Skip all overlapping or adjacent old pending hunks @@ -355,7 +354,7 @@ impl BufferDiffInner { .cmp(&buffer_range.end, buffer) .is_le() }) { - old_pending_hunks.next(buffer); + old_pending_hunks.next(); } if (stage && secondary_status == DiffHunkSecondaryStatus::NoSecondaryHunk) @@ -379,10 +378,10 @@ impl BufferDiffInner { ); } // append the remainder - pending_hunks.append(old_pending_hunks.suffix(buffer), buffer); + pending_hunks.append(old_pending_hunks.suffix(), buffer); let mut unstaged_hunk_cursor = unstaged_diff.hunks.cursor::(buffer); - unstaged_hunk_cursor.next(buffer); + unstaged_hunk_cursor.next(); // then, iterate over all pending hunks (both new ones and the existing ones) and compute the edits let mut prev_unstaged_hunk_buffer_end = 0; @@ -397,8 +396,7 @@ impl BufferDiffInner { }) = pending_hunks_iter.next() { // Advance unstaged_hunk_cursor to skip unstaged hunks before current hunk - let skipped_unstaged = - unstaged_hunk_cursor.slice(&buffer_range.start, Bias::Left, buffer); + let skipped_unstaged = unstaged_hunk_cursor.slice(&buffer_range.start, Bias::Left); if let Some(unstaged_hunk) = skipped_unstaged.last() { prev_unstaged_hunk_base_text_end = unstaged_hunk.diff_base_byte_range.end; @@ -425,7 +423,7 @@ impl BufferDiffInner { buffer_offset_range.end = buffer_offset_range.end.max(unstaged_hunk_offset_range.end); - unstaged_hunk_cursor.next(buffer); + unstaged_hunk_cursor.next(); continue; } } @@ -514,7 +512,7 @@ impl BufferDiffInner { }); let anchor_iter = iter::from_fn(move || { - cursor.next(buffer); + cursor.next(); cursor.item() }) .flat_map(move |hunk| { @@ -531,12 +529,12 @@ impl BufferDiffInner { }); let mut pending_hunks_cursor = self.pending_hunks.cursor::(buffer); - pending_hunks_cursor.next(buffer); + pending_hunks_cursor.next(); let mut secondary_cursor = None; if let Some(secondary) = secondary.as_ref() { let mut cursor = secondary.hunks.cursor::(buffer); - cursor.next(buffer); + cursor.next(); secondary_cursor = Some(cursor); } @@ -564,7 +562,7 @@ impl BufferDiffInner { .cmp(&pending_hunks_cursor.start().buffer_range.start, buffer) .is_gt() { - pending_hunks_cursor.seek_forward(&start_anchor, Bias::Left, buffer); + pending_hunks_cursor.seek_forward(&start_anchor, Bias::Left); } if let Some(pending_hunk) = pending_hunks_cursor.item() { @@ -590,7 +588,7 @@ impl BufferDiffInner { .cmp(&secondary_cursor.start().buffer_range.start, buffer) .is_gt() { - secondary_cursor.seek_forward(&start_anchor, Bias::Left, buffer); + secondary_cursor.seek_forward(&start_anchor, Bias::Left); } if let Some(secondary_hunk) = secondary_cursor.item() { @@ -635,7 +633,7 @@ impl BufferDiffInner { }); iter::from_fn(move || { - cursor.prev(buffer); + cursor.prev(); let hunk = cursor.item()?; let range = hunk.buffer_range.to_point(buffer); @@ -653,8 +651,8 @@ impl BufferDiffInner { fn compare(&self, old: &Self, new_snapshot: &text::BufferSnapshot) -> Option> { let mut new_cursor = self.hunks.cursor::<()>(new_snapshot); let mut old_cursor = old.hunks.cursor::<()>(new_snapshot); - old_cursor.next(new_snapshot); - new_cursor.next(new_snapshot); + old_cursor.next(); + new_cursor.next(); let mut start = None; let mut end = None; @@ -669,7 +667,7 @@ impl BufferDiffInner { Ordering::Less => { start.get_or_insert(new_hunk.buffer_range.start); end.replace(new_hunk.buffer_range.end); - new_cursor.next(new_snapshot); + new_cursor.next(); } Ordering::Equal => { if new_hunk != old_hunk { @@ -686,25 +684,25 @@ impl BufferDiffInner { } } - new_cursor.next(new_snapshot); - old_cursor.next(new_snapshot); + new_cursor.next(); + old_cursor.next(); } Ordering::Greater => { start.get_or_insert(old_hunk.buffer_range.start); end.replace(old_hunk.buffer_range.end); - old_cursor.next(new_snapshot); + old_cursor.next(); } } } (Some(new_hunk), None) => { start.get_or_insert(new_hunk.buffer_range.start); end.replace(new_hunk.buffer_range.end); - new_cursor.next(new_snapshot); + new_cursor.next(); } (None, Some(old_hunk)) => { start.get_or_insert(old_hunk.buffer_range.start); end.replace(old_hunk.buffer_range.end); - old_cursor.next(new_snapshot); + old_cursor.next(); } (None, None) => break, } diff --git a/crates/channel/src/channel_chat.rs b/crates/channel/src/channel_chat.rs index 8394972d43754e07d0f197a315a4e17879aa17fe..866e3ccd90f962c63718e09e216b1dac78ba5bca 100644 --- a/crates/channel/src/channel_chat.rs +++ b/crates/channel/src/channel_chat.rs @@ -333,7 +333,7 @@ impl ChannelChat { if first_id <= message_id { let mut cursor = chat.messages.cursor::<(ChannelMessageId, Count)>(&()); let message_id = ChannelMessageId::Saved(message_id); - cursor.seek(&message_id, Bias::Left, &()); + cursor.seek(&message_id, Bias::Left); return ControlFlow::Break( if cursor .item() @@ -499,7 +499,7 @@ impl ChannelChat { pub fn message(&self, ix: usize) -> &ChannelMessage { let mut cursor = self.messages.cursor::(&()); - cursor.seek(&Count(ix), Bias::Right, &()); + cursor.seek(&Count(ix), Bias::Right); cursor.item().unwrap() } @@ -516,13 +516,13 @@ impl ChannelChat { pub fn messages_in_range(&self, range: Range) -> impl Iterator { let mut cursor = self.messages.cursor::(&()); - cursor.seek(&Count(range.start), Bias::Right, &()); + cursor.seek(&Count(range.start), Bias::Right); cursor.take(range.len()) } pub fn pending_messages(&self) -> impl Iterator { let mut cursor = self.messages.cursor::(&()); - cursor.seek(&ChannelMessageId::Pending(0), Bias::Left, &()); + cursor.seek(&ChannelMessageId::Pending(0), Bias::Left); cursor } @@ -588,9 +588,9 @@ impl ChannelChat { .collect::>(); let mut old_cursor = self.messages.cursor::<(ChannelMessageId, Count)>(&()); - let mut new_messages = old_cursor.slice(&first_message.id, Bias::Left, &()); + 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, &()); + let removed_messages = old_cursor.slice(&last_message.id, Bias::Right); let removed_count = removed_messages.summary().count; let new_count = messages.summary().count; let end_ix = start_ix + removed_count; @@ -599,10 +599,10 @@ impl ChannelChat { let mut ranges = Vec::>::new(); if new_messages.last().unwrap().is_pending() { - new_messages.append(old_cursor.suffix(&()), &()); + new_messages.append(old_cursor.suffix(), &()); } else { new_messages.append( - old_cursor.slice(&ChannelMessageId::Pending(0), Bias::Left, &()), + old_cursor.slice(&ChannelMessageId::Pending(0), Bias::Left), &(), ); @@ -617,7 +617,7 @@ impl ChannelChat { } else { new_messages.push(message.clone(), &()); } - old_cursor.next(&()); + old_cursor.next(); } } @@ -641,12 +641,12 @@ impl ChannelChat { fn message_removed(&mut self, id: u64, cx: &mut Context) { let mut cursor = self.messages.cursor::(&()); - let mut messages = cursor.slice(&ChannelMessageId::Saved(id), Bias::Left, &()); + 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(&()), &()); + cursor.next(); + messages.append(cursor.suffix(), &()); drop(cursor); self.messages = messages; @@ -680,7 +680,7 @@ impl ChannelChat { cx: &mut Context, ) { let mut cursor = self.messages.cursor::(&()); - let mut messages = cursor.slice(&id, Bias::Left, &()); + let mut messages = cursor.slice(&id, Bias::Left); let ix = messages.summary().count; if let Some(mut message_to_update) = cursor.item().cloned() { @@ -688,10 +688,10 @@ impl ChannelChat { message_to_update.mentions = mentions; message_to_update.edited_at = edited_at; messages.push(message_to_update, &()); - cursor.next(&()); + cursor.next(); } - messages.append(cursor.suffix(&()), &()); + messages.append(cursor.suffix(), &()); drop(cursor); self.messages = messages; diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index c761e0d69ceea5a8e36441df410071016dc1f200..85495a2611ed1fd1d57c6165ab40eba66388602f 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -524,10 +524,10 @@ impl BlockMap { // * Isomorphic transforms that end *at* the start of the edit // * 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, &()), &()); + 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 + && cursor.end() == old_start && transform .block .as_ref() @@ -535,13 +535,13 @@ impl BlockMap { { // Preserve the transform (push and next) new_transforms.push(transform.clone(), &()); - cursor.next(&()); + 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(&()); + cursor.next(); } else { break; } @@ -579,8 +579,8 @@ impl BlockMap { let mut new_end = WrapRow(edit.new.end); loop { // Seek to the transform starting at or after the end of the edit - cursor.seek(&old_end, Bias::Left, &()); - cursor.next(&()); + cursor.seek(&old_end, Bias::Left); + cursor.next(); // Extend edit to the end of the discarded transform so it is reconstructed in full let transform_rows_after_edit = cursor.start().0 - old_end.0; @@ -592,8 +592,8 @@ impl BlockMap { if next_edit.old.start <= cursor.start().0 { old_end = WrapRow(next_edit.old.end); new_end = WrapRow(next_edit.new.end); - cursor.seek(&old_end, Bias::Left, &()); - cursor.next(&()); + cursor.seek(&old_end, Bias::Left); + cursor.next(); edits.next(); } else { break; @@ -608,7 +608,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()) { - cursor.next(&()); + cursor.next(); } else { break; } @@ -720,7 +720,7 @@ impl BlockMap { push_isomorphic(&mut new_transforms, rows_after_last_block, wrap_snapshot); } - new_transforms.append(cursor.suffix(&()), &()); + new_transforms.append(cursor.suffix(), &()); debug_assert_eq!( new_transforms.summary().input_rows, wrap_snapshot.max_point().row() + 1 @@ -971,7 +971,7 @@ impl BlockMapReader<'_> { ); let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>(&()); - cursor.seek(&start_wrap_row, Bias::Left, &()); + cursor.seek(&start_wrap_row, Bias::Left); while let Some(transform) = cursor.item() { if cursor.start().0 > end_wrap_row { break; @@ -982,7 +982,7 @@ impl BlockMapReader<'_> { return Some(cursor.start().1); } } - cursor.next(&()); + cursor.next(); } None @@ -1293,7 +1293,7 @@ impl BlockSnapshot { let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows); let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); - cursor.seek(&BlockRow(rows.start), Bias::Right, &()); + cursor.seek(&BlockRow(rows.start), Bias::Right); let transform_output_start = cursor.start().0.0; let transform_input_start = cursor.start().1.0; @@ -1325,7 +1325,7 @@ impl BlockSnapshot { pub(super) fn row_infos(&self, start_row: BlockRow) -> BlockRows<'_> { let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); - cursor.seek(&start_row, Bias::Right, &()); + cursor.seek(&start_row, Bias::Right); let (output_start, input_start) = cursor.start(); let overshoot = if cursor .item() @@ -1346,9 +1346,9 @@ impl BlockSnapshot { pub fn blocks_in_range(&self, rows: Range) -> impl Iterator { let mut cursor = self.transforms.cursor::(&()); - cursor.seek(&BlockRow(rows.start), Bias::Left, &()); - while cursor.start().0 < rows.start && cursor.end(&()).0 <= rows.start { - cursor.next(&()); + cursor.seek(&BlockRow(rows.start), Bias::Left); + while cursor.start().0 < rows.start && cursor.end().0 <= rows.start { + cursor.next(); } std::iter::from_fn(move || { @@ -1364,10 +1364,10 @@ impl BlockSnapshot { break; } if let Some(block) = &transform.block { - cursor.next(&()); + cursor.next(); return Some((start_row, block)); } else { - cursor.next(&()); + cursor.next(); } } None @@ -1377,7 +1377,7 @@ impl BlockSnapshot { pub fn sticky_header_excerpt(&self, position: f32) -> Option> { let top_row = position as u32; let mut cursor = self.transforms.cursor::(&()); - cursor.seek(&BlockRow(top_row), Bias::Right, &()); + cursor.seek(&BlockRow(top_row), Bias::Right); while let Some(transform) = cursor.item() { match &transform.block { @@ -1386,7 +1386,7 @@ impl BlockSnapshot { } Some(block) if block.is_buffer_header() => return None, _ => { - cursor.prev(&()); + cursor.prev(); continue; } } @@ -1414,7 +1414,7 @@ impl BlockSnapshot { let wrap_row = WrapRow(wrap_point.row()); let mut cursor = self.transforms.cursor::(&()); - cursor.seek(&wrap_row, Bias::Left, &()); + cursor.seek(&wrap_row, Bias::Left); while let Some(transform) = cursor.item() { if let Some(block) = transform.block.as_ref() { @@ -1425,7 +1425,7 @@ impl BlockSnapshot { break; } - cursor.next(&()); + cursor.next(); } None @@ -1442,7 +1442,7 @@ impl BlockSnapshot { pub fn longest_row_in_range(&self, range: Range) -> BlockRow { let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); - cursor.seek(&range.start, Bias::Right, &()); + cursor.seek(&range.start, Bias::Right); let mut longest_row = range.start; let mut longest_row_chars = 0; @@ -1453,7 +1453,7 @@ impl BlockSnapshot { let wrap_start_row = input_start.0 + overshoot; let wrap_end_row = cmp::min( input_start.0 + (range.end.0 - output_start.0), - cursor.end(&()).1.0, + cursor.end().1.0, ); let summary = self .wrap_snapshot @@ -1461,12 +1461,12 @@ impl BlockSnapshot { longest_row = BlockRow(range.start.0 + summary.longest_row); longest_row_chars = summary.longest_row_chars; } - cursor.next(&()); + cursor.next(); } let cursor_start_row = cursor.start().0; if range.end > cursor_start_row { - let summary = cursor.summary::<_, TransformSummary>(&range.end, Bias::Right, &()); + let summary = cursor.summary::<_, TransformSummary>(&range.end, Bias::Right); if summary.longest_row_chars > longest_row_chars { longest_row = BlockRow(cursor_start_row.0 + summary.longest_row); longest_row_chars = summary.longest_row_chars; @@ -1493,7 +1493,7 @@ impl BlockSnapshot { pub(super) fn line_len(&self, row: BlockRow) -> u32 { let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); - cursor.seek(&BlockRow(row.0), Bias::Right, &()); + cursor.seek(&BlockRow(row.0), Bias::Right); if let Some(transform) = cursor.item() { let (output_start, input_start) = cursor.start(); let overshoot = row.0 - output_start.0; @@ -1511,13 +1511,13 @@ impl BlockSnapshot { pub(super) fn is_block_line(&self, row: BlockRow) -> bool { let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); - cursor.seek(&row, Bias::Right, &()); + 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)>(&()); - cursor.seek(&row, Bias::Right, &()); + cursor.seek(&row, Bias::Right); let Some(transform) = cursor.item() else { return false; }; @@ -1529,7 +1529,7 @@ impl BlockSnapshot { .wrap_snapshot .make_wrap_point(Point::new(row.0, 0), Bias::Left); let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>(&()); - cursor.seek(&WrapRow(wrap_point.row()), Bias::Right, &()); + cursor.seek(&WrapRow(wrap_point.row()), Bias::Right); cursor.item().map_or(false, |transform| { transform .block @@ -1540,17 +1540,17 @@ impl BlockSnapshot { pub fn clip_point(&self, point: BlockPoint, bias: Bias) -> BlockPoint { let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); - cursor.seek(&BlockRow(point.row), Bias::Right, &()); + cursor.seek(&BlockRow(point.row), Bias::Right); let max_input_row = WrapRow(self.transforms.summary().input_rows); let mut search_left = - (bias == Bias::Left && cursor.start().1.0 > 0) || cursor.end(&()).1 == max_input_row; + (bias == Bias::Left && cursor.start().1.0 > 0) || cursor.end().1 == max_input_row; let mut reversed = false; 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 (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); @@ -1584,23 +1584,23 @@ impl BlockSnapshot { } if search_left { - cursor.prev(&()); + cursor.prev(); } else { - cursor.next(&()); + cursor.next(); } } else if reversed { return self.max_point(); } else { reversed = true; search_left = !search_left; - cursor.seek(&BlockRow(point.row), Bias::Right, &()); + cursor.seek(&BlockRow(point.row), Bias::Right); } } } pub fn to_block_point(&self, wrap_point: WrapPoint) -> BlockPoint { let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>(&()); - cursor.seek(&WrapRow(wrap_point.row()), Bias::Right, &()); + 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) @@ -1618,7 +1618,7 @@ impl BlockSnapshot { pub fn to_wrap_point(&self, block_point: BlockPoint, bias: Bias) -> WrapPoint { let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(&()); - cursor.seek(&BlockRow(block_point.row), Bias::Right, &()); + cursor.seek(&BlockRow(block_point.row), Bias::Right); if let Some(transform) = cursor.item() { match transform.block.as_ref() { Some(block) => { @@ -1630,7 +1630,7 @@ impl BlockSnapshot { } else if bias == Bias::Left { WrapPoint::new(cursor.start().1.0, 0) } else { - let wrap_row = cursor.end(&()).1.0 - 1; + let wrap_row = cursor.end().1.0 - 1; WrapPoint::new(wrap_row, self.wrap_snapshot.line_len(wrap_row)) } } @@ -1650,14 +1650,14 @@ impl BlockChunks<'_> { /// Go to the next transform fn advance(&mut self) { self.input_chunk = Chunk::default(); - self.transforms.next(&()); + self.transforms.next(); while let Some(transform) = self.transforms.item() { if transform .block .as_ref() .map_or(false, |block| block.height() == 0) { - self.transforms.next(&()); + self.transforms.next(); } else { break; } @@ -1672,7 +1672,7 @@ impl BlockChunks<'_> { let start_output_row = self.transforms.start().0.0; if start_output_row < self.max_output_row { let end_input_row = cmp::min( - self.transforms.end(&()).1.0, + self.transforms.end().1.0, start_input_row + (self.max_output_row - start_output_row), ); self.input_chunks.seek(start_input_row..end_input_row); @@ -1696,7 +1696,7 @@ impl<'a> Iterator for BlockChunks<'a> { let transform = self.transforms.item()?; if transform.block.is_some() { let block_start = self.transforms.start().0.0; - let mut block_end = self.transforms.end(&()).0.0; + let mut block_end = self.transforms.end().0.0; self.advance(); if self.transforms.item().is_none() { block_end -= 1; @@ -1731,7 +1731,7 @@ impl<'a> Iterator for BlockChunks<'a> { } } - let transform_end = self.transforms.end(&()).0.0; + let transform_end = self.transforms.end().0.0; let (prefix_rows, prefix_bytes) = offset_for_row(self.input_chunk.text, transform_end - self.output_row); self.output_row += prefix_rows; @@ -1770,15 +1770,15 @@ impl Iterator for BlockRows<'_> { self.started = true; } - if self.output_row.0 >= self.transforms.end(&()).0.0 { - self.transforms.next(&()); + if self.output_row.0 >= self.transforms.end().0.0 { + self.transforms.next(); while let Some(transform) = self.transforms.item() { if transform .block .as_ref() .map_or(false, |block| block.height() == 0) { - self.transforms.next(&()); + self.transforms.next(); } else { break; } diff --git a/crates/editor/src/display_map/crease_map.rs b/crates/editor/src/display_map/crease_map.rs index e6fe4270eccf86dacf38fa1e06a2c5a4f6081546..bdac982fa785e7b6628352572ab143fd978938b2 100644 --- a/crates/editor/src/display_map/crease_map.rs +++ b/crates/editor/src/display_map/crease_map.rs @@ -52,15 +52,15 @@ impl CreaseSnapshot { ) -> Option<&'a Crease> { let start = snapshot.anchor_before(Point::new(row.0, 0)); let mut cursor = self.creases.cursor::(snapshot); - cursor.seek(&start, Bias::Left, snapshot); + cursor.seek(&start, Bias::Left); while let Some(item) = cursor.item() { match Ord::cmp(&item.crease.range().start.to_point(snapshot).row, &row.0) { - Ordering::Less => cursor.next(snapshot), + Ordering::Less => cursor.next(), Ordering::Equal => { if item.crease.range().start.is_valid(snapshot) { return Some(&item.crease); } else { - cursor.next(snapshot); + cursor.next(); } } Ordering::Greater => break, @@ -76,11 +76,11 @@ impl CreaseSnapshot { ) -> impl 'a + Iterator> { let start = snapshot.anchor_before(Point::new(range.start.0, 0)); let mut cursor = self.creases.cursor::(snapshot); - cursor.seek(&start, Bias::Left, snapshot); + cursor.seek(&start, Bias::Left); std::iter::from_fn(move || { while let Some(item) = cursor.item() { - cursor.next(snapshot); + cursor.next(); let crease_range = item.crease.range(); let crease_start = crease_range.start.to_point(snapshot); let crease_end = crease_range.end.to_point(snapshot); @@ -102,13 +102,13 @@ impl CreaseSnapshot { let mut cursor = self.creases.cursor::(snapshot); let mut results = Vec::new(); - cursor.next(snapshot); + cursor.next(); while let Some(item) = cursor.item() { let crease_range = item.crease.range(); let start_point = crease_range.start.to_point(snapshot); let end_point = crease_range.end.to_point(snapshot); results.push((item.id, start_point..end_point)); - cursor.next(snapshot); + cursor.next(); } results @@ -298,7 +298,7 @@ impl CreaseMap { let mut cursor = self.snapshot.creases.cursor::(snapshot); for crease in creases { let crease_range = crease.range().clone(); - new_creases.append(cursor.slice(&crease_range, Bias::Left, snapshot), snapshot); + new_creases.append(cursor.slice(&crease_range, Bias::Left), snapshot); let id = self.next_id; self.next_id.0 += 1; @@ -306,7 +306,7 @@ impl CreaseMap { new_creases.push(CreaseItem { crease, id }, snapshot); new_ids.push(id); } - new_creases.append(cursor.suffix(snapshot), snapshot); + new_creases.append(cursor.suffix(), snapshot); new_creases }; new_ids @@ -332,9 +332,9 @@ impl CreaseMap { let mut cursor = self.snapshot.creases.cursor::(snapshot); for (id, range) in &removals { - new_creases.append(cursor.slice(range, Bias::Left, snapshot), snapshot); + new_creases.append(cursor.slice(range, Bias::Left), snapshot); while let Some(item) = cursor.item() { - cursor.next(snapshot); + cursor.next(); if item.id == *id { break; } else { @@ -343,7 +343,7 @@ impl CreaseMap { } } - new_creases.append(cursor.suffix(snapshot), snapshot); + new_creases.append(cursor.suffix(), snapshot); new_creases }; diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index f37e7063e7228176b0f5455c278f331ed31d6ba0..829d34ff5895c115a80b54adf3af5dbbc6e429b5 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -99,7 +99,7 @@ impl FoldPoint { pub fn to_inlay_point(self, snapshot: &FoldSnapshot) -> InlayPoint { let mut cursor = snapshot.transforms.cursor::<(FoldPoint, InlayPoint)>(&()); - cursor.seek(&self, Bias::Right, &()); + cursor.seek(&self, Bias::Right); let overshoot = self.0 - cursor.start().0.0; InlayPoint(cursor.start().1.0 + overshoot) } @@ -108,7 +108,7 @@ impl FoldPoint { let mut cursor = snapshot .transforms .cursor::<(FoldPoint, TransformSummary)>(&()); - cursor.seek(&self, Bias::Right, &()); + cursor.seek(&self, Bias::Right); let overshoot = self.0 - cursor.start().1.output.lines; let mut offset = cursor.start().1.output.len; if !overshoot.is_zero() { @@ -187,10 +187,10 @@ impl FoldMapWriter<'_> { width: None, }, ); - new_tree.append(cursor.slice(&fold.range, Bias::Right, buffer), buffer); + new_tree.append(cursor.slice(&fold.range, Bias::Right), buffer); new_tree.push(fold, buffer); } - new_tree.append(cursor.suffix(buffer), buffer); + new_tree.append(cursor.suffix(), buffer); new_tree }; @@ -252,7 +252,7 @@ impl FoldMapWriter<'_> { fold_ixs_to_delete.push(*folds_cursor.start()); self.0.snapshot.fold_metadata_by_id.remove(&fold.id); } - folds_cursor.next(buffer); + folds_cursor.next(); } } @@ -263,10 +263,10 @@ impl FoldMapWriter<'_> { let mut cursor = self.0.snapshot.folds.cursor::(buffer); let mut folds = SumTree::new(buffer); for fold_ix in fold_ixs_to_delete { - folds.append(cursor.slice(&fold_ix, Bias::Right, buffer), buffer); - cursor.next(buffer); + folds.append(cursor.slice(&fold_ix, Bias::Right), buffer); + cursor.next(); } - folds.append(cursor.suffix(buffer), buffer); + folds.append(cursor.suffix(), buffer); folds }; @@ -412,7 +412,7 @@ impl FoldMap { let mut new_transforms = SumTree::::default(); let mut cursor = self.snapshot.transforms.cursor::(&()); - cursor.seek(&InlayOffset(0), Bias::Right, &()); + cursor.seek(&InlayOffset(0), Bias::Right); while let Some(mut edit) = inlay_edits_iter.next() { if let Some(item) = cursor.item() { @@ -421,19 +421,19 @@ impl FoldMap { |transform| { if !transform.is_fold() { transform.summary.add_summary(&item.summary, &()); - cursor.next(&()); + cursor.next(); } }, &(), ); } } - new_transforms.append(cursor.slice(&edit.old.start, Bias::Left, &()), &()); + new_transforms.append(cursor.slice(&edit.old.start, Bias::Left), &()); edit.new.start -= edit.old.start - *cursor.start(); edit.old.start = *cursor.start(); - cursor.seek(&edit.old.end, Bias::Right, &()); - cursor.next(&()); + cursor.seek(&edit.old.end, Bias::Right); + cursor.next(); let mut delta = edit.new_len().0 as isize - edit.old_len().0 as isize; loop { @@ -449,8 +449,8 @@ impl FoldMap { if next_edit.old.end >= edit.old.end { edit.old.end = next_edit.old.end; - cursor.seek(&edit.old.end, Bias::Right, &()); - cursor.next(&()); + cursor.seek(&edit.old.end, Bias::Right); + cursor.next(); } } else { break; @@ -467,11 +467,7 @@ impl FoldMap { .snapshot .folds .cursor::(&inlay_snapshot.buffer); - folds_cursor.seek( - &FoldRange(anchor..Anchor::max()), - Bias::Left, - &inlay_snapshot.buffer, - ); + folds_cursor.seek(&FoldRange(anchor..Anchor::max()), Bias::Left); let mut folds = iter::from_fn({ let inlay_snapshot = &inlay_snapshot; @@ -485,7 +481,7 @@ impl FoldMap { ..inlay_snapshot.to_inlay_offset(buffer_end), ) }); - folds_cursor.next(&inlay_snapshot.buffer); + folds_cursor.next(); item } }) @@ -558,7 +554,7 @@ impl FoldMap { } } - new_transforms.append(cursor.suffix(&()), &()); + new_transforms.append(cursor.suffix(), &()); if new_transforms.is_empty() { let text_summary = inlay_snapshot.text_summary(); push_isomorphic(&mut new_transforms, text_summary); @@ -575,31 +571,31 @@ impl FoldMap { let mut new_transforms = new_transforms.cursor::<(InlayOffset, FoldOffset)>(&()); for mut edit in inlay_edits { - old_transforms.seek(&edit.old.start, Bias::Left, &()); + old_transforms.seek(&edit.old.start, Bias::Left); if old_transforms.item().map_or(false, |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, &()); + old_transforms.seek_forward(&edit.old.end, Bias::Right); if old_transforms.item().map_or(false, |t| t.is_fold()) { - old_transforms.next(&()); + old_transforms.next(); edit.old.end = old_transforms.start().0; } let old_end = old_transforms.start().1.0 + (edit.old.end - old_transforms.start().0).0; - new_transforms.seek(&edit.new.start, Bias::Left, &()); + new_transforms.seek(&edit.new.start, Bias::Left); if new_transforms.item().map_or(false, |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, &()); + new_transforms.seek_forward(&edit.new.end, Bias::Right); if new_transforms.item().map_or(false, |t| t.is_fold()) { - new_transforms.next(&()); + new_transforms.next(); edit.new.end = new_transforms.start().0; } let new_end = @@ -656,10 +652,10 @@ impl FoldSnapshot { let mut summary = TextSummary::default(); let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>(&()); - cursor.seek(&range.start, Bias::Right, &()); + cursor.seek(&range.start, Bias::Right); if let Some(transform) = cursor.item() { let start_in_transform = range.start.0 - cursor.start().0.0; - let end_in_transform = cmp::min(range.end, cursor.end(&()).0).0 - cursor.start().0.0; + let end_in_transform = cmp::min(range.end, cursor.end().0).0 - cursor.start().0.0; if let Some(placeholder) = transform.placeholder.as_ref() { summary = TextSummary::from( &placeholder.text @@ -678,10 +674,10 @@ impl FoldSnapshot { } } - if range.end > cursor.end(&()).0 { - cursor.next(&()); + if range.end > cursor.end().0 { + cursor.next(); summary += &cursor - .summary::<_, TransformSummary>(&range.end, Bias::Right, &()) + .summary::<_, TransformSummary>(&range.end, Bias::Right) .output; if let Some(transform) = cursor.item() { let end_in_transform = range.end.0 - cursor.start().0.0; @@ -705,19 +701,16 @@ impl FoldSnapshot { pub fn to_fold_point(&self, point: InlayPoint, bias: Bias) -> FoldPoint { let mut cursor = self.transforms.cursor::<(InlayPoint, FoldPoint)>(&()); - cursor.seek(&point, Bias::Right, &()); + cursor.seek(&point, Bias::Right); if cursor.item().map_or(false, |t| t.is_fold()) { if bias == Bias::Left || point == cursor.start().0 { cursor.start().1 } else { - cursor.end(&()).1 + cursor.end().1 } } else { let overshoot = point.0 - cursor.start().0.0; - FoldPoint(cmp::min( - cursor.start().1.0 + overshoot, - cursor.end(&()).1.0, - )) + FoldPoint(cmp::min(cursor.start().1.0 + overshoot, cursor.end().1.0)) } } @@ -742,7 +735,7 @@ impl FoldSnapshot { let fold_point = FoldPoint::new(start_row, 0); let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>(&()); - cursor.seek(&fold_point, Bias::Left, &()); + cursor.seek(&fold_point, Bias::Left); let overshoot = fold_point.0 - cursor.start().0.0; let inlay_point = InlayPoint(cursor.start().1.0 + overshoot); @@ -773,7 +766,7 @@ impl FoldSnapshot { let mut folds = intersecting_folds(&self.inlay_snapshot, &self.folds, range, false); iter::from_fn(move || { let item = folds.item(); - folds.next(&self.inlay_snapshot.buffer); + folds.next(); item }) } @@ -785,7 +778,7 @@ impl FoldSnapshot { let buffer_offset = offset.to_offset(&self.inlay_snapshot.buffer); let inlay_offset = self.inlay_snapshot.to_inlay_offset(buffer_offset); let mut cursor = self.transforms.cursor::(&()); - cursor.seek(&inlay_offset, Bias::Right, &()); + cursor.seek(&inlay_offset, Bias::Right); cursor.item().map_or(false, |t| t.placeholder.is_some()) } @@ -794,7 +787,7 @@ impl FoldSnapshot { .inlay_snapshot .to_inlay_point(Point::new(buffer_row.0, 0)); let mut cursor = self.transforms.cursor::(&()); - cursor.seek(&inlay_point, Bias::Right, &()); + cursor.seek(&inlay_point, Bias::Right); loop { match cursor.item() { Some(transform) => { @@ -808,11 +801,11 @@ impl FoldSnapshot { None => return false, } - if cursor.end(&()).row() == inlay_point.row() { - cursor.next(&()); + if cursor.end().row() == inlay_point.row() { + cursor.next(); } else { inlay_point.0 += Point::new(1, 0); - cursor.seek(&inlay_point, Bias::Right, &()); + cursor.seek(&inlay_point, Bias::Right); } } } @@ -824,14 +817,14 @@ impl FoldSnapshot { highlights: Highlights<'a>, ) -> FoldChunks<'a> { let mut transform_cursor = self.transforms.cursor::<(FoldOffset, InlayOffset)>(&()); - transform_cursor.seek(&range.start, Bias::Right, &()); + transform_cursor.seek(&range.start, Bias::Right); let inlay_start = { let overshoot = range.start.0 - transform_cursor.start().0.0; transform_cursor.start().1 + InlayOffset(overshoot) }; - let transform_end = transform_cursor.end(&()); + let transform_end = transform_cursor.end(); let inlay_end = if transform_cursor .item() @@ -879,14 +872,14 @@ impl FoldSnapshot { pub fn clip_point(&self, point: FoldPoint, bias: Bias) -> FoldPoint { let mut cursor = self.transforms.cursor::<(FoldPoint, InlayPoint)>(&()); - cursor.seek(&point, Bias::Right, &()); + cursor.seek(&point, Bias::Right); if let Some(transform) = cursor.item() { let transform_start = cursor.start().0.0; if transform.placeholder.is_some() { if point.0 == transform_start || matches!(bias, Bias::Left) { FoldPoint(transform_start) } else { - FoldPoint(cursor.end(&()).0.0) + FoldPoint(cursor.end().0.0) } } else { let overshoot = InlayPoint(point.0 - transform_start); @@ -945,7 +938,7 @@ fn intersecting_folds<'a>( start_cmp == Ordering::Less && end_cmp == Ordering::Greater } }); - cursor.next(buffer); + cursor.next(); cursor } @@ -1211,7 +1204,7 @@ pub struct FoldRows<'a> { impl FoldRows<'_> { pub(crate) fn seek(&mut self, row: u32) { let fold_point = FoldPoint::new(row, 0); - self.cursor.seek(&fold_point, Bias::Left, &()); + self.cursor.seek(&fold_point, Bias::Left); let overshoot = fold_point.0 - self.cursor.start().0.0; let inlay_point = InlayPoint(self.cursor.start().1.0 + overshoot); self.input_rows.seek(inlay_point.row()); @@ -1224,8 +1217,8 @@ impl Iterator for FoldRows<'_> { fn next(&mut self) -> Option { let mut traversed_fold = false; - while self.fold_point > self.cursor.end(&()).0 { - self.cursor.next(&()); + while self.fold_point > self.cursor.end().0 { + self.cursor.next(); traversed_fold = true; if self.cursor.item().is_none() { break; @@ -1330,14 +1323,14 @@ pub struct FoldChunks<'a> { impl FoldChunks<'_> { pub(crate) fn seek(&mut self, range: Range) { - self.transform_cursor.seek(&range.start, Bias::Right, &()); + self.transform_cursor.seek(&range.start, Bias::Right); let inlay_start = { let overshoot = range.start.0 - self.transform_cursor.start().0.0; self.transform_cursor.start().1 + InlayOffset(overshoot) }; - let transform_end = self.transform_cursor.end(&()); + let transform_end = self.transform_cursor.end(); let inlay_end = if self .transform_cursor @@ -1376,10 +1369,10 @@ impl<'a> Iterator for FoldChunks<'a> { self.inlay_chunk.take(); self.inlay_offset += InlayOffset(transform.summary.input.len); - while self.inlay_offset >= self.transform_cursor.end(&()).1 + while self.inlay_offset >= self.transform_cursor.end().1 && self.transform_cursor.item().is_some() { - self.transform_cursor.next(&()); + self.transform_cursor.next(); } self.output_offset.0 += placeholder.text.len(); @@ -1396,7 +1389,7 @@ impl<'a> Iterator for FoldChunks<'a> { && self.inlay_chunks.offset() != self.inlay_offset { let transform_start = self.transform_cursor.start(); - let transform_end = self.transform_cursor.end(&()); + let transform_end = self.transform_cursor.end(); let inlay_end = if self.max_output_offset < transform_end.0 { let overshoot = self.max_output_offset.0 - transform_start.0.0; transform_start.1 + InlayOffset(overshoot) @@ -1417,14 +1410,14 @@ impl<'a> Iterator for FoldChunks<'a> { if let Some((buffer_chunk_start, mut inlay_chunk)) = self.inlay_chunk.clone() { let chunk = &mut inlay_chunk.chunk; let buffer_chunk_end = buffer_chunk_start + InlayOffset(chunk.text.len()); - let transform_end = self.transform_cursor.end(&()).1; + let transform_end = self.transform_cursor.end().1; let chunk_end = buffer_chunk_end.min(transform_end); chunk.text = &chunk.text [(self.inlay_offset - buffer_chunk_start).0..(chunk_end - buffer_chunk_start).0]; if chunk_end == transform_end { - self.transform_cursor.next(&()); + self.transform_cursor.next(); } else if chunk_end == buffer_chunk_end { self.inlay_chunk.take(); } @@ -1456,7 +1449,7 @@ impl FoldOffset { let mut cursor = snapshot .transforms .cursor::<(FoldOffset, TransformSummary)>(&()); - cursor.seek(&self, Bias::Right, &()); + 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) } else { @@ -1470,7 +1463,7 @@ impl FoldOffset { #[cfg(test)] pub fn to_inlay_offset(self, snapshot: &FoldSnapshot) -> InlayOffset { let mut cursor = snapshot.transforms.cursor::<(FoldOffset, InlayOffset)>(&()); - cursor.seek(&self, Bias::Right, &()); + 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 f7a696860a1c85d6955fe9e6f5aa00c0fa32a156..a36d18ff6d0482d08d6a261b4d0ce8166ee2fccc 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -263,7 +263,7 @@ pub struct InlayChunk<'a> { impl InlayChunks<'_> { pub fn seek(&mut self, new_range: Range) { - self.transforms.seek(&new_range.start, Bias::Right, &()); + self.transforms.seek(&new_range.start, Bias::Right); let buffer_range = self.snapshot.to_buffer_offset(new_range.start) ..self.snapshot.to_buffer_offset(new_range.end); @@ -296,12 +296,12 @@ impl<'a> Iterator for InlayChunks<'a> { *chunk = self.buffer_chunks.next().unwrap(); } - let desired_bytes = self.transforms.end(&()).0.0 - self.output_offset.0; + let desired_bytes = self.transforms.end().0.0 - self.output_offset.0; // If we're already at the transform boundary, skip to the next transform if desired_bytes == 0 { self.inlay_chunks = None; - self.transforms.next(&()); + self.transforms.next(); return self.next(); } @@ -397,7 +397,7 @@ impl<'a> Iterator for InlayChunks<'a> { let inlay_chunks = self.inlay_chunks.get_or_insert_with(|| { let start = offset_in_inlay; - let end = cmp::min(self.max_output_offset, self.transforms.end(&()).0) + let end = cmp::min(self.max_output_offset, self.transforms.end().0) - self.transforms.start().0; inlay.text.chunks_in_range(start.0..end.0) }); @@ -441,9 +441,9 @@ impl<'a> Iterator for InlayChunks<'a> { } }; - if self.output_offset >= self.transforms.end(&()).0 { + if self.output_offset >= self.transforms.end().0 { self.inlay_chunks = None; - self.transforms.next(&()); + self.transforms.next(); } Some(chunk) @@ -453,7 +453,7 @@ impl<'a> Iterator for InlayChunks<'a> { impl InlayBufferRows<'_> { pub fn seek(&mut self, row: u32) { let inlay_point = InlayPoint::new(row, 0); - self.transforms.seek(&inlay_point, Bias::Left, &()); + self.transforms.seek(&inlay_point, Bias::Left); let mut buffer_point = self.transforms.start().1; let buffer_row = MultiBufferRow(if row == 0 { @@ -487,7 +487,7 @@ impl Iterator for InlayBufferRows<'_> { self.inlay_row += 1; self.transforms - .seek_forward(&InlayPoint::new(self.inlay_row, 0), Bias::Left, &()); + .seek_forward(&InlayPoint::new(self.inlay_row, 0), Bias::Left); Some(buffer_row) } @@ -556,18 +556,18 @@ impl InlayMap { let mut cursor = snapshot.transforms.cursor::<(usize, InlayOffset)>(&()); 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, &()), &()); + 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 { + if cursor.end().0 == buffer_edit.old.start { push_isomorphic(&mut new_transforms, *transform); - cursor.next(&()); + cursor.next(); } } // Remove all the inlays and transforms contained by the edit. let old_start = cursor.start().1 + InlayOffset(buffer_edit.old.start - cursor.start().0); - cursor.seek(&buffer_edit.old.end, Bias::Right, &()); + cursor.seek(&buffer_edit.old.end, Bias::Right); let old_end = cursor.start().1 + InlayOffset(buffer_edit.old.end - cursor.start().0); @@ -625,20 +625,20 @@ impl InlayMap { // we can push its remainder. if buffer_edits_iter .peek() - .map_or(true, |edit| edit.old.start >= cursor.end(&()).0) + .map_or(true, |edit| edit.old.start >= cursor.end().0) { let transform_start = new_transforms.summary().input.len; let transform_end = - buffer_edit.new.end + (cursor.end(&()).0 - buffer_edit.old.end); + buffer_edit.new.end + (cursor.end().0 - buffer_edit.old.end); push_isomorphic( &mut new_transforms, buffer_snapshot.text_summary_for_range(transform_start..transform_end), ); - cursor.next(&()); + cursor.next(); } } - new_transforms.append(cursor.suffix(&()), &()); + new_transforms.append(cursor.suffix(), &()); if new_transforms.is_empty() { new_transforms.push(Transform::Isomorphic(Default::default()), &()); } @@ -773,7 +773,7 @@ impl InlaySnapshot { let mut cursor = self .transforms .cursor::<(InlayOffset, (InlayPoint, usize))>(&()); - cursor.seek(&offset, Bias::Right, &()); + cursor.seek(&offset, Bias::Right); let overshoot = offset.0 - cursor.start().0.0; match cursor.item() { Some(Transform::Isomorphic(_)) => { @@ -803,7 +803,7 @@ impl InlaySnapshot { let mut cursor = self .transforms .cursor::<(InlayPoint, (InlayOffset, Point))>(&()); - cursor.seek(&point, Bias::Right, &()); + cursor.seek(&point, Bias::Right); let overshoot = point.0 - cursor.start().0.0; match cursor.item() { Some(Transform::Isomorphic(_)) => { @@ -822,7 +822,7 @@ impl InlaySnapshot { } pub fn to_buffer_point(&self, point: InlayPoint) -> Point { let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(&()); - cursor.seek(&point, Bias::Right, &()); + cursor.seek(&point, Bias::Right); match cursor.item() { Some(Transform::Isomorphic(_)) => { let overshoot = point.0 - cursor.start().0.0; @@ -834,7 +834,7 @@ impl InlaySnapshot { } pub fn to_buffer_offset(&self, offset: InlayOffset) -> usize { let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(&()); - cursor.seek(&offset, Bias::Right, &()); + cursor.seek(&offset, Bias::Right); match cursor.item() { Some(Transform::Isomorphic(_)) => { let overshoot = offset - cursor.start().0; @@ -847,19 +847,19 @@ impl InlaySnapshot { pub fn to_inlay_offset(&self, offset: usize) -> InlayOffset { let mut cursor = self.transforms.cursor::<(usize, InlayOffset)>(&()); - cursor.seek(&offset, Bias::Left, &()); + cursor.seek(&offset, Bias::Left); loop { match cursor.item() { Some(Transform::Isomorphic(_)) => { - if offset == cursor.end(&()).0 { + if offset == cursor.end().0 { while let Some(Transform::Inlay(inlay)) = cursor.next_item() { if inlay.position.bias() == Bias::Right { break; } else { - cursor.next(&()); + cursor.next(); } } - return cursor.end(&()).1; + return cursor.end().1; } else { let overshoot = offset - cursor.start().0; return InlayOffset(cursor.start().1.0 + overshoot); @@ -867,7 +867,7 @@ impl InlaySnapshot { } Some(Transform::Inlay(inlay)) => { if inlay.position.bias() == Bias::Left { - cursor.next(&()); + cursor.next(); } else { return cursor.start().1; } @@ -880,19 +880,19 @@ impl InlaySnapshot { } pub fn to_inlay_point(&self, point: Point) -> InlayPoint { let mut cursor = self.transforms.cursor::<(Point, InlayPoint)>(&()); - cursor.seek(&point, Bias::Left, &()); + cursor.seek(&point, Bias::Left); loop { match cursor.item() { Some(Transform::Isomorphic(_)) => { - if point == cursor.end(&()).0 { + if point == cursor.end().0 { while let Some(Transform::Inlay(inlay)) = cursor.next_item() { if inlay.position.bias() == Bias::Right { break; } else { - cursor.next(&()); + cursor.next(); } } - return cursor.end(&()).1; + return cursor.end().1; } else { let overshoot = point - cursor.start().0; return InlayPoint(cursor.start().1.0 + overshoot); @@ -900,7 +900,7 @@ impl InlaySnapshot { } Some(Transform::Inlay(inlay)) => { if inlay.position.bias() == Bias::Left { - cursor.next(&()); + cursor.next(); } else { return cursor.start().1; } @@ -914,7 +914,7 @@ impl InlaySnapshot { pub fn clip_point(&self, mut point: InlayPoint, mut bias: Bias) -> InlayPoint { let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(&()); - cursor.seek(&point, Bias::Left, &()); + cursor.seek(&point, Bias::Left); loop { match cursor.item() { Some(Transform::Isomorphic(transform)) => { @@ -923,7 +923,7 @@ impl InlaySnapshot { if inlay.position.bias() == Bias::Left { return point; } else if bias == Bias::Left { - cursor.prev(&()); + cursor.prev(); } else if transform.first_line_chars == 0 { point.0 += Point::new(1, 0); } else { @@ -932,12 +932,12 @@ impl InlaySnapshot { } else { return point; } - } else if cursor.end(&()).0 == point { + } else if cursor.end().0 == point { if let Some(Transform::Inlay(inlay)) = cursor.next_item() { if inlay.position.bias() == Bias::Right { return point; } else if bias == Bias::Right { - cursor.next(&()); + cursor.next(); } else if point.0.column == 0 { point.0.row -= 1; point.0.column = self.line_len(point.0.row); @@ -970,7 +970,7 @@ impl InlaySnapshot { } _ => return point, } - } else if point == cursor.end(&()).0 && inlay.position.bias() == Bias::Left { + } else if point == cursor.end().0 && inlay.position.bias() == Bias::Left { match cursor.next_item() { Some(Transform::Inlay(inlay)) => { if inlay.position.bias() == Bias::Right { @@ -983,9 +983,9 @@ impl InlaySnapshot { if bias == Bias::Left { point = cursor.start().0; - cursor.prev(&()); + cursor.prev(); } else { - cursor.next(&()); + cursor.next(); point = cursor.start().0; } } @@ -993,9 +993,9 @@ impl InlaySnapshot { bias = bias.invert(); if bias == Bias::Left { point = cursor.start().0; - cursor.prev(&()); + cursor.prev(); } else { - cursor.next(&()); + cursor.next(); point = cursor.start().0; } } @@ -1011,7 +1011,7 @@ impl InlaySnapshot { let mut summary = TextSummary::default(); let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(&()); - cursor.seek(&range.start, Bias::Right, &()); + cursor.seek(&range.start, Bias::Right); let overshoot = range.start.0 - cursor.start().0.0; match cursor.item() { @@ -1019,22 +1019,22 @@ impl InlaySnapshot { let buffer_start = cursor.start().1; let suffix_start = buffer_start + overshoot; let suffix_end = - buffer_start + (cmp::min(cursor.end(&()).0, range.end).0 - cursor.start().0.0); + buffer_start + (cmp::min(cursor.end().0, range.end).0 - cursor.start().0.0); summary = self.buffer.text_summary_for_range(suffix_start..suffix_end); - cursor.next(&()); + cursor.next(); } Some(Transform::Inlay(inlay)) => { let suffix_start = overshoot; - let suffix_end = cmp::min(cursor.end(&()).0, range.end).0 - cursor.start().0.0; + let suffix_end = cmp::min(cursor.end().0, range.end).0 - cursor.start().0.0; summary = inlay.text.cursor(suffix_start).summary(suffix_end); - cursor.next(&()); + cursor.next(); } None => {} } if range.end > cursor.start().0 { summary += cursor - .summary::<_, TransformSummary>(&range.end, Bias::Right, &()) + .summary::<_, TransformSummary>(&range.end, Bias::Right) .output; let overshoot = range.end.0 - cursor.start().0.0; @@ -1060,7 +1060,7 @@ impl InlaySnapshot { pub fn row_infos(&self, row: u32) -> InlayBufferRows<'_> { let mut cursor = self.transforms.cursor::<(InlayPoint, Point)>(&()); let inlay_point = InlayPoint::new(row, 0); - cursor.seek(&inlay_point, Bias::Left, &()); + cursor.seek(&inlay_point, Bias::Left); let max_buffer_row = self.buffer.max_row(); let mut buffer_point = cursor.start().1; @@ -1101,7 +1101,7 @@ impl InlaySnapshot { highlights: Highlights<'a>, ) -> InlayChunks<'a> { let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(&()); - cursor.seek(&range.start, Bias::Right, &()); + cursor.seek(&range.start, Bias::Right); let buffer_range = self.to_buffer_offset(range.start)..self.to_buffer_offset(range.end); let buffer_chunks = CustomHighlightsChunks::new( diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index a29bf5388271e422bea6aa890d5617ab0cc3f5ee..d55577826e7f9126bb076a50b4c4baa7bbb1d5a0 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -72,7 +72,7 @@ pub struct WrapRows<'a> { impl WrapRows<'_> { pub(crate) fn seek(&mut self, start_row: u32) { self.transforms - .seek(&WrapPoint::new(start_row, 0), Bias::Left, &()); + .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()) { input_row += start_row - self.transforms.start().0.row(); @@ -340,7 +340,7 @@ impl WrapSnapshot { let mut tab_edits_iter = tab_edits.iter().peekable(); new_transforms = - old_cursor.slice(&tab_edits_iter.peek().unwrap().old.start, Bias::Right, &()); + old_cursor.slice(&tab_edits_iter.peek().unwrap().old.start, Bias::Right); while let Some(edit) = tab_edits_iter.next() { if edit.new.start > TabPoint::from(new_transforms.summary().input.lines) { @@ -356,31 +356,29 @@ impl WrapSnapshot { )); } - old_cursor.seek_forward(&edit.old.end, Bias::Right, &()); + old_cursor.seek_forward(&edit.old.end, Bias::Right); if let Some(next_edit) = tab_edits_iter.peek() { - if next_edit.old.start > old_cursor.end(&()) { - if old_cursor.end(&()) > edit.old.end { + if next_edit.old.start > old_cursor.end() { + if old_cursor.end() > edit.old.end { let summary = self .tab_snapshot - .text_summary_for_range(edit.old.end..old_cursor.end(&())); + .text_summary_for_range(edit.old.end..old_cursor.end()); new_transforms.push_or_extend(Transform::isomorphic(summary)); } - old_cursor.next(&()); - new_transforms.append( - old_cursor.slice(&next_edit.old.start, Bias::Right, &()), - &(), - ); + old_cursor.next(); + new_transforms + .append(old_cursor.slice(&next_edit.old.start, Bias::Right), &()); } } else { - if old_cursor.end(&()) > edit.old.end { + if old_cursor.end() > edit.old.end { let summary = self .tab_snapshot - .text_summary_for_range(edit.old.end..old_cursor.end(&())); + .text_summary_for_range(edit.old.end..old_cursor.end()); new_transforms.push_or_extend(Transform::isomorphic(summary)); } - old_cursor.next(&()); - new_transforms.append(old_cursor.suffix(&()), &()); + old_cursor.next(); + new_transforms.append(old_cursor.suffix(), &()); } } } @@ -441,7 +439,6 @@ impl WrapSnapshot { new_transforms = old_cursor.slice( &TabPoint::new(row_edits.peek().unwrap().old_rows.start, 0), Bias::Right, - &(), ); while let Some(edit) = row_edits.next() { @@ -516,34 +513,31 @@ impl WrapSnapshot { } new_transforms.extend(edit_transforms, &()); - old_cursor.seek_forward(&TabPoint::new(edit.old_rows.end, 0), Bias::Right, &()); + old_cursor.seek_forward(&TabPoint::new(edit.old_rows.end, 0), Bias::Right); if let Some(next_edit) = row_edits.peek() { - if next_edit.old_rows.start > old_cursor.end(&()).row() { - if old_cursor.end(&()) > TabPoint::new(edit.old_rows.end, 0) { + if next_edit.old_rows.start > old_cursor.end().row() { + if old_cursor.end() > TabPoint::new(edit.old_rows.end, 0) { let summary = self.tab_snapshot.text_summary_for_range( - TabPoint::new(edit.old_rows.end, 0)..old_cursor.end(&()), + TabPoint::new(edit.old_rows.end, 0)..old_cursor.end(), ); new_transforms.push_or_extend(Transform::isomorphic(summary)); } - old_cursor.next(&()); + old_cursor.next(); new_transforms.append( - old_cursor.slice( - &TabPoint::new(next_edit.old_rows.start, 0), - Bias::Right, - &(), - ), + old_cursor + .slice(&TabPoint::new(next_edit.old_rows.start, 0), Bias::Right), &(), ); } } else { - if old_cursor.end(&()) > TabPoint::new(edit.old_rows.end, 0) { + if old_cursor.end() > TabPoint::new(edit.old_rows.end, 0) { let summary = self.tab_snapshot.text_summary_for_range( - TabPoint::new(edit.old_rows.end, 0)..old_cursor.end(&()), + TabPoint::new(edit.old_rows.end, 0)..old_cursor.end(), ); new_transforms.push_or_extend(Transform::isomorphic(summary)); } - old_cursor.next(&()); - new_transforms.append(old_cursor.suffix(&()), &()); + old_cursor.next(); + new_transforms.append(old_cursor.suffix(), &()); } } } @@ -570,19 +564,19 @@ impl WrapSnapshot { tab_edit.new.start.0.column = 0; tab_edit.new.end.0 += Point::new(1, 0); - old_cursor.seek(&tab_edit.old.start, Bias::Right, &()); + old_cursor.seek(&tab_edit.old.start, Bias::Right); let mut old_start = old_cursor.start().output.lines; old_start += tab_edit.old.start.0 - old_cursor.start().input.lines; - old_cursor.seek(&tab_edit.old.end, Bias::Right, &()); + old_cursor.seek(&tab_edit.old.end, Bias::Right); let mut old_end = old_cursor.start().output.lines; old_end += tab_edit.old.end.0 - old_cursor.start().input.lines; - new_cursor.seek(&tab_edit.new.start, Bias::Right, &()); + new_cursor.seek(&tab_edit.new.start, Bias::Right); let mut new_start = new_cursor.start().output.lines; new_start += tab_edit.new.start.0 - new_cursor.start().input.lines; - new_cursor.seek(&tab_edit.new.end, Bias::Right, &()); + new_cursor.seek(&tab_edit.new.end, Bias::Right); let mut new_end = new_cursor.start().output.lines; new_end += tab_edit.new.end.0 - new_cursor.start().input.lines; @@ -605,7 +599,7 @@ impl WrapSnapshot { let output_start = WrapPoint::new(rows.start, 0); let output_end = WrapPoint::new(rows.end, 0); let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); - transforms.seek(&output_start, Bias::Right, &()); + 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()) { input_start.0 += output_start.0 - transforms.start().0.0; @@ -633,7 +627,7 @@ impl WrapSnapshot { pub fn line_len(&self, row: u32) -> u32 { let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); - cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Left, &()); + cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Left); if cursor .item() .map_or(false, |transform| transform.is_isomorphic()) @@ -658,10 +652,10 @@ impl WrapSnapshot { let end = WrapPoint::new(rows.end, 0); let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); - cursor.seek(&start, Bias::Right, &()); + cursor.seek(&start, Bias::Right); if let Some(transform) = cursor.item() { let start_in_transform = start.0 - cursor.start().0.0; - let end_in_transform = cmp::min(end, cursor.end(&()).0).0 - cursor.start().0.0; + let end_in_transform = cmp::min(end, cursor.end().0).0 - cursor.start().0.0; if transform.is_isomorphic() { let tab_start = TabPoint(cursor.start().1.0 + start_in_transform); let tab_end = TabPoint(cursor.start().1.0 + end_in_transform); @@ -678,12 +672,12 @@ impl WrapSnapshot { }; } - cursor.next(&()); + cursor.next(); } if rows.end > cursor.start().0.row() { summary += &cursor - .summary::<_, TransformSummary>(&WrapPoint::new(rows.end, 0), Bias::Right, &()) + .summary::<_, TransformSummary>(&WrapPoint::new(rows.end, 0), Bias::Right) .output; if let Some(transform) = cursor.item() { @@ -712,7 +706,7 @@ impl WrapSnapshot { pub fn soft_wrap_indent(&self, row: u32) -> Option { let mut cursor = self.transforms.cursor::(&()); - cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Right, &()); + cursor.seek(&WrapPoint::new(row + 1, 0), Bias::Right); cursor.item().and_then(|transform| { if transform.is_isomorphic() { None @@ -728,7 +722,7 @@ impl WrapSnapshot { pub fn row_infos(&self, start_row: u32) -> WrapRows<'_> { let mut transforms = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); - transforms.seek(&WrapPoint::new(start_row, 0), Bias::Left, &()); + 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()) { input_row += start_row - transforms.start().0.row(); @@ -748,7 +742,7 @@ impl WrapSnapshot { pub fn to_tab_point(&self, point: WrapPoint) -> TabPoint { let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); - cursor.seek(&point, Bias::Right, &()); + cursor.seek(&point, Bias::Right); let mut tab_point = cursor.start().1.0; if cursor.item().map_or(false, |t| t.is_isomorphic()) { tab_point += point.0 - cursor.start().0.0; @@ -766,14 +760,14 @@ impl WrapSnapshot { pub fn tab_point_to_wrap_point(&self, point: TabPoint) -> WrapPoint { let mut cursor = self.transforms.cursor::<(TabPoint, WrapPoint)>(&()); - cursor.seek(&point, Bias::Right, &()); + cursor.seek(&point, Bias::Right); WrapPoint(cursor.start().1.0 + (point.0 - cursor.start().0.0)) } pub fn clip_point(&self, mut point: WrapPoint, bias: Bias) -> WrapPoint { if bias == Bias::Left { let mut cursor = self.transforms.cursor::(&()); - cursor.seek(&point, Bias::Right, &()); + cursor.seek(&point, Bias::Right); if cursor.item().map_or(false, |t| !t.is_isomorphic()) { point = *cursor.start(); *point.column_mut() -= 1; @@ -791,16 +785,16 @@ impl WrapSnapshot { *point.column_mut() = 0; let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); - cursor.seek(&point, Bias::Right, &()); + cursor.seek(&point, Bias::Right); if cursor.item().is_none() { - cursor.prev(&()); + cursor.prev(); } while let Some(transform) = cursor.item() { if transform.is_isomorphic() && cursor.start().1.column() == 0 { - return cmp::min(cursor.end(&()).0.row(), point.row()); + return cmp::min(cursor.end().0.row(), point.row()); } else { - cursor.prev(&()); + cursor.prev(); } } @@ -811,12 +805,12 @@ impl WrapSnapshot { point.0 += Point::new(1, 0); let mut cursor = self.transforms.cursor::<(WrapPoint, TabPoint)>(&()); - cursor.seek(&point, Bias::Right, &()); + cursor.seek(&point, Bias::Right); while let Some(transform) = cursor.item() { if transform.is_isomorphic() && cursor.start().1.column() == 0 { return Some(cmp::max(cursor.start().0.row(), point.row())); } else { - cursor.next(&()); + cursor.next(); } } @@ -889,7 +883,7 @@ impl WrapChunks<'_> { pub(crate) fn seek(&mut self, rows: Range) { let output_start = WrapPoint::new(rows.start, 0); let output_end = WrapPoint::new(rows.end, 0); - self.transforms.seek(&output_start, Bias::Right, &()); + 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()) { input_start.0 += output_start.0 - self.transforms.start().0.0; @@ -930,7 +924,7 @@ impl<'a> Iterator for WrapChunks<'a> { } self.output_position.0 += summary; - self.transforms.next(&()); + self.transforms.next(); return Some(Chunk { text: &display_text[start_ix..end_ix], ..Default::default() @@ -942,7 +936,7 @@ impl<'a> Iterator for WrapChunks<'a> { } let mut input_len = 0; - let transform_end = self.transforms.end(&()).0; + let transform_end = self.transforms.end().0; for c in self.input_chunk.text.chars() { let char_len = c.len_utf8(); input_len += char_len; @@ -954,7 +948,7 @@ impl<'a> Iterator for WrapChunks<'a> { } if self.output_position >= transform_end { - self.transforms.next(&()); + self.transforms.next(); break; } } @@ -982,7 +976,7 @@ impl Iterator for WrapRows<'_> { self.output_row += 1; self.transforms - .seek_forward(&WrapPoint::new(self.output_row, 0), Bias::Left, &()); + .seek_forward(&WrapPoint::new(self.output_row, 0), Bias::Left); if self.transforms.item().map_or(false, |t| t.is_isomorphic()) { self.input_buffer_row = self.input_buffer_rows.next().unwrap(); self.soft_wrapped = false; diff --git a/crates/editor/src/git/blame.rs b/crates/editor/src/git/blame.rs index d4c9e37895444aba8045aec94f2c10af9df72a55..fc350a5a15b4f7b105872e61e5a2401d183c1a6d 100644 --- a/crates/editor/src/git/blame.rs +++ b/crates/editor/src/git/blame.rs @@ -296,7 +296,7 @@ impl GitBlame { let row = info .buffer_row .filter(|_| info.buffer_id == Some(buffer_id))?; - cursor.seek_forward(&row, Bias::Right, &()); + cursor.seek_forward(&row, Bias::Right); cursor.item()?.blame.clone() }) } @@ -389,7 +389,7 @@ impl GitBlame { } } - new_entries.append(cursor.slice(&edit.old.start, Bias::Right, &()), &()); + new_entries.append(cursor.slice(&edit.old.start, Bias::Right), &()); if edit.new.start > new_entries.summary().rows { new_entries.push( @@ -401,7 +401,7 @@ impl GitBlame { ); } - cursor.seek(&edit.old.end, Bias::Right, &()); + cursor.seek(&edit.old.end, Bias::Right); if !edit.new.is_empty() { new_entries.push( GitBlameEntry { @@ -412,7 +412,7 @@ impl GitBlame { ); } - let old_end = cursor.end(&()); + let old_end = cursor.end(); if row_edits .peek() .map_or(true, |next_edit| next_edit.old.start >= old_end) @@ -421,18 +421,18 @@ impl GitBlame { if old_end > edit.old.end { new_entries.push( GitBlameEntry { - rows: cursor.end(&()) - edit.old.end, + rows: cursor.end() - edit.old.end, blame: entry.blame.clone(), }, &(), ); } - cursor.next(&()); + cursor.next(); } } } - new_entries.append(cursor.suffix(&()), &()); + new_entries.append(cursor.suffix(), &()); drop(cursor); self.buffer_snapshot = new_snapshot; diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index f24d38794f7611ee173dbe913ed51a53bdef73ed..328a6a4cc1dffb12999e074e59d9f6707002a411 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -249,8 +249,8 @@ impl ListState { let state = &mut *self.0.borrow_mut(); let mut old_items = state.items.cursor::(&()); - let mut new_items = old_items.slice(&Count(old_range.start), Bias::Right, &()); - old_items.seek_forward(&Count(old_range.end), Bias::Right, &()); + let mut new_items = old_items.slice(&Count(old_range.start), Bias::Right); + old_items.seek_forward(&Count(old_range.end), Bias::Right); let mut spliced_count = 0; new_items.extend( @@ -260,7 +260,7 @@ impl ListState { }), &(), ); - new_items.append(old_items.suffix(&()), &()); + new_items.append(old_items.suffix(), &()); drop(old_items); state.items = new_items; @@ -300,14 +300,14 @@ impl ListState { let current_offset = self.logical_scroll_top(); let state = &mut *self.0.borrow_mut(); let mut cursor = state.items.cursor::(&()); - cursor.seek(&Count(current_offset.item_ix), Bias::Right, &()); + cursor.seek(&Count(current_offset.item_ix), Bias::Right); let start_pixel_offset = cursor.start().height + current_offset.offset_in_item; let new_pixel_offset = (start_pixel_offset + distance).max(px(0.)); if new_pixel_offset > start_pixel_offset { - cursor.seek_forward(&Height(new_pixel_offset), Bias::Right, &()); + cursor.seek_forward(&Height(new_pixel_offset), Bias::Right); } else { - cursor.seek(&Height(new_pixel_offset), Bias::Right, &()); + cursor.seek(&Height(new_pixel_offset), Bias::Right); } state.logical_scroll_top = Some(ListOffset { @@ -343,11 +343,11 @@ impl ListState { scroll_top.offset_in_item = px(0.); } else { let mut cursor = state.items.cursor::(&()); - cursor.seek(&Count(ix + 1), Bias::Right, &()); + cursor.seek(&Count(ix + 1), Bias::Right); let bottom = cursor.start().height + padding.top; let goal_top = px(0.).max(bottom - height + padding.bottom); - cursor.seek(&Height(goal_top), Bias::Left, &()); + cursor.seek(&Height(goal_top), Bias::Left); let start_ix = cursor.start().count; let start_item_top = cursor.start().height; @@ -372,11 +372,11 @@ impl ListState { } let mut cursor = state.items.cursor::<(Count, Height)>(&()); - cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &()); + 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, &()); + cursor.seek_forward(&Count(ix), Bias::Right); if let Some(&ListItem::Measured { size, .. }) = cursor.item() { let &(Count(count), Height(top)) = cursor.start(); if count == ix { @@ -431,7 +431,7 @@ impl ListState { let mut cursor = state.items.cursor::(&()); let summary: ListItemSummary = - cursor.summary(&Count(logical_scroll_top.item_ix), Bias::Right, &()); + cursor.summary(&Count(logical_scroll_top.item_ix), Bias::Right); let content_height = state.items.summary().height; let drag_offset = // if dragging the scrollbar, we want to offset the point if the height changed @@ -450,9 +450,9 @@ impl ListState { impl StateInner { fn visible_range(&self, height: Pixels, scroll_top: &ListOffset) -> Range { let mut cursor = self.items.cursor::(&()); - cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &()); + cursor.seek(&Count(scroll_top.item_ix), Bias::Right); let start_y = cursor.start().height + scroll_top.offset_in_item; - cursor.seek_forward(&Height(start_y + height), Bias::Left, &()); + cursor.seek_forward(&Height(start_y + height), Bias::Left); scroll_top.item_ix..cursor.start().count + 1 } @@ -482,7 +482,7 @@ impl StateInner { self.logical_scroll_top = None; } else { let mut cursor = self.items.cursor::(&()); - cursor.seek(&Height(new_scroll_top), Bias::Right, &()); + cursor.seek(&Height(new_scroll_top), Bias::Right); let item_ix = cursor.start().count; let offset_in_item = new_scroll_top - cursor.start().height; self.logical_scroll_top = Some(ListOffset { @@ -523,7 +523,7 @@ impl StateInner { fn scroll_top(&self, logical_scroll_top: &ListOffset) -> Pixels { let mut cursor = self.items.cursor::(&()); - cursor.seek(&Count(logical_scroll_top.item_ix), Bias::Right, &()); + cursor.seek(&Count(logical_scroll_top.item_ix), Bias::Right); cursor.start().height + logical_scroll_top.offset_in_item } @@ -553,7 +553,7 @@ impl StateInner { let mut cursor = old_items.cursor::(&()); // Render items after the scroll top, including those in the trailing overdraw - cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &()); + cursor.seek(&Count(scroll_top.item_ix), Bias::Right); for (ix, item) in cursor.by_ref().enumerate() { let visible_height = rendered_height - scroll_top.offset_in_item; if visible_height >= available_height + self.overdraw { @@ -592,13 +592,13 @@ impl StateInner { rendered_height += padding.bottom; // Prepare to start walking upward from the item at the scroll top. - cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &()); + cursor.seek(&Count(scroll_top.item_ix), Bias::Right); // If the rendered items do not fill the visible region, then adjust // the scroll top upward. if rendered_height - scroll_top.offset_in_item < available_height { while rendered_height < available_height { - cursor.prev(&()); + cursor.prev(); if let Some(item) = cursor.item() { let item_index = cursor.start().0; let mut element = (self.render_item)(item_index, window, cx); @@ -645,7 +645,7 @@ impl StateInner { // Measure items in the leading overdraw let mut leading_overdraw = scroll_top.offset_in_item; while leading_overdraw < self.overdraw { - cursor.prev(&()); + cursor.prev(); if let Some(item) = cursor.item() { let size = if let ListItem::Measured { size, .. } = item { *size @@ -666,10 +666,10 @@ impl StateInner { let measured_range = cursor.start().0..(cursor.start().0 + measured_items.len()); let mut cursor = old_items.cursor::(&()); - let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right, &()); + let mut new_items = cursor.slice(&Count(measured_range.start), Bias::Right); new_items.extend(measured_items, &()); - cursor.seek(&Count(measured_range.end), Bias::Right, &()); - new_items.append(cursor.suffix(&()), &()); + cursor.seek(&Count(measured_range.end), Bias::Right); + new_items.append(cursor.suffix(), &()); self.items = new_items; // If none of the visible items are focused, check if an off-screen item is focused @@ -679,7 +679,7 @@ impl StateInner { let mut cursor = self .items .filter::<_, Count>(&(), |summary| summary.has_focus_handles); - cursor.next(&()); + cursor.next(); while let Some(item) = cursor.item() { if item.contains_focused(window, cx) { let item_index = cursor.start().0; @@ -692,7 +692,7 @@ impl StateInner { }); break; } - cursor.next(&()); + cursor.next(); } } @@ -741,7 +741,7 @@ impl StateInner { }); } else if autoscroll_bounds.bottom() > bounds.bottom() { let mut cursor = self.items.cursor::(&()); - cursor.seek(&Count(item.index), Bias::Right, &()); + 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. @@ -749,7 +749,7 @@ impl StateInner { // Keep decreasing the scroll top until we fill all the available space. while height > Pixels::ZERO { - cursor.prev(&()); + cursor.prev(); let Some(item) = cursor.item() else { break }; let size = item.size().unwrap_or_else(|| { @@ -806,7 +806,7 @@ impl StateInner { self.logical_scroll_top = None; } else { let mut cursor = self.items.cursor::(&()); - cursor.seek(&Height(new_scroll_top), Bias::Right, &()); + cursor.seek(&Height(new_scroll_top), Bias::Right); let item_ix = cursor.start().count; let offset_in_item = new_scroll_top - cursor.start().height; diff --git a/crates/language/src/diagnostic_set.rs b/crates/language/src/diagnostic_set.rs index 661e3ef217a65a24ca59e37f93b137c89ed31dc6..613c445652fbcfe87232afec559480ce943b15e3 100644 --- a/crates/language/src/diagnostic_set.rs +++ b/crates/language/src/diagnostic_set.rs @@ -158,17 +158,17 @@ impl DiagnosticSet { }); if reversed { - cursor.prev(buffer); + cursor.prev(); } else { - cursor.next(buffer); + cursor.next(); } iter::from_fn({ move || { if let Some(diagnostic) = cursor.item() { if reversed { - cursor.prev(buffer); + cursor.prev(); } else { - cursor.next(buffer); + cursor.next(); } Some(diagnostic.resolve(buffer)) } else { diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index da05416e894e6a713121affe767f71d953408684..f441114a908ae86a4baf3a2a777e254d96695531 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -297,10 +297,10 @@ impl SyntaxSnapshot { let mut first_edit_ix_for_depth = 0; let mut prev_depth = 0; let mut cursor = self.layers.cursor::(text); - cursor.next(text); + cursor.next(); 'outer: loop { - let depth = cursor.end(text).max_depth; + let depth = cursor.end().max_depth; if depth > prev_depth { first_edit_ix_for_depth = 0; prev_depth = depth; @@ -313,7 +313,7 @@ impl SyntaxSnapshot { position: edit_range.start, }; if target.cmp(cursor.start(), text).is_gt() { - let slice = cursor.slice(&target, Bias::Left, text); + let slice = cursor.slice(&target, Bias::Left); layers.append(slice, text); } } @@ -327,7 +327,6 @@ impl SyntaxSnapshot { language: None, }, Bias::Left, - text, ); layers.append(slice, text); continue; @@ -394,10 +393,10 @@ impl SyntaxSnapshot { } layers.push(layer, text); - cursor.next(text); + cursor.next(); } - layers.append(cursor.suffix(text), text); + layers.append(cursor.suffix(), text); drop(cursor); self.layers = layers; } @@ -420,7 +419,7 @@ impl SyntaxSnapshot { let mut cursor = self .layers .filter::<_, ()>(text, |summary| summary.contains_unknown_injections); - cursor.next(text); + cursor.next(); while let Some(layer) = cursor.item() { let SyntaxLayerContent::Pending { language_name } = &layer.content else { unreachable!() @@ -436,7 +435,7 @@ impl SyntaxSnapshot { resolved_injection_ranges.push(range); } - cursor.next(text); + cursor.next(); } drop(cursor); @@ -469,7 +468,7 @@ impl SyntaxSnapshot { let max_depth = self.layers.summary().max_depth; let mut cursor = self.layers.cursor::(text); - cursor.next(text); + cursor.next(); let mut layers = SumTree::new(text); let mut changed_regions = ChangeRegionSet::default(); @@ -514,7 +513,7 @@ impl SyntaxSnapshot { }; let mut done = cursor.item().is_none(); - while !done && position.cmp(&cursor.end(text), text).is_gt() { + while !done && position.cmp(&cursor.end(), text).is_gt() { done = true; let bounded_position = SyntaxLayerPositionBeforeChange { @@ -522,16 +521,16 @@ impl SyntaxSnapshot { change: changed_regions.start_position(), }; if bounded_position.cmp(cursor.start(), text).is_gt() { - let slice = cursor.slice(&bounded_position, Bias::Left, text); + let slice = cursor.slice(&bounded_position, Bias::Left); if !slice.is_empty() { layers.append(slice, text); - if changed_regions.prune(cursor.end(text), text) { + if changed_regions.prune(cursor.end(), text) { done = false; } } } - while position.cmp(&cursor.end(text), text).is_gt() { + while position.cmp(&cursor.end(), text).is_gt() { let Some(layer) = cursor.item() else { break }; if changed_regions.intersects(layer, text) { @@ -555,8 +554,8 @@ impl SyntaxSnapshot { layers.push(layer.clone(), text); } - cursor.next(text); - if changed_regions.prune(cursor.end(text), text) { + cursor.next(); + if changed_regions.prune(cursor.end(), text) { done = false; } } @@ -572,7 +571,7 @@ impl SyntaxSnapshot { if layer.range.to_offset(text) == (step_start_byte..step_end_byte) && layer.content.language_id() == step.language.id() { - cursor.next(text); + cursor.next(); } else { old_layer = None; } @@ -918,7 +917,7 @@ impl SyntaxSnapshot { } }); - cursor.next(buffer); + cursor.next(); iter::from_fn(move || { while let Some(layer) = cursor.item() { let mut info = None; @@ -940,7 +939,7 @@ impl SyntaxSnapshot { }); } } - cursor.next(buffer); + cursor.next(); if info.is_some() { return info; } diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 2cc8ea59abace61ea1cec23112524f9b3ec2dda8..f0913e30fb0cfa84c1ac2e1e62f564464768d5ce 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -1211,7 +1211,7 @@ impl MultiBuffer { let buffer = buffer_state.buffer.read(cx); for range in buffer.edited_ranges_for_transaction_id::(*buffer_transaction) { for excerpt_id in &buffer_state.excerpts { - cursor.seek(excerpt_id, Bias::Left, &()); + cursor.seek(excerpt_id, Bias::Left); if let Some(excerpt) = cursor.item() { if excerpt.locator == *excerpt_id { let excerpt_buffer_start = @@ -1322,7 +1322,7 @@ impl MultiBuffer { let start_locator = snapshot.excerpt_locator_for_id(selection.start.excerpt_id); let end_locator = snapshot.excerpt_locator_for_id(selection.end.excerpt_id); - cursor.seek(&Some(start_locator), Bias::Left, &()); + cursor.seek(&Some(start_locator), Bias::Left); while let Some(excerpt) = cursor.item() { if excerpt.locator > *end_locator { break; @@ -1347,7 +1347,7 @@ impl MultiBuffer { goal: selection.goal, }); - cursor.next(&()); + cursor.next(); } } @@ -1769,13 +1769,13 @@ impl MultiBuffer { let mut next_excerpt_id = move || ExcerptId(post_inc(&mut next_excerpt_id)); let mut excerpts_cursor = snapshot.excerpts.cursor::>(&()); - excerpts_cursor.next(&()); + excerpts_cursor.next(); loop { let new = new_iter.peek(); let existing = if let Some(existing_id) = existing_iter.peek() { let locator = snapshot.excerpt_locator_for_id(*existing_id); - excerpts_cursor.seek_forward(&Some(locator), Bias::Left, &()); + excerpts_cursor.seek_forward(&Some(locator), Bias::Left); if let Some(excerpt) = excerpts_cursor.item() { if excerpt.buffer_id != buffer_snapshot.remote_id() { to_remove.push(*existing_id); @@ -1970,7 +1970,7 @@ impl MultiBuffer { let mut prev_locator = snapshot.excerpt_locator_for_id(prev_excerpt_id).clone(); let mut new_excerpt_ids = mem::take(&mut snapshot.excerpt_ids); let mut cursor = snapshot.excerpts.cursor::>(&()); - let mut new_excerpts = cursor.slice(&prev_locator, Bias::Right, &()); + let mut new_excerpts = cursor.slice(&prev_locator, Bias::Right); prev_locator = cursor.start().unwrap_or(Locator::min_ref()).clone(); let edit_start = ExcerptOffset::new(new_excerpts.summary().text.len); @@ -2019,7 +2019,7 @@ impl MultiBuffer { let edit_end = ExcerptOffset::new(new_excerpts.summary().text.len); - let suffix = cursor.suffix(&()); + let suffix = cursor.suffix(); let changed_trailing_excerpt = suffix.is_empty(); new_excerpts.append(suffix, &()); drop(cursor); @@ -2104,7 +2104,7 @@ impl MultiBuffer { .into_iter() .flatten() { - cursor.seek_forward(&Some(locator), Bias::Left, &()); + cursor.seek_forward(&Some(locator), Bias::Left); if let Some(excerpt) = cursor.item() { if excerpt.locator == *locator { excerpts.push((excerpt.id, excerpt.range.clone())); @@ -2124,25 +2124,25 @@ impl MultiBuffer { let mut diff_transforms = snapshot .diff_transforms .cursor::<(ExcerptDimension, OutputDimension)>(&()); - diff_transforms.next(&()); + diff_transforms.next(); let locators = buffers .get(&buffer_id) .into_iter() .flat_map(|state| &state.excerpts); let mut result = Vec::new(); for locator in locators { - excerpts.seek_forward(&Some(locator), Bias::Left, &()); + 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); - diff_transforms.seek_forward(&excerpt_start, Bias::Left, &()); + 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, &()); + 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; @@ -2290,7 +2290,7 @@ impl MultiBuffer { self.paths_by_excerpt.remove(&excerpt_id); // Seek to the next excerpt to remove, preserving any preceding excerpts. let locator = snapshot.excerpt_locator_for_id(excerpt_id); - new_excerpts.append(cursor.slice(&Some(locator), Bias::Left, &()), &()); + new_excerpts.append(cursor.slice(&Some(locator), Bias::Left), &()); if let Some(mut excerpt) = cursor.item() { if excerpt.id != excerpt_id { @@ -2311,7 +2311,7 @@ impl MultiBuffer { removed_buffer_ids.push(excerpt.buffer_id); } } - cursor.next(&()); + cursor.next(); // Skip over any subsequent excerpts that are also removed. if let Some(&next_excerpt_id) = excerpt_ids.peek() { @@ -2344,7 +2344,7 @@ impl MultiBuffer { }); } } - let suffix = cursor.suffix(&()); + let suffix = cursor.suffix(); let changed_trailing_excerpt = suffix.is_empty(); new_excerpts.append(suffix, &()); drop(cursor); @@ -2493,7 +2493,7 @@ impl MultiBuffer { let mut cursor = snapshot .excerpts .cursor::<(Option<&Locator>, ExcerptOffset)>(&()); - cursor.seek_forward(&Some(locator), Bias::Left, &()); + 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); @@ -2724,7 +2724,7 @@ impl MultiBuffer { let snapshot = self.read(cx); let mut cursor = snapshot.diff_transforms.cursor::(&()); let offset_range = range.to_offset(&snapshot); - cursor.seek(&offset_range.start, Bias::Left, &()); + cursor.seek(&offset_range.start, Bias::Left); while let Some(item) = cursor.item() { if *cursor.start() >= offset_range.end && *cursor.start() > offset_range.start { break; @@ -2732,7 +2732,7 @@ impl MultiBuffer { if item.hunk_info().is_some() { return true; } - cursor.next(&()); + cursor.next(); } false } @@ -2746,7 +2746,7 @@ impl MultiBuffer { let end = snapshot.point_to_offset(Point::new(range.end.row + 1, 0)); let start = start.saturating_sub(1); let end = snapshot.len().min(end + 1); - cursor.seek(&start, Bias::Right, &()); + cursor.seek(&start, Bias::Right); while let Some(item) = cursor.item() { if *cursor.start() >= end { break; @@ -2754,7 +2754,7 @@ impl MultiBuffer { if item.hunk_info().is_some() { return true; } - cursor.next(&()); + cursor.next(); } } false @@ -2848,7 +2848,7 @@ impl MultiBuffer { .cursor::<(Option<&Locator>, ExcerptOffset)>(&()); let mut edits = Vec::>::new(); - let prefix = cursor.slice(&Some(locator), Bias::Left, &()); + let prefix = cursor.slice(&Some(locator), Bias::Left); new_excerpts.append(prefix, &()); let mut excerpt = cursor.item().unwrap().clone(); @@ -2883,9 +2883,9 @@ impl MultiBuffer { new_excerpts.push(excerpt, &()); - cursor.next(&()); + cursor.next(); - new_excerpts.append(cursor.suffix(&()), &()); + new_excerpts.append(cursor.suffix(), &()); drop(cursor); snapshot.excerpts = new_excerpts; @@ -2925,7 +2925,7 @@ impl MultiBuffer { let mut edits = Vec::>::new(); for locator in &locators { - let prefix = cursor.slice(&Some(locator), Bias::Left, &()); + let prefix = cursor.slice(&Some(locator), Bias::Left); new_excerpts.append(prefix, &()); let mut excerpt = cursor.item().unwrap().clone(); @@ -2987,10 +2987,10 @@ impl MultiBuffer { new_excerpts.push(excerpt, &()); - cursor.next(&()); + cursor.next(); } - new_excerpts.append(cursor.suffix(&()), &()); + new_excerpts.append(cursor.suffix(), &()); drop(cursor); snapshot.excerpts = new_excerpts; @@ -3070,7 +3070,7 @@ impl MultiBuffer { .cursor::<(Option<&Locator>, ExcerptOffset)>(&()); for (locator, buffer, buffer_edited) in excerpts_to_edit { - new_excerpts.append(cursor.slice(&Some(locator), Bias::Left, &()), &()); + new_excerpts.append(cursor.slice(&Some(locator), Bias::Left), &()); let old_excerpt = cursor.item().unwrap(); let buffer = buffer.read(cx); let buffer_id = buffer.remote_id(); @@ -3112,9 +3112,9 @@ impl MultiBuffer { } new_excerpts.push(new_excerpt, &()); - cursor.next(&()); + cursor.next(); } - new_excerpts.append(cursor.suffix(&()), &()); + new_excerpts.append(cursor.suffix(), &()); drop(cursor); snapshot.excerpts = new_excerpts; @@ -3145,23 +3145,22 @@ impl MultiBuffer { let mut excerpt_edits = excerpt_edits.into_iter().peekable(); while let Some(edit) = excerpt_edits.next() { - excerpts.seek_forward(&edit.new.start, Bias::Right, &()); + excerpts.seek_forward(&edit.new.start, Bias::Right); if excerpts.item().is_none() && *excerpts.start() == edit.new.start { - excerpts.prev(&()); + excerpts.prev(); } // Keep any transforms that are before the edit. if at_transform_boundary { at_transform_boundary = false; - let transforms_before_edit = - old_diff_transforms.slice(&edit.old.start, Bias::Left, &()); + 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 + 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(&()); + old_diff_transforms.next(); } } } @@ -3203,7 +3202,7 @@ impl MultiBuffer { // 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 + 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() { @@ -3218,8 +3217,8 @@ impl MultiBuffer { let mut excerpt_offset = edit.new.end; if !keep_next_old_transform { - excerpt_offset += old_diff_transforms.end(&()).0 - edit.old.end; - old_diff_transforms.next(&()); + excerpt_offset += old_diff_transforms.end().0 - edit.old.end; + old_diff_transforms.next(); } old_expanded_hunks.clear(); @@ -3234,7 +3233,7 @@ impl MultiBuffer { } // Keep any transforms that are after the last edit. - self.append_diff_transforms(&mut new_diff_transforms, old_diff_transforms.suffix(&())); + self.append_diff_transforms(&mut new_diff_transforms, old_diff_transforms.suffix()); // Ensure there's always at least one buffer content transform. if new_diff_transforms.is_empty() { @@ -3283,10 +3282,10 @@ impl MultiBuffer { ); old_expanded_hunks.insert(hunk_info); } - if old_diff_transforms.end(&()).0 > edit.old.end { + if old_diff_transforms.end().0 > edit.old.end { break; } - old_diff_transforms.next(&()); + old_diff_transforms.next(); } // Avoid querying diff hunks if there's no possibility of hunks being expanded. @@ -3413,8 +3412,8 @@ impl MultiBuffer { } } - if excerpts.end(&()) <= edit.new.end { - excerpts.next(&()); + if excerpts.end() <= edit.new.end { + excerpts.next(); } else { break; } @@ -3439,9 +3438,9 @@ impl MultiBuffer { *summary, ) { let mut cursor = subtree.cursor::<()>(&()); - cursor.next(&()); - cursor.next(&()); - new_transforms.append(cursor.suffix(&()), &()); + cursor.next(); + cursor.next(); + new_transforms.append(cursor.suffix(), &()); return; } } @@ -4715,14 +4714,14 @@ impl MultiBufferSnapshot { { let range = range.start.to_offset(self)..range.end.to_offset(self); let mut cursor = self.diff_transforms.cursor::<(usize, ExcerptOffset)>(&()); - cursor.seek(&range.start, Bias::Right, &()); + cursor.seek(&range.start, Bias::Right); let Some(first_transform) = cursor.item() else { return D::from_text_summary(&TextSummary::default()); }; let diff_transform_start = cursor.start().0; - let diff_transform_end = cursor.end(&()).0; + let diff_transform_end = cursor.end().0; let diff_start = range.start; let start_overshoot = diff_start - diff_transform_start; let end_overshoot = std::cmp::min(range.end, diff_transform_end) - diff_transform_start; @@ -4765,12 +4764,10 @@ impl MultiBufferSnapshot { return result; } - cursor.next(&()); - result.add_assign(&D::from_text_summary(&cursor.summary( - &range.end, - Bias::Right, - &(), - ))); + cursor.next(); + result.add_assign(&D::from_text_summary( + &cursor.summary(&range.end, Bias::Right), + )); let Some(last_transform) = cursor.item() else { return result; @@ -4813,9 +4810,9 @@ impl MultiBufferSnapshot { // let mut range = range.start..range.end; let mut summary = D::zero(&()); let mut cursor = self.excerpts.cursor::(&()); - cursor.seek(&range.start, Bias::Right, &()); + cursor.seek(&range.start, Bias::Right); if let Some(excerpt) = cursor.item() { - let mut end_before_newline = cursor.end(&()); + let mut end_before_newline = cursor.end(); if excerpt.has_trailing_newline { end_before_newline -= ExcerptOffset::new(1); } @@ -4834,13 +4831,13 @@ impl MultiBufferSnapshot { summary.add_assign(&D::from_text_summary(&TextSummary::from("\n"))); } - cursor.next(&()); + cursor.next(); } if range.end > *cursor.start() { summary.add_assign( &cursor - .summary::<_, ExcerptDimension>(&range.end, Bias::Right, &()) + .summary::<_, ExcerptDimension>(&range.end, Bias::Right) .0, ); if let Some(excerpt) = cursor.item() { @@ -4876,11 +4873,11 @@ impl MultiBufferSnapshot { D: TextDimension + Ord + Sub, { loop { - let transform_end_position = diff_transforms.end(&()).0.0; + let transform_end_position = diff_transforms.end().0.0; let at_transform_end = excerpt_position == transform_end_position && diff_transforms.item().is_some(); if at_transform_end && anchor.text_anchor.bias == Bias::Right { - diff_transforms.next(&()); + diff_transforms.next(); continue; } @@ -4906,7 +4903,7 @@ impl MultiBufferSnapshot { ); position.add_assign(&position_in_hunk); } else if at_transform_end { - diff_transforms.next(&()); + diff_transforms.next(); continue; } } @@ -4915,7 +4912,7 @@ impl MultiBufferSnapshot { } _ => { if at_transform_end && anchor.diff_base_anchor.is_some() { - diff_transforms.next(&()); + diff_transforms.next(); continue; } let overshoot = excerpt_position - diff_transforms.start().0.0; @@ -4933,9 +4930,9 @@ impl MultiBufferSnapshot { .cursor::<(Option<&Locator>, ExcerptOffset)>(&()); let locator = self.excerpt_locator_for_id(anchor.excerpt_id); - cursor.seek(&Some(locator), Bias::Left, &()); + cursor.seek(&Some(locator), Bias::Left); if cursor.item().is_none() { - cursor.next(&()); + cursor.next(); } let mut position = cursor.start().1; @@ -4975,7 +4972,7 @@ impl MultiBufferSnapshot { let mut diff_transforms_cursor = self .diff_transforms .cursor::<(ExcerptDimension, OutputDimension)>(&()); - diff_transforms_cursor.next(&()); + diff_transforms_cursor.next(); let mut summaries = Vec::new(); while let Some(anchor) = anchors.peek() { @@ -4990,9 +4987,9 @@ impl MultiBufferSnapshot { }); let locator = self.excerpt_locator_for_id(excerpt_id); - cursor.seek_forward(locator, Bias::Left, &()); + cursor.seek_forward(locator, Bias::Left); if cursor.item().is_none() { - cursor.next(&()); + cursor.next(); } let excerpt_start_position = D::from_text_summary(&cursor.start().text); @@ -5022,11 +5019,8 @@ impl MultiBufferSnapshot { } if position > diff_transforms_cursor.start().0.0 { - diff_transforms_cursor.seek_forward( - &ExcerptDimension(position), - Bias::Left, - &(), - ); + diff_transforms_cursor + .seek_forward(&ExcerptDimension(position), Bias::Left); } summaries.push(self.resolve_summary_for_anchor( @@ -5036,11 +5030,8 @@ impl MultiBufferSnapshot { )); } } else { - diff_transforms_cursor.seek_forward( - &ExcerptDimension(excerpt_start_position), - Bias::Left, - &(), - ); + diff_transforms_cursor + .seek_forward(&ExcerptDimension(excerpt_start_position), Bias::Left); let position = self.resolve_summary_for_anchor( &Anchor::max(), excerpt_start_position, @@ -5099,7 +5090,7 @@ impl MultiBufferSnapshot { { let mut anchors = anchors.into_iter().enumerate().peekable(); let mut cursor = self.excerpts.cursor::>(&()); - cursor.next(&()); + cursor.next(); let mut result = Vec::new(); @@ -5108,10 +5099,10 @@ impl MultiBufferSnapshot { // Find the location where this anchor's excerpt should be. let old_locator = self.excerpt_locator_for_id(old_excerpt_id); - cursor.seek_forward(&Some(old_locator), Bias::Left, &()); + cursor.seek_forward(&Some(old_locator), Bias::Left); if cursor.item().is_none() { - cursor.next(&()); + cursor.next(); } let next_excerpt = cursor.item(); @@ -5211,13 +5202,13 @@ 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)>(&()); - diff_transforms.seek(&offset, Bias::Right, &()); + 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(&()); + diff_transforms.prev(); } _ => {} } @@ -5260,13 +5251,13 @@ impl MultiBufferSnapshot { let mut excerpts = self .excerpts .cursor::<(ExcerptOffset, Option)>(&()); - excerpts.seek(&excerpt_offset, Bias::Right, &()); + excerpts.seek(&excerpt_offset, Bias::Right); if excerpts.item().is_none() && excerpt_offset == excerpts.start().0 && bias == Bias::Left { - excerpts.prev(&()); + excerpts.prev(); } if let Some(excerpt) = excerpts.item() { let mut overshoot = excerpt_offset.saturating_sub(excerpts.start().0).value; - if excerpt.has_trailing_newline && excerpt_offset == excerpts.end(&()).0 { + if excerpt.has_trailing_newline && excerpt_offset == excerpts.end().0 { overshoot -= 1; bias = Bias::Right; } @@ -5297,7 +5288,7 @@ impl MultiBufferSnapshot { let excerpt_id = self.latest_excerpt_id(excerpt_id); let locator = self.excerpt_locator_for_id(excerpt_id); let mut cursor = self.excerpts.cursor::>(&()); - cursor.seek(locator, Bias::Left, &()); + cursor.seek(locator, Bias::Left); if let Some(excerpt) = cursor.item() { if excerpt.id == excerpt_id { let text_anchor = excerpt.clip_anchor(text_anchor); @@ -5351,13 +5342,13 @@ impl MultiBufferSnapshot { let mut excerpts = self .excerpts .cursor::<(Option<&Locator>, ExcerptDimension)>(&()); - excerpts.seek(&Some(start_locator), Bias::Left, &()); - excerpts.prev(&()); + excerpts.seek(&Some(start_locator), Bias::Left); + excerpts.prev(); let mut diff_transforms = self.diff_transforms.cursor::>(&()); - diff_transforms.seek(&excerpts.start().1, Bias::Left, &()); - if diff_transforms.end(&()).excerpt_dimension < excerpts.start().1 { - diff_transforms.next(&()); + diff_transforms.seek(&excerpts.start().1, Bias::Left); + if diff_transforms.end().excerpt_dimension < excerpts.start().1 { + diff_transforms.next(); } let excerpt = excerpts.item()?; @@ -6193,7 +6184,7 @@ impl MultiBufferSnapshot { Locator::max_ref() } else { let mut cursor = self.excerpt_ids.cursor::(&()); - cursor.seek(&id, Bias::Left, &()); + cursor.seek(&id, Bias::Left); if let Some(entry) = cursor.item() { if entry.id == id { return &entry.locator; @@ -6229,7 +6220,7 @@ impl MultiBufferSnapshot { let mut cursor = self.excerpt_ids.cursor::(&()); for id in sorted_ids { - if cursor.seek_forward(&id, Bias::Left, &()) { + if cursor.seek_forward(&id, Bias::Left) { locators.push(cursor.item().unwrap().locator.clone()); } else { panic!("invalid excerpt id {:?}", id); @@ -6253,16 +6244,16 @@ impl MultiBufferSnapshot { .excerpts .cursor::<(Option<&Locator>, ExcerptDimension)>(&()); let locator = self.excerpt_locator_for_id(excerpt_id); - if cursor.seek(&Some(locator), Bias::Left, &()) { + if cursor.seek(&Some(locator), Bias::Left) { let start = cursor.start().1.clone(); - let end = cursor.end(&()).1; + let end = cursor.end().1; let mut diff_transforms = self .diff_transforms .cursor::<(ExcerptDimension, OutputDimension)>(&()); - diff_transforms.seek(&start, Bias::Left, &()); + diff_transforms.seek(&start, Bias::Left); let overshoot = start.0 - diff_transforms.start().0.0; let start = diff_transforms.start().1.0 + overshoot; - diff_transforms.seek(&end, Bias::Right, &()); + diff_transforms.seek(&end, Bias::Right); let overshoot = end.0 - diff_transforms.start().0.0; let end = diff_transforms.start().1.0 + overshoot; Some(start..end) @@ -6274,7 +6265,7 @@ impl MultiBufferSnapshot { pub fn buffer_range_for_excerpt(&self, excerpt_id: ExcerptId) -> Option> { let mut cursor = self.excerpts.cursor::>(&()); let locator = self.excerpt_locator_for_id(excerpt_id); - if cursor.seek(&Some(locator), Bias::Left, &()) { + if cursor.seek(&Some(locator), Bias::Left) { if let Some(excerpt) = cursor.item() { return Some(excerpt.range.context.clone()); } @@ -6285,7 +6276,7 @@ impl MultiBufferSnapshot { fn excerpt(&self, excerpt_id: ExcerptId) -> Option<&Excerpt> { let mut cursor = self.excerpts.cursor::>(&()); let locator = self.excerpt_locator_for_id(excerpt_id); - cursor.seek(&Some(locator), Bias::Left, &()); + cursor.seek(&Some(locator), Bias::Left); if let Some(excerpt) = cursor.item() { if excerpt.id == excerpt_id { return Some(excerpt); @@ -6333,7 +6324,7 @@ impl MultiBufferSnapshot { let mut cursor = self.excerpts.cursor::(&()); let start_locator = self.excerpt_locator_for_id(range.start.excerpt_id); let end_locator = self.excerpt_locator_for_id(range.end.excerpt_id); - cursor.seek(start_locator, Bias::Left, &()); + cursor.seek(start_locator, Bias::Left); cursor .take_while(move |excerpt| excerpt.locator <= *end_locator) .flat_map(move |excerpt| { @@ -6472,11 +6463,11 @@ where fn seek(&mut self, position: &D) { self.cached_region.take(); self.diff_transforms - .seek(&OutputDimension(*position), Bias::Right, &()); + .seek(&OutputDimension(*position), Bias::Right); if self.diff_transforms.item().is_none() && *position == self.diff_transforms.start().output_dimension.0 { - self.diff_transforms.prev(&()); + self.diff_transforms.prev(); } let mut excerpt_position = self.diff_transforms.start().excerpt_dimension.0; @@ -6486,20 +6477,20 @@ where } self.excerpts - .seek(&ExcerptDimension(excerpt_position), Bias::Right, &()); + .seek(&ExcerptDimension(excerpt_position), Bias::Right); if self.excerpts.item().is_none() && excerpt_position == self.excerpts.start().0 { - self.excerpts.prev(&()); + self.excerpts.prev(); } } fn seek_forward(&mut self, position: &D) { self.cached_region.take(); self.diff_transforms - .seek_forward(&OutputDimension(*position), Bias::Right, &()); + .seek_forward(&OutputDimension(*position), Bias::Right); if self.diff_transforms.item().is_none() && *position == self.diff_transforms.start().output_dimension.0 { - self.diff_transforms.prev(&()); + self.diff_transforms.prev(); } let overshoot = *position - self.diff_transforms.start().output_dimension.0; @@ -6509,31 +6500,30 @@ where } self.excerpts - .seek_forward(&ExcerptDimension(excerpt_position), Bias::Right, &()); + .seek_forward(&ExcerptDimension(excerpt_position), Bias::Right); if self.excerpts.item().is_none() && excerpt_position == self.excerpts.start().0 { - self.excerpts.prev(&()); + self.excerpts.prev(); } } fn next_excerpt(&mut self) { - self.excerpts.next(&()); + self.excerpts.next(); self.seek_to_start_of_current_excerpt(); } fn prev_excerpt(&mut self) { - self.excerpts.prev(&()); + self.excerpts.prev(); self.seek_to_start_of_current_excerpt(); } fn seek_to_start_of_current_excerpt(&mut self) { self.cached_region.take(); - self.diff_transforms - .seek(self.excerpts.start(), Bias::Left, &()); - if self.diff_transforms.end(&()).excerpt_dimension == *self.excerpts.start() + self.diff_transforms.seek(self.excerpts.start(), Bias::Left); + if self.diff_transforms.end().excerpt_dimension == *self.excerpts.start() && self.diff_transforms.start().excerpt_dimension < *self.excerpts.start() && self.diff_transforms.next_item().is_some() { - self.diff_transforms.next(&()); + self.diff_transforms.next(); } } @@ -6541,18 +6531,18 @@ where self.cached_region.take(); match self .diff_transforms - .end(&()) + .end() .excerpt_dimension - .cmp(&self.excerpts.end(&())) + .cmp(&self.excerpts.end()) { - cmp::Ordering::Less => self.diff_transforms.next(&()), - cmp::Ordering::Greater => self.excerpts.next(&()), + cmp::Ordering::Less => self.diff_transforms.next(), + cmp::Ordering::Greater => self.excerpts.next(), cmp::Ordering::Equal => { - self.diff_transforms.next(&()); - if self.diff_transforms.end(&()).excerpt_dimension > self.excerpts.end(&()) + self.diff_transforms.next(); + if self.diff_transforms.end().excerpt_dimension > self.excerpts.end() || self.diff_transforms.item().is_none() { - self.excerpts.next(&()); + self.excerpts.next(); } else if let Some(DiffTransform::DeletedHunk { hunk_info, .. }) = self.diff_transforms.item() { @@ -6561,7 +6551,7 @@ where .item() .map_or(false, |excerpt| excerpt.id != hunk_info.excerpt_id) { - self.excerpts.next(&()); + self.excerpts.next(); } } } @@ -6576,14 +6566,14 @@ where .excerpt_dimension .cmp(self.excerpts.start()) { - cmp::Ordering::Less => self.excerpts.prev(&()), - cmp::Ordering::Greater => self.diff_transforms.prev(&()), + cmp::Ordering::Less => self.excerpts.prev(), + cmp::Ordering::Greater => self.diff_transforms.prev(), cmp::Ordering::Equal => { - self.diff_transforms.prev(&()); + self.diff_transforms.prev(); if self.diff_transforms.start().excerpt_dimension < *self.excerpts.start() || self.diff_transforms.item().is_none() { - self.excerpts.prev(&()); + self.excerpts.prev(); } } } @@ -6603,9 +6593,9 @@ where return true; } - self.diff_transforms.prev(&()); + self.diff_transforms.prev(); let prev_transform = self.diff_transforms.item(); - self.diff_transforms.next(&()); + self.diff_transforms.next(); prev_transform.map_or(true, |next_transform| { matches!(next_transform, DiffTransform::BufferContent { .. }) @@ -6613,9 +6603,9 @@ where } fn is_at_end_of_excerpt(&mut self) -> bool { - if self.diff_transforms.end(&()).excerpt_dimension < self.excerpts.end(&()) { + if self.diff_transforms.end().excerpt_dimension < self.excerpts.end() { return false; - } else if self.diff_transforms.end(&()).excerpt_dimension > self.excerpts.end(&()) + } else if self.diff_transforms.end().excerpt_dimension > self.excerpts.end() || self.diff_transforms.item().is_none() { return true; @@ -6636,7 +6626,7 @@ where let buffer = &excerpt.buffer; let buffer_context_start = excerpt.range.context.start.summary::(buffer); let mut buffer_start = buffer_context_start; - let overshoot = self.diff_transforms.end(&()).excerpt_dimension.0 - self.excerpts.start().0; + let overshoot = self.diff_transforms.end().excerpt_dimension.0 - self.excerpts.start().0; buffer_start.add_assign(&overshoot); Some(buffer_start) } @@ -6659,7 +6649,7 @@ where let mut buffer_end = buffer_start; 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; + let end = self.diff_transforms.end().output_dimension.0; return Some(MultiBufferRegion { buffer, excerpt, @@ -6693,16 +6683,16 @@ where let mut end; let mut buffer_end; let has_trailing_newline; - if self.diff_transforms.end(&()).excerpt_dimension.0 < self.excerpts.end(&()).0 { + if self.diff_transforms.end().excerpt_dimension.0 < self.excerpts.end().0 { let overshoot = - self.diff_transforms.end(&()).excerpt_dimension.0 - self.excerpts.start().0; - end = self.diff_transforms.end(&()).output_dimension.0; + self.diff_transforms.end().excerpt_dimension.0 - self.excerpts.start().0; + end = self.diff_transforms.end().output_dimension.0; buffer_end = buffer_context_start; buffer_end.add_assign(&overshoot); has_trailing_newline = false; } else { let overshoot = - self.excerpts.end(&()).0 - self.diff_transforms.start().excerpt_dimension.0; + self.excerpts.end().0 - self.diff_transforms.start().excerpt_dimension.0; end = self.diff_transforms.start().output_dimension.0; end.add_assign(&overshoot); buffer_end = excerpt.range.context.end.summary::(buffer); @@ -7086,11 +7076,11 @@ impl<'a> MultiBufferExcerpt<'a> { /// Maps a range within the [`MultiBuffer`] to a range within the [`Buffer`] pub fn map_range_to_buffer(&mut self, range: Range) -> Range { self.diff_transforms - .seek(&OutputDimension(range.start), Bias::Right, &()); + .seek(&OutputDimension(range.start), Bias::Right); let start = self.map_offset_to_buffer_internal(range.start); let end = if range.end > range.start { self.diff_transforms - .seek_forward(&OutputDimension(range.end), Bias::Right, &()); + .seek_forward(&OutputDimension(range.end), Bias::Right); self.map_offset_to_buffer_internal(range.end) } else { start @@ -7123,7 +7113,7 @@ impl<'a> MultiBufferExcerpt<'a> { } let overshoot = buffer_range.start - self.buffer_offset; let excerpt_offset = ExcerptDimension(self.excerpt_offset.0 + overshoot); - self.diff_transforms.seek(&excerpt_offset, Bias::Right, &()); + self.diff_transforms.seek(&excerpt_offset, Bias::Right); if excerpt_offset.0 < self.diff_transforms.start().excerpt_dimension.0 { log::warn!( "Attempting to map a range from a buffer offset that starts before the current buffer offset" @@ -7137,7 +7127,7 @@ impl<'a> MultiBufferExcerpt<'a> { let overshoot = buffer_range.end - self.buffer_offset; let excerpt_offset = ExcerptDimension(self.excerpt_offset.0 + overshoot); self.diff_transforms - .seek_forward(&excerpt_offset, Bias::Right, &()); + .seek_forward(&excerpt_offset, Bias::Right); let overshoot = excerpt_offset.0 - self.diff_transforms.start().excerpt_dimension.0; self.diff_transforms.start().output_dimension.0 + overshoot } else { @@ -7509,7 +7499,7 @@ impl Iterator for MultiBufferRows<'_> { if let Some(next_region) = self.cursor.region() { region = next_region; } else { - if self.point == self.cursor.diff_transforms.end(&()).output_dimension.0 { + if self.point == self.cursor.diff_transforms.end().output_dimension.0 { let multibuffer_row = MultiBufferRow(self.point.row); let last_excerpt = self .cursor @@ -7615,14 +7605,14 @@ impl<'a> MultiBufferChunks<'a> { } pub fn seek(&mut self, range: Range) { - self.diff_transforms.seek(&range.end, Bias::Right, &()); + self.diff_transforms.seek(&range.end, Bias::Right); let mut excerpt_end = self.diff_transforms.start().1; if let Some(DiffTransform::BufferContent { .. }) = self.diff_transforms.item() { let overshoot = range.end - self.diff_transforms.start().0; excerpt_end.value += overshoot; } - self.diff_transforms.seek(&range.start, Bias::Right, &()); + self.diff_transforms.seek(&range.start, Bias::Right); let mut excerpt_start = self.diff_transforms.start().1; if let Some(DiffTransform::BufferContent { .. }) = self.diff_transforms.item() { let overshoot = range.start - self.diff_transforms.start().0; @@ -7636,7 +7626,7 @@ impl<'a> MultiBufferChunks<'a> { fn seek_to_excerpt_offset_range(&mut self, new_range: Range) { self.excerpt_offset_range = new_range.clone(); - self.excerpts.seek(&new_range.start, Bias::Right, &()); + self.excerpts.seek(&new_range.start, Bias::Right); if let Some(excerpt) = self.excerpts.item() { let excerpt_start = *self.excerpts.start(); if let Some(excerpt_chunks) = self @@ -7669,7 +7659,7 @@ impl<'a> MultiBufferChunks<'a> { self.excerpt_offset_range.start.value += chunk.text.len(); return Some(chunk); } else { - self.excerpts.next(&()); + self.excerpts.next(); let excerpt = self.excerpts.item()?; self.excerpt_chunks = Some(excerpt.chunks_in_range( 0..(self.excerpt_offset_range.end - *self.excerpts.start()).value, @@ -7712,12 +7702,12 @@ impl<'a> Iterator for MultiBufferChunks<'a> { if self.range.start >= self.range.end { return None; } - if self.range.start == self.diff_transforms.end(&()).0 { - self.diff_transforms.next(&()); + if self.range.start == self.diff_transforms.end().0 { + self.diff_transforms.next(); } let diff_transform_start = self.diff_transforms.start().0; - let diff_transform_end = self.diff_transforms.end(&()).0; + let diff_transform_end = self.diff_transforms.end().0; debug_assert!(self.range.start < diff_transform_end); let diff_transform = self.diff_transforms.item()?; diff --git a/crates/notifications/src/notification_store.rs b/crates/notifications/src/notification_store.rs index c2f18e57001bdc748738850c2f87948ea6196cef..0329a53cc7efd285fa06cfeda3af9699e53acd8a 100644 --- a/crates/notifications/src/notification_store.rs +++ b/crates/notifications/src/notification_store.rs @@ -132,12 +132,12 @@ impl NotificationStore { } let ix = count - 1 - ix; let mut cursor = self.notifications.cursor::(&()); - cursor.seek(&Count(ix), Bias::Right, &()); + cursor.seek(&Count(ix), Bias::Right); cursor.item() } pub fn notification_for_id(&self, id: u64) -> Option<&NotificationEntry> { let mut cursor = self.notifications.cursor::(&()); - cursor.seek(&NotificationId(id), Bias::Left, &()); + cursor.seek(&NotificationId(id), Bias::Left); if let Some(item) = cursor.item() { if item.id == id { return Some(item); @@ -365,7 +365,7 @@ impl NotificationStore { let mut old_range = 0..0; for (i, (id, new_notification)) in notifications.into_iter().enumerate() { - new_notifications.append(cursor.slice(&NotificationId(id), Bias::Left, &()), &()); + new_notifications.append(cursor.slice(&NotificationId(id), Bias::Left), &()); if i == 0 { old_range.start = cursor.start().1.0; @@ -374,7 +374,7 @@ impl NotificationStore { let old_notification = cursor.item(); if let Some(old_notification) = old_notification { if old_notification.id == id { - cursor.next(&()); + cursor.next(); if let Some(new_notification) = &new_notification { if new_notification.is_read { @@ -403,7 +403,7 @@ impl NotificationStore { old_range.end = cursor.start().1.0; let new_count = new_notifications.summary().count - old_range.start; - new_notifications.append(cursor.suffix(&()), &()); + new_notifications.append(cursor.suffix(), &()); drop(cursor); self.notifications = new_notifications; diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 6e3d27deffd8f470b327462aefd8c8bc3b81ad65..eb16446daf5170fddb2d7f47a79f0a64033d3226 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -4279,7 +4279,7 @@ 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.seek_forward(&PathTarget::Path(repo_path), Bias::Left) { if cursor.item().is_some_and(|entry| entry.status == *status) { continue; } @@ -4292,7 +4292,7 @@ impl Repository { } let mut cursor = prev_statuses.cursor::(&()); for path in changed_paths.into_iter() { - if cursor.seek_forward(&PathTarget::Path(&path), Bias::Left, &()) { + if cursor.seek_forward(&PathTarget::Path(&path), Bias::Left) { changed_path_statuses.push(Edit::Remove(PathKey(path.0))); } } diff --git a/crates/project/src/git_store/git_traversal.rs b/crates/project/src/git_store/git_traversal.rs index 68ed03cfe9e41abf480fbe7a5bf10f84e10ce553..cd173d5714863f1ed845dd9e7116dc73d214f710 100644 --- a/crates/project/src/git_store/git_traversal.rs +++ b/crates/project/src/git_store/git_traversal.rs @@ -72,14 +72,13 @@ impl<'a> GitTraversal<'a> { if entry.is_dir() { let mut statuses = statuses.clone(); - statuses.seek_forward(&PathTarget::Path(repo_path.as_ref()), Bias::Left, &()); - let summary = - statuses.summary(&PathTarget::Successor(repo_path.as_ref()), Bias::Left, &()); + statuses.seek_forward(&PathTarget::Path(repo_path.as_ref()), Bias::Left); + let summary = statuses.summary(&PathTarget::Successor(repo_path.as_ref()), Bias::Left); self.current_entry_summary = Some(summary); } else if entry.is_file() { // For a file entry, park the cursor on the corresponding status - if statuses.seek_forward(&PathTarget::Path(repo_path.as_ref()), Bias::Left, &()) { + if statuses.seek_forward(&PathTarget::Path(repo_path.as_ref()), Bias::Left) { // TODO: Investigate statuses.item() being None here. self.current_entry_summary = statuses.item().map(|item| item.status.into()); } else { diff --git a/crates/rope/src/rope.rs b/crates/rope/src/rope.rs index 535b863b7d7b1e66b8621b2da02c8f8d9c7f3912..515cd7133153c94978eabc27df21a55333d60c28 100644 --- a/crates/rope/src/rope.rs +++ b/crates/rope/src/rope.rs @@ -41,9 +41,9 @@ impl Rope { self.push_chunk(chunk.as_slice()); let mut chunks = rope.chunks.cursor::<()>(&()); - chunks.next(&()); - chunks.next(&()); - self.chunks.append(chunks.suffix(&()), &()); + chunks.next(); + chunks.next(); + self.chunks.append(chunks.suffix(), &()); self.check_invariants(); return; } @@ -283,7 +283,7 @@ impl Rope { return self.summary().len_utf16; } let mut cursor = self.chunks.cursor::<(usize, OffsetUtf16)>(&()); - cursor.seek(&offset, Bias::Left, &()); + cursor.seek(&offset, Bias::Left); let overshoot = offset - cursor.start().0; cursor.start().1 + cursor.item().map_or(Default::default(), |chunk| { @@ -296,7 +296,7 @@ impl Rope { return self.summary().len; } let mut cursor = self.chunks.cursor::<(OffsetUtf16, usize)>(&()); - cursor.seek(&offset, Bias::Left, &()); + cursor.seek(&offset, Bias::Left); let overshoot = offset - cursor.start().0; cursor.start().1 + cursor.item().map_or(Default::default(), |chunk| { @@ -309,7 +309,7 @@ impl Rope { return self.summary().lines; } let mut cursor = self.chunks.cursor::<(usize, Point)>(&()); - cursor.seek(&offset, Bias::Left, &()); + cursor.seek(&offset, Bias::Left); let overshoot = offset - cursor.start().0; cursor.start().1 + cursor.item().map_or(Point::zero(), |chunk| { @@ -322,7 +322,7 @@ impl Rope { return self.summary().lines_utf16(); } let mut cursor = self.chunks.cursor::<(usize, PointUtf16)>(&()); - cursor.seek(&offset, Bias::Left, &()); + cursor.seek(&offset, Bias::Left); let overshoot = offset - cursor.start().0; cursor.start().1 + cursor.item().map_or(PointUtf16::zero(), |chunk| { @@ -335,7 +335,7 @@ impl Rope { return self.summary().lines_utf16(); } let mut cursor = self.chunks.cursor::<(Point, PointUtf16)>(&()); - cursor.seek(&point, Bias::Left, &()); + cursor.seek(&point, Bias::Left); let overshoot = point - cursor.start().0; cursor.start().1 + cursor.item().map_or(PointUtf16::zero(), |chunk| { @@ -348,7 +348,7 @@ impl Rope { return self.summary().len; } let mut cursor = self.chunks.cursor::<(Point, usize)>(&()); - cursor.seek(&point, Bias::Left, &()); + cursor.seek(&point, Bias::Left); let overshoot = point - cursor.start().0; cursor.start().1 + cursor @@ -369,7 +369,7 @@ impl Rope { return self.summary().len; } let mut cursor = self.chunks.cursor::<(PointUtf16, usize)>(&()); - cursor.seek(&point, Bias::Left, &()); + cursor.seek(&point, Bias::Left); let overshoot = point - cursor.start().0; cursor.start().1 + cursor.item().map_or(0, |chunk| { @@ -382,7 +382,7 @@ impl Rope { return self.summary().lines; } let mut cursor = self.chunks.cursor::<(PointUtf16, Point)>(&()); - cursor.seek(&point.0, Bias::Left, &()); + cursor.seek(&point.0, Bias::Left); let overshoot = Unclipped(point.0 - cursor.start().0); cursor.start().1 + cursor.item().map_or(Point::zero(), |chunk| { @@ -392,7 +392,7 @@ impl Rope { pub fn clip_offset(&self, mut offset: usize, bias: Bias) -> usize { let mut cursor = self.chunks.cursor::(&()); - cursor.seek(&offset, Bias::Left, &()); + cursor.seek(&offset, Bias::Left); if let Some(chunk) = cursor.item() { let mut ix = offset - cursor.start(); while !chunk.text.is_char_boundary(ix) { @@ -415,7 +415,7 @@ impl Rope { pub fn clip_offset_utf16(&self, offset: OffsetUtf16, bias: Bias) -> OffsetUtf16 { let mut cursor = self.chunks.cursor::(&()); - cursor.seek(&offset, Bias::Right, &()); + cursor.seek(&offset, Bias::Right); if let Some(chunk) = cursor.item() { let overshoot = offset - cursor.start(); *cursor.start() + chunk.as_slice().clip_offset_utf16(overshoot, bias) @@ -426,7 +426,7 @@ impl Rope { pub fn clip_point(&self, point: Point, bias: Bias) -> Point { let mut cursor = self.chunks.cursor::(&()); - cursor.seek(&point, Bias::Right, &()); + cursor.seek(&point, Bias::Right); if let Some(chunk) = cursor.item() { let overshoot = point - cursor.start(); *cursor.start() + chunk.as_slice().clip_point(overshoot, bias) @@ -437,7 +437,7 @@ impl Rope { pub fn clip_point_utf16(&self, point: Unclipped, bias: Bias) -> PointUtf16 { let mut cursor = self.chunks.cursor::(&()); - cursor.seek(&point.0, Bias::Right, &()); + cursor.seek(&point.0, Bias::Right); if let Some(chunk) = cursor.item() { let overshoot = Unclipped(point.0 - cursor.start()); *cursor.start() + chunk.as_slice().clip_point_utf16(overshoot, bias) @@ -450,10 +450,6 @@ impl Rope { self.clip_point(Point::new(row, u32::MAX), Bias::Left) .column } - - pub fn ptr_eq(&self, other: &Self) -> bool { - self.chunks.ptr_eq(&other.chunks) - } } impl<'a> From<&'a str> for Rope { @@ -514,7 +510,7 @@ pub struct Cursor<'a> { impl<'a> Cursor<'a> { pub fn new(rope: &'a Rope, offset: usize) -> Self { let mut chunks = rope.chunks.cursor(&()); - chunks.seek(&offset, Bias::Right, &()); + chunks.seek(&offset, Bias::Right); Self { rope, chunks, @@ -525,7 +521,7 @@ impl<'a> Cursor<'a> { pub fn seek_forward(&mut self, end_offset: usize) { debug_assert!(end_offset >= self.offset); - self.chunks.seek_forward(&end_offset, Bias::Right, &()); + self.chunks.seek_forward(&end_offset, Bias::Right); self.offset = end_offset; } @@ -540,14 +536,14 @@ impl<'a> Cursor<'a> { let mut slice = Rope::new(); if let Some(start_chunk) = self.chunks.item() { let start_ix = self.offset - self.chunks.start(); - let end_ix = cmp::min(end_offset, self.chunks.end(&())) - self.chunks.start(); + let end_ix = cmp::min(end_offset, self.chunks.end()) - self.chunks.start(); slice.push_chunk(start_chunk.slice(start_ix..end_ix)); } - if end_offset > self.chunks.end(&()) { - self.chunks.next(&()); + if end_offset > self.chunks.end() { + self.chunks.next(); slice.append(Rope { - chunks: self.chunks.slice(&end_offset, Bias::Right, &()), + chunks: self.chunks.slice(&end_offset, Bias::Right), }); if let Some(end_chunk) = self.chunks.item() { let end_ix = end_offset - self.chunks.start(); @@ -565,13 +561,13 @@ impl<'a> Cursor<'a> { let mut summary = D::zero(&()); if let Some(start_chunk) = self.chunks.item() { let start_ix = self.offset - self.chunks.start(); - let end_ix = cmp::min(end_offset, self.chunks.end(&())) - self.chunks.start(); + let end_ix = cmp::min(end_offset, self.chunks.end()) - self.chunks.start(); summary.add_assign(&D::from_chunk(start_chunk.slice(start_ix..end_ix))); } - if end_offset > self.chunks.end(&()) { - self.chunks.next(&()); - summary.add_assign(&self.chunks.summary(&end_offset, Bias::Right, &())); + if end_offset > self.chunks.end() { + self.chunks.next(); + summary.add_assign(&self.chunks.summary(&end_offset, Bias::Right)); if let Some(end_chunk) = self.chunks.item() { let end_ix = end_offset - self.chunks.start(); summary.add_assign(&D::from_chunk(end_chunk.slice(0..end_ix))); @@ -603,10 +599,10 @@ impl<'a> Chunks<'a> { pub fn new(rope: &'a Rope, range: Range, reversed: bool) -> Self { let mut chunks = rope.chunks.cursor(&()); let offset = if reversed { - chunks.seek(&range.end, Bias::Left, &()); + chunks.seek(&range.end, Bias::Left); range.end } else { - chunks.seek(&range.start, Bias::Right, &()); + chunks.seek(&range.start, Bias::Right); range.start }; Self { @@ -642,10 +638,10 @@ impl<'a> Chunks<'a> { Bias::Right }; - if offset >= self.chunks.end(&()) { - self.chunks.seek_forward(&offset, bias, &()); + if offset >= self.chunks.end() { + self.chunks.seek_forward(&offset, bias); } else { - self.chunks.seek(&offset, bias, &()); + self.chunks.seek(&offset, bias); } self.offset = offset; @@ -674,25 +670,25 @@ impl<'a> Chunks<'a> { found = self.offset <= self.range.end; } else { self.chunks - .search_forward(|summary| summary.text.lines.row > 0, &()); + .search_forward(|summary| summary.text.lines.row > 0); self.offset = *self.chunks.start(); if let Some(newline_ix) = self.peek().and_then(|chunk| chunk.find('\n')) { self.offset += newline_ix + 1; found = self.offset <= self.range.end; } else { - self.offset = self.chunks.end(&()); + self.offset = self.chunks.end(); } } - if self.offset == self.chunks.end(&()) { + if self.offset == self.chunks.end() { self.next(); } } if self.offset > self.range.end { self.offset = cmp::min(self.offset, self.range.end); - self.chunks.seek(&self.offset, Bias::Right, &()); + self.chunks.seek(&self.offset, Bias::Right); } found @@ -711,7 +707,7 @@ impl<'a> Chunks<'a> { let initial_offset = self.offset; if self.offset == *self.chunks.start() { - self.chunks.prev(&()); + self.chunks.prev(); } if let Some(chunk) = self.chunks.item() { @@ -729,14 +725,14 @@ impl<'a> Chunks<'a> { } self.chunks - .search_backward(|summary| summary.text.lines.row > 0, &()); + .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(&()); + if self.offset == self.chunks.end() { + self.chunks.next(); } return true; @@ -746,7 +742,7 @@ impl<'a> Chunks<'a> { if !self.offset_is_valid() || self.chunks.item().is_none() { self.offset = self.range.start; - self.chunks.seek(&self.offset, Bias::Right, &()); + self.chunks.seek(&self.offset, Bias::Right); } self.offset < initial_offset && self.offset == 0 @@ -765,7 +761,7 @@ impl<'a> Chunks<'a> { slice_start..slice_end } else { let slice_start = self.offset - chunk_start; - let slice_end = cmp::min(self.chunks.end(&()), self.range.end) - chunk_start; + let slice_end = cmp::min(self.chunks.end(), self.range.end) - chunk_start; slice_start..slice_end }; @@ -825,12 +821,12 @@ impl<'a> Iterator for Chunks<'a> { if self.reversed { self.offset -= chunk.len(); if self.offset <= *self.chunks.start() { - self.chunks.prev(&()); + self.chunks.prev(); } } else { self.offset += chunk.len(); - if self.offset >= self.chunks.end(&()) { - self.chunks.next(&()); + if self.offset >= self.chunks.end() { + self.chunks.next(); } } @@ -848,9 +844,9 @@ impl<'a> Bytes<'a> { pub fn new(rope: &'a Rope, range: Range, reversed: bool) -> Self { let mut chunks = rope.chunks.cursor(&()); if reversed { - chunks.seek(&range.end, Bias::Left, &()); + chunks.seek(&range.end, Bias::Left); } else { - chunks.seek(&range.start, Bias::Right, &()); + chunks.seek(&range.start, Bias::Right); } Self { chunks, @@ -861,7 +857,7 @@ impl<'a> Bytes<'a> { pub fn peek(&self) -> Option<&'a [u8]> { let chunk = self.chunks.item()?; - if self.reversed && self.range.start >= self.chunks.end(&()) { + if self.reversed && self.range.start >= self.chunks.end() { return None; } let chunk_start = *self.chunks.start(); @@ -881,9 +877,9 @@ impl<'a> Iterator for Bytes<'a> { let result = self.peek(); if result.is_some() { if self.reversed { - self.chunks.prev(&()); + self.chunks.prev(); } else { - self.chunks.next(&()); + self.chunks.next(); } } result @@ -905,9 +901,9 @@ impl io::Read for Bytes<'_> { if len == chunk.len() { if self.reversed { - self.chunks.prev(&()); + self.chunks.prev(); } else { - self.chunks.next(&()); + self.chunks.next(); } } Ok(len) diff --git a/crates/sum_tree/src/cursor.rs b/crates/sum_tree/src/cursor.rs index 8edd04afcef12781f1acc43eb2edb5805128c3b6..50a556a6d279d0b7f733d0d80c6c2e7e3d6c61cd 100644 --- a/crates/sum_tree/src/cursor.rs +++ b/crates/sum_tree/src/cursor.rs @@ -25,6 +25,7 @@ pub struct Cursor<'a, T: Item, D> { position: D, did_seek: bool, at_end: bool, + cx: &'a ::Context, } impl fmt::Debug for Cursor<'_, T, D> @@ -52,21 +53,22 @@ where T: Item, D: Dimension<'a, T::Summary>, { - pub fn new(tree: &'a SumTree, cx: &::Context) -> Self { + pub fn new(tree: &'a SumTree, cx: &'a ::Context) -> Self { Self { tree, stack: ArrayVec::new(), position: D::zero(cx), did_seek: false, at_end: tree.is_empty(), + cx, } } - fn reset(&mut self, cx: &::Context) { + fn reset(&mut self) { self.did_seek = false; self.at_end = self.tree.is_empty(); self.stack.truncate(0); - self.position = D::zero(cx); + self.position = D::zero(self.cx); } pub fn start(&self) -> &D { @@ -74,10 +76,10 @@ where } #[track_caller] - pub fn end(&self, cx: &::Context) -> D { + pub fn end(&self) -> D { if let Some(item_summary) = self.item_summary() { let mut end = self.start().clone(); - end.add_summary(item_summary, cx); + end.add_summary(item_summary, self.cx); end } else { self.start().clone() @@ -202,12 +204,12 @@ where } #[track_caller] - pub fn prev(&mut self, cx: &::Context) { - self.search_backward(|_| true, cx) + pub fn prev(&mut self) { + self.search_backward(|_| true) } #[track_caller] - pub fn search_backward(&mut self, mut filter_node: F, cx: &::Context) + pub fn search_backward(&mut self, mut filter_node: F) where F: FnMut(&T::Summary) -> bool, { @@ -217,13 +219,13 @@ where } if self.at_end { - self.position = D::zero(cx); + self.position = D::zero(self.cx); self.at_end = self.tree.is_empty(); if !self.tree.is_empty() { self.stack.push(StackEntry { tree: self.tree, index: self.tree.0.child_summaries().len(), - position: D::from_summary(self.tree.summary(), cx), + position: D::from_summary(self.tree.summary(), self.cx), }); } } @@ -233,7 +235,7 @@ where if let Some(StackEntry { position, .. }) = self.stack.iter().rev().nth(1) { self.position = position.clone(); } else { - self.position = D::zero(cx); + self.position = D::zero(self.cx); } let entry = self.stack.last_mut().unwrap(); @@ -247,7 +249,7 @@ where } for summary in &entry.tree.0.child_summaries()[..entry.index] { - self.position.add_summary(summary, cx); + self.position.add_summary(summary, self.cx); } entry.position = self.position.clone(); @@ -257,7 +259,7 @@ where if descending { let tree = &child_trees[entry.index]; self.stack.push(StackEntry { - position: D::zero(cx), + position: D::zero(self.cx), tree, index: tree.0.child_summaries().len() - 1, }) @@ -273,12 +275,12 @@ where } #[track_caller] - pub fn next(&mut self, cx: &::Context) { - self.search_forward(|_| true, cx) + pub fn next(&mut self) { + self.search_forward(|_| true) } #[track_caller] - pub fn search_forward(&mut self, mut filter_node: F, cx: &::Context) + pub fn search_forward(&mut self, mut filter_node: F) where F: FnMut(&T::Summary) -> bool, { @@ -289,7 +291,7 @@ where self.stack.push(StackEntry { tree: self.tree, index: 0, - position: D::zero(cx), + position: D::zero(self.cx), }); descend = true; } @@ -316,8 +318,8 @@ where break; } else { entry.index += 1; - entry.position.add_summary(next_summary, cx); - self.position.add_summary(next_summary, cx); + entry.position.add_summary(next_summary, self.cx); + self.position.add_summary(next_summary, self.cx); } } @@ -327,8 +329,8 @@ where if !descend { let item_summary = &item_summaries[entry.index]; entry.index += 1; - entry.position.add_summary(item_summary, cx); - self.position.add_summary(item_summary, cx); + entry.position.add_summary(item_summary, self.cx); + self.position.add_summary(item_summary, self.cx); } loop { @@ -337,8 +339,8 @@ where return; } else { entry.index += 1; - entry.position.add_summary(next_item_summary, cx); - self.position.add_summary(next_item_summary, cx); + entry.position.add_summary(next_item_summary, self.cx); + self.position.add_summary(next_item_summary, self.cx); } } else { break None; @@ -380,71 +382,51 @@ where D: Dimension<'a, T::Summary>, { #[track_caller] - pub fn seek( - &mut self, - pos: &Target, - bias: Bias, - cx: &::Context, - ) -> bool + pub fn seek(&mut self, pos: &Target, bias: Bias) -> bool where Target: SeekTarget<'a, T::Summary, D>, { - self.reset(cx); - self.seek_internal(pos, bias, &mut (), cx) + self.reset(); + self.seek_internal(pos, bias, &mut ()) } #[track_caller] - pub fn seek_forward( - &mut self, - pos: &Target, - bias: Bias, - cx: &::Context, - ) -> bool + pub fn seek_forward(&mut self, pos: &Target, bias: Bias) -> bool where Target: SeekTarget<'a, T::Summary, D>, { - self.seek_internal(pos, bias, &mut (), cx) + self.seek_internal(pos, bias, &mut ()) } /// Advances the cursor and returns traversed items as a tree. #[track_caller] - pub fn slice( - &mut self, - end: &Target, - bias: Bias, - cx: &::Context, - ) -> SumTree + pub fn slice(&mut self, end: &Target, bias: Bias) -> SumTree where Target: SeekTarget<'a, T::Summary, D>, { let mut slice = SliceSeekAggregate { - tree: SumTree::new(cx), + tree: SumTree::new(self.cx), leaf_items: ArrayVec::new(), leaf_item_summaries: ArrayVec::new(), - leaf_summary: ::zero(cx), + leaf_summary: ::zero(self.cx), }; - self.seek_internal(end, bias, &mut slice, cx); + self.seek_internal(end, bias, &mut slice); slice.tree } #[track_caller] - pub fn suffix(&mut self, cx: &::Context) -> SumTree { - self.slice(&End::new(), Bias::Right, cx) + pub fn suffix(&mut self) -> SumTree { + self.slice(&End::new(), Bias::Right) } #[track_caller] - pub fn summary( - &mut self, - end: &Target, - bias: Bias, - cx: &::Context, - ) -> Output + pub fn summary(&mut self, end: &Target, bias: Bias) -> Output where Target: SeekTarget<'a, T::Summary, D>, Output: Dimension<'a, T::Summary>, { - let mut summary = SummarySeekAggregate(Output::zero(cx)); - self.seek_internal(end, bias, &mut summary, cx); + let mut summary = SummarySeekAggregate(Output::zero(self.cx)); + self.seek_internal(end, bias, &mut summary); summary.0 } @@ -455,10 +437,9 @@ where target: &dyn SeekTarget<'a, T::Summary, D>, bias: Bias, aggregate: &mut dyn SeekAggregate<'a, T>, - cx: &::Context, ) -> bool { assert!( - target.cmp(&self.position, cx) >= Ordering::Equal, + target.cmp(&self.position, self.cx) >= Ordering::Equal, "cannot seek backward", ); @@ -467,7 +448,7 @@ where self.stack.push(StackEntry { tree: self.tree, index: 0, - position: D::zero(cx), + position: D::zero(self.cx), }); } @@ -489,14 +470,14 @@ where .zip(&child_summaries[entry.index..]) { let mut child_end = self.position.clone(); - child_end.add_summary(child_summary, cx); + child_end.add_summary(child_summary, self.cx); - let comparison = target.cmp(&child_end, cx); + let comparison = target.cmp(&child_end, self.cx); if comparison == Ordering::Greater || (comparison == Ordering::Equal && bias == Bias::Right) { self.position = child_end; - aggregate.push_tree(child_tree, child_summary, cx); + aggregate.push_tree(child_tree, child_summary, self.cx); entry.index += 1; entry.position = self.position.clone(); } else { @@ -522,22 +503,22 @@ where .zip(&item_summaries[entry.index..]) { let mut child_end = self.position.clone(); - child_end.add_summary(item_summary, cx); + child_end.add_summary(item_summary, self.cx); - let comparison = target.cmp(&child_end, cx); + let comparison = target.cmp(&child_end, self.cx); if comparison == Ordering::Greater || (comparison == Ordering::Equal && bias == Bias::Right) { self.position = child_end; - aggregate.push_item(item, item_summary, cx); + aggregate.push_item(item, item_summary, self.cx); entry.index += 1; } else { - aggregate.end_leaf(cx); + aggregate.end_leaf(self.cx); break 'outer; } } - aggregate.end_leaf(cx); + aggregate.end_leaf(self.cx); } } @@ -551,11 +532,11 @@ where let mut end = self.position.clone(); if bias == Bias::Left { if let Some(summary) = self.item_summary() { - end.add_summary(summary, cx); + end.add_summary(summary, self.cx); } } - target.cmp(&end, cx) == Ordering::Equal + target.cmp(&end, self.cx) == Ordering::Equal } } @@ -624,21 +605,19 @@ impl<'a, T: Item> Iterator for Iter<'a, T> { } } -impl<'a, T, S, D> Iterator for Cursor<'a, T, D> +impl<'a, T: Item, D> Iterator for Cursor<'a, T, D> where - T: Item

    , - S: Summary, D: Dimension<'a, T::Summary>, { type Item = &'a T; fn next(&mut self) -> Option { if !self.did_seek { - self.next(&()); + self.next(); } if let Some(item) = self.item() { - self.next(&()); + self.next(); Some(item) } else { None @@ -651,7 +630,7 @@ pub struct FilterCursor<'a, F, T: Item, D> { filter_node: F, } -impl<'a, F, T, D> FilterCursor<'a, F, T, D> +impl<'a, F, T: Item, D> FilterCursor<'a, F, T, D> where F: FnMut(&T::Summary) -> bool, T: Item, @@ -659,7 +638,7 @@ where { pub fn new( tree: &'a SumTree, - cx: &::Context, + cx: &'a ::Context, filter_node: F, ) -> Self { let cursor = tree.cursor::(cx); @@ -673,8 +652,8 @@ where self.cursor.start() } - pub fn end(&self, cx: &::Context) -> D { - self.cursor.end(cx) + pub fn end(&self) -> D { + self.cursor.end() } pub fn item(&self) -> Option<&'a T> { @@ -685,31 +664,29 @@ where self.cursor.item_summary() } - pub fn next(&mut self, cx: &::Context) { - self.cursor.search_forward(&mut self.filter_node, cx); + pub fn next(&mut self) { + self.cursor.search_forward(&mut self.filter_node); } - pub fn prev(&mut self, cx: &::Context) { - self.cursor.search_backward(&mut self.filter_node, cx); + pub fn prev(&mut self) { + self.cursor.search_backward(&mut self.filter_node); } } -impl<'a, F, T, S, U> Iterator for FilterCursor<'a, F, T, U> +impl<'a, F, T: Item, U> Iterator for FilterCursor<'a, F, T, U> where F: FnMut(&T::Summary) -> bool, - T: Item, - S: Summary, //Context for the summary must be unit type, as .next() doesn't take arguments U: Dimension<'a, T::Summary>, { type Item = &'a T; fn next(&mut self) -> Option { if !self.cursor.did_seek { - self.next(&()); + self.next(); } if let Some(item) = self.item() { - self.cursor.search_forward(&mut self.filter_node, &()); + self.cursor.search_forward(&mut self.filter_node); Some(item) } else { None @@ -795,3 +772,23 @@ where self.0.add_summary(summary, cx); } } + +struct End(PhantomData); + +impl End { + fn new() -> Self { + Self(PhantomData) + } +} + +impl<'a, S: Summary, D: Dimension<'a, S>> SeekTarget<'a, S, D> for End { + fn cmp(&self, _: &D, _: &S::Context) -> Ordering { + Ordering::Greater + } +} + +impl fmt::Debug for End { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("End").finish() + } +} diff --git a/crates/sum_tree/src/sum_tree.rs b/crates/sum_tree/src/sum_tree.rs index 82022d668554e904fe52f445dfa17dd72b0dd6bf..4f9e01ce201f5d9db9f8fd8be8e76bbb43c6f2bb 100644 --- a/crates/sum_tree/src/sum_tree.rs +++ b/crates/sum_tree/src/sum_tree.rs @@ -38,7 +38,6 @@ pub trait Summary: Clone { type Context; fn zero(cx: &Self::Context) -> Self; - fn add_summary(&mut self, summary: &Self, cx: &Self::Context); } @@ -138,26 +137,6 @@ where } } -struct End(PhantomData); - -impl End { - fn new() -> Self { - Self(PhantomData) - } -} - -impl<'a, S: Summary, D: Dimension<'a, S>> SeekTarget<'a, S, D> for End { - fn cmp(&self, _: &D, _: &S::Context) -> Ordering { - Ordering::Greater - } -} - -impl fmt::Debug for End { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("End").finish() - } -} - /// Bias is used to settle ambiguities when determining positions in an ordered sequence. /// /// The primary use case is for text, where Bias influences @@ -372,10 +351,10 @@ impl SumTree { pub fn items(&self, cx: &::Context) -> Vec { let mut items = Vec::new(); let mut cursor = self.cursor::<()>(cx); - cursor.next(cx); + cursor.next(); while let Some(item) = cursor.item() { items.push(item.clone()); - cursor.next(cx); + cursor.next(); } items } @@ -384,7 +363,7 @@ impl SumTree { Iter::new(self) } - pub fn cursor<'a, S>(&'a self, cx: &::Context) -> Cursor<'a, T, S> + pub fn cursor<'a, S>(&'a self, cx: &'a ::Context) -> Cursor<'a, T, S> where S: Dimension<'a, T::Summary>, { @@ -395,7 +374,7 @@ impl SumTree { /// that is returned cannot be used with Rust's iterators. pub fn filter<'a, F, U>( &'a self, - cx: &::Context, + cx: &'a ::Context, filter_node: F, ) -> FilterCursor<'a, F, T, U> where @@ -525,10 +504,6 @@ impl SumTree { } } - pub fn ptr_eq(&self, other: &Self) -> bool { - Arc::ptr_eq(&self.0, &other.0) - } - fn push_tree_recursive( &mut self, other: SumTree, @@ -686,11 +661,6 @@ impl SumTree { } => child_trees.last().unwrap().rightmost_leaf(), } } - - #[cfg(debug_assertions)] - pub fn _debug_entries(&self) -> Vec<&T> { - self.iter().collect::>() - } } impl PartialEq for SumTree { @@ -710,15 +680,15 @@ impl SumTree { let mut replaced = None; *self = { let mut cursor = self.cursor::(cx); - let mut new_tree = cursor.slice(&item.key(), Bias::Left, 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(cx); + cursor.next(); } } new_tree.push(item, cx); - new_tree.append(cursor.suffix(cx), cx); + new_tree.append(cursor.suffix(), cx); new_tree }; replaced @@ -728,14 +698,14 @@ impl SumTree { let mut removed = None; *self = { let mut cursor = self.cursor::(cx); - let mut new_tree = cursor.slice(key, Bias::Left, 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(cx); + cursor.next(); } } - new_tree.append(cursor.suffix(cx), cx); + new_tree.append(cursor.suffix(), cx); new_tree }; removed @@ -758,7 +728,7 @@ impl SumTree { let mut new_tree = SumTree::new(cx); let mut buffered_items = Vec::new(); - cursor.seek(&T::Key::zero(cx), Bias::Left, cx); + cursor.seek(&T::Key::zero(cx), Bias::Left); for edit in edits { let new_key = edit.key(); let mut old_item = cursor.item(); @@ -768,7 +738,7 @@ impl SumTree { .map_or(false, |old_item| old_item.key() < new_key) { new_tree.extend(buffered_items.drain(..), cx); - let slice = cursor.slice(&new_key, Bias::Left, cx); + let slice = cursor.slice(&new_key, Bias::Left); new_tree.append(slice, cx); old_item = cursor.item(); } @@ -776,7 +746,7 @@ impl SumTree { if let Some(old_item) = old_item { if old_item.key() == new_key { removed.push(old_item.clone()); - cursor.next(cx); + cursor.next(); } } @@ -789,70 +759,25 @@ impl SumTree { } new_tree.extend(buffered_items, cx); - new_tree.append(cursor.suffix(cx), cx); + new_tree.append(cursor.suffix(), cx); new_tree }; removed } - pub fn get(&self, key: &T::Key, cx: &::Context) -> Option<&T> { + pub fn get<'a>( + &'a self, + key: &T::Key, + cx: &'a ::Context, + ) -> Option<&'a T> { let mut cursor = self.cursor::(cx); - if cursor.seek(key, Bias::Left, cx) { + if cursor.seek(key, Bias::Left) { cursor.item() } else { None } } - - #[inline] - pub fn contains(&self, key: &T::Key, cx: &::Context) -> bool { - self.get(key, cx).is_some() - } - - pub fn update( - &mut self, - key: &T::Key, - cx: &::Context, - f: F, - ) -> Option - where - F: FnOnce(&mut T) -> R, - { - let mut cursor = self.cursor::(cx); - let mut new_tree = cursor.slice(key, Bias::Left, cx); - let mut result = None; - if Ord::cmp(key, &cursor.end(cx)) == Ordering::Equal { - let mut updated = cursor.item().unwrap().clone(); - result = Some(f(&mut updated)); - new_tree.push(updated, cx); - cursor.next(cx); - } - new_tree.append(cursor.suffix(cx), cx); - drop(cursor); - *self = new_tree; - result - } - - pub fn retain bool>( - &mut self, - cx: &::Context, - mut predicate: F, - ) { - let mut new_map = SumTree::new(cx); - - let mut cursor = self.cursor::(cx); - cursor.next(cx); - while let Some(item) = cursor.item() { - if predicate(&item) { - new_map.push(item.clone(), cx); - } - cursor.next(cx); - } - drop(cursor); - - *self = new_map; - } } impl Default for SumTree @@ -1061,14 +986,14 @@ mod tests { tree = { let mut cursor = tree.cursor::(&()); - let mut new_tree = cursor.slice(&Count(splice_start), Bias::Right, &()); + let mut new_tree = cursor.slice(&Count(splice_start), Bias::Right); if rng.r#gen() { new_tree.extend(new_items, &()); } else { new_tree.par_extend(new_items, &()); } - cursor.seek(&Count(splice_end), Bias::Right, &()); - new_tree.append(cursor.slice(&tree_end, Bias::Right, &()), &()); + cursor.seek(&Count(splice_end), Bias::Right); + new_tree.append(cursor.slice(&tree_end, Bias::Right), &()); new_tree }; @@ -1090,10 +1015,10 @@ mod tests { .collect::>(); let mut item_ix = if rng.r#gen() { - filter_cursor.next(&()); + filter_cursor.next(); 0 } else { - filter_cursor.prev(&()); + filter_cursor.prev(); expected_filtered_items.len().saturating_sub(1) }; while item_ix < expected_filtered_items.len() { @@ -1103,19 +1028,19 @@ mod tests { assert_eq!(actual_item, &reference_item); assert_eq!(filter_cursor.start().0, reference_index); log::info!("next"); - filter_cursor.next(&()); + filter_cursor.next(); item_ix += 1; while item_ix > 0 && rng.gen_bool(0.2) { log::info!("prev"); - filter_cursor.prev(&()); + filter_cursor.prev(); item_ix -= 1; if item_ix == 0 && rng.gen_bool(0.2) { - filter_cursor.prev(&()); + filter_cursor.prev(); assert_eq!(filter_cursor.item(), None); assert_eq!(filter_cursor.start().0, 0); - filter_cursor.next(&()); + filter_cursor.next(); } } } @@ -1124,9 +1049,9 @@ mod tests { let mut before_start = false; let mut cursor = tree.cursor::(&()); let start_pos = rng.gen_range(0..=reference_items.len()); - cursor.seek(&Count(start_pos), Bias::Right, &()); + cursor.seek(&Count(start_pos), Bias::Right); let mut pos = rng.gen_range(start_pos..=reference_items.len()); - cursor.seek_forward(&Count(pos), Bias::Right, &()); + cursor.seek_forward(&Count(pos), Bias::Right); for i in 0..10 { assert_eq!(cursor.start().0, pos); @@ -1152,13 +1077,13 @@ mod tests { } if i < 5 { - cursor.next(&()); + cursor.next(); if pos < reference_items.len() { pos += 1; before_start = false; } } else { - cursor.prev(&()); + cursor.prev(); if pos == 0 { before_start = true; } @@ -1174,11 +1099,11 @@ mod tests { let end_bias = if rng.r#gen() { Bias::Left } else { Bias::Right }; let mut cursor = tree.cursor::(&()); - cursor.seek(&Count(start), start_bias, &()); - let slice = cursor.slice(&Count(end), end_bias, &()); + cursor.seek(&Count(start), start_bias); + let slice = cursor.slice(&Count(end), end_bias); - cursor.seek(&Count(start), start_bias, &()); - let summary = cursor.summary::<_, Sum>(&Count(end), end_bias, &()); + cursor.seek(&Count(start), start_bias); + let summary = cursor.summary::<_, Sum>(&Count(end), end_bias); assert_eq!(summary.0, slice.summary().sum); } @@ -1191,19 +1116,19 @@ mod tests { let tree = SumTree::::default(); let mut cursor = tree.cursor::(&()); assert_eq!( - cursor.slice(&Count(0), Bias::Right, &()).items(&()), + cursor.slice(&Count(0), Bias::Right).items(&()), Vec::::new() ); assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), None); assert_eq!(cursor.next_item(), None); assert_eq!(cursor.start().sum, 0); - cursor.prev(&()); + cursor.prev(); assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), None); assert_eq!(cursor.next_item(), None); assert_eq!(cursor.start().sum, 0); - cursor.next(&()); + cursor.next(); assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), None); assert_eq!(cursor.next_item(), None); @@ -1214,7 +1139,7 @@ mod tests { tree.extend(vec![1], &()); let mut cursor = tree.cursor::(&()); assert_eq!( - cursor.slice(&Count(0), Bias::Right, &()).items(&()), + cursor.slice(&Count(0), Bias::Right).items(&()), Vec::::new() ); assert_eq!(cursor.item(), Some(&1)); @@ -1222,29 +1147,29 @@ mod tests { assert_eq!(cursor.next_item(), None); assert_eq!(cursor.start().sum, 0); - cursor.next(&()); + cursor.next(); assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), Some(&1)); assert_eq!(cursor.next_item(), None); assert_eq!(cursor.start().sum, 1); - cursor.prev(&()); + cursor.prev(); assert_eq!(cursor.item(), Some(&1)); assert_eq!(cursor.prev_item(), None); assert_eq!(cursor.next_item(), None); assert_eq!(cursor.start().sum, 0); let mut cursor = tree.cursor::(&()); - assert_eq!(cursor.slice(&Count(1), Bias::Right, &()).items(&()), [1]); + assert_eq!(cursor.slice(&Count(1), Bias::Right).items(&()), [1]); assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), Some(&1)); assert_eq!(cursor.next_item(), None); assert_eq!(cursor.start().sum, 1); - cursor.seek(&Count(0), Bias::Right, &()); + cursor.seek(&Count(0), Bias::Right); assert_eq!( cursor - .slice(&tree.extent::(&()), Bias::Right, &()) + .slice(&tree.extent::(&()), Bias::Right) .items(&()), [1] ); @@ -1258,80 +1183,80 @@ mod tests { tree.extend(vec![1, 2, 3, 4, 5, 6], &()); let mut cursor = tree.cursor::(&()); - assert_eq!(cursor.slice(&Count(2), Bias::Right, &()).items(&()), [1, 2]); + assert_eq!(cursor.slice(&Count(2), Bias::Right).items(&()), [1, 2]); assert_eq!(cursor.item(), Some(&3)); assert_eq!(cursor.prev_item(), Some(&2)); assert_eq!(cursor.next_item(), Some(&4)); assert_eq!(cursor.start().sum, 3); - cursor.next(&()); + cursor.next(); assert_eq!(cursor.item(), Some(&4)); assert_eq!(cursor.prev_item(), Some(&3)); assert_eq!(cursor.next_item(), Some(&5)); assert_eq!(cursor.start().sum, 6); - cursor.next(&()); + cursor.next(); assert_eq!(cursor.item(), Some(&5)); assert_eq!(cursor.prev_item(), Some(&4)); assert_eq!(cursor.next_item(), Some(&6)); assert_eq!(cursor.start().sum, 10); - cursor.next(&()); + cursor.next(); assert_eq!(cursor.item(), Some(&6)); assert_eq!(cursor.prev_item(), Some(&5)); assert_eq!(cursor.next_item(), None); assert_eq!(cursor.start().sum, 15); - cursor.next(&()); - cursor.next(&()); + cursor.next(); + cursor.next(); assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), Some(&6)); assert_eq!(cursor.next_item(), None); assert_eq!(cursor.start().sum, 21); - cursor.prev(&()); + cursor.prev(); assert_eq!(cursor.item(), Some(&6)); assert_eq!(cursor.prev_item(), Some(&5)); assert_eq!(cursor.next_item(), None); assert_eq!(cursor.start().sum, 15); - cursor.prev(&()); + cursor.prev(); assert_eq!(cursor.item(), Some(&5)); assert_eq!(cursor.prev_item(), Some(&4)); assert_eq!(cursor.next_item(), Some(&6)); assert_eq!(cursor.start().sum, 10); - cursor.prev(&()); + cursor.prev(); assert_eq!(cursor.item(), Some(&4)); assert_eq!(cursor.prev_item(), Some(&3)); assert_eq!(cursor.next_item(), Some(&5)); assert_eq!(cursor.start().sum, 6); - cursor.prev(&()); + cursor.prev(); assert_eq!(cursor.item(), Some(&3)); assert_eq!(cursor.prev_item(), Some(&2)); assert_eq!(cursor.next_item(), Some(&4)); assert_eq!(cursor.start().sum, 3); - cursor.prev(&()); + cursor.prev(); assert_eq!(cursor.item(), Some(&2)); assert_eq!(cursor.prev_item(), Some(&1)); assert_eq!(cursor.next_item(), Some(&3)); assert_eq!(cursor.start().sum, 1); - cursor.prev(&()); + cursor.prev(); assert_eq!(cursor.item(), Some(&1)); assert_eq!(cursor.prev_item(), None); assert_eq!(cursor.next_item(), Some(&2)); assert_eq!(cursor.start().sum, 0); - cursor.prev(&()); + cursor.prev(); assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), None); assert_eq!(cursor.next_item(), Some(&1)); assert_eq!(cursor.start().sum, 0); - cursor.next(&()); + cursor.next(); assert_eq!(cursor.item(), Some(&1)); assert_eq!(cursor.prev_item(), None); assert_eq!(cursor.next_item(), Some(&2)); @@ -1340,7 +1265,7 @@ mod tests { let mut cursor = tree.cursor::(&()); assert_eq!( cursor - .slice(&tree.extent::(&()), Bias::Right, &()) + .slice(&tree.extent::(&()), Bias::Right) .items(&()), tree.items(&()) ); @@ -1349,10 +1274,10 @@ mod tests { assert_eq!(cursor.next_item(), None); assert_eq!(cursor.start().sum, 21); - cursor.seek(&Count(3), Bias::Right, &()); + cursor.seek(&Count(3), Bias::Right); assert_eq!( cursor - .slice(&tree.extent::(&()), Bias::Right, &()) + .slice(&tree.extent::(&()), Bias::Right) .items(&()), [4, 5, 6] ); @@ -1362,25 +1287,16 @@ mod tests { assert_eq!(cursor.start().sum, 21); // Seeking can bias left or right - cursor.seek(&Count(1), Bias::Left, &()); + cursor.seek(&Count(1), Bias::Left); assert_eq!(cursor.item(), Some(&1)); - cursor.seek(&Count(1), Bias::Right, &()); + cursor.seek(&Count(1), Bias::Right); assert_eq!(cursor.item(), Some(&2)); // Slicing without resetting starts from where the cursor is parked at. - cursor.seek(&Count(1), Bias::Right, &()); - assert_eq!( - cursor.slice(&Count(3), Bias::Right, &()).items(&()), - vec![2, 3] - ); - assert_eq!( - cursor.slice(&Count(6), Bias::Left, &()).items(&()), - vec![4, 5] - ); - assert_eq!( - cursor.slice(&Count(6), Bias::Right, &()).items(&()), - vec![6] - ); + cursor.seek(&Count(1), Bias::Right); + assert_eq!(cursor.slice(&Count(3), Bias::Right).items(&()), vec![2, 3]); + assert_eq!(cursor.slice(&Count(6), Bias::Left).items(&()), vec![4, 5]); + assert_eq!(cursor.slice(&Count(6), Bias::Right).items(&()), vec![6]); } #[test] diff --git a/crates/sum_tree/src/tree_map.rs b/crates/sum_tree/src/tree_map.rs index 884042b722aef0bb84db180ac02fe795a6b8b45e..0397f16182133c77f618d04c0ca32622686c8e25 100644 --- a/crates/sum_tree/src/tree_map.rs +++ b/crates/sum_tree/src/tree_map.rs @@ -54,7 +54,7 @@ impl TreeMap { pub fn get(&self, key: &K) -> Option<&V> { let mut cursor = self.0.cursor::>(&()); - cursor.seek(&MapKeyRef(Some(key)), Bias::Left, &()); + cursor.seek(&MapKeyRef(Some(key)), Bias::Left); if let Some(item) = cursor.item() { if Some(key) == item.key().0.as_ref() { Some(&item.value) @@ -86,12 +86,12 @@ impl TreeMap { let mut removed = None; let mut cursor = self.0.cursor::>(&()); let key = MapKeyRef(Some(key)); - let mut new_tree = cursor.slice(&key, Bias::Left, &()); - if key.cmp(&cursor.end(&()), &()) == Ordering::Equal { + let mut new_tree = cursor.slice(&key, Bias::Left); + if key.cmp(&cursor.end(), &()) == Ordering::Equal { removed = Some(cursor.item().unwrap().value.clone()); - cursor.next(&()); + cursor.next(); } - new_tree.append(cursor.suffix(&()), &()); + new_tree.append(cursor.suffix(), &()); drop(cursor); self.0 = new_tree; removed @@ -101,9 +101,9 @@ impl TreeMap { let start = MapSeekTargetAdaptor(start); let end = MapSeekTargetAdaptor(end); let mut cursor = self.0.cursor::>(&()); - let mut new_tree = cursor.slice(&start, Bias::Left, &()); - cursor.seek(&end, Bias::Left, &()); - new_tree.append(cursor.suffix(&()), &()); + let mut new_tree = cursor.slice(&start, Bias::Left); + cursor.seek(&end, Bias::Left); + new_tree.append(cursor.suffix(), &()); drop(cursor); self.0 = new_tree; } @@ -112,15 +112,15 @@ impl TreeMap { pub fn closest(&self, key: &K) -> Option<(&K, &V)> { let mut cursor = self.0.cursor::>(&()); let key = MapKeyRef(Some(key)); - cursor.seek(&key, Bias::Right, &()); - cursor.prev(&()); + cursor.seek(&key, Bias::Right); + cursor.prev(); cursor.item().map(|item| (&item.key, &item.value)) } pub fn iter_from<'a>(&'a self, from: &K) -> impl Iterator + 'a { let mut cursor = self.0.cursor::>(&()); let from_key = MapKeyRef(Some(from)); - cursor.seek(&from_key, Bias::Left, &()); + cursor.seek(&from_key, Bias::Left); cursor.map(|map_entry| (&map_entry.key, &map_entry.value)) } @@ -131,15 +131,15 @@ impl TreeMap { { let mut cursor = self.0.cursor::>(&()); let key = MapKeyRef(Some(key)); - let mut new_tree = cursor.slice(&key, Bias::Left, &()); + let mut new_tree = cursor.slice(&key, Bias::Left); let mut result = None; - if key.cmp(&cursor.end(&()), &()) == Ordering::Equal { + if key.cmp(&cursor.end(), &()) == Ordering::Equal { let mut updated = cursor.item().unwrap().clone(); result = Some(f(&mut updated.value)); new_tree.push(updated, &()); - cursor.next(&()); + cursor.next(); } - new_tree.append(cursor.suffix(&()), &()); + new_tree.append(cursor.suffix(), &()); drop(cursor); self.0 = new_tree; result @@ -149,12 +149,12 @@ impl TreeMap { let mut new_map = SumTree::>::default(); let mut cursor = self.0.cursor::>(&()); - cursor.next(&()); + cursor.next(); while let Some(item) = cursor.item() { if predicate(&item.key, &item.value) { new_map.push(item.clone(), &()); } - cursor.next(&()); + cursor.next(); } drop(cursor); diff --git a/crates/text/src/anchor.rs b/crates/text/src/anchor.rs index 83a4fc84298f855fc4185a0b8bdc428cfe67856b..5807d3aae01ccd6388ed4b821e056524b07e2189 100644 --- a/crates/text/src/anchor.rs +++ b/crates/text/src/anchor.rs @@ -101,7 +101,7 @@ impl Anchor { } else { let fragment_id = buffer.fragment_id_for_anchor(self); let mut fragment_cursor = buffer.fragments.cursor::<(Option<&Locator>, usize)>(&None); - fragment_cursor.seek(&Some(fragment_id), Bias::Left, &None); + fragment_cursor.seek(&Some(fragment_id), Bias::Left); fragment_cursor .item() .map_or(false, |fragment| fragment.visible) diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index aa9682029efb2fbfd1436561eed281432b325162..c1da0649dafed5f2ce07e29710161b358a0924a0 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -856,14 +856,13 @@ impl Buffer { let mut new_ropes = RopeBuilder::new(self.visible_text.cursor(0), self.deleted_text.cursor(0)); let mut old_fragments = self.fragments.cursor::(&None); - let mut new_fragments = - old_fragments.slice(&edits.peek().unwrap().0.start, Bias::Right, &None); + let mut new_fragments = old_fragments.slice(&edits.peek().unwrap().0.start, Bias::Right); new_ropes.append(new_fragments.summary().text); let mut fragment_start = old_fragments.start().visible; for (range, new_text) in edits { let new_text = LineEnding::normalize_arc(new_text.into()); - let fragment_end = old_fragments.end(&None).visible; + let fragment_end = old_fragments.end().visible; // If the current fragment ends before this range, then jump ahead to the first fragment // that extends past the start of this range, reusing any intervening fragments. @@ -879,10 +878,10 @@ impl Buffer { new_ropes.push_fragment(&suffix, suffix.visible); new_fragments.push(suffix, &None); } - old_fragments.next(&None); + old_fragments.next(); } - let slice = old_fragments.slice(&range.start, Bias::Right, &None); + let slice = old_fragments.slice(&range.start, Bias::Right); new_ropes.append(slice.summary().text); new_fragments.append(slice, &None); fragment_start = old_fragments.start().visible; @@ -935,7 +934,7 @@ impl Buffer { // portions as deleted. while fragment_start < range.end { let fragment = old_fragments.item().unwrap(); - let fragment_end = old_fragments.end(&None).visible; + let fragment_end = old_fragments.end().visible; let mut intersection = fragment.clone(); let intersection_end = cmp::min(range.end, fragment_end); if fragment.visible { @@ -962,7 +961,7 @@ impl Buffer { fragment_start = intersection_end; } if fragment_end <= range.end { - old_fragments.next(&None); + old_fragments.next(); } } @@ -974,7 +973,7 @@ impl Buffer { // If the current fragment has been partially consumed, then consume the rest of it // and advance to the next fragment before slicing. if fragment_start > old_fragments.start().visible { - let fragment_end = old_fragments.end(&None).visible; + let fragment_end = old_fragments.end().visible; if fragment_end > fragment_start { let mut suffix = old_fragments.item().unwrap().clone(); suffix.len = fragment_end - fragment_start; @@ -983,10 +982,10 @@ impl Buffer { new_ropes.push_fragment(&suffix, suffix.visible); new_fragments.push(suffix, &None); } - old_fragments.next(&None); + old_fragments.next(); } - let suffix = old_fragments.suffix(&None); + let suffix = old_fragments.suffix(); new_ropes.append(suffix.summary().text); new_fragments.append(suffix, &None); let (visible_text, deleted_text) = new_ropes.finish(); @@ -1073,16 +1072,13 @@ impl Buffer { 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 new_fragments = old_fragments.slice( - &VersionedFullOffset::Offset(ranges[0].start), - Bias::Left, - &cx, - ); + let mut new_fragments = + old_fragments.slice(&VersionedFullOffset::Offset(ranges[0].start), Bias::Left); new_ropes.append(new_fragments.summary().text); let mut fragment_start = old_fragments.start().0.full_offset(); for (range, new_text) in edits { - let fragment_end = old_fragments.end(&cx).0.full_offset(); + let fragment_end = old_fragments.end().0.full_offset(); // If the current fragment ends before this range, then jump ahead to the first fragment // that extends past the start of this range, reusing any intervening fragments. @@ -1099,18 +1095,18 @@ impl Buffer { new_ropes.push_fragment(&suffix, suffix.visible); new_fragments.push(suffix, &None); } - old_fragments.next(&cx); + old_fragments.next(); } let slice = - old_fragments.slice(&VersionedFullOffset::Offset(range.start), Bias::Left, &cx); + old_fragments.slice(&VersionedFullOffset::Offset(range.start), Bias::Left); new_ropes.append(slice.summary().text); new_fragments.append(slice, &None); fragment_start = old_fragments.start().0.full_offset(); } // If we are at the end of a non-concurrent fragment, advance to the next one. - let fragment_end = old_fragments.end(&cx).0.full_offset(); + let fragment_end = old_fragments.end().0.full_offset(); if fragment_end == range.start && fragment_end > fragment_start { let mut fragment = old_fragments.item().unwrap().clone(); fragment.len = fragment_end.0 - fragment_start.0; @@ -1118,7 +1114,7 @@ impl Buffer { new_insertions.push(InsertionFragment::insert_new(&fragment)); new_ropes.push_fragment(&fragment, fragment.visible); new_fragments.push(fragment, &None); - old_fragments.next(&cx); + old_fragments.next(); fragment_start = old_fragments.start().0.full_offset(); } @@ -1128,7 +1124,7 @@ impl Buffer { if fragment_start == range.start && fragment.timestamp > timestamp { new_ropes.push_fragment(fragment, fragment.visible); new_fragments.push(fragment.clone(), &None); - old_fragments.next(&cx); + old_fragments.next(); debug_assert_eq!(fragment_start, range.start); } else { break; @@ -1184,7 +1180,7 @@ impl Buffer { // portions as deleted. while fragment_start < range.end { let fragment = old_fragments.item().unwrap(); - let fragment_end = old_fragments.end(&cx).0.full_offset(); + let fragment_end = old_fragments.end().0.full_offset(); let mut intersection = fragment.clone(); let intersection_end = cmp::min(range.end, fragment_end); if fragment.was_visible(version, &self.undo_map) { @@ -1213,7 +1209,7 @@ impl Buffer { fragment_start = intersection_end; } if fragment_end <= range.end { - old_fragments.next(&cx); + old_fragments.next(); } } } @@ -1221,7 +1217,7 @@ impl Buffer { // If the current fragment has been partially consumed, then consume the rest of it // and advance to the next fragment before slicing. if fragment_start > old_fragments.start().0.full_offset() { - let fragment_end = old_fragments.end(&cx).0.full_offset(); + let fragment_end = old_fragments.end().0.full_offset(); if fragment_end > fragment_start { let mut suffix = old_fragments.item().unwrap().clone(); suffix.len = fragment_end.0 - fragment_start.0; @@ -1230,10 +1226,10 @@ impl Buffer { new_ropes.push_fragment(&suffix, suffix.visible); new_fragments.push(suffix, &None); } - old_fragments.next(&cx); + old_fragments.next(); } - let suffix = old_fragments.suffix(&cx); + let suffix = old_fragments.suffix(); new_ropes.append(suffix.summary().text); new_fragments.append(suffix, &None); let (visible_text, deleted_text) = new_ropes.finish(); @@ -1282,7 +1278,6 @@ impl Buffer { split_offset: insertion_slice.range.start, }, Bias::Left, - &(), ); } while let Some(item) = insertions_cursor.item() { @@ -1292,7 +1287,7 @@ impl Buffer { break; } fragment_ids.push(&item.fragment_id); - insertions_cursor.next(&()); + insertions_cursor.next(); } } fragment_ids.sort_unstable(); @@ -1309,7 +1304,7 @@ impl Buffer { RopeBuilder::new(self.visible_text.cursor(0), self.deleted_text.cursor(0)); for fragment_id in self.fragment_ids_for_edits(undo.counts.keys()) { - let preceding_fragments = old_fragments.slice(&Some(fragment_id), Bias::Left, &None); + let preceding_fragments = old_fragments.slice(&Some(fragment_id), Bias::Left); new_ropes.append(preceding_fragments.summary().text); new_fragments.append(preceding_fragments, &None); @@ -1336,11 +1331,11 @@ impl Buffer { new_ropes.push_fragment(&fragment, fragment_was_visible); new_fragments.push(fragment, &None); - old_fragments.next(&None); + old_fragments.next(); } } - let suffix = old_fragments.suffix(&None); + let suffix = old_fragments.suffix(); new_ropes.append(suffix.summary().text); new_fragments.append(suffix, &None); @@ -1571,7 +1566,7 @@ impl Buffer { .fragment_ids_for_edits(edit_ids.into_iter()) .into_iter() .filter_map(move |fragment_id| { - cursor.seek_forward(&Some(fragment_id), Bias::Left, &None); + cursor.seek_forward(&Some(fragment_id), Bias::Left); let fragment = cursor.item()?; let start_offset = cursor.start().1; let end_offset = start_offset + if fragment.visible { fragment.len } else { 0 }; @@ -1793,7 +1788,7 @@ impl Buffer { let mut cursor = self.snapshot.fragments.cursor::>(&None); for insertion_fragment in self.snapshot.insertions.cursor::<()>(&()) { - cursor.seek(&Some(&insertion_fragment.fragment_id), Bias::Left, &None); + cursor.seek(&Some(&insertion_fragment.fragment_id), Bias::Left); let fragment = cursor.item().unwrap(); assert_eq!(insertion_fragment.fragment_id, fragment.id); assert_eq!(insertion_fragment.split_offset, fragment.insertion_offset); @@ -1912,7 +1907,7 @@ impl BufferSnapshot { .filter::<_, FragmentTextSummary>(&None, move |summary| { !version.observed_all(&summary.max_version) }); - cursor.next(&None); + cursor.next(); let mut visible_cursor = self.visible_text.cursor(0); let mut deleted_cursor = self.deleted_text.cursor(0); @@ -1925,18 +1920,18 @@ impl BufferSnapshot { if fragment.was_visible(version, &self.undo_map) { if fragment.visible { - let text = visible_cursor.slice(cursor.end(&None).visible); + let text = visible_cursor.slice(cursor.end().visible); rope.append(text); } else { deleted_cursor.seek_forward(cursor.start().deleted); - let text = deleted_cursor.slice(cursor.end(&None).deleted); + let text = deleted_cursor.slice(cursor.end().deleted); rope.append(text); } } else if fragment.visible { - visible_cursor.seek_forward(cursor.end(&None).visible); + visible_cursor.seek_forward(cursor.end().visible); } - cursor.next(&None); + cursor.next(); } if cursor.start().visible > visible_cursor.offset() { @@ -2252,7 +2247,7 @@ impl BufferSnapshot { timestamp: anchor.timestamp, split_offset: anchor.offset, }; - insertion_cursor.seek(&anchor_key, anchor.bias, &()); + insertion_cursor.seek(&anchor_key, anchor.bias); if let Some(insertion) = insertion_cursor.item() { let comparison = sum_tree::KeyedItem::key(insertion).cmp(&anchor_key); if comparison == Ordering::Greater @@ -2260,15 +2255,15 @@ impl BufferSnapshot { && comparison == Ordering::Equal && anchor.offset > 0) { - insertion_cursor.prev(&()); + insertion_cursor.prev(); } } else { - insertion_cursor.prev(&()); + insertion_cursor.prev(); } let insertion = insertion_cursor.item().expect("invalid insertion"); assert_eq!(insertion.timestamp, anchor.timestamp, "invalid insertion"); - fragment_cursor.seek_forward(&Some(&insertion.fragment_id), Bias::Left, &None); + fragment_cursor.seek_forward(&Some(&insertion.fragment_id), Bias::Left); let fragment = fragment_cursor.item().unwrap(); let mut fragment_offset = fragment_cursor.start().1; if fragment.visible { @@ -2299,7 +2294,7 @@ impl BufferSnapshot { split_offset: anchor.offset, }; let mut insertion_cursor = self.insertions.cursor::(&()); - insertion_cursor.seek(&anchor_key, anchor.bias, &()); + insertion_cursor.seek(&anchor_key, anchor.bias); if let Some(insertion) = insertion_cursor.item() { let comparison = sum_tree::KeyedItem::key(insertion).cmp(&anchor_key); if comparison == Ordering::Greater @@ -2307,10 +2302,10 @@ impl BufferSnapshot { && comparison == Ordering::Equal && anchor.offset > 0) { - insertion_cursor.prev(&()); + insertion_cursor.prev(); } } else { - insertion_cursor.prev(&()); + insertion_cursor.prev(); } let Some(insertion) = insertion_cursor @@ -2324,7 +2319,7 @@ impl BufferSnapshot { }; let mut fragment_cursor = self.fragments.cursor::<(Option<&Locator>, usize)>(&None); - fragment_cursor.seek(&Some(&insertion.fragment_id), Bias::Left, &None); + fragment_cursor.seek(&Some(&insertion.fragment_id), Bias::Left); let fragment = fragment_cursor.item().unwrap(); let mut fragment_offset = fragment_cursor.start().1; if fragment.visible { @@ -2345,7 +2340,7 @@ impl BufferSnapshot { split_offset: anchor.offset, }; let mut insertion_cursor = self.insertions.cursor::(&()); - insertion_cursor.seek(&anchor_key, anchor.bias, &()); + insertion_cursor.seek(&anchor_key, anchor.bias); if let Some(insertion) = insertion_cursor.item() { let comparison = sum_tree::KeyedItem::key(insertion).cmp(&anchor_key); if comparison == Ordering::Greater @@ -2353,10 +2348,10 @@ impl BufferSnapshot { && comparison == Ordering::Equal && anchor.offset > 0) { - insertion_cursor.prev(&()); + insertion_cursor.prev(); } } else { - insertion_cursor.prev(&()); + insertion_cursor.prev(); } let Some(insertion) = insertion_cursor.item().filter(|insertion| { @@ -2395,7 +2390,7 @@ impl BufferSnapshot { Anchor::MAX } else { let mut fragment_cursor = self.fragments.cursor::(&None); - fragment_cursor.seek(&offset, bias, &None); + fragment_cursor.seek(&offset, bias); let fragment = fragment_cursor.item().unwrap(); let overshoot = offset - *fragment_cursor.start(); Anchor { @@ -2475,7 +2470,7 @@ impl BufferSnapshot { let mut cursor = self.fragments.filter(&None, move |summary| { !since.observed_all(&summary.max_version) }); - cursor.next(&None); + cursor.next(); Some(cursor) }; let mut cursor = self @@ -2483,7 +2478,7 @@ impl BufferSnapshot { .cursor::<(Option<&Locator>, FragmentTextSummary)>(&None); let start_fragment_id = self.fragment_id_for_anchor(&range.start); - cursor.seek(&Some(start_fragment_id), Bias::Left, &None); + cursor.seek(&Some(start_fragment_id), Bias::Left); let mut visible_start = cursor.start().1.visible; let mut deleted_start = cursor.start().1.deleted; if let Some(fragment) = cursor.item() { @@ -2516,7 +2511,7 @@ impl BufferSnapshot { let mut cursor = self.fragments.filter::<_, usize>(&None, move |summary| { !since.observed_all(&summary.max_version) }); - cursor.next(&None); + cursor.next(); while let Some(fragment) = cursor.item() { if fragment.id > *end_fragment_id { break; @@ -2528,7 +2523,7 @@ impl BufferSnapshot { return true; } } - cursor.next(&None); + cursor.next(); } } false @@ -2539,14 +2534,14 @@ impl BufferSnapshot { let mut cursor = self.fragments.filter::<_, usize>(&None, move |summary| { !since.observed_all(&summary.max_version) }); - cursor.next(&None); + cursor.next(); while let Some(fragment) = cursor.item() { let was_visible = fragment.was_visible(since, &self.undo_map); let is_visible = fragment.visible; if was_visible != is_visible { return true; } - cursor.next(&None); + cursor.next(); } } false @@ -2651,7 +2646,7 @@ impl bool> Iterator for Ed while let Some(fragment) = cursor.item() { if fragment.id < *self.range.start.0 { - cursor.next(&None); + cursor.next(); continue; } else if fragment.id > *self.range.end.0 { break; @@ -2684,7 +2679,7 @@ impl bool> Iterator for Ed }; if !fragment.was_visible(self.since, self.undos) && fragment.visible { - let mut visible_end = cursor.end(&None).visible; + let mut visible_end = cursor.end().visible; if fragment.id == *self.range.end.0 { visible_end = cmp::min( visible_end, @@ -2710,7 +2705,7 @@ impl bool> Iterator for Ed self.new_end = new_end; } else if fragment.was_visible(self.since, self.undos) && !fragment.visible { - let mut deleted_end = cursor.end(&None).deleted; + let mut deleted_end = cursor.end().deleted; if fragment.id == *self.range.end.0 { deleted_end = cmp::min( deleted_end, @@ -2740,7 +2735,7 @@ impl bool> Iterator for Ed self.old_end = old_end; } - cursor.next(&None); + cursor.next(); } pending_edit diff --git a/crates/text/src/undo_map.rs b/crates/text/src/undo_map.rs index ed363cfc6b6d77aa6a7e091acac0b0a76824b61c..6a409189fa8d2a9bd3bc821e37b9923b5ed884dd 100644 --- a/crates/text/src/undo_map.rs +++ b/crates/text/src/undo_map.rs @@ -74,7 +74,6 @@ impl UndoMap { undo_id: Default::default(), }, Bias::Left, - &(), ); let mut undo_count = 0; @@ -99,7 +98,6 @@ impl UndoMap { undo_id: Default::default(), }, Bias::Left, - &(), ); let mut undo_count = 0; diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 8c407fdd3eab5a6b7189f67ff46b8ce76d1a428d..4fc6b91abbf770ab2fc15c0e69da55b84eac4961 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -2454,16 +2454,16 @@ impl Snapshot { self.entries_by_path = { let mut cursor = self.entries_by_path.cursor::(&()); let mut new_entries_by_path = - cursor.slice(&TraversalTarget::path(&removed_entry.path), Bias::Left, &()); + cursor.slice(&TraversalTarget::path(&removed_entry.path), Bias::Left); while let Some(entry) = cursor.item() { if entry.path.starts_with(&removed_entry.path) { self.entries_by_id.remove(&entry.id, &()); - cursor.next(&()); + cursor.next(); } else { break; } } - new_entries_by_path.append(cursor.suffix(&()), &()); + new_entries_by_path.append(cursor.suffix(), &()); new_entries_by_path }; @@ -2576,7 +2576,6 @@ impl Snapshot { include_ignored, }, Bias::Right, - &(), ); Traversal { snapshot: self, @@ -2632,7 +2631,7 @@ impl Snapshot { options: ChildEntriesOptions, ) -> ChildEntriesIter<'a> { let mut cursor = self.entries_by_path.cursor(&()); - cursor.seek(&TraversalTarget::path(parent_path), Bias::Right, &()); + cursor.seek(&TraversalTarget::path(parent_path), Bias::Right); let traversal = Traversal { snapshot: self, cursor, @@ -3056,9 +3055,9 @@ impl BackgroundScannerState { .snapshot .entries_by_path .cursor::(&()); - new_entries = cursor.slice(&TraversalTarget::path(path), Bias::Left, &()); - removed_entries = cursor.slice(&TraversalTarget::successor(path), Bias::Left, &()); - new_entries.append(cursor.suffix(&()), &()); + new_entries = cursor.slice(&TraversalTarget::path(path), Bias::Left); + removed_entries = cursor.slice(&TraversalTarget::successor(path), Bias::Left); + new_entries.append(cursor.suffix(), &()); } self.snapshot.entries_by_path = new_entries; @@ -4925,15 +4924,15 @@ fn build_diff( let mut old_paths = old_snapshot.entries_by_path.cursor::(&()); let mut new_paths = new_snapshot.entries_by_path.cursor::(&()); let mut last_newly_loaded_dir_path = None; - old_paths.next(&()); - new_paths.next(&()); + old_paths.next(); + 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) { - old_paths.seek_forward(&path, Bias::Left, &()); + old_paths.seek_forward(&path, Bias::Left); } if new_paths.item().map_or(false, |e| e.path < path.0) { - new_paths.seek_forward(&path, Bias::Left, &()); + new_paths.seek_forward(&path, Bias::Left); } loop { match (old_paths.item(), new_paths.item()) { @@ -4949,7 +4948,7 @@ fn build_diff( match Ord::cmp(&old_entry.path, &new_entry.path) { Ordering::Less => { changes.push((old_entry.path.clone(), old_entry.id, Removed)); - old_paths.next(&()); + old_paths.next(); } Ordering::Equal => { if phase == EventsReceivedDuringInitialScan { @@ -4975,8 +4974,8 @@ fn build_diff( changes.push((new_entry.path.clone(), new_entry.id, Updated)); } } - old_paths.next(&()); - new_paths.next(&()); + old_paths.next(); + new_paths.next(); } Ordering::Greater => { let is_newly_loaded = phase == InitialScan @@ -4988,13 +4987,13 @@ fn build_diff( new_entry.id, if is_newly_loaded { Loaded } else { Added }, )); - new_paths.next(&()); + new_paths.next(); } } } (Some(old_entry), None) => { changes.push((old_entry.path.clone(), old_entry.id, Removed)); - old_paths.next(&()); + old_paths.next(); } (None, Some(new_entry)) => { let is_newly_loaded = phase == InitialScan @@ -5006,7 +5005,7 @@ fn build_diff( new_entry.id, if is_newly_loaded { Loaded } else { Added }, )); - new_paths.next(&()); + new_paths.next(); } (None, None) => break, } @@ -5255,7 +5254,7 @@ impl<'a> Traversal<'a> { start_path: &Path, ) -> Self { let mut cursor = snapshot.entries_by_path.cursor(&()); - cursor.seek(&TraversalTarget::path(start_path), Bias::Left, &()); + cursor.seek(&TraversalTarget::path(start_path), Bias::Left); let mut traversal = Self { snapshot, cursor, @@ -5282,14 +5281,13 @@ impl<'a> Traversal<'a> { include_ignored: self.include_ignored, }, Bias::Left, - &(), ) } pub fn advance_to_sibling(&mut self) -> bool { while let Some(entry) = self.cursor.item() { self.cursor - .seek_forward(&TraversalTarget::successor(&entry.path), Bias::Left, &()); + .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()) @@ -5307,7 +5305,7 @@ impl<'a> Traversal<'a> { return false; }; self.cursor - .seek(&TraversalTarget::path(parent_path), Bias::Left, &()) + .seek(&TraversalTarget::path(parent_path), Bias::Left) } pub fn entry(&self) -> Option<&'a Entry> { @@ -5326,7 +5324,7 @@ impl<'a> Traversal<'a> { pub fn end_offset(&self) -> usize { self.cursor - .end(&()) + .end() .count(self.include_files, self.include_dirs, self.include_ignored) } } From caa520c4999a06907e3cdc6a81ecb634421f0c6a Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 22 Jul 2025 18:52:17 +0200 Subject: [PATCH 260/658] workspace: Clean up empty panes left over from file opening failures (#34908) Closes #34583 Release Notes: - Fixed empty pane being left after a binary file is dropped into a new pane.s --- crates/workspace/src/pane.rs | 41 ++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index e57b103c61988c4a48f2078cdeb600cc3bd34978..c7a2562a1b3b6e0c7eb4e775ffd648a013bdd20c 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -3239,28 +3239,37 @@ impl Pane { split_direction = None; } - if let Ok(open_task) = workspace.update_in(cx, |workspace, window, cx| { - if let Some(split_direction) = split_direction { - to_pane = workspace.split_pane(to_pane, split_direction, window, cx); - } - workspace.open_paths( - paths, - OpenOptions { - visible: Some(OpenVisible::OnlyDirectories), - ..Default::default() - }, - Some(to_pane.downgrade()), - window, - cx, - ) - }) { + if let Ok((open_task, to_pane)) = + workspace.update_in(cx, |workspace, window, cx| { + if let Some(split_direction) = split_direction { + to_pane = + workspace.split_pane(to_pane, split_direction, window, cx); + } + ( + workspace.open_paths( + paths, + OpenOptions { + visible: Some(OpenVisible::OnlyDirectories), + ..Default::default() + }, + Some(to_pane.downgrade()), + window, + cx, + ), + to_pane, + ) + }) + { let opened_items: Vec<_> = open_task.await; - _ = workspace.update(cx, |workspace, cx| { + _ = workspace.update_in(cx, |workspace, window, cx| { for item in opened_items.into_iter().flatten() { if let Err(e) = item { workspace.show_error(&e, cx); } } + if to_pane.read(cx).items_len() == 0 { + workspace.remove_pane(to_pane, None, window, cx); + } }); } }) From 14cea06f0fb591129df0faac2163dcaf74444a83 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Tue, 22 Jul 2025 12:18:59 -0500 Subject: [PATCH 261/658] keymap_ui: Fix panic in clear keystrokes (#34909) Closes #ISSUE Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/settings_ui/src/keybindings.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 5f940e8a25cc784a5ad0bd529e213c2e0b69a03d..9e885f69f6efde4ff7636b1d682a19c515a09bc6 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -2784,6 +2784,7 @@ impl KeystrokeInput { else { log::trace!("No keybinding to stop recording keystrokes in keystroke input"); self.close_keystrokes.take(); + self.close_keystrokes_start.take(); return CloseKeystrokeResult::None; }; let action_keystrokes = keybind_for_close_action.keystrokes(); @@ -2976,7 +2977,9 @@ impl KeystrokeInput { return; } window.focus(&self.outer_focus_handle); - if let Some(close_keystrokes_start) = self.close_keystrokes_start.take() { + if let Some(close_keystrokes_start) = self.close_keystrokes_start.take() + && close_keystrokes_start < self.keystrokes.len() + { self.keystrokes.drain(close_keystrokes_start..); } self.close_keystrokes.take(); From d81a8178e916f92c3277098f1f199b05b0d20bde Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Tue, 22 Jul 2025 11:35:58 -0600 Subject: [PATCH 262/658] Bind "j k" to `NormalBefore` in initial keymap examples (#34912) It looks like typically vim configurations bind "j k" to be the same as escape, which has the "NormalBefore" behavior positioning the block cursor on the character before the insertion cursor. The [vim mode docs](https://zed.dev/docs/vim#useful-contexts-for-vim-mode-key-bindings) also use NormalBefore here. Thanks to @omniwrench for mentioning this in https://github.com/zed-industries/zed/discussions/6661#discussioncomment-13848043 . This was a mistake in #31163. Release Notes: - N/A --- assets/keymaps/initial.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/keymaps/initial.json b/assets/keymaps/initial.json index 0cfd28f0e5d458e0bbffdbbce6cd3b53168ece57..ff6069a81671e421b766763d90649385058efc58 100644 --- a/assets/keymaps/initial.json +++ b/assets/keymaps/initial.json @@ -15,7 +15,7 @@ { "context": "Editor && vim_mode == insert && !menu", "bindings": { - // "j k": "vim::SwitchToNormalMode" + // "j k": "vim::NormalBefore" } } ] From 9e280d09057ec1d8481f64b48b24869d325a5574 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 22 Jul 2025 13:42:07 -0400 Subject: [PATCH 263/658] collab: Remove unneeded caching of Stripe price IDs by meter ID (#34915) This PR removes the caching of Stripe price IDs by meter ID on the `StripeBilling` object, as we weren't actually reading them anywhere. Release Notes: - N/A --- crates/collab/src/stripe_billing.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/crates/collab/src/stripe_billing.rs b/crates/collab/src/stripe_billing.rs index 707928d5cd8952cda154531fd9c4d2e21d661f0e..50accf9557201a586079014a3a7dbcfc452f2905 100644 --- a/crates/collab/src/stripe_billing.rs +++ b/crates/collab/src/stripe_billing.rs @@ -30,7 +30,6 @@ pub struct StripeBilling { #[derive(Default)] struct StripeBillingState { - price_ids_by_meter_id: HashMap, prices_by_lookup_key: HashMap, } @@ -63,13 +62,7 @@ impl StripeBilling { for price in prices { if let Some(lookup_key) = price.lookup_key.clone() { - state.prices_by_lookup_key.insert(lookup_key, price.clone()); - } - - if let Some(recurring) = price.recurring { - if let Some(meter) = recurring.meter { - state.price_ids_by_meter_id.insert(meter, price.id); - } + state.prices_by_lookup_key.insert(lookup_key, price); } } From 99466f4aebbf20dc00077dcce55f3b4ce1d2b1d3 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Tue, 22 Jul 2025 13:57:36 -0400 Subject: [PATCH 264/658] Make zooming from menus not persist (#34910) Closes: https://github.com/zed-industries/zed/issues/34479 Follow-up to: https://github.com/zed-industries/zed/issues/23505 View->Zoom In / Zoom Out / Reset Zoom were not reverted to match when the default keybindings were reverted. Release Notes: - N/A --- crates/zed/src/zed/app_menus.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/zed/src/zed/app_menus.rs b/crates/zed/src/zed/app_menus.rs index c4131dbee9a6ed1c4723630ad60a55bedd2fa365..78532b10b48fcb7bff17588050c1927a7904cee8 100644 --- a/crates/zed/src/zed/app_menus.rs +++ b/crates/zed/src/zed/app_menus.rs @@ -145,15 +145,15 @@ pub fn app_menus() -> Vec { items: vec![ MenuItem::action( "Zoom In", - zed_actions::IncreaseBufferFontSize { persist: true }, + zed_actions::IncreaseBufferFontSize { persist: false }, ), MenuItem::action( "Zoom Out", - zed_actions::DecreaseBufferFontSize { persist: true }, + zed_actions::DecreaseBufferFontSize { persist: false }, ), MenuItem::action( "Reset Zoom", - zed_actions::ResetBufferFontSize { persist: true }, + zed_actions::ResetBufferFontSize { persist: false }, ), MenuItem::separator(), MenuItem::action("Toggle Left Dock", workspace::ToggleLeftDock), From 4272c1508e22a96a5822b422ee5a0b3dd1621512 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 22 Jul 2025 15:41:12 -0300 Subject: [PATCH 265/658] ai onboarding: Copyedit the whole flow (#34916) Release Notes: - N/A Co-authored-by: Katie Geer --- .../src/agent_api_keys_onboarding.rs | 4 +- crates/ai_onboarding/src/ai_onboarding.rs | 41 ++++++++----------- .../ai_onboarding/src/young_account_banner.rs | 2 +- 3 files changed, 19 insertions(+), 28 deletions(-) diff --git a/crates/ai_onboarding/src/agent_api_keys_onboarding.rs b/crates/ai_onboarding/src/agent_api_keys_onboarding.rs index 883317e5666bb07cc17471cd64764a859ba0ec6b..5f56e4d26e83daa4af92a58cf957a199d4839ff2 100644 --- a/crates/ai_onboarding/src/agent_api_keys_onboarding.rs +++ b/crates/ai_onboarding/src/agent_api_keys_onboarding.rs @@ -93,7 +93,7 @@ impl Render for ApiKeysWithProviders { div() .w_full() .child( - Label::new("Or start now using API keys from your environment for the following providers:") + Label::new("Start now using API keys from your environment for the following providers:") .color(Color::Muted) ) ) @@ -129,7 +129,7 @@ impl RenderOnce for ApiKeysWithoutProviders { .child(Divider::horizontal()), ) .child(List::new().child(BulletItem::new( - "You can also use AI in Zed by bringing your own API keys", + "Add your own keys to use AI without signing in.", ))) .child( Button::new("configure-providers", "Configure Providers") diff --git a/crates/ai_onboarding/src/ai_onboarding.rs b/crates/ai_onboarding/src/ai_onboarding.rs index 88c962c1ba5271c8bf713af72a7aa2492d10c84d..e8ce22ff4e51bf0348796b9bac4e8cc37b836b5f 100644 --- a/crates/ai_onboarding/src/ai_onboarding.rs +++ b/crates/ai_onboarding/src/ai_onboarding.rs @@ -141,22 +141,18 @@ impl ZedAiOnboarding { ) .child( List::new() + .child(BulletItem::new("50 prompts per month with Claude models")) .child(BulletItem::new( - "50 prompts per month with the Claude models", - )) - .child(BulletItem::new( - "2000 accepted edit predictions using our open-source Zeta model", + "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( - "150 prompts per month with the Claude models", - )) - .child(BulletItem::new( - "Unlimited accepted edit predictions using our open-source Zeta model", + "Unlimited accepted edit predictions with Zeta, our open-source model", )) } @@ -178,12 +174,12 @@ impl ZedAiOnboarding { List::new() .child(BulletItem::new("500 prompts per month with Claude models")) .child(BulletItem::new( - "Unlimited accepted edit predictions using our open-source Zeta model", + "Unlimited accepted edit predictions with Zeta, our open-source model", )) - .child(BulletItem::new("USD $20 per month")), + .child(BulletItem::new("$20 USD per month")), ) .child( - Button::new("pro", "Start with Pro") + Button::new("pro", "Get Started") .full_width() .style(ButtonStyle::Tinted(ui::TintColor::Accent)) .on_click(move |_, _window, cx| { @@ -206,11 +202,11 @@ impl ZedAiOnboarding { List::new() .child(self.pro_trial_definition()) .child(BulletItem::new( - "Try it out for 14 days with no charge and no credit card required", + "Try it out for 14 days for free, no credit card required", )), ) .child( - Button::new("pro", "Start Pro Trial") + Button::new("pro", "Start Free Trial") .full_width() .style(ButtonStyle::Tinted(ui::TintColor::Accent)) .on_click(move |_, _window, cx| { @@ -225,14 +221,14 @@ impl ZedAiOnboarding { v_flex() .gap_1() .w_full() - .child(Headline::new("Before starting…")) + .child(Headline::new("Accept Terms of Service")) .child( - Label::new("Make sure you have read and accepted Zed AI's terms of service.") + 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", "View and Read the Terms of Service") + Button::new("terms_of_service", "Review Terms of Service") .full_width() .style(ButtonStyle::Outlined) .icon(IconName::ArrowUpRight) @@ -241,7 +237,7 @@ impl ZedAiOnboarding { .on_click(move |_, _window, cx| cx.open_url(&zed_urls::terms_of_service(cx))), ) .child( - Button::new("accept_terms", "I've read it and accept it") + Button::new("accept_terms", "Accept") .full_width() .style(ButtonStyle::Tinted(TintColor::Accent)) .on_click({ @@ -259,13 +255,13 @@ impl ZedAiOnboarding { .gap_1() .child(Headline::new("Welcome to Zed AI")) .child( - Label::new("Sign in to start using AI in Zed with a free trial of the Pro plan, which includes:") + Label::new("Sign in to try Zed Pro for 14 days, no credit card required.") .color(Color::Muted) .mb_2(), ) .child(self.pro_trial_definition()) .child( - Button::new("sign_in", "Sign in to Start Trial") + Button::new("sign_in", "Try Zed Pro for Free") .disabled(signing_in) .full_width() .style(ButtonStyle::Tinted(ui::TintColor::Accent)) @@ -284,11 +280,6 @@ impl ZedAiOnboarding { .relative() .gap_1() .child(Headline::new("Welcome to Zed AI")) - .child( - Label::new("Choose how you want to start.") - .color(Color::Muted) - .mb_2(), - ) .map(|this| { if self.account_too_young { this.child(young_account_banner) @@ -318,7 +309,7 @@ impl ZedAiOnboarding { v_flex() .relative() .gap_1() - .child(Headline::new("Welcome to the Zed Pro free trial")) + .child(Headline::new("Welcome to the Zed Pro Trial")) .child( Label::new("Here's what you get for the next 14 days:") .color(Color::Muted) diff --git a/crates/ai_onboarding/src/young_account_banner.rs b/crates/ai_onboarding/src/young_account_banner.rs index 1e1ed3a8653d0cb39955fb54b10dd1dc3937ceb3..a43625a60eecf88724bfcf151edc89a8a29d370b 100644 --- a/crates/ai_onboarding/src/young_account_banner.rs +++ b/crates/ai_onboarding/src/young_account_banner.rs @@ -6,7 +6,7 @@ pub struct YoungAccountBanner; impl RenderOnce for YoungAccountBanner { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - const YOUNG_ACCOUNT_DISCLAIMER: &str = "To prevent abuse of our service, we cannot offer plans to GitHub accounts created fewer than 30 days ago. To request an exception, reach out to billing@zed.dev."; + const YOUNG_ACCOUNT_DISCLAIMER: &str = "To prevent abuse of our service, we cannot offer plans to GitHub accounts created fewer than 30 days ago. To request an exception, reach out to billing-support@zed.dev."; let label = div() .w_full() From 708c2645d104e39d01122ada2ebc0771d4f1623b Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 22 Jul 2025 20:53:57 +0200 Subject: [PATCH 266/658] collab: Tweak screen selector appearance (#34919) Co-authored-by: Danilo Leal Release Notes: - N/A Co-authored-by: Danilo Leal --- crates/git_ui/src/git_ui.rs | 2 +- crates/title_bar/src/collab.rs | 21 +++++++--- .../ui/src/components/button/split_button.rs | 40 ++++++++++++++----- crates/ui/src/components/list/list.rs | 4 +- 4 files changed, 48 insertions(+), 19 deletions(-) diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index a9ccaf716074783b2bf3a5e4d969c0702320557e..02b9c243fb22e0eda12c1cbd0f1f9bb61c677b26 100644 --- a/crates/git_ui/src/git_ui.rs +++ b/crates/git_ui/src/git_ui.rs @@ -501,7 +501,7 @@ mod remote_button { ) .into_any_element(); - SplitButton { left, right } + SplitButton::new(left, right) } } diff --git a/crates/title_bar/src/collab.rs b/crates/title_bar/src/collab.rs index 1eebc0de0c7012231933619a432263ef911a8b7e..056c981ccf16a1e56fcaff99c32b4a284553a706 100644 --- a/crates/title_bar/src/collab.rs +++ b/crates/title_bar/src/collab.rs @@ -11,8 +11,8 @@ use gpui::{App, Task, Window, actions}; use rpc::proto::{self}; use theme::ActiveTheme; use ui::{ - Avatar, AvatarAudioStatusIndicator, ContextMenu, ContextMenuItem, Facepile, PopoverMenu, - SplitButton, TintColor, Tooltip, prelude::*, + Avatar, AvatarAudioStatusIndicator, ContextMenu, ContextMenuItem, Divider, Facepile, + PopoverMenu, SplitButton, SplitButtonStyle, TintColor, Tooltip, prelude::*, }; use util::maybe; use workspace::notifications::DetachAndPromptErr; @@ -383,6 +383,7 @@ impl TitleBar { .detach_and_log_err(cx); }), ) + .child(Divider::vertical()) .into_any_element(), ); @@ -497,6 +498,7 @@ impl TitleBar { trigger.render(window, cx), self.render_screen_list().into_any_element(), ) + .style(SplitButtonStyle::Outlined) .into_any_element(), ); } @@ -547,10 +549,17 @@ impl TitleBar { entry_render: Box::new(move |_, _| { h_flex() .gap_2() - .child(Icon::new(IconName::Screen).when( - active_screenshare_id == Some(meta.id), - |this| this.color(Color::Accent), - )) + .child( + Icon::new(IconName::Screen) + .size(IconSize::XSmall) + .map(|this| { + if active_screenshare_id == Some(meta.id) { + this.color(Color::Accent) + } else { + this.color(Color::Muted) + } + }), + ) .child(Label::new(label.clone())) .child( Label::new(resolution.clone()) diff --git a/crates/ui/src/components/button/split_button.rs b/crates/ui/src/components/button/split_button.rs index c0811ecbab9f3897328edd25c8fdd6bd85ffabbc..a7fa2106d127662c4f1498e41372780065649e56 100644 --- a/crates/ui/src/components/button/split_button.rs +++ b/crates/ui/src/components/button/split_button.rs @@ -1,6 +1,6 @@ use gpui::{ AnyElement, App, BoxShadow, IntoElement, ParentElement, RenderOnce, Styled, Window, div, hsla, - point, px, + point, prelude::FluentBuilder, px, }; use theme::ActiveTheme; @@ -8,6 +8,12 @@ use crate::{ElevationIndex, h_flex}; use super::ButtonLike; +#[derive(Clone, Copy, PartialEq)] +pub enum SplitButtonStyle { + Filled, + Outlined, +} + /// /// A button with two parts: a primary action on the left and a secondary action on the right. /// /// The left side is a [`ButtonLike`] with the main action, while the right side can contain @@ -18,11 +24,21 @@ use super::ButtonLike; pub struct SplitButton { pub left: ButtonLike, pub right: AnyElement, + style: SplitButtonStyle, } impl SplitButton { pub fn new(left: ButtonLike, right: AnyElement) -> Self { - Self { left, right } + Self { + left, + right, + style: SplitButtonStyle::Filled, + } + } + + pub fn style(mut self, style: SplitButtonStyle) -> Self { + self.style = style; + self } } @@ -31,21 +47,23 @@ impl RenderOnce for SplitButton { h_flex() .rounded_sm() .border_1() - .border_color(cx.theme().colors().text_muted.alpha(0.12)) + .border_color(cx.theme().colors().border.opacity(0.5)) .child(div().flex_grow().child(self.left)) .child( div() .h_full() .w_px() - .bg(cx.theme().colors().text_muted.alpha(0.16)), + .bg(cx.theme().colors().border.opacity(0.5)), ) .child(self.right) - .bg(ElevationIndex::Surface.on_elevation_bg(cx)) - .shadow(vec![BoxShadow { - color: hsla(0.0, 0.0, 0.0, 0.16), - offset: point(px(0.), px(1.)), - blur_radius: px(0.), - spread_radius: px(0.), - }]) + .when(self.style == SplitButtonStyle::Filled, |this| { + this.bg(ElevationIndex::Surface.on_elevation_bg(cx)) + .shadow(vec![BoxShadow { + color: hsla(0.0, 0.0, 0.0, 0.16), + offset: point(px(0.), px(1.)), + blur_radius: px(0.), + spread_radius: px(0.), + }]) + }) } } diff --git a/crates/ui/src/components/list/list.rs b/crates/ui/src/components/list/list.rs index 1402b5d3d3328f0b5344bb4d81c8bc8413a0dac0..b6950f06a4449265cccd48f9f13590650619a01c 100644 --- a/crates/ui/src/components/list/list.rs +++ b/crates/ui/src/components/list/list.rs @@ -84,7 +84,9 @@ impl RenderOnce for List { (false, _) => this.children(self.children), (true, Some(false)) => this, (true, _) => match self.empty_message { - EmptyMessage::Text(text) => this.child(Label::new(text).color(Color::Muted)), + EmptyMessage::Text(text) => { + this.px_2().child(Label::new(text).color(Color::Muted)) + } EmptyMessage::Element(element) => this.child(element), }, }) From c0f75e1a175f0bf7a0931ecad9fb9e803034a3b0 Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 22 Jul 2025 21:39:52 +0200 Subject: [PATCH 267/658] debugger: Fix built-in JavaScript debug tasks were not working due missing `type` field value (#34894) Release Notes: - Debugger: Fix built-in JavaScript debug tasks were not working due missing `type` field value --- assets/settings/initial_debug_tasks.json | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/assets/settings/initial_debug_tasks.json b/assets/settings/initial_debug_tasks.json index 78fc1fc5f02a03bc83c93a4cf5cc7c517fd301c7..af4512bd51aa82d57ce62e605b45ee61e8f98030 100644 --- a/assets/settings/initial_debug_tasks.json +++ b/assets/settings/initial_debug_tasks.json @@ -15,13 +15,15 @@ "adapter": "JavaScript", "program": "$ZED_FILE", "request": "launch", - "cwd": "$ZED_WORKTREE_ROOT" + "cwd": "$ZED_WORKTREE_ROOT", + "type": "pwa-node" }, { "label": "JavaScript debug terminal", "adapter": "JavaScript", "request": "launch", "cwd": "$ZED_WORKTREE_ROOT", - "console": "integratedTerminal" + "console": "integratedTerminal", + "type": "pwa-node" } ] From 446d333515fc6d6fca0a8c5ef3e929506c49396f Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Tue, 22 Jul 2025 21:40:11 +0200 Subject: [PATCH 268/658] debugger: Fix debug console persist to history when reusing a previous item (#34893) Closes #34887 Release Notes: - Debugger: Fix debug console persist to history when reusing a previous item --- crates/project/src/search_history.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/project/src/search_history.rs b/crates/project/src/search_history.rs index b84c2e09820196e68ce013b1de453d4c0c283391..90b169bb0c5c83a9eb722c964bfb549ead1d5494 100644 --- a/crates/project/src/search_history.rs +++ b/crates/project/src/search_history.rs @@ -45,12 +45,6 @@ impl SearchHistory { } pub fn add(&mut self, cursor: &mut SearchHistoryCursor, search_string: String) { - if let Some(selected_ix) = cursor.selection { - if self.history.get(selected_ix) == Some(&search_string) { - return; - } - } - if self.insertion_behavior == QueryInsertionBehavior::ReplacePreviousIfContains { if let Some(previously_searched) = self.history.back_mut() { if search_string.contains(previously_searched.as_str()) { @@ -144,6 +138,14 @@ mod tests { ); assert_eq!(search_history.current(&cursor), Some("rustlang")); + // add item when it equals to current item if it's not the last one + search_history.add(&mut cursor, "php".to_string()); + search_history.previous(&mut cursor); + assert_eq!(search_history.current(&cursor), Some("rustlang")); + search_history.add(&mut cursor, "rustlang".to_string()); + assert_eq!(search_history.history.len(), 3, "Should add item"); + assert_eq!(search_history.current(&cursor), Some("rustlang")); + // push enough items to test SEARCH_HISTORY_LIMIT for i in 0..MAX_HISTORY_LEN * 2 { search_history.add(&mut cursor, format!("item{i}")); From f3c332d839a2fa4d3b8ba6906a497171f5de9a74 Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Wed, 23 Jul 2025 01:16:25 +0530 Subject: [PATCH 269/658] Fix new crate license symlink (#34922) The license file is not properly linked to actual license. This was casued due to new-crate script linking the license to wrong file. Fixed both of them. Reference logs: ``` 2025-07-22T17:16:19+05:30 ERROR [worktree] error reading target of symlink "/Users/umesh/code/zed/crates/onboarding/LICENSE-GPL": canonicalizing ``` Release Notes: - N/A --- crates/onboarding/LICENSE-GPL | 2 +- script/new-crate | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/onboarding/LICENSE-GPL b/crates/onboarding/LICENSE-GPL index dd648cce4f61e4a93871ac05a9f381e485f80319..89e542f750cd3860a0598eff0dc34b56d7336dc4 120000 --- a/crates/onboarding/LICENSE-GPL +++ b/crates/onboarding/LICENSE-GPL @@ -1 +1 @@ -../../../LICENSE-GPL \ No newline at end of file +../../LICENSE-GPL \ No newline at end of file diff --git a/script/new-crate b/script/new-crate index df574981e739a465f3f4f92d8a05c8df7cffdb82..52ee900b30837cbf77fa1e3145e0282fa5e19b7c 100755 --- a/script/new-crate +++ b/script/new-crate @@ -39,7 +39,7 @@ CRATE_PATH="crates/$CRATE_NAME" mkdir -p "$CRATE_PATH/src" # Symlink the license -ln -sf "../../../$LICENSE_FILE" "$CRATE_PATH/$LICENSE_FILE" +ln -sf "../../$LICENSE_FILE" "$CRATE_PATH/$LICENSE_FILE" CARGO_TOML_TEMPLATE=$(cat << 'EOF' [package] From 7f70325a93b63d47cc08ce9e040ba0dcb27f306c Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 22 Jul 2025 16:04:08 -0400 Subject: [PATCH 270/658] language_models: Rename `handler` to `handle` in Bedrock provider (#34923) This PR renames the `handler` field to `handle` on the `BedrockLanguageModelProvider` and `BedrockModel` structs. Release Notes: - N/A --- crates/language_models/src/provider/bedrock.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/language_models/src/provider/bedrock.rs b/crates/language_models/src/provider/bedrock.rs index a022511b11e84719043b4d436a60669590afc625..a86b3e78f54f85f0fe396cd02c9e9b71975a9acd 100644 --- a/crates/language_models/src/provider/bedrock.rs +++ b/crates/language_models/src/provider/bedrock.rs @@ -243,7 +243,7 @@ impl State { pub struct BedrockLanguageModelProvider { http_client: AwsHttpClient, - handler: tokio::runtime::Handle, + handle: tokio::runtime::Handle, state: gpui::Entity, } @@ -260,7 +260,7 @@ impl BedrockLanguageModelProvider { Self { http_client: AwsHttpClient::new(http_client.clone()), - handler: Tokio::handle(cx), + handle: Tokio::handle(cx), state, } } @@ -270,7 +270,7 @@ impl BedrockLanguageModelProvider { id: LanguageModelId::from(model.id().to_string()), model, http_client: self.http_client.clone(), - handler: self.handler.clone(), + handle: self.handle.clone(), state: self.state.clone(), client: OnceCell::new(), request_limiter: RateLimiter::new(4), @@ -371,7 +371,7 @@ struct BedrockModel { id: LanguageModelId, model: Model, http_client: AwsHttpClient, - handler: tokio::runtime::Handle, + handle: tokio::runtime::Handle, client: OnceCell, state: gpui::Entity, request_limiter: RateLimiter, @@ -443,7 +443,7 @@ impl BedrockModel { } } - let config = self.handler.block_on(config_builder.load()); + let config = self.handle.block_on(config_builder.load()); anyhow::Ok(BedrockClient::new(&config)) }) .context("initializing Bedrock client")?; From 5d985fa1d8684aa85e1bbde944c4f1b42591fedd Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Tue, 22 Jul 2025 19:14:34 -0300 Subject: [PATCH 271/658] Improve MCP server responses (#34927) Release Notes: - N/A --- crates/agent_servers/src/claude/mcp_server.rs | 26 ++++++++----------- crates/agent_servers/src/claude/tools.rs | 10 ------- 2 files changed, 11 insertions(+), 25 deletions(-) diff --git a/crates/agent_servers/src/claude/mcp_server.rs b/crates/agent_servers/src/claude/mcp_server.rs index 468027c4c3dd4c3391dc8196e54a840ce01a965b..2405603550db376453b85261ca31b5791e464d09 100644 --- a/crates/agent_servers/src/claude/mcp_server.rs +++ b/crates/agent_servers/src/claude/mcp_server.rs @@ -19,7 +19,7 @@ use util::debug_panic; use crate::claude::{ McpServerConfig, - tools::{ClaudeTool, EditToolParams, EditToolResponse, ReadToolParams, ReadToolResponse}, + tools::{ClaudeTool, EditToolParams, ReadToolParams}, }; pub struct ClaudeMcpServer { @@ -179,11 +179,9 @@ impl ClaudeMcpServer { let input = serde_json::from_value(request.arguments.context("Arguments required")?)?; - let result = Self::handle_read_tool_call(input, delegate, cx).await?; + let content = Self::handle_read_tool_call(input, delegate, cx).await?; Ok(CallToolResponse { - content: vec![ToolResponseContent::Text { - text: serde_json::to_string(&result)?, - }], + content, is_error: None, meta: None, }) @@ -191,11 +189,9 @@ impl ClaudeMcpServer { let input = serde_json::from_value(request.arguments.context("Arguments required")?)?; - let result = Self::handle_edit_tool_call(input, delegate, cx).await?; + Self::handle_edit_tool_call(input, delegate, cx).await?; Ok(CallToolResponse { - content: vec![ToolResponseContent::Text { - text: serde_json::to_string(&result)?, - }], + content: vec![], is_error: None, meta: None, }) @@ -209,7 +205,7 @@ impl ClaudeMcpServer { params: ReadToolParams, delegate: AcpClientDelegate, cx: &AsyncApp, - ) -> Task> { + ) -> Task>> { cx.foreground_executor().spawn(async move { let response = delegate .read_text_file(ReadTextFileParams { @@ -219,9 +215,9 @@ impl ClaudeMcpServer { }) .await?; - Ok(ReadToolResponse { - content: response.content, - }) + Ok(vec![ToolResponseContent::Text { + text: response.content, + }]) }) } @@ -229,7 +225,7 @@ impl ClaudeMcpServer { params: EditToolParams, delegate: AcpClientDelegate, cx: &AsyncApp, - ) -> Task> { + ) -> Task> { cx.foreground_executor().spawn(async move { let response = delegate .read_text_file_reusing_snapshot(ReadTextFileParams { @@ -251,7 +247,7 @@ impl ClaudeMcpServer { }) .await?; - Ok(EditToolResponse) + Ok(()) }) } diff --git a/crates/agent_servers/src/claude/tools.rs b/crates/agent_servers/src/claude/tools.rs index 9c82139a07f38d9f78df8fa4719ecba420cd8838..75a26ee23096da3c03b25ae7335337e455e9d6f7 100644 --- a/crates/agent_servers/src/claude/tools.rs +++ b/crates/agent_servers/src/claude/tools.rs @@ -434,10 +434,6 @@ pub struct EditToolParams { pub new_text: String, } -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -pub struct EditToolResponse; - #[derive(Deserialize, JsonSchema, Debug)] pub struct ReadToolParams { /// The absolute path to the file to read. @@ -450,12 +446,6 @@ pub struct ReadToolParams { pub limit: Option, } -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -pub struct ReadToolResponse { - pub content: String, -} - #[derive(Deserialize, JsonSchema, Debug)] pub struct WriteToolParams { /// Absolute path for new file From 056003860af3894327b8685caf705cc4f873ba85 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 22 Jul 2025 18:57:07 -0400 Subject: [PATCH 272/658] collab: Remove `POST /billing/subscriptions` endpoint (#34928) This PR removes the `POST /billing/subscriptions` endpoint, as it has been moved to `cloud.zed.dev`. Release Notes: - N/A --- crates/collab/src/api/billing.rs | 119 +-------- crates/collab/src/stripe_billing.rs | 104 +------- .../collab/src/tests/stripe_billing_tests.rs | 249 +----------------- 3 files changed, 9 insertions(+), 463 deletions(-) diff --git a/crates/collab/src/api/billing.rs b/crates/collab/src/api/billing.rs index 9aa6578b2a54cb8806d819c32e0112dd397bdd23..d6e42ad2fb9a66fc742a93b7bd34d73d47c8dcbe 100644 --- a/crates/collab/src/api/billing.rs +++ b/crates/collab/src/api/billing.rs @@ -25,7 +25,7 @@ use crate::llm::db::subscription_usage_meter::{self, CompletionMode}; use crate::rpc::{ResultExt as _, Server}; use crate::stripe_client::{ StripeCancellationDetailsReason, StripeClient, StripeCustomerId, StripeSubscription, - StripeSubscriptionId, UpdateCustomerParams, + StripeSubscriptionId, }; use crate::{AppState, Error, Result}; use crate::{db::UserId, llm::db::LlmDatabase}; @@ -40,7 +40,6 @@ use crate::{ pub fn router() -> Router { Router::new() - .route("/billing/subscriptions", post(create_billing_subscription)) .route( "/billing/subscriptions/manage", post(manage_billing_subscription), @@ -51,122 +50,6 @@ pub fn router() -> Router { ) } -#[derive(Debug, PartialEq, Clone, Copy, Deserialize)] -#[serde(rename_all = "snake_case")] -enum ProductCode { - ZedPro, - ZedProTrial, -} - -#[derive(Debug, Deserialize)] -struct CreateBillingSubscriptionBody { - github_user_id: i32, - product: ProductCode, -} - -#[derive(Debug, Serialize)] -struct CreateBillingSubscriptionResponse { - checkout_session_url: String, -} - -/// Initiates a Stripe Checkout session for creating a billing subscription. -async fn create_billing_subscription( - Extension(app): Extension>, - extract::Json(body): extract::Json, -) -> Result> { - let user = app - .db - .get_user_by_github_user_id(body.github_user_id) - .await? - .context("user not found")?; - - let Some(stripe_billing) = app.stripe_billing.clone() else { - log::error!("failed to retrieve Stripe billing object"); - Err(Error::http( - StatusCode::NOT_IMPLEMENTED, - "not supported".into(), - ))? - }; - - if let Some(existing_subscription) = app.db.get_active_billing_subscription(user.id).await? { - let is_checkout_allowed = body.product == ProductCode::ZedProTrial - && existing_subscription.kind == Some(SubscriptionKind::ZedFree); - - if !is_checkout_allowed { - return Err(Error::http( - StatusCode::CONFLICT, - "user already has an active subscription".into(), - )); - } - } - - let existing_billing_customer = app.db.get_billing_customer_by_user_id(user.id).await?; - if let Some(existing_billing_customer) = &existing_billing_customer { - if existing_billing_customer.has_overdue_invoices { - return Err(Error::http( - StatusCode::PAYMENT_REQUIRED, - "user has overdue invoices".into(), - )); - } - } - - let customer_id = if let Some(existing_customer) = &existing_billing_customer { - let customer_id = StripeCustomerId(existing_customer.stripe_customer_id.clone().into()); - if let Some(email) = user.email_address.as_deref() { - stripe_billing - .client() - .update_customer(&customer_id, UpdateCustomerParams { email: Some(email) }) - .await - // Update of email address is best-effort - continue checkout even if it fails - .context("error updating stripe customer email address") - .log_err(); - } - customer_id - } else { - stripe_billing - .find_or_create_customer_by_email(user.email_address.as_deref()) - .await? - }; - - let success_url = format!( - "{}/account?checkout_complete=1", - app.config.zed_dot_dev_url() - ); - - let checkout_session_url = match body.product { - ProductCode::ZedPro => { - stripe_billing - .checkout_with_zed_pro(&customer_id, &user.github_login, &success_url) - .await? - } - ProductCode::ZedProTrial => { - if let Some(existing_billing_customer) = &existing_billing_customer { - if existing_billing_customer.trial_started_at.is_some() { - return Err(Error::http( - StatusCode::FORBIDDEN, - "user already used free trial".into(), - )); - } - } - - let feature_flags = app.db.get_user_flags(user.id).await?; - - stripe_billing - .checkout_with_zed_pro_trial( - &customer_id, - &user.github_login, - feature_flags, - &success_url, - ) - .await? - } - }; - - Ok(Json(CreateBillingSubscriptionResponse { - checkout_session_url, - })) -} - #[derive(Debug, PartialEq, Deserialize)] #[serde(rename_all = "snake_case")] enum ManageSubscriptionIntent { diff --git a/crates/collab/src/stripe_billing.rs b/crates/collab/src/stripe_billing.rs index 50accf9557201a586079014a3a7dbcfc452f2905..850b716a9fa19aa1efa2fe0ba27e82a63db2acb8 100644 --- a/crates/collab/src/stripe_billing.rs +++ b/crates/collab/src/stripe_billing.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use anyhow::{Context as _, anyhow}; +use anyhow::anyhow; use chrono::Utc; use collections::HashMap; use stripe::SubscriptionStatus; @@ -9,18 +9,13 @@ use uuid::Uuid; use crate::Result; use crate::db::billing_subscription::SubscriptionKind; -use crate::llm::AGENT_EXTENDED_TRIAL_FEATURE_FLAG; use crate::stripe_client::{ - RealStripeClient, StripeAutomaticTax, StripeBillingAddressCollection, - StripeCheckoutSessionMode, StripeCheckoutSessionPaymentMethodCollection, StripeClient, - StripeCreateCheckoutSessionLineItems, StripeCreateCheckoutSessionParams, - StripeCreateCheckoutSessionSubscriptionData, StripeCreateMeterEventParams, + RealStripeClient, StripeAutomaticTax, StripeClient, StripeCreateMeterEventParams, StripeCreateMeterEventPayload, StripeCreateSubscriptionItems, StripeCreateSubscriptionParams, - StripeCustomerId, StripeCustomerUpdate, StripeCustomerUpdateAddress, StripeCustomerUpdateName, - StripePrice, StripePriceId, StripeSubscription, StripeSubscriptionId, + StripeCustomerId, StripePrice, StripePriceId, StripeSubscription, StripeSubscriptionId, StripeSubscriptionTrialSettings, StripeSubscriptionTrialSettingsEndBehavior, - StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, StripeTaxIdCollection, - UpdateSubscriptionItems, UpdateSubscriptionParams, + StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, UpdateSubscriptionItems, + UpdateSubscriptionParams, }; pub struct StripeBilling { @@ -214,95 +209,6 @@ impl StripeBilling { Ok(()) } - pub async fn checkout_with_zed_pro( - &self, - customer_id: &StripeCustomerId, - github_login: &str, - success_url: &str, - ) -> Result { - let zed_pro_price_id = self.zed_pro_price_id().await?; - - let mut params = StripeCreateCheckoutSessionParams::default(); - params.mode = Some(StripeCheckoutSessionMode::Subscription); - params.customer = Some(customer_id); - params.client_reference_id = Some(github_login); - params.line_items = Some(vec![StripeCreateCheckoutSessionLineItems { - price: Some(zed_pro_price_id.to_string()), - quantity: Some(1), - }]); - params.success_url = Some(success_url); - params.billing_address_collection = Some(StripeBillingAddressCollection::Required); - params.customer_update = Some(StripeCustomerUpdate { - address: Some(StripeCustomerUpdateAddress::Auto), - name: Some(StripeCustomerUpdateName::Auto), - shipping: None, - }); - params.tax_id_collection = Some(StripeTaxIdCollection { enabled: true }); - - let session = self.client.create_checkout_session(params).await?; - Ok(session.url.context("no checkout session URL")?) - } - - pub async fn checkout_with_zed_pro_trial( - &self, - customer_id: &StripeCustomerId, - github_login: &str, - feature_flags: Vec, - success_url: &str, - ) -> Result { - let zed_pro_price_id = self.zed_pro_price_id().await?; - - let eligible_for_extended_trial = feature_flags - .iter() - .any(|flag| flag == AGENT_EXTENDED_TRIAL_FEATURE_FLAG); - - let trial_period_days = if eligible_for_extended_trial { 60 } else { 14 }; - - let mut subscription_metadata = std::collections::HashMap::new(); - if eligible_for_extended_trial { - subscription_metadata.insert( - "promo_feature_flag".to_string(), - AGENT_EXTENDED_TRIAL_FEATURE_FLAG.to_string(), - ); - } - - let mut params = StripeCreateCheckoutSessionParams::default(); - params.subscription_data = Some(StripeCreateCheckoutSessionSubscriptionData { - trial_period_days: Some(trial_period_days), - trial_settings: Some(StripeSubscriptionTrialSettings { - end_behavior: StripeSubscriptionTrialSettingsEndBehavior { - missing_payment_method: - StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Cancel, - }, - }), - metadata: if !subscription_metadata.is_empty() { - Some(subscription_metadata) - } else { - None - }, - }); - params.mode = Some(StripeCheckoutSessionMode::Subscription); - params.payment_method_collection = - Some(StripeCheckoutSessionPaymentMethodCollection::IfRequired); - params.customer = Some(customer_id); - params.client_reference_id = Some(github_login); - params.line_items = Some(vec![StripeCreateCheckoutSessionLineItems { - price: Some(zed_pro_price_id.to_string()), - quantity: Some(1), - }]); - params.success_url = Some(success_url); - params.billing_address_collection = Some(StripeBillingAddressCollection::Required); - params.customer_update = Some(StripeCustomerUpdate { - address: Some(StripeCustomerUpdateAddress::Auto), - name: Some(StripeCustomerUpdateName::Auto), - shipping: None, - }); - params.tax_id_collection = Some(StripeTaxIdCollection { enabled: true }); - - let session = self.client.create_checkout_session(params).await?; - Ok(session.url.context("no checkout session URL")?) - } - pub async fn subscribe_to_zed_free( &self, customer_id: StripeCustomerId, diff --git a/crates/collab/src/tests/stripe_billing_tests.rs b/crates/collab/src/tests/stripe_billing_tests.rs index c19eb0a23432fb835b99007b0ebca2e4a5a8f2e6..5c5bcd58328b2aa6540cd2f6a31e65ff1355e48c 100644 --- a/crates/collab/src/tests/stripe_billing_tests.rs +++ b/crates/collab/src/tests/stripe_billing_tests.rs @@ -3,17 +3,11 @@ use std::sync::Arc; use chrono::{Duration, Utc}; use pretty_assertions::assert_eq; -use crate::llm::AGENT_EXTENDED_TRIAL_FEATURE_FLAG; use crate::stripe_billing::StripeBilling; use crate::stripe_client::{ - FakeStripeClient, StripeBillingAddressCollection, StripeCheckoutSessionMode, - StripeCheckoutSessionPaymentMethodCollection, StripeCreateCheckoutSessionLineItems, - StripeCreateCheckoutSessionSubscriptionData, StripeCustomerId, StripeCustomerUpdate, - StripeCustomerUpdateAddress, StripeCustomerUpdateName, StripeMeter, StripeMeterId, StripePrice, - StripePriceId, StripePriceRecurring, StripeSubscription, StripeSubscriptionId, - StripeSubscriptionItem, StripeSubscriptionItemId, StripeSubscriptionTrialSettings, - StripeSubscriptionTrialSettingsEndBehavior, - StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod, UpdateSubscriptionItems, + FakeStripeClient, StripeCustomerId, StripeMeter, StripeMeterId, StripePrice, StripePriceId, + StripePriceRecurring, StripeSubscription, StripeSubscriptionId, StripeSubscriptionItem, + StripeSubscriptionItemId, UpdateSubscriptionItems, }; fn make_stripe_billing() -> (StripeBilling, Arc) { @@ -364,240 +358,3 @@ async fn test_bill_model_request_usage() { ); assert_eq!(create_meter_event_calls[0].value, 73); } - -#[gpui::test] -async fn test_checkout_with_zed_pro() { - let (stripe_billing, stripe_client) = make_stripe_billing(); - - let customer_id = StripeCustomerId("cus_test".into()); - let github_login = "zeduser1"; - let success_url = "https://example.com/success"; - - // It returns an error when the Zed Pro price doesn't exist. - { - let result = stripe_billing - .checkout_with_zed_pro(&customer_id, github_login, success_url) - .await; - - assert!(result.is_err()); - assert_eq!( - result.err().unwrap().to_string(), - r#"no price ID found for "zed-pro""# - ); - } - - // Successful checkout. - { - let price = StripePrice { - id: StripePriceId("price_1".into()), - unit_amount: Some(2000), - lookup_key: Some("zed-pro".to_string()), - recurring: None, - }; - stripe_client - .prices - .lock() - .insert(price.id.clone(), price.clone()); - - stripe_billing.initialize().await.unwrap(); - - let checkout_url = stripe_billing - .checkout_with_zed_pro(&customer_id, github_login, success_url) - .await - .unwrap(); - - assert!(checkout_url.starts_with("https://checkout.stripe.com/c/pay")); - - let create_checkout_session_calls = stripe_client - .create_checkout_session_calls - .lock() - .drain(..) - .collect::>(); - assert_eq!(create_checkout_session_calls.len(), 1); - let call = create_checkout_session_calls.into_iter().next().unwrap(); - assert_eq!(call.customer, Some(customer_id)); - assert_eq!(call.client_reference_id.as_deref(), Some(github_login)); - assert_eq!(call.mode, Some(StripeCheckoutSessionMode::Subscription)); - assert_eq!( - call.line_items, - Some(vec![StripeCreateCheckoutSessionLineItems { - price: Some(price.id.to_string()), - quantity: Some(1) - }]) - ); - assert_eq!(call.payment_method_collection, None); - assert_eq!(call.subscription_data, None); - assert_eq!(call.success_url.as_deref(), Some(success_url)); - assert_eq!( - call.billing_address_collection, - Some(StripeBillingAddressCollection::Required) - ); - assert_eq!( - call.customer_update, - Some(StripeCustomerUpdate { - address: Some(StripeCustomerUpdateAddress::Auto), - name: Some(StripeCustomerUpdateName::Auto), - shipping: None, - }) - ); - } -} - -#[gpui::test] -async fn test_checkout_with_zed_pro_trial() { - let (stripe_billing, stripe_client) = make_stripe_billing(); - - let customer_id = StripeCustomerId("cus_test".into()); - let github_login = "zeduser1"; - let success_url = "https://example.com/success"; - - // It returns an error when the Zed Pro price doesn't exist. - { - let result = stripe_billing - .checkout_with_zed_pro_trial(&customer_id, github_login, Vec::new(), success_url) - .await; - - assert!(result.is_err()); - assert_eq!( - result.err().unwrap().to_string(), - r#"no price ID found for "zed-pro""# - ); - } - - let price = StripePrice { - id: StripePriceId("price_1".into()), - unit_amount: Some(2000), - lookup_key: Some("zed-pro".to_string()), - recurring: None, - }; - stripe_client - .prices - .lock() - .insert(price.id.clone(), price.clone()); - - stripe_billing.initialize().await.unwrap(); - - // Successful checkout. - { - let checkout_url = stripe_billing - .checkout_with_zed_pro_trial(&customer_id, github_login, Vec::new(), success_url) - .await - .unwrap(); - - assert!(checkout_url.starts_with("https://checkout.stripe.com/c/pay")); - - let create_checkout_session_calls = stripe_client - .create_checkout_session_calls - .lock() - .drain(..) - .collect::>(); - assert_eq!(create_checkout_session_calls.len(), 1); - let call = create_checkout_session_calls.into_iter().next().unwrap(); - assert_eq!(call.customer.as_ref(), Some(&customer_id)); - assert_eq!(call.client_reference_id.as_deref(), Some(github_login)); - assert_eq!(call.mode, Some(StripeCheckoutSessionMode::Subscription)); - assert_eq!( - call.line_items, - Some(vec![StripeCreateCheckoutSessionLineItems { - price: Some(price.id.to_string()), - quantity: Some(1) - }]) - ); - assert_eq!( - call.payment_method_collection, - Some(StripeCheckoutSessionPaymentMethodCollection::IfRequired) - ); - assert_eq!( - call.subscription_data, - Some(StripeCreateCheckoutSessionSubscriptionData { - trial_period_days: Some(14), - trial_settings: Some(StripeSubscriptionTrialSettings { - end_behavior: StripeSubscriptionTrialSettingsEndBehavior { - missing_payment_method: - StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Cancel, - }, - }), - metadata: None, - }) - ); - assert_eq!(call.success_url.as_deref(), Some(success_url)); - assert_eq!( - call.billing_address_collection, - Some(StripeBillingAddressCollection::Required) - ); - assert_eq!( - call.customer_update, - Some(StripeCustomerUpdate { - address: Some(StripeCustomerUpdateAddress::Auto), - name: Some(StripeCustomerUpdateName::Auto), - shipping: None, - }) - ); - } - - // Successful checkout with extended trial. - { - let checkout_url = stripe_billing - .checkout_with_zed_pro_trial( - &customer_id, - github_login, - vec![AGENT_EXTENDED_TRIAL_FEATURE_FLAG.to_string()], - success_url, - ) - .await - .unwrap(); - - assert!(checkout_url.starts_with("https://checkout.stripe.com/c/pay")); - - let create_checkout_session_calls = stripe_client - .create_checkout_session_calls - .lock() - .drain(..) - .collect::>(); - assert_eq!(create_checkout_session_calls.len(), 1); - let call = create_checkout_session_calls.into_iter().next().unwrap(); - assert_eq!(call.customer, Some(customer_id)); - assert_eq!(call.client_reference_id.as_deref(), Some(github_login)); - assert_eq!(call.mode, Some(StripeCheckoutSessionMode::Subscription)); - assert_eq!( - call.line_items, - Some(vec![StripeCreateCheckoutSessionLineItems { - price: Some(price.id.to_string()), - quantity: Some(1) - }]) - ); - assert_eq!( - call.payment_method_collection, - Some(StripeCheckoutSessionPaymentMethodCollection::IfRequired) - ); - assert_eq!( - call.subscription_data, - Some(StripeCreateCheckoutSessionSubscriptionData { - trial_period_days: Some(60), - trial_settings: Some(StripeSubscriptionTrialSettings { - end_behavior: StripeSubscriptionTrialSettingsEndBehavior { - missing_payment_method: - StripeSubscriptionTrialSettingsEndBehaviorMissingPaymentMethod::Cancel, - }, - }), - metadata: Some(std::collections::HashMap::from_iter([( - "promo_feature_flag".into(), - AGENT_EXTENDED_TRIAL_FEATURE_FLAG.into() - )])), - }) - ); - assert_eq!(call.success_url.as_deref(), Some(success_url)); - assert_eq!( - call.billing_address_collection, - Some(StripeBillingAddressCollection::Required) - ); - assert_eq!( - call.customer_update, - Some(StripeCustomerUpdate { - address: Some(StripeCustomerUpdateAddress::Auto), - name: Some(StripeCustomerUpdateName::Auto), - shipping: None, - }) - ); - } -} From e90cf0b9412b7f8fcb557d1edb2c3f586c7bd45b Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Wed, 23 Jul 2025 06:46:24 +0530 Subject: [PATCH 273/658] workspace: Fix last removed folder from workspace used to reopen on Zed startup (#34925) Closes #34924 Now, when `local_paths` are empty, we detach `session_id` from that workspace serialization item. This way, when we restore it using the default "last_session", we don't restore this workspace back. This is same as when we use `cmd-w` to close window, which also sets `session_id` to `None` before serialization. Release Notes: - Fixed an issue where last removed folder from workspace used to reopen on Zed startup. --- crates/workspace/src/persistence.rs | 8 +++ crates/workspace/src/workspace.rs | 87 ++++++++++++++++++----------- 2 files changed, 61 insertions(+), 34 deletions(-) diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 406f37419d1b02d14817ad165d4fa5cdd4c6d452..3f8b098203e4fcf82e740cffc4e0de06d170a34e 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -1336,6 +1336,14 @@ impl WorkspaceDb { } } + query! { + pub(crate) async fn set_session_id(workspace_id: WorkspaceId, session_id: Option) -> Result<()> { + UPDATE workspaces + SET session_id = ?2 + WHERE workspace_id = ?1 + } + } + pub async fn toolchain( &self, workspace_id: WorkspaceId, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index f37abe59e24b64cc8a7e4f699afb1e3cc569a42f..2c223c476bcdf6872d24dd5f596b88c511c7a5b0 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1016,6 +1016,15 @@ pub enum OpenVisible { OnlyDirectories, } +enum WorkspaceLocation { + // Valid local paths or SSH project to serialize + Location(SerializedWorkspaceLocation), + // No valid location found hence clear session id + DetachFromSession, + // No valid location found to serialize + None, +} + type PromptForNewPath = Box< dyn Fn( &mut Workspace, @@ -1135,7 +1144,6 @@ impl Workspace { this.update_window_title(window, cx); this.serialize_workspace(window, cx); // This event could be triggered by `AddFolderToProject` or `RemoveFromProject`. - // So we need to update the history. this.update_history(cx); } @@ -5218,48 +5226,58 @@ impl Workspace { } } - if let Some(location) = self.serialize_workspace_location(cx) { - let breakpoints = self.project.update(cx, |project, cx| { - project - .breakpoint_store() - .read(cx) - .all_source_breakpoints(cx) - }); + match self.serialize_workspace_location(cx) { + WorkspaceLocation::Location(location) => { + let breakpoints = self.project.update(cx, |project, cx| { + project + .breakpoint_store() + .read(cx) + .all_source_breakpoints(cx) + }); - let center_group = build_serialized_pane_group(&self.center.root, window, cx); - let docks = build_serialized_docks(self, window, cx); - let window_bounds = Some(SerializedWindowBounds(window.window_bounds())); - let serialized_workspace = SerializedWorkspace { - id: database_id, - location, - center_group, - window_bounds, - display: Default::default(), - docks, - centered_layout: self.centered_layout, - session_id: self.session_id.clone(), - breakpoints, - window_id: Some(window.window_handle().window_id().as_u64()), - }; + let center_group = build_serialized_pane_group(&self.center.root, window, cx); + let docks = build_serialized_docks(self, window, cx); + let window_bounds = Some(SerializedWindowBounds(window.window_bounds())); + let serialized_workspace = SerializedWorkspace { + id: database_id, + location, + center_group, + window_bounds, + display: Default::default(), + docks, + centered_layout: self.centered_layout, + session_id: self.session_id.clone(), + breakpoints, + window_id: Some(window.window_handle().window_id().as_u64()), + }; - return window.spawn(cx, async move |_| { - persistence::DB.save_workspace(serialized_workspace).await; - }); + window.spawn(cx, async move |_| { + persistence::DB.save_workspace(serialized_workspace).await; + }) + } + WorkspaceLocation::DetachFromSession => window.spawn(cx, async move |_| { + persistence::DB + .set_session_id(database_id, None) + .await + .log_err(); + }), + WorkspaceLocation::None => Task::ready(()), } - Task::ready(()) } - fn serialize_workspace_location(&self, cx: &App) -> Option { + fn serialize_workspace_location(&self, cx: &App) -> WorkspaceLocation { if let Some(ssh_project) = &self.serialized_ssh_project { - Some(SerializedWorkspaceLocation::Ssh(ssh_project.clone())) + WorkspaceLocation::Location(SerializedWorkspaceLocation::Ssh(ssh_project.clone())) } else if let Some(local_paths) = self.local_paths(cx) { if !local_paths.is_empty() { - Some(SerializedWorkspaceLocation::from_local_paths(local_paths)) + WorkspaceLocation::Location(SerializedWorkspaceLocation::from_local_paths( + local_paths, + )) } else { - None + WorkspaceLocation::DetachFromSession } } else { - None + WorkspaceLocation::None } } @@ -5267,8 +5285,9 @@ impl Workspace { let Some(id) = self.database_id() else { return; }; - let Some(location) = self.serialize_workspace_location(cx) else { - return; + let location = match self.serialize_workspace_location(cx) { + WorkspaceLocation::Location(location) => location, + _ => return, }; if let Some(manager) = HistoryManager::global(cx) { manager.update(cx, |this, cx| { From 11ac83f3d46b2f979a3a13918ea9912f73d21501 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Wed, 23 Jul 2025 06:48:31 +0530 Subject: [PATCH 274/658] workspace: Fix closing remote workspace restores last local workspace on startup (#34931) Closes #7759 While opening a new SSH project if we are reusing an existing window, i.e. drop the existing workspace and create a new one, then before dropping it, we should remove `session_id` from that workspace's serialized entry. That way: 1. Upon closing (cmd-w) this remote workspace (which also clears `session_id` for this), and then quitting. No workspace with that `session_id` is found, and it starts fresh. 2. Upon directly quitting (cmd-q) this remote workspace, only this workspace exists in db (among two of them) with that `session_id`, and it restores correctly. Release Notes: - Fixed an issue while closing remote workspace restores last local workspace on startup. --- crates/workspace/src/workspace.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 2c223c476bcdf6872d24dd5f596b88c511c7a5b0..4c70c52d5a18bc529498d1b3ac09a3e328208575 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -7378,6 +7378,17 @@ async fn open_ssh_project_inner( return Err(project_path_errors.pop().context("no paths given")?); } + if let Some(detach_session_task) = window + .update(cx, |_workspace, window, cx| { + cx.spawn_in(window, async move |this, cx| { + this.update_in(cx, |this, window, cx| this.remove_from_session(window, cx)) + }) + }) + .ok() + { + detach_session_task.await.ok(); + } + cx.update_window(window.into(), |_, window, cx| { window.replace_root(cx, |window, cx| { telemetry::event!("SSH Project Opened"); From 3e27fa1d92d084a3374691a83bd3ab5766b14ad8 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Wed, 23 Jul 2025 10:10:40 +0800 Subject: [PATCH 275/658] gpui: Allow Animation to be cloned (#34933) Release Notes: - N/A --- Let `Animation` able to clone, then we can define one to share for multiple places. For example: image --- crates/gpui/src/elements/animation.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/gpui/src/elements/animation.rs b/crates/gpui/src/elements/animation.rs index bcdfa3562c747999dde96498e046ce7bd4629ac2..11dd19e260c20e49b87e05137771be73a3f816ea 100644 --- a/crates/gpui/src/elements/animation.rs +++ b/crates/gpui/src/elements/animation.rs @@ -1,4 +1,7 @@ -use std::time::{Duration, Instant}; +use std::{ + rc::Rc, + time::{Duration, Instant}, +}; use crate::{ AnyElement, App, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, Window, @@ -8,6 +11,7 @@ pub use easing::*; use smallvec::SmallVec; /// An animation that can be applied to an element. +#[derive(Clone)] pub struct Animation { /// The amount of time for which this animation should run pub duration: Duration, @@ -15,7 +19,7 @@ pub struct Animation { pub oneshot: bool, /// A function that takes a delta between 0 and 1 and returns a new delta /// between 0 and 1 based on the given easing function. - pub easing: Box f32>, + pub easing: Rc f32>, } impl Animation { @@ -25,7 +29,7 @@ impl Animation { Self { duration, oneshot: true, - easing: Box::new(linear), + easing: Rc::new(linear), } } @@ -39,7 +43,7 @@ impl Animation { /// The easing function will take a time delta between 0 and 1 and return a new delta /// between 0 and 1 pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self { - self.easing = Box::new(easing); + self.easing = Rc::new(easing); self } } From 500ceaabcde9d78b3a1a0d14674523e497dbc6e4 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 22 Jul 2025 22:39:32 -0400 Subject: [PATCH 276/658] Add an `editor: diff clipboard with selection` action (#33283) https://github.com/user-attachments/assets/d472fbdd-7736-4bd7-8a90-8cca356b2815 This PR adds `editor: diff clipboard with selection` - good for spotting the differences in eerily-similar code, which is when refactoring code, as you need to see what needs to be passed in in order to maintain previous behavior of both snippets. 1. Copy some text from anywhere 2. Highlight some text in Zed 3. Run `editor: diff clipboard with selection` Like JetBrains' IDEs and VS Code with the `PartialDiff` package, if the selection is empty, we take the entire buffer as the selection. Caveats: - We do not know the language of the text in the clipboard. I went ahead and just assumed that in most cases, it will be the same language as the selected text, which does mean we will highlight the old text incorrectly if they are copying from a different language, but I think in most cases, it will be the same, and the alternative of always having no syntax highlighting is worse. PyCharm seems to do the same thing. Release Notes: - Added an `editor: diff clipboard with selection` action --------- Co-authored-by: Junkui Zhang <364772080@qq.com> Co-authored-by: Ben Kunkle --- crates/editor/src/actions.rs | 9 + crates/editor/src/editor.rs | 36 ++ crates/editor/src/element.rs | 1 + .../src/{diff_view.rs => file_diff_view.rs} | 106 ++-- crates/git_ui/src/git_ui.rs | 15 +- crates/git_ui/src/text_diff_view.rs | 554 ++++++++++++++++++ crates/gpui/src/app/entity_map.rs | 2 +- crates/zed/src/zed/open_listener.rs | 4 +- 8 files changed, 671 insertions(+), 56 deletions(-) rename crates/git_ui/src/{diff_view.rs => file_diff_view.rs} (89%) create mode 100644 crates/git_ui/src/text_diff_view.rs diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index 8557b57f4602e35174cc711aa62e2bebf8c8a140..f80a6afbbb00e6b7bf8d3a58ab9dbfd91d090e86 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -259,6 +259,13 @@ pub struct SpawnNearestTask { pub reveal: task::RevealStrategy, } +#[derive(Clone, PartialEq, Action)] +#[action(no_json, no_register)] +pub struct DiffClipboardWithSelectionData { + pub clipboard_text: String, + pub editor: Entity, +} + #[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Default)] pub enum UuidVersion { #[default] @@ -398,6 +405,8 @@ actions!( DeleteToNextSubwordEnd, /// Deletes to the start of the previous subword. DeleteToPreviousSubwordStart, + /// Diffs the text stored in the clipboard against the current selection. + DiffClipboardWithSelection, /// Displays names of all active cursors. DisplayCursorNames, /// Duplicates the current line below. diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1f985eeb7c225be0778cf13de925276583063e16..a31f789fb03e3220d706a7b13afd5d7d2e7132c3 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -213,6 +213,7 @@ use workspace::{ notifications::{DetachAndPromptErr, NotificationId, NotifyTaskExt}, searchable::SearchEvent, }; +use zed_actions; use crate::{ code_context_menus::CompletionsMenuSource, @@ -12154,6 +12155,41 @@ impl Editor { }); } + pub fn diff_clipboard_with_selection( + &mut self, + _: &DiffClipboardWithSelection, + window: &mut Window, + cx: &mut Context, + ) { + let selections = self.selections.all::(cx); + + if selections.is_empty() { + log::warn!("There should always be at least one selection in Zed. This is a bug."); + return; + }; + + let clipboard_text = match cx.read_from_clipboard() { + Some(item) => match item.entries().first() { + Some(ClipboardEntry::String(text)) => Some(text.text().to_string()), + _ => None, + }, + None => None, + }; + + let Some(clipboard_text) = clipboard_text else { + log::warn!("Clipboard doesn't contain text."); + return; + }; + + window.dispatch_action( + Box::new(DiffClipboardWithSelectionData { + clipboard_text, + editor: cx.entity(), + }), + cx, + ); + } + pub fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context) { self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); if let Some(item) = cx.read_from_clipboard() { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index cbff544c7e2e4159ae290e37b3fbe8b631696184..1b372a7d5378d2d41e9aef3a56ff91f73101db49 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -262,6 +262,7 @@ impl EditorElement { register_action(editor, window, Editor::kill_ring_yank); register_action(editor, window, Editor::copy); register_action(editor, window, Editor::copy_and_trim); + register_action(editor, window, Editor::diff_clipboard_with_selection); register_action(editor, window, Editor::paste); register_action(editor, window, Editor::undo); register_action(editor, window, Editor::redo); diff --git a/crates/git_ui/src/diff_view.rs b/crates/git_ui/src/file_diff_view.rs similarity index 89% rename from crates/git_ui/src/diff_view.rs rename to crates/git_ui/src/file_diff_view.rs index 9e03dd5f38c016873e34ab38eb1f8dfd717c886a..2f8a744ed893761f6491f23a31e19bfb55a4db62 100644 --- a/crates/git_ui/src/diff_view.rs +++ b/crates/git_ui/src/file_diff_view.rs @@ -1,4 +1,4 @@ -//! DiffView provides a UI for displaying differences between two buffers. +//! FileDiffView provides a UI for displaying differences between two buffers. use anyhow::Result; use buffer_diff::{BufferDiff, BufferDiffSnapshot}; @@ -25,7 +25,7 @@ use workspace::{ searchable::SearchableItemHandle, }; -pub struct DiffView { +pub struct FileDiffView { editor: Entity, old_buffer: Entity, new_buffer: Entity, @@ -35,7 +35,7 @@ pub struct DiffView { const RECALCULATE_DIFF_DEBOUNCE: Duration = Duration::from_millis(250); -impl DiffView { +impl FileDiffView { pub fn open( old_path: PathBuf, new_path: PathBuf, @@ -57,7 +57,7 @@ impl DiffView { workspace.update_in(cx, |workspace, window, cx| { let diff_view = cx.new(|cx| { - DiffView::new( + FileDiffView::new( old_buffer, new_buffer, buffer_diff, @@ -190,15 +190,15 @@ async fn build_buffer_diff( }) } -impl EventEmitter for DiffView {} +impl EventEmitter for FileDiffView {} -impl Focusable for DiffView { +impl Focusable for FileDiffView { fn focus_handle(&self, cx: &App) -> FocusHandle { self.editor.focus_handle(cx) } } -impl Item for DiffView { +impl Item for FileDiffView { type Event = EditorEvent; fn tab_icon(&self, _window: &Window, _cx: &App) -> Option { @@ -216,48 +216,37 @@ impl Item for DiffView { } fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString { - let old_filename = self - .old_buffer - .read(cx) - .file() - .and_then(|file| { - Some( - file.full_path(cx) - .file_name()? - .to_string_lossy() - .to_string(), - ) - }) - .unwrap_or_else(|| "untitled".into()); - let new_filename = self - .new_buffer - .read(cx) - .file() - .and_then(|file| { - Some( - file.full_path(cx) - .file_name()? - .to_string_lossy() - .to_string(), - ) - }) - .unwrap_or_else(|| "untitled".into()); + let title_text = |buffer: &Entity| { + buffer + .read(cx) + .file() + .and_then(|file| { + Some( + file.full_path(cx) + .file_name()? + .to_string_lossy() + .to_string(), + ) + }) + .unwrap_or_else(|| "untitled".into()) + }; + let old_filename = title_text(&self.old_buffer); + let new_filename = title_text(&self.new_buffer); + format!("{old_filename} ↔ {new_filename}").into() } fn tab_tooltip_text(&self, cx: &App) -> Option { - let old_path = self - .old_buffer - .read(cx) - .file() - .map(|file| file.full_path(cx).compact().to_string_lossy().to_string()) - .unwrap_or_else(|| "untitled".into()); - let new_path = self - .new_buffer - .read(cx) - .file() - .map(|file| file.full_path(cx).compact().to_string_lossy().to_string()) - .unwrap_or_else(|| "untitled".into()); + let path = |buffer: &Entity| { + buffer + .read(cx) + .file() + .map(|file| file.full_path(cx).compact().to_string_lossy().to_string()) + .unwrap_or_else(|| "untitled".into()) + }; + let old_path = path(&self.old_buffer); + let new_path = path(&self.new_buffer); + Some(format!("{old_path} ↔ {new_path}").into()) } @@ -363,7 +352,7 @@ impl Item for DiffView { } } -impl Render for DiffView { +impl Render for FileDiffView { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { self.editor.clone() } @@ -407,16 +396,16 @@ mod tests { ) .await; - let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; + let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; let (workspace, mut cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); let diff_view = workspace .update_in(cx, |workspace, window, cx| { - DiffView::open( - PathBuf::from(path!("/test/old_file.txt")), - PathBuf::from(path!("/test/new_file.txt")), + FileDiffView::open( + path!("/test/old_file.txt").into(), + path!("/test/new_file.txt").into(), workspace, window, cx, @@ -510,6 +499,21 @@ mod tests { ", ), ); + + diff_view.read_with(cx, |diff_view, cx| { + assert_eq!( + diff_view.tab_content_text(0, cx), + "old_file.txt ↔ new_file.txt" + ); + assert_eq!( + diff_view.tab_tooltip_text(cx).unwrap(), + format!( + "{} ↔ {}", + path!("test/old_file.txt"), + path!("test/new_file.txt") + ) + ); + }) } #[gpui::test] @@ -533,7 +537,7 @@ mod tests { let diff_view = workspace .update_in(cx, |workspace, window, cx| { - DiffView::open( + FileDiffView::open( PathBuf::from(path!("/test/old_file.txt")), PathBuf::from(path!("/test/new_file.txt")), workspace, diff --git a/crates/git_ui/src/git_ui.rs b/crates/git_ui/src/git_ui.rs index 02b9c243fb22e0eda12c1cbd0f1f9bb61c677b26..2d7fba13c590ae48e11b93b0698f3c823641b908 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; +use editor::{Editor, actions::DiffClipboardWithSelectionData}; mod blame_ui; use git::{ repository::{Branch, Upstream, UpstreamTracking, UpstreamTrackingStatus}, @@ -15,6 +15,9 @@ use onboarding::GitOnboardingModal; use project_diff::ProjectDiff; use ui::prelude::*; use workspace::Workspace; +use zed_actions; + +use crate::text_diff_view::TextDiffView; mod askpass_modal; pub mod branch_picker; @@ -22,7 +25,7 @@ mod commit_modal; pub mod commit_tooltip; mod commit_view; mod conflict_view; -pub mod diff_view; +pub mod file_diff_view; pub mod git_panel; mod git_panel_settings; pub mod onboarding; @@ -30,6 +33,7 @@ pub mod picker_prompt; pub mod project_diff; pub(crate) mod remote_output; pub mod repository_selector; +pub mod text_diff_view; actions!( git, @@ -152,6 +156,13 @@ pub fn init(cx: &mut App) { workspace.register_action(|workspace, _: &git::OpenModifiedFiles, window, cx| { open_modified_files(workspace, window, cx); }); + workspace.register_action( + |workspace, action: &DiffClipboardWithSelectionData, window, cx| { + if let Some(task) = TextDiffView::open(action, workspace, window, cx) { + task.detach(); + }; + }, + ); }) .detach(); } diff --git a/crates/git_ui/src/text_diff_view.rs b/crates/git_ui/src/text_diff_view.rs new file mode 100644 index 0000000000000000000000000000000000000000..e7386cf7bdaaab1542f351fa348819bd756389ab --- /dev/null +++ b/crates/git_ui/src/text_diff_view.rs @@ -0,0 +1,554 @@ +//! TextDiffView currently provides a UI for displaying differences between the clipboard and selected text. + +use anyhow::Result; +use buffer_diff::{BufferDiff, BufferDiffSnapshot}; +use editor::{Editor, EditorEvent, MultiBuffer, ToPoint, actions::DiffClipboardWithSelectionData}; +use futures::{FutureExt, select_biased}; +use gpui::{ + AnyElement, AnyView, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, + FocusHandle, Focusable, IntoElement, Render, Task, Window, +}; +use language::{self, Buffer, Point}; +use project::Project; +use std::{ + any::{Any, TypeId}, + ops::Range, + pin::pin, + sync::Arc, + time::Duration, +}; +use ui::{Color, Icon, IconName, Label, LabelCommon as _, SharedString}; +use util::paths::PathExt; + +use workspace::{ + Item, ItemHandle as _, ItemNavHistory, ToolbarItemLocation, Workspace, + item::{BreadcrumbText, ItemEvent, SaveOptions, TabContentParams}, + searchable::SearchableItemHandle, +}; + +pub struct TextDiffView { + diff_editor: Entity, + title: SharedString, + path: Option, + buffer_changes_tx: watch::Sender<()>, + _recalculate_diff_task: Task>, +} + +const RECALCULATE_DIFF_DEBOUNCE: Duration = Duration::from_millis(250); + +impl TextDiffView { + pub fn open( + diff_data: &DiffClipboardWithSelectionData, + workspace: &Workspace, + window: &mut Window, + cx: &mut App, + ) -> Option>>> { + let source_editor = diff_data.editor.clone(); + + let source_editor_buffer_and_range = source_editor.update(cx, |editor, cx| { + let multibuffer = editor.buffer().read(cx); + let source_buffer = multibuffer.as_singleton()?.clone(); + let selections = editor.selections.all::(cx); + let buffer_snapshot = source_buffer.read(cx); + let first_selection = selections.first()?; + let selection_range = if first_selection.is_empty() { + Point::new(0, 0)..buffer_snapshot.max_point() + } else { + first_selection.start..first_selection.end + }; + + Some((source_buffer, selection_range)) + }); + + let Some((source_buffer, selected_range)) = source_editor_buffer_and_range else { + log::warn!("There should always be at least one selection in Zed. This is a bug."); + return None; + }; + + let clipboard_text = diff_data.clipboard_text.clone(); + + let workspace = workspace.weak_handle(); + + let diff_buffer = cx.new(|cx| { + let source_buffer_snapshot = source_buffer.read(cx).snapshot(); + let diff = BufferDiff::new(&source_buffer_snapshot.text, cx); + diff + }); + + let clipboard_buffer = + build_clipboard_buffer(clipboard_text, &source_buffer, selected_range.clone(), cx); + + let task = window.spawn(cx, async move |cx| { + let project = workspace.update(cx, |workspace, _| workspace.project().clone())?; + + update_diff_buffer(&diff_buffer, &source_buffer, &clipboard_buffer, cx).await?; + + workspace.update_in(cx, |workspace, window, cx| { + let diff_view = cx.new(|cx| { + TextDiffView::new( + clipboard_buffer, + source_editor, + source_buffer, + selected_range, + diff_buffer, + project, + window, + cx, + ) + }); + + let pane = workspace.active_pane(); + pane.update(cx, |pane, cx| { + pane.add_item(Box::new(diff_view.clone()), true, true, None, window, cx); + }); + + diff_view + }) + }); + + Some(task) + } + + pub fn new( + clipboard_buffer: Entity, + source_editor: Entity, + source_buffer: Entity, + source_range: Range, + diff_buffer: Entity, + project: Entity, + window: &mut Window, + cx: &mut Context, + ) -> Self { + let multibuffer = cx.new(|cx| { + let mut multibuffer = MultiBuffer::new(language::Capability::ReadWrite); + + multibuffer.push_excerpts( + source_buffer.clone(), + [editor::ExcerptRange::new(source_range)], + cx, + ); + + multibuffer.add_diff(diff_buffer.clone(), cx); + multibuffer + }); + let diff_editor = cx.new(|cx| { + let mut editor = Editor::for_multibuffer(multibuffer, Some(project), window, cx); + editor.start_temporary_diff_override(); + editor.disable_diagnostics(cx); + editor.set_expand_all_diff_hunks(cx); + editor.set_render_diff_hunk_controls( + Arc::new(|_, _, _, _, _, _, _, _| gpui::Empty.into_any_element()), + cx, + ); + editor + }); + + let (buffer_changes_tx, mut buffer_changes_rx) = watch::channel(()); + + cx.subscribe(&source_buffer, move |this, _, event, _| match event { + language::BufferEvent::Edited + | language::BufferEvent::LanguageChanged + | language::BufferEvent::Reparsed => { + this.buffer_changes_tx.send(()).ok(); + } + _ => {} + }) + .detach(); + + let editor = source_editor.read(cx); + let title = editor.buffer().read(cx).title(cx).to_string(); + let selection_location_text = selection_location_text(editor, cx); + let selection_location_title = selection_location_text + .as_ref() + .map(|text| format!("{} @ {}", title, text)) + .unwrap_or(title); + + let path = editor + .buffer() + .read(cx) + .as_singleton() + .and_then(|b| { + b.read(cx) + .file() + .map(|f| f.full_path(cx).compact().to_string_lossy().to_string()) + }) + .unwrap_or("untitled".into()); + + let selection_location_path = selection_location_text + .map(|text| format!("{} @ {}", path, text)) + .unwrap_or(path); + + Self { + diff_editor, + title: format!("Clipboard ↔ {selection_location_title}").into(), + 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 { + loop { + let mut timer = cx + .background_executor() + .timer(RECALCULATE_DIFF_DEBOUNCE) + .fuse(); + let mut recv = pin!(buffer_changes_rx.recv().fuse()); + select_biased! { + _ = timer => break, + _ = recv => continue, + } + } + + log::trace!("start recalculating"); + update_diff_buffer(&diff_buffer, &source_buffer, &clipboard_buffer, cx).await?; + log::trace!("finish recalculating"); + } + Ok(()) + }), + } + } +} + +fn build_clipboard_buffer( + clipboard_text: String, + source_buffer: &Entity, + selected_range: Range, + cx: &mut App, +) -> Entity { + let source_buffer_snapshot = source_buffer.read(cx).snapshot(); + cx.new(|cx| { + let mut buffer = language::Buffer::local(source_buffer_snapshot.text(), cx); + let language = source_buffer.read(cx).language().cloned(); + buffer.set_language(language, cx); + + let range_start = source_buffer_snapshot.point_to_offset(selected_range.start); + let range_end = source_buffer_snapshot.point_to_offset(selected_range.end); + buffer.edit([(range_start..range_end, clipboard_text)], None, cx); + + buffer + }) +} + +async fn update_diff_buffer( + diff: &Entity, + source_buffer: &Entity, + clipboard_buffer: &Entity, + cx: &mut AsyncApp, +) -> Result<()> { + 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 diff_snapshot = cx + .update(|cx| { + BufferDiffSnapshot::new_with_base_buffer( + source_buffer_snapshot.text.clone(), + Some(Arc::new(base_text)), + base_buffer_snapshot, + cx, + ) + })? + .await; + + diff.update(cx, |diff, cx| { + diff.set_snapshot(diff_snapshot, &source_buffer_snapshot.text, cx); + })?; + Ok(()) +} + +impl EventEmitter for TextDiffView {} + +impl Focusable for TextDiffView { + fn focus_handle(&self, cx: &App) -> FocusHandle { + self.diff_editor.focus_handle(cx) + } +} + +impl Item for TextDiffView { + type Event = EditorEvent; + + fn tab_icon(&self, _window: &Window, _cx: &App) -> Option { + Some(Icon::new(IconName::Diff).color(Color::Muted)) + } + + fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement { + Label::new(self.tab_content_text(params.detail.unwrap_or_default(), cx)) + .color(if params.selected { + Color::Default + } else { + Color::Muted + }) + .into_any_element() + } + + fn tab_content_text(&self, _detail: usize, _: &App) -> SharedString { + self.title.clone() + } + + fn tab_tooltip_text(&self, _: &App) -> Option { + self.path.clone() + } + + fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) { + Editor::to_item_events(event, f) + } + + fn telemetry_event_text(&self) -> Option<&'static str> { + Some("Diff View Opened") + } + + fn deactivated(&mut self, window: &mut Window, cx: &mut Context) { + self.diff_editor + .update(cx, |editor, cx| editor.deactivated(window, cx)); + } + + fn is_singleton(&self, _: &App) -> bool { + false + } + + fn act_as_type<'a>( + &'a self, + type_id: TypeId, + self_handle: &'a Entity, + _: &'a App, + ) -> Option { + if type_id == TypeId::of::() { + Some(self_handle.to_any()) + } else if type_id == TypeId::of::() { + Some(self.diff_editor.to_any()) + } else { + None + } + } + + fn as_searchable(&self, _: &Entity) -> Option> { + Some(Box::new(self.diff_editor.clone())) + } + + fn for_each_project_item( + &self, + cx: &App, + f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem), + ) { + self.diff_editor.for_each_project_item(cx, f) + } + + fn set_nav_history( + &mut self, + nav_history: ItemNavHistory, + _: &mut Window, + cx: &mut Context, + ) { + self.diff_editor.update(cx, |editor, _| { + editor.set_nav_history(Some(nav_history)); + }); + } + + fn navigate( + &mut self, + data: Box, + window: &mut Window, + cx: &mut Context, + ) -> bool { + self.diff_editor + .update(cx, |editor, cx| editor.navigate(data, window, cx)) + } + + fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation { + ToolbarItemLocation::PrimaryLeft + } + + fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option> { + self.diff_editor.breadcrumbs(theme, cx) + } + + fn added_to_workspace( + &mut self, + workspace: &mut Workspace, + window: &mut Window, + cx: &mut Context, + ) { + self.diff_editor.update(cx, |editor, cx| { + editor.added_to_workspace(workspace, window, cx) + }); + } + + fn can_save(&self, cx: &App) -> bool { + // The editor handles the new buffer, so delegate to it + self.diff_editor.read(cx).can_save(cx) + } + + fn save( + &mut self, + options: SaveOptions, + project: Entity, + window: &mut Window, + cx: &mut Context, + ) -> Task> { + // Delegate saving to the editor, which manages the new buffer + self.diff_editor + .update(cx, |editor, cx| editor.save(options, project, window, cx)) + } +} + +pub fn selection_location_text(editor: &Editor, cx: &App) -> Option { + let buffer = editor.buffer().read(cx); + let buffer_snapshot = buffer.snapshot(cx); + let first_selection = editor.selections.disjoint.first()?; + + let (start_row, start_column, end_row, end_column) = + if first_selection.start == first_selection.end { + let max_point = buffer_snapshot.max_point(); + (0, 0, max_point.row, max_point.column) + } else { + let selection_start = first_selection.start.to_point(&buffer_snapshot); + let selection_end = first_selection.end.to_point(&buffer_snapshot); + + ( + selection_start.row, + selection_start.column, + selection_end.row, + selection_end.column, + ) + }; + + let range_text = if start_row == end_row { + format!("L{}:{}-{}", start_row + 1, start_column + 1, end_column + 1) + } else { + format!( + "L{}:{}-L{}:{}", + start_row + 1, + start_column + 1, + end_row + 1, + end_column + 1 + ) + }; + + Some(range_text) +} + +impl Render for TextDiffView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + self.diff_editor.clone() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + use editor::{actions, test::editor_test_context::assert_state_with_diff}; + use gpui::{TestAppContext, VisualContext}; + use project::{FakeFs, Project}; + use serde_json::json; + use settings::{Settings, SettingsStore}; + use unindent::unindent; + use util::path; + + 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); + workspace::init_settings(cx); + editor::init_settings(cx); + theme::ThemeSettings::register(cx) + }); + } + + #[gpui::test] + async fn test_diffing_clipboard_against_specific_selection(cx: &mut TestAppContext) { + base_test(true, cx).await; + } + + #[gpui::test] + async fn test_diffing_clipboard_against_empty_selection_uses_full_buffer( + cx: &mut TestAppContext, + ) { + base_test(false, cx).await; + } + + async fn base_test(select_all_text: bool, cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + path!("/test"), + json!({ + "a": { + "b": { + "text.txt": "new line 1\nline 2\nnew line 3\nline 4" + } + } + }), + ) + .await; + + let project = Project::test(fs, [path!("/test").as_ref()], cx).await; + + let (workspace, mut cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + + let buffer = project + .update(cx, |project, cx| { + project.open_local_buffer(path!("/test/a/b/text.txt"), cx) + }) + .await + .unwrap(); + + let editor = cx.new_window_entity(|window, cx| { + let mut editor = Editor::for_buffer(buffer, None, window, cx); + editor.set_text("new line 1\nline 2\nnew line 3\nline 4\n", window, cx); + + if select_all_text { + editor.select_all(&actions::SelectAll, window, cx); + } + + editor + }); + + let diff_view = workspace + .update_in(cx, |workspace, window, cx| { + TextDiffView::open( + &DiffClipboardWithSelectionData { + clipboard_text: "old line 1\nline 2\nold line 3\nline 4\n".to_string(), + editor, + }, + workspace, + window, + cx, + ) + }) + .unwrap() + .await + .unwrap(); + + cx.executor().run_until_parked(); + + assert_state_with_diff( + &diff_view.read_with(cx, |diff_view, _| diff_view.diff_editor.clone()), + &mut cx, + &unindent( + " + - old line 1 + + ˇnew line 1 + line 2 + - old line 3 + + new line 3 + line 4 + ", + ), + ); + + diff_view.read_with(cx, |diff_view, cx| { + assert_eq!( + diff_view.tab_content_text(0, cx), + "Clipboard ↔ text.txt @ L1:1-L5:1" + ); + assert_eq!( + diff_view.tab_tooltip_text(cx).unwrap(), + format!("Clipboard ↔ {}", path!("test/a/b/text.txt @ L1:1-L5:1")) + ); + }); + } +} diff --git a/crates/gpui/src/app/entity_map.rs b/crates/gpui/src/app/entity_map.rs index d4e5b2570ed5851b47c8557638de47760bedba2f..fccb417caa70c7526a0f15a307d74caeabcdab77 100644 --- a/crates/gpui/src/app/entity_map.rs +++ b/crates/gpui/src/app/entity_map.rs @@ -370,7 +370,7 @@ impl std::fmt::Debug for AnyEntity { } } -/// A strong, well typed reference to a struct which is managed +/// A strong, well-typed reference to a struct which is managed /// by GPUI #[derive(Deref, DerefMut)] pub struct Entity { diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index b6feb0073e3d46fb43c45e0f17069eef730f2481..2fd9b0a68c7c14fd6df0ba2a52c537e34cdd7ceb 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -12,7 +12,7 @@ use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; use futures::channel::{mpsc, oneshot}; use futures::future::join_all; use futures::{FutureExt, SinkExt, StreamExt}; -use git_ui::diff_view::DiffView; +use git_ui::file_diff_view::FileDiffView; use gpui::{App, AsyncApp, Global, WindowHandle}; use language::Point; use recent_projects::{SshSettings, open_ssh_project}; @@ -262,7 +262,7 @@ pub async fn open_paths_with_positions( let old_path = Path::new(&diff_pair[0]).canonicalize()?; let new_path = Path::new(&diff_pair[1]).canonicalize()?; if let Ok(diff_view) = workspace.update(cx, |workspace, window, cx| { - DiffView::open(old_path, new_path, 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)))) From 6122f460953c2f162a1ea91213b72db914b39456 Mon Sep 17 00:00:00 2001 From: maan2003 <49202620+maan2003@users.noreply.github.com> Date: Wed, 23 Jul 2025 11:48:45 +0530 Subject: [PATCH 277/658] project: Fix search filter patterns on remote projects (#34748) we were join(",") and split(",") to serialize the patterns. This doesn't work when pattern includes a "," example: *.{ts,tsx} (very common pattern used by agent) help needed: how will this work on version mismatch? Release Notes: - Fixed search filter patterns on remote projects. --- crates/project/src/search.rs | 49 +++++++++++++++++++++++---------- crates/proto/proto/buffer.proto | 6 ++-- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs index 44732b23cd4fb7ff8044b65e55d0ea09f0c3b5c9..4f024837c8be8946c8feb00f398779b604afbbf0 100644 --- a/crates/project/src/search.rs +++ b/crates/project/src/search.rs @@ -193,6 +193,30 @@ impl SearchQuery { } pub fn from_proto(message: proto::SearchQuery) -> Result { + let files_to_include = if message.files_to_include.is_empty() { + message + .files_to_include_legacy + .split(',') + .map(str::trim) + .filter(|&glob_str| !glob_str.is_empty()) + .map(|s| s.to_string()) + .collect() + } else { + message.files_to_include + }; + + let files_to_exclude = if message.files_to_exclude.is_empty() { + message + .files_to_exclude_legacy + .split(',') + .map(str::trim) + .filter(|&glob_str| !glob_str.is_empty()) + .map(|s| s.to_string()) + .collect() + } else { + message.files_to_exclude + }; + if message.regex { Self::regex( message.query, @@ -200,8 +224,8 @@ impl SearchQuery { message.case_sensitive, message.include_ignored, false, - deserialize_path_matches(&message.files_to_include)?, - deserialize_path_matches(&message.files_to_exclude)?, + PathMatcher::new(files_to_include)?, + PathMatcher::new(files_to_exclude)?, message.match_full_paths, None, // search opened only don't need search remote ) @@ -211,8 +235,8 @@ impl SearchQuery { message.whole_word, message.case_sensitive, message.include_ignored, - deserialize_path_matches(&message.files_to_include)?, - deserialize_path_matches(&message.files_to_exclude)?, + PathMatcher::new(files_to_include)?, + PathMatcher::new(files_to_exclude)?, false, None, // search opened only don't need search remote ) @@ -236,15 +260,20 @@ impl SearchQuery { } pub fn to_proto(&self) -> proto::SearchQuery { + let files_to_include = self.files_to_include().sources().to_vec(); + let files_to_exclude = self.files_to_exclude().sources().to_vec(); proto::SearchQuery { query: self.as_str().to_string(), regex: self.is_regex(), whole_word: self.whole_word(), case_sensitive: self.case_sensitive(), include_ignored: self.include_ignored(), - files_to_include: self.files_to_include().sources().join(","), - files_to_exclude: self.files_to_exclude().sources().join(","), + files_to_include: files_to_include.clone(), + files_to_exclude: files_to_exclude.clone(), match_full_paths: self.match_full_paths(), + // Populate legacy fields for backwards compatibility + files_to_include_legacy: files_to_include.join(","), + files_to_exclude_legacy: files_to_exclude.join(","), } } @@ -520,14 +549,6 @@ impl SearchQuery { } } -pub fn deserialize_path_matches(glob_set: &str) -> anyhow::Result { - let globs = glob_set - .split(',') - .map(str::trim) - .filter(|&glob_str| !glob_str.is_empty()); - Ok(PathMatcher::new(globs)?) -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/proto/proto/buffer.proto b/crates/proto/proto/buffer.proto index 09a05a50cd84381c4aaccd17a846e2eb38822392..f4dacf2fdca97bf9766c8de348a67cd18f8fb973 100644 --- a/crates/proto/proto/buffer.proto +++ b/crates/proto/proto/buffer.proto @@ -288,10 +288,12 @@ message SearchQuery { bool regex = 3; bool whole_word = 4; bool case_sensitive = 5; - string files_to_include = 6; - string files_to_exclude = 7; + repeated string files_to_include = 10; + repeated string files_to_exclude = 11; bool match_full_paths = 9; bool include_ignored = 8; + string files_to_include_legacy = 6; + string files_to_exclude_legacy = 7; } message FindSearchCandidates { From 7db110f48d23344e1fc2fa9f30f6b1fe3878bc73 Mon Sep 17 00:00:00 2001 From: tidely <43219534+tidely@users.noreply.github.com> Date: Wed, 23 Jul 2025 10:57:57 +0300 Subject: [PATCH 278/658] sum_tree: Utilize `size_hint` in `TreeSet::extend` (#34936) Collect the iterator instead of manually looping over it to utilize possible size hints. Zed usually passes in owned `Vec`'s, meaning we get to reuse memory as well. Release Notes: - N/A --- crates/sum_tree/src/tree_map.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/sum_tree/src/tree_map.rs b/crates/sum_tree/src/tree_map.rs index 0397f16182133c77f618d04c0ca32622686c8e25..54e8ae8343f4778e04a37a7ebd3dbe2b6da587cd 100644 --- a/crates/sum_tree/src/tree_map.rs +++ b/crates/sum_tree/src/tree_map.rs @@ -71,10 +71,10 @@ impl TreeMap { } pub fn extend(&mut self, iter: impl IntoIterator) { - let mut edits = Vec::new(); - for (key, value) in iter { - edits.push(Edit::Insert(MapEntry { key, value })); - } + let edits: Vec<_> = iter + .into_iter() + .map(|(key, value)| Edit::Insert(MapEntry { key, value })) + .collect(); self.0.edit(edits, &()); } From 6cd3726a5adb67c067158514db4719fde2e8b58b Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 23 Jul 2025 10:28:06 +0200 Subject: [PATCH 279/658] Revert "chore: Bump taffy to 0.8.3" (#34938) Reverts zed-industries/zed#34876 From our Slack: image https://github.com/user-attachments/assets/828964be-9b6e-4496-9361-9e3a2e9aa208 Release Notes: - N/A --- Cargo.lock | 9 +++++---- crates/gpui/Cargo.toml | 2 +- crates/gpui/src/taffy.rs | 26 +++++++++++++------------- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 08d29cdc8000bd0a6136d0d7f9237f79f15f830c..c64995b01b7bdd0c511ed1b94000c9e647fcd976 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7401,9 +7401,9 @@ dependencies = [ [[package]] name = "grid" -version = "0.17.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71b01d27060ad58be4663b9e4ac9e2d4806918e8876af8912afbddd1a91d5eaa" +checksum = "be136d9dacc2a13cc70bb6c8f902b414fb2641f8db1314637c6b7933411a8f82" [[package]] name = "group" @@ -15958,12 +15958,13 @@ dependencies = [ [[package]] name = "taffy" -version = "0.8.3" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aaef0ac998e6527d6d0d5582f7e43953bb17221ac75bb8eb2fcc2db3396db1c" +checksum = "e8b61630cba2afd2c851821add2e1bb1b7851a2436e839ab73b56558b009035e" dependencies = [ "arrayvec", "grid", + "num-traits", "serde", "slotmap", ] diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 68c0ea89c70f10c634ac3026b9b6997108d33996..878794647a4633b63eedd36ec07d18ec67ee1ef8 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.5.1" thiserror.workspace = true util.workspace = true uuid.workspace = true diff --git a/crates/gpui/src/taffy.rs b/crates/gpui/src/taffy.rs index f7fa54256df20b38170ecb4d3e48c22913e44ae6..6228a604904f6aa40d6d15fb7f9c5ff19b29f6a1 100644 --- a/crates/gpui/src/taffy.rs +++ b/crates/gpui/src/taffy.rs @@ -283,7 +283,7 @@ impl ToTaffy for Length { fn to_taffy(&self, rem_size: Pixels) -> taffy::prelude::LengthPercentageAuto { match self { Length::Definite(length) => length.to_taffy(rem_size), - Length::Auto => taffy::prelude::LengthPercentageAuto::auto(), + Length::Auto => taffy::prelude::LengthPercentageAuto::Auto, } } } @@ -292,7 +292,7 @@ impl ToTaffy for Length { fn to_taffy(&self, rem_size: Pixels) -> taffy::prelude::Dimension { match self { Length::Definite(length) => length.to_taffy(rem_size), - Length::Auto => taffy::prelude::Dimension::auto(), + Length::Auto => taffy::prelude::Dimension::Auto, } } } @@ -302,14 +302,14 @@ impl ToTaffy for DefiniteLength { match self { DefiniteLength::Absolute(length) => match length { AbsoluteLength::Pixels(pixels) => { - taffy::style::LengthPercentage::length(pixels.into()) + taffy::style::LengthPercentage::Length(pixels.into()) } AbsoluteLength::Rems(rems) => { - taffy::style::LengthPercentage::length((*rems * rem_size).into()) + taffy::style::LengthPercentage::Length((*rems * rem_size).into()) } }, DefiniteLength::Fraction(fraction) => { - taffy::style::LengthPercentage::percent(*fraction) + taffy::style::LengthPercentage::Percent(*fraction) } } } @@ -320,14 +320,14 @@ impl ToTaffy for DefiniteLength { match self { DefiniteLength::Absolute(length) => match length { AbsoluteLength::Pixels(pixels) => { - taffy::style::LengthPercentageAuto::length(pixels.into()) + taffy::style::LengthPercentageAuto::Length(pixels.into()) } AbsoluteLength::Rems(rems) => { - taffy::style::LengthPercentageAuto::length((*rems * rem_size).into()) + taffy::style::LengthPercentageAuto::Length((*rems * rem_size).into()) } }, DefiniteLength::Fraction(fraction) => { - taffy::style::LengthPercentageAuto::percent(*fraction) + taffy::style::LengthPercentageAuto::Percent(*fraction) } } } @@ -337,12 +337,12 @@ impl ToTaffy for DefiniteLength { fn to_taffy(&self, rem_size: Pixels) -> taffy::style::Dimension { match self { DefiniteLength::Absolute(length) => match length { - AbsoluteLength::Pixels(pixels) => taffy::style::Dimension::length(pixels.into()), + AbsoluteLength::Pixels(pixels) => taffy::style::Dimension::Length(pixels.into()), AbsoluteLength::Rems(rems) => { - taffy::style::Dimension::length((*rems * rem_size).into()) + taffy::style::Dimension::Length((*rems * rem_size).into()) } }, - DefiniteLength::Fraction(fraction) => taffy::style::Dimension::percent(*fraction), + DefiniteLength::Fraction(fraction) => taffy::style::Dimension::Percent(*fraction), } } } @@ -350,9 +350,9 @@ impl ToTaffy for DefiniteLength { impl ToTaffy for AbsoluteLength { fn to_taffy(&self, rem_size: Pixels) -> taffy::style::LengthPercentage { match self { - AbsoluteLength::Pixels(pixels) => taffy::style::LengthPercentage::length(pixels.into()), + AbsoluteLength::Pixels(pixels) => taffy::style::LengthPercentage::Length(pixels.into()), AbsoluteLength::Rems(rems) => { - taffy::style::LengthPercentage::length((*rems * rem_size).into()) + taffy::style::LengthPercentage::Length((*rems * rem_size).into()) } } } From c2c2264a60abf65e8a5e14859d6b588e307142c2 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 23 Jul 2025 12:17:23 +0200 Subject: [PATCH 280/658] gpui: Add tree example (#34942) This commit adds an example with deep children hierarchy. The depth of a tree can be tweaked with GPUI_TREE_DEPTH env variable. With depth=100 image With this example, I can trigger a stack overflow at depth=633 (and higher). Release Notes: - N/A --- crates/gpui/Cargo.toml | 4 ++++ crates/gpui/examples/tree.rs | 46 ++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 crates/gpui/examples/tree.rs diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 878794647a4633b63eedd36ec07d18ec67ee1ef8..b446ea8bd8197149a10ad02fd7c506622971d4c5 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -295,6 +295,10 @@ path = "examples/text.rs" name = "text_wrapper" path = "examples/text_wrapper.rs" +[[example]] +name = "tree" +path = "examples/tree.rs" + [[example]] name = "uniform_list" path = "examples/uniform_list.rs" diff --git a/crates/gpui/examples/tree.rs b/crates/gpui/examples/tree.rs new file mode 100644 index 0000000000000000000000000000000000000000..1bd45920037839c27ea5773f23daa9dcbbceae0e --- /dev/null +++ b/crates/gpui/examples/tree.rs @@ -0,0 +1,46 @@ +//! Renders a div with deep children hierarchy. This example is useful to exemplify that Zed can +//! handle deep hierarchies (even though it cannot just yet!). +use std::sync::LazyLock; + +use gpui::{ + App, Application, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px, + size, +}; + +struct Tree {} + +static DEPTH: LazyLock = LazyLock::new(|| { + std::env::var("GPUI_TREE_DEPTH") + .ok() + .and_then(|depth| depth.parse().ok()) + .unwrap_or_else(|| 50) +}); + +impl Render for Tree { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + let mut depth = *DEPTH; + static COLORS: [gpui::Hsla; 4] = [gpui::red(), gpui::blue(), gpui::green(), gpui::yellow()]; + let mut colors = COLORS.iter().cycle().copied(); + let mut next_div = || div().p_0p5().bg(colors.next().unwrap()); + let mut innermost_node = next_div(); + while depth > 0 { + innermost_node = next_div().child(innermost_node); + depth -= 1; + } + innermost_node + } +} + +fn main() { + Application::new().run(|cx: &mut App| { + let bounds = Bounds::centered(None, size(px(300.0), px(300.0)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |_, cx| cx.new(|_| Tree {}), + ) + .unwrap(); + }); +} From 2bc6e18ac913f363e7f75fd12efccc02dc3c05d9 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Wed, 23 Jul 2025 12:20:09 +0200 Subject: [PATCH 281/658] Ensure `disable_ai` is properly respected (#34941) Quick follow up to #34896 which ensures that the Agent Panel cannot be caught by actions like `workspace: toggle left dock` when `disable_ai` is set to true. Also removes a method that was introduced but unused in the workspace because `first_enabled_panel_idx` already covers all cases this method could be useful for. Release Notes: - N/A --- crates/agent_ui/src/agent_panel.rs | 9 +++------ crates/workspace/src/dock.rs | 6 ------ 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 7e9360a0cbf65bdae58b0dc68ffe0f4526ab4926..95ce2896083f0045bc3788c94847c08746d6e0bc 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1,5 +1,5 @@ use std::cell::RefCell; -use std::ops::Range; +use std::ops::{Not, Range}; use std::path::Path; use std::rc::Rc; use std::sync::Arc; @@ -1666,10 +1666,7 @@ impl Panel for AgentPanel { } fn icon(&self, _window: &Window, cx: &App) -> Option { - (self.enabled(cx) - && AgentSettings::get_global(cx).button - && !DisableAiSettings::get_global(cx).disable_ai) - .then_some(IconName::ZedAssistant) + (self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAssistant) } fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> { @@ -1685,7 +1682,7 @@ impl Panel for AgentPanel { } fn enabled(&self, cx: &App) -> bool { - AgentSettings::get_global(cx).enabled + DisableAiSettings::get_global(cx).disable_ai.not() && AgentSettings::get_global(cx).enabled } fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool { diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 3f047e2f114f972380eba43d9478adac71cc7257..7165de23ecf275249c894a12e6aaa9261a4faeee 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -374,12 +374,6 @@ impl Dock { }) } - pub fn first_enabled_panel_idx_excluding(&self, exclude_name: &str, cx: &App) -> Option { - self.panel_entries.iter().position(|entry| { - entry.panel.persistent_name() != exclude_name && entry.panel.enabled(cx) - }) - } - fn active_panel_entry(&self) -> Option<&PanelEntry> { self.active_panel_index .and_then(|index| self.panel_entries.get(index)) From 326ab5fa3fda2e0c128f533772060d48397dc5cc Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Wed, 23 Jul 2025 10:04:53 -0400 Subject: [PATCH 282/658] Improve collab channel organization keybinds (#34821) Change channel reorganization (move up/down) from `cmd-up/down` (mac) / `ctrl-up/down` (linux) to `alt-up/down` (both) to match moving lines in the editor. Also fix an issue where if you selected channels using down/up in the filter field, the movement shortcuts would not work (`editing` vs `not_editing`). Release Notes: - N/A --- assets/keymaps/default-linux.json | 11 ++++++++--- assets/keymaps/default-macos.json | 12 +++++++++--- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 4918e654fc50e7282cf5ee99228c77381d6997ee..b097be90fdbe8a8cd9cd821ef9df2c3f9ccaf26a 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -975,9 +975,14 @@ "context": "CollabPanel && not_editing", "bindings": { "ctrl-backspace": "collab_panel::Remove", - "space": "menu::Confirm", - "ctrl-up": "collab_panel::MoveChannelUp", - "ctrl-down": "collab_panel::MoveChannelDown" + "space": "menu::Confirm" + } + }, + { + "context": "CollabPanel", + "bindings": { + "alt-up": "collab_panel::MoveChannelUp", + "alt-down": "collab_panel::MoveChannelDown" } }, { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 60f29b1da148e26d72744f42252f6086894cd5db..e33786f1b2bda9807dc1a46c401db14dd605e9e4 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -1037,9 +1037,15 @@ "use_key_equivalents": true, "bindings": { "ctrl-backspace": "collab_panel::Remove", - "space": "menu::Confirm", - "cmd-up": "collab_panel::MoveChannelUp", - "cmd-down": "collab_panel::MoveChannelDown" + "space": "menu::Confirm" + } + }, + { + "context": "CollabPanel", + "use_key_equivalents": true, + "bindings": { + "alt-up": "collab_panel::MoveChannelUp", + "alt-down": "collab_panel::MoveChannelDown" } }, { From 14171e0721235a5bbcc4ed568d2db5bdd4ff821a Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 23 Jul 2025 10:30:08 -0400 Subject: [PATCH 283/658] collab: Add `POST /users/:id/update_plan` endpoint (#34953) This PR adds a new `POST /users/:id/update_plan` endpoint to Collab to allow Cloud to push down plan updates to users. Release Notes: - N/A --- crates/collab/src/api.rs | 79 ++++++++++++++++++++++++++++++++ crates/collab/src/api/billing.rs | 2 +- crates/collab/src/rpc.rs | 30 ++++++++---- 3 files changed, 101 insertions(+), 10 deletions(-) diff --git a/crates/collab/src/api.rs b/crates/collab/src/api.rs index 8f1433a26f1a09fd820e8272684b08ff1b6d9581..3b0f5396a77b924ab3452a971d3e7af0878110a6 100644 --- a/crates/collab/src/api.rs +++ b/crates/collab/src/api.rs @@ -11,7 +11,9 @@ use crate::{ db::{User, UserId}, rpc, }; +use ::rpc::proto; use anyhow::Context as _; +use axum::extract; use axum::{ Extension, Json, Router, body::Body, @@ -23,6 +25,7 @@ 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; @@ -101,6 +104,7 @@ pub fn routes(rpc_server: Arc) -> Router<(), Body> { .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(billing::router()) .merge(contributors::router()) @@ -347,3 +351,78 @@ async fn refresh_llm_tokens( Ok(Json(RefreshLlmTokensResponse {})) } + +#[derive(Debug, Serialize, Deserialize)] +struct UpdatePlanBody { + pub plan: zed_llm_client::Plan, + pub subscription_period: SubscriptionPeriod, + pub usage: zed_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 { + zed_llm_client::Plan::ZedFree => proto::Plan::Free, + zed_llm_client::Plan::ZedPro => proto::Plan::ZedPro, + zed_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: zed_llm_client::UsageLimit) -> proto::UsageLimit { + proto::UsageLimit { + variant: Some(match limit { + zed_llm_client::UsageLimit::Limited(limit) => { + proto::usage_limit::Variant::Limited(proto::usage_limit::Limited { + limit: limit as u32, + }) + } + zed_llm_client::UsageLimit::Unlimited => { + proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {}) + } + }), + } +} diff --git a/crates/collab/src/api/billing.rs b/crates/collab/src/api/billing.rs index d6e42ad2fb9a66fc742a93b7bd34d73d47c8dcbe..bd7b99b3eb4584e36ae4d78f04e8976b7c09424a 100644 --- a/crates/collab/src/api/billing.rs +++ b/crates/collab/src/api/billing.rs @@ -785,7 +785,7 @@ async fn handle_customer_subscription_event( // When the user's subscription changes, push down any changes to their plan. rpc_server - .update_plan_for_user(billing_customer.user_id) + .update_plan_for_user_legacy(billing_customer.user_id) .await .trace_err(); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 924784109b1de0a56abca60c4866ab137d14e7c3..0735b08e8928d999682f6544e7bfd8b93a3d4fa2 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -1002,7 +1002,26 @@ impl Server { Ok(()) } - pub async fn update_plan_for_user(self: &Arc, user_id: UserId) -> Result<()> { + 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 @@ -1018,14 +1037,7 @@ impl Server { ) .await?; - 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(()) + self.update_plan_for_user(user_id, update_user_plan).await } pub async fn refresh_llm_tokens_for_user(self: &Arc, user_id: UserId) { From 1f4c9b9427437952839489a6399e5300dd579dfe Mon Sep 17 00:00:00 2001 From: claytonrcarter Date: Wed, 23 Jul 2025 11:08:52 -0400 Subject: [PATCH 284/658] language: Update block_comment and documentation comment (#34861) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As suggested in https://github.com/zed-industries/zed/pull/34418, this proposes various changes to language configs to make block comments and doc-block-style comments more similar. In doing so, it introduces some breaking changes into the extension schema. This change is needed to support the changes I'm working on in #34418, to be able to support `rewrap` in block comments like `/* really long comment ... */`. As is, we can do this in C-style doc-block comments (eg `/** ... */`) because of the config in `documentation`, but we can't do this in regular block comments because we lack the info about what the line prefix and indentation should be. And while I was here, I did various other clean-ups, many of which feel nice but are optional. I would love special attention on the changes to the schema, version and related changes; I'm totally unfamiliar with that part of Zed. **Summary of changes** - break: changes type of `block_comment` to same type as `documentation_comment` (**this is the important change**) - break: rename `documentation` to `documentation_comment` (optional, but improves consistency w/ `line_comments` and `block_comment`) - break/refactor?: removes some whitespace in the declaration of `block_comment` delimiters (optional, may break things, need input; some langs had no spaces, others did) - refactor: change `tab_size` from `NonZeroU32` to just a `u32` (some block comments don't seem to need/want indent past the initial delimiter, so we need this be 0 sometimes) - refactor: moves the `documentation_comment` declarations to appear next to `block_comment`, rearranges the order of the fields in the TOML for `documentation_comment`, rename backing `struct` (all optional) **Future scope** I believe that this will also allow us to extend regular block comments on newline – as we do doc-block comments – but I haven't looked into this yet. (eg, in JS try pressing enter in both of these: `/* */` and `/** */`; the latter should extend w/ a `*` prefixed line, while the former does not.) Release Notes: - BREAKING CHANGE: update extension schema version from 1 to 2, change format of `block_comment` and rename `documentation_comment` /cc @smitbarmase --- Cargo.lock | 1 + crates/editor/src/editor.rs | 28 ++-- crates/editor/src/editor_tests.rs | 18 +- .../src/test/editor_lsp_test_context.rs | 10 +- crates/language/Cargo.toml | 1 + crates/language/src/buffer_tests.rs | 76 +++++++-- crates/language/src/language.rs | 157 +++++++++++++++--- crates/languages/src/c/config.toml | 2 +- crates/languages/src/cpp/config.toml | 2 +- crates/languages/src/css/config.toml | 2 +- crates/languages/src/go/config.toml | 2 +- crates/languages/src/javascript/config.toml | 6 +- crates/languages/src/markdown/config.toml | 2 +- crates/languages/src/rust/config.toml | 2 +- crates/languages/src/tsx/config.toml | 6 +- crates/languages/src/typescript/config.toml | 4 +- extensions/glsl/languages/glsl/config.toml | 2 +- extensions/html/languages/html/config.toml | 2 +- 18 files changed, 249 insertions(+), 74 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c64995b01b7bdd0c511ed1b94000c9e647fcd976..6c346e331e951352c49425daa5533226a40f4852 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9018,6 +9018,7 @@ dependencies = [ "task", "text", "theme", + "toml 0.8.20", "tree-sitter", "tree-sitter-elixir", "tree-sitter-embedded-template", diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a31f789fb03e3220d706a7b13afd5d7d2e7132c3..d5448f30f362d0a49edb074b5d16f6b860efcbaa 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -109,10 +109,10 @@ use inline_completion::{EditPredictionProvider, InlineCompletionProviderHandle}; pub use items::MAX_TAB_TITLE_LEN; use itertools::Itertools; use language::{ - AutoindentMode, BracketMatch, BracketPair, Buffer, Capability, CharKind, CodeLabel, - CursorShape, DiagnosticEntry, DiffOptions, DocumentationConfig, EditPredictionsMode, - EditPreview, HighlightedText, IndentKind, IndentSize, Language, OffsetRangeExt, Point, - Selection, SelectionGoal, TextObject, TransactionId, TreeSitterOptions, WordsQuery, + 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, language_settings::{ self, InlayHintSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode, all_language_settings, language_settings, @@ -4408,7 +4408,9 @@ impl Editor { }) .max_by_key(|(_, len)| *len)?; - if let Some((block_start, _)) = language.block_comment_delimiters() + if let Some(BlockCommentConfig { + start: block_start, .. + }) = language.block_comment() { let block_start_trimmed = block_start.trim_end(); if block_start_trimmed.starts_with(delimiter.trim_end()) { @@ -4445,13 +4447,12 @@ impl Editor { return None; } - let DocumentationConfig { + let BlockCommentConfig { start: start_tag, end: end_tag, prefix: delimiter, tab_size: len, - } = language.documentation()?; - + } = language.documentation_comment()?; let is_within_block_comment = buffer .language_scope_at(start_point) .is_some_and(|scope| scope.override_name() == Some("comment")); @@ -4521,7 +4522,7 @@ impl Editor { let cursor_is_at_start_of_end_tag = column == end_tag_offset; if cursor_is_at_start_of_end_tag { - indent_on_extra_newline.len = (*len).into(); + indent_on_extra_newline.len = *len; } } cursor_is_before_end_tag @@ -4534,7 +4535,7 @@ impl Editor { && cursor_is_before_end_tag_if_exists { if cursor_is_after_start_tag { - indent_on_newline.len = (*len).into(); + indent_on_newline.len = *len; } Some(delimiter.clone()) } else { @@ -14349,8 +14350,11 @@ impl Editor { (position..position, first_prefix.clone()) })); } - } else if let Some((full_comment_prefix, comment_suffix)) = - language.block_comment_delimiters() + } else if let Some(BlockCommentConfig { + start: full_comment_prefix, + end: comment_suffix, + .. + }) = language.block_comment() { let comment_prefix = full_comment_prefix.trim_end_matches(' '); let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..]; diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index fbb877796cbdb066179f104d862905d0ce71c25c..b9ca8c37552412959e64f98dc04c1f840a45fde5 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -2875,11 +2875,11 @@ async fn test_newline_documentation_comments(cx: &mut TestAppContext) { let language = Arc::new( Language::new( LanguageConfig { - documentation: Some(language::DocumentationConfig { + documentation_comment: Some(language::BlockCommentConfig { start: "/**".into(), end: "*/".into(), prefix: "* ".into(), - tab_size: NonZeroU32::new(1).unwrap(), + tab_size: 1, }), ..LanguageConfig::default() @@ -3089,7 +3089,12 @@ async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) { let lua_language = Arc::new(Language::new( LanguageConfig { line_comments: vec!["--".into()], - block_comment: Some(("--[[".into(), "]]".into())), + block_comment: Some(language::BlockCommentConfig { + start: "--[[".into(), + prefix: "".into(), + end: "]]".into(), + tab_size: 0, + }), ..LanguageConfig::default() }, None, @@ -13806,7 +13811,12 @@ async fn test_toggle_block_comment(cx: &mut TestAppContext) { Language::new( LanguageConfig { name: "HTML".into(), - block_comment: Some(("".into())), + block_comment: Some(BlockCommentConfig { + start: "".into(), + tab_size: 0, + }), ..Default::default() }, Some(tree_sitter_html::LANGUAGE.into()), diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index f7f34135f3ccd5432b088351029632acef420cc9..c59786b1eb387835a21e2c155efaf6acefd4ff4a 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -14,7 +14,8 @@ use futures::Future; use gpui::{Context, Entity, Focusable as _, VisualTestContext, Window}; use indoc::indoc; use language::{ - FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageQueries, point_to_lsp, + BlockCommentConfig, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageQueries, + point_to_lsp, }; use lsp::{notification, request}; use multi_buffer::ToPointUtf16; @@ -269,7 +270,12 @@ impl EditorLspTestContext { path_suffixes: vec!["html".into()], ..Default::default() }, - block_comment: Some(("".into())), + block_comment: Some(BlockCommentConfig { + start: "".into(), + tab_size: 0, + }), completion_query_characters: ['-'].into_iter().collect(), ..Default::default() }, diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 477b978517d56d0f70270a4bf413b285b455ca94..4ab56d6647db5246bf0af7343c8485d946c8b156 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -92,6 +92,7 @@ tree-sitter-python.workspace = true tree-sitter-ruby.workspace = true tree-sitter-rust.workspace = true tree-sitter-typescript.workspace = true +toml.workspace = true unindent.workspace = true util = { workspace = true, features = ["test-support"] } zlog.workspace = true diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 6955cd054925076f8d2678eff58c44e0b82351d0..2e2df7e658596daaca3b338ef830794fd0d3bef8 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -2273,7 +2273,12 @@ fn test_language_scope_at_with_javascript(cx: &mut App) { LanguageConfig { name: "JavaScript".into(), line_comments: vec!["// ".into()], - block_comment: Some(("/*".into(), "*/".into())), + block_comment: Some(BlockCommentConfig { + start: "/*".into(), + end: "*/".into(), + prefix: "* ".into(), + tab_size: 1, + }), brackets: BracketPairConfig { pairs: vec![ BracketPair { @@ -2300,7 +2305,12 @@ fn test_language_scope_at_with_javascript(cx: &mut App) { "element".into(), LanguageConfigOverride { line_comments: Override::Remove { remove: true }, - block_comment: Override::Set(("{/*".into(), "*/}".into())), + block_comment: Override::Set(BlockCommentConfig { + start: "{/*".into(), + prefix: "".into(), + end: "*/}".into(), + tab_size: 0, + }), ..Default::default() }, )] @@ -2338,9 +2348,15 @@ fn test_language_scope_at_with_javascript(cx: &mut App) { let config = snapshot.language_scope_at(0).unwrap(); assert_eq!(config.line_comment_prefixes(), &[Arc::from("// ")]); assert_eq!( - config.block_comment_delimiters(), - Some((&"/*".into(), &"*/".into())) + config.block_comment(), + Some(&BlockCommentConfig { + start: "/*".into(), + prefix: "* ".into(), + end: "*/".into(), + tab_size: 1, + }) ); + // Both bracket pairs are enabled assert_eq!( config.brackets().map(|e| e.1).collect::>(), @@ -2360,8 +2376,13 @@ fn test_language_scope_at_with_javascript(cx: &mut App) { .unwrap(); assert_eq!(string_config.line_comment_prefixes(), &[Arc::from("// ")]); assert_eq!( - string_config.block_comment_delimiters(), - Some((&"/*".into(), &"*/".into())) + string_config.block_comment(), + Some(&BlockCommentConfig { + start: "/*".into(), + prefix: "* ".into(), + end: "*/".into(), + tab_size: 1, + }) ); // Second bracket pair is disabled assert_eq!( @@ -2391,8 +2412,13 @@ fn test_language_scope_at_with_javascript(cx: &mut App) { .unwrap(); assert_eq!(tag_config.line_comment_prefixes(), &[Arc::from("// ")]); assert_eq!( - tag_config.block_comment_delimiters(), - Some((&"/*".into(), &"*/".into())) + tag_config.block_comment(), + Some(&BlockCommentConfig { + start: "/*".into(), + prefix: "* ".into(), + end: "*/".into(), + tab_size: 1, + }) ); assert_eq!( tag_config.brackets().map(|e| e.1).collect::>(), @@ -2408,8 +2434,13 @@ fn test_language_scope_at_with_javascript(cx: &mut App) { &[Arc::from("// ")] ); assert_eq!( - expression_in_element_config.block_comment_delimiters(), - Some((&"/*".into(), &"*/".into())) + expression_in_element_config.block_comment(), + Some(&BlockCommentConfig { + start: "/*".into(), + prefix: "* ".into(), + end: "*/".into(), + tab_size: 1, + }) ); assert_eq!( expression_in_element_config @@ -2528,13 +2559,18 @@ fn test_language_scope_at_with_combined_injections(cx: &mut App) { let html_config = snapshot.language_scope_at(Point::new(2, 4)).unwrap(); assert_eq!(html_config.line_comment_prefixes(), &[]); assert_eq!( - html_config.block_comment_delimiters(), - Some((&"".into())) + html_config.block_comment(), + Some(&BlockCommentConfig { + start: "".into(), + prefix: "".into(), + tab_size: 0, + }) ); let ruby_config = snapshot.language_scope_at(Point::new(3, 12)).unwrap(); assert_eq!(ruby_config.line_comment_prefixes(), &[Arc::from("# ")]); - assert_eq!(ruby_config.block_comment_delimiters(), None); + assert_eq!(ruby_config.block_comment(), None); buffer }); @@ -3490,7 +3526,12 @@ fn html_lang() -> Language { Language::new( LanguageConfig { name: LanguageName::new("HTML"), - block_comment: Some(("".into())), + block_comment: Some(BlockCommentConfig { + start: "".into(), + tab_size: 0, + }), ..Default::default() }, Some(tree_sitter_html::LANGUAGE.into()), @@ -3521,7 +3562,12 @@ fn erb_lang() -> Language { path_suffixes: vec!["erb".to_string()], ..Default::default() }, - block_comment: Some(("<%#".into(), "%>".into())), + block_comment: Some(BlockCommentConfig { + start: "<%#".into(), + prefix: "".into(), + end: "%>".into(), + tab_size: 0, + }), ..Default::default() }, Some(tree_sitter_embedded_template::LANGUAGE.into()), diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 1ad057ff41eb3eef961d687d9e7ee097c0364c43..1df33286ee93ec158effe37107d9acfbf6a7844e 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -727,9 +727,12 @@ pub struct LanguageConfig { /// used for comment continuations on the next line, but only the first one is used for Editor::ToggleComments. #[serde(default)] pub line_comments: Vec>, - /// Starting and closing characters of a block comment. + /// Delimiters and configuration for recognizing and formatting block comments. #[serde(default)] - pub block_comment: Option<(Arc, Arc)>, + pub block_comment: Option, + /// Delimiters and configuration for recognizing and formatting documentation comments. + #[serde(default, alias = "documentation")] + pub documentation_comment: Option, /// A list of additional regex patterns that should be treated as prefixes /// for creating boundaries during rewrapping, ensuring content from one /// prefixed section doesn't merge with another (e.g., markdown list items). @@ -774,10 +777,6 @@ pub struct LanguageConfig { /// A list of preferred debuggers for this language. #[serde(default)] pub debuggers: IndexSet, - /// Whether to treat documentation comment of this language differently by - /// auto adding prefix on new line, adjusting the indenting , etc. - #[serde(default)] - pub documentation: Option, } #[derive(Clone, Debug, Deserialize, Default, JsonSchema)] @@ -837,17 +836,56 @@ pub struct JsxTagAutoCloseConfig { pub erroneous_close_tag_name_node_name: Option, } -/// The configuration for documentation block for this language. -#[derive(Clone, Deserialize, JsonSchema)] -pub struct DocumentationConfig { - /// A start tag of documentation block. +/// The configuration for block comments for this language. +#[derive(Clone, Debug, JsonSchema, PartialEq)] +pub struct BlockCommentConfig { + /// A start tag of block comment. pub start: Arc, - /// A end tag of documentation block. + /// A end tag of block comment. pub end: Arc, - /// A character to add as a prefix when a new line is added to a documentation block. + /// A character to add as a prefix when a new line is added to a block comment. pub prefix: Arc, /// A indent to add for prefix and end line upon new line. - pub tab_size: NonZeroU32, + pub tab_size: u32, +} + +impl<'de> Deserialize<'de> for BlockCommentConfig { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(untagged)] + enum BlockCommentConfigHelper { + New { + start: Arc, + end: Arc, + prefix: Arc, + tab_size: u32, + }, + Old([Arc; 2]), + } + + match BlockCommentConfigHelper::deserialize(deserializer)? { + BlockCommentConfigHelper::New { + start, + end, + prefix, + tab_size, + } => Ok(BlockCommentConfig { + start, + end, + prefix, + tab_size, + }), + BlockCommentConfigHelper::Old([start, end]) => Ok(BlockCommentConfig { + start, + end, + prefix: "".into(), + tab_size: 0, + }), + } + } } /// Represents a language for the given range. Some languages (e.g. HTML) @@ -864,7 +902,7 @@ pub struct LanguageConfigOverride { #[serde(default)] pub line_comments: Override>>, #[serde(default)] - pub block_comment: Override<(Arc, Arc)>, + pub block_comment: Override, #[serde(skip)] pub disabled_bracket_ixs: Vec, #[serde(default)] @@ -916,6 +954,7 @@ impl Default for LanguageConfig { autoclose_before: Default::default(), line_comments: Default::default(), block_comment: Default::default(), + documentation_comment: Default::default(), rewrap_prefixes: Default::default(), scope_opt_in_language_servers: Default::default(), overrides: Default::default(), @@ -929,7 +968,6 @@ impl Default for LanguageConfig { jsx_tag_auto_close: None, completion_query_characters: Default::default(), debuggers: Default::default(), - documentation: None, } } } @@ -1847,12 +1885,17 @@ impl LanguageScope { .map_or([].as_slice(), |e| e.as_slice()) } - pub fn block_comment_delimiters(&self) -> Option<(&Arc, &Arc)> { + /// Config for block comments for this language. + pub fn block_comment(&self) -> Option<&BlockCommentConfig> { Override::as_option( self.config_override().map(|o| &o.block_comment), self.language.config.block_comment.as_ref(), ) - .map(|e| (&e.0, &e.1)) + } + + /// Config for documentation-style block comments for this language. + pub fn documentation_comment(&self) -> Option<&BlockCommentConfig> { + self.language.config.documentation_comment.as_ref() } /// Returns additional regex patterns that act as prefix markers for creating @@ -1897,14 +1940,6 @@ impl LanguageScope { .unwrap_or(false) } - /// Returns config to documentation block for this language. - /// - /// Used for documentation styles that require a leading character on each line, - /// such as the asterisk in JSDoc, Javadoc, etc. - pub fn documentation(&self) -> Option<&DocumentationConfig> { - self.language.config.documentation.as_ref() - } - /// Returns a list of bracket pairs for a given language with an additional /// piece of information about whether the particular bracket pair is currently active for a given language. pub fn brackets(&self) -> impl Iterator { @@ -2299,6 +2334,7 @@ pub fn range_from_lsp(range: lsp::Range) -> Range> { mod tests { use super::*; use gpui::TestAppContext; + use pretty_assertions::assert_matches; #[gpui::test(iterations = 10)] async fn test_language_loading(cx: &mut TestAppContext) { @@ -2460,4 +2496,75 @@ mod tests { "LSP completion items with duplicate label and detail, should omit the detail" ); } + + #[test] + fn test_deserializing_comments_backwards_compat() { + // current version of `block_comment` and `documentation_comment` work + { + let config: LanguageConfig = ::toml::from_str( + r#" + name = "Foo" + block_comment = { start = "a", end = "b", prefix = "c", tab_size = 1 } + documentation_comment = { start = "d", end = "e", prefix = "f", tab_size = 2 } + "#, + ) + .unwrap(); + assert_matches!(config.block_comment, Some(BlockCommentConfig { .. })); + assert_matches!( + config.documentation_comment, + Some(BlockCommentConfig { .. }) + ); + + let block_config = config.block_comment.unwrap(); + assert_eq!(block_config.start.as_ref(), "a"); + assert_eq!(block_config.end.as_ref(), "b"); + assert_eq!(block_config.prefix.as_ref(), "c"); + assert_eq!(block_config.tab_size, 1); + + let doc_config = config.documentation_comment.unwrap(); + assert_eq!(doc_config.start.as_ref(), "d"); + assert_eq!(doc_config.end.as_ref(), "e"); + assert_eq!(doc_config.prefix.as_ref(), "f"); + assert_eq!(doc_config.tab_size, 2); + } + + // former `documentation` setting is read into `documentation_comment` + { + let config: LanguageConfig = ::toml::from_str( + r#" + name = "Foo" + documentation = { start = "a", end = "b", prefix = "c", tab_size = 1} + "#, + ) + .unwrap(); + assert_matches!( + config.documentation_comment, + Some(BlockCommentConfig { .. }) + ); + + let config = config.documentation_comment.unwrap(); + assert_eq!(config.start.as_ref(), "a"); + assert_eq!(config.end.as_ref(), "b"); + assert_eq!(config.prefix.as_ref(), "c"); + assert_eq!(config.tab_size, 1); + } + + // old block_comment format is read into BlockCommentConfig + { + let config: LanguageConfig = ::toml::from_str( + r#" + name = "Foo" + block_comment = ["a", "b"] + "#, + ) + .unwrap(); + assert_matches!(config.block_comment, Some(BlockCommentConfig { .. })); + + let config = config.block_comment.unwrap(); + assert_eq!(config.start.as_ref(), "a"); + assert_eq!(config.end.as_ref(), "b"); + assert_eq!(config.prefix.as_ref(), ""); + assert_eq!(config.tab_size, 0); + } + } } diff --git a/crates/languages/src/c/config.toml b/crates/languages/src/c/config.toml index 78213da5be43da1ba13e1566a72f552f7db3986c..74290fd9e2b31db93bb62187ab707110c818fc44 100644 --- a/crates/languages/src/c/config.toml +++ b/crates/languages/src/c/config.toml @@ -16,4 +16,4 @@ brackets = [ { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] }, ] debuggers = ["CodeLLDB", "GDB"] -documentation = { start = "/*", end = "*/", prefix = "* ", tab_size = 1 } +documentation_comment = { start = "/*", prefix = "* ", end = "*/", tab_size = 1 } diff --git a/crates/languages/src/cpp/config.toml b/crates/languages/src/cpp/config.toml index 1e283816053f27fa39d985e79bc4e7d89db4477a..fab88266d7444875e29d57a82a770c843d9b2faf 100644 --- a/crates/languages/src/cpp/config.toml +++ b/crates/languages/src/cpp/config.toml @@ -16,4 +16,4 @@ brackets = [ { start = "/*", end = " */", close = true, newline = false, not_in = ["string", "comment"] }, ] debuggers = ["CodeLLDB", "GDB"] -documentation = { start = "/*", end = "*/", prefix = "* ", tab_size = 1 } +documentation_comment = { start = "/*", prefix = "* ", end = "*/", tab_size = 1 } diff --git a/crates/languages/src/css/config.toml b/crates/languages/src/css/config.toml index 0e0b7315e0e1449641c428fc4397d5d39f92f131..a2ca96e76d3427c2ff2eb249d9a2f93a68d8f1c0 100644 --- a/crates/languages/src/css/config.toml +++ b/crates/languages/src/css/config.toml @@ -10,5 +10,5 @@ brackets = [ { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] }, ] completion_query_characters = ["-"] -block_comment = ["/* ", " */"] +block_comment = { start = "/*", prefix = "* ", end = "*/", tab_size = 1 } prettier_parser_name = "css" diff --git a/crates/languages/src/go/config.toml b/crates/languages/src/go/config.toml index 84e35d8f0f7e268c32b9838fd0f6b2907aff909d..0a5122c038e1e38e0c963c3d22581f794656c276 100644 --- a/crates/languages/src/go/config.toml +++ b/crates/languages/src/go/config.toml @@ -15,4 +15,4 @@ brackets = [ tab_size = 4 hard_tabs = true debuggers = ["Delve"] -documentation = { start = "/*", end = "*/", prefix = "* ", tab_size = 1 } +documentation_comment = { start = "/*", prefix = "* ", end = "*/", tab_size = 1 } diff --git a/crates/languages/src/javascript/config.toml b/crates/languages/src/javascript/config.toml index ac87a9befd7af1abcd8153cda07ce10b577cceb8..0df57d985e82595bdabb97517f56e79591343e7b 100644 --- a/crates/languages/src/javascript/config.toml +++ b/crates/languages/src/javascript/config.toml @@ -4,7 +4,8 @@ path_suffixes = ["js", "jsx", "mjs", "cjs"] # [/ ] is so we match "env node" or "/node" but not "ts-node" first_line_pattern = '^#!.*\b(?:[/ ]node|deno run.*--ext[= ]js)\b' line_comments = ["// "] -block_comment = ["/*", "*/"] +block_comment = { start = "/*", prefix = "* ", end = "*/", tab_size = 1 } +documentation_comment = { start = "/**", prefix = "* ", end = "*/", tab_size = 1 } autoclose_before = ";:.,=}])>" brackets = [ { start = "{", end = "}", close = true, newline = true }, @@ -21,7 +22,6 @@ tab_size = 2 scope_opt_in_language_servers = ["tailwindcss-language-server", "emmet-language-server"] prettier_parser_name = "babel" debuggers = ["JavaScript"] -documentation = { start = "/**", end = "*/", prefix = "* ", tab_size = 1 } [jsx_tag_auto_close] open_tag_node_name = "jsx_opening_element" @@ -31,7 +31,7 @@ tag_name_node_name = "identifier" [overrides.element] line_comments = { remove = true } -block_comment = ["{/* ", " */}"] +block_comment = { start = "{/* ", prefix = "", end = "*/}", tab_size = 0 } opt_into_language_servers = ["emmet-language-server"] [overrides.string] diff --git a/crates/languages/src/markdown/config.toml b/crates/languages/src/markdown/config.toml index 059e52de9444b10cb8d6b089a2bdf8ec6d49485d..926dcd70d9f9207c03154690e7d4e9866f9aacea 100644 --- a/crates/languages/src/markdown/config.toml +++ b/crates/languages/src/markdown/config.toml @@ -2,7 +2,7 @@ name = "Markdown" grammar = "markdown" path_suffixes = ["md", "mdx", "mdwn", "markdown", "MD"] completion_query_characters = ["-"] -block_comment = [""] +block_comment = { start = "", tab_size = 0 } autoclose_before = ";:.,=}])>" brackets = [ { start = "{", end = "}", close = true, newline = true }, diff --git a/crates/languages/src/rust/config.toml b/crates/languages/src/rust/config.toml index b55b6da4abdf0cd2eb3da8d5388c172169f53ff9..fe8b4ffdcba4f8b7949b6fe9187d16c8504d6688 100644 --- a/crates/languages/src/rust/config.toml +++ b/crates/languages/src/rust/config.toml @@ -16,4 +16,4 @@ brackets = [ ] collapsed_placeholder = " /* ... */ " debuggers = ["CodeLLDB", "GDB"] -documentation = { start = "/*", end = "*/", prefix = "* ", tab_size = 1 } +documentation_comment = { start = "/*", prefix = "* ", end = "*/", tab_size = 1 } diff --git a/crates/languages/src/tsx/config.toml b/crates/languages/src/tsx/config.toml index 4176e622158089b44cc393a83d25727a2e6efd98..5849b9842fd7f3483f89bbedbdb7b74b3fc1572d 100644 --- a/crates/languages/src/tsx/config.toml +++ b/crates/languages/src/tsx/config.toml @@ -2,7 +2,8 @@ name = "TSX" grammar = "tsx" path_suffixes = ["tsx"] line_comments = ["// "] -block_comment = ["/*", "*/"] +block_comment = { start = "/*", prefix = "* ", end = "*/", tab_size = 1 } +documentation_comment = { start = "/**", prefix = "* ", end = "*/", tab_size = 1 } autoclose_before = ";:.,=}])>" brackets = [ { start = "{", end = "}", close = true, newline = true }, @@ -19,7 +20,6 @@ scope_opt_in_language_servers = ["tailwindcss-language-server", "emmet-language- prettier_parser_name = "typescript" tab_size = 2 debuggers = ["JavaScript"] -documentation = { start = "/**", end = "*/", prefix = "* ", tab_size = 1 } [jsx_tag_auto_close] open_tag_node_name = "jsx_opening_element" @@ -30,7 +30,7 @@ tag_name_node_name_alternates = ["member_expression"] [overrides.element] line_comments = { remove = true } -block_comment = ["{/* ", " */}"] +block_comment = { start = "{/*", prefix = "", end = "*/}", tab_size = 0 } opt_into_language_servers = ["emmet-language-server"] [overrides.string] diff --git a/crates/languages/src/typescript/config.toml b/crates/languages/src/typescript/config.toml index db0f32aa0d767ef2735189df0e520dc566e2c5c6..d7e3e4bd3d1569f96636b7f7572deea306b46df7 100644 --- a/crates/languages/src/typescript/config.toml +++ b/crates/languages/src/typescript/config.toml @@ -3,7 +3,8 @@ grammar = "typescript" path_suffixes = ["ts", "cts", "mts"] first_line_pattern = '^#!.*\b(?:deno run|ts-node|bun|tsx|[/ ]node)\b' line_comments = ["// "] -block_comment = ["/*", "*/"] +block_comment = { start = "/*", prefix = "* ", end = "*/", tab_size = 1 } +documentation_comment = { start = "/**", prefix = "* ", end = "*/", tab_size = 1 } autoclose_before = ";:.,=}])>" brackets = [ { start = "{", end = "}", close = true, newline = true }, @@ -19,7 +20,6 @@ word_characters = ["#", "$"] prettier_parser_name = "typescript" tab_size = 2 debuggers = ["JavaScript"] -documentation = { start = "/**", end = "*/", prefix = "* ", tab_size = 1 } [overrides.string] completion_query_characters = ["."] diff --git a/extensions/glsl/languages/glsl/config.toml b/extensions/glsl/languages/glsl/config.toml index 0144e981cc4d446192c4e433c6c5cc2c3929bb4a..0c71419c91e40f4b5fc65c10c882ac5c542a080c 100644 --- a/extensions/glsl/languages/glsl/config.toml +++ b/extensions/glsl/languages/glsl/config.toml @@ -12,7 +12,7 @@ path_suffixes = [ ] first_line_pattern = '^#version \d+' line_comments = ["// "] -block_comment = ["/* ", " */"] +block_comment = { start = "/* ", prefix = "* ", end = "*/", tab_size = 1 } brackets = [ { start = "{", end = "}", close = true, newline = true }, { start = "[", end = "]", close = true, newline = true }, diff --git a/extensions/html/languages/html/config.toml b/extensions/html/languages/html/config.toml index 6f52cc8f65e85bb0ec4ab0c8a32ba2f89bf41361..f74db2888eb71e6e9f9afcbb1b41ab98e232a7a7 100644 --- a/extensions/html/languages/html/config.toml +++ b/extensions/html/languages/html/config.toml @@ -2,7 +2,7 @@ name = "HTML" grammar = "html" path_suffixes = ["html", "htm", "shtml"] autoclose_before = ">})" -block_comment = [""] +block_comment = { start = "", tab_size = 0 } brackets = [ { start = "{", end = "}", close = true, newline = true }, { start = "[", end = "]", close = true, newline = true }, From 326fe05b331118cdb7d630f04c379dd0facb28ad Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 23 Jul 2025 08:44:45 -0700 Subject: [PATCH 285/658] Resizable columns (#34794) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds resizable columns to the keymap editor and the ability to double-click on a resizable column to set a column back to its default size. The table uses a column's width to calculate what position it should be laid out at. So `column[i]` x position is calculated by the summation of `column[..i]`. When resizing `column[i]`, `column[i+1]`’s size is adjusted to keep all columns’ relative positions the same. If `column[i+1]` is at its minimum size, we keep seeking to the right to find a column with space left to take. An improvement to resizing behavior and double-clicking could be made by checking both column ranges `0..i-1` and `i+1..COLS`, since only one range of columns is checked for resize capacity. Release Notes: - N/A --------- Co-authored-by: Anthony Co-authored-by: Remco Smits --- Cargo.lock | 1 + crates/settings_ui/Cargo.toml | 1 + crates/settings_ui/src/keybindings.rs | 30 +- crates/settings_ui/src/ui_components/table.rs | 455 ++++++++++++++++-- crates/workspace/src/pane_group.rs | 11 + 5 files changed, 449 insertions(+), 49 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6c346e331e951352c49425daa5533226a40f4852..765ae002498ba4dd3811d3157f29728b62276f3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14779,6 +14779,7 @@ dependencies = [ "fs", "fuzzy", "gpui", + "itertools 0.14.0", "language", "log", "menu", diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml index 651397dd51b1b2406cc4149f0951d3f506b73689..02327045fdb2279597342a5d838d356d7b738c73 100644 --- a/crates/settings_ui/Cargo.toml +++ b/crates/settings_ui/Cargo.toml @@ -23,6 +23,7 @@ feature_flags.workspace = true fs.workspace = true fuzzy.workspace = true gpui.workspace = true +itertools.workspace = true language.workspace = true log.workspace = true menu.workspace = true diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 9e885f69f6efde4ff7636b1d682a19c515a09bc6..8fdacf7ae81c88351e0133f15d12d6ad89953724 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -13,8 +13,8 @@ use gpui::{ Action, Animation, AnimationExt, AppContext as _, AsyncApp, Axis, ClickEvent, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Global, IsZero, KeyContext, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, Point, ScrollStrategy, - ScrollWheelEvent, StyledText, Subscription, Task, TextStyleRefinement, WeakEntity, actions, - anchored, deferred, div, + ScrollWheelEvent, Stateful, StyledText, Subscription, Task, TextStyleRefinement, WeakEntity, + actions, anchored, deferred, div, }; use language::{Language, LanguageConfig, ToOffset as _}; use notifications::status_toast::{StatusToast, ToastIcon}; @@ -36,7 +36,7 @@ use workspace::{ use crate::{ keybindings::persistence::KEYBINDING_EDITORS, - ui_components::table::{Table, TableInteractionState}, + ui_components::table::{ColumnWidths, ResizeBehavior, Table, TableInteractionState}, }; const NO_ACTION_ARGUMENTS_TEXT: SharedString = SharedString::new_static(""); @@ -284,6 +284,7 @@ struct KeymapEditor { context_menu: Option<(Entity, Point, Subscription)>, previous_edit: Option, humanized_action_names: HumanizedActionNameCache, + current_widths: Entity>, show_hover_menus: bool, /// In order for the JSON LSP to run in the actions arguments editor, we /// require a backing file In order to avoid issues (primarily log spam) @@ -400,6 +401,7 @@ impl KeymapEditor { show_hover_menus: true, action_args_temp_dir: None, action_args_temp_dir_worktree: None, + current_widths: cx.new(|cx| ColumnWidths::new(cx)), }; this.on_keymap_changed(window, cx); @@ -1433,6 +1435,18 @@ impl Render for KeymapEditor { DefiniteLength::Fraction(0.45), DefiniteLength::Fraction(0.08), ]) + .resizable_columns( + [ + ResizeBehavior::None, + ResizeBehavior::Resizable, + ResizeBehavior::Resizable, + ResizeBehavior::Resizable, + ResizeBehavior::Resizable, + ResizeBehavior::Resizable, // this column doesn't matter + ], + &self.current_widths, + cx, + ) .header(["", "Action", "Arguments", "Keystrokes", "Context", "Source"]) .uniform_list( "keymap-editor-table", @@ -1594,15 +1608,14 @@ impl Render for KeymapEditor { .collect() }), ) - .map_row( - cx.processor(|this, (row_index, row): (usize, Div), _window, cx| { + .map_row(cx.processor( + |this, (row_index, row): (usize, Stateful
    ), _window, cx| { let is_conflict = this.has_conflict(row_index); let is_selected = this.selected_index == Some(row_index); let row_id = row_group_id(row_index); let row = row - .id(row_id.clone()) .on_any_mouse_down(cx.listener( move |this, mouse_down_event: &gpui::MouseDownEvent, @@ -1636,11 +1649,12 @@ impl Render for KeymapEditor { }) .when(is_selected, |row| { row.border_color(cx.theme().colors().panel_focused_border) + .border_2() }); row.into_any_element() - }), - ), + }, + )), ) .on_scroll_wheel(cx.listener(|this, event: &ScrollWheelEvent, _, cx| { // This ensures that the menu is not dismissed in cases where scroll events diff --git a/crates/settings_ui/src/ui_components/table.rs b/crates/settings_ui/src/ui_components/table.rs index 6ea59cd2f42eb570237465430ccffbf8f753b16f..70472918d2b7cbe199922c53d3208daf45062825 100644 --- a/crates/settings_ui/src/ui_components/table.rs +++ b/crates/settings_ui/src/ui_components/table.rs @@ -2,19 +2,24 @@ use std::{ops::Range, rc::Rc, time::Duration}; use editor::{EditorSettings, ShowScrollbar, scroll::ScrollbarAutoHide}; use gpui::{ - AppContext, Axis, Context, Entity, FocusHandle, Length, ListHorizontalSizingBehavior, - ListSizingBehavior, MouseButton, Point, Task, UniformListScrollHandle, WeakEntity, - transparent_black, uniform_list, + AbsoluteLength, AppContext, Axis, Context, DefiniteLength, DragMoveEvent, Entity, FocusHandle, + Length, ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton, Point, Stateful, Task, + UniformListScrollHandle, WeakEntity, transparent_black, uniform_list, }; + +use itertools::intersperse_with; use settings::Settings as _; use ui::{ ActiveTheme as _, AnyElement, App, Button, ButtonCommon as _, ButtonStyle, Color, Component, ComponentScope, Div, ElementId, FixedWidth as _, FluentBuilder as _, Indicator, - InteractiveElement as _, IntoElement, ParentElement, Pixels, RegisterComponent, RenderOnce, - Scrollbar, ScrollbarState, StatefulInteractiveElement as _, Styled, StyledExt as _, + InteractiveElement, IntoElement, ParentElement, Pixels, RegisterComponent, RenderOnce, + Scrollbar, ScrollbarState, StatefulInteractiveElement, Styled, StyledExt as _, StyledTypography, Window, div, example_group_with_title, h_flex, px, single_example, v_flex, }; +#[derive(Debug)] +struct DraggedColumn(usize); + struct UniformListData { render_item_fn: Box, &mut Window, &mut App) -> Vec<[AnyElement; COLS]>>, element_id: ElementId, @@ -191,6 +196,87 @@ impl TableInteractionState { } } + fn render_resize_handles( + &self, + column_widths: &[Length; COLS], + resizable_columns: &[ResizeBehavior; COLS], + initial_sizes: [DefiniteLength; COLS], + columns: Option>>, + window: &mut Window, + cx: &mut App, + ) -> AnyElement { + let spacers = column_widths + .iter() + .map(|width| base_cell_style(Some(*width)).into_any_element()); + + let mut column_ix = 0; + let resizable_columns_slice = *resizable_columns; + let mut resizable_columns = resizable_columns.into_iter(); + let dividers = intersperse_with(spacers, || { + window.with_id(column_ix, |window| { + let mut resize_divider = div() + // This is required because this is evaluated at a different time than the use_state call above + .id(column_ix) + .relative() + .top_0() + .w_0p5() + .h_full() + .bg(cx.theme().colors().border.opacity(0.5)); + + let mut resize_handle = div() + .id("column-resize-handle") + .absolute() + .left_neg_0p5() + .w(px(5.0)) + .h_full(); + + if resizable_columns + .next() + .is_some_and(ResizeBehavior::is_resizable) + { + let hovered = window.use_state(cx, |_window, _cx| false); + resize_divider = resize_divider.when(*hovered.read(cx), |div| { + div.bg(cx.theme().colors().border_focused) + }); + resize_handle = resize_handle + .on_hover(move |&was_hovered, _, cx| hovered.write(cx, was_hovered)) + .cursor_col_resize() + .when_some(columns.clone(), |this, columns| { + this.on_click(move |event, window, cx| { + if event.down.click_count >= 2 { + columns.update(cx, |columns, _| { + columns.on_double_click( + column_ix, + &initial_sizes, + &resizable_columns_slice, + window, + ); + }) + } + + cx.stop_propagation(); + }) + }) + .on_drag(DraggedColumn(column_ix), |_, _offset, _window, cx| { + cx.new(|_cx| gpui::Empty) + }) + } + + column_ix += 1; + resize_divider.child(resize_handle).into_any_element() + }) + }); + + div() + .id("resize-handles") + .h_flex() + .absolute() + .w_full() + .inset_0() + .children(dividers) + .into_any_element() + } + fn render_vertical_scrollbar_track( this: &Entity, parent: Div, @@ -369,6 +455,217 @@ impl TableInteractionState { } } +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum ResizeBehavior { + None, + Resizable, + MinSize(f32), +} + +impl ResizeBehavior { + pub fn is_resizable(&self) -> bool { + *self != ResizeBehavior::None + } + + pub fn min_size(&self) -> Option { + match self { + ResizeBehavior::None => None, + ResizeBehavior::Resizable => Some(0.05), + ResizeBehavior::MinSize(min_size) => Some(*min_size), + } + } +} + +pub struct ColumnWidths { + widths: [DefiniteLength; COLS], + cached_bounds_width: Pixels, + initialized: bool, +} + +impl ColumnWidths { + pub fn new(_: &mut App) -> Self { + Self { + widths: [DefiniteLength::default(); COLS], + cached_bounds_width: Default::default(), + initialized: false, + } + } + + fn get_fraction(length: &DefiniteLength, bounds_width: Pixels, rem_size: Pixels) -> f32 { + match length { + DefiniteLength::Absolute(AbsoluteLength::Pixels(pixels)) => *pixels / bounds_width, + DefiniteLength::Absolute(AbsoluteLength::Rems(rems_width)) => { + rems_width.to_pixels(rem_size) / bounds_width + } + DefiniteLength::Fraction(fraction) => *fraction, + } + } + + fn on_double_click( + &mut self, + double_click_position: usize, + initial_sizes: &[DefiniteLength; COLS], + resize_behavior: &[ResizeBehavior; COLS], + window: &mut Window, + ) { + let bounds_width = self.cached_bounds_width; + let rem_size = window.rem_size(); + + let diff = + Self::get_fraction( + &initial_sizes[double_click_position], + bounds_width, + rem_size, + ) - Self::get_fraction(&self.widths[double_click_position], bounds_width, rem_size); + + let mut curr_column = double_click_position + 1; + let mut diff_left = diff; + + while diff != 0.0 && curr_column < COLS { + let Some(min_size) = resize_behavior[curr_column].min_size() else { + curr_column += 1; + continue; + }; + + let mut curr_width = + Self::get_fraction(&self.widths[curr_column], bounds_width, rem_size) - diff_left; + + diff_left = 0.0; + if min_size > curr_width { + diff_left += min_size - curr_width; + curr_width = min_size; + } + self.widths[curr_column] = DefiniteLength::Fraction(curr_width); + curr_column += 1; + } + + self.widths[double_click_position] = DefiniteLength::Fraction( + Self::get_fraction(&self.widths[double_click_position], bounds_width, rem_size) + + (diff - diff_left), + ); + } + + fn on_drag_move( + &mut self, + drag_event: &DragMoveEvent, + resize_behavior: &[ResizeBehavior; COLS], + window: &mut Window, + cx: &mut Context, + ) { + // - [ ] Fix bugs in resize + let drag_position = drag_event.event.position; + let bounds = drag_event.bounds; + + let mut col_position = 0.0; + let rem_size = window.rem_size(); + let bounds_width = bounds.right() - bounds.left(); + let col_idx = drag_event.drag(cx).0; + + for length in self.widths[0..=col_idx].iter() { + col_position += Self::get_fraction(length, bounds_width, rem_size); + } + + let mut total_length_ratio = col_position; + for length in self.widths[col_idx + 1..].iter() { + total_length_ratio += Self::get_fraction(length, bounds_width, rem_size); + } + + let drag_fraction = (drag_position.x - bounds.left()) / bounds_width; + let drag_fraction = drag_fraction * total_length_ratio; + let diff = drag_fraction - col_position; + + let is_dragging_right = diff > 0.0; + + let mut diff_left = diff; + let mut curr_column = col_idx + 1; + + if is_dragging_right { + while diff_left > 0.0 && curr_column < COLS { + let Some(min_size) = resize_behavior[curr_column - 1].min_size() else { + curr_column += 1; + continue; + }; + + let mut curr_width = + Self::get_fraction(&self.widths[curr_column], bounds_width, rem_size) + - diff_left; + + diff_left = 0.0; + if min_size > curr_width { + diff_left += min_size - curr_width; + curr_width = min_size; + } + self.widths[curr_column] = DefiniteLength::Fraction(curr_width); + curr_column += 1; + } + + self.widths[col_idx] = DefiniteLength::Fraction( + Self::get_fraction(&self.widths[col_idx], bounds_width, rem_size) + + (diff - diff_left), + ); + } else { + curr_column = col_idx; + // Resize behavior should be improved in the future by also seeking to the right column when there's not enough space + while diff_left < 0.0 { + let Some(min_size) = resize_behavior[curr_column.saturating_sub(1)].min_size() + else { + if curr_column == 0 { + break; + } + curr_column -= 1; + continue; + }; + + let mut curr_width = + Self::get_fraction(&self.widths[curr_column], bounds_width, rem_size) + + diff_left; + + diff_left = 0.0; + if curr_width < min_size { + diff_left = curr_width - min_size; + curr_width = min_size + } + + self.widths[curr_column] = DefiniteLength::Fraction(curr_width); + if curr_column == 0 { + break; + } + curr_column -= 1; + } + + self.widths[col_idx + 1] = DefiniteLength::Fraction( + Self::get_fraction(&self.widths[col_idx + 1], bounds_width, rem_size) + - (diff - diff_left), + ); + } + } +} + +pub struct TableWidths { + initial: [DefiniteLength; COLS], + current: Option>>, + resizable: [ResizeBehavior; COLS], +} + +impl TableWidths { + pub fn new(widths: [impl Into; COLS]) -> Self { + let widths = widths.map(Into::into); + + TableWidths { + initial: widths, + current: None, + resizable: [ResizeBehavior::None; COLS], + } + } + + fn lengths(&self, cx: &App) -> [Length; COLS] { + self.current + .as_ref() + .map(|entity| entity.read(cx).widths.map(Length::Definite)) + .unwrap_or(self.initial.map(Length::Definite)) + } +} + /// A table component #[derive(RegisterComponent, IntoElement)] pub struct Table { @@ -377,23 +674,23 @@ pub struct Table { headers: Option<[AnyElement; COLS]>, rows: TableContents, interaction_state: Option>, - column_widths: Option<[Length; COLS]>, - map_row: Option AnyElement>>, + col_widths: Option>, + map_row: Option), &mut Window, &mut App) -> AnyElement>>, empty_table_callback: Option AnyElement>>, } impl Table { /// number of headers provided. pub fn new() -> Self { - Table { + Self { striped: false, width: None, headers: None, rows: TableContents::Vec(Vec::new()), interaction_state: None, - column_widths: None, map_row: None, empty_table_callback: None, + col_widths: None, } } @@ -454,14 +751,38 @@ impl Table { self } - pub fn column_widths(mut self, widths: [impl Into; COLS]) -> Self { - self.column_widths = Some(widths.map(Into::into)); + pub fn column_widths(mut self, widths: [impl Into; COLS]) -> Self { + if self.col_widths.is_none() { + self.col_widths = Some(TableWidths::new(widths)); + } + self + } + + pub fn resizable_columns( + mut self, + resizable: [ResizeBehavior; COLS], + column_widths: &Entity>, + cx: &mut App, + ) -> Self { + if let Some(table_widths) = self.col_widths.as_mut() { + table_widths.resizable = resizable; + let column_widths = table_widths + .current + .get_or_insert_with(|| column_widths.clone()); + + column_widths.update(cx, |widths, _| { + if !widths.initialized { + widths.initialized = true; + widths.widths = table_widths.initial; + } + }) + } self } pub fn map_row( mut self, - callback: impl Fn((usize, Div), &mut Window, &mut App) -> AnyElement + 'static, + callback: impl Fn((usize, Stateful
    ), &mut Window, &mut App) -> AnyElement + 'static, ) -> Self { self.map_row = Some(Rc::new(callback)); self @@ -477,18 +798,21 @@ impl Table { } } -fn base_cell_style(width: Option, cx: &App) -> Div { +fn base_cell_style(width: Option) -> Div { div() .px_1p5() .when_some(width, |this, width| this.w(width)) .when(width.is_none(), |this| this.flex_1()) .justify_start() - .text_ui(cx) .whitespace_nowrap() .text_ellipsis() .overflow_hidden() } +fn base_cell_style_text(width: Option, cx: &App) -> Div { + base_cell_style(width).text_ui(cx) +} + pub fn render_row( row_index: usize, items: [impl IntoElement; COLS], @@ -507,33 +831,33 @@ pub fn render_row( .column_widths .map_or([None; COLS], |widths| widths.map(Some)); - let row = div().w_full().child( - h_flex() - .id("table_row") - .w_full() - .justify_between() - .px_1p5() - .py_1() - .when_some(bg, |row, bg| row.bg(bg)) - .when(!is_striped, |row| { - row.border_b_1() - .border_color(transparent_black()) - .when(!is_last, |row| row.border_color(cx.theme().colors().border)) - }) - .children( - items - .map(IntoElement::into_any_element) - .into_iter() - .zip(column_widths) - .map(|(cell, width)| base_cell_style(width, cx).child(cell)), - ), + let mut row = h_flex() + .h_full() + .id(("table_row", row_index)) + .w_full() + .justify_between() + .when_some(bg, |row, bg| row.bg(bg)) + .when(!is_striped, |row| { + row.border_b_1() + .border_color(transparent_black()) + .when(!is_last, |row| row.border_color(cx.theme().colors().border)) + }); + + row = row.children( + items + .map(IntoElement::into_any_element) + .into_iter() + .zip(column_widths) + .map(|(cell, width)| base_cell_style_text(width, cx).px_1p5().py_1().child(cell)), ); - if let Some(map_row) = table_context.map_row { + let row = if let Some(map_row) = table_context.map_row { map_row((row_index, row), window, cx) } else { row.into_any_element() - } + }; + + div().h_full().w_full().child(row).into_any_element() } pub fn render_header( @@ -557,7 +881,7 @@ pub fn render_header( headers .into_iter() .zip(column_widths) - .map(|(h, width)| base_cell_style(width, cx).child(h)), + .map(|(h, width)| base_cell_style_text(width, cx).child(h)), ) } @@ -566,15 +890,15 @@ pub struct TableRenderContext { pub striped: bool, pub total_row_count: usize, pub column_widths: Option<[Length; COLS]>, - pub map_row: Option AnyElement>>, + pub map_row: Option), &mut Window, &mut App) -> AnyElement>>, } impl TableRenderContext { - fn new(table: &Table) -> Self { + fn new(table: &Table, cx: &App) -> Self { Self { striped: table.striped, total_row_count: table.rows.len(), - column_widths: table.column_widths, + column_widths: table.col_widths.as_ref().map(|widths| widths.lengths(cx)), map_row: table.map_row.clone(), } } @@ -582,8 +906,13 @@ impl TableRenderContext { impl RenderOnce for Table { fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let table_context = TableRenderContext::new(&self); + let table_context = TableRenderContext::new(&self, cx); let interaction_state = self.interaction_state.and_then(|state| state.upgrade()); + let current_widths = self + .col_widths + .as_ref() + .and_then(|widths| Some((widths.current.as_ref()?, widths.resizable))) + .map(|(curr, resize_behavior)| (curr.downgrade(), resize_behavior)); let scroll_track_size = px(16.); let h_scroll_offset = if interaction_state @@ -606,6 +935,31 @@ impl RenderOnce for Table { .when_some(self.headers.take(), |this, headers| { this.child(render_header(headers, table_context.clone(), cx)) }) + .when_some(current_widths, { + |this, (widths, resize_behavior)| { + this.on_drag_move::({ + let widths = widths.clone(); + move |e, window, cx| { + widths + .update(cx, |widths, cx| { + widths.on_drag_move(e, &resize_behavior, window, cx); + }) + .ok(); + } + }) + .on_children_prepainted(move |bounds, _, cx| { + widths + .update(cx, |widths, _| { + // This works because all children x axis bounds are the same + widths.cached_bounds_width = bounds[0].right() - bounds[0].left(); + }) + .ok(); + }) + } + }) + .on_drop::(|_, _, _| { + // Finish the resize operation + }) .child( div() .flex_grow() @@ -660,6 +1014,25 @@ impl RenderOnce for Table { ), ), }) + .when_some( + self.col_widths.as_ref().zip(interaction_state.as_ref()), + |parent, (table_widths, state)| { + parent.child(state.update(cx, |state, cx| { + let resizable_columns = table_widths.resizable; + let column_widths = table_widths.lengths(cx); + let columns = table_widths.current.clone(); + let initial_sizes = table_widths.initial; + state.render_resize_handles( + &column_widths, + &resizable_columns, + initial_sizes, + columns, + window, + cx, + ) + })) + }, + ) .when_some(interaction_state.as_ref(), |this, interaction_state| { this.map(|this| { TableInteractionState::render_vertical_scrollbar_track( diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 4565cef34719cdf3d4c506e7ba73dedb8cc6e3de..5c87206e9e96cf3866b183684d981b02692d039f 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -943,6 +943,8 @@ mod element { pub struct PaneAxisElement { axis: Axis, basis: usize, + /// Equivalent to ColumnWidths (but in terms of flexes instead of percentages) + /// For example, flexes "1.33, 1, 1", instead of "40%, 30%, 30%" flexes: Arc>>, bounding_boxes: Arc>>>>, children: SmallVec<[AnyElement; 2]>, @@ -998,6 +1000,7 @@ mod element { let mut flexes = flexes.lock(); debug_assert!(flex_values_in_bounds(flexes.as_slice())); + // Math to convert a flex value to a pixel value let size = move |ix, flexes: &[f32]| { container_size.along(axis) * (flexes[ix] / flexes.len() as f32) }; @@ -1007,9 +1010,13 @@ mod element { return; } + // This is basically a "bucket" of pixel changes that need to be applied in response to this + // mouse event. Probably a small, fractional number like 0.5 or 1.5 pixels let mut proposed_current_pixel_change = (e.position - child_start).along(axis) - size(ix, flexes.as_slice()); + // This takes a pixel change, and computes the flex changes that correspond to this pixel change + // as well as the next one, for some reason let flex_changes = |pixel_dx, target_ix, next: isize, flexes: &[f32]| { let flex_change = pixel_dx / container_size.along(axis); let current_target_flex = flexes[target_ix] + flex_change; @@ -1017,6 +1024,9 @@ mod element { (current_target_flex, next_target_flex) }; + // Generate the list of flex successors, from the current index. + // If you're dragging column 3 forward, out of 6 columns, then this code will produce [4, 5, 6] + // If you're dragging column 3 backward, out of 6 columns, then this code will produce [2, 1, 0] let mut successors = iter::from_fn({ let forward = proposed_current_pixel_change > px(0.); let mut ix_offset = 0; @@ -1034,6 +1044,7 @@ mod element { } }); + // Now actually loop over these, and empty our bucket of pixel changes while proposed_current_pixel_change.abs() > px(0.) { let Some(current_ix) = successors.next() else { break; From 8713c556d660b7cdacda236bda373aa39bde589c Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Wed, 23 Jul 2025 18:03:04 +0200 Subject: [PATCH 286/658] keymap_ui: Dim keybinds that are overridden by other keybinds (#34952) This change dims rows in the keymap editor for which the corresponding keybind is overridden by other keybinds coming from higher priority sources. Release Notes: - N/A --- assets/keymaps/default-linux.json | 5 +- assets/keymaps/default-macos.json | 5 +- crates/settings/src/keymap_file.rs | 30 +- crates/settings_ui/src/keybindings.rs | 907 ++++++++++++++++---------- 4 files changed, 601 insertions(+), 346 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index b097be90fdbe8a8cd9cd821ef9df2c3f9ccaf26a..31adef8cd595bfa4010919dbd29a0ed6c470a1f0 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -1137,7 +1137,10 @@ "alt-ctrl-f": "keymap_editor::ToggleKeystrokeSearch", "alt-c": "keymap_editor::ToggleConflictFilter", "enter": "keymap_editor::EditBinding", - "alt-enter": "keymap_editor::CreateBinding" + "alt-enter": "keymap_editor::CreateBinding", + "ctrl-c": "keymap_editor::CopyAction", + "ctrl-shift-c": "keymap_editor::CopyContext", + "ctrl-t": "keymap_editor::ShowMatchingKeybinds" } }, { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index e33786f1b2bda9807dc1a46c401db14dd605e9e4..f942c6f8ae1daa830aa10c473b76a4a99dd8320f 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -1239,7 +1239,10 @@ "cmd-alt-f": "keymap_editor::ToggleKeystrokeSearch", "cmd-alt-c": "keymap_editor::ToggleConflictFilter", "enter": "keymap_editor::EditBinding", - "alt-enter": "keymap_editor::CreateBinding" + "alt-enter": "keymap_editor::CreateBinding", + "cmd-c": "keymap_editor::CopyAction", + "cmd-shift-c": "keymap_editor::CopyContext", + "cmd-t": "keymap_editor::ShowMatchingKeybinds" } }, { diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 67e8f7e7b2a503ce037c75745b2656c968f9b897..7802671fecdcafe26a22057b8484ddfcbe7556fd 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -959,19 +959,21 @@ impl<'a> KeybindUpdateTarget<'a> { } } -#[derive(Clone, Copy, PartialEq, Eq)] +#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)] pub enum KeybindSource { User, - Default, - Base, Vim, + Base, + #[default] + Default, + Unknown, } impl KeybindSource { - const BASE: KeyBindingMetaIndex = KeyBindingMetaIndex(0); - const DEFAULT: KeyBindingMetaIndex = KeyBindingMetaIndex(1); - const VIM: KeyBindingMetaIndex = KeyBindingMetaIndex(2); - const USER: KeyBindingMetaIndex = KeyBindingMetaIndex(3); + const BASE: KeyBindingMetaIndex = KeyBindingMetaIndex(KeybindSource::Base as u32); + const DEFAULT: KeyBindingMetaIndex = KeyBindingMetaIndex(KeybindSource::Default as u32); + const VIM: KeyBindingMetaIndex = KeyBindingMetaIndex(KeybindSource::Vim as u32); + const USER: KeyBindingMetaIndex = KeyBindingMetaIndex(KeybindSource::User as u32); pub fn name(&self) -> &'static str { match self { @@ -979,6 +981,7 @@ impl KeybindSource { KeybindSource::Default => "Default", KeybindSource::Base => "Base", KeybindSource::Vim => "Vim", + KeybindSource::Unknown => "Unknown", } } @@ -988,21 +991,18 @@ impl KeybindSource { KeybindSource::Default => Self::DEFAULT, KeybindSource::Base => Self::BASE, KeybindSource::Vim => Self::VIM, + KeybindSource::Unknown => KeyBindingMetaIndex(*self as u32), } } pub fn from_meta(index: KeyBindingMetaIndex) -> Self { - Self::try_from_meta(index).unwrap() - } - - pub fn try_from_meta(index: KeyBindingMetaIndex) -> Result { - Ok(match index { + match index { Self::USER => KeybindSource::User, Self::BASE => KeybindSource::Base, Self::DEFAULT => KeybindSource::Default, Self::VIM => KeybindSource::Vim, - _ => anyhow::bail!("Invalid keybind source {:?}", index), - }) + _ => KeybindSource::Unknown, + } } } @@ -1014,7 +1014,7 @@ impl From for KeybindSource { impl From for KeyBindingMetaIndex { fn from(source: KeybindSource) -> Self { - return source.meta(); + source.meta() } } diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 8fdacf7ae81c88351e0133f15d12d6ad89953724..a0cbdb9680b59f5faa8c8e5c33399762b9f286b4 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -1,4 +1,5 @@ use std::{ + cmp::{self}, ops::{Not as _, Range}, sync::Arc, time::Duration, @@ -20,15 +21,13 @@ use language::{Language, LanguageConfig, ToOffset as _}; use notifications::status_toast::{StatusToast, ToastIcon}; use project::Project; use settings::{BaseKeymap, KeybindSource, KeymapFile, Settings as _, SettingsAssets}; - -use util::ResultExt; - use ui::{ ActiveTheme as _, App, Banner, BorrowAppContext, ContextMenu, IconButtonShape, Indicator, Modal, ModalFooter, ModalHeader, ParentElement as _, Render, Section, SharedString, Styled as _, Tooltip, Window, prelude::*, }; use ui_input::SingleLineInput; +use util::ResultExt; use workspace::{ Item, ModalView, SerializableItem, Workspace, notifications::NotifyTaskExt as _, register_serializable_item, @@ -68,6 +67,8 @@ actions!( ToggleKeystrokeSearch, /// Toggles exact matching for keystroke search ToggleExactKeystrokeMatching, + /// Shows matching keystrokes for the currently selected binding + ShowMatchingKeybinds ] ); @@ -192,76 +193,134 @@ struct KeybindConflict { } impl KeybindConflict { - fn from_iter<'a>(mut indices: impl Iterator) -> Option { - indices.next().map(|index| Self { - first_conflict_index: *index, + 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, + overridden_source: Option, + index: usize, +} + +impl ConflictOrigin { + fn new(source: KeybindSource, index: usize) -> Self { + Self { + override_source: source, + index, + overridden_source: None, + } + } + + fn with_overridden_source(self, source: KeybindSource) -> Self { + Self { + overridden_source: Some(source), + ..self + } + } + + fn get_conflict_with(&self, other: &Self) -> Option { + if self.override_source == KeybindSource::User + && other.override_source == KeybindSource::User + { + Some( + Self::new(KeybindSource::User, other.index) + .with_overridden_source(self.override_source), + ) + } else if self.override_source > other.override_source { + Some(other.with_overridden_source(self.override_source)) + } else { + None + } + } + + fn is_user_keybind_conflict(&self) -> bool { + self.override_source == KeybindSource::User + && self.overridden_source == Some(KeybindSource::User) + } +} + #[derive(Default)] struct ConflictState { - conflicts: Vec, - keybind_mapping: HashMap>, + conflicts: Vec>, + keybind_mapping: HashMap>, + has_user_conflicts: bool, } impl ConflictState { - fn new(key_bindings: &[ProcessedKeybinding]) -> Self { - let mut action_keybind_mapping: HashMap<_, Vec> = HashMap::default(); + fn new(key_bindings: &[ProcessedBinding]) -> Self { + let mut action_keybind_mapping: HashMap<_, Vec> = HashMap::default(); - key_bindings + let mut largest_index = 0; + for (index, binding) in key_bindings .iter() .enumerate() - .filter(|(_, binding)| { - binding.keystrokes().is_some() - && binding - .source - .as_ref() - .is_some_and(|source| matches!(source.0, KeybindSource::User)) - }) - .for_each(|(index, binding)| { - action_keybind_mapping - .entry(binding.get_action_mapping()) - .or_default() - .push(index); - }); + .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)); + 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 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; + } Self { - conflicts: action_keybind_mapping - .values() - .filter(|indices| indices.len() > 1) - .flatten() - .copied() - .collect(), + conflicts, keybind_mapping: action_keybind_mapping, + has_user_conflicts, } } fn conflicting_indices_for_mapping( &self, action_mapping: &ActionMapping, - keybind_idx: usize, + keybind_idx: Option, ) -> Option { self.keybind_mapping .get(action_mapping) .and_then(|indices| { - KeybindConflict::from_iter(indices.iter().filter(|&idx| *idx != keybind_idx)) + KeybindConflict::from_iter( + indices + .iter() + .filter(|&conflict| Some(conflict.index) != keybind_idx), + ) }) } - fn will_conflict(&self, action_mapping: &ActionMapping) -> Option { - self.keybind_mapping - .get(action_mapping) - .and_then(|indices| KeybindConflict::from_iter(indices.iter())) + fn conflict_for_idx(&self, idx: usize) -> Option { + self.conflicts.get(idx).copied().flatten() } - fn has_conflict(&self, candidate_idx: &usize) -> bool { - self.conflicts.contains(candidate_idx) + fn has_user_conflict(&self, candidate_idx: usize) -> bool { + self.conflict_for_idx(candidate_idx) + .is_some_and(|conflict| conflict.is_user_keybind_conflict()) } - fn any_conflicts(&self) -> bool { - !self.conflicts.is_empty() + fn any_user_binding_conflicts(&self) -> bool { + self.has_user_conflicts } } @@ -269,7 +328,7 @@ struct KeymapEditor { workspace: WeakEntity, focus_handle: FocusHandle, _keymap_subscription: Subscription, - keybindings: Vec, + keybindings: Vec, keybinding_conflict_state: ConflictState, filter_state: FilterState, search_mode: SearchMode, @@ -426,24 +485,6 @@ impl KeymapEditor { } } - fn filter_on_selected_binding_keystrokes(&mut self, cx: &mut Context) { - let Some(selected_binding) = self.selected_binding() else { - return; - }; - - let keystrokes = selected_binding - .keystrokes() - .map(Vec::from) - .unwrap_or_default(); - - self.filter_state = FilterState::All; - self.search_mode = SearchMode::KeyStroke { exact_match: true }; - - self.keystroke_editor.update(cx, |editor, cx| { - editor.set_keystrokes(keystrokes, cx); - }); - } - fn on_query_changed(&mut self, cx: &mut Context) { let action_query = self.current_action_query(cx); let keystroke_query = self.current_keystroke_query(cx); @@ -505,7 +546,7 @@ impl KeymapEditor { FilterState::Conflicts => { matches.retain(|candidate| { this.keybinding_conflict_state - .has_conflict(&candidate.candidate_id) + .has_user_conflict(candidate.candidate_id) }); } FilterState::All => {} @@ -551,20 +592,11 @@ impl KeymapEditor { } if action_query.is_empty() { - // apply default sort - // sorts by source precedence, and alphabetically by action name within each source - matches.sort_by_key(|match_item| { - let keybind = &this.keybindings[match_item.candidate_id]; - let source = keybind.source.as_ref().map(|s| s.0); - use KeybindSource::*; - let source_precedence = match source { - Some(User) => 0, - Some(Vim) => 1, - Some(Base) => 2, - Some(Default) => 3, - None => 4, - }; - return (source_precedence, keybind.action_name); + matches.sort_by(|item1, item2| { + let binding1 = &this.keybindings[item1.candidate_id]; + let binding2 = &this.keybindings[item2.candidate_id]; + + binding1.cmp(binding2) }); } this.selected_index.take(); @@ -574,11 +606,11 @@ impl KeymapEditor { }) } - fn has_conflict(&self, row_index: usize) -> bool { - self.matches - .get(row_index) - .map(|candidate| candidate.candidate_id) - .is_some_and(|id| self.keybinding_conflict_state.has_conflict(&id)) + fn get_conflict(&self, row_index: usize) -> Option { + self.matches.get(row_index).and_then(|candidate| { + self.keybinding_conflict_state + .conflict_for_idx(candidate.candidate_id) + }) } fn process_bindings( @@ -586,7 +618,7 @@ impl KeymapEditor { zed_keybind_context_language: Arc, humanized_action_names: &HumanizedActionNameCache, cx: &mut App, - ) -> (Vec, Vec) { + ) -> (Vec, Vec) { let key_bindings_ptr = cx.key_bindings(); let lock = key_bindings_ptr.borrow(); let key_bindings = lock.bindings(); @@ -606,14 +638,12 @@ impl KeymapEditor { for key_binding in key_bindings { let source = key_binding .meta() - .map(settings::KeybindSource::try_from_meta) - .and_then(|source| source.log_err()); + .map(KeybindSource::from_meta) + .unwrap_or(KeybindSource::Unknown); let keystroke_text = ui::text_for_keystrokes(key_binding.keystrokes(), cx); - let ui_key_binding = Some( - ui::KeyBinding::new_from_gpui(key_binding.clone(), cx) - .vim_mode(source == Some(settings::KeybindSource::Vim)), - ); + let ui_key_binding = ui::KeyBinding::new_from_gpui(key_binding.clone(), cx) + .vim_mode(source == KeybindSource::Vim); let context = key_binding .predicate() @@ -625,48 +655,46 @@ impl KeymapEditor { }) .unwrap_or(KeybindContextString::Global); - let source = source.map(|source| (source, source.name().into())); - let action_name = key_binding.action().name(); unmapped_action_names.remove(&action_name); + let action_arguments = key_binding .action_input() .map(|arguments| SyntaxHighlightedText::new(arguments, json_language.clone())); - let action_docs = action_documentation.get(action_name).copied(); + let action_information = ActionInformation::new( + action_name, + action_arguments, + &actions_with_schemas, + &action_documentation, + &humanized_action_names, + ); let index = processed_bindings.len(); - let humanized_action_name = humanized_action_names.get(action_name); - let string_match_candidate = StringMatchCandidate::new(index, &humanized_action_name); - processed_bindings.push(ProcessedKeybinding { - keystroke_text: keystroke_text.into(), + let string_match_candidate = + StringMatchCandidate::new(index, &action_information.humanized_name); + processed_bindings.push(ProcessedBinding::new_mapped( + keystroke_text, ui_key_binding, - action_name, - action_arguments, - humanized_action_name, - action_docs, - has_schema: actions_with_schemas.contains(action_name), - context: Some(context), + context, source, - }); + action_information, + )); string_match_candidates.push(string_match_candidate); } - let empty = SharedString::new_static(""); for action_name in unmapped_action_names.into_iter() { let index = processed_bindings.len(); - let humanized_action_name = humanized_action_names.get(action_name); - let string_match_candidate = StringMatchCandidate::new(index, &humanized_action_name); - processed_bindings.push(ProcessedKeybinding { - keystroke_text: empty.clone(), - ui_key_binding: None, + let action_information = ActionInformation::new( action_name, - action_arguments: None, - humanized_action_name, - action_docs: action_documentation.get(action_name).copied(), - has_schema: actions_with_schemas.contains(action_name), - context: None, - source: None, - }); + None, + &actions_with_schemas, + &action_documentation, + &humanized_action_names, + ); + let string_match_candidate = + StringMatchCandidate::new(index, &action_information.humanized_name); + + processed_bindings.push(ProcessedBinding::Unmapped(action_information)); string_match_candidates.push(string_match_candidate); } @@ -728,8 +756,9 @@ impl KeymapEditor { let scroll_position = this.matches.iter().enumerate().find_map(|(index, item)| { let binding = &this.keybindings[item.candidate_id]; - if binding.get_action_mapping() == action_mapping - && binding.action_name == action_name + if binding.get_action_mapping().is_some_and(|binding_mapping| { + binding_mapping == action_mapping + }) && binding.action().name == action_name { Some(index) } else { @@ -799,12 +828,12 @@ impl KeymapEditor { .map(|r#match| r#match.candidate_id) } - fn selected_keybind_and_index(&self) -> Option<(&ProcessedKeybinding, usize)> { + fn selected_keybind_and_index(&self) -> Option<(&ProcessedBinding, usize)> { self.selected_keybind_index() .map(|keybind_index| (&self.keybindings[keybind_index], keybind_index)) } - fn selected_binding(&self) -> Option<&ProcessedKeybinding> { + fn selected_binding(&self) -> Option<&ProcessedBinding> { self.selected_keybind_index() .and_then(|keybind_index| self.keybindings.get(keybind_index)) } @@ -832,15 +861,13 @@ impl KeymapEditor { window: &mut Window, cx: &mut Context, ) { - let weak = cx.weak_entity(); self.context_menu = self.selected_binding().map(|selected_binding| { let selected_binding_has_no_context = selected_binding - .context - .as_ref() + .context() .and_then(KeybindContextString::local) .is_none(); - let selected_binding_is_unbound = selected_binding.keystrokes().is_none(); + let selected_binding_is_unbound = selected_binding.is_unbound(); let context_menu = ContextMenu::build(window, cx, |menu, _window, _cx| { menu.context(self.focus_handle.clone()) @@ -863,14 +890,11 @@ impl KeymapEditor { Box::new(CopyContext), ) .separator() - .entry("Show Matching Keybindings", None, { - move |_, cx| { - weak.update(cx, |this, cx| { - this.filter_on_selected_binding_keystrokes(cx); - }) - .ok(); - } - }) + .action_disabled_when( + selected_binding_has_no_context, + "Show Matching Keybindings", + Box::new(ShowMatchingKeybinds), + ) }); let context_menu_handle = context_menu.focus_handle(cx); @@ -898,10 +922,98 @@ impl KeymapEditor { self.context_menu.is_some() } + fn create_row_button( + &self, + index: usize, + conflict: Option, + cx: &mut Context, + ) -> IconButton { + if self.filter_state != FilterState::Conflicts + && let Some(conflict) = conflict + { + if conflict.is_user_keybind_conflict() { + base_button_style(index, IconName::Warning) + .icon_color(Color::Warning) + .tooltip(|window, cx| { + Tooltip::with_meta( + "View conflicts", + Some(&ToggleConflictFilter), + "Use alt+click to show all conflicts", + window, + cx, + ) + }) + .on_click(cx.listener(move |this, click: &ClickEvent, window, cx| { + if click.modifiers().alt { + this.set_filter_state(FilterState::Conflicts, cx); + } else { + this.select_index(index, None, window, cx); + this.open_edit_keybinding_modal(false, window, cx); + cx.stop_propagation(); + } + })) + } else if self.search_mode.exact_match() { + base_button_style(index, IconName::Info) + .tooltip(|window, cx| { + Tooltip::with_meta( + "Edit this binding", + Some(&ShowMatchingKeybinds), + "This binding is overridden by other bindings.", + window, + cx, + ) + }) + .on_click(cx.listener(move |this, _: &ClickEvent, window, cx| { + this.select_index(index, None, window, cx); + this.open_edit_keybinding_modal(false, window, cx); + cx.stop_propagation(); + })) + } else { + base_button_style(index, IconName::Info) + .tooltip(|window, cx| { + Tooltip::with_meta( + "Show matching keybinds", + Some(&ShowMatchingKeybinds), + "This binding is overridden by other bindings.\nUse alt+click to edit this binding", + window, + cx, + ) + }) + .on_click(cx.listener(move |this, click: &ClickEvent, window, cx| { + if click.modifiers().alt { + this.select_index(index, None, window, cx); + this.open_edit_keybinding_modal(false, window, cx); + cx.stop_propagation(); + } else { + this.show_matching_keystrokes(&Default::default(), window, cx); + } + })) + } + } else { + base_button_style(index, IconName::Pencil) + .visible_on_hover(if self.selected_index == Some(index) { + "".into() + } else if self.show_hover_menus { + row_group_id(index) + } else { + "never-show".into() + }) + .when( + self.show_hover_menus && !self.context_menu_deployed(), + |this| this.tooltip(Tooltip::for_action_title("Edit Keybinding", &EditBinding)), + ) + .on_click(cx.listener(move |this, _, window, cx| { + this.select_index(index, None, window, cx); + this.open_edit_keybinding_modal(false, window, cx); + cx.stop_propagation(); + })) + } + } + fn render_no_matches_hint(&self, _window: &mut Window, _cx: &App) -> AnyElement { let hint = match (self.filter_state, &self.search_mode) { (FilterState::Conflicts, _) => { - if self.keybinding_conflict_state.any_conflicts() { + if self.keybinding_conflict_state.any_user_binding_conflicts() { "No conflicting keybinds found that match the provided query" } else { "No conflicting keybinds found" @@ -982,20 +1094,22 @@ impl KeymapEditor { let keybind = keybind.clone(); let keymap_editor = cx.entity(); + let keystroke = keybind.keystroke_text().cloned().unwrap_or_default(); let arguments = keybind - .action_arguments + .action() + .arguments .as_ref() .map(|arguments| arguments.text.clone()); let context = keybind - .context - .as_ref() + .context() .map(|context| context.local_str().unwrap_or("global")); - let source = keybind.source.as_ref().map(|source| source.1.clone()); + let action = keybind.action().name; + let source = keybind.keybind_source().map(|source| source.name()); telemetry::event!( "Edit Keybinding Modal Opened", - keystroke = keybind.keystroke_text, - action = keybind.action_name, + keystroke = keystroke, + action = action, source = source, context = context, arguments = arguments, @@ -1063,7 +1177,7 @@ impl KeymapEditor { ) { let context = self .selected_binding() - .and_then(|binding| binding.context.as_ref()) + .and_then(|binding| binding.context()) .and_then(KeybindContextString::local_str) .map(|context| context.to_string()); let Some(context) = context else { @@ -1082,7 +1196,7 @@ impl KeymapEditor { ) { let action = self .selected_binding() - .map(|binding| binding.action_name.to_string()); + .map(|binding| binding.action().name.to_string()); let Some(action) = action else { return; }; @@ -1142,6 +1256,29 @@ impl KeymapEditor { *exact_match = !(*exact_match); self.on_query_changed(cx); } + + fn show_matching_keystrokes( + &mut self, + _: &ShowMatchingKeybinds, + _: &mut Window, + cx: &mut Context, + ) { + let Some(selected_binding) = self.selected_binding() else { + return; + }; + + let keystrokes = selected_binding + .keystrokes() + .map(Vec::from) + .unwrap_or_default(); + + self.filter_state = FilterState::All; + self.search_mode = SearchMode::KeyStroke { exact_match: true }; + + self.keystroke_editor.update(cx, |editor, cx| { + editor.set_keystrokes(keystrokes, cx); + }); + } } struct HumanizedActionNameCache { @@ -1168,35 +1305,134 @@ impl HumanizedActionNameCache { } #[derive(Clone)] -struct ProcessedKeybinding { +struct KeybindInformation { keystroke_text: SharedString, - ui_key_binding: Option, - action_name: &'static str, - humanized_action_name: SharedString, - action_arguments: Option, - action_docs: Option<&'static str>, - has_schema: bool, - context: Option, - source: Option<(KeybindSource, SharedString)>, + ui_binding: ui::KeyBinding, + context: KeybindContextString, + source: KeybindSource, } -impl ProcessedKeybinding { +impl KeybindInformation { fn get_action_mapping(&self) -> ActionMapping { ActionMapping { - keystrokes: self.keystrokes().map(Vec::from).unwrap_or_default(), - context: self - .context - .as_ref() - .and_then(|context| context.local()) - .cloned(), + keystrokes: self.ui_binding.keystrokes.clone(), + context: self.context.local().cloned(), } } +} + +#[derive(Clone)] +struct ActionInformation { + name: &'static str, + humanized_name: SharedString, + arguments: Option, + documentation: Option<&'static str>, + has_schema: bool, +} + +impl ActionInformation { + fn new( + action_name: &'static str, + action_arguments: Option, + actions_with_schemas: &HashSet<&'static str>, + action_documentation: &HashMap<&'static str, &'static str>, + action_name_cache: &HumanizedActionNameCache, + ) -> Self { + Self { + humanized_name: action_name_cache.get(action_name), + has_schema: actions_with_schemas.contains(action_name), + arguments: action_arguments, + documentation: action_documentation.get(action_name).copied(), + name: action_name, + } + } +} + +#[derive(Clone)] +enum ProcessedBinding { + Mapped(KeybindInformation, ActionInformation), + Unmapped(ActionInformation), +} + +impl ProcessedBinding { + fn new_mapped( + keystroke_text: impl Into, + ui_key_binding: ui::KeyBinding, + context: KeybindContextString, + source: KeybindSource, + action_information: ActionInformation, + ) -> Self { + Self::Mapped( + KeybindInformation { + keystroke_text: keystroke_text.into(), + ui_binding: ui_key_binding, + context, + source, + }, + action_information, + ) + } + + fn is_unbound(&self) -> bool { + matches!(self, Self::Unmapped(_)) + } + + fn get_action_mapping(&self) -> Option { + self.keybind_information() + .map(|keybind| keybind.get_action_mapping()) + } fn keystrokes(&self) -> Option<&[Keystroke]> { - self.ui_key_binding - .as_ref() + self.ui_key_binding() .map(|binding| binding.keystrokes.as_slice()) } + + fn keybind_information(&self) -> Option<&KeybindInformation> { + match self { + Self::Mapped(keybind_information, _) => Some(keybind_information), + Self::Unmapped(_) => None, + } + } + + fn keybind_source(&self) -> Option { + self.keybind_information().map(|keybind| keybind.source) + } + + fn context(&self) -> Option<&KeybindContextString> { + self.keybind_information().map(|keybind| &keybind.context) + } + + fn ui_key_binding(&self) -> Option<&ui::KeyBinding> { + self.keybind_information() + .map(|keybind| &keybind.ui_binding) + } + + fn keystroke_text(&self) -> Option<&SharedString> { + self.keybind_information() + .map(|binding| &binding.keystroke_text) + } + + fn action(&self) -> &ActionInformation { + match self { + Self::Mapped(_, action) | Self::Unmapped(action) => action, + } + } + + fn cmp(&self, other: &Self) -> cmp::Ordering { + match (self, other) { + (Self::Mapped(keybind1, action1), Self::Mapped(keybind2, action2)) => { + match keybind1.source.cmp(&keybind2.source) { + cmp::Ordering::Equal => action1.humanized_name.cmp(&action2.humanized_name), + ordering => ordering, + } + } + (Self::Mapped(_, _), Self::Unmapped(_)) => cmp::Ordering::Less, + (Self::Unmapped(_), Self::Mapped(_, _)) => cmp::Ordering::Greater, + (Self::Unmapped(action1), Self::Unmapped(action2)) => { + action1.humanized_name.cmp(&action2.humanized_name) + } + } + } } #[derive(Clone, Debug, IntoElement, PartialEq, Eq, Hash)] @@ -1275,6 +1511,7 @@ impl Render for KeymapEditor { .on_action(cx.listener(Self::toggle_conflict_filter)) .on_action(cx.listener(Self::toggle_keystroke_search)) .on_action(cx.listener(Self::toggle_exact_keystroke_matching)) + .on_action(cx.listener(Self::show_matching_keystrokes)) .on_mouse_move(cx.listener(|this, _, _window, _cx| { this.show_hover_menus = true; })) @@ -1335,9 +1572,12 @@ impl Render for KeymapEditor { .child( IconButton::new("KeymapEditorConflictIcon", IconName::Warning) .shape(ui::IconButtonShape::Square) - .when(self.keybinding_conflict_state.any_conflicts(), |this| { - this.indicator(Indicator::dot().color(Color::Warning)) - }) + .when( + self.keybinding_conflict_state.any_user_binding_conflicts(), + |this| { + this.indicator(Indicator::dot().color(Color::Warning)) + }, + ) .tooltip({ let filter_state = self.filter_state; let focus_handle = focus_handle.clone(); @@ -1377,7 +1617,10 @@ impl Render for KeymapEditor { this.child( h_flex() .map(|this| { - if self.keybinding_conflict_state.any_conflicts() { + if self + .keybinding_conflict_state + .any_user_binding_conflicts() + { this.pr(rems_from_px(54.)) } else { this.pr_7() @@ -1457,73 +1700,21 @@ impl Render for KeymapEditor { .filter_map(|index| { let candidate_id = this.matches.get(index)?.candidate_id; let binding = &this.keybindings[candidate_id]; - let action_name = binding.action_name; + let action_name = binding.action().name; + let conflict = this.get_conflict(index); + let is_overridden = conflict.is_some_and(|conflict| { + !conflict.is_user_keybind_conflict() + }); - let icon = if this.filter_state != FilterState::Conflicts - && this.has_conflict(index) - { - base_button_style(index, IconName::Warning) - .icon_color(Color::Warning) - .tooltip(|window, cx| { - Tooltip::with_meta( - "View conflicts", - Some(&ToggleConflictFilter), - "Use alt+click to show all conflicts", - window, - cx, - ) - }) - .on_click(cx.listener( - move |this, click: &ClickEvent, window, cx| { - if click.modifiers().alt { - this.set_filter_state( - FilterState::Conflicts, - cx, - ); - } else { - this.select_index(index, None, window, cx); - this.open_edit_keybinding_modal( - false, window, cx, - ); - cx.stop_propagation(); - } - }, - )) - .into_any_element() - } else { - base_button_style(index, IconName::Pencil) - .visible_on_hover( - if this.selected_index == Some(index) { - "".into() - } else if this.show_hover_menus { - row_group_id(index) - } else { - "never-show".into() - }, - ) - .when( - this.show_hover_menus && !context_menu_deployed, - |this| { - this.tooltip(Tooltip::for_action_title( - "Edit Keybinding", - &EditBinding, - )) - }, - ) - .on_click(cx.listener(move |this, _, window, cx| { - this.select_index(index, None, window, cx); - this.open_edit_keybinding_modal(false, window, cx); - cx.stop_propagation(); - })) - .into_any_element() - }; + let icon = this.create_row_button(index, conflict, cx); let action = div() .id(("keymap action", index)) .child({ if action_name != gpui::NoAction.name() { binding - .humanized_action_name + .action() + .humanized_name .clone() .into_any_element() } else { @@ -1534,11 +1725,14 @@ impl Render for KeymapEditor { } }) .when( - !context_menu_deployed && this.show_hover_menus, + !context_menu_deployed + && this.show_hover_menus + && !is_overridden, |this| { this.tooltip({ - let action_name = binding.action_name; - let action_docs = binding.action_docs; + let action_name = binding.action().name; + let action_docs = + binding.action().documentation; move |_, cx| { let action_tooltip = Tooltip::new(action_name); @@ -1552,14 +1746,19 @@ impl Render for KeymapEditor { }, ) .into_any_element(); - let keystrokes = binding.ui_key_binding.clone().map_or( - binding.keystroke_text.clone().into_any_element(), + let keystrokes = binding.ui_key_binding().cloned().map_or( + binding + .keystroke_text() + .cloned() + .unwrap_or_default() + .into_any_element(), IntoElement::into_any_element, ); - let action_arguments = match binding.action_arguments.clone() { + let action_arguments = match binding.action().arguments.clone() + { Some(arguments) => arguments.into_any_element(), None => { - if binding.has_schema { + if binding.action().has_schema { muted_styled_text(NO_ACTION_ARGUMENTS_TEXT, cx) .into_any_element() } else { @@ -1567,7 +1766,7 @@ impl Render for KeymapEditor { } } }; - let context = binding.context.clone().map_or( + let context = binding.context().cloned().map_or( gpui::Empty.into_any_element(), |context| { let is_local = context.local().is_some(); @@ -1578,6 +1777,7 @@ impl Render for KeymapEditor { .when( is_local && !context_menu_deployed + && !is_overridden && this.show_hover_menus, |this| { this.tooltip(Tooltip::element({ @@ -1591,13 +1791,12 @@ impl Render for KeymapEditor { }, ); let source = binding - .source - .clone() - .map(|(_source, name)| name) + .keybind_source() + .map(|source| source.name()) .unwrap_or_default() .into_any_element(); Some([ - icon, + icon.into_any_element(), action, action_arguments, keystrokes, @@ -1610,51 +1809,90 @@ impl Render for KeymapEditor { ) .map_row(cx.processor( |this, (row_index, row): (usize, Stateful
    ), _window, cx| { - let is_conflict = this.has_conflict(row_index); + let conflict = this.get_conflict(row_index); let is_selected = this.selected_index == Some(row_index); let row_id = row_group_id(row_index); - let row = row - .on_any_mouse_down(cx.listener( - move |this, - mouse_down_event: &gpui::MouseDownEvent, - window, - cx| { - match mouse_down_event.button { - MouseButton::Right => { + div() + .id(("keymap-row-wrapper", row_index)) + .child( + row.id(row_id.clone()) + .on_any_mouse_down(cx.listener( + move |this, + 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, + ); + } + _ => {} + } + }, + )) + .on_click(cx.listener( + move |this, event: &ClickEvent, window, cx| { this.select_index(row_index, None, window, cx); - this.create_context_menu( - mouse_down_event.position, - window, - cx, - ); - } - _ => {} - } - }, - )) - .on_click(cx.listener( - move |this, event: &ClickEvent, window, cx| { - this.select_index(row_index, None, window, cx); - if event.up.click_count == 2 { - this.open_edit_keybinding_modal(false, window, cx); - } - }, - )) - .group(row_id) + if event.up.click_count == 2 { + this.open_edit_keybinding_modal( + false, window, cx, + ); + } + }, + )) + .group(row_id) + .when( + conflict.is_some_and(|conflict| { + !conflict.is_user_keybind_conflict() + }), + |row| { + const OVERRIDDEN_OPACITY: f32 = 0.5; + row.opacity(OVERRIDDEN_OPACITY) + }, + ) + .when_some( + conflict.filter(|conflict| { + !this.context_menu_deployed() && + !conflict.is_user_keybind_conflict() + }), + |row, conflict| { + let overriding_binding = this.keybindings.get(conflict.index); + let context = overriding_binding.and_then(|binding| { + match conflict.override_source { + KeybindSource::User => Some("your keymap"), + KeybindSource::Vim => Some("the vim keymap"), + KeybindSource::Base => Some("your base keymap"), + _ => { + log::error!("Unexpected override from the {} keymap", conflict.override_source.name()); + None + } + }.map(|source| format!("This keybinding is overridden by the '{}' binding from {}.", binding.action().humanized_name, source)) + }).unwrap_or_else(|| "This binding is overridden.".to_string()); + + row.tooltip(Tooltip::text(context))}, + ), + ) .border_2() - .when(is_conflict, |row| { - row.bg(cx.theme().status().error_background) - }) + .when( + conflict.is_some_and(|conflict| { + conflict.is_user_keybind_conflict() + }), + |row| row.bg(cx.theme().status().error_background), + ) .when(is_selected, |row| { row.border_color(cx.theme().colors().panel_focused_border) - .border_2() - }); - - row.into_any_element() - }, - )), + }) + .into_any_element() + }), + ), ) .on_scroll_wheel(cx.listener(|this, event: &ScrollWheelEvent, _, cx| { // This ensures that the menu is not dismissed in cases where scroll events @@ -1762,7 +2000,7 @@ impl InputError { struct KeybindingEditorModal { creating: bool, - editing_keybind: ProcessedKeybinding, + editing_keybind: ProcessedBinding, editing_keybind_idx: usize, keybind_editor: Entity, context_editor: Entity, @@ -1787,7 +2025,7 @@ impl Focusable for KeybindingEditorModal { impl KeybindingEditorModal { pub fn new( create: bool, - editing_keybind: ProcessedKeybinding, + editing_keybind: ProcessedBinding, editing_keybind_idx: usize, keymap_editor: Entity, action_args_temp_dir: Option<&std::path::Path>, @@ -1805,8 +2043,7 @@ impl KeybindingEditorModal { .label_size(LabelSize::Default); if let Some(context) = editing_keybind - .context - .as_ref() + .context() .and_then(KeybindContextString::local) { input.editor().update(cx, |editor, cx| { @@ -1840,14 +2077,15 @@ impl KeybindingEditorModal { input }); - let action_arguments_editor = editing_keybind.has_schema.then(|| { + let action_arguments_editor = editing_keybind.action().has_schema.then(|| { let arguments = editing_keybind - .action_arguments + .action() + .arguments .as_ref() .map(|args| args.text.clone()); cx.new(|cx| { ActionArgumentsEditor::new( - editing_keybind.action_name, + editing_keybind.action().name, arguments, action_args_temp_dir, workspace.clone(), @@ -1905,7 +2143,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) } @@ -1956,17 +2194,14 @@ impl KeybindingEditorModal { context: new_context.map(SharedString::from), }; - let conflicting_indices = if self.creating { - self.keymap_editor - .read(cx) - .keybinding_conflict_state - .will_conflict(&action_mapping) - } else { - self.keymap_editor - .read(cx) - .keybinding_conflict_state - .conflicting_indices_for_mapping(&action_mapping, self.editing_keybind_idx) - }; + let conflicting_indices = self + .keymap_editor + .read(cx) + .keybinding_conflict_state + .conflicting_indices_for_mapping( + &action_mapping, + self.creating.not().then_some(self.editing_keybind_idx), + ); conflicting_indices.map(|KeybindConflict { first_conflict_index, @@ -1978,7 +2213,7 @@ impl KeybindingEditorModal { .read(cx) .keybindings .get(first_conflict_index) - .map(|keybind| keybind.action_name); + .map(|keybind| keybind.action().name); let warning_message = match conflicting_action_name { Some(name) => { @@ -2013,7 +2248,7 @@ impl KeybindingEditorModal { let status_toast = StatusToast::new( format!( "Saved edits to the {} action.", - &self.editing_keybind.humanized_action_name + &self.editing_keybind.action().humanized_name ), cx, move |this, _cx| { @@ -2030,7 +2265,7 @@ impl KeybindingEditorModal { .log_err(); cx.spawn(async move |this, cx| { - let action_name = existing_keybind.action_name; + let action_name = existing_keybind.action().name; if let Err(err) = save_keybinding_update( create, @@ -2127,13 +2362,18 @@ impl Render for KeybindingEditorModal { .border_b_1() .border_color(theme.border_variant) .child(Label::new( - self.editing_keybind.humanized_action_name.clone(), + self.editing_keybind.action().humanized_name.clone(), )) - .when_some(self.editing_keybind.action_docs, |this, docs| { - this.child( - Label::new(docs).size(LabelSize::Small).color(Color::Muted), - ) - }), + .when_some( + self.editing_keybind.action().documentation, + |this, docs| { + this.child( + Label::new(docs) + .size(LabelSize::Small) + .color(Color::Muted), + ) + }, + ), ), ) .section( @@ -2296,14 +2536,32 @@ impl ActionArgumentsEditor { ) })?; - let file_name = project::lsp_store::json_language_server_ext::normalized_action_file_name(action_name); + let file_name = + project::lsp_store::json_language_server_ext::normalized_action_file_name( + action_name, + ); - let (buffer, backup_temp_dir) = Self::create_temp_buffer(temp_dir, file_name.clone(), project.clone(), fs, cx).await.context("Failed to create temporary buffer for action arguments. Auto-complete will not work") - ?; + let (buffer, backup_temp_dir) = + Self::create_temp_buffer(temp_dir, file_name.clone(), project.clone(), fs, cx) + .await + .context(concat!( + "Failed to create temporary buffer for action arguments. ", + "Auto-complete will not work" + ))?; let editor = cx.new_window_entity(|window, cx| { let multi_buffer = cx.new(|cx| editor::MultiBuffer::singleton(buffer, cx)); - let mut editor = Editor::new(editor::EditorMode::Full { scale_ui_elements_with_buffer_font_size: true, show_active_line_background: false, sized_by_content: true },multi_buffer, project.upgrade(), window, cx); + let mut editor = Editor::new( + editor::EditorMode::Full { + scale_ui_elements_with_buffer_font_size: true, + show_active_line_background: false, + sized_by_content: true, + }, + multi_buffer, + project.upgrade(), + window, + cx, + ); editor.set_searchable(false); editor.disable_scrollbars_and_minimap(window, cx); editor.set_show_edit_predictions(Some(false), window, cx); @@ -2322,7 +2580,8 @@ impl ActionArgumentsEditor { })?; anyhow::Ok(()) - }.await; + } + .await; if result.is_err() { let json_language = load_json_language(workspace.clone(), cx).await; this.update(cx, |this, cx| { @@ -2334,10 +2593,12 @@ impl ActionArgumentsEditor { } }) // .context("Failed to load JSON language for editing keybinding action arguments input") - }).ok(); + }) + .ok(); this.update(cx, |this, _cx| { this.is_loading = false; - }).ok(); + }) + .ok(); } return result; }) @@ -2582,7 +2843,7 @@ async fn load_keybind_context_language( async fn save_keybinding_update( create: bool, - existing: ProcessedKeybinding, + existing: ProcessedBinding, action_mapping: &ActionMapping, new_args: Option<&str>, fs: &Arc, @@ -2593,37 +2854,31 @@ async fn save_keybinding_update( .context("Failed to load keymap file")?; let existing_keystrokes = existing.keystrokes().unwrap_or_default(); - let existing_context = existing - .context - .as_ref() - .and_then(KeybindContextString::local_str); + let existing_context = existing.context().and_then(KeybindContextString::local_str); let existing_args = existing - .action_arguments + .action() + .arguments .as_ref() .map(|args| args.text.as_ref()); 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, }; let operation = if !create { settings::KeybindUpdateOperation::Replace { target, - target_keybind_source: existing - .source - .as_ref() - .map(|(source, _name)| *source) - .unwrap_or(KeybindSource::User), + target_keybind_source: existing.keybind_source().unwrap_or(KeybindSource::User), source, } } else { @@ -2655,7 +2910,7 @@ async fn save_keybinding_update( } async fn remove_keybinding( - existing: ProcessedKeybinding, + existing: ProcessedBinding, fs: &Arc, tab_size: usize, ) -> anyhow::Result<()> { @@ -2668,22 +2923,16 @@ async fn remove_keybinding( let operation = settings::KeybindUpdateOperation::Remove { target: settings::KeybindUpdateTarget { - context: existing - .context - .as_ref() - .and_then(KeybindContextString::local_str), + context: existing.context().and_then(KeybindContextString::local_str), keystrokes, - action_name: &existing.action_name, + action_name: &existing.action().name, action_arguments: existing - .action_arguments + .action() + .arguments .as_ref() .map(|arguments| arguments.text.as_ref()), }, - target_keybind_source: existing - .source - .as_ref() - .map(|(source, _name)| *source) - .unwrap_or(KeybindSource::User), + target_keybind_source: existing.keybind_source().unwrap_or(KeybindSource::User), }; let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry(); From 986b446749e69873dd46bdb6b062464ec0169a71 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Wed, 23 Jul 2025 12:18:55 -0400 Subject: [PATCH 287/658] keymap ui: Resizable column follow up (#34955) I cherry picked a small fix that didn't get into the original column resizable branch PR because I turned on auto merge. Release Notes: - N/A --- crates/settings_ui/src/ui_components/table.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/settings_ui/src/ui_components/table.rs b/crates/settings_ui/src/ui_components/table.rs index 70472918d2b7cbe199922c53d3208daf45062825..35f2c773067b0f98013c620694cba5649e40fa67 100644 --- a/crates/settings_ui/src/ui_components/table.rs +++ b/crates/settings_ui/src/ui_components/table.rs @@ -521,7 +521,7 @@ impl ColumnWidths { let mut curr_column = double_click_position + 1; let mut diff_left = diff; - while diff != 0.0 && curr_column < COLS { + while diff_left != 0.0 && curr_column < COLS { let Some(min_size) = resize_behavior[curr_column].min_size() else { curr_column += 1; continue; @@ -607,8 +607,7 @@ impl ColumnWidths { curr_column = col_idx; // Resize behavior should be improved in the future by also seeking to the right column when there's not enough space while diff_left < 0.0 { - let Some(min_size) = resize_behavior[curr_column.saturating_sub(1)].min_size() - else { + let Some(min_size) = resize_behavior[curr_column].min_size() else { if curr_column == 0 { break; } From 3b428e2ecc41185403c1e3e3a0d9de3ac2c15306 Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Wed, 23 Jul 2025 11:33:40 -0600 Subject: [PATCH 288/658] Remove `!menu` from `j k` binding in initial keymap examples (#34959) See https://github.com/zed-industries/zed/pull/34912#issuecomment-3108802582 Release Notes: - N/A --- assets/keymaps/initial.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/keymaps/initial.json b/assets/keymaps/initial.json index ff6069a81671e421b766763d90649385058efc58..8e4fe59f44ea7346a51e1c064ffa0553315da3b9 100644 --- a/assets/keymaps/initial.json +++ b/assets/keymaps/initial.json @@ -13,7 +13,7 @@ } }, { - "context": "Editor && vim_mode == insert && !menu", + "context": "Editor && vim_mode == insert", "bindings": { // "j k": "vim::NormalBefore" } From fdcd86617a823b8d6cedfe84bdfd13fc4c2346c8 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 23 Jul 2025 14:33:53 -0300 Subject: [PATCH 289/658] ai onboarding: Add telemetry event capturing (#34960) Release Notes: - N/A Co-authored-by: Katie Geer Co-authored-by: Joseph T. Lyons --- Cargo.lock | 1 + crates/agent_ui/src/message_editor.rs | 4 +++ crates/ai_onboarding/Cargo.toml | 1 + crates/ai_onboarding/src/ai_onboarding.rs | 37 +++++++++++++++++++---- 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 765ae002498ba4dd3811d3157f29728b62276f3a..8be4c9d7be7b8bd045f29ba4450874b505e38e6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -347,6 +347,7 @@ dependencies = [ "proto", "serde", "smallvec", + "telemetry", "ui", "workspace-hack", "zed_actions", diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index 78037532925d8214b3f6fe8c780039e3e590a7f7..ab8ba762f4e64a90679c6bf485c4554631106f78 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -910,6 +910,10 @@ impl MessageEditor { .on_click({ let focus_handle = focus_handle.clone(); move |_event, window, cx| { + telemetry::event!( + "Agent Message Sent", + agent = "zed", + ); focus_handle.dispatch_action( &Chat, window, cx, ); diff --git a/crates/ai_onboarding/Cargo.toml b/crates/ai_onboarding/Cargo.toml index e9208a724865e2d0d5288f493925f5a944d67642..9031e14e29d8a909ec6cde6d75607c138321e110 100644 --- a/crates/ai_onboarding/Cargo.toml +++ b/crates/ai_onboarding/Cargo.toml @@ -22,6 +22,7 @@ language_model.workspace = true proto.workspace = true serde.workspace = true smallvec.workspace = true +telemetry.workspace = true ui.workspace = true workspace-hack.workspace = true zed_actions.workspace = true diff --git a/crates/ai_onboarding/src/ai_onboarding.rs b/crates/ai_onboarding/src/ai_onboarding.rs index e8ce22ff4e51bf0348796b9bac4e8cc37b836b5f..9d32b1ee09b38e0f1b6eba80809ec6d4fcd7c55d 100644 --- a/crates/ai_onboarding/src/ai_onboarding.rs +++ b/crates/ai_onboarding/src/ai_onboarding.rs @@ -183,6 +183,7 @@ impl ZedAiOnboarding { .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)) }), ) @@ -210,6 +211,7 @@ impl ZedAiOnboarding { .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)) }), ) @@ -234,7 +236,10 @@ impl ZedAiOnboarding { .icon(IconName::ArrowUpRight) .icon_color(Color::Muted) .icon_size(IconSize::XSmall) - .on_click(move |_, _window, cx| cx.open_url(&zed_urls::terms_of_service(cx))), + .on_click(move |_, _window, cx| { + telemetry::event!("Review Terms of Service Click"); + cx.open_url(&zed_urls::terms_of_service(cx)) + }), ) .child( Button::new("accept_terms", "Accept") @@ -242,7 +247,9 @@ impl ZedAiOnboarding { .style(ButtonStyle::Tinted(TintColor::Accent)) .on_click({ let callback = self.accept_terms_of_service.clone(); - move |_, window, cx| (callback)(window, cx) + move |_, window, cx| { + telemetry::event!("Accepted Terms of Service"); + (callback)(window, cx)} }), ) .into_any_element() @@ -267,7 +274,10 @@ impl ZedAiOnboarding { .style(ButtonStyle::Tinted(ui::TintColor::Accent)) .on_click({ let callback = self.sign_in.clone(); - move |_, window, cx| callback(window, cx) + move |_, window, cx| { + telemetry::event!("Start Trial Clicked", state = "pre-sign-in"); + callback(window, cx) + } }), ) .into_any_element() @@ -294,7 +304,13 @@ impl ZedAiOnboarding { IconButton::new("dismiss_onboarding", IconName::Close) .icon_size(IconSize::Small) .tooltip(Tooltip::text("Dismiss")) - .on_click(move |_, window, cx| callback(window, cx)), + .on_click(move |_, window, cx| { + telemetry::event!( + "Banner Dismissed", + source = "AI Onboarding", + ); + callback(window, cx) + }), ), ) }, @@ -331,7 +347,13 @@ impl ZedAiOnboarding { IconButton::new("dismiss_onboarding", IconName::Close) .icon_size(IconSize::Small) .tooltip(Tooltip::text("Dismiss")) - .on_click(move |_, window, cx| callback(window, cx)), + .on_click(move |_, window, cx| { + telemetry::event!( + "Banner Dismissed", + source = "AI Onboarding", + ); + callback(window, cx) + }), ), ) }, @@ -359,7 +381,10 @@ impl ZedAiOnboarding { .style(ButtonStyle::Outlined) .on_click({ let callback = self.continue_with_zed_ai.clone(); - move |_, window, cx| callback(window, cx) + move |_, window, cx| { + telemetry::event!("Banner Dismissed", source = "AI Onboarding"); + callback(window, cx) + } }), ) .into_any_element() From 56b64b1d3f60dcb15997310c85cd2454ce300d43 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Wed, 23 Jul 2025 13:45:49 -0400 Subject: [PATCH 290/658] keymap ui: Improve resize columns on double click (#34961) This PR splits the resize logic into separate left/right propagation methods and improve code organization around column width adjustments. It also allows resize to work for both the left and right sides as well, instead of only checking the right side for room Release Notes: - N/A *or* Added/Fixed/Improved ... --------- Co-authored-by: Ben Kunkle --- crates/settings_ui/src/ui_components/table.rs | 190 ++++++++++-------- 1 file changed, 108 insertions(+), 82 deletions(-) diff --git a/crates/settings_ui/src/ui_components/table.rs b/crates/settings_ui/src/ui_components/table.rs index 35f2c773067b0f98013c620694cba5649e40fa67..69207f559b89b83b6709bd41ab861e3a71be6616 100644 --- a/crates/settings_ui/src/ui_components/table.rs +++ b/crates/settings_ui/src/ui_components/table.rs @@ -510,39 +510,48 @@ impl ColumnWidths { ) { let bounds_width = self.cached_bounds_width; let rem_size = window.rem_size(); + let initial_sizes = + initial_sizes.map(|length| Self::get_fraction(&length, bounds_width, rem_size)); + let mut widths = self + .widths + .map(|length| Self::get_fraction(&length, bounds_width, rem_size)); + + let diff = initial_sizes[double_click_position] - widths[double_click_position]; + + if diff > 0.0 { + let diff_remaining = self.propagate_resize_diff_right( + diff, + double_click_position, + &mut widths, + resize_behavior, + ); - let diff = - Self::get_fraction( - &initial_sizes[double_click_position], - bounds_width, - rem_size, - ) - Self::get_fraction(&self.widths[double_click_position], bounds_width, rem_size); - - let mut curr_column = double_click_position + 1; - let mut diff_left = diff; - - while diff_left != 0.0 && curr_column < COLS { - let Some(min_size) = resize_behavior[curr_column].min_size() else { - curr_column += 1; - continue; - }; - - let mut curr_width = - Self::get_fraction(&self.widths[curr_column], bounds_width, rem_size) - diff_left; + if diff_remaining > 0.0 && double_click_position > 0 { + self.propagate_resize_diff_left( + -diff_remaining, + double_click_position - 1, + &mut widths, + resize_behavior, + ); + } + } else if double_click_position > 0 { + let diff_remaining = self.propagate_resize_diff_left( + diff, + double_click_position, + &mut widths, + resize_behavior, + ); - diff_left = 0.0; - if min_size > curr_width { - diff_left += min_size - curr_width; - curr_width = min_size; + if diff_remaining < 0.0 { + self.propagate_resize_diff_right( + -diff_remaining, + double_click_position, + &mut widths, + resize_behavior, + ); } - self.widths[curr_column] = DefiniteLength::Fraction(curr_width); - curr_column += 1; } - - self.widths[double_click_position] = DefiniteLength::Fraction( - Self::get_fraction(&self.widths[double_click_position], bounds_width, rem_size) - + (diff - diff_left), - ); + self.widths = widths.map(DefiniteLength::Fraction); } fn on_drag_move( @@ -552,7 +561,6 @@ impl ColumnWidths { window: &mut Window, cx: &mut Context, ) { - // - [ ] Fix bugs in resize let drag_position = drag_event.event.position; let bounds = drag_event.bounds; @@ -561,13 +569,17 @@ impl ColumnWidths { let bounds_width = bounds.right() - bounds.left(); let col_idx = drag_event.drag(cx).0; - for length in self.widths[0..=col_idx].iter() { - col_position += Self::get_fraction(length, bounds_width, rem_size); + let mut widths = self + .widths + .map(|length| Self::get_fraction(&length, bounds_width, rem_size)); + + for length in widths[0..=col_idx].iter() { + col_position += length; } let mut total_length_ratio = col_position; - for length in self.widths[col_idx + 1..].iter() { - total_length_ratio += Self::get_fraction(length, bounds_width, rem_size); + for length in widths[col_idx + 1..].iter() { + total_length_ratio += length; } let drag_fraction = (drag_position.x - bounds.left()) / bounds_width; @@ -576,67 +588,81 @@ impl ColumnWidths { let is_dragging_right = diff > 0.0; - let mut diff_left = diff; + if is_dragging_right { + self.propagate_resize_diff_right(diff, col_idx, &mut widths, resize_behavior); + } else { + // Resize behavior should be improved in the future by also seeking to the right column when there's not enough space + self.propagate_resize_diff_left(diff, col_idx, &mut widths, resize_behavior); + } + self.widths = widths.map(DefiniteLength::Fraction); + } + + fn propagate_resize_diff_right( + &self, + diff: f32, + col_idx: usize, + widths: &mut [f32; COLS], + resize_behavior: &[ResizeBehavior; COLS], + ) -> f32 { + let mut diff_remaining = diff; let mut curr_column = col_idx + 1; - if is_dragging_right { - while diff_left > 0.0 && curr_column < COLS { - let Some(min_size) = resize_behavior[curr_column - 1].min_size() else { - curr_column += 1; - continue; - }; - - let mut curr_width = - Self::get_fraction(&self.widths[curr_column], bounds_width, rem_size) - - diff_left; - - diff_left = 0.0; - if min_size > curr_width { - diff_left += min_size - curr_width; - curr_width = min_size; - } - self.widths[curr_column] = DefiniteLength::Fraction(curr_width); + while diff_remaining > 0.0 && curr_column < COLS { + let Some(min_size) = resize_behavior[curr_column - 1].min_size() else { curr_column += 1; + continue; + }; + + let mut curr_width = widths[curr_column] - diff_remaining; + + diff_remaining = 0.0; + if min_size > curr_width { + diff_remaining += min_size - curr_width; + curr_width = min_size; } + widths[curr_column] = curr_width; + curr_column += 1; + } - self.widths[col_idx] = DefiniteLength::Fraction( - Self::get_fraction(&self.widths[col_idx], bounds_width, rem_size) - + (diff - diff_left), - ); - } else { - curr_column = col_idx; - // Resize behavior should be improved in the future by also seeking to the right column when there's not enough space - while diff_left < 0.0 { - let Some(min_size) = resize_behavior[curr_column].min_size() else { - if curr_column == 0 { - break; - } - curr_column -= 1; - continue; - }; - - let mut curr_width = - Self::get_fraction(&self.widths[curr_column], bounds_width, rem_size) - + diff_left; - - diff_left = 0.0; - if curr_width < min_size { - diff_left = curr_width - min_size; - curr_width = min_size - } + widths[col_idx] = widths[col_idx] + (diff - diff_remaining); + return diff_remaining; + } - self.widths[curr_column] = DefiniteLength::Fraction(curr_width); + fn propagate_resize_diff_left( + &mut self, + diff: f32, + mut curr_column: usize, + widths: &mut [f32; COLS], + resize_behavior: &[ResizeBehavior; COLS], + ) -> f32 { + let mut diff_remaining = diff; + let col_idx = curr_column; + while diff_remaining < 0.0 { + let Some(min_size) = resize_behavior[curr_column].min_size() else { if curr_column == 0 { break; } curr_column -= 1; + continue; + }; + + let mut curr_width = widths[curr_column] + diff_remaining; + + diff_remaining = 0.0; + if curr_width < min_size { + diff_remaining = curr_width - min_size; + curr_width = min_size } - self.widths[col_idx + 1] = DefiniteLength::Fraction( - Self::get_fraction(&self.widths[col_idx + 1], bounds_width, rem_size) - - (diff - diff_left), - ); + widths[curr_column] = curr_width; + if curr_column == 0 { + break; + } + curr_column -= 1; } + widths[col_idx + 1] = widths[col_idx + 1] - (diff - diff_remaining); + + return diff_remaining; } } From 5f0edd38f896a2f31971d1eb53eec944adc3a0e3 Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Wed, 23 Jul 2025 13:01:16 -0500 Subject: [PATCH 291/658] Add TestPanic feature flag (#34963) Now the `dev: panic` action can be run on all release channels if the user has the feature flag enabled. Release Notes: - N/A --- Cargo.lock | 1 + crates/feature_flags/src/feature_flags.rs | 5 +++++ crates/zed/Cargo.toml | 1 + crates/zed/src/zed.rs | 20 +++++++++----------- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8be4c9d7be7b8bd045f29ba4450874b505e38e6c..6ded3ce5eb0952c627d84851a0d1fdb4426a2955 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20211,6 +20211,7 @@ dependencies = [ "extension", "extension_host", "extensions_ui", + "feature_flags", "feedback", "file_finder", "fs", diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs index da85133bb9b6e201c271811e08fff9920f5503c5..631bafc8413f1b2a8b24e1bdf1741126fb67fb40 100644 --- a/crates/feature_flags/src/feature_flags.rs +++ b/crates/feature_flags/src/feature_flags.rs @@ -85,6 +85,11 @@ impl FeatureFlag for ThreadAutoCaptureFeatureFlag { false } } +pub struct PanicFeatureFlag; + +impl FeatureFlag for PanicFeatureFlag { + const NAME: &'static str = "panic"; +} pub struct JjUiFeatureFlag {} diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index e565aba26b4caae298a063b7cd2036f5a7ee648d..1b564941458ed05d1d2ac168e7f88dc6fca119fa 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -56,6 +56,7 @@ env_logger.workspace = true extension.workspace = true extension_host.workspace = true extensions_ui.workspace = true +feature_flags.workspace = true feedback.workspace = true file_finder.workspace = true fs.workspace = true diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 24c7ab5ba278b2beff9c61202216aaf1876cf945..57534c8cd540c171069751281e2bc824ed05c343 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -19,6 +19,7 @@ use collections::VecDeque; use debugger_ui::debugger_panel::DebugPanel; use editor::ProposedChangesEditorToolbar; use editor::{Editor, MultiBuffer}; +use feature_flags::{FeatureFlagAppExt, PanicFeatureFlag}; use futures::future::Either; use futures::{StreamExt, channel::mpsc, select_biased}; use git_ui::git_panel::GitPanel; @@ -53,9 +54,12 @@ use settings::{ initial_local_debug_tasks_content, initial_project_settings_content, initial_tasks_content, update_settings_file, }; -use std::path::PathBuf; -use std::sync::atomic::{self, AtomicBool}; -use std::{borrow::Cow, path::Path, sync::Arc}; +use std::{ + borrow::Cow, + path::{Path, PathBuf}, + sync::Arc, + sync::atomic::{self, AtomicBool}, +}; use terminal_view::terminal_panel::{self, TerminalPanel}; use theme::{ActiveTheme, ThemeSettings}; use ui::{PopoverMenuHandle, prelude::*}; @@ -120,11 +124,9 @@ pub fn init(cx: &mut App) { cx.on_action(quit); cx.on_action(|_: &RestoreBanner, cx| title_bar::restore_banner(cx)); - - if ReleaseChannel::global(cx) == ReleaseChannel::Dev { - cx.on_action(test_panic); + if ReleaseChannel::global(cx) == ReleaseChannel::Dev || cx.has_flag::() { + cx.on_action(|_: &TestPanic, _| panic!("Ran the TestPanic action")); } - cx.on_action(|_: &OpenLog, cx| { with_active_or_new_workspace(cx, |workspace, window, cx| { open_log_file(workspace, window, cx); @@ -987,10 +989,6 @@ fn about( .detach(); } -fn test_panic(_: &TestPanic, _: &mut App) { - panic!("Ran the TestPanic action") -} - fn install_cli( _: &mut Workspace, _: &install_cli::Install, From a48247a313adbe084abc53455f3e34f92c78cf44 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 23 Jul 2025 14:14:39 -0400 Subject: [PATCH 292/658] Bump Zed to v0.198 (#34964) 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 6ded3ce5eb0952c627d84851a0d1fdb4426a2955..851c658735c2991fa78a42a1b615fe9849c0af0b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20170,7 +20170,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.197.0" +version = "0.198.0" dependencies = [ "activity_indicator", "agent", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 1b564941458ed05d1d2ac168e7f88dc6fca119fa..a864ece68379b2669a524994aee8764400d7cb79 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.197.0" +version = "0.198.0" publish.workspace = true license = "GPL-3.0-or-later" authors = ["Zed Team "] From 9863c8a44e2958d56313370f3c517b9c3ca0bfe0 Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Wed, 23 Jul 2025 23:58:05 +0530 Subject: [PATCH 293/658] agent_ui: Show keybindings for NewThread and NewTextThread in new thread button (#34967) I believe in this PR: #34829 we moved to context menu entry from action but the side effect of that was we also removed the Keybindings from showing it in the new thread button dropdown. This PR fixes that. cc @danilo-leal | Before | After | |--------|--------| | CleanShot 2025-07-23 at 23 36
28@2x | CleanShot 2025-07-23 at 23 37
17@2x | Release Notes: - N/A --- crates/agent_ui/src/agent_panel.rs | 169 +++++++++++++++-------------- 1 file changed, 90 insertions(+), 79 deletions(-) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 95ce2896083f0045bc3788c94847c08746d6e0bc..6ae2f12b5ebadb730656d2fdffaa9f9aaef990f1 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1901,85 +1901,96 @@ impl AgentPanel { ) .anchor(Corner::TopRight) .with_handle(self.new_thread_menu_handle.clone()) - .menu(move |window, cx| { - let active_thread = active_thread.clone(); - Some(ContextMenu::build(window, cx, |mut menu, _window, cx| { - menu = menu - .when(cx.has_flag::(), |this| { - this.header("Zed Agent") - }) - .item( - ContextMenuEntry::new("New Thread") - .icon(IconName::NewThread) - .icon_color(Color::Muted) - .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) - .handler(move |window, cx| { - window.dispatch_action(NewTextThread.boxed_clone(), cx); - }), - ) - .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::NewFromSummary) - .icon_color(Color::Muted) - .handler(move |window, cx| { - window.dispatch_action( - Box::new(NewThread { - from_thread_id: Some(thread_id.clone()), - }), - cx, - ); - }), - ) - } else { - this - } - }) - .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, - ); - }), - ) - }); - menu - })) + .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") + }) + .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); + + if !thread.is_empty() { + let thread_id = thread.id().clone(); + this.item( + ContextMenuEntry::new("New From Summary") + .icon(IconName::NewFromSummary) + .icon_color(Color::Muted) + .handler(move |window, cx| { + window.dispatch_action( + Box::new(NewThread { + from_thread_id: Some(thread_id.clone()), + }), + cx, + ); + }), + ) + } else { + this + } + }) + .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, + ); + }), + ) + }); + menu + })) + } }); let agent_panel_menu = PopoverMenu::new("agent-options-menu") From be0d9eecb72678fb25eed2abc7e781f0d9e0bb2d Mon Sep 17 00:00:00 2001 From: Nicolas Rodriguez <55200060+NRodriguezcuellar@users.noreply.github.com> Date: Wed, 23 Jul 2025 20:52:44 +0200 Subject: [PATCH 294/658] Add collapse functionality to outline entries (#33490) partly Closes #23075 Release Notes: - Now provides collapse and enables functionality to outline entries - Add a new expand_outlines_with_depth setting to customize how deep the tree is expanded by when a file is opened part 2 is in #34164 **Visual examples** ![image](https://github.com/user-attachments/assets/5dcdb83b-6e3e-4bfd-8ef4-76ae2ce4d3e6) ![image](https://github.com/user-attachments/assets/7b786a5a-1a8c-4f34-aaa5-4a8d0afa9668) ![image](https://github.com/user-attachments/assets/1817be06-ac71-4480-8f17-0bd862e913c8) --- assets/settings/default.json | 5 +- crates/outline_panel/src/outline_panel.rs | 862 +++++++++++++++++- .../src/outline_panel_settings.rs | 8 + 3 files changed, 826 insertions(+), 49 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index dab1684aef4412bf2f297787e6443dba6ce01f67..3a7a48efc2769e5942962d786221940e5d967156 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -691,7 +691,10 @@ // 5. Never show the scrollbar: // "never" "show": null - } + }, + // Default depth to expand outline items in the current file. + // Set to 0 to collapse all items that have children, 1 or higher to collapse items at that depth or deeper. + "expand_outlines_with_depth": 100 }, "collaboration_panel": { // Whether to show the collaboration panel button in the status bar. diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 12dcab9e8702a98dbcecd8549ce40fe86fa45e0f..50c6c2dcce95493bb05a38bd10dded196d88aa67 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -1,19 +1,5 @@ mod outline_panel_settings; -use std::{ - cmp, - collections::BTreeMap, - hash::Hash, - ops::Range, - path::{MAIN_SEPARATOR_STR, Path, PathBuf}, - sync::{ - Arc, OnceLock, - atomic::{self, AtomicBool}, - }, - time::Duration, - u32, -}; - use anyhow::Context as _; use collections::{BTreeSet, HashMap, HashSet, hash_map}; use db::kvp::KEY_VALUE_STORE; @@ -36,8 +22,21 @@ use gpui::{ uniform_list, }; use itertools::Itertools; -use language::{BufferId, BufferSnapshot, OffsetRangeExt, OutlineItem}; +use language::{Anchor, BufferId, BufferSnapshot, OffsetRangeExt, OutlineItem}; use menu::{Cancel, SelectFirst, SelectLast, SelectNext, SelectPrevious}; +use std::{ + cmp, + collections::BTreeMap, + hash::Hash, + ops::Range, + path::{MAIN_SEPARATOR_STR, Path, PathBuf}, + sync::{ + Arc, OnceLock, + atomic::{self, AtomicBool}, + }, + time::Duration, + u32, +}; use outline_panel_settings::{OutlinePanelDockPosition, OutlinePanelSettings, ShowIndentGuides}; use project::{File, Fs, GitEntry, GitTraversal, Project, ProjectItem}; @@ -132,6 +131,8 @@ pub struct OutlinePanel { hide_scrollbar_task: Option>, max_width_item_index: Option, preserve_selection_on_buffer_fold_toggles: HashSet, + pending_default_expansion_depth: Option, + outline_children_cache: HashMap, usize), bool>>, } #[derive(Debug)] @@ -318,12 +319,13 @@ struct CachedEntry { entry: PanelEntry, } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] enum CollapsedEntry { Dir(WorktreeId, ProjectEntryId), File(WorktreeId, BufferId), ExternalFile(BufferId), Excerpt(BufferId, ExcerptId), + Outline(BufferId, ExcerptId, Range), } #[derive(Debug)] @@ -803,8 +805,56 @@ impl OutlinePanel { outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx); } } else if &outline_panel_settings != new_settings { + let old_expansion_depth = outline_panel_settings.expand_outlines_with_depth; outline_panel_settings = *new_settings; - cx.notify(); + + if old_expansion_depth != new_settings.expand_outlines_with_depth { + let old_collapsed_entries = outline_panel.collapsed_entries.clone(); + outline_panel + .collapsed_entries + .retain(|entry| !matches!(entry, CollapsedEntry::Outline(..))); + + let new_depth = new_settings.expand_outlines_with_depth; + + for (buffer_id, excerpts) in &outline_panel.excerpts { + for (excerpt_id, excerpt) in excerpts { + if let ExcerptOutlines::Outlines(outlines) = &excerpt.outlines { + for outline in outlines { + if outline_panel + .outline_children_cache + .get(buffer_id) + .and_then(|children_map| { + let key = + (outline.range.clone(), outline.depth); + children_map.get(&key) + }) + .copied() + .unwrap_or(false) + && (new_depth == 0 || outline.depth >= new_depth) + { + outline_panel.collapsed_entries.insert( + CollapsedEntry::Outline( + *buffer_id, + *excerpt_id, + outline.range.clone(), + ), + ); + } + } + } + } + } + + if old_collapsed_entries != outline_panel.collapsed_entries { + outline_panel.update_cached_entries( + Some(UPDATE_DEBOUNCE), + window, + cx, + ); + } + } else { + cx.notify(); + } } }); @@ -841,6 +891,7 @@ impl OutlinePanel { updating_cached_entries: false, new_entries_for_fs_update: HashSet::default(), preserve_selection_on_buffer_fold_toggles: HashSet::default(), + pending_default_expansion_depth: None, fs_entries_update_task: Task::ready(()), cached_entries_update_task: Task::ready(()), reveal_selection_task: Task::ready(Ok(())), @@ -855,6 +906,7 @@ impl OutlinePanel { workspace_subscription, filter_update_subscription, ], + outline_children_cache: HashMap::default(), }; if let Some((item, editor)) = workspace_active_editor(workspace, cx) { outline_panel.replace_active_editor(item, editor, window, cx); @@ -1462,7 +1514,12 @@ impl OutlinePanel { PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => { Some(CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id)) } - PanelEntry::Search(_) | PanelEntry::Outline(..) => return, + PanelEntry::Outline(OutlineEntry::Outline(outline)) => Some(CollapsedEntry::Outline( + outline.buffer_id, + outline.excerpt_id, + outline.outline.range.clone(), + )), + PanelEntry::Search(_) => return, }; let Some(collapsed_entry) = entry_to_expand else { return; @@ -1565,7 +1622,14 @@ impl OutlinePanel { PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => self .collapsed_entries .insert(CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id)), - PanelEntry::Search(_) | PanelEntry::Outline(..) => false, + PanelEntry::Outline(OutlineEntry::Outline(outline)) => { + self.collapsed_entries.insert(CollapsedEntry::Outline( + outline.buffer_id, + outline.excerpt_id, + outline.outline.range.clone(), + )) + } + PanelEntry::Search(_) => false, }; if collapsed { @@ -1780,7 +1844,17 @@ impl OutlinePanel { self.collapsed_entries.insert(collapsed_entry); } } - PanelEntry::Search(_) | PanelEntry::Outline(..) => return, + PanelEntry::Outline(OutlineEntry::Outline(outline)) => { + let collapsed_entry = CollapsedEntry::Outline( + outline.buffer_id, + outline.excerpt_id, + outline.outline.range.clone(), + ); + if !self.collapsed_entries.remove(&collapsed_entry) { + self.collapsed_entries.insert(collapsed_entry); + } + } + _ => {} } active_editor.update(cx, |editor, cx| { @@ -2108,7 +2182,7 @@ impl OutlinePanel { PanelEntry::Outline(OutlineEntry::Excerpt(excerpt.clone())), item_id, depth, - Some(icon), + icon, is_active, label_element, window, @@ -2160,10 +2234,31 @@ impl OutlinePanel { _ => false, }; - let icon = if self.is_singleton_active(cx) { - None + let has_children = self + .outline_children_cache + .get(&outline.buffer_id) + .and_then(|children_map| { + let key = (outline.outline.range.clone(), outline.outline.depth); + children_map.get(&key) + }) + .copied() + .unwrap_or(false); + let is_expanded = !self.collapsed_entries.contains(&CollapsedEntry::Outline( + outline.buffer_id, + outline.excerpt_id, + outline.outline.range.clone(), + )); + + let icon = if has_children { + FileIcons::get_chevron_icon(is_expanded, cx) + .map(|icon_path| { + Icon::from_path(icon_path) + .color(entry_label_color(is_active)) + .into_any_element() + }) + .unwrap_or_else(empty_icon) } else { - Some(empty_icon()) + empty_icon() }; self.entry_element( @@ -2287,7 +2382,7 @@ impl OutlinePanel { PanelEntry::Fs(rendered_entry.clone()), item_id, depth, - Some(icon), + icon, is_active, label_element, window, @@ -2358,7 +2453,7 @@ impl OutlinePanel { PanelEntry::FoldedDirs(folded_dir.clone()), item_id, depth, - Some(icon), + icon, is_active, label_element, window, @@ -2449,7 +2544,7 @@ impl OutlinePanel { }), ElementId::from(SharedString::from(format!("search-{match_range:?}"))), depth, - None, + empty_icon(), is_active, entire_label, window, @@ -2462,7 +2557,7 @@ impl OutlinePanel { rendered_entry: PanelEntry, item_id: ElementId, depth: usize, - icon_element: Option, + icon_element: AnyElement, is_active: bool, label_element: gpui::AnyElement, window: &mut Window, @@ -2478,8 +2573,10 @@ impl OutlinePanel { if event.down.button == MouseButton::Right || event.down.first_mouse { return; } + let change_focus = event.down.click_count > 1; outline_panel.toggle_expanded(&clicked_entry, window, cx); + outline_panel.scroll_editor_to_entry( &clicked_entry, true, @@ -2495,10 +2592,11 @@ impl OutlinePanel { .indent_level(depth) .indent_step_size(px(settings.indent_size)) .toggle_state(is_active) - .when_some(icon_element, |list_item, icon_element| { - list_item.child(h_flex().child(icon_element)) - }) - .child(h_flex().h_6().child(label_element).ml_1()) + .child( + h_flex() + .child(h_flex().w(px(16.)).justify_center().child(icon_element)) + .child(h_flex().h_6().child(label_element).ml_1()), + ) .on_secondary_mouse_down(cx.listener( move |outline_panel, event: &MouseDownEvent, window, cx| { // Stop propagation to prevent the catch-all context menu for the project @@ -2940,7 +3038,12 @@ impl OutlinePanel { outline_panel.fs_entries_depth = new_depth_map; outline_panel.fs_children_count = new_children_count; outline_panel.update_non_fs_items(window, cx); - outline_panel.update_cached_entries(debounce, window, cx); + + // Only update cached entries if we don't have outlines to fetch + // If we do have outlines to fetch, let fetch_outdated_outlines handle the update + if outline_panel.excerpt_fetch_ranges(cx).is_empty() { + outline_panel.update_cached_entries(debounce, window, cx); + } cx.notify(); }) @@ -2956,6 +3059,12 @@ impl OutlinePanel { cx: &mut Context, ) { self.clear_previous(window, cx); + + let default_expansion_depth = + OutlinePanelSettings::get_global(cx).expand_outlines_with_depth; + // We'll apply the expansion depth after outlines are loaded + self.pending_default_expansion_depth = Some(default_expansion_depth); + let buffer_search_subscription = cx.subscribe_in( &new_active_editor, window, @@ -3004,6 +3113,7 @@ impl OutlinePanel { self.selected_entry = SelectedEntry::None; self.pinned = false; self.mode = ItemsDisplayMode::Outline; + self.pending_default_expansion_depth = None; } fn location_for_editor_selection( @@ -3259,25 +3369,74 @@ impl OutlinePanel { || buffer_language.as_ref() == buffer_snapshot.language_at(outline.range.start) }); - outlines + + let outlines_with_children = outlines + .windows(2) + .filter_map(|window| { + let current = &window[0]; + let next = &window[1]; + if next.depth > current.depth { + Some((current.range.clone(), current.depth)) + } else { + None + } + }) + .collect::>(); + + (outlines, outlines_with_children) }) .await; + + let (fetched_outlines, outlines_with_children) = fetched_outlines; + outline_panel .update_in(cx, |outline_panel, window, cx| { + let pending_default_depth = + outline_panel.pending_default_expansion_depth.take(); + + let debounce = + if first_update.fetch_and(false, atomic::Ordering::AcqRel) { + None + } else { + Some(UPDATE_DEBOUNCE) + }; + if let Some(excerpt) = outline_panel .excerpts .entry(buffer_id) .or_default() .get_mut(&excerpt_id) { - let debounce = if first_update - .fetch_and(false, atomic::Ordering::AcqRel) - { - None - } else { - Some(UPDATE_DEBOUNCE) - }; excerpt.outlines = ExcerptOutlines::Outlines(fetched_outlines); + + if let Some(default_depth) = pending_default_depth { + if 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(), + ), + ); + }); + } + } + + // Even if no outlines to check, we still need to update cached entries + // to show the outline entries that were just fetched outline_panel.update_cached_entries(debounce, window, cx); } }) @@ -4083,7 +4242,7 @@ impl OutlinePanel { } fn add_excerpt_entries( - &self, + &mut self, state: &mut GenerationState, buffer_id: BufferId, entries_to_add: &[ExcerptId], @@ -4094,6 +4253,8 @@ impl OutlinePanel { cx: &mut Context, ) { if let Some(excerpts) = self.excerpts.get(&buffer_id) { + let buffer_snapshot = self.buffer_snapshot_for_id(buffer_id, cx); + for &excerpt_id in entries_to_add { let Some(excerpt) = excerpts.get(&excerpt_id) else { continue; @@ -4123,15 +4284,84 @@ impl OutlinePanel { continue; } - for outline in excerpt.iter_outlines() { + let mut last_depth_at_level: Vec>> = vec![None; 10]; + + let all_outlines: Vec<_> = excerpt.iter_outlines().collect(); + + let mut outline_has_children = HashMap::default(); + let mut visible_outlines = Vec::new(); + let mut collapsed_state: Option<(usize, Range)> = None; + + for (i, &outline) in all_outlines.iter().enumerate() { + let has_children = all_outlines + .get(i + 1) + .map(|next| next.depth > outline.depth) + .unwrap_or(false); + + outline_has_children + .insert((outline.range.clone(), outline.depth), has_children); + + let mut should_include = true; + + if let Some((collapsed_depth, collapsed_range)) = &collapsed_state { + if outline.depth <= *collapsed_depth { + collapsed_state = None; + } else if let Some(buffer_snapshot) = buffer_snapshot.as_ref() { + let outline_start = outline.range.start; + if outline_start + .cmp(&collapsed_range.start, buffer_snapshot) + .is_ge() + && outline_start + .cmp(&collapsed_range.end, buffer_snapshot) + .is_lt() + { + should_include = false; // Skip - inside collapsed range + } else { + collapsed_state = None; + } + } + } + + // Check if this outline itself is collapsed + if should_include + && self.collapsed_entries.contains(&CollapsedEntry::Outline( + buffer_id, + excerpt_id, + outline.range.clone(), + )) + { + collapsed_state = Some((outline.depth, outline.range.clone())); + } + + if should_include { + visible_outlines.push(outline); + } + } + + self.outline_children_cache + .entry(buffer_id) + .or_default() + .extend(outline_has_children); + + for outline in visible_outlines { + let outline_entry = OutlineEntryOutline { + buffer_id, + excerpt_id, + outline: outline.clone(), + }; + + if outline.depth < last_depth_at_level.len() { + last_depth_at_level[outline.depth] = Some(outline.range.clone()); + // Clear deeper levels when we go back to a shallower depth + for d in (outline.depth + 1)..last_depth_at_level.len() { + last_depth_at_level[d] = None; + } + } + self.push_entry( state, track_matches, - PanelEntry::Outline(OutlineEntry::Outline(OutlineEntryOutline { - buffer_id, - excerpt_id, - outline: outline.clone(), - })), + PanelEntry::Outline(OutlineEntry::Outline(outline_entry)), outline_base_depth + outline.depth, cx, ); @@ -6908,4 +7138,540 @@ outline: struct OutlineEntryExcerpt multi_buffer_snapshot.text_for_range(line_start..line_end).collect::().trim().to_owned() }) } + + #[gpui::test] + async fn test_outline_keyboard_expand_collapse(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/test", + json!({ + "src": { + "lib.rs": indoc!(" + mod outer { + pub struct OuterStruct { + field: String, + } + impl OuterStruct { + pub fn new() -> Self { + Self { field: String::new() } + } + pub fn method(&self) { + println!(\"{}\", self.field); + } + } + mod inner { + pub fn inner_function() { + let x = 42; + println!(\"{}\", x); + } + pub struct InnerStruct { + value: i32, + } + } + } + fn main() { + let s = outer::OuterStruct::new(); + s.method(); + } + "), + } + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; + project.read_with(cx, |project, _| { + project.languages().add(Arc::new( + rust_lang() + .with_outline_query( + r#" + (struct_item + (visibility_modifier)? @context + "struct" @context + name: (_) @name) @item + (impl_item + "impl" @context + trait: (_)? @context + "for"? @context + type: (_) @context + body: (_)) @item + (function_item + (visibility_modifier)? @context + "fn" @context + name: (_) @name + parameters: (_) @context) @item + (mod_item + (visibility_modifier)? @context + "mod" @context + name: (_) @name) @item + (enum_item + (visibility_modifier)? @context + "enum" @context + name: (_) @name) @item + (field_declaration + (visibility_modifier)? @context + name: (_) @name + ":" @context + type: (_) @context) @item + "#, + ) + .unwrap(), + )) + }); + let workspace = add_outline_panel(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let outline_panel = outline_panel(&workspace, cx); + + outline_panel.update_in(cx, |outline_panel, window, cx| { + outline_panel.set_active(true, window, cx) + }); + + workspace + .update(cx, |workspace, window, cx| { + workspace.open_abs_path( + PathBuf::from("/test/src/lib.rs"), + OpenOptions { + visible: Some(OpenVisible::All), + ..Default::default() + }, + window, + cx, + ) + }) + .unwrap() + .await + .unwrap(); + + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500)); + cx.run_until_parked(); + + // Force another update cycle to ensure outlines are fetched + outline_panel.update_in(cx, |panel, window, cx| { + panel.update_non_fs_items(window, cx); + panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx); + }); + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500)); + cx.run_until_parked(); + + outline_panel.update(cx, |outline_panel, cx| { + assert_eq!( + display_entries( + &project, + &snapshot(&outline_panel, cx), + &outline_panel.cached_entries, + outline_panel.selected_entry(), + cx, + ), + indoc!( + " +outline: mod outer <==== selected + outline: pub struct OuterStruct + outline: field: String + outline: impl OuterStruct + outline: pub fn new() + outline: pub fn method(&self) + outline: mod inner + outline: pub fn inner_function() + outline: pub struct InnerStruct + outline: value: i32 +outline: fn main()" + ) + ); + }); + + let parent_outline = outline_panel + .read_with(cx, |panel, _cx| { + panel + .cached_entries + .iter() + .find_map(|entry| match &entry.entry { + PanelEntry::Outline(OutlineEntry::Outline(outline)) + if panel + .outline_children_cache + .get(&outline.buffer_id) + .and_then(|children_map| { + let key = + (outline.outline.range.clone(), outline.outline.depth); + children_map.get(&key) + }) + .copied() + .unwrap_or(false) => + { + Some(entry.entry.clone()) + } + _ => None, + }) + }) + .expect("Should find an outline with children"); + + outline_panel.update_in(cx, |panel, window, cx| { + panel.select_entry(parent_outline.clone(), true, window, cx); + panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx); + }); + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); + cx.run_until_parked(); + + outline_panel.update(cx, |outline_panel, cx| { + assert_eq!( + display_entries( + &project, + &snapshot(&outline_panel, cx), + &outline_panel.cached_entries, + outline_panel.selected_entry(), + cx, + ), + indoc!( + " +outline: mod outer <==== selected +outline: fn main()" + ) + ); + }); + + outline_panel.update_in(cx, |panel, window, cx| { + panel.expand_selected_entry(&ExpandSelectedEntry, window, cx); + }); + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); + cx.run_until_parked(); + + outline_panel.update(cx, |outline_panel, cx| { + assert_eq!( + display_entries( + &project, + &snapshot(&outline_panel, cx), + &outline_panel.cached_entries, + outline_panel.selected_entry(), + cx, + ), + indoc!( + " +outline: mod outer <==== selected + outline: pub struct OuterStruct + outline: field: String + outline: impl OuterStruct + outline: pub fn new() + outline: pub fn method(&self) + outline: mod inner + outline: pub fn inner_function() + outline: pub struct InnerStruct + outline: value: i32 +outline: fn main()" + ) + ); + }); + + outline_panel.update_in(cx, |panel, window, cx| { + panel.collapsed_entries.clear(); + panel.update_cached_entries(None, window, cx); + }); + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); + cx.run_until_parked(); + + outline_panel.update_in(cx, |panel, window, cx| { + let outlines_with_children: Vec<_> = panel + .cached_entries + .iter() + .filter_map(|entry| match &entry.entry { + PanelEntry::Outline(OutlineEntry::Outline(outline)) + if panel + .outline_children_cache + .get(&outline.buffer_id) + .and_then(|children_map| { + let key = (outline.outline.range.clone(), outline.outline.depth); + children_map.get(&key) + }) + .copied() + .unwrap_or(false) => + { + Some(entry.entry.clone()) + } + _ => None, + }) + .collect(); + + for outline in outlines_with_children { + panel.select_entry(outline, false, window, cx); + panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx); + } + }); + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); + cx.run_until_parked(); + + outline_panel.update(cx, |outline_panel, cx| { + assert_eq!( + display_entries( + &project, + &snapshot(&outline_panel, cx), + &outline_panel.cached_entries, + outline_panel.selected_entry(), + cx, + ), + indoc!( + " +outline: mod outer +outline: fn main()" + ) + ); + }); + + let collapsed_entries_count = + outline_panel.read_with(cx, |panel, _| panel.collapsed_entries.len()); + assert!( + collapsed_entries_count > 0, + "Should have collapsed entries tracked" + ); + } + + #[gpui::test] + async fn test_outline_click_toggle_behavior(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/test", + json!({ + "src": { + "main.rs": indoc!(" + struct Config { + name: String, + value: i32, + } + impl Config { + fn new(name: String) -> Self { + Self { name, value: 0 } + } + fn get_value(&self) -> i32 { + self.value + } + } + enum Status { + Active, + Inactive, + } + fn process_config(config: Config) -> Status { + if config.get_value() > 0 { + Status::Active + } else { + Status::Inactive + } + } + fn main() { + let config = Config::new(\"test\".to_string()); + let status = process_config(config); + } + "), + } + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; + project.read_with(cx, |project, _| { + project.languages().add(Arc::new( + rust_lang() + .with_outline_query( + r#" + (struct_item + (visibility_modifier)? @context + "struct" @context + name: (_) @name) @item + (impl_item + "impl" @context + trait: (_)? @context + "for"? @context + type: (_) @context + body: (_)) @item + (function_item + (visibility_modifier)? @context + "fn" @context + name: (_) @name + parameters: (_) @context) @item + (mod_item + (visibility_modifier)? @context + "mod" @context + name: (_) @name) @item + (enum_item + (visibility_modifier)? @context + "enum" @context + name: (_) @name) @item + (field_declaration + (visibility_modifier)? @context + name: (_) @name + ":" @context + type: (_) @context) @item + "#, + ) + .unwrap(), + )) + }); + + let workspace = add_outline_panel(&project, cx).await; + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let outline_panel = outline_panel(&workspace, cx); + + outline_panel.update_in(cx, |outline_panel, window, cx| { + outline_panel.set_active(true, window, cx) + }); + + let _editor = workspace + .update(cx, |workspace, window, cx| { + workspace.open_abs_path( + PathBuf::from("/test/src/main.rs"), + OpenOptions { + visible: Some(OpenVisible::All), + ..Default::default() + }, + window, + cx, + ) + }) + .unwrap() + .await + .unwrap(); + + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); + cx.run_until_parked(); + + outline_panel.update(cx, |outline_panel, _cx| { + outline_panel.selected_entry = SelectedEntry::None; + }); + + // Check initial state - all entries should be expanded by default + outline_panel.update(cx, |outline_panel, cx| { + assert_eq!( + display_entries( + &project, + &snapshot(&outline_panel, cx), + &outline_panel.cached_entries, + outline_panel.selected_entry(), + cx, + ), + indoc!( + " +outline: struct Config + outline: name: String + outline: value: i32 +outline: impl Config + outline: fn new(name: String) + outline: fn get_value(&self) +outline: enum Status +outline: fn process_config(config: Config) +outline: fn main()" + ) + ); + }); + + outline_panel.update(cx, |outline_panel, _cx| { + outline_panel.selected_entry = SelectedEntry::None; + }); + + cx.update(|window, cx| { + outline_panel.update(cx, |outline_panel, cx| { + outline_panel.select_first(&SelectFirst, window, cx); + }); + }); + + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); + cx.run_until_parked(); + + outline_panel.update(cx, |outline_panel, cx| { + assert_eq!( + display_entries( + &project, + &snapshot(&outline_panel, cx), + &outline_panel.cached_entries, + outline_panel.selected_entry(), + cx, + ), + indoc!( + " +outline: struct Config <==== selected + outline: name: String + outline: value: i32 +outline: impl Config + outline: fn new(name: String) + outline: fn get_value(&self) +outline: enum Status +outline: fn process_config(config: Config) +outline: fn main()" + ) + ); + }); + + cx.update(|window, cx| { + outline_panel.update(cx, |outline_panel, cx| { + outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx); + }); + }); + + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); + cx.run_until_parked(); + + outline_panel.update(cx, |outline_panel, cx| { + assert_eq!( + display_entries( + &project, + &snapshot(&outline_panel, cx), + &outline_panel.cached_entries, + outline_panel.selected_entry(), + cx, + ), + indoc!( + " +outline: struct Config <==== selected +outline: impl Config + outline: fn new(name: String) + outline: fn get_value(&self) +outline: enum Status +outline: fn process_config(config: Config) +outline: fn main()" + ) + ); + }); + + cx.update(|window, cx| { + outline_panel.update(cx, |outline_panel, cx| { + outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx); + }); + }); + + cx.executor() + .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); + cx.run_until_parked(); + + outline_panel.update(cx, |outline_panel, cx| { + assert_eq!( + display_entries( + &project, + &snapshot(&outline_panel, cx), + &outline_panel.cached_entries, + outline_panel.selected_entry(), + cx, + ), + indoc!( + " +outline: struct Config <==== selected + outline: name: String + outline: value: i32 +outline: impl Config + outline: fn new(name: String) + outline: fn get_value(&self) +outline: enum Status +outline: fn process_config(config: Config) +outline: fn main()" + ) + ); + }); + } } diff --git a/crates/outline_panel/src/outline_panel_settings.rs b/crates/outline_panel/src/outline_panel_settings.rs index 6b70cb54fbc23e03fdbc13c90b912648daf9515b..133d28b748d2978e07a540b3c8c7517b03dc4767 100644 --- a/crates/outline_panel/src/outline_panel_settings.rs +++ b/crates/outline_panel/src/outline_panel_settings.rs @@ -31,6 +31,7 @@ pub struct OutlinePanelSettings { pub auto_reveal_entries: bool, pub auto_fold_dirs: bool, pub scrollbar: ScrollbarSettings, + pub expand_outlines_with_depth: usize, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] @@ -105,6 +106,13 @@ pub struct OutlinePanelSettingsContent { pub indent_guides: Option, /// Scrollbar-related settings pub scrollbar: Option, + /// Default depth to expand outline items in the current file. + /// The default depth to which outline entries are expanded on reveal. + /// - Set to 0 to collapse all items that have children + /// - Set to 1 or higher to collapse items at that depth or deeper + /// + /// Default: 100 + pub expand_outlines_with_depth: Option, } impl Settings for OutlinePanelSettings { From 50985b7d2306988c03f87a78e89376dc44f7faac Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 23 Jul 2025 16:30:21 -0400 Subject: [PATCH 295/658] Fix telemetry event type names (#34974) Release Notes: - N/A --- crates/ai_onboarding/src/ai_onboarding.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ai_onboarding/src/ai_onboarding.rs b/crates/ai_onboarding/src/ai_onboarding.rs index 9d32b1ee09b38e0f1b6eba80809ec6d4fcd7c55d..f9a91503aee351a6c745d3f3a0e6aea2cc05a165 100644 --- a/crates/ai_onboarding/src/ai_onboarding.rs +++ b/crates/ai_onboarding/src/ai_onboarding.rs @@ -237,7 +237,7 @@ impl ZedAiOnboarding { .icon_color(Color::Muted) .icon_size(IconSize::XSmall) .on_click(move |_, _window, cx| { - telemetry::event!("Review Terms of Service Click"); + telemetry::event!("Review Terms of Service Clicked"); cx.open_url(&zed_urls::terms_of_service(cx)) }), ) @@ -248,7 +248,7 @@ impl ZedAiOnboarding { .on_click({ let callback = self.accept_terms_of_service.clone(); move |_, window, cx| { - telemetry::event!("Accepted Terms of Service"); + telemetry::event!("Terms of Service Accepted"); (callback)(window, cx)} }), ) From edceb7284f895539fdd71533c1e59c31c4b7940e Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Wed, 23 Jul 2025 16:55:13 -0400 Subject: [PATCH 296/658] Redact secrets from environment in LSP Server Info (#34971) In "Server Info" view of LSP logs: - Redacts sensitive values from environment - Sorts environment by name | Before | After | | - | - | | Screenshot 2025-07-23 at 14 10 14 | image | Release Notes: - Improved display of environment variables in LSP Logs: Server Info view --- crates/lsp/src/lsp.rs | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 7dcfa61f471680b6a0753f3002d723b7b8194935..a820aaf748f9f6749d31bc690b9bba8181545170 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -4,7 +4,7 @@ pub use lsp_types::request::*; pub use lsp_types::*; use anyhow::{Context as _, Result, anyhow}; -use collections::HashMap; +use collections::{BTreeMap, HashMap}; use futures::{ AsyncRead, AsyncWrite, Future, FutureExt, channel::oneshot::{self, Canceled}, @@ -40,7 +40,7 @@ use std::{ time::{Duration, Instant}, }; use std::{path::Path, process::Stdio}; -use util::{ConnectionResult, ResultExt, TryFutureExt}; +use util::{ConnectionResult, ResultExt, TryFutureExt, redact}; const JSON_RPC_VERSION: &str = "2.0"; const CONTENT_LEN_HEADER: &str = "Content-Length: "; @@ -62,7 +62,7 @@ pub enum IoKind { /// Represents a launchable language server. This can either be a standalone binary or the path /// to a runtime with arguments to instruct it to launch the actual language server file. -#[derive(Debug, Clone, Deserialize)] +#[derive(Clone, Deserialize)] pub struct LanguageServerBinary { pub path: PathBuf, pub arguments: Vec, @@ -1448,6 +1448,33 @@ impl fmt::Debug for LanguageServer { } } +impl fmt::Debug for LanguageServerBinary { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut debug = f.debug_struct("LanguageServerBinary"); + debug.field("path", &self.path); + debug.field("arguments", &self.arguments); + + if let Some(env) = &self.env { + let redacted_env: BTreeMap = env + .iter() + .map(|(key, value)| { + let redacted_value = if redact::should_redact(key) { + "REDACTED".to_string() + } else { + value.clone() + }; + (key.clone(), redacted_value) + }) + .collect(); + debug.field("env", &Some(redacted_env)); + } else { + debug.field("env", &self.env); + } + + debug.finish() + } +} + impl Drop for Subscription { fn drop(&mut self) { match self { From 8bf7dcb6131e6d40c59c03a0ddd6c53aacbe7516 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 23 Jul 2025 18:09:05 -0300 Subject: [PATCH 297/658] agent: Fix follow button disabled state (#34978) Release Notes: - N/A --- crates/agent_ui/src/message_editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index ab8ba762f4e64a90679c6bf485c4554631106f78..62be5629f1e1eac1e6fa65e13d459dab3324dc48 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -625,7 +625,7 @@ impl MessageEditor { .unwrap_or(false); IconButton::new("follow-agent", IconName::Crosshair) - .disabled(is_model_selected) + .disabled(!is_model_selected) .icon_size(IconSize::Small) .icon_color(Color::Muted) .toggle_state(following) From 7e9d6cc25c1829089918d514b85f3a419ca26dc7 Mon Sep 17 00:00:00 2001 From: Renato Lochetti Date: Wed, 23 Jul 2025 22:27:25 +0100 Subject: [PATCH 298/658] mistral: Add support for Mistral Devstral Medium (#34888) Mistral released their new DevstralMedium model to be used via API: https://mistral.ai/news/devstral-2507 Release Notes: - Add support for Mistral Devstral Medium --- crates/mistral/src/mistral.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/mistral/src/mistral.rs b/crates/mistral/src/mistral.rs index a3a017be83544ce60dc1b8bac09cde5e5058b4f4..bf6ccf288328b49ea8d2770ab6db609f6038d299 100644 --- a/crates/mistral/src/mistral.rs +++ b/crates/mistral/src/mistral.rs @@ -58,6 +58,8 @@ pub enum Model { OpenMistralNemo, #[serde(rename = "open-codestral-mamba", alias = "open-codestral-mamba")] OpenCodestralMamba, + #[serde(rename = "devstral-medium-latest", alias = "devstral-medium-latest")] + DevstralMediumLatest, #[serde(rename = "devstral-small-latest", alias = "devstral-small-latest")] DevstralSmallLatest, #[serde(rename = "pixtral-12b-latest", alias = "pixtral-12b-latest")] @@ -91,6 +93,7 @@ impl Model { "mistral-small-latest" => Ok(Self::MistralSmallLatest), "open-mistral-nemo" => Ok(Self::OpenMistralNemo), "open-codestral-mamba" => Ok(Self::OpenCodestralMamba), + "devstral-medium-latest" => Ok(Self::DevstralMediumLatest), "devstral-small-latest" => Ok(Self::DevstralSmallLatest), "pixtral-12b-latest" => Ok(Self::Pixtral12BLatest), "pixtral-large-latest" => Ok(Self::PixtralLargeLatest), @@ -106,6 +109,7 @@ impl Model { Self::MistralSmallLatest => "mistral-small-latest", Self::OpenMistralNemo => "open-mistral-nemo", Self::OpenCodestralMamba => "open-codestral-mamba", + Self::DevstralMediumLatest => "devstral-medium-latest", Self::DevstralSmallLatest => "devstral-small-latest", Self::Pixtral12BLatest => "pixtral-12b-latest", Self::PixtralLargeLatest => "pixtral-large-latest", @@ -121,6 +125,7 @@ impl Model { Self::MistralSmallLatest => "mistral-small-latest", Self::OpenMistralNemo => "open-mistral-nemo", Self::OpenCodestralMamba => "open-codestral-mamba", + Self::DevstralMediumLatest => "devstral-medium-latest", Self::DevstralSmallLatest => "devstral-small-latest", Self::Pixtral12BLatest => "pixtral-12b-latest", Self::PixtralLargeLatest => "pixtral-large-latest", @@ -138,6 +143,7 @@ impl Model { Self::MistralSmallLatest => 32000, Self::OpenMistralNemo => 131000, Self::OpenCodestralMamba => 256000, + Self::DevstralMediumLatest => 128000, Self::DevstralSmallLatest => 262144, Self::Pixtral12BLatest => 128000, Self::PixtralLargeLatest => 128000, @@ -162,6 +168,7 @@ impl Model { | Self::MistralSmallLatest | Self::OpenMistralNemo | Self::OpenCodestralMamba + | Self::DevstralMediumLatest | Self::DevstralSmallLatest | Self::Pixtral12BLatest | Self::PixtralLargeLatest => true, @@ -179,6 +186,7 @@ impl Model { | Self::MistralLargeLatest | Self::OpenMistralNemo | Self::OpenCodestralMamba + | Self::DevstralMediumLatest | Self::DevstralSmallLatest => false, Self::Custom { supports_images, .. From b63d820be23762f10313bce17e9c98cbba7ab9dd Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Thu, 24 Jul 2025 03:46:29 +0530 Subject: [PATCH 299/658] editor: Fix move line up panic when selection is at end of line next to fold marker (#34982) Closes #34826 In move line up method, make use of `prev_line_boundary` which accounts for fold map, etc., for selection start row so that we don't incorrectly calculate row range to move up. Release Notes: - Fixed an issue where `editor: move line up` action sometimes crashed if the cursor was at the end of a line beside a fold marker. --- crates/editor/src/editor.rs | 10 +++++++++- crates/editor/src/editor_tests.rs | 27 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d5448f30f362d0a49edb074b5d16f6b860efcbaa..a695c8fd0c35ddcf7e44f7c47b0c8ff09f6754fb 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -22258,7 +22258,7 @@ fn consume_contiguous_rows( selections: &mut Peekable>>, ) -> (MultiBufferRow, MultiBufferRow) { contiguous_row_selections.push(selection.clone()); - let start_row = MultiBufferRow(selection.start.row); + let start_row = starting_row(selection, display_map); let mut end_row = ending_row(selection, display_map); while let Some(next_selection) = selections.peek() { @@ -22272,6 +22272,14 @@ fn consume_contiguous_rows( (start_row, end_row) } +fn starting_row(selection: &Selection, display_map: &DisplaySnapshot) -> MultiBufferRow { + if selection.start.column > 0 { + MultiBufferRow(display_map.prev_line_boundary(selection.start).0.row) + } else { + MultiBufferRow(selection.start.row) + } +} + fn ending_row(next_selection: &Selection, display_map: &DisplaySnapshot) -> MultiBufferRow { if next_selection.end.column > 0 || next_selection.is_empty() { MultiBufferRow(display_map.next_line_boundary(next_selection.end).0.row + 1) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index b9ca8c37552412959e64f98dc04c1f840a45fde5..0d69c067ee7cc572e3b246a8ba91d55ff4ac306c 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -5069,6 +5069,33 @@ fn test_move_line_up_down(cx: &mut TestAppContext) { }); } +#[gpui::test] +fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let editor = cx.add_window(|window, cx| { + let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx); + build_editor(buffer, window, cx) + }); + _ = editor.update(cx, |editor, window, cx| { + editor.fold_creases( + vec![Crease::simple( + Point::new(6, 4)..Point::new(7, 4), + FoldPlaceholder::test(), + )], + true, + window, + cx, + ); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([Point::new(7, 4)..Point::new(7, 4)]) + }); + assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc"); + editor.move_line_up(&MoveLineUp, window, cx); + let buffer_text = editor.buffer.read(cx).snapshot(cx).text(); + assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc"); + }); +} + #[gpui::test] fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { init_test(cx, |_| {}); From 3da23cc65bf8b900219b85002b5b38dca0d3b83e Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 24 Jul 2025 00:33:43 +0200 Subject: [PATCH 300/658] Re-land taffy 0.8.3 (#34939) Re #34938 - **chore: Bump taffy to 0.8.3** - **editor: Fix sticky multi-buffer header not extending to the full width** Release Notes: - N/A --- Cargo.lock | 9 ++++----- crates/editor/src/code_context_menus.rs | 2 +- crates/editor/src/element.rs | 1 + crates/gpui/Cargo.toml | 2 +- crates/gpui/src/taffy.rs | 26 ++++++++++++------------- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 851c658735c2991fa78a42a1b615fe9849c0af0b..8f791d395afe43d47cac363009f88c244d63bb69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7402,9 +7402,9 @@ dependencies = [ [[package]] name = "grid" -version = "0.14.0" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be136d9dacc2a13cc70bb6c8f902b414fb2641f8db1314637c6b7933411a8f82" +checksum = "71b01d27060ad58be4663b9e4ac9e2d4806918e8876af8912afbddd1a91d5eaa" [[package]] name = "group" @@ -15961,13 +15961,12 @@ dependencies = [ [[package]] name = "taffy" -version = "0.5.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8b61630cba2afd2c851821add2e1bb1b7851a2436e839ab73b56558b009035e" +checksum = "7aaef0ac998e6527d6d0d5582f7e43953bb17221ac75bb8eb2fcc2db3396db1c" dependencies = [ "arrayvec", "grid", - "num-traits", "serde", "slotmap", ] diff --git a/crates/editor/src/code_context_menus.rs b/crates/editor/src/code_context_menus.rs index 9f842836ed20bb960c1e112398b8939d6f77e6cc..52446ceafcaa47dc3e26ac4ee0684645df4bd99a 100644 --- a/crates/editor/src/code_context_menus.rs +++ b/crates/editor/src/code_context_menus.rs @@ -844,7 +844,7 @@ impl CompletionsMenu { .with_sizing_behavior(ListSizingBehavior::Infer) .w(rems(34.)); - Popover::new().child(list).into_any_element() + Popover::new().child(div().child(list)).into_any_element() } fn render_aside( diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 1b372a7d5378d2d41e9aef3a56ff91f73101db49..d2ee9d6b0a8411f862f395b19b0016bdf79ca765 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -4010,6 +4010,7 @@ impl EditorElement { let available_width = hitbox.bounds.size.width - right_margin; let mut header = v_flex() + .w_full() .relative() .child( div() diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index b446ea8bd8197149a10ad02fd7c506622971d4c5..29e81269e32a9fd7dfd378f38fdad21c5031fbf9 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.5.1" +taffy = "=0.8.3" thiserror.workspace = true util.workspace = true uuid.workspace = true diff --git a/crates/gpui/src/taffy.rs b/crates/gpui/src/taffy.rs index 6228a604904f6aa40d6d15fb7f9c5ff19b29f6a1..f7fa54256df20b38170ecb4d3e48c22913e44ae6 100644 --- a/crates/gpui/src/taffy.rs +++ b/crates/gpui/src/taffy.rs @@ -283,7 +283,7 @@ impl ToTaffy for Length { fn to_taffy(&self, rem_size: Pixels) -> taffy::prelude::LengthPercentageAuto { match self { Length::Definite(length) => length.to_taffy(rem_size), - Length::Auto => taffy::prelude::LengthPercentageAuto::Auto, + Length::Auto => taffy::prelude::LengthPercentageAuto::auto(), } } } @@ -292,7 +292,7 @@ impl ToTaffy for Length { fn to_taffy(&self, rem_size: Pixels) -> taffy::prelude::Dimension { match self { Length::Definite(length) => length.to_taffy(rem_size), - Length::Auto => taffy::prelude::Dimension::Auto, + Length::Auto => taffy::prelude::Dimension::auto(), } } } @@ -302,14 +302,14 @@ impl ToTaffy for DefiniteLength { match self { DefiniteLength::Absolute(length) => match length { AbsoluteLength::Pixels(pixels) => { - taffy::style::LengthPercentage::Length(pixels.into()) + taffy::style::LengthPercentage::length(pixels.into()) } AbsoluteLength::Rems(rems) => { - taffy::style::LengthPercentage::Length((*rems * rem_size).into()) + taffy::style::LengthPercentage::length((*rems * rem_size).into()) } }, DefiniteLength::Fraction(fraction) => { - taffy::style::LengthPercentage::Percent(*fraction) + taffy::style::LengthPercentage::percent(*fraction) } } } @@ -320,14 +320,14 @@ impl ToTaffy for DefiniteLength { match self { DefiniteLength::Absolute(length) => match length { AbsoluteLength::Pixels(pixels) => { - taffy::style::LengthPercentageAuto::Length(pixels.into()) + taffy::style::LengthPercentageAuto::length(pixels.into()) } AbsoluteLength::Rems(rems) => { - taffy::style::LengthPercentageAuto::Length((*rems * rem_size).into()) + taffy::style::LengthPercentageAuto::length((*rems * rem_size).into()) } }, DefiniteLength::Fraction(fraction) => { - taffy::style::LengthPercentageAuto::Percent(*fraction) + taffy::style::LengthPercentageAuto::percent(*fraction) } } } @@ -337,12 +337,12 @@ impl ToTaffy for DefiniteLength { fn to_taffy(&self, rem_size: Pixels) -> taffy::style::Dimension { match self { DefiniteLength::Absolute(length) => match length { - AbsoluteLength::Pixels(pixels) => taffy::style::Dimension::Length(pixels.into()), + AbsoluteLength::Pixels(pixels) => taffy::style::Dimension::length(pixels.into()), AbsoluteLength::Rems(rems) => { - taffy::style::Dimension::Length((*rems * rem_size).into()) + taffy::style::Dimension::length((*rems * rem_size).into()) } }, - DefiniteLength::Fraction(fraction) => taffy::style::Dimension::Percent(*fraction), + DefiniteLength::Fraction(fraction) => taffy::style::Dimension::percent(*fraction), } } } @@ -350,9 +350,9 @@ impl ToTaffy for DefiniteLength { impl ToTaffy for AbsoluteLength { fn to_taffy(&self, rem_size: Pixels) -> taffy::style::LengthPercentage { match self { - AbsoluteLength::Pixels(pixels) => taffy::style::LengthPercentage::Length(pixels.into()), + AbsoluteLength::Pixels(pixels) => taffy::style::LengthPercentage::length(pixels.into()), AbsoluteLength::Rems(rems) => { - taffy::style::LengthPercentage::Length((*rems * rem_size).into()) + taffy::style::LengthPercentage::length((*rems * rem_size).into()) } } } From 4a87397d376331a4fd5189678a62269a427d714d Mon Sep 17 00:00:00 2001 From: Maksim Bondarenkov <119937608+ognevny@users.noreply.github.com> Date: Wed, 23 Jul 2025 22:53:13 +0000 Subject: [PATCH 301/658] livekit_client: Revert a change that broke MinGW builds (#34977) the change was made in https://github.com/zed-industries/zed/pull/34223 for unknown reason. it wasn't required actually, and the code can be safely left as before update: after this revert Zed compiles with MinGW as before Release Notes: - N/A --- crates/livekit_client/src/lib.rs | 35 +++++++++++++++++++++++++++----- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/crates/livekit_client/src/lib.rs b/crates/livekit_client/src/lib.rs index f94181b8f8b143c68d2269260d8e01dfbaaaf946..149859fdc8ecd8533332c9462a090adb5496f100 100644 --- a/crates/livekit_client/src/lib.rs +++ b/crates/livekit_client/src/lib.rs @@ -3,16 +3,41 @@ use collections::HashMap; mod remote_video_track_view; pub use remote_video_track_view::{RemoteVideoTrackView, RemoteVideoTrackViewEvent}; -#[cfg(not(any(test, feature = "test-support", target_os = "freebsd")))] +#[cfg(not(any( + test, + feature = "test-support", + all(target_os = "windows", target_env = "gnu"), + target_os = "freebsd" +)))] mod livekit_client; -#[cfg(not(any(test, feature = "test-support", target_os = "freebsd")))] +#[cfg(not(any( + test, + feature = "test-support", + all(target_os = "windows", target_env = "gnu"), + target_os = "freebsd" +)))] pub use livekit_client::*; -#[cfg(any(test, feature = "test-support", target_os = "freebsd"))] +#[cfg(any( + test, + feature = "test-support", + all(target_os = "windows", target_env = "gnu"), + target_os = "freebsd" +))] mod mock_client; -#[cfg(any(test, feature = "test-support", target_os = "freebsd"))] +#[cfg(any( + test, + feature = "test-support", + all(target_os = "windows", target_env = "gnu"), + target_os = "freebsd" +))] pub mod test; -#[cfg(any(test, feature = "test-support", target_os = "freebsd"))] +#[cfg(any( + test, + feature = "test-support", + all(target_os = "windows", target_env = "gnu"), + target_os = "freebsd" +))] pub use mock_client::*; #[derive(Debug, Clone)] From 3d4266bb8f23d6982eaa49e47c296c717287a662 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 23 Jul 2025 19:30:00 -0400 Subject: [PATCH 302/658] collab: Remove `POST /billing/subscriptions/manage` endpoint (#34986) This PR removes the `POST /billing/subscriptions/manage` endpoint, as it has been moved to `cloud.zed.dev`. Release Notes: - N/A --- crates/collab/src/api/billing.rs | 272 +------------------------------ 1 file changed, 7 insertions(+), 265 deletions(-) diff --git a/crates/collab/src/api/billing.rs b/crates/collab/src/api/billing.rs index bd7b99b3eb4584e36ae4d78f04e8976b7c09424a..9a27e22f87f5fd954f545c78f7c105aad6f61bf2 100644 --- a/crates/collab/src/api/billing.rs +++ b/crates/collab/src/api/billing.rs @@ -5,16 +5,8 @@ use collections::{HashMap, HashSet}; use reqwest::StatusCode; use sea_orm::ActiveValue; use serde::{Deserialize, Serialize}; -use std::{str::FromStr, sync::Arc, time::Duration}; -use stripe::{ - BillingPortalSession, CancellationDetailsReason, CreateBillingPortalSession, - CreateBillingPortalSessionFlowData, CreateBillingPortalSessionFlowDataAfterCompletion, - CreateBillingPortalSessionFlowDataAfterCompletionRedirect, - CreateBillingPortalSessionFlowDataSubscriptionUpdateConfirm, - CreateBillingPortalSessionFlowDataSubscriptionUpdateConfirmItems, - CreateBillingPortalSessionFlowDataType, CustomerId, EventObject, EventType, ListEvents, - PaymentMethod, Subscription, SubscriptionId, SubscriptionStatus, -}; +use std::{sync::Arc, time::Duration}; +use stripe::{CancellationDetailsReason, EventObject, EventType, ListEvents, SubscriptionStatus}; use util::{ResultExt, maybe}; use zed_llm_client::LanguageModelProvider; @@ -31,7 +23,7 @@ use crate::{AppState, Error, Result}; use crate::{db::UserId, llm::db::LlmDatabase}; use crate::{ db::{ - BillingSubscriptionId, CreateBillingCustomerParams, CreateBillingSubscriptionParams, + CreateBillingCustomerParams, CreateBillingSubscriptionParams, CreateProcessedStripeEventParams, UpdateBillingCustomerParams, UpdateBillingSubscriptionParams, billing_customer, }, @@ -39,260 +31,10 @@ use crate::{ }; pub fn router() -> Router { - Router::new() - .route( - "/billing/subscriptions/manage", - post(manage_billing_subscription), - ) - .route( - "/billing/subscriptions/sync", - post(sync_billing_subscription), - ) -} - -#[derive(Debug, PartialEq, Deserialize)] -#[serde(rename_all = "snake_case")] -enum ManageSubscriptionIntent { - /// The user intends to manage their subscription. - /// - /// This will open the Stripe billing portal without putting the user in a specific flow. - ManageSubscription, - /// The user intends to update their payment method. - UpdatePaymentMethod, - /// The user intends to upgrade to Zed Pro. - UpgradeToPro, - /// The user intends to cancel their subscription. - Cancel, - /// The user intends to stop the cancellation of their subscription. - StopCancellation, -} - -#[derive(Debug, Deserialize)] -struct ManageBillingSubscriptionBody { - github_user_id: i32, - intent: ManageSubscriptionIntent, - /// The ID of the subscription to manage. - subscription_id: BillingSubscriptionId, - redirect_to: Option, -} - -#[derive(Debug, Serialize)] -struct ManageBillingSubscriptionResponse { - billing_portal_session_url: Option, -} - -/// Initiates a Stripe customer portal session for managing a billing subscription. -async fn manage_billing_subscription( - Extension(app): Extension>, - extract::Json(body): extract::Json, -) -> Result> { - let user = app - .db - .get_user_by_github_user_id(body.github_user_id) - .await? - .context("user not found")?; - - let Some(stripe_client) = app.real_stripe_client.clone() else { - log::error!("failed to retrieve Stripe client"); - Err(Error::http( - StatusCode::NOT_IMPLEMENTED, - "not supported".into(), - ))? - }; - - let Some(stripe_billing) = app.stripe_billing.clone() else { - log::error!("failed to retrieve Stripe billing object"); - Err(Error::http( - StatusCode::NOT_IMPLEMENTED, - "not supported".into(), - ))? - }; - - let customer = app - .db - .get_billing_customer_by_user_id(user.id) - .await? - .context("billing customer not found")?; - let customer_id = CustomerId::from_str(&customer.stripe_customer_id) - .context("failed to parse customer ID")?; - - let subscription = app - .db - .get_billing_subscription_by_id(body.subscription_id) - .await? - .context("subscription not found")?; - let subscription_id = SubscriptionId::from_str(&subscription.stripe_subscription_id) - .context("failed to parse subscription ID")?; - - if body.intent == ManageSubscriptionIntent::StopCancellation { - let updated_stripe_subscription = Subscription::update( - &stripe_client, - &subscription_id, - stripe::UpdateSubscription { - cancel_at_period_end: Some(false), - ..Default::default() - }, - ) - .await?; - - app.db - .update_billing_subscription( - subscription.id, - &UpdateBillingSubscriptionParams { - stripe_cancel_at: ActiveValue::set( - updated_stripe_subscription - .cancel_at - .and_then(|cancel_at| DateTime::from_timestamp(cancel_at, 0)) - .map(|time| time.naive_utc()), - ), - ..Default::default() - }, - ) - .await?; - - return Ok(Json(ManageBillingSubscriptionResponse { - billing_portal_session_url: None, - })); - } - - let flow = match body.intent { - ManageSubscriptionIntent::ManageSubscription => None, - ManageSubscriptionIntent::UpgradeToPro => { - let zed_pro_price_id: stripe::PriceId = - stripe_billing.zed_pro_price_id().await?.try_into()?; - let zed_free_price_id: stripe::PriceId = - stripe_billing.zed_free_price_id().await?.try_into()?; - - let stripe_subscription = - Subscription::retrieve(&stripe_client, &subscription_id, &[]).await?; - - let is_on_zed_pro_trial = stripe_subscription.status == SubscriptionStatus::Trialing - && stripe_subscription.items.data.iter().any(|item| { - item.price - .as_ref() - .map_or(false, |price| price.id == zed_pro_price_id) - }); - if is_on_zed_pro_trial { - let payment_methods = PaymentMethod::list( - &stripe_client, - &stripe::ListPaymentMethods { - customer: Some(stripe_subscription.customer.id()), - ..Default::default() - }, - ) - .await?; - - let has_payment_method = !payment_methods.data.is_empty(); - if !has_payment_method { - return Err(Error::http( - StatusCode::BAD_REQUEST, - "missing payment method".into(), - )); - } - - // If the user is already on a Zed Pro trial and wants to upgrade to Pro, we just need to end their trial early. - Subscription::update( - &stripe_client, - &stripe_subscription.id, - stripe::UpdateSubscription { - trial_end: Some(stripe::Scheduled::now()), - ..Default::default() - }, - ) - .await?; - - return Ok(Json(ManageBillingSubscriptionResponse { - billing_portal_session_url: None, - })); - } - - let subscription_item_to_update = stripe_subscription - .items - .data - .iter() - .find_map(|item| { - let price = item.price.as_ref()?; - - if price.id == zed_free_price_id { - Some(item.id.clone()) - } else { - None - } - }) - .context("No subscription item to update")?; - - Some(CreateBillingPortalSessionFlowData { - type_: CreateBillingPortalSessionFlowDataType::SubscriptionUpdateConfirm, - subscription_update_confirm: Some( - CreateBillingPortalSessionFlowDataSubscriptionUpdateConfirm { - subscription: subscription.stripe_subscription_id, - items: vec![ - CreateBillingPortalSessionFlowDataSubscriptionUpdateConfirmItems { - id: subscription_item_to_update.to_string(), - price: Some(zed_pro_price_id.to_string()), - quantity: Some(1), - }, - ], - discounts: None, - }, - ), - ..Default::default() - }) - } - ManageSubscriptionIntent::UpdatePaymentMethod => Some(CreateBillingPortalSessionFlowData { - type_: CreateBillingPortalSessionFlowDataType::PaymentMethodUpdate, - after_completion: Some(CreateBillingPortalSessionFlowDataAfterCompletion { - type_: stripe::CreateBillingPortalSessionFlowDataAfterCompletionType::Redirect, - redirect: Some(CreateBillingPortalSessionFlowDataAfterCompletionRedirect { - return_url: format!( - "{}{path}", - app.config.zed_dot_dev_url(), - path = body.redirect_to.unwrap_or_else(|| "/account".to_string()) - ), - }), - ..Default::default() - }), - ..Default::default() - }), - ManageSubscriptionIntent::Cancel => { - if subscription.kind == Some(SubscriptionKind::ZedFree) { - return Err(Error::http( - StatusCode::BAD_REQUEST, - "free subscription cannot be canceled".into(), - )); - } - - Some(CreateBillingPortalSessionFlowData { - type_: CreateBillingPortalSessionFlowDataType::SubscriptionCancel, - after_completion: Some(CreateBillingPortalSessionFlowDataAfterCompletion { - type_: stripe::CreateBillingPortalSessionFlowDataAfterCompletionType::Redirect, - redirect: Some(CreateBillingPortalSessionFlowDataAfterCompletionRedirect { - return_url: format!("{}/account", app.config.zed_dot_dev_url()), - }), - ..Default::default() - }), - subscription_cancel: Some( - stripe::CreateBillingPortalSessionFlowDataSubscriptionCancel { - subscription: subscription.stripe_subscription_id, - retention: None, - }, - ), - ..Default::default() - }) - } - ManageSubscriptionIntent::StopCancellation => unreachable!(), - }; - - let mut params = CreateBillingPortalSession::new(customer_id); - params.flow_data = flow; - let return_url = format!("{}/account", app.config.zed_dot_dev_url()); - params.return_url = Some(&return_url); - - let session = BillingPortalSession::create(&stripe_client, params).await?; - - Ok(Json(ManageBillingSubscriptionResponse { - billing_portal_session_url: Some(session.url), - })) + Router::new().route( + "/billing/subscriptions/sync", + post(sync_billing_subscription), + ) } #[derive(Debug, Deserialize)] From 31afda3c0c3ea326cbec7e3d99a0d334ad7d638b Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Thu, 24 Jul 2025 05:26:12 +0530 Subject: [PATCH 303/658] project_panel: Automatically open project panel when Rename or Duplicate is triggered from workspace (#34988) In project panel, `rename` and `duplicate` action further needs user input for editing, so if panel is closed we should open it. Release Notes: - Fixed project panel not opening when `project panel: rename` and `project panel: duplicate` actions are triggered from workspace. --- crates/project_panel/src/project_panel.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 44f4e8985ad90462f3c68b21e7f12274725e3673..b0073f294fa991e8b50fe15b4e73a88b8fb0fc61 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -322,6 +322,7 @@ pub fn init(cx: &mut App) { }); workspace.register_action(|workspace, action: &Rename, window, cx| { + workspace.open_panel::(window, cx); if let Some(panel) = workspace.panel::(cx) { panel.update(cx, |panel, cx| { if let Some(first_marked) = panel.marked_entries.first() { @@ -335,6 +336,7 @@ pub fn init(cx: &mut App) { }); workspace.register_action(|workspace, action: &Duplicate, window, cx| { + workspace.open_panel::(window, cx); if let Some(panel) = workspace.panel::(cx) { panel.update(cx, |panel, cx| { panel.duplicate(action, window, cx); From 67027bb241ef1a0f4de60aace75539a60fa867c8 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 24 Jul 2025 02:13:47 +0200 Subject: [PATCH 304/658] agent: Fix Zed header in settings view (#34993) Follow-up to taffy bump (#34939), fixes an issue reported by @MrSubidubi Release Notes: - N/A --- crates/agent_ui/src/agent_configuration.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 334c5ee6dc297d7e4bedba0c417a22a7d960c84d..7a160a5649ce784a0a60be447fa89ae2779c9301 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -186,6 +186,7 @@ impl AgentConfiguration { }; v_flex() + .w_full() .when(is_expanded, |this| this.mb_2()) .child( div() @@ -216,6 +217,7 @@ impl AgentConfiguration { .hover(|hover| hover.bg(cx.theme().colors().element_hover)) .child( h_flex() + .w_full() .gap_2() .child( Icon::new(provider.icon()) @@ -224,6 +226,7 @@ impl AgentConfiguration { ) .child( h_flex() + .w_full() .gap_1() .child( Label::new(provider_name.clone()) @@ -307,6 +310,7 @@ impl AgentConfiguration { let providers = LanguageModelRegistry::read_global(cx).providers(); v_flex() + .w_full() .child( h_flex() .p(DynamicSpacing::Base16.rems(cx)) @@ -361,6 +365,7 @@ impl AgentConfiguration { ) .child( div() + .w_full() .pl(DynamicSpacing::Base08.rems(cx)) .pr(DynamicSpacing::Base20.rems(cx)) .children( From b93e1c736b33615e0b80a8e7fb3a294f40c70862 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Wed, 23 Jul 2025 23:13:49 -0400 Subject: [PATCH 305/658] mistral: Add support for magistral-small and magistral-medium (#34983) Release Notes: - mistral: Added support for magistral-small and magistral-medium --- crates/mistral/src/mistral.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/crates/mistral/src/mistral.rs b/crates/mistral/src/mistral.rs index bf6ccf288328b49ea8d2770ab6db609f6038d299..c466a598a0ca509654ec501614169c24c2094409 100644 --- a/crates/mistral/src/mistral.rs +++ b/crates/mistral/src/mistral.rs @@ -48,20 +48,29 @@ pub enum Model { #[serde(rename = "codestral-latest", alias = "codestral-latest")] #[default] CodestralLatest, + #[serde(rename = "mistral-large-latest", alias = "mistral-large-latest")] MistralLargeLatest, #[serde(rename = "mistral-medium-latest", alias = "mistral-medium-latest")] MistralMediumLatest, #[serde(rename = "mistral-small-latest", alias = "mistral-small-latest")] MistralSmallLatest, + + #[serde(rename = "magistral-medium-latest", alias = "magistral-medium-latest")] + MagistralMediumLatest, + #[serde(rename = "magistral-small-latest", alias = "magistral-small-latest")] + MagistralSmallLatest, + #[serde(rename = "open-mistral-nemo", alias = "open-mistral-nemo")] OpenMistralNemo, #[serde(rename = "open-codestral-mamba", alias = "open-codestral-mamba")] OpenCodestralMamba, + #[serde(rename = "devstral-medium-latest", alias = "devstral-medium-latest")] DevstralMediumLatest, #[serde(rename = "devstral-small-latest", alias = "devstral-small-latest")] DevstralSmallLatest, + #[serde(rename = "pixtral-12b-latest", alias = "pixtral-12b-latest")] Pixtral12BLatest, #[serde(rename = "pixtral-large-latest", alias = "pixtral-large-latest")] @@ -91,6 +100,8 @@ impl Model { "mistral-large-latest" => Ok(Self::MistralLargeLatest), "mistral-medium-latest" => Ok(Self::MistralMediumLatest), "mistral-small-latest" => Ok(Self::MistralSmallLatest), + "magistral-medium-latest" => Ok(Self::MagistralMediumLatest), + "magistral-small-latest" => Ok(Self::MagistralSmallLatest), "open-mistral-nemo" => Ok(Self::OpenMistralNemo), "open-codestral-mamba" => Ok(Self::OpenCodestralMamba), "devstral-medium-latest" => Ok(Self::DevstralMediumLatest), @@ -107,6 +118,8 @@ impl Model { Self::MistralLargeLatest => "mistral-large-latest", Self::MistralMediumLatest => "mistral-medium-latest", Self::MistralSmallLatest => "mistral-small-latest", + Self::MagistralMediumLatest => "magistral-medium-latest", + Self::MagistralSmallLatest => "magistral-small-latest", Self::OpenMistralNemo => "open-mistral-nemo", Self::OpenCodestralMamba => "open-codestral-mamba", Self::DevstralMediumLatest => "devstral-medium-latest", @@ -123,6 +136,8 @@ impl Model { Self::MistralLargeLatest => "mistral-large-latest", Self::MistralMediumLatest => "mistral-medium-latest", Self::MistralSmallLatest => "mistral-small-latest", + Self::MagistralMediumLatest => "magistral-medium-latest", + Self::MagistralSmallLatest => "magistral-small-latest", Self::OpenMistralNemo => "open-mistral-nemo", Self::OpenCodestralMamba => "open-codestral-mamba", Self::DevstralMediumLatest => "devstral-medium-latest", @@ -141,6 +156,8 @@ impl Model { Self::MistralLargeLatest => 131000, Self::MistralMediumLatest => 128000, Self::MistralSmallLatest => 32000, + Self::MagistralMediumLatest => 40000, + Self::MagistralSmallLatest => 40000, Self::OpenMistralNemo => 131000, Self::OpenCodestralMamba => 256000, Self::DevstralMediumLatest => 128000, @@ -166,6 +183,8 @@ impl Model { | Self::MistralLargeLatest | Self::MistralMediumLatest | Self::MistralSmallLatest + | Self::MagistralMediumLatest + | Self::MagistralSmallLatest | Self::OpenMistralNemo | Self::OpenCodestralMamba | Self::DevstralMediumLatest @@ -184,6 +203,8 @@ impl Model { | Self::MistralSmallLatest => true, Self::CodestralLatest | Self::MistralLargeLatest + | Self::MagistralMediumLatest + | Self::MagistralSmallLatest | Self::OpenMistralNemo | Self::OpenCodestralMamba | Self::DevstralMediumLatest From c08851a85ea4e3a495cbc8cb951b06353ccfa015 Mon Sep 17 00:00:00 2001 From: versecafe <147033096+versecafe@users.noreply.github.com> Date: Wed, 23 Jul 2025 21:17:54 -0700 Subject: [PATCH 306/658] ollama: Add Magistral to Ollama (#35000) See also: #34983 Release Notes: - Added magistral support to ollama --- crates/ollama/src/ollama.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/ollama/src/ollama.rs b/crates/ollama/src/ollama.rs index 109fea7353d8e35ccaabb3fce4fd76e9b3529a9b..62c32b4161de9d05ee3ed86192df873530fd411f 100644 --- a/crates/ollama/src/ollama.rs +++ b/crates/ollama/src/ollama.rs @@ -55,6 +55,7 @@ fn get_max_tokens(name: &str) -> u64 { "codellama" | "starcoder2" => 16384, "mistral" | "codestral" | "mixstral" | "llava" | "qwen2" | "qwen2.5-coder" | "dolphin-mixtral" => 32768, + "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, From 8b0ec287a53aa41d56232088b280a1a25cb6e74d Mon Sep 17 00:00:00 2001 From: AidanV <84053180+AidanV@users.noreply.github.com> Date: Wed, 23 Jul 2025 22:06:05 -0700 Subject: [PATCH 307/658] vim: Add `:norm` support (#33232) Closes #21198 Release Notes: - Adds support for `:norm` - Allows for vim and zed style modified keys specified in issue - Vim style and zed style - Differs from vim in how multi-line is handled - vim is sequential - zed is combinational (with multi-cursor) --- crates/editor/src/editor.rs | 18 +- crates/vim/src/command.rs | 195 +++++++++++++++++- crates/vim/src/insert.rs | 2 +- crates/vim/test_data/test_normal_command.json | 64 ++++++ crates/workspace/src/workspace.rs | 97 +++++---- 5 files changed, 328 insertions(+), 48 deletions(-) create mode 100644 crates/vim/test_data/test_normal_command.json diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a695c8fd0c35ddcf7e44f7c47b0c8ff09f6754fb..069d8cffb3fcfbea83fa74fe9c28f11c825acc0f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -16968,7 +16968,7 @@ impl Editor { now: Instant, window: &mut Window, cx: &mut Context, - ) { + ) -> Option { self.end_selection(window, cx); if let Some(tx_id) = self .buffer @@ -16978,7 +16978,10 @@ impl Editor { .insert_transaction(tx_id, self.selections.disjoint_anchors()); cx.emit(EditorEvent::TransactionBegun { transaction_id: tx_id, - }) + }); + Some(tx_id) + } else { + None } } @@ -17006,6 +17009,17 @@ impl Editor { } } + pub fn modify_transaction_selection_history( + &mut self, + transaction_id: TransactionId, + modify: impl FnOnce(&mut (Arc<[Selection]>, Option]>>)), + ) -> bool { + self.selection_history + .transaction_mut(transaction_id) + .map(modify) + .is_some() + } + pub fn set_mark(&mut self, _: &actions::SetMark, window: &mut Window, cx: &mut Context) { if self.selection_mark_mode { self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 23e04cae2c1efda237caf93414d16256f11eff04..7963db35712a22395a49e0a2767b7d24edba7654 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -6,7 +6,7 @@ use editor::{ actions::{SortLinesCaseInsensitive, SortLinesCaseSensitive}, display_map::ToDisplayPoint, }; -use gpui::{Action, App, AppContext as _, Context, Global, Window, actions}; +use gpui::{Action, App, AppContext as _, Context, Global, Keystroke, Window, actions}; use itertools::Itertools; use language::Point; use multi_buffer::MultiBufferRow; @@ -202,6 +202,7 @@ actions!( ArgumentRequired ] ); + /// Opens the specified file for editing. #[derive(Clone, PartialEq, Action)] #[action(namespace = vim, no_json, no_register)] @@ -209,6 +210,13 @@ struct VimEdit { pub filename: String, } +#[derive(Clone, PartialEq, Action)] +#[action(namespace = vim, no_json, no_register)] +struct VimNorm { + pub range: Option, + pub command: String, +} + #[derive(Debug)] struct WrappedAction(Box); @@ -447,6 +455,81 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { }); }); + Vim::action(editor, cx, |vim, action: &VimNorm, window, cx| { + let keystrokes = action + .command + .chars() + .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() + }); + if let Some(range) = &action.range { + let result = vim.update_editor(window, cx, |vim, editor, window, cx| { + let range = range.buffer_range(vim, editor, window, cx)?; + editor.change_selections( + SelectionEffects::no_scroll().nav_history(false), + window, + cx, + |s| { + s.select_ranges( + (range.start.0..=range.end.0) + .map(|line| Point::new(line, 0)..Point::new(line, 0)), + ); + }, + ); + anyhow::Ok(()) + }); + if let Some(Err(err)) = result { + log::error!("Error selecting range: {}", err); + return; + } + }; + + let Some(workspace) = vim.workspace(window) else { + return; + }; + let task = workspace.update(cx, |workspace, cx| { + workspace.send_keystrokes_impl(keystrokes, window, cx) + }); + let had_range = action.range.is_some(); + + 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| { + if had_range { + editor.change_selections(SelectionEffects::default(), window, cx, |s| { + s.select_anchor_ranges([s.newest_anchor().range()]); + }) + } + }); + if matches!(vim.mode, Mode::Insert | Mode::Replace) { + vim.normal_before(&Default::default(), window, cx); + } else { + vim.switch_mode(Mode::Normal, true, window, cx); + } + vim.update_editor(window, cx, |_, editor, _, cx| { + if let Some(first_sel) = initial_selections { + if 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); + }); + } + } + }); + }) + .ok(); + }) + .detach(); + }); + Vim::action(editor, cx, |vim, _: &CountCommand, window, cx| { let Some(workspace) = vim.workspace(window) else { return; @@ -675,14 +758,15 @@ impl VimCommand { } else { return None; }; - if !args.is_empty() { + + let action = if args.is_empty() { + action + } else { // if command does not accept args and we have args then we should do no action - if let Some(args_fn) = &self.args { - args_fn.deref()(action, args) - } else { - None - } - } else if let Some(range) = range { + self.args.as_ref()?(action, args)? + }; + + if let Some(range) = range { self.range.as_ref().and_then(|f| f(action, range)) } else { Some(action) @@ -1061,6 +1145,27 @@ fn generate_commands(_: &App) -> Vec { save_intent: Some(SaveIntent::Skip), close_pinned: true, }), + VimCommand::new( + ("norm", "al"), + VimNorm { + command: "".into(), + range: None, + }, + ) + .args(|_, args| { + Some( + VimNorm { + command: args, + range: None, + } + .boxed_clone(), + ) + }) + .range(|action, range| { + let mut action: VimNorm = action.as_any().downcast_ref::().unwrap().clone(); + action.range.replace(range.clone()); + Some(Box::new(action)) + }), VimCommand::new(("bn", "ext"), workspace::ActivateNextItem).count(), VimCommand::new(("bN", "ext"), workspace::ActivatePreviousItem).count(), VimCommand::new(("bp", "revious"), workspace::ActivatePreviousItem).count(), @@ -2298,4 +2403,78 @@ mod test { }); assert!(mark.is_none()) } + + #[gpui::test] + async fn test_normal_command(cx: &mut TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! {" + The quick + brown« fox + jumpsˇ» over + the lazy dog + "}) + .await; + + cx.simulate_shared_keystrokes(": n o r m space w C w o r d") + .await; + cx.simulate_shared_keystrokes("enter").await; + + cx.shared_state().await.assert_eq(indoc! {" + The quick + brown word + jumps worˇd + the lazy dog + "}); + + cx.simulate_shared_keystrokes(": n o r m space _ w c i w t e s t") + .await; + cx.simulate_shared_keystrokes("enter").await; + + cx.shared_state().await.assert_eq(indoc! {" + The quick + brown word + jumps tesˇt + the lazy dog + "}); + + cx.simulate_shared_keystrokes("_ l v l : n o r m space s l a") + .await; + cx.simulate_shared_keystrokes("enter").await; + + cx.shared_state().await.assert_eq(indoc! {" + The quick + brown word + lˇaumps test + the lazy dog + "}); + + cx.set_shared_state(indoc! {" + ˇThe quick + brown fox + jumps over + the lazy dog + "}) + .await; + + cx.simulate_shared_keystrokes("c i w M y escape").await; + + cx.shared_state().await.assert_eq(indoc! {" + Mˇy quick + brown fox + jumps over + the lazy dog + "}); + + cx.simulate_shared_keystrokes(": n o r m space u").await; + cx.simulate_shared_keystrokes("enter").await; + + cx.shared_state().await.assert_eq(indoc! {" + ˇThe quick + brown fox + jumps over + the lazy dog + "}); + // Once ctrl-v to input character literals is added there should be a test for redo + } } diff --git a/crates/vim/src/insert.rs b/crates/vim/src/insert.rs index 89c60adee7f7c2a92b9f5c7d671cbcfac7045843..0a370e16ba418ae04cdfe47e1ccbdb3904b6af45 100644 --- a/crates/vim/src/insert.rs +++ b/crates/vim/src/insert.rs @@ -21,7 +21,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { } impl Vim { - fn normal_before( + pub(crate) fn normal_before( &mut self, action: &NormalBefore, window: &mut Window, diff --git a/crates/vim/test_data/test_normal_command.json b/crates/vim/test_data/test_normal_command.json new file mode 100644 index 0000000000000000000000000000000000000000..efd1d532c4261976a5e1ef00e85fdac9b2b90fab --- /dev/null +++ b/crates/vim/test_data/test_normal_command.json @@ -0,0 +1,64 @@ +{"Put":{"state":"The quick\nbrown« fox\njumpsˇ» over\nthe lazy dog\n"}} +{"Key":":"} +{"Key":"n"} +{"Key":"o"} +{"Key":"r"} +{"Key":"m"} +{"Key":"space"} +{"Key":"w"} +{"Key":"C"} +{"Key":"w"} +{"Key":"o"} +{"Key":"r"} +{"Key":"d"} +{"Key":"enter"} +{"Get":{"state":"The quick\nbrown word\njumps worˇd\nthe lazy dog\n","mode":"Normal"}} +{"Key":":"} +{"Key":"n"} +{"Key":"o"} +{"Key":"r"} +{"Key":"m"} +{"Key":"space"} +{"Key":"_"} +{"Key":"w"} +{"Key":"c"} +{"Key":"i"} +{"Key":"w"} +{"Key":"t"} +{"Key":"e"} +{"Key":"s"} +{"Key":"t"} +{"Key":"enter"} +{"Get":{"state":"The quick\nbrown word\njumps tesˇt\nthe lazy dog\n","mode":"Normal"}} +{"Key":"_"} +{"Key":"l"} +{"Key":"v"} +{"Key":"l"} +{"Key":":"} +{"Key":"n"} +{"Key":"o"} +{"Key":"r"} +{"Key":"m"} +{"Key":"space"} +{"Key":"s"} +{"Key":"l"} +{"Key":"a"} +{"Key":"enter"} +{"Get":{"state":"The quick\nbrown word\nlˇaumps test\nthe lazy dog\n","mode":"Normal"}} +{"Put":{"state":"ˇThe quick\nbrown fox\njumps over\nthe lazy dog\n"}} +{"Key":"c"} +{"Key":"i"} +{"Key":"w"} +{"Key":"M"} +{"Key":"y"} +{"Key":"escape"} +{"Get":{"state":"Mˇy quick\nbrown fox\njumps over\nthe lazy dog\n","mode":"Normal"}} +{"Key":":"} +{"Key":"n"} +{"Key":"o"} +{"Key":"r"} +{"Key":"m"} +{"Key":"space"} +{"Key":"u"} +{"Key":"enter"} +{"Get":{"state":"ˇThe quick\nbrown fox\njumps over\nthe lazy dog\n","mode":"Normal"}} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 4c70c52d5a18bc529498d1b3ac09a3e328208575..0ee8177dd87c396e4b09a1b11d119034a6ef548d 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -32,7 +32,7 @@ use futures::{ mpsc::{self, UnboundedReceiver, UnboundedSender}, oneshot, }, - future::try_join_all, + future::{Shared, try_join_all}, }; use gpui::{ Action, AnyEntity, AnyView, AnyWeakView, App, AsyncApp, AsyncWindowContext, Bounds, Context, @@ -87,7 +87,7 @@ use std::{ borrow::Cow, cell::RefCell, cmp, - collections::hash_map::DefaultHasher, + collections::{VecDeque, hash_map::DefaultHasher}, env, hash::{Hash, Hasher}, path::{Path, PathBuf}, @@ -1043,6 +1043,13 @@ type PromptForOpenPath = Box< ) -> oneshot::Receiver>>, >; +#[derive(Default)] +struct DispatchingKeystrokes { + dispatched: HashSet>, + queue: VecDeque, + task: Option>>, +} + /// Collects everything project-related for a certain window opened. /// In some way, is a counterpart of a window, as the [`WindowHandle`] could be downcast into `Workspace`. /// @@ -1080,7 +1087,7 @@ pub struct Workspace { leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>, database_id: Option, app_state: Arc, - dispatching_keystrokes: Rc, Vec)>>, + dispatching_keystrokes: Rc>, _subscriptions: Vec, _apply_leader_updates: Task>, _observe_current_user: Task>, @@ -2311,49 +2318,65 @@ impl Workspace { window: &mut Window, cx: &mut Context, ) { - let mut state = self.dispatching_keystrokes.borrow_mut(); - if !state.0.insert(action.0.clone()) { - cx.propagate(); - return; - } - let mut keystrokes: Vec = action + let keystrokes: Vec = action .0 .split(' ') .flat_map(|k| Keystroke::parse(k).log_err()) .collect(); - keystrokes.reverse(); + let _ = self.send_keystrokes_impl(keystrokes, window, cx); + } + + pub fn send_keystrokes_impl( + &mut self, + keystrokes: Vec, + window: &mut Window, + cx: &mut Context, + ) -> Shared> { + let mut state = self.dispatching_keystrokes.borrow_mut(); + if !state.dispatched.insert(keystrokes.clone()) { + cx.propagate(); + return state.task.clone().unwrap(); + } - state.1.append(&mut keystrokes); - drop(state); + state.queue.extend(keystrokes); let keystrokes = self.dispatching_keystrokes.clone(); - window - .spawn(cx, async move |cx| { - // limit to 100 keystrokes to avoid infinite recursion. - for _ in 0..100 { - let Some(keystroke) = keystrokes.borrow_mut().1.pop() else { - keystrokes.borrow_mut().0.clear(); - return Ok(()); - }; - cx.update(|window, cx| { - let focused = window.focused(cx); - window.dispatch_keystroke(keystroke.clone(), cx); - if window.focused(cx) != focused { - // dispatch_keystroke may cause the focus to change. - // draw's side effect is to schedule the FocusChanged events in the current flush effect cycle - // And we need that to happen before the next keystroke to keep vim mode happy... - // (Note that the tests always do this implicitly, so you must manually test with something like: - // "bindings": { "g z": ["workspace::SendKeystrokes", ": j u"]} - // ) - window.draw(cx).clear(); + if state.task.is_none() { + state.task = Some( + window + .spawn(cx, async move |cx| { + // limit to 100 keystrokes to avoid infinite recursion. + for _ in 0..100 { + let mut state = keystrokes.borrow_mut(); + let Some(keystroke) = state.queue.pop_front() else { + state.dispatched.clear(); + state.task.take(); + return; + }; + drop(state); + cx.update(|window, cx| { + let focused = window.focused(cx); + window.dispatch_keystroke(keystroke.clone(), cx); + if window.focused(cx) != focused { + // dispatch_keystroke may cause the focus to change. + // draw's side effect is to schedule the FocusChanged events in the current flush effect cycle + // And we need that to happen before the next keystroke to keep vim mode happy... + // (Note that the tests always do this implicitly, so you must manually test with something like: + // "bindings": { "g z": ["workspace::SendKeystrokes", ": j u"]} + // ) + window.draw(cx).clear(); + } + }) + .ok(); } - })?; - } - *keystrokes.borrow_mut() = Default::default(); - anyhow::bail!("over 100 keystrokes passed to send_keystrokes"); - }) - .detach_and_log_err(cx); + *keystrokes.borrow_mut() = Default::default(); + log::error!("over 100 keystrokes passed to send_keystrokes"); + }) + .shared(), + ); + } + state.task.clone().unwrap() } fn save_all_internal( From a6956eebcbf87ed36fe3cc82bd204b8ae6b997f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Ram=C3=B3n=20Guevara?= <50140021+praguevara@users.noreply.github.com> Date: Thu, 24 Jul 2025 07:27:07 +0200 Subject: [PATCH 308/658] Improve Helix insert (#34765) Closes #34763 Release Notes: - Improved insert in `helix_mode` when a selection exists to better match helix's behavior: collapse selection to avoid replacing it - Improved append (`insert_after`) to better match helix's behavior: move cursor to end of selection if it exists --- assets/keymaps/vim.json | 6 ++- crates/vim/src/helix.rs | 110 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 112 insertions(+), 4 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index d0cf4621a59d8614022f2a4ba176a79b40f8d331..6458ac1510f7d6e801f5bc9dc68610364842a378 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -220,6 +220,8 @@ { "context": "vim_mode == normal", "bindings": { + "i": "vim::InsertBefore", + "a": "vim::InsertAfter", "ctrl-[": "editor::Cancel", ":": "command_palette::Toggle", "c": "vim::PushChange", @@ -353,9 +355,7 @@ "shift-d": "vim::DeleteToEndOfLine", "shift-j": "vim::JoinLines", "shift-y": "vim::YankLine", - "i": "vim::InsertBefore", "shift-i": "vim::InsertFirstNonWhitespace", - "a": "vim::InsertAfter", "shift-a": "vim::InsertEndOfLine", "o": "vim::InsertLineBelow", "shift-o": "vim::InsertLineAbove", @@ -377,6 +377,8 @@ { "context": "vim_mode == helix_normal && !menu", "bindings": { + "i": "vim::HelixInsert", + "a": "vim::HelixAppend", "ctrl-[": "editor::Cancel", ";": "vim::HelixCollapseSelection", ":": "command_palette::Toggle", diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index ec9b959b1220939394956e22e8936141c74fae1b..798af3bff35b2e476ad4b1c090b4d48d9facab74 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -4,18 +4,28 @@ use gpui::{Context, Window}; use language::{CharClassifier, CharKind}; use text::SelectionGoal; -use crate::{Vim, motion::Motion, state::Mode}; +use crate::{ + Vim, + motion::{Motion, right}, + state::Mode, +}; actions!( vim, [ /// Switches to normal mode after the cursor (Helix-style). - HelixNormalAfter + HelixNormalAfter, + /// Inserts at the beginning of the selection. + HelixInsert, + /// Appends at the end of the selection. + HelixAppend, ] ); 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); } impl Vim { @@ -299,6 +309,38 @@ impl Vim { _ => self.helix_move_and_collapse(motion, times, window, cx), } } + + fn helix_insert(&mut self, _: &HelixInsert, window: &mut Window, cx: &mut Context) { + self.start_recording(cx); + self.update_editor(window, cx, |_, editor, window, 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 + if !selection.is_empty() { + selection.collapse_to(selection.start, SelectionGoal::None); + } + }); + }); + }); + self.switch_mode(Mode::Insert, false, window, cx); + } + + 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| { + editor.change_selections(Default::default(), window, cx, |s| { + s.move_with(|map, selection| { + let point = if selection.is_empty() { + right(map, selection.head(), 1) + } else { + selection.end + }; + selection.collapse_to(point, SelectionGoal::None); + }); + }); + }); + } } #[cfg(test)] @@ -497,4 +539,68 @@ mod test { cx.assert_state("«ˇaa»\n", Mode::HelixNormal); } + + #[gpui::test] + async fn test_insert_selected(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.simulate_keystrokes("i"); + + cx.assert_state( + indoc! {" + ˇThe quick brown + fox jumps over + the lazy dog."}, + Mode::Insert, + ); + } + + #[gpui::test] + async fn test_append(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + // test from the end of the selection + cx.set_state( + indoc! {" + «Theˇ» quick brown + fox jumps over + the lazy dog."}, + Mode::HelixNormal, + ); + + cx.simulate_keystrokes("a"); + + cx.assert_state( + indoc! {" + Theˇ quick brown + fox jumps over + the lazy dog."}, + Mode::Insert, + ); + + // test from the beginning of the selection + cx.set_state( + indoc! {" + «ˇThe» quick brown + fox jumps over + the lazy dog."}, + Mode::HelixNormal, + ); + + cx.simulate_keystrokes("a"); + + cx.assert_state( + indoc! {" + Theˇ quick brown + fox jumps over + the lazy dog."}, + Mode::Insert, + ); + } } From 34bf6ebba68cdbff01dfb4b3ff3c3a9c63240c08 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 23 Jul 2025 23:45:01 -0600 Subject: [PATCH 309/658] Disable auto-close in search (#35005) Currently if you type `\(`, it auto-closes to `\()` which is broken. It's arguably nice that if you type `(` it auto-closes to `()`, but I am much more likely to be looking for a function call `name\(` than to be starting a group in search. Release Notes: - search: Regex search will no longer try to close parenthesis automatically. --- crates/search/src/buffer_search.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index c2590ec9b04df03434a9434ebbd44af9c6ebb698..91b7fe488ef41739e666bf45419d84b3914ebacd 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -700,7 +700,11 @@ impl BufferSearchBar { window: &mut Window, cx: &mut Context, ) -> Self { - let query_editor = cx.new(|cx| Editor::single_line(window, cx)); + let query_editor = cx.new(|cx| { + let mut editor = Editor::single_line(window, cx); + editor.set_use_autoclose(false); + editor + }); cx.subscribe_in(&query_editor, window, Self::on_query_editor_event) .detach(); let replacement_editor = cx.new(|cx| Editor::single_line(window, cx)); From ddd50aabbad67db1174255b2ce4743794b9618d9 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Thu, 24 Jul 2025 02:52:02 -0400 Subject: [PATCH 310/658] Fix some bugs with `editor: diff clipboard with selection` (#34999) Improves testing around `editor: diff clipboard with selection` as well. Release Notes: - Fixed some bugs with `editor: diff clipboard with selection` --- crates/editor/src/editor_tests.rs | 2 +- crates/git_ui/src/text_diff_view.rs | 338 +++++++++++++++++++++------- 2 files changed, 263 insertions(+), 77 deletions(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 0d69c067ee7cc572e3b246a8ba91d55ff4ac306c..8d121972d0631459f61ec911bd3edf511c3f7fb0 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -16864,7 +16864,7 @@ async fn test_multibuffer_reverts(cx: &mut TestAppContext) { } #[gpui::test] -async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) { +async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) { init_test(cx, |_| {}); let cols = 4; diff --git a/crates/git_ui/src/text_diff_view.rs b/crates/git_ui/src/text_diff_view.rs index e7386cf7bdaaab1542f351fa348819bd756389ab..be1866a3544579b1686c2c2abb2639ca33580914 100644 --- a/crates/git_ui/src/text_diff_view.rs +++ b/crates/git_ui/src/text_diff_view.rs @@ -12,6 +12,7 @@ use language::{self, Buffer, Point}; use project::Project; use std::{ any::{Any, TypeId}, + cmp, ops::Range, pin::pin, sync::Arc, @@ -45,38 +46,60 @@ impl TextDiffView { ) -> Option>>> { let source_editor = diff_data.editor.clone(); - let source_editor_buffer_and_range = source_editor.update(cx, |editor, cx| { + let selection_data = source_editor.update(cx, |editor, cx| { let multibuffer = editor.buffer().read(cx); let source_buffer = multibuffer.as_singleton()?.clone(); let selections = editor.selections.all::(cx); let buffer_snapshot = source_buffer.read(cx); let first_selection = selections.first()?; - let selection_range = if first_selection.is_empty() { - Point::new(0, 0)..buffer_snapshot.max_point() + let max_point = buffer_snapshot.max_point(); + + if first_selection.is_empty() { + let full_range = Point::new(0, 0)..max_point; + return Some((source_buffer, full_range)); + } + + let start = first_selection.start; + let end = first_selection.end; + let expanded_start = Point::new(start.row, 0); + + let expanded_end = if end.column > 0 { + let next_row = end.row + 1; + cmp::min(max_point, Point::new(next_row, 0)) } else { - first_selection.start..first_selection.end + end }; - - Some((source_buffer, selection_range)) + Some((source_buffer, expanded_start..expanded_end)) }); - let Some((source_buffer, selected_range)) = source_editor_buffer_and_range else { + let Some((source_buffer, expanded_selection_range)) = selection_data else { log::warn!("There should always be at least one selection in Zed. This is a bug."); return None; }; - let clipboard_text = diff_data.clipboard_text.clone(); + source_editor.update(cx, |source_editor, cx| { + source_editor.change_selections(Default::default(), window, cx, |s| { + s.select_ranges(vec![ + expanded_selection_range.start..expanded_selection_range.end, + ]); + }) + }); - let workspace = workspace.weak_handle(); + let source_buffer_snapshot = source_buffer.read(cx).snapshot(); + let mut clipboard_text = diff_data.clipboard_text.clone(); - let diff_buffer = cx.new(|cx| { - let source_buffer_snapshot = source_buffer.read(cx).snapshot(); - let diff = BufferDiff::new(&source_buffer_snapshot.text, cx); - diff - }); + if !clipboard_text.ends_with("\n") { + clipboard_text.push_str("\n"); + } - let clipboard_buffer = - build_clipboard_buffer(clipboard_text, &source_buffer, selected_range.clone(), cx); + let workspace = workspace.weak_handle(); + let diff_buffer = cx.new(|cx| BufferDiff::new(&source_buffer_snapshot.text, cx)); + let clipboard_buffer = build_clipboard_buffer( + clipboard_text, + &source_buffer, + expanded_selection_range.clone(), + cx, + ); let task = window.spawn(cx, async move |cx| { let project = workspace.update(cx, |workspace, _| workspace.project().clone())?; @@ -89,7 +112,7 @@ impl TextDiffView { clipboard_buffer, source_editor, source_buffer, - selected_range, + expanded_selection_range, diff_buffer, project, window, @@ -208,9 +231,9 @@ impl TextDiffView { } fn build_clipboard_buffer( - clipboard_text: String, + text: String, source_buffer: &Entity, - selected_range: Range, + replacement_range: Range, cx: &mut App, ) -> Entity { let source_buffer_snapshot = source_buffer.read(cx).snapshot(); @@ -219,9 +242,9 @@ fn build_clipboard_buffer( let language = source_buffer.read(cx).language().cloned(); buffer.set_language(language, cx); - let range_start = source_buffer_snapshot.point_to_offset(selected_range.start); - let range_end = source_buffer_snapshot.point_to_offset(selected_range.end); - buffer.edit([(range_start..range_end, clipboard_text)], None, cx); + let range_start = source_buffer_snapshot.point_to_offset(replacement_range.start); + let range_end = source_buffer_snapshot.point_to_offset(replacement_range.end); + buffer.edit([(range_start..range_end, text)], None, cx); buffer }) @@ -395,21 +418,13 @@ pub fn selection_location_text(editor: &Editor, cx: &App) -> Option { let buffer_snapshot = buffer.snapshot(cx); let first_selection = editor.selections.disjoint.first()?; - let (start_row, start_column, end_row, end_column) = - if first_selection.start == first_selection.end { - let max_point = buffer_snapshot.max_point(); - (0, 0, max_point.row, max_point.column) - } else { - let selection_start = first_selection.start.to_point(&buffer_snapshot); - let selection_end = first_selection.end.to_point(&buffer_snapshot); - - ( - selection_start.row, - selection_start.column, - selection_end.row, - selection_end.column, - ) - }; + let selection_start = first_selection.start.to_point(&buffer_snapshot); + let selection_end = first_selection.end.to_point(&buffer_snapshot); + + let start_row = selection_start.row; + let start_column = selection_start.column; + let end_row = selection_end.row; + let end_column = selection_end.column; let range_text = if start_row == end_row { format!("L{}:{}-{}", start_row + 1, start_column + 1, end_column + 1) @@ -435,14 +450,13 @@ impl Render for TextDiffView { #[cfg(test)] mod tests { use super::*; - - use editor::{actions, test::editor_test_context::assert_state_with_diff}; + use editor::test::editor_test_context::assert_state_with_diff; use gpui::{TestAppContext, VisualContext}; use project::{FakeFs, Project}; use serde_json::json; use settings::{Settings, SettingsStore}; use unindent::unindent; - use util::path; + use util::{path, test::marked_text_ranges}; fn init_test(cx: &mut TestAppContext) { cx.update(|cx| { @@ -457,52 +471,236 @@ mod tests { } #[gpui::test] - async fn test_diffing_clipboard_against_specific_selection(cx: &mut TestAppContext) { - base_test(true, cx).await; + async fn test_diffing_clipboard_against_empty_selection_uses_full_buffer_selection( + cx: &mut TestAppContext, + ) { + base_test( + path!("/test"), + path!("/test/text.txt"), + "def process_incoming_inventory(items, warehouse_id):\n pass\n", + "def process_outgoing_inventory(items, warehouse_id):\n passˇ\n", + &unindent( + " + - def process_incoming_inventory(items, warehouse_id): + + ˇdef process_outgoing_inventory(items, warehouse_id): + pass + ", + ), + "Clipboard ↔ text.txt @ L1:1-L3:1", + &format!("Clipboard ↔ {} @ L1:1-L3:1", path!("test/text.txt")), + cx, + ) + .await; } #[gpui::test] - async fn test_diffing_clipboard_against_empty_selection_uses_full_buffer( + async fn test_diffing_clipboard_against_multiline_selection_expands_to_full_lines( cx: &mut TestAppContext, ) { - base_test(false, cx).await; + base_test( + path!("/test"), + path!("/test/text.txt"), + "def process_incoming_inventory(items, warehouse_id):\n pass\n", + "«def process_outgoing_inventory(items, warehouse_id):\n passˇ»\n", + &unindent( + " + - def process_incoming_inventory(items, warehouse_id): + + ˇdef process_outgoing_inventory(items, warehouse_id): + pass + ", + ), + "Clipboard ↔ text.txt @ L1:1-L3:1", + &format!("Clipboard ↔ {} @ L1:1-L3:1", path!("test/text.txt")), + cx, + ) + .await; + } + + #[gpui::test] + async fn test_diffing_clipboard_against_single_line_selection(cx: &mut TestAppContext) { + base_test( + path!("/test"), + path!("/test/text.txt"), + "a", + "«bbˇ»", + &unindent( + " + - a + + ˇbb", + ), + "Clipboard ↔ text.txt @ L1:1-3", + &format!("Clipboard ↔ {} @ L1:1-3", path!("test/text.txt")), + cx, + ) + .await; + } + + #[gpui::test] + async fn test_diffing_clipboard_with_leading_whitespace_against_line(cx: &mut TestAppContext) { + base_test( + path!("/test"), + path!("/test/text.txt"), + " a", + "«bbˇ»", + &unindent( + " + - a + + ˇbb", + ), + "Clipboard ↔ text.txt @ L1:1-3", + &format!("Clipboard ↔ {} @ L1:1-3", path!("test/text.txt")), + cx, + ) + .await; + } + + #[gpui::test] + async fn test_diffing_clipboard_against_line_with_leading_whitespace(cx: &mut TestAppContext) { + base_test( + path!("/test"), + path!("/test/text.txt"), + "a", + " «bbˇ»", + &unindent( + " + - a + + ˇ bb", + ), + "Clipboard ↔ text.txt @ L1:1-7", + &format!("Clipboard ↔ {} @ L1:1-7", path!("test/text.txt")), + cx, + ) + .await; } - async fn base_test(select_all_text: bool, cx: &mut TestAppContext) { + #[gpui::test] + async fn test_diffing_clipboard_against_line_with_leading_whitespace_included_in_selection( + cx: &mut TestAppContext, + ) { + base_test( + path!("/test"), + path!("/test/text.txt"), + "a", + "« bbˇ»", + &unindent( + " + - a + + ˇ bb", + ), + "Clipboard ↔ text.txt @ L1:1-7", + &format!("Clipboard ↔ {} @ L1:1-7", path!("test/text.txt")), + cx, + ) + .await; + } + + #[gpui::test] + async fn test_diffing_clipboard_with_leading_whitespace_against_line_with_leading_whitespace( + cx: &mut TestAppContext, + ) { + base_test( + path!("/test"), + path!("/test/text.txt"), + " a", + " «bbˇ»", + &unindent( + " + - a + + ˇ bb", + ), + "Clipboard ↔ text.txt @ L1:1-7", + &format!("Clipboard ↔ {} @ L1:1-7", path!("test/text.txt")), + cx, + ) + .await; + } + + #[gpui::test] + async fn test_diffing_clipboard_with_leading_whitespace_against_line_with_leading_whitespace_included_in_selection( + cx: &mut TestAppContext, + ) { + base_test( + path!("/test"), + path!("/test/text.txt"), + " a", + "« bbˇ»", + &unindent( + " + - a + + ˇ bb", + ), + "Clipboard ↔ text.txt @ L1:1-7", + &format!("Clipboard ↔ {} @ L1:1-7", path!("test/text.txt")), + cx, + ) + .await; + } + + #[gpui::test] + async fn test_diffing_clipboard_against_partial_selection_expands_to_include_trailing_characters( + cx: &mut TestAppContext, + ) { + base_test( + path!("/test"), + path!("/test/text.txt"), + "a", + "«bˇ»b", + &unindent( + " + - a + + ˇbb", + ), + "Clipboard ↔ text.txt @ L1:1-3", + &format!("Clipboard ↔ {} @ L1:1-3", path!("test/text.txt")), + cx, + ) + .await; + } + + async fn base_test( + project_root: &str, + file_path: &str, + clipboard_text: &str, + editor_text: &str, + expected_diff: &str, + expected_tab_title: &str, + expected_tab_tooltip: &str, + cx: &mut TestAppContext, + ) { init_test(cx); + let file_name = std::path::Path::new(file_path) + .file_name() + .unwrap() + .to_str() + .unwrap(); + let fs = FakeFs::new(cx.executor()); fs.insert_tree( - path!("/test"), + project_root, json!({ - "a": { - "b": { - "text.txt": "new line 1\nline 2\nnew line 3\nline 4" - } - } + file_name: editor_text }), ) .await; - let project = Project::test(fs, [path!("/test").as_ref()], cx).await; + let project = Project::test(fs, [project_root.as_ref()], cx).await; let (workspace, mut cx) = cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); let buffer = project - .update(cx, |project, cx| { - project.open_local_buffer(path!("/test/a/b/text.txt"), cx) - }) + .update(cx, |project, cx| project.open_local_buffer(file_path, cx)) .await .unwrap(); let editor = cx.new_window_entity(|window, cx| { let mut editor = Editor::for_buffer(buffer, None, window, cx); - editor.set_text("new line 1\nline 2\nnew line 3\nline 4\n", window, cx); - - if select_all_text { - editor.select_all(&actions::SelectAll, window, cx); - } + let (unmarked_text, selection_ranges) = marked_text_ranges(editor_text, false); + editor.set_text(unmarked_text, window, cx); + editor.change_selections(Default::default(), window, cx, |s| { + s.select_ranges(selection_ranges) + }); editor }); @@ -511,7 +709,7 @@ mod tests { .update_in(cx, |workspace, window, cx| { TextDiffView::open( &DiffClipboardWithSelectionData { - clipboard_text: "old line 1\nline 2\nold line 3\nline 4\n".to_string(), + clipboard_text: clipboard_text.to_string(), editor, }, workspace, @@ -528,26 +726,14 @@ mod tests { assert_state_with_diff( &diff_view.read_with(cx, |diff_view, _| diff_view.diff_editor.clone()), &mut cx, - &unindent( - " - - old line 1 - + ˇnew line 1 - line 2 - - old line 3 - + new line 3 - line 4 - ", - ), + expected_diff, ); diff_view.read_with(cx, |diff_view, cx| { - assert_eq!( - diff_view.tab_content_text(0, cx), - "Clipboard ↔ text.txt @ L1:1-L5:1" - ); + assert_eq!(diff_view.tab_content_text(0, cx), expected_tab_title); assert_eq!( diff_view.tab_tooltip_text(cx).unwrap(), - format!("Clipboard ↔ {}", path!("test/a/b/text.txt @ L1:1-L5:1")) + expected_tab_tooltip ); }); } From 65759d43163bd36cce8f8aa492ba2628ca005a7a Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 24 Jul 2025 16:27:29 +0800 Subject: [PATCH 311/658] gpui: Fix Interactivity prepaint to update scroll_handle bounds (#35013) It took a long time to check this problem. Finally, I found that due to a detail missing when changing #34832, the bounds of `ScrollHandle` was not updated in the Interactivity `prepaint` phase. ```diff - scroll_handle_state.padded_content_size = padded_content_size; + scroll_handle_state.max_offset = scroll_max; ``` It was correct before the change, because the `padded_content_size` (including `bounds.size`) was saved before, and the bounds was missing after changing to `max_offset`, but the bounds were not updated anywhere. So when `scroll_handle.bounds()` is obtained outside, it is always 0px here. @MrSubidubi Release Notes: - N/A --- crates/gpui/src/elements/div.rs | 7 +------ crates/gpui/src/elements/uniform_list.rs | 5 ++--- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index 4655c92409d3f21fd8a2a919154368a56da9567e..fa47758581d79399fad4530e00e62bc311bab515 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -1334,7 +1334,6 @@ impl Element for Div { } else if let Some(scroll_handle) = self.interactivity.tracked_scroll_handle.as_ref() { let mut state = scroll_handle.0.borrow_mut(); state.child_bounds = Vec::with_capacity(request_layout.child_layout_ids.len()); - state.bounds = bounds; for child_layout_id in &request_layout.child_layout_ids { let child_bounds = window.layout_bounds(*child_layout_id); child_min = child_min.min(&child_bounds.origin); @@ -1706,6 +1705,7 @@ impl Interactivity { if let Some(mut scroll_handle_state) = tracked_scroll_handle { scroll_handle_state.max_offset = scroll_max; + scroll_handle_state.bounds = bounds; } *scroll_offset @@ -3007,11 +3007,6 @@ impl ScrollHandle { self.0.borrow().bounds } - /// Set the bounds into which this child is painted - pub(super) fn set_bounds(&self, bounds: Bounds) { - self.0.borrow_mut().bounds = bounds; - } - /// Get the bounds for a specific child. pub fn bounds_for_item(&self, ix: usize) -> Option> { self.0.borrow().child_bounds.get(ix).cloned() diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index 52e2015c20f9983e78c126cc920ed115eef0fd7a..e80656a07878f640843afa747d2d48e4448acdc5 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -295,9 +295,8 @@ impl Element for UniformList { bounds.bottom_right() - point(border.right + padding.right, border.bottom), ); - let y_flipped = if let Some(scroll_handle) = self.scroll_handle.as_mut() { - let mut scroll_state = scroll_handle.0.borrow_mut(); - scroll_state.base_handle.set_bounds(bounds); + let y_flipped = if let Some(scroll_handle) = &self.scroll_handle { + let scroll_state = scroll_handle.0.borrow(); scroll_state.y_flipped } else { false From cd9bcc7f0963c7dd06c6623fce039e18b42f682e Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Thu, 24 Jul 2025 10:40:36 +0200 Subject: [PATCH 312/658] agent_ui: Improve wrapping behavior in provider configuration header (#34989) This ensures that the "Add provider" button does not move offscreen too fast and ensures the text wraps for smaller panel sizes. | Before | After | | --- | --- | | image | image | Release Notes: - N/A --- crates/agent_ui/src/agent_configuration.rs | 86 +++++++++++++--------- 1 file changed, 51 insertions(+), 35 deletions(-) diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 7a160a5649ce784a0a60be447fa89ae2779c9301..1870c3e3d243491ff7c178fefbabdf4acdd6931d 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -321,46 +321,62 @@ impl AgentConfiguration { .justify_between() .child( v_flex() + .w_full() .gap_0p5() - .child(Headline::new("LLM Providers")) + .child( + h_flex() + .w_full() + .gap_2() + .justify_between() + .child(Headline::new("LLM Providers")) + .child( + PopoverMenu::new("add-provider-popover") + .trigger( + Button::new("add-provider", "Add Provider") + .icon_position(IconPosition::Start) + .icon(IconName::Plus) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .label_size(LabelSize::Small), + ) + .anchor(gpui::Corner::TopRight) + .menu({ + let workspace = self.workspace.clone(); + move |window, cx| { + Some(ContextMenu::build( + window, + cx, + |menu, _window, _cx| { + menu.header("Compatible APIs").entry( + "OpenAI", + None, + { + let workspace = + workspace.clone(); + move |window, cx| { + workspace + .update(cx, |workspace, cx| { + AddLlmProviderModal::toggle( + LlmCompatibleProvider::OpenAi, + workspace, + window, + cx, + ); + }) + .log_err(); + } + }, + ) + }, + )) + } + }), + ), + ) .child( Label::new("Add at least one provider to use AI-powered features.") .color(Color::Muted), ), - ) - .child( - PopoverMenu::new("add-provider-popover") - .trigger( - Button::new("add-provider", "Add Provider") - .icon_position(IconPosition::Start) - .icon(IconName::Plus) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .label_size(LabelSize::Small), - ) - .anchor(gpui::Corner::TopRight) - .menu({ - let workspace = self.workspace.clone(); - move |window, cx| { - Some(ContextMenu::build(window, cx, |menu, _window, _cx| { - menu.header("Compatible APIs").entry("OpenAI", None, { - let workspace = workspace.clone(); - move |window, cx| { - workspace - .update(cx, |workspace, cx| { - AddLlmProviderModal::toggle( - LlmCompatibleProvider::OpenAi, - workspace, - window, - cx, - ); - }) - .log_err(); - } - }) - })) - } - }), ), ) .child( From 5c9363b1c47426e9f65858f7dbc2e6af42bbe403 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Thu, 24 Jul 2025 04:43:28 -0400 Subject: [PATCH 313/658] Differentiate between file and selection diff events (#35014) Release Notes: - N/A --- crates/git_ui/src/text_diff_view.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/git_ui/src/text_diff_view.rs b/crates/git_ui/src/text_diff_view.rs index be1866a3544579b1686c2c2abb2639ca33580914..005c1e18b40727f42df81437c7038f4e5a7ef905 100644 --- a/crates/git_ui/src/text_diff_view.rs +++ b/crates/git_ui/src/text_diff_view.rs @@ -316,7 +316,7 @@ impl Item for TextDiffView { } fn telemetry_event_text(&self) -> Option<&'static str> { - Some("Diff View Opened") + Some("Selection Diff View Opened") } fn deactivated(&mut self, window: &mut Window, cx: &mut Context) { From 913b9296d755d88a85ac6fad2d57f6c1e5d7cef6 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Thu, 24 Jul 2025 04:49:04 -0400 Subject: [PATCH 314/658] Add `editor: convert to sentence case` (#35015) This PR adds an `editor: convert to sentence case` action. I frequently find myself copying branch names and then removing the hyphens and ensuring the first letter is capitalized, and then using the result text for the commit message. For example: image You can achieve this with a combination of other text manipulation commands, but this action makes it even easier. Also, moved `toggle_case` down into the area where all other commands internally using `manipulate_text` are located. Release Notes: - Added `editor: convert to sentence case` --- crates/editor/src/actions.rs | 2 ++ crates/editor/src/editor.rs | 31 ++++++++++++++++++++----------- crates/editor/src/editor_tests.rs | 17 +++++++++++++++++ crates/editor/src/element.rs | 3 ++- 4 files changed, 41 insertions(+), 12 deletions(-) diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index f80a6afbbb00e6b7bf8d3a58ab9dbfd91d090e86..1212651cb3f4683bf802f173c78849585cc18262 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -365,6 +365,8 @@ actions!( ConvertToLowerCase, /// Toggles the case of selected text. ConvertToOppositeCase, + /// Converts selected text to sentence case. + ConvertToSentenceCase, /// Converts selected text to snake_case. ConvertToSnakeCase, /// Converts selected text to Title Case. diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 069d8cffb3fcfbea83fa74fe9c28f11c825acc0f..8f57fb1a2063f51caf415d9d3d1e6c0b7f80ae1d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -10878,17 +10878,6 @@ impl Editor { }); } - pub fn toggle_case(&mut self, _: &ToggleCase, window: &mut Window, cx: &mut Context) { - self.manipulate_text(window, cx, |text| { - let has_upper_case_characters = text.chars().any(|c| c.is_uppercase()); - if has_upper_case_characters { - text.to_lowercase() - } else { - text.to_uppercase() - } - }) - } - fn manipulate_immutable_lines( &mut self, window: &mut Window, @@ -11144,6 +11133,26 @@ impl Editor { }) } + pub fn convert_to_sentence_case( + &mut self, + _: &ConvertToSentenceCase, + window: &mut Window, + cx: &mut Context, + ) { + self.manipulate_text(window, cx, |text| text.to_case(Case::Sentence)) + } + + pub fn toggle_case(&mut self, _: &ToggleCase, window: &mut Window, cx: &mut Context) { + self.manipulate_text(window, cx, |text| { + let has_upper_case_characters = text.chars().any(|c| c.is_uppercase()); + if has_upper_case_characters { + text.to_lowercase() + } else { + text.to_uppercase() + } + }) + } + pub fn convert_to_rot13( &mut self, _: &ConvertToRot13, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 8d121972d0631459f61ec911bd3edf511c3f7fb0..03b047e92e48c5397e60528dae930822bdd626cd 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -4724,6 +4724,23 @@ async fn test_toggle_case(cx: &mut TestAppContext) { "}); } +#[gpui::test] +async fn test_convert_to_sentence_case(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + cx.set_state(indoc! {" + «implement-windows-supportˇ» + "}); + cx.update_editor(|e, window, cx| { + e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx) + }); + cx.assert_editor_state(indoc! {" + «Implement windows supportˇ» + "}); +} + #[gpui::test] async fn test_manipulate_text(cx: &mut TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index d2ee9d6b0a8411f862f395b19b0016bdf79ca765..5fd6b028f4ef972021e7e7dedb08e9b6bc7ece60 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -230,7 +230,6 @@ impl EditorElement { register_action(editor, window, Editor::sort_lines_case_insensitive); register_action(editor, window, Editor::reverse_lines); register_action(editor, window, Editor::shuffle_lines); - register_action(editor, window, Editor::toggle_case); register_action(editor, window, Editor::convert_indentation_to_spaces); register_action(editor, window, Editor::convert_indentation_to_tabs); register_action(editor, window, Editor::convert_to_upper_case); @@ -241,6 +240,8 @@ impl EditorElement { register_action(editor, window, Editor::convert_to_upper_camel_case); register_action(editor, window, Editor::convert_to_lower_camel_case); register_action(editor, window, Editor::convert_to_opposite_case); + register_action(editor, window, Editor::convert_to_sentence_case); + register_action(editor, window, Editor::toggle_case); register_action(editor, window, Editor::convert_to_rot13); register_action(editor, window, Editor::convert_to_rot47); register_action(editor, window, Editor::delete_to_previous_word_start); From dd52fb58feddd1d6200bc40c641e9189836ad753 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Thu, 24 Jul 2025 10:51:40 +0200 Subject: [PATCH 315/658] terminal_view: Ensure breadcrumbs are updated on settings change (#35016) Currently, terminal breadcrumbs are only updated after a settings change once the terminal view is focused again. This change ensures that the breadcrumbs are updated instantaneously once the breadcrumb settings changes. Release Notes: - Fixed an issue where terminal breadcrumbs would not react instantly to settings changes. --- crates/terminal_view/src/terminal_view.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 1cc1fbcf6f671c8968975b807f080bfdce04317f..bf65a736e832833da250937dc2d0855f92075391 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -430,6 +430,7 @@ impl TerminalView { fn settings_changed(&mut self, cx: &mut Context) { let settings = TerminalSettings::get_global(cx); + let breadcrumb_visibility_changed = self.show_breadcrumbs != settings.toolbar.breadcrumbs; self.show_breadcrumbs = settings.toolbar.breadcrumbs; let new_cursor_shape = settings.cursor_shape.unwrap_or_default(); @@ -441,6 +442,9 @@ impl TerminalView { }); } + if breadcrumb_visibility_changed { + cx.emit(ItemEvent::UpdateBreadcrumbs); + } cx.notify(); } From 0af690080bc4353d71d1039374e5cbbfce656718 Mon Sep 17 00:00:00 2001 From: Oleksiy Syvokon Date: Thu, 24 Jul 2025 15:22:57 +0300 Subject: [PATCH 316/658] linux: Fix `ctrl-0..9`, `ctrl-[`, `ctrl-^` (#35028) There were two different underlying reasons for the issues with ctrl-number and ctrl-punctuation: 1. Some keys in the ctrl-0..9 range send codes in the `\1b`..`\1f` range. For example, `ctrl-2` sends keycode for `ctrl-[` (0x1b), but we want to map it to `2`, not to `[`. 2. `ctrl-[` and four other ctrl-punctuation were incorrectly mapped, since the expected conversion is by adding 0x40 Closes #35012 Release Notes: - N/A --- crates/gpui/src/platform/linux/platform.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index d65118e994e4cd488c23436bea8d3666495881ec..fe6a36baa854856eb961a020ab35a7bd0195d465 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -845,9 +845,15 @@ impl crate::Keystroke { { if key.is_ascii_graphic() { key_utf8.to_lowercase() - // map ctrl-a to a - } else if key_utf32 <= 0x1f { - ((key_utf32 as u8 + 0x60) as char).to_string() + // map ctrl-a to `a` + // ctrl-0..9 may emit control codes like ctrl-[, but + // we don't want to map them to `[` + } else if key_utf32 <= 0x1f + && !name.chars().next().is_some_and(|c| c.is_ascii_digit()) + { + ((key_utf32 as u8 + 0x40) as char) + .to_ascii_lowercase() + .to_string() } else { name } From 1e2b0fcab6535f53857f401ba7ef2435c40b7e1e Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 24 Jul 2025 14:30:49 +0200 Subject: [PATCH 317/658] sum_tree: Remove Unit type (#35027) This solves one ~TODO, as Unit type was a workaround for a lack of ability to implement Summary for (). Release Notes: - N/A --- crates/sum_tree/src/sum_tree.rs | 12 +++++------- crates/worktree/src/worktree.rs | 14 ++++---------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/crates/sum_tree/src/sum_tree.rs b/crates/sum_tree/src/sum_tree.rs index 4f9e01ce201f5d9db9f8fd8be8e76bbb43c6f2bb..4c5ce3959092751d8624f6cc91f5b8708ede69e6 100644 --- a/crates/sum_tree/src/sum_tree.rs +++ b/crates/sum_tree/src/sum_tree.rs @@ -41,16 +41,14 @@ pub trait Summary: Clone { fn add_summary(&mut self, summary: &Self, cx: &Self::Context); } -/// This type exists because we can't implement Summary for () without causing -/// type resolution errors -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub struct Unit; - -impl Summary for Unit { +/// Catch-all implementation for when you need something that implements [`Summary`] without a specific type. +/// We implement it on a &'static, as that avoids blanket impl collisions with `impl Dimension for T` +/// (as we also need unit type to be a fill-in dimension) +impl Summary for &'static () { type Context = (); fn zero(_: &()) -> Self { - Unit + &() } fn add_summary(&mut self, _: &Self, _: &()) {} diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 4fc6b91abbf770ab2fc15c0e69da55b84eac4961..e6949f62df2eb6e2459f4c077f03ef4dfe87b53c 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, Unit}; +use sum_tree::{Bias, Edit, KeyedItem, SeekTarget, SumTree, Summary, TreeMap, TreeSet}; use text::{LineEnding, Rope}; use util::{ ResultExt, @@ -407,12 +407,12 @@ struct LocalRepositoryEntry { } impl sum_tree::Item for LocalRepositoryEntry { - type Summary = PathSummary; + type Summary = PathSummary<&'static ()>; fn summary(&self, _: &::Context) -> Self::Summary { PathSummary { max_path: self.work_directory.path_key().0, - item_summary: Unit, + item_summary: &(), } } } @@ -425,12 +425,6 @@ impl KeyedItem for LocalRepositoryEntry { } } -//impl LocalRepositoryEntry { -// pub fn repo(&self) -> &Arc { -// &self.repo_ptr -// } -//} - impl Deref for LocalRepositoryEntry { type Target = WorkDirectory; @@ -5417,7 +5411,7 @@ impl<'a> SeekTarget<'a, EntrySummary, TraversalProgress<'a>> for TraversalTarget } } -impl<'a> SeekTarget<'a, PathSummary, TraversalProgress<'a>> for TraversalTarget<'_> { +impl<'a> SeekTarget<'a, PathSummary<&'static ()>, TraversalProgress<'a>> for TraversalTarget<'_> { fn cmp(&self, cursor_location: &TraversalProgress<'a>, _: &()) -> Ordering { self.cmp_progress(cursor_location) } From 4fb540d6d2adb58b2e61ab1bdfa7c820eb9798d8 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 24 Jul 2025 09:39:10 -0300 Subject: [PATCH 318/658] dock: Add divider between panels on the right side, too (#35003) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A while ago, we added a divider in the left side of the status bar between the icon buttons that open panels vs. those that open something else (tabs, popover menus, etc.). There isn't a super good reason why we wouldn't do the same in the right side, even more so given that (at least by default) we usually have more buttons on that side; buttons that _don't_ open panels (regardless of being docked to the bottom or right). So, this PR does this! CleanShot 2025-07-24 at 1  59 10@2x Release Notes: - N/A --- crates/workspace/src/dock.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 7165de23ecf275249c894a12e6aaa9261a4faeee..ca63d3e5532a393436046e04a3b50a448a0e94f0 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -934,6 +934,10 @@ impl Render for PanelButtons { h_flex() .gap_1() + .when( + has_buttons && dock.position == DockPosition::Bottom, + |this| this.child(Divider::vertical().color(DividerColor::Border)), + ) .children(buttons) .when(has_buttons && dock.position == DockPosition::Left, |this| { this.child(Divider::vertical().color(DividerColor::Border)) From fab450e39d39364ff6e2f08121c6d31e301a47ca Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Thu, 24 Jul 2025 09:20:25 -0400 Subject: [PATCH 319/658] Fix invalid regular expressions highlighting all search fields (#35001) Closes https://github.com/zed-industries/zed/issues/34969 Closes https://github.com/zed-industries/zed/issues/34970 Only highlight the search field on regex error (buffer search and project search). Clear errors when the buffer search hidden so stale errors aren't shown on next search. Before (all fields highlighted red): Screenshot 2025-07-23 at 22 59 45 After (only query field highlighted red): Screenshot 2025-07-23 at 23 10 49 Release Notes: - Improved highlighting of regex errors in search dialogs --- crates/search/src/buffer_search.rs | 22 +++++++++++++--------- crates/search/src/project_search.rs | 13 +++++++------ 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 91b7fe488ef41739e666bf45419d84b3914ebacd..5d77a95027a6cae193ec293ce7e9254a0fa0363c 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -228,16 +228,17 @@ impl Render for BufferSearchBar { if in_replace { key_context.add("in_replace"); } - let editor_border = if self.query_error.is_some() { + 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 = || { + let input_base_styles = |border_color| { h_flex() .min_w_32() .w(input_width) @@ -246,7 +247,7 @@ impl Render for BufferSearchBar { .pr_1() .py_1() .border_1() - .border_color(editor_border) + .border_color(border_color) .rounded_lg() }; @@ -256,7 +257,7 @@ impl Render for BufferSearchBar { el.child(Label::new("Find in results").color(Color::Hint)) }) .child( - input_base_styles() + 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)) @@ -430,11 +431,13 @@ impl Render for BufferSearchBar { let replace_line = should_show_replace_input.then(|| { h_flex() .gap_2() - .child(input_base_styles().child(self.render_text_input( - &self.replacement_editor, - None, - cx, - ))) + .child( + input_base_styles(replacement_border).child(self.render_text_input( + &self.replacement_editor, + None, + cx, + )), + ) .child( h_flex() .min_w_64() @@ -775,6 +778,7 @@ impl BufferSearchBar { pub fn dismiss(&mut self, _: &Dismiss, window: &mut Window, cx: &mut Context) { self.dismissed = true; + self.query_error = None; for searchable_item in self.searchable_items_with_matches.keys() { if let Some(searchable_item) = WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 57ca5e56b9447f8552abac55c6d79a5f6e8326a1..3b9700c5f1615f98cc5f123f00de6cc35a67a40f 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -195,6 +195,7 @@ pub struct ProjectSearch { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] enum InputPanel { Query, + Replacement, Exclude, Include, } @@ -1962,7 +1963,7 @@ impl Render for ProjectSearchBar { MultipleInputs, } - let input_base_styles = |base_style: BaseStyle| { + let input_base_styles = |base_style: BaseStyle, panel: InputPanel| { h_flex() .min_w_32() .map(|div| match base_style { @@ -1974,11 +1975,11 @@ impl Render for ProjectSearchBar { .pr_1() .py_1() .border_1() - .border_color(search.border_color_for(InputPanel::Query, cx)) + .border_color(search.border_color_for(panel, cx)) .rounded_lg() }; - let query_column = input_base_styles(BaseStyle::SingleInput) + let query_column = input_base_styles(BaseStyle::SingleInput, 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) @@ -2167,7 +2168,7 @@ 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) + let replace_column = input_base_styles(BaseStyle::SingleInput, InputPanel::Replacement) .child(self.render_text_input(&search.replacement_editor, cx)); let focus_handle = search.replacement_editor.read(cx).focus_handle(cx); @@ -2241,7 +2242,7 @@ impl Render for ProjectSearchBar { .gap_2() .w(input_width) .child( - input_base_styles(BaseStyle::MultipleInputs) + input_base_styles(BaseStyle::MultipleInputs, InputPanel::Include) .on_action(cx.listener(|this, action, window, cx| { this.previous_history_query(action, window, cx) })) @@ -2251,7 +2252,7 @@ impl Render for ProjectSearchBar { .child(self.render_text_input(&search.included_files_editor, cx)), ) .child( - input_base_styles(BaseStyle::MultipleInputs) + input_base_styles(BaseStyle::MultipleInputs, InputPanel::Exclude) .on_action(cx.listener(|this, action, window, cx| { this.previous_history_query(action, window, cx) })) From 29332c1962809b0cf812df5c444324e854c62011 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Thu, 24 Jul 2025 11:26:15 -0300 Subject: [PATCH 320/658] ai onboarding: Add overall fixes to the whole flow (#34996) Closes https://github.com/zed-industries/zed/issues/34979 Release Notes: - N/A --------- Co-authored-by: Agus Zubiaga Co-authored-by: Ben Kunkle --- crates/agent/src/thread_store.rs | 9 +- crates/agent_ui/src/agent_configuration.rs | 11 +- crates/agent_ui/src/agent_panel.rs | 56 +++--- crates/agent_ui/src/message_editor.rs | 52 +++--- crates/agent_ui/src/ui.rs | 1 - crates/agent_ui/src/ui/end_trial_upsell.rs | 55 +++--- crates/agent_ui/src/ui/upsell.rs | 163 ------------------ .../src/agent_panel_onboarding_content.rs | 7 +- crates/ai_onboarding/src/ai_onboarding.rs | 30 +++- crates/assistant_context/src/context_store.rs | 5 + crates/client/src/user.rs | 6 +- crates/language_models/src/provider/cloud.rs | 5 +- 12 files changed, 150 insertions(+), 250 deletions(-) delete mode 100644 crates/agent_ui/src/ui/upsell.rs diff --git a/crates/agent/src/thread_store.rs b/crates/agent/src/thread_store.rs index 0347156cd4df0d8b5d953def949739cab1135025..cc7cb50c9195a6a9a5bd0e0e1a17bd76caf153ee 100644 --- a/crates/agent/src/thread_store.rs +++ b/crates/agent/src/thread_store.rs @@ -41,6 +41,9 @@ use std::{ }; use util::ResultExt as _; +pub static ZED_STATELESS: std::sync::LazyLock = + 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")] @@ -874,7 +877,11 @@ impl ThreadsDatabase { let needs_migration_from_heed = mdb_path.exists(); - let connection = Connection::open_file(&sqlite_path.to_string_lossy()); + let connection = if *ZED_STATELESS { + Connection::open_memory(Some("THREAD_FALLBACK_DB")) + } else { + Connection::open_file(&sqlite_path.to_string_lossy()) + }; connection.exec(indoc! {" CREATE TABLE IF NOT EXISTS threads ( diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 1870c3e3d243491ff7c178fefbabdf4acdd6931d..cacd409ac627d8534a9fb0b5c9ac1a060851ad81 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -185,6 +185,13 @@ impl AgentConfiguration { None }; + let is_signed_in = self + .workspace + .read_with(cx, |workspace, _| { + workspace.client().status().borrow().is_connected() + }) + .unwrap_or(false); + v_flex() .w_full() .when(is_expanded, |this| this.mb_2()) @@ -233,8 +240,8 @@ impl AgentConfiguration { .size(LabelSize::Large), ) .map(|this| { - if is_zed_provider { - this.gap_2().child( + if is_zed_provider && is_signed_in { + this.child( self.render_zed_plan_info(current_plan, cx), ) } else { diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 6ae2f12b5ebadb730656d2fdffaa9f9aaef990f1..a0250816a03a2ec46c606e0c7bc404371c524af8 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -564,6 +564,17 @@ impl AgentPanel { let inline_assist_context_store = cx.new(|_cx| ContextStore::new(project.downgrade(), Some(thread_store.downgrade()))); + let thread_id = thread.read(cx).id().clone(); + + let history_store = cx.new(|cx| { + HistoryStore::new( + thread_store.clone(), + context_store.clone(), + [HistoryEntryId::Thread(thread_id)], + cx, + ) + }); + let message_editor = cx.new(|cx| { MessageEditor::new( fs.clone(), @@ -573,22 +584,13 @@ impl AgentPanel { prompt_store.clone(), thread_store.downgrade(), context_store.downgrade(), + Some(history_store.downgrade()), thread.clone(), window, cx, ) }); - let thread_id = thread.read(cx).id().clone(); - let history_store = cx.new(|cx| { - HistoryStore::new( - thread_store.clone(), - context_store.clone(), - [HistoryEntryId::Thread(thread_id)], - cx, - ) - }); - cx.observe(&history_store, |_, _, cx| cx.notify()).detach(); let active_thread = cx.new(|cx| { @@ -851,6 +853,7 @@ impl AgentPanel { self.prompt_store.clone(), self.thread_store.downgrade(), self.context_store.downgrade(), + Some(self.history_store.downgrade()), thread.clone(), window, cx, @@ -1124,6 +1127,7 @@ impl AgentPanel { self.prompt_store.clone(), self.thread_store.downgrade(), self.context_store.downgrade(), + Some(self.history_store.downgrade()), thread.clone(), window, cx, @@ -2283,20 +2287,21 @@ impl AgentPanel { } match &self.active_view { - ActiveView::Thread { thread, .. } => thread - .read(cx) - .thread() - .read(cx) - .configured_model() - .map_or(true, |model| { - model.provider.id() == language_model::ZED_CLOUD_PROVIDER_ID - }), - ActiveView::TextThread { .. } => LanguageModelRegistry::global(cx) - .read(cx) - .default_model() - .map_or(true, |model| { - model.provider.id() == language_model::ZED_CLOUD_PROVIDER_ID - }), + ActiveView::Thread { .. } | ActiveView::TextThread { .. } => { + let history_is_empty = 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() + .iter() + .any(|provider| { + provider.is_authenticated(cx) + && provider.id() != language_model::ZED_CLOUD_PROVIDER_ID + }); + + history_is_empty || !has_configured_non_zed_providers + } ActiveView::ExternalAgentThread { .. } | ActiveView::History | ActiveView::Configuration => false, @@ -2317,9 +2322,8 @@ impl AgentPanel { Some( div() - .size_full() .when(thread_view, |this| { - this.bg(cx.theme().colors().panel_background) + this.size_full().bg(cx.theme().colors().panel_background) }) .when(text_thread_view, |this| { this.bg(cx.theme().colors().editor_background) diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index 62be5629f1e1eac1e6fa65e13d459dab3324dc48..c160f1de04488b264d9420d2ad78b2d6bbbe3c7c 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -9,6 +9,7 @@ use crate::ui::{ MaxModeTooltip, preview::{AgentPreview, UsageCallout}, }; +use agent::history_store::HistoryStore; use agent::{ context::{AgentContextKey, ContextLoadResult, load_context}, context_store::ContextStoreEvent, @@ -29,8 +30,9 @@ use fs::Fs; use futures::future::Shared; use futures::{FutureExt as _, future}; use gpui::{ - Animation, AnimationExt, App, Entity, EventEmitter, Focusable, KeyContext, Subscription, Task, - TextStyle, WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between, + Animation, AnimationExt, App, Entity, EventEmitter, Focusable, IntoElement, KeyContext, + Subscription, Task, TextStyle, WeakEntity, linear_color_stop, linear_gradient, point, + pulsating_between, }; use language::{Buffer, Language, Point}; use language_model::{ @@ -80,6 +82,7 @@ pub struct MessageEditor { user_store: Entity, context_store: Entity, prompt_store: Option>, + history_store: Option>, context_strip: Entity, context_picker_menu_handle: PopoverMenuHandle, model_selector: Entity, @@ -161,6 +164,7 @@ impl MessageEditor { prompt_store: Option>, thread_store: WeakEntity, text_thread_store: WeakEntity, + history_store: Option>, thread: Entity, window: &mut Window, cx: &mut Context, @@ -233,6 +237,7 @@ impl MessageEditor { workspace, context_store, prompt_store, + history_store, context_strip, context_picker_menu_handle, load_context_task: None, @@ -1661,32 +1666,36 @@ impl Render for MessageEditor { let line_height = TextSize::Small.rems(cx).to_pixels(window.rem_size()) * 1.5; - let in_pro_trial = matches!( - self.user_store.read(cx).current_plan(), - Some(proto::Plan::ZedProTrial) - ); + let has_configured_providers = LanguageModelRegistry::read_global(cx) + .providers() + .iter() + .filter(|provider| { + provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID + }) + .count() + > 0; - let pro_user = matches!( - self.user_store.read(cx).current_plan(), - Some(proto::Plan::ZedPro) - ); + let is_signed_out = self + .workspace + .read_with(cx, |workspace, _| { + workspace.client().status().borrow().is_signed_out() + }) + .unwrap_or(true); - let configured_providers: Vec<(IconName, SharedString)> = - LanguageModelRegistry::read_global(cx) - .providers() - .iter() - .filter(|provider| { - provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID - }) - .map(|provider| (provider.icon(), provider.name().0.clone())) - .collect(); - let has_existing_providers = configured_providers.len() > 0; + let has_history = self + .history_store + .as_ref() + .and_then(|hs| hs.update(cx, |hs, cx| hs.entries(cx).len() > 0).ok()) + .unwrap_or(false) + || self + .thread + .read_with(cx, |thread, _| thread.messages().len() > 0); v_flex() .size_full() .bg(cx.theme().colors().panel_background) .when( - has_existing_providers && !in_pro_trial && !pro_user, + !has_history && is_signed_out && has_configured_providers, |this| this.child(cx.new(ApiKeysWithProviders::new)), ) .when(changed_buffers.len() > 0, |parent| { @@ -1778,6 +1787,7 @@ impl AgentPreview for MessageEditor { None, thread_store.downgrade(), text_thread_store.downgrade(), + None, thread, window, cx, diff --git a/crates/agent_ui/src/ui.rs b/crates/agent_ui/src/ui.rs index 15f2e28e5824242c3a6da258c15810263c0d9b83..b477a8c385c5f8aee85b54cf5f82cdd49e2e2484 100644 --- a/crates/agent_ui/src/ui.rs +++ b/crates/agent_ui/src/ui.rs @@ -5,7 +5,6 @@ mod end_trial_upsell; mod new_thread_button; mod onboarding_modal; pub mod preview; -mod upsell; pub use agent_notification::*; pub use burn_mode_tooltip::*; diff --git a/crates/agent_ui/src/ui/end_trial_upsell.rs b/crates/agent_ui/src/ui/end_trial_upsell.rs index 9c2dd98d2000d83733ad41147c3fa4486240de55..36770c219755e23fd080a2a1db509af05cd8f607 100644 --- a/crates/agent_ui/src/ui/end_trial_upsell.rs +++ b/crates/agent_ui/src/ui/end_trial_upsell.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use ai_onboarding::{AgentPanelOnboardingCard, BulletItem}; use client::zed_urls; use gpui::{AnyElement, App, IntoElement, RenderOnce, Window}; -use ui::{Divider, List, prelude::*}; +use ui::{Divider, List, Tooltip, prelude::*}; #[derive(IntoElement, RegisterComponent)] pub struct EndTrialUpsell { @@ -33,14 +33,19 @@ impl RenderOnce for EndTrialUpsell { ) .child( List::new() - .child(BulletItem::new("500 prompts per month with Claude models")) - .child(BulletItem::new("Unlimited edit predictions")), + .child(BulletItem::new("500 prompts with Claude models")) + .child(BulletItem::new( + "Unlimited edit predictions with Zeta, our open-source model", + )), ) .child( Button::new("cta-button", "Upgrade to Zed Pro") .full_width() .style(ButtonStyle::Tinted(ui::TintColor::Accent)) - .on_click(|_, _, cx| cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))), + .on_click(move |_, _window, cx| { + telemetry::event!("Upgrade To Pro Clicked", state = "end-of-trial"); + cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx)) + }), ); let free_section = v_flex() @@ -55,37 +60,43 @@ impl RenderOnce for EndTrialUpsell { .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 the Claude models", - )) - .child(BulletItem::new( - "2000 accepted edit predictions using our open-source Zeta model", - )), - ) - .child( - Button::new("dismiss-button", "Stay on Free") - .full_width() - .style(ButtonStyle::Outlined) - .on_click({ - let callback = self.dismiss_upsell.clone(); - move |_, window, cx| callback(window, cx) - }), + .child(BulletItem::new("50 prompts with the Claude models")) + .child(BulletItem::new("2,000 accepted edit predictions")), ); AgentPanelOnboardingCard::new() - .child(Headline::new("Your Zed Pro trial has expired.")) + .child(Headline::new("Your Zed Pro Trial has expired")) .child( Label::new("You've been automatically reset to the Free plan.") - .size(LabelSize::Small) .color(Color::Muted) - .mb_1(), + .mb_2(), ) .child(pro_section) .child(free_section) + .child( + h_flex().absolute().top_4().right_4().child( + IconButton::new("dismiss_onboarding", IconName::Close) + .icon_size(IconSize::Small) + .tooltip(Tooltip::text("Dismiss")) + .on_click({ + let callback = self.dismiss_upsell.clone(); + move |_, window, cx| { + telemetry::event!("Banner Dismissed", source = "AI Onboarding"); + callback(window, cx) + } + }), + ), + ) } } diff --git a/crates/agent_ui/src/ui/upsell.rs b/crates/agent_ui/src/ui/upsell.rs deleted file mode 100644 index f311aade22770d534189f25494b3f06588a70d9d..0000000000000000000000000000000000000000 --- a/crates/agent_ui/src/ui/upsell.rs +++ /dev/null @@ -1,163 +0,0 @@ -use component::{Component, ComponentScope, single_example}; -use gpui::{ - AnyElement, App, ClickEvent, IntoElement, ParentElement, RenderOnce, SharedString, Styled, - Window, -}; -use theme::ActiveTheme; -use ui::{ - Button, ButtonCommon, ButtonStyle, Checkbox, Clickable, Color, Label, LabelCommon, - RegisterComponent, ToggleState, h_flex, v_flex, -}; - -/// A component that displays an upsell message with a call-to-action button -/// -/// # Example -/// ``` -/// let upsell = Upsell::new( -/// "Upgrade to Zed Pro", -/// "Get access to advanced AI features and more", -/// "Upgrade Now", -/// Box::new(|_, _window, cx| { -/// cx.open_url("https://zed.dev/pricing"); -/// }), -/// Box::new(|_, _window, cx| { -/// // Handle dismiss -/// }), -/// Box::new(|checked, window, cx| { -/// // Handle don't show again -/// }), -/// ); -/// ``` -#[derive(IntoElement, RegisterComponent)] -pub struct Upsell { - title: SharedString, - message: SharedString, - cta_text: SharedString, - on_click: Box, - on_dismiss: Box, - on_dont_show_again: Box, -} - -impl Upsell { - /// Create a new upsell component - pub fn new( - title: impl Into, - message: impl Into, - cta_text: impl Into, - on_click: Box, - on_dismiss: Box, - on_dont_show_again: Box, - ) -> Self { - Self { - title: title.into(), - message: message.into(), - cta_text: cta_text.into(), - on_click, - on_dismiss, - on_dont_show_again, - } - } -} - -impl RenderOnce for Upsell { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - v_flex() - .w_full() - .p_4() - .gap_3() - .bg(cx.theme().colors().surface_background) - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border) - .child( - v_flex() - .gap_1() - .child( - Label::new(self.title) - .size(ui::LabelSize::Large) - .weight(gpui::FontWeight::BOLD), - ) - .child(Label::new(self.message).color(Color::Muted)), - ) - .child( - h_flex() - .w_full() - .justify_between() - .items_center() - .child( - h_flex() - .items_center() - .gap_1() - .child( - Checkbox::new("dont-show-again", ToggleState::Unselected).on_click( - move |_, window, cx| { - (self.on_dont_show_again)(true, window, cx); - }, - ), - ) - .child( - Label::new("Don't show again") - .color(Color::Muted) - .size(ui::LabelSize::Small), - ), - ) - .child( - h_flex() - .gap_2() - .child( - Button::new("dismiss-button", "No Thanks") - .style(ButtonStyle::Subtle) - .on_click(self.on_dismiss), - ) - .child( - Button::new("cta-button", self.cta_text) - .style(ButtonStyle::Filled) - .on_click(self.on_click), - ), - ), - ) - } -} - -impl Component for Upsell { - fn scope() -> ComponentScope { - ComponentScope::Agent - } - - fn name() -> &'static str { - "Upsell" - } - - fn description() -> Option<&'static str> { - Some("A promotional component that displays a message with a call-to-action.") - } - - fn preview(window: &mut Window, cx: &mut App) -> Option { - let examples = vec![ - single_example( - "Default", - Upsell::new( - "Upgrade to Zed Pro", - "Get unlimited access to AI features and more with Zed Pro. Unlock advanced AI capabilities and other premium features.", - "Upgrade Now", - Box::new(|_, _, _| {}), - Box::new(|_, _, _| {}), - Box::new(|_, _, _| {}), - ).render(window, cx).into_any_element(), - ), - single_example( - "Short Message", - Upsell::new( - "Try Zed Pro for free", - "Start your 7-day trial today.", - "Start Trial", - Box::new(|_, _, _| {}), - Box::new(|_, _, _| {}), - Box::new(|_, _, _| {}), - ).render(window, cx).into_any_element(), - ), - ]; - - Some(v_flex().gap_4().children(examples).into_any_element()) - } -} diff --git a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs index 771482abf3f5ba871f2955d8579514013c6704f0..e8a62f7ff279f496b0fb114927024c6245040b59 100644 --- a/crates/ai_onboarding/src/agent_panel_onboarding_content.rs +++ b/crates/ai_onboarding/src/agent_panel_onboarding_content.rs @@ -61,6 +61,11 @@ impl Render for AgentPanelOnboarding { Some(proto::Plan::ZedProTrial) ); + let is_pro_user = matches!( + self.user_store.read(cx).current_plan(), + Some(proto::Plan::ZedPro) + ); + AgentPanelOnboardingCard::new() .child( ZedAiOnboarding::new( @@ -75,7 +80,7 @@ impl Render for AgentPanelOnboarding { }), ) .map(|this| { - if enrolled_in_trial || self.configured_providers.len() >= 1 { + if enrolled_in_trial || is_pro_user || self.configured_providers.len() >= 1 { this } else { this.child(ApiKeysWithoutProviders::new()) diff --git a/crates/ai_onboarding/src/ai_onboarding.rs b/crates/ai_onboarding/src/ai_onboarding.rs index f9a91503aee351a6c745d3f3a0e6aea2cc05a165..7fffb60ecc32f65ddd97264f511ec4f5d735cfbe 100644 --- a/crates/ai_onboarding/src/ai_onboarding.rs +++ b/crates/ai_onboarding/src/ai_onboarding.rs @@ -16,6 +16,7 @@ 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, } @@ -28,18 +29,27 @@ impl BulletItem { } } -impl IntoElement for BulletItem { - type Element = AnyElement; +impl RenderOnce for BulletItem { + fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement { + let line_height = 0.85 * window.line_height(); - fn into_element(self) -> Self::Element { ListItem::new("list-item") .selectable(false) - .start_slot( - Icon::new(IconName::Dash) - .size(IconSize::XSmall) - .color(Color::Hidden), + .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))), ) - .child(div().w_full().child(Label::new(self.label))) .into_any_element() } } @@ -373,7 +383,9 @@ impl ZedAiOnboarding { .child( List::new() .child(BulletItem::new("500 prompts with Claude models")) - .child(BulletItem::new("Unlimited edit predictions")), + .child(BulletItem::new( + "Unlimited edit predictions with Zeta, our open-source model", + )), ) .child( Button::new("pro", "Continue with Zed Pro") diff --git a/crates/assistant_context/src/context_store.rs b/crates/assistant_context/src/context_store.rs index 3400913eb86ed0717ca29681511fd0d2cb506603..3090a7b23439de9ae8fe1bd5287f439895f17a98 100644 --- a/crates/assistant_context/src/context_store.rs +++ b/crates/assistant_context/src/context_store.rs @@ -767,6 +767,11 @@ impl ContextStore { fn reload(&mut self, cx: &mut Context) -> Task> { let fs = self.fs.clone(); cx.spawn(async move |this, cx| { + pub static ZED_STATELESS: LazyLock = + LazyLock::new(|| std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty())); + if *ZED_STATELESS { + return Ok(()); + } fs.create_dir(contexts_dir()).await?; let mut paths = fs.read_dir(contexts_dir()).await?; diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index f5213fbcb6c42db9d6a63ab312d024ca0e909f3f..5ed258aa8ed55e7124ef06c5e769c3ccc45c0618 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -765,12 +765,14 @@ impl UserStore { pub fn current_plan(&self) -> Option { #[cfg(debug_assertions)] - if let Ok(plan) = std::env::var("ZED_SIMULATE_ZED_PRO_PLAN").as_ref() { + 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), - _ => None, + _ => { + panic!("ZED_SIMULATE_PLAN must be one of 'free', 'trial', or 'pro'"); + } }; } diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index fac88107143919437c1851b8417210343bb44b0c..09a2ac6e0ab17a69e8bf3c85f871dab35733d1a3 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -1159,19 +1159,20 @@ impl RenderOnce for ZedAiConfiguration { let manage_subscription_buttons = if is_pro { Button::new("manage_settings", "Manage Subscription") + .full_width() .style(ButtonStyle::Tinted(TintColor::Accent)) .on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))) .into_any_element() } else if self.plan.is_none() || self.eligible_for_trial { Button::new("start_trial", "Start 14-day Free Pro Trial") - .style(ui::ButtonStyle::Tinted(ui::TintColor::Accent)) .full_width() + .style(ui::ButtonStyle::Tinted(ui::TintColor::Accent)) .on_click(|_, _, cx| cx.open_url(&zed_urls::start_trial_url(cx))) .into_any_element() } else { Button::new("upgrade", "Upgrade to Pro") - .style(ui::ButtonStyle::Tinted(ui::TintColor::Accent)) .full_width() + .style(ui::ButtonStyle::Tinted(ui::TintColor::Accent)) .on_click(|_, _, cx| cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))) .into_any_element() }; From 7cdd808db23caa9082b542abbd13b1cace73a501 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Ram=C3=B3n=20Guevara?= <50140021+praguevara@users.noreply.github.com> Date: Thu, 24 Jul 2025 16:29:58 +0200 Subject: [PATCH 321/658] helix: Fix replace in helix mode (#34789) Closes https://github.com/zed-industries/zed/issues/33076 Release Notes: - Fixed replace command on helix mode: now it actually replaces what was selected and keeps the replaced text selected to better match helix --- crates/vim/src/helix.rs | 104 +++++++++++++++++++++++++++++++++++++++- crates/vim/src/vim.rs | 1 + 2 files changed, 103 insertions(+), 2 deletions(-) diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index 798af3bff35b2e476ad4b1c090b4d48d9facab74..ca93c9c1de0993f4627073d645b347fa9a875ca0 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -1,8 +1,8 @@ -use editor::{DisplayPoint, Editor, movement}; +use editor::{DisplayPoint, Editor, SelectionEffects, ToOffset, ToPoint, movement}; use gpui::{Action, actions}; use gpui::{Context, Window}; use language::{CharClassifier, CharKind}; -use text::SelectionGoal; +use text::{Bias, SelectionGoal}; use crate::{ Vim, @@ -341,6 +341,80 @@ impl Vim { }); }); } + + pub fn helix_replace(&mut self, text: &str, window: &mut Window, cx: &mut Context) { + self.update_editor(window, cx, |_, editor, window, cx| { + editor.transact(window, cx, |editor, window, cx| { + let (map, selections) = editor.selections.all_display(cx); + + // Store selection info for positioning after edit + let selection_info: Vec<_> = selections + .iter() + .map(|selection| { + let range = selection.range(); + let start_offset = range.start.to_offset(&map, Bias::Left); + let end_offset = range.end.to_offset(&map, Bias::Left); + let was_empty = range.is_empty(); + let was_reversed = selection.reversed; + ( + map.buffer_snapshot.anchor_at(start_offset, Bias::Left), + end_offset - start_offset, + was_empty, + was_reversed, + ) + }) + .collect(); + + let mut edits = Vec::new(); + for selection in &selections { + let mut range = selection.range(); + + // For empty selections, extend to replace one character + if range.is_empty() { + range.end = movement::saturating_right(&map, range.start); + } + + let byte_range = range.start.to_offset(&map, Bias::Left) + ..range.end.to_offset(&map, Bias::Left); + + if !byte_range.is_empty() { + let replacement_text = text.repeat(byte_range.len()); + edits.push((byte_range, replacement_text)); + } + } + + editor.edit(edits, cx); + + // Restore selections based on original info + let snapshot = editor.buffer().read(cx).snapshot(cx); + let ranges: Vec<_> = selection_info + .into_iter() + .map(|(start_anchor, original_len, was_empty, was_reversed)| { + let start_point = start_anchor.to_point(&snapshot); + if was_empty { + // For cursor-only, collapse to start + start_point..start_point + } else { + // For selections, span the replaced text + let replacement_len = text.len() * original_len; + let end_offset = start_anchor.to_offset(&snapshot) + replacement_len; + let end_point = snapshot.offset_to_point(end_offset); + if was_reversed { + end_point..start_point + } else { + start_point..end_point + } + } + }) + .collect(); + + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges(ranges); + }); + }); + }); + self.switch_mode(Mode::HelixNormal, true, window, cx); + } } #[cfg(test)] @@ -603,4 +677,30 @@ mod test { Mode::Insert, ); } + + #[gpui::test] + async fn test_replace(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + // No selection (single character) + cx.set_state("ˇaa", Mode::HelixNormal); + + cx.simulate_keystrokes("r x"); + + cx.assert_state("ˇxa", Mode::HelixNormal); + + // Cursor at the beginning + cx.set_state("«ˇaa»", Mode::HelixNormal); + + cx.simulate_keystrokes("r x"); + + cx.assert_state("«ˇxx»", Mode::HelixNormal); + + // Cursor at the end + cx.set_state("«aaˇ»", Mode::HelixNormal); + + cx.simulate_keystrokes("r x"); + + cx.assert_state("«xxˇ»", Mode::HelixNormal); + } } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 95a08d7c66a49b0ca3f0f1d40ecc378276fbf131..c747c30462bb0ab7c36b7c525adc1453d2778a4f 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -1639,6 +1639,7 @@ impl Vim { Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { self.visual_replace(text, window, cx) } + Mode::HelixNormal => self.helix_replace(&text, window, cx), _ => self.clear_operator(window, cx), }, Some(Operator::Digraph { first_char }) => { From fa788a39a4b8dded19cd27d79c9bd0c63d601039 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Thu, 24 Jul 2025 20:07:38 +0530 Subject: [PATCH 322/658] project_panel: Reuse `index_for_entry` in `index_for_selection` (#35034) Just refactor I came across while working on another issue. `index_for_entry` and `index_for_selection` have the exact same logic, here we can simply reuse `index_for_entry` for `index_for_selection`. Release Notes: - N/A --- crates/project_panel/src/project_panel.rs | 27 ++++------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index b0073f294fa991e8b50fe15b4e73a88b8fb0fc61..76be97e393695fb263a872e2d7ed8b26c7847b10 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -2723,26 +2723,7 @@ impl ProjectPanel { } fn index_for_selection(&self, selection: SelectedEntry) -> Option<(usize, usize, usize)> { - let mut entry_index = 0; - let mut visible_entries_index = 0; - for (worktree_index, (worktree_id, worktree_entries, _)) in - self.visible_entries.iter().enumerate() - { - if *worktree_id == selection.worktree_id { - for entry in worktree_entries { - if entry.id == selection.entry_id { - return Some((worktree_index, entry_index, visible_entries_index)); - } else { - visible_entries_index += 1; - entry_index += 1; - } - } - break; - } else { - visible_entries_index += worktree_entries.len(); - } - } - None + self.index_for_entry(selection.entry_id, selection.worktree_id) } fn disjoint_entries(&self, cx: &App) -> BTreeSet { @@ -3353,12 +3334,12 @@ impl ProjectPanel { entry_id: ProjectEntryId, worktree_id: WorktreeId, ) -> Option<(usize, usize, usize)> { - let mut worktree_ix = 0; let mut total_ix = 0; - for (current_worktree_id, visible_worktree_entries, _) in &self.visible_entries { + 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(); - worktree_ix += 1; continue; } From 2a9355a3d25ddd887556741a30bbafd5d6ff58f0 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Thu, 24 Jul 2025 11:11:26 -0400 Subject: [PATCH 323/658] Don't auto-retry in certain circumstances (#35037) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Someone encountered this in production, which should not happen: Screenshot 2025-07-24 at 10 38
40 AM This moves certain errors into the category of "never retry" and reduces the number of retries for some others. Also it adds some diagnostic logging for retry policy. It's not a complete fix for the above, because the underlying issues is that the server is sending a HTTP 403 response and although we were already treating 403s as "do not retry" it was deciding to retry with 2 attempts anyway. So further debugging is needed to figure out why it wasn't going down the 403 branch by the time the request got here. Release Notes: - N/A --- crates/agent/src/thread.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 1b3b022ab23cf026c8298555dd53075f74d4f23c..1af27ca8a7a7e98f9ba06c7ed5d5549a66e071a4 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -2037,6 +2037,12 @@ impl Thread { if let Some(retry_strategy) = Thread::get_retry_strategy(completion_error) { + log::info!( + "Retrying with {:?} for language model completion error {:?}", + retry_strategy, + completion_error + ); + retry_scheduled = thread .handle_retryable_error_with_delay( &completion_error, @@ -2246,15 +2252,14 @@ impl Thread { .. } | AuthenticationError { .. } - | PermissionError { .. } => None, - // These errors might be transient, so retry them - SerializeRequest { .. } - | BuildRequestBody { .. } - | PromptTooLarge { .. } + | PermissionError { .. } + | NoApiKey { .. } | ApiEndpointNotFound { .. } - | NoApiKey { .. } => Some(RetryStrategy::Fixed { + | PromptTooLarge { .. } => None, + // These errors might be transient, so retry them + SerializeRequest { .. } | BuildRequestBody { .. } => Some(RetryStrategy::Fixed { delay: BASE_RETRY_DELAY, - max_attempts: 2, + max_attempts: 1, }), // Retry all other 4xx and 5xx errors once. HttpResponseError { status_code, .. } From 2658b2801e382ed01261b4c9566759d735c8ce17 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 24 Jul 2025 18:24:53 +0300 Subject: [PATCH 324/658] Shutdown language servers better (#35038) Follow-up of https://github.com/zed-industries/zed/pull/33417 * adjust prettier mock LSP to handle `shutdown` and `exit` messages * removed another `?.log_err()` backtrace from logs and improved the logging info * always handle the last parts of the shutdown logic even if the shutdown response had failed Release Notes: - N/A --- crates/lsp/src/lsp.rs | 58 +++++++++++++------------- crates/prettier/src/prettier_server.js | 4 ++ 2 files changed, 34 insertions(+), 28 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index a820aaf748f9f6749d31bc690b9bba8181545170..9978d7ebb1eaa271564c9f33d5b154f986a6d665 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -877,39 +877,41 @@ impl LanguageServer { let server = self.server.clone(); let name = self.name.clone(); + let server_id = self.server_id; let mut timer = self.executor.timer(SERVER_SHUTDOWN_TIMEOUT).fuse(); - Some( - async move { - log::debug!("language server shutdown started"); - - select! { - request_result = shutdown_request.fuse() => { - match request_result { - ConnectionResult::Timeout => { - log::warn!("timeout waiting for language server {name} to shutdown"); - }, - ConnectionResult::ConnectionReset => {}, - ConnectionResult::Result(r) => r?, - } + Some(async move { + log::debug!("language server shutdown started"); + + select! { + request_result = shutdown_request.fuse() => { + match request_result { + ConnectionResult::Timeout => { + log::warn!("timeout waiting for language server {name} (id {server_id}) to shutdown"); + }, + ConnectionResult::ConnectionReset => { + log::warn!("language server {name} (id {server_id}) closed the shutdown request connection"); + }, + ConnectionResult::Result(Err(e)) => { + log::error!("Shutdown request failure, server {name} (id {server_id}): {e:#}"); + }, + ConnectionResult::Result(Ok(())) => {} } - - _ = timer => { - log::info!("timeout waiting for language server {name} to shutdown"); - }, } - response_handlers.lock().take(); - Self::notify_internal::(&outbound_tx, &()).ok(); - outbound_tx.close(); - output_done.recv().await; - server.lock().take().map(|mut child| child.kill()); - log::debug!("language server shutdown finished"); - - drop(tasks); - anyhow::Ok(()) + _ = timer => { + log::info!("timeout waiting for language server {name} (id {server_id}) to shutdown"); + }, } - .log_err(), - ) + + response_handlers.lock().take(); + Self::notify_internal::(&outbound_tx, &()).ok(); + outbound_tx.close(); + output_done.recv().await; + server.lock().take().map(|mut child| child.kill()); + drop(tasks); + log::debug!("language server shutdown finished"); + Some(()) + }) } else { None } diff --git a/crates/prettier/src/prettier_server.js b/crates/prettier/src/prettier_server.js index 6799b4acebc8be68d99c95610a617f5b7f1ea1c4..b3d8a660a40d6f629ba63847f5e00d91046b7cd7 100644 --- a/crates/prettier/src/prettier_server.js +++ b/crates/prettier/src/prettier_server.js @@ -152,6 +152,10 @@ async function handleMessage(message, prettier) { throw new Error(`Message method is undefined: ${JSON.stringify(message)}`); } else if (method == "initialized") { return; + } else if (method === "shutdown") { + sendResponse({ result: {} }); + } else if (method == "exit") { + process.exit(0); } if (id === undefined) { From 45ddf32a1d0f8dda645f564486f6a4128dacd1a6 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Thu, 24 Jul 2025 13:08:02 -0400 Subject: [PATCH 325/658] Fix variable name in project type reporting (#35045) Release Notes: - N/A --- crates/client/src/telemetry.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 4983fda5efa034c73326c627f555180afe753dfa..7d39464e4a9063848be7112dd1209e7c00c1eaab 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -358,13 +358,13 @@ impl Telemetry { worktree_id: WorktreeId, updated_entries_set: &UpdatedEntriesSet, ) { - let Some(project_type_names) = self.detect_project_types(worktree_id, updated_entries_set) + let Some(project_types) = self.detect_project_types(worktree_id, updated_entries_set) else { return; }; - for project_type_name in project_type_names { - telemetry::event!("Project Opened", project_type = project_type_name); + for project_type in project_types { + telemetry::event!("Project Opened", project_type = project_type); } } From 2d0f10c48a3308bde3079fdb5301006256f8d50b Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Thu, 24 Jul 2025 14:39:29 -0300 Subject: [PATCH 326/658] Refactor to use new ACP crate (#35043) This will prepare us for running the protocol over MCP Release Notes: - N/A --------- Co-authored-by: Ben Brandt Co-authored-by: Conrad Irwin Co-authored-by: Richard Feldman --- Cargo.lock | 15 +- Cargo.toml | 1 + crates/acp_thread/Cargo.toml | 1 + crates/acp_thread/src/acp_thread.rs | 1002 +++++++---------- crates/acp_thread/src/connection.rs | 36 +- crates/acp_thread/src/old_acp_support.rs | 461 ++++++++ crates/agent_servers/Cargo.toml | 1 + crates/agent_servers/src/agent_servers.rs | 11 +- crates/agent_servers/src/claude.rs | 507 +++++---- crates/agent_servers/src/claude/mcp_server.rs | 179 +-- crates/agent_servers/src/claude/tools.rs | 275 ++--- crates/agent_servers/src/e2e_tests.rs | 95 +- crates/agent_servers/src/gemini.rs | 98 +- .../agent_servers/src/stdio_agent_server.rs | 119 -- crates/agent_ui/Cargo.toml | 4 +- crates/agent_ui/src/acp/thread_view.rs | 644 +++++------ crates/agent_ui/src/agent_diff.rs | 6 +- crates/agent_ui/src/agent_panel.rs | 2 +- crates/context_server/src/client.rs | 87 +- crates/context_server/src/protocol.rs | 20 + crates/context_server/src/types.rs | 16 +- 21 files changed, 1831 insertions(+), 1749 deletions(-) create mode 100644 crates/acp_thread/src/old_acp_support.rs delete mode 100644 crates/agent_servers/src/stdio_agent_server.rs diff --git a/Cargo.lock b/Cargo.lock index 8f791d395afe43d47cac363009f88c244d63bb69..2c65131db0a712dcd3f18fddbffc5724837cb88a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6,6 +6,7 @@ version = 4 name = "acp_thread" version = "0.1.0" dependencies = [ + "agent-client-protocol", "agentic-coding-protocol", "anyhow", "assistant_tool", @@ -135,11 +136,23 @@ dependencies = [ "zstd", ] +[[package]] +name = "agent-client-protocol" +version = "0.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb7f39671e02f8a1aeb625652feae40b6fc2597baaa97e028a98863477aecbd" +dependencies = [ + "schemars", + "serde", + "serde_json", +] + [[package]] name = "agent_servers" version = "0.1.0" dependencies = [ "acp_thread", + "agent-client-protocol", "agentic-coding-protocol", "anyhow", "collections", @@ -195,9 +208,9 @@ version = "0.1.0" dependencies = [ "acp_thread", "agent", + "agent-client-protocol", "agent_servers", "agent_settings", - "agentic-coding-protocol", "ai_onboarding", "anyhow", "assistant_context", diff --git a/Cargo.toml b/Cargo.toml index ec793a7429a1e786ef8f7620a65169283354ff05..90629501276778ebd4e8f534d5be3144ebf6989d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -413,6 +413,7 @@ zlog_settings = { path = "crates/zlog_settings" } # agentic-coding-protocol = "0.0.10" +agent-client-protocol = "0.0.10" 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 b44c25ccc998f5924277e6ca6ef393ca15e8e345..011f26f364ad2c2a7cdb97331f9ec90f8f06ed82 100644 --- a/crates/acp_thread/Cargo.toml +++ b/crates/acp_thread/Cargo.toml @@ -16,6 +16,7 @@ doctest = false 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 diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 9af1eeb1872fb9c44e3159a3d1772b68c98e67d7..3c6c21205f825bac9dcdc6539a207d1c25caa646 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -1,17 +1,15 @@ mod connection; +mod old_acp_support; pub use connection::*; +pub use old_acp_support::*; -pub use acp::ToolCallId; -use agentic_coding_protocol::{ - self as acp, AgentRequest, ProtocolVersion, ToolCallConfirmationOutcome, ToolCallLocation, - UserMessageChunk, -}; +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 futures::{FutureExt, channel::oneshot, future::BoxFuture}; -use gpui::{AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity}; +use gpui::{AppContext, Context, Entity, EventEmitter, SharedString, Task}; use itertools::Itertools; use language::{ Anchor, Buffer, BufferSnapshot, Capability, LanguageRegistry, OffsetRangeExt as _, Point, @@ -21,46 +19,37 @@ use markdown::Markdown; use project::{AgentLocation, Project}; use std::collections::HashMap; use std::error::Error; -use std::fmt::{Formatter, Write}; +use std::fmt::Formatter; +use std::rc::Rc; use std::{ fmt::Display, mem, path::{Path, PathBuf}, sync::Arc, }; -use ui::{App, IconName}; +use ui::App; use util::ResultExt; -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Debug)] pub struct UserMessage { - pub content: Entity, + pub content: ContentBlock, } impl UserMessage { pub fn from_acp( - message: &acp::SendUserMessageParams, + message: impl IntoIterator, language_registry: Arc, cx: &mut App, ) -> Self { - let mut md_source = String::new(); - - for chunk in &message.chunks { - match chunk { - UserMessageChunk::Text { text } => md_source.push_str(&text), - UserMessageChunk::Path { path } => { - write!(&mut md_source, "{}", MentionPath(&path)).unwrap() - } - } - } - - Self { - content: cx - .new(|cx| Markdown::new(md_source.into(), Some(language_registry), None, cx)), + 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.read(cx).source()) + format!("## User\n\n{}\n\n", self.content.to_markdown(cx)) } } @@ -96,7 +85,7 @@ impl Display for MentionPath<'_> { } } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Debug, PartialEq)] pub struct AssistantMessage { pub chunks: Vec, } @@ -113,42 +102,24 @@ impl AssistantMessage { } } -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Debug, PartialEq)] pub enum AssistantMessageChunk { - Text { chunk: Entity }, - Thought { chunk: Entity }, + Message { block: ContentBlock }, + Thought { block: ContentBlock }, } impl AssistantMessageChunk { - pub fn from_acp( - chunk: acp::AssistantMessageChunk, - language_registry: Arc, - cx: &mut App, - ) -> Self { - match chunk { - acp::AssistantMessageChunk::Text { text } => Self::Text { - chunk: cx.new(|cx| Markdown::new(text.into(), Some(language_registry), None, cx)), - }, - acp::AssistantMessageChunk::Thought { thought } => Self::Thought { - chunk: cx - .new(|cx| Markdown::new(thought.into(), Some(language_registry), None, cx)), - }, - } - } - - pub fn from_str(chunk: &str, language_registry: Arc, cx: &mut App) -> Self { - Self::Text { - chunk: cx.new(|cx| { - Markdown::new(chunk.to_owned().into(), Some(language_registry), None, cx) - }), + pub fn from_str(chunk: &str, language_registry: &Arc, cx: &mut App) -> Self { + Self::Message { + block: ContentBlock::new(chunk.into(), language_registry, cx), } } fn to_markdown(&self, cx: &App) -> String { match self { - Self::Text { chunk } => chunk.read(cx).source().to_string(), - Self::Thought { chunk } => { - format!("\n{}\n", chunk.read(cx).source()) + Self::Message { block } => block.to_markdown(cx).to_string(), + Self::Thought { block } => { + format!("\n{}\n", block.to_markdown(cx)) } } } @@ -166,19 +137,15 @@ impl AgentThreadEntry { match self { Self::UserMessage(message) => message.to_markdown(cx), Self::AssistantMessage(message) => message.to_markdown(cx), - Self::ToolCall(too_call) => too_call.to_markdown(cx), + Self::ToolCall(tool_call) => tool_call.to_markdown(cx), } } - pub fn diff(&self) -> Option<&Diff> { - if let AgentThreadEntry::ToolCall(ToolCall { - content: Some(ToolCallContent::Diff { diff }), - .. - }) = self - { - Some(&diff) + pub fn diffs(&self) -> impl Iterator { + if let AgentThreadEntry::ToolCall(call) = self { + itertools::Either::Left(call.diffs()) } else { - None + itertools::Either::Right(std::iter::empty()) } } @@ -195,20 +162,54 @@ impl AgentThreadEntry { pub struct ToolCall { pub id: acp::ToolCallId, pub label: Entity, - pub icon: IconName, - pub content: Option, + pub kind: acp::ToolKind, + pub content: Vec, pub status: ToolCallStatus, pub locations: Vec, } impl ToolCall { + fn from_acp( + tool_call: acp::ToolCall, + status: ToolCallStatus, + language_registry: Arc, + cx: &mut App, + ) -> Self { + Self { + id: tool_call.id, + label: cx.new(|cx| { + Markdown::new( + tool_call.label.into(), + Some(language_registry.clone()), + None, + cx, + ) + }), + kind: tool_call.kind, + content: tool_call + .content + .into_iter() + .map(|content| ToolCallContent::from_acp(content, language_registry.clone(), cx)) + .collect(), + locations: tool_call.locations, + status, + } + } + + pub fn diffs(&self) -> impl Iterator { + self.content.iter().filter_map(|content| match content { + ToolCallContent::ContentBlock { .. } => None, + ToolCallContent::Diff { diff } => Some(diff), + }) + } + fn to_markdown(&self, cx: &App) -> String { let mut markdown = format!( "**Tool Call: {}**\nStatus: {}\n\n", self.label.read(cx).source(), self.status ); - if let Some(content) = &self.content { + for content in &self.content { markdown.push_str(content.to_markdown(cx).as_str()); markdown.push_str("\n\n"); } @@ -219,8 +220,8 @@ impl ToolCall { #[derive(Debug)] pub enum ToolCallStatus { WaitingForConfirmation { - confirmation: ToolCallConfirmation, - respond_tx: oneshot::Sender, + options: Vec, + respond_tx: oneshot::Sender, }, Allowed { status: acp::ToolCallStatus, @@ -237,9 +238,9 @@ impl Display for ToolCallStatus { match self { ToolCallStatus::WaitingForConfirmation { .. } => "Waiting for confirmation", ToolCallStatus::Allowed { status } => match status { - acp::ToolCallStatus::Running => "Running", - acp::ToolCallStatus::Finished => "Finished", - acp::ToolCallStatus::Error => "Error", + acp::ToolCallStatus::InProgress => "In Progress", + acp::ToolCallStatus::Completed => "Completed", + acp::ToolCallStatus::Failed => "Failed", }, ToolCallStatus::Rejected => "Rejected", ToolCallStatus::Canceled => "Canceled", @@ -248,86 +249,92 @@ impl Display for ToolCallStatus { } } -#[derive(Debug)] -pub enum ToolCallConfirmation { - Edit { - description: Option>, - }, - Execute { - command: String, - root_command: String, - description: Option>, - }, - Mcp { - server_name: String, - tool_name: String, - tool_display_name: String, - description: Option>, - }, - Fetch { - urls: Vec, - description: Option>, - }, - Other { - description: Entity, - }, +#[derive(Debug, PartialEq, Clone)] +pub enum ContentBlock { + Empty, + Markdown { markdown: Entity }, } -impl ToolCallConfirmation { - pub fn from_acp( - confirmation: acp::ToolCallConfirmation, +impl ContentBlock { + pub fn new( + block: acp::ContentBlock, + language_registry: &Arc, + cx: &mut App, + ) -> Self { + let mut this = Self::Empty; + this.append(block, language_registry, cx); + this + } + + pub fn new_combined( + blocks: impl IntoIterator, language_registry: Arc, cx: &mut App, ) -> Self { - let to_md = |description: String, cx: &mut App| -> Entity { - cx.new(|cx| { - Markdown::new( - description.into(), - Some(language_registry.clone()), - None, - cx, - ) - }) + let mut this = Self::Empty; + for block in blocks { + this.append(block, &language_registry, cx); + } + this + } + + pub fn append( + &mut self, + block: acp::ContentBlock, + language_registry: &Arc, + cx: &mut App, + ) { + 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())) + } else { + resource_link.uri.clone() + } + } + acp::ContentBlock::Image(_) + | acp::ContentBlock::Audio(_) + | acp::ContentBlock::Resource(_) => String::new(), }; - match confirmation { - acp::ToolCallConfirmation::Edit { description } => Self::Edit { - description: description.map(|description| to_md(description, cx)), - }, - acp::ToolCallConfirmation::Execute { - command, - root_command, - description, - } => Self::Execute { - command, - root_command, - description: description.map(|description| to_md(description, cx)), - }, - acp::ToolCallConfirmation::Mcp { - server_name, - tool_name, - tool_display_name, - description, - } => Self::Mcp { - server_name, - tool_name, - tool_display_name, - description: description.map(|description| to_md(description, cx)), - }, - acp::ToolCallConfirmation::Fetch { urls, description } => Self::Fetch { - urls: urls.iter().map(|url| url.into()).collect(), - description: description.map(|description| to_md(description, cx)), - }, - acp::ToolCallConfirmation::Other { description } => Self::Other { - description: to_md(description, cx), - }, + 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)); + } + } + } + + fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str { + match self { + ContentBlock::Empty => "", + ContentBlock::Markdown { markdown } => markdown.read(cx).source(), + } + } + + pub fn markdown(&self) -> Option<&Entity> { + match self { + ContentBlock::Empty => None, + ContentBlock::Markdown { markdown } => Some(markdown), } } } #[derive(Debug)] pub enum ToolCallContent { - Markdown { markdown: Entity }, + ContentBlock { content: ContentBlock }, Diff { diff: Diff }, } @@ -338,8 +345,8 @@ impl ToolCallContent { cx: &mut App, ) -> Self { match content { - acp::ToolCallContent::Markdown { markdown } => Self::Markdown { - markdown: cx.new(|cx| Markdown::new_text(markdown.into(), cx)), + acp::ToolCallContent::ContentBlock { content } => Self::ContentBlock { + content: ContentBlock::new(content, &language_registry, cx), }, acp::ToolCallContent::Diff { diff } => Self::Diff { diff: Diff::from_acp(diff, language_registry, cx), @@ -347,9 +354,9 @@ impl ToolCallContent { } } - fn to_markdown(&self, cx: &App) -> String { + pub fn to_markdown(&self, cx: &App) -> String { match self { - Self::Markdown { markdown } => markdown.read(cx).source().to_string(), + Self::ContentBlock { content } => content.to_markdown(cx).to_string(), Self::Diff { diff } => diff.to_markdown(cx), } } @@ -520,8 +527,8 @@ pub struct AcpThread { action_log: Entity, shared_buffers: HashMap, BufferSnapshot>, send_task: Option>, - connection: Arc, - child_status: Option>>, + connection: Rc, + session_id: acp::SessionId, } pub enum AcpThreadEvent { @@ -563,10 +570,9 @@ impl Error for LoadError {} impl AcpThread { pub fn new( - connection: impl AgentConnection + 'static, - title: SharedString, - child_status: Option>>, + connection: Rc, project: Entity, + session_id: acp::SessionId, cx: &mut Context, ) -> Self { let action_log = cx.new(|_| ActionLog::new(project.clone())); @@ -576,24 +582,11 @@ impl AcpThread { shared_buffers: Default::default(), entries: Default::default(), plan: Default::default(), - title, + title: connection.name().into(), project, send_task: None, - connection: Arc::new(connection), - child_status, - } - } - - /// Send a request to the agent and wait for a response. - pub fn request( - &self, - params: R, - ) -> impl use + Future> { - let params = params.into_any(); - let result = self.connection.request_any(params); - async move { - let result = result.await?; - Ok(R::response_from_any(result)?) + connection, + session_id, } } @@ -629,15 +622,7 @@ impl AcpThread { for entry in self.entries.iter().rev() { match entry { AgentThreadEntry::UserMessage(_) => return false, - AgentThreadEntry::ToolCall(ToolCall { - status: - ToolCallStatus::Allowed { - status: acp::ToolCallStatus::Running, - .. - }, - content: Some(ToolCallContent::Diff { .. }), - .. - }) => return true, + AgentThreadEntry::ToolCall(call) if call.diffs().next().is_some() => return true, AgentThreadEntry::ToolCall(_) | AgentThreadEntry::AssistantMessage(_) => {} } } @@ -652,42 +637,37 @@ impl AcpThread { pub fn push_assistant_chunk( &mut self, - chunk: acp::AssistantMessageChunk, + chunk: acp::ContentBlock, + is_thought: bool, 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::AssistantMessage(AssistantMessage { chunks }) = last_entry { cx.emit(AcpThreadEvent::EntryUpdated(entries_len - 1)); - - match (chunks.last_mut(), &chunk) { - ( - Some(AssistantMessageChunk::Text { chunk: old_chunk }), - acp::AssistantMessageChunk::Text { text: new_chunk }, - ) - | ( - Some(AssistantMessageChunk::Thought { chunk: old_chunk }), - acp::AssistantMessageChunk::Thought { thought: new_chunk }, - ) => { - old_chunk.update(cx, |old_chunk, cx| { - old_chunk.append(&new_chunk, cx); - }); + match (chunks.last_mut(), is_thought) { + (Some(AssistantMessageChunk::Message { block }), false) + | (Some(AssistantMessageChunk::Thought { block }), true) => { + block.append(chunk, &language_registry, cx) } _ => { - chunks.push(AssistantMessageChunk::from_acp( - chunk, - self.project.read(cx).languages().clone(), - cx, - )); + let block = ContentBlock::new(chunk, &language_registry, cx); + if is_thought { + chunks.push(AssistantMessageChunk::Thought { block }) + } else { + chunks.push(AssistantMessageChunk::Message { block }) + } } } } else { - let chunk = AssistantMessageChunk::from_acp( - chunk, - self.project.read(cx).languages().clone(), - cx, - ); + let block = ContentBlock::new(chunk, &language_registry, cx); + let chunk = if is_thought { + AssistantMessageChunk::Thought { block } + } else { + AssistantMessageChunk::Message { block } + }; self.push_entry( AgentThreadEntry::AssistantMessage(AssistantMessage { @@ -698,122 +678,122 @@ impl AcpThread { } } - pub fn request_new_tool_call( - &mut self, - tool_call: acp::RequestToolCallConfirmationParams, - cx: &mut Context, - ) -> ToolCallRequest { - let (tx, rx) = oneshot::channel(); - - let status = ToolCallStatus::WaitingForConfirmation { - confirmation: ToolCallConfirmation::from_acp( - tool_call.confirmation, - self.project.read(cx).languages().clone(), - cx, - ), - respond_tx: tx, - }; - - let id = self.insert_tool_call(tool_call.tool_call, status, cx); - ToolCallRequest { id, outcome: rx } - } - - pub fn request_tool_call_confirmation( + pub fn update_tool_call( &mut self, - tool_call_id: ToolCallId, - confirmation: acp::ToolCallConfirmation, + id: acp::ToolCallId, + status: acp::ToolCallStatus, + content: Option>, cx: &mut Context, - ) -> Result { - let project = self.project.read(cx).languages().clone(); - let Some((idx, call)) = self.tool_call_mut(tool_call_id) else { - anyhow::bail!("Tool call not found"); - }; - - let (tx, rx) = oneshot::channel(); + ) -> Result<()> { + let languages = self.project.read(cx).languages().clone(); + let (ix, current_call) = self.tool_call_mut(&id).context("Tool call not found")?; - call.status = ToolCallStatus::WaitingForConfirmation { - confirmation: ToolCallConfirmation::from_acp(confirmation, project, cx), - respond_tx: tx, - }; + if let Some(content) = content { + current_call.content = content + .into_iter() + .map(|chunk| ToolCallContent::from_acp(chunk, languages.clone(), cx)) + .collect(); + } + current_call.status = ToolCallStatus::Allowed { status }; - cx.emit(AcpThreadEvent::EntryUpdated(idx)); + cx.emit(AcpThreadEvent::EntryUpdated(ix)); - Ok(ToolCallRequest { - id: tool_call_id, - outcome: rx, - }) + Ok(()) } - pub fn push_tool_call( - &mut self, - request: acp::PushToolCallParams, - cx: &mut Context, - ) -> acp::ToolCallId { + /// 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 { - status: acp::ToolCallStatus::Running, + status: tool_call.status, }; - - self.insert_tool_call(request, status, cx) + self.upsert_tool_call_inner(tool_call, status, cx) } - fn insert_tool_call( + pub fn upsert_tool_call_inner( &mut self, - tool_call: acp::PushToolCallParams, + tool_call: acp::ToolCall, status: ToolCallStatus, cx: &mut Context, - ) -> acp::ToolCallId { + ) { let language_registry = self.project.read(cx).languages().clone(); - let id = acp::ToolCallId(self.entries.len() as u64); - let call = ToolCall { - id, - label: cx.new(|cx| { - Markdown::new( - tool_call.label.into(), - Some(language_registry.clone()), - None, - cx, - ) - }), - icon: acp_icon_to_ui_icon(tool_call.icon), - content: tool_call - .content - .map(|content| ToolCallContent::from_acp(content, language_registry, cx)), - locations: tool_call.locations, - status, - }; + let call = ToolCall::from_acp(tool_call, status, language_registry, cx); let location = call.locations.last().cloned(); + + if let Some((ix, current_call)) = self.tool_call_mut(&call.id) { + *current_call = call; + + cx.emit(AcpThreadEvent::EntryUpdated(ix)); + } else { + self.push_entry(AgentThreadEntry::ToolCall(call), cx); + } + if let Some(location) = location { self.set_project_location(location, cx) } + } + + fn tool_call_mut(&mut self, id: &acp::ToolCallId) -> Option<(usize, &mut ToolCall)> { + // The tool call we are looking for is typically the last one, or very close to the end. + // At the moment, it doesn't seem like a hashmap would be a good fit for this use case. + self.entries + .iter_mut() + .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 request_tool_call_permission( + &mut self, + tool_call: acp::ToolCall, + options: Vec, + cx: &mut Context, + ) -> oneshot::Receiver { + let (tx, rx) = oneshot::channel(); - self.push_entry(AgentThreadEntry::ToolCall(call), cx); + let status = ToolCallStatus::WaitingForConfirmation { + options, + respond_tx: tx, + }; - id + self.upsert_tool_call_inner(tool_call, status, cx); + rx } pub fn authorize_tool_call( &mut self, id: acp::ToolCallId, - outcome: acp::ToolCallConfirmationOutcome, + option_id: acp::PermissionOptionId, + option_kind: acp::PermissionOptionKind, cx: &mut Context, ) { - let Some((ix, call)) = self.tool_call_mut(id) else { + let Some((ix, call)) = self.tool_call_mut(&id) else { return; }; - let new_status = if outcome == acp::ToolCallConfirmationOutcome::Reject { - ToolCallStatus::Rejected - } else { - ToolCallStatus::Allowed { - status: acp::ToolCallStatus::Running, + let new_status = match option_kind { + acp::PermissionOptionKind::RejectOnce | acp::PermissionOptionKind::RejectAlways => { + ToolCallStatus::Rejected + } + acp::PermissionOptionKind::AllowOnce | acp::PermissionOptionKind::AllowAlways => { + ToolCallStatus::Allowed { + status: acp::ToolCallStatus::InProgress, + } } }; let curr_status = mem::replace(&mut call.status, new_status); if let ToolCallStatus::WaitingForConfirmation { respond_tx, .. } = curr_status { - respond_tx.send(outcome).log_err(); + respond_tx.send(option_id).log_err(); } else if cfg!(debug_assertions) { panic!("tried to authorize an already authorized tool call"); } @@ -821,70 +801,11 @@ impl AcpThread { cx.emit(AcpThreadEvent::EntryUpdated(ix)); } - pub fn update_tool_call( - &mut self, - id: acp::ToolCallId, - new_status: acp::ToolCallStatus, - new_content: Option, - cx: &mut Context, - ) -> Result<()> { - let language_registry = self.project.read(cx).languages().clone(); - let (ix, call) = self.tool_call_mut(id).context("Entry not found")?; - - if let Some(new_content) = new_content { - call.content = Some(ToolCallContent::from_acp( - new_content, - language_registry, - cx, - )); - } - - match &mut call.status { - ToolCallStatus::Allowed { status } => { - *status = new_status; - } - ToolCallStatus::WaitingForConfirmation { .. } => { - anyhow::bail!("Tool call hasn't been authorized yet") - } - ToolCallStatus::Rejected => { - anyhow::bail!("Tool call was rejected and therefore can't be updated") - } - ToolCallStatus::Canceled => { - call.status = ToolCallStatus::Allowed { status: new_status }; - } - } - - let location = call.locations.last().cloned(); - if let Some(location) = location { - self.set_project_location(location, cx) - } - - cx.emit(AcpThreadEvent::EntryUpdated(ix)); - Ok(()) - } - - fn tool_call_mut(&mut self, id: acp::ToolCallId) -> Option<(usize, &mut ToolCall)> { - let entry = self.entries.get_mut(id.0 as usize); - debug_assert!( - entry.is_some(), - "We shouldn't give out ids to entries that don't exist" - ); - match entry { - Some(AgentThreadEntry::ToolCall(call)) if call.id == id => Some((id.0 as usize, call)), - _ => { - if cfg!(debug_assertions) { - panic!("entry is not a tool call"); - } - None - } - } - } - pub fn plan(&self) -> &Plan { &self.plan } - pub fn update_plan(&mut self, request: acp::UpdatePlanParams, cx: &mut Context) { + pub fn update_plan(&mut self, request: acp::Plan, cx: &mut Context) { self.plan = Plan { entries: request .entries @@ -896,14 +817,14 @@ impl AcpThread { cx.notify(); } - pub fn clear_completed_plan_entries(&mut self, cx: &mut Context) { + fn clear_completed_plan_entries(&mut self, cx: &mut Context) { self.plan .entries .retain(|entry| !matches!(entry.status, acp::PlanEntryStatus::Completed)); cx.notify(); } - pub fn set_project_location(&self, location: ToolCallLocation, cx: &mut Context) { + 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; @@ -953,14 +874,8 @@ impl AcpThread { false } - pub fn initialize(&self) -> impl use<> + Future> { - self.request(acp::InitializeParams { - protocol_version: ProtocolVersion::latest(), - }) - } - - pub fn authenticate(&self) -> impl use<> + Future> { - self.request(acp::AuthenticateParams) + pub fn authenticate(&self, cx: &mut App) -> impl use<> + Future> { + self.connection.authenticate(cx) } #[cfg(any(test, feature = "test-support"))] @@ -968,39 +883,50 @@ impl AcpThread { &mut self, message: &str, cx: &mut Context, - ) -> BoxFuture<'static, Result<(), acp::Error>> { + ) -> BoxFuture<'static, Result<()>> { self.send( - acp::SendUserMessageParams { - chunks: vec![acp::UserMessageChunk::Text { - text: message.to_string(), - }], - }, + vec![acp::ContentBlock::Text(acp::TextContent { + text: message.to_string(), + annotations: None, + })], cx, ) } pub fn send( &mut self, - message: acp::SendUserMessageParams, + message: Vec, cx: &mut Context, - ) -> BoxFuture<'static, Result<(), acp::Error>> { + ) -> BoxFuture<'static, Result<()>> { + let block = ContentBlock::new_combined( + message.clone(), + self.project.read(cx).languages().clone(), + cx, + ); self.push_entry( - AgentThreadEntry::UserMessage(UserMessage::from_acp( - &message, - self.project.read(cx).languages().clone(), - cx, - )), + AgentThreadEntry::UserMessage(UserMessage { content: block }), cx, ); + self.clear_completed_plan_entries(cx); let (tx, rx) = oneshot::channel(); - let cancel = self.cancel(cx); + let cancel_task = self.cancel(cx); self.send_task = Some(cx.spawn(async move |this, cx| { async { - cancel.await.log_err(); - - let result = this.update(cx, |this, _| this.request(message))?.await; + cancel_task.await; + + let result = this + .update(cx, |this, cx| { + this.connection.prompt( + acp::PromptToolArguments { + 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(()) @@ -1018,48 +944,38 @@ impl AcpThread { .boxed() } - pub fn cancel(&mut self, cx: &mut Context) -> Task> { - if self.send_task.take().is_some() { - let request = self.request(acp::CancelSendMessageParams); - cx.spawn(async move |this, cx| { - request.await?; - this.update(cx, |this, _cx| { - for entry in this.entries.iter_mut() { - if let AgentThreadEntry::ToolCall(call) = entry { - let cancel = matches!( - call.status, - ToolCallStatus::WaitingForConfirmation { .. } - | ToolCallStatus::Allowed { - status: acp::ToolCallStatus::Running - } - ); - - if cancel { - let curr_status = - mem::replace(&mut call.status, ToolCallStatus::Canceled); - - if let ToolCallStatus::WaitingForConfirmation { - respond_tx, .. - } = curr_status - { - respond_tx - .send(acp::ToolCallConfirmationOutcome::Cancel) - .ok(); - } - } + pub fn cancel(&mut self, cx: &mut Context) -> Task<()> { + let Some(send_task) = self.send_task.take() else { + return Task::ready(()); + }; + + for entry in self.entries.iter_mut() { + if let AgentThreadEntry::ToolCall(call) = entry { + let cancel = matches!( + call.status, + ToolCallStatus::WaitingForConfirmation { .. } + | ToolCallStatus::Allowed { + status: acp::ToolCallStatus::InProgress } - } - })?; - Ok(()) - }) - } else { - Task::ready(Ok(())) + ); + + if cancel { + call.status = ToolCallStatus::Canceled; + } + } } + + self.connection.cancel(&self.session_id, cx); + + // Wait for the send task to complete + cx.foreground_executor().spawn(send_task) } pub fn read_text_file( &self, - request: acp::ReadTextFileParams, + path: PathBuf, + line: Option, + limit: Option, reuse_shared_snapshot: bool, cx: &mut Context, ) -> Task> { @@ -1068,7 +984,7 @@ impl AcpThread { cx.spawn(async move |this, cx| { let load = project.update(cx, |project, cx| { let path = project - .project_path_for_absolute_path(&request.path, cx) + .project_path_for_absolute_path(&path, cx) .context("invalid path")?; anyhow::Ok(project.open_buffer(path, cx)) }); @@ -1094,7 +1010,7 @@ impl AcpThread { let position = buffer .read(cx) .snapshot() - .anchor_before(Point::new(request.line.unwrap_or_default(), 0)); + .anchor_before(Point::new(line.unwrap_or_default(), 0)); project.set_agent_location( Some(AgentLocation { buffer: buffer.downgrade(), @@ -1110,11 +1026,11 @@ impl AcpThread { this.update(cx, |this, _| { let text = snapshot.text(); this.shared_buffers.insert(buffer.clone(), snapshot); - if request.line.is_none() && request.limit.is_none() { + if line.is_none() && limit.is_none() { return Ok(text); } - let limit = request.limit.unwrap_or(u32::MAX) as usize; - let Some(line) = request.line else { + let limit = limit.unwrap_or(u32::MAX) as usize; + let Some(line) = line else { return Ok(text.lines().take(limit).collect::()); }; @@ -1199,197 +1115,15 @@ impl AcpThread { }) } - pub fn child_status(&mut self) -> Option>> { - self.child_status.take() - } - pub fn to_markdown(&self, cx: &App) -> String { self.entries.iter().map(|e| e.to_markdown(cx)).collect() } } -#[derive(Clone)] -pub struct AcpClientDelegate { - thread: WeakEntity, - cx: AsyncApp, - // sent_buffer_versions: HashMap, HashMap>, -} - -impl AcpClientDelegate { - pub fn new(thread: WeakEntity, cx: AsyncApp) -> Self { - Self { thread, cx } - } - - pub async fn clear_completed_plan_entries(&self) -> Result<()> { - let cx = &mut self.cx.clone(); - cx.update(|cx| { - self.thread - .update(cx, |thread, cx| thread.clear_completed_plan_entries(cx)) - })? - .context("Failed to update thread")?; - - Ok(()) - } - - pub async fn request_existing_tool_call_confirmation( - &self, - tool_call_id: ToolCallId, - confirmation: acp::ToolCallConfirmation, - ) -> Result { - let cx = &mut self.cx.clone(); - let ToolCallRequest { outcome, .. } = cx - .update(|cx| { - self.thread.update(cx, |thread, cx| { - thread.request_tool_call_confirmation(tool_call_id, confirmation, cx) - }) - })? - .context("Failed to update thread")??; - - Ok(outcome.await?) - } - - pub async fn read_text_file_reusing_snapshot( - &self, - request: acp::ReadTextFileParams, - ) -> Result { - let content = self - .cx - .update(|cx| { - self.thread - .update(cx, |thread, cx| thread.read_text_file(request, true, cx)) - })? - .context("Failed to update thread")? - .await?; - Ok(acp::ReadTextFileResponse { content }) - } -} - -impl acp::Client for AcpClientDelegate { - async fn stream_assistant_message_chunk( - &self, - params: acp::StreamAssistantMessageChunkParams, - ) -> Result<(), acp::Error> { - let cx = &mut self.cx.clone(); - - cx.update(|cx| { - self.thread - .update(cx, |thread, cx| { - thread.push_assistant_chunk(params.chunk, cx) - }) - .ok(); - })?; - - Ok(()) - } - - async fn request_tool_call_confirmation( - &self, - request: acp::RequestToolCallConfirmationParams, - ) -> Result { - let cx = &mut self.cx.clone(); - let ToolCallRequest { id, outcome } = cx - .update(|cx| { - self.thread - .update(cx, |thread, cx| thread.request_new_tool_call(request, cx)) - })? - .context("Failed to update thread")?; - - Ok(acp::RequestToolCallConfirmationResponse { - id, - outcome: outcome.await.map_err(acp::Error::into_internal_error)?, - }) - } - - async fn push_tool_call( - &self, - request: acp::PushToolCallParams, - ) -> Result { - let cx = &mut self.cx.clone(); - let id = cx - .update(|cx| { - self.thread - .update(cx, |thread, cx| thread.push_tool_call(request, cx)) - })? - .context("Failed to update thread")?; - - Ok(acp::PushToolCallResponse { id }) - } - - async fn update_tool_call(&self, request: acp::UpdateToolCallParams) -> Result<(), acp::Error> { - let cx = &mut self.cx.clone(); - - cx.update(|cx| { - self.thread.update(cx, |thread, cx| { - thread.update_tool_call(request.tool_call_id, request.status, request.content, cx) - }) - })? - .context("Failed to update thread")??; - - Ok(()) - } - - async fn update_plan(&self, request: acp::UpdatePlanParams) -> Result<(), acp::Error> { - let cx = &mut self.cx.clone(); - - cx.update(|cx| { - self.thread - .update(cx, |thread, cx| thread.update_plan(request, cx)) - })? - .context("Failed to update thread")?; - - Ok(()) - } - - async fn read_text_file( - &self, - request: acp::ReadTextFileParams, - ) -> Result { - let content = self - .cx - .update(|cx| { - self.thread - .update(cx, |thread, cx| thread.read_text_file(request, false, cx)) - })? - .context("Failed to update thread")? - .await?; - Ok(acp::ReadTextFileResponse { content }) - } - - async fn write_text_file(&self, request: acp::WriteTextFileParams) -> Result<(), acp::Error> { - self.cx - .update(|cx| { - self.thread.update(cx, |thread, cx| { - thread.write_text_file(request.path, request.content, cx) - }) - })? - .context("Failed to update thread")? - .await?; - - Ok(()) - } -} - -fn acp_icon_to_ui_icon(icon: acp::Icon) -> IconName { - match icon { - acp::Icon::FileSearch => IconName::ToolSearch, - acp::Icon::Folder => IconName::ToolFolder, - acp::Icon::Globe => IconName::ToolWeb, - acp::Icon::Hammer => IconName::ToolHammer, - acp::Icon::LightBulb => IconName::ToolBulb, - acp::Icon::Pencil => IconName::ToolPencil, - acp::Icon::Regex => IconName::ToolRegex, - acp::Icon::Terminal => IconName::ToolTerminal, - } -} - -pub struct ToolCallRequest { - pub id: acp::ToolCallId, - pub outcome: oneshot::Receiver, -} - #[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, select}; @@ -1400,6 +1134,7 @@ mod tests { use settings::SettingsStore; use smol::{future::BoxedLocal, stream::StreamExt as _}; use std::{cell::RefCell, rc::Rc, time::Duration}; + use util::path; fn init_test(cx: &mut TestAppContext) { @@ -1424,8 +1159,8 @@ mod tests { fake_server.on_user_message(move |_, server, mut cx| async move { server .update(&mut cx, |server, _| { - server.send_to_zed(acp::StreamAssistantMessageChunkParams { - chunk: acp::AssistantMessageChunk::Thought { + server.send_to_zed(acp_old::StreamAssistantMessageChunkParams { + chunk: acp_old::AssistantMessageChunk::Thought { thought: "Thinking ".into(), }, }) @@ -1434,8 +1169,8 @@ mod tests { .unwrap(); server .update(&mut cx, |server, _| { - server.send_to_zed(acp::StreamAssistantMessageChunkParams { - chunk: acp::AssistantMessageChunk::Thought { + server.send_to_zed(acp_old::StreamAssistantMessageChunkParams { + chunk: acp_old::AssistantMessageChunk::Thought { thought: "hard!".into(), }, }) @@ -1501,7 +1236,7 @@ mod tests { async move { let content = server .update(&mut cx, |server, _| { - server.send_to_zed(acp::ReadTextFileParams { + server.send_to_zed(acp_old::ReadTextFileParams { path: path!("/tmp/foo").into(), line: None, limit: None, @@ -1513,7 +1248,7 @@ mod tests { read_file_tx.take().unwrap().send(()).unwrap(); server .update(&mut cx, |server, _| { - server.send_to_zed(acp::WriteTextFileParams { + server.send_to_zed(acp_old::WriteTextFileParams { path: path!("/tmp/foo").into(), content: "one\ntwo\nthree\nfour\nfive\n".to_string(), }) @@ -1564,9 +1299,9 @@ mod tests { async move { let tool_call_result = server .update(&mut cx, |server, _| { - server.send_to_zed(acp::PushToolCallParams { + server.send_to_zed(acp_old::PushToolCallParams { label: "Fetch".to_string(), - icon: acp::Icon::Globe, + icon: acp_old::Icon::Globe, content: None, locations: vec![], }) @@ -1592,7 +1327,7 @@ mod tests { thread.entries[1], AgentThreadEntry::ToolCall(ToolCall { status: ToolCallStatus::Allowed { - status: acp::ToolCallStatus::Running, + status: acp::ToolCallStatus::InProgress, .. }, .. @@ -1602,10 +1337,7 @@ mod tests { cx.run_until_parked(); - thread - .update(cx, |thread, cx| thread.cancel(cx)) - .await - .unwrap(); + thread.update(cx, |thread, cx| thread.cancel(cx)).await; thread.read_with(cx, |thread, _| { assert!(matches!( @@ -1619,9 +1351,9 @@ mod tests { fake_server .update(cx, |fake_server, _| { - fake_server.send_to_zed(acp::UpdateToolCallParams { + fake_server.send_to_zed(acp_old::UpdateToolCallParams { tool_call_id: tool_call_id.borrow().unwrap(), - status: acp::ToolCallStatus::Finished, + status: acp_old::ToolCallStatus::Finished, content: None, }) }) @@ -1629,14 +1361,14 @@ mod tests { .unwrap(); drop(end_turn_tx); - request.await.unwrap(); + assert!(request.await.unwrap_err().to_string().contains("canceled")); thread.read_with(cx, |thread, _| { assert!(matches!( thread.entries[1], AgentThreadEntry::ToolCall(ToolCall { status: ToolCallStatus::Allowed { - status: acp::ToolCallStatus::Finished, + status: acp::ToolCallStatus::Completed, .. }, .. @@ -1681,8 +1413,10 @@ mod tests { let thread = cx.new(|cx| { let foreground_executor = cx.foreground_executor().clone(); - let (connection, io_fut) = acp::AgentConnection::connect_to_agent( - AcpClientDelegate::new(cx.entity().downgrade(), cx.to_async()), + 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| { @@ -1696,23 +1430,34 @@ mod tests { Ok(()) } }); - AcpThread::new(connection, "Test".into(), Some(io_task), project, cx) + let connection = OldAcpAgentConnection { + name: "test", + connection, + child_status: io_task, + }; + + 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::ClientConnection, + connection: acp_old::ClientConnection, _io_task: Task<()>, on_user_message: Option< Rc< dyn Fn( - acp::SendUserMessageParams, + acp_old::SendUserMessageParams, Entity, AsyncApp, - ) -> LocalBoxFuture<'static, Result<(), acp::Error>>, + ) -> LocalBoxFuture<'static, Result<(), acp_old::Error>>, >, >, } @@ -1721,31 +1466,38 @@ mod tests { struct FakeAgent { server: Entity, cx: AsyncApp, + cancel_tx: Rc>>>, } - impl acp::Agent for FakeAgent { + impl acp_old::Agent for FakeAgent { async fn initialize( &self, - params: acp::InitializeParams, - ) -> Result { - Ok(acp::InitializeResponse { + params: acp_old::InitializeParams, + ) -> Result { + Ok(acp_old::InitializeResponse { protocol_version: params.protocol_version, is_authenticated: true, }) } - async fn authenticate(&self) -> Result<(), acp::Error> { + async fn authenticate(&self) -> Result<(), acp_old::Error> { Ok(()) } - async fn cancel_send_message(&self) -> Result<(), acp::Error> { + 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::SendUserMessageParams, - ) -> Result<(), acp::Error> { + 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 @@ -1753,7 +1505,10 @@ mod tests { .ok() .flatten(); if let Some(handler) = handler { - handler(request, self.server.clone(), self.cx.clone()).await + select! { + _ = cancel_rx.fuse() => Err(anyhow::anyhow!("Message sending canceled").into()), + _ = handler(request, self.server.clone(), self.cx.clone()).fuse() => Ok(()), + } } else { Err(anyhow::anyhow!("No handler for on_user_message").into()) } @@ -1765,10 +1520,11 @@ mod tests { 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::ClientConnection::connect_to_client( + let (connection, io_fut) = acp_old::ClientConnection::connect_to_client( agent.clone(), stdout, stdin, @@ -1787,10 +1543,14 @@ mod tests { fn on_user_message( &mut self, - handler: impl for<'a> Fn(acp::SendUserMessageParams, Entity, AsyncApp) -> F + handler: impl for<'a> Fn( + acp_old::SendUserMessageParams, + Entity, + AsyncApp, + ) -> F + 'static, ) where - F: Future> + 'static, + F: Future> + 'static, { self.on_user_message .replace(Rc::new(move |request, server, cx| { @@ -1798,7 +1558,7 @@ mod tests { })); } - fn send_to_zed( + fn send_to_zed( &self, message: T, ) -> BoxedLocal> { diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs index 7c0ba4f41c70c019dc42acc457c59e795679556f..fde167da5f6001c7b91c78988b66b0bfd04a8b10 100644 --- a/crates/acp_thread/src/connection.rs +++ b/crates/acp_thread/src/connection.rs @@ -1,20 +1,26 @@ -use agentic_coding_protocol as acp; +use std::{path::Path, rc::Rc}; + +use agent_client_protocol as acp; use anyhow::Result; -use futures::future::{FutureExt as _, LocalBoxFuture}; +use gpui::{AsyncApp, Entity, Task}; +use project::Project; +use ui::App; + +use crate::AcpThread; pub trait AgentConnection { - fn request_any( - &self, - params: acp::AnyAgentRequest, - ) -> LocalBoxFuture<'static, Result>; -} + fn name(&self) -> &'static str; + + fn new_thread( + self: Rc, + project: Entity, + cwd: &Path, + cx: &mut AsyncApp, + ) -> Task>>; + + fn authenticate(&self, cx: &mut App) -> Task>; + + fn prompt(&self, params: acp::PromptToolArguments, cx: &mut App) -> Task>; -impl AgentConnection for acp::AgentConnection { - fn request_any( - &self, - params: acp::AnyAgentRequest, - ) -> LocalBoxFuture<'static, Result> { - let task = self.request_any(params); - async move { Ok(task.await?) }.boxed_local() - } + fn cancel(&self, session_id: &acp::SessionId, cx: &mut App); } diff --git a/crates/acp_thread/src/old_acp_support.rs b/crates/acp_thread/src/old_acp_support.rs new file mode 100644 index 0000000000000000000000000000000000000000..316a5bcf25183cb186210e1cecdeffdf399aafdc --- /dev/null +++ b/crates/acp_thread/src/old_acp_support.rs @@ -0,0 +1,461 @@ +// 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 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 ui::App; + +use crate::{AcpThread, AcpThreadEvent, AgentConnection, ToolCallContent, ToolCallStatus}; + +#[derive(Clone)] +pub struct OldAcpClientDelegate { + thread: Rc>>, + cx: AsyncApp, + next_tool_call_id: Rc>, + // sent_buffer_versions: HashMap, HashMap>, +} + +impl OldAcpClientDelegate { + pub 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_chunk(text.into(), false, cx) + } + acp_old::AssistantMessageChunk::Thought { thought } => { + thread.push_assistant_chunk(thought.into(), true, cx) + } + }) + .ok(); + })?; + + 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()), + label, + kind, + }) + } + + let response = cx + .update(|cx| { + self.thread.borrow().update(cx, |thread, cx| { + thread.request_tool_call_permission(tool_call, 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: 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| { + let languages = thread.project.read(cx).languages().clone(); + + if let Some((ix, tool_call)) = thread + .tool_call_mut(&acp::ToolCallId(request.tool_call_id.0.to_string().into())) + { + tool_call.status = ToolCallStatus::Allowed { + status: into_new_tool_call_status(request.status), + }; + tool_call.content = request + .content + .into_iter() + .map(|content| { + ToolCallContent::from_acp( + into_new_tool_call_content(content), + languages.clone(), + cx, + ) + }) + .collect(); + + cx.emit(AcpThreadEvent::EntryUpdated(ix)); + anyhow::Ok(()) + } else { + anyhow::bail!("Tool call not found") + } + }) + })? + .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: id, + label: 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(), + } +} + +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 } => acp::ToolCallContent::ContentBlock { + content: acp::ContentBlock::Text(acp::TextContent { + annotations: None, + text: markdown, + }), + }, + 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, + } +} + +#[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 name: &'static str, + pub connection: acp_old::AgentConnection, + pub child_status: Task>, +} + +impl AgentConnection for OldAcpAgentConnection { + fn name(&self) -> &'static str { + self.name + } + + fn new_thread( + self: Rc, + project: Entity, + _cwd: &Path, + cx: &mut AsyncApp, + ) -> Task>> { + let task = self.connection.request_any( + acp_old::InitializeParams { + protocol_version: acp_old::ProtocolVersion::latest(), + } + .into_any(), + ); + cx.spawn(async move |cx| { + let result = task.await?; + let result = acp_old::InitializeParams::response_from_any(result)?; + + if !result.is_authenticated { + anyhow::bail!(Unauthenticated) + } + + 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) + }); + thread + }) + }) + } + + fn authenticate(&self, 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, params: acp::PromptToolArguments, 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(()) + }) + } + + 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) + } +} diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml index 4714245b94fd9b519cfe1987817d51ff6ecbc7fd..4371f7684dd4d618d755eb5468c8b8f62d4a8432 100644 --- a/crates/agent_servers/Cargo.toml +++ b/crates/agent_servers/Cargo.toml @@ -18,6 +18,7 @@ doctest = false [dependencies] acp_thread.workspace = true +agent-client-protocol.workspace = true agentic-coding-protocol.workspace = true anyhow.workspace = true collections.workspace = true diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs index 6d9c77f2968d7b39302391829d2d14b6d4493a91..660f61f9071132c5cc0f01eeda168bb829dcaab7 100644 --- a/crates/agent_servers/src/agent_servers.rs +++ b/crates/agent_servers/src/agent_servers.rs @@ -1,7 +1,6 @@ mod claude; mod gemini; mod settings; -mod stdio_agent_server; #[cfg(test)] mod e2e_tests; @@ -9,9 +8,8 @@ mod e2e_tests; pub use claude::*; pub use gemini::*; pub use settings::*; -pub use stdio_agent_server::*; -use acp_thread::AcpThread; +use acp_thread::AgentConnection; use anyhow::Result; use collections::HashMap; use gpui::{App, AsyncApp, Entity, SharedString, Task}; @@ -20,6 +18,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::{ path::{Path, PathBuf}, + rc::Rc, sync::Arc, }; use util::ResultExt as _; @@ -33,14 +32,14 @@ pub trait AgentServer: Send { fn name(&self) -> &'static str; fn empty_state_headline(&self) -> &'static str; fn empty_state_message(&self) -> &'static str; - fn supports_always_allow(&self) -> bool; - fn new_thread( + fn connect( &self, + // these will go away when old_acp is fully removed root_dir: &Path, project: &Entity, cx: &mut App, - ) -> Task>>; + ) -> Task>>; } impl std::fmt::Debug for AgentServerCommand { diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 835efbd6552423e7e5bcd1d321ca193581e5ab0a..5f35b4af734fc7cd7daf9cdae07ecff42c0f3985 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -1,5 +1,5 @@ mod mcp_server; -mod tools; +pub mod tools; use collections::HashMap; use project::Project; @@ -12,28 +12,24 @@ use std::pin::pin; use std::rc::Rc; use uuid::Uuid; -use agentic_coding_protocol::{ - self as acp, AnyAgentRequest, AnyAgentResult, Client, ProtocolVersion, - StreamAssistantMessageChunkParams, ToolCallContent, UpdateToolCallParams, -}; +use agent_client_protocol as acp; use anyhow::{Result, anyhow}; use futures::channel::oneshot; -use futures::future::LocalBoxFuture; -use futures::{AsyncBufReadExt, AsyncWriteExt, SinkExt}; +use futures::{AsyncBufReadExt, AsyncWriteExt}; use futures::{ AsyncRead, AsyncWrite, FutureExt, StreamExt, channel::mpsc::{self, UnboundedReceiver, UnboundedSender}, io::BufReader, select_biased, }; -use gpui::{App, AppContext, Entity, Task}; +use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity}; use serde::{Deserialize, Serialize}; use util::ResultExt; -use crate::claude::mcp_server::ClaudeMcpServer; +use crate::claude::mcp_server::{ClaudeZedMcpServer, McpConfig}; use crate::claude::tools::ClaudeTool; use crate::{AgentServer, AgentServerCommand, AllAgentServersSettings}; -use acp_thread::{AcpClientDelegate, AcpThread, AgentConnection}; +use acp_thread::{AcpThread, AgentConnection}; #[derive(Clone)] pub struct ClaudeCode; @@ -55,29 +51,57 @@ impl AgentServer for ClaudeCode { ui::IconName::AiClaude } - fn supports_always_allow(&self) -> bool { - false + fn connect( + &self, + _root_dir: &Path, + _project: &Entity, + _cx: &mut App, + ) -> Task>> { + let connection = ClaudeAgentConnection { + sessions: Default::default(), + }; + + Task::ready(Ok(Rc::new(connection) as _)) + } +} + +#[cfg(unix)] +fn send_interrupt(pid: libc::pid_t) -> anyhow::Result<()> { + let pid = nix::unistd::Pid::from_raw(pid); + + nix::sys::signal::kill(pid, nix::sys::signal::SIGINT) + .map_err(|e| anyhow!("Failed to interrupt process: {}", e)) +} + +#[cfg(windows)] +fn send_interrupt(_pid: i32) -> anyhow::Result<()> { + panic!("Cancel not implemented on Windows") +} + +struct ClaudeAgentConnection { + sessions: Rc>>, +} + +impl AgentConnection for ClaudeAgentConnection { + fn name(&self) -> &'static str { + ClaudeCode.name() } fn new_thread( - &self, - root_dir: &Path, - project: &Entity, - cx: &mut App, + self: Rc, + project: Entity, + cwd: &Path, + cx: &mut AsyncApp, ) -> Task>> { - let project = project.clone(); - let root_dir = root_dir.to_path_buf(); - let title = self.name().into(); + let cwd = cwd.to_owned(); cx.spawn(async move |cx| { - let (mut delegate_tx, delegate_rx) = watch::channel(None); - let tool_id_map = Rc::new(RefCell::new(HashMap::default())); - - let mcp_server = ClaudeMcpServer::new(delegate_rx, tool_id_map.clone(), cx).await?; + let (mut thread_tx, thread_rx) = watch::channel(WeakEntity::new_invalid()); + let permission_mcp_server = ClaudeZedMcpServer::new(thread_rx.clone(), cx).await?; let mut mcp_servers = HashMap::default(); mcp_servers.insert( mcp_server::SERVER_NAME.to_string(), - mcp_server.server_config()?, + permission_mcp_server.server_config()?, ); let mcp_config = McpConfig { mcp_servers }; @@ -104,177 +128,180 @@ impl AgentServer for ClaudeCode { let (outgoing_tx, outgoing_rx) = mpsc::unbounded(); let (cancel_tx, mut cancel_rx) = mpsc::unbounded::>>(); - let session_id = Uuid::new_v4(); + let session_id = acp::SessionId(Uuid::new_v4().to_string().into()); log::trace!("Starting session with id: {}", session_id); - cx.background_spawn(async move { - let mut outgoing_rx = Some(outgoing_rx); - let mut mode = ClaudeSessionMode::Start; - - loop { - let mut child = - spawn_claude(&command, mode, session_id, &mcp_config_path, &root_dir) - .await?; - mode = ClaudeSessionMode::Resume; - - let pid = child.id(); - log::trace!("Spawned (pid: {})", pid); - - let mut io_fut = pin!( - ClaudeAgentConnection::handle_io( - outgoing_rx.take().unwrap(), - incoming_message_tx.clone(), - child.stdin.take().unwrap(), - child.stdout.take().unwrap(), + cx.background_spawn({ + let session_id = session_id.clone(); + async move { + let mut outgoing_rx = Some(outgoing_rx); + let mut mode = ClaudeSessionMode::Start; + + loop { + let mut child = spawn_claude( + &command, + mode, + session_id.clone(), + &mcp_config_path, + &cwd, ) - .fuse() - ); - - select_biased! { - done_tx = cancel_rx.next() => { - if let Some(done_tx) = done_tx { - log::trace!("Interrupted (pid: {})", pid); - let result = send_interrupt(pid as i32); - outgoing_rx.replace(io_fut.await?); - done_tx.send(result).log_err(); - continue; + .await?; + mode = ClaudeSessionMode::Resume; + + let pid = child.id(); + log::trace!("Spawned (pid: {})", pid); + + let mut io_fut = pin!( + ClaudeAgentSession::handle_io( + outgoing_rx.take().unwrap(), + incoming_message_tx.clone(), + child.stdin.take().unwrap(), + child.stdout.take().unwrap(), + ) + .fuse() + ); + + select_biased! { + done_tx = cancel_rx.next() => { + if let Some(done_tx) = done_tx { + log::trace!("Interrupted (pid: {})", pid); + let result = send_interrupt(pid as i32); + outgoing_rx.replace(io_fut.await?); + done_tx.send(result).log_err(); + continue; + } + } + result = io_fut => { + result?; } } - result = io_fut => { - result?; - } + + log::trace!("Stopped (pid: {})", pid); + break; } - log::trace!("Stopped (pid: {})", pid); - break; + drop(mcp_config_path); + anyhow::Ok(()) } - - drop(mcp_config_path); - anyhow::Ok(()) }) .detach(); - cx.new(|cx| { - let end_turn_tx = Rc::new(RefCell::new(None)); - let delegate = AcpClientDelegate::new(cx.entity().downgrade(), cx.to_async()); - delegate_tx.send(Some(delegate.clone())).log_err(); - - let handler_task = cx.foreground_executor().spawn({ - let end_turn_tx = end_turn_tx.clone(); - let tool_id_map = tool_id_map.clone(); - let delegate = delegate.clone(); - async move { - while let Some(message) = incoming_message_rx.next().await { - ClaudeAgentConnection::handle_message( - delegate.clone(), - message, - end_turn_tx.clone(), - tool_id_map.clone(), - ) - .await - } + 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(); + async move |cx| { + while let Some(message) = incoming_message_rx.next().await { + ClaudeAgentSession::handle_message( + thread_rx.clone(), + message, + end_turn_tx.clone(), + cx, + ) + .await } - }); - - let mut connection = ClaudeAgentConnection { - delegate, - outgoing_tx, - end_turn_tx, - cancel_tx, - session_id, - _handler_task: handler_task, - _mcp_server: None, - }; + } + }); - connection._mcp_server = Some(mcp_server); - acp_thread::AcpThread::new(connection, title, None, project.clone(), cx) - }) - }) - } -} + let thread = + cx.new(|cx| AcpThread::new(self.clone(), project, session_id.clone(), cx))?; -#[cfg(unix)] -fn send_interrupt(pid: libc::pid_t) -> anyhow::Result<()> { - let pid = nix::unistd::Pid::from_raw(pid); + thread_tx.send(thread.downgrade())?; - nix::sys::signal::kill(pid, nix::sys::signal::SIGINT) - .map_err(|e| anyhow!("Failed to interrupt process: {}", e)) -} + let session = ClaudeAgentSession { + outgoing_tx, + end_turn_tx, + cancel_tx, + _handler_task: handler_task, + _mcp_server: Some(permission_mcp_server), + }; -#[cfg(windows)] -fn send_interrupt(_pid: i32) -> anyhow::Result<()> { - panic!("Cancel not implemented on Windows") -} + self.sessions.borrow_mut().insert(session_id, session); -impl AgentConnection for ClaudeAgentConnection { - /// Send a request to the agent and wait for a response. - fn request_any( - &self, - params: AnyAgentRequest, - ) -> LocalBoxFuture<'static, Result> { - let delegate = self.delegate.clone(); - let end_turn_tx = self.end_turn_tx.clone(); - let outgoing_tx = self.outgoing_tx.clone(); - let mut cancel_tx = self.cancel_tx.clone(); - let session_id = self.session_id; - async move { - match params { - // todo: consider sending an empty request so we get the init response? - AnyAgentRequest::InitializeParams(_) => Ok(AnyAgentResult::InitializeResponse( - acp::InitializeResponse { - is_authenticated: true, - protocol_version: ProtocolVersion::latest(), - }, - )), - AnyAgentRequest::AuthenticateParams(_) => { - Err(anyhow!("Authentication not supported")) + Ok(thread) + }) + } + + fn authenticate(&self, _cx: &mut App) -> Task> { + Task::ready(Err(anyhow!("Authentication not supported"))) + } + + fn prompt(&self, params: acp::PromptToolArguments, cx: &mut App) -> Task> { + let sessions = self.sessions.borrow(); + let Some(session) = sessions.get(¶ms.session_id) else { + return Task::ready(Err(anyhow!( + "Attempted to send message to nonexistent session {}", + params.session_id + ))); + }; + + let (tx, rx) = oneshot::channel(); + session.end_turn_tx.borrow_mut().replace(tx); + + let mut content = String::new(); + for chunk in params.prompt { + match chunk { + acp::ContentBlock::Text(text_content) => { + content.push_str(&text_content.text); } - AnyAgentRequest::SendUserMessageParams(message) => { - delegate.clear_completed_plan_entries().await?; - - let (tx, rx) = oneshot::channel(); - end_turn_tx.borrow_mut().replace(tx); - let mut content = String::new(); - for chunk in message.chunks { - match chunk { - agentic_coding_protocol::UserMessageChunk::Text { text } => { - content.push_str(&text) - } - agentic_coding_protocol::UserMessageChunk::Path { path } => { - content.push_str(&format!("@{path:?}")) - } - } - } - outgoing_tx.unbounded_send(SdkMessage::User { - message: Message { - role: Role::User, - content: Content::UntaggedText(content), - id: None, - model: None, - stop_reason: None, - stop_sequence: None, - usage: None, - }, - session_id: Some(session_id), - })?; - rx.await??; - Ok(AnyAgentResult::SendUserMessageResponse( - acp::SendUserMessageResponse, - )) + acp::ContentBlock::ResourceLink(resource_link) => { + content.push_str(&format!("@{}", resource_link.uri)); } - AnyAgentRequest::CancelSendMessageParams(_) => { - let (done_tx, done_rx) = oneshot::channel(); - cancel_tx.send(done_tx).await?; - done_rx.await??; - - Ok(AnyAgentResult::CancelSendMessageResponse( - acp::CancelSendMessageResponse, - )) + acp::ContentBlock::Audio(_) + | acp::ContentBlock::Image(_) + | acp::ContentBlock::Resource(_) => { + // TODO } } } - .boxed_local() + + if let Err(err) = session.outgoing_tx.unbounded_send(SdkMessage::User { + message: Message { + role: Role::User, + content: Content::UntaggedText(content), + id: None, + model: None, + stop_reason: None, + stop_sequence: None, + usage: None, + }, + session_id: Some(params.session_id.to_string()), + }) { + return Task::ready(Err(anyhow!(err))); + } + + cx.foreground_executor().spawn(async move { + rx.await??; + Ok(()) + }) + } + + fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { + let sessions = self.sessions.borrow(); + let Some(session) = sessions.get(&session_id) else { + log::warn!("Attempted to cancel nonexistent session {}", session_id); + return; + }; + + let (done_tx, done_rx) = oneshot::channel(); + if session + .cancel_tx + .unbounded_send(done_tx) + .log_err() + .is_some() + { + let end_turn_tx = session.end_turn_tx.clone(); + cx.foreground_executor() + .spawn(async move { + done_rx.await??; + if let Some(end_turn_tx) = end_turn_tx.take() { + end_turn_tx.send(Ok(())).ok(); + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } } } @@ -287,7 +314,7 @@ enum ClaudeSessionMode { async fn spawn_claude( command: &AgentServerCommand, mode: ClaudeSessionMode, - session_id: Uuid, + session_id: acp::SessionId, mcp_config_path: &Path, root_dir: &Path, ) -> Result { @@ -327,88 +354,103 @@ async fn spawn_claude( Ok(child) } -struct ClaudeAgentConnection { - delegate: AcpClientDelegate, - session_id: Uuid, +struct ClaudeAgentSession { outgoing_tx: UnboundedSender, end_turn_tx: Rc>>>>, cancel_tx: UnboundedSender>>, - _mcp_server: Option, + _mcp_server: Option, _handler_task: Task<()>, } -impl ClaudeAgentConnection { +impl ClaudeAgentSession { async fn handle_message( - delegate: AcpClientDelegate, + mut thread_rx: watch::Receiver>, message: SdkMessage, end_turn_tx: Rc>>>>, - tool_id_map: Rc>>, + cx: &mut AsyncApp, ) { match message { - SdkMessage::Assistant { message, .. } | SdkMessage::User { message, .. } => { + SdkMessage::Assistant { + message, + session_id: _, + } + | 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) => { - delegate - .stream_assistant_message_chunk(StreamAssistantMessageChunkParams { - chunk: acp::AssistantMessageChunk::Text { text }, + thread + .update(cx, |thread, cx| { + thread.push_assistant_chunk(text.into(), false, cx) }) - .await .log_err(); } ContentChunk::ToolUse { id, name, input } => { let claude_tool = ClaudeTool::infer(&name, input); - if let ClaudeTool::TodoWrite(Some(params)) = claude_tool { - delegate - .update_plan(acp::UpdatePlanParams { - entries: params.todos.into_iter().map(Into::into).collect(), - }) - .await - .log_err(); - } else if let Some(resp) = delegate - .push_tool_call(claude_tool.as_acp()) - .await - .log_err() - { - tool_id_map.borrow_mut().insert(id, resp.id); - } + thread + .update(cx, |thread, cx| { + if let ClaudeTool::TodoWrite(Some(params)) = claude_tool { + thread.update_plan( + acp::Plan { + entries: params + .todos + .into_iter() + .map(Into::into) + .collect(), + }, + cx, + ) + } else { + thread.upsert_tool_call( + claude_tool.as_acp(acp::ToolCallId(id.into())), + cx, + ); + } + }) + .log_err(); } ContentChunk::ToolResult { content, tool_use_id, } => { - let id = tool_id_map.borrow_mut().remove(&tool_use_id); - if let Some(id) = id { - let content = content.to_string(); - delegate - .update_tool_call(UpdateToolCallParams { - tool_call_id: id, - status: acp::ToolCallStatus::Finished, - // Don't unset existing content - content: (!content.is_empty()).then_some( - ToolCallContent::Markdown { - // For now we only include text content - markdown: content, - }, - ), - }) - .await - .log_err(); - } + let content = content.to_string(); + thread + .update(cx, |thread, cx| { + thread.update_tool_call( + acp::ToolCallId(tool_use_id.into()), + acp::ToolCallStatus::Completed, + (!content.is_empty()).then(|| vec![content.into()]), + cx, + ) + }) + .log_err(); } ContentChunk::Image | ContentChunk::Document | ContentChunk::Thinking | ContentChunk::RedactedThinking | ContentChunk::WebSearchToolResult => { - delegate - .stream_assistant_message_chunk(StreamAssistantMessageChunkParams { - chunk: acp::AssistantMessageChunk::Text { - text: format!("Unsupported content: {:?}", chunk), - }, + thread + .update(cx, |thread, cx| { + thread.push_assistant_chunk( + format!("Unsupported content: {:?}", chunk).into(), + false, + cx, + ) }) - .await .log_err(); } } @@ -592,14 +634,14 @@ enum SdkMessage { Assistant { message: Message, // from Anthropic SDK #[serde(skip_serializing_if = "Option::is_none")] - session_id: Option, + session_id: Option, }, // A user message User { message: Message, // from Anthropic SDK #[serde(skip_serializing_if = "Option::is_none")] - session_id: Option, + session_id: Option, }, // Emitted as the last message in a conversation @@ -661,21 +703,6 @@ enum PermissionMode { Plan, } -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct McpConfig { - mcp_servers: HashMap, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct McpServerConfig { - command: String, - args: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - env: Option>, -} - #[cfg(test)] pub(crate) mod tests { use super::*; diff --git a/crates/agent_servers/src/claude/mcp_server.rs b/crates/agent_servers/src/claude/mcp_server.rs index 2405603550db376453b85261ca31b5791e464d09..0a39a02931caaa4100677b41b2daec2f127137ea 100644 --- a/crates/agent_servers/src/claude/mcp_server.rs +++ b/crates/agent_servers/src/claude/mcp_server.rs @@ -1,29 +1,22 @@ -use std::{cell::RefCell, rc::Rc}; +use std::path::PathBuf; -use acp_thread::AcpClientDelegate; -use agentic_coding_protocol::{self as acp, Client, ReadTextFileParams, WriteTextFileParams}; +use acp_thread::AcpThread; +use agent_client_protocol as acp; use anyhow::{Context, Result}; use collections::HashMap; -use context_server::{ - listener::McpServer, - types::{ - CallToolParams, CallToolResponse, Implementation, InitializeParams, InitializeResponse, - ListToolsResponse, ProtocolVersion, ServerCapabilities, Tool, ToolAnnotations, - ToolResponseContent, ToolsCapabilities, requests, - }, +use context_server::types::{ + CallToolParams, CallToolResponse, Implementation, InitializeParams, InitializeResponse, + ListToolsResponse, ProtocolVersion, ServerCapabilities, Tool, ToolAnnotations, + ToolResponseContent, ToolsCapabilities, requests, }; -use gpui::{App, AsyncApp, Task}; +use gpui::{App, AsyncApp, Entity, Task, WeakEntity}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use util::debug_panic; -use crate::claude::{ - McpServerConfig, - tools::{ClaudeTool, EditToolParams, ReadToolParams}, -}; +use crate::claude::tools::{ClaudeTool, EditToolParams, ReadToolParams}; -pub struct ClaudeMcpServer { - server: McpServer, +pub struct ClaudeZedMcpServer { + server: context_server::listener::McpServer, } pub const SERVER_NAME: &str = "zed"; @@ -52,17 +45,16 @@ enum PermissionToolBehavior { Deny, } -impl ClaudeMcpServer { +impl ClaudeZedMcpServer { pub async fn new( - delegate: watch::Receiver>, - tool_id_map: Rc>>, + thread_rx: watch::Receiver>, cx: &AsyncApp, ) -> Result { - let mut mcp_server = McpServer::new(cx).await?; + let mut mcp_server = context_server::listener::McpServer::new(cx).await?; mcp_server.handle_request::(Self::handle_initialize); mcp_server.handle_request::(Self::handle_list_tools); mcp_server.handle_request::(move |request, cx| { - Self::handle_call_tool(request, delegate.clone(), tool_id_map.clone(), cx) + Self::handle_call_tool(request, thread_rx.clone(), cx) }); Ok(Self { server: mcp_server }) @@ -70,9 +62,7 @@ impl ClaudeMcpServer { pub fn server_config(&self) -> Result { let zed_path = std::env::current_exe() - .context("finding current executable path for use in mcp_server")? - .to_string_lossy() - .to_string(); + .context("finding current executable path for use in mcp_server")?; Ok(McpServerConfig { command: zed_path, @@ -152,22 +142,19 @@ impl ClaudeMcpServer { fn handle_call_tool( request: CallToolParams, - mut delegate_watch: watch::Receiver>, - tool_id_map: Rc>>, + mut thread_rx: watch::Receiver>, cx: &App, ) -> Task> { cx.spawn(async move |cx| { - let Some(delegate) = delegate_watch.recv().await? else { - debug_panic!("Sent None delegate"); - anyhow::bail!("Server not available"); + let Some(thread) = thread_rx.recv().await?.upgrade() else { + anyhow::bail!("Thread closed"); }; if request.name.as_str() == PERMISSION_TOOL { let input = serde_json::from_value(request.arguments.context("Arguments required")?)?; - let result = - Self::handle_permissions_tool_call(input, delegate, tool_id_map, cx).await?; + let result = Self::handle_permissions_tool_call(input, thread, cx).await?; Ok(CallToolResponse { content: vec![ToolResponseContent::Text { text: serde_json::to_string(&result)?, @@ -179,7 +166,7 @@ impl ClaudeMcpServer { let input = serde_json::from_value(request.arguments.context("Arguments required")?)?; - let content = Self::handle_read_tool_call(input, delegate, cx).await?; + let content = Self::handle_read_tool_call(input, thread, cx).await?; Ok(CallToolResponse { content, is_error: None, @@ -189,7 +176,7 @@ impl ClaudeMcpServer { let input = serde_json::from_value(request.arguments.context("Arguments required")?)?; - Self::handle_edit_tool_call(input, delegate, cx).await?; + Self::handle_edit_tool_call(input, thread, cx).await?; Ok(CallToolResponse { content: vec![], is_error: None, @@ -202,49 +189,46 @@ impl ClaudeMcpServer { } fn handle_read_tool_call( - params: ReadToolParams, - delegate: AcpClientDelegate, + ReadToolParams { + abs_path, + offset, + limit, + }: ReadToolParams, + thread: Entity, cx: &AsyncApp, ) -> Task>> { - cx.foreground_executor().spawn(async move { - let response = delegate - .read_text_file(ReadTextFileParams { - path: params.abs_path, - line: params.offset, - limit: params.limit, - }) + cx.spawn(async move |cx| { + let content = thread + .update(cx, |thread, cx| { + thread.read_text_file(abs_path, offset, limit, false, cx) + })? .await?; - Ok(vec![ToolResponseContent::Text { - text: response.content, - }]) + Ok(vec![ToolResponseContent::Text { text: content }]) }) } fn handle_edit_tool_call( params: EditToolParams, - delegate: AcpClientDelegate, + thread: Entity, cx: &AsyncApp, ) -> Task> { - cx.foreground_executor().spawn(async move { - let response = delegate - .read_text_file_reusing_snapshot(ReadTextFileParams { - path: params.abs_path.clone(), - line: None, - limit: None, - }) + cx.spawn(async move |cx| { + let content = thread + .update(cx, |threads, cx| { + threads.read_text_file(params.abs_path.clone(), None, None, true, cx) + })? .await?; - let new_content = response.content.replace(¶ms.old_text, ¶ms.new_text); - if new_content == response.content { + let new_content = content.replace(¶ms.old_text, ¶ms.new_text); + if new_content == content { return Err(anyhow::anyhow!("The old_text was not found in the content")); } - delegate - .write_text_file(WriteTextFileParams { - path: params.abs_path, - content: new_content, - }) + thread + .update(cx, |threads, cx| { + threads.write_text_file(params.abs_path, new_content, cx) + })? .await?; Ok(()) @@ -253,44 +237,65 @@ impl ClaudeMcpServer { fn handle_permissions_tool_call( params: PermissionToolParams, - delegate: AcpClientDelegate, - tool_id_map: Rc>>, + thread: Entity, cx: &AsyncApp, ) -> Task> { - cx.foreground_executor().spawn(async move { + cx.spawn(async move |cx| { let claude_tool = ClaudeTool::infer(¶ms.tool_name, params.input.clone()); - let tool_call_id = match params.tool_use_id { - Some(tool_use_id) => tool_id_map - .borrow() - .get(&tool_use_id) - .cloned() - .context("Tool call ID not found")?, + let tool_call_id = + acp::ToolCallId(params.tool_use_id.context("Tool ID required")?.into()); - None => delegate.push_tool_call(claude_tool.as_acp()).await?.id, - }; + let allow_option_id = acp::PermissionOptionId("allow".into()); + let reject_option_id = acp::PermissionOptionId("reject".into()); - let outcome = delegate - .request_existing_tool_call_confirmation( - tool_call_id, - claude_tool.confirmation(None), - ) + let chosen_option = thread + .update(cx, |thread, cx| { + thread.request_tool_call_permission( + claude_tool.as_acp(tool_call_id), + vec![ + acp::PermissionOption { + id: allow_option_id.clone(), + label: "Allow".into(), + kind: acp::PermissionOptionKind::AllowOnce, + }, + acp::PermissionOption { + id: reject_option_id, + label: "Reject".into(), + kind: acp::PermissionOptionKind::RejectOnce, + }, + ], + cx, + ) + })? .await?; - match outcome { - acp::ToolCallConfirmationOutcome::Allow - | acp::ToolCallConfirmationOutcome::AlwaysAllow - | acp::ToolCallConfirmationOutcome::AlwaysAllowMcpServer - | acp::ToolCallConfirmationOutcome::AlwaysAllowTool => Ok(PermissionToolResponse { + if chosen_option == allow_option_id { + Ok(PermissionToolResponse { behavior: PermissionToolBehavior::Allow, updated_input: params.input, - }), - acp::ToolCallConfirmationOutcome::Reject - | acp::ToolCallConfirmationOutcome::Cancel => Ok(PermissionToolResponse { + }) + } else { + Ok(PermissionToolResponse { behavior: PermissionToolBehavior::Deny, updated_input: params.input, - }), + }) } }) } } + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct McpConfig { + pub mcp_servers: HashMap, +} + +#[derive(Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct McpServerConfig { + pub command: PathBuf, + pub args: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub env: Option>, +} diff --git a/crates/agent_servers/src/claude/tools.rs b/crates/agent_servers/src/claude/tools.rs index 75a26ee23096da3c03b25ae7335337e455e9d6f7..ed25f9af7f1c57375470575910aba4235ad4121d 100644 --- a/crates/agent_servers/src/claude/tools.rs +++ b/crates/agent_servers/src/claude/tools.rs @@ -1,6 +1,6 @@ use std::path::PathBuf; -use agentic_coding_protocol::{self as acp, PushToolCallParams, ToolCallLocation}; +use agent_client_protocol as acp; use itertools::Itertools; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -115,51 +115,36 @@ impl ClaudeTool { Self::Other { name, .. } => name.clone(), } } - - pub fn content(&self) -> Option { + pub fn content(&self) -> Vec { match &self { - Self::Other { input, .. } => Some(acp::ToolCallContent::Markdown { - markdown: format!( + Self::Other { input, .. } => vec![ + format!( "```json\n{}```", serde_json::to_string_pretty(&input).unwrap_or("{}".to_string()) - ), - }), - Self::Task(Some(params)) => Some(acp::ToolCallContent::Markdown { - markdown: params.prompt.clone(), - }), - Self::NotebookRead(Some(params)) => Some(acp::ToolCallContent::Markdown { - markdown: params.notebook_path.display().to_string(), - }), - Self::NotebookEdit(Some(params)) => Some(acp::ToolCallContent::Markdown { - markdown: params.new_source.clone(), - }), - Self::Terminal(Some(params)) => Some(acp::ToolCallContent::Markdown { - markdown: format!( + ) + .into(), + ], + Self::Task(Some(params)) => vec![params.prompt.clone().into()], + Self::NotebookRead(Some(params)) => { + vec![params.notebook_path.display().to_string().into()] + } + Self::NotebookEdit(Some(params)) => vec![params.new_source.clone().into()], + Self::Terminal(Some(params)) => vec![ + format!( "`{}`\n\n{}", params.command, params.description.as_deref().unwrap_or_default() - ), - }), - Self::ReadFile(Some(params)) => Some(acp::ToolCallContent::Markdown { - markdown: params.abs_path.display().to_string(), - }), - Self::Ls(Some(params)) => Some(acp::ToolCallContent::Markdown { - markdown: params.path.display().to_string(), - }), - Self::Glob(Some(params)) => Some(acp::ToolCallContent::Markdown { - markdown: params.to_string(), - }), - Self::Grep(Some(params)) => Some(acp::ToolCallContent::Markdown { - markdown: format!("`{params}`"), - }), - Self::WebFetch(Some(params)) => Some(acp::ToolCallContent::Markdown { - markdown: params.prompt.clone(), - }), - Self::WebSearch(Some(params)) => Some(acp::ToolCallContent::Markdown { - markdown: params.to_string(), - }), - Self::TodoWrite(Some(params)) => Some(acp::ToolCallContent::Markdown { - markdown: params + ) + .into(), + ], + Self::ReadFile(Some(params)) => vec![params.abs_path.display().to_string().into()], + Self::Ls(Some(params)) => vec![params.path.display().to_string().into()], + Self::Glob(Some(params)) => vec![params.to_string().into()], + 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| { @@ -174,34 +159,39 @@ impl ClaudeTool { todo.content ) }) - .join("\n"), - }), - Self::ExitPlanMode(Some(params)) => Some(acp::ToolCallContent::Markdown { - markdown: params.plan.clone(), - }), - Self::Edit(Some(params)) => Some(acp::ToolCallContent::Diff { + .join("\n") + .into(), + ], + Self::ExitPlanMode(Some(params)) => vec![params.plan.clone().into()], + Self::Edit(Some(params)) => vec![acp::ToolCallContent::Diff { diff: acp::Diff { path: params.abs_path.clone(), old_text: Some(params.old_text.clone()), new_text: params.new_text.clone(), }, - }), - Self::Write(Some(params)) => Some(acp::ToolCallContent::Diff { + }], + Self::Write(Some(params)) => vec![acp::ToolCallContent::Diff { diff: acp::Diff { path: params.file_path.clone(), old_text: None, new_text: params.content.clone(), }, - }), + }], Self::MultiEdit(Some(params)) => { // todo: show multiple edits in a multibuffer? - params.edits.first().map(|edit| acp::ToolCallContent::Diff { - diff: acp::Diff { - path: params.file_path.clone(), - old_text: Some(edit.old_string.clone()), - new_text: edit.new_string.clone(), - }, - }) + params + .edits + .first() + .map(|edit| { + vec![acp::ToolCallContent::Diff { + diff: acp::Diff { + path: params.file_path.clone(), + old_text: Some(edit.old_string.clone()), + new_text: edit.new_string.clone(), + }, + }] + }) + .unwrap_or_default() } Self::Task(None) | Self::NotebookRead(None) @@ -217,181 +207,80 @@ impl ClaudeTool { | Self::ExitPlanMode(None) | Self::Edit(None) | Self::Write(None) - | Self::MultiEdit(None) => None, + | Self::MultiEdit(None) => vec![], } } - pub fn icon(&self) -> acp::Icon { + pub fn kind(&self) -> acp::ToolKind { match self { - Self::Task(_) => acp::Icon::Hammer, - Self::NotebookRead(_) => acp::Icon::FileSearch, - Self::NotebookEdit(_) => acp::Icon::Pencil, - Self::Edit(_) => acp::Icon::Pencil, - Self::MultiEdit(_) => acp::Icon::Pencil, - Self::Write(_) => acp::Icon::Pencil, - Self::ReadFile(_) => acp::Icon::FileSearch, - Self::Ls(_) => acp::Icon::Folder, - Self::Glob(_) => acp::Icon::FileSearch, - Self::Grep(_) => acp::Icon::Regex, - Self::Terminal(_) => acp::Icon::Terminal, - Self::WebSearch(_) => acp::Icon::Globe, - Self::WebFetch(_) => acp::Icon::Globe, - Self::TodoWrite(_) => acp::Icon::LightBulb, - Self::ExitPlanMode(_) => acp::Icon::Hammer, - Self::Other { .. } => acp::Icon::Hammer, - } - } - - pub fn confirmation(&self, description: Option) -> acp::ToolCallConfirmation { - match &self { - Self::Edit(_) | Self::Write(_) | Self::NotebookEdit(_) | Self::MultiEdit(_) => { - acp::ToolCallConfirmation::Edit { description } - } - Self::WebFetch(params) => acp::ToolCallConfirmation::Fetch { - urls: params - .as_ref() - .map(|p| vec![p.url.clone()]) - .unwrap_or_default(), - description, - }, - Self::Terminal(Some(BashToolParams { - description, - command, - .. - })) => acp::ToolCallConfirmation::Execute { - command: command.clone(), - root_command: command.clone(), - description: description.clone(), - }, - Self::ExitPlanMode(Some(params)) => acp::ToolCallConfirmation::Other { - description: if let Some(description) = description { - format!("{description} {}", params.plan) - } else { - params.plan.clone() - }, - }, - Self::Task(Some(params)) => acp::ToolCallConfirmation::Other { - description: if let Some(description) = description { - format!("{description} {}", params.description) - } else { - params.description.clone() - }, - }, - Self::Ls(Some(LsToolParams { path, .. })) - | Self::ReadFile(Some(ReadToolParams { abs_path: path, .. })) => { - let path = path.display(); - acp::ToolCallConfirmation::Other { - description: if let Some(description) = description { - format!("{description} {path}") - } else { - path.to_string() - }, - } - } - Self::NotebookRead(Some(NotebookReadToolParams { notebook_path, .. })) => { - let path = notebook_path.display(); - acp::ToolCallConfirmation::Other { - description: if let Some(description) = description { - format!("{description} {path}") - } else { - path.to_string() - }, - } - } - Self::Glob(Some(params)) => acp::ToolCallConfirmation::Other { - description: if let Some(description) = description { - format!("{description} {params}") - } else { - params.to_string() - }, - }, - Self::Grep(Some(params)) => acp::ToolCallConfirmation::Other { - description: if let Some(description) = description { - format!("{description} {params}") - } else { - params.to_string() - }, - }, - Self::WebSearch(Some(params)) => acp::ToolCallConfirmation::Other { - description: if let Some(description) = description { - format!("{description} {params}") - } else { - params.to_string() - }, - }, - Self::TodoWrite(Some(params)) => { - let params = params.todos.iter().map(|todo| &todo.content).join(", "); - acp::ToolCallConfirmation::Other { - description: if let Some(description) = description { - format!("{description} {params}") - } else { - params - }, - } - } - Self::Terminal(None) - | Self::Task(None) - | Self::NotebookRead(None) - | Self::ExitPlanMode(None) - | Self::Ls(None) - | Self::Glob(None) - | Self::Grep(None) - | Self::ReadFile(None) - | Self::WebSearch(None) - | Self::TodoWrite(None) - | Self::Other { .. } => acp::ToolCallConfirmation::Other { - description: description.unwrap_or("".to_string()), - }, + Self::Task(_) => acp::ToolKind::Think, + Self::NotebookRead(_) => acp::ToolKind::Read, + Self::NotebookEdit(_) => acp::ToolKind::Edit, + Self::Edit(_) => acp::ToolKind::Edit, + Self::MultiEdit(_) => acp::ToolKind::Edit, + Self::Write(_) => acp::ToolKind::Edit, + Self::ReadFile(_) => acp::ToolKind::Read, + Self::Ls(_) => acp::ToolKind::Search, + Self::Glob(_) => acp::ToolKind::Search, + Self::Grep(_) => acp::ToolKind::Search, + Self::Terminal(_) => acp::ToolKind::Execute, + Self::WebSearch(_) => acp::ToolKind::Search, + Self::WebFetch(_) => acp::ToolKind::Fetch, + Self::TodoWrite(_) => acp::ToolKind::Think, + Self::ExitPlanMode(_) => acp::ToolKind::Think, + Self::Other { .. } => acp::ToolKind::Other, } } pub fn locations(&self) -> Vec { match &self { - Self::Edit(Some(EditToolParams { abs_path, .. })) => vec![ToolCallLocation { + Self::Edit(Some(EditToolParams { abs_path, .. })) => vec![acp::ToolCallLocation { path: abs_path.clone(), line: None, }], Self::MultiEdit(Some(MultiEditToolParams { file_path, .. })) => { - vec![ToolCallLocation { + vec![acp::ToolCallLocation { + path: file_path.clone(), + line: None, + }] + } + Self::Write(Some(WriteToolParams { file_path, .. })) => { + vec![acp::ToolCallLocation { path: file_path.clone(), line: None, }] } - Self::Write(Some(WriteToolParams { file_path, .. })) => vec![ToolCallLocation { - path: file_path.clone(), - line: None, - }], Self::ReadFile(Some(ReadToolParams { abs_path, offset, .. - })) => vec![ToolCallLocation { + })) => vec![acp::ToolCallLocation { path: abs_path.clone(), line: *offset, }], Self::NotebookRead(Some(NotebookReadToolParams { notebook_path, .. })) => { - vec![ToolCallLocation { + vec![acp::ToolCallLocation { path: notebook_path.clone(), line: None, }] } Self::NotebookEdit(Some(NotebookEditToolParams { notebook_path, .. })) => { - vec![ToolCallLocation { + vec![acp::ToolCallLocation { path: notebook_path.clone(), line: None, }] } Self::Glob(Some(GlobToolParams { path: Some(path), .. - })) => vec![ToolCallLocation { + })) => vec![acp::ToolCallLocation { path: path.clone(), line: None, }], - Self::Ls(Some(LsToolParams { path, .. })) => vec![ToolCallLocation { + Self::Ls(Some(LsToolParams { path, .. })) => vec![acp::ToolCallLocation { path: path.clone(), line: None, }], Self::Grep(Some(GrepToolParams { path: Some(path), .. - })) => vec![ToolCallLocation { + })) => vec![acp::ToolCallLocation { path: PathBuf::from(path), line: None, }], @@ -414,11 +303,13 @@ impl ClaudeTool { } } - pub fn as_acp(&self) -> PushToolCallParams { - PushToolCallParams { + pub fn as_acp(&self, id: acp::ToolCallId) -> acp::ToolCall { + acp::ToolCall { + id, + kind: self.kind(), + status: acp::ToolCallStatus::InProgress, label: self.label(), content: self.content(), - icon: self.icon(), locations: self.locations(), } } diff --git a/crates/agent_servers/src/e2e_tests.rs b/crates/agent_servers/src/e2e_tests.rs index 12f74cb13e41fea5c313729edf857173af94f74e..9bc6fd60fe5b99dc41120131d8e5415008beae51 100644 --- a/crates/agent_servers/src/e2e_tests.rs +++ b/crates/agent_servers/src/e2e_tests.rs @@ -1,10 +1,9 @@ use std::{path::Path, sync::Arc, time::Duration}; use crate::{AgentServer, AgentServerSettings, AllAgentServersSettings}; -use acp_thread::{ - AcpThread, AgentThreadEntry, ToolCall, ToolCallConfirmation, ToolCallContent, ToolCallStatus, -}; -use agentic_coding_protocol as acp; +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 indoc::indoc; @@ -54,19 +53,25 @@ pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut Tes thread .update(cx, |thread, cx| { thread.send( - acp::SendUserMessageParams { - chunks: vec![ - acp::UserMessageChunk::Text { - text: "Read the file ".into(), - }, - acp::UserMessageChunk::Path { - path: Path::new("foo.rs").into(), - }, - acp::UserMessageChunk::Text { - text: " and tell me what the content of the println! is".into(), - }, - ], - }, + vec![ + acp::ContentBlock::Text(acp::TextContent { + text: "Read the file ".into(), + annotations: None, + }), + acp::ContentBlock::ResourceLink(acp::ResourceLink { + uri: "foo.rs".into(), + name: "foo.rs".into(), + annotations: None, + description: None, + mime_type: None, + size: None, + title: None, + }), + acp::ContentBlock::Text(acp::TextContent { + text: " and tell me what the content of the println! is".into(), + annotations: None, + }), + ], cx, ) }) @@ -161,11 +166,8 @@ pub async fn test_tool_call_with_confirmation( let tool_call_id = thread.read_with(cx, |thread, _cx| { let AgentThreadEntry::ToolCall(ToolCall { id, - status: - ToolCallStatus::WaitingForConfirmation { - confirmation: ToolCallConfirmation::Execute { root_command, .. }, - .. - }, + content, + status: ToolCallStatus::WaitingForConfirmation { .. }, .. }) = &thread .entries() @@ -176,13 +178,18 @@ pub async fn test_tool_call_with_confirmation( panic!(); }; - assert!(root_command.contains("touch")); + assert!(content.iter().any(|c| c.to_markdown(_cx).contains("touch"))); - *id + id.clone() }); thread.update(cx, |thread, cx| { - thread.authorize_tool_call(tool_call_id, acp::ToolCallConfirmationOutcome::Allow, cx); + thread.authorize_tool_call( + tool_call_id, + acp::PermissionOptionId("0".into()), + acp::PermissionOptionKind::AllowOnce, + cx, + ); assert!(thread.entries().iter().any(|entry| matches!( entry, @@ -197,7 +204,7 @@ pub async fn test_tool_call_with_confirmation( thread.read_with(cx, |thread, cx| { let AgentThreadEntry::ToolCall(ToolCall { - content: Some(ToolCallContent::Markdown { markdown }), + content, status: ToolCallStatus::Allowed { .. }, .. }) = thread @@ -209,13 +216,10 @@ pub async fn test_tool_call_with_confirmation( panic!(); }; - markdown.read_with(cx, |md, _cx| { - assert!( - md.source().contains("Hello"), - r#"Expected '{}' to contain "Hello""#, - md.source() - ); - }); + assert!( + content.iter().any(|c| c.to_markdown(cx).contains("Hello")), + "Expected content to contain 'Hello'" + ); }); } @@ -249,26 +253,20 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon thread.read_with(cx, |thread, _cx| { let AgentThreadEntry::ToolCall(ToolCall { id, - status: - ToolCallStatus::WaitingForConfirmation { - confirmation: ToolCallConfirmation::Execute { root_command, .. }, - .. - }, + content, + status: ToolCallStatus::WaitingForConfirmation { .. }, .. }) = &thread.entries()[first_tool_call_ix] else { panic!("{:?}", thread.entries()[1]); }; - assert!(root_command.contains("touch")); + assert!(content.iter().any(|c| c.to_markdown(_cx).contains("touch"))); - *id + id.clone() }); - thread - .update(cx, |thread, cx| thread.cancel(cx)) - .await - .unwrap(); + let _ = thread.update(cx, |thread, cx| thread.cancel(cx)); full_turn.await.unwrap(); thread.read_with(cx, |thread, _| { let AgentThreadEntry::ToolCall(ToolCall { @@ -369,15 +367,16 @@ pub async fn new_test_thread( current_dir: impl AsRef, cx: &mut TestAppContext, ) -> Entity { - let thread = cx - .update(|cx| server.new_thread(current_dir.as_ref(), &project, cx)) + let connection = cx + .update(|cx| server.connect(current_dir.as_ref(), &project, cx)) .await .unwrap(); - thread - .update(cx, |thread, _| thread.initialize()) + let thread = connection + .new_thread(project.clone(), current_dir.as_ref(), &mut cx.to_async()) .await .unwrap(); + thread } diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs index 8ad147cbffb2ce0a1881ccbb52aad135d4d79dc6..47b965cdada579019364f8798abb3c0baef691ff 100644 --- a/crates/agent_servers/src/gemini.rs +++ b/crates/agent_servers/src/gemini.rs @@ -1,9 +1,17 @@ -use crate::stdio_agent_server::StdioAgentServer; -use crate::{AgentServerCommand, AgentServerVersion}; +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::{AsyncApp, Entity}; +use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity}; use project::Project; use settings::SettingsStore; +use ui::App; use crate::AllAgentServersSettings; @@ -12,7 +20,7 @@ pub struct Gemini; const ACP_ARG: &str = "--experimental-acp"; -impl StdioAgentServer for Gemini { +impl AgentServer for Gemini { fn name(&self) -> &'static str { "Gemini" } @@ -25,14 +33,88 @@ impl StdioAgentServer for Gemini { "Ask questions, edit files, run commands.\nBe specific for the best results." } - fn supports_always_allow(&self) -> bool { - true - } - fn logo(&self) -> ui::IconName { ui::IconName::AiGemini } + fn connect( + &self, + root_dir: &Path, + 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(); + + cx.spawn(async move |cx| { + let command = this.command(&project, cx).await?; + + 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) => { + 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, + }); + + Ok(connection) + }) + } +} + +impl Gemini { async fn command( &self, project: &Entity, diff --git a/crates/agent_servers/src/stdio_agent_server.rs b/crates/agent_servers/src/stdio_agent_server.rs deleted file mode 100644 index e60dd39de45925223196f43fcb6025c49281c4c9..0000000000000000000000000000000000000000 --- a/crates/agent_servers/src/stdio_agent_server.rs +++ /dev/null @@ -1,119 +0,0 @@ -use crate::{AgentServer, AgentServerCommand, AgentServerVersion}; -use acp_thread::{AcpClientDelegate, AcpThread, LoadError}; -use agentic_coding_protocol as acp; -use anyhow::{Result, anyhow}; -use gpui::{App, AsyncApp, Entity, Task, prelude::*}; -use project::Project; -use std::path::Path; -use util::ResultExt; - -pub trait StdioAgentServer: Send + Clone { - 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 supports_always_allow(&self) -> bool; - - fn command( - &self, - project: &Entity, - cx: &mut AsyncApp, - ) -> impl Future>; - - fn version( - &self, - command: &AgentServerCommand, - ) -> impl Future> + Send; -} - -impl AgentServer for T { - fn name(&self) -> &'static str { - self.name() - } - - fn empty_state_headline(&self) -> &'static str { - self.empty_state_headline() - } - - fn empty_state_message(&self) -> &'static str { - self.empty_state_message() - } - - fn logo(&self) -> ui::IconName { - self.logo() - } - - fn supports_always_allow(&self) -> bool { - self.supports_always_allow() - } - - fn new_thread( - &self, - root_dir: &Path, - project: &Entity, - cx: &mut App, - ) -> Task>> { - let root_dir = root_dir.to_path_buf(); - let project = project.clone(); - let this = self.clone(); - let title = self.name().into(); - - cx.spawn(async move |cx| { - let command = this.command(&project, cx).await?; - - 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(); - - cx.new(|cx| { - let foreground_executor = cx.foreground_executor().clone(); - - let (connection, io_fut) = acp::AgentConnection::connect_to_agent( - AcpClientDelegate::new(cx.entity().downgrade(), cx.to_async()), - 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 - }); - - AcpThread::new(connection, title, Some(child_status), project.clone(), cx) - }) - }) - } -} diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index 7d3b84e42e682cd1aa824f99adf9255df5f75124..fbd53e8d09e5d8a74c2fca4d34e36cb95fc58192 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -17,10 +17,10 @@ test-support = ["gpui/test-support", "language/test-support"] [dependencies] acp_thread.workspace = true +agent-client-protocol.workspace = true agent.workspace = true -agentic-coding-protocol.workspace = true -agent_settings.workspace = true agent_servers.workspace = true +agent_settings.workspace = true ai_onboarding.workspace = true anyhow.workspace = true assistant_context.workspace = true diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 95f4f81205f198f270eb5d4dec6597f9b04efd2f..7f5de9db5f2c8f7b900a698193f0599e6c7270a2 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -1,4 +1,4 @@ -use acp_thread::Plan; +use acp_thread::{AgentConnection, Plan}; use agent_servers::AgentServer; use std::cell::RefCell; use std::collections::BTreeMap; @@ -7,7 +7,7 @@ use std::rc::Rc; use std::sync::Arc; use std::time::Duration; -use agentic_coding_protocol::{self as acp}; +use agent_client_protocol as acp; use assistant_tool::ActionLog; use buffer_diff::BufferDiff; use collections::{HashMap, HashSet}; @@ -16,7 +16,6 @@ use editor::{ EditorStyle, MinimapVisibility, MultiBuffer, PathKey, }; use file_icons::FileIcons; -use futures::channel::oneshot; use gpui::{ Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId, FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, SharedString, StyleRefinement, @@ -39,8 +38,7 @@ use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage}; use ::acp_thread::{ AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk, Diff, - LoadError, MentionPath, ThreadStatus, ToolCall, ToolCallConfirmation, ToolCallContent, - ToolCallId, ToolCallStatus, + LoadError, MentionPath, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus, }; use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet}; @@ -64,12 +62,13 @@ pub struct AcpThreadView { last_error: Option>, list_state: ListState, auth_task: Option>, - expanded_tool_calls: HashSet, + expanded_tool_calls: HashSet, expanded_thinking_blocks: HashSet<(usize, usize)>, edits_expanded: bool, plan_expanded: bool, editor_expanded: bool, - message_history: Rc>>, + message_history: Rc>>>, + _cancel_task: Option>, } enum ThreadState { @@ -82,22 +81,16 @@ enum ThreadState { }, LoadError(LoadError), Unauthenticated { - thread: Entity, + connection: Rc, }, } -struct AlwaysAllowOption { - id: &'static str, - label: SharedString, - outcome: acp::ToolCallConfirmationOutcome, -} - impl AcpThreadView { pub fn new( agent: Rc, workspace: WeakEntity, project: Entity, - message_history: Rc>>, + message_history: Rc>>>, min_lines: usize, max_lines: Option, window: &mut Window, @@ -191,6 +184,7 @@ impl AcpThreadView { plan_expanded: false, editor_expanded: false, message_history, + _cancel_task: None, } } @@ -208,9 +202,9 @@ impl AcpThreadView { .map(|worktree| worktree.read(cx).abs_path()) .unwrap_or_else(|| paths::home_dir().as_path().into()); - let task = agent.new_thread(&root_dir, &project, cx); + let connect_task = agent.connect(&root_dir, &project, cx); let load_task = cx.spawn_in(window, async move |this, cx| { - let thread = match task.await { + let connection = match connect_task.await { Ok(thread) => thread, Err(err) => { this.update(cx, |this, cx| { @@ -222,48 +216,30 @@ impl AcpThreadView { } }; - let init_response = async { - let resp = thread - .read_with(cx, |thread, _cx| thread.initialize())? - .await?; - anyhow::Ok(resp) - }; - - let result = match init_response.await { + let result = match connection + .clone() + .new_thread(project.clone(), &root_dir, cx) + .await + { Err(e) => { let mut cx = cx.clone(); - if e.downcast_ref::().is_some() { - let child_status = thread - .update(&mut cx, |thread, _| thread.child_status()) - .ok() - .flatten(); - if let Some(child_status) = child_status { - match child_status.await { - Ok(_) => Err(e), - Err(e) => Err(e), - } - } else { - Err(e) - } - } else { - Err(e) - } - } - Ok(response) => { - if !response.is_authenticated { - this.update(cx, |this, _| { - this.thread_state = ThreadState::Unauthenticated { thread }; + if e.downcast_ref::().is_some() { + this.update(&mut cx, |this, cx| { + this.thread_state = ThreadState::Unauthenticated { connection }; + cx.notify(); }) .ok(); return; - }; - Ok(()) + } else { + Err(e) + } } + Ok(session_id) => Ok(session_id), }; this.update_in(cx, |this, window, cx| { match result { - Ok(()) => { + Ok(thread) => { let thread_subscription = cx.subscribe_in(&thread, window, Self::handle_thread_event); @@ -305,10 +281,10 @@ impl AcpThreadView { pub fn thread(&self) -> Option<&Entity> { match &self.thread_state { - ThreadState::Ready { thread, .. } | ThreadState::Unauthenticated { thread } => { - Some(thread) - } - ThreadState::Loading { .. } | ThreadState::LoadError(..) => None, + ThreadState::Ready { thread, .. } => Some(thread), + ThreadState::Unauthenticated { .. } + | ThreadState::Loading { .. } + | ThreadState::LoadError(..) => None, } } @@ -325,7 +301,7 @@ impl AcpThreadView { self.last_error.take(); if let Some(thread) = self.thread() { - thread.update(cx, |thread, cx| thread.cancel(cx)).detach(); + self._cancel_task = Some(thread.update(cx, |thread, cx| thread.cancel(cx))); } } @@ -362,7 +338,7 @@ impl AcpThreadView { self.last_error.take(); let mut ix = 0; - let mut chunks: Vec = Vec::new(); + let mut chunks: Vec = Vec::new(); let project = self.project.clone(); self.message_editor.update(cx, |editor, cx| { let text = editor.text(cx); @@ -374,12 +350,19 @@ impl AcpThreadView { { let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot); if crease_range.start > ix { - chunks.push(acp::UserMessageChunk::Text { - text: text[ix..crease_range.start].to_string(), - }); + chunks.push(text[ix..crease_range.start].into()); } if let Some(abs_path) = project.read(cx).absolute_path(&project_path, cx) { - chunks.push(acp::UserMessageChunk::Path { path: abs_path }); + 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, + })); } ix = crease_range.end; } @@ -388,9 +371,7 @@ impl AcpThreadView { if ix < text.len() { let last_chunk = text[ix..].trim(); if !last_chunk.is_empty() { - chunks.push(acp::UserMessageChunk::Text { - text: last_chunk.into(), - }); + chunks.push(last_chunk.into()); } } }) @@ -401,8 +382,7 @@ impl AcpThreadView { } let Some(thread) = self.thread() else { return }; - let message = acp::SendUserMessageParams { chunks }; - let task = thread.update(cx, |thread, cx| thread.send(message.clone(), cx)); + let task = thread.update(cx, |thread, cx| thread.send(chunks.clone(), cx)); cx.spawn(async move |this, cx| { let result = task.await; @@ -424,7 +404,7 @@ impl AcpThreadView { editor.remove_creases(mention_set.lock().drain(), cx) }); - self.message_history.borrow_mut().push(message); + self.message_history.borrow_mut().push(chunks); } fn previous_history_message( @@ -490,7 +470,7 @@ impl AcpThreadView { message_editor: Entity, mention_set: Arc>, project: Entity, - message: Option<&acp::SendUserMessageParams>, + message: Option<&Vec>, window: &mut Window, cx: &mut Context, ) -> bool { @@ -503,18 +483,19 @@ impl AcpThreadView { let mut text = String::new(); let mut mentions = Vec::new(); - for chunk in &message.chunks { + for chunk in message { match chunk { - acp::UserMessageChunk::Text { text: chunk } => { - text.push_str(&chunk); + acp::ContentBlock::Text(text_content) => { + text.push_str(&text_content.text); } - acp::UserMessageChunk::Path { path } => { + acp::ContentBlock::ResourceLink(resource_link) => { + let path = Path::new(&resource_link.uri); let start = text.len(); - let content = MentionPath::new(path).to_string(); + let content = MentionPath::new(&path).to_string(); text.push_str(&content); let end = text.len(); if let Some(project_path) = - project.read(cx).project_path_for_absolute_path(path, cx) + project.read(cx).project_path_for_absolute_path(&path, cx) { let filename: SharedString = path .file_name() @@ -525,6 +506,9 @@ impl AcpThreadView { mentions.push((start..end, project_path, filename)); } } + acp::ContentBlock::Image(_) + | acp::ContentBlock::Audio(_) + | acp::ContentBlock::Resource(_) => {} } } @@ -590,71 +574,79 @@ impl AcpThreadView { window: &mut Window, cx: &mut Context, ) { - let Some(multibuffer) = self.entry_diff_multibuffer(entry_ix, cx) else { + let Some(multibuffers) = self.entry_diff_multibuffers(entry_ix, cx) else { return; }; - if self.diff_editors.contains_key(&multibuffer.entity_id()) { - return; - } + let multibuffers = multibuffers.collect::>(); - 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(TextStyleRefinement { - font_size: Some( - TextSize::Small - .rems(cx) - .to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx)) - .into(), - ), - ..Default::default() + 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(TextStyleRefinement { + font_size: Some( + TextSize::Small + .rems(cx) + .to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx)) + .into(), + ), + ..Default::default() + }); + editor }); - editor - }); - let entity_id = multibuffer.entity_id(); - cx.observe_release(&multibuffer, move |this, _, _| { - this.diff_editors.remove(&entity_id); - }) - .detach(); + 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); + self.diff_editors.insert(entity_id, editor); + } } - fn entry_diff_multibuffer(&self, entry_ix: usize, cx: &App) -> Option> { + fn entry_diff_multibuffers( + &self, + entry_ix: usize, + cx: &App, + ) -> Option>> { let entry = self.thread()?.read(cx).entries().get(entry_ix)?; - entry.diff().map(|diff| diff.multibuffer.clone()) + Some(entry.diffs().map(|diff| diff.multibuffer.clone())) } fn authenticate(&mut self, window: &mut Window, cx: &mut Context) { - let Some(thread) = self.thread().cloned() else { + let ThreadState::Unauthenticated { ref connection } = self.thread_state else { return; }; self.last_error.take(); - let authenticate = thread.read(cx).authenticate(); + let authenticate = connection.authenticate(cx); self.auth_task = Some(cx.spawn_in(window, { let project = self.project.clone(); let agent = self.agent.clone(); @@ -684,15 +676,16 @@ impl AcpThreadView { fn authorize_tool_call( &mut self, - id: ToolCallId, - outcome: acp::ToolCallConfirmationOutcome, + tool_call_id: acp::ToolCallId, + option_id: acp::PermissionOptionId, + option_kind: acp::PermissionOptionKind, cx: &mut Context, ) { let Some(thread) = self.thread() else { return; }; thread.update(cx, |thread, cx| { - thread.authorize_tool_call(id, outcome, cx); + thread.authorize_tool_call(tool_call_id, option_id, option_kind, cx); }); cx.notify(); } @@ -719,10 +712,12 @@ impl AcpThreadView { .border_1() .border_color(cx.theme().colors().border) .text_xs() - .child(self.render_markdown( - message.content.clone(), - user_message_markdown_style(window, cx), - )), + .children(message.content.markdown().map(|md| { + self.render_markdown( + md.clone(), + user_message_markdown_style(window, cx), + ) + })), ) .into_any(), AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => { @@ -730,20 +725,28 @@ impl AcpThreadView { let message_body = v_flex() .w_full() .gap_2p5() - .children(chunks.iter().enumerate().map(|(chunk_ix, chunk)| { - match chunk { - AssistantMessageChunk::Text { chunk } => self - .render_markdown(chunk.clone(), style.clone()) - .into_any_element(), - AssistantMessageChunk::Thought { chunk } => self.render_thinking_block( - index, - chunk_ix, - chunk.clone(), - window, - cx, - ), - } - })) + .children(chunks.iter().enumerate().filter_map( + |(chunk_ix, chunk)| match chunk { + AssistantMessageChunk::Message { block } => { + block.markdown().map(|md| { + self.render_markdown(md.clone(), style.clone()) + .into_any_element() + }) + } + AssistantMessageChunk::Thought { block } => { + block.markdown().map(|md| { + self.render_thinking_block( + index, + chunk_ix, + md.clone(), + window, + cx, + ) + .into_any_element() + }) + } + }, + )) .into_any(); v_flex() @@ -871,7 +874,7 @@ impl AcpThreadView { let status_icon = match &tool_call.status { ToolCallStatus::WaitingForConfirmation { .. } => None, ToolCallStatus::Allowed { - status: acp::ToolCallStatus::Running, + status: acp::ToolCallStatus::InProgress, .. } => Some( Icon::new(IconName::ArrowCircle) @@ -885,13 +888,13 @@ impl AcpThreadView { .into_any(), ), ToolCallStatus::Allowed { - status: acp::ToolCallStatus::Finished, + status: acp::ToolCallStatus::Completed, .. } => None, ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Allowed { - status: acp::ToolCallStatus::Error, + status: acp::ToolCallStatus::Failed, .. } => Some( Icon::new(IconName::X) @@ -909,34 +912,9 @@ impl AcpThreadView { .any(|content| matches!(content, ToolCallContent::Diff { .. })), }; - let is_collapsible = tool_call.content.is_some() && !needs_confirmation; + let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation; let is_open = !is_collapsible || self.expanded_tool_calls.contains(&tool_call.id); - let content = if is_open { - match &tool_call.status { - ToolCallStatus::WaitingForConfirmation { confirmation, .. } => { - Some(self.render_tool_call_confirmation( - tool_call.id, - confirmation, - tool_call.content.as_ref(), - window, - cx, - )) - } - ToolCallStatus::Allowed { .. } | ToolCallStatus::Canceled => { - tool_call.content.as_ref().map(|content| { - div() - .py_1p5() - .child(self.render_tool_call_content(content, window, cx)) - .into_any_element() - }) - } - ToolCallStatus::Rejected => None, - } - } else { - None - }; - v_flex() .when(needs_confirmation, |this| { this.rounded_lg() @@ -976,9 +954,17 @@ impl AcpThreadView { }) .gap_1p5() .child( - Icon::new(tool_call.icon) - .size(IconSize::Small) - .color(Color::Muted), + Icon::new(match tool_call.kind { + acp::ToolKind::Read => IconName::ToolRead, + acp::ToolKind::Edit => IconName::ToolPencil, + 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), ) .child(if tool_call.locations.len() == 1 { let name = tool_call.locations[0] @@ -1023,16 +1009,16 @@ impl AcpThreadView { .gap_0p5() .when(is_collapsible, |this| { this.child( - Disclosure::new(("expand", tool_call.id.0), is_open) + Disclosure::new(("expand", entry_ix), is_open) .opened_icon(IconName::ChevronUp) .closed_icon(IconName::ChevronDown) .on_click(cx.listener({ - let id = tool_call.id; + 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); + this.expanded_tool_calls.insert(id.clone()); } cx.notify(); } @@ -1042,12 +1028,12 @@ impl AcpThreadView { .children(status_icon), ) .on_click(cx.listener({ - let id = tool_call.id; + 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); + this.expanded_tool_calls.insert(id.clone()); } cx.notify(); } @@ -1055,7 +1041,7 @@ impl AcpThreadView { ) .when(is_open, |this| { this.child( - div() + v_flex() .text_xs() .when(is_collapsible, |this| { this.mt_1() @@ -1064,7 +1050,44 @@ impl AcpThreadView { .bg(cx.theme().colors().editor_background) .rounded_lg() }) - .children(content), + .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, window, cx, + ), + ) + .into_any_element() + })) + .child(self.render_permission_buttons( + options, + entry_ix, + tool_call.id.clone(), + cx, + )), + ToolCallStatus::Allowed { .. } | ToolCallStatus::Canceled => { + this.children(tool_call.content.iter().map(|content| { + div() + .py_1p5() + .child( + self.render_tool_call_content( + content, window, cx, + ), + ) + .into_any_element() + })) + } + ToolCallStatus::Rejected => this, + } + } else { + this + } + }), ) }) } @@ -1076,14 +1099,20 @@ impl AcpThreadView { cx: &Context, ) -> AnyElement { match content { - ToolCallContent::Markdown { markdown } => { - div() - .p_2() - .child(self.render_markdown( - markdown.clone(), - default_markdown_style(false, window, cx), - )) - .into_any_element() + 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() + } else { + Empty.into_any_element() + } } ToolCallContent::Diff { diff: Diff { multibuffer, .. }, @@ -1092,223 +1121,53 @@ impl AcpThreadView { } } - fn render_tool_call_confirmation( + fn render_permission_buttons( &self, - tool_call_id: ToolCallId, - confirmation: &ToolCallConfirmation, - content: Option<&ToolCallContent>, - window: &Window, - cx: &Context, - ) -> AnyElement { - let confirmation_container = v_flex().mt_1().py_1p5(); - - match confirmation { - ToolCallConfirmation::Edit { description } => confirmation_container - .child( - div() - .px_2() - .children(description.clone().map(|description| { - self.render_markdown( - description, - default_markdown_style(false, window, cx), - ) - })), - ) - .children(content.map(|content| self.render_tool_call_content(content, window, cx))) - .child(self.render_confirmation_buttons( - &[AlwaysAllowOption { - id: "always_allow", - label: "Always Allow Edits".into(), - outcome: acp::ToolCallConfirmationOutcome::AlwaysAllow, - }], - tool_call_id, - cx, - )) - .into_any(), - ToolCallConfirmation::Execute { - command, - root_command, - description, - } => confirmation_container - .child(v_flex().px_2().pb_1p5().child(command.clone()).children( - description.clone().map(|description| { - self.render_markdown(description, default_markdown_style(false, window, cx)) - .on_url_click({ - let workspace = self.workspace.clone(); - move |text, window, cx| { - Self::open_link(text, &workspace, window, cx); - } - }) - }), - )) - .children(content.map(|content| self.render_tool_call_content(content, window, cx))) - .child(self.render_confirmation_buttons( - &[AlwaysAllowOption { - id: "always_allow", - label: format!("Always Allow {root_command}").into(), - outcome: acp::ToolCallConfirmationOutcome::AlwaysAllow, - }], - tool_call_id, - cx, - )) - .into_any(), - ToolCallConfirmation::Mcp { - server_name, - tool_name: _, - tool_display_name, - description, - } => confirmation_container - .child( - v_flex() - .px_2() - .pb_1p5() - .child(format!("{server_name} - {tool_display_name}")) - .children(description.clone().map(|description| { - self.render_markdown( - description, - default_markdown_style(false, window, cx), - ) - })), - ) - .children(content.map(|content| self.render_tool_call_content(content, window, cx))) - .child(self.render_confirmation_buttons( - &[ - AlwaysAllowOption { - id: "always_allow_server", - label: format!("Always Allow {server_name}").into(), - outcome: acp::ToolCallConfirmationOutcome::AlwaysAllowMcpServer, - }, - AlwaysAllowOption { - id: "always_allow_tool", - label: format!("Always Allow {tool_display_name}").into(), - outcome: acp::ToolCallConfirmationOutcome::AlwaysAllowTool, - }, - ], - tool_call_id, - cx, - )) - .into_any(), - ToolCallConfirmation::Fetch { description, urls } => confirmation_container - .child( - v_flex() - .px_2() - .pb_1p5() - .gap_1() - .children(urls.iter().map(|url| { - h_flex().child( - Button::new(url.clone(), url) - .icon(IconName::ArrowUpRight) - .icon_color(Color::Muted) - .icon_size(IconSize::XSmall) - .on_click({ - let url = url.clone(); - move |_, _, cx| cx.open_url(&url) - }), - ) - })) - .children(description.clone().map(|description| { - self.render_markdown( - description, - default_markdown_style(false, window, cx), - ) - })), - ) - .children(content.map(|content| self.render_tool_call_content(content, window, cx))) - .child(self.render_confirmation_buttons( - &[AlwaysAllowOption { - id: "always_allow", - label: "Always Allow".into(), - outcome: acp::ToolCallConfirmationOutcome::AlwaysAllow, - }], - tool_call_id, - cx, - )) - .into_any(), - ToolCallConfirmation::Other { description } => confirmation_container - .child(v_flex().px_2().pb_1p5().child(self.render_markdown( - description.clone(), - default_markdown_style(false, window, cx), - ))) - .children(content.map(|content| self.render_tool_call_content(content, window, cx))) - .child(self.render_confirmation_buttons( - &[AlwaysAllowOption { - id: "always_allow", - label: "Always Allow".into(), - outcome: acp::ToolCallConfirmationOutcome::AlwaysAllow, - }], - tool_call_id, - cx, - )) - .into_any(), - } - } - - fn render_confirmation_buttons( - &self, - always_allow_options: &[AlwaysAllowOption], - tool_call_id: ToolCallId, + options: &[acp::PermissionOption], + entry_ix: usize, + tool_call_id: acp::ToolCallId, cx: &Context, ) -> Div { h_flex() - .pt_1p5() + .py_1p5() .px_1p5() .gap_1() .justify_end() .border_t_1() .border_color(self.tool_card_border_color(cx)) - .when(self.agent.supports_always_allow(), |this| { - this.children(always_allow_options.into_iter().map(|always_allow_option| { - let outcome = always_allow_option.outcome; - Button::new( - (always_allow_option.id, tool_call_id.0), - always_allow_option.label.clone(), - ) - .icon(IconName::CheckDouble) - .icon_position(IconPosition::Start) - .icon_size(IconSize::XSmall) - .icon_color(Color::Success) - .on_click(cx.listener({ - let id = tool_call_id; - move |this, _, _, cx| { - this.authorize_tool_call(id, outcome, cx); + .children(options.iter().map(|option| { + let option_id = SharedString::from(option.id.0.clone()); + Button::new((option_id, entry_ix), option.label.clone()) + .map(|this| match option.kind { + acp::PermissionOptionKind::AllowOnce => { + this.icon(IconName::Check).icon_color(Color::Success) } - })) - })) - }) - .child( - Button::new(("allow", tool_call_id.0), "Allow") - .icon(IconName::Check) - .icon_position(IconPosition::Start) - .icon_size(IconSize::XSmall) - .icon_color(Color::Success) - .on_click(cx.listener({ - let id = tool_call_id; - move |this, _, _, cx| { - this.authorize_tool_call( - id, - acp::ToolCallConfirmationOutcome::Allow, - cx, - ); + acp::PermissionOptionKind::AllowAlways => { + this.icon(IconName::CheckDouble).icon_color(Color::Success) } - })), - ) - .child( - Button::new(("reject", tool_call_id.0), "Reject") - .icon(IconName::X) + acp::PermissionOptionKind::RejectOnce => { + this.icon(IconName::X).icon_color(Color::Error) + } + acp::PermissionOptionKind::RejectAlways => { + this.icon(IconName::X).icon_color(Color::Error) + } + }) .icon_position(IconPosition::Start) .icon_size(IconSize::XSmall) - .icon_color(Color::Error) .on_click(cx.listener({ - let id = tool_call_id; + let tool_call_id = tool_call_id.clone(); + let option_id = option.id.clone(); + let option_kind = option.kind; move |this, _, _, cx| { this.authorize_tool_call( - id, - acp::ToolCallConfirmationOutcome::Reject, + tool_call_id.clone(), + option_id.clone(), + option_kind, cx, ); } - })), - ) + })) + })) } fn render_diff_editor(&self, multibuffer: &Entity) -> AnyElement { @@ -2245,12 +2104,11 @@ impl AcpThreadView { .languages .language_for_name("Markdown"); - let (thread_summary, markdown) = match &self.thread_state { - ThreadState::Ready { thread, .. } | ThreadState::Unauthenticated { thread } => { - let thread = thread.read(cx); - (thread.title().to_string(), thread.to_markdown(cx)) - } - ThreadState::Loading { .. } | ThreadState::LoadError(..) => return Task::ready(Ok(())), + let (thread_summary, markdown) = if let Some(thread) = self.thread() { + let thread = thread.read(cx); + (thread.title().to_string(), thread.to_markdown(cx)) + } else { + return Task::ready(Ok(())); }; window.spawn(cx, async move |cx| { diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index e69664ce882df70915a84d2ba38cf7ec8521fd4b..ec0a11f86b6b2ef8b2b12179a93fdffaf4926d79 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -1506,8 +1506,7 @@ impl AgentDiff { .read(cx) .entries() .last() - .and_then(|entry| entry.diff()) - .is_some() + .map_or(false, |entry| entry.diffs().next().is_some()) { self.update_reviewing_editors(workspace, window, cx); } @@ -1517,8 +1516,7 @@ impl AgentDiff { .read(cx) .entries() .get(*ix) - .and_then(|entry| entry.diff()) - .is_some() + .map_or(false, |entry| entry.diffs().next().is_some()) { self.update_reviewing_editors(workspace, window, cx); } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index a0250816a03a2ec46c606e0c7bc404371c524af8..4b3db4bc1df00466291c69be89b4edba90ccedc2 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -440,7 +440,7 @@ pub struct AgentPanel { local_timezone: UtcOffset, active_view: ActiveView, acp_message_history: - Rc>>, + Rc>>>, previous_view: Option, history_store: Entity, history: Entity, diff --git a/crates/context_server/src/client.rs b/crates/context_server/src/client.rs index 6b24d9b136efc2d9cc99843e54027058e1602861..8c5e7da0f12773b8f1551266c3cf69abe57c58e8 100644 --- a/crates/context_server/src/client.rs +++ b/crates/context_server/src/client.rs @@ -1,6 +1,6 @@ use anyhow::{Context as _, Result, anyhow}; use collections::HashMap; -use futures::{FutureExt, StreamExt, channel::oneshot, select}; +use futures::{FutureExt, StreamExt, channel::oneshot, future, select}; use gpui::{AppContext as _, AsyncApp, BackgroundExecutor, Task}; use parking_lot::Mutex; use postage::barrier; @@ -10,15 +10,19 @@ use smol::channel; use std::{ fmt, path::PathBuf, + pin::pin, sync::{ Arc, atomic::{AtomicI32, Ordering::SeqCst}, }, time::{Duration, Instant}, }; -use util::TryFutureExt; +use util::{ResultExt, TryFutureExt}; -use crate::transport::{StdioTransport, Transport}; +use crate::{ + transport::{StdioTransport, Transport}, + types::{CancelledParams, ClientNotification, Notification as _, notifications::Cancelled}, +}; const JSON_RPC_VERSION: &str = "2.0"; const REQUEST_TIMEOUT: Duration = Duration::from_secs(60); @@ -32,6 +36,7 @@ pub const INTERNAL_ERROR: i32 = -32603; type ResponseHandler = Box)>; type NotificationHandler = Box; +type RequestHandler = Box; #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] #[serde(untagged)] @@ -78,6 +83,15 @@ pub struct Request<'a, T> { pub params: T, } +#[derive(Serialize, Deserialize)] +pub struct AnyRequest<'a> { + pub jsonrpc: &'a str, + pub id: RequestId, + pub method: &'a str, + #[serde(skip_serializing_if = "is_null_value")] + pub params: Option<&'a RawValue>, +} + #[derive(Serialize, Deserialize)] struct AnyResponse<'a> { jsonrpc: &'a str, @@ -176,15 +190,23 @@ impl Client { Arc::new(Mutex::new(HashMap::<_, NotificationHandler>::default())); let response_handlers = Arc::new(Mutex::new(Some(HashMap::<_, ResponseHandler>::default()))); + let request_handlers = Arc::new(Mutex::new(HashMap::<_, RequestHandler>::default())); let receive_input_task = cx.spawn({ let notification_handlers = notification_handlers.clone(); let response_handlers = response_handlers.clone(); + let request_handlers = request_handlers.clone(); let transport = transport.clone(); async move |cx| { - Self::handle_input(transport, notification_handlers, response_handlers, cx) - .log_err() - .await + Self::handle_input( + transport, + notification_handlers, + request_handlers, + response_handlers, + cx, + ) + .log_err() + .await } }); let receive_err_task = cx.spawn({ @@ -230,13 +252,24 @@ impl Client { async fn handle_input( transport: Arc, notification_handlers: Arc>>, + request_handlers: Arc>>, response_handlers: Arc>>>, cx: &mut AsyncApp, ) -> anyhow::Result<()> { let mut receiver = transport.receive(); while let Some(message) = receiver.next().await { - if let Ok(response) = serde_json::from_str::(&message) { + log::trace!("recv: {}", &message); + if let Ok(request) = serde_json::from_str::(&message) { + let mut request_handlers = request_handlers.lock(); + if let Some(handler) = request_handlers.get_mut(request.method) { + handler( + request.id, + request.params.unwrap_or(RawValue::NULL), + cx.clone(), + ); + } + } else if let Ok(response) = serde_json::from_str::(&message) { if let Some(handlers) = response_handlers.lock().as_mut() { if let Some(handler) = handlers.remove(&response.id) { handler(Ok(message.to_string())); @@ -247,6 +280,8 @@ impl Client { if let Some(handler) = notification_handlers.get_mut(notification.method.as_str()) { handler(notification.params.unwrap_or(Value::Null), cx.clone()); } + } else { + log::error!("Unhandled JSON from context_server: {}", message); } } @@ -294,6 +329,24 @@ impl Client { &self, method: &str, params: impl Serialize, + ) -> Result { + self.request_impl(method, params, None).await + } + + pub async fn cancellable_request( + &self, + method: &str, + params: impl Serialize, + cancel_rx: oneshot::Receiver<()>, + ) -> Result { + self.request_impl(method, params, Some(cancel_rx)).await + } + + pub async fn request_impl( + &self, + method: &str, + params: impl Serialize, + cancel_rx: Option>, ) -> Result { let id = self.next_id.fetch_add(1, SeqCst); let request = serde_json::to_string(&Request { @@ -330,6 +383,16 @@ impl Client { send?; let mut timeout = executor.timer(REQUEST_TIMEOUT).fuse(); + let mut cancel_fut = pin!( + match cancel_rx { + Some(rx) => future::Either::Left(async { + rx.await.log_err(); + }), + None => future::Either::Right(future::pending()), + } + .fuse() + ); + select! { response = rx.fuse() => { let elapsed = started.elapsed(); @@ -348,6 +411,16 @@ impl Client { Err(_) => anyhow::bail!("cancelled") } } + _ = cancel_fut => { + self.notify( + Cancelled::METHOD, + ClientNotification::Cancelled(CancelledParams { + request_id: RequestId::Int(id), + reason: None + }) + ).log_err(); + anyhow::bail!("Request cancelled") + } _ = timeout => { log::error!("cancelled csp request task for {method:?} id {id} which took over {:?}", REQUEST_TIMEOUT); anyhow::bail!("Context server request timeout"); diff --git a/crates/context_server/src/protocol.rs b/crates/context_server/src/protocol.rs index d8bbac60d616268dcb771d653cf02ee3adc59122..7263f502fa44b05b6bba72eda58c7ad84b52ebf7 100644 --- a/crates/context_server/src/protocol.rs +++ b/crates/context_server/src/protocol.rs @@ -6,6 +6,9 @@ //! of messages. use anyhow::Result; +use futures::channel::oneshot; +use gpui::AsyncApp; +use serde_json::Value; use crate::client::Client; use crate::types::{self, Notification, Request}; @@ -95,7 +98,24 @@ impl InitializedContextServerProtocol { self.inner.request(T::METHOD, params).await } + pub async fn cancellable_request( + &self, + params: T::Params, + cancel_rx: oneshot::Receiver<()>, + ) -> Result { + self.inner + .cancellable_request(T::METHOD, params, cancel_rx) + .await + } + pub fn notify(&self, params: T::Params) -> Result<()> { self.inner.notify(T::METHOD, params) } + + pub fn on_notification(&self, method: &'static str, f: F) + where + F: 'static + Send + FnMut(Value, AsyncApp), + { + self.inner.on_notification(method, f); + } } diff --git a/crates/context_server/src/types.rs b/crates/context_server/src/types.rs index 4a6fdcabd3421e14cab3ff89ce6962023935059a..f92c86aa3cd722fdf5e6f68aeaba5df137fcde62 100644 --- a/crates/context_server/src/types.rs +++ b/crates/context_server/src/types.rs @@ -3,6 +3,8 @@ use serde::de::DeserializeOwned; use serde::{Deserialize, Serialize}; use url::Url; +use crate::client::RequestId; + pub const LATEST_PROTOCOL_VERSION: &str = "2025-03-26"; pub const VERSION_2024_11_05: &str = "2024-11-05"; @@ -100,6 +102,7 @@ pub mod notifications { notification!("notifications/initialized", Initialized, ()); notification!("notifications/progress", Progress, ProgressParams); notification!("notifications/message", Message, MessageParams); + notification!("notifications/cancelled", Cancelled, CancelledParams); notification!( "notifications/resources/updated", ResourcesUpdated, @@ -617,11 +620,14 @@ pub enum ClientNotification { Initialized, Progress(ProgressParams), RootsListChanged, - Cancelled { - request_id: String, - #[serde(skip_serializing_if = "Option::is_none")] - reason: Option, - }, + Cancelled(CancelledParams), +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct CancelledParams { + pub request_id: RequestId, + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, } #[derive(Debug, Serialize, Deserialize)] From 1f7ff956bcef31f93db4b9841737c0bc893bafc9 Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Thu, 24 Jul 2025 14:29:28 -0400 Subject: [PATCH 327/658] Fix environment loading with nushell (#35002) Closes https://github.com/zed-industries/zed/issues/34739 I believe this is a regression introduced here: - https://github.com/zed-industries/zed/pull/33599 Release Notes: - Fixed a regression with loading environment variables in nushell --- crates/util/src/shell_env.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/crates/util/src/shell_env.rs b/crates/util/src/shell_env.rs index 21f6096f19fa0c89bf4516b122878be04361ddcd..ed6c8a23cc644e2991e3c999b5c68093e3a3eb49 100644 --- a/crates/util/src/shell_env.rs +++ b/crates/util/src/shell_env.rs @@ -18,10 +18,13 @@ pub fn capture(directory: &std::path::Path) -> Result format!(">[1={}]", ENV_OUTPUT_FD), // `[1=0]` - _ => format!(">&{}", ENV_OUTPUT_FD), // `>&0` + const FD_STDIN: std::os::fd::RawFd = 0; + const FD_STDOUT: std::os::fd::RawFd = 1; + + let (fd_num, redir) = match shell_name { + Some("rc") => (FD_STDIN, format!(">[1={}]", FD_STDIN)), // `[1=0]` + Some("nu") => (FD_STDOUT, "".to_string()), + _ => (FD_STDIN, format!(">&{}", FD_STDIN)), // `>&0` }; command.stdin(Stdio::null()); command.stdout(Stdio::piped()); @@ -48,7 +51,7 @@ pub fn capture(directory: &std::path::Path) -> Result Date: Thu, 24 Jul 2025 11:44:26 -0700 Subject: [PATCH 328/658] Increase the number of parallel request handlers per connection (#35046) Discussed this with @ConradIrwin. The problem we're having with collab is that a bunch of LSP requests take a really long time to resolve, particularly `RefreshCodeLens`. But Those requests are sharing a limited amount of concurrency that we've allocated for all message traffic on one connection. That said, there's not _that_ many concurrent requests made at any one time. The burst traffic seems to top out in the low hundreds for these requests. So let's just expand the amount of space in the queue to accommodate these long-running requests while we work on upgrading our cloud infrastructure. Release Notes: - N/A Co-authored-by: finn --- 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 0735b08e8928d999682f6544e7bfd8b93a3d4fa2..5c5de2f36efa3bfba644fcce64305598ff04890b 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -829,7 +829,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(256)); + let concurrent_handlers = Arc::new(Semaphore::new(512)); loop { let next_message = async { let permit = concurrent_handlers.clone().acquire_owned().await.unwrap(); From 707df516647f07c05e0c04e67ee032279b3d8afa Mon Sep 17 00:00:00 2001 From: Peter Tripp Date: Thu, 24 Jul 2025 15:39:13 -0400 Subject: [PATCH 329/658] Fix environment loading with tcsh (#35054) Closes https://github.com/zed-industries/zed/issues/34973 Fixes an issue where environment variables were not loaded when the user's shell was tcsh and instead a file named `0` was dumped in the current working directory with a copy of your environment variables as json. Follow-up to: - https://github.com/zed-industries/zed/pull/35002 - https://github.com/zed-industries/zed/pull/33599 Release Notes: - Fixed a regression with loading environment variables in nushell --- crates/util/src/shell_env.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/util/src/shell_env.rs b/crates/util/src/shell_env.rs index ed6c8a23cc644e2991e3c999b5c68093e3a3eb49..d737999e4569cad51466f77d234b48def81c95ef 100644 --- a/crates/util/src/shell_env.rs +++ b/crates/util/src/shell_env.rs @@ -23,7 +23,7 @@ pub fn capture(directory: &std::path::Path) -> Result (FD_STDIN, format!(">[1={}]", FD_STDIN)), // `[1=0]` - Some("nu") => (FD_STDOUT, "".to_string()), + Some("nu") | Some("tcsh") => (FD_STDOUT, "".to_string()), _ => (FD_STDIN, format!(">&{}", FD_STDIN)), // `>&0` }; command.stdin(Stdio::null()); From 66acc2698aa39468ea9a0beecc8892cc57f1a91f Mon Sep 17 00:00:00 2001 From: Zachary Hamm Date: Thu, 24 Jul 2025 17:25:21 -0500 Subject: [PATCH 330/658] git_hosting_providers: Support GitHub remote URLs that start with a slash (#34134) Remote URLs like `git@github.com:/zed-industries/zed` are valid git remotes, but currently Zed does not parse them correctly. The result is invalid "permalink" generation, and presumably other bugs anywhere that git remote urls are required for the current repository. This ensures Zed will parse those URLs correctly. Release Notes: - Improved support for GitHub remote urls where the username/organization is preceded by an extra `/` to match the behavior of `git`, `gh` and `github.com`. Co-authored-by: Peter Tripp --- .../src/providers/github.rs | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/crates/git_hosting_providers/src/providers/github.rs b/crates/git_hosting_providers/src/providers/github.rs index 649b2f30aeef92be46317a0039c24738d1981bd5..30f8d058a7c46798209685930518f4b040dbe714 100644 --- a/crates/git_hosting_providers/src/providers/github.rs +++ b/crates/git_hosting_providers/src/providers/github.rs @@ -159,7 +159,11 @@ impl GitHostingProvider for Github { } let mut path_segments = url.path_segments()?; - let owner = path_segments.next()?; + let mut owner = path_segments.next()?; + if owner.is_empty() { + owner = path_segments.next()?; + } + let repo = path_segments.next()?.trim_end_matches(".git"); Some(ParsedGitRemote { @@ -244,6 +248,22 @@ mod tests { use super::*; + #[test] + fn test_remote_url_with_root_slash() { + let remote_url = "git@github.com:/zed-industries/zed"; + let parsed_remote = Github::public_instance() + .parse_remote_url(remote_url) + .unwrap(); + + assert_eq!( + parsed_remote, + ParsedGitRemote { + owner: "zed-industries".into(), + repo: "zed".into(), + } + ); + } + #[test] fn test_invalid_self_hosted_remote_url() { let remote_url = "git@github.com:zed-industries/zed.git"; From f78a1123873526bb112183033641d9077b47981c Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Fri, 25 Jul 2025 05:13:09 +0530 Subject: [PATCH 331/658] gpui: Add `scroll_to_item_with_offset` to `UniformListScrollState` (#35064) Previously we had `ScrollStrategy::ToPosition(usize)` which lets you define the offset where you want to scroll that item to. This is the same as `ScrollStrategy::Top` but imagine some space reserved at the top. This PR removes `ScrollStrategy::ToPosition` in favor of `scroll_to_item_with_offset` which is the method to do the same. The reason to add this method is that now not just `ScrollStrategy::Top` but `ScrollStrategy::Center` can also uses this offset to center the item in the remaining unreserved space. ```rs // Before scroll_handle.scroll_to_item(index, ScrollStrategy::ToPosition(offset)); // After scroll_handle.scroll_to_item_with_offset(index, ScrollStrategy::Top, offset); // New! Centers item skipping first x items scroll_handle.scroll_to_item_with_offset(index, ScrollStrategy::Center, offset); ``` This will be useful for follow up PR. Release Notes: - N/A --- crates/gpui/src/elements/uniform_list.rs | 64 ++++++++++++++++------- crates/project_panel/src/project_panel.rs | 5 +- 2 files changed, 45 insertions(+), 24 deletions(-) diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index e80656a07878f640843afa747d2d48e4448acdc5..cdf90d4eb8934de99a21c65b6c9efa2a2fdde258 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -88,15 +88,24 @@ pub enum ScrollStrategy { /// May not be possible if there's not enough list items above the item scrolled to: /// in this case, the element will be placed at the closest possible position. Center, - /// Scrolls the element to be at the given item index from the top of the viewport. - ToPosition(usize), +} + +#[derive(Clone, Copy, Debug)] +#[allow(missing_docs)] +pub struct DeferredScrollToItem { + /// The item index to scroll to + pub item_index: usize, + /// The scroll strategy to use + pub strategy: ScrollStrategy, + /// The offset in number of items + pub offset: usize, } #[derive(Clone, Debug, Default)] #[allow(missing_docs)] pub struct UniformListScrollState { pub base_handle: ScrollHandle, - pub deferred_scroll_to_item: Option<(usize, ScrollStrategy)>, + pub deferred_scroll_to_item: Option, /// Size of the item, captured during last layout. pub last_item_size: Option, /// Whether the list was vertically flipped during last layout. @@ -126,7 +135,24 @@ impl UniformListScrollHandle { /// Scroll the list to the given item index. pub fn scroll_to_item(&self, ix: usize, strategy: ScrollStrategy) { - self.0.borrow_mut().deferred_scroll_to_item = Some((ix, strategy)); + self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem { + item_index: ix, + strategy, + offset: 0, + }); + } + + /// Scroll the list to the given item index with an offset. + /// + /// For ScrollStrategy::Top, the item will be placed at the offset position from the top. + /// + /// For ScrollStrategy::Center, the item will be centered between offset and the last visible item. + pub fn scroll_to_item_with_offset(&self, ix: usize, strategy: ScrollStrategy, offset: usize) { + self.0.borrow_mut().deferred_scroll_to_item = Some(DeferredScrollToItem { + item_index: ix, + strategy, + offset, + }); } /// Check if the list is flipped vertically. @@ -139,7 +165,8 @@ impl UniformListScrollHandle { pub fn logical_scroll_top_index(&self) -> usize { let this = self.0.borrow(); this.deferred_scroll_to_item - .map(|(ix, _)| ix) + .as_ref() + .map(|deferred| deferred.item_index) .unwrap_or_else(|| this.base_handle.logical_scroll_top().0) } @@ -320,7 +347,8 @@ impl Element for UniformList { scroll_offset.x = Pixels::ZERO; } - if let Some((mut ix, scroll_strategy)) = shared_scroll_to_item { + if let Some(deferred_scroll) = shared_scroll_to_item { + let mut ix = deferred_scroll.item_index; if y_flipped { ix = self.item_count.saturating_sub(ix + 1); } @@ -329,23 +357,28 @@ impl Element for UniformList { let item_top = item_height * ix + padding.top; let item_bottom = item_top + item_height; let scroll_top = -updated_scroll_offset.y; + let offset_pixels = item_height * deferred_scroll.offset; let mut scrolled_to_top = false; - if item_top < scroll_top + padding.top { + + if item_top < scroll_top + padding.top + offset_pixels { scrolled_to_top = true; - updated_scroll_offset.y = -(item_top) + padding.top; + updated_scroll_offset.y = -(item_top) + padding.top + offset_pixels; } else if item_bottom > scroll_top + list_height - padding.bottom { scrolled_to_top = true; updated_scroll_offset.y = -(item_bottom - list_height) - padding.bottom; } - match scroll_strategy { + match deferred_scroll.strategy { ScrollStrategy::Top => {} ScrollStrategy::Center => { if scrolled_to_top { let item_center = item_top + item_height / 2.0; - let target_scroll_top = item_center - list_height / 2.0; - if item_top < scroll_top + let viewport_height = list_height - offset_pixels; + let viewport_center = offset_pixels + viewport_height / 2.0; + let target_scroll_top = item_center - viewport_center; + + if item_top < scroll_top + offset_pixels || item_bottom > scroll_top + list_height { updated_scroll_offset.y = -target_scroll_top @@ -355,15 +388,6 @@ impl Element for UniformList { } } } - ScrollStrategy::ToPosition(sticky_index) => { - let target_y_in_viewport = item_height * sticky_index; - let target_scroll_top = item_top - target_y_in_viewport; - let max_scroll_top = - (content_height - list_height).max(Pixels::ZERO); - let new_scroll_top = - target_scroll_top.clamp(Pixels::ZERO, max_scroll_top); - updated_scroll_offset.y = -new_scroll_top; - } } scroll_offset = *updated_scroll_offset } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 76be97e393695fb263a872e2d7ed8b26c7847b10..dd680981fc231d358e2dd3432dfe3c7e794ffdaf 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -4207,10 +4207,7 @@ impl ProjectPanel { this.marked_entries.clear(); if is_sticky { if let Some((_, _, index)) = this.index_for_entry(entry_id, worktree_id) { - let strategy = sticky_index - .map(ScrollStrategy::ToPosition) - .unwrap_or(ScrollStrategy::Top); - this.scroll_handle.scroll_to_item(index, strategy); + this.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 From 4ee52433ae606b095a9054162060e59d33874e16 Mon Sep 17 00:00:00 2001 From: Daniel Sauble Date: Thu, 24 Jul 2025 17:00:59 -0700 Subject: [PATCH 332/658] Do not subtract gutter margin twice from the editor width (#34564) Closes #33176 In auto-height layouts, horizontal autoscroll can occur right before soft wrapping is triggered. This seems to be caused by gutter margin being subtracted twice from the editor width. If we subtract gutter width only once, the horizontal autoscroll behavior goes away. Before: https://github.com/user-attachments/assets/03b6687e-3c0e-4b34-8e07-a228bcc6f798 After: https://github.com/user-attachments/assets/84e54088-b5bd-4259-a193-d9fcf32cd3a7 Release Notes: - Fixes issue with auto-height layouts where horizontal autoscroll is triggered right before text wraps --------- Co-authored-by: MrSubidubi --- crates/editor/src/element.rs | 62 +++++++++++------------------------- 1 file changed, 18 insertions(+), 44 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 5fd6b028f4ef972021e7e7dedb08e9b6bc7ece60..7e77f113ac598b2013840070bc8d123c267f45f1 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -7944,17 +7944,11 @@ impl Element for EditorElement { right: right_margin, }; - // Offset the content_bounds from the text_bounds by the gutter margin (which - // is roughly half a character wide) to make hit testing work more like how we want. - let content_offset = point(editor_margins.gutter.margin, Pixels::ZERO); - - let editor_content_width = editor_width - content_offset.x; - snapshot = self.editor.update(cx, |editor, cx| { editor.last_bounds = Some(bounds); editor.gutter_dimensions = gutter_dimensions; editor.set_visible_line_count(bounds.size.height / line_height, window, cx); - editor.set_visible_column_count(editor_content_width / em_advance); + editor.set_visible_column_count(editor_width / em_advance); if matches!( editor.mode, @@ -7966,10 +7960,10 @@ impl Element for EditorElement { let wrap_width = match editor.soft_wrap_mode(cx) { SoftWrap::GitDiff => None, SoftWrap::None => Some(wrap_width_for(MAX_LINE_LEN as u32 / 2)), - SoftWrap::EditorWidth => Some(editor_content_width), + SoftWrap::EditorWidth => Some(editor_width), SoftWrap::Column(column) => Some(wrap_width_for(column)), SoftWrap::Bounded(column) => { - Some(editor_content_width.min(wrap_width_for(column))) + Some(editor_width.min(wrap_width_for(column))) } }; @@ -7994,13 +7988,12 @@ impl Element for EditorElement { HitboxBehavior::Normal, ); + // Offset the content_bounds from the text_bounds by the gutter margin (which + // is roughly half a character wide) to make hit testing work more like how we want. + let content_offset = point(editor_margins.gutter.margin, Pixels::ZERO); let content_origin = text_hitbox.origin + content_offset; - let editor_text_bounds = - Bounds::from_corners(content_origin, bounds.bottom_right()); - - let height_in_lines = editor_text_bounds.size.height / line_height; - + let height_in_lines = bounds.size.height / line_height; let max_row = snapshot.max_point().row().as_f32(); // The max scroll position for the top of the window @@ -8384,7 +8377,6 @@ impl Element for EditorElement { glyph_grid_cell, size(longest_line_width, max_row.as_f32() * line_height), longest_line_blame_width, - editor_width, EditorSettings::get_global(cx), ); @@ -8456,7 +8448,7 @@ impl Element for EditorElement { MultiBufferRow(end_anchor.to_point(&snapshot.buffer_snapshot).row); let scroll_max = point( - ((scroll_width - editor_content_width) / em_advance).max(0.0), + ((scroll_width - editor_width) / em_advance).max(0.0), max_scroll_top, ); @@ -8468,7 +8460,7 @@ impl Element for EditorElement { if needs_horizontal_autoscroll.0 && let Some(new_scroll_position) = editor.autoscroll_horizontally( start_row, - editor_content_width, + editor_width, scroll_width, em_advance, &line_layouts, @@ -9049,7 +9041,6 @@ impl ScrollbarLayoutInformation { glyph_grid_cell: Size, document_size: Size, longest_line_blame_width: Pixels, - editor_width: Pixels, settings: &EditorSettings, ) -> Self { let vertical_overscroll = match settings.scroll_beyond_last_line { @@ -9060,19 +9051,11 @@ impl ScrollbarLayoutInformation { } }; - let right_margin = if document_size.width + longest_line_blame_width >= editor_width { - glyph_grid_cell.width - } else { - px(0.0) - }; - - let overscroll = size(right_margin + longest_line_blame_width, vertical_overscroll); - - let scroll_range = document_size + overscroll; + let overscroll = size(longest_line_blame_width, vertical_overscroll); ScrollbarLayoutInformation { editor_bounds, - scroll_range, + scroll_range: document_size + overscroll, glyph_grid_cell, } } @@ -9177,7 +9160,7 @@ struct EditorScrollbars { impl EditorScrollbars { pub fn from_scrollbar_axes( - settings_visibility: ScrollbarAxes, + show_scrollbar: ScrollbarAxes, layout_information: &ScrollbarLayoutInformation, content_offset: gpui::Point, scroll_position: gpui::Point, @@ -9215,22 +9198,13 @@ impl EditorScrollbars { }; let mut create_scrollbar_layout = |axis| { - settings_visibility - .along(axis) + let viewport_size = viewport_size.along(axis); + let scroll_range = scroll_range.along(axis); + + // We always want a vertical scrollbar track for scrollbar diagnostic visibility. + (show_scrollbar.along(axis) + && (axis == ScrollbarAxis::Vertical || scroll_range > viewport_size)) .then(|| { - ( - viewport_size.along(axis) - content_offset.along(axis), - scroll_range.along(axis), - ) - }) - .filter(|(viewport_size, scroll_range)| { - // The scrollbar should only be rendered if the content does - // not entirely fit into the editor - // However, this only applies to the horizontal scrollbar, as information about the - // vertical scrollbar layout is always needed for scrollbar diagnostics. - axis != ScrollbarAxis::Horizontal || viewport_size < scroll_range - }) - .map(|(viewport_size, scroll_range)| { ScrollbarLayout::new( window.insert_hitbox(scrollbar_bounds_for(axis), HitboxBehavior::Normal), viewport_size, From 6e4f7470413ab709f766793cd23c0aa65700cf99 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Fri, 25 Jul 2025 06:21:38 +0530 Subject: [PATCH 333/658] project_panel: Fix autoscroll to treat entries behind sticky items as out of viewport (#35067) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #34831 Autoscroll centers items only if they’re out of viewport. Before this PR, entry behind sticky items was not considered out of viewport, and hence actions like `reveal in project panel` or focusing buffer would not autoscroll that entry into the view in that case. This PR fixes that by using recently added `scroll_to_item_with_offset` in https://github.com/zed-industries/zed/pull/35064. Release Notes: - Fixed issue where `pane: reveal in project panel` action was not working if the entry was behind sticky items. --- crates/project_panel/src/project_panel.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index dd680981fc231d358e2dd3432dfe3c7e794ffdaf..05e6bfe4dfcc64c6ca5779ce054ec2675a639df9 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -114,6 +114,7 @@ pub struct ProjectPanel { mouse_down: bool, hover_expand_task: Option>, previous_drag_position: Option>, + sticky_items_count: usize, } struct DragTargetEntry { @@ -572,6 +573,9 @@ impl ProjectPanel { if project_panel_settings.hide_root != new_settings.hide_root { this.update_visible_entries(None, cx); } + if project_panel_settings.sticky_scroll && !new_settings.sticky_scroll { + this.sticky_items_count = 0; + } project_panel_settings = new_settings; this.update_diagnostics(cx); cx.notify(); @@ -615,6 +619,7 @@ impl ProjectPanel { mouse_down: false, hover_expand_task: None, previous_drag_position: None, + sticky_items_count: 0, }; this.update_visible_entries(None, cx); @@ -2267,8 +2272,11 @@ impl ProjectPanel { fn autoscroll(&mut self, cx: &mut Context) { if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) { - self.scroll_handle - .scroll_to_item(index, ScrollStrategy::Center); + self.scroll_handle.scroll_to_item_with_offset( + index, + ScrollStrategy::Center, + self.sticky_items_count, + ); cx.notify(); } } @@ -5344,7 +5352,10 @@ impl Render for ProjectPanel { items }, |this, marker_entry, window, cx| { - this.render_sticky_entries(marker_entry, window, cx) + let sticky_entries = + this.render_sticky_entries(marker_entry, window, cx); + this.sticky_items_count = sticky_entries.len(); + sticky_entries }, ); list.with_decoration(if show_indent_guides { From a0f2019b6f031c0933022797932f8ca90871cfb4 Mon Sep 17 00:00:00 2001 From: morgankrey Date: Thu, 24 Jul 2025 19:18:40 -0600 Subject: [PATCH 334/658] Add sales tax to docs (#35059) Release Notes: - N/A --- docs/src/ai/billing.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/src/ai/billing.md b/docs/src/ai/billing.md index c49bacd8831c2c4df384339b303bc332bb2165cd..e8587e1fefa1268f16731cbc2e9b416980553b4a 100644 --- a/docs/src/ai/billing.md +++ b/docs/src/ai/billing.md @@ -25,3 +25,12 @@ From Stripe’s secure portal, you can download all current and historical invoi You can update your payment method, company name, address, and tax information through the billing portal. Please note that changes to billing information will **only** affect future invoices — **we cannot modify historical invoices**. + +## Sales Tax {#sales-tax} + +Zed partners with [Sphere](https://www.getsphere.com/) to calculate indirect tax rate for invoices, based on customer location and the product being sold. Tax is listed as a separate line item on invoices, based preferentially on your billing address, followed by the card issue country known to Stripe. + +If you have a VAT/GST ID, you can add it at [zed.dev/account](https://zed.dev/account) by clicking "Manage" on your subscription. Check the box that denotes you as a business. + +Please note that changes to VAT/GST IDs and address will **only** affect future invoices — **we cannot modify historical invoices**. +Questions or issues can be directed to billing-support@zed.dev. From b446d66be780c5b73f5b429033055e950edf27e2 Mon Sep 17 00:00:00 2001 From: morgankrey Date: Thu, 24 Jul 2025 19:25:39 -0600 Subject: [PATCH 335/658] Telemetry docs cleanup (#35060) Release Notes: - N/A --- docs/src/telemetry.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/src/telemetry.md b/docs/src/telemetry.md index 20018b920a4c5f95285f6a84d881ad20de932789..7f5994be0c097ba45325add1885ebc2a200dfdb0 100644 --- a/docs/src/telemetry.md +++ b/docs/src/telemetry.md @@ -22,8 +22,9 @@ 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 -- [Snowflake](https://snowflake.com): Business Intelligence platform - stores both diagnostic and metric events -- [Metabase](https://www.metabase.com): Dashboards - dashboards built around data pulled from Snowflake +- [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 ## Types of Telemetry @@ -33,7 +34,7 @@ Diagnostic events include debug information (stack traces) from crash reports. R 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. -### Usage Data (Metrics) {#metrics} +### Client-Side Usage Data {#client-metrics} To improve Zed and understand how it is being used in the wild, Zed optionally collects usage data like the following: @@ -50,6 +51,12 @@ You can audit the metrics data that Zed has reported by running the command {#ac You can see the full list of the event types and exactly the data sent for each by inspecting the `Event` enum and the associated structs 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 repository. +### Server-Side Usage Data {#metrics} + +When using Zed's hosted services, we may collect, generate, and Process data to allow us to support users and improve our hosted offering. Examples include metadata around rate limiting and billing metrics/token usage. Zed does not persistently store user content or use user content to evaluate and/or improve our AI features, unless it is explicitly shared with Zed, and we have a zero-data retention agreement with Anthropic. + +You can see more about our stance on data collection (and that any prompt data shared with Zed is explicitly opt-in) at [AI Improvement](./ai/ai-improvement.md). + ## Concerns and Questions If you have concerns about telemetry, please feel free to [open an issue](https://github.com/zed-industries/zed/issues/new/choose). From 15c9da4ea4e740faa22cf7c740bed666fdf9ff4a Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Thu, 24 Jul 2025 23:19:20 -0300 Subject: [PATCH 336/658] Add ability to register tools in `McpServer` (#35068) Makes it easier to add tools to a server by implementing a trait Release Notes: - N/A --- crates/agent_servers/src/claude.rs | 11 +- crates/agent_servers/src/claude/mcp_server.rs | 412 +++++++++--------- crates/context_server/src/listener.rs | 217 ++++++++- crates/context_server/src/types.rs | 6 +- 4 files changed, 405 insertions(+), 241 deletions(-) diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs index 5f35b4af734fc7cd7daf9cdae07ecff42c0f3985..d63d8c43cfbf0951cf469a3eae5e982d8d136cf3 100644 --- a/crates/agent_servers/src/claude.rs +++ b/crates/agent_servers/src/claude.rs @@ -2,6 +2,7 @@ mod mcp_server; pub mod tools; use collections::HashMap; +use context_server::listener::McpServerTool; use project::Project; use settings::SettingsStore; use smol::process::Child; @@ -332,10 +333,16 @@ async fn spawn_claude( &format!( "mcp__{}__{}", mcp_server::SERVER_NAME, - mcp_server::PERMISSION_TOOL + mcp_server::PermissionTool::NAME, ), "--allowedTools", - "mcp__zed__Read,mcp__zed__Edit", + &format!( + "mcp__{}__{},mcp__{}__{}", + mcp_server::SERVER_NAME, + mcp_server::EditTool::NAME, + mcp_server::SERVER_NAME, + mcp_server::ReadTool::NAME + ), "--disallowedTools", "Read,Edit", ]) diff --git a/crates/agent_servers/src/claude/mcp_server.rs b/crates/agent_servers/src/claude/mcp_server.rs index 0a39a02931caaa4100677b41b2daec2f127137ea..4272a972dc419c446d709222eca09a4c3ce85200 100644 --- a/crates/agent_servers/src/claude/mcp_server.rs +++ b/crates/agent_servers/src/claude/mcp_server.rs @@ -1,49 +1,24 @@ use std::path::PathBuf; +use crate::claude::tools::{ClaudeTool, EditToolParams, ReadToolParams}; use acp_thread::AcpThread; use agent_client_protocol as acp; use anyhow::{Context, Result}; use collections::HashMap; +use context_server::listener::{McpServerTool, ToolResponse}; use context_server::types::{ - CallToolParams, CallToolResponse, Implementation, InitializeParams, InitializeResponse, - ListToolsResponse, ProtocolVersion, ServerCapabilities, Tool, ToolAnnotations, - ToolResponseContent, ToolsCapabilities, requests, + Implementation, InitializeParams, InitializeResponse, ProtocolVersion, ServerCapabilities, + ToolAnnotations, ToolResponseContent, ToolsCapabilities, requests, }; -use gpui::{App, AsyncApp, Entity, Task, WeakEntity}; +use gpui::{App, AsyncApp, Task, WeakEntity}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use crate::claude::tools::{ClaudeTool, EditToolParams, ReadToolParams}; - pub struct ClaudeZedMcpServer { server: context_server::listener::McpServer, } pub const SERVER_NAME: &str = "zed"; -pub const READ_TOOL: &str = "Read"; -pub const EDIT_TOOL: &str = "Edit"; -pub const PERMISSION_TOOL: &str = "Confirmation"; - -#[derive(Deserialize, JsonSchema, Debug)] -struct PermissionToolParams { - tool_name: String, - input: serde_json::Value, - tool_use_id: Option, -} - -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct PermissionToolResponse { - behavior: PermissionToolBehavior, - updated_input: serde_json::Value, -} - -#[derive(Serialize)] -#[serde(rename_all = "snake_case")] -enum PermissionToolBehavior { - Allow, - Deny, -} impl ClaudeZedMcpServer { pub async fn new( @@ -52,9 +27,15 @@ impl ClaudeZedMcpServer { ) -> Result { let mut mcp_server = context_server::listener::McpServer::new(cx).await?; mcp_server.handle_request::(Self::handle_initialize); - mcp_server.handle_request::(Self::handle_list_tools); - mcp_server.handle_request::(move |request, cx| { - Self::handle_call_tool(request, thread_rx.clone(), cx) + + mcp_server.add_tool(PermissionTool { + thread_rx: thread_rx.clone(), + }); + mcp_server.add_tool(ReadTool { + thread_rx: thread_rx.clone(), + }); + mcp_server.add_tool(EditTool { + thread_rx: thread_rx.clone(), }); Ok(Self { server: mcp_server }) @@ -96,206 +77,203 @@ impl ClaudeZedMcpServer { }) }) } +} - fn handle_list_tools(_: (), cx: &App) -> Task> { - cx.foreground_executor().spawn(async move { - Ok(ListToolsResponse { - tools: vec![ - Tool { - name: PERMISSION_TOOL.into(), - input_schema: schemars::schema_for!(PermissionToolParams).into(), - description: None, - annotations: None, - }, - Tool { - name: READ_TOOL.into(), - input_schema: schemars::schema_for!(ReadToolParams).into(), - description: Some("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.".to_string()), - annotations: Some(ToolAnnotations { - title: Some("Read file".to_string()), - read_only_hint: Some(true), - destructive_hint: Some(false), - open_world_hint: Some(false), - // if time passes the contents might change, but it's not going to do anything different - // true or false seem too strong, let's try a none. - idempotent_hint: None, - }), - }, - Tool { - name: EDIT_TOOL.into(), - input_schema: schemars::schema_for!(EditToolParams).into(), - description: Some("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.".to_string()), - annotations: Some(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), - }), - }, - ], - next_cursor: None, - meta: None, - }) - }) +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct McpConfig { + pub mcp_servers: HashMap, +} + +#[derive(Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct McpServerConfig { + pub command: PathBuf, + pub args: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub env: Option>, +} + +// Tools + +#[derive(Clone)] +pub struct PermissionTool { + thread_rx: watch::Receiver>, +} + +#[derive(Deserialize, JsonSchema, Debug)] +pub struct PermissionToolParams { + tool_name: String, + input: serde_json::Value, + tool_use_id: Option, +} + +#[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; + const NAME: &'static str = "Confirmation"; + + fn description(&self) -> &'static str { + "Request permission for tool calls" } - fn handle_call_tool( - request: CallToolParams, - mut thread_rx: watch::Receiver>, - cx: &App, - ) -> Task> { - cx.spawn(async move |cx| { - let Some(thread) = thread_rx.recv().await?.upgrade() else { - anyhow::bail!("Thread closed"); - }; - - if request.name.as_str() == PERMISSION_TOOL { - let input = - serde_json::from_value(request.arguments.context("Arguments required")?)?; - - let result = Self::handle_permissions_tool_call(input, thread, cx).await?; - Ok(CallToolResponse { - content: vec![ToolResponseContent::Text { - text: serde_json::to_string(&result)?, - }], - is_error: None, - meta: None, - }) - } else if request.name.as_str() == READ_TOOL { - let input = - serde_json::from_value(request.arguments.context("Arguments required")?)?; - - let content = Self::handle_read_tool_call(input, thread, cx).await?; - Ok(CallToolResponse { - content, - is_error: None, - meta: None, - }) - } else if request.name.as_str() == EDIT_TOOL { - let input = - serde_json::from_value(request.arguments.context("Arguments required")?)?; - - Self::handle_edit_tool_call(input, thread, cx).await?; - Ok(CallToolResponse { - content: vec![], - is_error: None, - meta: None, - }) - } else { - anyhow::bail!("Unsupported tool"); + 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 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()); + + let chosen_option = thread + .update(cx, |thread, cx| { + thread.request_tool_call_permission( + claude_tool.as_acp(tool_call_id), + vec![ + acp::PermissionOption { + id: allow_option_id.clone(), + label: "Allow".into(), + kind: acp::PermissionOptionKind::AllowOnce, + }, + acp::PermissionOption { + id: reject_option_id.clone(), + label: "Reject".into(), + kind: acp::PermissionOptionKind::RejectOnce, + }, + ], + cx, + ) + })? + .await?; + + let response = if chosen_option == allow_option_id { + PermissionToolResponse { + behavior: PermissionToolBehavior::Allow, + updated_input: input.input, } - }) - } + } else { + PermissionToolResponse { + behavior: PermissionToolBehavior::Deny, + updated_input: input.input, + } + }; - fn handle_read_tool_call( - ReadToolParams { - abs_path, - offset, - limit, - }: ReadToolParams, - thread: Entity, - cx: &AsyncApp, - ) -> Task>> { - cx.spawn(async move |cx| { - let content = thread - .update(cx, |thread, cx| { - thread.read_text_file(abs_path, offset, limit, false, cx) - })? - .await?; - - Ok(vec![ToolResponseContent::Text { text: content }]) + Ok(ToolResponse { + content: vec![ToolResponseContent::Text { + text: serde_json::to_string(&response)?, + }], + structured_content: None, }) } +} - fn handle_edit_tool_call( - params: EditToolParams, - thread: Entity, - cx: &AsyncApp, - ) -> Task> { - cx.spawn(async move |cx| { - let content = thread - .update(cx, |threads, cx| { - threads.read_text_file(params.abs_path.clone(), None, None, true, cx) - })? - .await?; - - let new_content = content.replace(¶ms.old_text, ¶ms.new_text); - if new_content == content { - return Err(anyhow::anyhow!("The old_text was not found in the content")); - } +#[derive(Clone)] +pub struct ReadTool { + thread_rx: watch::Receiver>, +} - thread - .update(cx, |threads, cx| { - threads.write_text_file(params.abs_path, new_content, cx) - })? - .await?; +impl McpServerTool for ReadTool { + type Input = ReadToolParams; + const NAME: &'static str = "Read"; - Ok(()) - }) + 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 handle_permissions_tool_call( - params: PermissionToolParams, - thread: Entity, - cx: &AsyncApp, - ) -> Task> { - cx.spawn(async move |cx| { - let claude_tool = ClaudeTool::infer(¶ms.tool_name, params.input.clone()); - - let tool_call_id = - acp::ToolCallId(params.tool_use_id.context("Tool ID required")?.into()); - - let allow_option_id = acp::PermissionOptionId("allow".into()); - let reject_option_id = acp::PermissionOptionId("reject".into()); - - let chosen_option = thread - .update(cx, |thread, cx| { - thread.request_tool_call_permission( - claude_tool.as_acp(tool_call_id), - vec![ - acp::PermissionOption { - id: allow_option_id.clone(), - label: "Allow".into(), - kind: acp::PermissionOptionKind::AllowOnce, - }, - acp::PermissionOption { - id: reject_option_id, - label: "Reject".into(), - kind: acp::PermissionOptionKind::RejectOnce, - }, - ], - cx, - ) - })? - .await?; - - if chosen_option == allow_option_id { - Ok(PermissionToolResponse { - behavior: PermissionToolBehavior::Allow, - updated_input: params.input, - }) - } else { - Ok(PermissionToolResponse { - behavior: PermissionToolBehavior::Deny, - updated_input: params.input, - }) - } + 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 { + 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: None, }) } } -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -pub struct McpConfig { - pub mcp_servers: HashMap, +#[derive(Clone)] +pub struct EditTool { + thread_rx: watch::Receiver>, } -#[derive(Serialize, Clone)] -#[serde(rename_all = "camelCase")] -pub struct McpServerConfig { - pub command: PathBuf, - pub args: Vec, - #[serde(skip_serializing_if = "Option::is_none")] - pub env: Option>, +impl McpServerTool for EditTool { + type Input = EditToolParams; + 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 { + 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: None, + }) + } } diff --git a/crates/context_server/src/listener.rs b/crates/context_server/src/listener.rs index 9295ad979c99ee9c1a16a844edc1df456cea61d7..087395a96182122603d94bc9b7121626476d3327 100644 --- a/crates/context_server/src/listener.rs +++ b/crates/context_server/src/listener.rs @@ -9,6 +9,8 @@ use futures::{ }; use gpui::{App, AppContext, AsyncApp, Task}; use net::async_net::{UnixListener, UnixStream}; +use schemars::JsonSchema; +use serde::de::DeserializeOwned; use serde_json::{json, value::RawValue}; use smol::stream::StreamExt; use std::{ @@ -20,16 +22,28 @@ use util::ResultExt; use crate::{ client::{CspResult, RequestId, Response}, - types::Request, + types::{ + CallToolParams, CallToolResponse, ListToolsResponse, Request, Tool, ToolAnnotations, + ToolResponseContent, + requests::{CallTool, ListTools}, + }, }; pub struct McpServer { socket_path: PathBuf, - handlers: Rc>>, + tools: Rc>>, + handlers: Rc>>, _server_task: Task<()>, } -type McpHandler = Box>, &App) -> Task>; +struct RegisteredTool { + tool: Tool, + handler: ToolHandler, +} + +type ToolHandler = + Box, &mut AsyncApp) -> Task>>; +type RequestHandler = Box>, &App) -> Task>; impl McpServer { pub fn new(cx: &AsyncApp) -> Task> { @@ -43,12 +57,14 @@ impl McpServer { cx.spawn(async move |cx| { let (temp_dir, socket_path, listener) = task.await?; + let tools = Rc::new(RefCell::new(HashMap::default())); let handlers = Rc::new(RefCell::new(HashMap::default())); let server_task = cx.spawn({ + let tools = tools.clone(); let handlers = handlers.clone(); async move |cx| { while let Ok((stream, _)) = listener.accept().await { - Self::serve_connection(stream, handlers.clone(), cx); + Self::serve_connection(stream, tools.clone(), handlers.clone(), cx); } drop(temp_dir) } @@ -56,11 +72,40 @@ impl McpServer { Ok(Self { socket_path, _server_task: server_task, - handlers: handlers.clone(), + tools, + handlers: handlers, }) }) } + pub fn add_tool(&mut self, tool: T) { + let registered_tool = RegisteredTool { + tool: Tool { + name: T::NAME.into(), + description: Some(tool.description().into()), + input_schema: schemars::schema_for!(T::Input).into(), + 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), + None => serde_json::from_value(serde_json::Value::Null), + }; + + let tool = tool.clone(); + match input { + Ok(input) => cx.spawn(async move |cx| tool.run(input, cx).await), + Err(err) => Task::ready(Err(err.into())), + } + } + }), + }; + + self.tools.borrow_mut().insert(T::NAME, registered_tool); + } + pub fn handle_request( &mut self, f: impl Fn(R::Params, &App) -> Task> + 'static, @@ -120,7 +165,8 @@ impl McpServer { fn serve_connection( stream: UnixStream, - handlers: Rc>>, + tools: Rc>>, + handlers: Rc>>, cx: &mut AsyncApp, ) { let (read, write) = smol::io::split(stream); @@ -135,7 +181,13 @@ impl McpServer { let Some(request_id) = request.id.clone() else { continue; }; - if let Some(handler) = handlers.borrow().get(&request.method.as_ref()) { + + if request.method == CallTool::METHOD { + Self::handle_call_tool(request_id, request.params, &tools, &outgoing_tx, cx) + .await; + } else if request.method == ListTools::METHOD { + Self::handle_list_tools(request.id.unwrap(), &tools, &outgoing_tx); + } else if let Some(handler) = handlers.borrow().get(&request.method.as_ref()) { let outgoing_tx = outgoing_tx.clone(); if let Some(task) = cx @@ -149,25 +201,122 @@ impl McpServer { .detach(); } } else { - outgoing_tx - .unbounded_send( - serde_json::to_string(&Response::<()> { - jsonrpc: "2.0", - id: request.id.unwrap(), - value: CspResult::Error(Some(crate::client::Error { - message: format!("unhandled method {}", request.method), - code: -32601, - })), - }) - .unwrap(), - ) - .ok(); + Self::send_err( + request_id, + format!("unhandled method {}", request.method), + &outgoing_tx, + ); } } }) .detach(); } + fn handle_list_tools( + request_id: RequestId, + tools: &Rc>>, + outgoing_tx: &UnboundedSender, + ) { + let response = ListToolsResponse { + tools: tools.borrow().values().map(|t| t.tool.clone()).collect(), + next_cursor: None, + meta: None, + }; + + outgoing_tx + .unbounded_send( + serde_json::to_string(&Response { + jsonrpc: "2.0", + id: request_id, + value: CspResult::Ok(Some(response)), + }) + .unwrap_or_default(), + ) + .ok(); + } + + async fn handle_call_tool( + request_id: RequestId, + params: Option>, + tools: &Rc>>, + outgoing_tx: &UnboundedSender, + cx: &mut AsyncApp, + ) { + let result: Result = match params.as_ref() { + Some(params) => serde_json::from_str(params.get()), + None => serde_json::from_value(serde_json::Value::Null), + }; + + match result { + Ok(params) => { + if let Some(tool) = tools.borrow().get(¶ms.name.as_ref()) { + let outgoing_tx = outgoing_tx.clone(); + + let task = (tool.handler)(params.arguments, cx); + cx.spawn(async move |_| { + let response = match task.await { + Ok(result) => CallToolResponse { + content: result.content, + is_error: Some(false), + meta: None, + structured_content: result.structured_content, + }, + Err(err) => CallToolResponse { + content: vec![ToolResponseContent::Text { + text: err.to_string(), + }], + is_error: Some(true), + meta: None, + structured_content: None, + }, + }; + + outgoing_tx + .unbounded_send( + serde_json::to_string(&Response { + jsonrpc: "2.0", + id: request_id, + value: CspResult::Ok(Some(response)), + }) + .unwrap_or_default(), + ) + .ok(); + }) + .detach(); + } else { + Self::send_err( + request_id, + format!("Tool not found: {}", params.name), + &outgoing_tx, + ); + } + } + Err(err) => { + Self::send_err(request_id, err.to_string(), &outgoing_tx); + } + } + } + + fn send_err( + request_id: RequestId, + message: impl Into, + outgoing_tx: &UnboundedSender, + ) { + outgoing_tx + .unbounded_send( + serde_json::to_string(&Response::<()> { + jsonrpc: "2.0", + id: request_id, + value: CspResult::Error(Some(crate::client::Error { + message: message.into(), + code: -32601, + })), + }) + .unwrap(), + ) + .ok(); + } + async fn handle_io( mut outgoing_rx: UnboundedReceiver, incoming_tx: UnboundedSender, @@ -216,6 +365,34 @@ impl McpServer { } } +pub trait McpServerTool { + type Input: DeserializeOwned + JsonSchema; + const NAME: &'static str; + + fn description(&self) -> &'static str; + + fn annotations(&self) -> ToolAnnotations { + ToolAnnotations { + title: None, + read_only_hint: None, + destructive_hint: None, + idempotent_hint: None, + open_world_hint: None, + } + } + + fn run( + &self, + input: Self::Input, + cx: &mut AsyncApp, + ) -> impl Future>; +} + +pub struct ToolResponse { + pub content: Vec, + pub structured_content: Option, +} + #[derive(Serialize, Deserialize)] struct RawRequest { #[serde(skip_serializing_if = "Option::is_none")] diff --git a/crates/context_server/src/types.rs b/crates/context_server/src/types.rs index f92c86aa3cd722fdf5e6f68aeaba5df137fcde62..c95d9008bc8885815196350ddebd6903d625584b 100644 --- a/crates/context_server/src/types.rs +++ b/crates/context_server/src/types.rs @@ -495,7 +495,7 @@ pub struct RootsCapabilities { pub list_changed: Option, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Tool { pub name: String, @@ -506,7 +506,7 @@ pub struct Tool { pub annotations: Option, } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ToolAnnotations { /// A human-readable title for the tool. @@ -679,6 +679,8 @@ pub struct CallToolResponse { pub is_error: Option, #[serde(rename = "_meta", skip_serializing_if = "Option::is_none")] pub meta: Option>, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub structured_content: Option, } #[derive(Debug, Serialize, Deserialize)] From af0c909924d5b5b46432847fce2afb4bc8d78ee2 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Thu, 24 Jul 2025 23:57:18 -0300 Subject: [PATCH 337/658] McpServerTool output schema (#35069) Add an `Output` associated type to `McpServerTool`, so that we can include its schema in `tools/list`. Release Notes: - N/A --- crates/agent_servers/src/claude/mcp_server.rs | 30 +++++++++++--- crates/context_server/src/listener.rs | 40 +++++++++++++++---- crates/context_server/src/types.rs | 2 + 3 files changed, 59 insertions(+), 13 deletions(-) diff --git a/crates/agent_servers/src/claude/mcp_server.rs b/crates/agent_servers/src/claude/mcp_server.rs index 4272a972dc419c446d709222eca09a4c3ce85200..a320a6d37fb5e8714790fe4cf35ef4011e8774e1 100644 --- a/crates/agent_servers/src/claude/mcp_server.rs +++ b/crates/agent_servers/src/claude/mcp_server.rs @@ -124,13 +124,19 @@ enum PermissionToolBehavior { 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 { + 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"); @@ -178,7 +184,7 @@ impl McpServerTool for PermissionTool { content: vec![ToolResponseContent::Text { text: serde_json::to_string(&response)?, }], - structured_content: None, + structured_content: (), }) } } @@ -190,6 +196,8 @@ pub struct ReadTool { impl McpServerTool for ReadTool { type Input = ReadToolParams; + type Output = (); + const NAME: &'static str = "Read"; fn description(&self) -> &'static str { @@ -206,7 +214,11 @@ impl McpServerTool for ReadTool { } } - async fn run(&self, input: Self::Input, cx: &mut AsyncApp) -> Result { + 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"); @@ -220,7 +232,7 @@ impl McpServerTool for ReadTool { Ok(ToolResponse { content: vec![ToolResponseContent::Text { text: content }], - structured_content: None, + structured_content: (), }) } } @@ -232,6 +244,8 @@ pub struct EditTool { impl McpServerTool for EditTool { type Input = EditToolParams; + type Output = (); + const NAME: &'static str = "Edit"; fn description(&self) -> &'static str { @@ -248,7 +262,11 @@ impl McpServerTool for EditTool { } } - async fn run(&self, input: Self::Input, cx: &mut AsyncApp) -> Result { + 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"); @@ -273,7 +291,7 @@ impl McpServerTool for EditTool { Ok(ToolResponse { content: vec![], - structured_content: None, + structured_content: (), }) } } diff --git a/crates/context_server/src/listener.rs b/crates/context_server/src/listener.rs index 087395a96182122603d94bc9b7121626476d3327..192f53081604bbc534475889547831df906badbd 100644 --- a/crates/context_server/src/listener.rs +++ b/crates/context_server/src/listener.rs @@ -41,8 +41,12 @@ struct RegisteredTool { handler: ToolHandler, } -type ToolHandler = - Box, &mut AsyncApp) -> Task>>; +type ToolHandler = Box< + dyn Fn( + Option, + &mut AsyncApp, + ) -> Task>>, +>; type RequestHandler = Box>, &App) -> Task>; impl McpServer { @@ -79,11 +83,19 @@ 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 registered_tool = RegisteredTool { tool: Tool { name: T::NAME.into(), description: Some(tool.description().into()), input_schema: schemars::schema_for!(T::Input).into(), + output_schema: if output_schema == unit_schema { + None + } else { + Some(output_schema.into()) + }, annotations: Some(tool.annotations()), }, handler: Box::new({ @@ -96,7 +108,15 @@ impl McpServer { let tool = tool.clone(); match input { - Ok(input) => cx.spawn(async move |cx| tool.run(input, cx).await), + Ok(input) => cx.spawn(async move |cx| { + let output = tool.run(input, cx).await?; + + Ok(ToolResponse { + content: output.content, + structured_content: serde_json::to_value(output.structured_content) + .unwrap_or_default(), + }) + }), Err(err) => Task::ready(Err(err.into())), } } @@ -259,7 +279,11 @@ impl McpServer { content: result.content, is_error: Some(false), meta: None, - structured_content: result.structured_content, + structured_content: if result.structured_content.is_null() { + None + } else { + Some(result.structured_content) + }, }, Err(err) => CallToolResponse { content: vec![ToolResponseContent::Text { @@ -367,6 +391,8 @@ impl McpServer { pub trait McpServerTool { type Input: DeserializeOwned + JsonSchema; + type Output: Serialize + JsonSchema; + const NAME: &'static str; fn description(&self) -> &'static str; @@ -385,12 +411,12 @@ pub trait McpServerTool { &self, input: Self::Input, cx: &mut AsyncApp, - ) -> impl Future>; + ) -> impl Future>>; } -pub struct ToolResponse { +pub struct ToolResponse { pub content: Vec, - pub structured_content: Option, + pub structured_content: T, } #[derive(Serialize, Deserialize)] diff --git a/crates/context_server/src/types.rs b/crates/context_server/src/types.rs index c95d9008bc8885815196350ddebd6903d625584b..cd97ff95bc733c1e437c68ac366cca66b54ff80f 100644 --- a/crates/context_server/src/types.rs +++ b/crates/context_server/src/types.rs @@ -502,6 +502,8 @@ pub struct Tool { #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, pub input_schema: serde_json::Value, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub output_schema: Option, #[serde(skip_serializing_if = "Option::is_none")] pub annotations: Option, } From 631f9a1b31c16b53e65126163094622f54669960 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 25 Jul 2025 13:18:19 +0200 Subject: [PATCH 338/658] worktree: Improve performance with large # of repositories (#35052) In this PR we've reworked how git status updates are processed. Most notable change is moving the processing into a background thread (and splitting it across multiple background workers). We believe it is safe to do so, as worktree events are not deterministic (fs updates are not guaranteed to come in any order etc), so I've figured that git store should not be overly order-reliant anyways. Note that this PR does not solve perf issues wholesale - other parts of the system are still slow to process stuff (which I plan to nuke soon). Related to #34302 Release Notes: - Improved Zed's performance in projects with large # of repositories --------- Co-authored-by: Anthony Eid --- crates/project/src/git_store.rs | 132 ++++++++++++++---- crates/project/src/git_store/git_traversal.rs | 48 +++++-- 2 files changed, 142 insertions(+), 38 deletions(-) diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index eb16446daf5170fddb2d7f47a79f0a64033d3226..d131b6dd41538d698171c96e9b7e0d58bfd7fe33 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -14,9 +14,10 @@ use collections::HashMap; pub use conflict_set::{ConflictRegion, ConflictSet, ConflictSetSnapshot, ConflictSetUpdate}; use fs::Fs; use futures::{ - FutureExt, StreamExt as _, + FutureExt, StreamExt, channel::{mpsc, oneshot}, future::{self, Shared}, + stream::FuturesOrdered, }; use git::{ BuildPermalinkParams, GitHostingProviderRegistry, WORK_DIRECTORY_REPO_PATH, @@ -63,8 +64,8 @@ use sum_tree::{Edit, SumTree, TreeSet}; use text::{Bias, BufferId}; use util::{ResultExt, debug_panic, post_inc}; use worktree::{ - File, PathKey, PathProgress, PathSummary, PathTarget, UpdatedGitRepositoriesSet, - UpdatedGitRepository, Worktree, + File, PathChange, PathKey, PathProgress, PathSummary, PathTarget, ProjectEntryId, + UpdatedGitRepositoriesSet, UpdatedGitRepository, Worktree, }; pub struct GitStore { @@ -1083,27 +1084,26 @@ impl GitStore { match event { WorktreeStoreEvent::WorktreeUpdatedEntries(worktree_id, updated_entries) => { - let mut paths_by_git_repo = HashMap::<_, Vec<_>>::default(); - for (relative_path, _, _) in updated_entries.iter() { - let Some((repo, repo_path)) = self.repository_and_path_for_project_path( - &(*worktree_id, relative_path.clone()).into(), - cx, - ) else { - continue; - }; - paths_by_git_repo.entry(repo).or_default().push(repo_path) - } - - for (repo, paths) in paths_by_git_repo { - repo.update(cx, |repo, cx| { - repo.paths_changed( - paths, - downstream - .as_ref() - .map(|downstream| downstream.updates_tx.clone()), - cx, - ); - }); + if let Some(worktree) = self + .worktree_store + .read(cx) + .worktree_for_id(*worktree_id, cx) + { + let paths_by_git_repo = + self.process_updated_entries(&worktree, updated_entries, cx); + let downstream = downstream + .as_ref() + .map(|downstream| downstream.updates_tx.clone()); + cx.spawn(async move |_, cx| { + let paths_by_git_repo = paths_by_git_repo.await; + for (repo, paths) in paths_by_git_repo { + repo.update(cx, |repo, cx| { + repo.paths_changed(paths, downstream.clone(), cx); + }) + .ok(); + } + }) + .detach(); } } WorktreeStoreEvent::WorktreeUpdatedGitRepositories(worktree_id, changed_repos) => { @@ -2191,6 +2191,80 @@ impl GitStore { .map(|(id, repo)| (*id, repo.read(cx).snapshot.clone())) .collect() } + + fn process_updated_entries( + &self, + worktree: &Entity, + updated_entries: &[(Arc, ProjectEntryId, PathChange)], + cx: &mut App, + ) -> Task, Vec>> { + let mut repo_paths = self + .repositories + .values() + .map(|repo| (repo.read(cx).work_directory_abs_path.clone(), repo.clone())) + .collect::>(); + let mut entries: Vec<_> = updated_entries + .iter() + .map(|(path, _, _)| path.clone()) + .collect(); + entries.sort(); + let worktree = worktree.read(cx); + + let entries = entries + .into_iter() + .filter_map(|path| worktree.absolutize(&path).ok()) + .collect::>(); + + let executor = cx.background_executor().clone(); + cx.background_executor().spawn(async move { + repo_paths.sort_by(|lhs, rhs| lhs.0.cmp(&rhs.0)); + let mut paths_by_git_repo = HashMap::<_, Vec<_>>::default(); + let mut tasks = FuturesOrdered::new(); + for (repo_path, repo) in repo_paths.into_iter().rev() { + let entries = entries.clone(); + let task = executor.spawn(async move { + // Find all repository paths that belong to this repo + let mut ix = entries.partition_point(|path| path < &*repo_path); + if ix == entries.len() { + return None; + }; + + let mut paths = vec![]; + // 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) + { + paths.push((repo_path, ix)); + ix += 1; + } + Some((repo, paths)) + }); + tasks.push_back(task); + } + + // Now, let's filter out the "duplicate" entries that were processed by multiple distinct repos. + let mut path_was_used = vec![false; entries.len()]; + let tasks = tasks.collect::>().await; + // Process tasks from the back: iterating backwards allows us to see more-specific paths first. + // We always want to assign a path to it's innermost repository. + for t in tasks { + let Some((repo, paths)) = t else { + continue; + }; + let entry = paths_by_git_repo.entry(repo).or_default(); + for (repo_path, ix) in paths { + if path_was_used[ix] { + continue; + } + path_was_used[ix] = true; + entry.push(repo_path); + } + } + + paths_by_git_repo + }) + } } impl BufferGitState { @@ -2660,8 +2734,16 @@ impl RepositorySnapshot { } pub fn abs_path_to_repo_path(&self, abs_path: &Path) -> Option { + Self::abs_path_to_repo_path_inner(&self.work_directory_abs_path, abs_path) + } + + #[inline] + fn abs_path_to_repo_path_inner( + work_directory_abs_path: &Path, + abs_path: &Path, + ) -> Option { abs_path - .strip_prefix(&self.work_directory_abs_path) + .strip_prefix(&work_directory_abs_path) .map(RepoPath::from) .ok() } diff --git a/crates/project/src/git_store/git_traversal.rs b/crates/project/src/git_store/git_traversal.rs index cd173d5714863f1ed845dd9e7116dc73d214f710..777042cb02cf87c127f050a88d8504dcb181678c 100644 --- a/crates/project/src/git_store/git_traversal.rs +++ b/crates/project/src/git_store/git_traversal.rs @@ -1,6 +1,6 @@ use collections::HashMap; -use git::status::GitSummary; -use std::{ops::Deref, path::Path}; +use git::{repository::RepoPath, status::GitSummary}; +use std::{collections::BTreeMap, ops::Deref, path::Path}; use sum_tree::Cursor; use text::Bias; use worktree::{Entry, PathProgress, PathTarget, Traversal}; @@ -11,7 +11,7 @@ use super::{RepositoryId, RepositorySnapshot, StatusEntry}; pub struct GitTraversal<'a> { traversal: Traversal<'a>, current_entry_summary: Option, - repo_snapshots: &'a HashMap, + repo_root_to_snapshot: BTreeMap<&'a Path, &'a RepositorySnapshot>, repo_location: Option<(RepositoryId, Cursor<'a, StatusEntry, PathProgress<'a>>)>, } @@ -20,16 +20,46 @@ impl<'a> GitTraversal<'a> { repo_snapshots: &'a HashMap, traversal: Traversal<'a>, ) -> GitTraversal<'a> { + let repo_root_to_snapshot = repo_snapshots + .values() + .map(|snapshot| (&*snapshot.work_directory_abs_path, snapshot)) + .collect(); let mut this = GitTraversal { traversal, - repo_snapshots, current_entry_summary: None, repo_location: None, + repo_root_to_snapshot, }; this.synchronize_statuses(true); this } + fn repo_root_for_path(&self, path: &Path) -> Option<(&'a RepositorySnapshot, RepoPath)> { + // We might need to perform a range search multiple times, as there may be a nested repository inbetween + // the target and our path. E.g: + // /our_root_repo/ + // .git/ + // other_repo/ + // .git/ + // our_query.txt + let mut query = path.ancestors(); + while let Some(query) = query.next() { + let (_, snapshot) = self + .repo_root_to_snapshot + .range(Path::new("")..=query) + .last()?; + + let stripped = snapshot + .abs_path_to_repo_path(path) + .map(|repo_path| (*snapshot, repo_path)); + if stripped.is_some() { + return stripped; + } + } + + None + } + fn synchronize_statuses(&mut self, reset: bool) { self.current_entry_summary = None; @@ -42,15 +72,7 @@ impl<'a> GitTraversal<'a> { return; }; - let Some((repo, repo_path)) = self - .repo_snapshots - .values() - .filter_map(|repo_snapshot| { - let repo_path = repo_snapshot.abs_path_to_repo_path(&abs_path)?; - Some((repo_snapshot, repo_path)) - }) - .max_by_key(|(repo, _)| repo.work_directory_abs_path.clone()) - else { + let Some((repo, repo_path)) = self.repo_root_for_path(&abs_path) else { self.repo_location = None; return; }; From 57b463fd0df7785d1695ae126e86c88542969be3 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 25 Jul 2025 14:23:52 +0200 Subject: [PATCH 339/658] typescript: Fix handling of jest/vitest tests with regex characters in name (#35090) Closes #35065 Release Notes: - JavaScript/TypeScript tasks: Fixed handling of Vitest/Jest tests with regex-specific characters in their name. --- 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 34b9c3224eecf9c86dd61cf64cb0d5e33572a810..fb515448415eebb1e2be1e45eb2a1a1df9f52cc7 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -512,7 +512,7 @@ fn eslint_server_binary_arguments(server_path: &Path) -> Vec { fn replace_test_name_parameters(test_name: &str) -> String { let pattern = regex::Regex::new(r"(%|\$)[0-9a-zA-Z]+").unwrap(); - pattern.replace_all(test_name, "(.+?)").to_string() + regex::escape(&pattern.replace_all(test_name, "(.+?)")) } pub struct TypeScriptLspAdapter { From 9071341a1da9b0c9e3a88616e58646a4928c5414 Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Fri, 25 Jul 2025 07:40:26 -0500 Subject: [PATCH 340/658] Add TestCrash action (#35088) This triggers a crash that avoids our panic-handler, which is useful for testing that our crash reporting works even when you don't get a panic. Release Notes: - N/A --- crates/zed/src/zed.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 57534c8cd540c171069751281e2bc824ed05c343..0a90f89fa41d1ea087bfb6e24e010dbe81e90705 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -111,6 +111,8 @@ actions!( Zoom, /// Triggers a test panic for debugging. TestPanic, + /// Triggers a hard crash for debugging. + TestCrash, ] ); @@ -126,6 +128,14 @@ pub fn init(cx: &mut App) { cx.on_action(|_: &RestoreBanner, cx| title_bar::restore_banner(cx)); if ReleaseChannel::global(cx) == ReleaseChannel::Dev || cx.has_flag::() { cx.on_action(|_: &TestPanic, _| panic!("Ran the TestPanic action")); + cx.on_action(|_: &TestCrash, _| { + unsafe extern "C" { + fn puts(s: *const i8); + } + unsafe { + puts(0xabad1d3a as *const i8); + } + }); } cx.on_action(|_: &OpenLog, cx| { with_active_or_new_workspace(cx, |workspace, window, cx| { From a2408f353ba27b42abdfd3c1d13d9344566779d7 Mon Sep 17 00:00:00 2001 From: Julia Ryan Date: Fri, 25 Jul 2025 08:12:55 -0500 Subject: [PATCH 341/658] Don't separately rebuild crates for the build platform (#35084) macos: - `cargo build`: 1838 -> 1400 - `cargo build -r`1891 -> 1400 linux: - `cargo build`: 1893 -> 1455 - `cargo build -r`: 1893 -> 1455 windows: - `cargo build`: 1830 -> 1392 - `cargo build -r`: 1880 -> 1392 We definitely want this change for debug builds, for release builds it's _possible_ that it pessimizes the critical path, but we'll see if it impacts CI times before merging. Release Notes: - N/A --------- Co-authored-by: Rahul Butani --- Cargo.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index 90629501276778ebd4e8f534d5be3144ebf6989d..ea01003f365ebe82ae464f3c5e816ec46025e164 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -720,6 +720,11 @@ workspace-hack = { path = "tooling/workspace-hack" } split-debuginfo = "unpacked" codegen-units = 16 +# mirror configuration for crates compiled for the build platform +# (without this cargo will compile ~400 crates twice) +[profile.dev.build-override] +codegen-units = 16 + [profile.dev.package] taffy = { opt-level = 3 } cranelift-codegen = { opt-level = 3 } From f21ba9e2c6789fadd3a59f88e0eb1ff4e1f919db Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Fri, 25 Jul 2025 19:06:43 +0530 Subject: [PATCH 342/658] lmstudio: Propagate actual error message from server (#34538) Discovered in this issue: #34513 Previously, we were propagating deserialization errors to users when using LMStudio, instead of the actual error message sent from the LMStudio server. This change will help users understand why their request failed while streaming responses. Release Notes: - lmsudio: Display specific backend error messaging on failure rather than generic ones --------- Signed-off-by: Umesh Yadav Co-authored-by: Peter Tripp --- crates/lmstudio/src/lmstudio.rs | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/crates/lmstudio/src/lmstudio.rs b/crates/lmstudio/src/lmstudio.rs index a5477994ff844c31be536c6910b66c41fca54b25..43c78115cdd4f517a51052991121620a0a93c363 100644 --- a/crates/lmstudio/src/lmstudio.rs +++ b/crates/lmstudio/src/lmstudio.rs @@ -1,4 +1,4 @@ -use anyhow::{Context as _, Result}; +use anyhow::{Context as _, Result, anyhow}; use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream}; use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest, http}; use serde::{Deserialize, Serialize}; @@ -275,11 +275,16 @@ impl Capabilities { } } +#[derive(Serialize, Deserialize, Debug)] +pub struct LmStudioError { + pub message: String, +} + #[derive(Serialize, Deserialize, Debug)] #[serde(untagged)] pub enum ResponseStreamResult { Ok(ResponseStreamEvent), - Err { error: String }, + Err { error: LmStudioError }, } #[derive(Serialize, Deserialize, Debug)] @@ -392,7 +397,6 @@ pub async fn stream_chat_completion( let mut response = client.send(request).await?; if response.status().is_success() { let reader = BufReader::new(response.into_body()); - Ok(reader .lines() .filter_map(|line| async move { @@ -402,18 +406,16 @@ pub async fn stream_chat_completion( if line == "[DONE]" { None } else { - let result = serde_json::from_str(&line) - .context("Unable to parse chat completions response"); - if let Err(ref e) = result { - eprintln!("Error parsing line: {e}\nLine content: '{line}'"); + match serde_json::from_str(line) { + Ok(ResponseStreamResult::Ok(response)) => Some(Ok(response)), + Ok(ResponseStreamResult::Err { error, .. }) => { + Some(Err(anyhow!(error.message))) + } + Err(error) => Some(Err(anyhow!(error))), } - Some(result) } } - Err(e) => { - eprintln!("Error reading line: {e}"); - Some(Err(e.into())) - } + Err(error) => Some(Err(anyhow!(error))), } }) .boxed()) From 5de544eb4bf7841889da94c43b5f00d12398b2ef Mon Sep 17 00:00:00 2001 From: etimvr <80552426+etimvr@users.noreply.github.com> Date: Fri, 25 Jul 2025 20:58:05 +0700 Subject: [PATCH 343/658] Fix unnecessary Ollama model loading (#35032) Closes https://github.com/zed-industries/zed/issues/35031 Similar solution as in https://github.com/zed-industries/zed/pull/30589 Release Notes: - Fix unnecessary ollama model loading --- crates/language_models/src/provider/ollama.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/language_models/src/provider/ollama.rs b/crates/language_models/src/provider/ollama.rs index dc81e8be1897aa3ae51b8d2cb26b7cdec0e55cbf..c20ea0ee1e1388d4afcfe20f6b110516c62b97bf 100644 --- a/crates/language_models/src/provider/ollama.rs +++ b/crates/language_models/src/provider/ollama.rs @@ -192,12 +192,16 @@ impl LanguageModelProvider for OllamaLanguageModelProvider { IconName::AiOllama } - fn default_model(&self, cx: &App) -> Option> { - self.provided_models(cx).into_iter().next() + fn default_model(&self, _: &App) -> Option> { + // We shouldn't try to select default model, because it might lead to a load call for an unloaded model. + // In a constrained environment where user might not have enough resources it'll be a bad UX to select something + // to load by default. + None } - fn default_fast_model(&self, cx: &App) -> Option> { - self.default_model(cx) + fn default_fast_model(&self, _: &App) -> Option> { + // See explanation for default_model. + None } fn provided_models(&self, cx: &App) -> Vec> { From 0e9d955e9bf25383f637bc7d4e3a385f43d30e09 Mon Sep 17 00:00:00 2001 From: Justin Su Date: Fri, 25 Jul 2025 10:53:57 -0400 Subject: [PATCH 344/658] Hide Copilot commands when AI functionality is disabled (#35055) Follow-up of https://github.com/zed-industries/zed/pull/34896 Related to https://github.com/zed-industries/zed/issues/31346 cc @rtfeldman Release Notes: - Hide copilot commands when AI functionality is disabled --- crates/agent_ui/src/agent_ui.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index cac0f1adace1113dea78537ee000fb951f54d74a..6ae78585decb4797496d60afbfff2ea643030da0 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -262,7 +262,9 @@ fn update_command_palette_filter(cx: &mut App) { if disable_ai { filter.hide_namespace("agent"); filter.hide_namespace("assistant"); + filter.hide_namespace("copilot"); filter.hide_namespace("zed_predict_onboarding"); + filter.hide_namespace("edit_prediction"); use editor::actions::{ @@ -282,6 +284,7 @@ fn update_command_palette_filter(cx: &mut App) { } else { filter.show_namespace("agent"); filter.show_namespace("assistant"); + filter.show_namespace("copilot"); filter.show_namespace("zed_predict_onboarding"); filter.show_namespace("edit_prediction"); From 985350f9e8fcaea66750c8b4a99d07a0ef0f39cf Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 25 Jul 2025 17:39:50 +0200 Subject: [PATCH 345/658] terminal: Support ~ in cwd field of task definitions (#35097) Closes #35022 Release Notes: - Fixed `~` not being expanded correctly in `cwd` field of task definitions. --- crates/project/src/terminals.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 8cfbdff31183cc6763455e4cbe5acf635440343e..973d4e881191dcb21414fbd5d0f7cc85467e329c 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -213,17 +213,24 @@ impl Project { cx: &mut Context, ) -> Result> { let this = &mut *self; + let ssh_details = this.ssh_details(cx); let path: Option> = match &kind { TerminalKind::Shell(path) => path.as_ref().map(|path| Arc::from(path.as_ref())), TerminalKind::Task(spawn_task) => { if let Some(cwd) = &spawn_task.cwd { - Some(Arc::from(cwd.as_ref())) + if ssh_details.is_some() { + Some(Arc::from(cwd.as_ref())) + } else { + let cwd = cwd.to_string_lossy(); + let tilde_substituted = shellexpand::tilde(&cwd); + Some(Arc::from(Path::new(tilde_substituted.as_ref()))) + } } else { this.active_project_directory(cx) } } }; - let ssh_details = this.ssh_details(cx); + let is_ssh_terminal = ssh_details.is_some(); let mut settings_location = None; From abb3ed1ed1bee71a8fd0c5e805f4020a9aa20ab6 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 25 Jul 2025 17:48:09 +0200 Subject: [PATCH 346/658] git: Fix commit modal contents being searchable (#35099) Fixes #35093 Release Notes: - Fixed Git commit editor being searchable. --- crates/git_ui/src/git_panel.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 061833a6c72cee8110ebf6bb8a3d7398bf5c6cde..a8d1da7daf70b1d07de965ae9f1f00902ccff5d5 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -380,6 +380,9 @@ pub(crate) fn commit_message_editor( window: &mut Window, cx: &mut Context, ) -> Editor { + project.update(cx, |this, cx| { + this.mark_buffer_as_non_searchable(commit_message_buffer.read(cx).remote_id(), cx); + }); let buffer = cx.new(|cx| MultiBuffer::singleton(commit_message_buffer, cx)); let max_lines = if in_panel { MAX_PANEL_EDITOR_LINES } else { 18 }; let mut commit_editor = Editor::new( From 993d5753d5997ca0766626dc225771aac76031f7 Mon Sep 17 00:00:00 2001 From: Haojian Wu Date: Fri, 25 Jul 2025 18:14:44 +0200 Subject: [PATCH 347/658] docs: Add a missing "," in the C/C++ debugger configuration (#35098) Release Notes: - N/A --- docs/src/languages/c.md | 2 +- docs/src/languages/cpp.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/languages/c.md b/docs/src/languages/c.md index 14a11c0d665e9e5b3a9499284d36b133945ad866..8db1bb671257397f0bcf668af374d700142db658 100644 --- a/docs/src/languages/c.md +++ b/docs/src/languages/c.md @@ -77,7 +77,7 @@ You can use CodeLLDB or GDB to debug native binaries. (Make sure that your build "command": "make", "args": ["-j8"], "cwd": "$ZED_WORKTREE_ROOT" - } + }, "program": "$ZED_WORKTREE_ROOT/build/prog", "request": "launch", "adapter": "CodeLLDB" diff --git a/docs/src/languages/cpp.md b/docs/src/languages/cpp.md index 1273bce2ac0b6a92cbda8e63cd0f477965500a11..e84bb6ea507f264240a40e986f41c5cd3a23610d 100644 --- a/docs/src/languages/cpp.md +++ b/docs/src/languages/cpp.md @@ -127,7 +127,7 @@ You can use CodeLLDB or GDB to debug native binaries. (Make sure that your build "command": "make", "args": ["-j8"], "cwd": "$ZED_WORKTREE_ROOT" - } + }, "program": "$ZED_WORKTREE_ROOT/build/prog", "request": "launch", "adapter": "CodeLLDB" From 2f812c339c1e1f01a3c04d38ca5190520df73c85 Mon Sep 17 00:00:00 2001 From: Umesh Yadav <23421535+imumesh18@users.noreply.github.com> Date: Fri, 25 Jul 2025 22:25:38 +0530 Subject: [PATCH 348/658] agent_ui: Fix delay when loading keybindings in the Agent panel settings (#34954) Fixes a annoying lag in agent settings panel where the keybindings would show up after a lag. Close to 1-2 secs. It was a simple fix previously we were not passing the focus handler to context menu which made the keybindings lookup slower compared to other parts like git panel and title bar profile dropdown. | Before | After | |--------|--------| | | | Release Notes: - Fix delay when loading keybindings in the Agent panel settings --- crates/agent_ui/src/agent_panel.rs | 116 +++++++++++++++-------------- 1 file changed, 60 insertions(+), 56 deletions(-) diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 4b3db4bc1df00466291c69be89b4edba90ccedc2..43c1167af80cd35a48f340ded8933fda3f4e6175 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -2016,65 +2016,69 @@ impl AgentPanel { ) .anchor(Corner::TopRight) .with_handle(self.agent_panel_menu_handle.clone()) - .menu(move |window, cx| { - Some(ContextMenu::build(window, cx, |mut menu, _window, _| { - if let Some(usage) = usage { - menu = menu - .header_with_link("Prompt Usage", "Manage", account_url.clone()) - .custom_entry( - move |_window, cx| { - let used_percentage = match usage.limit { - UsageLimit::Limited(limit) => { - Some((usage.amount as f32 / limit as f32) * 100.) - } - UsageLimit::Unlimited => None, - }; + .menu({ + let focus_handle = focus_handle.clone(); + move |window, cx| { + Some(ContextMenu::build(window, cx, |mut menu, _window, _| { + menu = menu.context(focus_handle.clone()); + if let Some(usage) = usage { + menu = menu + .header_with_link("Prompt Usage", "Manage", account_url.clone()) + .custom_entry( + move |_window, cx| { + let used_percentage = match usage.limit { + UsageLimit::Limited(limit) => { + Some((usage.amount as f32 / limit as f32) * 100.) + } + UsageLimit::Unlimited => None, + }; + + h_flex() + .flex_1() + .gap_1p5() + .children(used_percentage.map(|percent| { + ProgressBar::new("usage", percent, 100., cx) + })) + .child( + Label::new(match usage.limit { + UsageLimit::Limited(limit) => { + format!("{} / {limit}", usage.amount) + } + UsageLimit::Unlimited => { + format!("{} / ∞", usage.amount) + } + }) + .size(LabelSize::Small) + .color(Color::Muted), + ) + .into_any_element() + }, + move |_, cx| cx.open_url(&zed_urls::account_url(cx)), + ) + .separator() + } - h_flex() - .flex_1() - .gap_1p5() - .children(used_percentage.map(|percent| { - ProgressBar::new("usage", percent, 100., cx) - })) - .child( - Label::new(match usage.limit { - UsageLimit::Limited(limit) => { - format!("{} / {limit}", usage.amount) - } - UsageLimit::Unlimited => { - format!("{} / ∞", usage.amount) - } - }) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .into_any_element() - }, - move |_, cx| cx.open_url(&zed_urls::account_url(cx)), + menu = menu + .header("MCP Servers") + .action( + "View Server Extensions", + Box::new(zed_actions::Extensions { + category_filter: Some( + zed_actions::ExtensionCategoryFilter::ContextServers, + ), + id: None, + }), ) - .separator() - } + .action("Add Custom Server…", Box::new(AddContextServer)) + .separator(); - menu = menu - .header("MCP Servers") - .action( - "View Server Extensions", - Box::new(zed_actions::Extensions { - category_filter: Some( - zed_actions::ExtensionCategoryFilter::ContextServers, - ), - id: None, - }), - ) - .action("Add Custom Server…", Box::new(AddContextServer)) - .separator(); - - menu = menu - .action("Rules…", Box::new(OpenRulesLibrary::default())) - .action("Settings", Box::new(OpenConfiguration)) - .action(zoom_in_label, Box::new(ToggleZoom)); - menu - })) + menu = menu + .action("Rules…", Box::new(OpenRulesLibrary::default())) + .action("Settings", Box::new(OpenConfiguration)) + .action(zoom_in_label, Box::new(ToggleZoom)); + menu + })) + } }); h_flex() From cd5095872748ef2d56fa03b54e40b6aaa5ff7906 Mon Sep 17 00:00:00 2001 From: Kainoa Kanter Date: Fri, 25 Jul 2025 10:17:16 -0700 Subject: [PATCH 349/658] Add icon for SurrealQL files (#34855) Release Notes: - Added icon for SurrealQL (`.surql`) files --------- Co-authored-by: Danilo Leal --- assets/icons/file_icons/surrealql.svg | 3 +++ crates/theme/src/icon_theme.rs | 2 ++ 2 files changed, 5 insertions(+) create mode 100644 assets/icons/file_icons/surrealql.svg diff --git a/assets/icons/file_icons/surrealql.svg b/assets/icons/file_icons/surrealql.svg new file mode 100644 index 0000000000000000000000000000000000000000..076f93e808fc38a28313956e905a95561422b58c --- /dev/null +++ b/assets/icons/file_icons/surrealql.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/theme/src/icon_theme.rs b/crates/theme/src/icon_theme.rs index 09f5df06b05bfa47b3a9d0e7b32a54f10d999d76..baa928d7223267d0371383bd7b5d80182e2e0e35 100644 --- a/crates/theme/src/icon_theme.rs +++ b/crates/theme/src/icon_theme.rs @@ -216,6 +216,7 @@ const FILE_SUFFIXES_BY_ICON_KEY: &[(&str, &[&str])] = &[ "stylelintrc.yml", ], ), + ("surrealql", &["surql"]), ("svelte", &["svelte"]), ("swift", &["swift"]), ("tcl", &["tcl"]), @@ -340,6 +341,7 @@ const FILE_ICONS: &[(&str, &str)] = &[ ("solidity", "icons/file_icons/file.svg"), ("storage", "icons/file_icons/database.svg"), ("stylelint", "icons/file_icons/javascript.svg"), + ("surrealql", "icons/file_icons/surrealql.svg"), ("svelte", "icons/file_icons/html.svg"), ("swift", "icons/file_icons/swift.svg"), ("tcl", "icons/file_icons/tcl.svg"), From 4abe14f94a30c0fc9c4e0e3ef14cae7609f7b429 Mon Sep 17 00:00:00 2001 From: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com> Date: Fri, 25 Jul 2025 13:19:05 -0400 Subject: [PATCH 350/658] keymap ui: Resize columns on double click improvement (#35095) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR improves the behavior of resetting a column's size by double-clicking on its column handle. We now shrink/grow to the side that has more leftover/additional space. We also improved the below 1. dragging was a couple of pixels off to the left because we didn't take the column handle’s width into account. 2. Column dragging now has memory and will shift things to their exact position when reversing a drag before letting the drag handle go. 3. Improved our test infrastructure. 4. Double clicking on a column's header resizes the column Release Notes: - N/A --------- Co-authored-by: Ben Kunkle --- crates/settings_ui/Cargo.toml | 3 + crates/settings_ui/src/ui_components/table.rs | 635 +++++++++++++++--- 2 files changed, 545 insertions(+), 93 deletions(-) diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml index 02327045fdb2279597342a5d838d356d7b738c73..25f033469d7b00e1b351c7a0385b2de5bc10d9ad 100644 --- a/crates/settings_ui/Cargo.toml +++ b/crates/settings_ui/Cargo.toml @@ -45,3 +45,6 @@ ui_input.workspace = true util.workspace = true workspace-hack.workspace = true workspace.workspace = true + +[dev-dependencies] +db = {"workspace"= true, "features" = ["test-support"]} diff --git a/crates/settings_ui/src/ui_components/table.rs b/crates/settings_ui/src/ui_components/table.rs index 69207f559b89b83b6709bd41ab861e3a71be6616..65778c20eb4bc60f22dd15b634de433dc781cd97 100644 --- a/crates/settings_ui/src/ui_components/table.rs +++ b/crates/settings_ui/src/ui_components/table.rs @@ -2,9 +2,9 @@ use std::{ops::Range, rc::Rc, time::Duration}; use editor::{EditorSettings, ShowScrollbar, scroll::ScrollbarAutoHide}; use gpui::{ - AbsoluteLength, AppContext, Axis, Context, DefiniteLength, DragMoveEvent, Entity, FocusHandle, - Length, ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton, Point, Stateful, Task, - UniformListScrollHandle, WeakEntity, transparent_black, uniform_list, + AbsoluteLength, AppContext, Axis, Context, DefiniteLength, DragMoveEvent, Entity, EntityId, + FocusHandle, Length, ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton, Point, + Stateful, Task, UniformListScrollHandle, WeakEntity, transparent_black, uniform_list, }; use itertools::intersperse_with; @@ -13,10 +13,12 @@ use ui::{ ActiveTheme as _, AnyElement, App, Button, ButtonCommon as _, ButtonStyle, Color, Component, ComponentScope, Div, ElementId, FixedWidth as _, FluentBuilder as _, Indicator, InteractiveElement, IntoElement, ParentElement, Pixels, RegisterComponent, RenderOnce, - Scrollbar, ScrollbarState, StatefulInteractiveElement, Styled, StyledExt as _, + Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement, Styled, StyledExt as _, StyledTypography, Window, div, example_group_with_title, h_flex, px, single_example, v_flex, }; +const RESIZE_COLUMN_WIDTH: f32 = 5.0; + #[derive(Debug)] struct DraggedColumn(usize); @@ -227,7 +229,7 @@ impl TableInteractionState { .id("column-resize-handle") .absolute() .left_neg_0p5() - .w(px(5.0)) + .w(px(RESIZE_COLUMN_WIDTH)) .h_full(); if resizable_columns @@ -478,6 +480,7 @@ impl ResizeBehavior { pub struct ColumnWidths { widths: [DefiniteLength; COLS], + visible_widths: [DefiniteLength; COLS], cached_bounds_width: Pixels, initialized: bool, } @@ -486,6 +489,7 @@ impl ColumnWidths { pub fn new(_: &mut App) -> Self { Self { widths: [DefiniteLength::default(); COLS], + visible_widths: [DefiniteLength::default(); COLS], cached_bounds_width: Default::default(), initialized: false, } @@ -512,46 +516,105 @@ impl ColumnWidths { let rem_size = window.rem_size(); let initial_sizes = initial_sizes.map(|length| Self::get_fraction(&length, bounds_width, rem_size)); - let mut widths = self + let widths = self .widths .map(|length| Self::get_fraction(&length, bounds_width, rem_size)); - let diff = initial_sizes[double_click_position] - widths[double_click_position]; + let updated_widths = Self::reset_to_initial_size( + double_click_position, + widths, + initial_sizes, + resize_behavior, + ); + self.widths = updated_widths.map(DefiniteLength::Fraction); + self.visible_widths = self.widths; + } - if diff > 0.0 { - let diff_remaining = self.propagate_resize_diff_right( - diff, - double_click_position, - &mut widths, - resize_behavior, - ); + fn reset_to_initial_size( + col_idx: usize, + mut widths: [f32; COLS], + initial_sizes: [f32; COLS], + resize_behavior: &[ResizeBehavior; COLS], + ) -> [f32; COLS] { + // RESET: + // Part 1: + // Figure out if we should shrink/grow the selected column + // Get diff which represents the change in column we want to make initial size delta curr_size = diff + // + // Part 2: We need to decide which side column we should move and where + // + // If we want to grow our column we should check the left/right columns diff to see what side + // has a greater delta than their initial size. Likewise, if we shrink our column we should check + // the left/right column diffs to see what side has the smallest delta. + // + // Part 3: resize + // + // col_idx represents the column handle to the right of an active column + // + // If growing and right has the greater delta { + // shift col_idx to the right + // } else if growing and left has the greater delta { + // shift col_idx - 1 to the left + // } else if shrinking and the right has the greater delta { + // shift + // } { + // + // } + // } + // + // if we need to shrink, then if the right + // + + // DRAGGING + // we get diff which represents the change in the _drag handle_ position + // -diff => dragging left -> + // grow the column to the right of the handle as much as we can shrink columns to the left of the handle + // +diff => dragging right -> growing handles column + // grow the column to the left of the handle as much as we can shrink columns to the right of the handle + // + + let diff = initial_sizes[col_idx] - widths[col_idx]; + + let left_diff = + initial_sizes[..col_idx].iter().sum::() - widths[..col_idx].iter().sum::(); + let right_diff = initial_sizes[col_idx + 1..].iter().sum::() + - widths[col_idx + 1..].iter().sum::(); + + let go_left_first = if diff < 0.0 { + left_diff > right_diff + } else { + left_diff < right_diff + }; + + if !go_left_first { + let diff_remaining = + Self::propagate_resize_diff(diff, col_idx, &mut widths, resize_behavior, 1); - if diff_remaining > 0.0 && double_click_position > 0 { - self.propagate_resize_diff_left( - -diff_remaining, - double_click_position - 1, + if diff_remaining != 0.0 && col_idx > 0 { + Self::propagate_resize_diff( + diff_remaining, + col_idx, &mut widths, resize_behavior, + -1, ); } - } else if double_click_position > 0 { - let diff_remaining = self.propagate_resize_diff_left( - diff, - double_click_position, - &mut widths, - resize_behavior, - ); + } else { + let diff_remaining = + Self::propagate_resize_diff(diff, col_idx, &mut widths, resize_behavior, -1); - if diff_remaining < 0.0 { - self.propagate_resize_diff_right( - -diff_remaining, - double_click_position, + if diff_remaining != 0.0 { + Self::propagate_resize_diff( + diff_remaining, + col_idx, &mut widths, resize_behavior, + 1, ); } } - self.widths = widths.map(DefiniteLength::Fraction); + + widths } fn on_drag_move( @@ -569,98 +632,102 @@ impl ColumnWidths { let bounds_width = bounds.right() - bounds.left(); let col_idx = drag_event.drag(cx).0; + let column_handle_width = Self::get_fraction( + &DefiniteLength::Absolute(AbsoluteLength::Pixels(px(RESIZE_COLUMN_WIDTH))), + bounds_width, + rem_size, + ); + let mut widths = self .widths .map(|length| Self::get_fraction(&length, bounds_width, rem_size)); for length in widths[0..=col_idx].iter() { - col_position += length; + col_position += length + column_handle_width; } let mut total_length_ratio = col_position; for length in widths[col_idx + 1..].iter() { total_length_ratio += length; } + total_length_ratio += (COLS - 1 - col_idx) as f32 * column_handle_width; let drag_fraction = (drag_position.x - bounds.left()) / bounds_width; let drag_fraction = drag_fraction * total_length_ratio; - let diff = drag_fraction - col_position; + let diff = drag_fraction - col_position - column_handle_width / 2.0; - let is_dragging_right = diff > 0.0; + Self::drag_column_handle(diff, col_idx, &mut widths, resize_behavior); - if is_dragging_right { - self.propagate_resize_diff_right(diff, col_idx, &mut widths, resize_behavior); - } else { - // Resize behavior should be improved in the future by also seeking to the right column when there's not enough space - self.propagate_resize_diff_left(diff, col_idx, &mut widths, resize_behavior); - } - self.widths = widths.map(DefiniteLength::Fraction); + self.visible_widths = widths.map(DefiniteLength::Fraction); } - fn propagate_resize_diff_right( - &self, + fn drag_column_handle( diff: f32, col_idx: usize, widths: &mut [f32; COLS], resize_behavior: &[ResizeBehavior; COLS], - ) -> f32 { - let mut diff_remaining = diff; - let mut curr_column = col_idx + 1; - - while diff_remaining > 0.0 && curr_column < COLS { - let Some(min_size) = resize_behavior[curr_column - 1].min_size() else { - curr_column += 1; - continue; - }; - - let mut curr_width = widths[curr_column] - diff_remaining; - - diff_remaining = 0.0; - if min_size > curr_width { - diff_remaining += min_size - curr_width; - curr_width = min_size; - } - widths[curr_column] = curr_width; - curr_column += 1; + ) { + // if diff > 0.0 then go right + if diff > 0.0 { + Self::propagate_resize_diff(diff, col_idx, widths, resize_behavior, 1); + } else { + Self::propagate_resize_diff(-diff, col_idx + 1, widths, resize_behavior, -1); } - - widths[col_idx] = widths[col_idx] + (diff - diff_remaining); - return diff_remaining; } - fn propagate_resize_diff_left( - &mut self, + fn propagate_resize_diff( diff: f32, - mut curr_column: usize, + col_idx: usize, widths: &mut [f32; COLS], resize_behavior: &[ResizeBehavior; COLS], + direction: i8, ) -> f32 { let mut diff_remaining = diff; - let col_idx = curr_column; - while diff_remaining < 0.0 { + if resize_behavior[col_idx].min_size().is_none() { + return diff; + } + + let step_right; + let step_left; + if direction < 0 { + step_right = 0; + step_left = 1; + } else { + step_right = 1; + step_left = 0; + } + if col_idx == 0 && direction < 0 { + return diff; + } + let mut curr_column = col_idx + step_right - step_left; + + while diff_remaining != 0.0 && curr_column < COLS { let Some(min_size) = resize_behavior[curr_column].min_size() else { if curr_column == 0 { break; } - curr_column -= 1; + curr_column -= step_left; + curr_column += step_right; continue; }; - let mut curr_width = widths[curr_column] + diff_remaining; + let curr_width = widths[curr_column] - diff_remaining; + widths[curr_column] = curr_width; - diff_remaining = 0.0; - if curr_width < min_size { - diff_remaining = curr_width - min_size; - curr_width = min_size + if min_size > curr_width { + diff_remaining = min_size - curr_width; + widths[curr_column] = min_size; + } else { + diff_remaining = 0.0; + break; } - - widths[curr_column] = curr_width; if curr_column == 0 { break; } - curr_column -= 1; + curr_column -= step_left; + curr_column += step_right; } - widths[col_idx + 1] = widths[col_idx + 1] - (diff - diff_remaining); + widths[col_idx] = widths[col_idx] + (diff - diff_remaining); return diff_remaining; } @@ -686,7 +753,7 @@ impl TableWidths { fn lengths(&self, cx: &App) -> [Length; COLS] { self.current .as_ref() - .map(|entity| entity.read(cx).widths.map(Length::Definite)) + .map(|entity| entity.read(cx).visible_widths.map(Length::Definite)) .unwrap_or(self.initial.map(Length::Definite)) } } @@ -799,6 +866,7 @@ impl Table { if !widths.initialized { widths.initialized = true; widths.widths = table_widths.initial; + widths.visible_widths = widths.widths; } }) } @@ -888,11 +956,24 @@ pub fn render_row( pub fn render_header( headers: [impl IntoElement; COLS], table_context: TableRenderContext, + columns_widths: Option<( + WeakEntity>, + [ResizeBehavior; COLS], + [DefiniteLength; COLS], + )>, + entity_id: Option, cx: &mut App, ) -> impl IntoElement { let column_widths = table_context .column_widths .map_or([None; COLS], |widths| widths.map(Some)); + + let element_id = entity_id + .map(|entity| entity.to_string()) + .unwrap_or_default(); + + let shared_element_id: SharedString = format!("table-{}", element_id).into(); + div() .flex() .flex_row() @@ -902,12 +983,39 @@ pub fn render_header( .p_2() .border_b_1() .border_color(cx.theme().colors().border) - .children( - headers - .into_iter() - .zip(column_widths) - .map(|(h, width)| base_cell_style_text(width, cx).child(h)), - ) + .children(headers.into_iter().enumerate().zip(column_widths).map( + |((header_idx, h), width)| { + base_cell_style_text(width, cx) + .child(h) + .id(ElementId::NamedInteger( + shared_element_id.clone(), + header_idx as u64, + )) + .when_some( + columns_widths.as_ref().cloned(), + |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 { + column_widths + .update(cx, |column, _| { + column.on_double_click( + header_idx, + &initial_sizes, + &resizables, + window, + ); + }) + .ok(); + } + }) + } else { + this + } + }, + ) + }, + )) } #[derive(Clone)] @@ -939,6 +1047,12 @@ impl RenderOnce for Table { .and_then(|widths| Some((widths.current.as_ref()?, widths.resizable))) .map(|(curr, resize_behavior)| (curr.downgrade(), resize_behavior)); + let current_widths_with_initial_sizes = self + .col_widths + .as_ref() + .and_then(|widths| Some((widths.current.as_ref()?, widths.resizable, widths.initial))) + .map(|(curr, resize_behavior, initial)| (curr.downgrade(), resize_behavior, initial)); + let scroll_track_size = px(16.); let h_scroll_offset = if interaction_state .as_ref() @@ -958,7 +1072,13 @@ impl RenderOnce for Table { .h_full() .v_flex() .when_some(self.headers.take(), |this, headers| { - this.child(render_header(headers, table_context.clone(), cx)) + this.child(render_header( + headers, + table_context.clone(), + current_widths_with_initial_sizes, + interaction_state.as_ref().map(Entity::entity_id), + cx, + )) }) .when_some(current_widths, { |this, (widths, resize_behavior)| { @@ -972,19 +1092,28 @@ impl RenderOnce for Table { .ok(); } }) - .on_children_prepainted(move |bounds, _, cx| { + .on_children_prepainted({ + let widths = widths.clone(); + move |bounds, _, cx| { + widths + .update(cx, |widths, _| { + // This works because all children x axis bounds are the same + widths.cached_bounds_width = + bounds[0].right() - bounds[0].left(); + }) + .ok(); + } + }) + .on_drop::(move |_, _, cx| { widths .update(cx, |widths, _| { - // This works because all children x axis bounds are the same - widths.cached_bounds_width = bounds[0].right() - bounds[0].left(); + widths.widths = widths.visible_widths; }) .ok(); + // Finish the resize operation }) } }) - .on_drop::(|_, _, _| { - // Finish the resize operation - }) .child( div() .flex_grow() @@ -1313,3 +1442,323 @@ impl Component for Table<3> { ) } } + +#[cfg(test)] +mod test { + use super::*; + + fn is_almost_eq(a: &[f32], b: &[f32]) -> bool { + a.len() == b.len() && a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-6) + } + + fn cols_to_str(cols: &[f32; COLS], total_size: f32) -> String { + cols.map(|f| "*".repeat(f32::round(f * total_size) as usize)) + .join("|") + } + + fn parse_resize_behavior( + input: &str, + total_size: f32, + ) -> [ResizeBehavior; COLS] { + let mut resize_behavior = [ResizeBehavior::None; COLS]; + let mut max_index = 0; + for (index, col) in input.split('|').enumerate() { + if col.starts_with('X') || col.is_empty() { + resize_behavior[index] = ResizeBehavior::None; + } else if col.starts_with('*') { + resize_behavior[index] = ResizeBehavior::MinSize(col.len() as f32 / total_size); + } else { + panic!("invalid test input: unrecognized resize behavior: {}", col); + } + max_index = index; + } + + if max_index + 1 != COLS { + panic!("invalid test input: too many columns"); + } + resize_behavior + } + + mod reset_column_size { + use super::*; + + fn parse(input: &str) -> ([f32; COLS], f32, Option) { + let mut widths = [f32::NAN; COLS]; + let mut column_index = None; + for (index, col) in input.split('|').enumerate() { + widths[index] = col.len() as f32; + if col.starts_with('X') { + column_index = Some(index); + } + } + + for w in widths { + assert!(w.is_finite(), "incorrect number of columns"); + } + let total = widths.iter().sum::(); + for width in &mut widths { + *width /= total; + } + (widths, total, column_index) + } + + #[track_caller] + fn check_reset_size( + initial_sizes: &str, + widths: &str, + expected: &str, + resize_behavior: &str, + ) { + let (initial_sizes, total_1, None) = parse::(initial_sizes) else { + panic!("invalid test input: initial sizes should not be marked"); + }; + let (widths, total_2, Some(column_index)) = parse::(widths) else { + panic!("invalid test input: widths should be marked"); + }; + assert_eq!( + total_1, total_2, + "invalid test input: total width not the same {total_1}, {total_2}" + ); + let (expected, total_3, None) = parse::(expected) else { + panic!("invalid test input: expected should not be marked: {expected:?}"); + }; + assert_eq!( + total_2, total_3, + "invalid test input: total width not the same" + ); + let resize_behavior = parse_resize_behavior::(resize_behavior, total_1); + let result = ColumnWidths::reset_to_initial_size( + column_index, + widths, + initial_sizes, + &resize_behavior, + ); + let is_eq = is_almost_eq(&result, &expected); + if !is_eq { + let result_str = cols_to_str(&result, total_1); + let expected_str = cols_to_str(&expected, total_1); + panic!( + "resize failed\ncomputed: {result_str}\nexpected: {expected_str}\n\ncomputed values: {result:?}\nexpected values: {expected:?}\n:minimum widths: {resize_behavior:?}" + ); + } + } + + macro_rules! check_reset_size { + (columns: $cols:expr, starting: $initial:expr, snapshot: $current:expr, expected: $expected:expr, resizing: $resizing:expr $(,)?) => { + check_reset_size::<$cols>($initial, $current, $expected, $resizing); + }; + ($name:ident, columns: $cols:expr, starting: $initial:expr, snapshot: $current:expr, expected: $expected:expr, minimums: $resizing:expr $(,)?) => { + #[test] + fn $name() { + check_reset_size::<$cols>($initial, $current, $expected, $resizing); + } + }; + } + + check_reset_size!( + basic_right, + columns: 5, + starting: "**|**|**|**|**", + snapshot: "**|**|X|***|**", + expected: "**|**|**|**|**", + minimums: "X|*|*|*|*", + ); + + check_reset_size!( + basic_left, + columns: 5, + starting: "**|**|**|**|**", + snapshot: "**|**|***|X|**", + expected: "**|**|**|**|**", + minimums: "X|*|*|*|**", + ); + + check_reset_size!( + squashed_left_reset_col2, + columns: 6, + starting: "*|***|**|**|****|*", + snapshot: "*|*|X|*|*|********", + expected: "*|*|**|*|*|*******", + minimums: "X|*|*|*|*|*", + ); + + check_reset_size!( + grow_cascading_right, + columns: 6, + starting: "*|***|****|**|***|*", + snapshot: "*|***|X|**|**|*****", + expected: "*|***|****|*|*|****", + minimums: "X|*|*|*|*|*", + ); + + check_reset_size!( + squashed_right_reset_col4, + columns: 6, + starting: "*|***|**|**|****|*", + snapshot: "*|********|*|*|X|*", + expected: "*|*****|*|*|****|*", + minimums: "X|*|*|*|*|*", + ); + + check_reset_size!( + reset_col6_right, + columns: 6, + starting: "*|***|**|***|***|**", + snapshot: "*|***|**|***|**|XXX", + expected: "*|***|**|***|***|**", + minimums: "X|*|*|*|*|*", + ); + + check_reset_size!( + reset_col6_left, + columns: 6, + starting: "*|***|**|***|***|**", + snapshot: "*|***|**|***|****|X", + expected: "*|***|**|***|***|**", + minimums: "X|*|*|*|*|*", + ); + + check_reset_size!( + last_column_grow_cascading, + columns: 6, + starting: "*|***|**|**|**|***", + snapshot: "*|*******|*|**|*|X", + expected: "*|******|*|*|*|***", + minimums: "X|*|*|*|*|*", + ); + + check_reset_size!( + goes_left_when_left_has_extreme_diff, + columns: 6, + starting: "*|***|****|**|**|***", + snapshot: "*|********|X|*|**|**", + expected: "*|*****|****|*|**|**", + minimums: "X|*|*|*|*|*", + ); + + check_reset_size!( + basic_shrink_right, + columns: 6, + starting: "**|**|**|**|**|**", + snapshot: "**|**|XXX|*|**|**", + expected: "**|**|**|**|**|**", + minimums: "X|*|*|*|*|*", + ); + + check_reset_size!( + shrink_should_go_left, + columns: 6, + starting: "*|***|**|*|*|*", + snapshot: "*|*|XXX|**|*|*", + expected: "*|**|**|**|*|*", + minimums: "X|*|*|*|*|*", + ); + + check_reset_size!( + shrink_should_go_right, + columns: 6, + starting: "*|***|**|**|**|*", + snapshot: "*|****|XXX|*|*|*", + expected: "*|****|**|**|*|*", + minimums: "X|*|*|*|*|*", + ); + } + + mod drag_handle { + use super::*; + + fn parse(input: &str) -> ([f32; COLS], f32, Option) { + let mut widths = [f32::NAN; COLS]; + let column_index = input.replace("*", "").find("I"); + for (index, col) in input.replace("I", "|").split('|').enumerate() { + widths[index] = col.len() as f32; + } + + for w in widths { + assert!(w.is_finite(), "incorrect number of columns"); + } + let total = widths.iter().sum::(); + for width in &mut widths { + *width /= total; + } + (widths, total, column_index) + } + + #[track_caller] + fn check( + distance: i32, + widths: &str, + expected: &str, + resize_behavior: &str, + ) { + let (mut widths, total_1, Some(column_index)) = parse::(widths) else { + panic!("invalid test input: widths should be marked"); + }; + let (expected, total_2, None) = parse::(expected) else { + panic!("invalid test input: expected should not be marked: {expected:?}"); + }; + assert_eq!( + total_1, total_2, + "invalid test input: total width not the same" + ); + let resize_behavior = parse_resize_behavior::(resize_behavior, total_1); + + let distance = distance as f32 / total_1; + + let result = ColumnWidths::drag_column_handle( + distance, + column_index, + &mut widths, + &resize_behavior, + ); + + let is_eq = is_almost_eq(&widths, &expected); + if !is_eq { + let result_str = cols_to_str(&widths, total_1); + let expected_str = cols_to_str(&expected, total_1); + panic!( + "resize failed\ncomputed: {result_str}\nexpected: {expected_str}\n\ncomputed values: {result:?}\nexpected values: {expected:?}\n:minimum widths: {resize_behavior:?}" + ); + } + } + + macro_rules! check { + (columns: $cols:expr, distance: $dist:expr, snapshot: $current:expr, expected: $expected:expr, resizing: $resizing:expr $(,)?) => { + check!($cols, $dist, $snapshot, $expected, $resizing); + }; + ($name:ident, columns: $cols:expr, distance: $dist:expr, snapshot: $current:expr, expected: $expected:expr, minimums: $resizing:expr $(,)?) => { + #[test] + fn $name() { + check::<$cols>($dist, $current, $expected, $resizing); + } + }; + } + + check!( + basic_right_drag, + columns: 3, + distance: 1, + snapshot: "**|**I**", + expected: "**|***|*", + minimums: "X|*|*", + ); + + check!( + drag_left_against_mins, + columns: 5, + distance: -1, + snapshot: "*|*|*|*I*******", + expected: "*|*|*|*|*******", + minimums: "X|*|*|*|*", + ); + + check!( + drag_left, + columns: 5, + distance: -2, + snapshot: "*|*|*|*****I***", + expected: "*|*|*|***|*****", + minimums: "X|*|*|*|*", + ); + } +} From ff67f18e0d334b4d5521f17412e0bd704452edaa Mon Sep 17 00:00:00 2001 From: Cole Miller Date: Fri, 25 Jul 2025 10:36:58 -0700 Subject: [PATCH 351/658] Bump livekit dep to try to fix flaky builds (#35103) Let's see if https://github.com/zed-industries/livekit-rust-sdks/pull/6 helps. Release Notes: - N/A --------- Signed-off-by: Cole Miller --- Cargo.lock | 22 +++++++++++++++------- crates/livekit_client/Cargo.toml | 4 ++-- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2c65131db0a712dcd3f18fddbffc5724837cb88a..82790da17f32e118df8cc7a4a260111391b1214f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7688,6 +7688,12 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-literal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcaaec4551594c969335c98c903c1397853d4198408ea609190f420500f6be71" + [[package]] name = "hexf-parse" version = "0.2.1" @@ -9408,7 +9414,7 @@ dependencies = [ [[package]] name = "libwebrtc" version = "0.3.10" -source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4" +source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=2806c1554bb2e3627ea35089208a196e19f24847#2806c1554bb2e3627ea35089208a196e19f24847" dependencies = [ "cxx", "jni", @@ -9488,7 +9494,7 @@ checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" [[package]] name = "livekit" version = "0.7.8" -source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4" +source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=2806c1554bb2e3627ea35089208a196e19f24847#2806c1554bb2e3627ea35089208a196e19f24847" dependencies = [ "chrono", "futures-util", @@ -9511,7 +9517,7 @@ dependencies = [ [[package]] name = "livekit-api" version = "0.4.2" -source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4" +source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=2806c1554bb2e3627ea35089208a196e19f24847#2806c1554bb2e3627ea35089208a196e19f24847" dependencies = [ "futures-util", "http 0.2.12", @@ -9535,7 +9541,7 @@ dependencies = [ [[package]] name = "livekit-protocol" version = "0.3.9" -source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4" +source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=2806c1554bb2e3627ea35089208a196e19f24847#2806c1554bb2e3627ea35089208a196e19f24847" dependencies = [ "futures-util", "livekit-runtime", @@ -9552,7 +9558,7 @@ dependencies = [ [[package]] name = "livekit-runtime" version = "0.4.0" -source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4" +source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=2806c1554bb2e3627ea35089208a196e19f24847#2806c1554bb2e3627ea35089208a196e19f24847" dependencies = [ "tokio", "tokio-stream", @@ -18539,7 +18545,7 @@ dependencies = [ [[package]] name = "webrtc-sys" version = "0.3.7" -source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4" +source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=2806c1554bb2e3627ea35089208a196e19f24847#2806c1554bb2e3627ea35089208a196e19f24847" dependencies = [ "cc", "cxx", @@ -18552,13 +18558,15 @@ dependencies = [ [[package]] name = "webrtc-sys-build" version = "0.3.6" -source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4#d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4" +source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=2806c1554bb2e3627ea35089208a196e19f24847#2806c1554bb2e3627ea35089208a196e19f24847" dependencies = [ "fs2", + "hex-literal", "regex", "reqwest 0.11.27", "scratch", "semver", + "sha2", "zip", ] diff --git a/crates/livekit_client/Cargo.toml b/crates/livekit_client/Cargo.toml index a0c11d46e6a9317c88fde9bda081e64cf09bff27..c367e03bb76b0082deebfc7ad258af00da582e92 100644 --- a/crates/livekit_client/Cargo.toml +++ b/crates/livekit_client/Cargo.toml @@ -40,8 +40,8 @@ util.workspace = true workspace-hack.workspace = true [target.'cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))'.dependencies] -libwebrtc = { rev = "d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4", git = "https://github.com/zed-industries/livekit-rust-sdks" } -livekit = { rev = "d2eade7a6b15d6dbdb38ba12a1ff7bf07fcebba4", git = "https://github.com/zed-industries/livekit-rust-sdks", features = [ +libwebrtc = { rev = "383e5377f8b7de1f8627ee16f0cf11c5293337bd", git = "https://github.com/zed-industries/livekit-rust-sdks" } +livekit = { rev = "383e5377f8b7de1f8627ee16f0cf11c5293337bd", git = "https://github.com/zed-industries/livekit-rust-sdks", features = [ "__rustls-tls" ] } From 0e7eea0d102cc39bdf1249db13dc9d5bb5ff9afd Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Fri, 25 Jul 2025 14:39:33 -0300 Subject: [PATCH 352/658] Add new Nightly app icons (#35104) Release Notes: - N/A --- crates/zed/resources/app-icon-nightly.png | Bin 239870 -> 232863 bytes crates/zed/resources/app-icon-nightly@2x.png | Bin 716288 -> 848597 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/crates/zed/resources/app-icon-nightly.png b/crates/zed/resources/app-icon-nightly.png index 5f1304a6af8d57bbf7414d175611829989d122da..562588955e81265c9a44ab399d5da5813f6805fd 100644 GIT binary patch literal 232863 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

    +?Wo|0z4^sw5h|6Krp9oiT}4cG`b*XgkRaS%$;c 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:}) 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%0A3_h?vJvMlqHf2#RDT} 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%0D_5j(Q?dHrBu{E2k41g9mdC=*As7Py9 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

    rLQrS(+4(c` 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

    f`)%y!Nh0_Vb=_>o7RuyH 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

    yO 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%0AYOMU%Dwg@BTz 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

    (mdg2_&zpbqzQlJD6p`pAAXc& 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


    >5AG`^#Zdw~orQQcm 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 z

    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 literal 716288 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 !Ao2I*Keyj7hIk8AHAaK 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%0DL#Bx+#vt9->fK5JxCR{8n?*F3P&< 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