Compare commits
1 Commits
run-comman
...
agent-serv
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4d838bb14 |
@@ -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
2
.gitattributes
vendored
@@ -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
|
||||
|
||||
@@ -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
136
Cargo.lock
generated
@@ -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",
|
||||
|
||||
31
Cargo.toml
31
Cargo.toml
@@ -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"] }
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
{
|
||||
"context": "Workspace || Editor",
|
||||
"bindings": {
|
||||
"alt-f12": "terminal_panel::Toggle",
|
||||
"alt-f12": "terminal_panel::ToggleFocus",
|
||||
"ctrl-shift-k": "git::Push"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
{
|
||||
"context": "Workspace || Editor",
|
||||
"bindings": {
|
||||
"alt-f12": "terminal_panel::Toggle",
|
||||
"alt-f12": "terminal_panel::ToggleFocus",
|
||||
"cmd-shift-k": "git::Push"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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>)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
178
crates/agent_servers/src/settings.rs
Normal file
178
crates/agent_servers/src/settings.rs
Normal 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(),
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -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()),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"))]
|
||||
{
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -20,8 +20,5 @@ strum.workspace = true
|
||||
theme.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
documented.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
@@ -227,8 +227,6 @@ pub trait Component {
|
||||
/// Example:
|
||||
///
|
||||
/// ```
|
||||
/// use documented::Documented;
|
||||
///
|
||||
/// /// This is a doc comment.
|
||||
/// #[derive(Documented)]
|
||||
/// struct MyComponent;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, _| {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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>,
|
||||
) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
});
|
||||
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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…"),
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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?)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
);
|
||||
})
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, |_| {});
|
||||
|
||||
@@ -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>)]) {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user