Merge branch 'main' into mcp-acp-gemini
This commit is contained in:
commit
477731d77d
22 changed files with 637 additions and 174 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -11028,6 +11028,7 @@ dependencies = [
|
|||
"ui",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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,
|
||||
".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,
|
||||
"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,
|
||||
"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]
|
||||
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",
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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>,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -26,3 +26,4 @@ theme.workspace = true
|
|||
ui.workspace = true
|
||||
workspace.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
|
|
@ -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);
|
||||
|
|
276
crates/onboarding/src/welcome.rs
Normal file
276
crates/onboarding/src/welcome.rs
Normal 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)
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -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)?;
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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).
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue