Merge branch 'main' into mcp-acp-gemini

Agus Zubiaga created

Change summary

Cargo.lock                                    |   1 
assets/keymaps/linux/jetbrains.json           |  27 +
assets/keymaps/macos/jetbrains.json           |  22 +
crates/assistant_tools/src/edit_file_tool.rs  | 288 +++++++++++++++-----
crates/collab/src/api.rs                      |   1 
crates/collab/src/api/billing.rs              |  69 ----
crates/editor/src/code_completion_tests.rs    |  24 +
crates/editor/src/code_context_menus.rs       |   6 
crates/gpui/Cargo.toml                        |   4 
crates/gpui/src/platform/keystroke.rs         |   6 
crates/gpui/src/tab_stop.rs                   |   2 
crates/gpui/src/window.rs                     |   8 
crates/onboarding/Cargo.toml                  |   1 
crates/onboarding/src/onboarding.rs           |  36 ++
crates/onboarding/src/welcome.rs              | 276 ++++++++++++++++++++
crates/settings_ui/src/keybindings.rs         |  11 
crates/settings_ui/src/ui_components/table.rs |  19 
crates/ui/src/components/keybinding.rs        |   2 
crates/ui/src/components/popover.rs           |   2 
docs/src/extensions/installing-extensions.md  |   2 
docs/src/getting-started.md                   |   2 
docs/src/key-bindings.md                      |   2 
22 files changed, 637 insertions(+), 174 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -11028,6 +11028,7 @@ dependencies = [
  "ui",
  "workspace",
  "workspace-hack",
+ "zed_actions",
 ]
 
 [[package]]

assets/keymaps/linux/jetbrains.json 🔗

@@ -4,6 +4,7 @@
       "ctrl-alt-s": "zed::OpenSettings",
       "ctrl-{": "pane::ActivatePreviousItem",
       "ctrl-}": "pane::ActivateNextItem",
+      "shift-escape": null, // Unmap workspace::zoom
       "ctrl-f2": "debugger::Stop",
       "f6": "debugger::Pause",
       "f7": "debugger::StepInto",
@@ -44,8 +45,8 @@
       "ctrl-alt-right": "pane::GoForward",
       "alt-f7": "editor::FindAllReferences",
       "ctrl-alt-f7": "editor::FindAllReferences",
-      // "ctrl-b": "editor::GoToDefinition", // Conflicts with workspace::ToggleLeftDock
-      // "ctrl-alt-b": "editor::GoToDefinitionSplit", // Conflicts with workspace::ToggleLeftDock
+      "ctrl-b": "editor::GoToDefinition", // Conflicts with workspace::ToggleLeftDock
+      "ctrl-alt-b": "editor::GoToDefinitionSplit", // Conflicts with workspace::ToggleRightDock
       "ctrl-shift-b": "editor::GoToTypeDefinition",
       "ctrl-alt-shift-b": "editor::GoToTypeDefinitionSplit",
       "f2": "editor::GoToDiagnostic",
@@ -100,12 +101,27 @@
       "shift shift": "command_palette::Toggle",
       "ctrl-alt-shift-n": "project_symbols::Toggle",
       "alt-0": "git_panel::ToggleFocus",
-      "alt-1": "workspace::ToggleLeftDock",
+      "alt-1": "project_panel::ToggleFocus",
       "alt-5": "debug_panel::ToggleFocus",
       "alt-6": "diagnostics::Deploy",
       "alt-7": "outline_panel::ToggleFocus"
     }
   },
+  {
+    "context": "Pane", // this is to override the default Pane mappings to switch tabs
+    "bindings": {
+      "alt-1": "project_panel::ToggleFocus",
+      "alt-2": null, // Bookmarks (left dock)
+      "alt-3": null, // Find Panel (bottom dock)
+      "alt-4": null, // Run Panel (bottom dock)
+      "alt-5": "debug_panel::ToggleFocus",
+      "alt-6": "diagnostics::Deploy",
+      "alt-7": "outline_panel::ToggleFocus",
+      "alt-8": null, // Services (bottom dock)
+      "alt-9": null, // Git History (bottom dock)
+      "alt-0": "git_panel::ToggleFocus"
+    }
+  },
   {
     "context": "Workspace || Editor",
     "bindings": {
@@ -151,6 +167,9 @@
   { "context": "OutlinePanel", "bindings": { "alt-7": "workspace::CloseActiveDock" } },
   {
     "context": "Dock || Workspace || Terminal || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)",
-    "bindings": { "escape": "editor::ToggleFocus" }
+    "bindings": {
+      "escape": "editor::ToggleFocus",
+      "shift-escape": "workspace::CloseActiveDock"
+    }
   }
 ]

assets/keymaps/macos/jetbrains.json 🔗

@@ -4,6 +4,7 @@
       "cmd-{": "pane::ActivatePreviousItem",
       "cmd-}": "pane::ActivateNextItem",
       "cmd-0": "git_panel::ToggleFocus", // overrides `cmd-0` zoom reset
+      "shift-escape": null, // Unmap workspace::zoom
       "ctrl-f2": "debugger::Stop",
       "f6": "debugger::Pause",
       "f7": "debugger::StepInto",
@@ -108,6 +109,21 @@
       "cmd-7": "outline_panel::ToggleFocus"
     }
   },
+  {
+    "context": "Pane", // this is to override the default Pane mappings to switch tabs
+    "bindings": {
+      "cmd-1": "project_panel::ToggleFocus",
+      "cmd-2": null, // Bookmarks (left dock)
+      "cmd-3": null, // Find Panel (bottom dock)
+      "cmd-4": null, // Run Panel (bottom dock)
+      "cmd-5": "debug_panel::ToggleFocus",
+      "cmd-6": "diagnostics::Deploy",
+      "cmd-7": "outline_panel::ToggleFocus",
+      "cmd-8": null, // Services (bottom dock)
+      "cmd-9": null, // Git History (bottom dock)
+      "cmd-0": "git_panel::ToggleFocus"
+    }
+  },
   {
     "context": "Workspace || Editor",
     "bindings": {
@@ -146,11 +162,15 @@
     }
   },
   { "context": "GitPanel", "bindings": { "cmd-0": "workspace::CloseActiveDock" } },
+  { "context": "ProjectPanel", "bindings": { "cmd-1": "workspace::CloseActiveDock" } },
   { "context": "DebugPanel", "bindings": { "cmd-5": "workspace::CloseActiveDock" } },
   { "context": "Diagnostics > Editor", "bindings": { "cmd-6": "pane::CloseActiveItem" } },
   { "context": "OutlinePanel", "bindings": { "cmd-7": "workspace::CloseActiveDock" } },
   {
     "context": "Dock || Workspace || Terminal || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)",
-    "bindings": { "escape": "editor::ToggleFocus" }
+    "bindings": {
+      "escape": "editor::ToggleFocus",
+      "shift-escape": "workspace::CloseActiveDock"
+    }
   }
 ]

crates/assistant_tools/src/edit_file_tool.rs 🔗

@@ -25,6 +25,7 @@ use language::{
 };
 use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
 use markdown::{Markdown, MarkdownElement, MarkdownStyle};
+use paths;
 use project::{
     Project, ProjectPath,
     lsp_store::{FormatTrigger, LspFormatTarget},
@@ -141,27 +142,32 @@ impl Tool for EditFileTool {
             return false;
         };
 
-        let path = Path::new(&input.path);
-
-        // If any path component is ".zed", then this could affect
+        // If any path component matches the local settings folder, then this could affect
         // the editor in ways beyond the project source, so prompt.
+        let local_settings_folder = paths::local_settings_folder_relative_path();
+        let path = Path::new(&input.path);
         if path
             .components()
-            .any(|component| component.as_os_str() == ".zed")
+            .any(|component| component.as_os_str() == local_settings_folder.as_os_str())
         {
             return true;
         }
 
-        // If the path is outside the project, then prompt.
-        let is_outside_project = project
-            .read(cx)
-            .find_project_path(&input.path, cx)
-            .is_none();
-        if is_outside_project {
-            return true;
+        // It's also possible that the global config dir is configured to be inside the project,
+        // so check for that edge case too.
+        if let Ok(canonical_path) = std::fs::canonicalize(&input.path) {
+            if canonical_path.starts_with(paths::config_dir()) {
+                return true;
+            }
         }
 
-        false
+        // Check if path is inside the global config directory
+        // First check if it's already inside project - if not, try to canonicalize
+        let project_path = project.read(cx).find_project_path(&input.path, cx);
+
+        // If the path is inside the project, and it's not one of the above edge cases,
+        // then no confirmation is necessary. Otherwise, confirmation is necessary.
+        project_path.is_none()
     }
 
     fn may_perform_edits(&self) -> bool {
@@ -187,8 +193,16 @@ impl Tool for EditFileTool {
                 let mut description = input.display_description.clone();
 
                 // Add context about why confirmation may be needed
-                if path.components().any(|c| c.as_os_str() == ".zed") {
-                    description.push_str(" (Zed settings)");
+                let local_settings_folder = paths::local_settings_folder_relative_path();
+                if path
+                    .components()
+                    .any(|c| c.as_os_str() == local_settings_folder.as_os_str())
+                {
+                    description.push_str(" (local settings)");
+                } else if let Ok(canonical_path) = std::fs::canonicalize(&input.path) {
+                    if canonical_path.starts_with(paths::config_dir()) {
+                        description.push_str(" (global settings)");
+                    }
                 }
 
                 description
@@ -1219,19 +1233,20 @@ async fn build_buffer_diff(
 #[cfg(test)]
 mod tests {
     use super::*;
+    use ::fs::Fs;
     use client::TelemetrySettings;
-    use fs::{FakeFs, Fs};
     use gpui::{TestAppContext, UpdateGlobal};
     use language_model::fake_provider::FakeLanguageModel;
     use serde_json::json;
     use settings::SettingsStore;
+    use std::fs;
     use util::path;
 
     #[gpui::test]
     async fn test_edit_nonexistent_file(cx: &mut TestAppContext) {
         init_test(cx);
 
-        let fs = FakeFs::new(cx.executor());
+        let fs = project::FakeFs::new(cx.executor());
         fs.insert_tree("/root", json!({})).await;
         let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
         let action_log = cx.new(|_| ActionLog::new(project.clone()));
@@ -1321,7 +1336,7 @@ mod tests {
     ) -> anyhow::Result<ProjectPath> {
         init_test(cx);
 
-        let fs = FakeFs::new(cx.executor());
+        let fs = project::FakeFs::new(cx.executor());
         fs.insert_tree(
             "/root",
             json!({
@@ -1433,11 +1448,25 @@ mod tests {
         });
     }
 
+    fn init_test_with_config(cx: &mut TestAppContext, data_dir: &Path) {
+        cx.update(|cx| {
+            // Set custom data directory (config will be under data_dir/config)
+            paths::set_custom_data_dir(data_dir.to_str().unwrap());
+
+            let settings_store = SettingsStore::test(cx);
+            cx.set_global(settings_store);
+            language::init(cx);
+            TelemetrySettings::register(cx);
+            agent_settings::AgentSettings::register(cx);
+            Project::init_settings(cx);
+        });
+    }
+
     #[gpui::test]
     async fn test_format_on_save(cx: &mut TestAppContext) {
         init_test(cx);
 
-        let fs = FakeFs::new(cx.executor());
+        let fs = project::FakeFs::new(cx.executor());
         fs.insert_tree("/root", json!({"src": {}})).await;
 
         let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
@@ -1636,7 +1665,7 @@ mod tests {
     async fn test_remove_trailing_whitespace(cx: &mut TestAppContext) {
         init_test(cx);
 
-        let fs = FakeFs::new(cx.executor());
+        let fs = project::FakeFs::new(cx.executor());
         fs.insert_tree("/root", json!({"src": {}})).await;
 
         // Create a simple file with trailing whitespace
@@ -1773,7 +1802,7 @@ mod tests {
     async fn test_needs_confirmation(cx: &mut TestAppContext) {
         init_test(cx);
         let tool = Arc::new(EditFileTool);
-        let fs = FakeFs::new(cx.executor());
+        let fs = project::FakeFs::new(cx.executor());
         fs.insert_tree("/root", json!({})).await;
 
         // Test 1: Path with .zed component should require confirmation
@@ -1847,44 +1876,66 @@ mod tests {
     }
 
     #[gpui::test]
-    fn test_ui_text_with_confirmation_context(cx: &mut TestAppContext) {
-        init_test(cx);
+    async fn test_ui_text_shows_correct_context(cx: &mut TestAppContext) {
+        // Set up a custom config directory for testing
+        let temp_dir = tempfile::tempdir().unwrap();
+        init_test_with_config(cx, temp_dir.path());
+
         let tool = Arc::new(EditFileTool);
 
-        // Test ui_text shows context for .zed paths
-        let input_zed = json!({
-            "display_description": "Update settings",
-            "path": ".zed/settings.json",
-            "mode": "edit"
-        });
-        cx.update(|_cx| {
-            let ui_text = tool.ui_text(&input_zed);
-            assert_eq!(
-                ui_text, "Update settings (Zed settings)",
-                "UI text should indicate Zed settings file"
-            );
-        });
+        // Test ui_text shows context for various paths
+        let test_cases = vec![
+            (
+                json!({
+                    "display_description": "Update config",
+                    "path": ".zed/settings.json",
+                    "mode": "edit"
+                }),
+                "Update config (local settings)",
+                ".zed path should show local settings context",
+            ),
+            (
+                json!({
+                    "display_description": "Fix bug",
+                    "path": "src/.zed/local.json",
+                    "mode": "edit"
+                }),
+                "Fix bug (local settings)",
+                "Nested .zed path should show local settings context",
+            ),
+            (
+                json!({
+                    "display_description": "Update readme",
+                    "path": "README.md",
+                    "mode": "edit"
+                }),
+                "Update readme",
+                "Normal path should not show additional context",
+            ),
+            (
+                json!({
+                    "display_description": "Edit config",
+                    "path": "config.zed",
+                    "mode": "edit"
+                }),
+                "Edit config",
+                ".zed as extension should not show context",
+            ),
+        ];
 
-        // Test ui_text for normal paths
-        let input_normal = json!({
-            "display_description": "Edit source file",
-            "path": "src/main.rs",
-            "mode": "edit"
-        });
-        cx.update(|_cx| {
-            let ui_text = tool.ui_text(&input_normal);
-            assert_eq!(
-                ui_text, "Edit source file",
-                "UI text should not have additional context for normal paths"
-            );
-        });
+        for (input, expected_text, description) in test_cases {
+            cx.update(|_cx| {
+                let ui_text = tool.ui_text(&input);
+                assert_eq!(ui_text, expected_text, "Failed for case: {}", description);
+            });
+        }
     }
 
     #[gpui::test]
     async fn test_needs_confirmation_outside_project(cx: &mut TestAppContext) {
         init_test(cx);
         let tool = Arc::new(EditFileTool);
-        let fs = FakeFs::new(cx.executor());
+        let fs = project::FakeFs::new(cx.executor());
 
         // Create a project in /project directory
         fs.insert_tree("/project", json!({})).await;
@@ -1918,33 +1969,58 @@ mod tests {
     }
 
     #[gpui::test]
-    async fn test_needs_confirmation_zed_paths(cx: &mut TestAppContext) {
-        init_test(cx);
+    async fn test_needs_confirmation_config_paths(cx: &mut TestAppContext) {
+        // Set up a custom data directory for testing
+        let temp_dir = tempfile::tempdir().unwrap();
+        init_test_with_config(cx, temp_dir.path());
+
         let tool = Arc::new(EditFileTool);
-        let fs = FakeFs::new(cx.executor());
+        let fs = project::FakeFs::new(cx.executor());
         fs.insert_tree("/home/user/myproject", json!({})).await;
         let project = Project::test(fs.clone(), [path!("/home/user/myproject").as_ref()], cx).await;
 
-        // Test various .zed path patterns
+        // Get the actual local settings folder name
+        let local_settings_folder = paths::local_settings_folder_relative_path();
+
+        // Test various config path patterns
         let test_cases = vec![
-            (".zed/settings.json", true, "Top-level .zed file"),
-            ("myproject/.zed/settings.json", true, ".zed in project path"),
-            ("src/.zed/config.toml", true, ".zed in subdirectory"),
             (
-                ".zed.backup/file.txt",
+                format!("{}/settings.json", local_settings_folder.display()),
+                true,
+                "Top-level local settings file".to_string(),
+            ),
+            (
+                format!(
+                    "myproject/{}/settings.json",
+                    local_settings_folder.display()
+                ),
+                true,
+                "Local settings in project path".to_string(),
+            ),
+            (
+                format!("src/{}/config.toml", local_settings_folder.display()),
+                true,
+                "Local settings in subdirectory".to_string(),
+            ),
+            (
+                ".zed.backup/file.txt".to_string(),
                 true,
-                ".zed.backup is outside project (not a .zed component issue)",
+                ".zed.backup is outside project".to_string(),
             ),
             (
-                "my.zed/file.txt",
+                "my.zed/file.txt".to_string(),
                 true,
-                "my.zed is outside project (not a .zed component issue)",
+                "my.zed is outside project".to_string(),
+            ),
+            (
+                "myproject/src/file.zed".to_string(),
+                false,
+                ".zed as file extension".to_string(),
             ),
-            ("myproject/src/file.zed", false, ".zed as file extension"),
             (
-                "myproject/normal/path/file.rs",
+                "myproject/normal/path/file.rs".to_string(),
                 false,
-                "Normal file without .zed",
+                "Normal file without config paths".to_string(),
             ),
         ];
 
@@ -1966,11 +2042,69 @@ mod tests {
         }
     }
 
+    #[gpui::test]
+    async fn test_needs_confirmation_global_config(cx: &mut TestAppContext) {
+        // Set up a custom data directory for testing
+        let temp_dir = tempfile::tempdir().unwrap();
+        init_test_with_config(cx, temp_dir.path());
+
+        let tool = Arc::new(EditFileTool);
+        let fs = project::FakeFs::new(cx.executor());
+
+        // Create test files in the global config directory
+        let global_config_dir = paths::config_dir();
+        fs::create_dir_all(&global_config_dir).unwrap();
+        let global_settings_path = global_config_dir.join("settings.json");
+        fs::write(&global_settings_path, "{}").unwrap();
+
+        fs.insert_tree("/project", json!({})).await;
+        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
+
+        // Test global config paths
+        let test_cases = vec![
+            (
+                global_settings_path.to_str().unwrap().to_string(),
+                true,
+                "Global settings file should require confirmation",
+            ),
+            (
+                global_config_dir
+                    .join("keymap.json")
+                    .to_str()
+                    .unwrap()
+                    .to_string(),
+                true,
+                "Global keymap file should require confirmation",
+            ),
+            (
+                "project/normal_file.rs".to_string(),
+                false,
+                "Normal project file should not require confirmation",
+            ),
+        ];
+
+        for (path, should_confirm, description) in test_cases {
+            let input = json!({
+                "display_description": "Edit file",
+                "path": path,
+                "mode": "edit"
+            });
+            cx.update(|cx| {
+                assert_eq!(
+                    tool.needs_confirmation(&input, &project, cx),
+                    should_confirm,
+                    "Failed for case: {}",
+                    description
+                );
+            });
+        }
+    }
+
     #[gpui::test]
     async fn test_needs_confirmation_with_multiple_worktrees(cx: &mut TestAppContext) {
         init_test(cx);
         let tool = Arc::new(EditFileTool);
-        let fs = FakeFs::new(cx.executor());
+        let fs = project::FakeFs::new(cx.executor());
 
         // Create multiple worktree directories
         fs.insert_tree(
@@ -2052,7 +2186,7 @@ mod tests {
     async fn test_needs_confirmation_edge_cases(cx: &mut TestAppContext) {
         init_test(cx);
         let tool = Arc::new(EditFileTool);
-        let fs = FakeFs::new(cx.executor());
+        let fs = project::FakeFs::new(cx.executor());
         fs.insert_tree(
             "/project",
             json!({
@@ -2112,7 +2246,7 @@ mod tests {
     }
 
     #[gpui::test]
-    async fn test_ui_text_shows_correct_context(cx: &mut TestAppContext) {
+    async fn test_ui_text_with_all_path_types(cx: &mut TestAppContext) {
         init_test(cx);
         let tool = Arc::new(EditFileTool);
 
@@ -2124,8 +2258,8 @@ mod tests {
                     "path": ".zed/settings.json",
                     "mode": "edit"
                 }),
-                "Update config (Zed settings)",
-                ".zed path should show Zed settings context",
+                "Update config (local settings)",
+                ".zed path should show local settings context",
             ),
             (
                 json!({
@@ -2133,8 +2267,8 @@ mod tests {
                     "path": "src/.zed/local.json",
                     "mode": "edit"
                 }),
-                "Fix bug (Zed settings)",
-                "Nested .zed path should show Zed settings context",
+                "Fix bug (local settings)",
+                "Nested .zed path should show local settings context",
             ),
             (
                 json!({
@@ -2168,7 +2302,7 @@ mod tests {
     async fn test_needs_confirmation_with_different_modes(cx: &mut TestAppContext) {
         init_test(cx);
         let tool = Arc::new(EditFileTool);
-        let fs = FakeFs::new(cx.executor());
+        let fs = project::FakeFs::new(cx.executor());
         fs.insert_tree(
             "/project",
             json!({
@@ -2235,9 +2369,12 @@ mod tests {
 
     #[gpui::test]
     async fn test_always_allow_tool_actions_bypasses_all_checks(cx: &mut TestAppContext) {
-        init_test(cx);
+        // Set up with custom directories for deterministic testing
+        let temp_dir = tempfile::tempdir().unwrap();
+        init_test_with_config(cx, temp_dir.path());
+
         let tool = Arc::new(EditFileTool);
-        let fs = FakeFs::new(cx.executor());
+        let fs = project::FakeFs::new(cx.executor());
         fs.insert_tree("/project", json!({})).await;
         let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
 
@@ -2249,9 +2386,14 @@ mod tests {
         });
 
         // Test that all paths that normally require confirmation are bypassed
+        let global_settings_path = paths::config_dir().join("settings.json");
+        fs::create_dir_all(paths::config_dir()).unwrap();
+        fs::write(&global_settings_path, "{}").unwrap();
+
         let test_cases = vec![
             ".zed/settings.json",
             "project/.zed/config.toml",
+            global_settings_path.to_str().unwrap(),
             "/etc/hosts",
             "/absolute/path/file.txt",
             "../outside/project.txt",

crates/collab/src/api.rs 🔗

@@ -106,7 +106,6 @@ pub fn routes(rpc_server: Arc<rpc::Server>) -> Router<(), Body> {
         .route("/users/:id/refresh_llm_tokens", post(refresh_llm_tokens))
         .route("/users/:id/update_plan", post(update_plan))
         .route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
-        .merge(billing::router())
         .merge(contributors::router())
         .layer(
             ServiceBuilder::new()

crates/collab/src/api/billing.rs 🔗

@@ -1,15 +1,13 @@
 use anyhow::{Context as _, bail};
-use axum::{Extension, Json, Router, extract, routing::post};
 use chrono::{DateTime, Utc};
 use collections::{HashMap, HashSet};
-use reqwest::StatusCode;
 use sea_orm::ActiveValue;
-use serde::{Deserialize, Serialize};
 use std::{sync::Arc, time::Duration};
 use stripe::{CancellationDetailsReason, EventObject, EventType, ListEvents, SubscriptionStatus};
 use util::{ResultExt, maybe};
 use zed_llm_client::LanguageModelProvider;
 
+use crate::AppState;
 use crate::db::billing_subscription::{
     StripeCancellationReason, StripeSubscriptionStatus, SubscriptionKind,
 };
@@ -19,7 +17,6 @@ use crate::stripe_client::{
     StripeCancellationDetailsReason, StripeClient, StripeCustomerId, StripeSubscription,
     StripeSubscriptionId,
 };
-use crate::{AppState, Error, Result};
 use crate::{db::UserId, llm::db::LlmDatabase};
 use crate::{
     db::{
@@ -30,70 +27,6 @@ use crate::{
     stripe_billing::StripeBilling,
 };
 
-pub fn router() -> Router {
-    Router::new().route(
-        "/billing/subscriptions/sync",
-        post(sync_billing_subscription),
-    )
-}
-
-#[derive(Debug, Deserialize)]
-struct SyncBillingSubscriptionBody {
-    github_user_id: i32,
-}
-
-#[derive(Debug, Serialize)]
-struct SyncBillingSubscriptionResponse {
-    stripe_customer_id: String,
-}
-
-async fn sync_billing_subscription(
-    Extension(app): Extension<Arc<AppState>>,
-    extract::Json(body): extract::Json<SyncBillingSubscriptionBody>,
-) -> Result<Json<SyncBillingSubscriptionResponse>> {
-    let Some(stripe_client) = app.stripe_client.clone() else {
-        log::error!("failed to retrieve Stripe client");
-        Err(Error::http(
-            StatusCode::NOT_IMPLEMENTED,
-            "not supported".into(),
-        ))?
-    };
-
-    let user = app
-        .db
-        .get_user_by_github_user_id(body.github_user_id)
-        .await?
-        .context("user not found")?;
-
-    let billing_customer = app
-        .db
-        .get_billing_customer_by_user_id(user.id)
-        .await?
-        .context("billing customer not found")?;
-    let stripe_customer_id = StripeCustomerId(billing_customer.stripe_customer_id.clone().into());
-
-    let subscriptions = stripe_client
-        .list_subscriptions_for_customer(&stripe_customer_id)
-        .await?;
-
-    for subscription in subscriptions {
-        let subscription_id = subscription.id.clone();
-
-        sync_subscription(&app, &stripe_client, subscription)
-            .await
-            .with_context(|| {
-                format!(
-                    "failed to sync subscription {subscription_id} for user {}",
-                    user.id,
-                )
-            })?;
-    }
-
-    Ok(Json(SyncBillingSubscriptionResponse {
-        stripe_customer_id: billing_customer.stripe_customer_id.clone(),
-    }))
-}
-
 /// The amount of time we wait in between each poll of Stripe events.
 ///
 /// This value should strike a balance between:

crates/editor/src/code_completion_tests.rs 🔗

@@ -94,7 +94,7 @@ async fn test_fuzzy_score(cx: &mut TestAppContext) {
             filter_and_sort_matches("set_text", &completions, SnippetSortOrder::Top, cx).await;
         assert_eq!(matches[0].string, "set_text");
         assert_eq!(matches[1].string, "set_text_style_refinement");
-        assert_eq!(matches[2].string, "set_context_menu_options");
+        assert_eq!(matches[2].string, "set_placeholder_text");
     }
 
     // fuzzy filter text over label, sort_text and sort_kind
@@ -216,6 +216,28 @@ async fn test_sort_positions(cx: &mut TestAppContext) {
     assert_eq!(matches[0].string, "rounded-full");
 }
 
+#[gpui::test]
+async fn test_fuzzy_over_sort_positions(cx: &mut TestAppContext) {
+    let completions = vec![
+        CompletionBuilder::variable("lsp_document_colors", None, "7fffffff"), // 0.29 fuzzy score
+        CompletionBuilder::function(
+            "language_servers_running_disk_based_diagnostics",
+            None,
+            "7fffffff",
+        ), // 0.168 fuzzy score
+        CompletionBuilder::function("code_lens", None, "7fffffff"),           // 3.2 fuzzy score
+        CompletionBuilder::variable("lsp_code_lens", None, "7fffffff"),       // 3.2 fuzzy score
+        CompletionBuilder::function("fetch_code_lens", None, "7fffffff"),     // 3.2 fuzzy score
+    ];
+
+    let matches =
+        filter_and_sort_matches("lens", &completions, SnippetSortOrder::default(), cx).await;
+
+    assert_eq!(matches[0].string, "code_lens");
+    assert_eq!(matches[1].string, "lsp_code_lens");
+    assert_eq!(matches[2].string, "fetch_code_lens");
+}
+
 async fn test_for_each_prefix<F>(
     target: &str,
     completions: &Vec<Completion>,

crates/editor/src/code_context_menus.rs 🔗

@@ -844,7 +844,7 @@ impl CompletionsMenu {
         .with_sizing_behavior(ListSizingBehavior::Infer)
         .w(rems(34.));
 
-        Popover::new().child(div().child(list)).into_any_element()
+        Popover::new().child(list).into_any_element()
     }
 
     fn render_aside(
@@ -1057,9 +1057,9 @@ impl CompletionsMenu {
         enum MatchTier<'a> {
             WordStartMatch {
                 sort_exact: Reverse<i32>,
-                sort_positions: Vec<usize>,
                 sort_snippet: Reverse<i32>,
                 sort_score: Reverse<OrderedFloat<f64>>,
+                sort_positions: Vec<usize>,
                 sort_text: Option<&'a str>,
                 sort_kind: usize,
                 sort_label: &'a str,
@@ -1137,9 +1137,9 @@ impl CompletionsMenu {
 
                 MatchTier::WordStartMatch {
                     sort_exact,
-                    sort_positions,
                     sort_snippet,
                     sort_score,
+                    sort_positions,
                     sort_text,
                     sort_kind,
                     sort_label,

crates/gpui/Cargo.toml 🔗

@@ -287,6 +287,10 @@ path = "examples/shadow.rs"
 name = "svg"
 path = "examples/svg/svg.rs"
 
+[[example]]
+name = "tab_stop"
+path = "examples/tab_stop.rs"
+
 [[example]]
 name = "text"
 path = "examples/text.rs"

crates/gpui/src/platform/keystroke.rs 🔗

@@ -534,11 +534,7 @@ impl Modifiers {
 
     /// Checks if this [`Modifiers`] is a subset of another [`Modifiers`].
     pub fn is_subset_of(&self, other: &Modifiers) -> bool {
-        (other.control || !self.control)
-            && (other.alt || !self.alt)
-            && (other.shift || !self.shift)
-            && (other.platform || !self.platform)
-            && (other.function || !self.function)
+        (*other & *self) == *self
     }
 }
 

crates/gpui/src/tab_stop.rs 🔗

@@ -5,7 +5,7 @@ use crate::{FocusHandle, FocusId};
 /// Used to manage the `Tab` event to switch between focus handles.
 #[derive(Default)]
 pub(crate) struct TabHandles {
-    handles: Vec<FocusHandle>,
+    pub(crate) handles: Vec<FocusHandle>,
 }
 
 impl TabHandles {

crates/gpui/src/window.rs 🔗

@@ -702,6 +702,7 @@ pub(crate) struct PaintIndex {
     input_handlers_index: usize,
     cursor_styles_index: usize,
     accessed_element_states_index: usize,
+    tab_handle_index: usize,
     line_layout_index: LineLayoutIndex,
 }
 
@@ -2208,6 +2209,7 @@ impl Window {
             input_handlers_index: self.next_frame.input_handlers.len(),
             cursor_styles_index: self.next_frame.cursor_styles.len(),
             accessed_element_states_index: self.next_frame.accessed_element_states.len(),
+            tab_handle_index: self.next_frame.tab_handles.handles.len(),
             line_layout_index: self.text_system.layout_index(),
         }
     }
@@ -2237,6 +2239,12 @@ impl Window {
                 .iter()
                 .map(|(id, type_id)| (GlobalElementId(id.0.clone()), *type_id)),
         );
+        self.next_frame.tab_handles.handles.extend(
+            self.rendered_frame.tab_handles.handles
+                [range.start.tab_handle_index..range.end.tab_handle_index]
+                .iter()
+                .cloned(),
+        );
 
         self.text_system
             .reuse_layouts(range.start.line_layout_index..range.end.line_layout_index);

crates/onboarding/Cargo.toml 🔗

@@ -26,3 +26,4 @@ theme.workspace = true
 ui.workspace = true
 workspace.workspace = true
 workspace-hack.workspace = true
+zed_actions.workspace = true

crates/onboarding/src/onboarding.rs 🔗

@@ -1,3 +1,4 @@
+use crate::welcome::{ShowWelcome, WelcomePage};
 use command_palette_hooks::CommandPaletteFilter;
 use db::kvp::KEY_VALUE_STORE;
 use feature_flags::{FeatureFlag, FeatureFlagViewExt as _};
@@ -20,6 +21,8 @@ use workspace::{
     open_new, with_active_or_new_workspace,
 };
 
+mod welcome;
+
 pub struct OnBoardingFeatureFlag {}
 
 impl FeatureFlag for OnBoardingFeatureFlag {
@@ -63,12 +66,43 @@ pub fn init(cx: &mut App) {
                 .detach();
         });
     });
+
+    cx.on_action(|_: &ShowWelcome, cx| {
+        with_active_or_new_workspace(cx, |workspace, window, cx| {
+            workspace
+                .with_local_workspace(window, cx, |workspace, window, cx| {
+                    let existing = workspace
+                        .active_pane()
+                        .read(cx)
+                        .items()
+                        .find_map(|item| item.downcast::<WelcomePage>());
+
+                    if let Some(existing) = existing {
+                        workspace.activate_item(&existing, true, true, window, cx);
+                    } else {
+                        let settings_page = WelcomePage::new(cx);
+                        workspace.add_item_to_active_pane(
+                            Box::new(settings_page),
+                            None,
+                            true,
+                            window,
+                            cx,
+                        )
+                    }
+                })
+                .detach();
+        });
+    });
+
     cx.observe_new::<Workspace>(|_, window, cx| {
         let Some(window) = window else {
             return;
         };
 
-        let onboarding_actions = [std::any::TypeId::of::<OpenOnboarding>()];
+        let onboarding_actions = [
+            std::any::TypeId::of::<OpenOnboarding>(),
+            std::any::TypeId::of::<ShowWelcome>(),
+        ];
 
         CommandPaletteFilter::update_global(cx, |filter, _cx| {
             filter.hide_action_types(&onboarding_actions);

crates/onboarding/src/welcome.rs 🔗

@@ -0,0 +1,276 @@
+use gpui::{
+    Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
+    NoAction, ParentElement, Render, Styled, Window, actions,
+};
+use ui::{ButtonLike, Divider, DividerColor, KeyBinding, Vector, VectorName, prelude::*};
+use workspace::{
+    NewFile, Open, Workspace, WorkspaceId,
+    item::{Item, ItemEvent},
+};
+use zed_actions::{Extensions, OpenSettings, command_palette};
+
+actions!(
+    zed,
+    [
+        /// Show the Zed welcome screen
+        ShowWelcome
+    ]
+);
+
+const CONTENT: (Section<4>, Section<3>) = (
+    Section {
+        title: "Get Started",
+        entries: [
+            SectionEntry {
+                icon: IconName::Plus,
+                title: "New File",
+                action: &NewFile,
+            },
+            SectionEntry {
+                icon: IconName::FolderOpen,
+                title: "Open Project",
+                action: &Open,
+            },
+            SectionEntry {
+                // TODO: use proper icon
+                icon: IconName::Download,
+                title: "Clone a Repo",
+                // TODO: use proper action
+                action: &NoAction,
+            },
+            SectionEntry {
+                icon: IconName::ListCollapse,
+                title: "Open Command Palette",
+                action: &command_palette::Toggle,
+            },
+        ],
+    },
+    Section {
+        title: "Configure",
+        entries: [
+            SectionEntry {
+                icon: IconName::Settings,
+                title: "Open Settings",
+                action: &OpenSettings,
+            },
+            SectionEntry {
+                icon: IconName::ZedAssistant,
+                title: "View AI Settings",
+                // TODO: use proper action
+                action: &NoAction,
+            },
+            SectionEntry {
+                icon: IconName::Blocks,
+                title: "Explore Extensions",
+                action: &Extensions {
+                    category_filter: None,
+                    id: None,
+                },
+            },
+        ],
+    },
+);
+
+struct Section<const COLS: usize> {
+    title: &'static str,
+    entries: [SectionEntry; COLS],
+}
+
+impl<const COLS: usize> Section<COLS> {
+    fn render(
+        self,
+        index_offset: usize,
+        focus: &FocusHandle,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> impl IntoElement {
+        v_flex()
+            .min_w_full()
+            .gap_2()
+            .child(
+                h_flex()
+                    .px_1()
+                    .gap_4()
+                    .child(
+                        Label::new(self.title.to_ascii_uppercase())
+                            .buffer_font(cx)
+                            .color(Color::Muted)
+                            .size(LabelSize::XSmall),
+                    )
+                    .child(Divider::horizontal().color(DividerColor::Border)),
+            )
+            .children(
+                self.entries
+                    .iter()
+                    .enumerate()
+                    .map(|(index, entry)| entry.render(index_offset + index, &focus, window, cx)),
+            )
+    }
+}
+
+struct SectionEntry {
+    icon: IconName,
+    title: &'static str,
+    action: &'static dyn Action,
+}
+
+impl SectionEntry {
+    fn render(
+        &self,
+        button_index: usize,
+        focus: &FocusHandle,
+        window: &Window,
+        cx: &App,
+    ) -> impl IntoElement {
+        ButtonLike::new(("onboarding-button-id", button_index))
+            .full_width()
+            .child(
+                h_flex()
+                    .w_full()
+                    .gap_1()
+                    .justify_between()
+                    .child(
+                        h_flex()
+                            .gap_2()
+                            .child(
+                                Icon::new(self.icon)
+                                    .color(Color::Muted)
+                                    .size(IconSize::XSmall),
+                            )
+                            .child(Label::new(self.title)),
+                    )
+                    .children(KeyBinding::for_action_in(self.action, focus, window, cx)),
+            )
+            .on_click(|_, window, cx| window.dispatch_action(self.action.boxed_clone(), cx))
+    }
+}
+
+pub struct WelcomePage {
+    focus_handle: FocusHandle,
+}
+
+impl Render for WelcomePage {
+    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let (first_section, second_entries) = CONTENT;
+        let first_section_entries = first_section.entries.len();
+
+        h_flex()
+            .size_full()
+            .justify_center()
+            .overflow_hidden()
+            .bg(cx.theme().colors().editor_background)
+            .key_context("Welcome")
+            .track_focus(&self.focus_handle(cx))
+            .child(
+                h_flex()
+                    .px_12()
+                    .py_40()
+                    .size_full()
+                    .relative()
+                    .max_w(px(1100.))
+                    .child(
+                        div()
+                            .size_full()
+                            .max_w_128()
+                            .mx_auto()
+                            .child(
+                                h_flex()
+                                    .w_full()
+                                    .justify_center()
+                                    .gap_4()
+                                    .child(Vector::square(VectorName::ZedLogo, rems(2.)))
+                                    .child(
+                                        div().child(Headline::new("Welcome to Zed")).child(
+                                            Label::new("The editor for what's next")
+                                                .size(LabelSize::Small)
+                                                .color(Color::Muted)
+                                                .italic(),
+                                        ),
+                                    ),
+                            )
+                            .child(
+                                v_flex()
+                                    .mt_12()
+                                    .gap_8()
+                                    .child(first_section.render(
+                                        Default::default(),
+                                        &self.focus_handle,
+                                        window,
+                                        cx,
+                                    ))
+                                    .child(second_entries.render(
+                                        first_section_entries,
+                                        &self.focus_handle,
+                                        window,
+                                        cx,
+                                    ))
+                                    .child(
+                                        h_flex()
+                                            .w_full()
+                                            .pt_4()
+                                            .justify_center()
+                                            // We call this a hack
+                                            .rounded_b_xs()
+                                            .border_t_1()
+                                            .border_color(DividerColor::Border.hsla(cx))
+                                            .border_dashed()
+                                            .child(
+                                                div().child(
+                                                    Button::new("welcome-exit", "Return to Setup")
+                                                        .full_width()
+                                                        .label_size(LabelSize::XSmall),
+                                                ),
+                                            ),
+                                    ),
+                            ),
+                    ),
+            )
+    }
+}
+
+impl WelcomePage {
+    pub fn new(cx: &mut Context<Workspace>) -> Entity<Self> {
+        let this = cx.new(|cx| WelcomePage {
+            focus_handle: cx.focus_handle(),
+        });
+
+        this
+    }
+}
+
+impl EventEmitter<ItemEvent> for WelcomePage {}
+
+impl Focusable for WelcomePage {
+    fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
+        self.focus_handle.clone()
+    }
+}
+
+impl Item for WelcomePage {
+    type Event = ItemEvent;
+
+    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
+        "Welcome".into()
+    }
+
+    fn telemetry_event_text(&self) -> Option<&'static str> {
+        Some("New Welcome Page Opened")
+    }
+
+    fn show_toolbar(&self) -> bool {
+        false
+    }
+
+    fn clone_on_split(
+        &self,
+        _workspace_id: Option<WorkspaceId>,
+        _: &mut Window,
+        _: &mut Context<Self>,
+    ) -> Option<Entity<Self>> {
+        None
+    }
+
+    fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
+        f(*event)
+    }
+}

crates/settings_ui/src/keybindings.rs 🔗

@@ -1690,7 +1690,7 @@ impl Render for KeymapEditor {
                         move |window, cx| this.read(cx).render_no_matches_hint(window, cx)
                     })
                     .column_widths([
-                        DefiniteLength::Absolute(AbsoluteLength::Pixels(px(40.))),
+                        DefiniteLength::Absolute(AbsoluteLength::Pixels(px(36.))),
                         DefiniteLength::Fraction(0.25),
                         DefiniteLength::Fraction(0.20),
                         DefiniteLength::Fraction(0.14),
@@ -1765,6 +1765,7 @@ impl Render for KeymapEditor {
                                             },
                                         )
                                         .into_any_element();
+
                                     let keystrokes = binding.ui_key_binding().cloned().map_or(
                                         binding
                                             .keystroke_text()
@@ -1773,6 +1774,7 @@ impl Render for KeymapEditor {
                                             .into_any_element(),
                                         IntoElement::into_any_element,
                                     );
+
                                     let action_arguments = match binding.action().arguments.clone()
                                     {
                                         Some(arguments) => arguments.into_any_element(),
@@ -1785,6 +1787,7 @@ impl Render for KeymapEditor {
                                             }
                                         }
                                     };
+
                                     let context = binding.context().cloned().map_or(
                                         gpui::Empty.into_any_element(),
                                         |context| {
@@ -1809,11 +1812,13 @@ impl Render for KeymapEditor {
                                                 .into_any_element()
                                         },
                                     );
+
                                     let source = binding
                                         .keybind_source()
                                         .map(|source| source.name())
                                         .unwrap_or_default()
                                         .into_any_element();
+
                                     Some([
                                         icon.into_any_element(),
                                         action,
@@ -3109,7 +3114,9 @@ impl KeystrokeInput {
     ) {
         let keystrokes_len = self.keystrokes.len();
 
-        if event.modifiers.is_subset_of(&self.previous_modifiers) {
+        if self.previous_modifiers.modified()
+            && event.modifiers.is_subset_of(&self.previous_modifiers)
+        {
             self.previous_modifiers &= event.modifiers;
             cx.stop_propagation();
             return;

crates/settings_ui/src/ui_components/table.rs 🔗

@@ -17,7 +17,7 @@ use ui::{
     StyledTypography, Window, div, example_group_with_title, h_flex, px, single_example, v_flex,
 };
 
-const RESIZE_COLUMN_WIDTH: f32 = 5.0;
+const RESIZE_COLUMN_WIDTH: f32 = 8.0;
 
 #[derive(Debug)]
 struct DraggedColumn(usize);
@@ -214,6 +214,7 @@ impl TableInteractionState {
         let mut column_ix = 0;
         let resizable_columns_slice = *resizable_columns;
         let mut resizable_columns = resizable_columns.into_iter();
+
         let dividers = intersperse_with(spacers, || {
             window.with_id(column_ix, |window| {
                 let mut resize_divider = div()
@@ -221,9 +222,9 @@ impl TableInteractionState {
                     .id(column_ix)
                     .relative()
                     .top_0()
-                    .w_0p5()
+                    .w_px()
                     .h_full()
-                    .bg(cx.theme().colors().border.opacity(0.5));
+                    .bg(cx.theme().colors().border.opacity(0.8));
 
                 let mut resize_handle = div()
                     .id("column-resize-handle")
@@ -237,9 +238,11 @@ impl TableInteractionState {
                     .is_some_and(ResizeBehavior::is_resizable)
                 {
                     let hovered = window.use_state(cx, |_window, _cx| false);
+
                     resize_divider = resize_divider.when(*hovered.read(cx), |div| {
                         div.bg(cx.theme().colors().border_focused)
                     });
+
                     resize_handle = resize_handle
                         .on_hover(move |&was_hovered, _, cx| hovered.write(cx, was_hovered))
                         .cursor_col_resize()
@@ -269,12 +272,11 @@ impl TableInteractionState {
             })
         });
 
-        div()
+        h_flex()
             .id("resize-handles")
-            .h_flex()
             .absolute()
-            .w_full()
             .inset_0()
+            .w_full()
             .children(dividers)
             .into_any_element()
     }
@@ -896,7 +898,6 @@ fn base_cell_style(width: Option<Length>) -> Div {
         .px_1p5()
         .when_some(width, |this, width| this.w(width))
         .when(width.is_none(), |this| this.flex_1())
-        .justify_start()
         .whitespace_nowrap()
         .text_ellipsis()
         .overflow_hidden()
@@ -941,7 +942,7 @@ pub fn render_row<const COLS: usize>(
             .map(IntoElement::into_any_element)
             .into_iter()
             .zip(column_widths)
-            .map(|(cell, width)| base_cell_style_text(width, cx).px_1p5().py_1().child(cell)),
+            .map(|(cell, width)| base_cell_style_text(width, cx).px_1().py_0p5().child(cell)),
     );
 
     let row = if let Some(map_row) = table_context.map_row {
@@ -950,7 +951,7 @@ pub fn render_row<const COLS: usize>(
         row.into_any_element()
     };
 
-    div().h_full().w_full().child(row).into_any_element()
+    div().size_full().child(row).into_any_element()
 }
 
 pub fn render_header<const COLS: usize>(

crates/ui/src/components/keybinding.rs 🔗

@@ -44,7 +44,7 @@ impl KeyBinding {
     pub fn for_action_in(
         action: &dyn Action,
         focus: &FocusHandle,
-        window: &mut Window,
+        window: &Window,
         cx: &App,
     ) -> Option<Self> {
         let key_binding = window.highest_precedence_binding_for_action_in(action, focus)?;

crates/ui/src/components/popover.rs 🔗

@@ -50,7 +50,7 @@ impl RenderOnce for Popover {
                 v_flex()
                     .elevation_2(cx)
                     .py(POPOVER_Y_PADDING / 2.)
-                    .children(self.children),
+                    .child(div().children(self.children)),
             )
             .when_some(self.aside, |this, aside| {
                 this.child(

docs/src/extensions/installing-extensions.md 🔗

@@ -1,6 +1,6 @@
 # Installing Extensions
 
-You can search for extensions by launching the Zed Extension Gallery by pressing `cmd-shift-x` (macOS) or `ctrl-shift-x` (Linux), opening the command palette and selecting `zed: extensions` or by selecting "Zed > Extensions" from the menu bar.
+You can search for extensions by launching the Zed Extension Gallery by pressing {#kb zed::Extensions} , opening the command palette and selecting {#action zed::Extensions} or by selecting "Zed > Extensions" from the menu bar.
 
 Here you can view the extensions that you currently have installed or search and install new ones.
 

docs/src/getting-started.md 🔗

@@ -83,6 +83,6 @@ Visit [the AI overview page](./ai/overview.md) to learn how to quickly get start
 
 ## Set up your key bindings
 
-To open your custom keymap to add your key bindings, use the {#kb zed::OpenKeymap} keybinding.
+To edit your custom keymap and add or remap bindings, you can either use {#kb zed::OpenKeymapEditor} to spawn the Zed Keymap Editor ({#action zed::OpenKeymapEditor}) or you can directly open your Zed Keymap json (`~/.config/zed/keymap.json`) with {#action zed::OpenKeymap}.
 
 To access the default key binding set, open the Command Palette with {#kb command_palette::Toggle} and search for "zed: open default keymap". See [Key Bindings](./key-bindings.md) for more info.

docs/src/key-bindings.md 🔗

@@ -18,7 +18,7 @@ You can also enable `vim_mode`, which adds vim bindings too.
 
 ## User keymaps
 
-Zed reads your keymap from `~/.config/zed/keymap.json`. You can open the file within Zed with {#kb zed::OpenKeymap}, or via `zed: Open Keymap` in the command palette.
+Zed reads your keymap from `~/.config/zed/keymap.json`. You can open the file within Zed with {#action zed::OpenKeymap} from the command palette or to spawn the Zed Keymap Editor ({#action zed::OpenKeymapEditor}) use {#kb zed::OpenKeymapEditor}.
 
 The file contains a JSON array of objects with `"bindings"`. If no `"context"` is set the bindings are always active. If it is set the binding is only active when the [context matches](#contexts).