Merge branch 'main' into mcp-acp-gemini

This commit is contained in:
Agus Zubiaga 2025-07-28 18:43:25 -03:00
commit 477731d77d
22 changed files with 637 additions and 174 deletions

1
Cargo.lock generated
View file

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

View file

@ -4,6 +4,7 @@
"ctrl-alt-s": "zed::OpenSettings", "ctrl-alt-s": "zed::OpenSettings",
"ctrl-{": "pane::ActivatePreviousItem", "ctrl-{": "pane::ActivatePreviousItem",
"ctrl-}": "pane::ActivateNextItem", "ctrl-}": "pane::ActivateNextItem",
"shift-escape": null, // Unmap workspace::zoom
"ctrl-f2": "debugger::Stop", "ctrl-f2": "debugger::Stop",
"f6": "debugger::Pause", "f6": "debugger::Pause",
"f7": "debugger::StepInto", "f7": "debugger::StepInto",
@ -44,8 +45,8 @@
"ctrl-alt-right": "pane::GoForward", "ctrl-alt-right": "pane::GoForward",
"alt-f7": "editor::FindAllReferences", "alt-f7": "editor::FindAllReferences",
"ctrl-alt-f7": "editor::FindAllReferences", "ctrl-alt-f7": "editor::FindAllReferences",
// "ctrl-b": "editor::GoToDefinition", // Conflicts with workspace::ToggleLeftDock "ctrl-b": "editor::GoToDefinition", // Conflicts with workspace::ToggleLeftDock
// "ctrl-alt-b": "editor::GoToDefinitionSplit", // Conflicts with workspace::ToggleLeftDock "ctrl-alt-b": "editor::GoToDefinitionSplit", // Conflicts with workspace::ToggleRightDock
"ctrl-shift-b": "editor::GoToTypeDefinition", "ctrl-shift-b": "editor::GoToTypeDefinition",
"ctrl-alt-shift-b": "editor::GoToTypeDefinitionSplit", "ctrl-alt-shift-b": "editor::GoToTypeDefinitionSplit",
"f2": "editor::GoToDiagnostic", "f2": "editor::GoToDiagnostic",
@ -100,12 +101,27 @@
"shift shift": "command_palette::Toggle", "shift shift": "command_palette::Toggle",
"ctrl-alt-shift-n": "project_symbols::Toggle", "ctrl-alt-shift-n": "project_symbols::Toggle",
"alt-0": "git_panel::ToggleFocus", "alt-0": "git_panel::ToggleFocus",
"alt-1": "workspace::ToggleLeftDock", "alt-1": "project_panel::ToggleFocus",
"alt-5": "debug_panel::ToggleFocus", "alt-5": "debug_panel::ToggleFocus",
"alt-6": "diagnostics::Deploy", "alt-6": "diagnostics::Deploy",
"alt-7": "outline_panel::ToggleFocus" "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", "context": "Workspace || Editor",
"bindings": { "bindings": {
@ -151,6 +167,9 @@
{ "context": "OutlinePanel", "bindings": { "alt-7": "workspace::CloseActiveDock" } }, { "context": "OutlinePanel", "bindings": { "alt-7": "workspace::CloseActiveDock" } },
{ {
"context": "Dock || Workspace || Terminal || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)", "context": "Dock || Workspace || Terminal || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)",
"bindings": { "escape": "editor::ToggleFocus" } "bindings": {
"escape": "editor::ToggleFocus",
"shift-escape": "workspace::CloseActiveDock"
}
} }
] ]

View file

@ -4,6 +4,7 @@
"cmd-{": "pane::ActivatePreviousItem", "cmd-{": "pane::ActivatePreviousItem",
"cmd-}": "pane::ActivateNextItem", "cmd-}": "pane::ActivateNextItem",
"cmd-0": "git_panel::ToggleFocus", // overrides `cmd-0` zoom reset "cmd-0": "git_panel::ToggleFocus", // overrides `cmd-0` zoom reset
"shift-escape": null, // Unmap workspace::zoom
"ctrl-f2": "debugger::Stop", "ctrl-f2": "debugger::Stop",
"f6": "debugger::Pause", "f6": "debugger::Pause",
"f7": "debugger::StepInto", "f7": "debugger::StepInto",
@ -108,6 +109,21 @@
"cmd-7": "outline_panel::ToggleFocus" "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", "context": "Workspace || Editor",
"bindings": { "bindings": {
@ -146,11 +162,15 @@
} }
}, },
{ "context": "GitPanel", "bindings": { "cmd-0": "workspace::CloseActiveDock" } }, { "context": "GitPanel", "bindings": { "cmd-0": "workspace::CloseActiveDock" } },
{ "context": "ProjectPanel", "bindings": { "cmd-1": "workspace::CloseActiveDock" } },
{ "context": "DebugPanel", "bindings": { "cmd-5": "workspace::CloseActiveDock" } }, { "context": "DebugPanel", "bindings": { "cmd-5": "workspace::CloseActiveDock" } },
{ "context": "Diagnostics > Editor", "bindings": { "cmd-6": "pane::CloseActiveItem" } }, { "context": "Diagnostics > Editor", "bindings": { "cmd-6": "pane::CloseActiveItem" } },
{ "context": "OutlinePanel", "bindings": { "cmd-7": "workspace::CloseActiveDock" } }, { "context": "OutlinePanel", "bindings": { "cmd-7": "workspace::CloseActiveDock" } },
{ {
"context": "Dock || Workspace || Terminal || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)", "context": "Dock || Workspace || Terminal || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)",
"bindings": { "escape": "editor::ToggleFocus" } "bindings": {
"escape": "editor::ToggleFocus",
"shift-escape": "workspace::CloseActiveDock"
}
} }
] ]

View file

@ -25,6 +25,7 @@ use language::{
}; };
use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat};
use markdown::{Markdown, MarkdownElement, MarkdownStyle}; use markdown::{Markdown, MarkdownElement, MarkdownStyle};
use paths;
use project::{ use project::{
Project, ProjectPath, Project, ProjectPath,
lsp_store::{FormatTrigger, LspFormatTarget}, lsp_store::{FormatTrigger, LspFormatTarget},
@ -141,27 +142,32 @@ impl Tool for EditFileTool {
return false; return false;
}; };
let path = Path::new(&input.path); // If any path component matches the local settings folder, then this could affect
// If any path component is ".zed", then this could affect
// the editor in ways beyond the project source, so prompt. // 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 if path
.components() .components()
.any(|component| component.as_os_str() == ".zed") .any(|component| component.as_os_str() == local_settings_folder.as_os_str())
{ {
return true; return true;
} }
// If the path is outside the project, then prompt. // It's also possible that the global config dir is configured to be inside the project,
let is_outside_project = project // so check for that edge case too.
.read(cx) if let Ok(canonical_path) = std::fs::canonicalize(&input.path) {
.find_project_path(&input.path, cx) if canonical_path.starts_with(paths::config_dir()) {
.is_none(); return true;
if is_outside_project { }
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 { fn may_perform_edits(&self) -> bool {
@ -187,8 +193,16 @@ impl Tool for EditFileTool {
let mut description = input.display_description.clone(); let mut description = input.display_description.clone();
// Add context about why confirmation may be needed // Add context about why confirmation may be needed
if path.components().any(|c| c.as_os_str() == ".zed") { let local_settings_folder = paths::local_settings_folder_relative_path();
description.push_str(" (Zed settings)"); 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 description
@ -1219,19 +1233,20 @@ async fn build_buffer_diff(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use ::fs::Fs;
use client::TelemetrySettings; use client::TelemetrySettings;
use fs::{FakeFs, Fs};
use gpui::{TestAppContext, UpdateGlobal}; use gpui::{TestAppContext, UpdateGlobal};
use language_model::fake_provider::FakeLanguageModel; use language_model::fake_provider::FakeLanguageModel;
use serde_json::json; use serde_json::json;
use settings::SettingsStore; use settings::SettingsStore;
use std::fs;
use util::path; use util::path;
#[gpui::test] #[gpui::test]
async fn test_edit_nonexistent_file(cx: &mut TestAppContext) { async fn test_edit_nonexistent_file(cx: &mut TestAppContext) {
init_test(cx); init_test(cx);
let fs = FakeFs::new(cx.executor()); let fs = project::FakeFs::new(cx.executor());
fs.insert_tree("/root", json!({})).await; fs.insert_tree("/root", json!({})).await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
let action_log = cx.new(|_| ActionLog::new(project.clone())); let action_log = cx.new(|_| ActionLog::new(project.clone()));
@ -1321,7 +1336,7 @@ mod tests {
) -> anyhow::Result<ProjectPath> { ) -> anyhow::Result<ProjectPath> {
init_test(cx); init_test(cx);
let fs = FakeFs::new(cx.executor()); let fs = project::FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/root", "/root",
json!({ 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] #[gpui::test]
async fn test_format_on_save(cx: &mut TestAppContext) { async fn test_format_on_save(cx: &mut TestAppContext) {
init_test(cx); init_test(cx);
let fs = FakeFs::new(cx.executor()); let fs = project::FakeFs::new(cx.executor());
fs.insert_tree("/root", json!({"src": {}})).await; fs.insert_tree("/root", json!({"src": {}})).await;
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).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) { async fn test_remove_trailing_whitespace(cx: &mut TestAppContext) {
init_test(cx); init_test(cx);
let fs = FakeFs::new(cx.executor()); let fs = project::FakeFs::new(cx.executor());
fs.insert_tree("/root", json!({"src": {}})).await; fs.insert_tree("/root", json!({"src": {}})).await;
// Create a simple file with trailing whitespace // Create a simple file with trailing whitespace
@ -1773,7 +1802,7 @@ mod tests {
async fn test_needs_confirmation(cx: &mut TestAppContext) { async fn test_needs_confirmation(cx: &mut TestAppContext) {
init_test(cx); init_test(cx);
let tool = Arc::new(EditFileTool); let tool = Arc::new(EditFileTool);
let fs = FakeFs::new(cx.executor()); let fs = project::FakeFs::new(cx.executor());
fs.insert_tree("/root", json!({})).await; fs.insert_tree("/root", json!({})).await;
// Test 1: Path with .zed component should require confirmation // Test 1: Path with .zed component should require confirmation
@ -1847,44 +1876,66 @@ mod tests {
} }
#[gpui::test] #[gpui::test]
fn test_ui_text_with_confirmation_context(cx: &mut TestAppContext) { async fn test_ui_text_shows_correct_context(cx: &mut TestAppContext) {
init_test(cx); // 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); let tool = Arc::new(EditFileTool);
// Test ui_text shows context for .zed paths // Test ui_text shows context for various paths
let input_zed = json!({ let test_cases = vec![
"display_description": "Update settings", (
"path": ".zed/settings.json", json!({
"mode": "edit" "display_description": "Update config",
}); "path": ".zed/settings.json",
cx.update(|_cx| { "mode": "edit"
let ui_text = tool.ui_text(&input_zed); }),
assert_eq!( "Update config (local settings)",
ui_text, "Update settings (Zed settings)", ".zed path should show local settings context",
"UI text should indicate Zed settings file" ),
); (
}); 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 for (input, expected_text, description) in test_cases {
let input_normal = json!({ cx.update(|_cx| {
"display_description": "Edit source file", let ui_text = tool.ui_text(&input);
"path": "src/main.rs", assert_eq!(ui_text, expected_text, "Failed for case: {}", description);
"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"
);
});
} }
#[gpui::test] #[gpui::test]
async fn test_needs_confirmation_outside_project(cx: &mut TestAppContext) { async fn test_needs_confirmation_outside_project(cx: &mut TestAppContext) {
init_test(cx); init_test(cx);
let tool = Arc::new(EditFileTool); let tool = Arc::new(EditFileTool);
let fs = FakeFs::new(cx.executor()); let fs = project::FakeFs::new(cx.executor());
// Create a project in /project directory // Create a project in /project directory
fs.insert_tree("/project", json!({})).await; fs.insert_tree("/project", json!({})).await;
@ -1918,33 +1969,58 @@ mod tests {
} }
#[gpui::test] #[gpui::test]
async fn test_needs_confirmation_zed_paths(cx: &mut TestAppContext) { async fn test_needs_confirmation_config_paths(cx: &mut TestAppContext) {
init_test(cx); // 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 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; fs.insert_tree("/home/user/myproject", json!({})).await;
let project = Project::test(fs.clone(), [path!("/home/user/myproject").as_ref()], cx).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![ 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, true,
".zed.backup is outside project (not a .zed component issue)", "Top-level local settings file".to_string(),
), ),
( (
"my.zed/file.txt", format!(
"myproject/{}/settings.json",
local_settings_folder.display()
),
true, true,
"my.zed is outside project (not a .zed component issue)", "Local settings in project path".to_string(),
), ),
("myproject/src/file.zed", false, ".zed as file extension"),
( (
"myproject/normal/path/file.rs", 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".to_string(),
),
(
"my.zed/file.txt".to_string(),
true,
"my.zed is outside project".to_string(),
),
(
"myproject/src/file.zed".to_string(),
false, false,
"Normal file without .zed", ".zed as file extension".to_string(),
),
(
"myproject/normal/path/file.rs".to_string(),
false,
"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] #[gpui::test]
async fn test_needs_confirmation_with_multiple_worktrees(cx: &mut TestAppContext) { async fn test_needs_confirmation_with_multiple_worktrees(cx: &mut TestAppContext) {
init_test(cx); init_test(cx);
let tool = Arc::new(EditFileTool); let tool = Arc::new(EditFileTool);
let fs = FakeFs::new(cx.executor()); let fs = project::FakeFs::new(cx.executor());
// Create multiple worktree directories // Create multiple worktree directories
fs.insert_tree( fs.insert_tree(
@ -2052,7 +2186,7 @@ mod tests {
async fn test_needs_confirmation_edge_cases(cx: &mut TestAppContext) { async fn test_needs_confirmation_edge_cases(cx: &mut TestAppContext) {
init_test(cx); init_test(cx);
let tool = Arc::new(EditFileTool); let tool = Arc::new(EditFileTool);
let fs = FakeFs::new(cx.executor()); let fs = project::FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/project", "/project",
json!({ json!({
@ -2112,7 +2246,7 @@ mod tests {
} }
#[gpui::test] #[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); init_test(cx);
let tool = Arc::new(EditFileTool); let tool = Arc::new(EditFileTool);
@ -2124,8 +2258,8 @@ mod tests {
"path": ".zed/settings.json", "path": ".zed/settings.json",
"mode": "edit" "mode": "edit"
}), }),
"Update config (Zed settings)", "Update config (local settings)",
".zed path should show Zed settings context", ".zed path should show local settings context",
), ),
( (
json!({ json!({
@ -2133,8 +2267,8 @@ mod tests {
"path": "src/.zed/local.json", "path": "src/.zed/local.json",
"mode": "edit" "mode": "edit"
}), }),
"Fix bug (Zed settings)", "Fix bug (local settings)",
"Nested .zed path should show Zed settings context", "Nested .zed path should show local settings context",
), ),
( (
json!({ json!({
@ -2168,7 +2302,7 @@ mod tests {
async fn test_needs_confirmation_with_different_modes(cx: &mut TestAppContext) { async fn test_needs_confirmation_with_different_modes(cx: &mut TestAppContext) {
init_test(cx); init_test(cx);
let tool = Arc::new(EditFileTool); let tool = Arc::new(EditFileTool);
let fs = FakeFs::new(cx.executor()); let fs = project::FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
"/project", "/project",
json!({ json!({
@ -2235,9 +2369,12 @@ mod tests {
#[gpui::test] #[gpui::test]
async fn test_always_allow_tool_actions_bypasses_all_checks(cx: &mut TestAppContext) { 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 tool = Arc::new(EditFileTool);
let fs = FakeFs::new(cx.executor()); let fs = project::FakeFs::new(cx.executor());
fs.insert_tree("/project", json!({})).await; fs.insert_tree("/project", json!({})).await;
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).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 // 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![ let test_cases = vec![
".zed/settings.json", ".zed/settings.json",
"project/.zed/config.toml", "project/.zed/config.toml",
global_settings_path.to_str().unwrap(),
"/etc/hosts", "/etc/hosts",
"/absolute/path/file.txt", "/absolute/path/file.txt",
"../outside/project.txt", "../outside/project.txt",

View file

@ -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/refresh_llm_tokens", post(refresh_llm_tokens))
.route("/users/:id/update_plan", post(update_plan)) .route("/users/:id/update_plan", post(update_plan))
.route("/rpc_server_snapshot", get(get_rpc_server_snapshot)) .route("/rpc_server_snapshot", get(get_rpc_server_snapshot))
.merge(billing::router())
.merge(contributors::router()) .merge(contributors::router())
.layer( .layer(
ServiceBuilder::new() ServiceBuilder::new()

View file

@ -1,15 +1,13 @@
use anyhow::{Context as _, bail}; use anyhow::{Context as _, bail};
use axum::{Extension, Json, Router, extract, routing::post};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use collections::{HashMap, HashSet}; use collections::{HashMap, HashSet};
use reqwest::StatusCode;
use sea_orm::ActiveValue; use sea_orm::ActiveValue;
use serde::{Deserialize, Serialize};
use std::{sync::Arc, time::Duration}; use std::{sync::Arc, time::Duration};
use stripe::{CancellationDetailsReason, EventObject, EventType, ListEvents, SubscriptionStatus}; use stripe::{CancellationDetailsReason, EventObject, EventType, ListEvents, SubscriptionStatus};
use util::{ResultExt, maybe}; use util::{ResultExt, maybe};
use zed_llm_client::LanguageModelProvider; use zed_llm_client::LanguageModelProvider;
use crate::AppState;
use crate::db::billing_subscription::{ use crate::db::billing_subscription::{
StripeCancellationReason, StripeSubscriptionStatus, SubscriptionKind, StripeCancellationReason, StripeSubscriptionStatus, SubscriptionKind,
}; };
@ -19,7 +17,6 @@ use crate::stripe_client::{
StripeCancellationDetailsReason, StripeClient, StripeCustomerId, StripeSubscription, StripeCancellationDetailsReason, StripeClient, StripeCustomerId, StripeSubscription,
StripeSubscriptionId, StripeSubscriptionId,
}; };
use crate::{AppState, Error, Result};
use crate::{db::UserId, llm::db::LlmDatabase}; use crate::{db::UserId, llm::db::LlmDatabase};
use crate::{ use crate::{
db::{ db::{
@ -30,70 +27,6 @@ use crate::{
stripe_billing::StripeBilling, 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. /// The amount of time we wait in between each poll of Stripe events.
/// ///
/// This value should strike a balance between: /// This value should strike a balance between:

View file

@ -94,7 +94,7 @@ async fn test_fuzzy_score(cx: &mut TestAppContext) {
filter_and_sort_matches("set_text", &completions, SnippetSortOrder::Top, cx).await; filter_and_sort_matches("set_text", &completions, SnippetSortOrder::Top, cx).await;
assert_eq!(matches[0].string, "set_text"); assert_eq!(matches[0].string, "set_text");
assert_eq!(matches[1].string, "set_text_style_refinement"); 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 // 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"); 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>( async fn test_for_each_prefix<F>(
target: &str, target: &str,
completions: &Vec<Completion>, completions: &Vec<Completion>,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,4 @@
use crate::welcome::{ShowWelcome, WelcomePage};
use command_palette_hooks::CommandPaletteFilter; use command_palette_hooks::CommandPaletteFilter;
use db::kvp::KEY_VALUE_STORE; use db::kvp::KEY_VALUE_STORE;
use feature_flags::{FeatureFlag, FeatureFlagViewExt as _}; use feature_flags::{FeatureFlag, FeatureFlagViewExt as _};
@ -20,6 +21,8 @@ use workspace::{
open_new, with_active_or_new_workspace, open_new, with_active_or_new_workspace,
}; };
mod welcome;
pub struct OnBoardingFeatureFlag {} pub struct OnBoardingFeatureFlag {}
impl FeatureFlag for OnBoardingFeatureFlag { impl FeatureFlag for OnBoardingFeatureFlag {
@ -63,12 +66,43 @@ pub fn init(cx: &mut App) {
.detach(); .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| { cx.observe_new::<Workspace>(|_, window, cx| {
let Some(window) = window else { let Some(window) = window else {
return; 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| { CommandPaletteFilter::update_global(cx, |filter, _cx| {
filter.hide_action_types(&onboarding_actions); filter.hide_action_types(&onboarding_actions);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
# Installing Extensions # 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. Here you can view the extensions that you currently have installed or search and install new ones.

View file

@ -83,6 +83,6 @@ Visit [the AI overview page](./ai/overview.md) to learn how to quickly get start
## Set up your key bindings ## 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. 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.

View file

@ -18,7 +18,7 @@ You can also enable `vim_mode`, which adds vim bindings too.
## User keymaps ## 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). 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).