Compare commits

...

16 Commits

Author SHA1 Message Date
Richard Feldman
c9b9b3194e zed 0.196.3 2025-07-17 19:25:14 -04:00
Richard Feldman
eeb9e242b4 Retry on burn mode (#34669)
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
2025-07-17 19:23:38 -04:00
Richard Feldman
f9c498318d Improve upstream error reporting (#34668)
Now we handle more upstream error cases using the same auto-retry logic.

Release Notes:

- N/A
2025-07-17 19:23:34 -04:00
gcp-cherry-pick-bot[bot]
cb40bb755e keymap ui: Fix keymap editor search bugs (cherry-pick #34579) (#34588)
Cherry-picked 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

Co-authored-by: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com>
2025-07-17 18:16:33 -04:00
Umesh Yadav
991887a3ea 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-17 13:37:58 -04:00
Anthony Eid
f249ee481d 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
2025-07-17 13:37:43 -04:00
gcp-cherry-pick-bot[bot]
484e39dcba keymap_ui: Show edit icon on hovered and selected row (cherry-pick #34630) (#34635)
Cherry-picked 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 ...

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-07-17 12:21:49 -04:00
gcp-cherry-pick-bot[bot]
ec7d6631a4 Add keymap editor UI telemetry events (cherry-pick #34571) (#34589)
Cherry-picked 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>

Co-authored-by: Anthony Eid <56899983+Anthony-Eid@users.noreply.github.com>
Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-07-17 11:58:31 -04:00
gcp-cherry-pick-bot[bot]
27691613c1 keymap_ui: Improve keybind display in menus (cherry-pick #34587) (#34632)
Cherry-picked 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>

Co-authored-by: Ben Kunkle <ben@zed.dev>
Co-authored-by: Finn <dev@bahn.sh>
2025-07-17 11:34:44 -04:00
Zed Bot
5f11e09a4b Bump to 0.196.2 for @osyvokon 2025-07-17 12:14:36 +00:00
gcp-cherry-pick-bot[bot]
34e63f9e55 agent: Disable project_notifications by default (cherry-pick #34615) (#34619)
Cherry-picked 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

Co-authored-by: Oleksiy Syvokon <oleksiy@zed.dev>
2025-07-17 15:09:12 +03:00
gcp-cherry-pick-bot[bot]
cbdca4e090 Fix shortcuts with Shift (cherry-pick #34614) (#34616)
Cherry-picked Fix shortcuts with `Shift` (#34614)

Closes #34605, #34606, #34609

Release Notes:

- (Preview only) Fixed shortcuts involving Shift

Co-authored-by: Oleksiy Syvokon <oleksiy@zed.dev>
2025-07-17 14:31:57 +03:00
Conrad Irwin
92105e92c3 Fix ctrl-q on AZERTY on Linux (#34597)
Closes #ISSUE

Release Notes:

- N/A
2025-07-16 21:28:51 -06:00
Zed Bot
632f09efd6 Bump to 0.196.1 for @ConradIrwin 2025-07-17 02:18:34 +00:00
gcp-cherry-pick-bot[bot]
192e0e32dd Don't override ascii graphical shortcuts (cherry-pick #34592) (#34595)
Cherry-picked Don't override ascii graphical shortcuts (#34592)

Closes #34536

Release Notes:

- (preview only) Fix shortcuts on Extended Latin keyboards on Linux

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2025-07-16 20:16:34 -06:00
Joseph T. Lyons
30cc8bd824 v0.196.x preview 2025-07-16 14:30:48 -04:00
20 changed files with 858 additions and 121 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.196.3"
dependencies = [
"activity_indicator",
"agent",

View File

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

View File

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

View File

@@ -817,7 +817,7 @@
"edit_file": true,
"fetch": true,
"list_directory": true,
"project_notifications": true,
"project_notifications": false,
"move_path": true,
"now": true,
"find_path": true,
@@ -837,7 +837,7 @@
"diagnostics": true,
"fetch": true,
"list_directory": true,
"project_notifications": true,
"project_notifications": false,
"now": true,
"find_path": true,
"read_file": true,

View File

@@ -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,
);
}
self.stream_completion(request, 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.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 {
@@ -2146,6 +2198,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,
@@ -2193,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;
};
@@ -2273,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
}
}
@@ -3183,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)]
@@ -3583,6 +3693,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);
@@ -4137,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));
@@ -4210,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));
@@ -4286,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));
@@ -4393,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));
@@ -4479,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>,
@@ -4647,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>,
@@ -4808,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>,
@@ -5081,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);
@@ -5088,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));

View File

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

View File

@@ -64,8 +64,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::{
@@ -2913,6 +2914,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)
@@ -2921,13 +2937,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>,
@@ -3169,6 +3244,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(),
)

View File

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

View File

@@ -822,11 +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 let Some(key) = key_utf8.chars().next()
&& key_utf8.len() == 1
&& key.is_ascii()
{
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()
} else {
name
}
} else if let Some(key_en) = guess_ascii(keycode, modifiers.shift) {
String::from(key_en)
} else {

View File

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

View File

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

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>,
@@ -259,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 {
@@ -294,7 +304,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
});
@@ -347,6 +357,8 @@ impl KeymapEditor {
context_menu: None,
previous_edit: None,
humanized_action_names,
search_query_debounce: None,
show_hover_menus: true,
};
this.on_keymap_changed(cx);
@@ -371,10 +383,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 +508,7 @@ impl KeymapEditor {
}
this.selected_index.take();
this.matches = matches;
cx.notify();
})
}
@@ -650,7 +685,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");
@@ -685,14 +720,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))
}
@@ -724,40 +764,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);
@@ -786,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() {
@@ -806,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;
@@ -831,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);
@@ -839,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);
@@ -847,23 +892,38 @@ 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 {
self.show_hover_menus = false;
let Some((keybind, keybind_index)) = self.selected_keybind_and_index() else {
return;
};
let keybind = keybind.clone();
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();
@@ -872,7 +932,7 @@ impl KeymapEditor {
let modal = KeybindingEditorModal::new(
create,
keybind,
keybind_idx,
keybind_index,
keymap_editor,
workspace_weak,
fs,
@@ -899,7 +959,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 +989,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 +1006,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()));
}
@@ -972,18 +1036,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));
}
}
}
@@ -1087,20 +1149,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))
@@ -1113,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()
@@ -1214,6 +1278,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(
@@ -1254,16 +1330,16 @@ 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| {
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,
)
@@ -1284,18 +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::text("Edit Keybinding"))
.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))
@@ -1313,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(),
@@ -1351,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()
},
);
@@ -2222,6 +2323,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 +2335,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 +2377,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 +2387,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(())
}
@@ -2298,7 +2417,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>,
@@ -2326,7 +2444,6 @@ impl KeystrokeInput {
Self {
keystrokes: Vec::new(),
placeholder_keystrokes,
highlight_on_focus: true,
inner_focus_handle,
outer_focus_handle,
intercept_subscription: None,
@@ -2474,6 +2591,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);
@@ -2543,10 +2662,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;
@@ -2706,7 +2821,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

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

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

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition.workspace = true
name = "zed"
version = "0.196.0"
version = "0.196.3"
publish.workspace = true
license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.dev>"]

View File

@@ -1 +1 @@
dev
preview

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