Merge branch 'main' into mcp-acp-gemini
This commit is contained in:
commit
f028ca4d1a
115 changed files with 3523 additions and 1437 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
@ -771,7 +771,7 @@ jobs:
|
|||
timeout-minutes: 120
|
||||
name: Create a Windows installer
|
||||
runs-on: [self-hosted, Windows, X64]
|
||||
if: false && (startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling'))
|
||||
if: (startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling'))
|
||||
needs: [windows_tests]
|
||||
env:
|
||||
AZURE_TENANT_ID: ${{ secrets.AZURE_SIGNING_TENANT_ID }}
|
||||
|
|
272
Cargo.lock
generated
272
Cargo.lock
generated
|
@ -90,6 +90,7 @@ dependencies = [
|
|||
"assistant_tools",
|
||||
"chrono",
|
||||
"client",
|
||||
"cloud_llm_client",
|
||||
"collections",
|
||||
"component",
|
||||
"context_server",
|
||||
|
@ -132,7 +133,6 @@ dependencies = [
|
|||
"uuid",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_llm_client",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
|
@ -187,6 +187,7 @@ name = "agent_settings"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cloud_llm_client",
|
||||
"collections",
|
||||
"fs",
|
||||
"gpui",
|
||||
|
@ -198,7 +199,6 @@ dependencies = [
|
|||
"serde_json_lenient",
|
||||
"settings",
|
||||
"workspace-hack",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -221,6 +221,7 @@ dependencies = [
|
|||
"buffer_diff",
|
||||
"chrono",
|
||||
"client",
|
||||
"cloud_llm_client",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"component",
|
||||
|
@ -292,7 +293,6 @@ dependencies = [
|
|||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -685,6 +685,7 @@ dependencies = [
|
|||
"chrono",
|
||||
"client",
|
||||
"clock",
|
||||
"cloud_llm_client",
|
||||
"collections",
|
||||
"context_server",
|
||||
"fs",
|
||||
|
@ -718,7 +719,6 @@ dependencies = [
|
|||
"uuid",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -826,6 +826,7 @@ dependencies = [
|
|||
"chrono",
|
||||
"client",
|
||||
"clock",
|
||||
"cloud_llm_client",
|
||||
"collections",
|
||||
"component",
|
||||
"derive_more 0.99.19",
|
||||
|
@ -879,7 +880,6 @@ dependencies = [
|
|||
"which 6.0.3",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_llm_client",
|
||||
"zlog",
|
||||
]
|
||||
|
||||
|
@ -2974,6 +2974,7 @@ dependencies = [
|
|||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"clock",
|
||||
"cloud_llm_client",
|
||||
"cocoa 0.26.0",
|
||||
"collections",
|
||||
"credentials_provider",
|
||||
|
@ -3016,7 +3017,6 @@ dependencies = [
|
|||
"windows 0.61.1",
|
||||
"workspace-hack",
|
||||
"worktree",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3029,6 +3029,19 @@ dependencies = [
|
|||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cloud_llm_client"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"pretty_assertions",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum 0.27.1",
|
||||
"uuid",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clru"
|
||||
version = "0.6.2"
|
||||
|
@ -3155,6 +3168,7 @@ dependencies = [
|
|||
"chrono",
|
||||
"client",
|
||||
"clock",
|
||||
"cloud_llm_client",
|
||||
"collab_ui",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
|
@ -3241,7 +3255,6 @@ dependencies = [
|
|||
"workspace",
|
||||
"workspace-hack",
|
||||
"worktree",
|
||||
"zed_llm_client",
|
||||
"zlog",
|
||||
]
|
||||
|
||||
|
@ -3682,17 +3695,6 @@ dependencies = [
|
|||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coreaudio-rs"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation-sys",
|
||||
"coreaudio-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coreaudio-rs"
|
||||
version = "0.12.1"
|
||||
|
@ -3750,29 +3752,6 @@ dependencies = [
|
|||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpal"
|
||||
version = "0.15.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779"
|
||||
dependencies = [
|
||||
"alsa",
|
||||
"core-foundation-sys",
|
||||
"coreaudio-rs 0.11.3",
|
||||
"dasp_sample",
|
||||
"jni",
|
||||
"js-sys",
|
||||
"libc",
|
||||
"mach2",
|
||||
"ndk 0.8.0",
|
||||
"ndk-context",
|
||||
"oboe",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows 0.54.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpal"
|
||||
version = "0.16.0"
|
||||
|
@ -3786,7 +3765,7 @@ dependencies = [
|
|||
"js-sys",
|
||||
"libc",
|
||||
"mach2",
|
||||
"ndk 0.9.0",
|
||||
"ndk",
|
||||
"ndk-context",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
|
@ -4790,7 +4769,6 @@ name = "docs_preprocessor"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"command_palette",
|
||||
"gpui",
|
||||
"mdbook",
|
||||
|
@ -4801,6 +4779,7 @@ dependencies = [
|
|||
"util",
|
||||
"workspace-hack",
|
||||
"zed",
|
||||
"zlog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -5261,6 +5240,7 @@ dependencies = [
|
|||
"chrono",
|
||||
"clap",
|
||||
"client",
|
||||
"cloud_llm_client",
|
||||
"collections",
|
||||
"debug_adapter_extension",
|
||||
"dirs 4.0.0",
|
||||
|
@ -5300,7 +5280,6 @@ dependencies = [
|
|||
"uuid",
|
||||
"watch",
|
||||
"workspace-hack",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -5365,6 +5344,12 @@ dependencies = [
|
|||
"zune-inflate",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "extended"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365"
|
||||
|
||||
[[package]]
|
||||
name = "extension"
|
||||
version = "0.1.0"
|
||||
|
@ -6376,6 +6361,7 @@ dependencies = [
|
|||
"call",
|
||||
"chrono",
|
||||
"client",
|
||||
"cloud_llm_client",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"component",
|
||||
|
@ -6418,7 +6404,6 @@ dependencies = [
|
|||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
"zed_llm_client",
|
||||
"zlog",
|
||||
]
|
||||
|
||||
|
@ -7380,8 +7365,9 @@ dependencies = [
|
|||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-cursor",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols 0.31.2",
|
||||
"wayland-protocols-plasma",
|
||||
"wayland-protocols-wlr",
|
||||
"windows 0.61.1",
|
||||
"windows-core 0.61.0",
|
||||
"windows-numerics",
|
||||
|
@ -7740,12 +7726,6 @@ dependencies = [
|
|||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hound"
|
||||
version = "3.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f"
|
||||
|
||||
[[package]]
|
||||
name = "html5ever"
|
||||
version = "0.27.0"
|
||||
|
@ -8384,6 +8364,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"cloud_llm_client",
|
||||
"copilot",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
|
@ -8406,7 +8387,6 @@ dependencies = [
|
|||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
"zed_llm_client",
|
||||
"zeta",
|
||||
]
|
||||
|
||||
|
@ -9088,6 +9068,7 @@ dependencies = [
|
|||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"client",
|
||||
"cloud_llm_client",
|
||||
"collections",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
|
@ -9105,7 +9086,6 @@ dependencies = [
|
|||
"thiserror 2.0.12",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -9121,6 +9101,7 @@ dependencies = [
|
|||
"bedrock",
|
||||
"chrono",
|
||||
"client",
|
||||
"cloud_llm_client",
|
||||
"collections",
|
||||
"component",
|
||||
"convert_case 0.8.0",
|
||||
|
@ -9162,7 +9143,6 @@ dependencies = [
|
|||
"vercel",
|
||||
"workspace-hack",
|
||||
"x_ai",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -9593,7 +9573,7 @@ dependencies = [
|
|||
"core-foundation 0.10.0",
|
||||
"core-video",
|
||||
"coreaudio-rs 0.12.1",
|
||||
"cpal 0.16.0",
|
||||
"cpal",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"gpui_tokio",
|
||||
|
@ -10364,20 +10344,6 @@ dependencies = [
|
|||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"jni-sys",
|
||||
"log",
|
||||
"ndk-sys 0.5.0+25.2.9519653",
|
||||
"num_enum",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk"
|
||||
version = "0.9.0"
|
||||
|
@ -10387,7 +10353,7 @@ dependencies = [
|
|||
"bitflags 2.9.0",
|
||||
"jni-sys",
|
||||
"log",
|
||||
"ndk-sys 0.6.0+11769913",
|
||||
"ndk-sys",
|
||||
"num_enum",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
@ -10398,15 +10364,6 @@ version = "0.1.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
|
||||
|
||||
[[package]]
|
||||
name = "ndk-sys"
|
||||
version = "0.5.0+25.2.9519653"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691"
|
||||
dependencies = [
|
||||
"jni-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk-sys"
|
||||
version = "0.6.0+11769913"
|
||||
|
@ -10976,29 +10933,6 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oboe"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb"
|
||||
dependencies = [
|
||||
"jni",
|
||||
"ndk 0.8.0",
|
||||
"ndk-context",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"oboe-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oboe-sys"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ollama"
|
||||
version = "0.1.0"
|
||||
|
@ -11019,9 +10953,12 @@ dependencies = [
|
|||
"anyhow",
|
||||
"command_palette_hooks",
|
||||
"db",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"gpui",
|
||||
"language",
|
||||
"project",
|
||||
"settings",
|
||||
"theme",
|
||||
"ui",
|
||||
|
@ -13778,12 +13715,15 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rodio"
|
||||
version = "0.20.1"
|
||||
version = "0.21.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e7ceb6607dd738c99bc8cb28eff249b7cd5c8ec88b9db96c0608c1480d140fb1"
|
||||
checksum = "e40ecf59e742e03336be6a3d53755e789fd05a059fa22dfa0ed624722319e183"
|
||||
dependencies = [
|
||||
"cpal 0.15.3",
|
||||
"hound",
|
||||
"cpal",
|
||||
"dasp_sample",
|
||||
"num-rational",
|
||||
"symphonia",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -15804,6 +15744,66 @@ dependencies = [
|
|||
"zeno",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "815c942ae7ee74737bb00f965fa5b5a2ac2ce7b6c01c0cc169bbeaf7abd5f5a9"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"symphonia-codec-pcm",
|
||||
"symphonia-core",
|
||||
"symphonia-format-riff",
|
||||
"symphonia-metadata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-codec-pcm"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f395a67057c2ebc5e84d7bb1be71cce1a7ba99f64e0f0f0e303a03f79116f89b"
|
||||
dependencies = [
|
||||
"log",
|
||||
"symphonia-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-core"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "798306779e3dc7d5231bd5691f5a813496dc79d3f56bf82e25789f2094e022c3"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bitflags 1.3.2",
|
||||
"bytemuck",
|
||||
"lazy_static",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-format-riff"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f7be232f962f937f4b7115cbe62c330929345434c834359425e043bfd15f50"
|
||||
dependencies = [
|
||||
"extended",
|
||||
"log",
|
||||
"symphonia-core",
|
||||
"symphonia-metadata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "symphonia-metadata"
|
||||
version = "0.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc622b9841a10089c5b18e99eb904f4341615d5aa55bbf4eedde1be721a4023c"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"symphonia-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.109"
|
||||
|
@ -18384,9 +18384,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wayland-backend"
|
||||
version = "0.3.8"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf"
|
||||
checksum = "fe770181423e5fc79d3e2a7f4410b7799d5aab1de4372853de3c6aa13ca24121"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"downcast-rs",
|
||||
|
@ -18398,9 +18398,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "wayland-client"
|
||||
version = "0.31.8"
|
||||
version = "0.31.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f"
|
||||
checksum = "978fa7c67b0847dbd6a9f350ca2569174974cd4082737054dbb7fbb79d7d9a61"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"rustix 0.38.44",
|
||||
|
@ -18431,6 +18431,18 @@ dependencies = [
|
|||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols"
|
||||
version = "0.32.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "779075454e1e9a521794fed15886323ea0feda3f8b0fc1390f5398141310422a"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols-plasma"
|
||||
version = "0.2.0"
|
||||
|
@ -18440,7 +18452,20 @@ dependencies = [
|
|||
"bitflags 2.9.0",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols 0.31.2",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wayland-protocols-wlr"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1cb6cdc73399c0e06504c437fe3cf886f25568dd5454473d565085b36d6a8bbf"
|
||||
dependencies = [
|
||||
"bitflags 2.9.0",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols 0.32.8",
|
||||
"wayland-scanner",
|
||||
]
|
||||
|
||||
|
@ -18504,11 +18529,11 @@ name = "web_search"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cloud_llm_client",
|
||||
"collections",
|
||||
"gpui",
|
||||
"serde",
|
||||
"workspace-hack",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -18517,6 +18542,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"cloud_llm_client",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"http_client",
|
||||
|
@ -18525,7 +18551,6 @@ dependencies = [
|
|||
"serde_json",
|
||||
"web_search",
|
||||
"workspace-hack",
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -19691,14 +19716,12 @@ dependencies = [
|
|||
"cc",
|
||||
"chrono",
|
||||
"cipher",
|
||||
"clang-sys",
|
||||
"clap",
|
||||
"clap_builder",
|
||||
"codespan-reporting 0.12.0",
|
||||
"concurrent-queue",
|
||||
"core-foundation 0.9.4",
|
||||
"core-foundation-sys",
|
||||
"coreaudio-sys",
|
||||
"cranelift-codegen",
|
||||
"crc32fast",
|
||||
"crossbeam-epoch",
|
||||
|
@ -20355,7 +20378,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zed_emmet"
|
||||
version = "0.0.3"
|
||||
version = "0.0.4"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
@ -20394,19 +20417,6 @@ dependencies = [
|
|||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_llm_client"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6607f74dee2a18a9ce0f091844944a0e59881359ab62e0768fb0618f55d4c1dc"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum 0.27.1",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_proto"
|
||||
version = "0.2.2"
|
||||
|
@ -20586,6 +20596,7 @@ dependencies = [
|
|||
"call",
|
||||
"client",
|
||||
"clock",
|
||||
"cloud_llm_client",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"copilot",
|
||||
|
@ -20627,7 +20638,6 @@ dependencies = [
|
|||
"workspace-hack",
|
||||
"worktree",
|
||||
"zed_actions",
|
||||
"zed_llm_client",
|
||||
"zlog",
|
||||
]
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ members = [
|
|||
"crates/cli",
|
||||
"crates/client",
|
||||
"crates/clock",
|
||||
"crates/cloud_llm_client",
|
||||
"crates/collab",
|
||||
"crates/collab_ui",
|
||||
"crates/collections",
|
||||
|
@ -70,7 +71,6 @@ members = [
|
|||
"crates/gpui",
|
||||
"crates/gpui_macros",
|
||||
"crates/gpui_tokio",
|
||||
|
||||
"crates/html_to_markdown",
|
||||
"crates/http_client",
|
||||
"crates/http_client_tls",
|
||||
|
@ -251,6 +251,7 @@ channel = { path = "crates/channel" }
|
|||
cli = { path = "crates/cli" }
|
||||
client = { path = "crates/client" }
|
||||
clock = { path = "crates/clock" }
|
||||
cloud_llm_client = { path = "crates/cloud_llm_client" }
|
||||
collab = { path = "crates/collab" }
|
||||
collab_ui = { path = "crates/collab_ui" }
|
||||
collections = { path = "crates/collections" }
|
||||
|
@ -645,7 +646,6 @@ which = "6.0.0"
|
|||
windows-core = "0.61"
|
||||
wit-component = "0.221"
|
||||
workspace-hack = "0.1.0"
|
||||
zed_llm_client = "= 0.8.6"
|
||||
zstd = "0.11"
|
||||
|
||||
[workspace.dependencies.async-stripe]
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# Zed
|
||||
|
||||
[](https://zed.dev)
|
||||
[](https://github.com/zed-industries/zed/actions/workflows/ci.yml)
|
||||
|
||||
Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter).
|
||||
|
|
8
assets/badge/v0.json
Normal file
8
assets/badge/v0.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"label": "",
|
||||
"message": "Zed",
|
||||
"logoSvg": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 96 96\"><rect width=\"96\" height=\"96\" fill=\"#000\"/><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M9 6C7.34315 6 6 7.34315 6 9V75H0V9C0 4.02944 4.02944 0 9 0H89.3787C93.3878 0 95.3955 4.84715 92.5607 7.68198L43.0551 57.1875H57V51H63V58.6875C63 61.1728 60.9853 63.1875 58.5 63.1875H37.0551L26.7426 73.5H73.5V36H79.5V73.5C79.5 76.8137 76.8137 79.5 73.5 79.5H20.7426L10.2426 90H87C88.6569 90 90 88.6569 90 87V21H96V87C96 91.9706 91.9706 96 87 96H6.62132C2.61224 96 0.604504 91.1529 3.43934 88.318L52.7574 39H39V45H33V37.5C33 35.0147 35.0147 33 37.5 33H58.7574L69.2574 22.5H22.5V60H16.5V22.5C16.5 19.1863 19.1863 16.5 22.5 16.5H75.2574L85.7574 6H9Z\" fill=\"#fff\"/></svg>",
|
||||
"logoWidth": 16,
|
||||
"labelColor": "black",
|
||||
"color": "white"
|
||||
}
|
|
@ -495,7 +495,7 @@
|
|||
"shift-f12": "editor::GoToImplementation",
|
||||
"alt-ctrl-f12": "editor::GoToTypeDefinitionSplit",
|
||||
"alt-shift-f12": "editor::FindAllReferences",
|
||||
"ctrl-m": "editor::MoveToEnclosingBracket",
|
||||
"ctrl-m": "editor::MoveToEnclosingBracket", // from jetbrains
|
||||
"ctrl-|": "editor::MoveToEnclosingBracket",
|
||||
"ctrl-{": "editor::Fold",
|
||||
"ctrl-}": "editor::UnfoldLines",
|
||||
|
|
|
@ -549,7 +549,7 @@
|
|||
"alt-cmd-f12": "editor::GoToTypeDefinitionSplit",
|
||||
"alt-shift-f12": "editor::FindAllReferences",
|
||||
"cmd-|": "editor::MoveToEnclosingBracket",
|
||||
"ctrl-m": "editor::MoveToEnclosingBracket",
|
||||
"ctrl-m": "editor::MoveToEnclosingBracket", // From Jetbrains
|
||||
"alt-cmd-[": "editor::Fold",
|
||||
"alt-cmd-]": "editor::UnfoldLines",
|
||||
"cmd-k cmd-l": "editor::ToggleFold",
|
||||
|
|
|
@ -1594,6 +1594,8 @@ mod tests {
|
|||
name: "test",
|
||||
connection,
|
||||
child_status: io_task,
|
||||
current_thread: thread_rc,
|
||||
agent_state: Default::default(),
|
||||
};
|
||||
|
||||
AcpThread::new(
|
||||
|
|
|
@ -13,6 +13,7 @@ use std::{
|
|||
rc::Rc,
|
||||
};
|
||||
use ui::App;
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::{AcpThread, AgentConnection};
|
||||
|
||||
|
@ -52,7 +53,7 @@ impl acp_old::Client for OldAcpClientDelegate {
|
|||
thread.push_assistant_content_block(thought.into(), true, cx)
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
.log_err();
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
|
@ -371,6 +372,7 @@ pub struct OldAcpAgentConnection {
|
|||
pub connection: acp_old::AgentConnection,
|
||||
pub child_status: Task<Result<()>>,
|
||||
pub agent_state: Rc<RefCell<acp::AgentState>>,
|
||||
pub current_thread: Rc<RefCell<WeakEntity<AcpThread>>>,
|
||||
}
|
||||
|
||||
impl AgentConnection for OldAcpAgentConnection {
|
||||
|
@ -386,6 +388,7 @@ impl AgentConnection for OldAcpAgentConnection {
|
|||
}
|
||||
.into_any(),
|
||||
);
|
||||
let current_thread = self.current_thread.clone();
|
||||
cx.spawn(async move |cx| {
|
||||
let result = task.await?;
|
||||
let result = acp_old::InitializeParams::response_from_any(result)?;
|
||||
|
@ -399,6 +402,7 @@ impl AgentConnection for OldAcpAgentConnection {
|
|||
let session_id = acp::SessionId("acp-old-no-id".into());
|
||||
AcpThread::new("Gemini", self.clone(), project, session_id, cx)
|
||||
});
|
||||
current_thread.replace(thread.downgrade());
|
||||
thread
|
||||
})
|
||||
})
|
||||
|
|
|
@ -25,6 +25,7 @@ assistant_context.workspace = true
|
|||
assistant_tool.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
component.workspace = true
|
||||
context_server.workspace = true
|
||||
|
@ -35,9 +36,9 @@ futures.workspace = true
|
|||
git.workspace = true
|
||||
gpui.workspace = true
|
||||
heed.workspace = true
|
||||
http_client.workspace = true
|
||||
icons.workspace = true
|
||||
indoc.workspace = true
|
||||
http_client.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
|
@ -63,7 +64,6 @@ time.workspace = true
|
|||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
zstd.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
@ -13,6 +13,7 @@ use anyhow::{Result, anyhow};
|
|||
use assistant_tool::{ActionLog, AnyToolCard, Tool, ToolWorkingSet};
|
||||
use chrono::{DateTime, Utc};
|
||||
use client::{ModelRequestUsage, RequestUsage};
|
||||
use cloud_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit};
|
||||
use collections::HashMap;
|
||||
use feature_flags::{self, FeatureFlagAppExt};
|
||||
use futures::{FutureExt, StreamExt as _, future::Shared};
|
||||
|
@ -49,7 +50,6 @@ use std::{
|
|||
use thiserror::Error;
|
||||
use util::{ResultExt as _, post_inc};
|
||||
use uuid::Uuid;
|
||||
use zed_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit};
|
||||
|
||||
const MAX_RETRY_ATTEMPTS: u8 = 4;
|
||||
const BASE_RETRY_DELAY: Duration = Duration::from_secs(5);
|
||||
|
@ -1681,7 +1681,7 @@ impl Thread {
|
|||
|
||||
let completion_mode = request
|
||||
.mode
|
||||
.unwrap_or(zed_llm_client::CompletionMode::Normal);
|
||||
.unwrap_or(cloud_llm_client::CompletionMode::Normal);
|
||||
|
||||
self.last_received_chunk_at = Some(Instant::now());
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ impl AcpConnection {
|
|||
pub async fn stdio(
|
||||
server_name: &'static str,
|
||||
command: AgentServerCommand,
|
||||
working_directory: Option<Arc<Path>>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Self> {
|
||||
let client: Arc<ContextServer> = ContextServer::stdio(
|
||||
|
@ -41,6 +42,7 @@ impl AcpConnection {
|
|||
args: command.args,
|
||||
env: command.env,
|
||||
},
|
||||
working_directory,
|
||||
)
|
||||
.into();
|
||||
ContextServer::start(client.clone(), cx).await?;
|
||||
|
|
|
@ -38,6 +38,7 @@ impl AgentServer for Codex {
|
|||
) -> Task<Result<Rc<dyn AgentConnection>>> {
|
||||
let project = project.clone();
|
||||
let server_name = self.name();
|
||||
let working_directory = project.read(cx).active_project_directory(cx);
|
||||
cx.spawn(async move |cx| {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).codex.clone()
|
||||
|
@ -50,7 +51,7 @@ impl AgentServer for Codex {
|
|||
};
|
||||
// todo! check supported version
|
||||
|
||||
let conn = AcpConnection::stdio(server_name, command, cx).await?;
|
||||
let conn = AcpConnection::stdio(server_name, command, working_directory, cx).await?;
|
||||
Ok(Rc::new(conn) as _)
|
||||
})
|
||||
}
|
||||
|
@ -70,7 +71,7 @@ pub(crate) mod tests {
|
|||
|
||||
AgentServerCommand {
|
||||
path: cli_path,
|
||||
args: vec!["mcp".into()],
|
||||
args: vec![],
|
||||
env: None,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ use futures::{FutureExt, StreamExt, channel::mpsc, select};
|
|||
use gpui::{Entity, TestAppContext};
|
||||
use indoc::indoc;
|
||||
use project::{FakeFs, Project};
|
||||
use serde_json::json;
|
||||
use settings::{Settings, SettingsStore};
|
||||
use util::path;
|
||||
|
||||
|
@ -27,7 +26,11 @@ pub async fn test_basic(server: impl AgentServer + 'static, cx: &mut TestAppCont
|
|||
.unwrap();
|
||||
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert_eq!(thread.entries().len(), 2);
|
||||
assert!(
|
||||
thread.entries().len() >= 2,
|
||||
"Expected at least 2 entries. Got: {:?}",
|
||||
thread.entries()
|
||||
);
|
||||
assert!(matches!(
|
||||
thread.entries()[0],
|
||||
AgentThreadEntry::UserMessage(_)
|
||||
|
@ -108,19 +111,19 @@ pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut Tes
|
|||
}
|
||||
|
||||
pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
|
||||
let fs = init_test(cx).await;
|
||||
fs.insert_tree(
|
||||
path!("/private/tmp"),
|
||||
json!({"foo": "Lorem ipsum dolor", "bar": "bar", "baz": "baz"}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await;
|
||||
let _fs = init_test(cx).await;
|
||||
|
||||
let tempdir = tempfile::tempdir().unwrap();
|
||||
let foo_path = tempdir.path().join("foo");
|
||||
std::fs::write(&foo_path, "Lorem ipsum dolor").expect("failed to write file");
|
||||
|
||||
let project = Project::example([tempdir.path()], &mut cx.to_async()).await;
|
||||
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send_raw(
|
||||
"Read the '/private/tmp/foo' file and tell me what you see.",
|
||||
&format!("Read {} and tell me what you see.", foo_path.display()),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
|
@ -143,6 +146,8 @@ pub async fn test_tool_call(server: impl AgentServer + 'static, cx: &mut TestApp
|
|||
.any(|entry| { matches!(entry, AgentThreadEntry::AssistantMessage(_)) })
|
||||
);
|
||||
});
|
||||
|
||||
drop(tempdir);
|
||||
}
|
||||
|
||||
pub async fn test_tool_call_with_confirmation(
|
||||
|
@ -155,7 +160,7 @@ pub async fn test_tool_call_with_confirmation(
|
|||
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
|
||||
let full_turn = thread.update(cx, |thread, cx| {
|
||||
thread.send_raw(
|
||||
r#"Run `touch hello.txt && echo "Hello, world!" | tee hello.txt`"#,
|
||||
r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
@ -175,10 +180,10 @@ pub async fn test_tool_call_with_confirmation(
|
|||
)
|
||||
.await;
|
||||
|
||||
let tool_call_id = thread.read_with(cx, |thread, _cx| {
|
||||
let tool_call_id = thread.read_with(cx, |thread, cx| {
|
||||
let AgentThreadEntry::ToolCall(ToolCall {
|
||||
id,
|
||||
content,
|
||||
label,
|
||||
status: ToolCallStatus::WaitingForConfirmation { .. },
|
||||
..
|
||||
}) = &thread
|
||||
|
@ -190,7 +195,8 @@ pub async fn test_tool_call_with_confirmation(
|
|||
panic!();
|
||||
};
|
||||
|
||||
assert!(content.iter().any(|c| c.to_markdown(_cx).contains("touch")));
|
||||
let label = label.read(cx).source();
|
||||
assert!(label.contains("touch"), "Got: {}", label);
|
||||
|
||||
id.clone()
|
||||
});
|
||||
|
@ -242,7 +248,7 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon
|
|||
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
|
||||
let full_turn = thread.update(cx, |thread, cx| {
|
||||
thread.send_raw(
|
||||
r#"Run `touch hello.txt && echo "Hello, world!" >> hello.txt`"#,
|
||||
r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
@ -262,10 +268,10 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon
|
|||
)
|
||||
.await;
|
||||
|
||||
thread.read_with(cx, |thread, _cx| {
|
||||
thread.read_with(cx, |thread, cx| {
|
||||
let AgentThreadEntry::ToolCall(ToolCall {
|
||||
id,
|
||||
content,
|
||||
label,
|
||||
status: ToolCallStatus::WaitingForConfirmation { .. },
|
||||
..
|
||||
}) = &thread.entries()[first_tool_call_ix]
|
||||
|
@ -273,7 +279,8 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon
|
|||
panic!("{:?}", thread.entries()[1]);
|
||||
};
|
||||
|
||||
assert!(content.iter().any(|c| c.to_markdown(_cx).contains("touch")));
|
||||
let label = label.read(cx).source();
|
||||
assert!(label.contains("touch"), "Got: {}", label);
|
||||
|
||||
id.clone()
|
||||
});
|
||||
|
|
|
@ -41,6 +41,7 @@ impl AgentServer for Gemini {
|
|||
) -> Task<Result<Rc<dyn AgentConnection>>> {
|
||||
let project = project.clone();
|
||||
let server_name = self.name();
|
||||
let working_directory = project.read(cx).active_project_directory(cx);
|
||||
cx.spawn(async move |cx| {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).gemini.clone()
|
||||
|
@ -53,7 +54,7 @@ impl AgentServer for Gemini {
|
|||
};
|
||||
// todo! check supported version
|
||||
|
||||
let conn = AcpConnection::stdio(server_name, command, cx).await?;
|
||||
let conn = AcpConnection::stdio(server_name, command, working_directory, cx).await?;
|
||||
Ok(Rc::new(conn) as _)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ path = "src/agent_settings.rs"
|
|||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
gpui.workspace = true
|
||||
language_model.workspace = true
|
||||
|
@ -20,7 +21,6 @@ schemars.workspace = true
|
|||
serde.workspace = true
|
||||
settings.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
fs.workspace = true
|
||||
|
|
|
@ -321,11 +321,11 @@ pub enum CompletionMode {
|
|||
Burn,
|
||||
}
|
||||
|
||||
impl From<CompletionMode> for zed_llm_client::CompletionMode {
|
||||
impl From<CompletionMode> for cloud_llm_client::CompletionMode {
|
||||
fn from(value: CompletionMode) -> Self {
|
||||
match value {
|
||||
CompletionMode::Normal => zed_llm_client::CompletionMode::Normal,
|
||||
CompletionMode::Burn => zed_llm_client::CompletionMode::Max,
|
||||
CompletionMode::Normal => cloud_llm_client::CompletionMode::Normal,
|
||||
CompletionMode::Burn => cloud_llm_client::CompletionMode::Max,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ audio.workspace = true
|
|||
buffer_diff.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
component.workspace = true
|
||||
|
@ -46,9 +47,9 @@ futures.workspace = true
|
|||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
html_to_markdown.workspace = true
|
||||
indoc.workspace = true
|
||||
http_client.workspace = true
|
||||
indexed_docs.workspace = true
|
||||
indoc.workspace = true
|
||||
inventory.workspace = true
|
||||
itertools.workspace = true
|
||||
jsonschema.workspace = true
|
||||
|
@ -97,7 +98,6 @@ watch.workspace = true
|
|||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
assistant_tools.workspace = true
|
||||
|
|
|
@ -14,6 +14,7 @@ use agent_settings::{AgentSettings, NotifyWhenAgentWaiting};
|
|||
use anyhow::Context as _;
|
||||
use assistant_tool::ToolUseStatus;
|
||||
use audio::{Audio, Sound};
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::actions::{MoveUp, Paste};
|
||||
use editor::scroll::Autoscroll;
|
||||
|
@ -52,7 +53,6 @@ use util::ResultExt as _;
|
|||
use util::markdown::MarkdownCodeBlock;
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use zed_actions::assistant::OpenRulesLibrary;
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
const CODEBLOCK_CONTAINER_GROUP: &str = "codeblock_container";
|
||||
const EDIT_PREVIOUS_MESSAGE_MIN_LINES: usize = 1;
|
||||
|
|
|
@ -44,6 +44,7 @@ use assistant_context::{AssistantContext, ContextEvent, ContextSummary};
|
|||
use assistant_slash_command::SlashCommandWorkingSet;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use client::{DisableAiSettings, UserStore, zed_urls};
|
||||
use cloud_llm_client::{CompletionIntent, UsageLimit};
|
||||
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
|
||||
use feature_flags::{self, FeatureFlagAppExt};
|
||||
use fs::Fs;
|
||||
|
@ -80,7 +81,6 @@ use zed_actions::{
|
|||
agent::{OpenConfiguration, OpenOnboardingModal, ResetOnboarding, ToggleModelSelector},
|
||||
assistant::{OpenRulesLibrary, ToggleFocus},
|
||||
};
|
||||
use zed_llm_client::{CompletionIntent, UsageLimit};
|
||||
|
||||
const AGENT_PANEL_KEY: &str = "agent_panel";
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ use agent::{
|
|||
use agent_settings::AgentSettings;
|
||||
use anyhow::{Context as _, Result};
|
||||
use client::telemetry::Telemetry;
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use collections::HashSet;
|
||||
use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
|
||||
use futures::{
|
||||
|
@ -35,7 +36,6 @@ use std::{
|
|||
};
|
||||
use streaming_diff::{CharOperation, LineDiff, LineOperation, StreamingDiff};
|
||||
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
pub struct BufferCodegen {
|
||||
alternatives: Vec<Entity<CodegenAlternative>>,
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
#![allow(unused, dead_code)]
|
||||
|
||||
use client::{ModelRequestUsage, RequestUsage};
|
||||
use cloud_llm_client::{Plan, UsageLimit};
|
||||
use gpui::Global;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use ui::prelude::*;
|
||||
use zed_llm_client::{Plan, UsageLimit};
|
||||
|
||||
/// Debug only: Used for testing various account states
|
||||
///
|
||||
|
|
|
@ -18,6 +18,7 @@ use agent_settings::{AgentSettings, CompletionMode};
|
|||
use ai_onboarding::ApiKeysWithProviders;
|
||||
use buffer_diff::BufferDiff;
|
||||
use client::UserStore;
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::actions::{MoveUp, Paste};
|
||||
use editor::display_map::CreaseId;
|
||||
|
@ -53,7 +54,6 @@ use util::ResultExt as _;
|
|||
use workspace::{CollaboratorId, Workspace};
|
||||
use zed_actions::agent::Chat;
|
||||
use zed_actions::agent::ToggleModelSelector;
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
|
||||
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
|
||||
|
@ -1300,11 +1300,11 @@ impl MessageEditor {
|
|||
let plan = user_store
|
||||
.current_plan()
|
||||
.map(|plan| match plan {
|
||||
Plan::Free => zed_llm_client::Plan::ZedFree,
|
||||
Plan::ZedPro => zed_llm_client::Plan::ZedPro,
|
||||
Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
|
||||
Plan::Free => cloud_llm_client::Plan::ZedFree,
|
||||
Plan::ZedPro => cloud_llm_client::Plan::ZedPro,
|
||||
Plan::ZedProTrial => cloud_llm_client::Plan::ZedProTrial,
|
||||
})
|
||||
.unwrap_or(zed_llm_client::Plan::ZedFree);
|
||||
.unwrap_or(cloud_llm_client::Plan::ZedFree);
|
||||
|
||||
let usage = user_store.model_request_usage()?;
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ use agent::{
|
|||
use agent_settings::AgentSettings;
|
||||
use anyhow::{Context as _, Result};
|
||||
use client::telemetry::Telemetry;
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use collections::{HashMap, VecDeque};
|
||||
use editor::{MultiBuffer, actions::SelectAll};
|
||||
use fs::Fs;
|
||||
|
@ -27,7 +28,6 @@ use terminal_view::TerminalView;
|
|||
use ui::prelude::*;
|
||||
use util::ResultExt;
|
||||
use workspace::{Toast, Workspace, notifications::NotificationId};
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
pub fn init(
|
||||
fs: Arc<dyn Fs>,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
use client::{ModelRequestUsage, RequestUsage, zed_urls};
|
||||
use cloud_llm_client::{Plan, UsageLimit};
|
||||
use component::{empty_example, example_group_with_title, single_example};
|
||||
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
|
||||
use ui::{Callout, prelude::*};
|
||||
use zed_llm_client::{Plan, UsageLimit};
|
||||
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
pub struct UsageCallout {
|
||||
|
|
|
@ -19,6 +19,7 @@ assistant_slash_commands.workspace = true
|
|||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
clock.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
context_server.workspace = true
|
||||
fs.workspace = true
|
||||
|
@ -48,7 +49,6 @@ util.workspace = true
|
|||
uuid.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
indoc.workspace = true
|
||||
|
|
|
@ -11,6 +11,7 @@ use assistant_slash_command::{
|
|||
use assistant_slash_commands::FileCommandMetadata;
|
||||
use client::{self, Client, proto, telemetry::Telemetry};
|
||||
use clock::ReplicaId;
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use collections::{HashMap, HashSet};
|
||||
use fs::{Fs, RenameOptions};
|
||||
use futures::{FutureExt, StreamExt, future::Shared};
|
||||
|
@ -46,7 +47,6 @@ use text::{BufferSnapshot, ToPoint};
|
|||
use ui::IconName;
|
||||
use util::{ResultExt, TryFutureExt, post_inc};
|
||||
use uuid::Uuid;
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
pub use crate::context_store::*;
|
||||
|
||||
|
|
|
@ -21,9 +21,11 @@ assistant_tool.workspace = true
|
|||
buffer_diff.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
component.workspace = true
|
||||
derive_more.workspace = true
|
||||
diffy = "0.4.2"
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
futures.workspace = true
|
||||
|
@ -63,8 +65,6 @@ web_search.workspace = true
|
|||
which.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
diffy = "0.4.2"
|
||||
|
||||
[dev-dependencies]
|
||||
lsp = { workspace = true, features = ["test-support"] }
|
||||
|
|
|
@ -7,6 +7,7 @@ mod streaming_fuzzy_matcher;
|
|||
use crate::{Template, Templates};
|
||||
use anyhow::Result;
|
||||
use assistant_tool::ActionLog;
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use create_file_parser::{CreateFileParser, CreateFileParserEvent};
|
||||
pub use edit_parser::EditFormat;
|
||||
use edit_parser::{EditParser, EditParserEvent, EditParserMetrics};
|
||||
|
@ -29,7 +30,6 @@ use std::{cmp, iter, mem, ops::Range, path::PathBuf, pin::Pin, sync::Arc, task::
|
|||
use streaming_diff::{CharOperation, StreamingDiff};
|
||||
use streaming_fuzzy_matcher::StreamingFuzzyMatcher;
|
||||
use util::debug_panic;
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct CreateFilePromptTemplate {
|
||||
|
|
|
@ -6,6 +6,7 @@ use anyhow::{Context as _, Result, anyhow};
|
|||
use assistant_tool::{
|
||||
ActionLog, Tool, ToolCard, ToolResult, ToolResultContent, ToolResultOutput, ToolUseStatus,
|
||||
};
|
||||
use cloud_llm_client::{WebSearchResponse, WebSearchResult};
|
||||
use futures::{Future, FutureExt, TryFutureExt};
|
||||
use gpui::{
|
||||
AnyWindowHandle, App, AppContext, Context, Entity, IntoElement, Task, WeakEntity, Window,
|
||||
|
@ -17,7 +18,6 @@ use serde::{Deserialize, Serialize};
|
|||
use ui::{IconName, Tooltip, prelude::*};
|
||||
use web_search::WebSearchRegistry;
|
||||
use workspace::Workspace;
|
||||
use zed_llm_client::{WebSearchResponse, WebSearchResult};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct WebSearchToolInput {
|
||||
|
|
|
@ -18,6 +18,6 @@ collections.workspace = true
|
|||
derive_more.workspace = true
|
||||
gpui.workspace = true
|
||||
parking_lot.workspace = true
|
||||
rodio = { version = "0.20.0", default-features = false, features = ["wav"] }
|
||||
rodio = { version = "0.21.1", default-features = false, features = ["wav", "playback", "tracing"] }
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
|
|
@ -3,12 +3,9 @@ use std::{io::Cursor, sync::Arc};
|
|||
use anyhow::{Context as _, Result};
|
||||
use collections::HashMap;
|
||||
use gpui::{App, AssetSource, Global};
|
||||
use rodio::{
|
||||
Decoder, Source,
|
||||
source::{Buffered, SamplesConverter},
|
||||
};
|
||||
use rodio::{Decoder, Source, source::Buffered};
|
||||
|
||||
type Sound = Buffered<SamplesConverter<Decoder<Cursor<Vec<u8>>>, f32>>;
|
||||
type Sound = Buffered<Decoder<Cursor<Vec<u8>>>>;
|
||||
|
||||
pub struct SoundRegistry {
|
||||
cache: Arc<parking_lot::Mutex<HashMap<String, Sound>>>,
|
||||
|
@ -48,7 +45,7 @@ impl SoundRegistry {
|
|||
.with_context(|| format!("No asset available for path {path}"))??
|
||||
.into_owned();
|
||||
let cursor = Cursor::new(bytes);
|
||||
let source = Decoder::new(cursor)?.convert_samples::<f32>().buffered();
|
||||
let source = Decoder::new(cursor)?.buffered();
|
||||
|
||||
self.cache.lock().insert(name.to_string(), source.clone());
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use assets::SoundRegistry;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use gpui::{App, AssetSource, BorrowAppContext, Global};
|
||||
use rodio::{OutputStream, OutputStreamHandle};
|
||||
use rodio::{OutputStream, OutputStreamBuilder};
|
||||
use util::ResultExt;
|
||||
|
||||
mod assets;
|
||||
|
@ -37,8 +37,7 @@ impl Sound {
|
|||
|
||||
#[derive(Default)]
|
||||
pub struct Audio {
|
||||
_output_stream: Option<OutputStream>,
|
||||
output_handle: Option<OutputStreamHandle>,
|
||||
output_handle: Option<OutputStream>,
|
||||
}
|
||||
|
||||
#[derive(Deref, DerefMut)]
|
||||
|
@ -51,11 +50,9 @@ impl Audio {
|
|||
Self::default()
|
||||
}
|
||||
|
||||
fn ensure_output_exists(&mut self) -> Option<&OutputStreamHandle> {
|
||||
fn ensure_output_exists(&mut self) -> Option<&OutputStream> {
|
||||
if self.output_handle.is_none() {
|
||||
let (_output_stream, output_handle) = OutputStream::try_default().log_err().unzip();
|
||||
self.output_handle = output_handle;
|
||||
self._output_stream = _output_stream;
|
||||
self.output_handle = OutputStreamBuilder::open_default_stream().log_err();
|
||||
}
|
||||
|
||||
self.output_handle.as_ref()
|
||||
|
@ -69,7 +66,7 @@ impl Audio {
|
|||
cx.update_global::<GlobalAudio, _>(|this, cx| {
|
||||
let output_handle = this.ensure_output_exists()?;
|
||||
let source = SoundRegistry::global(cx).get(sound.file()).log_err()?;
|
||||
output_handle.play_raw(source).log_err()?;
|
||||
output_handle.mixer().add(source);
|
||||
Some(())
|
||||
});
|
||||
}
|
||||
|
@ -80,7 +77,6 @@ impl Audio {
|
|||
}
|
||||
|
||||
cx.update_global::<GlobalAudio, _>(|this, _| {
|
||||
this._output_stream.take();
|
||||
this.output_handle.take();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ async-tungstenite = { workspace = true, features = ["tokio", "tokio-rustls-manua
|
|||
base64.workspace = true
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
clock.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
credentials_provider.workspace = true
|
||||
derive_more.workspace = true
|
||||
|
@ -33,8 +34,8 @@ http_client.workspace = true
|
|||
http_client_tls.workspace = true
|
||||
httparse = "1.10"
|
||||
log.workspace = true
|
||||
paths.workspace = true
|
||||
parking_lot.workspace = true
|
||||
paths.workspace = true
|
||||
postage.workspace = true
|
||||
rand.workspace = true
|
||||
regex.workspace = true
|
||||
|
@ -46,19 +47,18 @@ serde_json.workspace = true
|
|||
settings.workspace = true
|
||||
sha2.workspace = true
|
||||
smol.workspace = true
|
||||
telemetry.workspace = true
|
||||
telemetry_events.workspace = true
|
||||
text.workspace = true
|
||||
thiserror.workspace = true
|
||||
time.workspace = true
|
||||
tiny_http.workspace = true
|
||||
tokio-socks = { version = "0.5.2", default-features = false, features = ["futures-io"] }
|
||||
tokio.workspace = true
|
||||
url.workspace = true
|
||||
util.workspace = true
|
||||
worktree.workspace = true
|
||||
telemetry.workspace = true
|
||||
tokio.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
worktree.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
clock = { workspace = true, features = ["test-support"] }
|
||||
|
|
|
@ -21,7 +21,7 @@ use futures::{
|
|||
channel::oneshot, future::BoxFuture,
|
||||
};
|
||||
use gpui::{App, AsyncApp, Entity, Global, Task, WeakEntity, actions};
|
||||
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl};
|
||||
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl, http};
|
||||
use parking_lot::RwLock;
|
||||
use postage::watch;
|
||||
use proxy::connect_proxy_stream;
|
||||
|
@ -1138,7 +1138,7 @@ impl Client {
|
|||
.to_str()
|
||||
.map_err(EstablishConnectionError::other)?
|
||||
.to_string();
|
||||
Url::parse(&collab_url).with_context(|| format!("parsing colab rpc url {collab_url}"))
|
||||
Url::parse(&collab_url).with_context(|| format!("parsing collab rpc url {collab_url}"))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1158,6 +1158,7 @@ impl Client {
|
|||
|
||||
let http = self.http.clone();
|
||||
let proxy = http.proxy().cloned();
|
||||
let user_agent = http.user_agent().cloned();
|
||||
let credentials = credentials.clone();
|
||||
let rpc_url = self.rpc_url(http, release_channel);
|
||||
let system_id = self.telemetry.system_id();
|
||||
|
@ -1209,7 +1210,7 @@ impl Client {
|
|||
// We then modify the request to add our desired headers.
|
||||
let request_headers = request.headers_mut();
|
||||
request_headers.insert(
|
||||
"Authorization",
|
||||
http::header::AUTHORIZATION,
|
||||
HeaderValue::from_str(&credentials.authorization_header())?,
|
||||
);
|
||||
request_headers.insert(
|
||||
|
@ -1221,6 +1222,9 @@ impl Client {
|
|||
"x-zed-release-channel",
|
||||
HeaderValue::from_str(release_channel.map(|r| r.dev_name()).unwrap_or("unknown"))?,
|
||||
);
|
||||
if let Some(user_agent) = user_agent {
|
||||
request_headers.insert(http::header::USER_AGENT, user_agent);
|
||||
}
|
||||
if let Some(system_id) = system_id {
|
||||
request_headers.insert("x-zed-system-id", HeaderValue::from_str(&system_id)?);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
use super::{Client, Status, TypedEnvelope, proto};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use chrono::{DateTime, Utc};
|
||||
use cloud_llm_client::{
|
||||
EDIT_PREDICTIONS_USAGE_AMOUNT_HEADER_NAME, EDIT_PREDICTIONS_USAGE_LIMIT_HEADER_NAME,
|
||||
MODEL_REQUESTS_USAGE_AMOUNT_HEADER_NAME, MODEL_REQUESTS_USAGE_LIMIT_HEADER_NAME, UsageLimit,
|
||||
};
|
||||
use collections::{HashMap, HashSet, hash_map::Entry};
|
||||
use derive_more::Deref;
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
|
@ -17,10 +21,6 @@ use std::{
|
|||
};
|
||||
use text::ReplicaId;
|
||||
use util::{TryFutureExt as _, maybe};
|
||||
use zed_llm_client::{
|
||||
EDIT_PREDICTIONS_USAGE_AMOUNT_HEADER_NAME, EDIT_PREDICTIONS_USAGE_LIMIT_HEADER_NAME,
|
||||
MODEL_REQUESTS_USAGE_AMOUNT_HEADER_NAME, MODEL_REQUESTS_USAGE_LIMIT_HEADER_NAME, UsageLimit,
|
||||
};
|
||||
|
||||
pub type UserId = u64;
|
||||
|
||||
|
|
23
crates/cloud_llm_client/Cargo.toml
Normal file
23
crates/cloud_llm_client/Cargo.toml
Normal file
|
@ -0,0 +1,23 @@
|
|||
[package]
|
||||
name = "cloud_llm_client"
|
||||
version = "0.1.0"
|
||||
publish.workspace = true
|
||||
edition.workspace = true
|
||||
license = "Apache-2.0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/cloud_llm_client.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
serde = { workspace = true, features = ["derive", "rc"] }
|
||||
serde_json.workspace = true
|
||||
strum = { workspace = true, features = ["derive"] }
|
||||
uuid = { workspace = true, features = ["serde"] }
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions.workspace = true
|
1
crates/cloud_llm_client/LICENSE-APACHE
Symbolic link
1
crates/cloud_llm_client/LICENSE-APACHE
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../LICENSE-APACHE
|
370
crates/cloud_llm_client/src/cloud_llm_client.rs
Normal file
370
crates/cloud_llm_client/src/cloud_llm_client.rs
Normal file
|
@ -0,0 +1,370 @@
|
|||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{Display, EnumIter, EnumString};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// The name of the header used to indicate which version of Zed the client is running.
|
||||
pub const ZED_VERSION_HEADER_NAME: &str = "x-zed-version";
|
||||
|
||||
/// The name of the header used to indicate when a request failed due to an
|
||||
/// expired LLM token.
|
||||
///
|
||||
/// The client may use this as a signal to refresh the token.
|
||||
pub const EXPIRED_LLM_TOKEN_HEADER_NAME: &str = "x-zed-expired-token";
|
||||
|
||||
/// The name of the header used to indicate what plan the user is currently on.
|
||||
pub const CURRENT_PLAN_HEADER_NAME: &str = "x-zed-plan";
|
||||
|
||||
/// The name of the header used to indicate the usage limit for model requests.
|
||||
pub const MODEL_REQUESTS_USAGE_LIMIT_HEADER_NAME: &str = "x-zed-model-requests-usage-limit";
|
||||
|
||||
/// The name of the header used to indicate the usage amount for model requests.
|
||||
pub const MODEL_REQUESTS_USAGE_AMOUNT_HEADER_NAME: &str = "x-zed-model-requests-usage-amount";
|
||||
|
||||
/// The name of the header used to indicate the usage limit for edit predictions.
|
||||
pub const EDIT_PREDICTIONS_USAGE_LIMIT_HEADER_NAME: &str = "x-zed-edit-predictions-usage-limit";
|
||||
|
||||
/// The name of the header used to indicate the usage amount for edit predictions.
|
||||
pub const EDIT_PREDICTIONS_USAGE_AMOUNT_HEADER_NAME: &str = "x-zed-edit-predictions-usage-amount";
|
||||
|
||||
/// The name of the header used to indicate the resource for which the subscription limit has been reached.
|
||||
pub const SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME: &str = "x-zed-subscription-limit-resource";
|
||||
|
||||
pub const MODEL_REQUESTS_RESOURCE_HEADER_VALUE: &str = "model_requests";
|
||||
pub const EDIT_PREDICTIONS_RESOURCE_HEADER_VALUE: &str = "edit_predictions";
|
||||
|
||||
/// The name of the header used to indicate that the maximum number of consecutive tool uses has been reached.
|
||||
pub const TOOL_USE_LIMIT_REACHED_HEADER_NAME: &str = "x-zed-tool-use-limit-reached";
|
||||
|
||||
/// The name of the header used to indicate the the minimum required Zed version.
|
||||
///
|
||||
/// This can be used to force a Zed upgrade in order to continue communicating
|
||||
/// with the LLM service.
|
||||
pub const MINIMUM_REQUIRED_VERSION_HEADER_NAME: &str = "x-zed-minimum-required-version";
|
||||
|
||||
/// The name of the header used by the client to indicate to the server that it supports receiving status messages.
|
||||
pub const CLIENT_SUPPORTS_STATUS_MESSAGES_HEADER_NAME: &str =
|
||||
"x-zed-client-supports-status-messages";
|
||||
|
||||
/// The name of the header used by the server to indicate to the client that it supports sending status messages.
|
||||
pub const SERVER_SUPPORTS_STATUS_MESSAGES_HEADER_NAME: &str =
|
||||
"x-zed-server-supports-status-messages";
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum UsageLimit {
|
||||
Limited(i32),
|
||||
Unlimited,
|
||||
}
|
||||
|
||||
impl FromStr for UsageLimit {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
match value {
|
||||
"unlimited" => Ok(Self::Unlimited),
|
||||
limit => limit
|
||||
.parse::<i32>()
|
||||
.map(Self::Limited)
|
||||
.context("failed to parse limit"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Plan {
|
||||
#[default]
|
||||
#[serde(alias = "Free")]
|
||||
ZedFree,
|
||||
#[serde(alias = "ZedPro")]
|
||||
ZedPro,
|
||||
#[serde(alias = "ZedProTrial")]
|
||||
ZedProTrial,
|
||||
}
|
||||
|
||||
impl Plan {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Plan::ZedFree => "zed_free",
|
||||
Plan::ZedPro => "zed_pro",
|
||||
Plan::ZedProTrial => "zed_pro_trial",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn model_requests_limit(&self) -> UsageLimit {
|
||||
match self {
|
||||
Plan::ZedPro => UsageLimit::Limited(500),
|
||||
Plan::ZedProTrial => UsageLimit::Limited(150),
|
||||
Plan::ZedFree => UsageLimit::Limited(50),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn edit_predictions_limit(&self) -> UsageLimit {
|
||||
match self {
|
||||
Plan::ZedPro => UsageLimit::Unlimited,
|
||||
Plan::ZedProTrial => UsageLimit::Unlimited,
|
||||
Plan::ZedFree => UsageLimit::Limited(2_000),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Plan {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
match value {
|
||||
"zed_free" => Ok(Plan::ZedFree),
|
||||
"zed_pro" => Ok(Plan::ZedPro),
|
||||
"zed_pro_trial" => Ok(Plan::ZedProTrial),
|
||||
plan => Err(anyhow::anyhow!("invalid plan: {plan:?}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize, Deserialize, EnumString, EnumIter, Display,
|
||||
)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum LanguageModelProvider {
|
||||
Anthropic,
|
||||
OpenAi,
|
||||
Google,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PredictEditsBody {
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
pub outline: Option<String>,
|
||||
pub input_events: String,
|
||||
pub input_excerpt: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
pub speculated_output: Option<String>,
|
||||
/// Whether the user provided consent for sampling this interaction.
|
||||
#[serde(default, alias = "data_collection_permission")]
|
||||
pub can_collect_data: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
pub diagnostic_groups: Option<Vec<(String, serde_json::Value)>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PredictEditsResponse {
|
||||
pub request_id: Uuid,
|
||||
pub output_excerpt: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AcceptEditPredictionBody {
|
||||
pub request_id: Uuid,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CompletionMode {
|
||||
Normal,
|
||||
Max,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CompletionIntent {
|
||||
UserPrompt,
|
||||
ToolResults,
|
||||
ThreadSummarization,
|
||||
ThreadContextSummarization,
|
||||
CreateFile,
|
||||
EditFile,
|
||||
InlineAssist,
|
||||
TerminalInlineAssist,
|
||||
GenerateGitCommitMessage,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CompletionBody {
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
pub thread_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
pub prompt_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
pub intent: Option<CompletionIntent>,
|
||||
#[serde(skip_serializing_if = "Option::is_none", default)]
|
||||
pub mode: Option<CompletionMode>,
|
||||
pub provider: LanguageModelProvider,
|
||||
pub model: String,
|
||||
pub provider_request: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CompletionRequestStatus {
|
||||
Queued {
|
||||
position: usize,
|
||||
},
|
||||
Started,
|
||||
Failed {
|
||||
code: String,
|
||||
message: String,
|
||||
request_id: Uuid,
|
||||
/// Retry duration in seconds.
|
||||
retry_after: Option<f64>,
|
||||
},
|
||||
UsageUpdated {
|
||||
amount: usize,
|
||||
limit: UsageLimit,
|
||||
},
|
||||
ToolUseLimitReached,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CompletionEvent<T> {
|
||||
Status(CompletionRequestStatus),
|
||||
Event(T),
|
||||
}
|
||||
|
||||
impl<T> CompletionEvent<T> {
|
||||
pub fn into_status(self) -> Option<CompletionRequestStatus> {
|
||||
match self {
|
||||
Self::Status(status) => Some(status),
|
||||
Self::Event(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_event(self) -> Option<T> {
|
||||
match self {
|
||||
Self::Event(event) => Some(event),
|
||||
Self::Status(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct WebSearchBody {
|
||||
pub query: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct WebSearchResponse {
|
||||
pub results: Vec<WebSearchResult>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct WebSearchResult {
|
||||
pub title: String,
|
||||
pub url: String,
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct CountTokensBody {
|
||||
pub provider: LanguageModelProvider,
|
||||
pub model: String,
|
||||
pub provider_request: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct CountTokensResponse {
|
||||
pub tokens: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize)]
|
||||
pub struct LanguageModelId(pub Arc<str>);
|
||||
|
||||
impl std::fmt::Display for LanguageModelId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct LanguageModel {
|
||||
pub provider: LanguageModelProvider,
|
||||
pub id: LanguageModelId,
|
||||
pub display_name: String,
|
||||
pub max_token_count: usize,
|
||||
pub max_token_count_in_max_mode: Option<usize>,
|
||||
pub max_output_tokens: usize,
|
||||
pub supports_tools: bool,
|
||||
pub supports_images: bool,
|
||||
pub supports_thinking: bool,
|
||||
pub supports_max_mode: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ListModelsResponse {
|
||||
pub models: Vec<LanguageModel>,
|
||||
pub default_model: LanguageModelId,
|
||||
pub default_fast_model: LanguageModelId,
|
||||
pub recommended_models: Vec<LanguageModelId>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct GetSubscriptionResponse {
|
||||
pub plan: Plan,
|
||||
pub usage: Option<CurrentUsage>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CurrentUsage {
|
||||
pub model_requests: UsageData,
|
||||
pub edit_predictions: UsageData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct UsageData {
|
||||
pub used: u32,
|
||||
pub limit: UsageLimit,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_plan_deserialize_snake_case() {
|
||||
let plan = serde_json::from_value::<Plan>(json!("zed_free")).unwrap();
|
||||
assert_eq!(plan, Plan::ZedFree);
|
||||
|
||||
let plan = serde_json::from_value::<Plan>(json!("zed_pro")).unwrap();
|
||||
assert_eq!(plan, Plan::ZedPro);
|
||||
|
||||
let plan = serde_json::from_value::<Plan>(json!("zed_pro_trial")).unwrap();
|
||||
assert_eq!(plan, Plan::ZedProTrial);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plan_deserialize_aliases() {
|
||||
let plan = serde_json::from_value::<Plan>(json!("Free")).unwrap();
|
||||
assert_eq!(plan, Plan::ZedFree);
|
||||
|
||||
let plan = serde_json::from_value::<Plan>(json!("ZedPro")).unwrap();
|
||||
assert_eq!(plan, Plan::ZedPro);
|
||||
|
||||
let plan = serde_json::from_value::<Plan>(json!("ZedProTrial")).unwrap();
|
||||
assert_eq!(plan, Plan::ZedProTrial);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_usage_limit_from_str() {
|
||||
let limit = UsageLimit::from_str("unlimited").unwrap();
|
||||
assert!(matches!(limit, UsageLimit::Unlimited));
|
||||
|
||||
let limit = UsageLimit::from_str(&0.to_string()).unwrap();
|
||||
assert!(matches!(limit, UsageLimit::Limited(0)));
|
||||
|
||||
let limit = UsageLimit::from_str(&50.to_string()).unwrap();
|
||||
assert!(matches!(limit, UsageLimit::Limited(50)));
|
||||
|
||||
for value in ["not_a_number", "50xyz"] {
|
||||
let limit = UsageLimit::from_str(value);
|
||||
assert!(limit.is_err());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,13 +23,14 @@ async-stripe.workspace = true
|
|||
async-trait.workspace = true
|
||||
async-tungstenite.workspace = true
|
||||
aws-config = { version = "1.1.5" }
|
||||
aws-sdk-s3 = { version = "1.15.0" }
|
||||
aws-sdk-kinesis = "1.51.0"
|
||||
aws-sdk-s3 = { version = "1.15.0" }
|
||||
axum = { version = "0.6", features = ["json", "headers", "ws"] }
|
||||
axum-extra = { version = "0.4", features = ["erased-json"] }
|
||||
base64.workspace = true
|
||||
chrono.workspace = true
|
||||
clock.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
dashmap.workspace = true
|
||||
derive_more.workspace = true
|
||||
|
@ -75,7 +76,6 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json", "re
|
|||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
agent_settings.workspace = true
|
||||
|
|
|
@ -100,7 +100,6 @@ impl std::fmt::Display for SystemIdHeader {
|
|||
|
||||
pub fn routes(rpc_server: Arc<rpc::Server>) -> Router<(), Body> {
|
||||
Router::new()
|
||||
.route("/user", get(update_or_create_authenticated_user))
|
||||
.route("/users/look_up", get(look_up_user))
|
||||
.route("/users/:id/access_tokens", post(create_access_token))
|
||||
.route("/users/:id/refresh_llm_tokens", post(refresh_llm_tokens))
|
||||
|
@ -145,48 +144,6 @@ pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoR
|
|||
Ok::<_, Error>(next.run(req).await)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AuthenticatedUserParams {
|
||||
github_user_id: i32,
|
||||
github_login: String,
|
||||
github_email: Option<String>,
|
||||
github_name: Option<String>,
|
||||
github_user_created_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct AuthenticatedUserResponse {
|
||||
user: User,
|
||||
metrics_id: String,
|
||||
feature_flags: Vec<String>,
|
||||
}
|
||||
|
||||
async fn update_or_create_authenticated_user(
|
||||
Query(params): Query<AuthenticatedUserParams>,
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
) -> Result<Json<AuthenticatedUserResponse>> {
|
||||
let initial_channel_id = app.config.auto_join_channel_id;
|
||||
|
||||
let user = app
|
||||
.db
|
||||
.update_or_create_user_by_github_account(
|
||||
¶ms.github_login,
|
||||
params.github_user_id,
|
||||
params.github_email.as_deref(),
|
||||
params.github_name.as_deref(),
|
||||
params.github_user_created_at,
|
||||
initial_channel_id,
|
||||
)
|
||||
.await?;
|
||||
let metrics_id = app.db.get_user_metrics_id(user.id).await?;
|
||||
let feature_flags = app.db.get_user_flags(user.id).await?;
|
||||
Ok(Json(AuthenticatedUserResponse {
|
||||
user,
|
||||
metrics_id,
|
||||
feature_flags,
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct LookUpUserParams {
|
||||
identifier: String,
|
||||
|
@ -353,9 +310,9 @@ async fn refresh_llm_tokens(
|
|||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
struct UpdatePlanBody {
|
||||
pub plan: zed_llm_client::Plan,
|
||||
pub plan: cloud_llm_client::Plan,
|
||||
pub subscription_period: SubscriptionPeriod,
|
||||
pub usage: zed_llm_client::CurrentUsage,
|
||||
pub usage: cloud_llm_client::CurrentUsage,
|
||||
pub trial_started_at: Option<DateTime<Utc>>,
|
||||
pub is_usage_based_billing_enabled: bool,
|
||||
pub is_account_too_young: bool,
|
||||
|
@ -377,9 +334,9 @@ async fn update_plan(
|
|||
extract::Json(body): extract::Json<UpdatePlanBody>,
|
||||
) -> Result<Json<UpdatePlanResponse>> {
|
||||
let plan = match body.plan {
|
||||
zed_llm_client::Plan::ZedFree => proto::Plan::Free,
|
||||
zed_llm_client::Plan::ZedPro => proto::Plan::ZedPro,
|
||||
zed_llm_client::Plan::ZedProTrial => proto::Plan::ZedProTrial,
|
||||
cloud_llm_client::Plan::ZedFree => proto::Plan::Free,
|
||||
cloud_llm_client::Plan::ZedPro => proto::Plan::ZedPro,
|
||||
cloud_llm_client::Plan::ZedProTrial => proto::Plan::ZedProTrial,
|
||||
};
|
||||
|
||||
let update_user_plan = proto::UpdateUserPlan {
|
||||
|
@ -411,15 +368,15 @@ async fn update_plan(
|
|||
Ok(Json(UpdatePlanResponse {}))
|
||||
}
|
||||
|
||||
fn usage_limit_to_proto(limit: zed_llm_client::UsageLimit) -> proto::UsageLimit {
|
||||
fn usage_limit_to_proto(limit: cloud_llm_client::UsageLimit) -> proto::UsageLimit {
|
||||
proto::UsageLimit {
|
||||
variant: Some(match limit {
|
||||
zed_llm_client::UsageLimit::Limited(limit) => {
|
||||
cloud_llm_client::UsageLimit::Limited(limit) => {
|
||||
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
|
||||
limit: limit as u32,
|
||||
})
|
||||
}
|
||||
zed_llm_client::UsageLimit::Unlimited => {
|
||||
cloud_llm_client::UsageLimit::Unlimited => {
|
||||
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
|
||||
}
|
||||
}),
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
use anyhow::{Context as _, bail};
|
||||
use chrono::{DateTime, Utc};
|
||||
use cloud_llm_client::LanguageModelProvider;
|
||||
use collections::{HashMap, HashSet};
|
||||
use sea_orm::ActiveValue;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use stripe::{CancellationDetailsReason, EventObject, EventType, ListEvents, SubscriptionStatus};
|
||||
use util::{ResultExt, maybe};
|
||||
use zed_llm_client::LanguageModelProvider;
|
||||
|
||||
use crate::AppState;
|
||||
use crate::db::billing_subscription::{
|
||||
|
@ -87,6 +87,14 @@ async fn poll_stripe_events(
|
|||
stripe_client: &Arc<dyn StripeClient>,
|
||||
real_stripe_client: &stripe::Client,
|
||||
) -> anyhow::Result<()> {
|
||||
let feature_flags = app.db.list_feature_flags().await?;
|
||||
let sync_events_using_cloud = feature_flags
|
||||
.iter()
|
||||
.any(|flag| flag.flag == "cloud-stripe-events-polling" && flag.enabled_for_all);
|
||||
if sync_events_using_cloud {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
fn event_type_to_string(event_type: EventType) -> String {
|
||||
// Calling `to_string` on `stripe::EventType` members gives us a quoted string,
|
||||
// so we need to unquote it.
|
||||
|
@ -569,6 +577,14 @@ async fn sync_model_request_usage_with_stripe(
|
|||
llm_db: &Arc<LlmDatabase>,
|
||||
stripe_billing: &Arc<StripeBilling>,
|
||||
) -> anyhow::Result<()> {
|
||||
let feature_flags = app.db.list_feature_flags().await?;
|
||||
let sync_model_request_usage_using_cloud = feature_flags
|
||||
.iter()
|
||||
.any(|flag| flag.flag == "cloud-stripe-usage-meters-sync" && flag.enabled_for_all);
|
||||
if sync_model_request_usage_using_cloud {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
log::info!("Stripe usage sync: Starting");
|
||||
let started_at = Utc::now();
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ use axum::{
|
|||
use chrono::{NaiveDateTime, SecondsFormat};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::api::AuthenticatedUserParams;
|
||||
use crate::db::ContributorSelector;
|
||||
use crate::{AppState, Result};
|
||||
|
||||
|
@ -104,9 +103,18 @@ impl RenovateBot {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AddContributorBody {
|
||||
github_user_id: i32,
|
||||
github_login: String,
|
||||
github_email: Option<String>,
|
||||
github_name: Option<String>,
|
||||
github_user_created_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
async fn add_contributor(
|
||||
Extension(app): Extension<Arc<AppState>>,
|
||||
extract::Json(params): extract::Json<AuthenticatedUserParams>,
|
||||
extract::Json(params): extract::Json<AddContributorBody>,
|
||||
) -> Result<()> {
|
||||
let initial_channel_id = app.config.auto_join_channel_id;
|
||||
app.db
|
||||
|
|
|
@ -95,7 +95,7 @@ pub enum SubscriptionKind {
|
|||
ZedFree,
|
||||
}
|
||||
|
||||
impl From<SubscriptionKind> for zed_llm_client::Plan {
|
||||
impl From<SubscriptionKind> for cloud_llm_client::Plan {
|
||||
fn from(value: SubscriptionKind) -> Self {
|
||||
match value {
|
||||
SubscriptionKind::ZedPro => Self::ZedPro,
|
||||
|
|
|
@ -6,11 +6,11 @@ mod tables;
|
|||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use cloud_llm_client::LanguageModelProvider;
|
||||
use collections::HashMap;
|
||||
pub use ids::*;
|
||||
pub use seed::*;
|
||||
pub use tables::*;
|
||||
use zed_llm_client::LanguageModelProvider;
|
||||
|
||||
#[cfg(test)]
|
||||
pub use tests::TestLlmDb;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use cloud_llm_client::LanguageModelProvider;
|
||||
use pretty_assertions::assert_eq;
|
||||
use zed_llm_client::LanguageModelProvider;
|
||||
|
||||
use crate::llm::db::LlmDatabase;
|
||||
use crate::test_llm_db;
|
||||
|
|
|
@ -4,12 +4,12 @@ use crate::llm::{AGENT_EXTENDED_TRIAL_FEATURE_FLAG, BYPASS_ACCOUNT_AGE_CHECK_FEA
|
|||
use crate::{Config, db::billing_preference};
|
||||
use anyhow::{Context as _, Result};
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use cloud_llm_client::Plan;
|
||||
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
use zed_llm_client::Plan;
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
|
|
@ -23,6 +23,7 @@ use anyhow::{Context as _, anyhow, bail};
|
|||
use async_tungstenite::tungstenite::{
|
||||
Message as TungsteniteMessage, protocol::CloseFrame as TungsteniteCloseFrame,
|
||||
};
|
||||
use axum::headers::UserAgent;
|
||||
use axum::{
|
||||
Extension, Router, TypedHeader,
|
||||
body::Body,
|
||||
|
@ -750,6 +751,7 @@ impl Server {
|
|||
address: String,
|
||||
principal: Principal,
|
||||
zed_version: ZedVersion,
|
||||
user_agent: Option<String>,
|
||||
geoip_country_code: Option<String>,
|
||||
system_id: Option<String>,
|
||||
send_connection_id: Option<oneshot::Sender<ConnectionId>>,
|
||||
|
@ -762,9 +764,14 @@ impl Server {
|
|||
user_id=field::Empty,
|
||||
login=field::Empty,
|
||||
impersonator=field::Empty,
|
||||
user_agent=field::Empty,
|
||||
geoip_country_code=field::Empty
|
||||
);
|
||||
principal.update_span(&span);
|
||||
if let Some(user_agent) = user_agent {
|
||||
span.record("user_agent", user_agent);
|
||||
}
|
||||
|
||||
if let Some(country_code) = geoip_country_code.as_ref() {
|
||||
span.record("geoip_country_code", country_code);
|
||||
}
|
||||
|
@ -1172,6 +1179,7 @@ pub async fn handle_websocket_request(
|
|||
ConnectInfo(socket_address): ConnectInfo<SocketAddr>,
|
||||
Extension(server): Extension<Arc<Server>>,
|
||||
Extension(principal): Extension<Principal>,
|
||||
user_agent: Option<TypedHeader<UserAgent>>,
|
||||
country_code_header: Option<TypedHeader<CloudflareIpCountryHeader>>,
|
||||
system_id_header: Option<TypedHeader<SystemIdHeader>>,
|
||||
ws: WebSocketUpgrade,
|
||||
|
@ -1227,6 +1235,7 @@ pub async fn handle_websocket_request(
|
|||
socket_address,
|
||||
principal,
|
||||
version,
|
||||
user_agent.map(|header| header.to_string()),
|
||||
country_code_header.map(|header| header.to_string()),
|
||||
system_id_header.map(|header| header.to_string()),
|
||||
None,
|
||||
|
@ -2859,12 +2868,12 @@ async fn make_update_user_plan_message(
|
|||
}
|
||||
|
||||
fn model_requests_limit(
|
||||
plan: zed_llm_client::Plan,
|
||||
plan: cloud_llm_client::Plan,
|
||||
feature_flags: &Vec<String>,
|
||||
) -> zed_llm_client::UsageLimit {
|
||||
) -> cloud_llm_client::UsageLimit {
|
||||
match plan.model_requests_limit() {
|
||||
zed_llm_client::UsageLimit::Limited(limit) => {
|
||||
let limit = if plan == zed_llm_client::Plan::ZedProTrial
|
||||
cloud_llm_client::UsageLimit::Limited(limit) => {
|
||||
let limit = if plan == cloud_llm_client::Plan::ZedProTrial
|
||||
&& feature_flags
|
||||
.iter()
|
||||
.any(|flag| flag == AGENT_EXTENDED_TRIAL_FEATURE_FLAG)
|
||||
|
@ -2874,9 +2883,9 @@ fn model_requests_limit(
|
|||
limit
|
||||
};
|
||||
|
||||
zed_llm_client::UsageLimit::Limited(limit)
|
||||
cloud_llm_client::UsageLimit::Limited(limit)
|
||||
}
|
||||
zed_llm_client::UsageLimit::Unlimited => zed_llm_client::UsageLimit::Unlimited,
|
||||
cloud_llm_client::UsageLimit::Unlimited => cloud_llm_client::UsageLimit::Unlimited,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2886,21 +2895,21 @@ fn subscription_usage_to_proto(
|
|||
feature_flags: &Vec<String>,
|
||||
) -> proto::SubscriptionUsage {
|
||||
let plan = match plan {
|
||||
proto::Plan::Free => zed_llm_client::Plan::ZedFree,
|
||||
proto::Plan::ZedPro => zed_llm_client::Plan::ZedPro,
|
||||
proto::Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
|
||||
proto::Plan::Free => cloud_llm_client::Plan::ZedFree,
|
||||
proto::Plan::ZedPro => cloud_llm_client::Plan::ZedPro,
|
||||
proto::Plan::ZedProTrial => cloud_llm_client::Plan::ZedProTrial,
|
||||
};
|
||||
|
||||
proto::SubscriptionUsage {
|
||||
model_requests_usage_amount: usage.model_requests as u32,
|
||||
model_requests_usage_limit: Some(proto::UsageLimit {
|
||||
variant: Some(match model_requests_limit(plan, feature_flags) {
|
||||
zed_llm_client::UsageLimit::Limited(limit) => {
|
||||
cloud_llm_client::UsageLimit::Limited(limit) => {
|
||||
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
|
||||
limit: limit as u32,
|
||||
})
|
||||
}
|
||||
zed_llm_client::UsageLimit::Unlimited => {
|
||||
cloud_llm_client::UsageLimit::Unlimited => {
|
||||
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
|
||||
}
|
||||
}),
|
||||
|
@ -2908,12 +2917,12 @@ fn subscription_usage_to_proto(
|
|||
edit_predictions_usage_amount: usage.edit_predictions as u32,
|
||||
edit_predictions_usage_limit: Some(proto::UsageLimit {
|
||||
variant: Some(match plan.edit_predictions_limit() {
|
||||
zed_llm_client::UsageLimit::Limited(limit) => {
|
||||
cloud_llm_client::UsageLimit::Limited(limit) => {
|
||||
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
|
||||
limit: limit as u32,
|
||||
})
|
||||
}
|
||||
zed_llm_client::UsageLimit::Unlimited => {
|
||||
cloud_llm_client::UsageLimit::Unlimited => {
|
||||
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
|
||||
}
|
||||
}),
|
||||
|
@ -2926,21 +2935,21 @@ fn make_default_subscription_usage(
|
|||
feature_flags: &Vec<String>,
|
||||
) -> proto::SubscriptionUsage {
|
||||
let plan = match plan {
|
||||
proto::Plan::Free => zed_llm_client::Plan::ZedFree,
|
||||
proto::Plan::ZedPro => zed_llm_client::Plan::ZedPro,
|
||||
proto::Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
|
||||
proto::Plan::Free => cloud_llm_client::Plan::ZedFree,
|
||||
proto::Plan::ZedPro => cloud_llm_client::Plan::ZedPro,
|
||||
proto::Plan::ZedProTrial => cloud_llm_client::Plan::ZedProTrial,
|
||||
};
|
||||
|
||||
proto::SubscriptionUsage {
|
||||
model_requests_usage_amount: 0,
|
||||
model_requests_usage_limit: Some(proto::UsageLimit {
|
||||
variant: Some(match model_requests_limit(plan, feature_flags) {
|
||||
zed_llm_client::UsageLimit::Limited(limit) => {
|
||||
cloud_llm_client::UsageLimit::Limited(limit) => {
|
||||
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
|
||||
limit: limit as u32,
|
||||
})
|
||||
}
|
||||
zed_llm_client::UsageLimit::Unlimited => {
|
||||
cloud_llm_client::UsageLimit::Unlimited => {
|
||||
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
|
||||
}
|
||||
}),
|
||||
|
@ -2948,12 +2957,12 @@ fn make_default_subscription_usage(
|
|||
edit_predictions_usage_amount: 0,
|
||||
edit_predictions_usage_limit: Some(proto::UsageLimit {
|
||||
variant: Some(match plan.edit_predictions_limit() {
|
||||
zed_llm_client::UsageLimit::Limited(limit) => {
|
||||
cloud_llm_client::UsageLimit::Limited(limit) => {
|
||||
proto::usage_limit::Variant::Limited(proto::usage_limit::Limited {
|
||||
limit: limit as u32,
|
||||
})
|
||||
}
|
||||
zed_llm_client::UsageLimit::Unlimited => {
|
||||
cloud_llm_client::UsageLimit::Unlimited => {
|
||||
proto::usage_limit::Variant::Unlimited(proto::usage_limit::Unlimited {})
|
||||
}
|
||||
}),
|
||||
|
|
|
@ -256,6 +256,7 @@ impl TestServer {
|
|||
ZedVersion(SemanticVersion::new(1, 0, 0)),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(connection_id_tx),
|
||||
Executor::Deterministic(cx.background_executor().clone()),
|
||||
None,
|
||||
|
|
|
@ -158,6 +158,7 @@ impl Client {
|
|||
pub fn stdio(
|
||||
server_id: ContextServerId,
|
||||
binary: ModelContextServerBinary,
|
||||
working_directory: &Option<PathBuf>,
|
||||
cx: AsyncApp,
|
||||
) -> Result<Self> {
|
||||
log::info!(
|
||||
|
@ -172,7 +173,7 @@ impl Client {
|
|||
.map(|name| name.to_string_lossy().to_string())
|
||||
.unwrap_or_else(String::new);
|
||||
|
||||
let transport = Arc::new(StdioTransport::new(binary, &cx)?);
|
||||
let transport = Arc::new(StdioTransport::new(binary, working_directory, &cx)?);
|
||||
Self::new(server_id, server_name.into(), transport, cx)
|
||||
}
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ impl std::fmt::Debug for ContextServerCommand {
|
|||
}
|
||||
|
||||
enum ContextServerTransport {
|
||||
Stdio(ContextServerCommand),
|
||||
Stdio(ContextServerCommand, Option<PathBuf>),
|
||||
Custom(Arc<dyn crate::transport::Transport>),
|
||||
}
|
||||
|
||||
|
@ -64,11 +64,18 @@ pub struct ContextServer {
|
|||
}
|
||||
|
||||
impl ContextServer {
|
||||
pub fn stdio(id: ContextServerId, command: ContextServerCommand) -> Self {
|
||||
pub fn stdio(
|
||||
id: ContextServerId,
|
||||
command: ContextServerCommand,
|
||||
working_directory: Option<Arc<Path>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
id,
|
||||
client: RwLock::new(None),
|
||||
configuration: ContextServerTransport::Stdio(command),
|
||||
configuration: ContextServerTransport::Stdio(
|
||||
command,
|
||||
working_directory.map(|directory| directory.to_path_buf()),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -90,13 +97,14 @@ impl ContextServer {
|
|||
|
||||
pub async fn start(self: Arc<Self>, cx: &AsyncApp) -> Result<()> {
|
||||
let client = match &self.configuration {
|
||||
ContextServerTransport::Stdio(command) => Client::stdio(
|
||||
ContextServerTransport::Stdio(command, working_directory) => Client::stdio(
|
||||
client::ContextServerId(self.id.0.clone()),
|
||||
client::ModelContextServerBinary {
|
||||
executable: Path::new(&command.path).to_path_buf(),
|
||||
args: command.args.clone(),
|
||||
env: command.env.clone(),
|
||||
},
|
||||
working_directory,
|
||||
cx.clone(),
|
||||
)?,
|
||||
ContextServerTransport::Custom(transport) => Client::new(
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use std::path::PathBuf;
|
||||
use std::pin::Pin;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
|
@ -22,7 +23,11 @@ pub struct StdioTransport {
|
|||
}
|
||||
|
||||
impl StdioTransport {
|
||||
pub fn new(binary: ModelContextServerBinary, cx: &AsyncApp) -> Result<Self> {
|
||||
pub fn new(
|
||||
binary: ModelContextServerBinary,
|
||||
working_directory: &Option<PathBuf>,
|
||||
cx: &AsyncApp,
|
||||
) -> Result<Self> {
|
||||
let mut command = util::command::new_smol_command(&binary.executable);
|
||||
command
|
||||
.args(&binary.args)
|
||||
|
@ -32,6 +37,10 @@ impl StdioTransport {
|
|||
.stderr(std::process::Stdio::piped())
|
||||
.kill_on_drop(true);
|
||||
|
||||
if let Some(working_directory) = working_directory {
|
||||
command.current_dir(working_directory);
|
||||
}
|
||||
|
||||
let mut server = command.spawn().with_context(|| {
|
||||
format!(
|
||||
"failed to spawn command. (path={:?}, args={:?})",
|
||||
|
|
|
@ -7,17 +7,17 @@ license = "GPL-3.0-or-later"
|
|||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
clap.workspace = true
|
||||
command_palette.workspace = true
|
||||
gpui.workspace = true
|
||||
mdbook = "0.4.40"
|
||||
regex.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
regex.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed.workspace = true
|
||||
gpui.workspace = true
|
||||
command_palette.workspace = true
|
||||
zlog.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
use anyhow::Result;
|
||||
use clap::{Arg, ArgMatches, Command};
|
||||
use anyhow::{Context, Result};
|
||||
use mdbook::BookItem;
|
||||
use mdbook::book::{Book, Chapter};
|
||||
use mdbook::preprocess::CmdPreprocessor;
|
||||
use regex::Regex;
|
||||
use settings::KeymapFile;
|
||||
use std::collections::HashSet;
|
||||
use std::borrow::Cow;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::io::{self, Read};
|
||||
use std::process;
|
||||
use std::sync::LazyLock;
|
||||
use util::paths::PathExt;
|
||||
|
||||
static KEYMAP_MACOS: LazyLock<KeymapFile> = LazyLock::new(|| {
|
||||
load_keymap("keymaps/default-macos.json").expect("Failed to load MacOS keymap")
|
||||
|
@ -20,60 +21,68 @@ static KEYMAP_LINUX: LazyLock<KeymapFile> = LazyLock::new(|| {
|
|||
|
||||
static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(dump_all_gpui_actions);
|
||||
|
||||
pub fn make_app() -> Command {
|
||||
Command::new("zed-docs-preprocessor")
|
||||
.about("Preprocesses Zed Docs content to provide rich action & keybinding support and more")
|
||||
.subcommand(
|
||||
Command::new("supports")
|
||||
.arg(Arg::new("renderer").required(true))
|
||||
.about("Check whether a renderer is supported by this preprocessor"),
|
||||
)
|
||||
}
|
||||
const FRONT_MATTER_COMMENT: &'static str = "<!-- ZED_META {} -->";
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let matches = make_app().get_matches();
|
||||
zlog::init();
|
||||
zlog::init_output_stderr();
|
||||
// call a zed:: function so everything in `zed` crate is linked and
|
||||
// all actions in the actual app are registered
|
||||
zed::stdout_is_a_pty();
|
||||
let args = std::env::args().skip(1).collect::<Vec<_>>();
|
||||
|
||||
if let Some(sub_args) = matches.subcommand_matches("supports") {
|
||||
handle_supports(sub_args);
|
||||
} else {
|
||||
handle_preprocessing()?;
|
||||
match args.get(0).map(String::as_str) {
|
||||
Some("supports") => {
|
||||
let renderer = args.get(1).expect("Required argument");
|
||||
let supported = renderer != "not-supported";
|
||||
if supported {
|
||||
process::exit(0);
|
||||
} else {
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
Some("postprocess") => handle_postprocessing()?,
|
||||
_ => handle_preprocessing()?,
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
enum Error {
|
||||
enum PreprocessorError {
|
||||
ActionNotFound { action_name: String },
|
||||
DeprecatedActionUsed { used: String, should_be: String },
|
||||
InvalidFrontmatterLine(String),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
impl PreprocessorError {
|
||||
fn new_for_not_found_action(action_name: String) -> Self {
|
||||
for action in &*ALL_ACTIONS {
|
||||
for alias in action.deprecated_aliases {
|
||||
if alias == &action_name {
|
||||
return Error::DeprecatedActionUsed {
|
||||
return PreprocessorError::DeprecatedActionUsed {
|
||||
used: action_name.clone(),
|
||||
should_be: action.name.to_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Error::ActionNotFound {
|
||||
PreprocessorError::ActionNotFound {
|
||||
action_name: action_name.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Error {
|
||||
impl std::fmt::Display for PreprocessorError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Error::ActionNotFound { action_name } => write!(f, "Action not found: {}", action_name),
|
||||
Error::DeprecatedActionUsed { used, should_be } => write!(
|
||||
PreprocessorError::InvalidFrontmatterLine(line) => {
|
||||
write!(f, "Invalid frontmatter line: {}", line)
|
||||
}
|
||||
PreprocessorError::ActionNotFound { action_name } => {
|
||||
write!(f, "Action not found: {}", action_name)
|
||||
}
|
||||
PreprocessorError::DeprecatedActionUsed { used, should_be } => write!(
|
||||
f,
|
||||
"Deprecated action used: {} should be {}",
|
||||
used, should_be
|
||||
|
@ -89,8 +98,9 @@ fn handle_preprocessing() -> Result<()> {
|
|||
|
||||
let (_ctx, mut book) = CmdPreprocessor::parse_input(input.as_bytes())?;
|
||||
|
||||
let mut errors = HashSet::<Error>::new();
|
||||
let mut errors = HashSet::<PreprocessorError>::new();
|
||||
|
||||
handle_frontmatter(&mut book, &mut errors);
|
||||
template_and_validate_keybindings(&mut book, &mut errors);
|
||||
template_and_validate_actions(&mut book, &mut errors);
|
||||
|
||||
|
@ -108,19 +118,41 @@ fn handle_preprocessing() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_supports(sub_args: &ArgMatches) -> ! {
|
||||
let renderer = sub_args
|
||||
.get_one::<String>("renderer")
|
||||
.expect("Required argument");
|
||||
let supported = renderer != "not-supported";
|
||||
if supported {
|
||||
process::exit(0);
|
||||
} else {
|
||||
process::exit(1);
|
||||
}
|
||||
fn handle_frontmatter(book: &mut Book, errors: &mut HashSet<PreprocessorError>) {
|
||||
let frontmatter_regex = Regex::new(r"(?s)^\s*---(.*?)---").unwrap();
|
||||
for_each_chapter_mut(book, |chapter| {
|
||||
let new_content = frontmatter_regex.replace(&chapter.content, |caps: ®ex::Captures| {
|
||||
let frontmatter = caps[1].trim();
|
||||
let frontmatter = frontmatter.trim_matches(&[' ', '-', '\n']);
|
||||
let mut metadata = HashMap::<String, String>::default();
|
||||
for line in frontmatter.lines() {
|
||||
let Some((name, value)) = line.split_once(':') else {
|
||||
errors.insert(PreprocessorError::InvalidFrontmatterLine(format!(
|
||||
"{}: {}",
|
||||
chapter_breadcrumbs(&chapter),
|
||||
line
|
||||
)));
|
||||
continue;
|
||||
};
|
||||
let name = name.trim();
|
||||
let value = value.trim();
|
||||
metadata.insert(name.to_string(), value.to_string());
|
||||
}
|
||||
FRONT_MATTER_COMMENT.replace(
|
||||
"{}",
|
||||
&serde_json::to_string(&metadata).expect("Failed to serialize metadata"),
|
||||
)
|
||||
});
|
||||
match new_content {
|
||||
Cow::Owned(content) => {
|
||||
chapter.content = content;
|
||||
}
|
||||
Cow::Borrowed(_) => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet<Error>) {
|
||||
fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet<PreprocessorError>) {
|
||||
let regex = Regex::new(r"\{#kb (.*?)\}").unwrap();
|
||||
|
||||
for_each_chapter_mut(book, |chapter| {
|
||||
|
@ -128,7 +160,9 @@ fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet<Error
|
|||
.replace_all(&chapter.content, |caps: ®ex::Captures| {
|
||||
let action = caps[1].trim();
|
||||
if find_action_by_name(action).is_none() {
|
||||
errors.insert(Error::new_for_not_found_action(action.to_string()));
|
||||
errors.insert(PreprocessorError::new_for_not_found_action(
|
||||
action.to_string(),
|
||||
));
|
||||
return String::new();
|
||||
}
|
||||
let macos_binding = find_binding("macos", action).unwrap_or_default();
|
||||
|
@ -144,7 +178,7 @@ fn template_and_validate_keybindings(book: &mut Book, errors: &mut HashSet<Error
|
|||
});
|
||||
}
|
||||
|
||||
fn template_and_validate_actions(book: &mut Book, errors: &mut HashSet<Error>) {
|
||||
fn template_and_validate_actions(book: &mut Book, errors: &mut HashSet<PreprocessorError>) {
|
||||
let regex = Regex::new(r"\{#action (.*?)\}").unwrap();
|
||||
|
||||
for_each_chapter_mut(book, |chapter| {
|
||||
|
@ -152,7 +186,9 @@ fn template_and_validate_actions(book: &mut Book, errors: &mut HashSet<Error>) {
|
|||
.replace_all(&chapter.content, |caps: ®ex::Captures| {
|
||||
let name = caps[1].trim();
|
||||
let Some(action) = find_action_by_name(name) else {
|
||||
errors.insert(Error::new_for_not_found_action(name.to_string()));
|
||||
errors.insert(PreprocessorError::new_for_not_found_action(
|
||||
name.to_string(),
|
||||
));
|
||||
return String::new();
|
||||
};
|
||||
format!("<code class=\"hljs\">{}</code>", &action.human_name)
|
||||
|
@ -217,6 +253,13 @@ fn name_for_action(action_as_str: String) -> String {
|
|||
.unwrap_or(action_as_str)
|
||||
}
|
||||
|
||||
fn chapter_breadcrumbs(chapter: &Chapter) -> String {
|
||||
let mut breadcrumbs = Vec::with_capacity(chapter.parent_names.len() + 1);
|
||||
breadcrumbs.extend(chapter.parent_names.iter().map(String::as_str));
|
||||
breadcrumbs.push(chapter.name.as_str());
|
||||
format!("[{:?}] {}", chapter.source_path, breadcrumbs.join(" > "))
|
||||
}
|
||||
|
||||
fn load_keymap(asset_path: &str) -> Result<KeymapFile> {
|
||||
let content = util::asset_str::<settings::SettingsAssets>(asset_path);
|
||||
KeymapFile::parse(content.as_ref())
|
||||
|
@ -254,3 +297,126 @@ fn dump_all_gpui_actions() -> Vec<ActionDef> {
|
|||
|
||||
return actions;
|
||||
}
|
||||
|
||||
fn handle_postprocessing() -> Result<()> {
|
||||
let logger = zlog::scoped!("render");
|
||||
let mut ctx = mdbook::renderer::RenderContext::from_json(io::stdin())?;
|
||||
let output = ctx
|
||||
.config
|
||||
.get_mut("output")
|
||||
.expect("has output")
|
||||
.as_table_mut()
|
||||
.expect("output is table");
|
||||
let zed_html = output.remove("zed-html").expect("zed-html output defined");
|
||||
let default_description = zed_html
|
||||
.get("default-description")
|
||||
.expect("Default description not found")
|
||||
.as_str()
|
||||
.expect("Default description not a string")
|
||||
.to_string();
|
||||
let default_title = zed_html
|
||||
.get("default-title")
|
||||
.expect("Default title not found")
|
||||
.as_str()
|
||||
.expect("Default title not a string")
|
||||
.to_string();
|
||||
|
||||
output.insert("html".to_string(), zed_html);
|
||||
mdbook::Renderer::render(&mdbook::renderer::HtmlHandlebars::new(), &ctx)?;
|
||||
let ignore_list = ["toc.html"];
|
||||
|
||||
let root_dir = ctx.destination.clone();
|
||||
let mut files = Vec::with_capacity(128);
|
||||
let mut queue = Vec::with_capacity(64);
|
||||
queue.push(root_dir.clone());
|
||||
while let Some(dir) = queue.pop() {
|
||||
for entry in std::fs::read_dir(&dir).context(dir.to_sanitized_string())? {
|
||||
let Ok(entry) = entry else {
|
||||
continue;
|
||||
};
|
||||
let file_type = entry.file_type().context("Failed to determine file type")?;
|
||||
if file_type.is_dir() {
|
||||
queue.push(entry.path());
|
||||
}
|
||||
if file_type.is_file()
|
||||
&& matches!(
|
||||
entry.path().extension().and_then(std::ffi::OsStr::to_str),
|
||||
Some("html")
|
||||
)
|
||||
{
|
||||
if ignore_list.contains(&&*entry.file_name().to_string_lossy()) {
|
||||
zlog::info!(logger => "Ignoring {}", entry.path().to_string_lossy());
|
||||
} else {
|
||||
files.push(entry.path());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
zlog::info!(logger => "Processing {} `.html` files", files.len());
|
||||
let meta_regex = Regex::new(&FRONT_MATTER_COMMENT.replace("{}", "(.*)")).unwrap();
|
||||
for file in files {
|
||||
let contents = std::fs::read_to_string(&file)?;
|
||||
let mut meta_description = None;
|
||||
let mut meta_title = None;
|
||||
let contents = meta_regex.replace(&contents, |caps: ®ex::Captures| {
|
||||
let metadata: HashMap<String, String> = serde_json::from_str(&caps[1]).with_context(|| format!("JSON Metadata: {:?}", &caps[1])).expect("Failed to deserialize metadata");
|
||||
for (kind, content) in metadata {
|
||||
match kind.as_str() {
|
||||
"description" => {
|
||||
meta_description = Some(content);
|
||||
}
|
||||
"title" => {
|
||||
meta_title = Some(content);
|
||||
}
|
||||
_ => {
|
||||
zlog::warn!(logger => "Unrecognized frontmatter key: {} in {:?}", kind, pretty_path(&file, &root_dir));
|
||||
}
|
||||
}
|
||||
}
|
||||
String::new()
|
||||
});
|
||||
let meta_description = meta_description.as_ref().unwrap_or_else(|| {
|
||||
zlog::warn!(logger => "No meta description found for {:?}", pretty_path(&file, &root_dir));
|
||||
&default_description
|
||||
});
|
||||
let page_title = extract_title_from_page(&contents, pretty_path(&file, &root_dir));
|
||||
let meta_title = meta_title.as_ref().unwrap_or_else(|| {
|
||||
zlog::debug!(logger => "No meta title found for {:?}", pretty_path(&file, &root_dir));
|
||||
&default_title
|
||||
});
|
||||
let meta_title = format!("{} | {}", page_title, meta_title);
|
||||
zlog::trace!(logger => "Updating {:?}", pretty_path(&file, &root_dir));
|
||||
let contents = contents.replace("#description#", meta_description);
|
||||
let contents = TITLE_REGEX
|
||||
.replace(&contents, |_: ®ex::Captures| {
|
||||
format!("<title>{}</title>", meta_title)
|
||||
})
|
||||
.to_string();
|
||||
// let contents = contents.replace("#title#", &meta_title);
|
||||
std::fs::write(file, contents)?;
|
||||
}
|
||||
return Ok(());
|
||||
|
||||
fn pretty_path<'a>(
|
||||
path: &'a std::path::PathBuf,
|
||||
root: &'a std::path::PathBuf,
|
||||
) -> &'a std::path::Path {
|
||||
&path.strip_prefix(&root).unwrap_or(&path)
|
||||
}
|
||||
const TITLE_REGEX: std::cell::LazyCell<Regex> =
|
||||
std::cell::LazyCell::new(|| Regex::new(r"<title>\s*(.*?)\s*</title>").unwrap());
|
||||
fn extract_title_from_page(contents: &str, pretty_path: &std::path::Path) -> String {
|
||||
let title_tag_contents = &TITLE_REGEX
|
||||
.captures(&contents)
|
||||
.with_context(|| format!("Failed to find title in {:?}", pretty_path))
|
||||
.expect("Page has <title> element")[1];
|
||||
let title = title_tag_contents
|
||||
.trim()
|
||||
.strip_suffix("- Zed")
|
||||
.unwrap_or(title_tag_contents)
|
||||
.trim()
|
||||
.to_string();
|
||||
title
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,7 +65,7 @@ use display_map::*;
|
|||
pub use display_map::{ChunkRenderer, ChunkRendererContext, DisplayPoint, FoldPlaceholder};
|
||||
pub use editor_settings::{
|
||||
CurrentLineHighlight, DocumentColorsRenderMode, EditorSettings, HideMouseMode,
|
||||
ScrollBeyondLastLine, ScrollbarAxes, SearchSettings, ShowScrollbar,
|
||||
ScrollBeyondLastLine, ScrollbarAxes, SearchSettings, ShowMinimap, ShowScrollbar,
|
||||
};
|
||||
use editor_settings::{GoToDefinitionFallback, Minimap as MinimapSettings};
|
||||
pub use editor_settings_controls::*;
|
||||
|
|
|
@ -19,8 +19,8 @@ path = "src/explorer.rs"
|
|||
|
||||
[dependencies]
|
||||
agent.workspace = true
|
||||
agent_ui.workspace = true
|
||||
agent_settings.workspace = true
|
||||
agent_ui.workspace = true
|
||||
anyhow.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
assistant_tools.workspace = true
|
||||
|
@ -29,6 +29,7 @@ buffer_diff.workspace = true
|
|||
chrono.workspace = true
|
||||
clap.workspace = true
|
||||
client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
debug_adapter_extension.workspace = true
|
||||
dirs.workspace = true
|
||||
|
@ -68,4 +69,3 @@ util.workspace = true
|
|||
uuid.workspace = true
|
||||
watch.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
|
|
@ -15,11 +15,11 @@ use agent_settings::AgentProfileId;
|
|||
use anyhow::{Result, anyhow};
|
||||
use async_trait::async_trait;
|
||||
use buffer_diff::DiffHunkStatus;
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use collections::HashMap;
|
||||
use futures::{FutureExt as _, StreamExt, channel::mpsc, select_biased};
|
||||
use gpui::{App, AppContext, AsyncApp, Entity};
|
||||
use language_model::{LanguageModel, Role, StopReason};
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
pub const THREAD_EVENT_TIMEOUT: Duration = Duration::from_secs(60 * 2);
|
||||
|
||||
|
|
|
@ -106,7 +106,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn language_server_initialization_options(
|
||||
|
@ -131,7 +131,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn language_server_workspace_configuration(
|
||||
|
@ -154,7 +154,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn language_server_additional_initialization_options(
|
||||
|
@ -179,7 +179,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn language_server_additional_workspace_configuration(
|
||||
|
@ -204,7 +204,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn labels_for_completions(
|
||||
|
@ -230,7 +230,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn labels_for_symbols(
|
||||
|
@ -256,7 +256,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn complete_slash_command_argument(
|
||||
|
@ -275,7 +275,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn run_slash_command(
|
||||
|
@ -301,7 +301,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn context_server_command(
|
||||
|
@ -320,7 +320,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn context_server_configuration(
|
||||
|
@ -347,7 +347,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn suggest_docs_packages(&self, provider: Arc<str>) -> Result<Vec<String>> {
|
||||
|
@ -362,7 +362,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn index_docs(
|
||||
|
@ -388,7 +388,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn get_dap_binary(
|
||||
|
@ -410,7 +410,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
async fn dap_request_kind(
|
||||
&self,
|
||||
|
@ -427,7 +427,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn dap_config_to_scenario(&self, config: ZedDebugConfig) -> Result<DebugScenario> {
|
||||
|
@ -441,7 +441,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
|
||||
async fn dap_locator_create_scenario(
|
||||
|
@ -465,7 +465,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
async fn run_dap_locator(
|
||||
&self,
|
||||
|
@ -481,7 +481,7 @@ impl extension::Extension for WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
})
|
||||
.await
|
||||
.await?
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -761,7 +761,7 @@ impl WasmExtension {
|
|||
.with_context(|| format!("failed to load wasm extension {}", manifest.id))
|
||||
}
|
||||
|
||||
pub async fn call<T, Fn>(&self, f: Fn) -> T
|
||||
pub async fn call<T, Fn>(&self, f: Fn) -> Result<T>
|
||||
where
|
||||
T: 'static + Send,
|
||||
Fn: 'static
|
||||
|
@ -777,8 +777,19 @@ impl WasmExtension {
|
|||
}
|
||||
.boxed()
|
||||
}))
|
||||
.expect("wasm extension channel should not be closed yet");
|
||||
return_rx.await.expect("wasm extension channel")
|
||||
.map_err(|_| {
|
||||
anyhow!(
|
||||
"wasm extension channel should not be closed yet, extension {} (id {})",
|
||||
self.manifest.name,
|
||||
self.manifest.id,
|
||||
)
|
||||
})?;
|
||||
return_rx.await.with_context(|| {
|
||||
format!(
|
||||
"wasm extension channel, extension {} (id {})",
|
||||
self.manifest.name, self.manifest.id,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -799,8 +810,19 @@ impl WasmState {
|
|||
}
|
||||
.boxed_local()
|
||||
}))
|
||||
.expect("main thread message channel should not be closed yet");
|
||||
async move { return_rx.await.expect("main thread message channel") }
|
||||
.unwrap_or_else(|_| {
|
||||
panic!(
|
||||
"main thread message channel should not be closed yet, extension {} (id {})",
|
||||
self.manifest.name, self.manifest.id,
|
||||
)
|
||||
});
|
||||
let name = self.manifest.name.clone();
|
||||
let id = self.manifest.id.clone();
|
||||
async move {
|
||||
return_rx.await.unwrap_or_else(|_| {
|
||||
panic!("main thread message channel, extension {name} (id {id})")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn work_dir(&self) -> PathBuf {
|
||||
|
|
|
@ -1404,14 +1404,21 @@ impl PickerDelegate for FileFinderDelegate {
|
|||
} else {
|
||||
let path_position = PathWithPosition::parse_str(&raw_query);
|
||||
|
||||
#[cfg(windows)]
|
||||
let raw_query = raw_query.trim().to_owned().replace("/", "\\");
|
||||
#[cfg(not(windows))]
|
||||
let raw_query = raw_query.trim().to_owned();
|
||||
|
||||
let file_query_end = if path_position.path.to_str().unwrap_or(&raw_query) == raw_query {
|
||||
None
|
||||
} else {
|
||||
// Safe to unwrap as we won't get here when the unwrap in if fails
|
||||
Some(path_position.path.to_str().unwrap().len())
|
||||
};
|
||||
|
||||
let query = FileSearchQuery {
|
||||
raw_query: raw_query.trim().to_owned(),
|
||||
file_query_end: if path_position.path.to_str().unwrap_or(raw_query) == raw_query {
|
||||
None
|
||||
} else {
|
||||
// Safe to unwrap as we won't get here when the unwrap in if fails
|
||||
Some(path_position.path.to_str().unwrap().len())
|
||||
},
|
||||
raw_query,
|
||||
file_query_end,
|
||||
path_position,
|
||||
};
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ buffer_diff.workspace = true
|
|||
call.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
component.workspace = true
|
||||
|
@ -62,7 +63,6 @@ watch.workspace = true
|
|||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows.workspace = true
|
||||
|
|
|
@ -71,12 +71,12 @@ use ui::{
|
|||
use util::{ResultExt, TryFutureExt, maybe};
|
||||
use workspace::SERIALIZATION_THROTTLE_TIME;
|
||||
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use workspace::{
|
||||
Workspace,
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
notifications::{DetachAndPromptErr, ErrorMessagePrompt, NotificationId},
|
||||
};
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
actions!(
|
||||
git_panel,
|
||||
|
|
|
@ -47,6 +47,7 @@ wayland = [
|
|||
"wayland-cursor",
|
||||
"wayland-protocols",
|
||||
"wayland-protocols-plasma",
|
||||
"wayland-protocols-wlr",
|
||||
"filedescriptor",
|
||||
"xkbcommon",
|
||||
"open",
|
||||
|
@ -193,6 +194,9 @@ wayland-protocols = { version = "0.31.2", features = [
|
|||
wayland-protocols-plasma = { version = "0.2.0", features = [
|
||||
"client",
|
||||
], optional = true }
|
||||
wayland-protocols-wlr = { version = "0.3.8", features = [
|
||||
"client"
|
||||
], optional = true}
|
||||
|
||||
# X11
|
||||
as-raw-xcb-connection = { version = "1", optional = true }
|
||||
|
|
|
@ -6,6 +6,7 @@ use gpui::{
|
|||
actions!(example, [Tab, TabPrev]);
|
||||
|
||||
struct Example {
|
||||
focus_handle: FocusHandle,
|
||||
items: Vec<FocusHandle>,
|
||||
message: SharedString,
|
||||
}
|
||||
|
@ -20,8 +21,11 @@ impl Example {
|
|||
cx.focus_handle().tab_index(2).tab_stop(true),
|
||||
];
|
||||
|
||||
window.focus(items.first().unwrap());
|
||||
let focus_handle = cx.focus_handle();
|
||||
window.focus(&focus_handle);
|
||||
|
||||
Self {
|
||||
focus_handle,
|
||||
items,
|
||||
message: SharedString::from("Press `Tab`, `Shift-Tab` to switch focus."),
|
||||
}
|
||||
|
@ -40,6 +44,10 @@ impl Example {
|
|||
|
||||
impl Render for Example {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
fn tab_stop_style<T: Styled>(this: T) -> T {
|
||||
this.border_3().border_color(gpui::blue())
|
||||
}
|
||||
|
||||
fn button(id: impl Into<ElementId>) -> Stateful<Div> {
|
||||
div()
|
||||
.id(id)
|
||||
|
@ -52,12 +60,13 @@ impl Render for Example {
|
|||
.border_color(gpui::black())
|
||||
.bg(gpui::black())
|
||||
.text_color(gpui::white())
|
||||
.focus(|this| this.border_color(gpui::blue()))
|
||||
.focus(tab_stop_style)
|
||||
.shadow_sm()
|
||||
}
|
||||
|
||||
div()
|
||||
.id("app")
|
||||
.track_focus(&self.focus_handle)
|
||||
.on_action(cx.listener(Self::on_tab))
|
||||
.on_action(cx.listener(Self::on_tab_prev))
|
||||
.size_full()
|
||||
|
@ -86,7 +95,7 @@ impl Render for Example {
|
|||
.border_color(gpui::black())
|
||||
.when(
|
||||
item_handle.tab_stop && item_handle.is_focused(window),
|
||||
|this| this.border_color(gpui::blue()),
|
||||
tab_stop_style,
|
||||
)
|
||||
.map(|this| match item_handle.tab_stop {
|
||||
true => this
|
||||
|
|
|
@ -2023,6 +2023,10 @@ impl HttpClient for NullHttpClient {
|
|||
.boxed()
|
||||
}
|
||||
|
||||
fn user_agent(&self) -> Option<&http_client::http::HeaderValue> {
|
||||
None
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Url> {
|
||||
None
|
||||
}
|
||||
|
|
|
@ -1216,6 +1216,10 @@ pub enum WindowKind {
|
|||
/// A window that appears above all other windows, usually used for alerts or popups
|
||||
/// use sparingly!
|
||||
PopUp,
|
||||
/// An overlay such as a notification window, a launcher, ...
|
||||
///
|
||||
/// Only supported on wayland
|
||||
Overlay,
|
||||
}
|
||||
|
||||
/// The appearance of the window, as defined by the operating system.
|
||||
|
|
|
@ -61,6 +61,7 @@ use wayland_protocols::xdg::decoration::zv1::client::{
|
|||
};
|
||||
use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base};
|
||||
use wayland_protocols_plasma::blur::client::{org_kde_kwin_blur, org_kde_kwin_blur_manager};
|
||||
use wayland_protocols_wlr::layer_shell::v1::client::{zwlr_layer_shell_v1, zwlr_layer_surface_v1};
|
||||
use xkbcommon::xkb::ffi::XKB_KEYMAP_FORMAT_TEXT_V1;
|
||||
use xkbcommon::xkb::{self, KEYMAP_COMPILE_NO_FLAGS, Keycode};
|
||||
|
||||
|
@ -114,6 +115,7 @@ pub struct Globals {
|
|||
pub fractional_scale_manager:
|
||||
Option<wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1>,
|
||||
pub decoration_manager: Option<zxdg_decoration_manager_v1::ZxdgDecorationManagerV1>,
|
||||
pub layer_shell: Option<zwlr_layer_shell_v1::ZwlrLayerShellV1>,
|
||||
pub blur_manager: Option<org_kde_kwin_blur_manager::OrgKdeKwinBlurManager>,
|
||||
pub text_input_manager: Option<zwp_text_input_manager_v3::ZwpTextInputManagerV3>,
|
||||
pub executor: ForegroundExecutor,
|
||||
|
@ -151,6 +153,7 @@ impl Globals {
|
|||
viewporter: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
fractional_scale_manager: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
decoration_manager: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
layer_shell: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
blur_manager: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
text_input_manager: globals.bind(&qh, 1..=1, ()).ok(),
|
||||
executor,
|
||||
|
@ -929,6 +932,7 @@ delegate_noop!(WaylandClientStatePtr: ignore wl_buffer::WlBuffer);
|
|||
delegate_noop!(WaylandClientStatePtr: ignore wl_region::WlRegion);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore wp_fractional_scale_manager_v1::WpFractionalScaleManagerV1);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore zxdg_decoration_manager_v1::ZxdgDecorationManagerV1);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore zwlr_layer_shell_v1::ZwlrLayerShellV1);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_blur_manager::OrgKdeKwinBlurManager);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore zwp_text_input_manager_v3::ZwpTextInputManagerV3);
|
||||
delegate_noop!(WaylandClientStatePtr: ignore org_kde_kwin_blur::OrgKdeKwinBlur);
|
||||
|
@ -1074,6 +1078,31 @@ impl Dispatch<xdg_toplevel::XdgToplevel, ObjectId> for WaylandClientStatePtr {
|
|||
}
|
||||
}
|
||||
|
||||
impl Dispatch<zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, ObjectId> for WaylandClientStatePtr {
|
||||
fn event(
|
||||
this: &mut Self,
|
||||
_: &zwlr_layer_surface_v1::ZwlrLayerSurfaceV1,
|
||||
event: <zwlr_layer_surface_v1::ZwlrLayerSurfaceV1 as Proxy>::Event,
|
||||
surface_id: &ObjectId,
|
||||
_: &Connection,
|
||||
_: &QueueHandle<Self>,
|
||||
) {
|
||||
let client = this.get_client();
|
||||
let mut state = client.borrow_mut();
|
||||
let Some(window) = get_window(&mut state, surface_id) else {
|
||||
return;
|
||||
};
|
||||
drop(state);
|
||||
|
||||
let should_close = window.handle_layersurface_event(event);
|
||||
|
||||
if should_close {
|
||||
// The close logic will be handled in drop_window()
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dispatch<xdg_wm_base::XdgWmBase, ()> for WaylandClientStatePtr {
|
||||
fn event(
|
||||
_: &mut Self,
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
use blade_graphics as gpu;
|
||||
use collections::HashMap;
|
||||
use futures::channel::oneshot::Receiver;
|
||||
use std::{
|
||||
cell::{Ref, RefCell, RefMut},
|
||||
ffi::c_void,
|
||||
|
@ -6,9 +9,14 @@ use std::{
|
|||
sync::Arc,
|
||||
};
|
||||
|
||||
use blade_graphics as gpu;
|
||||
use collections::HashMap;
|
||||
use futures::channel::oneshot::Receiver;
|
||||
use crate::{
|
||||
Capslock,
|
||||
platform::{
|
||||
PlatformAtlas, PlatformInputHandler, PlatformWindow,
|
||||
blade::{BladeContext, BladeRenderer, BladeSurfaceConfig},
|
||||
linux::wayland::{display::WaylandDisplay, serial::SerialKind},
|
||||
},
|
||||
};
|
||||
|
||||
use raw_window_handle as rwh;
|
||||
use wayland_backend::client::ObjectId;
|
||||
|
@ -20,6 +28,8 @@ use wayland_protocols::xdg::decoration::zv1::client::zxdg_toplevel_decoration_v1
|
|||
use wayland_protocols::xdg::shell::client::xdg_surface;
|
||||
use wayland_protocols::xdg::shell::client::xdg_toplevel::{self};
|
||||
use wayland_protocols_plasma::blur::client::org_kde_kwin_blur;
|
||||
use wayland_protocols_wlr::layer_shell::v1::client::zwlr_layer_shell_v1::Layer;
|
||||
use wayland_protocols_wlr::layer_shell::v1::client::zwlr_layer_surface_v1;
|
||||
|
||||
use crate::scene::Scene;
|
||||
use crate::{
|
||||
|
@ -27,15 +37,7 @@ use crate::{
|
|||
PlatformDisplay, PlatformInput, Point, PromptButton, PromptLevel, RequestFrameOptions,
|
||||
ResizeEdge, ScaledPixels, Size, Tiling, WaylandClientStatePtr, WindowAppearance,
|
||||
WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowControls, WindowDecorations,
|
||||
WindowParams, px, size,
|
||||
};
|
||||
use crate::{
|
||||
Capslock,
|
||||
platform::{
|
||||
PlatformAtlas, PlatformInputHandler, PlatformWindow,
|
||||
blade::{BladeContext, BladeRenderer, BladeSurfaceConfig},
|
||||
linux::wayland::{display::WaylandDisplay, serial::SerialKind},
|
||||
},
|
||||
WindowKind, WindowParams, px, size,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
|
@ -81,14 +83,12 @@ struct InProgressConfigure {
|
|||
}
|
||||
|
||||
pub struct WaylandWindowState {
|
||||
xdg_surface: xdg_surface::XdgSurface,
|
||||
surface_state: WaylandSurfaceState,
|
||||
acknowledged_first_configure: bool,
|
||||
pub surface: wl_surface::WlSurface,
|
||||
decoration: Option<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1>,
|
||||
app_id: Option<String>,
|
||||
appearance: WindowAppearance,
|
||||
blur: Option<org_kde_kwin_blur::OrgKdeKwinBlur>,
|
||||
toplevel: xdg_toplevel::XdgToplevel,
|
||||
viewport: Option<wp_viewport::WpViewport>,
|
||||
outputs: HashMap<ObjectId, Output>,
|
||||
display: Option<(ObjectId, Output)>,
|
||||
|
@ -114,6 +114,78 @@ pub struct WaylandWindowState {
|
|||
client_inset: Option<Pixels>,
|
||||
}
|
||||
|
||||
pub enum WaylandSurfaceState {
|
||||
Xdg(WaylandXdgSurfaceState),
|
||||
LayerShell(WaylandLayerSurfaceState),
|
||||
}
|
||||
|
||||
pub struct WaylandXdgSurfaceState {
|
||||
xdg_surface: xdg_surface::XdgSurface,
|
||||
toplevel: xdg_toplevel::XdgToplevel,
|
||||
decoration: Option<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1>,
|
||||
}
|
||||
|
||||
pub struct WaylandLayerSurfaceState {
|
||||
layer_surface: zwlr_layer_surface_v1::ZwlrLayerSurfaceV1,
|
||||
}
|
||||
|
||||
impl WaylandSurfaceState {
|
||||
fn ack_configure(&self, serial: u32) {
|
||||
match self {
|
||||
WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { xdg_surface, .. }) => {
|
||||
xdg_surface.ack_configure(serial);
|
||||
}
|
||||
WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { layer_surface, .. }) => {
|
||||
layer_surface.ack_configure(serial);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn decoration(&self) -> Option<&zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1> {
|
||||
if let WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { decoration, .. }) = self {
|
||||
decoration.as_ref()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn toplevel(&self) -> Option<&xdg_toplevel::XdgToplevel> {
|
||||
if let WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { toplevel, .. }) = self {
|
||||
Some(toplevel)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn set_geometry(&self, x: i32, y: i32, width: i32, height: i32) {
|
||||
match self {
|
||||
WaylandSurfaceState::Xdg(WaylandXdgSurfaceState { xdg_surface, .. }) => {
|
||||
xdg_surface.set_window_geometry(x, y, width, height);
|
||||
}
|
||||
WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { layer_surface, .. }) => {
|
||||
// cannot set window position of a layer surface
|
||||
layer_surface.set_size(width as u32, height as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn destroy(&mut self) {
|
||||
match self {
|
||||
WaylandSurfaceState::Xdg(WaylandXdgSurfaceState {
|
||||
xdg_surface,
|
||||
toplevel,
|
||||
decoration: _decoration,
|
||||
}) => {
|
||||
toplevel.destroy();
|
||||
xdg_surface.destroy();
|
||||
}
|
||||
WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { layer_surface }) => {
|
||||
layer_surface.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct WaylandWindowStatePtr {
|
||||
state: Rc<RefCell<WaylandWindowState>>,
|
||||
|
@ -124,9 +196,7 @@ impl WaylandWindowState {
|
|||
pub(crate) fn new(
|
||||
handle: AnyWindowHandle,
|
||||
surface: wl_surface::WlSurface,
|
||||
xdg_surface: xdg_surface::XdgSurface,
|
||||
toplevel: xdg_toplevel::XdgToplevel,
|
||||
decoration: Option<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1>,
|
||||
surface_state: WaylandSurfaceState,
|
||||
appearance: WindowAppearance,
|
||||
viewport: Option<wp_viewport::WpViewport>,
|
||||
client: WaylandClientStatePtr,
|
||||
|
@ -156,13 +226,11 @@ impl WaylandWindowState {
|
|||
};
|
||||
|
||||
Ok(Self {
|
||||
xdg_surface,
|
||||
surface_state,
|
||||
acknowledged_first_configure: false,
|
||||
surface,
|
||||
decoration,
|
||||
app_id: None,
|
||||
blur: None,
|
||||
toplevel,
|
||||
viewport,
|
||||
globals,
|
||||
outputs: HashMap::default(),
|
||||
|
@ -235,17 +303,16 @@ impl Drop for WaylandWindow {
|
|||
let client = state.client.clone();
|
||||
|
||||
state.renderer.destroy();
|
||||
if let Some(decoration) = &state.decoration {
|
||||
if let Some(decoration) = &state.surface_state.decoration() {
|
||||
decoration.destroy();
|
||||
}
|
||||
if let Some(blur) = &state.blur {
|
||||
blur.release();
|
||||
}
|
||||
state.toplevel.destroy();
|
||||
state.surface_state.destroy();
|
||||
if let Some(viewport) = &state.viewport {
|
||||
viewport.destroy();
|
||||
}
|
||||
state.xdg_surface.destroy();
|
||||
state.surface.destroy();
|
||||
|
||||
let state_ptr = self.0.clone();
|
||||
|
@ -279,27 +346,65 @@ impl WaylandWindow {
|
|||
appearance: WindowAppearance,
|
||||
) -> anyhow::Result<(Self, ObjectId)> {
|
||||
let surface = globals.compositor.create_surface(&globals.qh, ());
|
||||
let xdg_surface = globals
|
||||
.wm_base
|
||||
.get_xdg_surface(&surface, &globals.qh, surface.id());
|
||||
let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id());
|
||||
|
||||
if let Some(size) = params.window_min_size {
|
||||
toplevel.set_min_size(size.width.0 as i32, size.height.0 as i32);
|
||||
}
|
||||
let surface_state = match (params.kind, globals.layer_shell.as_ref()) {
|
||||
// Matching on layer_shell here means that if kind is Overlay, but the compositor doesn't support layer_shell,
|
||||
// we end up defaulting to xdg_surface anyway
|
||||
(WindowKind::Overlay, Some(layer_shell)) => {
|
||||
let layer_surface = layer_shell.get_layer_surface(
|
||||
&surface,
|
||||
None,
|
||||
Layer::Overlay,
|
||||
"".to_string(),
|
||||
&globals.qh,
|
||||
surface.id(),
|
||||
);
|
||||
|
||||
let width = params.bounds.size.width.0;
|
||||
let height = params.bounds.size.height.0;
|
||||
layer_surface.set_size(width as u32, height as u32);
|
||||
layer_surface.set_keyboard_interactivity(
|
||||
zwlr_layer_surface_v1::KeyboardInteractivity::OnDemand,
|
||||
);
|
||||
|
||||
WaylandSurfaceState::LayerShell(WaylandLayerSurfaceState { layer_surface })
|
||||
}
|
||||
_ => {
|
||||
let xdg_surface =
|
||||
globals
|
||||
.wm_base
|
||||
.get_xdg_surface(&surface, &globals.qh, surface.id());
|
||||
|
||||
let toplevel = xdg_surface.get_toplevel(&globals.qh, surface.id());
|
||||
|
||||
if let Some(size) = params.window_min_size {
|
||||
toplevel.set_min_size(size.width.0 as i32, size.height.0 as i32);
|
||||
}
|
||||
|
||||
// Attempt to set up window decorations based on the requested configuration
|
||||
let decoration = globals
|
||||
.decoration_manager
|
||||
.as_ref()
|
||||
.map(|decoration_manager| {
|
||||
decoration_manager.get_toplevel_decoration(
|
||||
&toplevel,
|
||||
&globals.qh,
|
||||
surface.id(),
|
||||
)
|
||||
});
|
||||
|
||||
WaylandSurfaceState::Xdg(WaylandXdgSurfaceState {
|
||||
xdg_surface,
|
||||
toplevel,
|
||||
decoration,
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(fractional_scale_manager) = globals.fractional_scale_manager.as_ref() {
|
||||
fractional_scale_manager.get_fractional_scale(&surface, &globals.qh, surface.id());
|
||||
}
|
||||
|
||||
// Attempt to set up window decorations based on the requested configuration
|
||||
let decoration = globals
|
||||
.decoration_manager
|
||||
.as_ref()
|
||||
.map(|decoration_manager| {
|
||||
decoration_manager.get_toplevel_decoration(&toplevel, &globals.qh, surface.id())
|
||||
});
|
||||
|
||||
let viewport = globals
|
||||
.viewporter
|
||||
.as_ref()
|
||||
|
@ -309,9 +414,7 @@ impl WaylandWindow {
|
|||
state: Rc::new(RefCell::new(WaylandWindowState::new(
|
||||
handle,
|
||||
surface.clone(),
|
||||
xdg_surface,
|
||||
toplevel,
|
||||
decoration,
|
||||
surface_state,
|
||||
appearance,
|
||||
viewport,
|
||||
client,
|
||||
|
@ -403,7 +506,7 @@ impl WaylandWindowStatePtr {
|
|||
}
|
||||
}
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.xdg_surface.ack_configure(serial);
|
||||
state.surface_state.ack_configure(serial);
|
||||
|
||||
let window_geometry = inset_by_tiling(
|
||||
state.bounds.map_origin(|_| px(0.0)),
|
||||
|
@ -413,7 +516,7 @@ impl WaylandWindowStatePtr {
|
|||
.map(|v| v.0 as i32)
|
||||
.map_size(|v| if v <= 0 { 1 } else { v });
|
||||
|
||||
state.xdg_surface.set_window_geometry(
|
||||
state.surface_state.set_geometry(
|
||||
window_geometry.origin.x,
|
||||
window_geometry.origin.y,
|
||||
window_geometry.size.width,
|
||||
|
@ -578,6 +681,42 @@ impl WaylandWindowStatePtr {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn handle_layersurface_event(&self, event: zwlr_layer_surface_v1::Event) -> bool {
|
||||
match event {
|
||||
zwlr_layer_surface_v1::Event::Configure {
|
||||
width,
|
||||
height,
|
||||
serial,
|
||||
} => {
|
||||
let mut size = if width == 0 || height == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(size(px(width as f32), px(height as f32)))
|
||||
};
|
||||
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.in_progress_configure = Some(InProgressConfigure {
|
||||
size,
|
||||
fullscreen: false,
|
||||
maximized: false,
|
||||
resizing: false,
|
||||
tiling: Tiling::default(),
|
||||
});
|
||||
drop(state);
|
||||
|
||||
// just do the same thing we'd do as an xdg_surface
|
||||
self.handle_xdg_surface_event(xdg_surface::Event::Configure { serial });
|
||||
|
||||
false
|
||||
}
|
||||
zwlr_layer_surface_v1::Event::Closed => {
|
||||
// unlike xdg, we don't have a choice here: the surface is closing.
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::mutable_key_type)]
|
||||
pub fn handle_surface_event(
|
||||
&self,
|
||||
|
@ -840,7 +979,7 @@ impl PlatformWindow for WaylandWindow {
|
|||
let state_ptr = self.0.clone();
|
||||
let dp_size = size.to_device_pixels(self.scale_factor());
|
||||
|
||||
state.xdg_surface.set_window_geometry(
|
||||
state.surface_state.set_geometry(
|
||||
state.bounds.origin.x.0 as i32,
|
||||
state.bounds.origin.y.0 as i32,
|
||||
dp_size.width.0,
|
||||
|
@ -934,12 +1073,16 @@ impl PlatformWindow for WaylandWindow {
|
|||
}
|
||||
|
||||
fn set_title(&mut self, title: &str) {
|
||||
self.borrow().toplevel.set_title(title.to_string());
|
||||
if let Some(toplevel) = self.borrow().surface_state.toplevel() {
|
||||
toplevel.set_title(title.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
fn set_app_id(&mut self, app_id: &str) {
|
||||
let mut state = self.borrow_mut();
|
||||
state.toplevel.set_app_id(app_id.to_owned());
|
||||
if let Some(toplevel) = self.borrow().surface_state.toplevel() {
|
||||
toplevel.set_app_id(app_id.to_owned());
|
||||
}
|
||||
state.app_id = Some(app_id.to_owned());
|
||||
}
|
||||
|
||||
|
@ -950,24 +1093,30 @@ impl PlatformWindow for WaylandWindow {
|
|||
}
|
||||
|
||||
fn minimize(&self) {
|
||||
self.borrow().toplevel.set_minimized();
|
||||
if let Some(toplevel) = self.borrow().surface_state.toplevel() {
|
||||
toplevel.set_minimized();
|
||||
}
|
||||
}
|
||||
|
||||
fn zoom(&self) {
|
||||
let state = self.borrow();
|
||||
if !state.maximized {
|
||||
state.toplevel.set_maximized();
|
||||
} else {
|
||||
state.toplevel.unset_maximized();
|
||||
if let Some(toplevel) = state.surface_state.toplevel() {
|
||||
if !state.maximized {
|
||||
toplevel.set_maximized();
|
||||
} else {
|
||||
toplevel.unset_maximized();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_fullscreen(&self) {
|
||||
let mut state = self.borrow_mut();
|
||||
if !state.fullscreen {
|
||||
state.toplevel.set_fullscreen(None);
|
||||
} else {
|
||||
state.toplevel.unset_fullscreen();
|
||||
let mut state = self.borrow();
|
||||
if let Some(toplevel) = state.surface_state.toplevel() {
|
||||
if !state.fullscreen {
|
||||
toplevel.set_fullscreen(None);
|
||||
} else {
|
||||
toplevel.unset_fullscreen();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1032,27 +1181,33 @@ impl PlatformWindow for WaylandWindow {
|
|||
fn show_window_menu(&self, position: Point<Pixels>) {
|
||||
let state = self.borrow();
|
||||
let serial = state.client.get_serial(SerialKind::MousePress);
|
||||
state.toplevel.show_window_menu(
|
||||
&state.globals.seat,
|
||||
serial,
|
||||
position.x.0 as i32,
|
||||
position.y.0 as i32,
|
||||
);
|
||||
if let Some(toplevel) = state.surface_state.toplevel() {
|
||||
toplevel.show_window_menu(
|
||||
&state.globals.seat,
|
||||
serial,
|
||||
position.x.0 as i32,
|
||||
position.y.0 as i32,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn start_window_move(&self) {
|
||||
let state = self.borrow();
|
||||
let serial = state.client.get_serial(SerialKind::MousePress);
|
||||
state.toplevel._move(&state.globals.seat, serial);
|
||||
if let Some(toplevel) = state.surface_state.toplevel() {
|
||||
toplevel._move(&state.globals.seat, serial);
|
||||
}
|
||||
}
|
||||
|
||||
fn start_window_resize(&self, edge: crate::ResizeEdge) {
|
||||
let state = self.borrow();
|
||||
state.toplevel.resize(
|
||||
&state.globals.seat,
|
||||
state.client.get_serial(SerialKind::MousePress),
|
||||
edge.to_xdg(),
|
||||
)
|
||||
if let Some(toplevel) = state.surface_state.toplevel() {
|
||||
toplevel.resize(
|
||||
&state.globals.seat,
|
||||
state.client.get_serial(SerialKind::MousePress),
|
||||
edge.to_xdg(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn window_decorations(&self) -> Decorations {
|
||||
|
@ -1068,7 +1223,7 @@ impl PlatformWindow for WaylandWindow {
|
|||
fn request_decorations(&self, decorations: WindowDecorations) {
|
||||
let mut state = self.borrow_mut();
|
||||
state.decorations = decorations;
|
||||
if let Some(decoration) = state.decoration.as_ref() {
|
||||
if let Some(decoration) = state.surface_state.decoration() {
|
||||
decoration.set_mode(decorations.to_xdg());
|
||||
update_window(state);
|
||||
}
|
||||
|
|
|
@ -559,7 +559,7 @@ impl MacWindow {
|
|||
}
|
||||
|
||||
let native_window: id = match kind {
|
||||
WindowKind::Normal => msg_send![WINDOW_CLASS, alloc],
|
||||
WindowKind::Normal | WindowKind::Overlay => msg_send![WINDOW_CLASS, alloc],
|
||||
WindowKind::PopUp => {
|
||||
style_mask |= NSWindowStyleMaskNonactivatingPanel;
|
||||
msg_send![PANEL_CLASS, alloc]
|
||||
|
@ -711,7 +711,7 @@ impl MacWindow {
|
|||
native_window.makeFirstResponder_(native_view);
|
||||
|
||||
match kind {
|
||||
WindowKind::Normal => {
|
||||
WindowKind::Normal | WindowKind::Overlay => {
|
||||
native_window.setLevel_(NSNormalWindowLevel);
|
||||
native_window.setAcceptsMouseMovedEvents_(YES);
|
||||
}
|
||||
|
|
|
@ -32,20 +32,18 @@ impl TabHandles {
|
|||
self.handles.clear();
|
||||
}
|
||||
|
||||
fn current_index(&self, focused_id: Option<&FocusId>) -> usize {
|
||||
self.handles
|
||||
.iter()
|
||||
.position(|h| Some(&h.id) == focused_id)
|
||||
.unwrap_or_default()
|
||||
fn current_index(&self, focused_id: Option<&FocusId>) -> Option<usize> {
|
||||
self.handles.iter().position(|h| Some(&h.id) == focused_id)
|
||||
}
|
||||
|
||||
pub(crate) fn next(&self, focused_id: Option<&FocusId>) -> Option<FocusHandle> {
|
||||
let ix = self.current_index(focused_id);
|
||||
|
||||
let mut next_ix = ix + 1;
|
||||
if next_ix + 1 > self.handles.len() {
|
||||
next_ix = 0;
|
||||
}
|
||||
let next_ix = self
|
||||
.current_index(focused_id)
|
||||
.and_then(|ix| {
|
||||
let next_ix = ix + 1;
|
||||
(next_ix < self.handles.len()).then_some(next_ix)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Some(next_handle) = self.handles.get(next_ix) {
|
||||
Some(next_handle.clone())
|
||||
|
@ -55,7 +53,7 @@ impl TabHandles {
|
|||
}
|
||||
|
||||
pub(crate) fn prev(&self, focused_id: Option<&FocusId>) -> Option<FocusHandle> {
|
||||
let ix = self.current_index(focused_id);
|
||||
let ix = self.current_index(focused_id).unwrap_or_default();
|
||||
let prev_ix;
|
||||
if ix == 0 {
|
||||
prev_ix = self.handles.len().saturating_sub(1);
|
||||
|
@ -108,8 +106,14 @@ mod tests {
|
|||
]
|
||||
);
|
||||
|
||||
// next
|
||||
assert_eq!(tab.next(None), Some(tab.handles[1].clone()));
|
||||
// Select first tab index if no handle is currently focused.
|
||||
assert_eq!(tab.next(None), Some(tab.handles[0].clone()));
|
||||
// Select last tab index if no handle is currently focused.
|
||||
assert_eq!(
|
||||
tab.prev(None),
|
||||
Some(tab.handles[tab.handles.len() - 1].clone())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
tab.next(Some(&tab.handles[0].id)),
|
||||
Some(tab.handles[1].clone())
|
||||
|
|
|
@ -4,6 +4,7 @@ pub mod github;
|
|||
pub use anyhow::{Result, anyhow};
|
||||
pub use async_body::{AsyncBody, Inner};
|
||||
use derive_more::Deref;
|
||||
use http::HeaderValue;
|
||||
pub use http::{self, Method, Request, Response, StatusCode, Uri};
|
||||
|
||||
use futures::future::BoxFuture;
|
||||
|
@ -39,6 +40,8 @@ impl HttpRequestExt for http::request::Builder {
|
|||
pub trait HttpClient: 'static + Send + Sync {
|
||||
fn type_name(&self) -> &'static str;
|
||||
|
||||
fn user_agent(&self) -> Option<&HeaderValue>;
|
||||
|
||||
fn send(
|
||||
&self,
|
||||
req: http::Request<AsyncBody>,
|
||||
|
@ -118,6 +121,10 @@ impl HttpClient for HttpClientWithProxy {
|
|||
self.client.send(req)
|
||||
}
|
||||
|
||||
fn user_agent(&self) -> Option<&HeaderValue> {
|
||||
self.client.user_agent()
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Url> {
|
||||
self.proxy.as_ref()
|
||||
}
|
||||
|
@ -135,6 +142,10 @@ impl HttpClient for Arc<HttpClientWithProxy> {
|
|||
self.client.send(req)
|
||||
}
|
||||
|
||||
fn user_agent(&self) -> Option<&HeaderValue> {
|
||||
self.client.user_agent()
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Url> {
|
||||
self.proxy.as_ref()
|
||||
}
|
||||
|
@ -250,6 +261,10 @@ impl HttpClient for Arc<HttpClientWithUrl> {
|
|||
self.client.send(req)
|
||||
}
|
||||
|
||||
fn user_agent(&self) -> Option<&HeaderValue> {
|
||||
self.client.user_agent()
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Url> {
|
||||
self.client.proxy.as_ref()
|
||||
}
|
||||
|
@ -267,6 +282,10 @@ impl HttpClient for HttpClientWithUrl {
|
|||
self.client.send(req)
|
||||
}
|
||||
|
||||
fn user_agent(&self) -> Option<&HeaderValue> {
|
||||
self.client.user_agent()
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Url> {
|
||||
self.client.proxy.as_ref()
|
||||
}
|
||||
|
@ -314,6 +333,10 @@ impl HttpClient for BlockedHttpClient {
|
|||
})
|
||||
}
|
||||
|
||||
fn user_agent(&self) -> Option<&HeaderValue> {
|
||||
None
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Url> {
|
||||
None
|
||||
}
|
||||
|
@ -334,6 +357,7 @@ type FakeHttpHandler = Box<
|
|||
#[cfg(feature = "test-support")]
|
||||
pub struct FakeHttpClient {
|
||||
handler: FakeHttpHandler,
|
||||
user_agent: HeaderValue,
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-support")]
|
||||
|
@ -348,6 +372,7 @@ impl FakeHttpClient {
|
|||
client: HttpClientWithProxy {
|
||||
client: Arc::new(Self {
|
||||
handler: Box::new(move |req| Box::pin(handler(req))),
|
||||
user_agent: HeaderValue::from_static(type_name::<Self>()),
|
||||
}),
|
||||
proxy: None,
|
||||
},
|
||||
|
@ -390,6 +415,10 @@ impl HttpClient for FakeHttpClient {
|
|||
future
|
||||
}
|
||||
|
||||
fn user_agent(&self) -> Option<&HeaderValue> {
|
||||
Some(&self.user_agent)
|
||||
}
|
||||
|
||||
fn proxy(&self) -> Option<&Url> {
|
||||
None
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ doctest = false
|
|||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
copilot.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
|
@ -32,7 +33,6 @@ ui.workspace = true
|
|||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
zeta.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use anyhow::Result;
|
||||
use client::{DisableAiSettings, UserStore, zed_urls};
|
||||
use cloud_llm_client::UsageLimit;
|
||||
use copilot::{Copilot, Status};
|
||||
use editor::{
|
||||
Editor, SelectionEffects,
|
||||
|
@ -34,7 +35,6 @@ use workspace::{
|
|||
notifications::NotificationId,
|
||||
};
|
||||
use zed_actions::OpenBrowser;
|
||||
use zed_llm_client::UsageLimit;
|
||||
use zeta::RateCompletions;
|
||||
|
||||
actions!(
|
||||
|
|
|
@ -166,7 +166,6 @@ pub struct CachedLspAdapter {
|
|||
pub reinstall_attempt_count: AtomicU64,
|
||||
cached_binary: futures::lock::Mutex<Option<LanguageServerBinary>>,
|
||||
manifest_name: OnceLock<Option<ManifestName>>,
|
||||
attach_kind: OnceLock<Attach>,
|
||||
}
|
||||
|
||||
impl Debug for CachedLspAdapter {
|
||||
|
@ -202,7 +201,6 @@ impl CachedLspAdapter {
|
|||
adapter,
|
||||
cached_binary: Default::default(),
|
||||
reinstall_attempt_count: AtomicU64::new(0),
|
||||
attach_kind: Default::default(),
|
||||
manifest_name: Default::default(),
|
||||
})
|
||||
}
|
||||
|
@ -288,29 +286,6 @@ impl CachedLspAdapter {
|
|||
.get_or_init(|| self.adapter.manifest_name())
|
||||
.clone()
|
||||
}
|
||||
pub fn attach_kind(&self) -> Attach {
|
||||
*self.attach_kind.get_or_init(|| self.adapter.attach_kind())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Attach {
|
||||
/// Create a single language server instance per subproject root.
|
||||
InstancePerRoot,
|
||||
/// Use one shared language server instance for all subprojects within a project.
|
||||
Shared,
|
||||
}
|
||||
|
||||
impl Attach {
|
||||
pub fn root_path(
|
||||
&self,
|
||||
root_subproject_path: (WorktreeId, Arc<Path>),
|
||||
) -> (WorktreeId, Arc<Path>) {
|
||||
match self {
|
||||
Attach::InstancePerRoot => root_subproject_path,
|
||||
Attach::Shared => (root_subproject_path.0, Arc::from(Path::new(""))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines what gets sent out as a workspace folders content
|
||||
|
@ -611,10 +586,6 @@ pub trait LspAdapter: 'static + Send + Sync {
|
|||
Ok(original)
|
||||
}
|
||||
|
||||
fn attach_kind(&self) -> Attach {
|
||||
Attach::Shared
|
||||
}
|
||||
|
||||
/// Determines whether a language server supports workspace folders.
|
||||
///
|
||||
/// And does not trip over itself in the process.
|
||||
|
|
|
@ -20,6 +20,7 @@ anthropic = { workspace = true, features = ["schemars"] }
|
|||
anyhow.workspace = true
|
||||
base64.workspace = true
|
||||
client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
|
@ -37,7 +38,6 @@ telemetry_events.workspace = true
|
|||
thiserror.workspace = true
|
||||
util.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
|
|
|
@ -11,6 +11,7 @@ pub mod fake_provider;
|
|||
use anthropic::{AnthropicError, parse_prompt_too_long};
|
||||
use anyhow::{Result, anyhow};
|
||||
use client::Client;
|
||||
use cloud_llm_client::{CompletionMode, CompletionRequestStatus};
|
||||
use futures::FutureExt;
|
||||
use futures::{StreamExt, future::BoxFuture, stream::BoxStream};
|
||||
use gpui::{AnyElement, AnyView, App, AsyncApp, SharedString, Task, Window};
|
||||
|
@ -26,7 +27,6 @@ use std::time::Duration;
|
|||
use std::{fmt, io};
|
||||
use thiserror::Error;
|
||||
use util::serde::is_default;
|
||||
use zed_llm_client::{CompletionMode, CompletionRequestStatus};
|
||||
|
||||
pub use crate::model::*;
|
||||
pub use crate::rate_limiter::*;
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
use std::io::{Cursor, Write};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::role::Role;
|
||||
use crate::{LanguageModelToolUse, LanguageModelToolUseId};
|
||||
use anyhow::Result;
|
||||
use base64::write::EncoderWriter;
|
||||
use cloud_llm_client::{CompletionIntent, CompletionMode};
|
||||
use gpui::{
|
||||
App, AppContext as _, DevicePixels, Image, ImageFormat, ObjectFit, SharedString, Size, Task,
|
||||
point, px, size,
|
||||
|
@ -12,7 +11,9 @@ use gpui::{
|
|||
use image::codecs::png::PngEncoder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use util::ResultExt;
|
||||
use zed_llm_client::{CompletionIntent, CompletionMode};
|
||||
|
||||
use crate::role::Role;
|
||||
use crate::{LanguageModelToolUse, LanguageModelToolUseId};
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
|
||||
pub struct LanguageModelImage {
|
||||
|
|
|
@ -16,18 +16,17 @@ ai_onboarding.workspace = true
|
|||
anthropic = { workspace = true, features = ["schemars"] }
|
||||
anyhow.workspace = true
|
||||
aws-config = { workspace = true, features = ["behavior-version-latest"] }
|
||||
aws-credential-types = { workspace = true, features = [
|
||||
"hardcoded-credentials",
|
||||
] }
|
||||
aws-credential-types = { workspace = true, features = ["hardcoded-credentials"] }
|
||||
aws_http_client.workspace = true
|
||||
bedrock.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
component.workspace = true
|
||||
credentials_provider.workspace = true
|
||||
convert_case.workspace = true
|
||||
copilot.workspace = true
|
||||
credentials_provider.workspace = true
|
||||
deepseek = { workspace = true, features = ["schemars"] }
|
||||
editor.workspace = true
|
||||
futures.workspace = true
|
||||
|
@ -35,6 +34,7 @@ google_ai = { workspace = true, features = ["schemars"] }
|
|||
gpui.workspace = true
|
||||
gpui_tokio.workspace = true
|
||||
http_client.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
lmstudio = { workspace = true, features = ["schemars"] }
|
||||
log.workspace = true
|
||||
|
@ -43,8 +43,6 @@ mistral = { workspace = true, features = ["schemars"] }
|
|||
ollama = { workspace = true, features = ["schemars"] }
|
||||
open_ai = { workspace = true, features = ["schemars"] }
|
||||
open_router = { workspace = true, features = ["schemars"] }
|
||||
vercel = { workspace = true, features = ["schemars"] }
|
||||
x_ai = { workspace = true, features = ["schemars"] }
|
||||
partial-json-fixer.workspace = true
|
||||
proto.workspace = true
|
||||
release_channel.workspace = true
|
||||
|
@ -61,9 +59,9 @@ tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
|
|||
ui.workspace = true
|
||||
ui_input.workspace = true
|
||||
util.workspace = true
|
||||
vercel = { workspace = true, features = ["schemars"] }
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
language.workspace = true
|
||||
x_ai = { workspace = true, features = ["schemars"] }
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
|
|
|
@ -3,6 +3,13 @@ use anthropic::AnthropicModelMode;
|
|||
use anyhow::{Context as _, Result, anyhow};
|
||||
use chrono::{DateTime, Utc};
|
||||
use client::{Client, ModelRequestUsage, UserStore, zed_urls};
|
||||
use cloud_llm_client::{
|
||||
CLIENT_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, CURRENT_PLAN_HEADER_NAME, CompletionBody,
|
||||
CompletionEvent, CompletionRequestStatus, CountTokensBody, CountTokensResponse,
|
||||
EXPIRED_LLM_TOKEN_HEADER_NAME, ListModelsResponse, MODEL_REQUESTS_RESOURCE_HEADER_VALUE,
|
||||
SERVER_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME,
|
||||
TOOL_USE_LIMIT_REACHED_HEADER_NAME, ZED_VERSION_HEADER_NAME,
|
||||
};
|
||||
use futures::{
|
||||
AsyncBufReadExt, FutureExt, Stream, StreamExt, future::BoxFuture, stream::BoxStream,
|
||||
};
|
||||
|
@ -33,13 +40,6 @@ use std::time::Duration;
|
|||
use thiserror::Error;
|
||||
use ui::{TintColor, prelude::*};
|
||||
use util::{ResultExt as _, maybe};
|
||||
use zed_llm_client::{
|
||||
CLIENT_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, CURRENT_PLAN_HEADER_NAME, CompletionBody,
|
||||
CompletionRequestStatus, CountTokensBody, CountTokensResponse, EXPIRED_LLM_TOKEN_HEADER_NAME,
|
||||
ListModelsResponse, MODEL_REQUESTS_RESOURCE_HEADER_VALUE,
|
||||
SERVER_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME,
|
||||
TOOL_USE_LIMIT_REACHED_HEADER_NAME, ZED_VERSION_HEADER_NAME,
|
||||
};
|
||||
|
||||
use crate::provider::anthropic::{AnthropicEventMapper, count_anthropic_tokens, into_anthropic};
|
||||
use crate::provider::google::{GoogleEventMapper, into_google};
|
||||
|
@ -120,10 +120,10 @@ pub struct State {
|
|||
user_store: Entity<UserStore>,
|
||||
status: client::Status,
|
||||
accept_terms_of_service_task: Option<Task<Result<()>>>,
|
||||
models: Vec<Arc<zed_llm_client::LanguageModel>>,
|
||||
default_model: Option<Arc<zed_llm_client::LanguageModel>>,
|
||||
default_fast_model: Option<Arc<zed_llm_client::LanguageModel>>,
|
||||
recommended_models: Vec<Arc<zed_llm_client::LanguageModel>>,
|
||||
models: Vec<Arc<cloud_llm_client::LanguageModel>>,
|
||||
default_model: Option<Arc<cloud_llm_client::LanguageModel>>,
|
||||
default_fast_model: Option<Arc<cloud_llm_client::LanguageModel>>,
|
||||
recommended_models: Vec<Arc<cloud_llm_client::LanguageModel>>,
|
||||
_fetch_models_task: Task<()>,
|
||||
_settings_subscription: Subscription,
|
||||
_llm_token_subscription: Subscription,
|
||||
|
@ -238,8 +238,8 @@ impl State {
|
|||
// Right now we represent thinking variants of models as separate models on the client,
|
||||
// so we need to insert variants for any model that supports thinking.
|
||||
if model.supports_thinking {
|
||||
models.push(Arc::new(zed_llm_client::LanguageModel {
|
||||
id: zed_llm_client::LanguageModelId(format!("{}-thinking", model.id).into()),
|
||||
models.push(Arc::new(cloud_llm_client::LanguageModel {
|
||||
id: cloud_llm_client::LanguageModelId(format!("{}-thinking", model.id).into()),
|
||||
display_name: format!("{} Thinking", model.display_name),
|
||||
..model
|
||||
}));
|
||||
|
@ -328,7 +328,7 @@ impl CloudLanguageModelProvider {
|
|||
|
||||
fn create_language_model(
|
||||
&self,
|
||||
model: Arc<zed_llm_client::LanguageModel>,
|
||||
model: Arc<cloud_llm_client::LanguageModel>,
|
||||
llm_api_token: LlmApiToken,
|
||||
) -> Arc<dyn LanguageModel> {
|
||||
Arc::new(CloudLanguageModel {
|
||||
|
@ -518,7 +518,7 @@ fn render_accept_terms(
|
|||
|
||||
pub struct CloudLanguageModel {
|
||||
id: LanguageModelId,
|
||||
model: Arc<zed_llm_client::LanguageModel>,
|
||||
model: Arc<cloud_llm_client::LanguageModel>,
|
||||
llm_api_token: LlmApiToken,
|
||||
client: Arc<Client>,
|
||||
request_limiter: RateLimiter,
|
||||
|
@ -611,12 +611,12 @@ impl CloudLanguageModel {
|
|||
.headers()
|
||||
.get(CURRENT_PLAN_HEADER_NAME)
|
||||
.and_then(|plan| plan.to_str().ok())
|
||||
.and_then(|plan| zed_llm_client::Plan::from_str(plan).ok())
|
||||
.and_then(|plan| cloud_llm_client::Plan::from_str(plan).ok())
|
||||
{
|
||||
let plan = match plan {
|
||||
zed_llm_client::Plan::ZedFree => Plan::Free,
|
||||
zed_llm_client::Plan::ZedPro => Plan::ZedPro,
|
||||
zed_llm_client::Plan::ZedProTrial => Plan::ZedProTrial,
|
||||
cloud_llm_client::Plan::ZedFree => Plan::Free,
|
||||
cloud_llm_client::Plan::ZedPro => Plan::ZedPro,
|
||||
cloud_llm_client::Plan::ZedProTrial => Plan::ZedProTrial,
|
||||
};
|
||||
return Err(anyhow!(ModelRequestLimitReachedError { plan }));
|
||||
}
|
||||
|
@ -729,7 +729,7 @@ impl LanguageModel for CloudLanguageModel {
|
|||
}
|
||||
|
||||
fn upstream_provider_id(&self) -> LanguageModelProviderId {
|
||||
use zed_llm_client::LanguageModelProvider::*;
|
||||
use cloud_llm_client::LanguageModelProvider::*;
|
||||
match self.model.provider {
|
||||
Anthropic => language_model::ANTHROPIC_PROVIDER_ID,
|
||||
OpenAi => language_model::OPEN_AI_PROVIDER_ID,
|
||||
|
@ -738,7 +738,7 @@ impl LanguageModel for CloudLanguageModel {
|
|||
}
|
||||
|
||||
fn upstream_provider_name(&self) -> LanguageModelProviderName {
|
||||
use zed_llm_client::LanguageModelProvider::*;
|
||||
use cloud_llm_client::LanguageModelProvider::*;
|
||||
match self.model.provider {
|
||||
Anthropic => language_model::ANTHROPIC_PROVIDER_NAME,
|
||||
OpenAi => language_model::OPEN_AI_PROVIDER_NAME,
|
||||
|
@ -772,11 +772,11 @@ impl LanguageModel for CloudLanguageModel {
|
|||
|
||||
fn tool_input_format(&self) -> LanguageModelToolSchemaFormat {
|
||||
match self.model.provider {
|
||||
zed_llm_client::LanguageModelProvider::Anthropic
|
||||
| zed_llm_client::LanguageModelProvider::OpenAi => {
|
||||
cloud_llm_client::LanguageModelProvider::Anthropic
|
||||
| cloud_llm_client::LanguageModelProvider::OpenAi => {
|
||||
LanguageModelToolSchemaFormat::JsonSchema
|
||||
}
|
||||
zed_llm_client::LanguageModelProvider::Google => {
|
||||
cloud_llm_client::LanguageModelProvider::Google => {
|
||||
LanguageModelToolSchemaFormat::JsonSchemaSubset
|
||||
}
|
||||
}
|
||||
|
@ -795,15 +795,15 @@ impl LanguageModel for CloudLanguageModel {
|
|||
|
||||
fn cache_configuration(&self) -> Option<LanguageModelCacheConfiguration> {
|
||||
match &self.model.provider {
|
||||
zed_llm_client::LanguageModelProvider::Anthropic => {
|
||||
cloud_llm_client::LanguageModelProvider::Anthropic => {
|
||||
Some(LanguageModelCacheConfiguration {
|
||||
min_total_token: 2_048,
|
||||
should_speculate: true,
|
||||
max_cache_anchors: 4,
|
||||
})
|
||||
}
|
||||
zed_llm_client::LanguageModelProvider::OpenAi
|
||||
| zed_llm_client::LanguageModelProvider::Google => None,
|
||||
cloud_llm_client::LanguageModelProvider::OpenAi
|
||||
| cloud_llm_client::LanguageModelProvider::Google => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -813,15 +813,17 @@ impl LanguageModel for CloudLanguageModel {
|
|||
cx: &App,
|
||||
) -> BoxFuture<'static, Result<u64>> {
|
||||
match self.model.provider {
|
||||
zed_llm_client::LanguageModelProvider::Anthropic => count_anthropic_tokens(request, cx),
|
||||
zed_llm_client::LanguageModelProvider::OpenAi => {
|
||||
cloud_llm_client::LanguageModelProvider::Anthropic => {
|
||||
count_anthropic_tokens(request, cx)
|
||||
}
|
||||
cloud_llm_client::LanguageModelProvider::OpenAi => {
|
||||
let model = match open_ai::Model::from_id(&self.model.id.0) {
|
||||
Ok(model) => model,
|
||||
Err(err) => return async move { Err(anyhow!(err)) }.boxed(),
|
||||
};
|
||||
count_open_ai_tokens(request, model, cx)
|
||||
}
|
||||
zed_llm_client::LanguageModelProvider::Google => {
|
||||
cloud_llm_client::LanguageModelProvider::Google => {
|
||||
let client = self.client.clone();
|
||||
let llm_api_token = self.llm_api_token.clone();
|
||||
let model_id = self.model.id.to_string();
|
||||
|
@ -832,7 +834,7 @@ impl LanguageModel for CloudLanguageModel {
|
|||
let token = llm_api_token.acquire(&client).await?;
|
||||
|
||||
let request_body = CountTokensBody {
|
||||
provider: zed_llm_client::LanguageModelProvider::Google,
|
||||
provider: cloud_llm_client::LanguageModelProvider::Google,
|
||||
model: model_id,
|
||||
provider_request: serde_json::to_value(&google_ai::CountTokensRequest {
|
||||
generate_content_request,
|
||||
|
@ -893,7 +895,7 @@ impl LanguageModel for CloudLanguageModel {
|
|||
let app_version = cx.update(|cx| AppVersion::global(cx)).ok();
|
||||
let thinking_allowed = request.thinking_allowed;
|
||||
match self.model.provider {
|
||||
zed_llm_client::LanguageModelProvider::Anthropic => {
|
||||
cloud_llm_client::LanguageModelProvider::Anthropic => {
|
||||
let request = into_anthropic(
|
||||
request,
|
||||
self.model.id.to_string(),
|
||||
|
@ -924,7 +926,7 @@ impl LanguageModel for CloudLanguageModel {
|
|||
prompt_id,
|
||||
intent,
|
||||
mode,
|
||||
provider: zed_llm_client::LanguageModelProvider::Anthropic,
|
||||
provider: cloud_llm_client::LanguageModelProvider::Anthropic,
|
||||
model: request.model.clone(),
|
||||
provider_request: serde_json::to_value(&request)
|
||||
.map_err(|e| anyhow!(e))?,
|
||||
|
@ -948,7 +950,7 @@ impl LanguageModel for CloudLanguageModel {
|
|||
});
|
||||
async move { Ok(future.await?.boxed()) }.boxed()
|
||||
}
|
||||
zed_llm_client::LanguageModelProvider::OpenAi => {
|
||||
cloud_llm_client::LanguageModelProvider::OpenAi => {
|
||||
let client = self.client.clone();
|
||||
let model = match open_ai::Model::from_id(&self.model.id.0) {
|
||||
Ok(model) => model,
|
||||
|
@ -976,7 +978,7 @@ impl LanguageModel for CloudLanguageModel {
|
|||
prompt_id,
|
||||
intent,
|
||||
mode,
|
||||
provider: zed_llm_client::LanguageModelProvider::OpenAi,
|
||||
provider: cloud_llm_client::LanguageModelProvider::OpenAi,
|
||||
model: request.model.clone(),
|
||||
provider_request: serde_json::to_value(&request)
|
||||
.map_err(|e| anyhow!(e))?,
|
||||
|
@ -996,7 +998,7 @@ impl LanguageModel for CloudLanguageModel {
|
|||
});
|
||||
async move { Ok(future.await?.boxed()) }.boxed()
|
||||
}
|
||||
zed_llm_client::LanguageModelProvider::Google => {
|
||||
cloud_llm_client::LanguageModelProvider::Google => {
|
||||
let client = self.client.clone();
|
||||
let request =
|
||||
into_google(request, self.model.id.to_string(), GoogleModelMode::Default);
|
||||
|
@ -1016,7 +1018,7 @@ impl LanguageModel for CloudLanguageModel {
|
|||
prompt_id,
|
||||
intent,
|
||||
mode,
|
||||
provider: zed_llm_client::LanguageModelProvider::Google,
|
||||
provider: cloud_llm_client::LanguageModelProvider::Google,
|
||||
model: request.model.model_id.clone(),
|
||||
provider_request: serde_json::to_value(&request)
|
||||
.map_err(|e| anyhow!(e))?,
|
||||
|
@ -1040,15 +1042,8 @@ impl LanguageModel for CloudLanguageModel {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CloudCompletionEvent<T> {
|
||||
Status(CompletionRequestStatus),
|
||||
Event(T),
|
||||
}
|
||||
|
||||
fn map_cloud_completion_events<T, F>(
|
||||
stream: Pin<Box<dyn Stream<Item = Result<CloudCompletionEvent<T>>> + Send>>,
|
||||
stream: Pin<Box<dyn Stream<Item = Result<CompletionEvent<T>>> + Send>>,
|
||||
mut map_callback: F,
|
||||
) -> BoxStream<'static, Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>
|
||||
where
|
||||
|
@ -1063,10 +1058,10 @@ where
|
|||
Err(error) => {
|
||||
vec![Err(LanguageModelCompletionError::from(error))]
|
||||
}
|
||||
Ok(CloudCompletionEvent::Status(event)) => {
|
||||
Ok(CompletionEvent::Status(event)) => {
|
||||
vec![Ok(LanguageModelCompletionEvent::StatusUpdate(event))]
|
||||
}
|
||||
Ok(CloudCompletionEvent::Event(event)) => map_callback(event),
|
||||
Ok(CompletionEvent::Event(event)) => map_callback(event),
|
||||
})
|
||||
})
|
||||
.boxed()
|
||||
|
@ -1074,9 +1069,9 @@ where
|
|||
|
||||
fn usage_updated_event<T>(
|
||||
usage: Option<ModelRequestUsage>,
|
||||
) -> impl Stream<Item = Result<CloudCompletionEvent<T>>> {
|
||||
) -> impl Stream<Item = Result<CompletionEvent<T>>> {
|
||||
futures::stream::iter(usage.map(|usage| {
|
||||
Ok(CloudCompletionEvent::Status(
|
||||
Ok(CompletionEvent::Status(
|
||||
CompletionRequestStatus::UsageUpdated {
|
||||
amount: usage.amount as usize,
|
||||
limit: usage.limit,
|
||||
|
@ -1087,9 +1082,9 @@ fn usage_updated_event<T>(
|
|||
|
||||
fn tool_use_limit_reached_event<T>(
|
||||
tool_use_limit_reached: bool,
|
||||
) -> impl Stream<Item = Result<CloudCompletionEvent<T>>> {
|
||||
) -> impl Stream<Item = Result<CompletionEvent<T>>> {
|
||||
futures::stream::iter(tool_use_limit_reached.then(|| {
|
||||
Ok(CloudCompletionEvent::Status(
|
||||
Ok(CompletionEvent::Status(
|
||||
CompletionRequestStatus::ToolUseLimitReached,
|
||||
))
|
||||
}))
|
||||
|
@ -1098,7 +1093,7 @@ fn tool_use_limit_reached_event<T>(
|
|||
fn response_lines<T: DeserializeOwned>(
|
||||
response: Response<AsyncBody>,
|
||||
includes_status_messages: bool,
|
||||
) -> impl Stream<Item = Result<CloudCompletionEvent<T>>> {
|
||||
) -> impl Stream<Item = Result<CompletionEvent<T>>> {
|
||||
futures::stream::try_unfold(
|
||||
(String::new(), BufReader::new(response.into_body())),
|
||||
move |(mut line, mut body)| async move {
|
||||
|
@ -1106,9 +1101,9 @@ fn response_lines<T: DeserializeOwned>(
|
|||
Ok(0) => Ok(None),
|
||||
Ok(_) => {
|
||||
let event = if includes_status_messages {
|
||||
serde_json::from_str::<CloudCompletionEvent<T>>(&line)?
|
||||
serde_json::from_str::<CompletionEvent<T>>(&line)?
|
||||
} else {
|
||||
CloudCompletionEvent::Event(serde_json::from_str::<T>(&line)?)
|
||||
CompletionEvent::Event(serde_json::from_str::<T>(&line)?)
|
||||
};
|
||||
|
||||
line.clear();
|
||||
|
|
|
@ -3,6 +3,7 @@ use std::str::FromStr as _;
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Result, anyhow};
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use collections::HashMap;
|
||||
use copilot::copilot_chat::{
|
||||
ChatMessage, ChatMessageContent, ChatMessagePart, CopilotChat, ImageUrl,
|
||||
|
@ -30,7 +31,6 @@ use settings::SettingsStore;
|
|||
use std::time::Duration;
|
||||
use ui::prelude::*;
|
||||
use util::debug_panic;
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
use super::anthropic::count_anthropic_tokens;
|
||||
use super::google::count_google_tokens;
|
||||
|
|
|
@ -1625,6 +1625,10 @@ impl LspAdapter for BasedPyrightLspAdapter {
|
|||
fn manifest_name(&self) -> Option<ManifestName> {
|
||||
Some(SharedString::new_static("pyproject.toml").into())
|
||||
}
|
||||
|
||||
fn workspace_folders_content(&self) -> WorkspaceFoldersContent {
|
||||
WorkspaceFoldersContent::WorktreeRoot
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -18,12 +18,15 @@ default = []
|
|||
anyhow.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
project.workspace = true
|
||||
settings.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
workspace.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
|
287
crates/onboarding/src/editing_page.rs
Normal file
287
crates/onboarding/src/editing_page.rs
Normal file
|
@ -0,0 +1,287 @@
|
|||
use editor::{EditorSettings, ShowMinimap};
|
||||
use fs::Fs;
|
||||
use gpui::{App, IntoElement, Pixels, Window};
|
||||
use language::language_settings::AllLanguageSettings;
|
||||
use project::project_settings::ProjectSettings;
|
||||
use settings::{Settings as _, update_settings_file};
|
||||
use theme::{FontFamilyCache, FontFamilyName, ThemeSettings};
|
||||
use ui::{
|
||||
ContextMenu, DropdownMenu, IconButton, Label, LabelCommon, LabelSize, NumericStepper,
|
||||
ParentElement, SharedString, Styled, SwitchColor, SwitchField, ToggleButtonGroup,
|
||||
ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, div, h_flex, px, v_flex,
|
||||
};
|
||||
|
||||
fn read_show_mini_map(cx: &App) -> ShowMinimap {
|
||||
editor::EditorSettings::get_global(cx).minimap.show
|
||||
}
|
||||
|
||||
fn write_show_mini_map(show: ShowMinimap, cx: &mut App) {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
|
||||
update_settings_file::<EditorSettings>(fs, cx, move |editor_settings, _| {
|
||||
editor_settings.minimap.get_or_insert_default().show = Some(show);
|
||||
});
|
||||
}
|
||||
|
||||
fn read_inlay_hints(cx: &App) -> bool {
|
||||
AllLanguageSettings::get_global(cx)
|
||||
.defaults
|
||||
.inlay_hints
|
||||
.enabled
|
||||
}
|
||||
|
||||
fn write_inlay_hints(enabled: bool, cx: &mut App) {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
|
||||
update_settings_file::<AllLanguageSettings>(fs, cx, move |all_language_settings, cx| {
|
||||
all_language_settings
|
||||
.defaults
|
||||
.inlay_hints
|
||||
.get_or_insert_with(|| {
|
||||
AllLanguageSettings::get_global(cx)
|
||||
.clone()
|
||||
.defaults
|
||||
.inlay_hints
|
||||
})
|
||||
.enabled = enabled;
|
||||
});
|
||||
}
|
||||
|
||||
fn read_git_blame(cx: &App) -> bool {
|
||||
ProjectSettings::get_global(cx).git.inline_blame_enabled()
|
||||
}
|
||||
|
||||
fn set_git_blame(enabled: bool, cx: &mut App) {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
|
||||
update_settings_file::<ProjectSettings>(fs, cx, move |project_settings, _| {
|
||||
project_settings
|
||||
.git
|
||||
.inline_blame
|
||||
.get_or_insert_default()
|
||||
.enabled = enabled;
|
||||
});
|
||||
}
|
||||
|
||||
fn write_ui_font_family(font: SharedString, cx: &mut App) {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
|
||||
update_settings_file::<ThemeSettings>(fs, cx, move |theme_settings, _| {
|
||||
theme_settings.ui_font_family = Some(FontFamilyName(font.into()));
|
||||
});
|
||||
}
|
||||
|
||||
fn write_ui_font_size(size: Pixels, cx: &mut App) {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
|
||||
update_settings_file::<ThemeSettings>(fs, cx, move |theme_settings, _| {
|
||||
theme_settings.ui_font_size = Some(size.into());
|
||||
});
|
||||
}
|
||||
|
||||
fn write_buffer_font_size(size: Pixels, cx: &mut App) {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
|
||||
update_settings_file::<ThemeSettings>(fs, cx, move |theme_settings, _| {
|
||||
theme_settings.buffer_font_size = Some(size.into());
|
||||
});
|
||||
}
|
||||
|
||||
fn write_buffer_font_family(font_family: SharedString, cx: &mut App) {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
|
||||
update_settings_file::<ThemeSettings>(fs, cx, move |theme_settings, _| {
|
||||
theme_settings.buffer_font_family = Some(FontFamilyName(font_family.into()));
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme_settings = ThemeSettings::get_global(cx);
|
||||
let ui_font_size = theme_settings.ui_font_size(cx);
|
||||
let font_family = theme_settings.buffer_font.family.clone();
|
||||
let buffer_font_size = theme_settings.buffer_font_size(cx);
|
||||
|
||||
v_flex()
|
||||
.gap_4()
|
||||
.child(Label::new("Import Settings").size(LabelSize::Large))
|
||||
.child(
|
||||
Label::new("Automatically pull your settings from other editors.")
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.child(IconButton::new(
|
||||
"import-vs-code-settings",
|
||||
ui::IconName::Code,
|
||||
))
|
||||
.child(IconButton::new(
|
||||
"import-cursor-settings",
|
||||
ui::IconName::CursorIBeam,
|
||||
)),
|
||||
)
|
||||
.child(Label::new("Popular Settings").size(LabelSize::Large))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_4()
|
||||
.justify_between()
|
||||
.child(
|
||||
v_flex()
|
||||
.justify_between()
|
||||
.gap_1()
|
||||
.child(Label::new("UI Font"))
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.gap_2()
|
||||
.child(div().min_w(px(120.)).child(DropdownMenu::new(
|
||||
"ui-font-family",
|
||||
theme_settings.ui_font.family.clone(),
|
||||
ContextMenu::build(window, cx, |mut menu, _, cx| {
|
||||
let font_family_cache = FontFamilyCache::global(cx);
|
||||
|
||||
for font_name in font_family_cache.list_font_families(cx) {
|
||||
menu = menu.custom_entry(
|
||||
{
|
||||
let font_name = font_name.clone();
|
||||
move |_window, _cx| {
|
||||
Label::new(font_name.clone())
|
||||
.into_any_element()
|
||||
}
|
||||
},
|
||||
{
|
||||
let font_name = font_name.clone();
|
||||
move |_window, cx| {
|
||||
write_ui_font_family(font_name.clone(), cx);
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
menu
|
||||
}),
|
||||
)))
|
||||
.child(NumericStepper::new(
|
||||
"ui-font-size",
|
||||
ui_font_size.to_string(),
|
||||
move |_, _, cx| {
|
||||
write_ui_font_size(ui_font_size - px(1.), cx);
|
||||
},
|
||||
move |_, _, cx| {
|
||||
write_ui_font_size(ui_font_size + px(1.), cx);
|
||||
},
|
||||
)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.justify_between()
|
||||
.gap_1()
|
||||
.child(Label::new("Editor Font"))
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.gap_2()
|
||||
.child(DropdownMenu::new(
|
||||
"buffer-font-family",
|
||||
font_family,
|
||||
ContextMenu::build(window, cx, |mut menu, _, cx| {
|
||||
let font_family_cache = FontFamilyCache::global(cx);
|
||||
|
||||
for font_name in font_family_cache.list_font_families(cx) {
|
||||
menu = menu.custom_entry(
|
||||
{
|
||||
let font_name = font_name.clone();
|
||||
move |_window, _cx| {
|
||||
Label::new(font_name.clone())
|
||||
.into_any_element()
|
||||
}
|
||||
},
|
||||
{
|
||||
let font_name = font_name.clone();
|
||||
move |_window, cx| {
|
||||
write_buffer_font_family(
|
||||
font_name.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
menu
|
||||
}),
|
||||
))
|
||||
.child(NumericStepper::new(
|
||||
"buffer-font-size",
|
||||
buffer_font_size.to_string(),
|
||||
move |_, _, cx| {
|
||||
write_buffer_font_size(buffer_font_size - px(1.), cx);
|
||||
},
|
||||
move |_, _, cx| {
|
||||
write_buffer_font_size(buffer_font_size + px(1.), cx);
|
||||
},
|
||||
)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(Label::new("Mini Map"))
|
||||
.child(
|
||||
ToggleButtonGroup::single_row(
|
||||
"onboarding-show-mini-map",
|
||||
[
|
||||
ToggleButtonSimple::new("Auto", |_, _, cx| {
|
||||
write_show_mini_map(ShowMinimap::Auto, cx);
|
||||
}),
|
||||
ToggleButtonSimple::new("Always", |_, _, cx| {
|
||||
write_show_mini_map(ShowMinimap::Always, cx);
|
||||
}),
|
||||
ToggleButtonSimple::new("Never", |_, _, cx| {
|
||||
write_show_mini_map(ShowMinimap::Never, cx);
|
||||
}),
|
||||
],
|
||||
)
|
||||
.selected_index(match read_show_mini_map(cx) {
|
||||
ShowMinimap::Auto => 0,
|
||||
ShowMinimap::Always => 1,
|
||||
ShowMinimap::Never => 2,
|
||||
})
|
||||
.style(ToggleButtonGroupStyle::Outlined)
|
||||
.button_width(ui::rems_from_px(64.)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
SwitchField::new(
|
||||
"onboarding-enable-inlay-hints",
|
||||
"Inlay Hints",
|
||||
"See parameter names for function and method calls inline.",
|
||||
if read_inlay_hints(cx) {
|
||||
ui::ToggleState::Selected
|
||||
} else {
|
||||
ui::ToggleState::Unselected
|
||||
},
|
||||
|toggle_state, _, cx| {
|
||||
write_inlay_hints(toggle_state == &ToggleState::Selected, cx);
|
||||
},
|
||||
)
|
||||
.color(SwitchColor::Accent),
|
||||
)
|
||||
.child(
|
||||
SwitchField::new(
|
||||
"onboarding-git-blame-switch",
|
||||
"Git Blame",
|
||||
"See who committed each line on a given file.",
|
||||
if read_git_blame(cx) {
|
||||
ui::ToggleState::Selected
|
||||
} else {
|
||||
ui::ToggleState::Unselected
|
||||
},
|
||||
|toggle_state, _, cx| {
|
||||
set_git_blame(toggle_state == &ToggleState::Selected, cx);
|
||||
},
|
||||
)
|
||||
.color(SwitchColor::Accent),
|
||||
)
|
||||
}
|
|
@ -21,6 +21,7 @@ use workspace::{
|
|||
open_new, with_active_or_new_workspace,
|
||||
};
|
||||
|
||||
mod editing_page;
|
||||
mod welcome;
|
||||
|
||||
pub struct OnBoardingFeatureFlag {}
|
||||
|
@ -246,7 +247,9 @@ impl Onboarding {
|
|||
fn render_page(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
||||
match self.selected_page {
|
||||
SelectedPage::Basics => self.render_basics_page(window, cx).into_any_element(),
|
||||
SelectedPage::Editing => self.render_editing_page(window, cx).into_any_element(),
|
||||
SelectedPage::Editing => {
|
||||
crate::editing_page::render_editing_page(window, cx).into_any_element()
|
||||
}
|
||||
SelectedPage::AiSetup => self.render_ai_setup_page(window, cx).into_any_element(),
|
||||
}
|
||||
}
|
||||
|
@ -281,11 +284,6 @@ impl Onboarding {
|
|||
)
|
||||
}
|
||||
|
||||
fn render_editing_page(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
// div().child("editing page")
|
||||
"Right"
|
||||
}
|
||||
|
||||
fn render_ai_setup_page(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||
div().child("ai setup page")
|
||||
}
|
||||
|
|
|
@ -35,6 +35,7 @@ pub fn remote_server_dir_relative() -> &'static Path {
|
|||
|
||||
/// Sets a custom directory for all user data, overriding the default data directory.
|
||||
/// This function must be called before any other path operations that depend on the data directory.
|
||||
/// The directory's path will be canonicalized to an absolute path by a blocking FS operation.
|
||||
/// The directory will be created if it doesn't exist.
|
||||
///
|
||||
/// # Arguments
|
||||
|
@ -50,13 +51,20 @@ pub fn remote_server_dir_relative() -> &'static Path {
|
|||
///
|
||||
/// Panics if:
|
||||
/// * Called after the data directory has been initialized (e.g., via `data_dir` or `config_dir`)
|
||||
/// * The directory's path cannot be canonicalized to an absolute path
|
||||
/// * The directory cannot be created
|
||||
pub fn set_custom_data_dir(dir: &str) -> &'static PathBuf {
|
||||
if CURRENT_DATA_DIR.get().is_some() || CONFIG_DIR.get().is_some() {
|
||||
panic!("set_custom_data_dir called after data_dir or config_dir was initialized");
|
||||
}
|
||||
CUSTOM_DATA_DIR.get_or_init(|| {
|
||||
let path = PathBuf::from(dir);
|
||||
let mut path = PathBuf::from(dir);
|
||||
if path.is_relative() {
|
||||
let abs_path = path
|
||||
.canonicalize()
|
||||
.expect("failed to canonicalize custom data directory's path to an absolute path");
|
||||
path = PathBuf::from(util::paths::SanitizedPath::from(abs_path))
|
||||
}
|
||||
std::fs::create_dir_all(&path).expect("failed to create custom data directory");
|
||||
path
|
||||
})
|
||||
|
|
|
@ -13,6 +13,7 @@ use settings::{Settings as _, SettingsStore};
|
|||
use util::ResultExt as _;
|
||||
|
||||
use crate::{
|
||||
Project,
|
||||
project_settings::{ContextServerSettings, ProjectSettings},
|
||||
worktree_store::WorktreeStore,
|
||||
};
|
||||
|
@ -144,6 +145,7 @@ pub struct ContextServerStore {
|
|||
context_server_settings: HashMap<Arc<str>, ContextServerSettings>,
|
||||
servers: HashMap<ContextServerId, ContextServerState>,
|
||||
worktree_store: Entity<WorktreeStore>,
|
||||
project: WeakEntity<Project>,
|
||||
registry: Entity<ContextServerDescriptorRegistry>,
|
||||
update_servers_task: Option<Task<Result<()>>>,
|
||||
context_server_factory: Option<ContextServerFactory>,
|
||||
|
@ -161,12 +163,17 @@ pub enum Event {
|
|||
impl EventEmitter<Event> for ContextServerStore {}
|
||||
|
||||
impl ContextServerStore {
|
||||
pub fn new(worktree_store: Entity<WorktreeStore>, cx: &mut Context<Self>) -> Self {
|
||||
pub fn new(
|
||||
worktree_store: Entity<WorktreeStore>,
|
||||
weak_project: WeakEntity<Project>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Self::new_internal(
|
||||
true,
|
||||
None,
|
||||
ContextServerDescriptorRegistry::default_global(cx),
|
||||
worktree_store,
|
||||
weak_project,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
@ -184,9 +191,10 @@ impl ContextServerStore {
|
|||
pub fn test(
|
||||
registry: Entity<ContextServerDescriptorRegistry>,
|
||||
worktree_store: Entity<WorktreeStore>,
|
||||
weak_project: WeakEntity<Project>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Self::new_internal(false, None, registry, worktree_store, cx)
|
||||
Self::new_internal(false, None, registry, worktree_store, weak_project, cx)
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
|
@ -194,6 +202,7 @@ impl ContextServerStore {
|
|||
context_server_factory: ContextServerFactory,
|
||||
registry: Entity<ContextServerDescriptorRegistry>,
|
||||
worktree_store: Entity<WorktreeStore>,
|
||||
weak_project: WeakEntity<Project>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Self::new_internal(
|
||||
|
@ -201,6 +210,7 @@ impl ContextServerStore {
|
|||
Some(context_server_factory),
|
||||
registry,
|
||||
worktree_store,
|
||||
weak_project,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
@ -210,6 +220,7 @@ impl ContextServerStore {
|
|||
context_server_factory: Option<ContextServerFactory>,
|
||||
registry: Entity<ContextServerDescriptorRegistry>,
|
||||
worktree_store: Entity<WorktreeStore>,
|
||||
weak_project: WeakEntity<Project>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let subscriptions = if maintain_server_loop {
|
||||
|
@ -235,6 +246,7 @@ impl ContextServerStore {
|
|||
context_server_settings: Self::resolve_context_server_settings(&worktree_store, cx)
|
||||
.clone(),
|
||||
worktree_store,
|
||||
project: weak_project,
|
||||
registry,
|
||||
needs_server_update: false,
|
||||
servers: HashMap::default(),
|
||||
|
@ -360,7 +372,7 @@ impl ContextServerStore {
|
|||
let configuration = state.configuration();
|
||||
|
||||
self.stop_server(&state.server().id(), cx)?;
|
||||
let new_server = self.create_context_server(id.clone(), configuration.clone())?;
|
||||
let new_server = self.create_context_server(id.clone(), configuration.clone(), cx);
|
||||
self.run_server(new_server, configuration, cx);
|
||||
}
|
||||
Ok(())
|
||||
|
@ -449,14 +461,33 @@ impl ContextServerStore {
|
|||
&self,
|
||||
id: ContextServerId,
|
||||
configuration: Arc<ContextServerConfiguration>,
|
||||
) -> Result<Arc<ContextServer>> {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Arc<ContextServer> {
|
||||
let root_path = self
|
||||
.project
|
||||
.read_with(cx, |project, cx| project.active_project_directory(cx))
|
||||
.ok()
|
||||
.flatten()
|
||||
.or_else(|| {
|
||||
self.worktree_store.read_with(cx, |store, cx| {
|
||||
store.visible_worktrees(cx).fold(None, |acc, item| {
|
||||
if acc.is_none() {
|
||||
item.read(cx).root_dir()
|
||||
} else {
|
||||
acc
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
if let Some(factory) = self.context_server_factory.as_ref() {
|
||||
Ok(factory(id, configuration))
|
||||
factory(id, configuration)
|
||||
} else {
|
||||
Ok(Arc::new(ContextServer::stdio(
|
||||
Arc::new(ContextServer::stdio(
|
||||
id,
|
||||
configuration.command().clone(),
|
||||
)))
|
||||
root_path,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -553,7 +584,7 @@ impl ContextServerStore {
|
|||
let mut servers_to_remove = HashSet::default();
|
||||
let mut servers_to_stop = HashSet::default();
|
||||
|
||||
this.update(cx, |this, _cx| {
|
||||
this.update(cx, |this, cx| {
|
||||
for server_id in this.servers.keys() {
|
||||
// All servers that are not in desired_servers should be removed from the store.
|
||||
// This can happen if the user removed a server from the context server settings.
|
||||
|
@ -572,14 +603,10 @@ impl ContextServerStore {
|
|||
let existing_config = state.as_ref().map(|state| state.configuration());
|
||||
if existing_config.as_deref() != Some(&config) || is_stopped {
|
||||
let config = Arc::new(config);
|
||||
if let Some(server) = this
|
||||
.create_context_server(id.clone(), config.clone())
|
||||
.log_err()
|
||||
{
|
||||
servers_to_start.push((server, config));
|
||||
if this.servers.contains_key(&id) {
|
||||
servers_to_stop.insert(id);
|
||||
}
|
||||
let server = this.create_context_server(id.clone(), config.clone(), cx);
|
||||
servers_to_start.push((server, config));
|
||||
if this.servers.contains_key(&id) {
|
||||
servers_to_stop.insert(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -630,7 +657,12 @@ mod tests {
|
|||
|
||||
let registry = cx.new(|_| ContextServerDescriptorRegistry::new());
|
||||
let store = cx.new(|cx| {
|
||||
ContextServerStore::test(registry.clone(), project.read(cx).worktree_store(), cx)
|
||||
ContextServerStore::test(
|
||||
registry.clone(),
|
||||
project.read(cx).worktree_store(),
|
||||
project.downgrade(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let server_1_id = ContextServerId(SERVER_1_ID.into());
|
||||
|
@ -705,7 +737,12 @@ mod tests {
|
|||
|
||||
let registry = cx.new(|_| ContextServerDescriptorRegistry::new());
|
||||
let store = cx.new(|cx| {
|
||||
ContextServerStore::test(registry.clone(), project.read(cx).worktree_store(), cx)
|
||||
ContextServerStore::test(
|
||||
registry.clone(),
|
||||
project.read(cx).worktree_store(),
|
||||
project.downgrade(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let server_1_id = ContextServerId(SERVER_1_ID.into());
|
||||
|
@ -758,7 +795,12 @@ mod tests {
|
|||
|
||||
let registry = cx.new(|_| ContextServerDescriptorRegistry::new());
|
||||
let store = cx.new(|cx| {
|
||||
ContextServerStore::test(registry.clone(), project.read(cx).worktree_store(), cx)
|
||||
ContextServerStore::test(
|
||||
registry.clone(),
|
||||
project.read(cx).worktree_store(),
|
||||
project.downgrade(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let server_id = ContextServerId(SERVER_1_ID.into());
|
||||
|
@ -842,6 +884,7 @@ mod tests {
|
|||
}),
|
||||
registry.clone(),
|
||||
project.read(cx).worktree_store(),
|
||||
project.downgrade(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
@ -1074,6 +1117,7 @@ mod tests {
|
|||
}),
|
||||
registry.clone(),
|
||||
project.read(cx).worktree_store(),
|
||||
project.downgrade(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
|
|
@ -2425,36 +2425,12 @@ impl LocalLspStore {
|
|||
let server_id = server_node.server_id_or_init(
|
||||
|LaunchDisposition {
|
||||
server_name,
|
||||
attach,
|
||||
|
||||
path,
|
||||
settings,
|
||||
}| {
|
||||
let server_id = match attach {
|
||||
language::Attach::InstancePerRoot => {
|
||||
// todo: handle instance per root proper.
|
||||
if let Some(server_ids) = self
|
||||
.language_server_ids
|
||||
.get(&(worktree_id, server_name.clone()))
|
||||
{
|
||||
server_ids.iter().cloned().next().unwrap()
|
||||
} else {
|
||||
let language_name = language.name();
|
||||
let adapter = self.languages
|
||||
.lsp_adapters(&language_name)
|
||||
.into_iter()
|
||||
.find(|adapter| &adapter.name() == server_name)
|
||||
.expect("To find LSP adapter");
|
||||
let server_id = self.start_language_server(
|
||||
&worktree,
|
||||
delegate.clone(),
|
||||
adapter,
|
||||
settings,
|
||||
cx,
|
||||
);
|
||||
server_id
|
||||
}
|
||||
}
|
||||
language::Attach::Shared => {
|
||||
let server_id =
|
||||
{
|
||||
let uri = Url::from_file_path(
|
||||
worktree.read(cx).abs_path().join(&path.path),
|
||||
);
|
||||
|
@ -2489,7 +2465,7 @@ impl LocalLspStore {
|
|||
} else {
|
||||
unreachable!("Language server ID should be available, as it's registered on demand")
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
let lsp_store = self.weak.clone();
|
||||
let server_name = server_node.name();
|
||||
|
@ -4705,35 +4681,11 @@ impl LspStore {
|
|||
let server_id = node.server_id_or_init(
|
||||
|LaunchDisposition {
|
||||
server_name,
|
||||
attach,
|
||||
|
||||
path,
|
||||
settings,
|
||||
}| match attach {
|
||||
language::Attach::InstancePerRoot => {
|
||||
// todo: handle instance per root proper.
|
||||
if let Some(server_ids) = local
|
||||
.language_server_ids
|
||||
.get(&(worktree_id, server_name.clone()))
|
||||
{
|
||||
server_ids.iter().cloned().next().unwrap()
|
||||
} else {
|
||||
let adapter = local
|
||||
.languages
|
||||
.lsp_adapters(&language)
|
||||
.into_iter()
|
||||
.find(|adapter| &adapter.name() == server_name)
|
||||
.expect("To find LSP adapter");
|
||||
let server_id = local.start_language_server(
|
||||
&worktree,
|
||||
delegate.clone(),
|
||||
adapter,
|
||||
settings,
|
||||
cx,
|
||||
);
|
||||
server_id
|
||||
}
|
||||
}
|
||||
language::Attach::Shared => {
|
||||
}|
|
||||
{
|
||||
let uri = Url::from_file_path(
|
||||
worktree.read(cx).abs_path().join(&path.path),
|
||||
);
|
||||
|
@ -4762,7 +4714,6 @@ impl LspStore {
|
|||
}
|
||||
server_id
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if let Some(language_server_id) = server_id {
|
||||
|
@ -7442,21 +7393,23 @@ impl LspStore {
|
|||
}
|
||||
|
||||
pub(crate) async fn refresh_workspace_configurations(
|
||||
this: &WeakEntity<Self>,
|
||||
lsp_store: &WeakEntity<Self>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut AsyncApp,
|
||||
) {
|
||||
maybe!(async move {
|
||||
let servers = this
|
||||
.update(cx, |this, cx| {
|
||||
let Some(local) = this.as_local() else {
|
||||
let mut refreshed_servers = HashSet::default();
|
||||
let servers = lsp_store
|
||||
.update(cx, |lsp_store, cx| {
|
||||
let toolchain_store = lsp_store.toolchain_store(cx);
|
||||
let Some(local) = lsp_store.as_local() else {
|
||||
return Vec::default();
|
||||
};
|
||||
local
|
||||
.language_server_ids
|
||||
.iter()
|
||||
.flat_map(|((worktree_id, _), server_ids)| {
|
||||
let worktree = this
|
||||
let worktree = lsp_store
|
||||
.worktree_store
|
||||
.read(cx)
|
||||
.worktree_for_id(*worktree_id, cx);
|
||||
|
@ -7472,43 +7425,54 @@ impl LspStore {
|
|||
)
|
||||
});
|
||||
|
||||
server_ids.iter().filter_map(move |server_id| {
|
||||
let fs = fs.clone();
|
||||
let toolchain_store = toolchain_store.clone();
|
||||
server_ids.iter().filter_map(|server_id| {
|
||||
let delegate = delegate.clone()? as Arc<dyn LspAdapterDelegate>;
|
||||
let states = local.language_servers.get(server_id)?;
|
||||
|
||||
match states {
|
||||
LanguageServerState::Starting { .. } => None,
|
||||
LanguageServerState::Running {
|
||||
adapter, server, ..
|
||||
} => Some((
|
||||
adapter.adapter.clone(),
|
||||
server.clone(),
|
||||
delegate.clone()? as Arc<dyn LspAdapterDelegate>,
|
||||
)),
|
||||
} => {
|
||||
let fs = fs.clone();
|
||||
let toolchain_store = toolchain_store.clone();
|
||||
let adapter = adapter.clone();
|
||||
let server = server.clone();
|
||||
refreshed_servers.insert(server.name());
|
||||
Some(cx.spawn(async move |_, cx| {
|
||||
let settings =
|
||||
LocalLspStore::workspace_configuration_for_adapter(
|
||||
adapter.adapter.clone(),
|
||||
fs.as_ref(),
|
||||
&delegate,
|
||||
toolchain_store,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.ok()?;
|
||||
server
|
||||
.notify::<lsp::notification::DidChangeConfiguration>(
|
||||
&lsp::DidChangeConfigurationParams { settings },
|
||||
)
|
||||
.ok()?;
|
||||
Some(())
|
||||
}))
|
||||
}
|
||||
}
|
||||
})
|
||||
}).collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.ok()?;
|
||||
|
||||
let toolchain_store = this.update(cx, |this, cx| this.toolchain_store(cx)).ok()?;
|
||||
for (adapter, server, delegate) in servers {
|
||||
let settings = LocalLspStore::workspace_configuration_for_adapter(
|
||||
adapter,
|
||||
fs.as_ref(),
|
||||
&delegate,
|
||||
toolchain_store.clone(),
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
server
|
||||
.notify::<lsp::notification::DidChangeConfiguration>(
|
||||
&lsp::DidChangeConfigurationParams { settings },
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
log::info!("Refreshing workspace configurations for servers {refreshed_servers:?}");
|
||||
// TODO this asynchronous job runs concurrently with extension (de)registration and may take enough time for a certain extension
|
||||
// to stop and unregister its language server wrapper.
|
||||
// This is racy : an extension might have already removed all `local.language_servers` state, but here we `.clone()` and hold onto it anyway.
|
||||
// This now causes errors in the logs, we should find a way to remove such servers from the processing everywhere.
|
||||
let _: Vec<Option<()>> = join_all(servers).await;
|
||||
Some(())
|
||||
})
|
||||
.await;
|
||||
|
|
|
@ -13,10 +13,10 @@ use std::{
|
|||
sync::{Arc, Weak},
|
||||
};
|
||||
|
||||
use collections::{HashMap, IndexMap};
|
||||
use collections::IndexMap;
|
||||
use gpui::{App, AppContext as _, Entity, Subscription};
|
||||
use language::{
|
||||
Attach, CachedLspAdapter, LanguageName, LanguageRegistry, ManifestDelegate,
|
||||
CachedLspAdapter, LanguageName, LanguageRegistry, ManifestDelegate,
|
||||
language_settings::AllLanguageSettings,
|
||||
};
|
||||
use lsp::LanguageServerName;
|
||||
|
@ -38,7 +38,6 @@ pub(crate) struct ServersForWorktree {
|
|||
pub struct LanguageServerTree {
|
||||
manifest_tree: Entity<ManifestTree>,
|
||||
pub(crate) instances: BTreeMap<WorktreeId, ServersForWorktree>,
|
||||
attach_kind_cache: HashMap<LanguageServerName, Attach>,
|
||||
languages: Arc<LanguageRegistry>,
|
||||
_subscriptions: Subscription,
|
||||
}
|
||||
|
@ -53,7 +52,6 @@ pub struct LanguageServerTreeNode(Weak<InnerTreeNode>);
|
|||
#[derive(Debug)]
|
||||
pub(crate) struct LaunchDisposition<'a> {
|
||||
pub(crate) server_name: &'a LanguageServerName,
|
||||
pub(crate) attach: Attach,
|
||||
pub(crate) path: ProjectPath,
|
||||
pub(crate) settings: Arc<LspSettings>,
|
||||
}
|
||||
|
@ -62,7 +60,6 @@ impl<'a> From<&'a InnerTreeNode> for LaunchDisposition<'a> {
|
|||
fn from(value: &'a InnerTreeNode) -> Self {
|
||||
LaunchDisposition {
|
||||
server_name: &value.name,
|
||||
attach: value.attach,
|
||||
path: value.path.clone(),
|
||||
settings: value.settings.clone(),
|
||||
}
|
||||
|
@ -105,7 +102,6 @@ impl From<Weak<InnerTreeNode>> for LanguageServerTreeNode {
|
|||
pub struct InnerTreeNode {
|
||||
id: OnceLock<LanguageServerId>,
|
||||
name: LanguageServerName,
|
||||
attach: Attach,
|
||||
path: ProjectPath,
|
||||
settings: Arc<LspSettings>,
|
||||
}
|
||||
|
@ -113,14 +109,12 @@ pub struct InnerTreeNode {
|
|||
impl InnerTreeNode {
|
||||
fn new(
|
||||
name: LanguageServerName,
|
||||
attach: Attach,
|
||||
path: ProjectPath,
|
||||
settings: impl Into<Arc<LspSettings>>,
|
||||
) -> Self {
|
||||
InnerTreeNode {
|
||||
id: Default::default(),
|
||||
name,
|
||||
attach,
|
||||
path,
|
||||
settings: settings.into(),
|
||||
}
|
||||
|
@ -130,8 +124,11 @@ impl InnerTreeNode {
|
|||
/// Determines how the list of adapters to query should be constructed.
|
||||
pub(crate) enum AdapterQuery<'a> {
|
||||
/// Search for roots of all adapters associated with a given language name.
|
||||
/// Layman: Look for all project roots along the queried path that have any
|
||||
/// language server associated with this language running.
|
||||
Language(&'a LanguageName),
|
||||
/// Search for roots of adapter with a given name.
|
||||
/// Layman: Look for all project roots along the queried path that have this server running.
|
||||
Adapter(&'a LanguageServerName),
|
||||
}
|
||||
|
||||
|
@ -147,7 +144,7 @@ impl LanguageServerTree {
|
|||
}),
|
||||
manifest_tree,
|
||||
instances: Default::default(),
|
||||
attach_kind_cache: Default::default(),
|
||||
|
||||
languages,
|
||||
})
|
||||
}
|
||||
|
@ -223,7 +220,6 @@ impl LanguageServerTree {
|
|||
.and_then(|name| roots.get(&name))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| root_path.clone());
|
||||
let attach = adapter.attach_kind();
|
||||
|
||||
let inner_node = self
|
||||
.instances
|
||||
|
@ -237,7 +233,6 @@ impl LanguageServerTree {
|
|||
(
|
||||
Arc::new(InnerTreeNode::new(
|
||||
adapter.name(),
|
||||
attach,
|
||||
root_path.clone(),
|
||||
settings.clone(),
|
||||
)),
|
||||
|
@ -379,7 +374,6 @@ pub(crate) struct ServerTreeRebase<'a> {
|
|||
impl<'tree> ServerTreeRebase<'tree> {
|
||||
fn new(new_tree: &'tree mut LanguageServerTree) -> Self {
|
||||
let old_contents = std::mem::take(&mut new_tree.instances);
|
||||
new_tree.attach_kind_cache.clear();
|
||||
let all_server_ids = old_contents
|
||||
.values()
|
||||
.flat_map(|nodes| {
|
||||
|
@ -446,10 +440,7 @@ impl<'tree> ServerTreeRebase<'tree> {
|
|||
.get(&disposition.path.worktree_id)
|
||||
.and_then(|worktree_nodes| worktree_nodes.roots.get(&disposition.path.path))
|
||||
.and_then(|roots| roots.get(&disposition.name))
|
||||
.filter(|(old_node, _)| {
|
||||
disposition.attach == old_node.attach
|
||||
&& disposition.settings == old_node.settings
|
||||
})
|
||||
.filter(|(old_node, _)| disposition.settings == old_node.settings)
|
||||
else {
|
||||
return Some(node);
|
||||
};
|
||||
|
|
|
@ -998,8 +998,9 @@ impl Project {
|
|||
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
|
||||
.detach();
|
||||
|
||||
let weak_self = cx.weak_entity();
|
||||
let context_server_store =
|
||||
cx.new(|cx| ContextServerStore::new(worktree_store.clone(), cx));
|
||||
cx.new(|cx| ContextServerStore::new(worktree_store.clone(), weak_self, cx));
|
||||
|
||||
let environment = cx.new(|_| ProjectEnvironment::new(env));
|
||||
let manifest_tree = ManifestTree::new(worktree_store.clone(), cx);
|
||||
|
@ -1167,8 +1168,9 @@ impl Project {
|
|||
cx.subscribe(&worktree_store, Self::on_worktree_store_event)
|
||||
.detach();
|
||||
|
||||
let weak_self = cx.weak_entity();
|
||||
let context_server_store =
|
||||
cx.new(|cx| ContextServerStore::new(worktree_store.clone(), cx));
|
||||
cx.new(|cx| ContextServerStore::new(worktree_store.clone(), weak_self, cx));
|
||||
|
||||
let buffer_store = cx.new(|cx| {
|
||||
BufferStore::remote(
|
||||
|
@ -1428,8 +1430,6 @@ impl Project {
|
|||
let image_store = cx.new(|cx| {
|
||||
ImageStore::remote(worktree_store.clone(), client.clone().into(), remote_id, cx)
|
||||
})?;
|
||||
let context_server_store =
|
||||
cx.new(|cx| ContextServerStore::new(worktree_store.clone(), cx))?;
|
||||
|
||||
let environment = cx.new(|_| ProjectEnvironment::new(None))?;
|
||||
|
||||
|
@ -1496,6 +1496,10 @@ impl Project {
|
|||
|
||||
let snippets = SnippetProvider::new(fs.clone(), BTreeSet::from_iter([]), cx);
|
||||
|
||||
let weak_self = cx.weak_entity();
|
||||
let context_server_store =
|
||||
cx.new(|cx| ContextServerStore::new(worktree_store.clone(), weak_self, cx));
|
||||
|
||||
let mut worktrees = Vec::new();
|
||||
for worktree in response.payload.worktrees {
|
||||
let worktree =
|
||||
|
|
|
@ -20,6 +20,7 @@ static REDACT_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"key=[^&]+")
|
|||
pub struct ReqwestClient {
|
||||
client: reqwest::Client,
|
||||
proxy: Option<Url>,
|
||||
user_agent: Option<HeaderValue>,
|
||||
handle: tokio::runtime::Handle,
|
||||
}
|
||||
|
||||
|
@ -44,9 +45,11 @@ impl ReqwestClient {
|
|||
Ok(client.into())
|
||||
}
|
||||
|
||||
pub fn proxy_and_user_agent(proxy: Option<Url>, agent: &str) -> anyhow::Result<Self> {
|
||||
pub fn proxy_and_user_agent(proxy: Option<Url>, user_agent: &str) -> anyhow::Result<Self> {
|
||||
let user_agent = HeaderValue::from_str(user_agent)?;
|
||||
|
||||
let mut map = HeaderMap::new();
|
||||
map.insert(http::header::USER_AGENT, HeaderValue::from_str(agent)?);
|
||||
map.insert(http::header::USER_AGENT, user_agent.clone());
|
||||
let mut client = Self::builder().default_headers(map);
|
||||
let client_has_proxy;
|
||||
|
||||
|
@ -73,6 +76,7 @@ impl ReqwestClient {
|
|||
.build()?;
|
||||
let mut client: ReqwestClient = client.into();
|
||||
client.proxy = client_has_proxy.then_some(proxy).flatten();
|
||||
client.user_agent = Some(user_agent);
|
||||
Ok(client)
|
||||
}
|
||||
}
|
||||
|
@ -96,6 +100,7 @@ impl From<reqwest::Client> for ReqwestClient {
|
|||
client,
|
||||
handle,
|
||||
proxy: None,
|
||||
user_agent: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -216,6 +221,10 @@ impl http_client::HttpClient for ReqwestClient {
|
|||
type_name::<Self>()
|
||||
}
|
||||
|
||||
fn user_agent(&self) -> Option<&HeaderValue> {
|
||||
self.user_agent.as_ref()
|
||||
}
|
||||
|
||||
fn send(
|
||||
&self,
|
||||
req: http::Request<http_client::AsyncBody>,
|
||||
|
|
|
@ -48,3 +48,7 @@ workspace.workspace = true
|
|||
|
||||
[dev-dependencies]
|
||||
db = {"workspace"= true, "features" = ["test-support"]}
|
||||
fs = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
|
|
|
@ -11,11 +11,10 @@ use editor::{CompletionProvider, Editor, EditorEvent};
|
|||
use fs::Fs;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
Action, Animation, AnimationExt, AppContext as _, AsyncApp, Axis, ClickEvent, Context,
|
||||
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Global, IsZero,
|
||||
KeyContext, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, Point, ScrollStrategy,
|
||||
ScrollWheelEvent, Stateful, StyledText, Subscription, Task, TextStyleRefinement, WeakEntity,
|
||||
actions, anchored, deferred, div,
|
||||
Action, AppContext as _, AsyncApp, Axis, ClickEvent, Context, DismissEvent, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, Global, IsZero, KeyContext, Keystroke, MouseButton,
|
||||
Point, ScrollStrategy, ScrollWheelEvent, Stateful, StyledText, Subscription, Task,
|
||||
TextStyleRefinement, WeakEntity, actions, anchored, deferred, div,
|
||||
};
|
||||
use language::{Language, LanguageConfig, ToOffset as _};
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
|
@ -35,7 +34,10 @@ use workspace::{
|
|||
|
||||
use crate::{
|
||||
keybindings::persistence::KEYBINDING_EDITORS,
|
||||
ui_components::table::{ColumnWidths, ResizeBehavior, Table, TableInteractionState},
|
||||
ui_components::{
|
||||
keystroke_input::{ClearKeystrokes, KeystrokeInput, StartRecording, StopRecording},
|
||||
table::{ColumnWidths, ResizeBehavior, Table, TableInteractionState},
|
||||
},
|
||||
};
|
||||
|
||||
const NO_ACTION_ARGUMENTS_TEXT: SharedString = SharedString::new_static("<no arguments>");
|
||||
|
@ -72,18 +74,6 @@ actions!(
|
|||
]
|
||||
);
|
||||
|
||||
actions!(
|
||||
keystroke_input,
|
||||
[
|
||||
/// Starts recording keystrokes
|
||||
StartRecording,
|
||||
/// Stops recording keystrokes
|
||||
StopRecording,
|
||||
/// Clears the recorded keystrokes
|
||||
ClearKeystrokes,
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
let keymap_event_channel = KeymapEventChannel::new();
|
||||
cx.set_global(keymap_event_channel);
|
||||
|
@ -393,7 +383,7 @@ impl KeymapEditor {
|
|||
|
||||
let keystroke_editor = cx.new(|cx| {
|
||||
let mut keystroke_editor = KeystrokeInput::new(None, window, cx);
|
||||
keystroke_editor.search = true;
|
||||
keystroke_editor.set_search(true);
|
||||
keystroke_editor
|
||||
});
|
||||
|
||||
|
@ -2979,524 +2969,6 @@ async fn remove_keybinding(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
enum CloseKeystrokeResult {
|
||||
Partial,
|
||||
Close,
|
||||
None,
|
||||
}
|
||||
|
||||
struct KeystrokeInput {
|
||||
keystrokes: Vec<Keystroke>,
|
||||
placeholder_keystrokes: Option<Vec<Keystroke>>,
|
||||
outer_focus_handle: FocusHandle,
|
||||
inner_focus_handle: FocusHandle,
|
||||
intercept_subscription: Option<Subscription>,
|
||||
_focus_subscriptions: [Subscription; 2],
|
||||
search: bool,
|
||||
/// Handles tripe escape to stop recording
|
||||
close_keystrokes: Option<Vec<Keystroke>>,
|
||||
close_keystrokes_start: Option<usize>,
|
||||
previous_modifiers: Modifiers,
|
||||
}
|
||||
|
||||
impl KeystrokeInput {
|
||||
const KEYSTROKE_COUNT_MAX: usize = 3;
|
||||
|
||||
fn new(
|
||||
placeholder_keystrokes: Option<Vec<Keystroke>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let outer_focus_handle = cx.focus_handle();
|
||||
let inner_focus_handle = cx.focus_handle();
|
||||
let _focus_subscriptions = [
|
||||
cx.on_focus_in(&inner_focus_handle, window, Self::on_inner_focus_in),
|
||||
cx.on_focus_out(&inner_focus_handle, window, Self::on_inner_focus_out),
|
||||
];
|
||||
Self {
|
||||
keystrokes: Vec::new(),
|
||||
placeholder_keystrokes,
|
||||
inner_focus_handle,
|
||||
outer_focus_handle,
|
||||
intercept_subscription: None,
|
||||
_focus_subscriptions,
|
||||
search: false,
|
||||
close_keystrokes: None,
|
||||
close_keystrokes_start: None,
|
||||
previous_modifiers: Modifiers::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_keystrokes(&mut self, keystrokes: Vec<Keystroke>, cx: &mut Context<Self>) {
|
||||
self.keystrokes = keystrokes;
|
||||
self.keystrokes_changed(cx);
|
||||
}
|
||||
|
||||
fn dummy(modifiers: Modifiers) -> Keystroke {
|
||||
return Keystroke {
|
||||
modifiers,
|
||||
key: "".to_string(),
|
||||
key_char: None,
|
||||
};
|
||||
}
|
||||
|
||||
fn keystrokes_changed(&self, cx: &mut Context<Self>) {
|
||||
cx.emit(());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn key_context() -> KeyContext {
|
||||
let mut key_context = KeyContext::default();
|
||||
key_context.add("KeystrokeInput");
|
||||
key_context
|
||||
}
|
||||
|
||||
fn handle_possible_close_keystroke(
|
||||
&mut self,
|
||||
keystroke: &Keystroke,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> CloseKeystrokeResult {
|
||||
let Some(keybind_for_close_action) = window
|
||||
.highest_precedence_binding_for_action_in_context(&StopRecording, Self::key_context())
|
||||
else {
|
||||
log::trace!("No keybinding to stop recording keystrokes in keystroke input");
|
||||
self.close_keystrokes.take();
|
||||
self.close_keystrokes_start.take();
|
||||
return CloseKeystrokeResult::None;
|
||||
};
|
||||
let action_keystrokes = keybind_for_close_action.keystrokes();
|
||||
|
||||
if let Some(mut close_keystrokes) = self.close_keystrokes.take() {
|
||||
let mut index = 0;
|
||||
|
||||
while index < action_keystrokes.len() && index < close_keystrokes.len() {
|
||||
if !close_keystrokes[index].should_match(&action_keystrokes[index]) {
|
||||
break;
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
if index == close_keystrokes.len() {
|
||||
if index >= action_keystrokes.len() {
|
||||
self.close_keystrokes_start.take();
|
||||
return CloseKeystrokeResult::None;
|
||||
}
|
||||
if keystroke.should_match(&action_keystrokes[index]) {
|
||||
if action_keystrokes.len() >= 1 && index == action_keystrokes.len() - 1 {
|
||||
self.stop_recording(&StopRecording, window, cx);
|
||||
return CloseKeystrokeResult::Close;
|
||||
} else {
|
||||
close_keystrokes.push(keystroke.clone());
|
||||
self.close_keystrokes = Some(close_keystrokes);
|
||||
return CloseKeystrokeResult::Partial;
|
||||
}
|
||||
} else {
|
||||
self.close_keystrokes_start.take();
|
||||
return CloseKeystrokeResult::None;
|
||||
}
|
||||
}
|
||||
} else if let Some(first_action_keystroke) = action_keystrokes.first()
|
||||
&& keystroke.should_match(first_action_keystroke)
|
||||
{
|
||||
self.close_keystrokes = Some(vec![keystroke.clone()]);
|
||||
return CloseKeystrokeResult::Partial;
|
||||
}
|
||||
self.close_keystrokes_start.take();
|
||||
return CloseKeystrokeResult::None;
|
||||
}
|
||||
|
||||
fn on_modifiers_changed(
|
||||
&mut self,
|
||||
event: &ModifiersChangedEvent,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let keystrokes_len = self.keystrokes.len();
|
||||
|
||||
if self.previous_modifiers.modified()
|
||||
&& event.modifiers.is_subset_of(&self.previous_modifiers)
|
||||
{
|
||||
self.previous_modifiers &= event.modifiers;
|
||||
cx.stop_propagation();
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(last) = self.keystrokes.last_mut()
|
||||
&& last.key.is_empty()
|
||||
&& keystrokes_len <= Self::KEYSTROKE_COUNT_MAX
|
||||
{
|
||||
if self.search {
|
||||
if self.previous_modifiers.modified() {
|
||||
last.modifiers |= event.modifiers;
|
||||
self.previous_modifiers |= event.modifiers;
|
||||
} else {
|
||||
self.keystrokes.push(Self::dummy(event.modifiers));
|
||||
self.previous_modifiers |= event.modifiers;
|
||||
}
|
||||
} else if !event.modifiers.modified() {
|
||||
self.keystrokes.pop();
|
||||
} else {
|
||||
last.modifiers = event.modifiers;
|
||||
}
|
||||
|
||||
self.keystrokes_changed(cx);
|
||||
} else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX {
|
||||
self.keystrokes.push(Self::dummy(event.modifiers));
|
||||
if self.search {
|
||||
self.previous_modifiers |= event.modifiers;
|
||||
}
|
||||
self.keystrokes_changed(cx);
|
||||
}
|
||||
cx.stop_propagation();
|
||||
}
|
||||
|
||||
fn handle_keystroke(
|
||||
&mut self,
|
||||
keystroke: &Keystroke,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let close_keystroke_result = self.handle_possible_close_keystroke(keystroke, window, cx);
|
||||
if close_keystroke_result != CloseKeystrokeResult::Close {
|
||||
let key_len = self.keystrokes.len();
|
||||
if let Some(last) = self.keystrokes.last_mut()
|
||||
&& last.key.is_empty()
|
||||
&& key_len <= Self::KEYSTROKE_COUNT_MAX
|
||||
{
|
||||
if self.search {
|
||||
last.key = keystroke.key.clone();
|
||||
if close_keystroke_result == CloseKeystrokeResult::Partial
|
||||
&& self.close_keystrokes_start.is_none()
|
||||
{
|
||||
self.close_keystrokes_start = Some(self.keystrokes.len() - 1);
|
||||
}
|
||||
if self.search {
|
||||
self.previous_modifiers = keystroke.modifiers;
|
||||
}
|
||||
self.keystrokes_changed(cx);
|
||||
cx.stop_propagation();
|
||||
return;
|
||||
} else {
|
||||
self.keystrokes.pop();
|
||||
}
|
||||
}
|
||||
if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX {
|
||||
if close_keystroke_result == CloseKeystrokeResult::Partial
|
||||
&& self.close_keystrokes_start.is_none()
|
||||
{
|
||||
self.close_keystrokes_start = Some(self.keystrokes.len());
|
||||
}
|
||||
self.keystrokes.push(keystroke.clone());
|
||||
if self.search {
|
||||
self.previous_modifiers = keystroke.modifiers;
|
||||
} else if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX {
|
||||
self.keystrokes.push(Self::dummy(keystroke.modifiers));
|
||||
}
|
||||
} else if close_keystroke_result != CloseKeystrokeResult::Partial {
|
||||
self.clear_keystrokes(&ClearKeystrokes, window, cx);
|
||||
}
|
||||
}
|
||||
self.keystrokes_changed(cx);
|
||||
cx.stop_propagation();
|
||||
}
|
||||
|
||||
fn on_inner_focus_in(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.intercept_subscription.is_none() {
|
||||
let listener = cx.listener(|this, event: &gpui::KeystrokeEvent, window, cx| {
|
||||
this.handle_keystroke(&event.keystroke, window, cx);
|
||||
});
|
||||
self.intercept_subscription = Some(cx.intercept_keystrokes(listener))
|
||||
}
|
||||
}
|
||||
|
||||
fn on_inner_focus_out(
|
||||
&mut self,
|
||||
_event: gpui::FocusOutEvent,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.intercept_subscription.take();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn keystrokes(&self) -> &[Keystroke] {
|
||||
if let Some(placeholders) = self.placeholder_keystrokes.as_ref()
|
||||
&& self.keystrokes.is_empty()
|
||||
{
|
||||
return placeholders;
|
||||
}
|
||||
if !self.search
|
||||
&& self
|
||||
.keystrokes
|
||||
.last()
|
||||
.map_or(false, |last| last.key.is_empty())
|
||||
{
|
||||
return &self.keystrokes[..self.keystrokes.len() - 1];
|
||||
}
|
||||
return &self.keystrokes;
|
||||
}
|
||||
|
||||
fn render_keystrokes(&self, is_recording: bool) -> impl Iterator<Item = Div> {
|
||||
let keystrokes = if let Some(placeholders) = self.placeholder_keystrokes.as_ref()
|
||||
&& self.keystrokes.is_empty()
|
||||
{
|
||||
if is_recording {
|
||||
&[]
|
||||
} else {
|
||||
placeholders.as_slice()
|
||||
}
|
||||
} else {
|
||||
&self.keystrokes
|
||||
};
|
||||
keystrokes.iter().map(move |keystroke| {
|
||||
h_flex().children(ui::render_keystroke(
|
||||
keystroke,
|
||||
Some(Color::Default),
|
||||
Some(rems(0.875).into()),
|
||||
ui::PlatformStyle::platform(),
|
||||
false,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn start_recording(&mut self, _: &StartRecording, window: &mut Window, cx: &mut Context<Self>) {
|
||||
window.focus(&self.inner_focus_handle);
|
||||
self.clear_keystrokes(&ClearKeystrokes, window, cx);
|
||||
self.previous_modifiers = window.modifiers();
|
||||
cx.stop_propagation();
|
||||
}
|
||||
|
||||
fn stop_recording(&mut self, _: &StopRecording, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if !self.inner_focus_handle.is_focused(window) {
|
||||
return;
|
||||
}
|
||||
window.focus(&self.outer_focus_handle);
|
||||
if let Some(close_keystrokes_start) = self.close_keystrokes_start.take()
|
||||
&& close_keystrokes_start < self.keystrokes.len()
|
||||
{
|
||||
self.keystrokes.drain(close_keystrokes_start..);
|
||||
}
|
||||
self.close_keystrokes.take();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn clear_keystrokes(
|
||||
&mut self,
|
||||
_: &ClearKeystrokes,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.keystrokes.clear();
|
||||
self.keystrokes_changed(cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<()> for KeystrokeInput {}
|
||||
|
||||
impl Focusable for KeystrokeInput {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.outer_focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for KeystrokeInput {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let colors = cx.theme().colors();
|
||||
let is_focused = self.outer_focus_handle.contains_focused(window, cx);
|
||||
let is_recording = self.inner_focus_handle.is_focused(window);
|
||||
|
||||
let horizontal_padding = rems_from_px(64.);
|
||||
|
||||
let recording_bg_color = colors
|
||||
.editor_background
|
||||
.blend(colors.text_accent.opacity(0.1));
|
||||
|
||||
let recording_pulse = |color: Color| {
|
||||
Icon::new(IconName::Circle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Error)
|
||||
.with_animation(
|
||||
"recording-pulse",
|
||||
Animation::new(std::time::Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(gpui::pulsating_between(0.4, 0.8)),
|
||||
{
|
||||
let color = color.color(cx);
|
||||
move |this, delta| this.color(Color::Custom(color.opacity(delta)))
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
let recording_indicator = h_flex()
|
||||
.h_4()
|
||||
.pr_1()
|
||||
.gap_0p5()
|
||||
.border_1()
|
||||
.border_color(colors.border)
|
||||
.bg(colors
|
||||
.editor_background
|
||||
.blend(colors.text_accent.opacity(0.1)))
|
||||
.rounded_sm()
|
||||
.child(recording_pulse(Color::Error))
|
||||
.child(
|
||||
Label::new("REC")
|
||||
.size(LabelSize::XSmall)
|
||||
.weight(FontWeight::SEMIBOLD)
|
||||
.color(Color::Error),
|
||||
);
|
||||
|
||||
let search_indicator = h_flex()
|
||||
.h_4()
|
||||
.pr_1()
|
||||
.gap_0p5()
|
||||
.border_1()
|
||||
.border_color(colors.border)
|
||||
.bg(colors
|
||||
.editor_background
|
||||
.blend(colors.text_accent.opacity(0.1)))
|
||||
.rounded_sm()
|
||||
.child(recording_pulse(Color::Accent))
|
||||
.child(
|
||||
Label::new("SEARCH")
|
||||
.size(LabelSize::XSmall)
|
||||
.weight(FontWeight::SEMIBOLD)
|
||||
.color(Color::Accent),
|
||||
);
|
||||
|
||||
let record_icon = if self.search {
|
||||
IconName::MagnifyingGlass
|
||||
} else {
|
||||
IconName::PlayFilled
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.id("keystroke-input")
|
||||
.track_focus(&self.outer_focus_handle)
|
||||
.py_2()
|
||||
.px_3()
|
||||
.gap_2()
|
||||
.min_h_10()
|
||||
.w_full()
|
||||
.flex_1()
|
||||
.justify_between()
|
||||
.rounded_lg()
|
||||
.overflow_hidden()
|
||||
.map(|this| {
|
||||
if is_recording {
|
||||
this.bg(recording_bg_color)
|
||||
} else {
|
||||
this.bg(colors.editor_background)
|
||||
}
|
||||
})
|
||||
.border_1()
|
||||
.border_color(colors.border_variant)
|
||||
.when(is_focused, |parent| {
|
||||
parent.border_color(colors.border_focused)
|
||||
})
|
||||
.key_context(Self::key_context())
|
||||
.on_action(cx.listener(Self::start_recording))
|
||||
.on_action(cx.listener(Self::clear_keystrokes))
|
||||
.child(
|
||||
h_flex()
|
||||
.w(horizontal_padding)
|
||||
.gap_0p5()
|
||||
.justify_start()
|
||||
.flex_none()
|
||||
.when(is_recording, |this| {
|
||||
this.map(|this| {
|
||||
if self.search {
|
||||
this.child(search_indicator)
|
||||
} else {
|
||||
this.child(recording_indicator)
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.id("keystroke-input-inner")
|
||||
.track_focus(&self.inner_focus_handle)
|
||||
.on_modifiers_changed(cx.listener(Self::on_modifiers_changed))
|
||||
.size_full()
|
||||
.when(!self.search, |this| {
|
||||
this.focus(|mut style| {
|
||||
style.border_color = Some(colors.border_focused);
|
||||
style
|
||||
})
|
||||
})
|
||||
.w_full()
|
||||
.min_w_0()
|
||||
.justify_center()
|
||||
.flex_wrap()
|
||||
.gap(ui::DynamicSpacing::Base04.rems(cx))
|
||||
.children(self.render_keystrokes(is_recording)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w(horizontal_padding)
|
||||
.gap_0p5()
|
||||
.justify_end()
|
||||
.flex_none()
|
||||
.map(|this| {
|
||||
if is_recording {
|
||||
this.child(
|
||||
IconButton::new("stop-record-btn", IconName::StopFilled)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.map(|this| {
|
||||
this.tooltip(Tooltip::for_action_title(
|
||||
if self.search {
|
||||
"Stop Searching"
|
||||
} else {
|
||||
"Stop Recording"
|
||||
},
|
||||
&StopRecording,
|
||||
))
|
||||
})
|
||||
.icon_color(Color::Error)
|
||||
.on_click(cx.listener(|this, _event, window, cx| {
|
||||
this.stop_recording(&StopRecording, window, cx);
|
||||
})),
|
||||
)
|
||||
} else {
|
||||
this.child(
|
||||
IconButton::new("record-btn", record_icon)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.map(|this| {
|
||||
this.tooltip(Tooltip::for_action_title(
|
||||
if self.search {
|
||||
"Start Searching"
|
||||
} else {
|
||||
"Start Recording"
|
||||
},
|
||||
&StartRecording,
|
||||
))
|
||||
})
|
||||
.when(!is_focused, |this| this.icon_color(Color::Muted))
|
||||
.on_click(cx.listener(|this, _event, window, cx| {
|
||||
this.start_recording(&StartRecording, window, cx);
|
||||
})),
|
||||
)
|
||||
}
|
||||
})
|
||||
.child(
|
||||
IconButton::new("clear-btn", IconName::Delete)
|
||||
.shape(ui::IconButtonShape::Square)
|
||||
.tooltip(Tooltip::for_action_title(
|
||||
"Clear Keystrokes",
|
||||
&ClearKeystrokes,
|
||||
))
|
||||
.when(!is_recording || !is_focused, |this| {
|
||||
this.icon_color(Color::Muted)
|
||||
})
|
||||
.on_click(cx.listener(|this, _event, window, cx| {
|
||||
this.clear_keystrokes(&ClearKeystrokes, window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_contexts_from_assets() -> Vec<SharedString> {
|
||||
let mut keymap_assets = vec![
|
||||
util::asset_str::<SettingsAssets>(settings::DEFAULT_KEYMAP_PATH),
|
||||
|
|
1388
crates/settings_ui/src/ui_components/keystroke_input.rs
Normal file
1388
crates/settings_ui/src/ui_components/keystroke_input.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1 +1,2 @@
|
|||
pub mod keystroke_input;
|
||||
pub mod table;
|
||||
|
|
|
@ -291,14 +291,18 @@ impl Component for ToggleButton {
|
|||
}
|
||||
}
|
||||
|
||||
mod private {
|
||||
pub trait Sealed {}
|
||||
pub struct ButtonConfiguration {
|
||||
label: SharedString,
|
||||
icon: Option<IconName>,
|
||||
on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
|
||||
}
|
||||
|
||||
pub trait ButtonBuilder: 'static + private::Sealed {
|
||||
fn label(&self) -> impl Into<SharedString>;
|
||||
fn icon(&self) -> Option<IconName>;
|
||||
fn on_click(self) -> Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
|
||||
mod private {
|
||||
pub trait ToggleButtonStyle {}
|
||||
}
|
||||
|
||||
pub trait ButtonBuilder: 'static + private::ToggleButtonStyle {
|
||||
fn into_configuration(self) -> ButtonConfiguration;
|
||||
}
|
||||
|
||||
pub struct ToggleButtonSimple {
|
||||
|
@ -318,19 +322,15 @@ impl ToggleButtonSimple {
|
|||
}
|
||||
}
|
||||
|
||||
impl private::Sealed for ToggleButtonSimple {}
|
||||
impl private::ToggleButtonStyle for ToggleButtonSimple {}
|
||||
|
||||
impl ButtonBuilder for ToggleButtonSimple {
|
||||
fn label(&self) -> impl Into<SharedString> {
|
||||
self.label.clone()
|
||||
}
|
||||
|
||||
fn icon(&self) -> Option<IconName> {
|
||||
None
|
||||
}
|
||||
|
||||
fn on_click(self) -> Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static> {
|
||||
self.on_click
|
||||
fn into_configuration(self) -> ButtonConfiguration {
|
||||
ButtonConfiguration {
|
||||
label: self.label,
|
||||
icon: None,
|
||||
on_click: self.on_click,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -354,58 +354,14 @@ impl ToggleButtonWithIcon {
|
|||
}
|
||||
}
|
||||
|
||||
impl private::Sealed for ToggleButtonWithIcon {}
|
||||
impl private::ToggleButtonStyle for ToggleButtonWithIcon {}
|
||||
|
||||
impl ButtonBuilder for ToggleButtonWithIcon {
|
||||
fn label(&self) -> impl Into<SharedString> {
|
||||
self.label.clone()
|
||||
}
|
||||
|
||||
fn icon(&self) -> Option<IconName> {
|
||||
Some(self.icon)
|
||||
}
|
||||
|
||||
fn on_click(self) -> Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static> {
|
||||
self.on_click
|
||||
}
|
||||
}
|
||||
|
||||
struct ToggleButtonRow<T: ButtonBuilder> {
|
||||
items: Vec<T>,
|
||||
index_offset: usize,
|
||||
last_item_idx: usize,
|
||||
is_last_row: bool,
|
||||
}
|
||||
|
||||
impl<T: ButtonBuilder> ToggleButtonRow<T> {
|
||||
fn new(items: Vec<T>, index_offset: usize, is_last_row: bool) -> Self {
|
||||
Self {
|
||||
index_offset,
|
||||
last_item_idx: index_offset + items.len() - 1,
|
||||
is_last_row,
|
||||
items,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ToggleButtonGroupRows<T: ButtonBuilder> {
|
||||
Single(Vec<T>),
|
||||
Multiple(Vec<T>, Vec<T>),
|
||||
}
|
||||
|
||||
impl<T: ButtonBuilder> ToggleButtonGroupRows<T> {
|
||||
fn items(self) -> impl IntoIterator<Item = ToggleButtonRow<T>> {
|
||||
match self {
|
||||
ToggleButtonGroupRows::Single(items) => {
|
||||
vec![ToggleButtonRow::new(items, 0, true)]
|
||||
}
|
||||
ToggleButtonGroupRows::Multiple(first_row, second_row) => {
|
||||
let row_len = first_row.len();
|
||||
vec![
|
||||
ToggleButtonRow::new(first_row, 0, false),
|
||||
ToggleButtonRow::new(second_row, row_len, true),
|
||||
]
|
||||
}
|
||||
fn into_configuration(self) -> ButtonConfiguration {
|
||||
ButtonConfiguration {
|
||||
label: self.label,
|
||||
icon: Some(self.icon),
|
||||
on_click: self.on_click,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -418,48 +374,42 @@ pub enum ToggleButtonGroupStyle {
|
|||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ToggleButtonGroup<T>
|
||||
pub struct ToggleButtonGroup<T, const COLS: usize = 3, const ROWS: usize = 1>
|
||||
where
|
||||
T: ButtonBuilder,
|
||||
{
|
||||
group_name: SharedString,
|
||||
rows: ToggleButtonGroupRows<T>,
|
||||
group_name: &'static str,
|
||||
rows: [[T; COLS]; ROWS],
|
||||
style: ToggleButtonGroupStyle,
|
||||
button_width: Rems,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
impl<T: ButtonBuilder> ToggleButtonGroup<T> {
|
||||
pub fn single_row(
|
||||
group_name: impl Into<SharedString>,
|
||||
buttons: impl IntoIterator<Item = T>,
|
||||
) -> Self {
|
||||
impl<T: ButtonBuilder, const COLS: usize> ToggleButtonGroup<T, COLS> {
|
||||
pub fn single_row(group_name: &'static str, buttons: [T; COLS]) -> Self {
|
||||
Self {
|
||||
group_name: group_name.into(),
|
||||
rows: ToggleButtonGroupRows::Single(Vec::from_iter(buttons)),
|
||||
group_name,
|
||||
rows: [buttons],
|
||||
style: ToggleButtonGroupStyle::Transparent,
|
||||
button_width: rems_from_px(100.),
|
||||
selected_index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn multiple_rows<const ROWS: usize>(
|
||||
group_name: impl Into<SharedString>,
|
||||
first_row: [T; ROWS],
|
||||
second_row: [T; ROWS],
|
||||
) -> Self {
|
||||
impl<T: ButtonBuilder, const COLS: usize> ToggleButtonGroup<T, COLS, 2> {
|
||||
pub fn two_rows(group_name: &'static str, first_row: [T; COLS], second_row: [T; COLS]) -> Self {
|
||||
Self {
|
||||
group_name: group_name.into(),
|
||||
rows: ToggleButtonGroupRows::Multiple(
|
||||
Vec::from_iter(first_row),
|
||||
Vec::from_iter(second_row),
|
||||
),
|
||||
group_name,
|
||||
rows: [first_row, second_row],
|
||||
style: ToggleButtonGroupStyle::Transparent,
|
||||
button_width: rems_from_px(100.),
|
||||
selected_index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ButtonBuilder, const COLS: usize, const ROWS: usize> ToggleButtonGroup<T, COLS, ROWS> {
|
||||
pub fn style(mut self, style: ToggleButtonGroupStyle) -> Self {
|
||||
self.style = style;
|
||||
self
|
||||
|
@ -476,60 +426,56 @@ impl<T: ButtonBuilder> ToggleButtonGroup<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T: ButtonBuilder> RenderOnce for ToggleButtonGroup<T> {
|
||||
impl<T: ButtonBuilder, const COLS: usize, const ROWS: usize> RenderOnce
|
||||
for ToggleButtonGroup<T, COLS, ROWS>
|
||||
{
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let rows = self.rows.items().into_iter().map(|row| {
|
||||
(
|
||||
row.items
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(move |(index, item)| (index + row.index_offset, row.last_item_idx, item))
|
||||
.map(|(index, last_item_idx, item)| {
|
||||
(
|
||||
ButtonLike::new((self.group_name.clone(), index))
|
||||
.when(index == self.selected_index, |this| {
|
||||
this.toggle_state(true)
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
})
|
||||
.rounding(None)
|
||||
.when(self.style == ToggleButtonGroupStyle::Filled, |button| {
|
||||
button.style(ButtonStyle::Filled)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.min_w(self.button_width)
|
||||
.gap_1p5()
|
||||
.justify_center()
|
||||
.when_some(item.icon(), |this, icon| {
|
||||
this.child(Icon::new(icon).size(IconSize::XSmall).map(
|
||||
|this| {
|
||||
if index == self.selected_index {
|
||||
this.color(Color::Accent)
|
||||
} else {
|
||||
this.color(Color::Muted)
|
||||
}
|
||||
},
|
||||
))
|
||||
})
|
||||
.child(
|
||||
Label::new(item.label())
|
||||
.when(index == self.selected_index, |this| {
|
||||
this.color(Color::Accent)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.on_click(item.on_click()),
|
||||
index == last_item_idx,
|
||||
)
|
||||
}),
|
||||
row.is_last_row,
|
||||
)
|
||||
let entries = self.rows.into_iter().enumerate().map(|(row_index, row)| {
|
||||
row.into_iter().enumerate().map(move |(index, button)| {
|
||||
let ButtonConfiguration {
|
||||
label,
|
||||
icon,
|
||||
on_click,
|
||||
} = button.into_configuration();
|
||||
|
||||
ButtonLike::new((self.group_name, row_index * COLS + index))
|
||||
.when(index == self.selected_index, |this| {
|
||||
this.toggle_state(true)
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
|
||||
})
|
||||
.rounding(None)
|
||||
.when(self.style == ToggleButtonGroupStyle::Filled, |button| {
|
||||
button.style(ButtonStyle::Filled)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.min_w(self.button_width)
|
||||
.gap_1p5()
|
||||
.justify_center()
|
||||
.when_some(icon, |this, icon| {
|
||||
this.child(Icon::new(icon).size(IconSize::XSmall).map(|this| {
|
||||
if index == self.selected_index {
|
||||
this.color(Color::Accent)
|
||||
} else {
|
||||
this.color(Color::Muted)
|
||||
}
|
||||
}))
|
||||
})
|
||||
.child(
|
||||
Label::new(label).when(index == self.selected_index, |this| {
|
||||
this.color(Color::Accent)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.on_click(on_click)
|
||||
.into_any_element()
|
||||
})
|
||||
});
|
||||
|
||||
let border_color = cx.theme().colors().border.opacity(0.6);
|
||||
let is_outlined_or_filled = self.style == ToggleButtonGroupStyle::Outlined
|
||||
|| self.style == ToggleButtonGroupStyle::Filled;
|
||||
let is_transparent = self.style == ToggleButtonGroupStyle::Transparent;
|
||||
let border_color = cx.theme().colors().border.opacity(0.6);
|
||||
|
||||
v_flex()
|
||||
.rounded_md()
|
||||
|
@ -541,13 +487,15 @@ impl<T: ButtonBuilder> RenderOnce for ToggleButtonGroup<T> {
|
|||
this.border_1().border_color(border_color)
|
||||
}
|
||||
})
|
||||
.children(rows.map(|(items, last_row)| {
|
||||
.children(entries.enumerate().map(|(row_index, row)| {
|
||||
let last_row = row_index == ROWS - 1;
|
||||
h_flex()
|
||||
.when(!is_outlined_or_filled, |this| this.gap_px())
|
||||
.when(is_outlined_or_filled && !last_row, |this| {
|
||||
this.border_b_1().border_color(border_color)
|
||||
})
|
||||
.children(items.map(|(item, last_item)| {
|
||||
.children(row.enumerate().map(|(item_index, item)| {
|
||||
let last_item = item_index == COLS - 1;
|
||||
div()
|
||||
.when(is_outlined_or_filled && !last_item, |this| {
|
||||
this.border_r_1().border_color(border_color)
|
||||
|
@ -566,7 +514,9 @@ component::__private::inventory::submit! {
|
|||
component::ComponentFn::new(register_toggle_button_group)
|
||||
}
|
||||
|
||||
impl<T: ButtonBuilder> Component for ToggleButtonGroup<T> {
|
||||
impl<T: ButtonBuilder, const COLS: usize, const ROWS: usize> Component
|
||||
for ToggleButtonGroup<T, COLS, ROWS>
|
||||
{
|
||||
fn name() -> &'static str {
|
||||
"ToggleButtonGroup"
|
||||
}
|
||||
|
@ -628,7 +578,7 @@ impl<T: ButtonBuilder> Component for ToggleButtonGroup<T> {
|
|||
),
|
||||
single_example(
|
||||
"Multiple Row Group",
|
||||
ToggleButtonGroup::multiple_rows(
|
||||
ToggleButtonGroup::two_rows(
|
||||
"multiple_row_test",
|
||||
[
|
||||
ToggleButtonSimple::new("First", |_, _, _| {}),
|
||||
|
@ -647,7 +597,7 @@ impl<T: ButtonBuilder> Component for ToggleButtonGroup<T> {
|
|||
),
|
||||
single_example(
|
||||
"Multiple Row Group with Icons",
|
||||
ToggleButtonGroup::multiple_rows(
|
||||
ToggleButtonGroup::two_rows(
|
||||
"multiple_row_test_icons",
|
||||
[
|
||||
ToggleButtonWithIcon::new(
|
||||
|
@ -736,7 +686,7 @@ impl<T: ButtonBuilder> Component for ToggleButtonGroup<T> {
|
|||
),
|
||||
single_example(
|
||||
"Multiple Row Group",
|
||||
ToggleButtonGroup::multiple_rows(
|
||||
ToggleButtonGroup::two_rows(
|
||||
"multiple_row_test",
|
||||
[
|
||||
ToggleButtonSimple::new("First", |_, _, _| {}),
|
||||
|
@ -756,7 +706,7 @@ impl<T: ButtonBuilder> Component for ToggleButtonGroup<T> {
|
|||
),
|
||||
single_example(
|
||||
"Multiple Row Group with Icons",
|
||||
ToggleButtonGroup::multiple_rows(
|
||||
ToggleButtonGroup::two_rows(
|
||||
"multiple_row_test",
|
||||
[
|
||||
ToggleButtonWithIcon::new(
|
||||
|
@ -846,7 +796,7 @@ impl<T: ButtonBuilder> Component for ToggleButtonGroup<T> {
|
|||
),
|
||||
single_example(
|
||||
"Multiple Row Group",
|
||||
ToggleButtonGroup::multiple_rows(
|
||||
ToggleButtonGroup::two_rows(
|
||||
"multiple_row_test",
|
||||
[
|
||||
ToggleButtonSimple::new("First", |_, _, _| {}),
|
||||
|
@ -866,7 +816,7 @@ impl<T: ButtonBuilder> Component for ToggleButtonGroup<T> {
|
|||
),
|
||||
single_example(
|
||||
"Multiple Row Group with Icons",
|
||||
ToggleButtonGroup::multiple_rows(
|
||||
ToggleButtonGroup::two_rows(
|
||||
"multiple_row_test",
|
||||
[
|
||||
ToggleButtonWithIcon::new(
|
||||
|
|
|
@ -2,7 +2,7 @@ use gpui::ClickEvent;
|
|||
|
||||
use crate::{IconButtonShape, prelude::*};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
pub struct NumericStepper {
|
||||
id: ElementId,
|
||||
value: SharedString,
|
||||
|
@ -93,3 +93,34 @@ impl RenderOnce for NumericStepper {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for NumericStepper {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::Input
|
||||
}
|
||||
|
||||
fn name() -> &'static str {
|
||||
"NumericStepper"
|
||||
}
|
||||
|
||||
fn sort_name() -> &'static str {
|
||||
Self::name()
|
||||
}
|
||||
|
||||
fn description() -> Option<&'static str> {
|
||||
Some("A button used to increment or decrement a numeric value. ")
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
||||
Some(
|
||||
div()
|
||||
.child(NumericStepper::new(
|
||||
"numeric-stepper-component-preview",
|
||||
"10",
|
||||
move |_, _, _| {},
|
||||
move |_, _, _| {},
|
||||
))
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -109,7 +109,7 @@ impl Component for Animation {
|
|||
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
|
||||
let container_size = 128.0;
|
||||
let element_size = 32.0;
|
||||
let left_offset = element_size - container_size / 2.0;
|
||||
let offset = container_size / 2.0 - element_size / 2.0;
|
||||
Some(
|
||||
v_flex()
|
||||
.gap_6()
|
||||
|
@ -129,7 +129,7 @@ impl Component for Animation {
|
|||
.id("animate-in-from-bottom")
|
||||
.absolute()
|
||||
.size(px(element_size))
|
||||
.left(px(left_offset))
|
||||
.left(px(offset))
|
||||
.rounded_md()
|
||||
.bg(gpui::red())
|
||||
.animate_in(AnimationDirection::FromBottom, false),
|
||||
|
@ -148,7 +148,7 @@ impl Component for Animation {
|
|||
.id("animate-in-from-top")
|
||||
.absolute()
|
||||
.size(px(element_size))
|
||||
.left(px(left_offset))
|
||||
.left(px(offset))
|
||||
.rounded_md()
|
||||
.bg(gpui::blue())
|
||||
.animate_in(AnimationDirection::FromTop, false),
|
||||
|
@ -167,7 +167,7 @@ impl Component for Animation {
|
|||
.id("animate-in-from-left")
|
||||
.absolute()
|
||||
.size(px(element_size))
|
||||
.left(px(left_offset))
|
||||
.top(px(offset))
|
||||
.rounded_md()
|
||||
.bg(gpui::green())
|
||||
.animate_in(AnimationDirection::FromLeft, false),
|
||||
|
@ -186,7 +186,7 @@ impl Component for Animation {
|
|||
.id("animate-in-from-right")
|
||||
.absolute()
|
||||
.size(px(element_size))
|
||||
.left(px(left_offset))
|
||||
.top(px(offset))
|
||||
.rounded_md()
|
||||
.bg(gpui::yellow())
|
||||
.animate_in(AnimationDirection::FromRight, false),
|
||||
|
@ -211,7 +211,7 @@ impl Component for Animation {
|
|||
.id("fade-animate-in-from-bottom")
|
||||
.absolute()
|
||||
.size(px(element_size))
|
||||
.left(px(left_offset))
|
||||
.left(px(offset))
|
||||
.rounded_md()
|
||||
.bg(gpui::red())
|
||||
.animate_in(AnimationDirection::FromBottom, true),
|
||||
|
@ -230,7 +230,7 @@ impl Component for Animation {
|
|||
.id("fade-animate-in-from-top")
|
||||
.absolute()
|
||||
.size(px(element_size))
|
||||
.left(px(left_offset))
|
||||
.left(px(offset))
|
||||
.rounded_md()
|
||||
.bg(gpui::blue())
|
||||
.animate_in(AnimationDirection::FromTop, true),
|
||||
|
@ -249,7 +249,7 @@ impl Component for Animation {
|
|||
.id("fade-animate-in-from-left")
|
||||
.absolute()
|
||||
.size(px(element_size))
|
||||
.left(px(left_offset))
|
||||
.top(px(offset))
|
||||
.rounded_md()
|
||||
.bg(gpui::green())
|
||||
.animate_in(AnimationDirection::FromLeft, true),
|
||||
|
@ -268,7 +268,7 @@ impl Component for Animation {
|
|||
.id("fade-animate-in-from-right")
|
||||
.absolute()
|
||||
.size(px(element_size))
|
||||
.left(px(left_offset))
|
||||
.top(px(offset))
|
||||
.rounded_md()
|
||||
.bg(gpui::yellow())
|
||||
.animate_in(AnimationDirection::FromRight, true),
|
||||
|
|
|
@ -13,8 +13,8 @@ path = "src/web_search.rs"
|
|||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
gpui.workspace = true
|
||||
serde.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
use cloud_llm_client::WebSearchResponse;
|
||||
use collections::HashMap;
|
||||
use gpui::{App, AppContext as _, Context, Entity, Global, SharedString, Task};
|
||||
use std::sync::Arc;
|
||||
use zed_llm_client::WebSearchResponse;
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
let registry = cx.new(|_cx| WebSearchRegistry::default());
|
||||
|
|
|
@ -14,6 +14,7 @@ path = "src/web_search_providers.rs"
|
|||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
|
@ -22,4 +23,3 @@ serde.workspace = true
|
|||
serde_json.workspace = true
|
||||
web_search.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_llm_client.workspace = true
|
||||
|
|
|
@ -2,12 +2,12 @@ use std::sync::Arc;
|
|||
|
||||
use anyhow::{Context as _, Result};
|
||||
use client::Client;
|
||||
use cloud_llm_client::{EXPIRED_LLM_TOKEN_HEADER_NAME, WebSearchBody, WebSearchResponse};
|
||||
use futures::AsyncReadExt as _;
|
||||
use gpui::{App, AppContext, Context, Entity, Subscription, Task};
|
||||
use http_client::{HttpClient, Method};
|
||||
use language_model::{LlmApiToken, RefreshLlmTokenListener};
|
||||
use web_search::{WebSearchProvider, WebSearchProviderId};
|
||||
use zed_llm_client::{EXPIRED_LLM_TOKEN_HEADER_NAME, WebSearchBody, WebSearchResponse};
|
||||
|
||||
pub struct CloudWebSearchProvider {
|
||||
state: Entity<State>,
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue