Compare commits

..

1 Commits

Author SHA1 Message Date
Cole Miller
f4d838bb14 refactor agent server settings to store commands uniformly
Co-authored-by: Nia Espera <nia-e@haecceity.cc>
2025-09-04 18:12:27 -04:00
235 changed files with 8374 additions and 11202 deletions

View File

@@ -19,6 +19,8 @@ rustflags = [
"windows_slim_errors", # This cfg will reduce the size of `windows::core::Error` from 16 bytes to 4 bytes
"-C",
"target-feature=+crt-static", # This fixes the linking issue when compiling livekit on Windows
"-C",
"link-arg=-fuse-ld=lld",
]
[env]

2
.gitattributes vendored
View File

@@ -2,4 +2,4 @@
*.json linguist-language=JSON-with-Comments
# Ensure the WSL script always has LF line endings, even on Windows
crates/zed/resources/windows/zed.sh text eol=lf
crates/zed/resources/windows/zed-wsl text eol=lf

View File

@@ -65,7 +65,7 @@ If you would like to add a new icon to the Zed icon theme, [open a Discussion](h
## Bird's-eye view of Zed
We suggest you keep the [zed glossary](docs/src/development/glossary.md) at your side when starting out. It lists and explains some of the structures and terms you will see throughout the codebase.
We suggest you keep the [zed glossary](docs/src/development/GLOSSARY.md) at your side when starting out. It lists and explains some of the structures and terms you will see throughout the codebase.
Zed is made up of several smaller crates - let's go over those you're most likely to interact with:

136
Cargo.lock generated
View File

@@ -308,18 +308,22 @@ dependencies = [
"libc",
"log",
"nix 0.29.0",
"node_runtime",
"paths",
"project",
"reqwest_client",
"schemars",
"semver",
"serde",
"serde_json",
"settings",
"smol",
"task",
"tempfile",
"thiserror 2.0.12",
"ui",
"util",
"watch",
"which 6.0.3",
"workspace-hack",
]
@@ -2347,6 +2351,19 @@ dependencies = [
"digest",
]
[[package]]
name = "blake3"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0"
dependencies = [
"arrayref",
"arrayvec",
"cc",
"cfg-if",
"constant_time_eq 0.3.1",
]
[[package]]
name = "block"
version = "0.1.6"
@@ -3053,6 +3070,7 @@ dependencies = [
"clock",
"cloud_api_client",
"cloud_llm_client",
"cocoa 0.26.0",
"collections",
"credentials_provider",
"derive_more",
@@ -3065,7 +3083,6 @@ dependencies = [
"http_client_tls",
"httparse",
"log",
"objc2-foundation",
"parking_lot",
"paths",
"postage",
@@ -3498,7 +3515,6 @@ name = "component"
version = "0.1.0"
dependencies = [
"collections",
"documented",
"gpui",
"inventory",
"parking_lot",
@@ -3561,6 +3577,12 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]]
name = "constant_time_eq"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
[[package]]
name = "context_server"
version = "0.1.0"
@@ -5044,7 +5066,6 @@ dependencies = [
"multi_buffer",
"ordered-float 2.10.1",
"parking_lot",
"postage",
"pretty_assertions",
"project",
"rand 0.9.1",
@@ -6142,6 +6163,17 @@ dependencies = [
"futures-util",
]
[[package]]
name = "futures-batch"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f444c45a1cb86f2a7e301469fd50a82084a60dadc25d94529a8312276ecb71a"
dependencies = [
"futures 0.3.31",
"futures-timer",
"pin-utils",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
@@ -6237,6 +6269,12 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-timer"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
[[package]]
name = "futures-util"
version = "0.3.31"
@@ -9182,19 +9220,6 @@ dependencies = [
"x_ai",
]
[[package]]
name = "language_onboarding"
version = "0.1.0"
dependencies = [
"db",
"editor",
"gpui",
"project",
"ui",
"workspace",
"workspace-hack",
]
[[package]]
name = "language_selector"
version = "0.1.0"
@@ -9222,7 +9247,6 @@ dependencies = [
"anyhow",
"client",
"collections",
"command_palette_hooks",
"copilot",
"editor",
"futures 0.3.31",
@@ -9257,6 +9281,7 @@ dependencies = [
"chrono",
"collections",
"dap",
"feature_flags",
"futures 0.3.31",
"gpui",
"http_client",
@@ -9492,21 +9517,6 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "line_ending_selector"
version = "0.1.0"
dependencies = [
"editor",
"gpui",
"language",
"picker",
"project",
"ui",
"util",
"workspace",
"workspace-hack",
]
[[package]]
name = "link-cplusplus"
version = "1.0.10"
@@ -12614,7 +12624,6 @@ dependencies = [
"remote",
"rpc",
"schemars",
"semver",
"serde",
"serde_json",
"settings",
@@ -12634,7 +12643,6 @@ dependencies = [
"unindent",
"url",
"util",
"watch",
"which 6.0.3",
"workspace-hack",
"worktree",
@@ -14663,6 +14671,49 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749"
[[package]]
name = "semantic_index"
version = "0.1.0"
dependencies = [
"anyhow",
"arrayvec",
"blake3",
"client",
"clock",
"collections",
"feature_flags",
"fs",
"futures 0.3.31",
"futures-batch",
"gpui",
"heed",
"http_client",
"language",
"language_model",
"languages",
"log",
"open_ai",
"parking_lot",
"project",
"reqwest_client",
"serde",
"serde_json",
"settings",
"sha2",
"smol",
"streaming-iterator",
"tempfile",
"theme",
"tree-sitter",
"ui",
"unindent",
"util",
"workspace",
"workspace-hack",
"worktree",
"zlog",
]
[[package]]
name = "semantic_version"
version = "0.1.0"
@@ -15280,7 +15331,6 @@ dependencies = [
"futures 0.3.31",
"indoc",
"libsqlite3-sys",
"log",
"parking_lot",
"smol",
"sqlformat",
@@ -16934,15 +16984,10 @@ checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076"
name = "toolchain_selector"
version = "0.1.0"
dependencies = [
"anyhow",
"convert_case 0.8.0",
"editor",
"file_finder",
"futures 0.3.31",
"fuzzy",
"gpui",
"language",
"menu",
"picker",
"project",
"ui",
@@ -20378,6 +20423,7 @@ dependencies = [
"acp_tools",
"activity_indicator",
"agent",
"agent_servers",
"agent_settings",
"agent_ui",
"anyhow",
@@ -20441,12 +20487,10 @@ dependencies = [
"language_extension",
"language_model",
"language_models",
"language_onboarding",
"language_selector",
"language_tools",
"languages",
"libc",
"line_ending_selector",
"livekit_client",
"log",
"markdown",
@@ -20464,7 +20508,6 @@ dependencies = [
"parking_lot",
"paths",
"picker",
"postage",
"pretty_assertions",
"profiling",
"project",
@@ -20599,7 +20642,7 @@ dependencies = [
[[package]]
name = "zed_snippets"
version = "0.0.6"
version = "0.0.5"
dependencies = [
"serde_json",
"zed_extension_api 0.1.0",
@@ -20775,7 +20818,6 @@ dependencies = [
"language_model",
"log",
"menu",
"parking_lot",
"postage",
"project",
"rand 0.9.1",
@@ -20848,7 +20890,7 @@ dependencies = [
"aes",
"byteorder",
"bzip2",
"constant_time_eq",
"constant_time_eq 0.1.5",
"crc32fast",
"crossbeam-utils",
"flate2",

View File

@@ -94,11 +94,9 @@ members = [
"crates/language_extension",
"crates/language_model",
"crates/language_models",
"crates/language_onboarding",
"crates/language_selector",
"crates/language_tools",
"crates/languages",
"crates/line_ending_selector",
"crates/livekit_api",
"crates/livekit_client",
"crates/lmstudio",
@@ -144,6 +142,7 @@ members = [
"crates/rules_library",
"crates/schema_generator",
"crates/search",
"crates/semantic_index",
"crates/semantic_version",
"crates/session",
"crates/settings",
@@ -321,11 +320,9 @@ language = { path = "crates/language" }
language_extension = { path = "crates/language_extension" }
language_model = { path = "crates/language_model" }
language_models = { path = "crates/language_models" }
language_onboarding = { path = "crates/language_onboarding" }
language_selector = { path = "crates/language_selector" }
language_tools = { path = "crates/language_tools" }
languages = { path = "crates/languages" }
line_ending_selector = { path = "crates/line_ending_selector" }
livekit_api = { path = "crates/livekit_api" }
livekit_client = { path = "crates/livekit_client" }
lmstudio = { path = "crates/lmstudio" }
@@ -374,6 +371,7 @@ rope = { path = "crates/rope" }
rpc = { path = "crates/rpc" }
rules_library = { path = "crates/rules_library" }
search = { path = "crates/search" }
semantic_index = { path = "crates/semantic_index" }
semantic_version = { path = "crates/semantic_version" }
session = { path = "crates/session" }
settings = { path = "crates/settings" }
@@ -539,31 +537,6 @@ nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c80421
nix = "0.29"
num-format = "0.4.4"
objc = "0.2"
objc2-foundation = { version = "0.3", default-features = false, features = [
"NSArray",
"NSAttributedString",
"NSBundle",
"NSCoder",
"NSData",
"NSDate",
"NSDictionary",
"NSEnumerator",
"NSError",
"NSGeometry",
"NSNotification",
"NSNull",
"NSObjCRuntime",
"NSObject",
"NSProcessInfo",
"NSRange",
"NSRunLoop",
"NSString",
"NSURL",
"NSUndoManager",
"NSValue",
"objc2-core-foundation",
"std"
] }
open = "5.0.0"
ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] }

View File

@@ -16,7 +16,6 @@
"up": "menu::SelectPrevious",
"enter": "menu::Confirm",
"ctrl-enter": "menu::SecondaryConfirm",
"ctrl-escape": "menu::Cancel",
"ctrl-c": "menu::Cancel",
"escape": "menu::Cancel",
"alt-shift-enter": "menu::Restart",
@@ -583,7 +582,7 @@
"ctrl-n": "workspace::NewFile",
"shift-new": "workspace::NewWindow",
"ctrl-shift-n": "workspace::NewWindow",
"ctrl-`": "terminal_panel::Toggle",
"ctrl-`": "terminal_panel::ToggleFocus",
"f10": ["app_menu::OpenApplicationMenu", "Zed"],
"alt-1": ["workspace::ActivatePane", 0],
"alt-2": ["workspace::ActivatePane", 1],
@@ -628,7 +627,6 @@
"alt-save": "workspace::SaveAll",
"ctrl-alt-s": "workspace::SaveAll",
"ctrl-k m": "language_selector::Toggle",
"ctrl-k ctrl-m": "toolchain::AddToolchain",
"escape": "workspace::Unfollow",
"ctrl-k ctrl-left": "workspace::ActivatePaneLeft",
"ctrl-k ctrl-right": "workspace::ActivatePaneRight",
@@ -1029,13 +1027,6 @@
"tab": "channel_modal::ToggleMode"
}
},
{
"context": "ToolchainSelector",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-a": "toolchain::AddToolchain"
}
},
{
"context": "FileFinder || (FileFinder > Picker > Editor)",
"bindings": {

View File

@@ -649,7 +649,7 @@
"alt-shift-enter": "toast::RunAction",
"cmd-shift-s": "workspace::SaveAs",
"cmd-shift-n": "workspace::NewWindow",
"ctrl-`": "terminal_panel::Toggle",
"ctrl-`": "terminal_panel::ToggleFocus",
"cmd-1": ["workspace::ActivatePane", 0],
"cmd-2": ["workspace::ActivatePane", 1],
"cmd-3": ["workspace::ActivatePane", 2],
@@ -690,7 +690,6 @@
"cmd-?": "agent::ToggleFocus",
"cmd-alt-s": "workspace::SaveAll",
"cmd-k m": "language_selector::Toggle",
"cmd-k cmd-m": "toolchain::AddToolchain",
"escape": "workspace::Unfollow",
"cmd-k cmd-left": "workspace::ActivatePaneLeft",
"cmd-k cmd-right": "workspace::ActivatePaneRight",
@@ -1095,13 +1094,6 @@
"tab": "channel_modal::ToggleMode"
}
},
{
"context": "ToolchainSelector",
"use_key_equivalents": true,
"bindings": {
"cmd-shift-a": "toolchain::AddToolchain"
}
},
{
"context": "FileFinder || (FileFinder > Picker > Editor)",
"use_key_equivalents": true,

View File

@@ -25,6 +25,7 @@
"ctrl-alt-enter": ["picker::ConfirmInput", { "secondary": true }],
"ctrl-shift-w": "workspace::CloseWindow",
"shift-escape": "workspace::ToggleZoom",
"open": "workspace::Open",
"ctrl-o": "workspace::Open",
"ctrl-=": ["zed::IncreaseBufferFontSize", { "persist": false }],
"ctrl-shift-=": ["zed::IncreaseBufferFontSize", { "persist": false }],
@@ -67,13 +68,18 @@
"ctrl-k q": "editor::Rewrap",
"ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
"ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
"cut": "editor::Cut",
"shift-delete": "editor::Cut",
"ctrl-x": "editor::Cut",
"copy": "editor::Copy",
"ctrl-insert": "editor::Copy",
"ctrl-c": "editor::Copy",
"paste": "editor::Paste",
"shift-insert": "editor::Paste",
"ctrl-v": "editor::Paste",
"undo": "editor::Undo",
"ctrl-z": "editor::Undo",
"redo": "editor::Redo",
"ctrl-y": "editor::Redo",
"ctrl-shift-z": "editor::Redo",
"up": "editor::MoveUp",
@@ -132,6 +138,7 @@
"ctrl-shift-enter": "editor::NewlineAbove",
"ctrl-k ctrl-z": "editor::ToggleSoftWrap",
"ctrl-k z": "editor::ToggleSoftWrap",
"find": "buffer_search::Deploy",
"ctrl-f": "buffer_search::Deploy",
"ctrl-h": "buffer_search::DeployReplace",
"ctrl-shift-.": "assistant::QuoteSelection",
@@ -170,6 +177,7 @@
"context": "Markdown",
"use_key_equivalents": true,
"bindings": {
"copy": "markdown::Copy",
"ctrl-c": "markdown::Copy"
}
},
@@ -217,6 +225,7 @@
"bindings": {
"ctrl-enter": "assistant::Assist",
"ctrl-s": "workspace::Save",
"save": "workspace::Save",
"ctrl-shift-,": "assistant::InsertIntoEditor",
"shift-enter": "assistant::Split",
"ctrl-r": "assistant::CycleMessageRole",
@@ -263,6 +272,7 @@
"context": "AgentPanel > Markdown",
"use_key_equivalents": true,
"bindings": {
"copy": "markdown::CopyAsMarkdown",
"ctrl-c": "markdown::CopyAsMarkdown"
}
},
@@ -357,6 +367,7 @@
"context": "PromptLibrary",
"use_key_equivalents": true,
"bindings": {
"new": "rules_library::NewRule",
"ctrl-n": "rules_library::NewRule",
"ctrl-shift-s": "rules_library::ToggleDefaultRule"
}
@@ -370,6 +381,7 @@
"enter": "search::SelectNextMatch",
"shift-enter": "search::SelectPreviousMatch",
"alt-enter": "search::SelectAllMatches",
"find": "search::FocusSearch",
"ctrl-f": "search::FocusSearch",
"ctrl-h": "search::ToggleReplace",
"ctrl-l": "search::ToggleSelection"
@@ -396,6 +408,7 @@
"use_key_equivalents": true,
"bindings": {
"escape": "project_search::ToggleFocus",
"shift-find": "search::FocusSearch",
"ctrl-shift-f": "search::FocusSearch",
"ctrl-shift-h": "search::ToggleReplace",
"alt-r": "search::ToggleRegex" // vscode
@@ -459,12 +472,14 @@
"forward": "pane::GoForward",
"f3": "search::SelectNextMatch",
"shift-f3": "search::SelectPreviousMatch",
"shift-find": "project_search::ToggleFocus",
"ctrl-shift-f": "project_search::ToggleFocus",
"shift-alt-h": "search::ToggleReplace",
"alt-l": "search::ToggleSelection",
"alt-enter": "search::SelectAllMatches",
"alt-c": "search::ToggleCaseSensitive",
"alt-w": "search::ToggleWholeWord",
"alt-find": "project_search::ToggleFilters",
"alt-f": "project_search::ToggleFilters",
"alt-r": "search::ToggleRegex",
// "ctrl-shift-alt-x": "search::ToggleRegex",
@@ -564,21 +579,27 @@
"context": "Workspace",
"use_key_equivalents": true,
"bindings": {
"alt-open": ["projects::OpenRecent", { "create_new_window": false }],
// Change the default action on `menu::Confirm` by setting the parameter
// "ctrl-alt-o": ["projects::OpenRecent", { "create_new_window": true }],
"ctrl-r": ["projects::OpenRecent", { "create_new_window": false }],
"shift-alt-open": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
// Change to open path modal for existing remote connection by setting the parameter
// "ctrl-shift-alt-o": "["projects::OpenRemote", { "from_existing_connection": true }]",
"ctrl-shift-alt-o": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
"shift-alt-b": "branches::OpenRecent",
"shift-alt-enter": "toast::RunAction",
"ctrl-shift-`": "workspace::NewTerminal",
"save": "workspace::Save",
"ctrl-s": "workspace::Save",
"ctrl-k ctrl-shift-s": "workspace::SaveWithoutFormat",
"shift-save": "workspace::SaveAs",
"ctrl-shift-s": "workspace::SaveAs",
"new": "workspace::NewFile",
"ctrl-n": "workspace::NewFile",
"shift-new": "workspace::NewWindow",
"ctrl-shift-n": "workspace::NewWindow",
"ctrl-`": "terminal_panel::Toggle",
"ctrl-`": "terminal_panel::ToggleFocus",
"f10": ["app_menu::OpenApplicationMenu", "Zed"],
"alt-1": ["workspace::ActivatePane", 0],
"alt-2": ["workspace::ActivatePane", 1],
@@ -600,6 +621,7 @@
"shift-alt-0": "workspace::ResetOpenDocksSize",
"ctrl-shift-alt--": ["workspace::DecreaseOpenDocksSize", { "px": 0 }],
"ctrl-shift-alt-=": ["workspace::IncreaseOpenDocksSize", { "px": 0 }],
"shift-find": "pane::DeploySearch",
"ctrl-shift-f": "pane::DeploySearch",
"ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
"ctrl-shift-t": "pane::ReopenClosedItem",
@@ -619,9 +641,9 @@
"ctrl-shift-g": "git_panel::ToggleFocus",
"ctrl-shift-d": "debug_panel::ToggleFocus",
"ctrl-shift-/": "agent::ToggleFocus",
"alt-save": "workspace::SaveAll",
"ctrl-k s": "workspace::SaveAll",
"ctrl-k m": "language_selector::Toggle",
"ctrl-m ctrl-m": "toolchain::AddToolchain",
"escape": "workspace::Unfollow",
"ctrl-k ctrl-left": "workspace::ActivatePaneLeft",
"ctrl-k ctrl-right": "workspace::ActivatePaneRight",
@@ -826,7 +848,9 @@
"bindings": {
"left": "outline_panel::CollapseSelectedEntry",
"right": "outline_panel::ExpandSelectedEntry",
"alt-copy": "outline_panel::CopyPath",
"shift-alt-c": "outline_panel::CopyPath",
"shift-alt-copy": "workspace::CopyRelativePath",
"ctrl-shift-alt-c": "workspace::CopyRelativePath",
"ctrl-alt-r": "outline_panel::RevealInFileManager",
"space": "outline_panel::OpenSelectedEntry",
@@ -842,14 +866,21 @@
"bindings": {
"left": "project_panel::CollapseSelectedEntry",
"right": "project_panel::ExpandSelectedEntry",
"new": "project_panel::NewFile",
"ctrl-n": "project_panel::NewFile",
"alt-new": "project_panel::NewDirectory",
"alt-n": "project_panel::NewDirectory",
"cut": "project_panel::Cut",
"ctrl-x": "project_panel::Cut",
"copy": "project_panel::Copy",
"ctrl-insert": "project_panel::Copy",
"ctrl-c": "project_panel::Copy",
"paste": "project_panel::Paste",
"shift-insert": "project_panel::Paste",
"ctrl-v": "project_panel::Paste",
"alt-copy": "project_panel::CopyPath",
"shift-alt-c": "project_panel::CopyPath",
"shift-alt-copy": "workspace::CopyRelativePath",
"ctrl-k ctrl-shift-c": "workspace::CopyRelativePath",
"enter": "project_panel::Rename",
"f2": "project_panel::Rename",
@@ -861,6 +892,7 @@
"ctrl-alt-r": "project_panel::RevealInFileManager",
"ctrl-shift-enter": "project_panel::OpenWithSystem",
"alt-d": "project_panel::CompareMarkedFiles",
"shift-find": "project_panel::NewSearchInDirectory",
"ctrl-k ctrl-shift-f": "project_panel::NewSearchInDirectory",
"shift-down": "menu::SelectNext",
"shift-up": "menu::SelectPrevious",
@@ -1043,13 +1075,6 @@
"tab": "channel_modal::ToggleMode"
}
},
{
"context": "ToolchainSelector",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-a": "toolchain::AddToolchain"
}
},
{
"context": "FileFinder || (FileFinder > Picker > Editor)",
"use_key_equivalents": true,
@@ -1085,8 +1110,10 @@
"use_key_equivalents": true,
"bindings": {
"ctrl-alt-space": "terminal::ShowCharacterPalette",
"copy": "terminal::Copy",
"ctrl-insert": "terminal::Copy",
"ctrl-shift-c": "terminal::Copy",
"paste": "terminal::Paste",
"shift-insert": "terminal::Paste",
"ctrl-shift-v": "terminal::Paste",
"ctrl-enter": "assistant::InlineAssist",
@@ -1102,6 +1129,7 @@
"ctrl-w": ["terminal::SendKeystroke", "ctrl-w"],
"ctrl-backspace": ["terminal::SendKeystroke", "ctrl-w"],
"ctrl-shift-a": "editor::SelectAll",
"find": "buffer_search::Deploy",
"ctrl-shift-f": "buffer_search::Deploy",
"ctrl-shift-l": "terminal::Clear",
"ctrl-shift-w": "pane::CloseActiveItem",
@@ -1182,6 +1210,7 @@
"use_key_equivalents": true,
"bindings": {
"ctrl-f": "search::FocusSearch",
"alt-find": "keymap_editor::ToggleKeystrokeSearch",
"alt-f": "keymap_editor::ToggleKeystrokeSearch",
"alt-c": "keymap_editor::ToggleConflictFilter",
"enter": "keymap_editor::EditBinding",

View File

@@ -125,7 +125,7 @@
{
"context": "Workspace || Editor",
"bindings": {
"alt-f12": "terminal_panel::Toggle",
"alt-f12": "terminal_panel::ToggleFocus",
"ctrl-shift-k": "git::Push"
}
},

View File

@@ -127,7 +127,7 @@
{
"context": "Workspace || Editor",
"bindings": {
"alt-f12": "terminal_panel::Toggle",
"alt-f12": "terminal_panel::ToggleFocus",
"cmd-shift-k": "git::Push"
}
},

View File

@@ -32,6 +32,34 @@
"(": "vim::SentenceBackward",
")": "vim::SentenceForward",
"|": "vim::GoToColumn",
"] ]": "vim::NextSectionStart",
"] [": "vim::NextSectionEnd",
"[ [": "vim::PreviousSectionStart",
"[ ]": "vim::PreviousSectionEnd",
"] m": "vim::NextMethodStart",
"] shift-m": "vim::NextMethodEnd",
"[ m": "vim::PreviousMethodStart",
"[ shift-m": "vim::PreviousMethodEnd",
"[ *": "vim::PreviousComment",
"[ /": "vim::PreviousComment",
"] *": "vim::NextComment",
"] /": "vim::NextComment",
"[ -": "vim::PreviousLesserIndent",
"[ +": "vim::PreviousGreaterIndent",
"[ =": "vim::PreviousSameIndent",
"] -": "vim::NextLesserIndent",
"] +": "vim::NextGreaterIndent",
"] =": "vim::NextSameIndent",
"] b": "pane::ActivateNextItem",
"[ b": "pane::ActivatePreviousItem",
"] shift-b": "pane::ActivateLastItem",
"[ shift-b": ["pane::ActivateItem", 0],
"] space": "vim::InsertEmptyLineBelow",
"[ space": "vim::InsertEmptyLineAbove",
"[ e": "editor::MoveLineUp",
"] e": "editor::MoveLineDown",
"[ f": "workspace::FollowNextCollaborator",
"] f": "workspace::FollowNextCollaborator",
// Word motions
"w": "vim::NextWordStart",
@@ -55,6 +83,10 @@
"n": "vim::MoveToNextMatch",
"shift-n": "vim::MoveToPreviousMatch",
"%": "vim::Matching",
"] }": ["vim::UnmatchedForward", { "char": "}" }],
"[ {": ["vim::UnmatchedBackward", { "char": "{" }],
"] )": ["vim::UnmatchedForward", { "char": ")" }],
"[ (": ["vim::UnmatchedBackward", { "char": "(" }],
"f": ["vim::PushFindForward", { "before": false, "multiline": false }],
"t": ["vim::PushFindForward", { "before": true, "multiline": false }],
"shift-f": ["vim::PushFindBackward", { "after": false, "multiline": false }],
@@ -187,46 +219,6 @@
".": "vim::Repeat"
}
},
{
"context": "vim_mode == normal || vim_mode == visual || vim_mode == operator",
"bindings": {
"] ]": "vim::NextSectionStart",
"] [": "vim::NextSectionEnd",
"[ [": "vim::PreviousSectionStart",
"[ ]": "vim::PreviousSectionEnd",
"] m": "vim::NextMethodStart",
"] shift-m": "vim::NextMethodEnd",
"[ m": "vim::PreviousMethodStart",
"[ shift-m": "vim::PreviousMethodEnd",
"[ *": "vim::PreviousComment",
"[ /": "vim::PreviousComment",
"] *": "vim::NextComment",
"] /": "vim::NextComment",
"[ -": "vim::PreviousLesserIndent",
"[ +": "vim::PreviousGreaterIndent",
"[ =": "vim::PreviousSameIndent",
"] -": "vim::NextLesserIndent",
"] +": "vim::NextGreaterIndent",
"] =": "vim::NextSameIndent",
"] b": "pane::ActivateNextItem",
"[ b": "pane::ActivatePreviousItem",
"] shift-b": "pane::ActivateLastItem",
"[ shift-b": ["pane::ActivateItem", 0],
"] space": "vim::InsertEmptyLineBelow",
"[ space": "vim::InsertEmptyLineAbove",
"[ e": "editor::MoveLineUp",
"] e": "editor::MoveLineDown",
"[ f": "workspace::FollowNextCollaborator",
"] f": "workspace::FollowNextCollaborator",
"] }": ["vim::UnmatchedForward", { "char": "}" }],
"[ {": ["vim::UnmatchedBackward", { "char": "{" }],
"] )": ["vim::UnmatchedForward", { "char": ")" }],
"[ (": ["vim::UnmatchedBackward", { "char": "(" }],
// tree-sitter related commands
"[ x": "vim::SelectLargerSyntaxNode",
"] x": "vim::SelectSmallerSyntaxNode"
}
},
{
"context": "vim_mode == normal",
"bindings": {
@@ -257,6 +249,9 @@
"g w": "vim::PushRewrap",
"g q": "vim::PushRewrap",
"insert": "vim::InsertBefore",
// tree-sitter related commands
"[ x": "vim::SelectLargerSyntaxNode",
"] x": "vim::SelectSmallerSyntaxNode",
"] d": "editor::GoToDiagnostic",
"[ d": "editor::GoToPreviousDiagnostic",
"] c": "editor::GoToHunk",
@@ -322,7 +317,10 @@
"g w": "vim::Rewrap",
"g ?": "vim::ConvertToRot13",
// "g ?": "vim::ConvertToRot47",
"\"": "vim::PushRegister"
"\"": "vim::PushRegister",
// tree-sitter related commands
"[ x": "editor::SelectLargerSyntaxNode",
"] x": "editor::SelectSmallerSyntaxNode"
}
},
{
@@ -399,9 +397,6 @@
"ctrl-[": "editor::Cancel",
";": "vim::HelixCollapseSelection",
":": "command_palette::Toggle",
"m": "vim::PushHelixMatch",
"]": ["vim::PushHelixNext", { "around": true }],
"[": ["vim::PushHelixPrevious", { "around": true }],
"left": "vim::WrappingLeft",
"right": "vim::WrappingRight",
"h": "vim::WrappingLeft",
@@ -424,6 +419,13 @@
"insert": "vim::InsertBefore",
"alt-.": "vim::RepeatFind",
"alt-s": ["editor::SplitSelectionIntoLines", { "keep_selections": true }],
// tree-sitter related commands
"[ x": "editor::SelectLargerSyntaxNode",
"] x": "editor::SelectSmallerSyntaxNode",
"] d": "editor::GoToDiagnostic",
"[ d": "editor::GoToPreviousDiagnostic",
"] c": "editor::GoToHunk",
"[ c": "editor::GoToPreviousHunk",
// Goto mode
"g n": "pane::ActivateNextItem",
"g p": "pane::ActivatePreviousItem",
@@ -467,6 +469,9 @@
"space c": "editor::ToggleComments",
"space y": "editor::Copy",
"space p": "editor::Paste",
// Match mode
"m m": "vim::Matching",
"m i w": ["workspace::SendKeystrokes", "v i w"],
"shift-u": "editor::Redo",
"ctrl-c": "editor::ToggleComments",
"d": "vim::HelixDelete",
@@ -535,7 +540,7 @@
}
},
{
"context": "vim_operator == a || vim_operator == i || vim_operator == cs || vim_operator == helix_next || vim_operator == helix_previous",
"context": "vim_operator == a || vim_operator == i || vim_operator == cs",
"bindings": {
"w": "vim::Word",
"shift-w": ["vim::Word", { "ignore_punctuation": true }],
@@ -572,48 +577,6 @@
"e": "vim::EntireFile"
}
},
{
"context": "vim_operator == helix_m",
"bindings": {
"m": "vim::Matching"
}
},
{
"context": "vim_operator == helix_next",
"bindings": {
"z": "vim::NextSectionStart",
"shift-z": "vim::NextSectionEnd",
"*": "vim::NextComment",
"/": "vim::NextComment",
"-": "vim::NextLesserIndent",
"+": "vim::NextGreaterIndent",
"=": "vim::NextSameIndent",
"b": "pane::ActivateNextItem",
"shift-b": "pane::ActivateLastItem",
"x": "editor::SelectSmallerSyntaxNode",
"d": "editor::GoToDiagnostic",
"c": "editor::GoToHunk",
"space": "vim::InsertEmptyLineBelow"
}
},
{
"context": "vim_operator == helix_previous",
"bindings": {
"z": "vim::PreviousSectionStart",
"shift-z": "vim::PreviousSectionEnd",
"*": "vim::PreviousComment",
"/": "vim::PreviousComment",
"-": "vim::PreviousLesserIndent",
"+": "vim::PreviousGreaterIndent",
"=": "vim::PreviousSameIndent",
"b": "pane::ActivatePreviousItem",
"shift-b": ["pane::ActivateItem", 0],
"x": "editor::SelectLargerSyntaxNode",
"d": "editor::GoToPreviousDiagnostic",
"c": "editor::GoToPreviousHunk",
"space": "vim::InsertEmptyLineAbove"
}
},
{
"context": "vim_operator == c",
"bindings": {

View File

@@ -962,7 +962,7 @@
// Show git status colors in the editor tabs.
"git_status": false,
// Position of the close button on the editor tabs.
// One of: ["right", "left"]
// One of: ["right", "left", "hidden"]
"close_position": "right",
// Whether to show the file icon for a tab.
"file_icons": false,

View File

@@ -2758,7 +2758,7 @@ mod tests {
}));
let thread = cx
.update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx))
.update(|cx| connection.new_thread(project, Path::new("/test"), cx))
.await
.unwrap();

View File

@@ -212,8 +212,7 @@ impl ActivityIndicator {
server_name,
status,
} => {
let create_buffer =
project.update(cx, |project, cx| project.create_buffer(false, cx));
let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx));
let status = status.clone();
let server_name = server_name.clone();
cx.spawn_in(window, async move |workspace, cx| {

View File

@@ -35,15 +35,10 @@ impl AgentServer for NativeAgentServer {
fn connect(
&self,
_root_dir: Option<&Path>,
_root_dir: &Path,
delegate: AgentServerDelegate,
cx: &mut App,
) -> Task<
Result<(
Rc<dyn acp_thread::AgentConnection>,
Option<task::SpawnInTerminal>,
)>,
> {
) -> Task<Result<Rc<dyn acp_thread::AgentConnection>>> {
log::debug!(
"NativeAgentServer::connect called for path: {:?}",
_root_dir
@@ -65,10 +60,7 @@ impl AgentServer for NativeAgentServer {
let connection = NativeAgentConnection(agent);
log::debug!("NativeAgentServer connection established successfully");
Ok((
Rc::new(connection) as Rc<dyn acp_thread::AgentConnection>,
None,
))
Ok(Rc::new(connection) as Rc<dyn acp_thread::AgentConnection>)
})
}

View File

@@ -24,11 +24,7 @@ impl AgentTool for EchoTool {
acp::ToolKind::Other
}
fn initial_title(
&self,
_input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
"Echo".into()
}
@@ -59,11 +55,7 @@ impl AgentTool for DelayTool {
"delay"
}
fn initial_title(
&self,
input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
if let Ok(input) = input {
format!("Delay {}ms", input.ms).into()
} else {
@@ -108,11 +100,7 @@ impl AgentTool for ToolRequiringPermission {
acp::ToolKind::Other
}
fn initial_title(
&self,
_input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
"This tool requires permission".into()
}
@@ -147,11 +135,7 @@ impl AgentTool for InfiniteTool {
acp::ToolKind::Other
}
fn initial_title(
&self,
_input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
"Infinite Tool".into()
}
@@ -202,11 +186,7 @@ impl AgentTool for WordListTool {
acp::ToolKind::Other
}
fn initial_title(
&self,
_input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
"List of random words".into()
}

View File

@@ -741,7 +741,7 @@ impl Thread {
return;
};
let title = tool.initial_title(tool_use.input.clone(), cx);
let title = tool.initial_title(tool_use.input.clone());
let kind = tool.kind();
stream.send_tool_call(&tool_use.id, title, kind, tool_use.input.clone());
@@ -1062,11 +1062,7 @@ impl Thread {
self.action_log.clone(),
));
self.add_tool(DiagnosticsTool::new(self.project.clone()));
self.add_tool(EditFileTool::new(
self.project.clone(),
cx.weak_entity(),
language_registry,
));
self.add_tool(EditFileTool::new(cx.weak_entity(), language_registry));
self.add_tool(FetchTool::new(self.project.read(cx).client().http_client()));
self.add_tool(FindPathTool::new(self.project.clone()));
self.add_tool(GrepTool::new(self.project.clone()));
@@ -1518,7 +1514,7 @@ impl Thread {
let mut title = SharedString::from(&tool_use.name);
let mut kind = acp::ToolKind::Other;
if let Some(tool) = tool.as_ref() {
title = tool.initial_title(tool_use.input.clone(), cx);
title = tool.initial_title(tool_use.input.clone());
kind = tool.kind();
}
@@ -2152,11 +2148,7 @@ where
fn kind() -> acp::ToolKind;
/// The initial tool title to display. Can be updated during the tool run.
fn initial_title(
&self,
input: Result<Self::Input, serde_json::Value>,
cx: &mut App,
) -> SharedString;
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString;
/// Returns the JSON schema that describes the tool's input.
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Schema {
@@ -2204,7 +2196,7 @@ pub trait AnyAgentTool {
fn name(&self) -> SharedString;
fn description(&self) -> SharedString;
fn kind(&self) -> acp::ToolKind;
fn initial_title(&self, input: serde_json::Value, _cx: &mut App) -> SharedString;
fn initial_title(&self, input: serde_json::Value) -> SharedString;
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value>;
fn supported_provider(&self, _provider: &LanguageModelProviderId) -> bool {
true
@@ -2240,9 +2232,9 @@ where
T::kind()
}
fn initial_title(&self, input: serde_json::Value, _cx: &mut App) -> SharedString {
fn initial_title(&self, input: serde_json::Value) -> SharedString {
let parsed_input = serde_json::from_value(input.clone()).map_err(|_| input);
self.0.initial_title(parsed_input, _cx)
self.0.initial_title(parsed_input)
}
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {

View File

@@ -145,7 +145,7 @@ impl AnyAgentTool for ContextServerTool {
ToolKind::Other
}
fn initial_title(&self, _input: serde_json::Value, _cx: &mut App) -> SharedString {
fn initial_title(&self, _input: serde_json::Value) -> SharedString {
format!("Run MCP tool `{}`", self.tool.name).into()
}
@@ -176,7 +176,7 @@ impl AnyAgentTool for ContextServerTool {
return Task::ready(Err(anyhow!("Context server not found")));
};
let tool_name = self.tool.name.clone();
let authorize = event_stream.authorize(self.initial_title(input.clone(), cx), cx);
let authorize = event_stream.authorize(self.initial_title(input.clone()), cx);
cx.spawn(async move |_cx| {
authorize.await?;

View File

@@ -58,11 +58,7 @@ impl AgentTool for CopyPathTool {
ToolKind::Move
}
fn initial_title(
&self,
input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> ui::SharedString {
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> ui::SharedString {
if let Ok(input) = input {
let src = MarkdownInlineCode(&input.source_path);
let dest = MarkdownInlineCode(&input.destination_path);

View File

@@ -49,11 +49,7 @@ impl AgentTool for CreateDirectoryTool {
ToolKind::Read
}
fn initial_title(
&self,
input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
if let Ok(input) = input {
format!("Create directory {}", MarkdownInlineCode(&input.path)).into()
} else {

View File

@@ -52,11 +52,7 @@ impl AgentTool for DeletePathTool {
ToolKind::Delete
}
fn initial_title(
&self,
input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
if let Ok(input) = input {
format!("Delete “`{}`”", input.path).into()
} else {

View File

@@ -71,11 +71,7 @@ impl AgentTool for DiagnosticsTool {
acp::ToolKind::Read
}
fn initial_title(
&self,
input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
if let Some(path) = input.ok().and_then(|input| match input.path {
Some(path) if !path.is_empty() => Some(path),
_ => None,

View File

@@ -120,17 +120,11 @@ impl From<EditFileToolOutput> for LanguageModelToolResultContent {
pub struct EditFileTool {
thread: WeakEntity<Thread>,
language_registry: Arc<LanguageRegistry>,
project: Entity<Project>,
}
impl EditFileTool {
pub fn new(
project: Entity<Project>,
thread: WeakEntity<Thread>,
language_registry: Arc<LanguageRegistry>,
) -> Self {
pub fn new(thread: WeakEntity<Thread>, language_registry: Arc<LanguageRegistry>) -> Self {
Self {
project,
thread,
language_registry,
}
@@ -201,50 +195,22 @@ impl AgentTool for EditFileTool {
acp::ToolKind::Edit
}
fn initial_title(
&self,
input: Result<Self::Input, serde_json::Value>,
cx: &mut App,
) -> SharedString {
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
match input {
Ok(input) => self
.project
.read(cx)
.find_project_path(&input.path, cx)
.and_then(|project_path| {
self.project
.read(cx)
.short_full_path_for_project_path(&project_path, cx)
})
.unwrap_or(Path::new(&input.path).into())
.to_string_lossy()
.to_string()
.into(),
Ok(input) => input.display_description.into(),
Err(raw_input) => {
if let Some(input) =
serde_json::from_value::<EditFileToolPartialInput>(raw_input).ok()
{
let path = input.path.trim();
if !path.is_empty() {
return self
.project
.read(cx)
.find_project_path(&input.path, cx)
.and_then(|project_path| {
self.project
.read(cx)
.short_full_path_for_project_path(&project_path, cx)
})
.unwrap_or(Path::new(&input.path).into())
.to_string_lossy()
.to_string()
.into();
}
let description = input.display_description.trim();
if !description.is_empty() {
return description.to_string().into();
}
let path = input.path.trim().to_string();
if !path.is_empty() {
return path.into();
}
}
DEFAULT_UI_TEXT.into()
@@ -579,7 +545,7 @@ mod tests {
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
project,
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
@@ -594,12 +560,11 @@ mod tests {
path: "root/nonexistent_file.txt".into(),
mode: EditFileMode::Edit,
};
Arc::new(EditFileTool::new(
project,
thread.downgrade(),
language_registry,
))
.run(input, ToolCallEventStream::test().0, cx)
Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run(
input,
ToolCallEventStream::test().0,
cx,
)
})
.await;
assert_eq!(
@@ -778,7 +743,7 @@ mod tests {
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
project,
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
@@ -810,7 +775,6 @@ mod tests {
mode: EditFileMode::Overwrite,
};
Arc::new(EditFileTool::new(
project.clone(),
thread.downgrade(),
language_registry.clone(),
))
@@ -869,12 +833,11 @@ mod tests {
path: "root/src/main.rs".into(),
mode: EditFileMode::Overwrite,
};
Arc::new(EditFileTool::new(
project.clone(),
thread.downgrade(),
language_registry,
))
.run(input, ToolCallEventStream::test().0, cx)
Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run(
input,
ToolCallEventStream::test().0,
cx,
)
});
// Stream the unformatted content
@@ -922,7 +885,7 @@ mod tests {
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
project,
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
@@ -955,7 +918,6 @@ mod tests {
mode: EditFileMode::Overwrite,
};
Arc::new(EditFileTool::new(
project.clone(),
thread.downgrade(),
language_registry.clone(),
))
@@ -1007,12 +969,11 @@ mod tests {
path: "root/src/main.rs".into(),
mode: EditFileMode::Overwrite,
};
Arc::new(EditFileTool::new(
project.clone(),
thread.downgrade(),
language_registry,
))
.run(input, ToolCallEventStream::test().0, cx)
Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run(
input,
ToolCallEventStream::test().0,
cx,
)
});
// Stream the content with trailing whitespace
@@ -1051,7 +1012,7 @@ mod tests {
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
project,
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
@@ -1059,11 +1020,7 @@ mod tests {
cx,
)
});
let tool = Arc::new(EditFileTool::new(
project.clone(),
thread.downgrade(),
language_registry,
));
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
fs.insert_tree("/root", json!({})).await;
// Test 1: Path with .zed component should require confirmation
@@ -1191,7 +1148,7 @@ mod tests {
let model = Arc::new(FakeLanguageModel::default());
let thread = cx.new(|cx| {
Thread::new(
project.clone(),
project,
cx.new(|_cx| ProjectContext::default()),
context_server_registry,
Templates::new(),
@@ -1199,11 +1156,7 @@ mod tests {
cx,
)
});
let tool = Arc::new(EditFileTool::new(
project.clone(),
thread.downgrade(),
language_registry,
));
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
// Test global config paths - these should require confirmation if they exist and are outside the project
let test_cases = vec![
@@ -1311,11 +1264,7 @@ mod tests {
cx,
)
});
let tool = Arc::new(EditFileTool::new(
project.clone(),
thread.downgrade(),
language_registry,
));
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
// Test files in different worktrees
let test_cases = vec![
@@ -1395,11 +1344,7 @@ mod tests {
cx,
)
});
let tool = Arc::new(EditFileTool::new(
project.clone(),
thread.downgrade(),
language_registry,
));
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
// Test edge cases
let test_cases = vec![
@@ -1482,11 +1427,7 @@ mod tests {
cx,
)
});
let tool = Arc::new(EditFileTool::new(
project.clone(),
thread.downgrade(),
language_registry,
));
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
// Test different EditFileMode values
let modes = vec![
@@ -1566,67 +1507,48 @@ mod tests {
cx,
)
});
let tool = Arc::new(EditFileTool::new(
project,
thread.downgrade(),
language_registry,
));
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
cx.update(|cx| {
// ...
assert_eq!(
tool.initial_title(
Err(json!({
"path": "src/main.rs",
"display_description": "",
"old_string": "old code",
"new_string": "new code"
})),
cx
),
"src/main.rs"
);
assert_eq!(
tool.initial_title(
Err(json!({
"path": "",
"display_description": "Fix error handling",
"old_string": "old code",
"new_string": "new code"
})),
cx
),
"Fix error handling"
);
assert_eq!(
tool.initial_title(
Err(json!({
"path": "src/main.rs",
"display_description": "Fix error handling",
"old_string": "old code",
"new_string": "new code"
})),
cx
),
"src/main.rs"
);
assert_eq!(
tool.initial_title(
Err(json!({
"path": "",
"display_description": "",
"old_string": "old code",
"new_string": "new code"
})),
cx
),
DEFAULT_UI_TEXT
);
assert_eq!(
tool.initial_title(Err(serde_json::Value::Null), cx),
DEFAULT_UI_TEXT
);
});
assert_eq!(
tool.initial_title(Err(json!({
"path": "src/main.rs",
"display_description": "",
"old_string": "old code",
"new_string": "new code"
}))),
"src/main.rs"
);
assert_eq!(
tool.initial_title(Err(json!({
"path": "",
"display_description": "Fix error handling",
"old_string": "old code",
"new_string": "new code"
}))),
"Fix error handling"
);
assert_eq!(
tool.initial_title(Err(json!({
"path": "src/main.rs",
"display_description": "Fix error handling",
"old_string": "old code",
"new_string": "new code"
}))),
"Fix error handling"
);
assert_eq!(
tool.initial_title(Err(json!({
"path": "",
"display_description": "",
"old_string": "old code",
"new_string": "new code"
}))),
DEFAULT_UI_TEXT
);
assert_eq!(
tool.initial_title(Err(serde_json::Value::Null)),
DEFAULT_UI_TEXT
);
}
#[gpui::test]
@@ -1653,11 +1575,7 @@ mod tests {
// Ensure the diff is finalized after the edit completes.
{
let tool = Arc::new(EditFileTool::new(
project.clone(),
thread.downgrade(),
languages.clone(),
));
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
let edit = cx.update(|cx| {
tool.run(
@@ -1682,11 +1600,7 @@ mod tests {
// Ensure the diff is finalized if an error occurs while editing.
{
model.forbid_requests();
let tool = Arc::new(EditFileTool::new(
project.clone(),
thread.downgrade(),
languages.clone(),
));
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
let edit = cx.update(|cx| {
tool.run(
@@ -1709,11 +1623,7 @@ mod tests {
// Ensure the diff is finalized if the tool call gets dropped.
{
let tool = Arc::new(EditFileTool::new(
project.clone(),
thread.downgrade(),
languages.clone(),
));
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
let edit = cx.update(|cx| {
tool.run(

View File

@@ -126,11 +126,7 @@ impl AgentTool for FetchTool {
acp::ToolKind::Fetch
}
fn initial_title(
&self,
input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
match input {
Ok(input) => format!("Fetch {}", MarkdownEscaped(&input.url)).into(),
Err(_) => "Fetch URL".into(),

View File

@@ -93,11 +93,7 @@ impl AgentTool for FindPathTool {
acp::ToolKind::Search
}
fn initial_title(
&self,
input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
let mut title = "Find paths".to_string();
if let Ok(input) = input {
title.push_str(&format!(" matching “`{}`”", input.glob));

View File

@@ -75,11 +75,7 @@ impl AgentTool for GrepTool {
acp::ToolKind::Search
}
fn initial_title(
&self,
input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
match input {
Ok(input) => {
let page = input.page();

View File

@@ -59,11 +59,7 @@ impl AgentTool for ListDirectoryTool {
ToolKind::Read
}
fn initial_title(
&self,
input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
if let Ok(input) = input {
let path = MarkdownInlineCode(&input.path);
format!("List the {path} directory's contents").into()

View File

@@ -60,11 +60,7 @@ impl AgentTool for MovePathTool {
ToolKind::Move
}
fn initial_title(
&self,
input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
if let Ok(input) = input {
let src = MarkdownInlineCode(&input.source_path);
let dest = MarkdownInlineCode(&input.destination_path);

View File

@@ -41,11 +41,7 @@ impl AgentTool for NowTool {
acp::ToolKind::Other
}
fn initial_title(
&self,
_input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
"Get current time".into()
}

View File

@@ -45,11 +45,7 @@ impl AgentTool for OpenTool {
ToolKind::Execute
}
fn initial_title(
&self,
input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
if let Ok(input) = input {
format!("Open `{}`", MarkdownEscaped(&input.path_or_url)).into()
} else {
@@ -65,7 +61,7 @@ impl AgentTool for OpenTool {
) -> Task<Result<Self::Output>> {
// If path_or_url turns out to be a path in the project, make it absolute.
let abs_path = to_absolute_path(&input.path_or_url, self.project.clone(), cx);
let authorize = event_stream.authorize(self.initial_title(Ok(input.clone()), cx), cx);
let authorize = event_stream.authorize(self.initial_title(Ok(input.clone())), cx);
cx.background_spawn(async move {
authorize.await?;

View File

@@ -10,7 +10,7 @@ use project::{AgentLocation, ImageItem, Project, WorktreeSettings, image_store};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
use std::sync::Arc;
use std::{path::Path, sync::Arc};
use util::markdown::MarkdownCodeBlock;
use crate::{AgentTool, ToolCallEventStream};
@@ -68,31 +68,13 @@ impl AgentTool for ReadFileTool {
acp::ToolKind::Read
}
fn initial_title(
&self,
input: Result<Self::Input, serde_json::Value>,
cx: &mut App,
) -> SharedString {
if let Ok(input) = input
&& let Some(project_path) = self.project.read(cx).find_project_path(&input.path, cx)
&& let Some(path) = self
.project
.read(cx)
.short_full_path_for_project_path(&project_path, cx)
{
match (input.start_line, input.end_line) {
(Some(start), Some(end)) => {
format!("Read file `{}` (lines {}-{})", path.display(), start, end,)
}
(Some(start), None) => {
format!("Read file `{}` (from line {})", path.display(), start)
}
_ => format!("Read file `{}`", path.display()),
}
.into()
} else {
"Read file".into()
}
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
input
.ok()
.as_ref()
.and_then(|input| Path::new(&input.path).file_name())
.map(|file_name| file_name.to_string_lossy().to_string().into())
.unwrap_or_default()
}
fn run(
@@ -104,12 +86,6 @@ impl AgentTool for ReadFileTool {
let Some(project_path) = self.project.read(cx).find_project_path(&input.path, cx) else {
return Task::ready(Err(anyhow!("Path {} not found in project", &input.path)));
};
let Some(abs_path) = self.project.read(cx).absolute_path(&project_path, cx) else {
return Task::ready(Err(anyhow!(
"Failed to convert {} to absolute path",
&input.path
)));
};
// Error out if this path is either excluded or private in global settings
let global_settings = WorktreeSettings::get_global(cx);
@@ -145,14 +121,6 @@ impl AgentTool for ReadFileTool {
let file_path = input.path.clone();
event_stream.update_fields(ToolCallUpdateFields {
locations: Some(vec![acp::ToolCallLocation {
path: abs_path,
line: input.start_line.map(|line| line.saturating_sub(1)),
}]),
..Default::default()
});
if image_store::is_image_file(&self.project, &project_path, cx) {
return cx.spawn(async move |cx| {
let image_entity: Entity<ImageItem> = cx
@@ -261,25 +229,34 @@ impl AgentTool for ReadFileTool {
};
project.update(cx, |project, cx| {
project.set_agent_location(
Some(AgentLocation {
buffer: buffer.downgrade(),
position: anchor.unwrap_or(text::Anchor::MIN),
}),
cx,
);
if let Ok(LanguageModelToolResultContent::Text(text)) = &result {
let markdown = MarkdownCodeBlock {
tag: &input.path,
text,
}
.to_string();
if let Some(abs_path) = project.absolute_path(&project_path, cx) {
project.set_agent_location(
Some(AgentLocation {
buffer: buffer.downgrade(),
position: anchor.unwrap_or(text::Anchor::MIN),
}),
cx,
);
event_stream.update_fields(ToolCallUpdateFields {
content: Some(vec![acp::ToolCallContent::Content {
content: markdown.into(),
locations: Some(vec![acp::ToolCallLocation {
path: abs_path,
line: input.start_line.map(|line| line.saturating_sub(1)),
}]),
..Default::default()
})
});
if let Ok(LanguageModelToolResultContent::Text(text)) = &result {
let markdown = MarkdownCodeBlock {
tag: &input.path,
text,
}
.to_string();
event_stream.update_fields(ToolCallUpdateFields {
content: Some(vec![acp::ToolCallContent::Content {
content: markdown.into(),
}]),
..Default::default()
})
}
}
})?;

View File

@@ -60,11 +60,7 @@ impl AgentTool for TerminalTool {
acp::ToolKind::Execute
}
fn initial_title(
&self,
input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
if let Ok(input) = input {
let mut lines = input.command.lines();
let first_line = lines.next().unwrap_or_default();
@@ -97,7 +93,7 @@ impl AgentTool for TerminalTool {
Err(err) => return Task::ready(Err(err)),
};
let authorize = event_stream.authorize(self.initial_title(Ok(input.clone()), cx), cx);
let authorize = event_stream.authorize(self.initial_title(Ok(input.clone())), cx);
cx.spawn(async move |cx| {
authorize.await?;

View File

@@ -29,11 +29,7 @@ impl AgentTool for ThinkingTool {
acp::ToolKind::Think
}
fn initial_title(
&self,
_input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
"Thinking".into()
}

View File

@@ -48,11 +48,7 @@ impl AgentTool for WebSearchTool {
acp::ToolKind::Fetch
}
fn initial_title(
&self,
_input: Result<Self::Input, serde_json::Value>,
_cx: &mut App,
) -> SharedString {
fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
"Searching the Web".into()
}

View File

@@ -35,18 +35,22 @@ language.workspace = true
language_model.workspace = true
language_models.workspace = true
log.workspace = true
node_runtime.workspace = true
paths.workspace = true
project.workspace = true
reqwest_client = { workspace = true, optional = true }
schemars.workspace = true
semver.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
smol.workspace = true
task.workspace = true
tempfile.workspace = true
thiserror.workspace = true
ui.workspace = true
util.workspace = true
watch.workspace = true
which.workspace = true
workspace-hack.workspace = true
[target.'cfg(unix)'.dependencies]

View File

@@ -1,3 +1,4 @@
use crate::AgentServerCommand;
use acp_thread::AgentConnection;
use acp_tools::AcpConnectionRegistry;
use action_log::ActionLog;
@@ -7,10 +8,8 @@ use collections::HashMap;
use futures::AsyncBufReadExt as _;
use futures::io::BufReader;
use project::Project;
use project::agent_server_store::AgentServerCommand;
use serde::Deserialize;
use std::path::PathBuf;
use std::{any::Any, cell::RefCell};
use std::{path::Path, rc::Rc};
use thiserror::Error;
@@ -30,7 +29,6 @@ pub struct AcpConnection {
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
auth_methods: Vec<acp::AuthMethod>,
agent_capabilities: acp::AgentCapabilities,
root_dir: PathBuf,
_io_task: Task<Result<()>>,
_wait_task: Task<Result<()>>,
_stderr_task: Task<Result<()>>,
@@ -45,10 +43,9 @@ pub async fn connect(
server_name: SharedString,
command: AgentServerCommand,
root_dir: &Path,
is_remote: bool,
cx: &mut AsyncApp,
) -> Result<Rc<dyn AgentConnection>> {
let conn = AcpConnection::stdio(server_name, command.clone(), root_dir, is_remote, cx).await?;
let conn = AcpConnection::stdio(server_name, command.clone(), root_dir, cx).await?;
Ok(Rc::new(conn) as _)
}
@@ -59,21 +56,17 @@ impl AcpConnection {
server_name: SharedString,
command: AgentServerCommand,
root_dir: &Path,
is_remote: bool,
cx: &mut AsyncApp,
) -> Result<Self> {
let mut child = util::command::new_smol_command(command.path);
child
let mut child = util::command::new_smol_command(command.path)
.args(command.args.iter().map(|arg| arg.as_str()))
.envs(command.env.iter().flatten())
.current_dir(root_dir)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.kill_on_drop(true);
if !is_remote {
child.current_dir(root_dir);
}
let mut child = child.spawn()?;
.kill_on_drop(true)
.spawn()?;
let stdout = child.stdout.take().context("Failed to take stdout")?;
let stdin = child.stdin.take().context("Failed to take stdin")?;
@@ -152,7 +145,6 @@ impl AcpConnection {
Ok(Self {
auth_methods: response.auth_methods,
root_dir: root_dir.to_owned(),
connection,
server_name,
sessions,
@@ -166,10 +158,6 @@ impl AcpConnection {
pub fn prompt_capabilities(&self) -> &acp::PromptCapabilities {
&self.agent_capabilities.prompt_capabilities
}
pub fn root_dir(&self) -> &Path {
&self.root_dir
}
}
impl AgentConnection for AcpConnection {
@@ -183,36 +171,29 @@ impl AgentConnection for AcpConnection {
let sessions = self.sessions.clone();
let cwd = cwd.to_path_buf();
let context_server_store = project.read(cx).context_server_store().read(cx);
let mcp_servers = if project.read(cx).is_local() {
context_server_store
.configured_server_ids()
.iter()
.filter_map(|id| {
let configuration = context_server_store.configuration_for_server(id)?;
let command = configuration.command();
Some(acp::McpServer {
name: id.0.to_string(),
command: command.path.clone(),
args: command.args.clone(),
env: if let Some(env) = command.env.as_ref() {
env.iter()
.map(|(name, value)| acp::EnvVariable {
name: name.clone(),
value: value.clone(),
})
.collect()
} else {
vec![]
},
})
let mcp_servers = context_server_store
.configured_server_ids()
.iter()
.filter_map(|id| {
let configuration = context_server_store.configuration_for_server(id)?;
let command = configuration.command();
Some(acp::McpServer {
name: id.0.to_string(),
command: command.path.clone(),
args: command.args.clone(),
env: if let Some(env) = command.env.as_ref() {
env.iter()
.map(|(name, value)| acp::EnvVariable {
name: name.clone(),
value: value.clone(),
})
.collect()
} else {
vec![]
},
})
.collect()
} else {
// In SSH projects, the external agent is running on the remote
// machine, and currently we only run MCP servers on the local
// machine. So don't pass any MCP servers to the agent in that case.
Vec::new()
};
})
.collect();
cx.spawn(async move |cx| {
let response = conn

View File

@@ -2,25 +2,47 @@ mod acp;
mod claude;
mod custom;
mod gemini;
mod settings;
#[cfg(any(test, feature = "test-support"))]
pub mod e2e_tests;
use anyhow::Context as _;
pub use claude::*;
pub use custom::*;
use fs::Fs;
use fs::RemoveOptions;
use fs::RenameOptions;
use futures::StreamExt as _;
pub use gemini::*;
use project::agent_server_store::AgentServerStore;
use gpui::AppContext;
use node_runtime::NodeRuntime;
pub use settings::*;
use acp_thread::AgentConnection;
use acp_thread::LoadError;
use anyhow::Result;
use gpui::{App, Entity, SharedString, Task};
use anyhow::anyhow;
use collections::HashMap;
use gpui::{App, AsyncApp, Entity, SharedString, Task};
use project::Project;
use std::{any::Any, path::Path, rc::Rc};
use schemars::JsonSchema;
use semver::Version;
use serde::{Deserialize, Serialize};
use std::str::FromStr as _;
use std::{
any::Any,
path::{Path, PathBuf},
rc::Rc,
sync::Arc,
};
use util::ResultExt as _;
pub use acp::AcpConnection;
pub fn init(cx: &mut App) {
settings::init(cx);
}
pub struct AgentServerDelegate {
store: Entity<AgentServerStore>,
project: Entity<Project>,
status_tx: Option<watch::Sender<SharedString>>,
new_version_available: Option<watch::Sender<Option<String>>>,
@@ -28,13 +50,11 @@ pub struct AgentServerDelegate {
impl AgentServerDelegate {
pub fn new(
store: Entity<AgentServerStore>,
project: Entity<Project>,
status_tx: Option<watch::Sender<SharedString>>,
new_version_tx: Option<watch::Sender<Option<String>>>,
) -> Self {
Self {
store,
project,
status_tx,
new_version_available: new_version_tx,
@@ -44,6 +64,188 @@ impl AgentServerDelegate {
pub fn project(&self) -> &Entity<Project> {
&self.project
}
fn get_or_npm_install_builtin_agent(
self,
binary_name: SharedString,
package_name: SharedString,
entrypoint_path: PathBuf,
search_path: bool,
minimum_version: Option<Version>,
cx: &mut App,
) -> Task<Result<AgentServerCommand>> {
let project = self.project;
let fs = project.read(cx).fs().clone();
let Some(node_runtime) = project.read(cx).node_runtime().cloned() else {
return Task::ready(Err(anyhow!(
"External agents are not yet available in remote projects."
)));
};
let status_tx = self.status_tx;
let new_version_available = self.new_version_available;
cx.spawn(async move |cx| {
if search_path {
if let Some(bin) = find_bin_in_path(binary_name.clone(), &project, cx).await {
return Ok(AgentServerCommand {
path: bin,
args: Vec::new(),
env: Default::default(),
});
}
}
cx.spawn(async move |cx| {
let node_path = node_runtime.binary_path().await?;
let dir = paths::data_dir()
.join("external_agents")
.join(binary_name.as_str());
fs.create_dir(&dir).await?;
let mut stream = fs.read_dir(&dir).await?;
let mut versions = Vec::new();
let mut to_delete = Vec::new();
while let Some(entry) = stream.next().await {
let Ok(entry) = entry else { continue };
let Some(file_name) = entry.file_name() else {
continue;
};
if let Some(name) = file_name.to_str()
&& let Some(version) = semver::Version::from_str(name).ok()
&& fs
.is_file(&dir.join(file_name).join(&entrypoint_path))
.await
{
versions.push((version, file_name.to_owned()));
} else {
to_delete.push(file_name.to_owned())
}
}
versions.sort();
let newest_version = if let Some((version, file_name)) = versions.last().cloned()
&& minimum_version.is_none_or(|minimum_version| version >= minimum_version)
{
versions.pop();
Some(file_name)
} else {
None
};
log::debug!("existing version of {package_name}: {newest_version:?}");
to_delete.extend(versions.into_iter().map(|(_, file_name)| file_name));
cx.background_spawn({
let fs = fs.clone();
let dir = dir.clone();
async move {
for file_name in to_delete {
fs.remove_dir(
&dir.join(file_name),
RemoveOptions {
recursive: true,
ignore_if_not_exists: false,
},
)
.await
.ok();
}
}
})
.detach();
let version = if let Some(file_name) = newest_version {
cx.background_spawn({
let file_name = file_name.clone();
let dir = dir.clone();
let fs = fs.clone();
async move {
let latest_version =
node_runtime.npm_package_latest_version(&package_name).await;
if let Ok(latest_version) = latest_version
&& &latest_version != &file_name.to_string_lossy()
{
Self::download_latest_version(
fs,
dir.clone(),
node_runtime,
package_name,
)
.await
.log_err();
if let Some(mut new_version_available) = new_version_available {
new_version_available.send(Some(latest_version)).ok();
}
}
}
})
.detach();
file_name
} else {
if let Some(mut status_tx) = status_tx {
status_tx.send("Installing…".into()).ok();
}
let dir = dir.clone();
cx.background_spawn(Self::download_latest_version(
fs.clone(),
dir.clone(),
node_runtime,
package_name,
))
.await?
.into()
};
let agent_server_path = dir.join(version).join(entrypoint_path);
let agent_server_path_exists = fs.is_file(&agent_server_path).await;
anyhow::ensure!(
agent_server_path_exists,
"Missing entrypoint path {} after installation",
agent_server_path.to_string_lossy()
);
anyhow::Ok(AgentServerCommand {
path: node_path,
args: vec![agent_server_path.to_string_lossy().to_string()],
env: Default::default(),
})
})
.await
.map_err(|e| LoadError::FailedToInstall(e.to_string().into()).into())
})
}
async fn download_latest_version(
fs: Arc<dyn Fs>,
dir: PathBuf,
node_runtime: NodeRuntime,
package_name: SharedString,
) -> Result<String> {
log::debug!("downloading latest version of {package_name}");
let tmp_dir = tempfile::tempdir_in(&dir)?;
node_runtime
.npm_install_packages(tmp_dir.path(), &[(&package_name, "latest")])
.await?;
let version = node_runtime
.npm_package_installed_version(tmp_dir.path(), &package_name)
.await?
.context("expected package to be installed")?;
fs.rename(
&tmp_dir.keep(),
&dir.join(&version),
RenameOptions {
ignore_if_exists: true,
overwrite: false,
},
)
.await?;
anyhow::Ok(version)
}
}
pub trait AgentServer: Send {
@@ -53,10 +255,10 @@ pub trait AgentServer: Send {
fn connect(
&self,
root_dir: Option<&Path>,
root_dir: &Path,
delegate: AgentServerDelegate,
cx: &mut App,
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>>;
) -> Task<Result<Rc<dyn AgentConnection>>>;
fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
}
@@ -66,3 +268,120 @@ impl dyn AgentServer {
self.into_any().downcast().ok()
}
}
impl std::fmt::Debug for AgentServerCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let filtered_env = self.env.as_ref().map(|env| {
env.iter()
.map(|(k, v)| {
(
k,
if util::redact::should_redact(k) {
"[REDACTED]"
} else {
v
},
)
})
.collect::<Vec<_>>()
});
f.debug_struct("AgentServerCommand")
.field("path", &self.path)
.field("args", &self.args)
.field("env", &filtered_env)
.finish()
}
}
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
pub struct AgentServerCommand {
#[serde(rename = "command")]
pub path: PathBuf,
#[serde(default)]
pub args: Vec<String>,
pub env: Option<HashMap<String, String>>,
}
impl AgentServerCommand {
pub async fn resolve(
path_bin_name: &'static str,
extra_args: &[&'static str],
fallback_path: Option<&Path>,
settings: Option<BuiltinAgentServerSettings>,
project: &Entity<Project>,
cx: &mut AsyncApp,
) -> Option<Self> {
if let Some(settings) = settings
&& let Some(command) = settings.custom_command()
{
Some(command)
} else {
match find_bin_in_path(path_bin_name.into(), project, cx).await {
Some(path) => Some(Self {
path,
args: extra_args.iter().map(|arg| arg.to_string()).collect(),
env: None,
}),
None => fallback_path.and_then(|path| {
if path.exists() {
Some(Self {
path: path.to_path_buf(),
args: extra_args.iter().map(|arg| arg.to_string()).collect(),
env: None,
})
} else {
None
}
}),
}
}
}
}
async fn find_bin_in_path(
bin_name: SharedString,
project: &Entity<Project>,
cx: &mut AsyncApp,
) -> Option<PathBuf> {
let (env_task, root_dir) = project
.update(cx, |project, cx| {
let worktree = project.visible_worktrees(cx).next();
match worktree {
Some(worktree) => {
let env_task = project.environment().update(cx, |env, cx| {
env.get_worktree_environment(worktree.clone(), cx)
});
let path = worktree.read(cx).abs_path();
(env_task, path)
}
None => {
let path: Arc<Path> = paths::home_dir().as_path().into();
let env_task = project.environment().update(cx, |env, cx| {
env.get_directory_environment(path.clone(), cx)
});
(env_task, path)
}
}
})
.log_err()?;
cx.background_executor()
.spawn(async move {
let which_result = if cfg!(windows) {
which::which(bin_name.as_str())
} else {
let env = env_task.await.unwrap_or_default();
let shell_path = env.get("PATH").cloned();
which::which_in(bin_name.as_str(), shell_path.as_ref(), root_dir.as_ref())
};
if let Err(which::Error::CannotFindBinaryPath) = which_result {
return None;
}
which_result.log_err()
})
.await
}

View File

@@ -1,22 +1,63 @@
use settings::SettingsStore;
use std::path::Path;
use std::rc::Rc;
use std::{any::Any, path::PathBuf};
use anyhow::{Context as _, Result};
use gpui::{App, SharedString, Task};
use project::agent_server_store::CLAUDE_CODE_NAME;
use anyhow::{Result, bail};
use gpui::{App, AppContext as _, SharedString, Task};
use crate::{AgentServer, AgentServerDelegate};
use crate::{AgentServer, AgentServerDelegate, AllAgentServersSettings};
use acp_thread::AgentConnection;
#[derive(Clone)]
pub struct ClaudeCode;
pub struct AgentServerLoginCommand {
pub struct ClaudeCodeLoginCommand {
pub path: PathBuf,
pub arguments: Vec<String>,
}
impl ClaudeCode {
const BINARY_NAME: &'static str = "claude-code-acp";
const PACKAGE_NAME: &'static str = "@zed-industries/claude-code-acp";
pub fn login_command(
delegate: AgentServerDelegate,
cx: &mut App,
) -> Task<Result<ClaudeCodeLoginCommand>> {
let custom_command = cx.read_global(|settings: &SettingsStore, _| {
settings
.get::<AllAgentServersSettings>(None)
.get("claude")
.cloned()
});
cx.spawn(async move |cx| {
let mut command = if custom_command.is_some() {
bail!("Cannot construct login command because a custom command was specified for claude-code-acp in settings")
} else {
cx.update(|cx| {
delegate.get_or_npm_install_builtin_agent(
Self::BINARY_NAME.into(),
Self::PACKAGE_NAME.into(),
"node_modules/@anthropic-ai/claude-code/cli.js".into(),
false,
Some("0.2.5".parse().unwrap()),
cx,
)
})?
.await?
};
command.args.push("/login".into());
Ok(ClaudeCodeLoginCommand {
path: command.path,
arguments: command.args,
})
})
}
}
impl AgentServer for ClaudeCode {
fn telemetry_id(&self) -> &'static str {
"claude-code"
@@ -32,33 +73,50 @@ impl AgentServer for ClaudeCode {
fn connect(
&self,
root_dir: Option<&Path>,
root_dir: &Path,
delegate: AgentServerDelegate,
cx: &mut App,
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
let name = self.name();
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().to_string());
let is_remote = delegate.project.read(cx).is_via_remote_server();
let store = delegate.store.downgrade();
) -> Task<Result<Rc<dyn AgentConnection>>> {
let root_dir = root_dir.to_path_buf();
let fs = delegate.project().read(cx).fs().clone();
let server_name = self.name();
let custom_command = cx.read_global(|settings: &SettingsStore, _| {
settings
.get::<AllAgentServersSettings>(None)
.get("claude")
.cloned()
});
cx.spawn(async move |cx| {
let (command, root_dir, login) = store
.update(cx, |store, cx| {
let agent = store
.get_external_agent(&CLAUDE_CODE_NAME.into())
.context("Claude Code is not registered")?;
anyhow::Ok(agent.get_command(
root_dir.as_deref(),
Default::default(),
delegate.status_tx,
delegate.new_version_available,
&mut cx.to_async(),
))
})??
.await?;
let connection =
crate::acp::connect(name, command, root_dir.as_ref(), is_remote, cx).await?;
Ok((connection, login))
let mut command = if let Some(custom_command) = custom_command {
custom_command
} else {
cx.update(|cx| {
delegate.get_or_npm_install_builtin_agent(
Self::BINARY_NAME.into(),
Self::PACKAGE_NAME.into(),
format!("node_modules/{}/dist/index.js", Self::PACKAGE_NAME).into(),
false,
None,
cx,
)
})?
.await?
};
command
.env
.get_or_insert_default()
.insert("ANTHROPIC_API_KEY".to_owned(), "".to_owned());
let root_dir_exists = fs.is_dir(&root_dir).await;
anyhow::ensure!(
root_dir_exists,
"Session root {} does not exist or is not a directory",
root_dir.to_string_lossy()
);
crate::acp::connect(server_name, command.clone(), &root_dir, cx).await
})
}

View File

@@ -1,19 +1,19 @@
use crate::AgentServerDelegate;
use crate::{AgentServerCommand, AgentServerDelegate};
use acp_thread::AgentConnection;
use anyhow::{Context as _, Result};
use anyhow::Result;
use gpui::{App, SharedString, Task};
use project::agent_server_store::ExternalAgentServerName;
use std::{path::Path, rc::Rc};
use ui::IconName;
/// A generic agent server implementation for custom user-defined agents
pub struct CustomAgentServer {
name: SharedString,
command: AgentServerCommand,
}
impl CustomAgentServer {
pub fn new(name: SharedString) -> Self {
Self { name }
pub fn new(name: SharedString, command: AgentServerCommand) -> Self {
Self { name, command }
}
}
@@ -32,36 +32,14 @@ impl crate::AgentServer for CustomAgentServer {
fn connect(
&self,
root_dir: Option<&Path>,
delegate: AgentServerDelegate,
root_dir: &Path,
_delegate: AgentServerDelegate,
cx: &mut App,
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
let name = self.name();
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().to_string());
let is_remote = delegate.project.read(cx).is_via_remote_server();
let store = delegate.store.downgrade();
cx.spawn(async move |cx| {
let (command, root_dir, login) = store
.update(cx, |store, cx| {
let agent = store
.get_external_agent(&ExternalAgentServerName(name.clone()))
.with_context(|| {
format!("Custom agent server `{}` is not registered", name)
})?;
anyhow::Ok(agent.get_command(
root_dir.as_deref(),
Default::default(),
delegate.status_tx,
delegate.new_version_available,
&mut cx.to_async(),
))
})??
.await?;
let connection =
crate::acp::connect(name, command, root_dir.as_ref(), is_remote, cx).await?;
Ok((connection, login))
})
) -> Task<Result<Rc<dyn AgentConnection>>> {
let server_name = self.name();
let command = self.command.clone();
let root_dir = root_dir.to_path_buf();
cx.spawn(async move |cx| crate::acp::connect(server_name, command, &root_dir, cx).await)
}
fn into_any(self: Rc<Self>) -> Rc<dyn std::any::Any> {

View File

@@ -1,12 +1,12 @@
#[cfg(test)]
use crate::AgentServerCommand;
use crate::{AgentServer, AgentServerDelegate};
use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus};
use agent_client_protocol as acp;
use futures::{FutureExt, StreamExt, channel::mpsc, select};
use gpui::{AppContext, Entity, TestAppContext};
use indoc::indoc;
#[cfg(test)]
use project::agent_server_store::{AgentServerCommand, CustomAgentServerSettings};
use project::{FakeFs, Project, agent_server_store::AllAgentServersSettings};
use project::{FakeFs, Project};
use std::{
path::{Path, PathBuf},
sync::Arc,
@@ -449,6 +449,7 @@ pub use common_e2e_tests;
// Helpers
pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
#[cfg(test)]
use settings::Settings;
env_logger::try_init().ok();
@@ -467,20 +468,25 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
language_model::init(client.clone(), cx);
language_models::init(user_store, client, cx);
agent_settings::init(cx);
AllAgentServersSettings::register(cx);
crate::settings::init(cx);
#[cfg(test)]
AllAgentServersSettings::override_global(
AllAgentServersSettings {
claude: Some(CustomAgentServerSettings {
command: AgentServerCommand {
path: "claude-code-acp".into(),
args: vec![],
env: None,
},
}),
gemini: Some(crate::gemini::tests::local_command().into()),
custom: collections::HashMap::default(),
crate::AllAgentServersSettings::override_global(
crate::AllAgentServersSettings {
commands: [
(
"claude".into(),
AgentServerCommand {
path: "claude-code-acp".into(),
args: vec![],
env: None,
},
),
("gemini".into(), crate::gemini::tests::local_command()),
]
.into_iter()
.collect(),
gemini_is_system: false,
},
cx,
);
@@ -497,11 +503,10 @@ pub async fn new_test_thread(
current_dir: impl AsRef<Path>,
cx: &mut TestAppContext,
) -> Entity<AcpThread> {
let store = project.read_with(cx, |project, _| project.agent_server_store().clone());
let delegate = AgentServerDelegate::new(store, project.clone(), None, None);
let delegate = AgentServerDelegate::new(project.clone(), None, None);
let (connection, _) = cx
.update(|cx| server.connect(Some(current_dir.as_ref()), delegate, cx))
let connection = cx
.update(|cx| server.connect(current_dir.as_ref(), delegate, cx))
.await
.unwrap();

View File

@@ -1,17 +1,19 @@
use std::rc::Rc;
use std::{any::Any, path::Path};
use crate::{AgentServer, AgentServerDelegate};
use acp_thread::AgentConnection;
use anyhow::{Context as _, Result};
use collections::HashMap;
use gpui::{App, SharedString, Task};
use crate::acp::AcpConnection;
use crate::{AgentServer, AgentServerDelegate, AllAgentServersSettings};
use acp_thread::{AgentConnection, LoadError};
use anyhow::Result;
use gpui::{App, AppContext as _, SharedString, Task};
use language_models::provider::google::GoogleLanguageModelProvider;
use project::agent_server_store::GEMINI_NAME;
use settings::SettingsStore;
#[derive(Clone)]
pub struct Gemini;
const ACP_ARG: &str = "--experimental-acp";
impl AgentServer for Gemini {
fn telemetry_id(&self) -> &'static str {
"gemini-cli"
@@ -27,37 +29,119 @@ impl AgentServer for Gemini {
fn connect(
&self,
root_dir: Option<&Path>,
root_dir: &Path,
delegate: AgentServerDelegate,
cx: &mut App,
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
let name = self.name();
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().to_string());
let is_remote = delegate.project.read(cx).is_via_remote_server();
let store = delegate.store.downgrade();
) -> Task<Result<Rc<dyn AgentConnection>>> {
let root_dir = root_dir.to_path_buf();
let fs = delegate.project().read(cx).fs().clone();
let server_name = self.name();
let (custom_command, is_system) = cx.read_global(|settings: &SettingsStore, _| {
let s = settings.get::<AllAgentServersSettings>(None);
(
s.get("gemini").cloned(),
AllAgentServersSettings::is_system(s, "gemini"),
)
});
cx.spawn(async move |cx| {
let mut extra_env = HashMap::default();
if let Some(api_key) = cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() {
extra_env.insert("GEMINI_API_KEY".into(), api_key.key);
let mut command = if let Some(custom_command) = custom_command
{
custom_command
} else {
cx.update(|cx| {
delegate.get_or_npm_install_builtin_agent(
Self::BINARY_NAME.into(),
Self::PACKAGE_NAME.into(),
format!("node_modules/{}/dist/index.js", Self::PACKAGE_NAME).into(),
is_system,
Some(Self::MINIMUM_VERSION.parse().unwrap()),
cx,
)
})?
.await?
};
if !command.args.contains(&ACP_ARG.into()) {
command.args.push(ACP_ARG.into());
}
let (command, root_dir, login) = store
.update(cx, |store, cx| {
let agent = store
.get_external_agent(&GEMINI_NAME.into())
.context("Gemini CLI is not registered")?;
anyhow::Ok(agent.get_command(
root_dir.as_deref(),
extra_env,
delegate.status_tx,
delegate.new_version_available,
&mut cx.to_async(),
))
})??
.await?;
let connection =
crate::acp::connect(name, command, root_dir.as_ref(), is_remote, cx).await?;
Ok((connection, login))
if let Some(api_key) = cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() {
command
.env
.get_or_insert_default()
.insert("GEMINI_API_KEY".to_owned(), api_key.key);
}
let root_dir_exists = fs.is_dir(&root_dir).await;
anyhow::ensure!(
root_dir_exists,
"Session root {} does not exist or is not a directory",
root_dir.to_string_lossy()
);
let result = crate::acp::connect(server_name, command.clone(), &root_dir, cx).await;
match &result {
Ok(connection) => {
if let Some(connection) = connection.clone().downcast::<AcpConnection>()
&& !connection.prompt_capabilities().image
{
let version_output = util::command::new_smol_command(&command.path)
.args(command.args.iter())
.arg("--version")
.kill_on_drop(true)
.output()
.await;
let current_version =
String::from_utf8(version_output?.stdout)?.trim().to_owned();
log::error!("connected to gemini, but missing prompt_capabilities.image (version is {current_version})");
return Err(LoadError::Unsupported {
current_version: current_version.into(),
command: (command.path.to_string_lossy().to_string() + " " + &command.args.join(" ")).into(),
minimum_version: Self::MINIMUM_VERSION.into(),
}
.into());
}
}
Err(e) => {
let version_fut = util::command::new_smol_command(&command.path)
.args(command.args.iter())
.arg("--version")
.kill_on_drop(true)
.output();
let help_fut = util::command::new_smol_command(&command.path)
.args(command.args.iter())
.arg("--help")
.kill_on_drop(true)
.output();
let (version_output, help_output) =
futures::future::join(version_fut, help_fut).await;
let Some(version_output) = version_output.ok().and_then(|output| String::from_utf8(output.stdout).ok()) else {
return result;
};
let Some((help_stdout, help_stderr)) = help_output.ok().and_then(|output| String::from_utf8(output.stdout).ok().zip(String::from_utf8(output.stderr).ok())) else {
return result;
};
let current_version = version_output.trim().to_string();
let supported = help_stdout.contains(ACP_ARG) || current_version.parse::<semver::Version>().is_ok_and(|version| version >= Self::MINIMUM_VERSION.parse::<semver::Version>().unwrap());
log::error!("failed to create ACP connection to gemini (version is {current_version}, supported: {supported}): {e}");
log::debug!("gemini --help stdout: {help_stdout:?}");
log::debug!("gemini --help stderr: {help_stderr:?}");
if !supported {
return Err(LoadError::Unsupported {
current_version: current_version.into(),
command: (command.path.to_string_lossy().to_string() + " " + &command.args.join(" ")).into(),
minimum_version: Self::MINIMUM_VERSION.into(),
}
.into());
}
}
}
result
})
}
@@ -66,11 +150,18 @@ impl AgentServer for Gemini {
}
}
impl Gemini {
const PACKAGE_NAME: &str = "@google/gemini-cli";
const MINIMUM_VERSION: &str = "0.2.1";
const BINARY_NAME: &str = "gemini";
}
#[cfg(test)]
pub(crate) mod tests {
use project::agent_server_store::AgentServerCommand;
use super::*;
use crate::AgentServerCommand;
use std::path::Path;
crate::common_e2e_tests!(async |_, _, _| Gemini, allow_option_id = "proceed_once");

View File

@@ -0,0 +1,178 @@
use std::path::PathBuf;
use crate::AgentServerCommand;
use anyhow::Result;
use collections::HashMap;
use gpui::{App, SharedString};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
pub fn init(cx: &mut App) {
AllAgentServersSettings::register(cx);
}
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, SettingsUi, SettingsKey)]
#[settings_key(key = "agent_servers")]
pub struct AllAgentServersSettingsContent {
gemini: Option<GeminiSettingsContent>,
claude: Option<AgentServerCommand>,
/// Custom agent servers configured by the user
#[serde(flatten)]
pub custom: HashMap<SharedString, AgentServerCommand>,
}
#[derive(Clone, Debug, Default)]
pub struct AllAgentServersSettings {
pub commands: HashMap<SharedString, AgentServerCommand>,
pub gemini_is_system: bool,
}
impl AllAgentServersSettings {
pub fn is_system(this: &Self, name: &str) -> bool {
if name == "gemini" {
this.gemini_is_system
} else {
false
}
}
}
impl std::ops::Deref for AllAgentServersSettings {
type Target = HashMap<SharedString, AgentServerCommand>;
fn deref(&self) -> &Self::Target {
&self.commands
}
}
impl std::ops::DerefMut for AllAgentServersSettings {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.commands
}
}
#[derive(Serialize, Deserialize, Clone, Debug, JsonSchema, PartialEq)]
pub struct GeminiSettingsContent {
ignore_system_version: Option<bool>,
#[serde(flatten)]
inner: Option<AgentServerCommand>,
}
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
pub struct BuiltinAgentServerSettings {
/// Absolute path to a binary to be used when launching this agent.
///
/// This can be used to run a specific binary without automatic downloads or searching `$PATH`.
#[serde(rename = "command")]
pub path: Option<PathBuf>,
/// If a binary is specified in `command`, it will be passed these arguments.
pub args: Option<Vec<String>>,
/// If a binary is specified in `command`, it will be passed these environment variables.
pub env: Option<HashMap<String, String>>,
/// Whether to skip searching `$PATH` for an agent server binary when
/// launching this agent.
///
/// This has no effect if a `command` is specified. Otherwise, when this is
/// `false`, Zed will search `$PATH` for an agent server binary and, if one
/// is found, use it for threads with this agent. If no agent binary is
/// found on `$PATH`, Zed will automatically install and use its own binary.
/// When this is `true`, Zed will not search `$PATH`, and will always use
/// its own binary.
///
/// Default: true
pub ignore_system_version: Option<bool>,
}
impl BuiltinAgentServerSettings {
pub(crate) fn custom_command(self) -> Option<AgentServerCommand> {
self.path.map(|path| AgentServerCommand {
path,
args: self.args.unwrap_or_default(),
env: self.env,
})
}
}
impl From<AgentServerCommand> for BuiltinAgentServerSettings {
fn from(value: AgentServerCommand) -> Self {
BuiltinAgentServerSettings {
path: Some(value.path),
args: Some(value.args),
env: value.env,
..Default::default()
}
}
}
#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
pub struct AgentServerSettings {
#[serde(flatten)]
pub command: AgentServerCommand,
}
impl settings::Settings for AllAgentServersSettings {
type FileContent = AllAgentServersSettingsContent;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
let mut settings = AllAgentServersSettings::default();
for AllAgentServersSettingsContent {
gemini,
claude,
custom,
} in sources.defaults_and_customizations()
{
if let Some(gemini) = gemini {
if let Some(ignore) = gemini.ignore_system_version {
settings.gemini_is_system = !ignore;
}
if let Some(gemini) = gemini.inner.as_ref() {
settings.insert("gemini".into(), gemini.clone());
}
}
if let Some(claude) = claude.clone() {
settings.insert("claude".into(), claude);
}
// Merge custom agents
for (name, command) in custom {
// Skip built-in agent names to avoid conflicts
if name != "gemini" && name != "claude" {
settings.commands.insert(name.clone(), command.clone());
}
}
}
Ok(settings)
}
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use crate::{AgentServerCommand, GeminiSettingsContent};
#[test]
fn test_deserialization() {
let value = json!({
"command": "foo",
"args": ["bar"],
"ignore_system_version": false
});
let settings = serde_json::from_value::<GeminiSettingsContent>(value).unwrap();
assert_eq!(
settings,
GeminiSettingsContent {
ignore_system_version: Some(false),
inner: Some(AgentServerCommand {
path: "foo".into(),
args: vec!["bar".into()],
env: Default::default(),
})
}
)
}
}

View File

@@ -1025,31 +1025,43 @@ impl SlashCommandCompletion {
return None;
}
let (prefix, last_command) = line.rsplit_once('/')?;
if prefix.chars().last().is_some_and(|c| !c.is_whitespace())
|| last_command.starts_with(char::is_whitespace)
let last_command_start = line.rfind('/')?;
if last_command_start >= line.len() {
return Some(Self::default());
}
if last_command_start > 0
&& line
.chars()
.nth(last_command_start - 1)
.is_some_and(|c| !c.is_whitespace())
{
return None;
}
let mut argument = None;
let rest_of_line = &line[last_command_start + 1..];
let mut command = None;
if let Some((command_text, args)) = last_command.split_once(char::is_whitespace) {
if !args.is_empty() {
argument = Some(args.trim_end().to_string());
}
let mut argument = None;
let mut end = last_command_start + 1;
if let Some(command_text) = rest_of_line.split_whitespace().next() {
command = Some(command_text.to_string());
} else if !last_command.is_empty() {
command = Some(last_command.to_string());
};
end += command_text.len();
// Find the start of arguments after the command
if let Some(args_start) =
rest_of_line[command_text.len()..].find(|c: char| !c.is_whitespace())
{
let args = &rest_of_line[command_text.len() + args_start..].trim_end();
if !args.is_empty() {
argument = Some(args.to_string());
end += args.len() + 1;
}
}
}
Some(Self {
source_range: prefix.len() + offset_to_line
..line
.rfind(|c: char| !c.is_whitespace())
.unwrap_or_else(|| line.len())
+ 1
+ offset_to_line,
source_range: last_command_start + offset_to_line..end + offset_to_line,
command,
argument,
})
@@ -1168,15 +1180,6 @@ mod tests {
})
);
assert_eq!(
SlashCommandCompletion::try_parse("/拿不到命令 拿不到命令 ", 0),
Some(SlashCommandCompletion {
source_range: 0..30,
command: Some("拿不到命令".to_string()),
argument: Some("拿不到命令".to_string()),
})
);
assert_eq!(SlashCommandCompletion::try_parse("Lorem Ipsum", 0), None);
assert_eq!(SlashCommandCompletion::try_parse("Lorem /", 0), None);
@@ -1184,8 +1187,6 @@ mod tests {
assert_eq!(SlashCommandCompletion::try_parse("Lorem /help", 0), None);
assert_eq!(SlashCommandCompletion::try_parse("Lorem/", 0), None);
assert_eq!(SlashCommandCompletion::try_parse("/ ", 0), None);
}
#[test]

View File

@@ -493,13 +493,14 @@ impl MessageEditor {
let Some(entry) = self.project.read(cx).entry_for_path(&project_path, cx) else {
return Task::ready(Err(anyhow!("project entry not found")));
};
let directory_path = entry.path.clone();
let worktree_id = project_path.worktree_id;
let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx) else {
let Some(worktree) = self.project.read(cx).worktree_for_entry(entry.id, cx) else {
return Task::ready(Err(anyhow!("worktree not found")));
};
let project = self.project.clone();
cx.spawn(async move |_, cx| {
let directory_path = entry.path.clone();
let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?;
let file_paths = worktree.read_with(cx, |worktree, _cx| {
collect_files_in_path(worktree, &directory_path)
})?;
@@ -699,15 +700,10 @@ impl MessageEditor {
self.project.read(cx).fs().clone(),
self.history_store.clone(),
));
let delegate = AgentServerDelegate::new(
self.project.read(cx).agent_server_store().clone(),
self.project.clone(),
None,
None,
);
let connection = server.connect(None, delegate, cx);
let delegate = AgentServerDelegate::new(self.project.clone(), None, None);
let connection = server.connect(Path::new(""), delegate, cx);
cx.spawn(async move |_, cx| {
let (agent, _) = connection.await?;
let agent = connection.await?;
let agent = agent.downcast::<agent2::NativeAgentConnection>().unwrap();
let summary = agent
.0

View File

@@ -192,10 +192,8 @@ impl PickerDelegate for AcpModelPickerDelegate {
}
}
fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
cx.defer_in(window, |picker, window, cx| {
picker.set_query("", window, cx);
});
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
cx.emit(DismissEvent);
}
fn render_match(

View File

@@ -6,7 +6,7 @@ use acp_thread::{
use acp_thread::{AgentConnection, Plan};
use action_log::ActionLog;
use agent_client_protocol::{self as acp, PromptCapabilities};
use agent_servers::{AgentServer, AgentServerDelegate};
use agent_servers::{AgentServer, AgentServerDelegate, ClaudeCode};
use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting};
use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore, NativeAgentServer};
use anyhow::{Context as _, Result, anyhow, bail};
@@ -40,6 +40,7 @@ use std::path::Path;
use std::sync::Arc;
use std::time::Instant;
use std::{collections::BTreeMap, rc::Rc, time::Duration};
use task::SpawnInTerminal;
use terminal_view::terminal_panel::TerminalPanel;
use text::Anchor;
use theme::{AgentFontSize, ThemeSettings};
@@ -262,7 +263,6 @@ pub struct AcpThreadView {
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
thread_state: ThreadState,
login: Option<task::SpawnInTerminal>,
history_store: Entity<HistoryStore>,
hovered_recent_history_item: Option<usize>,
entry_view_state: Entity<EntryViewState>,
@@ -392,7 +392,6 @@ impl AcpThreadView {
project: project.clone(),
entry_view_state,
thread_state: Self::initial_state(agent, resume_thread, workspace, project, window, cx),
login: None,
message_editor,
model_selector: None,
profile_selector: None,
@@ -445,11 +444,9 @@ impl AcpThreadView {
window: &mut Window,
cx: &mut Context<Self>,
) -> ThreadState {
if project.read(cx).is_via_collab()
&& agent.clone().downcast::<NativeAgentServer>().is_none()
{
if !project.read(cx).is_local() && agent.clone().downcast::<NativeAgentServer>().is_none() {
return ThreadState::LoadError(LoadError::Other(
"External agents are not yet supported in shared projects.".into(),
"External agents are not yet supported for remote projects.".into(),
));
}
let mut worktrees = project.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
@@ -469,23 +466,20 @@ impl AcpThreadView {
Some(worktree.read(cx).abs_path())
}
})
.next();
.next()
.unwrap_or_else(|| paths::home_dir().as_path().into());
let (status_tx, mut status_rx) = watch::channel("Loading…".into());
let (new_version_available_tx, mut new_version_available_rx) = watch::channel(None);
let delegate = AgentServerDelegate::new(
project.read(cx).agent_server_store().clone(),
project.clone(),
Some(status_tx),
Some(new_version_available_tx),
);
let connect_task = agent.connect(root_dir.as_deref(), delegate, cx);
let connect_task = agent.connect(&root_dir, delegate, cx);
let load_task = cx.spawn_in(window, async move |this, cx| {
let connection = match connect_task.await {
Ok((connection, login)) => {
this.update(cx, |this, _| this.login = login).ok();
connection
}
Ok(connection) => connection,
Err(err) => {
this.update_in(cx, |this, window, cx| {
if err.downcast_ref::<LoadError>().is_some() {
@@ -512,14 +506,6 @@ impl AcpThreadView {
})
.log_err()
} else {
let root_dir = if let Some(acp_agent) = connection
.clone()
.downcast::<agent_servers::AcpConnection>()
{
acp_agent.root_dir().into()
} else {
root_dir.unwrap_or(paths::home_dir().as_path().into())
};
cx.update(|_, cx| {
connection
.clone()
@@ -1476,12 +1462,9 @@ impl AcpThreadView {
self.thread_error.take();
configuration_view.take();
pending_auth_method.replace(method.clone());
let authenticate = if (method.0.as_ref() == "claude-login"
|| method.0.as_ref() == "spawn-gemini-cli")
&& let Some(login) = self.login.clone()
{
let authenticate = if method.0.as_ref() == "claude-login" {
if let Some(workspace) = self.workspace.upgrade() {
Self::spawn_external_agent_login(login, workspace, false, window, cx)
Self::spawn_claude_login(&workspace, window, cx)
} else {
Task::ready(Ok(()))
}
@@ -1528,28 +1511,31 @@ impl AcpThreadView {
}));
}
fn spawn_external_agent_login(
login: task::SpawnInTerminal,
workspace: Entity<Workspace>,
previous_attempt: bool,
fn spawn_claude_login(
workspace: &Entity<Workspace>,
window: &mut Window,
cx: &mut App,
) -> Task<Result<()>> {
let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
return Task::ready(Ok(()));
};
let project = workspace.read(cx).project().clone();
let cwd = project.read(cx).first_project_directory(cx);
let shell = project.read(cx).terminal_settings(&cwd, cx).shell.clone();
let project_entity = workspace.read(cx).project();
let project = project_entity.read(cx);
let cwd = project.first_project_directory(cx);
let shell = project.terminal_settings(&cwd, cx).shell.clone();
let delegate = AgentServerDelegate::new(project_entity.clone(), None, None);
let command = ClaudeCode::login_command(delegate, cx);
window.spawn(cx, async move |cx| {
let mut task = login.clone();
task.command = task
.command
.map(|command| anyhow::Ok(shlex::try_quote(&command)?.to_string()))
.transpose()?;
task.args = task
.args
let login_command = command.await?;
let command = login_command
.path
.to_str()
.with_context(|| format!("invalid login command: {:?}", login_command.path))?;
let command = shlex::try_quote(command)?;
let args = login_command
.arguments
.iter()
.map(|arg| {
Ok(shlex::try_quote(arg)
@@ -1557,16 +1543,26 @@ impl AcpThreadView {
.to_string())
})
.collect::<Result<Vec<_>>>()?;
task.full_label = task.label.clone();
task.id = task::TaskId(format!("external-agent-{}-login", task.label));
task.command_label = task.label.clone();
task.use_new_terminal = true;
task.allow_concurrent_runs = true;
task.hide = task::HideStrategy::Always;
task.shell = shell;
let terminal = terminal_panel.update_in(cx, |terminal_panel, window, cx| {
terminal_panel.spawn_task(&login, window, cx)
terminal_panel.spawn_task(
&SpawnInTerminal {
id: task::TaskId("claude-login".into()),
full_label: "claude /login".to_owned(),
label: "claude /login".to_owned(),
command: Some(command.into()),
args,
command_label: "claude /login".to_owned(),
cwd,
use_new_terminal: true,
allow_concurrent_runs: true,
hide: task::HideStrategy::Always,
shell,
..Default::default()
},
window,
cx,
)
})?;
let terminal = terminal.await?;
@@ -1582,9 +1578,7 @@ impl AcpThreadView {
cx.background_executor().timer(Duration::from_secs(1)).await;
let content =
terminal.update(cx, |terminal, _cx| terminal.get_content())?;
if content.contains("Login successful")
|| content.contains("Type your message")
{
if content.contains("Login successful") {
return anyhow::Ok(());
}
}
@@ -1600,9 +1594,6 @@ impl AcpThreadView {
}
}
_ = exit_status => {
if !previous_attempt && project.read_with(cx, |project, _| project.is_via_remote_server())? && login.label.contains("gemini") {
return cx.update(|window, cx| Self::spawn_external_agent_login(login, workspace, true, window, cx))?.await
}
return Err(anyhow!("exited before logging in"));
}
}
@@ -2033,34 +2024,35 @@ impl AcpThreadView {
window: &Window,
cx: &Context<Self>,
) -> Div {
let has_location = tool_call.locations.len() == 1;
let card_header_id = SharedString::from("inner-tool-call-header");
let tool_icon = if tool_call.kind == acp::ToolKind::Edit && has_location {
FileIcons::get_icon(&tool_call.locations[0].path, cx)
.map(Icon::from_path)
.unwrap_or(Icon::new(IconName::ToolPencil))
} else {
Icon::new(match tool_call.kind {
acp::ToolKind::Read => IconName::ToolSearch,
acp::ToolKind::Edit => IconName::ToolPencil,
acp::ToolKind::Delete => IconName::ToolDeleteFile,
acp::ToolKind::Move => IconName::ArrowRightLeft,
acp::ToolKind::Search => IconName::ToolSearch,
acp::ToolKind::Execute => IconName::ToolTerminal,
acp::ToolKind::Think => IconName::ToolThink,
acp::ToolKind::Fetch => IconName::ToolWeb,
acp::ToolKind::Other => IconName::ToolHammer,
})
}
.size(IconSize::Small)
.color(Color::Muted);
let tool_icon =
if tool_call.kind == acp::ToolKind::Edit && tool_call.locations.len() == 1 {
FileIcons::get_icon(&tool_call.locations[0].path, cx)
.map(Icon::from_path)
.unwrap_or(Icon::new(IconName::ToolPencil))
} else {
Icon::new(match tool_call.kind {
acp::ToolKind::Read => IconName::ToolSearch,
acp::ToolKind::Edit => IconName::ToolPencil,
acp::ToolKind::Delete => IconName::ToolDeleteFile,
acp::ToolKind::Move => IconName::ArrowRightLeft,
acp::ToolKind::Search => IconName::ToolSearch,
acp::ToolKind::Execute => IconName::ToolTerminal,
acp::ToolKind::Think => IconName::ToolThink,
acp::ToolKind::Fetch => IconName::ToolWeb,
acp::ToolKind::Other => IconName::ToolHammer,
})
}
.size(IconSize::Small)
.color(Color::Muted);
let failed_or_canceled = match &tool_call.status {
ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed => true,
_ => false,
};
let has_location = tool_call.locations.len() == 1;
let needs_confirmation = matches!(
tool_call.status,
ToolCallStatus::WaitingForConfirmation { .. }
@@ -2203,6 +2195,13 @@ impl AcpThreadView {
.overflow_hidden()
.child(tool_icon)
.child(if has_location {
let name = tool_call.locations[0]
.path
.file_name()
.unwrap_or_default()
.display()
.to_string();
h_flex()
.id(("open-tool-call-location", entry_ix))
.w_full()
@@ -2213,13 +2212,7 @@ impl AcpThreadView {
this.text_color(cx.theme().colors().text_muted)
}
})
.child(self.render_markdown(
tool_call.label.clone(),
MarkdownStyle {
prevent_mouse_interaction: true,
..default_markdown_style(false, true, window, cx)
},
))
.child(name)
.tooltip(Tooltip::text("Jump to File"))
.on_click(cx.listener(move |this, _, window, cx| {
this.open_tool_call_location(entry_ix, 0, window, cx);
@@ -3097,38 +3090,26 @@ impl AcpThreadView {
})
.children(connection.auth_methods().iter().enumerate().rev().map(
|(ix, method)| {
let (method_id, name) = if self
.project
.read(cx)
.is_via_remote_server()
&& method.id.0.as_ref() == "oauth-personal"
&& method.name == "Log in with Google"
{
("spawn-gemini-cli".into(), "Log in with Gemini CLI".into())
} else {
(method.id.0.clone(), method.name.clone())
};
Button::new(
SharedString::from(method.id.0.clone()),
method.name.clone(),
)
.when(ix == 0, |el| {
el.style(ButtonStyle::Tinted(ui::TintColor::Warning))
})
.label_size(LabelSize::Small)
.on_click({
let method_id = method.id.clone();
cx.listener(move |this, _, window, cx| {
telemetry::event!(
"Authenticate Agent Started",
agent = this.agent.telemetry_id(),
method = method_id
);
Button::new(SharedString::from(method_id.clone()), name)
.when(ix == 0, |el| {
el.style(ButtonStyle::Tinted(ui::TintColor::Warning))
})
.label_size(LabelSize::Small)
.on_click({
cx.listener(move |this, _, window, cx| {
telemetry::event!(
"Authenticate Agent Started",
agent = this.agent.telemetry_id(),
method = method_id
);
this.authenticate(
acp::AuthMethodId(method_id.clone()),
window,
cx,
)
})
this.authenticate(method_id.clone(), window, cx)
})
})
},
)),
)
@@ -4089,15 +4070,15 @@ impl AcpThreadView {
MentionUri::PastedImage => {}
MentionUri::Directory { abs_path } => {
let project = workspace.project();
let Some(entry_id) = project.update(cx, |project, cx| {
let Some(entry) = project.update(cx, |project, cx| {
let path = project.find_project_path(abs_path, cx)?;
project.entry_for_path(&path, cx).map(|entry| entry.id)
project.entry_for_path(&path, cx)
}) else {
return;
};
project.update(cx, |_, cx| {
cx.emit(project::Event::RevealInProjectPanel(entry_id));
cx.emit(project::Event::RevealInProjectPanel(entry.id));
});
}
MentionUri::Symbol {
@@ -4110,9 +4091,11 @@ impl AcpThreadView {
line_range,
} => {
let project = workspace.project();
let Some(path) =
project.update(cx, |project, cx| project.find_project_path(path, cx))
else {
let Some((path, _)) = project.update(cx, |project, cx| {
let path = project.find_project_path(path, cx)?;
let entry = project.entry_for_path(&path, cx)?;
Some((path, entry))
}) else {
return;
};
@@ -4274,7 +4257,7 @@ impl AcpThreadView {
}
let buffer = project.update(cx, |project, cx| {
project.create_local_buffer(&markdown, Some(markdown_language), true, cx)
project.create_local_buffer(&markdown, Some(markdown_language), cx)
});
let buffer = cx.new(|cx| {
MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())
@@ -5731,11 +5714,11 @@ pub(crate) mod tests {
fn connect(
&self,
_root_dir: Option<&Path>,
_root_dir: &Path,
_delegate: AgentServerDelegate,
_cx: &mut App,
) -> Task<gpui::Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
Task::ready(Ok((Rc::new(self.connection.clone()), None)))
) -> Task<gpui::Result<Rc<dyn AgentConnection>>> {
Task::ready(Ok(Rc::new(self.connection.clone())))
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {

View File

@@ -3585,7 +3585,7 @@ pub(crate) fn open_active_thread_as_markdown(
}
let buffer = project.update(cx, |project, cx| {
project.create_local_buffer(&markdown, Some(markdown_language), true, cx)
project.create_local_buffer(&markdown, Some(markdown_language), cx)
});
let buffer =
cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone()));

View File

@@ -5,6 +5,7 @@ mod tool_picker;
use std::{ops::Range, sync::Arc};
use agent_servers::{AgentServerCommand, AllAgentServersSettings};
use agent_settings::AgentSettings;
use anyhow::Result;
use assistant_tool::{ToolSource, ToolWorkingSet};
@@ -19,16 +20,13 @@ use gpui::{
Action, AnyView, App, AsyncWindowContext, Corner, Entity, EventEmitter, FocusHandle, Focusable,
Hsla, ScrollHandle, Subscription, Task, WeakEntity,
};
use itertools::Itertools as _;
use language::LanguageRegistry;
use language_model::{
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
};
use notifications::status_toast::{StatusToast, ToastIcon};
use project::{
agent_server_store::{
AgentServerCommand, AgentServerStore, AllAgentServersSettings, CLAUDE_CODE_NAME,
CustomAgentServerSettings, GEMINI_NAME,
},
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
project_settings::{ContextServerSettings, ProjectSettings},
};
@@ -48,13 +46,11 @@ pub(crate) use manage_profiles_modal::ManageProfilesModal;
use crate::{
AddContextServer, ExternalAgent, NewExternalAgentThread,
agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider},
placeholder_command,
};
pub struct AgentConfiguration {
fs: Arc<dyn Fs>,
language_registry: Arc<LanguageRegistry>,
agent_server_store: Entity<AgentServerStore>,
workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle,
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
@@ -71,7 +67,6 @@ pub struct AgentConfiguration {
impl AgentConfiguration {
pub fn new(
fs: Arc<dyn Fs>,
agent_server_store: Entity<AgentServerStore>,
context_server_store: Entity<ContextServerStore>,
tools: Entity<ToolWorkingSet>,
language_registry: Arc<LanguageRegistry>,
@@ -110,7 +105,6 @@ impl AgentConfiguration {
workspace,
focus_handle,
configuration_views_by_provider: HashMap::default(),
agent_server_store,
context_server_store,
expanded_context_server_tools: HashMap::default(),
expanded_provider_configurations: HashMap::default(),
@@ -998,30 +992,18 @@ impl AgentConfiguration {
}
fn render_agent_servers_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let custom_settings = cx
.global::<SettingsStore>()
.get::<AllAgentServersSettings>(None)
.custom
.clone();
let user_defined_agents = self
.agent_server_store
.read(cx)
.external_agents()
.filter(|name| name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME)
.cloned()
.collect::<Vec<_>>();
let user_defined_agents = user_defined_agents
.into_iter()
.map(|name| {
let settings = AllAgentServersSettings::get_global(cx).clone();
let user_defined_agents = settings
.iter()
.filter(|(name, _)| *name != "gemini" && *name != "claude")
.sorted_by(|(l, _), (r, _)| l.cmp(r))
.map(|(name, command)| {
self.render_agent_server(
IconName::Ai,
name.clone(),
ExternalAgent::Custom {
name: name.clone().into(),
command: custom_settings
.get(&name.0)
.map(|settings| settings.command.clone())
.unwrap_or(placeholder_command()),
name: name.clone(),
command: command.clone(),
},
cx,
)
@@ -1300,6 +1282,7 @@ async fn open_new_agent_servers_entry_in_settings_editor(
let settings = cx.global::<SettingsStore>();
let mut unique_server_name = None;
// FIXME test that this still works
let edits = settings.edits_for_update::<AllAgentServersSettings>(&text, |file| {
let server_name: Option<SharedString> = (0..u8::MAX)
.map(|i| {
@@ -1314,12 +1297,10 @@ async fn open_new_agent_servers_entry_in_settings_editor(
unique_server_name = Some(server_name.clone());
file.custom.insert(
server_name,
CustomAgentServerSettings {
command: AgentServerCommand {
path: "path_to_executable".into(),
args: vec![],
env: Some(HashMap::default()),
},
AgentServerCommand {
path: "path_to_executable".into(),
args: vec![],
env: Some(HashMap::default()),
},
);
}

View File

@@ -251,7 +251,6 @@ pub struct ConfigureContextServerModal {
workspace: WeakEntity<Workspace>,
source: ConfigurationSource,
state: State,
original_server_id: Option<ContextServerId>,
}
impl ConfigureContextServerModal {
@@ -349,11 +348,6 @@ impl ConfigureContextServerModal {
context_server_store,
workspace: workspace_handle,
state: State::Idle,
original_server_id: match &target {
ConfigurationTarget::Existing { id, .. } => Some(id.clone()),
ConfigurationTarget::Extension { id, .. } => Some(id.clone()),
ConfigurationTarget::New => None,
},
source: ConfigurationSource::from_target(
target,
language_registry,
@@ -421,19 +415,9 @@ impl ConfigureContextServerModal {
// When we write the settings to the file, the context server will be restarted.
workspace.update(cx, |workspace, cx| {
let fs = workspace.app_state().fs.clone();
let original_server_id = self.original_server_id.clone();
update_settings_file::<ProjectSettings>(
fs.clone(),
cx,
move |project_settings, _| {
if let Some(original_id) = original_server_id {
if original_id != id {
project_settings.context_servers.remove(&original_id.0);
}
}
project_settings.context_servers.insert(id.0, settings);
},
);
update_settings_file::<ProjectSettings>(fs.clone(), cx, |project_settings, _| {
project_settings.context_servers.insert(id.0, settings);
});
});
} else if let Some(existing_server) = existing_server {
self.context_server_store

View File

@@ -5,11 +5,10 @@ use std::sync::Arc;
use std::time::Duration;
use acp_thread::AcpThread;
use agent_servers::AgentServerCommand;
use agent2::{DbThreadMetadata, HistoryEntry};
use db::kvp::{Dismissable, KEY_VALUE_STORE};
use project::agent_server_store::{
AgentServerCommand, AllAgentServersSettings, CLAUDE_CODE_NAME, GEMINI_NAME,
};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use zed_actions::OpenBrowser;
use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent};
@@ -35,9 +34,7 @@ use crate::{
thread_history::{HistoryEntryElement, ThreadHistory},
ui::{AgentOnboardingModal, EndTrialUpsell},
};
use crate::{
ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary, placeholder_command,
};
use crate::{ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary};
use agent::{
Thread, ThreadError, ThreadEvent, ThreadId, ThreadSummary, TokenUsageRatio,
context_store::ContextStore,
@@ -66,7 +63,7 @@ use project::{DisableAiSettings, Project, ProjectPath, Worktree};
use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
use rules_library::{RulesLibrary, open_rules_library};
use search::{BufferSearchBar, buffer_search};
use settings::{Settings, SettingsStore, update_settings_file};
use settings::{Settings, update_settings_file};
use theme::ThemeSettings;
use time::UtcOffset;
use ui::utils::WithRemSize;
@@ -1098,7 +1095,7 @@ impl AgentPanel {
let workspace = self.workspace.clone();
let project = self.project.clone();
let fs = self.fs.clone();
let is_via_collab = self.project.read(cx).is_via_collab();
let is_not_local = !self.project.read(cx).is_local();
const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent";
@@ -1130,7 +1127,7 @@ impl AgentPanel {
agent
}
None => {
if is_via_collab {
if is_not_local {
ExternalAgent::NativeAgent
} else {
cx.background_spawn(async move {
@@ -1507,7 +1504,6 @@ impl AgentPanel {
}
pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let agent_server_store = self.project.read(cx).agent_server_store().clone();
let context_server_store = self.project.read(cx).context_server_store();
let tools = self.thread_store.read(cx).tools();
let fs = self.fs.clone();
@@ -1516,7 +1512,6 @@ impl AgentPanel {
self.configuration = Some(cx.new(|cx| {
AgentConfiguration::new(
fs,
agent_server_store,
context_server_store,
tools,
self.language_registry.clone(),
@@ -2509,7 +2504,6 @@ impl AgentPanel {
}
fn render_toolbar_new(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let agent_server_store = self.project.read(cx).agent_server_store().clone();
let focus_handle = self.focus_handle(cx);
let active_thread = match &self.active_view {
@@ -2542,10 +2536,8 @@ impl AgentPanel {
.with_handle(self.new_thread_menu_handle.clone())
.menu({
let workspace = self.workspace.clone();
let is_via_collab = workspace
.update(cx, |workspace, cx| {
workspace.project().read(cx).is_via_collab()
})
let is_not_local = workspace
.update(cx, |workspace, cx| !workspace.project().read(cx).is_local())
.unwrap_or_default();
move |window, cx| {
@@ -2637,7 +2629,7 @@ impl AgentPanel {
ContextMenuEntry::new("New Gemini CLI Thread")
.icon(IconName::AiGemini)
.icon_color(Color::Muted)
.disabled(is_via_collab)
.disabled(is_not_local)
.handler({
let workspace = workspace.clone();
move |window, cx| {
@@ -2664,7 +2656,7 @@ impl AgentPanel {
menu.item(
ContextMenuEntry::new("New Claude Code Thread")
.icon(IconName::AiClaude)
.disabled(is_via_collab)
.disabled(is_not_local)
.icon_color(Color::Muted)
.handler({
let workspace = workspace.clone();
@@ -2689,25 +2681,25 @@ impl AgentPanel {
)
})
.when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |mut menu| {
let agent_names = agent_server_store
.read(cx)
.external_agents()
.filter(|name| {
name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME
})
.cloned()
.collect::<Vec<_>>();
let custom_settings = cx.global::<SettingsStore>().get::<AllAgentServersSettings>(None).custom.clone();
for agent_name in agent_names {
// Add custom agents from settings
let settings =
agent_servers::AllAgentServersSettings::get_global(cx);
for (agent_name, command) in
settings.iter().sorted_by(|(l, _), (r, _)| l.cmp(r))
{
if agent_name == "gemini" || agent_name == "claude" {
continue;
}
menu = menu.item(
ContextMenuEntry::new(format!("New {} Thread", agent_name))
.icon(IconName::Terminal)
.icon_color(Color::Muted)
.disabled(is_via_collab)
.disabled(is_not_local)
.handler({
let workspace = workspace.clone();
let agent_name = agent_name.clone();
let custom_settings = custom_settings.clone();
let command = command.clone();
move |window, cx| {
if let Some(workspace) = workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
@@ -2718,9 +2710,9 @@ impl AgentPanel {
panel.new_agent_thread(
AgentType::Custom {
name: agent_name
.clone()
.into(),
command: custom_settings.get(&agent_name.0).map(|settings| settings.command.clone()).unwrap_or(placeholder_command())
.clone(),
command: command
.clone(),
},
window,
cx,

View File

@@ -28,6 +28,7 @@ use std::rc::Rc;
use std::sync::Arc;
use agent::{Thread, ThreadId};
use agent_servers::AgentServerCommand;
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection};
use assistant_slash_command::SlashCommandRegistry;
use client::Client;
@@ -40,7 +41,6 @@ use language_model::{
ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
};
use project::DisableAiSettings;
use project::agent_server_store::AgentServerCommand;
use prompt_store::PromptBuilder;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -174,14 +174,6 @@ enum ExternalAgent {
},
}
fn placeholder_command() -> AgentServerCommand {
AgentServerCommand {
path: "/placeholder".into(),
args: vec![],
env: None,
}
}
impl ExternalAgent {
fn name(&self) -> &'static str {
match self {
@@ -201,9 +193,10 @@ impl ExternalAgent {
Self::Gemini => Rc::new(agent_servers::Gemini),
Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
Self::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)),
Self::Custom { name, command: _ } => {
Rc::new(agent_servers::CustomAgentServer::new(name.clone()))
}
Self::Custom { name, command } => Rc::new(agent_servers::CustomAgentServer::new(
name.clone(),
command.clone(),
)),
}
}
}
@@ -344,7 +337,8 @@ fn update_command_palette_filter(cx: &mut App) {
];
filter.show_action_types(edit_prediction_actions.iter());
filter.show_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
filter
.show_action_types([TypeId::of::<zed_actions::OpenZedPredictOnboarding>()].iter());
}
});
}

View File

@@ -987,8 +987,7 @@ impl MentionLink {
.read(cx)
.project()
.read(cx)
.entry_for_path(&project_path, cx)?
.clone();
.entry_for_path(&project_path, cx)?;
Some(MentionLink::File(project_path, entry))
}
Self::SYMBOL => {

View File

@@ -125,7 +125,6 @@ pub(crate) fn create_editor(
cx,
);
editor.set_placeholder_text("Message the agent @ to include context", cx);
editor.disable_word_completions();
editor.set_show_indent_guides(false, cx);
editor.set_soft_wrap();
editor.set_use_modal_editing(true);

View File

@@ -88,7 +88,10 @@ fn view_release_notes_locally(
.update_in(cx, |workspace, window, cx| {
let project = workspace.project().clone();
let buffer = project.update(cx, |project, cx| {
project.create_local_buffer("", markdown, false, cx)
let buffer = project.create_local_buffer("", markdown, cx);
project
.mark_buffer_as_non_searchable(buffer.read(cx).remote_id(), cx);
buffer
});
buffer.update(cx, |buffer, cx| {
buffer.edit([(0..0, body.release_notes)], None, cx)

View File

@@ -75,7 +75,7 @@ util = { workspace = true, features = ["test-support"] }
windows.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
objc2-foundation.workspace = true
cocoa.workspace = true
[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies]
tokio-native-tls = "0.3"

View File

@@ -84,10 +84,6 @@ static DOTNET_PROJECT_FILES_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"^(global\.json|Directory\.Build\.props|.*\.(csproj|fsproj|vbproj|sln))$").unwrap()
});
#[cfg(target_os = "macos")]
static MACOS_VERSION_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(\s*\(Build [^)]*[0-9]\))").unwrap());
pub fn os_name() -> String {
#[cfg(target_os = "macos")]
{
@@ -112,16 +108,19 @@ pub fn os_name() -> String {
pub fn os_version() -> String {
#[cfg(target_os = "macos")]
{
use objc2_foundation::NSProcessInfo;
let process_info = NSProcessInfo::processInfo();
let version_nsstring = unsafe { process_info.operatingSystemVersionString() };
// "Version 15.6.1 (Build 24G90)" -> "15.6.1 (Build 24G90)"
let version_string = version_nsstring.to_string().replace("Version ", "");
// "15.6.1 (Build 24G90)" -> "15.6.1"
// "26.0.0 (Build 25A5349a)" -> unchanged (Beta or Rapid Security Response; ends with letter)
MACOS_VERSION_REGEX
.replace_all(&version_string, "")
use cocoa::base::nil;
use cocoa::foundation::NSProcessInfo;
unsafe {
let process_info = cocoa::foundation::NSProcessInfo::processInfo(nil);
let version = process_info.operatingSystemVersion();
gpui::SemanticVersion::new(
version.majorVersion as usize,
version.minorVersion as usize,
version.patchVersion as usize,
)
.to_string()
}
}
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
{

View File

@@ -2098,7 +2098,7 @@ async fn test_following_after_replacement(cx_a: &mut TestAppContext, cx_b: &mut
share_workspace(&workspace, cx_a).await.unwrap();
let buffer = workspace.update(cx_a, |workspace, cx| {
workspace.project().update(cx, |project, cx| {
project.create_local_buffer(&sample_text(26, 5, 'a'), None, false, cx)
project.create_local_buffer(&sample_text(26, 5, 'a'), None, cx)
})
});
let multibuffer = cx_a.new(|cx| {

View File

@@ -2506,7 +2506,7 @@ async fn test_propagate_saves_and_fs_changes(
});
let new_buffer_a = project_a
.update(cx_a, |p, cx| p.create_buffer(false, cx))
.update(cx_a, |p, cx| p.create_buffer(cx))
.await
.unwrap();

View File

@@ -44,6 +44,14 @@ pub struct ChatPanelSettingsContent {
pub default_width: Option<f32>,
}
#[derive(Deserialize, Debug, SettingsKey)]
#[settings_key(key = "notification_panel")]
pub struct NotificationPanelSettings {
pub button: bool,
pub dock: DockPosition,
pub default_width: Pixels,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
#[settings_key(key = "collaboration_panel")]
pub struct PanelSettingsContent {
@@ -61,30 +69,6 @@ pub struct PanelSettingsContent {
pub default_width: Option<f32>,
}
#[derive(Deserialize, Debug)]
pub struct NotificationPanelSettings {
pub button: bool,
pub dock: DockPosition,
pub default_width: Pixels,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
#[settings_key(key = "notification_panel")]
pub struct NotificationPanelSettingsContent {
/// Whether to show the panel button in the status bar.
///
/// Default: true
pub button: Option<bool>,
/// Where to dock the panel.
///
/// Default: right
pub dock: Option<DockPosition>,
/// Default width of the panel in pixels.
///
/// Default: 300
pub default_width: Option<f32>,
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
#[settings_key(key = "message_editor")]
pub struct MessageEditorSettings {
@@ -122,7 +106,7 @@ impl Settings for ChatPanelSettings {
}
impl Settings for NotificationPanelSettings {
type FileContent = NotificationPanelSettingsContent;
type FileContent = PanelSettingsContent;
fn load(
sources: SettingsSources<Self::FileContent>,

View File

@@ -76,7 +76,7 @@ impl CommandPaletteFilter {
}
/// Hides all actions with the given types.
pub fn hide_action_types<'a>(&mut self, action_types: impl IntoIterator<Item = &'a TypeId>) {
pub fn hide_action_types(&mut self, action_types: &[TypeId]) {
for action_type in action_types {
self.hidden_action_types.insert(*action_type);
self.shown_action_types.remove(action_type);
@@ -84,7 +84,7 @@ impl CommandPaletteFilter {
}
/// Shows all actions with the given types.
pub fn show_action_types<'a>(&mut self, action_types: impl IntoIterator<Item = &'a TypeId>) {
pub fn show_action_types<'a>(&mut self, action_types: impl Iterator<Item = &'a TypeId>) {
for action_type in action_types {
self.shown_action_types.insert(*action_type);
self.hidden_action_types.remove(action_type);

View File

@@ -20,8 +20,5 @@ strum.workspace = true
theme.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
documented.workspace = true
[features]
default = []

View File

@@ -227,8 +227,6 @@ pub trait Component {
/// Example:
///
/// ```
/// use documented::Documented;
///
/// /// This is a doc comment.
/// #[derive(Documented)]
/// struct MyComponent;

View File

@@ -1095,7 +1095,7 @@ impl Copilot {
_ => {
filter.hide_action_types(&signed_in_actions);
filter.hide_action_types(&auth_actions);
filter.show_action_types(&no_auth_actions);
filter.show_action_types(no_auth_actions.iter());
}
}
}

View File

@@ -1,10 +1,8 @@
use dap::{DapRegistry, DebugRequest};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, Render, Task};
use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, Render};
use gpui::{Subscription, WeakEntity};
use picker::{Picker, PickerDelegate};
use project::Project;
use rpc::proto;
use task::ZedDebugConfig;
use util::debug_panic;
@@ -58,28 +56,29 @@ impl AttachModal {
pub fn new(
definition: ZedDebugConfig,
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
modal: bool,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let processes_task = get_processes_for_project(&project, cx);
let modal = Self::with_processes(workspace, definition, Arc::new([]), modal, window, cx);
cx.spawn_in(window, async move |this, cx| {
let processes = processes_task.await;
this.update_in(cx, |modal, window, cx| {
modal.picker.update(cx, |picker, cx| {
picker.delegate.candidates = processes;
picker.refresh(window, cx);
});
})?;
anyhow::Ok(())
})
.detach_and_log_err(cx);
modal
let mut processes: Box<[_]> = System::new_all()
.processes()
.values()
.map(|process| {
let name = process.name().to_string_lossy().into_owned();
Candidate {
name: name.into(),
pid: process.pid().as_u32(),
command: process
.cmd()
.iter()
.map(|s| s.to_string_lossy().to_string())
.collect::<Vec<_>>(),
}
})
.collect();
processes.sort_by_key(|k| k.name.clone());
let processes = processes.into_iter().collect();
Self::with_processes(workspace, definition, processes, modal, window, cx)
}
pub(super) fn with_processes(
@@ -333,57 +332,6 @@ impl PickerDelegate for AttachModalDelegate {
}
}
fn get_processes_for_project(project: &Entity<Project>, cx: &mut App) -> Task<Arc<[Candidate]>> {
let project = project.read(cx);
if let Some(remote_client) = project.remote_client() {
let proto_client = remote_client.read(cx).proto_client();
cx.spawn(async move |_cx| {
let response = proto_client
.request(proto::GetProcesses {
project_id: proto::REMOTE_SERVER_PROJECT_ID,
})
.await
.unwrap_or_else(|_| proto::GetProcessesResponse {
processes: Vec::new(),
});
let mut processes: Vec<Candidate> = response
.processes
.into_iter()
.map(|p| Candidate {
pid: p.pid,
name: p.name.into(),
command: p.command,
})
.collect();
processes.sort_by_key(|k| k.name.clone());
Arc::from(processes.into_boxed_slice())
})
} else {
let mut processes: Box<[_]> = System::new_all()
.processes()
.values()
.map(|process| {
let name = process.name().to_string_lossy().into_owned();
Candidate {
name: name.into(),
pid: process.pid().as_u32(),
command: process
.cmd()
.iter()
.map(|s| s.to_string_lossy().to_string())
.collect::<Vec<_>>(),
}
})
.collect();
processes.sort_by_key(|k| k.name.clone());
let processes = processes.into_iter().collect();
Task::ready(processes)
}
}
#[cfg(any(test, feature = "test-support"))]
pub(crate) fn _process_names(modal: &AttachModal, cx: &mut Context<AttachModal>) -> Vec<String> {
modal.picker.read_with(cx, |picker, _| {

View File

@@ -113,6 +113,23 @@ impl DebugPanel {
}
};
session_entries.push(root_entry);
session_entries.extend(
sessions_with_children
.by_ref()
.take_while(|(session, _)| {
session
.read(cx)
.session(cx)
.read(cx)
.parent_id(cx)
.is_some()
})
.map(|(session, _)| SessionListEntry {
leaf: session.clone(),
ancestors: vec![],
}),
);
}
let weak = cx.weak_entity();

View File

@@ -20,7 +20,7 @@ use gpui::{
};
use itertools::Itertools as _;
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
use project::{DebugScenarioContext, Project, TaskContexts, TaskSourceKind, task_store::TaskStore};
use project::{DebugScenarioContext, TaskContexts, TaskSourceKind, task_store::TaskStore};
use settings::Settings;
use task::{DebugScenario, RevealTarget, ZedDebugConfig};
use theme::ThemeSettings;
@@ -88,10 +88,8 @@ impl NewProcessModal {
})?;
workspace.update_in(cx, |workspace, window, cx| {
let workspace_handle = workspace.weak_handle();
let project = workspace.project().clone();
workspace.toggle_modal(window, cx, |window, cx| {
let attach_mode =
AttachMode::new(None, workspace_handle.clone(), project, window, cx);
let attach_mode = AttachMode::new(None, workspace_handle.clone(), window, cx);
let debug_picker = cx.new(|cx| {
let delegate =
@@ -942,7 +940,6 @@ impl AttachMode {
pub(super) fn new(
debugger: Option<DebugAdapterName>,
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
window: &mut Window,
cx: &mut Context<NewProcessModal>,
) -> Entity<Self> {
@@ -953,7 +950,7 @@ impl AttachMode {
stop_on_entry: Some(false),
};
let attach_picker = cx.new(|cx| {
let modal = AttachModal::new(definition.clone(), workspace, project, false, window, cx);
let modal = AttachModal::new(definition.clone(), workspace, false, window, cx);
window.focus(&modal.focus_handle(cx));
modal

View File

@@ -1,982 +0,0 @@
use crate::{
DIAGNOSTICS_UPDATE_DELAY, IncludeWarnings, ToggleWarnings, context_range_for_entry,
diagnostic_renderer::{DiagnosticBlock, DiagnosticRenderer},
toolbar_controls::DiagnosticsToolbarEditor,
};
use anyhow::Result;
use collections::HashMap;
use editor::{
Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,
display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
multibuffer_context_lines,
};
use gpui::{
AnyElement, App, AppContext, Context, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
Task, WeakEntity, Window, actions, div,
};
use language::{Buffer, DiagnosticEntry, Point};
use project::{
DiagnosticSummary, Event, Project, ProjectItem, ProjectPath,
project_settings::{DiagnosticSeverity, ProjectSettings},
};
use settings::Settings;
use std::{
any::{Any, TypeId},
cmp::Ordering,
sync::Arc,
};
use text::{Anchor, BufferSnapshot, OffsetRangeExt};
use ui::{Button, ButtonStyle, Icon, IconName, Label, Tooltip, h_flex, prelude::*};
use util::paths::PathExt;
use workspace::{
ItemHandle, ItemNavHistory, ToolbarItemLocation, Workspace,
item::{BreadcrumbText, Item, ItemEvent, TabContentParams},
};
actions!(
diagnostics,
[
/// Opens the project diagnostics view for the currently focused file.
DeployCurrentFile,
]
);
/// The `BufferDiagnosticsEditor` is meant to be used when dealing specifically
/// with diagnostics for a single buffer, as only the excerpts of the buffer
/// where diagnostics are available are displayed.
pub(crate) struct BufferDiagnosticsEditor {
pub project: Entity<Project>,
focus_handle: FocusHandle,
editor: Entity<Editor>,
/// The current diagnostic entries in the `BufferDiagnosticsEditor`. Used to
/// allow quick comparison of updated diagnostics, to confirm if anything
/// has changed.
pub(crate) diagnostics: Vec<DiagnosticEntry<Anchor>>,
/// The blocks used to display the diagnostics' content in the editor, next
/// to the excerpts where the diagnostic originated.
blocks: Vec<CustomBlockId>,
/// Multibuffer to contain all excerpts that contain diagnostics, which are
/// to be rendered in the editor.
multibuffer: Entity<MultiBuffer>,
/// The buffer for which the editor is displaying diagnostics and excerpts
/// for.
buffer: Option<Entity<Buffer>>,
/// The path for which the editor is displaying diagnostics for.
project_path: ProjectPath,
/// Summary of the number of warnings and errors for the path. Used to
/// display the number of warnings and errors in the tab's content.
summary: DiagnosticSummary,
/// Whether to include warnings in the list of diagnostics shown in the
/// editor.
pub(crate) include_warnings: bool,
/// Keeps track of whether there's a background task already running to
/// update the excerpts, in order to avoid firing multiple tasks for this purpose.
pub(crate) update_excerpts_task: Option<Task<Result<()>>>,
/// The project's subscription, responsible for processing events related to
/// diagnostics.
_subscription: Subscription,
}
impl BufferDiagnosticsEditor {
/// Creates new instance of the `BufferDiagnosticsEditor` which can then be
/// displayed by adding it to a pane.
pub fn new(
project_path: ProjectPath,
project_handle: Entity<Project>,
buffer: Option<Entity<Buffer>>,
include_warnings: bool,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
// Subscribe to project events related to diagnostics so the
// `BufferDiagnosticsEditor` can update its state accordingly.
let project_event_subscription = cx.subscribe_in(
&project_handle,
window,
|buffer_diagnostics_editor, _project, event, window, cx| match event {
Event::DiskBasedDiagnosticsStarted { .. } => {
cx.notify();
}
Event::DiskBasedDiagnosticsFinished { .. } => {
buffer_diagnostics_editor.update_all_excerpts(window, cx);
}
Event::DiagnosticsUpdated {
paths,
language_server_id,
} => {
// When diagnostics have been updated, the
// `BufferDiagnosticsEditor` should update its state only if
// one of the paths matches its `project_path`, otherwise
// the event should be ignored.
if paths.contains(&buffer_diagnostics_editor.project_path) {
buffer_diagnostics_editor.update_diagnostic_summary(cx);
if buffer_diagnostics_editor.editor.focus_handle(cx).contains_focused(window, cx) || buffer_diagnostics_editor.focus_handle.contains_focused(window, cx) {
log::debug!("diagnostics updated for server {language_server_id}. recording change");
} else {
log::debug!("diagnostics updated for server {language_server_id}. updating excerpts");
buffer_diagnostics_editor.update_all_excerpts(window, cx);
}
}
}
_ => {}
},
);
let focus_handle = cx.focus_handle();
cx.on_focus_in(
&focus_handle,
window,
|buffer_diagnostics_editor, window, cx| buffer_diagnostics_editor.focus_in(window, cx),
)
.detach();
cx.on_focus_out(
&focus_handle,
window,
|buffer_diagnostics_editor, _event, window, cx| {
buffer_diagnostics_editor.focus_out(window, cx)
},
)
.detach();
let summary = project_handle
.read(cx)
.diagnostic_summary_for_path(&project_path, cx);
let multibuffer = cx.new(|cx| MultiBuffer::new(project_handle.read(cx).capability()));
let max_severity = Self::max_diagnostics_severity(include_warnings);
let editor = cx.new(|cx| {
let mut editor = Editor::for_multibuffer(
multibuffer.clone(),
Some(project_handle.clone()),
window,
cx,
);
editor.set_vertical_scroll_margin(5, cx);
editor.disable_inline_diagnostics();
editor.set_max_diagnostics_severity(max_severity, cx);
editor.set_all_diagnostics_active(cx);
editor
});
// Subscribe to events triggered by the editor in order to correctly
// update the buffer's excerpts.
cx.subscribe_in(
&editor,
window,
|buffer_diagnostics_editor, _editor, event: &EditorEvent, window, cx| {
cx.emit(event.clone());
match event {
// If the user tries to focus on the editor but there's actually
// no excerpts for the buffer, focus back on the
// `BufferDiagnosticsEditor` instance.
EditorEvent::Focused => {
if buffer_diagnostics_editor.multibuffer.read(cx).is_empty() {
window.focus(&buffer_diagnostics_editor.focus_handle);
}
}
EditorEvent::Blurred => {
buffer_diagnostics_editor.update_all_excerpts(window, cx)
}
_ => {}
}
},
)
.detach();
let diagnostics = vec![];
let update_excerpts_task = None;
let mut buffer_diagnostics_editor = Self {
project: project_handle,
focus_handle,
editor,
diagnostics,
blocks: Default::default(),
multibuffer,
buffer,
project_path,
summary,
include_warnings,
update_excerpts_task,
_subscription: project_event_subscription,
};
buffer_diagnostics_editor.update_all_diagnostics(window, cx);
buffer_diagnostics_editor
}
fn deploy(
workspace: &mut Workspace,
_: &DeployCurrentFile,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
// Determine the currently opened path by finding the active editor and
// finding the project path for the buffer.
// If there's no active editor with a project path, avoiding deploying
// the buffer diagnostics view.
if let Some(editor) = workspace.active_item_as::<Editor>(cx)
&& let Some(project_path) = editor.project_path(cx)
{
// Check if there's already a `BufferDiagnosticsEditor` tab for this
// same path, and if so, focus on that one instead of creating a new
// one.
let existing_editor = workspace
.items_of_type::<BufferDiagnosticsEditor>(cx)
.find(|editor| editor.read(cx).project_path == project_path);
if let Some(editor) = existing_editor {
workspace.activate_item(&editor, true, true, window, cx);
} else {
let include_warnings = match cx.try_global::<IncludeWarnings>() {
Some(include_warnings) => include_warnings.0,
None => ProjectSettings::get_global(cx).diagnostics.include_warnings,
};
let item = cx.new(|cx| {
Self::new(
project_path,
workspace.project().clone(),
editor.read(cx).buffer().read(cx).as_singleton(),
include_warnings,
window,
cx,
)
});
workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
}
}
}
pub fn register(
workspace: &mut Workspace,
_window: Option<&mut Window>,
_: &mut Context<Workspace>,
) {
workspace.register_action(Self::deploy);
}
fn update_all_diagnostics(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.update_all_excerpts(window, cx);
}
fn update_diagnostic_summary(&mut self, cx: &mut Context<Self>) {
let project = self.project.read(cx);
self.summary = project.diagnostic_summary_for_path(&self.project_path, cx);
}
/// Enqueue an update to the excerpts and diagnostic blocks being shown in
/// the editor.
pub(crate) fn update_all_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
// If there's already a task updating the excerpts, early return and let
// the other task finish.
if self.update_excerpts_task.is_some() {
return;
}
let buffer = self.buffer.clone();
self.update_excerpts_task = Some(cx.spawn_in(window, async move |editor, cx| {
cx.background_executor()
.timer(DIAGNOSTICS_UPDATE_DELAY)
.await;
if let Some(buffer) = buffer {
editor
.update_in(cx, |editor, window, cx| {
editor.update_excerpts(buffer, window, cx)
})?
.await?;
};
let _ = editor.update(cx, |editor, cx| {
editor.update_excerpts_task = None;
cx.notify();
});
Ok(())
}));
}
/// Updates the excerpts in the `BufferDiagnosticsEditor` for a single
/// buffer.
fn update_excerpts(
&mut self,
buffer: Entity<Buffer>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let was_empty = self.multibuffer.read(cx).is_empty();
let multibuffer_context = multibuffer_context_lines(cx);
let buffer_snapshot = buffer.read(cx).snapshot();
let buffer_snapshot_max = buffer_snapshot.max_point();
let max_severity = Self::max_diagnostics_severity(self.include_warnings)
.into_lsp()
.unwrap_or(lsp::DiagnosticSeverity::WARNING);
cx.spawn_in(window, async move |buffer_diagnostics_editor, mut cx| {
// Fetch the diagnostics for the whole of the buffer
// (`Point::zero()..buffer_snapshot.max_point()`) so we can confirm
// if the diagnostics changed, if it didn't, early return as there's
// nothing to update.
let diagnostics = buffer_snapshot
.diagnostics_in_range::<_, Anchor>(Point::zero()..buffer_snapshot_max, false)
.collect::<Vec<_>>();
let unchanged =
buffer_diagnostics_editor.update(cx, |buffer_diagnostics_editor, _cx| {
if buffer_diagnostics_editor
.diagnostics_are_unchanged(&diagnostics, &buffer_snapshot)
{
return true;
}
buffer_diagnostics_editor.set_diagnostics(&diagnostics);
return false;
})?;
if unchanged {
return Ok(());
}
// Mapping between the Group ID and a vector of DiagnosticEntry.
let mut grouped: HashMap<usize, Vec<_>> = HashMap::default();
for entry in diagnostics {
grouped
.entry(entry.diagnostic.group_id)
.or_default()
.push(DiagnosticEntry {
range: entry.range.to_point(&buffer_snapshot),
diagnostic: entry.diagnostic,
})
}
let mut blocks: Vec<DiagnosticBlock> = Vec::new();
for (_, group) in grouped {
// If the minimum severity of the group is higher than the
// maximum severity, or it doesn't even have severity, skip this
// group.
if group
.iter()
.map(|d| d.diagnostic.severity)
.min()
.is_none_or(|severity| severity > max_severity)
{
continue;
}
let diagnostic_blocks = cx.update(|_window, cx| {
DiagnosticRenderer::diagnostic_blocks_for_group(
group,
buffer_snapshot.remote_id(),
Some(Arc::new(buffer_diagnostics_editor.clone())),
cx,
)
})?;
// For each of the diagnostic blocks to be displayed in the
// editor, figure out its index in the list of blocks.
//
// The following rules are used to determine the order:
// 1. Blocks with a lower start position should come first.
// 2. If two blocks have the same start position, the one with
// the higher end position should come first.
for diagnostic_block in diagnostic_blocks {
let index = blocks.partition_point(|probe| {
match probe
.initial_range
.start
.cmp(&diagnostic_block.initial_range.start)
{
Ordering::Less => true,
Ordering::Greater => false,
Ordering::Equal => {
probe.initial_range.end > diagnostic_block.initial_range.end
}
}
});
blocks.insert(index, diagnostic_block);
}
}
// Build the excerpt ranges for this specific buffer's diagnostics,
// so those excerpts can later be used to update the excerpts shown
// in the editor.
// This is done by iterating over the list of diagnostic blocks and
// determine what range does the diagnostic block span.
let mut excerpt_ranges: Vec<ExcerptRange<Point>> = Vec::new();
for diagnostic_block in blocks.iter() {
let excerpt_range = context_range_for_entry(
diagnostic_block.initial_range.clone(),
multibuffer_context,
buffer_snapshot.clone(),
&mut cx,
)
.await;
let index = excerpt_ranges
.binary_search_by(|probe| {
probe
.context
.start
.cmp(&excerpt_range.start)
.then(probe.context.end.cmp(&excerpt_range.end))
.then(
probe
.primary
.start
.cmp(&diagnostic_block.initial_range.start),
)
.then(probe.primary.end.cmp(&diagnostic_block.initial_range.end))
.then(Ordering::Greater)
})
.unwrap_or_else(|index| index);
excerpt_ranges.insert(
index,
ExcerptRange {
context: excerpt_range,
primary: diagnostic_block.initial_range.clone(),
},
)
}
// Finally, update the editor's content with the new excerpt ranges
// for this editor, as well as the diagnostic blocks.
buffer_diagnostics_editor.update_in(cx, |buffer_diagnostics_editor, window, cx| {
// Remove the list of `CustomBlockId` from the editor's display
// map, ensuring that if any diagnostics have been solved, the
// associated block stops being shown.
let block_ids = buffer_diagnostics_editor.blocks.clone();
buffer_diagnostics_editor.editor.update(cx, |editor, cx| {
editor.display_map.update(cx, |display_map, cx| {
display_map.remove_blocks(block_ids.into_iter().collect(), cx);
})
});
let (anchor_ranges, _) =
buffer_diagnostics_editor
.multibuffer
.update(cx, |multibuffer, cx| {
multibuffer.set_excerpt_ranges_for_path(
PathKey::for_buffer(&buffer, cx),
buffer.clone(),
&buffer_snapshot,
excerpt_ranges,
cx,
)
});
if was_empty {
if let Some(anchor_range) = anchor_ranges.first() {
let range_to_select = anchor_range.start..anchor_range.start;
buffer_diagnostics_editor.editor.update(cx, |editor, cx| {
editor.change_selections(Default::default(), window, cx, |selection| {
selection.select_anchor_ranges([range_to_select])
})
});
// If the `BufferDiagnosticsEditor` is currently
// focused, move focus to its editor.
if buffer_diagnostics_editor.focus_handle.is_focused(window) {
buffer_diagnostics_editor
.editor
.read(cx)
.focus_handle(cx)
.focus(window);
}
}
}
// Cloning the blocks before moving ownership so these can later
// be used to set the block contents for testing purposes.
#[cfg(test)]
let cloned_blocks = blocks.clone();
// Build new diagnostic blocks to be added to the editor's
// display map for the new diagnostics. Update the `blocks`
// property before finishing, to ensure the blocks are removed
// on the next execution.
let editor_blocks =
anchor_ranges
.into_iter()
.zip(blocks.into_iter())
.map(|(anchor, block)| {
let editor = buffer_diagnostics_editor.editor.downgrade();
BlockProperties {
placement: BlockPlacement::Near(anchor.start),
height: Some(1),
style: BlockStyle::Flex,
render: Arc::new(move |block_context| {
block.render_block(editor.clone(), block_context)
}),
priority: 1,
}
});
let block_ids = buffer_diagnostics_editor.editor.update(cx, |editor, cx| {
editor.display_map.update(cx, |display_map, cx| {
display_map.insert_blocks(editor_blocks, cx)
})
});
// In order to be able to verify which diagnostic blocks are
// rendered in the editor, the `set_block_content_for_tests`
// function must be used, so that the
// `editor::test::editor_content_with_blocks` function can then
// be called to fetch these blocks.
#[cfg(test)]
{
for (block_id, block) in block_ids.iter().zip(cloned_blocks.iter()) {
let markdown = block.markdown.clone();
editor::test::set_block_content_for_tests(
&buffer_diagnostics_editor.editor,
*block_id,
cx,
move |cx| {
markdown::MarkdownElement::rendered_text(
markdown.clone(),
cx,
editor::hover_popover::diagnostics_markdown_style,
)
},
);
}
}
buffer_diagnostics_editor.blocks = block_ids;
cx.notify()
})
})
}
fn set_diagnostics(&mut self, diagnostics: &Vec<DiagnosticEntry<Anchor>>) {
self.diagnostics = diagnostics.clone();
}
fn diagnostics_are_unchanged(
&self,
diagnostics: &Vec<DiagnosticEntry<Anchor>>,
snapshot: &BufferSnapshot,
) -> bool {
if self.diagnostics.len() != diagnostics.len() {
return false;
}
self.diagnostics
.iter()
.zip(diagnostics.iter())
.all(|(existing, new)| {
existing.diagnostic.message == new.diagnostic.message
&& existing.diagnostic.severity == new.diagnostic.severity
&& existing.diagnostic.is_primary == new.diagnostic.is_primary
&& existing.range.to_offset(snapshot) == new.range.to_offset(snapshot)
})
}
fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
// If the `BufferDiagnosticsEditor` is focused and the multibuffer is
// not empty, focus on the editor instead, which will allow the user to
// start interacting and editing the buffer's contents.
if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
self.editor.focus_handle(cx).focus(window)
}
}
fn focus_out(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if !self.focus_handle.is_focused(window) && !self.editor.focus_handle(cx).is_focused(window)
{
self.update_all_excerpts(window, cx);
}
}
pub fn toggle_warnings(
&mut self,
_: &ToggleWarnings,
window: &mut Window,
cx: &mut Context<Self>,
) {
let include_warnings = !self.include_warnings;
let max_severity = Self::max_diagnostics_severity(include_warnings);
self.editor.update(cx, |editor, cx| {
editor.set_max_diagnostics_severity(max_severity, cx);
});
self.include_warnings = include_warnings;
self.diagnostics.clear();
self.update_all_diagnostics(window, cx);
}
fn max_diagnostics_severity(include_warnings: bool) -> DiagnosticSeverity {
match include_warnings {
true => DiagnosticSeverity::Warning,
false => DiagnosticSeverity::Error,
}
}
#[cfg(test)]
pub fn editor(&self) -> &Entity<Editor> {
&self.editor
}
#[cfg(test)]
pub fn summary(&self) -> &DiagnosticSummary {
&self.summary
}
}
impl Focusable for BufferDiagnosticsEditor {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl EventEmitter<EditorEvent> for BufferDiagnosticsEditor {}
impl Item for BufferDiagnosticsEditor {
type Event = EditorEvent;
fn act_as_type<'a>(
&'a self,
type_id: std::any::TypeId,
self_handle: &'a Entity<Self>,
_: &'a App,
) -> Option<gpui::AnyView> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.to_any())
} else if type_id == TypeId::of::<Editor>() {
Some(self.editor.to_any())
} else {
None
}
}
fn added_to_workspace(
&mut self,
workspace: &mut Workspace,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.editor.update(cx, |editor, cx| {
editor.added_to_workspace(workspace, window, cx)
});
}
fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
ToolbarItemLocation::PrimaryLeft
}
fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
self.editor.breadcrumbs(theme, cx)
}
fn can_save(&self, _cx: &App) -> bool {
true
}
fn clone_on_split(
&self,
_workspace_id: Option<workspace::WorkspaceId>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<Entity<Self>>
where
Self: Sized,
{
Some(cx.new(|cx| {
BufferDiagnosticsEditor::new(
self.project_path.clone(),
self.project.clone(),
self.buffer.clone(),
self.include_warnings,
window,
cx,
)
}))
}
fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.editor
.update(cx, |editor, cx| editor.deactivated(window, cx));
}
fn for_each_project_item(&self, cx: &App, f: &mut dyn FnMut(EntityId, &dyn ProjectItem)) {
self.editor.for_each_project_item(cx, f);
}
fn has_conflict(&self, cx: &App) -> bool {
self.multibuffer.read(cx).has_conflict(cx)
}
fn has_deleted_file(&self, cx: &App) -> bool {
self.multibuffer.read(cx).has_deleted_file(cx)
}
fn is_dirty(&self, cx: &App) -> bool {
self.multibuffer.read(cx).is_dirty(cx)
}
fn is_singleton(&self, _cx: &App) -> bool {
false
}
fn navigate(
&mut self,
data: Box<dyn Any>,
window: &mut Window,
cx: &mut Context<Self>,
) -> bool {
self.editor
.update(cx, |editor, cx| editor.navigate(data, window, cx))
}
fn reload(
&mut self,
project: Entity<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
self.editor.reload(project, window, cx)
}
fn save(
&mut self,
options: workspace::item::SaveOptions,
project: Entity<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
self.editor.save(options, project, window, cx)
}
fn save_as(
&mut self,
_project: Entity<Project>,
_path: ProjectPath,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Task<Result<()>> {
unreachable!()
}
fn set_nav_history(
&mut self,
nav_history: ItemNavHistory,
_window: &mut Window,
cx: &mut Context<Self>,
) {
self.editor.update(cx, |editor, _| {
editor.set_nav_history(Some(nav_history));
})
}
// Builds the content to be displayed in the tab.
fn tab_content(&self, params: TabContentParams, _window: &Window, _cx: &App) -> AnyElement {
let error_count = self.summary.error_count;
let warning_count = self.summary.warning_count;
let label = Label::new(
self.project_path
.path
.file_name()
.map(|f| f.to_sanitized_string())
.unwrap_or_else(|| self.project_path.path.to_sanitized_string()),
);
h_flex()
.gap_1()
.child(label)
.when(error_count == 0 && warning_count == 0, |parent| {
parent.child(
h_flex()
.gap_1()
.child(Icon::new(IconName::Check).color(Color::Success)),
)
})
.when(error_count > 0, |parent| {
parent.child(
h_flex()
.gap_1()
.child(Icon::new(IconName::XCircle).color(Color::Error))
.child(Label::new(error_count.to_string()).color(params.text_color())),
)
})
.when(warning_count > 0, |parent| {
parent.child(
h_flex()
.gap_1()
.child(Icon::new(IconName::Warning).color(Color::Warning))
.child(Label::new(warning_count.to_string()).color(params.text_color())),
)
})
.into_any_element()
}
fn tab_content_text(&self, _detail: usize, _app: &App) -> SharedString {
"Buffer Diagnostics".into()
}
fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
Some(
format!(
"Buffer Diagnostics - {}",
self.project_path.path.to_sanitized_string()
)
.into(),
)
}
fn telemetry_event_text(&self) -> Option<&'static str> {
Some("Buffer Diagnostics Opened")
}
fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
Editor::to_item_events(event, f)
}
}
impl Render for BufferDiagnosticsEditor {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let filename = self.project_path.path.to_sanitized_string();
let error_count = self.summary.error_count;
let warning_count = match self.include_warnings {
true => self.summary.warning_count,
false => 0,
};
let child = if error_count + warning_count == 0 {
let label = match warning_count {
0 => "No problems in",
_ => "No errors in",
};
v_flex()
.key_context("EmptyPane")
.size_full()
.gap_1()
.justify_center()
.items_center()
.text_center()
.bg(cx.theme().colors().editor_background)
.child(
div()
.h_flex()
.child(Label::new(label).color(Color::Muted))
.child(
Button::new("open-file", filename)
.style(ButtonStyle::Transparent)
.tooltip(Tooltip::text("Open File"))
.on_click(cx.listener(|buffer_diagnostics, _, window, cx| {
if let Some(workspace) = window.root::<Workspace>().flatten() {
workspace.update(cx, |workspace, cx| {
workspace
.open_path(
buffer_diagnostics.project_path.clone(),
None,
true,
window,
cx,
)
.detach_and_log_err(cx);
})
}
})),
),
)
.when(self.summary.warning_count > 0, |div| {
let label = match self.summary.warning_count {
1 => "Show 1 warning".into(),
warning_count => format!("Show {} warnings", warning_count),
};
div.child(
Button::new("diagnostics-show-warning-label", label).on_click(cx.listener(
|buffer_diagnostics_editor, _, window, cx| {
buffer_diagnostics_editor.toggle_warnings(
&Default::default(),
window,
cx,
);
cx.notify();
},
)),
)
})
} else {
div().size_full().child(self.editor.clone())
};
div()
.key_context("Diagnostics")
.track_focus(&self.focus_handle(cx))
.size_full()
.child(child)
}
}
impl DiagnosticsToolbarEditor for WeakEntity<BufferDiagnosticsEditor> {
fn include_warnings(&self, cx: &App) -> bool {
self.read_with(cx, |buffer_diagnostics_editor, _cx| {
buffer_diagnostics_editor.include_warnings
})
.unwrap_or(false)
}
fn has_stale_excerpts(&self, _cx: &App) -> bool {
false
}
fn is_updating(&self, cx: &App) -> bool {
self.read_with(cx, |buffer_diagnostics_editor, cx| {
buffer_diagnostics_editor.update_excerpts_task.is_some()
|| buffer_diagnostics_editor
.project
.read(cx)
.language_servers_running_disk_based_diagnostics(cx)
.next()
.is_some()
})
.unwrap_or(false)
}
fn stop_updating(&self, cx: &mut App) {
let _ = self.update(cx, |buffer_diagnostics_editor, cx| {
buffer_diagnostics_editor.update_excerpts_task = None;
cx.notify();
});
}
fn refresh_diagnostics(&self, window: &mut Window, cx: &mut App) {
let _ = self.update(cx, |buffer_diagnostics_editor, cx| {
buffer_diagnostics_editor.update_all_excerpts(window, cx);
});
}
fn toggle_warnings(&self, window: &mut Window, cx: &mut App) {
let _ = self.update(cx, |buffer_diagnostics_editor, cx| {
buffer_diagnostics_editor.toggle_warnings(&Default::default(), window, cx);
});
}
fn get_diagnostics_for_buffer(
&self,
_buffer_id: text::BufferId,
cx: &App,
) -> Vec<language::DiagnosticEntry<text::Anchor>> {
self.read_with(cx, |buffer_diagnostics_editor, _cx| {
buffer_diagnostics_editor.diagnostics.clone()
})
.unwrap_or_default()
}
}

View File

@@ -18,7 +18,7 @@ use ui::{
};
use util::maybe;
use crate::toolbar_controls::DiagnosticsToolbarEditor;
use crate::ProjectDiagnosticsEditor;
pub struct DiagnosticRenderer;
@@ -26,7 +26,7 @@ impl DiagnosticRenderer {
pub fn diagnostic_blocks_for_group(
diagnostic_group: Vec<DiagnosticEntry<Point>>,
buffer_id: BufferId,
diagnostics_editor: Option<Arc<dyn DiagnosticsToolbarEditor>>,
diagnostics_editor: Option<WeakEntity<ProjectDiagnosticsEditor>>,
cx: &mut App,
) -> Vec<DiagnosticBlock> {
let Some(primary_ix) = diagnostic_group
@@ -130,7 +130,6 @@ impl editor::DiagnosticRenderer for DiagnosticRenderer {
cx: &mut App,
) -> Vec<BlockProperties<Anchor>> {
let blocks = Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, cx);
blocks
.into_iter()
.map(|block| {
@@ -183,7 +182,7 @@ pub(crate) struct DiagnosticBlock {
pub(crate) initial_range: Range<Point>,
pub(crate) severity: DiagnosticSeverity,
pub(crate) markdown: Entity<Markdown>,
pub(crate) diagnostics_editor: Option<Arc<dyn DiagnosticsToolbarEditor>>,
pub(crate) diagnostics_editor: Option<WeakEntity<ProjectDiagnosticsEditor>>,
}
impl DiagnosticBlock {
@@ -234,7 +233,7 @@ impl DiagnosticBlock {
pub fn open_link(
editor: &mut Editor,
diagnostics_editor: &Option<Arc<dyn DiagnosticsToolbarEditor>>,
diagnostics_editor: &Option<WeakEntity<ProjectDiagnosticsEditor>>,
link: SharedString,
window: &mut Window,
cx: &mut Context<Editor>,
@@ -255,10 +254,18 @@ impl DiagnosticBlock {
if let Some(diagnostics_editor) = diagnostics_editor {
if let Some(diagnostic) = diagnostics_editor
.get_diagnostics_for_buffer(buffer_id, cx)
.into_iter()
.filter(|d| d.diagnostic.group_id == group_id)
.nth(ix)
.read_with(cx, |diagnostics, _| {
diagnostics
.diagnostics
.get(&buffer_id)
.cloned()
.unwrap_or_default()
.into_iter()
.filter(|d| d.diagnostic.group_id == group_id)
.nth(ix)
})
.ok()
.flatten()
{
let multibuffer = editor.buffer().read(cx);
let Some(snapshot) = multibuffer
@@ -290,9 +297,9 @@ impl DiagnosticBlock {
};
}
fn jump_to<I: ToOffset>(
fn jump_to<T: ToOffset>(
editor: &mut Editor,
range: Range<I>,
range: Range<T>,
window: &mut Window,
cx: &mut Context<Editor>,
) {

View File

@@ -1,14 +1,12 @@
pub mod items;
mod toolbar_controls;
mod buffer_diagnostics;
mod diagnostic_renderer;
#[cfg(test)]
mod diagnostics_tests;
use anyhow::Result;
use buffer_diagnostics::BufferDiagnosticsEditor;
use collections::{BTreeSet, HashMap};
use diagnostic_renderer::DiagnosticBlock;
use editor::{
@@ -38,7 +36,6 @@ use std::{
};
use text::{BufferId, OffsetRangeExt};
use theme::ActiveTheme;
use toolbar_controls::DiagnosticsToolbarEditor;
pub use toolbar_controls::ToolbarControls;
use ui::{Icon, IconName, Label, h_flex, prelude::*};
use util::ResultExt;
@@ -67,7 +64,6 @@ impl Global for IncludeWarnings {}
pub fn init(cx: &mut App) {
editor::set_diagnostic_renderer(diagnostic_renderer::DiagnosticRenderer {}, cx);
cx.observe_new(ProjectDiagnosticsEditor::register).detach();
cx.observe_new(BufferDiagnosticsEditor::register).detach();
}
pub(crate) struct ProjectDiagnosticsEditor {
@@ -89,7 +85,6 @@ pub(crate) struct ProjectDiagnosticsEditor {
impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
const DIAGNOSTICS_UPDATE_DELAY: Duration = Duration::from_millis(50);
const DIAGNOSTICS_SUMMARY_UPDATE_DELAY: Duration = Duration::from_millis(30);
impl Render for ProjectDiagnosticsEditor {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
@@ -149,7 +144,7 @@ impl Render for ProjectDiagnosticsEditor {
}
impl ProjectDiagnosticsEditor {
pub fn register(
fn register(
workspace: &mut Workspace,
_window: Option<&mut Window>,
_: &mut Context<Workspace>,
@@ -165,7 +160,7 @@ impl ProjectDiagnosticsEditor {
cx: &mut Context<Self>,
) -> Self {
let project_event_subscription =
cx.subscribe_in(&project_handle, window, |this, _project, event, window, cx| match event {
cx.subscribe_in(&project_handle, window, |this, project, event, window, cx| match event {
project::Event::DiskBasedDiagnosticsStarted { .. } => {
cx.notify();
}
@@ -178,12 +173,13 @@ impl ProjectDiagnosticsEditor {
paths,
} => {
this.paths_to_update.extend(paths.clone());
let project = project.clone();
this.diagnostic_summary_update = cx.spawn(async move |this, cx| {
cx.background_executor()
.timer(DIAGNOSTICS_SUMMARY_UPDATE_DELAY)
.timer(Duration::from_millis(30))
.await;
this.update(cx, |this, cx| {
this.update_diagnostic_summary(cx);
this.summary = project.read(cx).diagnostic_summary(false, cx);
})
.log_err();
});
@@ -330,7 +326,6 @@ impl ProjectDiagnosticsEditor {
let is_active = workspace
.active_item(cx)
.is_some_and(|item| item.item_id() == existing.item_id());
workspace.activate_item(&existing, true, !is_active, window, cx);
} else {
let workspace_handle = cx.entity().downgrade();
@@ -388,25 +383,22 @@ impl ProjectDiagnosticsEditor {
/// currently have diagnostics or are currently present in this view.
fn update_all_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.project.update(cx, |project, cx| {
let mut project_paths = project
let mut paths = project
.diagnostic_summaries(false, cx)
.map(|(project_path, _, _)| project_path)
.map(|(path, _, _)| path)
.collect::<BTreeSet<_>>();
self.multibuffer.update(cx, |multibuffer, cx| {
for buffer in multibuffer.all_buffers() {
if let Some(file) = buffer.read(cx).file() {
project_paths.insert(ProjectPath {
paths.insert(ProjectPath {
path: file.path().clone(),
worktree_id: file.worktree_id(cx),
});
}
}
});
self.paths_to_update = project_paths;
self.paths_to_update = paths;
});
self.update_stale_excerpts(window, cx);
}
@@ -436,7 +428,6 @@ impl ProjectDiagnosticsEditor {
let was_empty = self.multibuffer.read(cx).is_empty();
let buffer_snapshot = buffer.read(cx).snapshot();
let buffer_id = buffer_snapshot.remote_id();
let max_severity = if self.include_warnings {
lsp::DiagnosticSeverity::WARNING
} else {
@@ -450,7 +441,6 @@ impl ProjectDiagnosticsEditor {
false,
)
.collect::<Vec<_>>();
let unchanged = this.update(cx, |this, _| {
if this.diagnostics.get(&buffer_id).is_some_and(|existing| {
this.diagnostics_are_unchanged(existing, &diagnostics, &buffer_snapshot)
@@ -485,7 +475,7 @@ impl ProjectDiagnosticsEditor {
crate::diagnostic_renderer::DiagnosticRenderer::diagnostic_blocks_for_group(
group,
buffer_snapshot.remote_id(),
Some(Arc::new(this.clone())),
Some(this.clone()),
cx,
)
})?;
@@ -515,7 +505,6 @@ impl ProjectDiagnosticsEditor {
cx,
)
.await;
let i = excerpt_ranges
.binary_search_by(|probe| {
probe
@@ -585,7 +574,6 @@ impl ProjectDiagnosticsEditor {
priority: 1,
}
});
let block_ids = this.editor.update(cx, |editor, cx| {
editor.display_map.update(cx, |display_map, cx| {
display_map.insert_blocks(editor_blocks, cx)
@@ -616,10 +604,6 @@ impl ProjectDiagnosticsEditor {
})
})
}
fn update_diagnostic_summary(&mut self, cx: &mut Context<Self>) {
self.summary = self.project.read(cx).diagnostic_summary(false, cx);
}
}
impl Focusable for ProjectDiagnosticsEditor {
@@ -828,68 +812,6 @@ impl Item for ProjectDiagnosticsEditor {
}
}
impl DiagnosticsToolbarEditor for WeakEntity<ProjectDiagnosticsEditor> {
fn include_warnings(&self, cx: &App) -> bool {
self.read_with(cx, |project_diagnostics_editor, _cx| {
project_diagnostics_editor.include_warnings
})
.unwrap_or(false)
}
fn has_stale_excerpts(&self, cx: &App) -> bool {
self.read_with(cx, |project_diagnostics_editor, _cx| {
!project_diagnostics_editor.paths_to_update.is_empty()
})
.unwrap_or(false)
}
fn is_updating(&self, cx: &App) -> bool {
self.read_with(cx, |project_diagnostics_editor, cx| {
project_diagnostics_editor.update_excerpts_task.is_some()
|| project_diagnostics_editor
.project
.read(cx)
.language_servers_running_disk_based_diagnostics(cx)
.next()
.is_some()
})
.unwrap_or(false)
}
fn stop_updating(&self, cx: &mut App) {
let _ = self.update(cx, |project_diagnostics_editor, cx| {
project_diagnostics_editor.update_excerpts_task = None;
cx.notify();
});
}
fn refresh_diagnostics(&self, window: &mut Window, cx: &mut App) {
let _ = self.update(cx, |project_diagnostics_editor, cx| {
project_diagnostics_editor.update_all_excerpts(window, cx);
});
}
fn toggle_warnings(&self, window: &mut Window, cx: &mut App) {
let _ = self.update(cx, |project_diagnostics_editor, cx| {
project_diagnostics_editor.toggle_warnings(&Default::default(), window, cx);
});
}
fn get_diagnostics_for_buffer(
&self,
buffer_id: text::BufferId,
cx: &App,
) -> Vec<language::DiagnosticEntry<text::Anchor>> {
self.read_with(cx, |project_diagnostics_editor, _cx| {
project_diagnostics_editor
.diagnostics
.get(&buffer_id)
.cloned()
.unwrap_or_default()
})
.unwrap_or_default()
}
}
const DIAGNOSTIC_EXPANSION_ROW_LIMIT: u32 = 32;
async fn context_range_for_entry(

View File

@@ -1567,440 +1567,6 @@ async fn go_to_diagnostic_with_severity(cx: &mut TestAppContext) {
cx.assert_editor_state(indoc! {"error ˇwarning info hint"});
}
#[gpui::test]
async fn test_buffer_diagnostics(cx: &mut TestAppContext) {
init_test(cx);
// We'll be creating two different files, both with diagnostics, so we can
// later verify that, since the `BufferDiagnosticsEditor` only shows
// diagnostics for the provided path, the diagnostics for the other file
// will not be shown, contrary to what happens with
// `ProjectDiagnosticsEditor`.
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/test"),
json!({
"main.rs": "
fn main() {
let x = vec![];
let y = vec![];
a(x);
b(y);
c(y);
d(x);
}
"
.unindent(),
"other.rs": "
fn other() {
let unused = 42;
undefined_function();
}
"
.unindent(),
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*window, cx);
let project_path = project::ProjectPath {
worktree_id: project.read_with(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id()
}),
path: Arc::from(Path::new("main.rs")),
};
let buffer = project
.update(cx, |project, cx| {
project.open_buffer(project_path.clone(), cx)
})
.await
.ok();
// Create the diagnostics for `main.rs`.
let language_server_id = LanguageServerId(0);
let uri = lsp::Uri::from_file_path(path!("/test/main.rs")).unwrap();
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.update_diagnostics(language_server_id, lsp::PublishDiagnosticsParams {
uri: uri.clone(),
diagnostics: vec![
lsp::Diagnostic{
range: lsp::Range::new(lsp::Position::new(5, 6), lsp::Position::new(5, 7)),
severity: Some(lsp::DiagnosticSeverity::WARNING),
message: "use of moved value\nvalue used here after move".to_string(),
related_information: Some(vec![
lsp::DiagnosticRelatedInformation {
location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 9))),
message: "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
},
lsp::DiagnosticRelatedInformation {
location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(4, 6), lsp::Position::new(4, 7))),
message: "value moved here".to_string()
},
]),
..Default::default()
},
lsp::Diagnostic{
range: lsp::Range::new(lsp::Position::new(6, 6), lsp::Position::new(6, 7)),
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: "use of moved value\nvalue used here after move".to_string(),
related_information: Some(vec![
lsp::DiagnosticRelatedInformation {
location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9))),
message: "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
},
lsp::DiagnosticRelatedInformation {
location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(3, 6), lsp::Position::new(3, 7))),
message: "value moved here".to_string()
},
]),
..Default::default()
}
],
version: None
}, None, DiagnosticSourceKind::Pushed, &[], cx).unwrap();
// Create diagnostics for other.rs to ensure that the file and
// diagnostics are not included in `BufferDiagnosticsEditor` when it is
// deployed for main.rs.
lsp_store.update_diagnostics(language_server_id, lsp::PublishDiagnosticsParams {
uri: lsp::Uri::from_file_path(path!("/test/other.rs")).unwrap(),
diagnostics: vec![
lsp::Diagnostic{
range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 14)),
severity: Some(lsp::DiagnosticSeverity::WARNING),
message: "unused variable: `unused`".to_string(),
..Default::default()
},
lsp::Diagnostic{
range: lsp::Range::new(lsp::Position::new(2, 4), lsp::Position::new(2, 22)),
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: "cannot find function `undefined_function` in this scope".to_string(),
..Default::default()
}
],
version: None
}, None, DiagnosticSourceKind::Pushed, &[], cx).unwrap();
});
let buffer_diagnostics = window.build_entity(cx, |window, cx| {
BufferDiagnosticsEditor::new(
project_path.clone(),
project.clone(),
buffer,
true,
window,
cx,
)
});
let editor = buffer_diagnostics.update(cx, |buffer_diagnostics, _| {
buffer_diagnostics.editor().clone()
});
// Since the excerpt updates is handled by a background task, we need to
// wait a little bit to ensure that the buffer diagnostic's editor content
// is rendered.
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
pretty_assertions::assert_eq!(
editor_content_with_blocks(&editor, cx),
indoc::indoc! {
"§ main.rs
§ -----
fn main() {
let x = vec![];
§ move occurs because `x` has type `Vec<char>`, which does not implement
§ the `Copy` trait (back)
let y = vec![];
§ move occurs because `y` has type `Vec<char>`, which does not implement
§ the `Copy` trait
a(x); § value moved here
b(y); § value moved here
c(y);
§ use of moved value
§ value used here after move
d(x);
§ use of moved value
§ value used here after move
§ hint: move occurs because `x` has type `Vec<char>`, which does not
§ implement the `Copy` trait
}"
}
);
}
#[gpui::test]
async fn test_buffer_diagnostics_without_warnings(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/test"),
json!({
"main.rs": "
fn main() {
let x = vec![];
let y = vec![];
a(x);
b(y);
c(y);
d(x);
}
"
.unindent(),
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*window, cx);
let project_path = project::ProjectPath {
worktree_id: project.read_with(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id()
}),
path: Arc::from(Path::new("main.rs")),
};
let buffer = project
.update(cx, |project, cx| {
project.open_buffer(project_path.clone(), cx)
})
.await
.ok();
let language_server_id = LanguageServerId(0);
let uri = lsp::Uri::from_file_path(path!("/test/main.rs")).unwrap();
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.update_diagnostics(language_server_id, lsp::PublishDiagnosticsParams {
uri: uri.clone(),
diagnostics: vec![
lsp::Diagnostic{
range: lsp::Range::new(lsp::Position::new(5, 6), lsp::Position::new(5, 7)),
severity: Some(lsp::DiagnosticSeverity::WARNING),
message: "use of moved value\nvalue used here after move".to_string(),
related_information: Some(vec![
lsp::DiagnosticRelatedInformation {
location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 9))),
message: "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
},
lsp::DiagnosticRelatedInformation {
location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(4, 6), lsp::Position::new(4, 7))),
message: "value moved here".to_string()
},
]),
..Default::default()
},
lsp::Diagnostic{
range: lsp::Range::new(lsp::Position::new(6, 6), lsp::Position::new(6, 7)),
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: "use of moved value\nvalue used here after move".to_string(),
related_information: Some(vec![
lsp::DiagnosticRelatedInformation {
location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9))),
message: "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
},
lsp::DiagnosticRelatedInformation {
location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(3, 6), lsp::Position::new(3, 7))),
message: "value moved here".to_string()
},
]),
..Default::default()
}
],
version: None
}, None, DiagnosticSourceKind::Pushed, &[], cx).unwrap();
});
let include_warnings = false;
let buffer_diagnostics = window.build_entity(cx, |window, cx| {
BufferDiagnosticsEditor::new(
project_path.clone(),
project.clone(),
buffer,
include_warnings,
window,
cx,
)
});
let editor = buffer_diagnostics.update(cx, |buffer_diagnostics, _cx| {
buffer_diagnostics.editor().clone()
});
// Since the excerpt updates is handled by a background task, we need to
// wait a little bit to ensure that the buffer diagnostic's editor content
// is rendered.
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
pretty_assertions::assert_eq!(
editor_content_with_blocks(&editor, cx),
indoc::indoc! {
"§ main.rs
§ -----
fn main() {
let x = vec![];
§ move occurs because `x` has type `Vec<char>`, which does not implement
§ the `Copy` trait (back)
let y = vec![];
a(x); § value moved here
b(y);
c(y);
d(x);
§ use of moved value
§ value used here after move
§ hint: move occurs because `x` has type `Vec<char>`, which does not
§ implement the `Copy` trait
}"
}
);
}
#[gpui::test]
async fn test_buffer_diagnostics_multiple_servers(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/test"),
json!({
"main.rs": "
fn main() {
let x = vec![];
let y = vec![];
a(x);
b(y);
c(y);
d(x);
}
"
.unindent(),
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*window, cx);
let project_path = project::ProjectPath {
worktree_id: project.read_with(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id()
}),
path: Arc::from(Path::new("main.rs")),
};
let buffer = project
.update(cx, |project, cx| {
project.open_buffer(project_path.clone(), cx)
})
.await
.ok();
// Create the diagnostics for `main.rs`.
// Two warnings are being created, one for each language server, in order to
// assert that both warnings are rendered in the editor.
let language_server_id_a = LanguageServerId(0);
let language_server_id_b = LanguageServerId(1);
let uri = lsp::Uri::from_file_path(path!("/test/main.rs")).unwrap();
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
lsp_store.update(cx, |lsp_store, cx| {
lsp_store
.update_diagnostics(
language_server_id_a,
lsp::PublishDiagnosticsParams {
uri: uri.clone(),
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(5, 6), lsp::Position::new(5, 7)),
severity: Some(lsp::DiagnosticSeverity::WARNING),
message: "use of moved value\nvalue used here after move".to_string(),
related_information: None,
..Default::default()
}],
version: None,
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
.unwrap();
lsp_store
.update_diagnostics(
language_server_id_b,
lsp::PublishDiagnosticsParams {
uri: uri.clone(),
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(6, 6), lsp::Position::new(6, 7)),
severity: Some(lsp::DiagnosticSeverity::WARNING),
message: "use of moved value\nvalue used here after move".to_string(),
related_information: None,
..Default::default()
}],
version: None,
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
.unwrap();
});
let buffer_diagnostics = window.build_entity(cx, |window, cx| {
BufferDiagnosticsEditor::new(
project_path.clone(),
project.clone(),
buffer,
true,
window,
cx,
)
});
let editor = buffer_diagnostics.update(cx, |buffer_diagnostics, _| {
buffer_diagnostics.editor().clone()
});
// Since the excerpt updates is handled by a background task, we need to
// wait a little bit to ensure that the buffer diagnostic's editor content
// is rendered.
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
pretty_assertions::assert_eq!(
editor_content_with_blocks(&editor, cx),
indoc::indoc! {
"§ main.rs
§ -----
a(x);
b(y);
c(y);
§ use of moved value
§ value used here after move
d(x);
§ use of moved value
§ value used here after move
}"
}
);
buffer_diagnostics.update(cx, |buffer_diagnostics, _cx| {
assert_eq!(
*buffer_diagnostics.summary(),
DiagnosticSummary {
warning_count: 2,
error_count: 0
}
);
})
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
zlog::init_test();

View File

@@ -1,56 +1,33 @@
use crate::{BufferDiagnosticsEditor, ProjectDiagnosticsEditor, ToggleDiagnosticsRefresh};
use gpui::{Context, EventEmitter, ParentElement, Render, Window};
use language::DiagnosticEntry;
use text::{Anchor, BufferId};
use crate::{ProjectDiagnosticsEditor, ToggleDiagnosticsRefresh};
use gpui::{Context, Entity, EventEmitter, ParentElement, Render, WeakEntity, Window};
use ui::prelude::*;
use ui::{IconButton, IconButtonShape, IconName, Tooltip};
use workspace::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, item::ItemHandle};
pub struct ToolbarControls {
editor: Option<Box<dyn DiagnosticsToolbarEditor>>,
}
pub(crate) trait DiagnosticsToolbarEditor: Send + Sync {
/// Informs the toolbar whether warnings are included in the diagnostics.
fn include_warnings(&self, cx: &App) -> bool;
/// Toggles whether warning diagnostics should be displayed by the
/// diagnostics editor.
fn toggle_warnings(&self, window: &mut Window, cx: &mut App);
/// Indicates whether any of the excerpts displayed by the diagnostics
/// editor are stale.
fn has_stale_excerpts(&self, cx: &App) -> bool;
/// Indicates whether the diagnostics editor is currently updating the
/// diagnostics.
fn is_updating(&self, cx: &App) -> bool;
/// Requests that the diagnostics editor stop updating the diagnostics.
fn stop_updating(&self, cx: &mut App);
/// Requests that the diagnostics editor updates the displayed diagnostics
/// with the latest information.
fn refresh_diagnostics(&self, window: &mut Window, cx: &mut App);
/// Returns a list of diagnostics for the provided buffer id.
fn get_diagnostics_for_buffer(
&self,
buffer_id: BufferId,
cx: &App,
) -> Vec<DiagnosticEntry<Anchor>>;
editor: Option<WeakEntity<ProjectDiagnosticsEditor>>,
}
impl Render for ToolbarControls {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let mut has_stale_excerpts = false;
let mut include_warnings = false;
let mut has_stale_excerpts = false;
let mut is_updating = false;
match &self.editor {
Some(editor) => {
include_warnings = editor.include_warnings(cx);
has_stale_excerpts = editor.has_stale_excerpts(cx);
is_updating = editor.is_updating(cx);
}
None => {}
if let Some(editor) = self.diagnostics() {
let diagnostics = editor.read(cx);
include_warnings = diagnostics.include_warnings;
has_stale_excerpts = !diagnostics.paths_to_update.is_empty();
is_updating = diagnostics.update_excerpts_task.is_some()
|| diagnostics
.project
.read(cx)
.language_servers_running_disk_based_diagnostics(cx)
.next()
.is_some();
}
let warning_tooltip = if include_warnings {
let tooltip = if include_warnings {
"Exclude Warnings"
} else {
"Include Warnings"
@@ -75,12 +52,11 @@ impl Render for ToolbarControls {
&ToggleDiagnosticsRefresh,
))
.on_click(cx.listener(move |toolbar_controls, _, _, cx| {
match toolbar_controls.editor() {
Some(editor) => {
editor.stop_updating(cx);
if let Some(diagnostics) = toolbar_controls.diagnostics() {
diagnostics.update(cx, |diagnostics, cx| {
diagnostics.update_excerpts_task = None;
cx.notify();
}
None => {}
});
}
})),
)
@@ -95,11 +71,12 @@ impl Render for ToolbarControls {
&ToggleDiagnosticsRefresh,
))
.on_click(cx.listener({
move |toolbar_controls, _, window, cx| match toolbar_controls
.editor()
{
Some(editor) => editor.refresh_diagnostics(window, cx),
None => {}
move |toolbar_controls, _, window, cx| {
if let Some(diagnostics) = toolbar_controls.diagnostics() {
diagnostics.update(cx, move |diagnostics, cx| {
diagnostics.update_all_excerpts(window, cx);
});
}
}
})),
)
@@ -109,10 +86,13 @@ impl Render for ToolbarControls {
IconButton::new("toggle-warnings", IconName::Warning)
.icon_color(warning_color)
.shape(IconButtonShape::Square)
.tooltip(Tooltip::text(warning_tooltip))
.on_click(cx.listener(|this, _, window, cx| match &this.editor {
Some(editor) => editor.toggle_warnings(window, cx),
None => {}
.tooltip(Tooltip::text(tooltip))
.on_click(cx.listener(|this, _, window, cx| {
if let Some(editor) = this.diagnostics() {
editor.update(cx, |editor, cx| {
editor.toggle_warnings(&Default::default(), window, cx);
});
}
})),
)
}
@@ -129,10 +109,7 @@ impl ToolbarItemView for ToolbarControls {
) -> ToolbarItemLocation {
if let Some(pane_item) = active_pane_item.as_ref() {
if let Some(editor) = pane_item.downcast::<ProjectDiagnosticsEditor>() {
self.editor = Some(Box::new(editor.downgrade()));
ToolbarItemLocation::PrimaryRight
} else if let Some(editor) = pane_item.downcast::<BufferDiagnosticsEditor>() {
self.editor = Some(Box::new(editor.downgrade()));
self.editor = Some(editor.downgrade());
ToolbarItemLocation::PrimaryRight
} else {
ToolbarItemLocation::Hidden
@@ -154,7 +131,7 @@ impl ToolbarControls {
ToolbarControls { editor: None }
}
fn editor(&self) -> Option<&dyn DiagnosticsToolbarEditor> {
self.editor.as_deref()
fn diagnostics(&self) -> Option<Entity<ProjectDiagnosticsEditor>> {
self.editor.as_ref()?.upgrade()
}
}

View File

@@ -92,7 +92,6 @@ uuid.workspace = true
workspace.workspace = true
zed_actions.workspace = true
workspace-hack.workspace = true
postage.workspace = true
[dev-dependencies]
ctor.workspace = true

View File

@@ -251,7 +251,7 @@ enum MarkdownCacheKey {
pub enum CompletionsMenuSource {
Normal,
SnippetChoices,
Words { ignore_threshold: bool },
Words,
}
// TODO: There should really be a wrapper around fuzzy match tasks that does this.

View File

@@ -177,15 +177,17 @@ use snippet::Snippet;
use std::{
any::TypeId,
borrow::Cow,
cell::{OnceCell, RefCell},
cell::OnceCell,
cell::RefCell,
cmp::{self, Ordering, Reverse},
iter::Peekable,
mem,
num::NonZeroU32,
ops::{ControlFlow, Deref, DerefMut, Not, Range, RangeInclusive},
ops::Not,
ops::{ControlFlow, Deref, DerefMut, Range, RangeInclusive},
path::{Path, PathBuf},
rc::Rc,
sync::{Arc, LazyLock},
sync::Arc,
time::{Duration, Instant},
};
use task::{ResolvedTask, RunnableTag, TaskTemplate, TaskVariables};
@@ -234,21 +236,6 @@ pub(crate) const EDIT_PREDICTION_KEY_CONTEXT: &str = "edit_prediction";
pub(crate) const EDIT_PREDICTION_CONFLICT_KEY_CONTEXT: &str = "edit_prediction_conflict";
pub(crate) const MINIMAP_FONT_SIZE: AbsoluteLength = AbsoluteLength::Pixels(px(2.));
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct LastCursorPosition {
pub path: PathBuf,
pub worktree_path: Arc<Path>,
pub point: Point,
}
pub static LAST_CURSOR_POSITION_WATCH: LazyLock<(
Mutex<postage::watch::Sender<Option<LastCursorPosition>>>,
postage::watch::Receiver<Option<LastCursorPosition>>,
)> = LazyLock::new(|| {
let (sender, receiver) = postage::watch::channel();
(Mutex::new(sender), receiver)
});
pub type RenderDiffHunkControlsFn = Arc<
dyn Fn(
u32,
@@ -1043,7 +1030,6 @@ pub struct Editor {
inline_diagnostics_update: Task<()>,
inline_diagnostics_enabled: bool,
diagnostics_enabled: bool,
word_completions_enabled: bool,
inline_diagnostics: Vec<(Anchor, InlineDiagnostic)>,
soft_wrap_mode_override: Option<language_settings::SoftWrap>,
hard_wrap: Option<usize>,
@@ -1808,7 +1794,7 @@ impl Editor {
let font_size = style.font_size.to_pixels(window.rem_size());
let editor = cx.entity().downgrade();
let fold_placeholder = FoldPlaceholder {
constrain_width: false,
constrain_width: true,
render: Arc::new(move |fold_id, fold_range, cx| {
let editor = editor.clone();
div()
@@ -2177,7 +2163,6 @@ impl Editor {
},
inline_diagnostics_enabled: full_mode,
diagnostics_enabled: full_mode,
word_completions_enabled: full_mode,
inline_value_cache: InlineValueCache::new(inlay_hint_settings.show_value_hints),
inlay_hint_cache: InlayHintCache::new(inlay_hint_settings),
gutter_hovered: false,
@@ -2632,7 +2617,7 @@ impl Editor {
cx: &mut Context<Workspace>,
) -> Task<Result<Entity<Editor>>> {
let project = workspace.project().clone();
let create = project.update(cx, |project, cx| project.create_buffer(true, cx));
let create = project.update(cx, |project, cx| project.create_buffer(cx));
cx.spawn_in(window, async move |workspace, cx| {
let buffer = create.await?;
@@ -2670,7 +2655,7 @@ impl Editor {
cx: &mut Context<Workspace>,
) {
let project = workspace.project().clone();
let create = project.update(cx, |project, cx| project.create_buffer(true, cx));
let create = project.update(cx, |project, cx| project.create_buffer(cx));
cx.spawn_in(window, async move |workspace, cx| {
let buffer = create.await?;
@@ -3077,28 +3062,10 @@ impl Editor {
let new_cursor_position = newest_selection.head();
let selection_start = newest_selection.start;
let new_cursor_point = new_cursor_position.to_point(buffer);
if let Some(project) = self.project()
&& let Some((path, worktree_path)) =
self.file_at(new_cursor_point, cx).and_then(|file| {
file.as_local().and_then(|file| {
let worktree =
project.read(cx).worktree_for_id(file.worktree_id(cx), cx)?;
Some((file.abs_path(cx), worktree.read(cx).abs_path()))
})
})
{
*LAST_CURSOR_POSITION_WATCH.0.lock().borrow_mut() = Some(LastCursorPosition {
path,
worktree_path,
point: new_cursor_point,
});
}
if effects.nav_history.is_none() || effects.nav_history == Some(true) {
self.push_to_nav_history(
*old_cursor_position,
Some(new_cursor_point),
Some(new_cursor_position.to_point(buffer)),
false,
effects.nav_history == Some(true),
cx,
@@ -4925,15 +4892,8 @@ impl Editor {
});
match completions_source {
Some(CompletionsMenuSource::Words { .. }) => {
self.open_or_update_completions_menu(
Some(CompletionsMenuSource::Words {
ignore_threshold: false,
}),
None,
window,
cx,
);
Some(CompletionsMenuSource::Words) => {
self.show_word_completions(&ShowWordCompletions, window, cx)
}
Some(CompletionsMenuSource::Normal)
| Some(CompletionsMenuSource::SnippetChoices)
@@ -5441,14 +5401,7 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Self>,
) {
self.open_or_update_completions_menu(
Some(CompletionsMenuSource::Words {
ignore_threshold: true,
}),
None,
window,
cx,
);
self.open_or_update_completions_menu(Some(CompletionsMenuSource::Words), None, window, cx);
}
pub fn show_completions(
@@ -5497,13 +5450,9 @@ impl Editor {
drop(multibuffer_snapshot);
let mut ignore_word_threshold = false;
let provider = match requested_source {
Some(CompletionsMenuSource::Normal) | None => self.completion_provider.clone(),
Some(CompletionsMenuSource::Words { ignore_threshold }) => {
ignore_word_threshold = ignore_threshold;
None
}
Some(CompletionsMenuSource::Words) => None,
Some(CompletionsMenuSource::SnippetChoices) => {
log::error!("bug: SnippetChoices requested_source is not handled");
None
@@ -5624,12 +5573,10 @@ impl Editor {
.as_ref()
.is_none_or(|query| !query.chars().any(|c| c.is_digit(10)));
let omit_word_completions = !self.word_completions_enabled
|| (!ignore_word_threshold
&& match &query {
Some(query) => query.chars().count() < completion_settings.words_min_length,
None => completion_settings.words_min_length != 0,
});
let omit_word_completions = match &query {
Some(query) => query.chars().count() < completion_settings.words_min_length,
None => completion_settings.words_min_length != 0,
};
let (mut words, provider_responses) = match &provider {
Some(provider) => {
@@ -11444,17 +11391,14 @@ impl Editor {
let mut edits = Vec::new();
let mut selection_adjustment = 0i32;
for selection in self.selections.all_adjusted(cx) {
for selection in self.selections.all::<usize>(cx) {
let selection_is_empty = selection.is_empty();
let (start, end) = if selection_is_empty {
let (word_range, _) = buffer.surrounding_word(selection.start, false);
(word_range.start, word_range.end)
} else {
(
buffer.point_to_offset(selection.start),
buffer.point_to_offset(selection.end),
)
(selection.start, selection.end)
};
let text = buffer.text_for_range(start..end).collect::<String>();
@@ -11465,8 +11409,7 @@ impl Editor {
start: (start as i32 - selection_adjustment) as usize,
end: ((start + text.len()) as i32 - selection_adjustment) as usize,
goal: SelectionGoal::None,
id: selection.id,
reversed: selection.reversed,
..selection
});
selection_adjustment += old_length - text.len() as i32;
@@ -17174,10 +17117,6 @@ impl Editor {
self.inline_diagnostics.clear();
}
pub fn disable_word_completions(&mut self) {
self.word_completions_enabled = false;
}
pub fn diagnostics_enabled(&self) -> bool {
self.diagnostics_enabled && self.mode.is_full()
}
@@ -19029,8 +18968,6 @@ impl Editor {
}
}
/// Returns the project path for the editor's buffer, if any buffer is
/// opened in the editor.
pub fn project_path(&self, cx: &App) -> Option<ProjectPath> {
if let Some(buffer) = self.buffer.read(cx).as_singleton() {
buffer.read(cx).project_path(cx)

View File

@@ -748,7 +748,6 @@ pub struct ScrollbarAxesContent {
#[derive(
Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi,
)]
#[settings_ui(group = "Gutter")]
pub struct GutterContent {
/// Whether to show line numbers in the gutter.
///

View File

@@ -5363,20 +5363,6 @@ async fn test_manipulate_text(cx: &mut TestAppContext) {
cx.assert_editor_state(indoc! {"
«HeLlO, wOrLD!ˇ»
"});
// Test selections with `line_mode = true`.
cx.update_editor(|editor, _window, _cx| editor.selections.line_mode = true);
cx.set_state(indoc! {"
«The quick brown
fox jumps over
tˇ»he lazy dog
"});
cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
cx.assert_editor_state(indoc! {"
«THE QUICK BROWN
FOX JUMPS OVER
THE LAZY DOGˇ»
"});
}
#[gpui::test]
@@ -14278,26 +14264,6 @@ async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppCont
}
});
cx.update_editor(|editor, window, cx| {
editor.show_word_completions(&ShowWordCompletions, window, cx);
});
cx.executor().run_until_parked();
cx.update_editor(|editor, window, cx| {
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
{
assert_eq!(completion_menu_entries(menu), &["wowser", "wowen", "wow"], "Even though the threshold is not met, invoking word completions with an action should provide the completions");
} else {
panic!("expected completion menu to be open after the word completions are called with an action");
}
editor.cancel(&Cancel, window, cx);
});
cx.update_editor(|editor, _, _| {
if editor.context_menu.borrow_mut().is_some() {
panic!("expected completion menu to be hidden after canceling");
}
});
cx.simulate_keystroke("o");
cx.executor().run_until_parked();
cx.update_editor(|editor, _, _| {
@@ -14320,50 +14286,6 @@ async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppCont
});
}
#[gpui::test]
async fn test_word_completions_disabled(cx: &mut TestAppContext) {
init_test(cx, |language_settings| {
language_settings.defaults.completions = Some(CompletionSettings {
words: WordsCompletionMode::Enabled,
words_min_length: 0,
lsp: true,
lsp_fetch_timeout_ms: 0,
lsp_insert_mode: LspInsertMode::Insert,
});
});
let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
cx.update_editor(|editor, _, _| {
editor.disable_word_completions();
});
cx.set_state(indoc! {"ˇ
wow
wowen
wowser
"});
cx.simulate_keystroke("w");
cx.executor().run_until_parked();
cx.update_editor(|editor, _, _| {
if editor.context_menu.borrow_mut().is_some() {
panic!(
"expected completion menu to be hidden, as words completion are disabled for this editor"
);
}
});
cx.update_editor(|editor, window, cx| {
editor.show_word_completions(&ShowWordCompletions, window, cx);
});
cx.executor().run_until_parked();
cx.update_editor(|editor, _, _| {
if editor.context_menu.borrow_mut().is_some() {
panic!(
"expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
);
}
});
}
fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
let position = || lsp::Position {
line: params.text_document_position.position.line,
@@ -15913,7 +15835,7 @@ async fn test_following(cx: &mut TestAppContext) {
let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
let buffer = project.update(cx, |project, cx| {
let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
cx.new(|cx| MultiBuffer::singleton(buffer, cx))
});
let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
@@ -16165,8 +16087,8 @@ async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
(
project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
)
});

View File

@@ -651,8 +651,7 @@ impl Item for Editor {
if let Some(path) = path_for_buffer(&self.buffer, detail, true, cx) {
path.to_string_lossy().to_string().into()
} else {
// Use the same logic as the displayed title for consistency
self.buffer.read(cx).title(cx).to_string().into()
"untitled".into()
}
}
@@ -1130,7 +1129,7 @@ impl SerializableItem for Editor {
// First create the empty buffer
let buffer = project
.update(cx, |project, cx| project.create_buffer(true, cx))?
.update(cx, |project, cx| project.create_buffer(cx))?
.await?;
// Then set the text so that the dirty bit is set correctly
@@ -1238,7 +1237,7 @@ impl SerializableItem for Editor {
..
} => window.spawn(cx, async move |cx| {
let buffer = project
.update(cx, |project, cx| project.create_buffer(true, cx))?
.update(cx, |project, cx| project.create_buffer(cx))?
.await?;
cx.update(|window, cx| {

View File

@@ -4,7 +4,7 @@
use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint};
use crate::{DisplayRow, EditorStyle, ToOffset, ToPoint, scroll::ScrollAnchor};
use gpui::{Pixels, WindowTextSystem};
use language::{CharClassifier, Point};
use language::Point;
use multi_buffer::{MultiBufferRow, MultiBufferSnapshot};
use serde::Deserialize;
use workspace::searchable::Direction;
@@ -405,18 +405,15 @@ pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> Dis
let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
find_preceding_boundary_display_point(map, point, FindRange::MultiLine, |left, right| {
is_subword_start(left, right, &classifier) || left == '\n'
let is_word_start =
classifier.kind(left) != classifier.kind(right) && !right.is_whitespace();
let is_subword_start = classifier.is_word('-') && left == '-' && right != '-'
|| left == '_' && right != '_'
|| left.is_lowercase() && right.is_uppercase();
is_word_start || is_subword_start || left == '\n'
})
}
pub fn is_subword_start(left: char, right: char, classifier: &CharClassifier) -> bool {
let is_word_start = classifier.kind(left) != classifier.kind(right) && !right.is_whitespace();
let is_subword_start = classifier.is_word('-') && left == '-' && right != '-'
|| left == '_' && right != '_'
|| left.is_lowercase() && right.is_uppercase();
is_word_start || is_subword_start
}
/// Returns a position of the next word boundary, where a word character is defined as either
/// uppercase letter, lowercase letter, '_' character or language-specific word character (like '-' in CSS).
pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint {
@@ -466,19 +463,15 @@ pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPo
let classifier = map.buffer_snapshot.char_classifier_at(raw_point);
find_boundary(map, point, FindRange::MultiLine, |left, right| {
is_subword_end(left, right, &classifier) || right == '\n'
let is_word_end =
(classifier.kind(left) != classifier.kind(right)) && !classifier.is_whitespace(left);
let is_subword_end = classifier.is_word('-') && left != '-' && right == '-'
|| left != '_' && right == '_'
|| left.is_lowercase() && right.is_uppercase();
is_word_end || is_subword_end || right == '\n'
})
}
pub fn is_subword_end(left: char, right: char, classifier: &CharClassifier) -> bool {
let is_word_end =
(classifier.kind(left) != classifier.kind(right)) && !classifier.is_whitespace(left);
let is_subword_end = classifier.is_word('-') && left != '-' && right == '-'
|| left != '_' && right == '_'
|| left.is_lowercase() && right.is_uppercase();
is_word_end || is_subword_end
}
/// Returns a position of the start of the current paragraph, where a paragraph
/// is defined as a run of non-blank lines.
pub fn start_of_paragraph(

View File

@@ -200,7 +200,7 @@ pub fn expand_macro_recursively(
}
let buffer = project
.update(cx, |project, cx| project.create_buffer(false, cx))?
.update(cx, |project, cx| project.create_buffer(cx))?
.await?;
workspace.update_in(cx, |workspace, window, cx| {
buffer.update(cx, |buffer, cx| {

View File

@@ -23,6 +23,7 @@ use workspace::Workspace;
pub(crate) struct OpenPathPrompt;
#[derive(Debug)]
pub struct OpenPathDelegate {
tx: Option<oneshot::Sender<Option<Vec<PathBuf>>>>,
lister: DirectoryLister,
@@ -34,9 +35,6 @@ pub struct OpenPathDelegate {
prompt_root: String,
path_style: PathStyle,
replace_prompt: Task<()>,
render_footer:
Arc<dyn Fn(&mut Window, &mut Context<Picker<Self>>) -> Option<AnyElement> + 'static>,
hidden_entries: bool,
}
impl OpenPathDelegate {
@@ -62,25 +60,9 @@ impl OpenPathDelegate {
},
path_style,
replace_prompt: Task::ready(()),
render_footer: Arc::new(|_, _| None),
hidden_entries: false,
}
}
pub fn with_footer(
mut self,
footer: Arc<
dyn Fn(&mut Window, &mut Context<Picker<Self>>) -> Option<AnyElement> + 'static,
>,
) -> Self {
self.render_footer = footer;
self
}
pub fn show_hidden(mut self) -> Self {
self.hidden_entries = true;
self
}
fn get_entry(&self, selected_match_index: usize) -> Option<CandidateInfo> {
match &self.directory_state {
DirectoryState::List { entries, .. } => {
@@ -287,7 +269,7 @@ impl PickerDelegate for OpenPathDelegate {
self.cancel_flag.store(true, atomic::Ordering::Release);
self.cancel_flag = Arc::new(AtomicBool::new(false));
let cancel_flag = self.cancel_flag.clone();
let hidden_entries = self.hidden_entries;
let parent_path_is_root = self.prompt_root == dir;
let current_dir = self.current_dir();
cx.spawn_in(window, async move |this, cx| {
@@ -381,7 +363,7 @@ impl PickerDelegate for OpenPathDelegate {
};
let mut max_id = 0;
if !suffix.starts_with('.') && !hidden_entries {
if !suffix.starts_with('.') {
new_entries.retain(|entry| {
max_id = max_id.max(entry.path.id);
!entry.path.string.starts_with('.')
@@ -799,14 +781,6 @@ impl PickerDelegate for OpenPathDelegate {
}
}
fn render_footer(
&self,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<AnyElement> {
(self.render_footer)(window, cx)
}
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
Some(match &self.directory_state {
DirectoryState::Create { .. } => SharedString::from("Type a path…"),

View File

@@ -20,9 +20,6 @@ use std::os::fd::{AsFd, AsRawFd};
#[cfg(unix)]
use std::os::unix::fs::{FileTypeExt, MetadataExt};
#[cfg(any(target_os = "macos", target_os = "freebsd"))]
use std::mem::MaybeUninit;
use async_tar::Archive;
use futures::{AsyncRead, Stream, StreamExt, future::BoxFuture};
use git::repository::{GitRepository, RealGitRepository};
@@ -264,15 +261,14 @@ impl FileHandle for std::fs::File {
};
let fd = self.as_fd();
let mut path_buf = MaybeUninit::<[u8; libc::PATH_MAX as usize]>::uninit();
let mut path_buf: [libc::c_char; libc::PATH_MAX as usize] = [0; libc::PATH_MAX as usize];
let result = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_GETPATH, path_buf.as_mut_ptr()) };
if result == -1 {
anyhow::bail!("fcntl returned -1".to_string());
}
// SAFETY: `fcntl` will initialize the path buffer.
let c_str = unsafe { CStr::from_ptr(path_buf.as_ptr().cast()) };
let c_str = unsafe { CStr::from_ptr(path_buf.as_ptr()) };
let path = PathBuf::from(OsStr::from_bytes(c_str.to_bytes()));
Ok(path)
}
@@ -300,16 +296,15 @@ impl FileHandle for std::fs::File {
};
let fd = self.as_fd();
let mut kif = MaybeUninit::<libc::kinfo_file>::uninit();
let mut kif: libc::kinfo_file = unsafe { std::mem::zeroed() };
kif.kf_structsize = libc::KINFO_FILE_SIZE;
let result = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_KINFO, kif.as_mut_ptr()) };
let result = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_KINFO, &mut kif) };
if result == -1 {
anyhow::bail!("fcntl returned -1".to_string());
}
// SAFETY: `fcntl` will initialize the kif.
let c_str = unsafe { CStr::from_ptr(kif.assume_init().kf_path.as_ptr()) };
let c_str = unsafe { CStr::from_ptr(kif.kf_path.as_ptr()) };
let path = PathBuf::from(OsStr::from_bytes(c_str.to_bytes()));
Ok(path)
}

View File

@@ -1205,10 +1205,9 @@ impl GitRepository for RealGitRepository {
env: Arc<HashMap<String, String>>,
) -> BoxFuture<'_, Result<()>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
self.executor
.spawn(async move {
let mut cmd = new_smol_command(&git_binary_path);
let mut cmd = new_smol_command("git");
cmd.current_dir(&working_directory?)
.envs(env.iter())
.args(["stash", "push", "--quiet"])
@@ -1230,10 +1229,9 @@ impl GitRepository for RealGitRepository {
fn stash_pop(&self, env: Arc<HashMap<String, String>>) -> BoxFuture<'_, Result<()>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
self.executor
.spawn(async move {
let mut cmd = new_smol_command(&git_binary_path);
let mut cmd = new_smol_command("git");
cmd.current_dir(&working_directory?)
.envs(env.iter())
.args(["stash", "pop"]);
@@ -1258,10 +1256,9 @@ impl GitRepository for RealGitRepository {
env: Arc<HashMap<String, String>>,
) -> BoxFuture<'_, Result<()>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
self.executor
.spawn(async move {
let mut cmd = new_smol_command(&git_binary_path);
let mut cmd = new_smol_command("git");
cmd.current_dir(&working_directory?)
.envs(env.iter())
.args(["commit", "--quiet", "-m"])
@@ -1305,7 +1302,7 @@ impl GitRepository for RealGitRepository {
let executor = cx.background_executor().clone();
async move {
let working_directory = working_directory?;
let mut command = new_smol_command(&self.git_binary_path);
let mut command = new_smol_command("git");
command
.envs(env.iter())
.current_dir(&working_directory)
@@ -1336,7 +1333,7 @@ impl GitRepository for RealGitRepository {
let working_directory = self.working_directory();
let executor = cx.background_executor().clone();
async move {
let mut command = new_smol_command(&self.git_binary_path);
let mut command = new_smol_command("git");
command
.envs(env.iter())
.current_dir(&working_directory?)
@@ -1362,7 +1359,7 @@ impl GitRepository for RealGitRepository {
let remote_name = format!("{}", fetch_options);
let executor = cx.background_executor().clone();
async move {
let mut command = new_smol_command(&self.git_binary_path);
let mut command = new_smol_command("git");
command
.envs(env.iter())
.current_dir(&working_directory?)

View File

@@ -388,6 +388,9 @@ pub(crate) fn commit_message_editor(
window: &mut Window,
cx: &mut Context<Editor>,
) -> Editor {
project.update(cx, |this, cx| {
this.mark_buffer_as_non_searchable(commit_message_buffer.read(cx).remote_id(), cx);
});
let buffer = cx.new(|cx| MultiBuffer::singleton(commit_message_buffer, cx));
let max_lines = if in_panel { MAX_PANEL_EDITOR_LINES } else { 18 };
let mut commit_editor = Editor::new(

View File

@@ -1358,7 +1358,12 @@ impl App {
F: FnOnce(AnyView, &mut Window, &mut App) -> T,
{
self.update(|cx| {
let mut window = cx.windows.get_mut(id)?.take()?;
let mut window = cx
.windows
.get_mut(id)
.context("window not found")?
.take()
.context("window not found")?;
let root_view = window.root.clone().unwrap();
@@ -1375,14 +1380,15 @@ impl App {
true
});
} else {
cx.windows.get_mut(id)?.replace(window);
cx.windows
.get_mut(id)
.context("window not found")?
.replace(window);
}
Some(result)
Ok(result)
})
.context("window not found")
}
/// Creates an `AsyncApp`, which can be cloned and has a static lifetime
/// so it can be held across `await` points.
pub fn to_async(&self) -> AsyncApp {

View File

@@ -1,9 +1,8 @@
use std::{
alloc::{self, handle_alloc_error},
cell::Cell,
num::NonZeroUsize,
ops::{Deref, DerefMut},
ptr::{self, NonNull},
ptr,
rc::Rc,
};
@@ -31,23 +30,23 @@ impl Drop for Chunk {
fn drop(&mut self) {
unsafe {
let chunk_size = self.end.offset_from_unsigned(self.start);
// SAFETY: This succeeded during allocation.
let layout = alloc::Layout::from_size_align_unchecked(chunk_size, 1);
// this never fails as it succeeded during allocation
let layout = alloc::Layout::from_size_align(chunk_size, 1).unwrap();
alloc::dealloc(self.start, layout);
}
}
}
impl Chunk {
fn new(chunk_size: NonZeroUsize) -> Self {
fn new(chunk_size: usize) -> Self {
unsafe {
// this only fails if chunk_size is unreasonably huge
let layout = alloc::Layout::from_size_align(chunk_size.get(), 1).unwrap();
let layout = alloc::Layout::from_size_align(chunk_size, 1).unwrap();
let start = alloc::alloc(layout);
if start.is_null() {
handle_alloc_error(layout);
}
let end = start.add(chunk_size.get());
let end = start.add(chunk_size);
Self {
start,
end,
@@ -56,14 +55,14 @@ impl Chunk {
}
}
fn allocate(&mut self, layout: alloc::Layout) -> Option<NonNull<u8>> {
fn allocate(&mut self, layout: alloc::Layout) -> Option<*mut u8> {
unsafe {
let aligned = self.offset.add(self.offset.align_offset(layout.align()));
let next = aligned.add(layout.size());
if next <= self.end {
self.offset = next;
NonNull::new(aligned)
Some(aligned)
} else {
None
}
@@ -80,7 +79,7 @@ pub struct Arena {
elements: Vec<ArenaElement>,
valid: Rc<Cell<bool>>,
current_chunk_index: usize,
chunk_size: NonZeroUsize,
chunk_size: usize,
}
impl Drop for Arena {
@@ -91,7 +90,7 @@ impl Drop for Arena {
impl Arena {
pub fn new(chunk_size: usize) -> Self {
let chunk_size = NonZeroUsize::try_from(chunk_size).unwrap();
assert!(chunk_size > 0);
Self {
chunks: vec![Chunk::new(chunk_size)],
elements: Vec::new(),
@@ -102,7 +101,7 @@ impl Arena {
}
pub fn capacity(&self) -> usize {
self.chunks.len() * self.chunk_size.get()
self.chunks.len() * self.chunk_size
}
pub fn clear(&mut self) {
@@ -137,7 +136,7 @@ impl Arena {
let layout = alloc::Layout::new::<T>();
let mut current_chunk = &mut self.chunks[self.current_chunk_index];
let ptr = if let Some(ptr) = current_chunk.allocate(layout) {
ptr.as_ptr()
ptr
} else {
self.current_chunk_index += 1;
if self.current_chunk_index >= self.chunks.len() {
@@ -150,7 +149,7 @@ impl Arena {
}
current_chunk = &mut self.chunks[self.current_chunk_index];
if let Some(ptr) = current_chunk.allocate(layout) {
ptr.as_ptr()
ptr
} else {
panic!(
"Arena chunk_size of {} is too small to allocate {} bytes",

View File

@@ -39,9 +39,9 @@ use crate::{
Action, AnyWindowHandle, App, AsyncWindowContext, BackgroundExecutor, Bounds,
DEFAULT_WINDOW_SIZE, DevicePixels, DispatchEventResult, Font, FontId, FontMetrics, FontRun,
ForegroundExecutor, GlyphId, GpuSpecs, ImageSource, Keymap, LineLayout, Pixels, PlatformInput,
Point, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, Scene, ShapedGlyph,
ShapedRun, SharedString, Size, SvgRenderer, SvgSize, SystemWindowTab, Task, TaskLabel, Window,
WindowControlArea, hash, point, px, size,
Point, RenderGlyphParams, RenderImage, RenderImageParams, RenderSvgParams, ScaledPixels, Scene,
ShapedGlyph, ShapedRun, SharedString, Size, SvgRenderer, SvgSize, SystemWindowTab, Task,
TaskLabel, Window, WindowControlArea, hash, point, px, size,
};
use anyhow::Result;
use async_task::Runnable;
@@ -548,7 +548,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn set_client_inset(&self, _inset: Pixels) {}
fn gpu_specs(&self) -> Option<GpuSpecs>;
fn update_ime_position(&self, _bounds: Bounds<Pixels>);
fn update_ime_position(&self, _bounds: Bounds<ScaledPixels>);
#[cfg(any(test, feature = "test-support"))]
fn as_test(&mut self) -> Option<&mut TestWindow> {

View File

@@ -75,8 +75,8 @@ use crate::{
FileDropEvent, ForegroundExecutor, KeyDownEvent, KeyUpEvent, Keystroke, LinuxCommon,
LinuxKeyboardLayout, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent,
MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels, PlatformDisplay,
PlatformInput, PlatformKeyboardLayout, Point, SCROLL_LINES, ScrollDelta, ScrollWheelEvent,
Size, TouchPhase, WindowParams, point, px, size,
PlatformInput, PlatformKeyboardLayout, Point, SCROLL_LINES, ScaledPixels, ScrollDelta,
ScrollWheelEvent, Size, TouchPhase, WindowParams, point, px, size,
};
use crate::{
SharedString,
@@ -323,7 +323,7 @@ impl WaylandClientStatePtr {
}
}
pub fn update_ime_position(&self, bounds: Bounds<Pixels>) {
pub fn update_ime_position(&self, bounds: Bounds<ScaledPixels>) {
let client = self.get_client();
let mut state = client.borrow_mut();
if state.composing || state.text_input.is_none() || state.pre_edit_text.is_some() {

View File

@@ -25,8 +25,9 @@ use crate::scene::Scene;
use crate::{
AnyWindowHandle, Bounds, Decorations, Globals, GpuSpecs, Modifiers, Output, Pixels,
PlatformDisplay, PlatformInput, Point, PromptButton, PromptLevel, RequestFrameOptions,
ResizeEdge, Size, Tiling, WaylandClientStatePtr, WindowAppearance, WindowBackgroundAppearance,
WindowBounds, WindowControlArea, WindowControls, WindowDecorations, WindowParams, px, size,
ResizeEdge, ScaledPixels, Size, Tiling, WaylandClientStatePtr, WindowAppearance,
WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowControls, WindowDecorations,
WindowParams, px, size,
};
use crate::{
Capslock,
@@ -1077,7 +1078,7 @@ impl PlatformWindow for WaylandWindow {
}
}
fn update_ime_position(&self, bounds: Bounds<Pixels>) {
fn update_ime_position(&self, bounds: Bounds<ScaledPixels>) {
let state = self.borrow();
state.client.update_ime_position(bounds);
}

View File

@@ -62,7 +62,8 @@ use crate::{
AnyWindowHandle, Bounds, ClipboardItem, CursorStyle, DisplayId, FileDropEvent, Keystroke,
LinuxKeyboardLayout, Modifiers, ModifiersChangedEvent, MouseButton, Pixels, Platform,
PlatformDisplay, PlatformInput, PlatformKeyboardLayout, Point, RequestFrameOptions,
ScrollDelta, Size, TouchPhase, WindowParams, X11Window, modifiers_from_xinput_info, point, px,
ScaledPixels, ScrollDelta, Size, TouchPhase, WindowParams, X11Window,
modifiers_from_xinput_info, point, px,
};
/// Value for DeviceId parameters which selects all devices.
@@ -251,7 +252,7 @@ impl X11ClientStatePtr {
}
}
pub fn update_ime_position(&self, bounds: Bounds<Pixels>) {
pub fn update_ime_position(&self, bounds: Bounds<ScaledPixels>) {
let Some(client) = self.get_client() else {
return;
};
@@ -269,7 +270,6 @@ impl X11ClientStatePtr {
state.ximc = Some(ximc);
return;
};
let scaled_bounds = bounds.scale(state.scale_factor);
let ic_attributes = ximc
.build_ic_attributes()
.push(
@@ -282,8 +282,8 @@ impl X11ClientStatePtr {
b.push(
xim::AttributeName::SpotLocation,
xim::Point {
x: u32::from(scaled_bounds.origin.x + scaled_bounds.size.width) as i16,
y: u32::from(scaled_bounds.origin.y + scaled_bounds.size.height) as i16,
x: u32::from(bounds.origin.x + bounds.size.width) as i16,
y: u32::from(bounds.origin.y + bounds.size.height) as i16,
},
);
})
@@ -703,14 +703,14 @@ impl X11Client {
state.xim_handler = Some(xim_handler);
return;
};
if let Some(scaled_area) = window.get_ime_area() {
if let Some(area) = window.get_ime_area() {
ic_attributes =
ic_attributes.nested_list(xim::AttributeName::PreeditAttributes, |b| {
b.push(
xim::AttributeName::SpotLocation,
xim::Point {
x: u32::from(scaled_area.origin.x + scaled_area.size.width) as i16,
y: u32::from(scaled_area.origin.y + scaled_area.size.height) as i16,
x: u32::from(area.origin.x + area.size.width) as i16,
y: u32::from(area.origin.y + area.size.height) as i16,
},
);
});
@@ -1351,7 +1351,7 @@ impl X11Client {
drop(state);
window.handle_ime_preedit(text);
if let Some(scaled_area) = window.get_ime_area() {
if let Some(area) = window.get_ime_area() {
let ic_attributes = ximc
.build_ic_attributes()
.push(
@@ -1364,8 +1364,8 @@ impl X11Client {
b.push(
xim::AttributeName::SpotLocation,
xim::Point {
x: u32::from(scaled_area.origin.x + scaled_area.size.width) as i16,
y: u32::from(scaled_area.origin.y + scaled_area.size.height) as i16,
x: u32::from(area.origin.x + area.size.width) as i16,
y: u32::from(area.origin.y + area.size.height) as i16,
},
);
})

View File

@@ -1019,9 +1019,8 @@ impl X11WindowStatePtr {
}
}
pub fn get_ime_area(&self) -> Option<Bounds<ScaledPixels>> {
pub fn get_ime_area(&self) -> Option<Bounds<Pixels>> {
let mut state = self.state.borrow_mut();
let scale_factor = state.scale_factor;
let mut bounds: Option<Bounds<Pixels>> = None;
if let Some(mut input_handler) = state.input_handler.take() {
drop(state);
@@ -1031,7 +1030,7 @@ impl X11WindowStatePtr {
let mut state = self.state.borrow_mut();
state.input_handler = Some(input_handler);
};
bounds.map(|b| b.scale(scale_factor))
bounds
}
pub fn set_bounds(&self, bounds: Bounds<i32>) -> anyhow::Result<()> {
@@ -1619,7 +1618,7 @@ impl PlatformWindow for X11Window {
}
}
fn update_ime_position(&self, bounds: Bounds<Pixels>) {
fn update_ime_position(&self, bounds: Bounds<ScaledPixels>) {
let mut state = self.0.state.borrow_mut();
let client = state.client.clone();
drop(state);

View File

@@ -4,9 +4,10 @@ use crate::{
ForegroundExecutor, KeyDownEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton,
MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay,
PlatformInput, PlatformWindow, Point, PromptButton, PromptLevel, RequestFrameOptions,
SharedString, Size, SystemWindowTab, Timer, WindowAppearance, WindowBackgroundAppearance,
WindowBounds, WindowControlArea, WindowKind, WindowParams, dispatch_get_main_queue,
dispatch_sys::dispatch_async_f, platform::PlatformInputHandler, point, px, size,
ScaledPixels, SharedString, Size, SystemWindowTab, Timer, WindowAppearance,
WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowKind, WindowParams,
dispatch_get_main_queue, dispatch_sys::dispatch_async_f, platform::PlatformInputHandler, point,
px, size,
};
use block::ConcreteBlock;
use cocoa::{
@@ -1479,7 +1480,7 @@ impl PlatformWindow for MacWindow {
None
}
fn update_ime_position(&self, _bounds: Bounds<Pixels>) {
fn update_ime_position(&self, _bounds: Bounds<ScaledPixels>) {
let executor = self.0.lock().executor.clone();
executor
.spawn(async move {

View File

@@ -1,8 +1,8 @@
use crate::{
AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, DispatchEventResult, GpuSpecs,
Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow,
Point, PromptButton, RequestFrameOptions, Size, TestPlatform, TileId, WindowAppearance,
WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowParams,
Point, PromptButton, RequestFrameOptions, ScaledPixels, Size, TestPlatform, TileId,
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowParams,
};
use collections::HashMap;
use parking_lot::Mutex;
@@ -289,7 +289,7 @@ impl PlatformWindow for TestWindow {
unimplemented!()
}
fn update_ime_position(&self, _bounds: Bounds<Pixels>) {}
fn update_ime_position(&self, _bounds: Bounds<ScaledPixels>) {}
fn gpu_specs(&self) -> Option<GpuSpecs> {
None

View File

@@ -8,9 +8,8 @@ use windows::Win32::{
D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_11_1,
},
Direct3D11::{
D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_CREATE_DEVICE_DEBUG,
D3D11_FEATURE_D3D10_X_HARDWARE_OPTIONS, D3D11_FEATURE_DATA_D3D10_X_HARDWARE_OPTIONS,
D3D11_SDK_VERSION, D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext,
D3D11_CREATE_DEVICE_BGRA_SUPPORT, D3D11_CREATE_DEVICE_DEBUG, D3D11_SDK_VERSION,
D3D11CreateDevice, ID3D11Device, ID3D11DeviceContext,
},
Dxgi::{
CreateDXGIFactory2, DXGI_CREATE_FACTORY_DEBUG, DXGI_CREATE_FACTORY_FLAGS,
@@ -55,10 +54,12 @@ impl DirectXDevices {
let adapter =
get_adapter(&dxgi_factory, debug_layer_available).context("Getting DXGI adapter")?;
let (device, device_context) = {
let mut device: Option<ID3D11Device> = None;
let mut context: Option<ID3D11DeviceContext> = None;
let mut feature_level = D3D_FEATURE_LEVEL::default();
let device = get_device(
get_device(
&adapter,
Some(&mut device),
Some(&mut context),
Some(&mut feature_level),
debug_layer_available,
@@ -76,7 +77,7 @@ impl DirectXDevices {
}
_ => unreachable!(),
}
(device, context.unwrap())
(device.unwrap(), context.unwrap())
};
Ok(Self {
@@ -133,7 +134,7 @@ fn get_adapter(dxgi_factory: &IDXGIFactory6, debug_layer_available: bool) -> Res
}
// Check to see whether the adapter supports Direct3D 11, but don't
// create the actual device yet.
if get_device(&adapter, None, None, debug_layer_available)
if get_device(&adapter, None, None, None, debug_layer_available)
.log_err()
.is_some()
{
@@ -147,11 +148,11 @@ fn get_adapter(dxgi_factory: &IDXGIFactory6, debug_layer_available: bool) -> Res
#[inline]
fn get_device(
adapter: &IDXGIAdapter1,
device: Option<*mut Option<ID3D11Device>>,
context: Option<*mut Option<ID3D11DeviceContext>>,
feature_level: Option<*mut D3D_FEATURE_LEVEL>,
debug_layer_available: bool,
) -> Result<ID3D11Device> {
let mut device: Option<ID3D11Device> = None;
) -> Result<()> {
let device_flags = if debug_layer_available {
D3D11_CREATE_DEVICE_BGRA_SUPPORT | D3D11_CREATE_DEVICE_DEBUG
} else {
@@ -170,30 +171,10 @@ fn get_device(
D3D_FEATURE_LEVEL_10_1,
]),
D3D11_SDK_VERSION,
Some(&mut device),
device,
feature_level,
context,
)?;
}
let device = device.unwrap();
let mut data = D3D11_FEATURE_DATA_D3D10_X_HARDWARE_OPTIONS::default();
unsafe {
device
.CheckFeatureSupport(
D3D11_FEATURE_D3D10_X_HARDWARE_OPTIONS,
&mut data as *mut _ as _,
std::mem::size_of::<D3D11_FEATURE_DATA_D3D10_X_HARDWARE_OPTIONS>() as u32,
)
.context("Checking GPU device feature support")?;
}
if data
.ComputeShaders_Plus_RawAndStructuredBuffers_Via_Shader_4_x
.as_bool()
{
Ok(device)
} else {
Err(anyhow::anyhow!(
"Required feature StructuredBuffer is not supported by GPU/driver"
))
}
Ok(())
}

View File

@@ -839,7 +839,7 @@ impl PlatformWindow for WindowsWindow {
self.0.state.borrow().renderer.gpu_specs().log_err()
}
fn update_ime_position(&self, _bounds: Bounds<Pixels>) {
fn update_ime_position(&self, _bounds: Bounds<ScaledPixels>) {
// There is no such thing on Windows.
}
}

View File

@@ -99,9 +99,9 @@ impl<T: Future> Future for WithTimeout<T> {
fn poll(self: Pin<&mut Self>, cx: &mut task::Context) -> task::Poll<Self::Output> {
// SAFETY: the fields of Timeout are private and we never move the future ourselves
// And its already pinned since we are being polled (all futures need to be pinned to be polled)
let this = unsafe { &raw mut *self.get_unchecked_mut() };
let future = unsafe { Pin::new_unchecked(&mut (*this).future) };
let timer = unsafe { Pin::new_unchecked(&mut (*this).timer) };
let this = unsafe { self.get_unchecked_mut() };
let future = unsafe { Pin::new_unchecked(&mut this.future) };
let timer = unsafe { Pin::new_unchecked(&mut this.timer) };
if let task::Poll::Ready(output) = future.poll(cx) {
task::Poll::Ready(Ok(output))

View File

@@ -4096,7 +4096,9 @@ impl Window {
self.on_next_frame(|window, cx| {
if let Some(mut input_handler) = window.platform_window.take_input_handler() {
if let Some(bounds) = input_handler.selected_bounds(window, cx) {
window.platform_window.update_ime_position(bounds);
window
.platform_window
.update_ime_position(bounds.scale(window.scale_factor()));
}
window.platform_window.set_input_handler(input_handler);
}

View File

@@ -284,14 +284,6 @@ pub enum Operation {
/// The language server ID.
server_id: LanguageServerId,
},
/// An update to the line ending type of this buffer.
UpdateLineEnding {
/// The line ending type.
line_ending: LineEnding,
/// The buffer's lamport timestamp.
lamport_timestamp: clock::Lamport,
},
}
/// An event that occurs in a buffer.
@@ -1248,21 +1240,6 @@ impl Buffer {
self.syntax_map.lock().language_registry()
}
/// Assign the line ending type to the buffer.
pub fn set_line_ending(&mut self, line_ending: LineEnding, cx: &mut Context<Self>) {
self.text.set_line_ending(line_ending);
let lamport_timestamp = self.text.lamport_clock.tick();
self.send_operation(
Operation::UpdateLineEnding {
line_ending,
lamport_timestamp,
},
true,
cx,
);
}
/// Assign the buffer a new [`Capability`].
pub fn set_capability(&mut self, capability: Capability, cx: &mut Context<Self>) {
if self.capability != capability {
@@ -2580,7 +2557,7 @@ impl Buffer {
Operation::UpdateSelections { selections, .. } => selections
.iter()
.all(|s| self.can_resolve(&s.start) && self.can_resolve(&s.end)),
Operation::UpdateCompletionTriggers { .. } | Operation::UpdateLineEnding { .. } => true,
Operation::UpdateCompletionTriggers { .. } => true,
}
}
@@ -2646,13 +2623,6 @@ impl Buffer {
}
self.text.lamport_clock.observe(lamport_timestamp);
}
Operation::UpdateLineEnding {
line_ending,
lamport_timestamp,
} => {
self.text.set_line_ending(line_ending);
self.text.lamport_clock.observe(lamport_timestamp);
}
}
}
@@ -4844,9 +4814,6 @@ impl operation_queue::Operation for Operation {
}
| Operation::UpdateCompletionTriggers {
lamport_timestamp, ..
}
| Operation::UpdateLineEnding {
lamport_timestamp, ..
} => *lamport_timestamp,
}
}

View File

@@ -67,78 +67,6 @@ fn test_line_endings(cx: &mut gpui::App) {
});
}
#[gpui::test]
fn test_set_line_ending(cx: &mut TestAppContext) {
let base = cx.new(|cx| Buffer::local("one\ntwo\nthree\n", cx));
let base_replica = cx.new(|cx| {
Buffer::from_proto(1, Capability::ReadWrite, base.read(cx).to_proto(cx), None).unwrap()
});
base.update(cx, |_buffer, cx| {
cx.subscribe(&base_replica, |this, _, event, cx| {
if let BufferEvent::Operation {
operation,
is_local: true,
} = event
{
this.apply_ops([operation.clone()], cx);
}
})
.detach();
});
base_replica.update(cx, |_buffer, cx| {
cx.subscribe(&base, |this, _, event, cx| {
if let BufferEvent::Operation {
operation,
is_local: true,
} = event
{
this.apply_ops([operation.clone()], cx);
}
})
.detach();
});
// Base
base_replica.read_with(cx, |buffer, _| {
assert_eq!(buffer.line_ending(), LineEnding::Unix);
});
base.update(cx, |buffer, cx| {
assert_eq!(buffer.line_ending(), LineEnding::Unix);
buffer.set_line_ending(LineEnding::Windows, cx);
assert_eq!(buffer.line_ending(), LineEnding::Windows);
});
base_replica.read_with(cx, |buffer, _| {
assert_eq!(buffer.line_ending(), LineEnding::Windows);
});
base.update(cx, |buffer, cx| {
buffer.set_line_ending(LineEnding::Unix, cx);
assert_eq!(buffer.line_ending(), LineEnding::Unix);
});
base_replica.read_with(cx, |buffer, _| {
assert_eq!(buffer.line_ending(), LineEnding::Unix);
});
// Replica
base.read_with(cx, |buffer, _| {
assert_eq!(buffer.line_ending(), LineEnding::Unix);
});
base_replica.update(cx, |buffer, cx| {
assert_eq!(buffer.line_ending(), LineEnding::Unix);
buffer.set_line_ending(LineEnding::Windows, cx);
assert_eq!(buffer.line_ending(), LineEnding::Windows);
});
base.read_with(cx, |buffer, _| {
assert_eq!(buffer.line_ending(), LineEnding::Windows);
});
base_replica.update(cx, |buffer, cx| {
buffer.set_line_ending(LineEnding::Unix, cx);
assert_eq!(buffer.line_ending(), LineEnding::Unix);
});
base.read_with(cx, |buffer, _| {
assert_eq!(buffer.line_ending(), LineEnding::Unix);
});
}
#[gpui::test]
fn test_select_language(cx: &mut App) {
init_settings(cx, |_| {});

View File

@@ -69,7 +69,6 @@ pub use text_diff::{
use theme::SyntaxTheme;
pub use toolchain::{
LanguageToolchainStore, LocalLanguageToolchainStore, Toolchain, ToolchainList, ToolchainLister,
ToolchainMetadata, ToolchainScope,
};
use tree_sitter::{self, Query, QueryCursor, WasmStore, wasmtime};
use util::serde::default_true;
@@ -590,11 +589,6 @@ pub trait LspAdapter: 'static + Send + Sync {
"Not implemented for this adapter. This method should only be called on the default JSON language server adapter"
);
}
/// True for the extension adapter and false otherwise.
fn is_extension(&self) -> bool {
false
}
}
async fn try_fetch_server_binary<L: LspAdapter + 'static + Send + Sync + ?Sized>(
@@ -2275,10 +2269,6 @@ impl LspAdapter for FakeLspAdapter {
let label_for_completion = self.label_for_completion.as_ref()?;
label_for_completion(item, language)
}
fn is_extension(&self) -> bool {
false
}
}
fn get_capture_indices(query: &Query, captures: &mut [(&str, &mut Option<u32>)]) {

View File

@@ -374,23 +374,14 @@ impl LanguageRegistry {
pub fn register_available_lsp_adapter(
&self,
name: LanguageServerName,
adapter: Arc<dyn LspAdapter>,
load: impl Fn() -> Arc<dyn LspAdapter> + 'static + Send + Sync,
) {
let mut state = self.state.write();
if adapter.is_extension()
&& let Some(existing_adapter) = state.all_lsp_adapters.get(&name)
&& !existing_adapter.adapter.is_extension()
{
log::warn!(
"not registering extension-provided language server {name:?}, since a builtin language server exists with that name",
);
return;
}
state.available_lsp_adapters.insert(
self.state.write().available_lsp_adapters.insert(
name,
Arc::new(move || CachedLspAdapter::new(adapter.clone())),
Arc::new(move || {
let lsp_adapter = load();
CachedLspAdapter::new(lsp_adapter)
}),
);
}
@@ -405,21 +396,13 @@ impl LanguageRegistry {
Some(load_lsp_adapter())
}
pub fn register_lsp_adapter(&self, language_name: LanguageName, adapter: Arc<dyn LspAdapter>) {
let mut state = self.state.write();
if adapter.is_extension()
&& let Some(existing_adapter) = state.all_lsp_adapters.get(&adapter.name())
&& !existing_adapter.adapter.is_extension()
{
log::warn!(
"not registering extension-provided language server {:?} for language {language_name:?}, since a builtin language server exists with that name",
adapter.name(),
);
return;
}
pub fn register_lsp_adapter(
&self,
language_name: LanguageName,
adapter: Arc<dyn LspAdapter>,
) -> Arc<CachedLspAdapter> {
let cached = CachedLspAdapter::new(adapter);
let mut state = self.state.write();
state
.lsp_adapters
.entry(language_name)
@@ -428,6 +411,8 @@ impl LanguageRegistry {
state
.all_lsp_adapters
.insert(cached.name.clone(), cached.clone());
cached
}
/// Register a fake language server and adapter

Some files were not shown because too many files have changed in this diff Show More