Compare commits

...

7 Commits

Author SHA1 Message Date
Anthony
75794eb7c7 Add tooltips for edit icon and exact match toggle icon 2025-07-16 18:09:39 -04:00
Umesh Yadav
e23a4564cc 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.
2025-07-16 17:30:08 -04:00
Peter Tripp
f82ef1f76f 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
2025-07-16 20:55:54 +00:00
Richard Feldman
b4c2ae5196 Handle upstream_http_error completion responses (#34573)
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
2025-07-16 16:31:31 -04:00
Peter Tripp
0023773c68 docs: Add Zed as Git Editor example (#34572)
Release Notes:

- N/A
2025-07-16 19:57:02 +00:00
Anthony Eid
0bde929d54 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>
2025-07-16 19:50:53 +00:00
Joseph T. Lyons
6f60939d30 Bump Zed to v0.197 (#34569)
Release Notes:

-N/A
2025-07-16 18:48:50 +00:00
12 changed files with 310 additions and 13 deletions

4
Cargo.lock generated
View File

@@ -14720,6 +14720,7 @@ dependencies = [
"serde",
"serde_json",
"settings",
"telemetry",
"theme",
"tree-sitter-json",
"tree-sitter-rust",
@@ -16451,6 +16452,7 @@ dependencies = [
"schemars",
"serde",
"settings",
"settings_ui",
"smallvec",
"story",
"telemetry",
@@ -20095,7 +20097,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.196.0"
version = "0.197.0"
dependencies = [
"activity_indicator",
"agent",

View File

@@ -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
),
}
}
}

View File

@@ -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))),
)

View File

@@ -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)]

View File

@@ -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

View File

@@ -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()));
}
@@ -1216,6 +1273,18 @@ impl Render for KeymapEditor {
)
.shape(IconButtonShape::Square)
.toggle_state(exact_match)
.tooltip(move |window, cx| {
Tooltip::for_action(
if exact_match {
"Partial match mode"
} else {
"Exact match mode"
},
&ToggleExactKeystrokeMatching,
window,
cx,
)
})
.on_click(
cx.listener(|_, _, window, cx| {
window.dispatch_action(
@@ -1288,7 +1357,14 @@ 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(|window, cx| {
Tooltip::for_action(
"Edit Keybinding",
&EditBinding,
window,
cx,
)
})
.on_click(cx.listener(move |this, _, window, cx| {
this.select_index(index, cx);
this.open_edit_keybinding_modal(false, window, cx);
@@ -2222,6 +2298,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 +2310,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 +2352,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 +2362,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(())
}

View File

@@ -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

View File

@@ -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(),

View File

@@ -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>"]

View File

@@ -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",

View File

@@ -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}

View File

@@ -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"
```