Compare commits

...

16 Commits

Author SHA1 Message Date
Conrad Irwin
0683f3cfbc +Codex 2025-07-16 20:33:52 -06:00
Anthony Eid
c0261a1ea9 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-16 18:05:26 -04:00
Marshall Bowers
f43bcc1492 collab: Remove GET /billing/subscriptions endpoint (#34580)
This PR removes the `GET /billing/subscriptions` endpoint, as it has
been moved to `cloud.zed.dev`.

Release Notes:

- N/A
2025-07-16 22:04:53 +00:00
Umesh Yadav
e23a4564cc keymap_ui: Open Keymap editor from settings dropdown (#34576)
@probably-neb I guess we should be opening the keymap editor from title
bar and menu as well. I believe this got missed in this: #34568.

Release Notes:

- Open Keymap editor from settings from menu and title bar.
2025-07-16 17:30:08 -04:00
Peter Tripp
f82ef1f76f agent: Support GEMINI_API_KEY environment variable (#34574)
Google Gemini Docs now recommend usage of `GEMINI_API_KEY` and the
legacy `GOOGLE_AI_API_KEY` variable is no longer supported in the modern
SDKs.

Zed will now accept either.

Release Notes:

- N/A
2025-07-16 20:55:54 +00:00
Richard Feldman
b4c2ae5196 Handle upstream_http_error completion responses (#34573)
Addresses upstream errors such as:
<img width="831" height="100" alt="Screenshot 2025-07-16 at 3 37 03 PM"
src="https://github.com/user-attachments/assets/2aeb0257-6761-4148-b687-25fae93c68d8"
/>

These should now automatically retry like other upstream HTTP error
codes.

Release Notes:

- N/A
2025-07-16 16:31:31 -04:00
Peter Tripp
0023773c68 docs: Add Zed as Git Editor example (#34572)
Release Notes:

- N/A
2025-07-16 19:57:02 +00:00
Anthony Eid
0bde929d54 Add keymap editor UI telemetry events (#34571)
- Search queries
- Keybinding update or removed
- Copy action name
- Copy context name

cc @katie-z-geer 

Release Notes:

- N/A

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-07-16 19:50:53 +00:00
Joseph T. Lyons
6f60939d30 Bump Zed to v0.197 (#34569)
Release Notes:

-N/A
2025-07-16 18:48:50 +00:00
Ben Kunkle
a6a7a1cc28 keymap_ui: Remove feature flag (#34568)
Closes #ISSUE

Release Notes:

- Rebound the keystroke to open the keymap file, to open the new keymap
editor
2025-07-16 18:28:44 +00:00
Anthony Eid
13f4a093c8 Improve keystroke search in keymap editor (#34567)
This PR improves Keystroke search by:

1.  Allow searching by modifiers without additional keys.
2. Take match count into consideration when deciding if we should show
an action as a search match.
3. Take order into consideration as well.

Release Notes:

- N/A

---------

Co-authored-by: Ben Kunkle <ben@zed.dev>
2025-07-16 18:19:41 +00:00
Ben Kunkle
573836a654 keymap_ui: Replace zed::NoAction with null (#34562)
Closes #ISSUE

This change applies both to the UI (we render `<null>` as muted text
instead of `zed::NoAction`) as well as how we update the keymap file
(the duplicated binding is bound to `null` instead of `"zed::NoAction"`)

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-07-16 17:55:58 +00:00
Marshall Bowers
048dc47d87 collab: Remove GET /billing/preferences endpoint (#34566)
This PR removes the `GET /billing/preferences` endpoint, as it has been
moved to `cloud.zed.dev`.

Release Notes:

- N/A
2025-07-16 17:55:01 +00:00
Smit Barmase
ffc69b07e5 editor: Fix sometimes green (+) cursor style appearing when cmd-clicking to navigate and back (#34557)
Regressed in https://github.com/zed-industries/zed/pull/33928

This PR clears the selection drag state when the editor focus is out.

To reproduce: 

1. Select some item in buffer that has a go to definition.
2. Cmd+Click mouse down on it, but don't let go.
3. Wait for 300ms+. 
4. Now cursor changed to green + (valid state, this is for selection
drag-n-drop).
5. Now let go of your mouse down, we switched to a different file.
Cursor looks normal.
6. Come back to the previous buffer, see green + cursor style (BUG!).

Release Notes:

- Fixed the issue where the green (+) cursor style sometimes appears
when navigating to the definition and then back to the previous buffer.
2025-07-16 23:24:02 +05:30
Piotr Osiewicz
dc8d0868ec project: Fix up documentation for Path Trie and add a test for having multiple present nodes (#34560)
cc @cole-miller I was worried with
https://github.com/zed-industries/zed/pull/34460#discussion_r2210814806
that PathTrie would not be able to support nested .git repositories, but
it seems fine.

Release Notes:

- N/A
2025-07-16 17:24:34 +00:00
Ben Kunkle
58807f0dd2 keymap_ui: Create language for Zed keybind context (#34558)
Closes #ISSUE

Creates a new language in the languages crate for the DSL used in Zed
keybinding context. Previously, keybind context was highlighted as Rust
in the keymap UI due to the expression syntax of Rust matching that of
the context DSL, however, this had the side effect of highlighting upper
case contexts (e.g. `Editor`) however Rust types would be highlighted
based on the theme. By extracting only the necessary pieces of the Rust
language `highlights.scm`, `brackets.scm`, and `config.toml`, and
continuing to use the Rust grammar, we get a better result across
different themes

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-07-16 17:00:47 +00:00
27 changed files with 715 additions and 345 deletions

4
Cargo.lock generated
View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -178,6 +178,21 @@ pub enum LanguageModelCompletionError {
}
impl LanguageModelCompletionError {
fn parse_upstream_error_json(message: &str) -> Option<(StatusCode, String)> {
let error_json = serde_json::from_str::<serde_json::Value>(message).ok()?;
let upstream_status = error_json
.get("upstream_status")
.and_then(|v| v.as_u64())
.and_then(|status| u16::try_from(status).ok())
.and_then(|status| StatusCode::from_u16(status).ok())?;
let inner_message = error_json
.get("message")
.and_then(|v| v.as_str())
.unwrap_or(message)
.to_string();
Some((upstream_status, inner_message))
}
pub fn from_cloud_failure(
upstream_provider: LanguageModelProviderName,
code: String,
@@ -191,6 +206,18 @@ impl LanguageModelCompletionError {
Self::PromptTooLarge {
tokens: Some(tokens),
}
} else if code == "upstream_http_error" {
if let Some((upstream_status, inner_message)) =
Self::parse_upstream_error_json(&message)
{
return Self::from_http_status(
upstream_provider,
upstream_status,
inner_message,
retry_after,
);
}
anyhow!("completion request failed, code: {code}, message: {message}").into()
} else if let Some(status_code) = code
.strip_prefix("upstream_http_")
.and_then(|code| StatusCode::from_str(code).ok())
@@ -701,3 +728,104 @@ impl From<String> for LanguageModelProviderName {
Self(SharedString::from(value))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_cloud_failure_with_upstream_http_error() {
let error = LanguageModelCompletionError::from_cloud_failure(
String::from("anthropic").into(),
"upstream_http_error".to_string(),
r#"{"code":"upstream_http_error","message":"Received an error from the Anthropic API: upstream connect error or disconnect/reset before headers. reset reason: connection timeout","upstream_status":503}"#.to_string(),
None,
);
match error {
LanguageModelCompletionError::ServerOverloaded { provider, .. } => {
assert_eq!(provider.0, "anthropic");
}
_ => panic!(
"Expected ServerOverloaded error for 503 status, got: {:?}",
error
),
}
let error = LanguageModelCompletionError::from_cloud_failure(
String::from("anthropic").into(),
"upstream_http_error".to_string(),
r#"{"code":"upstream_http_error","message":"Internal server error","upstream_status":500}"#.to_string(),
None,
);
match error {
LanguageModelCompletionError::ApiInternalServerError { provider, message } => {
assert_eq!(provider.0, "anthropic");
assert_eq!(message, "Internal server error");
}
_ => panic!(
"Expected ApiInternalServerError for 500 status, got: {:?}",
error
),
}
}
#[test]
fn test_from_cloud_failure_with_standard_format() {
let error = LanguageModelCompletionError::from_cloud_failure(
String::from("anthropic").into(),
"upstream_http_503".to_string(),
"Service unavailable".to_string(),
None,
);
match error {
LanguageModelCompletionError::ServerOverloaded { provider, .. } => {
assert_eq!(provider.0, "anthropic");
}
_ => panic!("Expected ServerOverloaded error for upstream_http_503"),
}
}
#[test]
fn test_upstream_http_error_connection_timeout() {
let error = LanguageModelCompletionError::from_cloud_failure(
String::from("anthropic").into(),
"upstream_http_error".to_string(),
r#"{"code":"upstream_http_error","message":"Received an error from the Anthropic API: upstream connect error or disconnect/reset before headers. reset reason: connection timeout","upstream_status":503}"#.to_string(),
None,
);
match error {
LanguageModelCompletionError::ServerOverloaded { provider, .. } => {
assert_eq!(provider.0, "anthropic");
}
_ => panic!(
"Expected ServerOverloaded error for connection timeout with 503 status, got: {:?}",
error
),
}
let error = LanguageModelCompletionError::from_cloud_failure(
String::from("anthropic").into(),
"upstream_http_error".to_string(),
r#"{"code":"upstream_http_error","message":"Received an error from the Anthropic API: upstream connect error or disconnect/reset before headers. reset reason: connection timeout","upstream_status":500}"#.to_string(),
None,
);
match error {
LanguageModelCompletionError::ApiInternalServerError { provider, message } => {
assert_eq!(provider.0, "anthropic");
assert_eq!(
message,
"Received an error from the Anthropic API: upstream connect error or disconnect/reset before headers. reset reason: connection timeout"
);
}
_ => panic!(
"Expected ApiInternalServerError for connection timeout with 500 status, got: {:?}",
error
),
}
}
}

View File

@@ -94,6 +94,7 @@ pub struct State {
_subscription: Subscription,
}
const GEMINI_API_KEY_VAR: &str = "GEMINI_API_KEY";
const GOOGLE_AI_API_KEY_VAR: &str = "GOOGLE_AI_API_KEY";
impl State {
@@ -151,6 +152,8 @@ impl State {
cx.spawn(async move |this, cx| {
let (api_key, from_env) = if let Ok(api_key) = std::env::var(GOOGLE_AI_API_KEY_VAR) {
(api_key, true)
} else if let Ok(api_key) = std::env::var(GEMINI_API_KEY_VAR) {
(api_key, true)
} else {
let (_, api_key) = credentials_provider
.read_credentials(&api_url, &cx)
@@ -903,7 +906,7 @@ impl Render for ConfigurationView {
)
.child(
Label::new(
format!("You can also assign the {GOOGLE_AI_API_KEY_VAR} environment variable and restart Zed."),
format!("You can also assign the {GEMINI_API_KEY_VAR} environment variable and restart Zed."),
)
.size(LabelSize::Small).color(Color::Muted),
)
@@ -922,7 +925,7 @@ impl Render for ConfigurationView {
.gap_1()
.child(Icon::new(IconName::Check).color(Color::Success))
.child(Label::new(if env_var_set {
format!("API key set in {GOOGLE_AI_API_KEY_VAR} environment variable.")
format!("API key set in {GEMINI_API_KEY_VAR} environment variable.")
} else {
"API key configured.".to_string()
})),
@@ -935,7 +938,7 @@ impl Render for ConfigurationView {
.icon_position(IconPosition::Start)
.disabled(env_var_set)
.when(env_var_set, |this| {
this.tooltip(Tooltip::text(format!("To reset your API key, unset the {GOOGLE_AI_API_KEY_VAR} environment variable.")))
this.tooltip(Tooltip::text(format!("To reset your API key, make sure {GEMINI_API_KEY_VAR} and {GOOGLE_AI_API_KEY_VAR} environment variables are unset.")))
})
.on_click(cx.listener(|this, _, window, cx| this.reset_api_key(window, cx))),
)

View File

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

View File

@@ -0,0 +1 @@
("(" @open ")" @close)

View File

@@ -0,0 +1,6 @@
name = "Zed Keybind Context"
grammar = "rust"
autoclose_before = ")"
brackets = [
{ start = "(", end = ")", close = true, newline = false },
]

View File

@@ -0,0 +1,23 @@
(identifier) @variable
[
"("
")"
] @punctuation.bracket
[
(integer_literal)
(float_literal)
] @number
(boolean_literal) @boolean
[
"!="
"=="
"=>"
">"
"&&"
"||"
"!"
] @operator

View File

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

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

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

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

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

View File

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

View File

@@ -1,5 +1,6 @@
use collab_ui::collab_panel;
use gpui::{Menu, MenuItem, OsAction};
use settings_ui::keybindings;
use terminal_view::terminal_panel;
pub fn app_menus() -> Vec<Menu> {
@@ -16,7 +17,7 @@ pub fn app_menus() -> Vec<Menu> {
name: "Settings".into(),
items: vec![
MenuItem::action("Open Settings", super::OpenSettings),
MenuItem::action("Open Key Bindings", zed_actions::OpenKeymap),
MenuItem::action("Open Key Bindings", keybindings::OpenKeymapEditor),
MenuItem::action("Open Default Settings", super::OpenDefaultSettings),
MenuItem::action(
"Open Default Key Bindings",

View File

@@ -237,7 +237,7 @@ You can use Gemini models with the Zed agent by choosing it via the model dropdo
The Google AI API key will be saved in your keychain.
Zed will also use the `GOOGLE_AI_API_KEY` environment variable if it's defined.
Zed will also use the `GEMINI_API_KEY` environment variable if it's defined. See [Using Gemini API keys](Using Gemini API keys) in the Gemini docs for more.
#### Custom Models {#google-ai-custom-models}

View File

@@ -151,3 +151,17 @@ When viewing files with changes, Zed displays diff hunks that can be expanded or
| {#action editor::ToggleSelectedDiffHunks} | {#kb editor::ToggleSelectedDiffHunks} |
> Not all actions have default keybindings, but can be bound by [customizing your keymap](./key-bindings.md#user-keymaps).
## Git CLI Configuration
If you would like to also use Zed for your [git commit message editor](https://git-scm.com/book/en/v2/Customizing-Git-Git-Configuration#_core_editor) when committing from the command line you can use `zed --wait`:
```sh
git config --global core.editor "zed --wait"
```
Or add the following to your shell environment (in `~/.zshrc`, `~/.bashrc`, etc):
```sh
export GIT_EDITOR="zed --wait"
```