Compare commits

..

1 Commits

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

View File

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

View File

@@ -26,7 +26,7 @@ third-party = [
# build of remote_server should not include scap / its x11 dependency
{ name = "scap", git = "https://github.com/zed-industries/scap", rev = "808aa5c45b41e8f44729d02e38fd00a2fe2722e7" },
# build of remote_server should not need to include on libalsa through rodio
{ name = "rodio", git = "https://github.com/RustAudio/rodio", branch = "better_wav_output"},
{ name = "rodio" },
]
[final-excludes]

2
.gitattributes vendored
View File

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

View File

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

234
Cargo.lock generated
View File

@@ -1385,19 +1385,12 @@ name = "audio"
version = "0.1.0"
dependencies = [
"anyhow",
"async-tar",
"collections",
"crossbeam",
"gpui",
"libwebrtc",
"log",
"parking_lot",
"rodio",
"schemars",
"serde",
"settings",
"smol",
"thiserror 2.0.12",
"util",
"workspace-hack",
]
@@ -2358,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"
@@ -2618,7 +2624,6 @@ dependencies = [
"audio",
"client",
"collections",
"feature_flags",
"fs",
"futures 0.3.31",
"gpui",
@@ -3065,6 +3070,7 @@ dependencies = [
"clock",
"cloud_api_client",
"cloud_llm_client",
"cocoa 0.26.0",
"collections",
"credentials_provider",
"derive_more",
@@ -3077,7 +3083,6 @@ dependencies = [
"http_client_tls",
"httparse",
"log",
"objc2-foundation",
"parking_lot",
"paths",
"postage",
@@ -3510,7 +3515,6 @@ name = "component"
version = "0.1.0"
dependencies = [
"collections",
"documented",
"gpui",
"inventory",
"parking_lot",
@@ -3573,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"
@@ -4152,19 +4162,6 @@ dependencies = [
"itertools 0.10.5",
]
[[package]]
name = "crossbeam"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8"
dependencies = [
"crossbeam-channel",
"crossbeam-deque",
"crossbeam-epoch",
"crossbeam-queue",
"crossbeam-utils",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
@@ -6166,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"
@@ -6261,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"
@@ -9233,7 +9247,6 @@ dependencies = [
"anyhow",
"client",
"collections",
"command_palette_hooks",
"copilot",
"editor",
"futures 0.3.31",
@@ -9504,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"
@@ -9673,7 +9671,6 @@ dependencies = [
"scap",
"serde",
"serde_json",
"serde_urlencoded",
"settings",
"sha2",
"simplelog",
@@ -13875,15 +13872,15 @@ dependencies = [
[[package]]
name = "rodio"
version = "0.21.1"
source = "git+https://github.com/RustAudio/rodio?branch=better_wav_output#82514bd1f2c6cfd9a1a885019b26a8ffea75bc5c"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e40ecf59e742e03336be6a3d53755e789fd05a059fa22dfa0ed624722319e183"
dependencies = [
"cpal",
"dasp_sample",
"hound",
"num-rational",
"rtrb",
"symphonia",
"thiserror 2.0.12",
"tracing",
]
[[package]]
@@ -13957,12 +13954,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rtrb"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad8388ea1a9e0ea807e442e8263a699e7edcb320ecbcd21b4fa8ff859acce3ba"
[[package]]
name = "rules_library"
version = "0.1.0"
@@ -14680,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"
@@ -15297,7 +15331,6 @@ dependencies = [
"futures 0.3.31",
"indoc",
"libsqlite3-sys",
"log",
"parking_lot",
"smol",
"sqlformat",
@@ -15924,53 +15957,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9"
dependencies = [
"lazy_static",
"symphonia-bundle-flac",
"symphonia-bundle-mp3",
"symphonia-codec-aac",
"symphonia-codec-pcm",
"symphonia-codec-vorbis",
"symphonia-core",
"symphonia-format-isomp4",
"symphonia-format-ogg",
"symphonia-format-riff",
"symphonia-metadata",
]
[[package]]
name = "symphonia-bundle-flac"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72e34f34298a7308d4397a6c7fbf5b84c5d491231ce3dd379707ba673ab3bd97"
dependencies = [
"log",
"symphonia-core",
"symphonia-metadata",
"symphonia-utils-xiph",
]
[[package]]
name = "symphonia-bundle-mp3"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c01c2aae70f0f1fb096b6f0ff112a930b1fb3626178fba3ae68b09dce71706d4"
dependencies = [
"lazy_static",
"log",
"symphonia-core",
"symphonia-metadata",
]
[[package]]
name = "symphonia-codec-aac"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdbf25b545ad0d3ee3e891ea643ad115aff4ca92f6aec472086b957a58522f70"
dependencies = [
"lazy_static",
"log",
"symphonia-core",
]
[[package]]
name = "symphonia-codec-pcm"
version = "0.5.4"
@@ -15981,17 +15973,6 @@ dependencies = [
"symphonia-core",
]
[[package]]
name = "symphonia-codec-vorbis"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a98765fb46a0a6732b007f7e2870c2129b6f78d87db7987e6533c8f164a9f30"
dependencies = [
"log",
"symphonia-core",
"symphonia-utils-xiph",
]
[[package]]
name = "symphonia-core"
version = "0.5.4"
@@ -16005,31 +15986,6 @@ dependencies = [
"log",
]
[[package]]
name = "symphonia-format-isomp4"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abfdf178d697e50ce1e5d9b982ba1b94c47218e03ec35022d9f0e071a16dc844"
dependencies = [
"encoding_rs",
"log",
"symphonia-core",
"symphonia-metadata",
"symphonia-utils-xiph",
]
[[package]]
name = "symphonia-format-ogg"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ada3505789516bcf00fc1157c67729eded428b455c27ca370e41f4d785bfa931"
dependencies = [
"log",
"symphonia-core",
"symphonia-metadata",
"symphonia-utils-xiph",
]
[[package]]
name = "symphonia-format-riff"
version = "0.5.4"
@@ -16054,16 +16010,6 @@ dependencies = [
"symphonia-core",
]
[[package]]
name = "symphonia-utils-xiph"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "484472580fa49991afda5f6550ece662237b00c6f562c7d9638d1b086ed010fe"
dependencies = [
"symphonia-core",
"symphonia-metadata",
]
[[package]]
name = "syn"
version = "1.0.109"
@@ -17038,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",
@@ -19954,7 +19895,6 @@ dependencies = [
"core-foundation-sys",
"cranelift-codegen",
"crc32fast",
"crossbeam-channel",
"crossbeam-epoch",
"crossbeam-utils",
"crypto-common",
@@ -19998,7 +19938,6 @@ dependencies = [
"libsqlite3-sys",
"linux-raw-sys 0.4.15",
"linux-raw-sys 0.9.4",
"livekit-runtime",
"log",
"lyon",
"lyon_path",
@@ -20552,7 +20491,7 @@ dependencies = [
"language_tools",
"languages",
"libc",
"line_ending_selector",
"livekit_client",
"log",
"markdown",
"markdown_preview",
@@ -20703,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",
@@ -20879,7 +20818,6 @@ dependencies = [
"language_model",
"log",
"menu",
"parking_lot",
"postage",
"project",
"rand 0.9.1",
@@ -20952,7 +20890,7 @@ dependencies = [
"aes",
"byteorder",
"bzip2",
"constant_time_eq",
"constant_time_eq 0.1.5",
"crc32fast",
"crossbeam-utils",
"flate2",

View File

@@ -97,7 +97,6 @@ members = [
"crates/language_selector",
"crates/language_tools",
"crates/languages",
"crates/line_ending_selector",
"crates/livekit_api",
"crates/livekit_client",
"crates/lmstudio",
@@ -143,6 +142,7 @@ members = [
"crates/rules_library",
"crates/schema_generator",
"crates/search",
"crates/semantic_index",
"crates/semantic_version",
"crates/session",
"crates/settings",
@@ -276,7 +276,6 @@ context_server = { path = "crates/context_server" }
copilot = { path = "crates/copilot" }
crashes = { path = "crates/crashes" }
credentials_provider = { path = "crates/credentials_provider" }
crossbeam = "0.8.4"
dap = { path = "crates/dap" }
dap_adapters = { path = "crates/dap_adapters" }
db = { path = "crates/db" }
@@ -324,7 +323,6 @@ language_models = { path = "crates/language_models" }
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" }
@@ -368,11 +366,12 @@ remote_server = { path = "crates/remote_server" }
repl = { path = "crates/repl" }
reqwest_client = { path = "crates/reqwest_client" }
rich_text = { path = "crates/rich_text" }
rodio = { git = "https://github.com/RustAudio/rodio", branch = "better_wav_output"}
rodio = { version = "0.21.1", default-features = false }
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" }
@@ -538,31 +537,6 @@ nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c80421
nix = "0.29"
num-format = "0.4.4"
objc = "0.2"
objc2-foundation = { version = "0.3", default-features = false, features = [
"NSArray",
"NSAttributedString",
"NSBundle",
"NSCoder",
"NSData",
"NSDate",
"NSDictionary",
"NSEnumerator",
"NSError",
"NSGeometry",
"NSNotification",
"NSNull",
"NSObjCRuntime",
"NSObject",
"NSProcessInfo",
"NSRange",
"NSRunLoop",
"NSString",
"NSURL",
"NSUndoManager",
"NSValue",
"objc2-core-foundation",
"std"
] }
open = "5.0.0"
ordered-float = "2.1.1"
palette = { version = "0.7.5", default-features = false, features = ["std"] }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -70,7 +70,7 @@ impl AgentServerDelegate {
binary_name: SharedString,
package_name: SharedString,
entrypoint_path: PathBuf,
ignore_system_version: bool,
search_path: bool,
minimum_version: Option<Version>,
cx: &mut App,
) -> Task<Result<AgentServerCommand>> {
@@ -85,7 +85,7 @@ impl AgentServerDelegate {
let new_version_available = self.new_version_available;
cx.spawn(async move |cx| {
if !ignore_system_version {
if search_path {
if let Some(bin) = find_bin_in_path(binary_name.clone(), &project, cx).await {
return Ok(AgentServerCommand {
path: bin,

View File

@@ -3,7 +3,7 @@ use std::path::Path;
use std::rc::Rc;
use std::{any::Any, path::PathBuf};
use anyhow::Result;
use anyhow::{Result, bail};
use gpui::{App, AppContext as _, SharedString, Task};
use crate::{AgentServer, AgentServerDelegate, AllAgentServersSettings};
@@ -25,20 +25,23 @@ impl ClaudeCode {
delegate: AgentServerDelegate,
cx: &mut App,
) -> Task<Result<ClaudeCodeLoginCommand>> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).claude.clone()
let custom_command = cx.read_global(|settings: &SettingsStore, _| {
settings
.get::<AllAgentServersSettings>(None)
.get("claude")
.cloned()
});
cx.spawn(async move |cx| {
let mut command = if let Some(settings) = settings {
settings.command
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(),
true,
false,
Some("0.2.5".parse().unwrap()),
cx,
)
@@ -77,35 +80,29 @@ impl AgentServer for ClaudeCode {
let root_dir = root_dir.to_path_buf();
let fs = delegate.project().read(cx).fs().clone();
let server_name = self.name();
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).claude.clone()
let custom_command = cx.read_global(|settings: &SettingsStore, _| {
settings
.get::<AllAgentServersSettings>(None)
.get("claude")
.cloned()
});
let project = delegate.project().clone();
cx.spawn(async move |cx| {
let mut project_env = project
.update(cx, |project, cx| {
project.directory_environment(root_dir.as_path().into(), cx)
})?
.await
.unwrap_or_default();
let mut command = if let Some(settings) = settings {
settings.command
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(),
true,
false,
None,
cx,
)
})?
.await?
};
project_env.extend(command.env.take().unwrap_or_default());
command.env = Some(project_env);
command
.env

View File

@@ -1,6 +1,6 @@
use crate::{AgentServer, AgentServerDelegate};
#[cfg(test)]
use crate::{AgentServerCommand, CustomAgentServerSettings};
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};
@@ -473,15 +473,20 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
#[cfg(test)]
crate::AllAgentServersSettings::override_global(
crate::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(),
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,
);

View File

@@ -2,15 +2,13 @@ use std::rc::Rc;
use std::{any::Any, path::Path};
use crate::acp::AcpConnection;
use crate::{AgentServer, AgentServerDelegate};
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 settings::SettingsStore;
use crate::AllAgentServersSettings;
#[derive(Clone)]
pub struct Gemini;
@@ -38,33 +36,25 @@ impl AgentServer for Gemini {
let root_dir = root_dir.to_path_buf();
let fs = delegate.project().read(cx).fs().clone();
let server_name = self.name();
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).gemini.clone()
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"),
)
});
let project = delegate.project().clone();
cx.spawn(async move |cx| {
let ignore_system_version = settings
.as_ref()
.and_then(|settings| settings.ignore_system_version)
.unwrap_or(true);
let mut project_env = project
.update(cx, |project, cx| {
project.directory_environment(root_dir.as_path().into(), cx)
})?
.await
.unwrap_or_default();
let mut command = if let Some(settings) = settings
&& let Some(command) = settings.custom_command()
let mut command = if let Some(custom_command) = custom_command
{
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(),
ignore_system_version,
is_system,
Some(Self::MINIMUM_VERSION.parse().unwrap()),
cx,
)
@@ -74,12 +64,13 @@ impl AgentServer for Gemini {
if !command.args.contains(&ACP_ARG.into()) {
command.args.push(ACP_ARG.into());
}
if let Some(api_key) = cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() {
project_env
command
.env
.get_or_insert_default()
.insert("GEMINI_API_KEY".to_owned(), api_key.key);
}
project_env.extend(command.env.take().unwrap_or_default());
command.env = Some(project_env);
let root_dir_exists = fs.is_dir(&root_dir).await;
anyhow::ensure!(

View File

@@ -14,13 +14,48 @@ pub fn init(cx: &mut App) {
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, SettingsUi, SettingsKey)]
#[settings_key(key = "agent_servers")]
pub struct AllAgentServersSettings {
pub gemini: Option<BuiltinAgentServerSettings>,
pub claude: Option<CustomAgentServerSettings>,
pub struct AllAgentServersSettingsContent {
gemini: Option<GeminiSettingsContent>,
claude: Option<AgentServerCommand>,
/// Custom agent servers configured by the user
#[serde(flatten)]
pub custom: HashMap<SharedString, CustomAgentServerSettings>,
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)]
@@ -70,35 +105,40 @@ impl From<AgentServerCommand> for BuiltinAgentServerSettings {
}
#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
pub struct CustomAgentServerSettings {
pub struct AgentServerSettings {
#[serde(flatten)]
pub command: AgentServerCommand,
}
impl settings::Settings for AllAgentServersSettings {
type FileContent = Self;
type FileContent = AllAgentServersSettingsContent;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
let mut settings = AllAgentServersSettings::default();
for AllAgentServersSettings {
for AllAgentServersSettingsContent {
gemini,
claude,
custom,
} in sources.defaults_and_customizations()
{
if gemini.is_some() {
settings.gemini = gemini.clone();
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 claude.is_some() {
settings.claude = claude.clone();
if let Some(claude) = claude.clone() {
settings.insert("claude".into(), claude);
}
// Merge custom agents
for (name, config) in custom {
for (name, command) in custom {
// Skip built-in agent names to avoid conflicts
if name != "gemini" && name != "claude" {
settings.custom.insert(name.clone(), config.clone());
settings.commands.insert(name.clone(), command.clone());
}
}
}
@@ -108,3 +148,31 @@ impl settings::Settings for AllAgentServersSettings {
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use crate::{AgentServerCommand, GeminiSettingsContent};
#[test]
fn test_deserialization() {
let value = json!({
"command": "foo",
"args": ["bar"],
"ignore_system_version": false
});
let settings = serde_json::from_value::<GeminiSettingsContent>(value).unwrap();
assert_eq!(
settings,
GeminiSettingsContent {
ignore_system_version: Some(false),
inner: Some(AgentServerCommand {
path: "foo".into(),
args: vec!["bar".into()],
env: Default::default(),
})
}
)
}
}

View File

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

View File

@@ -493,13 +493,14 @@ impl MessageEditor {
let Some(entry) = self.project.read(cx).entry_for_path(&project_path, cx) else {
return Task::ready(Err(anyhow!("project entry not found")));
};
let directory_path = entry.path.clone();
let worktree_id = project_path.worktree_id;
let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx) else {
let Some(worktree) = self.project.read(cx).worktree_for_entry(entry.id, cx) else {
return Task::ready(Err(anyhow!("worktree not found")));
};
let project = self.project.clone();
cx.spawn(async move |_, cx| {
let directory_path = entry.path.clone();
let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?;
let file_paths = worktree.read_with(cx, |worktree, _cx| {
collect_files_in_path(worktree, &directory_path)
})?;

View File

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

View File

@@ -4070,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 {
@@ -4091,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;
};
@@ -4255,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())

View File

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

View File

@@ -5,7 +5,7 @@ mod tool_picker;
use std::{ops::Range, sync::Arc};
use agent_servers::{AgentServerCommand, AllAgentServersSettings, CustomAgentServerSettings};
use agent_servers::{AgentServerCommand, AllAgentServersSettings};
use agent_settings::AgentSettings;
use anyhow::Result;
use assistant_tool::{ToolSource, ToolWorkingSet};
@@ -20,6 +20,7 @@ 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,
@@ -993,15 +994,16 @@ impl AgentConfiguration {
fn render_agent_servers_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let settings = AllAgentServersSettings::get_global(cx).clone();
let user_defined_agents = settings
.custom
.iter()
.map(|(name, settings)| {
.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(),
command: settings.command.clone(),
command: command.clone(),
},
cx,
)
@@ -1280,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| {
@@ -1294,12 +1297,10 @@ async fn open_new_agent_servers_entry_in_settings_editor(
unique_server_name = Some(server_name.clone());
file.custom.insert(
server_name,
CustomAgentServerSettings {
command: AgentServerCommand {
path: "path_to_executable".into(),
args: vec![],
env: Some(HashMap::default()),
},
AgentServerCommand {
path: "path_to_executable".into(),
args: vec![],
env: Some(HashMap::default()),
},
);
}

View File

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

View File

@@ -8,6 +8,7 @@ use acp_thread::AcpThread;
use agent_servers::AgentServerCommand;
use agent2::{DbThreadMetadata, HistoryEntry};
use db::kvp::{Dismissable, KEY_VALUE_STORE};
use itertools::Itertools;
use serde::{Deserialize, Serialize};
use zed_actions::OpenBrowser;
use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent};
@@ -2683,7 +2684,13 @@ impl AgentPanel {
// Add custom agents from settings
let settings =
agent_servers::AllAgentServersSettings::get_global(cx);
for (agent_name, agent_settings) in &settings.custom {
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)
@@ -2692,7 +2699,7 @@ impl AgentPanel {
.handler({
let workspace = workspace.clone();
let agent_name = agent_name.clone();
let agent_settings = agent_settings.clone();
let command = command.clone();
move |window, cx| {
if let Some(workspace) = workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
@@ -2704,8 +2711,7 @@ impl AgentPanel {
AgentType::Custom {
name: agent_name
.clone(),
command: agent_settings
.command
command: command
.clone(),
},
window,

View File

@@ -337,7 +337,8 @@ fn update_command_palette_filter(cx: &mut App) {
];
filter.show_action_types(edit_prediction_actions.iter());
filter.show_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
filter
.show_action_types([TypeId::of::<zed_actions::OpenZedPredictOnboarding>()].iter());
}
});
}

View File

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

View File

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

View File

@@ -14,20 +14,11 @@ doctest = false
[dependencies]
anyhow.workspace = true
async-tar.workspace = true
collections.workspace = true
crossbeam.workspace = true
gpui.workspace = true
log.workspace = true
parking_lot.workspace = true
rodio = { workspace = true, features = [ "wav", "playback", "wav_output" ] }
settings.workspace = true
schemars.workspace = true
serde.workspace = true
settings.workspace = true
smol.workspace = true
thiserror.workspace = true
rodio = { workspace = true, features = [ "wav", "playback", "tracing" ] }
util.workspace = true
workspace-hack.workspace = true
[target.'cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))'.dependencies]
libwebrtc = { rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d", git = "https://github.com/zed-industries/livekit-rust-sdks" }

View File

@@ -1,56 +1,19 @@
use anyhow::{Context as _, Result};
use anyhow::{Context as _, Result, anyhow};
use collections::HashMap;
use gpui::{App, BackgroundExecutor, BorrowAppContext, Global};
#[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))]
mod non_windows_and_freebsd_deps {
pub(super) use gpui::AsyncApp;
pub(super) use libwebrtc::native::apm;
pub(super) use log::info;
pub(super) use parking_lot::Mutex;
pub(super) use rodio::cpal::Sample;
pub(super) use rodio::source::{LimitSettings, UniformSourceIterator};
pub(super) use std::sync::Arc;
}
#[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))]
use non_windows_and_freebsd_deps::*;
use rodio::{
Decoder, OutputStream, OutputStreamBuilder, Source, mixer::Mixer, nz, source::Buffered,
};
use gpui::{App, BorrowAppContext, Global};
use rodio::{Decoder, OutputStream, OutputStreamBuilder, Source, source::Buffered};
use settings::Settings;
use std::{io::Cursor, num::NonZero, path::PathBuf, sync::atomic::Ordering, time::Duration};
use std::io::Cursor;
use util::ResultExt;
mod audio_settings;
mod replays;
mod rodio_ext;
pub use audio_settings::AudioSettings;
pub use rodio_ext::RodioExt;
use crate::audio_settings::LIVE_SETTINGS;
// NOTE: We used to use WebRTC's mixer which only supported
// 16kHz, 32kHz and 48kHz. As 48 is the most common "next step up"
// for audio output devices like speakers/bluetooth, we just hard-code
// this; and downsample when we need to.
//
// Since most noise cancelling requires 16kHz we will move to
// that in the future.
pub const SAMPLE_RATE: NonZero<u32> = nz!(48000);
pub const CHANNEL_COUNT: NonZero<u16> = nz!(2);
pub const BUFFER_SIZE: usize = // echo canceller and livekit want 10ms of audio
(SAMPLE_RATE.get() as usize / 100) * CHANNEL_COUNT.get() as usize;
pub const REPLAY_DURATION: Duration = Duration::from_secs(30);
pub fn init(cx: &mut App) {
AudioSettings::register(cx);
LIVE_SETTINGS.initialize(cx);
}
#[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)]
#[derive(Copy, Clone, Eq, Hash, PartialEq)]
pub enum Sound {
Joined,
Leave,
@@ -75,152 +38,32 @@ impl Sound {
}
}
#[derive(Default)]
pub struct Audio {
output_handle: Option<OutputStream>,
output_mixer: Option<Mixer>,
#[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))]
pub echo_canceller: Arc<Mutex<apm::AudioProcessingModule>>,
source_cache: HashMap<Sound, Buffered<Decoder<Cursor<Vec<u8>>>>>,
replays: replays::Replays,
}
impl Default for Audio {
fn default() -> Self {
Self {
output_handle: Default::default(),
output_mixer: Default::default(),
#[cfg(not(any(
all(target_os = "windows", target_env = "gnu"),
target_os = "freebsd"
)))]
echo_canceller: Arc::new(Mutex::new(apm::AudioProcessingModule::new(
true, false, false, false,
))),
source_cache: Default::default(),
replays: Default::default(),
}
}
}
impl Global for Audio {}
impl Audio {
fn ensure_output_exists(&mut self) -> Result<&Mixer> {
fn ensure_output_exists(&mut self) -> Option<&OutputStream> {
if self.output_handle.is_none() {
self.output_handle = Some(
OutputStreamBuilder::open_default_stream()
.context("Could not open default output stream")?,
);
if let Some(output_handle) = &self.output_handle {
let (mixer, source) = rodio::mixer::mixer(CHANNEL_COUNT, SAMPLE_RATE);
// or the mixer will end immediately as its empty.
mixer.add(rodio::source::Zero::new(CHANNEL_COUNT, SAMPLE_RATE));
self.output_mixer = Some(mixer);
// The webrtc apm is not yet compiling for windows & freebsd
#[cfg(not(any(
any(all(target_os = "windows", target_env = "gnu")),
target_os = "freebsd"
)))]
let echo_canceller = Arc::clone(&self.echo_canceller);
#[cfg(not(any(
any(all(target_os = "windows", target_env = "gnu")),
target_os = "freebsd"
)))]
let source = source.inspect_buffer::<BUFFER_SIZE, _>(move |buffer| {
let mut buf: [i16; _] = buffer.map(|s| s.to_sample());
echo_canceller
.lock()
.process_reverse_stream(
&mut buf,
SAMPLE_RATE.get() as i32,
CHANNEL_COUNT.get().into(),
)
.expect("Audio input and output threads should not panic");
});
output_handle.mixer().add(source);
}
self.output_handle = OutputStreamBuilder::open_default_stream().log_err();
}
Ok(self
.output_mixer
.as_ref()
.expect("we only get here if opening the outputstream succeeded"))
self.output_handle.as_ref()
}
pub fn save_replays(
&self,
executor: BackgroundExecutor,
) -> gpui::Task<anyhow::Result<(PathBuf, Duration)>> {
self.replays.replays_to_tar(executor)
}
#[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))]
pub fn open_microphone(voip_parts: VoipParts) -> anyhow::Result<impl Source> {
let stream = rodio::microphone::MicrophoneBuilder::new()
.default_device()?
.default_config()?
.prefer_sample_rates([SAMPLE_RATE, SAMPLE_RATE.saturating_mul(nz!(2))])
.prefer_channel_counts([nz!(1), nz!(2)])
.prefer_buffer_sizes(512..)
.open_stream()?;
info!("Opened microphone: {:?}", stream.config());
let (replay, stream) = UniformSourceIterator::new(stream, CHANNEL_COUNT, SAMPLE_RATE)
.limit(LimitSettings::live_performance())
.process_buffer::<BUFFER_SIZE, _>(move |buffer| {
let mut int_buffer: [i16; _] = buffer.map(|s| s.to_sample());
if voip_parts
.echo_canceller
.lock()
.process_stream(
&mut int_buffer,
SAMPLE_RATE.get() as i32,
CHANNEL_COUNT.get() as i32,
)
.context("livekit audio processor error")
.log_err()
.is_some()
{
for (sample, processed) in buffer.iter_mut().zip(&int_buffer) {
*sample = (*processed).to_sample();
}
}
})
.automatic_gain_control(1.0, 4.0, 0.0, 5.0)
.periodic_access(Duration::from_millis(100), move |agc_source| {
agc_source.set_enabled(LIVE_SETTINGS.control_input_volume.load(Ordering::Relaxed));
})
.replayable(REPLAY_DURATION)?;
voip_parts
.replays
.add_voip_stream("local microphone".to_string(), replay);
Ok(stream)
}
pub fn play_voip_stream(
pub fn play_source(
source: impl rodio::Source + Send + 'static,
speaker_name: String,
is_staff: bool,
cx: &mut App,
) -> anyhow::Result<()> {
let (replay_source, source) = source
.automatic_gain_control(1.0, 4.0, 0.0, 5.0)
.periodic_access(Duration::from_millis(100), move |agc_source| {
agc_source.set_enabled(LIVE_SETTINGS.control_input_volume.load(Ordering::Relaxed));
})
.replayable(REPLAY_DURATION)
.expect("REPLAY_DURATION is longer then 100ms");
cx.update_default_global(|this: &mut Self, _cx| {
let output_mixer = this
let output_handle = this
.ensure_output_exists()
.context("Could not get output mixer")?;
output_mixer.add(source);
if is_staff {
this.replays.add_voip_stream(speaker_name, replay_source);
}
.ok_or_else(|| anyhow!("Could not open audio output"))?;
output_handle.mixer().add(source);
Ok(())
})
}
@@ -228,12 +71,8 @@ impl Audio {
pub fn play_sound(sound: Sound, cx: &mut App) {
cx.update_default_global(|this: &mut Self, cx| {
let source = this.sound_source(sound, cx).log_err()?;
let output_mixer = this
.ensure_output_exists()
.context("Could not get output mixer")
.log_err()?;
output_mixer.add(source);
let output_handle = this.ensure_output_exists()?;
output_handle.mixer().add(source);
Some(())
});
}
@@ -264,23 +103,3 @@ impl Audio {
Ok(source)
}
}
#[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))]
pub struct VoipParts {
echo_canceller: Arc<Mutex<apm::AudioProcessingModule>>,
replays: replays::Replays,
}
#[cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))]
impl VoipParts {
pub fn new(cx: &AsyncApp) -> anyhow::Result<Self> {
let (apm, replays) = cx.try_read_default_global::<Audio, _>(|audio, _| {
(Arc::clone(&audio.echo_canceller), audio.replays.clone())
})?;
Ok(Self {
echo_canceller: apm,
replays,
})
}
}

View File

@@ -1,29 +1,14 @@
use std::sync::atomic::{AtomicBool, Ordering};
use anyhow::Result;
use gpui::App;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsKey, SettingsSources, SettingsStore, SettingsUi};
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
pub struct AudioSettings {
/// Opt into the new audio system.
#[serde(rename = "experimental.rodio_audio", default)]
pub rodio_audio: bool, // default is false
/// Requires 'rodio_audio: true'
///
/// Use the new audio systems automatic gain control for your microphone.
/// This affects how loud you sound to others.
#[serde(rename = "experimental.control_input_volume", default)]
pub control_input_volume: bool,
/// Requires 'rodio_audio: true'
///
/// Use the new audio systems automatic gain control on everyone in the
/// call. This makes call members who are too quite louder and those who are
/// too loud quieter. This only affects how things sound for you.
#[serde(rename = "experimental.control_output_volume", default)]
pub control_output_volume: bool,
}
/// Configuration of audio in Zed.
@@ -31,22 +16,9 @@ pub struct AudioSettings {
#[serde(default)]
#[settings_key(key = "audio")]
pub struct AudioSettingsContent {
/// Opt into the new audio system.
/// Whether to use the experimental audio system
#[serde(rename = "experimental.rodio_audio", default)]
pub rodio_audio: bool, // default is false
/// Requires 'rodio_audio: true'
///
/// Use the new audio systems automatic gain control for your microphone.
/// This affects how loud you sound to others.
#[serde(rename = "experimental.control_input_volume", default)]
pub control_input_volume: bool,
/// Requires 'rodio_audio: true'
///
/// Use the new audio systems automatic gain control on everyone in the
/// call. This makes call members who are too quite louder and those who are
/// too loud quieter. This only affects how things sound for you.
#[serde(rename = "experimental.control_output_volume", default)]
pub control_output_volume: bool,
pub rodio_audio: bool,
}
impl Settings for AudioSettings {
@@ -58,42 +30,3 @@ impl Settings for AudioSettings {
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
}
/// See docs on [LIVE_SETTINGS]
pub(crate) struct LiveSettings {
pub(crate) control_input_volume: AtomicBool,
pub(crate) control_output_volume: AtomicBool,
}
impl LiveSettings {
pub(crate) fn initialize(&self, cx: &mut App) {
cx.observe_global::<SettingsStore>(move |cx| {
LIVE_SETTINGS.control_input_volume.store(
AudioSettings::get_global(cx).control_input_volume,
Ordering::Relaxed,
);
LIVE_SETTINGS.control_output_volume.store(
AudioSettings::get_global(cx).control_output_volume,
Ordering::Relaxed,
);
})
.detach();
let init_settings = AudioSettings::get_global(cx);
LIVE_SETTINGS
.control_input_volume
.store(init_settings.control_input_volume, Ordering::Relaxed);
LIVE_SETTINGS
.control_output_volume
.store(init_settings.control_output_volume, Ordering::Relaxed);
}
}
/// Allows access to settings from the audio thread. Updated by
/// observer of SettingsStore. Needed because audio playback and recording are
/// real time and must each run in a dedicated OS thread, therefore we can not
/// use the background executor.
pub(crate) static LIVE_SETTINGS: LiveSettings = LiveSettings {
control_input_volume: AtomicBool::new(true),
control_output_volume: AtomicBool::new(true),
};

View File

@@ -1,77 +0,0 @@
use anyhow::{Context, anyhow};
use async_tar::{Builder, Header};
use gpui::{BackgroundExecutor, Task};
use collections::HashMap;
use parking_lot::Mutex;
use rodio::Source;
use smol::fs::File;
use std::{io, path::PathBuf, sync::Arc, time::Duration};
use crate::{REPLAY_DURATION, rodio_ext::Replay};
#[derive(Default, Clone)]
pub(crate) struct Replays(Arc<Mutex<HashMap<String, Replay>>>);
impl Replays {
pub(crate) fn add_voip_stream(&self, stream_name: String, source: Replay) {
let mut map = self.0.lock();
map.retain(|_, replay| replay.source_is_active());
map.insert(stream_name, source);
}
pub(crate) fn replays_to_tar(
&self,
executor: BackgroundExecutor,
) -> Task<anyhow::Result<(PathBuf, Duration)>> {
let map = Arc::clone(&self.0);
executor.spawn(async move {
let recordings: Vec<_> = map
.lock()
.iter_mut()
.map(|(name, replay)| {
let queued = REPLAY_DURATION.min(replay.duration_ready());
(name.clone(), replay.take_duration(queued).record())
})
.collect();
let longest = recordings
.iter()
.map(|(_, r)| {
r.total_duration()
.expect("SamplesBuffer always returns a total duration")
})
.max()
.ok_or(anyhow!("There is no audio to capture"))?;
let path = std::env::current_dir()
.context("Could not get current dir")?
.join("replays.tar");
let tar = File::create(&path)
.await
.context("Could not create file for tar")?;
let mut tar = Builder::new(tar);
for (name, recording) in recordings {
let mut writer = io::Cursor::new(Vec::new());
rodio::wav_to_writer(recording, &mut writer).context("failed to encode wav")?;
let wav_data = writer.into_inner();
let path = name.replace(' ', "_") + ".wav";
let mut header = Header::new_gnu();
// rw permissions for everyone
header.set_mode(0o666);
header.set_size(wav_data.len() as u64);
tar.append_data(&mut header, path, wav_data.as_slice())
.await
.context("failed to apped wav to tar")?;
}
tar.into_inner()
.await
.context("Could not finish writing tar")?
.sync_all()
.await
.context("Could not flush tar file to disk")?;
Ok((path, longest))
})
}
}

View File

@@ -1,598 +0,0 @@
use std::{
sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
},
time::Duration,
};
use crossbeam::queue::ArrayQueue;
use rodio::{ChannelCount, Sample, SampleRate, Source};
#[derive(Debug, thiserror::Error)]
#[error("Replay duration is too short must be >= 100ms")]
pub struct ReplayDurationTooShort;
pub trait RodioExt: Source + Sized {
fn process_buffer<const N: usize, F>(self, callback: F) -> ProcessBuffer<N, Self, F>
where
F: FnMut(&mut [Sample; N]);
fn inspect_buffer<const N: usize, F>(self, callback: F) -> InspectBuffer<N, Self, F>
where
F: FnMut(&[Sample; N]);
fn replayable(
self,
duration: Duration,
) -> Result<(Replay, Replayable<Self>), ReplayDurationTooShort>;
fn take_samples(self, n: usize) -> TakeSamples<Self>;
}
impl<S: Source> RodioExt for S {
fn process_buffer<const N: usize, F>(self, callback: F) -> ProcessBuffer<N, Self, F>
where
F: FnMut(&mut [Sample; N]),
{
ProcessBuffer {
inner: self,
callback,
buffer: [0.0; N],
next: N,
}
}
fn inspect_buffer<const N: usize, F>(self, callback: F) -> InspectBuffer<N, Self, F>
where
F: FnMut(&[Sample; N]),
{
InspectBuffer {
inner: self,
callback,
buffer: [0.0; N],
free: 0,
}
}
/// Maintains a live replay with a history of at least `duration` seconds.
///
/// Note:
/// History can be 100ms longer if the source drops before or while the
/// replay is being read
///
/// # Errors
/// If duration is smaller then 100ms
fn replayable(
self,
duration: Duration,
) -> Result<(Replay, Replayable<Self>), ReplayDurationTooShort> {
if duration < Duration::from_millis(100) {
return Err(ReplayDurationTooShort);
}
let samples_per_second = self.sample_rate().get() as usize * self.channels().get() as usize;
let samples_to_queue = duration.as_secs_f64() * samples_per_second as f64;
let samples_to_queue =
(samples_to_queue as usize).next_multiple_of(self.channels().get().into());
let chunk_size =
(samples_per_second.div_ceil(10)).next_multiple_of(self.channels().get() as usize);
let chunks_to_queue = samples_to_queue.div_ceil(chunk_size);
let is_active = Arc::new(AtomicBool::new(true));
let queue = Arc::new(ReplayQueue::new(chunks_to_queue, chunk_size));
Ok((
Replay {
rx: Arc::clone(&queue),
buffer: Vec::new().into_iter(),
sleep_duration: duration / 2,
sample_rate: self.sample_rate(),
channel_count: self.channels(),
source_is_active: is_active.clone(),
},
Replayable {
tx: queue,
inner: self,
buffer: Vec::with_capacity(chunk_size),
chunk_size,
is_active,
},
))
}
fn take_samples(self, n: usize) -> TakeSamples<S> {
TakeSamples {
inner: self,
left_to_take: n,
}
}
}
pub struct TakeSamples<S> {
inner: S,
left_to_take: usize,
}
impl<S: Source> Iterator for TakeSamples<S> {
type Item = Sample;
fn next(&mut self) -> Option<Self::Item> {
if self.left_to_take == 0 {
None
} else {
self.left_to_take -= 1;
self.inner.next()
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
(0, Some(self.left_to_take))
}
}
impl<S: Source> Source for TakeSamples<S> {
fn current_span_len(&self) -> Option<usize> {
None // does not support spans
}
fn channels(&self) -> ChannelCount {
self.inner.channels()
}
fn sample_rate(&self) -> SampleRate {
self.inner.sample_rate()
}
fn total_duration(&self) -> Option<Duration> {
Some(Duration::from_secs_f64(
self.left_to_take as f64
/ self.sample_rate().get() as f64
/ self.channels().get() as f64,
))
}
}
#[derive(Debug)]
struct ReplayQueue {
inner: ArrayQueue<Vec<Sample>>,
normal_chunk_len: usize,
/// The last chunk in the queue may be smaller then
/// the normal chunk size. This is always equal to the
/// size of the last element in the queue.
/// (so normally chunk_size)
last_chunk: Mutex<Vec<Sample>>,
}
impl ReplayQueue {
fn new(queue_len: usize, chunk_size: usize) -> Self {
Self {
inner: ArrayQueue::new(queue_len),
normal_chunk_len: chunk_size,
last_chunk: Mutex::new(Vec::new()),
}
}
/// Returns the length in samples
fn len(&self) -> usize {
self.inner.len().saturating_sub(1) * self.normal_chunk_len
+ self
.last_chunk
.lock()
.expect("Self::push_last can not poison this lock")
.len()
}
fn pop(&self) -> Option<Vec<Sample>> {
self.inner.pop() // removes element that was inserted first
}
fn push_last(&self, mut samples: Vec<Sample>) {
let mut last_chunk = self
.last_chunk
.lock()
.expect("Self::len can not poison this lock");
std::mem::swap(&mut *last_chunk, &mut samples);
}
fn push_normal(&self, samples: Vec<Sample>) {
let _pushed_out_of_ringbuf = self.inner.force_push(samples);
}
}
pub struct ProcessBuffer<const N: usize, S, F>
where
S: Source + Sized,
F: FnMut(&mut [Sample; N]),
{
inner: S,
callback: F,
/// Buffer used for both input and output.
buffer: [Sample; N],
/// Next already processed sample is at this index
/// in buffer.
///
/// If this is equal to the length of the buffer we have no more samples and
/// we must get new ones and process them
next: usize,
}
impl<const N: usize, S, F> Iterator for ProcessBuffer<N, S, F>
where
S: Source + Sized,
F: FnMut(&mut [Sample; N]),
{
type Item = Sample;
fn next(&mut self) -> Option<Self::Item> {
self.next += 1;
if self.next < self.buffer.len() {
let sample = self.buffer[self.next];
return Some(sample);
}
for sample in &mut self.buffer {
*sample = self.inner.next()?
}
(self.callback)(&mut self.buffer);
self.next = 0;
Some(self.buffer[0])
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<const N: usize, S, F> Source for ProcessBuffer<N, S, F>
where
S: Source + Sized,
F: FnMut(&mut [Sample; N]),
{
fn current_span_len(&self) -> Option<usize> {
None
}
fn channels(&self) -> rodio::ChannelCount {
self.inner.channels()
}
fn sample_rate(&self) -> rodio::SampleRate {
self.inner.sample_rate()
}
fn total_duration(&self) -> Option<std::time::Duration> {
self.inner.total_duration()
}
}
pub struct InspectBuffer<const N: usize, S, F>
where
S: Source + Sized,
F: FnMut(&[Sample; N]),
{
inner: S,
callback: F,
/// Stores already emitted samples, once its full we call the callback.
buffer: [Sample; N],
/// Next free element in buffer. If this is equal to the buffer length
/// we have no more free lements.
free: usize,
}
impl<const N: usize, S, F> Iterator for InspectBuffer<N, S, F>
where
S: Source + Sized,
F: FnMut(&[Sample; N]),
{
type Item = Sample;
fn next(&mut self) -> Option<Self::Item> {
let Some(sample) = self.inner.next() else {
return None;
};
self.buffer[self.free] = sample;
self.free += 1;
if self.free == self.buffer.len() {
(self.callback)(&self.buffer);
self.free = 0
}
Some(sample)
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<const N: usize, S, F> Source for InspectBuffer<N, S, F>
where
S: Source + Sized,
F: FnMut(&[Sample; N]),
{
fn current_span_len(&self) -> Option<usize> {
None
}
fn channels(&self) -> rodio::ChannelCount {
self.inner.channels()
}
fn sample_rate(&self) -> rodio::SampleRate {
self.inner.sample_rate()
}
fn total_duration(&self) -> Option<std::time::Duration> {
self.inner.total_duration()
}
}
#[derive(Debug)]
pub struct Replayable<S: Source> {
inner: S,
buffer: Vec<Sample>,
chunk_size: usize,
tx: Arc<ReplayQueue>,
is_active: Arc<AtomicBool>,
}
impl<S: Source> Iterator for Replayable<S> {
type Item = Sample;
fn next(&mut self) -> Option<Self::Item> {
if let Some(sample) = self.inner.next() {
self.buffer.push(sample);
// If the buffer is full send it
if self.buffer.len() == self.chunk_size {
self.tx.push_normal(std::mem::take(&mut self.buffer));
}
Some(sample)
} else {
let last_chunk = std::mem::take(&mut self.buffer);
self.tx.push_last(last_chunk);
self.is_active.store(false, Ordering::Relaxed);
None
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
}
impl<S: Source> Source for Replayable<S> {
fn current_span_len(&self) -> Option<usize> {
self.inner.current_span_len()
}
fn channels(&self) -> ChannelCount {
self.inner.channels()
}
fn sample_rate(&self) -> SampleRate {
self.inner.sample_rate()
}
fn total_duration(&self) -> Option<Duration> {
self.inner.total_duration()
}
}
#[derive(Debug)]
pub struct Replay {
rx: Arc<ReplayQueue>,
buffer: std::vec::IntoIter<Sample>,
sleep_duration: Duration,
sample_rate: SampleRate,
channel_count: ChannelCount,
source_is_active: Arc<AtomicBool>,
}
impl Replay {
pub fn source_is_active(&self) -> bool {
// - source could return None and not drop
// - source could be dropped before returning None
self.source_is_active.load(Ordering::Relaxed) && Arc::strong_count(&self.rx) < 2
}
/// Duration of what is in the buffer and can be returned without blocking.
pub fn duration_ready(&self) -> Duration {
let samples_per_second = self.channels().get() as u32 * self.sample_rate().get();
let seconds_queued = self.samples_ready() as f64 / samples_per_second as f64;
Duration::from_secs_f64(seconds_queued)
}
/// Number of samples in the buffer and can be returned without blocking.
pub fn samples_ready(&self) -> usize {
self.rx.len() + self.buffer.len()
}
}
impl Iterator for Replay {
type Item = Sample;
fn next(&mut self) -> Option<Self::Item> {
if let Some(sample) = self.buffer.next() {
return Some(sample);
}
loop {
if let Some(new_buffer) = self.rx.pop() {
self.buffer = new_buffer.into_iter();
return self.buffer.next();
}
if !self.source_is_active() {
return None;
}
// The queue does not support blocking on a next item. We want this queue as it
// is quite fast and provides a fixed size. We know how many samples are in a
// buffer so if we do not get one now we must be getting one after `sleep_duration`.
std::thread::sleep(self.sleep_duration);
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
((self.rx.len() + self.buffer.len()), None)
}
}
impl Source for Replay {
fn current_span_len(&self) -> Option<usize> {
None // source is not compatible with spans
}
fn channels(&self) -> ChannelCount {
self.channel_count
}
fn sample_rate(&self) -> SampleRate {
self.sample_rate
}
fn total_duration(&self) -> Option<Duration> {
None
}
}
#[cfg(test)]
mod tests {
use rodio::{nz, static_buffer::StaticSamplesBuffer};
use super::*;
const SAMPLES: [Sample; 5] = [0.0, 1.0, 2.0, 3.0, 4.0];
fn test_source() -> StaticSamplesBuffer {
StaticSamplesBuffer::new(nz!(1), nz!(1), &SAMPLES)
}
mod process_buffer {
use super::*;
#[test]
fn callback_gets_all_samples() {
let input = test_source();
let _ = input
.process_buffer::<{ SAMPLES.len() }, _>(|buffer| assert_eq!(*buffer, SAMPLES))
.count();
}
#[test]
fn callback_modifies_yielded() {
let input = test_source();
let yielded: Vec<_> = input
.process_buffer::<{ SAMPLES.len() }, _>(|buffer| {
for sample in buffer {
*sample += 1.0;
}
})
.collect();
assert_eq!(
yielded,
SAMPLES.into_iter().map(|s| s + 1.0).collect::<Vec<_>>()
)
}
#[test]
fn source_truncates_to_whole_buffers() {
let input = test_source();
let yielded = input
.process_buffer::<3, _>(|buffer| assert_eq!(buffer, &SAMPLES[..3]))
.count();
assert_eq!(yielded, 3)
}
}
mod inspect_buffer {
use super::*;
#[test]
fn callback_gets_all_samples() {
let input = test_source();
let _ = input
.inspect_buffer::<{ SAMPLES.len() }, _>(|buffer| assert_eq!(*buffer, SAMPLES))
.count();
}
#[test]
fn source_does_not_truncate() {
let input = test_source();
let yielded = input
.inspect_buffer::<3, _>(|buffer| assert_eq!(buffer, &SAMPLES[..3]))
.count();
assert_eq!(yielded, SAMPLES.len())
}
}
mod instant_replay {
use super::*;
#[test]
fn continues_after_history() {
let input = test_source();
let (mut replay, mut source) = input
.replayable(Duration::from_secs(3))
.expect("longer then 100ms");
source.by_ref().take(3).count();
let yielded: Vec<Sample> = replay.by_ref().take(3).collect();
assert_eq!(&yielded, &SAMPLES[0..3],);
source.count();
let yielded: Vec<Sample> = replay.collect();
assert_eq!(&yielded, &SAMPLES[3..5],);
}
#[test]
fn keeps_only_latest() {
let input = test_source();
let (mut replay, mut source) = input
.replayable(Duration::from_secs(2))
.expect("longer then 100ms");
source.by_ref().take(5).count(); // get all items but do not end the source
let yielded: Vec<Sample> = replay.by_ref().take(2).collect();
assert_eq!(&yielded, &SAMPLES[3..5]);
source.count(); // exhaust source
assert_eq!(replay.next(), None);
}
#[test]
fn keeps_correct_amount_of_seconds() {
let input = StaticSamplesBuffer::new(nz!(1), nz!(16_000), &[0.0; 40_000]);
let (replay, mut source) = input
.replayable(Duration::from_secs(2))
.expect("longer then 100ms");
// exhaust but do not yet end source
source.by_ref().take(40_000).count();
// take all samples we can without blocking
let ready = replay.samples_ready();
let n_yielded = replay.take_samples(ready).count();
let max = source.sample_rate().get() * source.channels().get() as u32 * 2;
let margin = 16_000 / 10; // 100ms
assert!(n_yielded as u32 >= max - margin);
}
#[test]
fn samples_ready() {
let input = StaticSamplesBuffer::new(nz!(1), nz!(16_000), &[0.0; 40_000]);
let (mut replay, source) = input
.replayable(Duration::from_secs(2))
.expect("longer then 100ms");
assert_eq!(replay.by_ref().samples_ready(), 0);
source.take(8000).count(); // half a second
let margin = 16_000 / 10; // 100ms
let ready = replay.samples_ready();
assert!(ready >= 8000 - margin);
}
}
}

View File

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

View File

@@ -29,7 +29,6 @@ client.workspace = true
collections.workspace = true
fs.workspace = true
futures.workspace = true
feature_flags.workspace = true
gpui = { workspace = true, features = ["screen-capture"] }
language.workspace = true
log.workspace = true

View File

@@ -9,7 +9,6 @@ use client::{
proto::{self, PeerId},
};
use collections::{BTreeMap, HashMap, HashSet};
use feature_flags::FeatureFlagAppExt;
use fs::Fs;
use futures::StreamExt;
use gpui::{
@@ -1323,18 +1322,8 @@ impl Room {
return Task::ready(Err(anyhow!("live-kit was not initialized")));
};
let is_staff = cx.is_staff();
let user_name = self
.user_store
.read(cx)
.current_user()
.and_then(|user| user.name.clone())
.unwrap_or_else(|| "unknown".to_string());
cx.spawn(async move |this, cx| {
let publication = room
.publish_local_microphone_track(user_name, is_staff, cx)
.await;
let publication = room.publish_local_microphone_track(cx).await;
this.update(cx, |this, cx| {
let live_kit = this
.live_kit

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1030,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>,
@@ -1795,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()
@@ -2164,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,
@@ -2619,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?;
@@ -2657,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?;
@@ -4894,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)
@@ -5410,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(
@@ -5466,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
@@ -5593,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) => {
@@ -11413,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>();
@@ -11434,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;
@@ -17143,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()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -218,23 +218,6 @@ impl AsyncApp {
Some(read(app.try_global()?, &app))
}
/// Reads the global state of the specified type, passing it to the given callback.
/// A default value is assigned if a global of this type has not yet been assigned.
///
/// # Errors
/// If the app has ben dropped this returns an error.
pub fn try_read_default_global<G: Global + Default, R>(
&self,
read: impl FnOnce(&G, &App) -> R,
) -> Result<R> {
let app = self.app.upgrade().context("app was released")?;
let mut app = app.borrow_mut();
app.update(|cx| {
cx.default_global::<G>();
});
Ok(read(app.try_global().context("app was released")?, &app))
}
/// A convenience method for [`App::update_global`](BorrowAppContext::update_global)
/// for updating the global state of the specified type.
pub fn update_global<G: Global, R>(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -69,7 +69,6 @@ pub use text_diff::{
use theme::SyntaxTheme;
pub use toolchain::{
LanguageToolchainStore, LocalLanguageToolchainStore, Toolchain, ToolchainList, ToolchainLister,
ToolchainMetadata, ToolchainScope,
};
use tree_sitter::{self, Query, QueryCursor, WasmStore, wasmtime};
use util::serde::default_true;

View File

@@ -208,9 +208,7 @@ impl LanguageSettings {
}
/// The provider that supplies edit predictions.
#[derive(
Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema, SettingsUi,
)]
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum EditPredictionProvider {
None,
@@ -233,14 +231,13 @@ impl EditPredictionProvider {
/// The settings for edit predictions, such as [GitHub Copilot](https://github.com/features/copilot)
/// or [Supermaven](https://supermaven.com).
#[derive(Clone, Debug, Default, SettingsUi)]
#[derive(Clone, Debug, Default)]
pub struct EditPredictionSettings {
/// The provider that supplies edit predictions.
pub provider: EditPredictionProvider,
/// A list of globs representing files that edit predictions should be disabled for.
/// This list adds to a pre-existing, sensible default set of globs.
/// Any additional ones you add are combined with them.
#[settings_ui(skip)]
pub disabled_globs: Vec<DisabledGlob>,
/// Configures how edit predictions are displayed in the buffer.
pub mode: EditPredictionsMode,
@@ -272,9 +269,7 @@ pub struct DisabledGlob {
}
/// The mode in which edit predictions should be displayed.
#[derive(
Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema, SettingsUi,
)]
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum EditPredictionsMode {
/// If provider supports it, display inline when holding modifier key (e.g., alt).
@@ -287,15 +282,13 @@ pub enum EditPredictionsMode {
Eager,
}
#[derive(Clone, Debug, Default, SettingsUi)]
#[derive(Clone, Debug, Default)]
pub struct CopilotSettings {
/// HTTP/HTTPS proxy to use for Copilot.
#[settings_ui(skip)]
pub proxy: Option<String>,
/// Disable certificate verification for proxy (not recommended).
pub proxy_no_verify: Option<bool>,
/// Enterprise URI for Copilot.
#[settings_ui(skip)]
pub enterprise_uri: Option<String>,
}
@@ -304,7 +297,6 @@ pub struct CopilotSettings {
Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey,
)]
#[settings_key(None)]
#[settings_ui(group = "Default Language Settings")]
pub struct AllLanguageSettingsContent {
/// The settings for enabling/disabling features.
#[serde(default)]
@@ -317,12 +309,10 @@ pub struct AllLanguageSettingsContent {
pub defaults: LanguageSettingsContent,
/// The settings for individual languages.
#[serde(default)]
#[settings_ui(skip)]
pub languages: LanguageToSettingsMap,
/// Settings for associating file extensions and filenames
/// with languages.
#[serde(default)]
#[settings_ui(skip)]
pub file_types: HashMap<Arc<str>, Vec<String>>,
}
@@ -355,7 +345,7 @@ inventory::submit! {
}
/// Controls how completions are processed for this language.
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct CompletionSettings {
/// Controls how words are completed.
@@ -430,7 +420,7 @@ fn default_3() -> usize {
}
/// The settings for a particular language.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, SettingsUi)]
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct LanguageSettingsContent {
/// How many columns a tab should occupy.
///
@@ -627,13 +617,12 @@ pub enum RewrapBehavior {
}
/// The contents of the edit prediction settings.
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, SettingsUi)]
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct EditPredictionSettingsContent {
/// A list of globs representing files that edit predictions should be disabled for.
/// This list adds to a pre-existing, sensible default set of globs.
/// Any additional ones you add are combined with them.
#[serde(default)]
#[settings_ui(skip)]
pub disabled_globs: Option<Vec<String>>,
/// The mode used to display edit predictions in the buffer.
/// Provider support required.
@@ -648,13 +637,12 @@ pub struct EditPredictionSettingsContent {
pub enabled_in_text_threads: bool,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, SettingsUi)]
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
pub struct CopilotSettingsContent {
/// HTTP/HTTPS proxy to use for Copilot.
///
/// Default: none
#[serde(default)]
#[settings_ui(skip)]
pub proxy: Option<String>,
/// Disable certificate verification for the proxy (not recommended).
///
@@ -665,21 +653,19 @@ pub struct CopilotSettingsContent {
///
/// Default: none
#[serde(default)]
#[settings_ui(skip)]
pub enterprise_uri: Option<String>,
}
/// The settings for enabling/disabling features.
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
#[settings_ui(group = "Features")]
pub struct FeaturesContent {
/// Determines which edit prediction provider to use.
pub edit_prediction_provider: Option<EditPredictionProvider>,
}
/// Controls the soft-wrapping behavior in the editor.
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum SoftWrap {
/// Prefer a single line generally, unless an overly long line is encountered.
@@ -948,9 +934,7 @@ pub enum Formatter {
}
/// The settings for indent guides.
#[derive(
Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, SettingsUi,
)]
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct IndentGuideSettings {
/// Whether to display indent guides in the editor.
///

View File

@@ -90,15 +90,6 @@ pub fn serialize_operation(operation: &crate::Operation) -> proto::Operation {
language_server_id: server_id.to_proto(),
},
),
crate::Operation::UpdateLineEnding {
line_ending,
lamport_timestamp,
} => proto::operation::Variant::UpdateLineEnding(proto::operation::UpdateLineEnding {
replica_id: lamport_timestamp.replica_id as u32,
lamport_timestamp: lamport_timestamp.value,
line_ending: serialize_line_ending(*line_ending) as i32,
}),
}),
}
}
@@ -350,18 +341,6 @@ pub fn deserialize_operation(message: proto::Operation) -> Result<crate::Operati
server_id: LanguageServerId::from_proto(message.language_server_id),
}
}
proto::operation::Variant::UpdateLineEnding(message) => {
crate::Operation::UpdateLineEnding {
lamport_timestamp: clock::Lamport {
replica_id: message.replica_id as ReplicaId,
value: message.lamport_timestamp,
},
line_ending: deserialize_line_ending(
proto::LineEnding::from_i32(message.line_ending)
.context("missing line_ending")?,
),
}
}
},
)
}
@@ -517,10 +496,6 @@ pub fn lamport_timestamp_for_operation(operation: &proto::Operation) -> Option<c
replica_id = op.replica_id;
value = op.lamport_timestamp;
}
proto::operation::Variant::UpdateLineEnding(op) => {
replica_id = op.replica_id;
value = op.lamport_timestamp;
}
}
Some(clock::Lamport {

View File

@@ -29,40 +29,6 @@ pub struct Toolchain {
pub as_json: serde_json::Value,
}
/// Declares a scope of a toolchain added by user.
///
/// When the user adds a toolchain, we give them an option to see that toolchain in:
/// - All of their projects
/// - A project they're currently in.
/// - Only in the subproject they're currently in.
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum ToolchainScope {
Subproject(WorktreeId, Arc<Path>),
Project,
/// Available in all projects on this box. It wouldn't make sense to show suggestions across machines.
Global,
}
impl ToolchainScope {
pub fn label(&self) -> &'static str {
match self {
ToolchainScope::Subproject(_, _) => "Subproject",
ToolchainScope::Project => "Project",
ToolchainScope::Global => "Global",
}
}
pub fn description(&self) -> &'static str {
match self {
ToolchainScope::Subproject(_, _) => {
"Available only in the subproject you're currently in."
}
ToolchainScope::Project => "Available in all locations in your current project.",
ToolchainScope::Global => "Available in all of your projects on this machine.",
}
}
}
impl std::hash::Hash for Toolchain {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
let Self {
@@ -92,41 +58,23 @@ impl PartialEq for Toolchain {
}
#[async_trait]
pub trait ToolchainLister: Send + Sync + 'static {
/// List all available toolchains for a given path.
pub trait ToolchainLister: Send + Sync {
async fn list(
&self,
worktree_root: PathBuf,
subroot_relative_path: Arc<Path>,
project_env: Option<HashMap<String, String>>,
) -> ToolchainList;
/// Given a user-created toolchain, resolve lister-specific details.
/// Put another way: fill in the details of the toolchain so the user does not have to.
async fn resolve(
&self,
path: PathBuf,
project_env: Option<HashMap<String, String>>,
) -> anyhow::Result<Toolchain>;
// Returns a term which we should use in UI to refer to a toolchain.
fn term(&self) -> SharedString;
/// Returns the name of the manifest file for this toolchain.
fn manifest_name(&self) -> ManifestName;
async fn activation_script(
&self,
toolchain: &Toolchain,
shell: ShellKind,
fs: &dyn Fs,
) -> Vec<String>;
/// Returns various "static" bits of information about this toolchain lister. This function should be pure.
fn meta(&self) -> ToolchainMetadata;
}
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct ToolchainMetadata {
/// Returns a term which we should use in UI to refer to toolchains produced by a given `[ToolchainLister]`.
pub term: SharedString,
/// A user-facing placeholder describing the semantic meaning of a path to a new toolchain.
pub new_toolchain_placeholder: SharedString,
/// The name of the manifest file for this toolchain.
pub manifest_name: ManifestName,
}
#[async_trait(?Send)]

View File

@@ -11,8 +11,8 @@ use language_model::{
LanguageModelToolUseId, MessageContent, RateLimiter, Role, StopReason, TokenUsage,
};
use ollama::{
ChatMessage, ChatOptions, ChatRequest, ChatResponseDelta, KeepAlive, OllamaFunctionCall,
OllamaFunctionTool, OllamaToolCall, get_models, show_model, stream_chat_completion,
ChatMessage, ChatOptions, ChatRequest, ChatResponseDelta, KeepAlive, OllamaFunctionTool,
OllamaToolCall, get_models, show_model, stream_chat_completion,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -282,85 +282,59 @@ impl OllamaLanguageModel {
fn to_ollama_request(&self, request: LanguageModelRequest) -> ChatRequest {
let supports_vision = self.model.supports_vision.unwrap_or(false);
let mut messages = Vec::with_capacity(request.messages.len());
ChatRequest {
model: self.model.name.clone(),
messages: request
.messages
.into_iter()
.map(|msg| {
let images = if supports_vision {
msg.content
.iter()
.filter_map(|content| match content {
MessageContent::Image(image) => Some(image.source.to_string()),
_ => None,
})
.collect::<Vec<String>>()
} else {
vec![]
};
for mut msg in request.messages.into_iter() {
let images = if supports_vision {
msg.content
.iter()
.filter_map(|content| match content {
MessageContent::Image(image) => Some(image.source.to_string()),
_ => None,
})
.collect::<Vec<String>>()
} else {
vec![]
};
match msg.role {
Role::User => {
for tool_result in msg
.content
.extract_if(.., |x| matches!(x, MessageContent::ToolResult(..)))
{
match tool_result {
MessageContent::ToolResult(tool_result) => {
messages.push(ChatMessage::Tool {
tool_name: tool_result.tool_name.to_string(),
content: tool_result.content.to_str().unwrap_or("").to_string(),
})
}
_ => unreachable!("Only tool result should be extracted"),
}
}
if !msg.content.is_empty() {
messages.push(ChatMessage::User {
match msg.role {
Role::User => ChatMessage::User {
content: msg.string_contents(),
images: if images.is_empty() {
None
} else {
Some(images)
},
})
}
}
Role::Assistant => {
let content = msg.string_contents();
let mut thinking = None;
let mut tool_calls = Vec::new();
for content in msg.content.into_iter() {
match content {
MessageContent::Thinking { text, .. } if !text.is_empty() => {
thinking = Some(text)
}
MessageContent::ToolUse(tool_use) => {
tool_calls.push(OllamaToolCall::Function(OllamaFunctionCall {
name: tool_use.name.to_string(),
arguments: tool_use.input,
}));
}
_ => (),
}
}
messages.push(ChatMessage::Assistant {
content,
tool_calls: Some(tool_calls),
images: if images.is_empty() {
None
} else {
Some(images)
},
thinking,
})
}
Role::System => messages.push(ChatMessage::System {
content: msg.string_contents(),
}),
}
}
ChatRequest {
model: self.model.name.clone(),
messages,
Role::Assistant => {
let content = msg.string_contents();
let thinking =
msg.content.into_iter().find_map(|content| match content {
MessageContent::Thinking { text, .. } if !text.is_empty() => {
Some(text)
}
_ => None,
});
ChatMessage::Assistant {
content,
tool_calls: None,
images: if images.is_empty() {
None
} else {
Some(images)
},
thinking,
}
}
Role::System => ChatMessage::System {
content: msg.string_contents(),
},
}
})
.collect(),
keep_alive: self.model.keep_alive.clone().unwrap_or_default(),
stream: true,
options: Some(ChatOptions {
@@ -509,9 +483,6 @@ fn map_to_language_model_completion_events(
ChatMessage::System { content } => {
events.push(Ok(LanguageModelCompletionEvent::Text(content)));
}
ChatMessage::Tool { content, .. } => {
events.push(Ok(LanguageModelCompletionEvent::Text(content)));
}
ChatMessage::Assistant {
content,
tool_calls,

View File

@@ -56,13 +56,13 @@ pub struct OpenAiLanguageModelProvider {
pub struct State {
api_key: Option<String>,
api_key_from_env: bool,
last_api_url: String,
_subscription: Subscription,
}
const OPENAI_API_KEY_VAR: &str = "OPENAI_API_KEY";
impl State {
//
fn is_authenticated(&self) -> bool {
self.api_key.is_some()
}
@@ -104,7 +104,11 @@ impl State {
})
}
fn get_api_key(&self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
fn authenticate(&self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
if self.is_authenticated() {
return Task::ready(Ok(()));
}
let credentials_provider = <dyn CredentialsProvider>::global(cx);
let api_url = AllLanguageModelSettings::get_global(cx)
.openai
@@ -132,52 +136,14 @@ impl State {
Ok(())
})
}
fn authenticate(&self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
if self.is_authenticated() {
return Task::ready(Ok(()));
}
self.get_api_key(cx)
}
}
impl OpenAiLanguageModelProvider {
pub fn new(http_client: Arc<dyn HttpClient>, cx: &mut App) -> Self {
let initial_api_url = AllLanguageModelSettings::get_global(cx)
.openai
.api_url
.clone();
let state = cx.new(|cx| State {
api_key: None,
api_key_from_env: false,
last_api_url: initial_api_url.clone(),
_subscription: cx.observe_global::<SettingsStore>(|this: &mut State, cx| {
let current_api_url = AllLanguageModelSettings::get_global(cx)
.openai
.api_url
.clone();
if this.last_api_url != current_api_url {
this.last_api_url = current_api_url;
if !this.api_key_from_env {
this.api_key = None;
let spawn_task = cx.spawn(async move |handle, cx| {
if let Ok(task) = handle.update(cx, |this, cx| this.get_api_key(cx)) {
if let Err(_) = task.await {
handle
.update(cx, |this, _| {
this.api_key = None;
this.api_key_from_env = false;
})
.ok();
}
}
});
spawn_task.detach();
}
}
_subscription: cx.observe_global::<SettingsStore>(|_this: &mut State, cx| {
cx.notify();
}),
});
@@ -620,9 +586,7 @@ impl OpenAiEventMapper {
};
if let Some(content) = choice.delta.content.clone() {
if !content.is_empty() {
events.push(Ok(LanguageModelCompletionEvent::Text(content)));
}
events.push(Ok(LanguageModelCompletionEvent::Text(content)));
}
if let Some(tool_calls) = choice.delta.tool_calls.as_ref() {

View File

@@ -113,7 +113,11 @@ impl State {
})
}
fn get_api_key(&self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
fn authenticate(&self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
if self.is_authenticated() {
return Task::ready(Ok(()));
}
let credentials_provider = <dyn CredentialsProvider>::global(cx);
let env_var_name = self.env_var_name.clone();
let api_url = self.settings.api_url.clone();
@@ -139,14 +143,6 @@ impl State {
Ok(())
})
}
fn authenticate(&self, cx: &mut Context<Self>) -> Task<Result<(), AuthenticateError>> {
if self.is_authenticated() {
return Task::ready(Ok(()));
}
self.get_api_key(cx)
}
}
impl OpenAiCompatibleLanguageModelProvider {
@@ -164,27 +160,11 @@ impl OpenAiCompatibleLanguageModelProvider {
api_key: None,
api_key_from_env: false,
_subscription: cx.observe_global::<SettingsStore>(|this: &mut State, cx| {
let Some(settings) = resolve_settings(&this.id, cx).cloned() else {
let Some(settings) = resolve_settings(&this.id, cx) else {
return;
};
if &this.settings != &settings {
if settings.api_url != this.settings.api_url && !this.api_key_from_env {
let spawn_task = cx.spawn(async move |handle, cx| {
if let Ok(task) = handle.update(cx, |this, cx| this.get_api_key(cx)) {
if let Err(_) = task.await {
handle
.update(cx, |this, _| {
this.api_key = None;
this.api_key_from_env = false;
})
.ok();
}
}
});
spawn_task.detach();
}
this.settings = settings;
if &this.settings != settings {
this.settings = settings.clone();
cx.notify();
}
}),

View File

@@ -92,7 +92,7 @@ pub struct State {
api_key_from_env: bool,
http_client: Arc<dyn HttpClient>,
available_models: Vec<open_router::Model>,
fetch_models_task: Option<Task<Result<(), LanguageModelCompletionError>>>,
fetch_models_task: Option<Task<Result<()>>>,
settings: OpenRouterSettings,
_subscription: Subscription,
}
@@ -178,35 +178,20 @@ impl State {
})
}
fn fetch_models(
&mut self,
cx: &mut Context<Self>,
) -> Task<Result<(), LanguageModelCompletionError>> {
fn fetch_models(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
let settings = &AllLanguageModelSettings::get_global(cx).open_router;
let http_client = self.http_client.clone();
let api_url = settings.api_url.clone();
let Some(api_key) = self.api_key.clone() else {
return Task::ready(Err(LanguageModelCompletionError::NoApiKey {
provider: PROVIDER_NAME,
}));
};
cx.spawn(async move |this, cx| {
let models = list_models(http_client.as_ref(), &api_url, &api_key)
let models = list_models(http_client.as_ref(), &api_url)
.await
.map_err(|e| {
LanguageModelCompletionError::Other(anyhow::anyhow!(
"OpenRouter error: {:?}",
e
))
})?;
.map_err(|e| anyhow::anyhow!("OpenRouter error: {:?}", e))?;
this.update(cx, |this, cx| {
this.available_models = models;
cx.notify();
})
.map_err(|e| LanguageModelCompletionError::Other(e))?;
Ok(())
})
}

View File

@@ -16,7 +16,6 @@ doctest = false
anyhow.workspace = true
client.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
copilot.workspace = true
editor.workspace = true
futures.workspace = true

View File

@@ -296,7 +296,7 @@ impl LanguageServerState {
.update(cx, |workspace, cx| {
workspace
.project()
.update(cx, |project, cx| project.create_buffer(false, cx))
.update(cx, |project, cx| project.create_buffer(cx))
})
.ok()
else {

View File

@@ -325,7 +325,7 @@ impl LspLogView {
let server_info = format!(
"* Server: {NAME} (id {ID})
* Binary: {BINARY}
* Binary: {BINARY:#?}
* Registered workspace folders:
{WORKSPACE_FOLDERS}
@@ -335,10 +335,10 @@ impl LspLogView {
* Configuration: {CONFIGURATION}",
NAME = info.name,
ID = info.id,
BINARY = info
.binary
.as_ref()
.map_or_else(|| "Unknown".to_string(), |binary| format!("{binary:#?}")),
BINARY = info.binary.as_ref().map_or_else(
|| "Unknown".to_string(),
|bin| bin.path.as_path().to_string_lossy().to_string()
),
WORKSPACE_FOLDERS = info.workspace_folders.join(", "),
CAPABILITIES = serde_json::to_string_pretty(&info.capabilities)
.unwrap_or_else(|e| format!("Failed to serialize capabilities: {e}")),
@@ -990,16 +990,10 @@ impl Render for LspLogToolbarItemView {
let server_id = server.server_id;
let rpc_trace_enabled = server.rpc_trace_enabled;
let log_view = log_view.clone();
let label = match server.selected_entry {
LogKind::Rpc => RPC_MESSAGES,
LogKind::Trace => SERVER_TRACE,
LogKind::Logs => SERVER_LOGS,
LogKind::ServerInfo => SERVER_INFO,
};
PopoverMenu::new("LspViewSelector")
.anchor(Corner::TopLeft)
.trigger(
Button::new("language_server_menu_header", label)
Button::new("language_server_menu_header", server.selected_entry.label())
.icon(IconName::ChevronDown)
.icon_size(IconSize::Small)
.icon_color(Color::Muted),

View File

@@ -1,22 +1,17 @@
use command_palette_hooks::CommandPaletteFilter;
use editor::{Anchor, Editor, ExcerptId, SelectionEffects, scroll::Autoscroll};
use gpui::{
App, AppContext as _, Context, Div, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
Hsla, InteractiveElement, IntoElement, MouseButton, MouseDownEvent, MouseMoveEvent,
ParentElement, Render, ScrollStrategy, SharedString, Styled, UniformListScrollHandle,
WeakEntity, Window, actions, div, rems, uniform_list,
App, AppContext as _, Context, Div, Entity, EventEmitter, FocusHandle, Focusable, Hsla,
InteractiveElement, IntoElement, MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement,
Render, ScrollStrategy, SharedString, Styled, UniformListScrollHandle, WeakEntity, Window,
actions, div, rems, uniform_list,
};
use language::{Buffer, OwnedSyntaxLayer};
use std::{any::TypeId, mem, ops::Range};
use std::{mem, ops::Range};
use theme::ActiveTheme;
use tree_sitter::{Node, TreeCursor};
use ui::{
ButtonCommon, ButtonLike, Clickable, Color, ContextMenu, FluentBuilder as _, IconButton,
IconName, Label, LabelCommon, LabelSize, PopoverMenu, StyledExt, Tooltip, h_flex, v_flex,
};
use ui::{ButtonLike, Color, ContextMenu, Label, LabelCommon, PopoverMenu, h_flex};
use workspace::{
Event as WorkspaceEvent, SplitDirection, ToolbarItemEvent, ToolbarItemLocation,
ToolbarItemView, Workspace,
SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
item::{Item, ItemHandle},
};
@@ -24,51 +19,17 @@ actions!(
dev,
[
/// Opens the syntax tree view for the current file.
OpenSyntaxTreeView,
]
);
actions!(
syntax_tree_view,
[
/// Update the syntax tree view to show the last focused file.
UseActiveEditor
OpenSyntaxTreeView
]
);
pub fn init(cx: &mut App) {
let syntax_tree_actions = [TypeId::of::<UseActiveEditor>()];
CommandPaletteFilter::update_global(cx, |this, _| {
this.hide_action_types(&syntax_tree_actions);
});
cx.observe_new(move |workspace: &mut Workspace, _, _| {
workspace.register_action(move |workspace, _: &OpenSyntaxTreeView, window, cx| {
CommandPaletteFilter::update_global(cx, |this, _| {
this.show_action_types(&syntax_tree_actions);
});
cx.observe_new(|workspace: &mut Workspace, _, _| {
workspace.register_action(|workspace, _: &OpenSyntaxTreeView, window, cx| {
let active_item = workspace.active_item(cx);
let workspace_handle = workspace.weak_handle();
let syntax_tree_view = cx.new(|cx| {
cx.on_release(move |view: &mut SyntaxTreeView, cx| {
if view
.workspace_handle
.read_with(cx, |workspace, cx| {
workspace.item_of_type::<SyntaxTreeView>(cx).is_none()
})
.unwrap_or_default()
{
CommandPaletteFilter::update_global(cx, |this, _| {
this.hide_action_types(&syntax_tree_actions);
});
}
})
.detach();
SyntaxTreeView::new(workspace_handle, active_item, window, cx)
});
let syntax_tree_view =
cx.new(|cx| SyntaxTreeView::new(workspace_handle, active_item, window, cx));
workspace.split_item(
SplitDirection::Right,
Box::new(syntax_tree_view),
@@ -76,13 +37,6 @@ pub fn init(cx: &mut App) {
cx,
)
});
workspace.register_action(|workspace, _: &UseActiveEditor, window, cx| {
if let Some(tree_view) = workspace.item_of_type::<SyntaxTreeView>(cx) {
tree_view.update(cx, |view, cx| {
view.update_active_editor(&Default::default(), window, cx)
})
}
});
})
.detach();
}
@@ -91,9 +45,6 @@ pub struct SyntaxTreeView {
workspace_handle: WeakEntity<Workspace>,
editor: Option<EditorState>,
list_scroll_handle: UniformListScrollHandle,
/// The last active editor in the workspace. Note that this is specifically not the
/// currently shown editor.
last_active_editor: Option<Entity<Editor>>,
selected_descendant_ix: Option<usize>,
hovered_descendant_ix: Option<usize>,
focus_handle: FocusHandle,
@@ -110,14 +61,6 @@ struct EditorState {
_subscription: gpui::Subscription,
}
impl EditorState {
fn has_language(&self) -> bool {
self.active_buffer
.as_ref()
.is_some_and(|buffer| buffer.active_layer.is_some())
}
}
#[derive(Clone)]
struct BufferState {
buffer: Entity<Buffer>,
@@ -136,25 +79,17 @@ impl SyntaxTreeView {
workspace_handle: workspace_handle.clone(),
list_scroll_handle: UniformListScrollHandle::new(),
editor: None,
last_active_editor: None,
hovered_descendant_ix: None,
selected_descendant_ix: None,
focus_handle: cx.focus_handle(),
};
this.handle_item_updated(active_item, window, cx);
cx.subscribe_in(
this.workspace_updated(active_item, window, cx);
cx.observe_in(
&workspace_handle.upgrade().unwrap(),
window,
move |this, workspace, event, window, cx| match event {
WorkspaceEvent::ItemAdded { .. } | WorkspaceEvent::ActiveItemChanged => {
this.handle_item_updated(workspace.read(cx).active_item(cx), window, cx)
}
WorkspaceEvent::ItemRemoved { item_id } => {
this.handle_item_removed(item_id, window, cx);
}
_ => {}
|this, workspace, window, cx| {
this.workspace_updated(workspace.read(cx).active_item(cx), window, cx);
},
)
.detach();
@@ -162,56 +97,20 @@ impl SyntaxTreeView {
this
}
fn handle_item_updated(
fn workspace_updated(
&mut self,
active_item: Option<Box<dyn ItemHandle>>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(editor) = active_item
.filter(|item| item.item_id() != cx.entity_id())
.and_then(|item| item.act_as::<Editor>(cx))
else {
return;
};
if let Some(editor_state) = self.editor.as_ref().filter(|state| state.has_language()) {
self.last_active_editor = (editor_state.editor != editor).then_some(editor);
} else {
if let Some(item) = active_item
&& item.item_id() != cx.entity_id()
&& let Some(editor) = item.act_as::<Editor>(cx)
{
self.set_editor(editor, window, cx);
}
}
fn handle_item_removed(
&mut self,
item_id: &EntityId,
window: &mut Window,
cx: &mut Context<Self>,
) {
if self
.editor
.as_ref()
.is_some_and(|state| state.editor.entity_id() == *item_id)
{
self.editor = None;
// Try activating the last active editor if there is one
self.update_active_editor(&Default::default(), window, cx);
cx.notify();
}
}
fn update_active_editor(
&mut self,
_: &UseActiveEditor,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(editor) = self.last_active_editor.take() else {
return;
};
self.set_editor(editor, window, cx);
}
fn set_editor(&mut self, editor: Entity<Editor>, window: &mut Window, cx: &mut Context<Self>) {
if let Some(state) = &self.editor {
if state.editor == editor {
@@ -395,153 +294,101 @@ impl SyntaxTreeView {
.pl(rems(depth as f32))
.hover(|style| style.bg(colors.element_hover))
}
fn compute_items(
&mut self,
layer: &OwnedSyntaxLayer,
range: Range<usize>,
cx: &Context<Self>,
) -> Vec<Div> {
let mut items = Vec::new();
let mut cursor = layer.node().walk();
let mut descendant_ix = range.start;
cursor.goto_descendant(descendant_ix);
let mut depth = cursor.depth();
let mut visited_children = false;
while descendant_ix < range.end {
if visited_children {
if cursor.goto_next_sibling() {
visited_children = false;
} else if cursor.goto_parent() {
depth -= 1;
} else {
break;
}
} else {
items.push(
Self::render_node(
&cursor,
depth,
Some(descendant_ix) == self.selected_descendant_ix,
cx,
)
.on_mouse_down(
MouseButton::Left,
cx.listener(move |tree_view, _: &MouseDownEvent, window, cx| {
tree_view.update_editor_with_range_for_descendant_ix(
descendant_ix,
window,
cx,
|editor, mut range, window, cx| {
// Put the cursor at the beginning of the node.
mem::swap(&mut range.start, &mut range.end);
editor.change_selections(
SelectionEffects::scroll(Autoscroll::newest()),
window,
cx,
|selections| {
selections.select_ranges(vec![range]);
},
);
},
);
}),
)
.on_mouse_move(cx.listener(
move |tree_view, _: &MouseMoveEvent, window, cx| {
if tree_view.hovered_descendant_ix != Some(descendant_ix) {
tree_view.hovered_descendant_ix = Some(descendant_ix);
tree_view.update_editor_with_range_for_descendant_ix(
descendant_ix,
window,
cx,
|editor, range, _, cx| {
editor.clear_background_highlights::<Self>(cx);
editor.highlight_background::<Self>(
&[range],
|theme| {
theme
.colors()
.editor_document_highlight_write_background
},
cx,
);
},
);
cx.notify();
}
},
)),
);
descendant_ix += 1;
if cursor.goto_first_child() {
depth += 1;
} else {
visited_children = true;
}
}
}
items
}
}
impl Render for SyntaxTreeView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.flex_1()
.bg(cx.theme().colors().editor_background)
.map(|this| {
let editor_state = self.editor.as_ref();
let mut rendered = div().flex_1().bg(cx.theme().colors().editor_background);
if let Some(layer) = editor_state
.and_then(|editor| editor.active_buffer.as_ref())
.and_then(|buffer| buffer.active_layer.as_ref())
{
let layer = layer.clone();
this.child(
uniform_list(
"SyntaxTreeView",
layer.node().descendant_count(),
cx.processor(move |this, range: Range<usize>, _, cx| {
this.compute_items(&layer, range, cx)
}),
)
.size_full()
.track_scroll(self.list_scroll_handle.clone())
.text_bg(cx.theme().colors().background)
.into_any_element(),
)
} else {
let inner_content = v_flex()
.items_center()
.text_center()
.gap_2()
.max_w_3_5()
.map(|this| {
if editor_state.is_some_and(|state| !state.has_language()) {
this.child(Label::new("Current editor has no associated language"))
.child(
Label::new(concat!(
"Try assigning a language or",
"switching to a different buffer"
))
.size(LabelSize::Small),
)
if let Some(layer) = self
.editor
.as_ref()
.and_then(|editor| editor.active_buffer.as_ref())
.and_then(|buffer| buffer.active_layer.as_ref())
{
let layer = layer.clone();
rendered = rendered.child(uniform_list(
"SyntaxTreeView",
layer.node().descendant_count(),
cx.processor(move |this, range: Range<usize>, _, cx| {
let mut items = Vec::new();
let mut cursor = layer.node().walk();
let mut descendant_ix = range.start;
cursor.goto_descendant(descendant_ix);
let mut depth = cursor.depth();
let mut visited_children = false;
while descendant_ix < range.end {
if visited_children {
if cursor.goto_next_sibling() {
visited_children = false;
} else if cursor.goto_parent() {
depth -= 1;
} else {
this.child(Label::new("Not attached to an editor")).child(
Label::new("Focus an editor to show a new tree view")
.size(LabelSize::Small),
)
break;
}
});
} else {
items.push(
Self::render_node(
&cursor,
depth,
Some(descendant_ix) == this.selected_descendant_ix,
cx,
)
.on_mouse_down(
MouseButton::Left,
cx.listener(move |tree_view, _: &MouseDownEvent, window, cx| {
tree_view.update_editor_with_range_for_descendant_ix(
descendant_ix,
window, cx,
|editor, mut range, window, cx| {
// Put the cursor at the beginning of the node.
mem::swap(&mut range.start, &mut range.end);
this.h_flex()
.size_full()
.justify_center()
.child(inner_content)
}
})
editor.change_selections(
SelectionEffects::scroll(Autoscroll::newest()),
window, cx,
|selections| {
selections.select_ranges(vec![range]);
},
);
},
);
}),
)
.on_mouse_move(cx.listener(
move |tree_view, _: &MouseMoveEvent, window, cx| {
if tree_view.hovered_descendant_ix != Some(descendant_ix) {
tree_view.hovered_descendant_ix = Some(descendant_ix);
tree_view.update_editor_with_range_for_descendant_ix(descendant_ix, window, cx, |editor, range, _, cx| {
editor.clear_background_highlights::<Self>( cx);
editor.highlight_background::<Self>(
&[range],
|theme| theme.colors().editor_document_highlight_write_background,
cx,
);
});
cx.notify();
}
},
)),
);
descendant_ix += 1;
if cursor.goto_first_child() {
depth += 1;
} else {
visited_children = true;
}
}
}
items
}),
)
.size_full()
.track_scroll(self.list_scroll_handle.clone())
.text_bg(cx.theme().colors().background).into_any_element());
}
rendered
}
}
@@ -659,26 +506,6 @@ impl SyntaxTreeToolbarItemView {
.child(Label::new(active_layer.language.name()))
.child(Label::new(format_node_range(active_layer.node())))
}
fn render_update_button(&mut self, cx: &mut Context<Self>) -> Option<IconButton> {
self.tree_view.as_ref().and_then(|view| {
view.update(cx, |view, cx| {
view.last_active_editor.as_ref().map(|editor| {
IconButton::new("syntax-view-update", IconName::RotateCw)
.tooltip({
let active_tab_name = editor.read_with(cx, |editor, cx| {
editor.tab_content_text(Default::default(), cx)
});
Tooltip::text(format!("Update view to '{active_tab_name}'"))
})
.on_click(cx.listener(|this, _, window, cx| {
this.update_active_editor(&Default::default(), window, cx);
}))
})
})
})
}
}
fn format_node_range(node: Node) -> String {
@@ -695,10 +522,8 @@ fn format_node_range(node: Node) -> String {
impl Render for SyntaxTreeToolbarItemView {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
h_flex()
.gap_1()
.children(self.render_menu(cx))
.children(self.render_update_button(cx))
self.render_menu(cx)
.unwrap_or_else(|| PopoverMenu::new("Empty Syntax Tree"))
}
}

View File

@@ -4,8 +4,6 @@
(field_identifier) @property
(package_identifier) @namespace
(label_name) @label
(keyed_element
.
(literal_element

View File

@@ -97,7 +97,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
let python_context_provider = Arc::new(python::PythonContextProvider);
let python_lsp_adapter = Arc::new(python::PythonLspAdapter::new(node.clone()));
let basedpyright_lsp_adapter = Arc::new(BasedPyrightLspAdapter::new());
let python_toolchain_provider = Arc::new(python::PythonToolchainProvider);
let python_toolchain_provider = Arc::new(python::PythonToolchainProvider::default());
let rust_context_provider = Arc::new(rust::RustContextProvider);
let rust_lsp_adapter = Arc::new(rust::RustLspAdapter);
let tailwind_adapter = Arc::new(tailwind::TailwindLspAdapter::new(node.clone()));

View File

@@ -5,19 +5,19 @@ use collections::HashMap;
use futures::AsyncBufReadExt;
use gpui::{App, Task};
use gpui::{AsyncApp, SharedString};
use language::Toolchain;
use language::ToolchainList;
use language::ToolchainLister;
use language::language_settings::language_settings;
use language::{ContextLocation, LanguageToolchainStore};
use language::{ContextProvider, LspAdapter, LspAdapterDelegate};
use language::{LanguageName, ManifestName, ManifestProvider, ManifestQuery};
use language::{Toolchain, ToolchainMetadata};
use lsp::LanguageServerBinary;
use lsp::LanguageServerName;
use node_runtime::{NodeRuntime, VersionStrategy};
use pet_core::Configuration;
use pet_core::os_environment::Environment;
use pet_core::python_environment::{PythonEnvironment, PythonEnvironmentKind};
use pet_core::python_environment::PythonEnvironmentKind;
use project::Fs;
use project::lsp_store::language_server_settings;
use serde_json::{Value, json};
@@ -688,7 +688,17 @@ fn python_env_kind_display(k: &PythonEnvironmentKind) -> &'static str {
}
}
pub(crate) struct PythonToolchainProvider;
pub(crate) struct PythonToolchainProvider {
term: SharedString,
}
impl Default for PythonToolchainProvider {
fn default() -> Self {
Self {
term: SharedString::new_static("Virtual Environment"),
}
}
}
static ENV_PRIORITY_LIST: &[PythonEnvironmentKind] = &[
// Prioritize non-Conda environments.
@@ -734,6 +744,9 @@ async fn get_worktree_venv_declaration(worktree_root: &Path) -> Option<String> {
#[async_trait]
impl ToolchainLister for PythonToolchainProvider {
fn manifest_name(&self) -> language::ManifestName {
ManifestName::from(SharedString::new_static("pyproject.toml"))
}
async fn list(
&self,
worktree_root: PathBuf,
@@ -834,7 +847,32 @@ impl ToolchainLister for PythonToolchainProvider {
let mut toolchains: Vec<_> = toolchains
.into_iter()
.filter_map(venv_to_toolchain)
.filter_map(|toolchain| {
let mut name = String::from("Python");
if let Some(version) = &toolchain.version {
_ = write!(name, " {version}");
}
let name_and_kind = match (&toolchain.name, &toolchain.kind) {
(Some(name), Some(kind)) => {
Some(format!("({name}; {})", python_env_kind_display(kind)))
}
(Some(name), None) => Some(format!("({name})")),
(None, Some(kind)) => Some(format!("({})", python_env_kind_display(kind))),
(None, None) => None,
};
if let Some(nk) = name_and_kind {
_ = write!(name, " {nk}");
}
Some(Toolchain {
name: name.into(),
path: toolchain.executable.as_ref()?.to_str()?.to_owned().into(),
language_name: LanguageName::new("Python"),
as_json: serde_json::to_value(toolchain.clone()).ok()?,
})
})
.collect();
toolchains.dedup();
ToolchainList {
@@ -843,34 +881,9 @@ impl ToolchainLister for PythonToolchainProvider {
groups: Default::default(),
}
}
fn meta(&self) -> ToolchainMetadata {
ToolchainMetadata {
term: SharedString::new_static("Virtual Environment"),
new_toolchain_placeholder: SharedString::new_static(
"A path to the python3 executable within a virtual environment, or path to virtual environment itself",
),
manifest_name: ManifestName::from(SharedString::new_static("pyproject.toml")),
}
fn term(&self) -> SharedString {
self.term.clone()
}
async fn resolve(
&self,
path: PathBuf,
env: Option<HashMap<String, String>>,
) -> anyhow::Result<Toolchain> {
let env = env.unwrap_or_default();
let environment = EnvironmentApi::from_env(&env);
let locators = pet::locators::create_locators(
Arc::new(pet_conda::Conda::from(&environment)),
Arc::new(pet_poetry::Poetry::from(&environment)),
&environment,
);
let toolchain = pet::resolve::resolve_environment(&path, &locators, &environment)
.context("Could not find a virtual environment in provided path")?;
let venv = toolchain.resolved.unwrap_or(toolchain.discovered);
venv_to_toolchain(venv).context("Could not convert a venv into a toolchain")
}
async fn activation_script(
&self,
toolchain: &Toolchain,
@@ -889,13 +902,6 @@ impl ToolchainLister for PythonToolchainProvider {
let env = toolchain.name.as_deref().unwrap_or("default");
activation_script.push(format!("pixi shell -e {env}"))
}
Some(PythonEnvironmentKind::Conda) => {
if let Some(name) = &toolchain.name {
activation_script.push(format!("conda activate {name}"));
} else {
activation_script.push("conda activate".to_string());
}
}
Some(PythonEnvironmentKind::Venv | PythonEnvironmentKind::VirtualEnv) => {
if let Some(prefix) = &toolchain.prefix {
let activate_keyword = match shell {
@@ -943,31 +949,6 @@ impl ToolchainLister for PythonToolchainProvider {
}
}
fn venv_to_toolchain(venv: PythonEnvironment) -> Option<Toolchain> {
let mut name = String::from("Python");
if let Some(ref version) = venv.version {
_ = write!(name, " {version}");
}
let name_and_kind = match (&venv.name, &venv.kind) {
(Some(name), Some(kind)) => Some(format!("({name}; {})", python_env_kind_display(kind))),
(Some(name), None) => Some(format!("({name})")),
(None, Some(kind)) => Some(format!("({})", python_env_kind_display(kind))),
(None, None) => None,
};
if let Some(nk) = name_and_kind {
_ = write!(name, " {nk}");
}
Some(Toolchain {
name: name.into(),
path: venv.executable.as_ref()?.to_str()?.to_owned().into(),
language_name: LanguageName::new("Python"),
as_json: serde_json::to_value(venv).ok()?,
})
}
pub struct EnvironmentApi<'a> {
global_search_locations: Arc<Mutex<Vec<PathBuf>>>,
project_env: &'a HashMap<String, String>,

View File

@@ -5,7 +5,6 @@
(primitive_type) @type.builtin
(self) @variable.special
(field_identifier) @property
(shorthand_field_identifier) @property
(trait_item name: (type_identifier) @type.interface)
(impl_item trait: (type_identifier) @type.interface)

View File

@@ -1,24 +0,0 @@
[package]
name = "line_ending_selector"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/line_ending_selector.rs"
doctest = false
[dependencies]
editor.workspace = true
gpui.workspace = true
language.workspace = true
picker.workspace = true
project.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
workspace-hack.workspace = true

View File

@@ -1,192 +0,0 @@
use editor::Editor;
use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity, actions};
use language::{Buffer, LineEnding};
use picker::{Picker, PickerDelegate};
use project::Project;
use std::sync::Arc;
use ui::{ListItem, ListItemSpacing, prelude::*};
use util::ResultExt;
use workspace::ModalView;
actions!(
line_ending,
[
/// Toggles the line ending selector modal.
Toggle
]
);
pub fn init(cx: &mut App) {
cx.observe_new(LineEndingSelector::register).detach();
}
pub struct LineEndingSelector {
picker: Entity<Picker<LineEndingSelectorDelegate>>,
}
impl LineEndingSelector {
fn register(editor: &mut Editor, _window: Option<&mut Window>, cx: &mut Context<Editor>) {
let editor_handle = cx.weak_entity();
editor
.register_action(move |_: &Toggle, window, cx| {
Self::toggle(&editor_handle, window, cx);
})
.detach();
}
fn toggle(editor: &WeakEntity<Editor>, window: &mut Window, cx: &mut App) {
let Some((workspace, buffer)) = editor
.update(cx, |editor, cx| {
Some((editor.workspace()?, editor.active_excerpt(cx)?.1))
})
.ok()
.flatten()
else {
return;
};
workspace.update(cx, |workspace, cx| {
let project = workspace.project().clone();
workspace.toggle_modal(window, cx, move |window, cx| {
LineEndingSelector::new(buffer, project, window, cx)
});
})
}
fn new(
buffer: Entity<Buffer>,
project: Entity<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let line_ending = buffer.read(cx).line_ending();
let delegate =
LineEndingSelectorDelegate::new(cx.entity().downgrade(), buffer, project, line_ending);
let picker = cx.new(|cx| Picker::nonsearchable_uniform_list(delegate, window, cx));
Self { picker }
}
}
impl Render for LineEndingSelector {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
v_flex().w(rems(34.)).child(self.picker.clone())
}
}
impl Focusable for LineEndingSelector {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl EventEmitter<DismissEvent> for LineEndingSelector {}
impl ModalView for LineEndingSelector {}
struct LineEndingSelectorDelegate {
line_ending_selector: WeakEntity<LineEndingSelector>,
buffer: Entity<Buffer>,
project: Entity<Project>,
line_ending: LineEnding,
matches: Vec<LineEnding>,
selected_index: usize,
}
impl LineEndingSelectorDelegate {
fn new(
line_ending_selector: WeakEntity<LineEndingSelector>,
buffer: Entity<Buffer>,
project: Entity<Project>,
line_ending: LineEnding,
) -> Self {
Self {
line_ending_selector,
buffer,
project,
line_ending,
matches: vec![LineEnding::Unix, LineEnding::Windows],
selected_index: 0,
}
}
}
impl PickerDelegate for LineEndingSelectorDelegate {
type ListItem = ListItem;
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Select a line ending…".into()
}
fn match_count(&self) -> usize {
self.matches.len()
}
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
if let Some(line_ending) = self.matches.get(self.selected_index) {
self.buffer.update(cx, |this, cx| {
this.set_line_ending(*line_ending, cx);
});
let buffer = self.buffer.clone();
let project = self.project.clone();
cx.defer(move |cx| {
project.update(cx, |this, cx| {
this.save_buffer(buffer, cx).detach();
});
});
}
self.dismissed(window, cx);
}
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
self.line_ending_selector
.update(cx, |_, cx| cx.emit(DismissEvent))
.log_err();
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(
&mut self,
ix: usize,
_window: &mut Window,
_: &mut Context<Picker<Self>>,
) {
self.selected_index = ix;
}
fn update_matches(
&mut self,
_query: String,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> gpui::Task<()> {
return Task::ready(());
}
fn render_match(
&self,
ix: usize,
selected: bool,
_: &mut Window,
_: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let line_ending = self.matches[ix];
let label = match line_ending {
LineEnding::Unix => "LF",
LineEnding::Windows => "CRLF",
};
let mut list_item = ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(Label::new(label));
if self.line_ending == line_ending {
list_item = list_item.end_slot(Icon::new(IconName::Check).color(Color::Muted));
}
Some(list_item)
}
}

View File

@@ -22,10 +22,10 @@ test-support = ["collections/test-support", "gpui/test-support"]
[dependencies]
anyhow.workspace = true
async-trait.workspace = true
audio.workspace = true
collections.workspace = true
cpal.workspace = true
futures.workspace = true
audio.workspace = true
gpui = { workspace = true, features = ["screen-capture", "x11", "wayland", "windows-manifest"] }
gpui_tokio.workspace = true
http_client_tls.workspace = true
@@ -35,15 +35,14 @@ log.workspace = true
nanoid.workspace = true
parking_lot.workspace = true
postage.workspace = true
rodio = { workspace = true, features = ["wav_output", "recording"] }
serde.workspace = true
serde_urlencoded.workspace = true
settings.workspace = true
smallvec.workspace = true
settings.workspace = true
tokio-tungstenite.workspace = true
util.workspace = true
workspace-hack.workspace = true
rodio = { workspace = true, features = ["wav_output"] }
[target.'cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))'.dependencies]
libwebrtc = { rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d", git = "https://github.com/zed-industries/livekit-rust-sdks" }
livekit = { rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d", git = "https://github.com/zed-industries/livekit-rust-sdks", features = [

View File

@@ -255,10 +255,7 @@ impl LivekitWindow {
} else {
let room = self.room.clone();
cx.spawn_in(window, async move |this, cx| {
let (publication, stream) = room
.publish_local_microphone_track("test_user".to_string(), false, cx)
.await
.unwrap();
let (publication, stream) = room.publish_local_microphone_track(cx).await.unwrap();
this.update(cx, |this, cx| {
this.microphone_track = Some(publication);
this.microphone_stream = Some(stream);

View File

@@ -97,13 +97,9 @@ impl Room {
pub async fn publish_local_microphone_track(
&self,
user_name: String,
is_staff: bool,
cx: &mut AsyncApp,
) -> Result<(LocalTrackPublication, playback::AudioStream)> {
let (track, stream) = self
.playback
.capture_local_microphone_track(user_name, is_staff, &cx)?;
let (track, stream) = self.playback.capture_local_microphone_track()?;
let publication = self
.local_participant()
.publish_track(
@@ -133,7 +129,7 @@ impl Room {
cx: &mut App,
) -> Result<playback::AudioStream> {
if AudioSettings::get_global(cx).rodio_audio {
info!("Using experimental.rodio_audio audio pipeline for output");
info!("Using experimental.rodio_audio audio pipeline");
playback::play_remote_audio_track(&track.0, cx)
} else {
Ok(self.playback.play_remote_audio_track(&track.0))

View File

@@ -1,12 +1,10 @@
use anyhow::{Context as _, Result};
use audio::{AudioSettings, CHANNEL_COUNT, SAMPLE_RATE};
use cpal::traits::{DeviceTrait, StreamTrait as _};
use futures::channel::mpsc::UnboundedSender;
use futures::{Stream, StreamExt as _};
use gpui::{
AsyncApp, BackgroundExecutor, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
Task,
BackgroundExecutor, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, Task,
};
use libwebrtc::native::{apm, audio_mixer, audio_resampler};
use livekit::track;
@@ -19,11 +17,8 @@ use livekit::webrtc::{
video_source::{RtcVideoSource, VideoResolution, native::NativeVideoSource},
video_stream::native::NativeVideoStream,
};
use log::info;
use parking_lot::Mutex;
use rodio::Source;
use serde::{Deserialize, Serialize};
use settings::Settings;
use std::cell::RefCell;
use std::sync::Weak;
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
@@ -41,28 +36,27 @@ pub(crate) struct AudioStack {
next_ssrc: AtomicI32,
}
// NOTE: We use WebRTC's mixer which only supports
// 16kHz, 32kHz and 48kHz. As 48 is the most common "next step up"
// for audio output devices like speakers/bluetooth, we just hard-code
// this; and downsample when we need to.
const SAMPLE_RATE: u32 = 48000;
const NUM_CHANNELS: u32 = 2;
pub(crate) fn play_remote_audio_track(
track: &livekit::track::RemoteAudioTrack,
cx: &mut gpui::App,
) -> Result<AudioStream> {
let stop_handle = Arc::new(AtomicBool::new(false));
let stop_handle_clone = stop_handle.clone();
let stream = source::LiveKitStream::new(cx.background_executor(), track);
let stream = stream
let stream = source::LiveKitStream::new(cx.background_executor(), track)
.stoppable()
.periodic_access(Duration::from_millis(50), move |s| {
if stop_handle.load(Ordering::Relaxed) {
s.stop();
}
});
let speaker: Speaker = serde_urlencoded::from_str(&track.name()).unwrap_or_else(|_| Speaker {
name: track.name(),
is_staff: false,
});
audio::Audio::play_voip_stream(stream, speaker.name, speaker.is_staff, cx)
.context("Could not play audio")?;
audio::Audio::play_source(stream, cx).context("Could not play audio")?;
let on_drop = util::defer(move || {
stop_handle_clone.store(true, Ordering::Relaxed);
@@ -96,8 +90,8 @@ impl AudioStack {
let next_ssrc = self.next_ssrc.fetch_add(1, Ordering::Relaxed);
let source = AudioMixerSource {
ssrc: next_ssrc,
sample_rate: SAMPLE_RATE.get(),
num_channels: CHANNEL_COUNT.get() as u32,
sample_rate: SAMPLE_RATE,
num_channels: NUM_CHANNELS,
buffer: Arc::default(),
};
self.mixer.lock().add_source(source.clone());
@@ -137,7 +131,7 @@ impl AudioStack {
let apm = self.apm.clone();
let mixer = self.mixer.clone();
async move {
Self::play_output(apm, mixer, SAMPLE_RATE.get(), CHANNEL_COUNT.get().into())
Self::play_output(apm, mixer, SAMPLE_RATE, NUM_CHANNELS)
.await
.log_err();
}
@@ -148,26 +142,17 @@ impl AudioStack {
pub(crate) fn capture_local_microphone_track(
&self,
user_name: String,
is_staff: bool,
cx: &AsyncApp,
) -> Result<(crate::LocalAudioTrack, AudioStream)> {
let source = NativeAudioSource::new(
// n.b. this struct's options are always ignored, noise cancellation is provided by apm.
AudioSourceOptions::default(),
SAMPLE_RATE.get(),
CHANNEL_COUNT.get().into(),
SAMPLE_RATE,
NUM_CHANNELS,
10,
);
let track_name = serde_urlencoded::to_string(Speaker {
name: user_name,
is_staff,
})
.context("Could not encode user information in track name")?;
let track = track::LocalAudioTrack::create_audio_track(
&track_name,
"microphone",
RtcAudioSource::Native(source.clone()),
);
@@ -181,26 +166,9 @@ impl AudioStack {
}
}
});
let rodio_pipeline =
AudioSettings::try_read_global(cx, |setting| setting.rodio_audio).unwrap_or_default();
let capture_task = if rodio_pipeline {
info!("Using experimental.rodio_audio audio pipeline");
let voip_parts = audio::VoipParts::new(cx)?;
// Audio needs to run real-time and should never be paused. That is why we are using a
// normal std::thread and not a background task
thread::spawn(move || {
// microphone is non send on mac
let microphone = audio::Audio::open_microphone(voip_parts)?;
send_to_livekit(frame_tx, microphone);
Ok::<(), anyhow::Error>(())
});
Task::ready(Ok(()))
} else {
self.executor.spawn(async move {
Self::capture_input(apm, frame_tx, SAMPLE_RATE.get(), CHANNEL_COUNT.get().into())
.await
})
};
let capture_task = self.executor.spawn(async move {
Self::capture_input(apm, frame_tx, SAMPLE_RATE, NUM_CHANNELS).await
});
let on_drop = util::defer(|| {
drop(transmit_task);
@@ -378,36 +346,6 @@ impl AudioStack {
}
}
#[derive(Serialize, Deserialize)]
struct Speaker {
name: String,
is_staff: bool,
}
fn send_to_livekit(frame_tx: UnboundedSender<AudioFrame<'static>>, mut microphone: impl Source) {
use cpal::Sample;
loop {
let sampled: Vec<_> = microphone
.by_ref()
.take(audio::BUFFER_SIZE)
.map(|s| s.to_sample())
.collect();
if frame_tx
.unbounded_send(AudioFrame {
sample_rate: SAMPLE_RATE.get(),
num_channels: CHANNEL_COUNT.get() as u32,
samples_per_channel: sampled.len() as u32 / CHANNEL_COUNT.get() as u32,
data: Cow::Owned(sampled),
})
.is_err()
{
// must rx has dropped or is not consuming
break;
}
}
}
use super::LocalVideoTrack;
pub enum AudioStream {

View File

@@ -1,21 +1,15 @@
use std::num::NonZero;
use futures::StreamExt;
use libwebrtc::{audio_stream::native::NativeAudioStream, prelude::AudioFrame};
use livekit::track::RemoteAudioTrack;
use rodio::{Source, buffer::SamplesBuffer, conversions::SampleTypeConverter, nz};
use rodio::{Source, buffer::SamplesBuffer, conversions::SampleTypeConverter};
use audio::{CHANNEL_COUNT, SAMPLE_RATE};
use crate::livekit_client::playback::{NUM_CHANNELS, SAMPLE_RATE};
fn frame_to_samplesbuffer(frame: AudioFrame) -> SamplesBuffer {
let samples = frame.data.iter().copied();
let samples = SampleTypeConverter::<_, _>::new(samples);
let samples: Vec<f32> = samples.collect();
SamplesBuffer::new(
nz!(2), // frame always has two channels
NonZero::new(frame.sample_rate).expect("audio frame sample rate is nonzero"),
samples,
)
SamplesBuffer::new(frame.num_channels as u16, frame.sample_rate, samples)
}
pub struct LiveKitStream {
@@ -26,11 +20,8 @@ pub struct LiveKitStream {
impl LiveKitStream {
pub fn new(executor: &gpui::BackgroundExecutor, track: &RemoteAudioTrack) -> Self {
let mut stream = NativeAudioStream::new(
track.rtc_track(),
SAMPLE_RATE.get() as i32,
CHANNEL_COUNT.get().into(),
);
let mut stream =
NativeAudioStream::new(track.rtc_track(), SAMPLE_RATE as i32, NUM_CHANNELS as i32);
let (queue_input, queue_output) = rodio::queue::queue(true);
// spawn rtc stream
let receiver_task = executor.spawn({
@@ -63,17 +54,11 @@ impl Source for LiveKitStream {
}
fn channels(&self) -> rodio::ChannelCount {
// This must be hardcoded because the playback source assumes constant
// sample rate and channel count. The queue upon which this is build
// will however report different counts and rates. Even though we put in
// only items with our (constant) CHANNEL_COUNT & SAMPLE_RATE this will
// play silence on one channel and at 44100 which is not what our
// constants are.
CHANNEL_COUNT
self.inner.channels()
}
fn sample_rate(&self) -> rodio::SampleRate {
SAMPLE_RATE // see comment on channels
self.inner.sample_rate()
}
fn total_duration(&self) -> Option<std::time::Duration> {

View File

@@ -1,6 +1,5 @@
use std::{
env,
num::NonZero,
path::{Path, PathBuf},
sync::{Arc, Mutex},
time::Duration,
@@ -84,12 +83,8 @@ fn write_out(
.expect("Stream has ended, callback cant hold the lock"),
);
let samples: Vec<f32> = SampleTypeConverter::<_, f32>::new(samples.into_iter()).collect();
let mut samples = SamplesBuffer::new(
NonZero::new(config.channels()).expect("config channel is never zero"),
NonZero::new(config.sample_rate().0).expect("config sample_rate is never zero"),
samples,
);
match rodio::wav_to_file(&mut samples, path) {
let mut samples = SamplesBuffer::new(config.channels(), config.sample_rate().0, samples);
match rodio::output_to_wav(&mut samples, path) {
Ok(_) => Ok(()),
Err(e) => Err(anyhow::anyhow!("Failed to write wav file: {}", e)),
}

View File

@@ -728,8 +728,6 @@ impl Room {
pub async fn publish_local_microphone_track(
&self,
_track_name: String,
_is_staff: bool,
cx: &mut AsyncApp,
) -> Result<(LocalTrackPublication, AudioStream)> {
self.local_participant().publish_microphone_track(cx).await

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