Compare commits
7 Commits
v0.196.0-p
...
keymap-ui-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75794eb7c7 | ||
|
|
e23a4564cc | ||
|
|
f82ef1f76f | ||
|
|
b4c2ae5196 | ||
|
|
0023773c68 | ||
|
|
0bde929d54 | ||
|
|
6f60939d30 |
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))),
|
||||
)
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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>"]
|
||||
|
||||
@@ -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 in the Gemini docs for more.
|
||||
|
||||
#### Custom Models {#google-ai-custom-models}
|
||||
|
||||
|
||||
@@ -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"
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user