Compare commits
16 Commits
x11-debug-
...
codex
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0683f3cfbc | ||
|
|
c0261a1ea9 | ||
|
|
f43bcc1492 | ||
|
|
e23a4564cc | ||
|
|
f82ef1f76f | ||
|
|
b4c2ae5196 | ||
|
|
0023773c68 | ||
|
|
0bde929d54 | ||
|
|
6f60939d30 | ||
|
|
a6a7a1cc28 | ||
|
|
13f4a093c8 | ||
|
|
573836a654 | ||
|
|
048dc47d87 | ||
|
|
ffc69b07e5 | ||
|
|
dc8d0868ec | ||
|
|
58807f0dd2 |
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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
118
crates/agent_servers/src/codex.rs
Normal file
118
crates/agent_servers/src/codex.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
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 Codex;
|
||||
|
||||
const ACP_ARG: &str = "acp";
|
||||
|
||||
impl StdioAgentServer for Codex {
|
||||
fn name(&self) -> &'static str {
|
||||
"Codex"
|
||||
}
|
||||
|
||||
fn empty_state_headline(&self) -> &'static str {
|
||||
"Welcome to Codex"
|
||||
}
|
||||
|
||||
fn empty_state_message(&self) -> &'static str {
|
||||
"Ask questions, edit files, run commands.\nBe specific for the best results."
|
||||
}
|
||||
|
||||
fn logo(&self) -> ui::IconName {
|
||||
ui::IconName::AiOpenAi
|
||||
}
|
||||
|
||||
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
|
||||
.codex
|
||||
.as_ref()
|
||||
.map(|codex_settings| AgentServerCommand {
|
||||
path: codex_settings.command.path.clone(),
|
||||
args: codex_settings
|
||||
.command
|
||||
.args
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(std::iter::once(ACP_ARG.into()))
|
||||
.collect(),
|
||||
env: codex_settings.command.env.clone(),
|
||||
})
|
||||
})?;
|
||||
|
||||
if let Some(custom_command) = custom_command {
|
||||
return Ok(custom_command);
|
||||
}
|
||||
|
||||
if let Some(path) = find_bin_in_path("codex", project, cx).await {
|
||||
return Ok(AgentServerCommand {
|
||||
path,
|
||||
args: vec![ACP_ARG.into()],
|
||||
env: None,
|
||||
});
|
||||
}
|
||||
|
||||
todo!()
|
||||
// let (fs, node_runtime) = project.update(cx, |project, _| {
|
||||
// (project.fs().clone(), project.node_runtime().cloned())
|
||||
// })?;
|
||||
// let node_runtime = node_runtime.context("codex not found on path")?;
|
||||
|
||||
// let directory = ::paths::agent_servers_dir().join("codex");
|
||||
// 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 Codex {} doesn't support the Agentic Coding Protocol (ACP).",
|
||||
current_version
|
||||
).into(),
|
||||
upgrade_message: "Upgrade Codex to Latest".into(),
|
||||
upgrade_command: "npm install -g @openai/codex@latest".into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,14 +48,8 @@ use crate::{
|
||||
|
||||
pub fn router() -> Router {
|
||||
Router::new()
|
||||
.route(
|
||||
"/billing/preferences",
|
||||
get(get_billing_preferences).put(update_billing_preferences),
|
||||
)
|
||||
.route(
|
||||
"/billing/subscriptions",
|
||||
get(list_billing_subscriptions).post(create_billing_subscription),
|
||||
)
|
||||
.route("/billing/preferences", put(update_billing_preferences))
|
||||
.route("/billing/subscriptions", post(create_billing_subscription))
|
||||
.route(
|
||||
"/billing/subscriptions/manage",
|
||||
post(manage_billing_subscription),
|
||||
@@ -66,11 +61,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 +69,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,
|
||||
@@ -210,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 {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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))),
|
||||
)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
1
crates/languages/src/zed-keybind-context/brackets.scm
Normal file
1
crates/languages/src/zed-keybind-context/brackets.scm
Normal file
@@ -0,0 +1 @@
|
||||
("(" @open ")" @close)
|
||||
6
crates/languages/src/zed-keybind-context/config.toml
Normal file
6
crates/languages/src/zed-keybind-context/config.toml
Normal file
@@ -0,0 +1,6 @@
|
||||
name = "Zed Keybind Context"
|
||||
grammar = "rust"
|
||||
autoclose_before = ")"
|
||||
brackets = [
|
||||
{ start = "(", end = ")", close = true, newline = false },
|
||||
]
|
||||
23
crates/languages/src/zed-keybind-context/highlights.scm
Normal file
23
crates/languages/src/zed-keybind-context/highlights.scm
Normal file
@@ -0,0 +1,23 @@
|
||||
(identifier) @variable
|
||||
|
||||
[
|
||||
"("
|
||||
")"
|
||||
] @punctuation.bracket
|
||||
|
||||
[
|
||||
(integer_literal)
|
||||
(float_literal)
|
||||
] @number
|
||||
|
||||
(boolean_literal) @boolean
|
||||
|
||||
[
|
||||
"!="
|
||||
"=="
|
||||
"=>"
|
||||
">"
|
||||
"&&"
|
||||
"||"
|
||||
"!"
|
||||
] @operator
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
@@ -881,6 +923,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) => {
|
||||
@@ -902,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)]
|
||||
@@ -1479,10 +1534,6 @@ mod tests {
|
||||
]"#
|
||||
.unindent(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_append() {
|
||||
check_keymap_update(
|
||||
r#"[
|
||||
{
|
||||
@@ -1529,5 +1580,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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,19 +1,19 @@
|
||||
use std::{
|
||||
ops::{Not as _, Range},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
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::{
|
||||
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};
|
||||
@@ -33,7 +33,6 @@ use workspace::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
SettingsUiFeatureFlag,
|
||||
keybindings::persistence::KEYBINDING_EDITORS,
|
||||
ui_components::table::{Table, TableInteractionState},
|
||||
};
|
||||
@@ -48,7 +47,6 @@ actions!(
|
||||
]
|
||||
);
|
||||
|
||||
const KEYMAP_EDITOR_NAMESPACE: &'static str = "keymap_editor";
|
||||
actions!(
|
||||
keymap_editor,
|
||||
[
|
||||
@@ -115,42 +113,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);
|
||||
}
|
||||
|
||||
@@ -190,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)]
|
||||
@@ -288,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>,
|
||||
@@ -297,6 +267,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 +280,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
|
||||
@@ -332,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
|
||||
});
|
||||
|
||||
@@ -360,6 +331,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 +355,8 @@ impl KeymapEditor {
|
||||
selected_index: None,
|
||||
context_menu: None,
|
||||
previous_edit: None,
|
||||
humanized_action_names,
|
||||
search_query_debounce: None,
|
||||
};
|
||||
|
||||
this.on_keymap_changed(cx);
|
||||
@@ -400,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| {
|
||||
@@ -460,11 +463,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
|
||||
},
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -487,11 +501,12 @@ 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();
|
||||
this.matches = matches;
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
}
|
||||
@@ -505,7 +520,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 +551,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);
|
||||
|
||||
@@ -554,7 +572,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(),
|
||||
@@ -571,7 +589,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(),
|
||||
@@ -588,11 +606,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);
|
||||
|
||||
@@ -878,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();
|
||||
@@ -913,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 {
|
||||
@@ -943,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()));
|
||||
}
|
||||
|
||||
@@ -958,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()));
|
||||
}
|
||||
|
||||
@@ -986,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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1020,7 +1061,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>,
|
||||
@@ -1266,7 +1307,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))
|
||||
@@ -1313,13 +1354,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,
|
||||
@@ -1590,13 +1644,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 },
|
||||
)));
|
||||
@@ -1762,7 +1823,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) => {
|
||||
@@ -1812,7 +1873,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,
|
||||
@@ -2131,25 +2192,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()),
|
||||
@@ -2210,6 +2277,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")?;
|
||||
@@ -2219,6 +2289,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(())
|
||||
}
|
||||
|
||||
@@ -2254,6 +2331,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")?;
|
||||
@@ -2263,6 +2341,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(())
|
||||
}
|
||||
|
||||
@@ -2273,15 +2358,25 @@ 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>>,
|
||||
highlight_on_focus: bool,
|
||||
outer_focus_handle: FocusHandle,
|
||||
inner_focus_handle: FocusHandle,
|
||||
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>,
|
||||
}
|
||||
@@ -2303,7 +2398,6 @@ impl KeystrokeInput {
|
||||
Self {
|
||||
keystrokes: Vec::new(),
|
||||
placeholder_keystrokes,
|
||||
highlight_on_focus: true,
|
||||
inner_focus_handle,
|
||||
outer_focus_handle,
|
||||
intercept_subscription: None,
|
||||
@@ -2403,11 +2497,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));
|
||||
@@ -2424,11 +2521,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
|
||||
@@ -2471,10 +2576,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];
|
||||
}
|
||||
@@ -2508,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;
|
||||
@@ -2671,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
|
||||
@@ -2917,3 +3019,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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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()
|
||||
};
|
||||
|
||||
@@ -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