Merge branch 'main' into new-acp

This commit is contained in:
Agus Zubiaga 2025-08-01 18:17:48 -03:00
commit 76f0f9163d
159 changed files with 10318 additions and 2467 deletions

View file

@ -19,7 +19,7 @@ runs:
shell: bash -euxo pipefail {0}
run: ./script/linux
- name: Check for broken links
- name: Check for broken links (in MD)
uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1
with:
args: --no-progress --exclude '^http' './docs/src/**/*'
@ -30,3 +30,9 @@ runs:
run: |
mkdir -p target/deploy
mdbook build ./docs --dest-dir=../target/deploy/docs/
- name: Check for broken links (in HTML)
uses: lycheeverse/lychee-action@82202e5e9c2f4ef1a55a3d02563e1cb6041e5332 # v2.4.1
with:
args: --no-progress --exclude '^http' 'target/deploy/docs/'
fail: true

View file

@ -771,7 +771,8 @@ jobs:
timeout-minutes: 120
name: Create a Windows installer
runs-on: [self-hosted, Windows, X64]
if: (startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-bundling'))
if: 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 }}

357
Cargo.lock generated
View file

@ -114,7 +114,6 @@ dependencies = [
"pretty_assertions",
"project",
"prompt_store",
"proto",
"rand 0.8.5",
"ref-cast",
"rope",
@ -357,10 +356,10 @@ name = "ai_onboarding"
version = "0.1.0"
dependencies = [
"client",
"cloud_llm_client",
"component",
"gpui",
"language_model",
"proto",
"serde",
"smallvec",
"telemetry",
@ -1077,17 +1076,6 @@ dependencies = [
"tracing",
]
[[package]]
name = "async-recursion"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7d78656ba01f1b93024b7c3a0467f1608e4be67d725749fdcd7d2c7678fd7a2"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "async-recursion"
version = "1.1.1"
@ -2973,11 +2961,11 @@ name = "client"
version = "0.1.0"
dependencies = [
"anyhow",
"async-recursion 0.3.2",
"async-tungstenite",
"base64 0.22.1",
"chrono",
"clock",
"cloud_api_client",
"cloud_llm_client",
"cocoa 0.26.0",
"collections",
@ -3033,6 +3021,31 @@ dependencies = [
"workspace-hack",
]
[[package]]
name = "cloud_api_client"
version = "0.1.0"
dependencies = [
"anyhow",
"cloud_api_types",
"futures 0.3.31",
"http_client",
"parking_lot",
"serde_json",
"workspace-hack",
]
[[package]]
name = "cloud_api_types"
version = "0.1.0"
dependencies = [
"chrono",
"cloud_llm_client",
"pretty_assertions",
"serde",
"serde_json",
"workspace-hack",
]
[[package]]
name = "cloud_llm_client"
version = "0.1.0"
@ -4271,41 +4284,6 @@ dependencies = [
"workspace-hack",
]
[[package]]
name = "darling"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 2.0.101",
]
[[package]]
name = "darling_macro"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
"darling_core",
"quote",
"syn 2.0.101",
]
[[package]]
name = "dashmap"
version = "5.5.3"
@ -4521,37 +4499,6 @@ dependencies = [
"serde",
]
[[package]]
name = "derive_builder"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
dependencies = [
"derive_builder_macro",
]
[[package]]
name = "derive_builder_core"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.101",
]
[[package]]
name = "derive_builder_macro"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
dependencies = [
"derive_builder_core",
"syn 2.0.101",
]
[[package]]
name = "derive_more"
version = "0.99.19"
@ -4962,6 +4909,7 @@ dependencies = [
"theme",
"time",
"tree-sitter-bash",
"tree-sitter-c",
"tree-sitter-html",
"tree-sitter-python",
"tree-sitter-rust",
@ -5930,7 +5878,7 @@ dependencies = [
"ignore",
"libc",
"log",
"notify",
"notify 8.0.0",
"objc",
"parking_lot",
"paths",
@ -7369,9 +7317,8 @@ dependencies = [
"wayland-backend",
"wayland-client",
"wayland-cursor",
"wayland-protocols 0.31.2",
"wayland-protocols",
"wayland-protocols-plasma",
"wayland-protocols-wlr",
"windows 0.61.1",
"windows-core 0.61.0",
"windows-numerics",
@ -7488,18 +7435,16 @@ dependencies = [
[[package]]
name = "handlebars"
version = "6.3.2"
version = "5.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "759e2d5aea3287cb1190c8ec394f42866cb5bf74fcbf213f354e3c856ea26098"
checksum = "d08485b96a0e6393e9e4d1b8d48cf74ad6c063cd905eb33f42c1ce3f0377539b"
dependencies = [
"derive_builder",
"log",
"num-order",
"pest",
"pest_derive",
"serde",
"serde_json",
"thiserror 2.0.12",
"thiserror 1.0.69",
]
[[package]]
@ -7680,12 +7625,6 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hex-literal"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcaaec4551594c969335c98c903c1397853d4198408ea609190f420500f6be71"
[[package]]
name = "hexf-parse"
version = "0.2.1"
@ -7863,6 +7802,7 @@ dependencies = [
"http 1.3.1",
"http-body 1.0.1",
"log",
"parking_lot",
"serde",
"serde_json",
"url",
@ -8170,12 +8110,6 @@ version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005"
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "1.0.3"
@ -8394,6 +8328,17 @@ dependencies = [
"zeta",
]
[[package]]
name = "inotify"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
dependencies = [
"bitflags 1.3.2",
"inotify-sys",
"libc",
]
[[package]]
name = "inotify"
version = "0.11.0"
@ -8547,7 +8492,7 @@ dependencies = [
"fnv",
"lazy_static",
"libc",
"mio",
"mio 1.0.3",
"rand 0.8.5",
"serde",
"tempfile",
@ -9129,7 +9074,6 @@ dependencies = [
"open_router",
"partial-json-fixer",
"project",
"proto",
"release_channel",
"schemars",
"serde",
@ -9401,7 +9345,7 @@ dependencies = [
[[package]]
name = "libwebrtc"
version = "0.3.10"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=383e5377f8b7de1f8627ee16f0cf11c5293337bd#383e5377f8b7de1f8627ee16f0cf11c5293337bd"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=5f04705ac3f356350ae31534ffbc476abc9ea83d#5f04705ac3f356350ae31534ffbc476abc9ea83d"
dependencies = [
"cxx",
"jni",
@ -9481,7 +9425,7 @@ checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856"
[[package]]
name = "livekit"
version = "0.7.8"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=383e5377f8b7de1f8627ee16f0cf11c5293337bd#383e5377f8b7de1f8627ee16f0cf11c5293337bd"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=5f04705ac3f356350ae31534ffbc476abc9ea83d#5f04705ac3f356350ae31534ffbc476abc9ea83d"
dependencies = [
"chrono",
"futures-util",
@ -9504,7 +9448,7 @@ dependencies = [
[[package]]
name = "livekit-api"
version = "0.4.2"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=383e5377f8b7de1f8627ee16f0cf11c5293337bd#383e5377f8b7de1f8627ee16f0cf11c5293337bd"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=5f04705ac3f356350ae31534ffbc476abc9ea83d#5f04705ac3f356350ae31534ffbc476abc9ea83d"
dependencies = [
"futures-util",
"http 0.2.12",
@ -9528,7 +9472,7 @@ dependencies = [
[[package]]
name = "livekit-protocol"
version = "0.3.9"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=383e5377f8b7de1f8627ee16f0cf11c5293337bd#383e5377f8b7de1f8627ee16f0cf11c5293337bd"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=5f04705ac3f356350ae31534ffbc476abc9ea83d#5f04705ac3f356350ae31534ffbc476abc9ea83d"
dependencies = [
"futures-util",
"livekit-runtime",
@ -9545,7 +9489,7 @@ dependencies = [
[[package]]
name = "livekit-runtime"
version = "0.4.0"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=383e5377f8b7de1f8627ee16f0cf11c5293337bd#383e5377f8b7de1f8627ee16f0cf11c5293337bd"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=5f04705ac3f356350ae31534ffbc476abc9ea83d#5f04705ac3f356350ae31534ffbc476abc9ea83d"
dependencies = [
"tokio",
"tokio-stream",
@ -9867,7 +9811,7 @@ name = "markdown_preview"
version = "0.1.0"
dependencies = [
"anyhow",
"async-recursion 1.1.1",
"async-recursion",
"collections",
"editor",
"fs",
@ -9987,9 +9931,9 @@ dependencies = [
[[package]]
name = "mdbook"
version = "0.4.48"
version = "0.4.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b6fbb4ac2d9fd7aa987c3510309ea3c80004a968d063c42f0d34fea070817c1"
checksum = "b45a38e19bd200220ef07c892b0157ad3d2365e5b5a267ca01ad12182491eea5"
dependencies = [
"ammonia",
"anyhow",
@ -9999,12 +9943,11 @@ dependencies = [
"elasticlunr-rs",
"env_logger 0.11.8",
"futures-util",
"handlebars 6.3.2",
"hex",
"handlebars 5.1.2",
"ignore",
"log",
"memchr",
"notify",
"notify 6.1.1",
"notify-debouncer-mini",
"once_cell",
"opener",
@ -10013,7 +9956,6 @@ dependencies = [
"regex",
"serde",
"serde_json",
"sha2",
"shlex",
"tempfile",
"tokio",
@ -10156,6 +10098,18 @@ version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff"
[[package]]
name = "mio"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [
"libc",
"log",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.48.0",
]
[[package]]
name = "mio"
version = "1.0.3"
@ -10502,6 +10456,25 @@ dependencies = [
"zed_actions",
]
[[package]]
name = "notify"
version = "6.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
dependencies = [
"bitflags 2.9.0",
"crossbeam-channel",
"filetime",
"fsevent-sys 4.1.0",
"inotify 0.9.6",
"kqueue",
"libc",
"log",
"mio 0.8.11",
"walkdir",
"windows-sys 0.48.0",
]
[[package]]
name = "notify"
version = "8.0.0"
@ -10510,11 +10483,11 @@ dependencies = [
"bitflags 2.9.0",
"filetime",
"fsevent-sys 4.1.0",
"inotify",
"inotify 0.11.0",
"kqueue",
"libc",
"log",
"mio",
"mio 1.0.3",
"notify-types",
"walkdir",
"windows-sys 0.59.0",
@ -10522,14 +10495,13 @@ dependencies = [
[[package]]
name = "notify-debouncer-mini"
version = "0.6.0"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a689eb4262184d9a1727f9087cd03883ea716682ab03ed24efec57d7716dccb8"
checksum = "5d40b221972a1fc5ef4d858a2f671fb34c75983eb385463dff3780eeff6a9d43"
dependencies = [
"crossbeam-channel",
"log",
"notify",
"notify-types",
"tempfile",
"notify 6.1.1",
]
[[package]]
@ -10669,21 +10641,6 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-modular"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f"
[[package]]
name = "num-order"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6"
dependencies = [
"num-modular",
]
[[package]]
name = "num-rational"
version = "0.4.2"
@ -10954,21 +10911,33 @@ dependencies = [
name = "onboarding"
version = "0.1.0"
dependencies = [
"ai_onboarding",
"anyhow",
"client",
"command_palette_hooks",
"component",
"db",
"documented",
"editor",
"feature_flags",
"fs",
"gpui",
"itertools 0.14.0",
"language",
"language_model",
"menu",
"project",
"schemars",
"serde",
"settings",
"theme",
"ui",
"util",
"vim_mode_setting",
"workspace",
"workspace-hack",
"zed_actions",
"zlog",
]
[[package]]
@ -14732,6 +14701,27 @@ dependencies = [
"zlog",
]
[[package]]
name = "settings_profile_selector"
version = "0.1.0"
dependencies = [
"client",
"editor",
"fuzzy",
"gpui",
"language",
"menu",
"picker",
"project",
"serde_json",
"settings",
"theme",
"ui",
"workspace",
"workspace-hack",
"zed_actions",
]
[[package]]
name = "settings_ui"
version = "0.1.0"
@ -14754,7 +14744,6 @@ dependencies = [
"notifications",
"paths",
"project",
"schemars",
"search",
"serde",
"serde_json",
@ -16191,7 +16180,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"assistant_slash_command",
"async-recursion 1.1.1",
"async-recursion",
"breadcrumbs",
"client",
"collections",
@ -16540,6 +16529,7 @@ dependencies = [
"call",
"chrono",
"client",
"cloud_llm_client",
"collections",
"db",
"gpui",
@ -16575,7 +16565,7 @@ dependencies = [
"backtrace",
"bytes 1.10.1",
"libc",
"mio",
"mio 1.0.3",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
@ -18388,9 +18378,9 @@ dependencies = [
[[package]]
name = "wayland-backend"
version = "0.3.10"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe770181423e5fc79d3e2a7f4410b7799d5aab1de4372853de3c6aa13ca24121"
checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf"
dependencies = [
"cc",
"downcast-rs",
@ -18402,9 +18392,9 @@ dependencies = [
[[package]]
name = "wayland-client"
version = "0.31.10"
version = "0.31.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978fa7c67b0847dbd6a9f350ca2569174974cd4082737054dbb7fbb79d7d9a61"
checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f"
dependencies = [
"bitflags 2.9.0",
"rustix 0.38.44",
@ -18435,18 +18425,6 @@ 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"
@ -18456,20 +18434,7 @@ dependencies = [
"bitflags 2.9.0",
"wayland-backend",
"wayland-client",
"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-protocols",
"wayland-scanner",
]
@ -18578,7 +18543,7 @@ dependencies = [
[[package]]
name = "webrtc-sys"
version = "0.3.7"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=383e5377f8b7de1f8627ee16f0cf11c5293337bd#383e5377f8b7de1f8627ee16f0cf11c5293337bd"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=5f04705ac3f356350ae31534ffbc476abc9ea83d#5f04705ac3f356350ae31534ffbc476abc9ea83d"
dependencies = [
"cc",
"cxx",
@ -18591,15 +18556,13 @@ dependencies = [
[[package]]
name = "webrtc-sys-build"
version = "0.3.6"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=383e5377f8b7de1f8627ee16f0cf11c5293337bd#383e5377f8b7de1f8627ee16f0cf11c5293337bd"
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=5f04705ac3f356350ae31534ffbc476abc9ea83d#5f04705ac3f356350ae31534ffbc476abc9ea83d"
dependencies = [
"fs2",
"hex-literal",
"regex",
"reqwest 0.11.27",
"scratch",
"semver",
"sha2",
"zip",
]
@ -18628,7 +18591,6 @@ dependencies = [
"serde",
"settings",
"telemetry",
"theme",
"ui",
"util",
"vim_mode_setting",
@ -19643,7 +19605,7 @@ version = "0.1.0"
dependencies = [
"any_vec",
"anyhow",
"async-recursion 1.1.1",
"async-recursion",
"bincode",
"call",
"client",
@ -19777,7 +19739,7 @@ dependencies = [
"md-5",
"memchr",
"miniz_oxide",
"mio",
"mio 1.0.3",
"naga",
"nix 0.29.0",
"nom",
@ -20168,7 +20130,7 @@ dependencies = [
"async-io",
"async-lock",
"async-process",
"async-recursion 1.1.1",
"async-recursion",
"async-task",
"async-trait",
"blocking",
@ -20221,7 +20183,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.198.0"
version = "0.199.0"
dependencies = [
"activity_indicator",
"agent",
@ -20324,6 +20286,7 @@ dependencies = [
"serde_json",
"session",
"settings",
"settings_profile_selector",
"settings_ui",
"shellexpand 2.1.2",
"smol",
@ -20600,6 +20563,7 @@ dependencies = [
"call",
"client",
"clock",
"cloud_api_types",
"cloud_llm_client",
"collections",
"command_palette_hooks",
@ -20620,7 +20584,6 @@ dependencies = [
"menu",
"postage",
"project",
"proto",
"regex",
"release_channel",
"reqwest_client",
@ -20645,6 +20608,42 @@ dependencies = [
"zlog",
]
[[package]]
name = "zeta_cli"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"client",
"debug_adapter_extension",
"extension",
"fs",
"futures 0.3.31",
"gpui",
"gpui_tokio",
"language",
"language_extension",
"language_model",
"language_models",
"languages",
"node_runtime",
"paths",
"project",
"prompt_store",
"release_channel",
"reqwest_client",
"serde",
"serde_json",
"settings",
"shellexpand 2.1.2",
"smol",
"terminal_view",
"util",
"watch",
"workspace-hack",
"zeta",
]
[[package]]
name = "zip"
version = "0.6.6"

View file

@ -1,13 +1,13 @@
[workspace]
resolver = "2"
members = [
"crates/activity_indicator",
"crates/acp_thread",
"crates/agent_ui",
"crates/activity_indicator",
"crates/agent",
"crates/agent_settings",
"crates/ai_onboarding",
"crates/agent_servers",
"crates/agent_settings",
"crates/agent_ui",
"crates/ai_onboarding",
"crates/anthropic",
"crates/askpass",
"crates/assets",
@ -29,6 +29,8 @@ members = [
"crates/cli",
"crates/client",
"crates/clock",
"crates/cloud_api_client",
"crates/cloud_api_types",
"crates/cloud_llm_client",
"crates/collab",
"crates/collab_ui",
@ -49,8 +51,8 @@ members = [
"crates/diagnostics",
"crates/docs_preprocessor",
"crates/editor",
"crates/explorer_command_injector",
"crates/eval",
"crates/explorer_command_injector",
"crates/extension",
"crates/extension_api",
"crates/extension_cli",
@ -99,7 +101,6 @@ members = [
"crates/markdown_preview",
"crates/media",
"crates/menu",
"crates/svg_preview",
"crates/migrator",
"crates/mistral",
"crates/multi_buffer",
@ -140,6 +141,7 @@ members = [
"crates/semantic_version",
"crates/session",
"crates/settings",
"crates/settings_profile_selector",
"crates/settings_ui",
"crates/snippet",
"crates/snippet_provider",
@ -152,6 +154,7 @@ members = [
"crates/sum_tree",
"crates/supermaven",
"crates/supermaven_api",
"crates/svg_preview",
"crates/tab_switcher",
"crates/task",
"crates/tasks_ui",
@ -186,6 +189,7 @@ members = [
"crates/zed",
"crates/zed_actions",
"crates/zeta",
"crates/zeta_cli",
"crates/zlog",
"crates/zlog_settings",
@ -251,6 +255,8 @@ channel = { path = "crates/channel" }
cli = { path = "crates/cli" }
client = { path = "crates/client" }
clock = { path = "crates/clock" }
cloud_api_client = { path = "crates/cloud_api_client" }
cloud_api_types = { path = "crates/cloud_api_types" }
cloud_llm_client = { path = "crates/cloud_llm_client" }
collab = { path = "crates/collab" }
collab_ui = { path = "crates/collab_ui" }
@ -338,6 +344,7 @@ picker = { path = "crates/picker" }
plugin = { path = "crates/plugin" }
plugin_macros = { path = "crates/plugin_macros" }
prettier = { path = "crates/prettier" }
settings_profile_selector = { path = "crates/settings_profile_selector" }
project = { path = "crates/project" }
project_panel = { path = "crates/project_panel" }
project_symbols = { path = "crates/project_symbols" }
@ -672,14 +679,16 @@ features = [
"UI_ViewManagement",
"Wdk_System_SystemServices",
"Win32_Globalization",
"Win32_Graphics_Direct2D",
"Win32_Graphics_Direct2D_Common",
"Win32_Graphics_Direct3D",
"Win32_Graphics_Direct3D11",
"Win32_Graphics_Direct3D_Fxc",
"Win32_Graphics_DirectComposition",
"Win32_Graphics_DirectWrite",
"Win32_Graphics_Dwm",
"Win32_Graphics_Dxgi",
"Win32_Graphics_Dxgi_Common",
"Win32_Graphics_Gdi",
"Win32_Graphics_Imaging",
"Win32_Graphics_Imaging_D2D",
"Win32_Networking_WinSock",
"Win32_Security",
"Win32_Security_Credentials",

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.3 KiB

View file

@ -0,0 +1,9 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path opacity="0.6" d="M3.5 11V5.5L8.5 8L3.5 11Z" fill="black"/>
<path opacity="0.4" d="M8.5 14L3.5 11L8.5 8V14Z" fill="black"/>
<path opacity="0.6" d="M8.5 5.5H3.5L8.5 2.5L8.5 5.5Z" fill="black"/>
<path opacity="0.8" d="M8.5 5.5V2.5L13.5 5.5H8.5Z" fill="black"/>
<path opacity="0.2" d="M13.5 11L8.5 14L11 9.5L13.5 11Z" fill="black"/>
<path opacity="0.5" d="M13.5 11L11 9.5L13.5 5V11Z" fill="black"/>
<path d="M3.5 11V5L8.5 2.11325L13.5 5V11L8.5 13.8868L3.5 11Z" stroke="black"/>
</svg>

After

Width:  |  Height:  |  Size: 583 B

View file

@ -0,0 +1,10 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_2716_663)">
<path d="M8.47552 2.45453C11.5167 2.45457 13.9814 4.94501 13.9814 8.01623C13.9814 11.0875 11.5167 13.578 8.47552 13.5781C5.43427 13.5781 2.96948 11.0875 2.96948 8.01623C2.9695 4.94498 5.43429 2.45453 8.47552 2.45453ZM10.8795 4.70348C10.7605 4.16887 10.1328 3.85468 9.53627 3.96342C8.97622 4.06552 7.62871 4.45681 7.62057 4.45916C9.29414 4.44469 9.57429 4.4726 9.69939 4.64751C9.77324 4.7508 9.66576 4.89248 9.21944 4.96538C8.73515 5.04447 7.73014 5.13958 7.72343 5.14022C6.75441 5.19776 6.07177 5.20168 5.86705 5.63512C5.73334 5.91827 6.00968 6.16857 6.13082 6.32527C6.64271 6.89455 7.38215 7.20158 7.85809 7.42767C8.03716 7.51274 8.56257 7.67345 8.56257 7.67345C7.01855 7.58853 5.90474 8.06267 5.2514 8.60855C4.51246 9.29204 4.83937 10.1067 6.35327 10.6084C7.24742 10.9047 7.69094 11.0439 9.02473 10.9238C9.81031 10.8815 9.9342 10.9068 9.94203 10.9712C9.95275 11.062 9.06932 11.2874 8.82812 11.357C8.21455 11.534 6.60645 11.8913 6.59758 11.8932C6.60115 11.8935 7.06249 11.9257 7.65531 11.8735C7.89632 11.8522 8.81142 11.7624 9.49557 11.6123C9.49557 11.6123 10.3297 11.4338 10.7759 11.2693C11.2429 11.0973 11.497 10.9512 11.6113 10.7443C11.6063 10.7019 11.6465 10.5516 11.4313 10.4613C10.8807 10.2304 10.2423 10.2721 8.9789 10.2453C7.57789 10.1972 7.11184 9.9626 6.86356 9.77373C6.62548 9.58212 6.74518 9.05204 7.76528 8.5851C8.27917 8.33646 10.2935 7.87759 10.2935 7.87759C9.61511 7.54227 8.35014 6.95284 8.09005 6.82552C7.86199 6.71388 7.49701 6.54572 7.4179 6.34233C7.32824 6.14709 7.6297 5.97888 7.79813 5.9307C8.34057 5.77424 9.10635 5.67701 9.8033 5.66609C10.1536 5.66061 10.2105 5.63806 10.2105 5.63806C10.6939 5.55787 11.0121 5.22722 10.8795 4.70348Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0_2716_663">
<rect width="12" height="12" fill="white" transform="translate(2.5 2)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.6725 13.9985C3.36161 13.9982 3.06354 13.8746 2.84371 13.6548C2.62388 13.435 2.50026 13.1369 2.5 12.826V7.494C2.5 6.8325 2.7675 6.185 3.2365 5.7165L6.219 2.736C6.45192 2.50247 6.72867 2.31724 7.03335 2.19094C7.33804 2.06464 7.66467 1.99975 7.9945 2H13.3275C13.6384 2.00027 13.9365 2.12388 14.1563 2.34371C14.3761 2.56354 14.4997 2.86162 14.5 3.1725V8.5045C14.4983 9.17074 14.2336 9.80936 13.7635 10.2815L10.781 13.264C10.5477 13.4976 10.2706 13.6829 9.96561 13.8092C9.66059 13.9355 9.33364 14.0003 9.0035 14V13.9985H3.6725ZM8.157 10.5715H5.243V11.257H8.157V10.5715ZM4.4815 5.257H11.243V12.0165L13.3715 9.888C13.7373 9.52036 13.9433 9.02316 13.9445 8.5045V3.1725C13.9445 2.8335 13.6685 2.5555 13.3275 2.5555H7.9945C7.73753 2.55499 7.483 2.6053 7.24556 2.70356C7.00813 2.80181 6.79246 2.94606 6.611 3.128L4.4815 5.257ZM4.3855 5.353L3.628 6.11C3.26258 6.47809 3.0569 6.97533 3.0555 7.494V12.826C3.0555 13.165 3.3315 13.443 3.6725 13.443H9.0055C9.26249 13.4434 9.51701 13.3929 9.75445 13.2946C9.99188 13.1963 10.2075 13.052 10.389 12.87L11.145 12.1145H4.3855V5.353Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.0945 8.01611C13.0945 7.87619 12.9911 7.79551 12.8642 7.8356L4.13456 10.6038C4.00742 10.6441 3.90427 10.7904 3.90427 10.9301V13.7593C3.90427 13.8992 4.00742 13.9801 4.13456 13.9398L12.8642 11.1719C12.9911 11.1315 13.0945 10.9852 13.0945 10.8453V8.01611Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.90427 7.92597C3.90427 8.06588 4.00742 8.21218 4.13456 8.25252L12.8655 11.0209C12.9926 11.0613 13.0958 10.9803 13.0958 10.8407V8.01124C13.0958 7.87158 12.9926 7.72529 12.8655 7.68494L4.13456 4.91652C4.00742 4.87618 3.90427 4.95686 3.90427 5.09677V7.92597Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.0945 2.20248C13.0945 2.06256 12.9911 1.98163 12.8642 2.02197L4.13456 4.78988C4.00742 4.83022 3.90427 4.97652 3.90427 5.11644V7.94563C3.90427 8.08554 4.00742 8.16622 4.13456 8.12614L12.8642 5.35797C12.9911 5.31763 13.0945 5.17133 13.0945 5.03167V2.20248Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.0094 13.9181C11.1984 13.9917 11.4139 13.987 11.6047 13.8952L14.0753 12.7064C14.3349 12.5814 14.5 12.3187 14.5 12.0305V3.9696C14.5 3.68136 14.3349 3.41862 14.0753 3.2937L11.6047 2.10485C11.3543 1.98438 11.0614 2.01389 10.8416 2.17363C10.8102 2.19645 10.7803 2.22193 10.7523 2.25001L6.02261 6.56498L3.96246 5.00115C3.77068 4.85558 3.50244 4.86751 3.32432 5.02953L2.66356 5.63059C2.44569 5.82877 2.44544 6.17152 2.66302 6.37004L4.44965 8.00001L2.66302 9.62998C2.44544 9.82849 2.44569 10.1713 2.66356 10.3694L3.32432 10.9705C3.50244 11.1325 3.77068 11.1444 3.96246 10.9989L6.02261 9.43504L10.7523 13.75C10.8271 13.8249 10.915 13.8812 11.0094 13.9181ZM11.5018 5.27587L7.91309 8.00001L11.5018 10.7241V5.27587Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 876 B

View file

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.0001 8.62505C13.0001 11.75 10.8126 13.3125 8.21266 14.2187C8.07651 14.2648 7.92862 14.2626 7.79392 14.2125C5.18771 13.3125 3.00024 11.75 3.00024 8.62505V4.25012C3.00024 4.08436 3.06609 3.92539 3.1833 3.80818C3.30051 3.69098 3.45948 3.62513 3.62523 3.62513C4.87521 3.62513 6.43769 2.87514 7.52517 1.92516C7.65758 1.81203 7.82601 1.74988 8.00016 1.74988C8.17431 1.74988 8.34275 1.81203 8.47515 1.92516C9.56889 2.88139 11.1251 3.62513 12.3751 3.62513C12.5408 3.62513 12.6998 3.69098 12.817 3.80818C12.9342 3.92539 13.0001 4.08436 13.0001 4.25012V8.62505Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 8.00002L7.33333 9.33335L10 6.66669" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 883 B

View file

@ -232,7 +232,7 @@
"ctrl-n": "agent::NewThread",
"ctrl-alt-n": "agent::NewTextThread",
"ctrl-shift-h": "agent::OpenHistory",
"ctrl-alt-c": "agent::OpenConfiguration",
"ctrl-alt-c": "agent::OpenSettings",
"ctrl-alt-p": "agent::OpenRulesLibrary",
"ctrl-i": "agent::ToggleProfileSelector",
"ctrl-alt-/": "agent::ToggleModelSelector",
@ -598,6 +598,7 @@
"ctrl-shift-t": "pane::ReopenClosedItem",
"ctrl-k ctrl-s": "zed::OpenKeymapEditor",
"ctrl-k ctrl-t": "theme_selector::Toggle",
"ctrl-alt-super-p": "settings_profile_selector::Toggle",
"ctrl-t": "project_symbols::Toggle",
"ctrl-p": "file_finder::Toggle",
"ctrl-tab": "tab_switcher::Toggle",
@ -1167,5 +1168,14 @@
"up": "menu::SelectPrevious",
"down": "menu::SelectNext"
}
},
{
"context": "Onboarding",
"use_key_equivalents": true,
"bindings": {
"ctrl-1": "onboarding::ActivateBasicsPage",
"ctrl-2": "onboarding::ActivateEditingPage",
"ctrl-3": "onboarding::ActivateAISetupPage"
}
}
]

View file

@ -272,7 +272,7 @@
"cmd-n": "agent::NewThread",
"cmd-alt-n": "agent::NewTextThread",
"cmd-shift-h": "agent::OpenHistory",
"cmd-alt-c": "agent::OpenConfiguration",
"cmd-alt-c": "agent::OpenSettings",
"cmd-alt-p": "agent::OpenRulesLibrary",
"cmd-i": "agent::ToggleProfileSelector",
"cmd-alt-/": "agent::ToggleModelSelector",
@ -665,6 +665,7 @@
"cmd-shift-t": "pane::ReopenClosedItem",
"cmd-k cmd-s": "zed::OpenKeymapEditor",
"cmd-k cmd-t": "theme_selector::Toggle",
"ctrl-alt-cmd-p": "settings_profile_selector::Toggle",
"cmd-t": "project_symbols::Toggle",
"cmd-p": "file_finder::Toggle",
"ctrl-tab": "tab_switcher::Toggle",
@ -1269,5 +1270,14 @@
"up": "menu::SelectPrevious",
"down": "menu::SelectNext"
}
},
{
"context": "Onboarding",
"use_key_equivalents": true,
"bindings": {
"cmd-1": "onboarding::ActivateBasicsPage",
"cmd-2": "onboarding::ActivateEditingPage",
"cmd-3": "onboarding::ActivateAISetupPage"
}
}
]

View file

@ -8,7 +8,7 @@
"ctrl-shift-i": "agent::ToggleFocus",
"ctrl-l": "agent::ToggleFocus",
"ctrl-shift-l": "agent::ToggleFocus",
"ctrl-shift-j": "agent::OpenConfiguration"
"ctrl-shift-j": "agent::OpenSettings"
}
},
{

View file

@ -95,7 +95,7 @@
"ctrl-shift-r": ["pane::DeploySearch", { "replace_enabled": true }],
"alt-shift-f10": "task::Spawn",
"ctrl-e": "file_finder::Toggle",
"ctrl-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
// "ctrl-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
"ctrl-shift-n": "file_finder::Toggle",
"ctrl-shift-a": "command_palette::Toggle",
"shift shift": "command_palette::Toggle",

View file

@ -8,7 +8,7 @@
"cmd-shift-i": "agent::ToggleFocus",
"cmd-l": "agent::ToggleFocus",
"cmd-shift-l": "agent::ToggleFocus",
"cmd-shift-j": "agent::OpenConfiguration"
"cmd-shift-j": "agent::OpenSettings"
}
},
{

View file

@ -97,7 +97,7 @@
"cmd-shift-r": ["pane::DeploySearch", { "replace_enabled": true }],
"ctrl-alt-r": "task::Spawn",
"cmd-e": "file_finder::Toggle",
"cmd-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
// "cmd-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
"cmd-shift-o": "file_finder::Toggle",
"cmd-shift-a": "command_palette::Toggle",
"shift shift": "command_palette::Toggle",

View file

@ -1877,5 +1877,25 @@
"save_breakpoints": true,
"dock": "bottom",
"button": true
}
},
// Configures any number of settings profiles that are temporarily applied on
// top of your existing user settings when selected from
// `settings profile selector: toggle`.
// Examples:
// "profiles": {
// "Presenting": {
// "agent_font_size": 20.0,
// "buffer_font_size": 20.0,
// "theme": "One Light",
// "ui_font_size": 20.0
// },
// "Python (ty)": {
// "languages": {
// "Python": {
// "language_servers": ["ty"]
// }
// }
// }
// }
"profiles": []
}

View file

@ -580,6 +580,9 @@ pub struct AcpThread {
pub enum AcpThreadEvent {
NewEntry,
EntryUpdated(usize),
ToolAuthorizationRequired,
Stopped,
Error,
}
impl EventEmitter<AcpThreadEvent> for AcpThread {}
@ -677,6 +680,18 @@ impl AcpThread {
false
}
pub fn used_tools_since_last_user_message(&self) -> bool {
for entry in self.entries.iter().rev() {
match entry {
AgentThreadEntry::UserMessage(..) => return false,
AgentThreadEntry::AssistantMessage(..) => continue,
AgentThreadEntry::ToolCall(..) => return true,
}
}
false
}
pub fn handle_session_update(
&mut self,
update: acp::SessionUpdate,
@ -880,6 +895,7 @@ impl AcpThread {
};
self.upsert_tool_call_inner(tool_call, status, cx);
cx.emit(AcpThreadEvent::ToolAuthorizationRequired);
rx
}
@ -1015,12 +1031,18 @@ impl AcpThread {
.log_err();
}));
async move {
match rx.await {
Ok(Err(e)) => Err(e)?,
_ => Ok(()),
cx.spawn(async move |this, cx| match rx.await {
Ok(Err(e)) => {
this.update(cx, |_, cx| cx.emit(AcpThreadEvent::Error))
.log_err();
Err(e)?
}
}
_ => {
this.update(cx, |_, cx| cx.emit(AcpThreadEvent::Stopped))
.log_err();
Ok(())
}
})
.boxed()
}

View file

@ -47,7 +47,6 @@ paths.workspace = true
postage.workspace = true
project.workspace = true
prompt_store.workspace = true
proto.workspace = true
ref-cast.workspace = true
rope.workspace = true
schemars.workspace = true

View file

@ -13,7 +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 cloud_llm_client::{CompletionIntent, CompletionRequestStatus, Plan, UsageLimit};
use collections::HashMap;
use feature_flags::{self, FeatureFlagAppExt};
use futures::{FutureExt, StreamExt as _, future::Shared};
@ -37,7 +37,6 @@ use project::{
git_store::{GitStore, GitStoreCheckpoint, RepositoryState},
};
use prompt_store::{ModelContext, PromptBuilder};
use proto::Plan;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
@ -3255,8 +3254,10 @@ impl Thread {
}
fn update_model_request_usage(&self, amount: u32, limit: UsageLimit, cx: &mut Context<Self>) {
self.project.update(cx, |project, cx| {
project.user_store().update(cx, |user_store, cx| {
self.project
.read(cx)
.user_store()
.update(cx, |user_store, cx| {
user_store.update_model_request_usage(
ModelRequestUsage(RequestUsage {
amount: amount as i32,
@ -3264,8 +3265,7 @@ impl Thread {
}),
cx,
)
})
});
});
}
pub fn deny_tool_use(

View file

@ -1,5 +1,7 @@
use acp_thread::{AgentConnection, Plan};
use agent_servers::AgentServer;
use agent_settings::{AgentSettings, NotifyWhenAgentWaiting};
use audio::{Audio, Sound};
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::path::Path;
@ -18,10 +20,10 @@ use editor::{
use file_icons::FileIcons;
use gpui::{
Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId,
FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, SharedString, StyleRefinement,
Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity,
Window, div, linear_color_stop, linear_gradient, list, percentage, point, prelude::*,
pulsating_between,
FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, PlatformDisplay, SharedString,
StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, Transformation,
UnderlineStyle, WeakEntity, Window, WindowHandle, div, linear_color_stop, linear_gradient,
list, percentage, point, prelude::*, pulsating_between,
};
use language::language_settings::SoftWrap;
use language::{Buffer, Language};
@ -45,7 +47,10 @@ use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSe
use crate::acp::message_history::MessageHistory;
use crate::agent_diff::AgentDiff;
use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES};
use crate::{AgentDiffPane, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, RejectAll};
use crate::ui::{AgentNotification, AgentNotificationEvent};
use crate::{
AgentDiffPane, AgentPanel, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, RejectAll,
};
const RESPONSE_PADDING_X: Pixels = px(19.);
@ -59,6 +64,8 @@ pub struct AcpThreadView {
message_set_from_history: bool,
_message_editor_subscription: Subscription,
mention_set: Arc<Mutex<MentionSet>>,
notifications: Vec<WindowHandle<AgentNotification>>,
notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
last_error: Option<Entity<Markdown>>,
list_state: ListState,
auth_task: Option<Task<()>>,
@ -174,6 +181,8 @@ impl AcpThreadView {
message_set_from_history: false,
_message_editor_subscription: message_editor_subscription,
mention_set,
notifications: Vec::new(),
notification_subscriptions: HashMap::default(),
diff_editors: Default::default(),
list_state: list_state,
last_error: None,
@ -382,7 +391,9 @@ impl AcpThreadView {
return;
}
let Some(thread) = self.thread() else { return };
let Some(thread) = self.thread() else {
return;
};
let task = thread.update(cx, |thread, cx| thread.send(chunks.clone(), cx));
cx.spawn(async move |this, cx| {
@ -565,6 +576,30 @@ impl AcpThreadView {
self.sync_thread_entry_view(index, window, cx);
self.list_state.splice(index..index + 1, 1);
}
AcpThreadEvent::ToolAuthorizationRequired => {
self.notify_with_sound("Waiting for tool confirmation", IconName::Info, window, cx);
}
AcpThreadEvent::Stopped => {
let used_tools = thread.read(cx).used_tools_since_last_user_message();
self.notify_with_sound(
if used_tools {
"Finished running tools"
} else {
"New message"
},
IconName::ZedAssistant,
window,
cx,
);
}
AcpThreadEvent::Error => {
self.notify_with_sound(
"Agent stopped due to an error",
IconName::Warning,
window,
cx,
);
}
}
cx.notify();
}
@ -2166,6 +2201,154 @@ impl AcpThreadView {
self.list_state.scroll_to(ListOffset::default());
cx.notify();
}
fn notify_with_sound(
&mut self,
caption: impl Into<SharedString>,
icon: IconName,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.play_notification_sound(window, cx);
self.show_notification(caption, icon, window, cx);
}
fn play_notification_sound(&self, window: &Window, cx: &mut App) {
let settings = AgentSettings::get_global(cx);
if settings.play_sound_when_agent_done && !window.is_window_active() {
Audio::play_sound(Sound::AgentDone, cx);
}
}
fn show_notification(
&mut self,
caption: impl Into<SharedString>,
icon: IconName,
window: &mut Window,
cx: &mut Context<Self>,
) {
if window.is_window_active() || !self.notifications.is_empty() {
return;
}
let title = self.title(cx);
match AgentSettings::get_global(cx).notify_when_agent_waiting {
NotifyWhenAgentWaiting::PrimaryScreen => {
if let Some(primary) = cx.primary_display() {
self.pop_up(icon, caption.into(), title, window, primary, cx);
}
}
NotifyWhenAgentWaiting::AllScreens => {
let caption = caption.into();
for screen in cx.displays() {
self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx);
}
}
NotifyWhenAgentWaiting::Never => {
// Don't show anything
}
}
}
fn pop_up(
&mut self,
icon: IconName,
caption: SharedString,
title: SharedString,
window: &mut Window,
screen: Rc<dyn PlatformDisplay>,
cx: &mut Context<Self>,
) {
let options = AgentNotification::window_options(screen, cx);
let project_name = self.workspace.upgrade().and_then(|workspace| {
workspace
.read(cx)
.project()
.read(cx)
.visible_worktrees(cx)
.next()
.map(|worktree| worktree.read(cx).root_name().to_string())
});
if let Some(screen_window) = cx
.open_window(options, |_, cx| {
cx.new(|_| {
AgentNotification::new(title.clone(), caption.clone(), icon, project_name)
})
})
.log_err()
{
if let Some(pop_up) = screen_window.entity(cx).log_err() {
self.notification_subscriptions
.entry(screen_window)
.or_insert_with(Vec::new)
.push(cx.subscribe_in(&pop_up, window, {
|this, _, event, window, cx| match event {
AgentNotificationEvent::Accepted => {
let handle = window.window_handle();
cx.activate(true);
let workspace_handle = this.workspace.clone();
// If there are multiple Zed windows, activate the correct one.
cx.defer(move |cx| {
handle
.update(cx, |_view, window, _cx| {
window.activate_window();
if let Some(workspace) = workspace_handle.upgrade() {
workspace.update(_cx, |workspace, cx| {
workspace.focus_panel::<AgentPanel>(window, cx);
});
}
})
.log_err();
});
this.dismiss_notifications(cx);
}
AgentNotificationEvent::Dismissed => {
this.dismiss_notifications(cx);
}
}
}));
self.notifications.push(screen_window);
// If the user manually refocuses the original window, dismiss the popup.
self.notification_subscriptions
.entry(screen_window)
.or_insert_with(Vec::new)
.push({
let pop_up_weak = pop_up.downgrade();
cx.observe_window_activation(window, move |_, window, cx| {
if window.is_window_active() {
if let Some(pop_up) = pop_up_weak.upgrade() {
pop_up.update(cx, |_, cx| {
cx.emit(AgentNotificationEvent::Dismissed);
});
}
}
})
});
}
}
}
fn dismiss_notifications(&mut self, cx: &mut Context<Self>) {
for window in self.notifications.drain(..) {
window
.update(cx, |_, window, _| {
window.remove_window();
})
.ok();
self.notification_subscriptions.remove(&window);
}
}
}
impl Focusable for AcpThreadView {
@ -2451,3 +2634,331 @@ fn plan_label_markdown_style(
..default_md_style
}
}
#[cfg(test)]
mod tests {
use agent_client_protocol::SessionId;
use editor::EditorSettings;
use fs::FakeFs;
use futures::future::try_join_all;
use gpui::{SemanticVersion, TestAppContext, VisualTestContext};
use rand::Rng;
use settings::SettingsStore;
use super::*;
#[gpui::test]
async fn test_notification_for_stop_event(cx: &mut TestAppContext) {
init_test(cx);
let (thread_view, cx) = setup_thread_view(StubAgentServer::default(), cx).await;
let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
message_editor.update_in(cx, |editor, window, cx| {
editor.set_text("Hello", window, cx);
});
cx.deactivate_window();
thread_view.update_in(cx, |thread_view, window, cx| {
thread_view.chat(&Chat, window, cx);
});
cx.run_until_parked();
assert!(
cx.windows()
.iter()
.any(|window| window.downcast::<AgentNotification>().is_some())
);
}
#[gpui::test]
async fn test_notification_for_error(cx: &mut TestAppContext) {
init_test(cx);
let (thread_view, cx) =
setup_thread_view(StubAgentServer::new(SaboteurAgentConnection), cx).await;
let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
message_editor.update_in(cx, |editor, window, cx| {
editor.set_text("Hello", window, cx);
});
cx.deactivate_window();
thread_view.update_in(cx, |thread_view, window, cx| {
thread_view.chat(&Chat, window, cx);
});
cx.run_until_parked();
assert!(
cx.windows()
.iter()
.any(|window| window.downcast::<AgentNotification>().is_some())
);
}
#[gpui::test]
async fn test_notification_for_tool_authorization(cx: &mut TestAppContext) {
init_test(cx);
let tool_call_id = acp::ToolCallId("1".into());
let tool_call = acp::ToolCall {
id: tool_call_id.clone(),
label: "Label".into(),
kind: acp::ToolKind::Edit,
status: acp::ToolCallStatus::Pending,
content: vec!["hi".into()],
locations: vec![],
raw_input: None,
};
let connection = StubAgentConnection::new(vec![acp::SessionUpdate::ToolCall(tool_call)])
.with_permission_requests(HashMap::from_iter([(
tool_call_id,
vec![acp::PermissionOption {
id: acp::PermissionOptionId("1".into()),
label: "Allow".into(),
kind: acp::PermissionOptionKind::AllowOnce,
}],
)]));
let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
message_editor.update_in(cx, |editor, window, cx| {
editor.set_text("Hello", window, cx);
});
cx.deactivate_window();
thread_view.update_in(cx, |thread_view, window, cx| {
thread_view.chat(&Chat, window, cx);
});
cx.run_until_parked();
assert!(
cx.windows()
.iter()
.any(|window| window.downcast::<AgentNotification>().is_some())
);
}
async fn setup_thread_view(
agent: impl AgentServer + 'static,
cx: &mut TestAppContext,
) -> (Entity<AcpThreadView>, &mut VisualTestContext) {
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let thread_view = cx.update(|window, cx| {
cx.new(|cx| {
AcpThreadView::new(
Rc::new(agent),
workspace.downgrade(),
project,
Rc::new(RefCell::new(MessageHistory::default())),
1,
None,
window,
cx,
)
})
});
cx.run_until_parked();
(thread_view, cx)
}
struct StubAgentServer<C> {
connection: C,
}
impl<C> StubAgentServer<C> {
fn new(connection: C) -> Self {
Self { connection }
}
}
impl StubAgentServer<StubAgentConnection> {
fn default() -> Self {
Self::new(StubAgentConnection::default())
}
}
impl<C> AgentServer for StubAgentServer<C>
where
C: 'static + AgentConnection + Send + Clone,
{
fn logo(&self) -> ui::IconName {
unimplemented!()
}
fn name(&self) -> &'static str {
unimplemented!()
}
fn empty_state_headline(&self) -> &'static str {
unimplemented!()
}
fn empty_state_message(&self) -> &'static str {
unimplemented!()
}
fn connect(
&self,
_root_dir: &Path,
_project: &Entity<Project>,
_cx: &mut App,
) -> Task<gpui::Result<Rc<dyn AgentConnection>>> {
Task::ready(Ok(Rc::new(self.connection.clone())))
}
}
#[derive(Clone, Default)]
struct StubAgentConnection {
sessions: Arc<Mutex<HashMap<acp::SessionId, WeakEntity<AcpThread>>>>,
permission_requests: HashMap<acp::ToolCallId, Vec<acp::PermissionOption>>,
updates: Vec<acp::SessionUpdate>,
}
impl StubAgentConnection {
fn new(updates: Vec<acp::SessionUpdate>) -> Self {
Self {
updates,
permission_requests: HashMap::default(),
sessions: Arc::default(),
}
}
fn with_permission_requests(
mut self,
permission_requests: HashMap<acp::ToolCallId, Vec<acp::PermissionOption>>,
) -> Self {
self.permission_requests = permission_requests;
self
}
}
impl AgentConnection for StubAgentConnection {
fn name(&self) -> &'static str {
"StubAgentConnection"
}
fn new_thread(
self: Rc<Self>,
project: Entity<Project>,
_cwd: &Path,
cx: &mut gpui::AsyncApp,
) -> Task<gpui::Result<Entity<AcpThread>>> {
let session_id = SessionId(
rand::thread_rng()
.sample_iter(&rand::distributions::Alphanumeric)
.take(7)
.map(char::from)
.collect::<String>()
.into(),
);
let thread = cx
.new(|cx| AcpThread::new(self.clone(), project, session_id.clone(), cx))
.unwrap();
self.sessions.lock().insert(session_id, thread.downgrade());
Task::ready(Ok(thread))
}
fn authenticate(&self, _cx: &mut App) -> Task<gpui::Result<()>> {
unimplemented!()
}
fn prompt(&self, params: acp::PromptArguments, cx: &mut App) -> Task<gpui::Result<()>> {
let sessions = self.sessions.lock();
let thread = sessions.get(&params.session_id).unwrap();
let mut tasks = vec![];
for update in &self.updates {
let thread = thread.clone();
let update = update.clone();
let permission_request = if let acp::SessionUpdate::ToolCall(tool_call) = &update
&& let Some(options) = self.permission_requests.get(&tool_call.id)
{
Some((tool_call.clone(), options.clone()))
} else {
None
};
let task = cx.spawn(async move |cx| {
if let Some((tool_call, options)) = permission_request {
let permission = thread.update(cx, |thread, cx| {
thread.request_tool_call_permission(
tool_call.clone(),
options.clone(),
cx,
)
})?;
permission.await?;
}
thread.update(cx, |thread, cx| {
thread.handle_session_update(update.clone(), cx).unwrap();
})?;
anyhow::Ok(())
});
tasks.push(task);
}
cx.spawn(async move |_| {
try_join_all(tasks).await?;
Ok(())
})
}
fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
unimplemented!()
}
}
#[derive(Clone)]
struct SaboteurAgentConnection;
impl AgentConnection for SaboteurAgentConnection {
fn name(&self) -> &'static str {
"SaboteurAgentConnection"
}
fn new_thread(
self: Rc<Self>,
project: Entity<Project>,
_cwd: &Path,
cx: &mut gpui::AsyncApp,
) -> Task<gpui::Result<Entity<AcpThread>>> {
Task::ready(Ok(cx
.new(|cx| AcpThread::new(self, project, SessionId("test".into()), cx))
.unwrap()))
}
fn authenticate(&self, _cx: &mut App) -> Task<gpui::Result<()>> {
unimplemented!()
}
fn prompt(&self, _params: acp::PromptArguments, _cx: &mut App) -> Task<gpui::Result<()>> {
Task::ready(Err(anyhow::anyhow!("Error prompting")))
}
fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
unimplemented!()
}
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
language::init(cx);
Project::init_settings(cx);
AgentSettings::register(cx);
workspace::init_settings(cx);
ThemeSettings::register(cx);
release_channel::init(SemanticVersion::default(), cx);
EditorSettings::register(cx);
});
}
}

View file

@ -7,6 +7,7 @@ use std::{sync::Arc, time::Duration};
use agent_settings::AgentSettings;
use assistant_tool::{ToolSource, ToolWorkingSet};
use cloud_llm_client::Plan;
use collections::HashMap;
use context_server::ContextServerId;
use extension::ExtensionManifest;
@ -25,7 +26,6 @@ use project::{
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
project_settings::{ContextServerSettings, ProjectSettings},
};
use proto::Plan;
use settings::{Settings, update_settings_file};
use ui::{
Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu,
@ -180,7 +180,7 @@ impl AgentConfiguration {
let current_plan = if is_zed_provider {
self.workspace
.upgrade()
.and_then(|workspace| workspace.read(cx).user_store().read(cx).current_plan())
.and_then(|workspace| workspace.read(cx).user_store().read(cx).plan())
} else {
None
};
@ -406,7 +406,9 @@ impl AgentConfiguration {
SwitchField::new(
"always-allow-tool-actions-switch",
"Allow running commands without asking for confirmation",
"The agent can perform potentially destructive actions without asking for your confirmation.",
Some(
"The agent can perform potentially destructive actions without asking for your confirmation.".into(),
),
always_allow_tool_actions,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
@ -424,7 +426,7 @@ impl AgentConfiguration {
SwitchField::new(
"single-file-review",
"Enable single-file agent reviews",
"Agent edits are also displayed in single-file editors for review.",
Some("Agent edits are also displayed in single-file editors for review.".into()),
single_file_review,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
@ -442,7 +444,9 @@ impl AgentConfiguration {
SwitchField::new(
"sound-notification",
"Play sound when finished generating",
"Hear a notification sound when the agent is done generating changes or needs your input.",
Some(
"Hear a notification sound when the agent is done generating changes or needs your input.".into(),
),
play_sound_when_agent_done,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
@ -460,7 +464,9 @@ impl AgentConfiguration {
SwitchField::new(
"modifier-send",
"Use modifier to submit a message",
"Make a modifier (cmd-enter on macOS, ctrl-enter on Linux) required to send messages.",
Some(
"Make a modifier (cmd-enter on macOS, ctrl-enter on Linux) required to send messages.".into(),
),
use_modifier_to_send,
move |state, _window, cx| {
let allow = state == &ToggleState::Selected;
@ -502,7 +508,7 @@ impl AgentConfiguration {
.blend(cx.theme().colors().text_accent.opacity(0.2));
let (plan_name, label_color, bg_color) = match plan {
Plan::Free => ("Free", Color::Default, free_chip_bg),
Plan::ZedFree => ("Free", Color::Default, free_chip_bg),
Plan::ZedProTrial => ("Pro Trial", Color::Accent, pro_chip_bg),
Plan::ZedPro => ("Pro", Color::Accent, pro_chip_bg),
};

View file

@ -1521,6 +1521,9 @@ impl AgentDiff {
self.update_reviewing_editors(workspace, window, cx);
}
}
AcpThreadEvent::Stopped
| AcpThreadEvent::ToolAuthorizationRequired
| AcpThreadEvent::Error => {}
}
}

View file

@ -44,7 +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 cloud_llm_client::{CompletionIntent, Plan, UsageLimit};
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
use feature_flags::{self, FeatureFlagAppExt};
use fs::Fs;
@ -60,7 +60,6 @@ use language_model::{
};
use project::{Project, ProjectPath, Worktree};
use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
use proto::Plan;
use rules_library::{RulesLibrary, open_rules_library};
use search::{BufferSearchBar, buffer_search};
use settings::{Settings, update_settings_file};
@ -78,7 +77,7 @@ use workspace::{
};
use zed_actions::{
DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize,
agent::{OpenConfiguration, OpenOnboardingModal, ResetOnboarding, ToggleModelSelector},
agent::{OpenOnboardingModal, OpenSettings, ResetOnboarding, ToggleModelSelector},
assistant::{OpenRulesLibrary, ToggleFocus},
};
@ -105,7 +104,7 @@ pub fn init(cx: &mut App) {
panel.update(cx, |panel, cx| panel.open_history(window, cx));
}
})
.register_action(|workspace, _: &OpenConfiguration, window, cx| {
.register_action(|workspace, _: &OpenSettings, window, cx| {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
workspace.focus_panel::<AgentPanel>(window, cx);
panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
@ -579,7 +578,6 @@ impl AgentPanel {
MessageEditor::new(
fs.clone(),
workspace.clone(),
user_store.clone(),
message_editor_context_store.clone(),
prompt_store.clone(),
thread_store.downgrade(),
@ -848,7 +846,6 @@ impl AgentPanel {
MessageEditor::new(
self.fs.clone(),
self.workspace.clone(),
self.user_store.clone(),
context_store.clone(),
self.prompt_store.clone(),
self.thread_store.downgrade(),
@ -1122,7 +1119,6 @@ impl AgentPanel {
MessageEditor::new(
self.fs.clone(),
self.workspace.clone(),
self.user_store.clone(),
context_store,
self.prompt_store.clone(),
self.thread_store.downgrade(),
@ -2074,7 +2070,7 @@ impl AgentPanel {
menu = menu
.action("Rules…", Box::new(OpenRulesLibrary::default()))
.action("Settings", Box::new(OpenConfiguration))
.action("Settings", Box::new(OpenSettings))
.action(zoom_in_label, Box::new(ToggleZoom));
menu
}))
@ -2279,10 +2275,10 @@ impl AgentPanel {
| ActiveView::Configuration => return false,
}
let plan = self.user_store.read(cx).current_plan();
let plan = self.user_store.read(cx).plan();
let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
matches!(plan, Some(Plan::Free)) && has_previous_trial
matches!(plan, Some(Plan::ZedFree)) && has_previous_trial
}
fn should_render_onboarding(&self, cx: &mut Context<Self>) -> bool {
@ -2468,14 +2464,14 @@ impl AgentPanel {
.icon_color(Color::Muted)
.full_width()
.key_binding(KeyBinding::for_action_in(
&OpenConfiguration,
&OpenSettings,
&focus_handle,
window,
cx,
))
.on_click(|_event, window, cx| {
window.dispatch_action(
OpenConfiguration.boxed_clone(),
OpenSettings.boxed_clone(),
cx,
)
}),
@ -2680,16 +2676,11 @@ impl AgentPanel {
.style(ButtonStyle::Tinted(ui::TintColor::Warning))
.label_size(LabelSize::Small)
.key_binding(
KeyBinding::for_action_in(
&OpenConfiguration,
&focus_handle,
window,
cx,
)
.map(|kb| kb.size(rems_from_px(12.))),
KeyBinding::for_action_in(&OpenSettings, &focus_handle, window, cx)
.map(|kb| kb.size(rems_from_px(12.))),
)
.on_click(|_event, window, cx| {
window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
window.dispatch_action(OpenSettings.boxed_clone(), cx)
}),
),
ConfigurationError::ProviderPendingTermsAcceptance(provider) => {
@ -2883,7 +2874,7 @@ impl AgentPanel {
) -> AnyElement {
let error_message = match plan {
Plan::ZedPro => "Upgrade to usage-based billing for more prompts.",
Plan::ZedProTrial | Plan::Free => "Upgrade to Zed Pro for more prompts.",
Plan::ZedProTrial | Plan::ZedFree => "Upgrade to Zed Pro for more prompts.",
};
let icon = Icon::new(IconName::XCircle)
@ -3193,7 +3184,7 @@ impl Render for AgentPanel {
.on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
this.open_history(window, cx);
}))
.on_action(cx.listener(|this, _: &OpenConfiguration, window, cx| {
.on_action(cx.listener(|this, _: &OpenSettings, window, cx| {
this.open_configuration(window, cx);
}))
.on_action(cx.listener(Self::open_active_thread_as_markdown))

View file

@ -263,8 +263,8 @@ fn update_command_palette_filter(cx: &mut App) {
filter.hide_namespace("agent");
filter.hide_namespace("assistant");
filter.hide_namespace("copilot");
filter.hide_namespace("supermaven");
filter.hide_namespace("zed_predict_onboarding");
filter.hide_namespace("edit_prediction");
use editor::actions::{

View file

@ -48,7 +48,7 @@ use text::{OffsetRangeExt, ToPoint as _};
use ui::prelude::*;
use util::{RangeExt, ResultExt, maybe};
use workspace::{ItemHandle, Toast, Workspace, dock::Panel, notifications::NotificationId};
use zed_actions::agent::OpenConfiguration;
use zed_actions::agent::OpenSettings;
pub fn init(
fs: Arc<dyn Fs>,
@ -345,7 +345,7 @@ impl InlineAssistant {
if let Some(answer) = answer {
if answer == 0 {
cx.update(|window, cx| {
window.dispatch_action(Box::new(OpenConfiguration), cx)
window.dispatch_action(Box::new(OpenSettings), cx)
})
.ok();
}

View file

@ -576,7 +576,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
.icon_position(IconPosition::Start)
.on_click(|_, window, cx| {
window.dispatch_action(
zed_actions::agent::OpenConfiguration.boxed_clone(),
zed_actions::agent::OpenSettings.boxed_clone(),
cx,
);
}),

View file

@ -17,7 +17,6 @@ use agent::{
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};
@ -43,7 +42,6 @@ use language_model::{
use multi_buffer;
use project::Project;
use prompt_store::PromptStore;
use proto::Plan;
use settings::Settings;
use std::time::Duration;
use theme::ThemeSettings;
@ -79,7 +77,6 @@ pub struct MessageEditor {
editor: Entity<Editor>,
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
user_store: Entity<UserStore>,
context_store: Entity<ContextStore>,
prompt_store: Option<Entity<PromptStore>>,
history_store: Option<WeakEntity<HistoryStore>>,
@ -159,7 +156,6 @@ impl MessageEditor {
pub fn new(
fs: Arc<dyn Fs>,
workspace: WeakEntity<Workspace>,
user_store: Entity<UserStore>,
context_store: Entity<ContextStore>,
prompt_store: Option<Entity<PromptStore>>,
thread_store: WeakEntity<ThreadStore>,
@ -231,7 +227,6 @@ impl MessageEditor {
Self {
editor: editor.clone(),
project: thread.read(cx).project().clone(),
user_store,
thread,
incompatible_tools_state: incompatible_tools.clone(),
workspace,
@ -1287,24 +1282,12 @@ impl MessageEditor {
return None;
}
let user_store = self.user_store.read(cx);
let ubb_enable = user_store
.usage_based_billing_enabled()
.map_or(false, |enabled| enabled);
if ubb_enable {
let user_store = self.project.read(cx).user_store().read(cx);
if user_store.is_usage_based_billing_enabled() {
return None;
}
let plan = user_store
.current_plan()
.map(|plan| match plan {
Plan::Free => cloud_llm_client::Plan::ZedFree,
Plan::ZedPro => cloud_llm_client::Plan::ZedPro,
Plan::ZedProTrial => cloud_llm_client::Plan::ZedProTrial,
})
.unwrap_or(cloud_llm_client::Plan::ZedFree);
let plan = user_store.plan().unwrap_or(cloud_llm_client::Plan::ZedFree);
let usage = user_store.model_request_usage()?;
@ -1769,7 +1752,6 @@ impl AgentPreview for MessageEditor {
) -> Option<AnyElement> {
if let Some(workspace) = workspace.upgrade() {
let fs = workspace.read(cx).app_state().fs.clone();
let user_store = workspace.read(cx).app_state().user_store.clone();
let project = workspace.read(cx).project().clone();
let weak_project = project.downgrade();
let context_store = cx.new(|_cx| ContextStore::new(weak_project, None));
@ -1782,7 +1764,6 @@ impl AgentPreview for MessageEditor {
MessageEditor::new(
fs,
workspace.downgrade(),
user_store,
context_store,
None,
thread_store.downgrade(),

View file

@ -16,10 +16,10 @@ default = []
[dependencies]
client.workspace = true
cloud_llm_client.workspace = true
component.workspace = true
gpui.workspace = true
language_model.workspace = true
proto.workspace = true
serde.workspace = true
smallvec.workspace = true
telemetry.workspace = true

View file

@ -136,10 +136,7 @@ impl RenderOnce for ApiKeysWithoutProviders {
.full_width()
.style(ButtonStyle::Outlined)
.on_click(move |_, window, cx| {
window.dispatch_action(
zed_actions::agent::OpenConfiguration.boxed_clone(),
cx,
);
window.dispatch_action(zed_actions::agent::OpenSettings.boxed_clone(), cx);
}),
)
}

View file

@ -1,6 +1,7 @@
use std::sync::Arc;
use client::{Client, UserStore};
use cloud_llm_client::Plan;
use gpui::{Entity, IntoElement, ParentElement};
use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
use ui::prelude::*;
@ -56,15 +57,8 @@ impl AgentPanelOnboarding {
impl Render for AgentPanelOnboarding {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let enrolled_in_trial = matches!(
self.user_store.read(cx).current_plan(),
Some(proto::Plan::ZedProTrial)
);
let is_pro_user = matches!(
self.user_store.read(cx).current_plan(),
Some(proto::Plan::ZedPro)
);
let enrolled_in_trial = self.user_store.read(cx).plan() == Some(Plan::ZedProTrial);
let is_pro_user = self.user_store.read(cx).plan() == Some(Plan::ZedPro);
AgentPanelOnboardingCard::new()
.child(

View file

@ -9,6 +9,7 @@ pub use agent_api_keys_onboarding::{ApiKeysWithProviders, ApiKeysWithoutProvider
pub use agent_panel_onboarding_card::AgentPanelOnboardingCard;
pub use agent_panel_onboarding_content::AgentPanelOnboarding;
pub use ai_upsell_card::AiUpsellCard;
use cloud_llm_client::Plan;
pub use edit_prediction_onboarding_content::EditPredictionOnboarding;
pub use young_account_banner::YoungAccountBanner;
@ -79,7 +80,7 @@ impl From<client::Status> for SignInStatus {
pub struct ZedAiOnboarding {
pub sign_in_status: SignInStatus,
pub has_accepted_terms_of_service: bool,
pub plan: Option<proto::Plan>,
pub plan: Option<Plan>,
pub account_too_young: bool,
pub continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
@ -99,8 +100,8 @@ impl ZedAiOnboarding {
Self {
sign_in_status: status.into(),
has_accepted_terms_of_service: store.current_user_has_accepted_terms().unwrap_or(false),
plan: store.current_plan(),
has_accepted_terms_of_service: store.has_accepted_terms_of_service(),
plan: store.plan(),
account_too_young: store.account_too_young(),
continue_with_zed_ai,
accept_terms_of_service: Arc::new({
@ -113,11 +114,9 @@ impl ZedAiOnboarding {
sign_in: Arc::new(move |_window, cx| {
cx.spawn({
let client = client.clone();
async move |cx| {
client.authenticate_and_connect(true, cx).await;
}
async move |cx| client.sign_in_with_optional_connect(true, cx).await
})
.detach();
.detach_and_log_err(cx);
}),
dismiss_onboarding: None,
}
@ -411,9 +410,9 @@ impl RenderOnce for ZedAiOnboarding {
if matches!(self.sign_in_status, SignInStatus::SignedIn) {
if self.has_accepted_terms_of_service {
match self.plan {
None | Some(proto::Plan::Free) => self.render_free_plan_state(cx),
Some(proto::Plan::ZedProTrial) => self.render_trial_state(cx),
Some(proto::Plan::ZedPro) => self.render_pro_plan_state(cx),
None | Some(Plan::ZedFree) => self.render_free_plan_state(cx),
Some(Plan::ZedProTrial) => self.render_trial_state(cx),
Some(Plan::ZedPro) => self.render_pro_plan_state(cx),
}
} else {
self.render_accept_terms_of_service()
@ -433,7 +432,7 @@ impl Component for ZedAiOnboarding {
fn onboarding(
sign_in_status: SignInStatus,
has_accepted_terms_of_service: bool,
plan: Option<proto::Plan>,
plan: Option<Plan>,
account_too_young: bool,
) -> AnyElement {
ZedAiOnboarding {
@ -468,25 +467,15 @@ impl Component for ZedAiOnboarding {
),
single_example(
"Free Plan",
onboarding(SignInStatus::SignedIn, true, Some(proto::Plan::Free), false),
onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedFree), false),
),
single_example(
"Pro Trial",
onboarding(
SignInStatus::SignedIn,
true,
Some(proto::Plan::ZedProTrial),
false,
),
onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedProTrial), false),
),
single_example(
"Pro Plan",
onboarding(
SignInStatus::SignedIn,
true,
Some(proto::Plan::ZedPro),
false,
),
onboarding(SignInStatus::SignedIn, true, Some(Plan::ZedPro), false),
),
])
.into_any_element(),

View file

@ -1,6 +1,7 @@
use std::sync::Arc;
use client::{Client, zed_urls};
use cloud_llm_client::Plan;
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
use ui::{Divider, List, Vector, VectorName, prelude::*};
@ -10,22 +11,22 @@ use crate::{BulletItem, SignInStatus};
pub struct AiUpsellCard {
pub sign_in_status: SignInStatus,
pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
pub user_plan: Option<Plan>,
}
impl AiUpsellCard {
pub fn new(client: Arc<Client>) -> Self {
pub fn new(client: Arc<Client>, user_plan: Option<Plan>) -> Self {
let status = *client.status().borrow();
Self {
user_plan,
sign_in_status: status.into(),
sign_in: Arc::new(move |_window, cx| {
cx.spawn({
let client = client.clone();
async move |cx| {
client.authenticate_and_connect(true, cx).await;
}
async move |cx| client.sign_in_with_optional_connect(true, cx).await
})
.detach();
.detach_and_log_err(cx);
}),
}
}
@ -34,6 +35,7 @@ impl AiUpsellCard {
impl RenderOnce for AiUpsellCard {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let pro_section = v_flex()
.flex_grow()
.w_full()
.gap_1()
.child(
@ -56,6 +58,7 @@ impl RenderOnce for AiUpsellCard {
);
let free_section = v_flex()
.flex_grow()
.w_full()
.gap_1()
.child(
@ -71,7 +74,7 @@ impl RenderOnce for AiUpsellCard {
)
.child(
List::new()
.child(BulletItem::new("50 prompts with the Claude models"))
.child(BulletItem::new("50 prompts with Claude models"))
.child(BulletItem::new("2,000 accepted edit predictions")),
);
@ -132,22 +135,28 @@ impl RenderOnce for AiUpsellCard {
v_flex()
.relative()
.p_6()
.pt_4()
.p_4()
.pt_3()
.border_1()
.border_color(cx.theme().colors().border)
.rounded_lg()
.overflow_hidden()
.child(grid_bg)
.child(gradient_bg)
.child(Headline::new("Try Zed AI"))
.child(Label::new(DESCRIPTION).color(Color::Muted).mb_2())
.child(Label::new("Try Zed AI").size(LabelSize::Large))
.child(
div()
.max_w_3_4()
.mb_2()
.child(Label::new(DESCRIPTION).color(Color::Muted)),
)
.child(
h_flex()
.w_full()
.mt_1p5()
.mb_2p5()
.items_start()
.gap_12()
.gap_6()
.child(free_section)
.child(pro_section),
)
@ -183,6 +192,7 @@ impl Component for AiUpsellCard {
AiUpsellCard {
sign_in_status: SignInStatus::SignedOut,
sign_in: Arc::new(|_, _| {}),
user_plan: None,
}
.into_any_element(),
),
@ -191,6 +201,7 @@ impl Component for AiUpsellCard {
AiUpsellCard {
sign_in_status: SignInStatus::SignedIn,
sign_in: Arc::new(|_, _| {}),
user_plan: None,
}
.into_any_element(),
),

View file

@ -126,7 +126,7 @@ impl ChannelMembership {
proto::channel_member::Kind::Member => 0,
proto::channel_member::Kind::Invitee => 1,
},
username_order: self.user.github_login.as_str(),
username_order: &self.user.github_login,
}
}
}

View file

@ -259,20 +259,6 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
assert_channels(&channel_store, &[(0, "the-channel".to_string())], cx);
});
let get_users = server.receive::<proto::GetUsers>().await.unwrap();
assert_eq!(get_users.payload.user_ids, vec![5]);
server.respond(
get_users.receipt(),
proto::UsersResponse {
users: vec![proto::User {
id: 5,
github_login: "nathansobo".into(),
avatar_url: "http://avatar.com/nathansobo".into(),
name: None,
}],
},
);
// Join a channel and populate its existing messages.
let channel = channel_store.update(cx, |store, cx| {
let channel_id = store.ordered_channels().next().unwrap().1.id;
@ -334,7 +320,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
.map(|message| (message.sender.github_login.clone(), message.body.clone()))
.collect::<Vec<_>>(),
&[
("nathansobo".into(), "a".into()),
("user-5".into(), "a".into()),
("maxbrunsfeld".into(), "b".into())
]
);
@ -437,7 +423,7 @@ async fn test_channel_messages(cx: &mut TestAppContext) {
.map(|message| (message.sender.github_login.clone(), message.body.clone()))
.collect::<Vec<_>>(),
&[
("nathansobo".into(), "y".into()),
("user-5".into(), "y".into()),
("maxbrunsfeld".into(), "z".into())
]
);

View file

@ -17,11 +17,11 @@ test-support = ["clock/test-support", "collections/test-support", "gpui/test-sup
[dependencies]
anyhow.workspace = true
async-recursion = "0.3"
async-tungstenite = { workspace = true, features = ["tokio", "tokio-rustls-manual-roots"] }
base64.workspace = true
chrono = { workspace = true, features = ["serde"] }
clock.workspace = true
cloud_api_client.workspace = true
cloud_llm_client.workspace = true
collections.workspace = true
credentials_provider.workspace = true

View file

@ -6,22 +6,21 @@ pub mod telemetry;
pub mod user;
pub mod zed_urls;
use anyhow::{Context as _, Result, anyhow, bail};
use async_recursion::async_recursion;
use anyhow::{Context as _, Result, anyhow};
use async_tungstenite::tungstenite::{
client::IntoClientRequest,
error::Error as WebsocketError,
http::{HeaderValue, Request, StatusCode},
};
use chrono::{DateTime, Utc};
use clock::SystemClock;
use cloud_api_client::CloudApiClient;
use credentials_provider::CredentialsProvider;
use futures::{
AsyncReadExt, FutureExt, SinkExt, Stream, StreamExt, TryFutureExt as _, TryStreamExt,
channel::oneshot, future::BoxFuture,
};
use gpui::{App, AsyncApp, Entity, Global, Task, WeakEntity, actions};
use http_client::{AsyncBody, HttpClient, HttpClientWithUrl, http};
use http_client::{HttpClient, HttpClientWithUrl, http};
use parking_lot::RwLock;
use postage::watch;
use proxy::connect_proxy_stream;
@ -31,7 +30,6 @@ use rpc::proto::{AnyTypedEnvelope, EnvelopedMessage, PeerId, RequestMessage};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
use std::pin::Pin;
use std::{
any::TypeId,
convert::TryFrom,
@ -45,6 +43,7 @@ use std::{
},
time::{Duration, Instant},
};
use std::{cmp, pin::Pin};
use telemetry::Telemetry;
use thiserror::Error;
use tokio::net::TcpStream;
@ -78,7 +77,7 @@ pub static ZED_ALWAYS_ACTIVE: LazyLock<bool> =
LazyLock::new(|| std::env::var("ZED_ALWAYS_ACTIVE").map_or(false, |e| !e.is_empty()));
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(500);
pub const MAX_RECONNECTION_DELAY: Duration = Duration::from_secs(10);
pub const MAX_RECONNECTION_DELAY: Duration = Duration::from_secs(30);
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(20);
actions!(
@ -162,20 +161,8 @@ pub fn init(client: &Arc<Client>, cx: &mut App) {
let client = client.clone();
move |_: &SignIn, cx| {
if let Some(client) = client.upgrade() {
cx.spawn(
async move |cx| match client.authenticate_and_connect(true, &cx).await {
ConnectionResult::Timeout => {
log::error!("Initial authentication timed out");
}
ConnectionResult::ConnectionReset => {
log::error!("Initial authentication connection reset");
}
ConnectionResult::Result(r) => {
r.log_err();
}
},
)
.detach();
cx.spawn(async move |cx| client.sign_in_with_optional_connect(true, &cx).await)
.detach_and_log_err(cx);
}
}
});
@ -213,6 +200,7 @@ pub struct Client {
id: AtomicU64,
peer: Arc<Peer>,
http: Arc<HttpClientWithUrl>,
cloud_client: Arc<CloudApiClient>,
telemetry: Arc<Telemetry>,
credentials_provider: ClientCredentialsProvider,
state: RwLock<ClientState>,
@ -283,6 +271,8 @@ pub enum Status {
SignedOut,
UpgradeRequired,
Authenticating,
Authenticated,
AuthenticationError,
Connecting,
ConnectionError,
Connected {
@ -586,6 +576,7 @@ impl Client {
id: AtomicU64::new(0),
peer: Peer::new(0),
telemetry: Telemetry::new(clock, http.clone(), cx),
cloud_client: Arc::new(CloudApiClient::new(http.clone())),
http,
credentials_provider: ClientCredentialsProvider::new(cx),
state: Default::default(),
@ -618,6 +609,10 @@ impl Client {
self.http.clone()
}
pub fn cloud_client(&self) -> Arc<CloudApiClient> {
self.cloud_client.clone()
}
pub fn set_id(&self, id: u64) -> &Self {
self.id.store(id, Ordering::SeqCst);
self
@ -704,7 +699,7 @@ impl Client {
let mut delay = INITIAL_RECONNECTION_DELAY;
loop {
match client.authenticate_and_connect(true, &cx).await {
match client.connect(true, &cx).await {
ConnectionResult::Timeout => {
log::error!("client connect attempt timed out")
}
@ -727,11 +722,10 @@ impl Client {
},
&cx,
);
cx.background_executor().timer(delay).await;
delay = delay
.mul_f32(rng.gen_range(0.5..=2.5))
.max(INITIAL_RECONNECTION_DELAY)
.min(MAX_RECONNECTION_DELAY);
let jitter =
Duration::from_millis(rng.gen_range(0..delay.as_millis() as u64));
cx.background_executor().timer(delay + jitter).await;
delay = cmp::min(delay * 2, MAX_RECONNECTION_DELAY);
} else {
break;
}
@ -875,17 +869,122 @@ impl Client {
.is_some()
}
#[async_recursion(?Send)]
pub async fn authenticate_and_connect(
pub async fn sign_in(
self: &Arc<Self>,
try_provider: bool,
cx: &AsyncApp,
) -> Result<Credentials> {
if self.status().borrow().is_signed_out() {
self.set_status(Status::Authenticating, cx);
} else {
self.set_status(Status::Reauthenticating, cx);
}
let mut credentials = None;
let old_credentials = self.state.read().credentials.clone();
if let Some(old_credentials) = old_credentials {
self.cloud_client.set_credentials(
old_credentials.user_id as u32,
old_credentials.access_token.clone(),
);
// Fetch the authenticated user with the old credentials, to ensure they are still valid.
if self.cloud_client.get_authenticated_user().await.is_ok() {
credentials = Some(old_credentials);
}
}
if credentials.is_none() && try_provider {
if let Some(stored_credentials) = self.credentials_provider.read_credentials(cx).await {
self.cloud_client.set_credentials(
stored_credentials.user_id as u32,
stored_credentials.access_token.clone(),
);
// Fetch the authenticated user with the stored credentials, and
// clear them from the credentials provider if that fails.
if self.cloud_client.get_authenticated_user().await.is_ok() {
credentials = Some(stored_credentials);
} else {
self.credentials_provider
.delete_credentials(cx)
.await
.log_err();
}
}
}
if credentials.is_none() {
let mut status_rx = self.status();
let _ = status_rx.next().await;
futures::select_biased! {
authenticate = self.authenticate(cx).fuse() => {
match authenticate {
Ok(creds) => {
if IMPERSONATE_LOGIN.is_none() {
self.credentials_provider
.write_credentials(creds.user_id, creds.access_token.clone(), cx)
.await
.log_err();
}
credentials = Some(creds);
},
Err(err) => {
self.set_status(Status::AuthenticationError, cx);
return Err(err);
}
}
}
_ = status_rx.next().fuse() => {
return Err(anyhow!("authentication canceled"));
}
}
}
let credentials = credentials.unwrap();
self.set_id(credentials.user_id);
self.cloud_client
.set_credentials(credentials.user_id as u32, credentials.access_token.clone());
self.state.write().credentials = Some(credentials.clone());
self.set_status(Status::Authenticated, cx);
Ok(credentials)
}
/// Performs a sign-in and also connects to Collab.
///
/// This is called in places where we *don't* need to connect in the future. We will replace these calls with calls
/// to `sign_in` when we're ready to remove auto-connection to Collab.
pub async fn sign_in_with_optional_connect(
self: &Arc<Self>,
try_provider: bool,
cx: &AsyncApp,
) -> Result<()> {
let credentials = self.sign_in(try_provider, cx).await?;
let connect_result = match self.connect_with_credentials(credentials, cx).await {
ConnectionResult::Timeout => Err(anyhow!("connection timed out")),
ConnectionResult::ConnectionReset => Err(anyhow!("connection reset")),
ConnectionResult::Result(result) => result.context("client auth and connect"),
};
connect_result.log_err();
Ok(())
}
pub async fn connect(
self: &Arc<Self>,
try_provider: bool,
cx: &AsyncApp,
) -> ConnectionResult<()> {
let was_disconnected = match *self.status().borrow() {
Status::SignedOut => true,
Status::SignedOut | Status::Authenticated => true,
Status::ConnectionError
| Status::ConnectionLost
| Status::Authenticating { .. }
| Status::AuthenticationError
| Status::Reauthenticating { .. }
| Status::ReconnectionError { .. } => false,
Status::Connected { .. } | Status::Connecting { .. } | Status::Reconnecting { .. } => {
@ -898,39 +997,10 @@ impl Client {
);
}
};
if was_disconnected {
self.set_status(Status::Authenticating, cx);
} else {
self.set_status(Status::Reauthenticating, cx)
}
let mut read_from_provider = false;
let mut credentials = self.state.read().credentials.clone();
if credentials.is_none() && try_provider {
credentials = self.credentials_provider.read_credentials(cx).await;
read_from_provider = credentials.is_some();
}
if credentials.is_none() {
let mut status_rx = self.status();
let _ = status_rx.next().await;
futures::select_biased! {
authenticate = self.authenticate(cx).fuse() => {
match authenticate {
Ok(creds) => credentials = Some(creds),
Err(err) => {
self.set_status(Status::ConnectionError, cx);
return ConnectionResult::Result(Err(err));
}
}
}
_ = status_rx.next().fuse() => {
return ConnectionResult::Result(Err(anyhow!("authentication canceled")));
}
}
}
let credentials = credentials.unwrap();
self.set_id(credentials.user_id);
let credentials = match self.sign_in(try_provider, cx).await {
Ok(credentials) => credentials,
Err(err) => return ConnectionResult::Result(Err(err)),
};
if was_disconnected {
self.set_status(Status::Connecting, cx);
@ -938,17 +1008,20 @@ impl Client {
self.set_status(Status::Reconnecting, cx);
}
self.connect_with_credentials(credentials, cx).await
}
async fn connect_with_credentials(
self: &Arc<Self>,
credentials: Credentials,
cx: &AsyncApp,
) -> ConnectionResult<()> {
let mut timeout =
futures::FutureExt::fuse(cx.background_executor().timer(CONNECTION_TIMEOUT));
futures::select_biased! {
connection = self.establish_connection(&credentials, cx).fuse() => {
match connection {
Ok(conn) => {
self.state.write().credentials = Some(credentials.clone());
if !read_from_provider && IMPERSONATE_LOGIN.is_none() {
self.credentials_provider.write_credentials(credentials.user_id, credentials.access_token, cx).await.log_err();
}
futures::select_biased! {
result = self.set_connection(conn, cx).fuse() => {
match result.context("client auth and connect") {
@ -966,15 +1039,8 @@ impl Client {
}
}
Err(EstablishConnectionError::Unauthorized) => {
self.state.write().credentials.take();
if read_from_provider {
self.credentials_provider.delete_credentials(cx).await.log_err();
self.set_status(Status::SignedOut, cx);
self.authenticate_and_connect(false, cx).await
} else {
self.set_status(Status::ConnectionError, cx);
ConnectionResult::Result(Err(EstablishConnectionError::Unauthorized).context("client auth and connect"))
}
self.set_status(Status::ConnectionError, cx);
ConnectionResult::Result(Err(EstablishConnectionError::Unauthorized).context("client auth and connect"))
}
Err(EstablishConnectionError::UpgradeRequired) => {
self.set_status(Status::UpgradeRequired, cx);
@ -1369,96 +1435,31 @@ impl Client {
self: &Arc<Self>,
http: Arc<HttpClientWithUrl>,
login: String,
mut api_token: String,
api_token: String,
) -> Result<Credentials> {
#[derive(Deserialize)]
struct AuthenticatedUserResponse {
user: User,
#[derive(Serialize)]
struct ImpersonateUserBody {
github_login: String,
}
#[derive(Deserialize)]
struct User {
id: u64,
struct ImpersonateUserResponse {
user_id: u64,
access_token: String,
}
let github_user = {
#[derive(Deserialize)]
struct GithubUser {
id: i32,
login: String,
created_at: DateTime<Utc>,
}
let request = {
let mut request_builder =
Request::get(&format!("https://api.github.com/users/{login}"));
if let Ok(github_token) = std::env::var("GITHUB_TOKEN") {
request_builder =
request_builder.header("Authorization", format!("Bearer {}", github_token));
}
request_builder.body(AsyncBody::empty())?
};
let mut response = http
.send(request)
.await
.context("error fetching GitHub user")?;
let mut body = Vec::new();
response
.body_mut()
.read_to_end(&mut body)
.await
.context("error reading GitHub user")?;
if !response.status().is_success() {
let text = String::from_utf8_lossy(body.as_slice());
bail!(
"status error {}, response: {text:?}",
response.status().as_u16()
);
}
serde_json::from_slice::<GithubUser>(body.as_slice()).map_err(|err| {
log::error!("Error deserializing: {:?}", err);
log::error!(
"GitHub API response text: {:?}",
String::from_utf8_lossy(body.as_slice())
);
anyhow!("error deserializing GitHub user")
})?
};
let query_params = [
("github_login", &github_user.login),
("github_user_id", &github_user.id.to_string()),
(
"github_user_created_at",
&github_user.created_at.to_rfc3339(),
),
];
// Use the collab server's admin API to retrieve the ID
// of the impersonated user.
let mut url = self.rpc_url(http.clone(), None).await?;
url.set_path("/user");
url.set_query(Some(
&query_params
.iter()
.map(|(key, value)| {
format!(
"{}={}",
key,
url::form_urlencoded::byte_serialize(value.as_bytes()).collect::<String>()
)
})
.collect::<Vec<String>>()
.join("&"),
));
let request: http_client::Request<AsyncBody> = Request::get(url.as_str())
.header("Authorization", format!("token {api_token}"))
.body("".into())?;
let url = self
.http
.build_zed_cloud_url("/internal/users/impersonate", &[])?;
let request = Request::post(url.as_str())
.header("Content-Type", "application/json")
.header("Authorization", format!("Bearer {api_token}"))
.body(
serde_json::to_string(&ImpersonateUserBody {
github_login: login,
})?
.into(),
)?;
let mut response = http.send(request).await?;
let mut body = String::new();
@ -1469,18 +1470,17 @@ impl Client {
response.status().as_u16(),
body,
);
let response: AuthenticatedUserResponse = serde_json::from_str(&body)?;
let response: ImpersonateUserResponse = serde_json::from_str(&body)?;
// Use the admin API token to authenticate as the impersonated user.
api_token.insert_str(0, "ADMIN_TOKEN:");
Ok(Credentials {
user_id: response.user.id,
access_token: api_token,
user_id: response.user_id,
access_token: response.access_token,
})
}
pub async fn sign_out(self: &Arc<Self>, cx: &AsyncApp) {
self.state.write().credentials = None;
self.cloud_client.clear_credentials();
self.disconnect(cx);
if self.has_credentials(cx).await {
@ -1790,7 +1790,7 @@ mod tests {
});
let auth_and_connect = cx.spawn({
let client = client.clone();
|cx| async move { client.authenticate_and_connect(false, &cx).await }
|cx| async move { client.connect(false, &cx).await }
});
executor.run_until_parked();
assert!(matches!(status.next().await, Some(Status::Connecting)));
@ -1867,7 +1867,7 @@ mod tests {
let _authenticate = cx.spawn({
let client = client.clone();
move |cx| async move { client.authenticate_and_connect(false, &cx).await }
move |cx| async move { client.connect(false, &cx).await }
});
executor.run_until_parked();
assert_eq!(*auth_count.lock(), 1);
@ -1875,7 +1875,7 @@ mod tests {
let _authenticate = cx.spawn({
let client = client.clone();
|cx| async move { client.authenticate_and_connect(false, &cx).await }
|cx| async move { client.connect(false, &cx).await }
});
executor.run_until_parked();
assert_eq!(*auth_count.lock(), 2);

View file

@ -1,8 +1,11 @@
use crate::{Client, Connection, Credentials, EstablishConnectionError, UserStore};
use anyhow::{Context as _, Result, anyhow};
use chrono::Duration;
use cloud_api_client::{AuthenticatedUser, GetAuthenticatedUserResponse, PlanInfo};
use cloud_llm_client::{CurrentUsage, Plan, UsageData, UsageLimit};
use futures::{StreamExt, stream::BoxStream};
use gpui::{AppContext as _, BackgroundExecutor, Entity, TestAppContext};
use http_client::{AsyncBody, Method, Request, http};
use parking_lot::Mutex;
use rpc::{
ConnectionId, Peer, Receipt, TypedEnvelope,
@ -39,6 +42,44 @@ impl FakeServer {
executor: cx.executor(),
};
client.http_client().as_fake().replace_handler({
let state = server.state.clone();
move |old_handler, req| {
let state = state.clone();
let old_handler = old_handler.clone();
async move {
match (req.method(), req.uri().path()) {
(&Method::GET, "/client/users/me") => {
let credentials = parse_authorization_header(&req);
if credentials
!= Some(Credentials {
user_id: client_user_id,
access_token: state.lock().access_token.to_string(),
})
{
return Ok(http_client::Response::builder()
.status(401)
.body("Unauthorized".into())
.unwrap());
}
Ok(http_client::Response::builder()
.status(200)
.body(
serde_json::to_string(&make_get_authenticated_user_response(
client_user_id as i32,
format!("user-{client_user_id}"),
))
.unwrap()
.into(),
)
.unwrap())
}
_ => old_handler(req).await,
}
}
}
});
client
.override_authenticate({
let state = Arc::downgrade(&server.state);
@ -105,7 +146,7 @@ impl FakeServer {
});
client
.authenticate_and_connect(false, &cx.to_async())
.connect(false, &cx.to_async())
.await
.into_response()
.unwrap();
@ -223,3 +264,54 @@ impl Drop for FakeServer {
self.disconnect();
}
}
pub fn parse_authorization_header(req: &Request<AsyncBody>) -> Option<Credentials> {
let mut auth_header = req
.headers()
.get(http::header::AUTHORIZATION)?
.to_str()
.ok()?
.split_whitespace();
let user_id = auth_header.next()?.parse().ok()?;
let access_token = auth_header.next()?;
Some(Credentials {
user_id,
access_token: access_token.to_string(),
})
}
pub fn make_get_authenticated_user_response(
user_id: i32,
github_login: String,
) -> GetAuthenticatedUserResponse {
GetAuthenticatedUserResponse {
user: AuthenticatedUser {
id: user_id,
metrics_id: format!("metrics-id-{user_id}"),
avatar_url: "".to_string(),
github_login,
name: None,
is_staff: false,
accepted_tos_at: None,
},
feature_flags: vec![],
plan: PlanInfo {
plan: Plan::ZedPro,
subscription_period: None,
usage: CurrentUsage {
model_requests: UsageData {
used: 0,
limit: UsageLimit::Limited(500),
},
edit_predictions: UsageData {
used: 250,
limit: UsageLimit::Unlimited,
},
},
trial_started_at: None,
is_usage_based_billing_enabled: false,
is_account_too_young: false,
has_overdue_invoices: false,
},
}
}

View file

@ -1,6 +1,7 @@
use super::{Client, Status, TypedEnvelope, proto};
use anyhow::{Context as _, Result, anyhow};
use chrono::{DateTime, Utc};
use cloud_api_client::{GetAuthenticatedUserResponse, PlanInfo};
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,
@ -20,7 +21,7 @@ use std::{
sync::{Arc, Weak},
};
use text::ReplicaId;
use util::{TryFutureExt as _, maybe};
use util::{ResultExt, TryFutureExt as _};
pub type UserId = u64;
@ -55,7 +56,7 @@ pub struct ParticipantIndex(pub u32);
#[derive(Default, Debug)]
pub struct User {
pub id: UserId,
pub github_login: String,
pub github_login: SharedString,
pub avatar_uri: SharedUri,
pub name: Option<String>,
}
@ -107,19 +108,14 @@ pub enum ContactRequestStatus {
pub struct UserStore {
users: HashMap<u64, Arc<User>>,
by_github_login: HashMap<String, u64>,
by_github_login: HashMap<SharedString, u64>,
participant_indices: HashMap<u64, ParticipantIndex>,
update_contacts_tx: mpsc::UnboundedSender<UpdateContacts>,
current_plan: Option<proto::Plan>,
subscription_period: Option<(DateTime<Utc>, DateTime<Utc>)>,
trial_started_at: Option<DateTime<Utc>>,
model_request_usage: Option<ModelRequestUsage>,
edit_prediction_usage: Option<EditPredictionUsage>,
is_usage_based_billing_enabled: Option<bool>,
account_too_young: Option<bool>,
has_overdue_invoices: Option<bool>,
plan_info: Option<PlanInfo>,
current_user: watch::Receiver<Option<Arc<User>>>,
accepted_tos_at: Option<Option<DateTime<Utc>>>,
accepted_tos_at: Option<Option<cloud_api_client::Timestamp>>,
contacts: Vec<Arc<Contact>>,
incoming_contact_requests: Vec<Arc<User>>,
outgoing_contact_requests: Vec<Arc<User>>,
@ -145,6 +141,7 @@ pub enum Event {
ShowContacts,
ParticipantIndicesChanged,
PrivateUserInfoUpdated,
PlanUpdated,
}
#[derive(Clone, Copy)]
@ -188,14 +185,9 @@ impl UserStore {
users: Default::default(),
by_github_login: Default::default(),
current_user: current_user_rx,
current_plan: None,
subscription_period: None,
trial_started_at: None,
plan_info: None,
model_request_usage: None,
edit_prediction_usage: None,
is_usage_based_billing_enabled: None,
account_too_young: None,
has_overdue_invoices: None,
accepted_tos_at: None,
contacts: Default::default(),
incoming_contact_requests: Default::default(),
@ -225,53 +217,30 @@ impl UserStore {
return Ok(());
};
match status {
Status::Connected { .. } => {
Status::Authenticated | Status::Connected { .. } => {
if let Some(user_id) = client.user_id() {
let fetch_user = if let Ok(fetch_user) =
this.update(cx, |this, cx| this.get_user(user_id, cx).log_err())
{
fetch_user
} else {
break;
};
let fetch_private_user_info =
client.request(proto::GetPrivateUserInfo {}).log_err();
let (user, info) =
futures::join!(fetch_user, fetch_private_user_info);
let response = client.cloud_client().get_authenticated_user().await;
let mut current_user = None;
cx.update(|cx| {
if let Some(info) = info {
let staff =
info.staff && !*feature_flags::ZED_DISABLE_STAFF;
cx.update_flags(staff, info.flags);
client.telemetry.set_authenticated_user_info(
Some(info.metrics_id.clone()),
staff,
);
if let Some(response) = response.log_err() {
let user = Arc::new(User {
id: user_id,
github_login: response.user.github_login.clone().into(),
avatar_uri: response.user.avatar_url.clone().into(),
name: response.user.name.clone(),
});
current_user = Some(user.clone());
this.update(cx, |this, cx| {
let accepted_tos_at = {
#[cfg(debug_assertions)]
if std::env::var("ZED_IGNORE_ACCEPTED_TOS").is_ok()
{
None
} else {
info.accepted_tos_at
}
#[cfg(not(debug_assertions))]
info.accepted_tos_at
};
this.set_current_user_accepted_tos_at(accepted_tos_at);
cx.emit(Event::PrivateUserInfoUpdated);
this.by_github_login
.insert(user.github_login.clone(), user_id);
this.users.insert(user_id, user);
this.update_authenticated_user(response, cx)
})
} else {
anyhow::Ok(())
}
})??;
current_user_tx.send(user).await.ok();
current_user_tx.send(current_user).await.ok();
this.update(cx, |_, cx| cx.notify())?;
}
@ -352,59 +321,22 @@ impl UserStore {
async fn handle_update_plan(
this: Entity<Self>,
message: TypedEnvelope<proto::UpdateUserPlan>,
_message: TypedEnvelope<proto::UpdateUserPlan>,
mut cx: AsyncApp,
) -> Result<()> {
let client = this
.read_with(&cx, |this, _| this.client.upgrade())?
.context("client was dropped")?;
let response = client
.cloud_client()
.get_authenticated_user()
.await
.context("failed to fetch authenticated user")?;
this.update(&mut cx, |this, cx| {
this.current_plan = Some(message.payload.plan());
this.subscription_period = maybe!({
let period = message.payload.subscription_period?;
let started_at = DateTime::from_timestamp(period.started_at as i64, 0)?;
let ended_at = DateTime::from_timestamp(period.ended_at as i64, 0)?;
Some((started_at, ended_at))
});
this.trial_started_at = message
.payload
.trial_started_at
.and_then(|trial_started_at| DateTime::from_timestamp(trial_started_at as i64, 0));
this.is_usage_based_billing_enabled = message.payload.is_usage_based_billing_enabled;
this.account_too_young = message.payload.account_too_young;
this.has_overdue_invoices = message.payload.has_overdue_invoices;
if let Some(usage) = message.payload.usage {
// limits are always present even though they are wrapped in Option
this.model_request_usage = usage
.model_requests_usage_limit
.and_then(|limit| {
RequestUsage::from_proto(usage.model_requests_usage_amount, limit)
})
.map(ModelRequestUsage);
this.edit_prediction_usage = usage
.edit_predictions_usage_limit
.and_then(|limit| {
RequestUsage::from_proto(usage.model_requests_usage_amount, limit)
})
.map(EditPredictionUsage);
}
cx.notify();
})?;
Ok(())
}
pub fn update_model_request_usage(&mut self, usage: ModelRequestUsage, cx: &mut Context<Self>) {
self.model_request_usage = Some(usage);
cx.notify();
}
pub fn update_edit_prediction_usage(
&mut self,
usage: EditPredictionUsage,
cx: &mut Context<Self>,
) {
self.edit_prediction_usage = Some(usage);
cx.notify();
this.update_authenticated_user(response, cx);
})
}
fn update_contacts(&mut self, message: UpdateContacts, cx: &Context<Self>) -> Task<Result<()>> {
@ -763,59 +695,131 @@ impl UserStore {
self.current_user.borrow().clone()
}
pub fn current_plan(&self) -> Option<proto::Plan> {
pub fn plan(&self) -> Option<cloud_llm_client::Plan> {
#[cfg(debug_assertions)]
if let Ok(plan) = std::env::var("ZED_SIMULATE_PLAN").as_ref() {
return match plan.as_str() {
"free" => Some(proto::Plan::Free),
"trial" => Some(proto::Plan::ZedProTrial),
"pro" => Some(proto::Plan::ZedPro),
"free" => Some(cloud_llm_client::Plan::ZedFree),
"trial" => Some(cloud_llm_client::Plan::ZedProTrial),
"pro" => Some(cloud_llm_client::Plan::ZedPro),
_ => {
panic!("ZED_SIMULATE_PLAN must be one of 'free', 'trial', or 'pro'");
}
};
}
self.current_plan
self.plan_info.as_ref().map(|info| info.plan)
}
pub fn subscription_period(&self) -> Option<(DateTime<Utc>, DateTime<Utc>)> {
self.subscription_period
self.plan_info
.as_ref()
.and_then(|plan| plan.subscription_period)
.map(|subscription_period| {
(
subscription_period.started_at.0,
subscription_period.ended_at.0,
)
})
}
pub fn trial_started_at(&self) -> Option<DateTime<Utc>> {
self.trial_started_at
self.plan_info
.as_ref()
.and_then(|plan| plan.trial_started_at)
.map(|trial_started_at| trial_started_at.0)
}
pub fn usage_based_billing_enabled(&self) -> Option<bool> {
self.is_usage_based_billing_enabled
/// Returns whether the user's account is too new to use the service.
pub fn account_too_young(&self) -> bool {
self.plan_info
.as_ref()
.map(|plan| plan.is_account_too_young)
.unwrap_or_default()
}
/// Returns whether the current user has overdue invoices and usage should be blocked.
pub fn has_overdue_invoices(&self) -> bool {
self.plan_info
.as_ref()
.map(|plan| plan.has_overdue_invoices)
.unwrap_or_default()
}
pub fn is_usage_based_billing_enabled(&self) -> bool {
self.plan_info
.as_ref()
.map(|plan| plan.is_usage_based_billing_enabled)
.unwrap_or_default()
}
pub fn model_request_usage(&self) -> Option<ModelRequestUsage> {
self.model_request_usage
}
pub fn update_model_request_usage(&mut self, usage: ModelRequestUsage, cx: &mut Context<Self>) {
self.model_request_usage = Some(usage);
cx.notify();
}
pub fn edit_prediction_usage(&self) -> Option<EditPredictionUsage> {
self.edit_prediction_usage
}
pub fn update_edit_prediction_usage(
&mut self,
usage: EditPredictionUsage,
cx: &mut Context<Self>,
) {
self.edit_prediction_usage = Some(usage);
cx.notify();
}
fn update_authenticated_user(
&mut self,
response: GetAuthenticatedUserResponse,
cx: &mut Context<Self>,
) {
let staff = response.user.is_staff && !*feature_flags::ZED_DISABLE_STAFF;
cx.update_flags(staff, response.feature_flags);
if let Some(client) = self.client.upgrade() {
client
.telemetry
.set_authenticated_user_info(Some(response.user.metrics_id.clone()), staff);
}
let accepted_tos_at = {
#[cfg(debug_assertions)]
if std::env::var("ZED_IGNORE_ACCEPTED_TOS").is_ok() {
None
} else {
response.user.accepted_tos_at
}
#[cfg(not(debug_assertions))]
response.user.accepted_tos_at
};
self.accepted_tos_at = Some(accepted_tos_at);
self.model_request_usage = Some(ModelRequestUsage(RequestUsage {
limit: response.plan.usage.model_requests.limit,
amount: response.plan.usage.model_requests.used as i32,
}));
self.edit_prediction_usage = Some(EditPredictionUsage(RequestUsage {
limit: response.plan.usage.edit_predictions.limit,
amount: response.plan.usage.edit_predictions.used as i32,
}));
self.plan_info = Some(response.plan);
cx.emit(Event::PrivateUserInfoUpdated);
}
pub fn watch_current_user(&self) -> watch::Receiver<Option<Arc<User>>> {
self.current_user.clone()
}
/// Returns whether the user's account is too new to use the service.
pub fn account_too_young(&self) -> bool {
self.account_too_young.unwrap_or(false)
}
/// Returns whether the current user has overdue invoices and usage should be blocked.
pub fn has_overdue_invoices(&self) -> bool {
self.has_overdue_invoices.unwrap_or(false)
}
pub fn current_user_has_accepted_terms(&self) -> Option<bool> {
pub fn has_accepted_terms_of_service(&self) -> bool {
self.accepted_tos_at
.map(|accepted_tos_at| accepted_tos_at.is_some())
.map_or(false, |accepted_tos_at| accepted_tos_at.is_some())
}
pub fn accept_terms_of_service(&self, cx: &Context<Self>) -> Task<Result<()>> {
@ -827,23 +831,18 @@ impl UserStore {
cx.spawn(async move |this, cx| -> anyhow::Result<()> {
let client = client.upgrade().context("client not found")?;
let response = client
.request(proto::AcceptTermsOfService {})
.cloud_client()
.accept_terms_of_service()
.await
.context("error accepting tos")?;
this.update(cx, |this, cx| {
this.set_current_user_accepted_tos_at(Some(response.accepted_tos_at));
this.accepted_tos_at = Some(response.user.accepted_tos_at);
cx.emit(Event::PrivateUserInfoUpdated);
})?;
Ok(())
})
}
fn set_current_user_accepted_tos_at(&mut self, accepted_tos_at: Option<u64>) {
self.accepted_tos_at = Some(
accepted_tos_at.and_then(|timestamp| DateTime::from_timestamp(timestamp as i64, 0)),
);
}
fn load_users(
&self,
request: impl RequestMessage<Response = UsersResponse>,
@ -902,7 +901,7 @@ impl UserStore {
let mut missing_user_ids = Vec::new();
for id in user_ids {
if let Some(github_login) = self.get_cached_user(id).map(|u| u.github_login.clone()) {
ret.insert(id, github_login.into());
ret.insert(id, github_login);
} else {
missing_user_ids.push(id)
}
@ -923,7 +922,7 @@ impl User {
fn new(message: proto::User) -> Arc<Self> {
Arc::new(User {
id: message.id,
github_login: message.github_login,
github_login: message.github_login.into(),
avatar_uri: message.avatar_url.into(),
name: message.name,
})

View file

@ -0,0 +1,21 @@
[package]
name = "cloud_api_client"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "Apache-2.0"
[lints]
workspace = true
[lib]
path = "src/cloud_api_client.rs"
[dependencies]
anyhow.workspace = true
cloud_api_types.workspace = true
futures.workspace = true
http_client.workspace = true
parking_lot.workspace = true
serde_json.workspace = true
workspace-hack.workspace = true

View file

@ -0,0 +1 @@
../../LICENSE-APACHE

View file

@ -0,0 +1,155 @@
use std::sync::Arc;
use anyhow::{Result, anyhow};
pub use cloud_api_types::*;
use futures::AsyncReadExt as _;
use http_client::http::request;
use http_client::{AsyncBody, HttpClientWithUrl, Method, Request};
use parking_lot::RwLock;
struct Credentials {
user_id: u32,
access_token: String,
}
pub struct CloudApiClient {
credentials: RwLock<Option<Credentials>>,
http_client: Arc<HttpClientWithUrl>,
}
impl CloudApiClient {
pub fn new(http_client: Arc<HttpClientWithUrl>) -> Self {
Self {
credentials: RwLock::new(None),
http_client,
}
}
pub fn has_credentials(&self) -> bool {
self.credentials.read().is_some()
}
pub fn set_credentials(&self, user_id: u32, access_token: String) {
*self.credentials.write() = Some(Credentials {
user_id,
access_token,
});
}
pub fn clear_credentials(&self) {
*self.credentials.write() = None;
}
fn authorization_header(&self) -> Result<String> {
let guard = self.credentials.read();
let credentials = guard
.as_ref()
.ok_or_else(|| anyhow!("No credentials provided"))?;
Ok(format!(
"{} {}",
credentials.user_id, credentials.access_token
))
}
fn build_request(
&self,
req: request::Builder,
body: impl Into<AsyncBody>,
) -> Result<Request<AsyncBody>> {
Ok(req
.header("Content-Type", "application/json")
.header("Authorization", self.authorization_header()?)
.body(body.into())?)
}
pub async fn get_authenticated_user(&self) -> Result<GetAuthenticatedUserResponse> {
let request = self.build_request(
Request::builder().method(Method::GET).uri(
self.http_client
.build_zed_cloud_url("/client/users/me", &[])?
.as_ref(),
),
AsyncBody::default(),
)?;
let mut response = self.http_client.send(request).await?;
if !response.status().is_success() {
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
anyhow::bail!(
"Failed to get authenticated user.\nStatus: {:?}\nBody: {body}",
response.status()
)
}
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
Ok(serde_json::from_str(&body)?)
}
pub async fn accept_terms_of_service(&self) -> Result<AcceptTermsOfServiceResponse> {
let request = self.build_request(
Request::builder().method(Method::POST).uri(
self.http_client
.build_zed_cloud_url("/client/terms_of_service/accept", &[])?
.as_ref(),
),
AsyncBody::default(),
)?;
let mut response = self.http_client.send(request).await?;
if !response.status().is_success() {
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
anyhow::bail!(
"Failed to accept terms of service.\nStatus: {:?}\nBody: {body}",
response.status()
)
}
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
Ok(serde_json::from_str(&body)?)
}
pub async fn create_llm_token(
&self,
system_id: Option<String>,
) -> Result<CreateLlmTokenResponse> {
let mut request_builder = Request::builder().method(Method::POST).uri(
self.http_client
.build_zed_cloud_url("/client/llm_tokens", &[])?
.as_ref(),
);
if let Some(system_id) = system_id {
request_builder = request_builder.header(ZED_SYSTEM_ID_HEADER_NAME, system_id);
}
let request = self.build_request(request_builder, AsyncBody::default())?;
let mut response = self.http_client.send(request).await?;
if !response.status().is_success() {
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
anyhow::bail!(
"Failed to create LLM token.\nStatus: {:?}\nBody: {body}",
response.status()
)
}
let mut body = String::new();
response.body_mut().read_to_string(&mut body).await?;
Ok(serde_json::from_str(&body)?)
}
}

View file

@ -0,0 +1,22 @@
[package]
name = "cloud_api_types"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "Apache-2.0"
[lints]
workspace = true
[lib]
path = "src/cloud_api_types.rs"
[dependencies]
chrono.workspace = true
cloud_llm_client.workspace = true
serde.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
pretty_assertions.workspace = true
serde_json.workspace = true

View file

@ -0,0 +1 @@
../../LICENSE-APACHE

View file

@ -0,0 +1,55 @@
mod timestamp;
use serde::{Deserialize, Serialize};
pub use crate::timestamp::Timestamp;
pub const ZED_SYSTEM_ID_HEADER_NAME: &str = "x-zed-system-id";
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct GetAuthenticatedUserResponse {
pub user: AuthenticatedUser,
pub feature_flags: Vec<String>,
pub plan: PlanInfo,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct AuthenticatedUser {
pub id: i32,
pub metrics_id: String,
pub avatar_url: String,
pub github_login: String,
pub name: Option<String>,
pub is_staff: bool,
pub accepted_tos_at: Option<Timestamp>,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct PlanInfo {
pub plan: cloud_llm_client::Plan,
pub subscription_period: Option<SubscriptionPeriod>,
pub usage: cloud_llm_client::CurrentUsage,
pub trial_started_at: Option<Timestamp>,
pub is_usage_based_billing_enabled: bool,
pub is_account_too_young: bool,
pub has_overdue_invoices: bool,
}
#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize)]
pub struct SubscriptionPeriod {
pub started_at: Timestamp,
pub ended_at: Timestamp,
}
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct AcceptTermsOfServiceResponse {
pub user: AuthenticatedUser,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct LlmToken(pub String);
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct CreateLlmTokenResponse {
pub token: LlmToken,
}

View file

@ -0,0 +1,166 @@
use chrono::{DateTime, NaiveDateTime, SecondsFormat, Utc};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
/// A timestamp with a serialized representation in RFC 3339 format.
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub struct Timestamp(pub DateTime<Utc>);
impl Timestamp {
pub fn new(datetime: DateTime<Utc>) -> Self {
Self(datetime)
}
}
impl From<DateTime<Utc>> for Timestamp {
fn from(value: DateTime<Utc>) -> Self {
Self(value)
}
}
impl From<NaiveDateTime> for Timestamp {
fn from(value: NaiveDateTime) -> Self {
Self(value.and_utc())
}
}
impl Serialize for Timestamp {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let rfc3339_string = self.0.to_rfc3339_opts(SecondsFormat::Millis, true);
serializer.serialize_str(&rfc3339_string)
}
}
impl<'de> Deserialize<'de> for Timestamp {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
let datetime = DateTime::parse_from_rfc3339(&value)
.map_err(serde::de::Error::custom)?
.to_utc();
Ok(Self(datetime))
}
}
#[cfg(test)]
mod tests {
use chrono::NaiveDate;
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_timestamp_serialization() {
let datetime = DateTime::parse_from_rfc3339("2023-12-25T14:30:45.123Z")
.unwrap()
.to_utc();
let timestamp = Timestamp::new(datetime);
let json = serde_json::to_string(&timestamp).unwrap();
assert_eq!(json, "\"2023-12-25T14:30:45.123Z\"");
}
#[test]
fn test_timestamp_deserialization() {
let json = "\"2023-12-25T14:30:45.123Z\"";
let timestamp: Timestamp = serde_json::from_str(json).unwrap();
let expected = DateTime::parse_from_rfc3339("2023-12-25T14:30:45.123Z")
.unwrap()
.to_utc();
assert_eq!(timestamp.0, expected);
}
#[test]
fn test_timestamp_roundtrip() {
let original = DateTime::parse_from_rfc3339("2023-12-25T14:30:45.123Z")
.unwrap()
.to_utc();
let timestamp = Timestamp::new(original);
let json = serde_json::to_string(&timestamp).unwrap();
let deserialized: Timestamp = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.0, original);
}
#[test]
fn test_timestamp_from_datetime_utc() {
let datetime = DateTime::parse_from_rfc3339("2023-12-25T14:30:45.123Z")
.unwrap()
.to_utc();
let timestamp = Timestamp::from(datetime);
assert_eq!(timestamp.0, datetime);
}
#[test]
fn test_timestamp_from_naive_datetime() {
let naive_dt = NaiveDate::from_ymd_opt(2023, 12, 25)
.unwrap()
.and_hms_milli_opt(14, 30, 45, 123)
.unwrap();
let timestamp = Timestamp::from(naive_dt);
let expected = naive_dt.and_utc();
assert_eq!(timestamp.0, expected);
}
#[test]
fn test_timestamp_serialization_with_microseconds() {
// Test that microseconds are truncated to milliseconds
let datetime = NaiveDate::from_ymd_opt(2023, 12, 25)
.unwrap()
.and_hms_micro_opt(14, 30, 45, 123456)
.unwrap()
.and_utc();
let timestamp = Timestamp::new(datetime);
let json = serde_json::to_string(&timestamp).unwrap();
// Should be truncated to milliseconds
assert_eq!(json, "\"2023-12-25T14:30:45.123Z\"");
}
#[test]
fn test_timestamp_deserialization_without_milliseconds() {
let json = "\"2023-12-25T14:30:45Z\"";
let timestamp: Timestamp = serde_json::from_str(json).unwrap();
let expected = NaiveDate::from_ymd_opt(2023, 12, 25)
.unwrap()
.and_hms_opt(14, 30, 45)
.unwrap()
.and_utc();
assert_eq!(timestamp.0, expected);
}
#[test]
fn test_timestamp_deserialization_with_timezone() {
let json = "\"2023-12-25T14:30:45.123+05:30\"";
let timestamp: Timestamp = serde_json::from_str(json).unwrap();
// Should be converted to UTC
let expected = NaiveDate::from_ymd_opt(2023, 12, 25)
.unwrap()
.and_hms_milli_opt(9, 0, 45, 123) // 14:30:45 + 5:30 = 20:00:45, but we want UTC so subtract 5:30
.unwrap()
.and_utc();
assert_eq!(timestamp.0, expected);
}
#[test]
fn test_timestamp_deserialization_with_invalid_format() {
let json = "\"invalid-date\"";
let result: Result<Timestamp, _> = serde_json::from_str(json);
assert!(result.is_err());
}
}

View file

@ -308,13 +308,13 @@ pub struct GetSubscriptionResponse {
pub usage: Option<CurrentUsage>,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct CurrentUsage {
pub model_requests: UsageData,
pub edit_predictions: UsageData,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct UsageData {
pub used: u32,
pub limit: UsageLimit,

View file

@ -42,7 +42,7 @@ use collections::{HashMap, HashSet};
pub use connection_pool::{ConnectionPool, ZedVersion};
use core::fmt::{self, Debug, Formatter};
use reqwest_client::ReqwestClient;
use rpc::proto::split_repository_update;
use rpc::proto::{MultiLspQuery, split_repository_update};
use supermaven_api::{CreateExternalUserRequest, SupermavenAdminApi};
use futures::{
@ -374,7 +374,7 @@ impl Server {
.add_request_handler(forward_mutating_project_request::<proto::OnTypeFormatting>)
.add_request_handler(forward_mutating_project_request::<proto::SaveBuffer>)
.add_request_handler(forward_mutating_project_request::<proto::BlameBuffer>)
.add_request_handler(forward_mutating_project_request::<proto::MultiLspQuery>)
.add_request_handler(multi_lsp_query)
.add_request_handler(forward_mutating_project_request::<proto::RestartLanguageServers>)
.add_request_handler(forward_mutating_project_request::<proto::StopLanguageServers>)
.add_request_handler(forward_mutating_project_request::<proto::LinkedEditingRange>)
@ -838,7 +838,7 @@ impl Server {
// This arrangement ensures we will attempt to process earlier messages first, but fall
// back to processing messages arrived later in the spirit of making progress.
let mut foreground_message_handlers = FuturesUnordered::new();
let concurrent_handlers = Arc::new(Semaphore::new(512));
let concurrent_handlers = Arc::new(Semaphore::new(256));
loop {
let next_message = async {
let permit = concurrent_handlers.clone().acquire_owned().await.unwrap();
@ -865,6 +865,7 @@ impl Server {
user_id=field::Empty,
login=field::Empty,
impersonator=field::Empty,
multi_lsp_query_request=field::Empty,
);
principal.update_span(&span);
let span_enter = span.enter();
@ -2329,6 +2330,15 @@ where
Ok(())
}
async fn multi_lsp_query(
request: MultiLspQuery,
response: Response<MultiLspQuery>,
session: Session,
) -> Result<()> {
tracing::Span::current().record("multi_lsp_query_request", request.request_str());
forward_mutating_project_request(request, response, session).await
}
/// Notify other participants that a new buffer has been created
async fn create_buffer_for_peer(
request: proto::CreateBufferForPeer,

View file

@ -38,12 +38,12 @@ fn room_participants(room: &Entity<Room>, cx: &mut TestAppContext) -> RoomPartic
let mut remote = room
.remote_participants()
.values()
.map(|participant| participant.user.github_login.clone())
.map(|participant| participant.user.github_login.clone().to_string())
.collect::<Vec<_>>();
let mut pending = room
.pending_participants()
.iter()
.map(|user| user.github_login.clone())
.map(|user| user.github_login.clone().to_string())
.collect::<Vec<_>>();
remote.sort();
pending.sort();

View file

@ -842,7 +842,7 @@ async fn test_client_disconnecting_from_room(
// Allow user A to reconnect to the server.
server.allow_connections();
executor.advance_clock(RECEIVE_TIMEOUT);
executor.advance_clock(RECONNECT_TIMEOUT);
// Call user B again from client A.
active_call_a
@ -1286,7 +1286,7 @@ async fn test_calls_on_multiple_connections(
client_b1.disconnect(&cx_b1.to_async());
executor.advance_clock(RECEIVE_TIMEOUT);
client_b1
.authenticate_and_connect(false, &cx_b1.to_async())
.connect(false, &cx_b1.to_async())
.await
.into_response()
.unwrap();
@ -1358,7 +1358,7 @@ async fn test_calls_on_multiple_connections(
// User A reconnects automatically, then calls user B again.
server.allow_connections();
executor.advance_clock(RECEIVE_TIMEOUT);
executor.advance_clock(RECONNECT_TIMEOUT);
active_call_a
.update(cx_a, |call, cx| {
call.invite(client_b1.user_id().unwrap(), None, cx)
@ -1667,7 +1667,7 @@ async fn test_project_reconnect(
// Client A reconnects. Their project is re-shared, and client B re-joins it.
server.allow_connections();
client_a
.authenticate_and_connect(false, &cx_a.to_async())
.connect(false, &cx_a.to_async())
.await
.into_response()
.unwrap();
@ -1796,7 +1796,7 @@ async fn test_project_reconnect(
// Client B reconnects. They re-join the room and the remaining shared project.
server.allow_connections();
client_b
.authenticate_and_connect(false, &cx_b.to_async())
.connect(false, &cx_b.to_async())
.await
.into_response()
.unwrap();
@ -1881,7 +1881,7 @@ async fn test_active_call_events(
vec![room::Event::RemoteProjectShared {
owner: Arc::new(User {
id: client_a.user_id().unwrap(),
github_login: "user_a".to_string(),
github_login: "user_a".into(),
avatar_uri: "avatar_a".into(),
name: None,
}),
@ -1900,7 +1900,7 @@ async fn test_active_call_events(
vec![room::Event::RemoteProjectShared {
owner: Arc::new(User {
id: client_b.user_id().unwrap(),
github_login: "user_b".to_string(),
github_login: "user_b".into(),
avatar_uri: "avatar_b".into(),
name: None,
}),
@ -5738,7 +5738,7 @@ async fn test_contacts(
server.allow_connections();
client_c
.authenticate_and_connect(false, &cx_c.to_async())
.connect(false, &cx_c.to_async())
.await
.into_response()
.unwrap();
@ -6079,7 +6079,7 @@ async fn test_contacts(
.iter()
.map(|contact| {
(
contact.user.github_login.clone(),
contact.user.github_login.clone().to_string(),
if contact.online { "online" } else { "offline" },
if contact.busy { "busy" } else { "free" },
)
@ -6269,7 +6269,7 @@ async fn test_contact_requests(
client.disconnect(&cx.to_async());
client.clear_contacts(cx).await;
client
.authenticate_and_connect(false, &cx.to_async())
.connect(false, &cx.to_async())
.await
.into_response()
.unwrap();

View file

@ -3,6 +3,7 @@ use std::sync::Arc;
use gpui::{BackgroundExecutor, TestAppContext};
use notifications::NotificationEvent;
use parking_lot::Mutex;
use pretty_assertions::assert_eq;
use rpc::{Notification, proto};
use crate::tests::TestServer;
@ -17,6 +18,9 @@ async fn test_notifications(
let client_a = server.create_client(cx_a, "user_a").await;
let client_b = server.create_client(cx_b, "user_b").await;
// Wait for authentication/connection to Collab to be established.
executor.run_until_parked();
let notification_events_a = Arc::new(Mutex::new(Vec::new()));
let notification_events_b = Arc::new(Mutex::new(Vec::new()));
client_a.notification_store().update(cx_a, |_, cx| {

View file

@ -8,6 +8,7 @@ use crate::{
use anyhow::anyhow;
use call::ActiveCall;
use channel::{ChannelBuffer, ChannelStore};
use client::test::{make_get_authenticated_user_response, parse_authorization_header};
use client::{
self, ChannelId, Client, Connection, Credentials, EstablishConnectionError, UserStore,
proto::PeerId,
@ -20,7 +21,7 @@ use fs::FakeFs;
use futures::{StreamExt as _, channel::oneshot};
use git::GitHostingProviderRegistry;
use gpui::{AppContext as _, BackgroundExecutor, Entity, Task, TestAppContext, VisualTestContext};
use http_client::FakeHttpClient;
use http_client::{FakeHttpClient, Method};
use language::LanguageRegistry;
use node_runtime::NodeRuntime;
use notifications::NotificationStore;
@ -161,6 +162,8 @@ impl TestServer {
}
pub async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient {
const ACCESS_TOKEN: &str = "the-token";
let fs = FakeFs::new(cx.executor());
cx.update(|cx| {
@ -175,7 +178,7 @@ impl TestServer {
});
let clock = Arc::new(FakeSystemClock::new());
let http = FakeHttpClient::with_404_response();
let user_id = if let Ok(Some(user)) = self.app_state.db.get_user_by_github_login(name).await
{
user.id
@ -197,6 +200,47 @@ impl TestServer {
.expect("creating user failed")
.user_id
};
let http = FakeHttpClient::create({
let name = name.to_string();
move |req| {
let name = name.clone();
async move {
match (req.method(), req.uri().path()) {
(&Method::GET, "/client/users/me") => {
let credentials = parse_authorization_header(&req);
if credentials
!= Some(Credentials {
user_id: user_id.to_proto(),
access_token: ACCESS_TOKEN.into(),
})
{
return Ok(http_client::Response::builder()
.status(401)
.body("Unauthorized".into())
.unwrap());
}
Ok(http_client::Response::builder()
.status(200)
.body(
serde_json::to_string(&make_get_authenticated_user_response(
user_id.0, name,
))
.unwrap()
.into(),
)
.unwrap())
}
_ => Ok(http_client::Response::builder()
.status(404)
.body("Not Found".into())
.unwrap()),
}
}
}
});
let client_name = name.to_string();
let mut client = cx.update(|cx| Client::new(clock, http.clone(), cx));
let server = self.server.clone();
@ -208,11 +252,10 @@ impl TestServer {
.unwrap()
.set_id(user_id.to_proto())
.override_authenticate(move |cx| {
let access_token = "the-token".to_string();
cx.spawn(async move |_| {
Ok(Credentials {
user_id: user_id.to_proto(),
access_token,
access_token: ACCESS_TOKEN.into(),
})
})
})
@ -221,7 +264,7 @@ impl TestServer {
credentials,
&Credentials {
user_id: user_id.0 as u64,
access_token: "the-token".into()
access_token: ACCESS_TOKEN.into(),
}
);
@ -319,7 +362,7 @@ impl TestServer {
});
client
.authenticate_and_connect(false, &cx.to_async())
.connect(false, &cx.to_async())
.await
.into_response()
.unwrap();
@ -692,17 +735,17 @@ impl TestClient {
current: store
.contacts()
.iter()
.map(|contact| contact.user.github_login.clone())
.map(|contact| contact.user.github_login.clone().to_string())
.collect(),
outgoing_requests: store
.outgoing_contact_requests()
.iter()
.map(|user| user.github_login.clone())
.map(|user| user.github_login.clone().to_string())
.collect(),
incoming_requests: store
.incoming_contact_requests()
.iter()
.map(|user| user.github_login.clone())
.map(|user| user.github_login.clone().to_string())
.collect(),
})
}

View file

@ -940,7 +940,7 @@ impl CollabPanel {
room.read(cx).local_participant().role == proto::ChannelRole::Admin
});
ListItem::new(SharedString::from(user.github_login.clone()))
ListItem::new(user.github_login.clone())
.start_slot(Avatar::new(user.avatar_uri.clone()))
.child(Label::new(user.github_login.clone()))
.toggle_state(is_selected)
@ -2331,7 +2331,7 @@ impl CollabPanel {
let client = this.client.clone();
cx.spawn_in(window, async move |_, cx| {
client
.authenticate_and_connect(true, &cx)
.connect(true, &cx)
.await
.into_response()
.notify_async_err(cx);
@ -2583,7 +2583,7 @@ impl CollabPanel {
) -> impl IntoElement {
let online = contact.online;
let busy = contact.busy || calling;
let github_login = SharedString::from(contact.user.github_login.clone());
let github_login = contact.user.github_login.clone();
let item = ListItem::new(github_login.clone())
.indent_level(1)
.indent_step_size(px(20.))
@ -2662,7 +2662,7 @@ impl CollabPanel {
is_selected: bool,
cx: &mut Context<Self>,
) -> impl IntoElement {
let github_login = SharedString::from(user.github_login.clone());
let github_login = user.github_login.clone();
let user_id = user.id;
let is_response_pending = self.user_store.read(cx).is_contact_request_pending(user);
let color = if is_response_pending {

View file

@ -634,13 +634,13 @@ impl Render for NotificationPanel {
.child(Icon::new(IconName::Envelope)),
)
.map(|this| {
if self.client.user_id().is_none() {
if !self.client.status().borrow().is_connected() {
this.child(
v_flex()
.gap_2()
.p_4()
.child(
Button::new("sign_in_prompt_button", "Sign in")
Button::new("connect_prompt_button", "Connect")
.icon_color(Color::Muted)
.icon(IconName::Github)
.icon_position(IconPosition::Start)
@ -652,10 +652,7 @@ impl Render for NotificationPanel {
let client = client.clone();
window
.spawn(cx, async move |cx| {
match client
.authenticate_and_connect(true, &cx)
.await
{
match client.connect(true, &cx).await {
util::ConnectionResult::Timeout => {
log::error!("Connection timeout");
}
@ -673,7 +670,7 @@ impl Render for NotificationPanel {
)
.child(
div().flex().w_full().items_center().child(
Label::new("Sign in to view notifications.")
Label::new("Connect to view notifications.")
.color(Color::Muted)
.size(LabelSize::Small),
),

View file

@ -295,7 +295,7 @@ mod tests {
request: dap_types::StartDebuggingRequestArgumentsRequest::Launch,
},
},
Box::new(|_| panic!("Did not expect to hit this code path")),
Box::new(|_| {}),
&mut cx.to_async(),
)
.await

View file

@ -883,6 +883,7 @@ impl FakeTransport {
break Err(anyhow!("exit in response to request"));
}
};
let success = response.success;
let message =
serde_json::to_string(&Message::Response(response)).unwrap();
@ -893,6 +894,25 @@ impl FakeTransport {
)
.await
.unwrap();
if request.command == dap_types::requests::Initialize::COMMAND
&& success
{
let message = serde_json::to_string(&Message::Event(Box::new(
dap_types::messages::Events::Initialized(Some(
Default::default(),
)),
)))
.unwrap();
writer
.write_all(
TransportDelegate::build_rpc_message(message)
.as_bytes(),
)
.await
.unwrap();
}
writer.flush().await.unwrap();
}
}

View file

@ -9,7 +9,9 @@ license = "GPL-3.0-or-later"
anyhow.workspace = true
command_palette.workspace = true
gpui.workspace = true
mdbook = "0.4.40"
# We are specifically pinning this version of mdbook, as later versions introduce issues with double-nested subdirectories.
# Ask @maxdeviant about this before bumping.
mdbook = "= 0.4.40"
regex.workspace = true
serde.workspace = true
serde_json.workspace = true

View file

@ -22,6 +22,7 @@ test-support = [
"theme/test-support",
"util/test-support",
"workspace/test-support",
"tree-sitter-c",
"tree-sitter-rust",
"tree-sitter-typescript",
"tree-sitter-html",
@ -76,6 +77,7 @@ telemetry.workspace = true
text.workspace = true
time.workspace = true
theme.workspace = true
tree-sitter-c = { workspace = true, optional = true }
tree-sitter-html = { workspace = true, optional = true }
tree-sitter-rust = { workspace = true, optional = true }
tree-sitter-typescript = { workspace = true, optional = true }
@ -106,6 +108,7 @@ settings = { workspace = true, features = ["test-support"] }
tempfile.workspace = true
text = { workspace = true, features = ["test-support"] }
theme = { workspace = true, features = ["test-support"] }
tree-sitter-c.workspace = true
tree-sitter-html.workspace = true
tree-sitter-rust.workspace = true
tree-sitter-typescript.workspace = true

View file

@ -56,7 +56,7 @@ use aho_corasick::AhoCorasick;
use anyhow::{Context as _, Result, anyhow};
use blink_manager::BlinkManager;
use buffer_diff::DiffHunkStatus;
use client::{Collaborator, ParticipantIndex};
use client::{Collaborator, DisableAiSettings, ParticipantIndex};
use clock::{AGENT_REPLICA_ID, ReplicaId};
use collections::{BTreeMap, HashMap, HashSet, VecDeque};
use convert_case::{Case, Casing};
@ -1305,6 +1305,7 @@ impl Default for SelectionHistoryMode {
///
/// Similarly, you might want to disable scrolling if you don't want the viewport to
/// move.
#[derive(Clone)]
pub struct SelectionEffects {
nav_history: Option<bool>,
completions: bool,
@ -2944,10 +2945,12 @@ impl Editor {
}
}
let selection_anchors = self.selections.disjoint_anchors();
if self.focus_handle.is_focused(window) && self.leader_id.is_none() {
self.buffer.update(cx, |buffer, cx| {
buffer.set_active_selections(
&self.selections.disjoint_anchors(),
&selection_anchors,
self.selections.line_mode,
self.cursor_shape,
cx,
@ -2964,9 +2967,8 @@ impl Editor {
self.select_next_state = None;
self.select_prev_state = None;
self.select_syntax_node_history.try_clear();
self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), buffer);
self.snippet_stack
.invalidate(&self.selections.disjoint_anchors(), buffer);
self.invalidate_autoclose_regions(&selection_anchors, buffer);
self.snippet_stack.invalidate(&selection_anchors, buffer);
self.take_rename(false, window, cx);
let newest_selection = self.selections.newest_anchor();
@ -4047,7 +4049,8 @@ impl Editor {
// then don't insert that closing bracket again; just move the selection
// past the closing bracket.
let should_skip = selection.end == region.range.end.to_point(&snapshot)
&& text.as_ref() == region.pair.end.as_str();
&& text.as_ref() == region.pair.end.as_str()
&& snapshot.contains_str_at(region.range.end, text.as_ref());
if should_skip {
let anchor = snapshot.anchor_after(selection.end);
new_selections
@ -4973,13 +4976,17 @@ impl Editor {
})
}
/// Remove any autoclose regions that no longer contain their selection.
/// Remove any autoclose regions that no longer contain their selection or have invalid anchors in ranges.
fn invalidate_autoclose_regions(
&mut self,
mut selections: &[Selection<Anchor>],
buffer: &MultiBufferSnapshot,
) {
self.autoclose_regions.retain(|state| {
if !state.range.start.is_valid(buffer) || !state.range.end.is_valid(buffer) {
return false;
}
let mut i = 0;
while let Some(selection) = selections.get(i) {
if selection.end.cmp(&state.range.start, buffer).is_lt() {
@ -5891,18 +5898,20 @@ impl Editor {
text: new_text[common_prefix_len..].into(),
});
self.transact(window, cx, |this, window, cx| {
self.transact(window, cx, |editor, window, cx| {
if let Some(mut snippet) = snippet {
snippet.text = new_text.to_string();
this.insert_snippet(&ranges, snippet, window, cx).log_err();
editor
.insert_snippet(&ranges, snippet, window, cx)
.log_err();
} else {
this.buffer.update(cx, |buffer, cx| {
editor.buffer.update(cx, |multi_buffer, cx| {
let auto_indent = match completion.insert_text_mode {
Some(InsertTextMode::AS_IS) => None,
_ => this.autoindent_mode.clone(),
_ => editor.autoindent_mode.clone(),
};
let edits = ranges.into_iter().map(|range| (range, new_text.as_str()));
buffer.edit(edits, auto_indent, cx);
multi_buffer.edit(edits, auto_indent, cx);
});
}
for (buffer, edits) in linked_edits {
@ -5921,8 +5930,9 @@ impl Editor {
})
}
this.refresh_inline_completion(true, false, window, cx);
editor.refresh_inline_completion(true, false, window, cx);
});
self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), &snapshot);
let show_new_completions_on_confirm = completion
.confirm
@ -7048,7 +7058,7 @@ impl Editor {
}
pub fn update_edit_prediction_settings(&mut self, cx: &mut Context<Self>) {
if self.edit_prediction_provider.is_none() {
if self.edit_prediction_provider.is_none() || DisableAiSettings::get_global(cx).disable_ai {
self.edit_prediction_settings = EditPredictionSettings::Disabled;
} else {
let selection = self.selections.newest_anchor();
@ -9562,27 +9572,46 @@ impl Editor {
// Check whether the just-entered snippet ends with an auto-closable bracket.
if self.autoclose_regions.is_empty() {
let snapshot = self.buffer.read(cx).snapshot(cx);
for selection in &mut self.selections.all::<Point>(cx) {
let mut all_selections = self.selections.all::<Point>(cx);
for selection in &mut all_selections {
let selection_head = selection.head();
let Some(scope) = snapshot.language_scope_at(selection_head) else {
continue;
};
let mut bracket_pair = None;
let next_chars = snapshot.chars_at(selection_head).collect::<String>();
let prev_chars = snapshot
.reversed_chars_at(selection_head)
.collect::<String>();
for (pair, enabled) in scope.brackets() {
if enabled
&& pair.close
&& prev_chars.starts_with(pair.start.as_str())
&& next_chars.starts_with(pair.end.as_str())
{
bracket_pair = Some(pair.clone());
break;
let max_lookup_length = scope
.brackets()
.map(|(pair, _)| {
pair.start
.as_str()
.chars()
.count()
.max(pair.end.as_str().chars().count())
})
.max();
if let Some(max_lookup_length) = max_lookup_length {
let next_text = snapshot
.chars_at(selection_head)
.take(max_lookup_length)
.collect::<String>();
let prev_text = snapshot
.reversed_chars_at(selection_head)
.take(max_lookup_length)
.collect::<String>();
for (pair, enabled) in scope.brackets() {
if enabled
&& pair.close
&& prev_text.starts_with(pair.start.as_str())
&& next_text.starts_with(pair.end.as_str())
{
bracket_pair = Some(pair.clone());
break;
}
}
}
if let Some(pair) = bracket_pair {
let snapshot_settings = snapshot.language_settings_at(selection_head, cx);
let autoclose_enabled =

View file

@ -8612,6 +8612,7 @@ async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
cx.language_registry().add(html_language.clone());
cx.language_registry().add(javascript_language.clone());
cx.executor().run_until_parked();
cx.update_buffer(|buffer, cx| {
buffer.set_language(Some(html_language), cx);
@ -13400,6 +13401,178 @@ async fn test_as_is_completions(cx: &mut TestAppContext) {
cx.assert_editor_state("fn a() {}\n unsafeˇ");
}
#[gpui::test]
async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let language =
Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
let mut cx = EditorLspTestContext::new(
language,
lsp::ServerCapabilities {
completion_provider: Some(lsp::CompletionOptions {
..lsp::CompletionOptions::default()
}),
..lsp::ServerCapabilities::default()
},
cx,
)
.await;
cx.set_state(
"#ifndef BAR_H
#define BAR_H
#include <stdbool.h>
int fn_branch(bool do_branch1, bool do_branch2);
#endif // BAR_H
ˇ",
);
cx.executor().run_until_parked();
cx.update_editor(|editor, window, cx| {
editor.handle_input("#", window, cx);
});
cx.executor().run_until_parked();
cx.update_editor(|editor, window, cx| {
editor.handle_input("i", window, cx);
});
cx.executor().run_until_parked();
cx.update_editor(|editor, window, cx| {
editor.handle_input("n", window, cx);
});
cx.executor().run_until_parked();
cx.assert_editor_state(
"#ifndef BAR_H
#define BAR_H
#include <stdbool.h>
int fn_branch(bool do_branch1, bool do_branch2);
#endif // BAR_H
#inˇ",
);
cx.lsp
.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
is_incomplete: false,
item_defaults: None,
items: vec![lsp::CompletionItem {
kind: Some(lsp::CompletionItemKind::SNIPPET),
label_details: Some(lsp::CompletionItemLabelDetails {
detail: Some("header".to_string()),
description: None,
}),
label: " include".to_string(),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range {
start: lsp::Position {
line: 8,
character: 1,
},
end: lsp::Position {
line: 8,
character: 1,
},
},
new_text: "include \"$0\"".to_string(),
})),
sort_text: Some("40b67681include".to_string()),
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
filter_text: Some("include".to_string()),
insert_text: Some("include \"$0\"".to_string()),
..lsp::CompletionItem::default()
}],
})))
});
cx.update_editor(|editor, window, cx| {
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
});
cx.executor().run_until_parked();
cx.update_editor(|editor, window, cx| {
editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
});
cx.executor().run_until_parked();
cx.assert_editor_state(
"#ifndef BAR_H
#define BAR_H
#include <stdbool.h>
int fn_branch(bool do_branch1, bool do_branch2);
#endif // BAR_H
#include \"ˇ\"",
);
cx.lsp
.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
is_incomplete: true,
item_defaults: None,
items: vec![lsp::CompletionItem {
kind: Some(lsp::CompletionItemKind::FILE),
label: "AGL/".to_string(),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range {
start: lsp::Position {
line: 8,
character: 10,
},
end: lsp::Position {
line: 8,
character: 11,
},
},
new_text: "AGL/".to_string(),
})),
sort_text: Some("40b67681AGL/".to_string()),
insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
filter_text: Some("AGL/".to_string()),
insert_text: Some("AGL/".to_string()),
..lsp::CompletionItem::default()
}],
})))
});
cx.update_editor(|editor, window, cx| {
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
});
cx.executor().run_until_parked();
cx.update_editor(|editor, window, cx| {
editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
});
cx.executor().run_until_parked();
cx.assert_editor_state(
r##"#ifndef BAR_H
#define BAR_H
#include <stdbool.h>
int fn_branch(bool do_branch1, bool do_branch2);
#endif // BAR_H
#include "AGL/ˇ"##,
);
cx.update_editor(|editor, window, cx| {
editor.handle_input("\"", window, cx);
});
cx.executor().run_until_parked();
cx.assert_editor_state(
r##"#ifndef BAR_H
#define BAR_H
#include <stdbool.h>
int fn_branch(bool do_branch1, bool do_branch2);
#endif // BAR_H
#include "AGL/"ˇ"##,
);
}
#[gpui::test]
async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
init_test(cx, |_| {});

View file

@ -18,7 +18,7 @@ use collections::{HashMap, HashSet};
use extension::ExtensionHostProxy;
use futures::future;
use gpui::http_client::read_proxy_from_env;
use gpui::{App, AppContext, Application, AsyncApp, Entity, SemanticVersion, UpdateGlobal};
use gpui::{App, AppContext, Application, AsyncApp, Entity, UpdateGlobal};
use gpui_tokio::Tokio;
use language::LanguageRegistry;
use language_model::{ConfiguredModel, LanguageModel, LanguageModelRegistry, SelectedModel};
@ -337,7 +337,8 @@ pub struct AgentAppState {
}
pub fn init(cx: &mut App) -> Arc<AgentAppState> {
release_channel::init(SemanticVersion::default(), cx);
let app_version = AppVersion::global(cx);
release_channel::init(app_version, cx);
gpui_tokio::init(cx);
let mut settings_store = SettingsStore::new(cx);
@ -350,7 +351,7 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> {
// Set User-Agent so we can download language servers from GitHub
let user_agent = format!(
"Zed/{} ({}; {})",
AppVersion::global(cx),
app_version,
std::env::consts::OS,
std::env::consts::ARCH
);

View file

@ -2416,7 +2416,7 @@ impl GitPanel {
.committer_name
.clone()
.or_else(|| participant.user.name.clone())
.unwrap_or_else(|| participant.user.github_login.clone());
.unwrap_or_else(|| participant.user.github_login.clone().to_string());
new_co_authors.push((name.clone(), email.clone()))
}
}
@ -2436,7 +2436,7 @@ impl GitPanel {
.name
.clone()
.or_else(|| user.name.clone())
.unwrap_or_else(|| user.github_login.clone());
.unwrap_or_else(|| user.github_login.clone().to_string());
Some((name, email))
}

View file

@ -47,7 +47,6 @@ wayland = [
"wayland-cursor",
"wayland-protocols",
"wayland-protocols-plasma",
"wayland-protocols-wlr",
"filedescriptor",
"xkbcommon",
"open",
@ -194,9 +193,6 @@ 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 }
@ -220,10 +216,6 @@ xim = { git = "https://github.com/XDeme1/xim-rs", rev = "d50d461764c2213655cd9cf
x11-clipboard = { version = "0.9.3", optional = true }
[target.'cfg(target_os = "windows")'.dependencies]
blade-util.workspace = true
bytemuck = "1"
blade-graphics.workspace = true
blade-macros.workspace = true
flume = "0.11"
rand.workspace = true
windows.workspace = true
@ -244,7 +236,6 @@ util = { workspace = true, features = ["test-support"] }
[target.'cfg(target_os = "windows")'.build-dependencies]
embed-resource = "3.0"
naga.workspace = true
[target.'cfg(target_os = "macos")'.build-dependencies]
bindgen = "0.71"

View file

@ -9,7 +9,10 @@ fn main() {
let target = env::var("CARGO_CFG_TARGET_OS");
println!("cargo::rustc-check-cfg=cfg(gles)");
#[cfg(any(not(target_os = "macos"), feature = "macos-blade"))]
#[cfg(any(
not(any(target_os = "macos", target_os = "windows")),
all(target_os = "macos", feature = "macos-blade")
))]
check_wgsl_shaders();
match target.as_deref() {
@ -17,21 +20,18 @@ fn main() {
#[cfg(target_os = "macos")]
macos::build();
}
#[cfg(all(target_os = "windows", feature = "windows-manifest"))]
Ok("windows") => {
let manifest = std::path::Path::new("resources/windows/gpui.manifest.xml");
let rc_file = std::path::Path::new("resources/windows/gpui.rc");
println!("cargo:rerun-if-changed={}", manifest.display());
println!("cargo:rerun-if-changed={}", rc_file.display());
embed_resource::compile(rc_file, embed_resource::NONE)
.manifest_required()
.unwrap();
#[cfg(target_os = "windows")]
windows::build();
}
_ => (),
};
}
#[allow(dead_code)]
#[cfg(any(
not(any(target_os = "macos", target_os = "windows")),
all(target_os = "macos", feature = "macos-blade")
))]
fn check_wgsl_shaders() {
use std::path::PathBuf;
use std::process;
@ -243,3 +243,215 @@ mod macos {
}
}
}
#[cfg(target_os = "windows")]
mod windows {
use std::{
fs,
io::Write,
path::{Path, PathBuf},
process::{self, Command},
};
pub(super) fn build() {
// Compile HLSL shaders
#[cfg(not(debug_assertions))]
compile_shaders();
// Embed the Windows manifest and resource file
#[cfg(feature = "windows-manifest")]
embed_resource();
}
#[cfg(feature = "windows-manifest")]
fn embed_resource() {
let manifest = std::path::Path::new("resources/windows/gpui.manifest.xml");
let rc_file = std::path::Path::new("resources/windows/gpui.rc");
println!("cargo:rerun-if-changed={}", manifest.display());
println!("cargo:rerun-if-changed={}", rc_file.display());
embed_resource::compile(rc_file, embed_resource::NONE)
.manifest_required()
.unwrap();
}
/// You can set the `GPUI_FXC_PATH` environment variable to specify the path to the fxc.exe compiler.
fn compile_shaders() {
let shader_path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
.join("src/platform/windows/shaders.hlsl");
let out_dir = std::env::var("OUT_DIR").unwrap();
println!("cargo:rerun-if-changed={}", shader_path.display());
// Check if fxc.exe is available
let fxc_path = find_fxc_compiler();
// Define all modules
let modules = [
"quad",
"shadow",
"path_rasterization",
"path_sprite",
"underline",
"monochrome_sprite",
"polychrome_sprite",
];
let rust_binding_path = format!("{}/shaders_bytes.rs", out_dir);
if Path::new(&rust_binding_path).exists() {
fs::remove_file(&rust_binding_path)
.expect("Failed to remove existing Rust binding file");
}
for module in modules {
compile_shader_for_module(
module,
&out_dir,
&fxc_path,
shader_path.to_str().unwrap(),
&rust_binding_path,
);
}
{
let shader_path = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap())
.join("src/platform/windows/color_text_raster.hlsl");
compile_shader_for_module(
"emoji_rasterization",
&out_dir,
&fxc_path,
shader_path.to_str().unwrap(),
&rust_binding_path,
);
}
}
/// You can set the `GPUI_FXC_PATH` environment variable to specify the path to the fxc.exe compiler.
fn find_fxc_compiler() -> String {
// Check environment variable
if let Ok(path) = std::env::var("GPUI_FXC_PATH") {
if Path::new(&path).exists() {
return path;
}
}
// Try to find in PATH
// NOTE: This has to be `where.exe` on Windows, not `where`, it must be ended with `.exe`
if let Ok(output) = std::process::Command::new("where.exe")
.arg("fxc.exe")
.output()
{
if output.status.success() {
let path = String::from_utf8_lossy(&output.stdout);
return path.trim().to_string();
}
}
// Check the default path
if Path::new(r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\fxc.exe")
.exists()
{
return r"C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64\fxc.exe"
.to_string();
}
panic!("Failed to find fxc.exe");
}
fn compile_shader_for_module(
module: &str,
out_dir: &str,
fxc_path: &str,
shader_path: &str,
rust_binding_path: &str,
) {
// Compile vertex shader
let output_file = format!("{}/{}_vs.h", out_dir, module);
let const_name = format!("{}_VERTEX_BYTES", module.to_uppercase());
compile_shader_impl(
fxc_path,
&format!("{module}_vertex"),
&output_file,
&const_name,
shader_path,
"vs_4_1",
);
generate_rust_binding(&const_name, &output_file, &rust_binding_path);
// Compile fragment shader
let output_file = format!("{}/{}_ps.h", out_dir, module);
let const_name = format!("{}_FRAGMENT_BYTES", module.to_uppercase());
compile_shader_impl(
fxc_path,
&format!("{module}_fragment"),
&output_file,
&const_name,
shader_path,
"ps_4_1",
);
generate_rust_binding(&const_name, &output_file, &rust_binding_path);
}
fn compile_shader_impl(
fxc_path: &str,
entry_point: &str,
output_path: &str,
var_name: &str,
shader_path: &str,
target: &str,
) {
let output = Command::new(fxc_path)
.args([
"/T",
target,
"/E",
entry_point,
"/Fh",
output_path,
"/Vn",
var_name,
"/O3",
shader_path,
])
.output();
match output {
Ok(result) => {
if result.status.success() {
return;
}
eprintln!(
"Shader compilation failed for {}:\n{}",
entry_point,
String::from_utf8_lossy(&result.stderr)
);
process::exit(1);
}
Err(e) => {
eprintln!("Failed to run fxc for {}: {}", entry_point, e);
process::exit(1);
}
}
}
fn generate_rust_binding(const_name: &str, head_file: &str, output_path: &str) {
let header_content = fs::read_to_string(head_file).expect("Failed to read header file");
let const_definition = {
let global_var_start = header_content.find("const BYTE").unwrap();
let global_var = &header_content[global_var_start..];
let equal = global_var.find('=').unwrap();
global_var[equal + 1..].trim()
};
let rust_binding = format!(
"const {}: &[u8] = &{}\n",
const_name,
const_definition.replace('{', "[").replace('}', "]")
);
let mut options = fs::OpenOptions::new()
.create(true)
.append(true)
.open(output_path)
.expect("Failed to open Rust binding file");
options
.write_all(rust_binding.as_bytes())
.expect("Failed to write Rust binding file");
}
}

View file

@ -198,7 +198,7 @@ impl RenderOnce for CharacterGrid {
"χ", "ψ", "", "а", "в", "Ж", "ж", "З", "з", "К", "к", "л", "м", "Н", "н", "Р", "р",
"У", "у", "ф", "ч", "ь", "ы", "Э", "э", "Я", "я", "ij", "öẋ", ".,", "⣝⣑", "~", "*",
"_", "^", "`", "'", "(", "{", "«", "#", "&", "@", "$", "¢", "%", "|", "?", "", "µ",
"", "<=", "!=", "==", "--", "++", "=>", "->",
"", "<=", "!=", "==", "--", "++", "=>", "->", "🏀", "🎊", "😍", "❤️", "👍", "👎",
];
let columns = 11;

View file

@ -35,6 +35,7 @@ pub(crate) fn swap_rgba_pa_to_bgra(color: &mut [u8]) {
/// An RGBA color
#[derive(PartialEq, Clone, Copy, Default)]
#[repr(C)]
pub struct Rgba {
/// The red component of the color, in the range 0.0 to 1.0
pub r: f32,

View file

@ -13,8 +13,7 @@ mod mac;
any(target_os = "linux", target_os = "freebsd"),
any(feature = "x11", feature = "wayland")
),
target_os = "windows",
feature = "macos-blade"
all(target_os = "macos", feature = "macos-blade")
))]
mod blade;
@ -448,6 +447,8 @@ impl Tiling {
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
pub(crate) struct RequestFrameOptions {
pub(crate) require_presentation: bool,
/// Force refresh of all rendering states when true
pub(crate) force_render: bool,
}
pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
@ -1216,10 +1217,6 @@ 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.

View file

@ -61,7 +61,6 @@ 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};
@ -115,7 +114,6 @@ 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,
@ -153,7 +151,6 @@ 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,
@ -932,7 +929,6 @@ 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);
@ -1078,31 +1074,6 @@ 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,

View file

@ -1,6 +1,3 @@
use blade_graphics as gpu;
use collections::HashMap;
use futures::channel::oneshot::Receiver;
use std::{
cell::{Ref, RefCell, RefMut},
ffi::c_void,
@ -9,14 +6,9 @@ use std::{
sync::Arc,
};
use crate::{
Capslock,
platform::{
PlatformAtlas, PlatformInputHandler, PlatformWindow,
blade::{BladeContext, BladeRenderer, BladeSurfaceConfig},
linux::wayland::{display::WaylandDisplay, serial::SerialKind},
},
};
use blade_graphics as gpu;
use collections::HashMap;
use futures::channel::oneshot::Receiver;
use raw_window_handle as rwh;
use wayland_backend::client::ObjectId;
@ -28,8 +20,6 @@ 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::{
@ -37,7 +27,15 @@ use crate::{
PlatformDisplay, PlatformInput, Point, PromptButton, PromptLevel, RequestFrameOptions,
ResizeEdge, ScaledPixels, Size, Tiling, WaylandClientStatePtr, WindowAppearance,
WindowBackgroundAppearance, WindowBounds, WindowControlArea, WindowControls, WindowDecorations,
WindowKind, WindowParams, px, size,
WindowParams, px, size,
};
use crate::{
Capslock,
platform::{
PlatformAtlas, PlatformInputHandler, PlatformWindow,
blade::{BladeContext, BladeRenderer, BladeSurfaceConfig},
linux::wayland::{display::WaylandDisplay, serial::SerialKind},
},
};
#[derive(Default)]
@ -83,12 +81,14 @@ struct InProgressConfigure {
}
pub struct WaylandWindowState {
surface_state: WaylandSurfaceState,
xdg_surface: xdg_surface::XdgSurface,
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,78 +114,6 @@ 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>>,
@ -196,7 +124,9 @@ impl WaylandWindowState {
pub(crate) fn new(
handle: AnyWindowHandle,
surface: wl_surface::WlSurface,
surface_state: WaylandSurfaceState,
xdg_surface: xdg_surface::XdgSurface,
toplevel: xdg_toplevel::XdgToplevel,
decoration: Option<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1>,
appearance: WindowAppearance,
viewport: Option<wp_viewport::WpViewport>,
client: WaylandClientStatePtr,
@ -226,11 +156,13 @@ impl WaylandWindowState {
};
Ok(Self {
surface_state,
xdg_surface,
acknowledged_first_configure: false,
surface,
decoration,
app_id: None,
blur: None,
toplevel,
viewport,
globals,
outputs: HashMap::default(),
@ -303,16 +235,17 @@ impl Drop for WaylandWindow {
let client = state.client.clone();
state.renderer.destroy();
if let Some(decoration) = &state.surface_state.decoration() {
if let Some(decoration) = &state.decoration {
decoration.destroy();
}
if let Some(blur) = &state.blur {
blur.release();
}
state.surface_state.destroy();
state.toplevel.destroy();
if let Some(viewport) = &state.viewport {
viewport.destroy();
}
state.xdg_surface.destroy();
state.surface.destroy();
let state_ptr = self.0.clone();
@ -346,65 +279,27 @@ 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());
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(size) = params.window_min_size {
toplevel.set_min_size(size.width.0 as i32, size.height.0 as i32);
}
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()
@ -414,7 +309,9 @@ impl WaylandWindow {
state: Rc::new(RefCell::new(WaylandWindowState::new(
handle,
surface.clone(),
surface_state,
xdg_surface,
toplevel,
decoration,
appearance,
viewport,
client,
@ -506,7 +403,7 @@ impl WaylandWindowStatePtr {
}
}
let mut state = self.state.borrow_mut();
state.surface_state.ack_configure(serial);
state.xdg_surface.ack_configure(serial);
let window_geometry = inset_by_tiling(
state.bounds.map_origin(|_| px(0.0)),
@ -516,7 +413,7 @@ impl WaylandWindowStatePtr {
.map(|v| v.0 as i32)
.map_size(|v| if v <= 0 { 1 } else { v });
state.surface_state.set_geometry(
state.xdg_surface.set_window_geometry(
window_geometry.origin.x,
window_geometry.origin.y,
window_geometry.size.width,
@ -681,42 +578,6 @@ 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,
@ -979,7 +840,7 @@ impl PlatformWindow for WaylandWindow {
let state_ptr = self.0.clone();
let dp_size = size.to_device_pixels(self.scale_factor());
state.surface_state.set_geometry(
state.xdg_surface.set_window_geometry(
state.bounds.origin.x.0 as i32,
state.bounds.origin.y.0 as i32,
dp_size.width.0,
@ -1073,16 +934,12 @@ impl PlatformWindow for WaylandWindow {
}
fn set_title(&mut self, title: &str) {
if let Some(toplevel) = self.borrow().surface_state.toplevel() {
toplevel.set_title(title.to_string());
}
self.borrow().toplevel.set_title(title.to_string());
}
fn set_app_id(&mut self, app_id: &str) {
let mut state = self.borrow_mut();
if let Some(toplevel) = self.borrow().surface_state.toplevel() {
toplevel.set_app_id(app_id.to_owned());
}
state.toplevel.set_app_id(app_id.to_owned());
state.app_id = Some(app_id.to_owned());
}
@ -1093,30 +950,24 @@ impl PlatformWindow for WaylandWindow {
}
fn minimize(&self) {
if let Some(toplevel) = self.borrow().surface_state.toplevel() {
toplevel.set_minimized();
}
self.borrow().toplevel.set_minimized();
}
fn zoom(&self) {
let state = self.borrow();
if let Some(toplevel) = state.surface_state.toplevel() {
if !state.maximized {
toplevel.set_maximized();
} else {
toplevel.unset_maximized();
}
if !state.maximized {
state.toplevel.set_maximized();
} else {
state.toplevel.unset_maximized();
}
}
fn toggle_fullscreen(&self) {
let mut state = self.borrow();
if let Some(toplevel) = state.surface_state.toplevel() {
if !state.fullscreen {
toplevel.set_fullscreen(None);
} else {
toplevel.unset_fullscreen();
}
let mut state = self.borrow_mut();
if !state.fullscreen {
state.toplevel.set_fullscreen(None);
} else {
state.toplevel.unset_fullscreen();
}
}
@ -1181,33 +1032,27 @@ impl PlatformWindow for WaylandWindow {
fn show_window_menu(&self, position: Point<Pixels>) {
let state = self.borrow();
let serial = state.client.get_serial(SerialKind::MousePress);
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,
);
}
state.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);
if let Some(toplevel) = state.surface_state.toplevel() {
toplevel._move(&state.globals.seat, serial);
}
state.toplevel._move(&state.globals.seat, serial);
}
fn start_window_resize(&self, edge: crate::ResizeEdge) {
let state = self.borrow();
if let Some(toplevel) = state.surface_state.toplevel() {
toplevel.resize(
&state.globals.seat,
state.client.get_serial(SerialKind::MousePress),
edge.to_xdg(),
)
}
state.toplevel.resize(
&state.globals.seat,
state.client.get_serial(SerialKind::MousePress),
edge.to_xdg(),
)
}
fn window_decorations(&self) -> Decorations {
@ -1223,7 +1068,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.surface_state.decoration() {
if let Some(decoration) = state.decoration.as_ref() {
decoration.set_mode(decorations.to_xdg());
update_window(state);
}

View file

@ -1004,12 +1004,13 @@ impl X11Client {
let mut keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
let keysym = state.xkb.key_get_one_sym(code);
// should be called after key_get_one_sym
state.xkb.update_key(code, xkbc::KeyDirection::Down);
if keysym.is_modifier_key() {
return Some(());
}
// should be called after key_get_one_sym
state.xkb.update_key(code, xkbc::KeyDirection::Down);
if let Some(mut compose_state) = state.compose_state.take() {
compose_state.feed(keysym);
match compose_state.status() {
@ -1067,12 +1068,13 @@ impl X11Client {
let keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
let keysym = state.xkb.key_get_one_sym(code);
// should be called after key_get_one_sym
state.xkb.update_key(code, xkbc::KeyDirection::Up);
if keysym.is_modifier_key() {
return Some(());
}
// should be called after key_get_one_sym
state.xkb.update_key(code, xkbc::KeyDirection::Up);
keystroke
};
drop(state);
@ -1793,6 +1795,7 @@ impl X11ClientState {
drop(state);
window.refresh(RequestFrameOptions {
require_presentation: expose_event_received,
force_render: false,
});
}
xcb_connection

View file

@ -559,7 +559,7 @@ impl MacWindow {
}
let native_window: id = match kind {
WindowKind::Normal | WindowKind::Overlay => msg_send![WINDOW_CLASS, alloc],
WindowKind::Normal => 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::Overlay => {
WindowKind::Normal => {
native_window.setLevel_(NSNormalWindowLevel);
native_window.setAcceptsMouseMovedEvents_(YES);
}

View file

@ -1,6 +1,8 @@
mod clipboard;
mod destination_list;
mod direct_write;
mod directx_atlas;
mod directx_renderer;
mod dispatcher;
mod display;
mod events;
@ -14,6 +16,8 @@ mod wrapper;
pub(crate) use clipboard::*;
pub(crate) use destination_list::*;
pub(crate) use direct_write::*;
pub(crate) use directx_atlas::*;
pub(crate) use directx_renderer::*;
pub(crate) use dispatcher::*;
pub(crate) use display::*;
pub(crate) use events::*;

View file

@ -0,0 +1,39 @@
struct RasterVertexOutput {
float4 position : SV_Position;
float2 texcoord : TEXCOORD0;
};
RasterVertexOutput emoji_rasterization_vertex(uint vertexID : SV_VERTEXID)
{
RasterVertexOutput output;
output.texcoord = float2((vertexID << 1) & 2, vertexID & 2);
output.position = float4(output.texcoord * 2.0f - 1.0f, 0.0f, 1.0f);
output.position.y = -output.position.y;
return output;
}
struct PixelInput {
float4 position: SV_Position;
float2 texcoord : TEXCOORD0;
};
struct Bounds {
int2 origin;
int2 size;
};
Texture2D<float4> t_layer : register(t0);
SamplerState s_layer : register(s0);
cbuffer GlyphLayerTextureParams : register(b0) {
Bounds bounds;
float4 run_color;
};
float4 emoji_rasterization_fragment(PixelInput input): SV_Target {
float3 sampled = t_layer.Sample(s_layer, input.texcoord.xy).rgb;
float alpha = (sampled.r + sampled.g + sampled.b) / 3;
return float4(run_color.rgb, alpha);
}

View file

@ -10,10 +10,11 @@ use windows::{
Foundation::*,
Globalization::GetUserDefaultLocaleName,
Graphics::{
Direct2D::{Common::*, *},
Direct3D::D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP,
Direct3D11::*,
DirectWrite::*,
Dxgi::Common::*,
Gdi::LOGFONTW,
Gdi::{IsRectEmpty, LOGFONTW},
Imaging::*,
},
System::SystemServices::LOCALE_NAME_MAX_LENGTH,
@ -40,16 +41,21 @@ struct DirectWriteComponent {
locale: String,
factory: IDWriteFactory5,
bitmap_factory: AgileReference<IWICImagingFactory>,
d2d1_factory: ID2D1Factory,
in_memory_loader: IDWriteInMemoryFontFileLoader,
builder: IDWriteFontSetBuilder1,
text_renderer: Arc<TextRendererWrapper>,
render_context: GlyphRenderContext,
render_params: IDWriteRenderingParams3,
gpu_state: GPUState,
}
struct GlyphRenderContext {
params: IDWriteRenderingParams3,
dc_target: ID2D1DeviceContext4,
struct GPUState {
device: ID3D11Device,
device_context: ID3D11DeviceContext,
sampler: [Option<ID3D11SamplerState>; 1],
blend_state: ID3D11BlendState,
vertex_shader: ID3D11VertexShader,
pixel_shader: ID3D11PixelShader,
}
struct DirectWriteState {
@ -70,12 +76,11 @@ struct FontIdentifier {
}
impl DirectWriteComponent {
pub fn new(bitmap_factory: &IWICImagingFactory) -> Result<Self> {
pub fn new(bitmap_factory: &IWICImagingFactory, gpu_context: &DirectXDevices) -> Result<Self> {
// todo: ideally this would not be a large unsafe block but smaller isolated ones for easier auditing
unsafe {
let factory: IDWriteFactory5 = DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED)?;
let bitmap_factory = AgileReference::new(bitmap_factory)?;
let d2d1_factory: ID2D1Factory =
D2D1CreateFactory(D2D1_FACTORY_TYPE_MULTI_THREADED, None)?;
// The `IDWriteInMemoryFontFileLoader` here is supported starting from
// Windows 10 Creators Update, which consequently requires the entire
// `DirectWriteTextSystem` to run on `win10 1703`+.
@ -86,60 +91,132 @@ impl DirectWriteComponent {
GetUserDefaultLocaleName(&mut locale_vec);
let locale = String::from_utf16_lossy(&locale_vec);
let text_renderer = Arc::new(TextRendererWrapper::new(&locale));
let render_context = GlyphRenderContext::new(&factory, &d2d1_factory)?;
let render_params = {
let default_params: IDWriteRenderingParams3 =
factory.CreateRenderingParams()?.cast()?;
let gamma = default_params.GetGamma();
let enhanced_contrast = default_params.GetEnhancedContrast();
let gray_contrast = default_params.GetGrayscaleEnhancedContrast();
let cleartype_level = default_params.GetClearTypeLevel();
let grid_fit_mode = default_params.GetGridFitMode();
factory.CreateCustomRenderingParams(
gamma,
enhanced_contrast,
gray_contrast,
cleartype_level,
DWRITE_PIXEL_GEOMETRY_RGB,
DWRITE_RENDERING_MODE1_NATURAL_SYMMETRIC,
grid_fit_mode,
)?
};
let gpu_state = GPUState::new(gpu_context)?;
Ok(DirectWriteComponent {
locale,
factory,
bitmap_factory,
d2d1_factory,
in_memory_loader,
builder,
text_renderer,
render_context,
render_params,
gpu_state,
})
}
}
}
impl GlyphRenderContext {
pub fn new(factory: &IDWriteFactory5, d2d1_factory: &ID2D1Factory) -> Result<Self> {
unsafe {
let default_params: IDWriteRenderingParams3 =
factory.CreateRenderingParams()?.cast()?;
let gamma = default_params.GetGamma();
let enhanced_contrast = default_params.GetEnhancedContrast();
let gray_contrast = default_params.GetGrayscaleEnhancedContrast();
let cleartype_level = default_params.GetClearTypeLevel();
let grid_fit_mode = default_params.GetGridFitMode();
impl GPUState {
fn new(gpu_context: &DirectXDevices) -> Result<Self> {
let device = gpu_context.device.clone();
let device_context = gpu_context.device_context.clone();
let params = factory.CreateCustomRenderingParams(
gamma,
enhanced_contrast,
gray_contrast,
cleartype_level,
DWRITE_PIXEL_GEOMETRY_RGB,
DWRITE_RENDERING_MODE1_NATURAL_SYMMETRIC,
grid_fit_mode,
)?;
let dc_target = {
let target = d2d1_factory.CreateDCRenderTarget(&get_render_target_property(
DXGI_FORMAT_B8G8R8A8_UNORM,
D2D1_ALPHA_MODE_PREMULTIPLIED,
))?;
let target = target.cast::<ID2D1DeviceContext4>()?;
target.SetTextRenderingParams(&params);
target
let blend_state = {
let mut blend_state = None;
let desc = D3D11_BLEND_DESC {
AlphaToCoverageEnable: false.into(),
IndependentBlendEnable: false.into(),
RenderTarget: [
D3D11_RENDER_TARGET_BLEND_DESC {
BlendEnable: true.into(),
SrcBlend: D3D11_BLEND_SRC_ALPHA,
DestBlend: D3D11_BLEND_INV_SRC_ALPHA,
BlendOp: D3D11_BLEND_OP_ADD,
SrcBlendAlpha: D3D11_BLEND_SRC_ALPHA,
DestBlendAlpha: D3D11_BLEND_INV_SRC_ALPHA,
BlendOpAlpha: D3D11_BLEND_OP_ADD,
RenderTargetWriteMask: D3D11_COLOR_WRITE_ENABLE_ALL.0 as u8,
},
Default::default(),
Default::default(),
Default::default(),
Default::default(),
Default::default(),
Default::default(),
Default::default(),
],
};
unsafe { device.CreateBlendState(&desc, Some(&mut blend_state)) }?;
blend_state.unwrap()
};
Ok(Self { params, dc_target })
}
let sampler = {
let mut sampler = None;
let desc = D3D11_SAMPLER_DESC {
Filter: D3D11_FILTER_MIN_MAG_MIP_POINT,
AddressU: D3D11_TEXTURE_ADDRESS_BORDER,
AddressV: D3D11_TEXTURE_ADDRESS_BORDER,
AddressW: D3D11_TEXTURE_ADDRESS_BORDER,
MipLODBias: 0.0,
MaxAnisotropy: 1,
ComparisonFunc: D3D11_COMPARISON_ALWAYS,
BorderColor: [0.0, 0.0, 0.0, 0.0],
MinLOD: 0.0,
MaxLOD: 0.0,
};
unsafe { device.CreateSamplerState(&desc, Some(&mut sampler)) }?;
[sampler]
};
let vertex_shader = {
let source = shader_resources::RawShaderBytes::new(
shader_resources::ShaderModule::EmojiRasterization,
shader_resources::ShaderTarget::Vertex,
)?;
let mut shader = None;
unsafe { device.CreateVertexShader(source.as_bytes(), None, Some(&mut shader)) }?;
shader.unwrap()
};
let pixel_shader = {
let source = shader_resources::RawShaderBytes::new(
shader_resources::ShaderModule::EmojiRasterization,
shader_resources::ShaderTarget::Fragment,
)?;
let mut shader = None;
unsafe { device.CreatePixelShader(source.as_bytes(), None, Some(&mut shader)) }?;
shader.unwrap()
};
Ok(Self {
device,
device_context,
sampler,
blend_state,
vertex_shader,
pixel_shader,
})
}
}
impl DirectWriteTextSystem {
pub(crate) fn new(bitmap_factory: &IWICImagingFactory) -> Result<Self> {
let components = DirectWriteComponent::new(bitmap_factory)?;
pub(crate) fn new(
gpu_context: &DirectXDevices,
bitmap_factory: &IWICImagingFactory,
) -> Result<Self> {
let components = DirectWriteComponent::new(bitmap_factory, gpu_context)?;
let system_font_collection = unsafe {
let mut result = std::mem::zeroed();
components
@ -648,15 +725,13 @@ impl DirectWriteState {
}
}
fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
let render_target = &self.components.render_context.dc_target;
unsafe {
render_target.SetUnitMode(D2D1_UNIT_MODE_DIPS);
render_target.SetDpi(96.0 * params.scale_factor, 96.0 * params.scale_factor);
}
fn create_glyph_run_analysis(
&self,
params: &RenderGlyphParams,
) -> Result<IDWriteGlyphRunAnalysis> {
let font = &self.fonts[params.font_id.0];
let glyph_id = [params.glyph_id.0 as u16];
let advance = [0.0f32];
let advance = [0.0];
let offset = [DWRITE_GLYPH_OFFSET::default()];
let glyph_run = DWRITE_GLYPH_RUN {
fontFace: unsafe { std::mem::transmute_copy(&font.font_face) },
@ -668,44 +743,87 @@ impl DirectWriteState {
isSideways: BOOL(0),
bidiLevel: 0,
};
let bounds = unsafe {
render_target.GetGlyphRunWorldBounds(
Vector2 { X: 0.0, Y: 0.0 },
&glyph_run,
DWRITE_MEASURING_MODE_NATURAL,
)?
let transform = DWRITE_MATRIX {
m11: params.scale_factor,
m12: 0.0,
m21: 0.0,
m22: params.scale_factor,
dx: 0.0,
dy: 0.0,
};
// todo(windows)
// This is a walkaround, deleted when figured out.
let y_offset;
let extra_height;
if params.is_emoji {
y_offset = 0;
extra_height = 0;
} else {
// make some room for scaler.
y_offset = -1;
extra_height = 2;
let subpixel_shift = params
.subpixel_variant
.map(|v| v as f32 / SUBPIXEL_VARIANTS as f32);
let baseline_origin_x = subpixel_shift.x / params.scale_factor;
let baseline_origin_y = subpixel_shift.y / params.scale_factor;
let mut rendering_mode = DWRITE_RENDERING_MODE1::default();
let mut grid_fit_mode = DWRITE_GRID_FIT_MODE::default();
unsafe {
font.font_face.GetRecommendedRenderingMode(
params.font_size.0,
// The dpi here seems that it has the same effect with `Some(&transform)`
1.0,
1.0,
Some(&transform),
false,
DWRITE_OUTLINE_THRESHOLD_ANTIALIASED,
DWRITE_MEASURING_MODE_NATURAL,
&self.components.render_params,
&mut rendering_mode,
&mut grid_fit_mode,
)?;
}
if bounds.right < bounds.left {
let glyph_analysis = unsafe {
self.components.factory.CreateGlyphRunAnalysis(
&glyph_run,
Some(&transform),
rendering_mode,
DWRITE_MEASURING_MODE_NATURAL,
grid_fit_mode,
// We're using cleartype not grayscale for monochrome is because it provides better quality
DWRITE_TEXT_ANTIALIAS_MODE_CLEARTYPE,
baseline_origin_x,
baseline_origin_y,
)
}?;
Ok(glyph_analysis)
}
fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
let glyph_analysis = self.create_glyph_run_analysis(params)?;
let bounds = unsafe { glyph_analysis.GetAlphaTextureBounds(DWRITE_TEXTURE_CLEARTYPE_3x1)? };
// Some glyphs cannot be drawn with ClearType, such as bitmap fonts. In that case
// GetAlphaTextureBounds() supposedly returns an empty RECT, but I haven't tested that yet.
if !unsafe { IsRectEmpty(&bounds) }.as_bool() {
Ok(Bounds {
origin: point(0.into(), 0.into()),
size: size(0.into(), 0.into()),
origin: point(bounds.left.into(), bounds.top.into()),
size: size(
(bounds.right - bounds.left).into(),
(bounds.bottom - bounds.top).into(),
),
})
} else {
Ok(Bounds {
origin: point(
((bounds.left * params.scale_factor).ceil() as i32).into(),
((bounds.top * params.scale_factor).ceil() as i32 + y_offset).into(),
),
size: size(
(((bounds.right - bounds.left) * params.scale_factor).ceil() as i32).into(),
(((bounds.bottom - bounds.top) * params.scale_factor).ceil() as i32
+ extra_height)
.into(),
),
})
// If it's empty, retry with grayscale AA.
let bounds =
unsafe { glyph_analysis.GetAlphaTextureBounds(DWRITE_TEXTURE_ALIASED_1x1)? };
if bounds.right < bounds.left {
Ok(Bounds {
origin: point(0.into(), 0.into()),
size: size(0.into(), 0.into()),
})
} else {
Ok(Bounds {
origin: point(bounds.left.into(), bounds.top.into()),
size: size(
(bounds.right - bounds.left).into(),
(bounds.bottom - bounds.top).into(),
),
})
}
}
}
@ -731,7 +849,95 @@ impl DirectWriteState {
anyhow::bail!("glyph bounds are empty");
}
let font_info = &self.fonts[params.font_id.0];
let bitmap_data = if params.is_emoji {
if let Ok(color) = self.rasterize_color(&params, glyph_bounds) {
color
} else {
let monochrome = self.rasterize_monochrome(params, glyph_bounds)?;
monochrome
.into_iter()
.flat_map(|pixel| [0, 0, 0, pixel])
.collect::<Vec<_>>()
}
} else {
self.rasterize_monochrome(params, glyph_bounds)?
};
Ok((glyph_bounds.size, bitmap_data))
}
fn rasterize_monochrome(
&self,
params: &RenderGlyphParams,
glyph_bounds: Bounds<DevicePixels>,
) -> Result<Vec<u8>> {
let mut bitmap_data =
vec![0u8; glyph_bounds.size.width.0 as usize * glyph_bounds.size.height.0 as usize * 3];
let glyph_analysis = self.create_glyph_run_analysis(params)?;
unsafe {
glyph_analysis.CreateAlphaTexture(
// We're using cleartype not grayscale for monochrome is because it provides better quality
DWRITE_TEXTURE_CLEARTYPE_3x1,
&RECT {
left: glyph_bounds.origin.x.0,
top: glyph_bounds.origin.y.0,
right: glyph_bounds.size.width.0 + glyph_bounds.origin.x.0,
bottom: glyph_bounds.size.height.0 + glyph_bounds.origin.y.0,
},
&mut bitmap_data,
)?;
}
let bitmap_factory = self.components.bitmap_factory.resolve()?;
let bitmap = unsafe {
bitmap_factory.CreateBitmapFromMemory(
glyph_bounds.size.width.0 as u32,
glyph_bounds.size.height.0 as u32,
&GUID_WICPixelFormat24bppRGB,
glyph_bounds.size.width.0 as u32 * 3,
&bitmap_data,
)
}?;
let grayscale_bitmap =
unsafe { WICConvertBitmapSource(&GUID_WICPixelFormat8bppGray, &bitmap) }?;
let mut bitmap_data =
vec![0u8; glyph_bounds.size.width.0 as usize * glyph_bounds.size.height.0 as usize];
unsafe {
grayscale_bitmap.CopyPixels(
std::ptr::null() as _,
glyph_bounds.size.width.0 as u32,
&mut bitmap_data,
)
}?;
Ok(bitmap_data)
}
fn rasterize_color(
&self,
params: &RenderGlyphParams,
glyph_bounds: Bounds<DevicePixels>,
) -> Result<Vec<u8>> {
let bitmap_size = glyph_bounds.size;
let subpixel_shift = params
.subpixel_variant
.map(|v| v as f32 / SUBPIXEL_VARIANTS as f32);
let baseline_origin_x = subpixel_shift.x / params.scale_factor;
let baseline_origin_y = subpixel_shift.y / params.scale_factor;
let transform = DWRITE_MATRIX {
m11: params.scale_factor,
m12: 0.0,
m21: 0.0,
m22: params.scale_factor,
dx: 0.0,
dy: 0.0,
};
let font = &self.fonts[params.font_id.0];
let glyph_id = [params.glyph_id.0 as u16];
let advance = [glyph_bounds.size.width.0 as f32];
let offset = [DWRITE_GLYPH_OFFSET {
@ -739,7 +945,7 @@ impl DirectWriteState {
ascenderOffset: glyph_bounds.origin.y.0 as f32 / params.scale_factor,
}];
let glyph_run = DWRITE_GLYPH_RUN {
fontFace: unsafe { std::mem::transmute_copy(&font_info.font_face) },
fontFace: unsafe { std::mem::transmute_copy(&font.font_face) },
fontEmSize: params.font_size.0,
glyphCount: 1,
glyphIndices: glyph_id.as_ptr(),
@ -749,160 +955,254 @@ impl DirectWriteState {
bidiLevel: 0,
};
// Add an extra pixel when the subpixel variant isn't zero to make room for anti-aliasing.
let mut bitmap_size = glyph_bounds.size;
if params.subpixel_variant.x > 0 {
bitmap_size.width += DevicePixels(1);
}
if params.subpixel_variant.y > 0 {
bitmap_size.height += DevicePixels(1);
}
let bitmap_size = bitmap_size;
// todo: support formats other than COLR
let color_enumerator = unsafe {
self.components.factory.TranslateColorGlyphRun(
Vector2::new(baseline_origin_x, baseline_origin_y),
&glyph_run,
None,
DWRITE_GLYPH_IMAGE_FORMATS_COLR,
DWRITE_MEASURING_MODE_NATURAL,
Some(&transform),
0,
)
}?;
let total_bytes;
let bitmap_format;
let render_target_property;
let bitmap_width;
let bitmap_height;
let bitmap_stride;
let bitmap_dpi;
if params.is_emoji {
total_bytes = bitmap_size.height.0 as usize * bitmap_size.width.0 as usize * 4;
bitmap_format = &GUID_WICPixelFormat32bppPBGRA;
render_target_property = get_render_target_property(
DXGI_FORMAT_B8G8R8A8_UNORM,
D2D1_ALPHA_MODE_PREMULTIPLIED,
);
bitmap_width = bitmap_size.width.0 as u32;
bitmap_height = bitmap_size.height.0 as u32;
bitmap_stride = bitmap_size.width.0 as u32 * 4;
bitmap_dpi = 96.0;
} else {
total_bytes = bitmap_size.height.0 as usize * bitmap_size.width.0 as usize;
bitmap_format = &GUID_WICPixelFormat8bppAlpha;
render_target_property =
get_render_target_property(DXGI_FORMAT_A8_UNORM, D2D1_ALPHA_MODE_STRAIGHT);
bitmap_width = bitmap_size.width.0 as u32 * 2;
bitmap_height = bitmap_size.height.0 as u32 * 2;
bitmap_stride = bitmap_size.width.0 as u32;
bitmap_dpi = 192.0;
let mut glyph_layers = Vec::new();
loop {
let color_run = unsafe { color_enumerator.GetCurrentRun() }?;
let color_run = unsafe { &*color_run };
let image_format = color_run.glyphImageFormat & !DWRITE_GLYPH_IMAGE_FORMATS_TRUETYPE;
if image_format == DWRITE_GLYPH_IMAGE_FORMATS_COLR {
let color_analysis = unsafe {
self.components.factory.CreateGlyphRunAnalysis(
&color_run.Base.glyphRun as *const _,
Some(&transform),
DWRITE_RENDERING_MODE1_NATURAL_SYMMETRIC,
DWRITE_MEASURING_MODE_NATURAL,
DWRITE_GRID_FIT_MODE_DEFAULT,
DWRITE_TEXT_ANTIALIAS_MODE_CLEARTYPE,
baseline_origin_x,
baseline_origin_y,
)
}?;
let color_bounds =
unsafe { color_analysis.GetAlphaTextureBounds(DWRITE_TEXTURE_CLEARTYPE_3x1) }?;
let color_size = size(
color_bounds.right - color_bounds.left,
color_bounds.bottom - color_bounds.top,
);
if color_size.width > 0 && color_size.height > 0 {
let mut alpha_data =
vec![0u8; (color_size.width * color_size.height * 3) as usize];
unsafe {
color_analysis.CreateAlphaTexture(
DWRITE_TEXTURE_CLEARTYPE_3x1,
&color_bounds,
&mut alpha_data,
)
}?;
let run_color = {
let run_color = color_run.Base.runColor;
Rgba {
r: run_color.r,
g: run_color.g,
b: run_color.b,
a: run_color.a,
}
};
let bounds = bounds(point(color_bounds.left, color_bounds.top), color_size);
let alpha_data = alpha_data
.chunks_exact(3)
.flat_map(|chunk| [chunk[0], chunk[1], chunk[2], 255])
.collect::<Vec<_>>();
glyph_layers.push(GlyphLayerTexture::new(
&self.components.gpu_state,
run_color,
bounds,
&alpha_data,
)?);
}
}
let has_next = unsafe { color_enumerator.MoveNext() }
.map(|e| e.as_bool())
.unwrap_or(false);
if !has_next {
break;
}
}
let bitmap_factory = self.components.bitmap_factory.resolve()?;
unsafe {
let bitmap = bitmap_factory.CreateBitmap(
bitmap_width,
bitmap_height,
bitmap_format,
WICBitmapCacheOnLoad,
)?;
let render_target = self
.components
.d2d1_factory
.CreateWicBitmapRenderTarget(&bitmap, &render_target_property)?;
let brush = render_target.CreateSolidColorBrush(&BRUSH_COLOR, None)?;
let subpixel_shift = params
.subpixel_variant
.map(|v| v as f32 / SUBPIXEL_VARIANTS as f32);
let baseline_origin = Vector2 {
X: subpixel_shift.x / params.scale_factor,
Y: subpixel_shift.y / params.scale_factor,
let gpu_state = &self.components.gpu_state;
let params_buffer = {
let desc = D3D11_BUFFER_DESC {
ByteWidth: std::mem::size_of::<GlyphLayerTextureParams>() as u32,
Usage: D3D11_USAGE_DYNAMIC,
BindFlags: D3D11_BIND_CONSTANT_BUFFER.0 as u32,
CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32,
MiscFlags: 0,
StructureByteStride: 0,
};
// This `cast()` action here should never fail since we are running on Win10+, and
// ID2D1DeviceContext4 requires Win8+
let render_target = render_target.cast::<ID2D1DeviceContext4>().unwrap();
render_target.SetUnitMode(D2D1_UNIT_MODE_DIPS);
render_target.SetDpi(
bitmap_dpi * params.scale_factor,
bitmap_dpi * params.scale_factor,
);
render_target.SetTextRenderingParams(&self.components.render_context.params);
render_target.BeginDraw();
let mut buffer = None;
unsafe {
gpu_state
.device
.CreateBuffer(&desc, None, Some(&mut buffer))
}?;
[buffer]
};
if params.is_emoji {
// WARN: only DWRITE_GLYPH_IMAGE_FORMATS_COLR has been tested
let enumerator = self.components.factory.TranslateColorGlyphRun(
baseline_origin,
&glyph_run as _,
None,
DWRITE_GLYPH_IMAGE_FORMATS_COLR
| DWRITE_GLYPH_IMAGE_FORMATS_SVG
| DWRITE_GLYPH_IMAGE_FORMATS_PNG
| DWRITE_GLYPH_IMAGE_FORMATS_JPEG
| DWRITE_GLYPH_IMAGE_FORMATS_PREMULTIPLIED_B8G8R8A8,
DWRITE_MEASURING_MODE_NATURAL,
None,
let render_target_texture = {
let mut texture = None;
let desc = D3D11_TEXTURE2D_DESC {
Width: bitmap_size.width.0 as u32,
Height: bitmap_size.height.0 as u32,
MipLevels: 1,
ArraySize: 1,
Format: DXGI_FORMAT_B8G8R8A8_UNORM,
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
Usage: D3D11_USAGE_DEFAULT,
BindFlags: D3D11_BIND_RENDER_TARGET.0 as u32,
CPUAccessFlags: 0,
MiscFlags: 0,
};
unsafe {
gpu_state
.device
.CreateTexture2D(&desc, None, Some(&mut texture))
}?;
texture.unwrap()
};
let render_target_view = {
let desc = D3D11_RENDER_TARGET_VIEW_DESC {
Format: DXGI_FORMAT_B8G8R8A8_UNORM,
ViewDimension: D3D11_RTV_DIMENSION_TEXTURE2D,
Anonymous: D3D11_RENDER_TARGET_VIEW_DESC_0 {
Texture2D: D3D11_TEX2D_RTV { MipSlice: 0 },
},
};
let mut rtv = None;
unsafe {
gpu_state.device.CreateRenderTargetView(
&render_target_texture,
Some(&desc),
Some(&mut rtv),
)
}?;
[rtv]
};
let staging_texture = {
let mut texture = None;
let desc = D3D11_TEXTURE2D_DESC {
Width: bitmap_size.width.0 as u32,
Height: bitmap_size.height.0 as u32,
MipLevels: 1,
ArraySize: 1,
Format: DXGI_FORMAT_B8G8R8A8_UNORM,
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
Usage: D3D11_USAGE_STAGING,
BindFlags: 0,
CPUAccessFlags: D3D11_CPU_ACCESS_READ.0 as u32,
MiscFlags: 0,
};
unsafe {
gpu_state
.device
.CreateTexture2D(&desc, None, Some(&mut texture))
}?;
texture.unwrap()
};
let device_context = &gpu_state.device_context;
unsafe { device_context.IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP) };
unsafe { device_context.VSSetShader(&gpu_state.vertex_shader, None) };
unsafe { device_context.PSSetShader(&gpu_state.pixel_shader, None) };
unsafe { device_context.VSSetConstantBuffers(0, Some(&params_buffer)) };
unsafe { device_context.PSSetConstantBuffers(0, Some(&params_buffer)) };
unsafe { device_context.OMSetRenderTargets(Some(&render_target_view), None) };
unsafe { device_context.PSSetSamplers(0, Some(&gpu_state.sampler)) };
unsafe { device_context.OMSetBlendState(&gpu_state.blend_state, None, 0xffffffff) };
for layer in glyph_layers {
let params = GlyphLayerTextureParams {
run_color: layer.run_color,
bounds: layer.bounds,
};
unsafe {
let mut dest = std::mem::zeroed();
gpu_state.device_context.Map(
params_buffer[0].as_ref().unwrap(),
0,
D3D11_MAP_WRITE_DISCARD,
0,
Some(&mut dest),
)?;
while enumerator.MoveNext().is_ok() {
let Ok(color_glyph) = enumerator.GetCurrentRun() else {
break;
};
let color_glyph = &*color_glyph;
let brush_color = translate_color(&color_glyph.Base.runColor);
brush.SetColor(&brush_color);
match color_glyph.glyphImageFormat {
DWRITE_GLYPH_IMAGE_FORMATS_PNG
| DWRITE_GLYPH_IMAGE_FORMATS_JPEG
| DWRITE_GLYPH_IMAGE_FORMATS_PREMULTIPLIED_B8G8R8A8 => render_target
.DrawColorBitmapGlyphRun(
color_glyph.glyphImageFormat,
baseline_origin,
&color_glyph.Base.glyphRun,
color_glyph.measuringMode,
D2D1_COLOR_BITMAP_GLYPH_SNAP_OPTION_DEFAULT,
),
DWRITE_GLYPH_IMAGE_FORMATS_SVG => render_target.DrawSvgGlyphRun(
baseline_origin,
&color_glyph.Base.glyphRun,
&brush,
None,
color_glyph.Base.paletteIndex as u32,
color_glyph.measuringMode,
),
_ => render_target.DrawGlyphRun(
baseline_origin,
&color_glyph.Base.glyphRun,
Some(color_glyph.Base.glyphRunDescription as *const _),
&brush,
color_glyph.measuringMode,
),
}
}
} else {
render_target.DrawGlyphRun(
baseline_origin,
&glyph_run,
None,
&brush,
DWRITE_MEASURING_MODE_NATURAL,
);
}
render_target.EndDraw(None, None)?;
std::ptr::copy_nonoverlapping(&params as *const _, dest.pData as *mut _, 1);
gpu_state
.device_context
.Unmap(params_buffer[0].as_ref().unwrap(), 0);
};
let mut raw_data = vec![0u8; total_bytes];
if params.is_emoji {
bitmap.CopyPixels(std::ptr::null() as _, bitmap_stride, &mut raw_data)?;
// Convert from BGRA with premultiplied alpha to BGRA with straight alpha.
for pixel in raw_data.chunks_exact_mut(4) {
let a = pixel[3] as f32 / 255.;
pixel[0] = (pixel[0] as f32 / a) as u8;
pixel[1] = (pixel[1] as f32 / a) as u8;
pixel[2] = (pixel[2] as f32 / a) as u8;
}
} else {
let scaler = bitmap_factory.CreateBitmapScaler()?;
scaler.Initialize(
&bitmap,
bitmap_size.width.0 as u32,
bitmap_size.height.0 as u32,
WICBitmapInterpolationModeHighQualityCubic,
)?;
scaler.CopyPixels(std::ptr::null() as _, bitmap_stride, &mut raw_data)?;
}
Ok((bitmap_size, raw_data))
let texture = [Some(layer.texture_view)];
unsafe { device_context.PSSetShaderResources(0, Some(&texture)) };
let viewport = [D3D11_VIEWPORT {
TopLeftX: layer.bounds.origin.x as f32,
TopLeftY: layer.bounds.origin.y as f32,
Width: layer.bounds.size.width as f32,
Height: layer.bounds.size.height as f32,
MinDepth: 0.0,
MaxDepth: 1.0,
}];
unsafe { device_context.RSSetViewports(Some(&viewport)) };
unsafe { device_context.Draw(4, 0) };
}
unsafe { device_context.CopyResource(&staging_texture, &render_target_texture) };
let mapped_data = {
let mut mapped_data = D3D11_MAPPED_SUBRESOURCE::default();
unsafe {
device_context.Map(
&staging_texture,
0,
D3D11_MAP_READ,
0,
Some(&mut mapped_data),
)
}?;
mapped_data
};
let mut rasterized =
vec![0u8; (bitmap_size.width.0 as u32 * bitmap_size.height.0 as u32 * 4) as usize];
for y in 0..bitmap_size.height.0 as usize {
let width = bitmap_size.width.0 as usize;
unsafe {
std::ptr::copy_nonoverlapping::<u8>(
(mapped_data.pData as *const u8).byte_add(mapped_data.RowPitch as usize * y),
rasterized
.as_mut_ptr()
.byte_add(width * y * std::mem::size_of::<u32>()),
width * std::mem::size_of::<u32>(),
)
};
}
Ok(rasterized)
}
fn get_typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>> {
@ -976,6 +1276,84 @@ impl Drop for DirectWriteState {
}
}
struct GlyphLayerTexture {
run_color: Rgba,
bounds: Bounds<i32>,
texture_view: ID3D11ShaderResourceView,
// holding on to the texture to not RAII drop it
_texture: ID3D11Texture2D,
}
impl GlyphLayerTexture {
pub fn new(
gpu_state: &GPUState,
run_color: Rgba,
bounds: Bounds<i32>,
alpha_data: &[u8],
) -> Result<Self> {
let texture_size = bounds.size;
let desc = D3D11_TEXTURE2D_DESC {
Width: texture_size.width as u32,
Height: texture_size.height as u32,
MipLevels: 1,
ArraySize: 1,
Format: DXGI_FORMAT_R8G8B8A8_UNORM,
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
Usage: D3D11_USAGE_DEFAULT,
BindFlags: D3D11_BIND_SHADER_RESOURCE.0 as u32,
CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32,
MiscFlags: 0,
};
let texture = {
let mut texture: Option<ID3D11Texture2D> = None;
unsafe {
gpu_state
.device
.CreateTexture2D(&desc, None, Some(&mut texture))?
};
texture.unwrap()
};
let texture_view = {
let mut view: Option<ID3D11ShaderResourceView> = None;
unsafe {
gpu_state
.device
.CreateShaderResourceView(&texture, None, Some(&mut view))?
};
view.unwrap()
};
unsafe {
gpu_state.device_context.UpdateSubresource(
&texture,
0,
None,
alpha_data.as_ptr() as _,
(texture_size.width * 4) as u32,
0,
)
};
Ok(GlyphLayerTexture {
run_color,
bounds,
texture_view,
_texture: texture,
})
}
}
#[repr(C)]
struct GlyphLayerTextureParams {
bounds: Bounds<i32>,
run_color: Rgba,
}
struct TextRendererWrapper(pub IDWriteTextRenderer);
impl TextRendererWrapper {
@ -1470,16 +1848,6 @@ fn get_name(string: IDWriteLocalizedStrings, locale: &str) -> Result<String> {
Ok(String::from_utf16_lossy(&name_vec[..name_length]))
}
#[inline]
fn translate_color(color: &DWRITE_COLOR_F) -> D2D1_COLOR_F {
D2D1_COLOR_F {
r: color.r,
g: color.g,
b: color.b,
a: color.a,
}
}
fn get_system_ui_font_name() -> SharedString {
unsafe {
let mut info: LOGFONTW = std::mem::zeroed();
@ -1504,24 +1872,6 @@ fn get_system_ui_font_name() -> SharedString {
}
}
#[inline]
fn get_render_target_property(
pixel_format: DXGI_FORMAT,
alpha_mode: D2D1_ALPHA_MODE,
) -> D2D1_RENDER_TARGET_PROPERTIES {
D2D1_RENDER_TARGET_PROPERTIES {
r#type: D2D1_RENDER_TARGET_TYPE_DEFAULT,
pixelFormat: D2D1_PIXEL_FORMAT {
format: pixel_format,
alphaMode: alpha_mode,
},
dpiX: 96.0,
dpiY: 96.0,
usage: D2D1_RENDER_TARGET_USAGE_NONE,
minLevel: D2D1_FEATURE_LEVEL_DEFAULT,
}
}
// One would think that with newer DirectWrite method: IDWriteFontFace4::GetGlyphImageFormats
// but that doesn't seem to work for some glyphs, say ❤
fn is_color_glyph(
@ -1561,12 +1911,6 @@ fn is_color_glyph(
}
const DEFAULT_LOCALE_NAME: PCWSTR = windows::core::w!("en-US");
const BRUSH_COLOR: D2D1_COLOR_F = D2D1_COLOR_F {
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
};
#[cfg(test)]
mod tests {

View file

@ -0,0 +1,309 @@
use collections::FxHashMap;
use etagere::BucketedAtlasAllocator;
use parking_lot::Mutex;
use windows::Win32::Graphics::{
Direct3D11::{
D3D11_BIND_SHADER_RESOURCE, D3D11_BOX, D3D11_CPU_ACCESS_WRITE, D3D11_TEXTURE2D_DESC,
D3D11_USAGE_DEFAULT, ID3D11Device, ID3D11DeviceContext, ID3D11ShaderResourceView,
ID3D11Texture2D,
},
Dxgi::Common::*,
};
use crate::{
AtlasKey, AtlasTextureId, AtlasTextureKind, AtlasTile, Bounds, DevicePixels, PlatformAtlas,
Point, Size, platform::AtlasTextureList,
};
pub(crate) struct DirectXAtlas(Mutex<DirectXAtlasState>);
struct DirectXAtlasState {
device: ID3D11Device,
device_context: ID3D11DeviceContext,
monochrome_textures: AtlasTextureList<DirectXAtlasTexture>,
polychrome_textures: AtlasTextureList<DirectXAtlasTexture>,
tiles_by_key: FxHashMap<AtlasKey, AtlasTile>,
}
struct DirectXAtlasTexture {
id: AtlasTextureId,
bytes_per_pixel: u32,
allocator: BucketedAtlasAllocator,
texture: ID3D11Texture2D,
view: [Option<ID3D11ShaderResourceView>; 1],
live_atlas_keys: u32,
}
impl DirectXAtlas {
pub(crate) fn new(device: &ID3D11Device, device_context: &ID3D11DeviceContext) -> Self {
DirectXAtlas(Mutex::new(DirectXAtlasState {
device: device.clone(),
device_context: device_context.clone(),
monochrome_textures: Default::default(),
polychrome_textures: Default::default(),
tiles_by_key: Default::default(),
}))
}
pub(crate) fn get_texture_view(
&self,
id: AtlasTextureId,
) -> [Option<ID3D11ShaderResourceView>; 1] {
let lock = self.0.lock();
let tex = lock.texture(id);
tex.view.clone()
}
pub(crate) fn handle_device_lost(
&self,
device: &ID3D11Device,
device_context: &ID3D11DeviceContext,
) {
let mut lock = self.0.lock();
lock.device = device.clone();
lock.device_context = device_context.clone();
lock.monochrome_textures = AtlasTextureList::default();
lock.polychrome_textures = AtlasTextureList::default();
lock.tiles_by_key.clear();
}
}
impl PlatformAtlas for DirectXAtlas {
fn get_or_insert_with<'a>(
&self,
key: &AtlasKey,
build: &mut dyn FnMut() -> anyhow::Result<
Option<(Size<DevicePixels>, std::borrow::Cow<'a, [u8]>)>,
>,
) -> anyhow::Result<Option<AtlasTile>> {
let mut lock = self.0.lock();
if let Some(tile) = lock.tiles_by_key.get(key) {
Ok(Some(tile.clone()))
} else {
let Some((size, bytes)) = build()? else {
return Ok(None);
};
let tile = lock
.allocate(size, key.texture_kind())
.ok_or_else(|| anyhow::anyhow!("failed to allocate"))?;
let texture = lock.texture(tile.texture_id);
texture.upload(&lock.device_context, tile.bounds, &bytes);
lock.tiles_by_key.insert(key.clone(), tile.clone());
Ok(Some(tile))
}
}
fn remove(&self, key: &AtlasKey) {
let mut lock = self.0.lock();
let Some(id) = lock.tiles_by_key.remove(key).map(|tile| tile.texture_id) else {
return;
};
let textures = match id.kind {
AtlasTextureKind::Monochrome => &mut lock.monochrome_textures,
AtlasTextureKind::Polychrome => &mut lock.polychrome_textures,
};
let Some(texture_slot) = textures.textures.get_mut(id.index as usize) else {
return;
};
if let Some(mut texture) = texture_slot.take() {
texture.decrement_ref_count();
if texture.is_unreferenced() {
textures.free_list.push(texture.id.index as usize);
lock.tiles_by_key.remove(key);
} else {
*texture_slot = Some(texture);
}
}
}
}
impl DirectXAtlasState {
fn allocate(
&mut self,
size: Size<DevicePixels>,
texture_kind: AtlasTextureKind,
) -> Option<AtlasTile> {
{
let textures = match texture_kind {
AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
};
if let Some(tile) = textures
.iter_mut()
.rev()
.find_map(|texture| texture.allocate(size))
{
return Some(tile);
}
}
let texture = self.push_texture(size, texture_kind)?;
texture.allocate(size)
}
fn push_texture(
&mut self,
min_size: Size<DevicePixels>,
kind: AtlasTextureKind,
) -> Option<&mut DirectXAtlasTexture> {
const DEFAULT_ATLAS_SIZE: Size<DevicePixels> = Size {
width: DevicePixels(1024),
height: DevicePixels(1024),
};
// Max texture size for DirectX. See:
// https://learn.microsoft.com/en-us/windows/win32/direct3d11/overviews-direct3d-11-resources-limits
const MAX_ATLAS_SIZE: Size<DevicePixels> = Size {
width: DevicePixels(16384),
height: DevicePixels(16384),
};
let size = min_size.min(&MAX_ATLAS_SIZE).max(&DEFAULT_ATLAS_SIZE);
let pixel_format;
let bind_flag;
let bytes_per_pixel;
match kind {
AtlasTextureKind::Monochrome => {
pixel_format = DXGI_FORMAT_R8_UNORM;
bind_flag = D3D11_BIND_SHADER_RESOURCE;
bytes_per_pixel = 1;
}
AtlasTextureKind::Polychrome => {
pixel_format = DXGI_FORMAT_B8G8R8A8_UNORM;
bind_flag = D3D11_BIND_SHADER_RESOURCE;
bytes_per_pixel = 4;
}
}
let texture_desc = D3D11_TEXTURE2D_DESC {
Width: size.width.0 as u32,
Height: size.height.0 as u32,
MipLevels: 1,
ArraySize: 1,
Format: pixel_format,
SampleDesc: DXGI_SAMPLE_DESC {
Count: 1,
Quality: 0,
},
Usage: D3D11_USAGE_DEFAULT,
BindFlags: bind_flag.0 as u32,
CPUAccessFlags: D3D11_CPU_ACCESS_WRITE.0 as u32,
MiscFlags: 0,
};
let mut texture: Option<ID3D11Texture2D> = None;
unsafe {
// This only returns None if the device is lost, which we will recreate later.
// So it's ok to return None here.
self.device
.CreateTexture2D(&texture_desc, None, Some(&mut texture))
.ok()?;
}
let texture = texture.unwrap();
let texture_list = match kind {
AtlasTextureKind::Monochrome => &mut self.monochrome_textures,
AtlasTextureKind::Polychrome => &mut self.polychrome_textures,
};
let index = texture_list.free_list.pop();
let view = unsafe {
let mut view = None;
self.device
.CreateShaderResourceView(&texture, None, Some(&mut view))
.ok()?;
[view]
};
let atlas_texture = DirectXAtlasTexture {
id: AtlasTextureId {
index: index.unwrap_or(texture_list.textures.len()) as u32,
kind,
},
bytes_per_pixel,
allocator: etagere::BucketedAtlasAllocator::new(size.into()),
texture,
view,
live_atlas_keys: 0,
};
if let Some(ix) = index {
texture_list.textures[ix] = Some(atlas_texture);
texture_list.textures.get_mut(ix).unwrap().as_mut()
} else {
texture_list.textures.push(Some(atlas_texture));
texture_list.textures.last_mut().unwrap().as_mut()
}
}
fn texture(&self, id: AtlasTextureId) -> &DirectXAtlasTexture {
let textures = match id.kind {
crate::AtlasTextureKind::Monochrome => &self.monochrome_textures,
crate::AtlasTextureKind::Polychrome => &self.polychrome_textures,
};
textures[id.index as usize].as_ref().unwrap()
}
}
impl DirectXAtlasTexture {
fn allocate(&mut self, size: Size<DevicePixels>) -> Option<AtlasTile> {
let allocation = self.allocator.allocate(size.into())?;
let tile = AtlasTile {
texture_id: self.id,
tile_id: allocation.id.into(),
bounds: Bounds {
origin: allocation.rectangle.min.into(),
size,
},
padding: 0,
};
self.live_atlas_keys += 1;
Some(tile)
}
fn upload(
&self,
device_context: &ID3D11DeviceContext,
bounds: Bounds<DevicePixels>,
bytes: &[u8],
) {
unsafe {
device_context.UpdateSubresource(
&self.texture,
0,
Some(&D3D11_BOX {
left: bounds.left().0 as u32,
top: bounds.top().0 as u32,
front: 0,
right: bounds.right().0 as u32,
bottom: bounds.bottom().0 as u32,
back: 1,
}),
bytes.as_ptr() as _,
bounds.size.width.to_bytes(self.bytes_per_pixel as u8),
0,
);
}
}
fn decrement_ref_count(&mut self) {
self.live_atlas_keys -= 1;
}
fn is_unreferenced(&mut self) -> bool {
self.live_atlas_keys == 0
}
}
impl From<Size<DevicePixels>> for etagere::Size {
fn from(size: Size<DevicePixels>) -> Self {
etagere::Size::new(size.width.into(), size.height.into())
}
}
impl From<etagere::Point> for Point<DevicePixels> {
fn from(value: etagere::Point) -> Self {
Point {
x: DevicePixels::from(value.x),
y: DevicePixels::from(value.y),
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -23,6 +23,7 @@ pub(crate) const WM_GPUI_CURSOR_STYLE_CHANGED: u32 = WM_USER + 1;
pub(crate) const WM_GPUI_CLOSE_ONE_WINDOW: u32 = WM_USER + 2;
pub(crate) const WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD: u32 = WM_USER + 3;
pub(crate) const WM_GPUI_DOCK_MENU_ACTION: u32 = WM_USER + 4;
pub(crate) const WM_GPUI_FORCE_UPDATE_WINDOW: u32 = WM_USER + 5;
const SIZE_MOVE_LOOP_TIMER_ID: usize = 1;
const AUTO_HIDE_TASKBAR_THICKNESS_PX: i32 = 1;
@ -37,6 +38,7 @@ pub(crate) fn handle_msg(
let handled = match msg {
WM_ACTIVATE => handle_activate_msg(wparam, state_ptr),
WM_CREATE => handle_create_msg(handle, state_ptr),
WM_DEVICECHANGE => handle_device_change_msg(handle, wparam, state_ptr),
WM_MOVE => handle_move_msg(handle, lparam, state_ptr),
WM_SIZE => handle_size_msg(wparam, lparam, state_ptr),
WM_GETMINMAXINFO => handle_get_min_max_info_msg(lparam, state_ptr),
@ -48,7 +50,7 @@ pub(crate) fn handle_msg(
WM_DISPLAYCHANGE => handle_display_change_msg(handle, state_ptr),
WM_NCHITTEST => handle_hit_test_msg(handle, msg, wparam, lparam, state_ptr),
WM_PAINT => handle_paint_msg(handle, state_ptr),
WM_CLOSE => handle_close_msg(handle, state_ptr),
WM_CLOSE => handle_close_msg(state_ptr),
WM_DESTROY => handle_destroy_msg(handle, state_ptr),
WM_MOUSEMOVE => handle_mouse_move_msg(handle, lparam, wparam, state_ptr),
WM_MOUSELEAVE | WM_NCMOUSELEAVE => handle_mouse_leave_msg(state_ptr),
@ -96,6 +98,7 @@ pub(crate) fn handle_msg(
WM_SETTINGCHANGE => handle_system_settings_changed(handle, wparam, lparam, state_ptr),
WM_INPUTLANGCHANGE => handle_input_language_changed(lparam, state_ptr),
WM_GPUI_CURSOR_STYLE_CHANGED => handle_cursor_changed(lparam, state_ptr),
WM_GPUI_FORCE_UPDATE_WINDOW => draw_window(handle, true, state_ptr),
_ => None,
};
if let Some(n) = handled {
@ -181,11 +184,9 @@ fn handle_size_msg(
let new_size = size(DevicePixels(width), DevicePixels(height));
let scale_factor = lock.scale_factor;
if lock.restore_from_minimized.is_some() {
lock.renderer
.update_drawable_size_even_if_unchanged(new_size);
lock.callbacks.request_frame = lock.restore_from_minimized.take();
} else {
lock.renderer.update_drawable_size(new_size);
lock.renderer.resize(new_size).log_err();
}
let new_size = new_size.to_pixels(scale_factor);
lock.logical_size = new_size;
@ -238,40 +239,14 @@ fn handle_timer_msg(
}
fn handle_paint_msg(handle: HWND, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
let mut lock = state_ptr.state.borrow_mut();
if let Some(mut request_frame) = lock.callbacks.request_frame.take() {
drop(lock);
request_frame(Default::default());
state_ptr.state.borrow_mut().callbacks.request_frame = Some(request_frame);
}
unsafe { ValidateRect(Some(handle), None).ok().log_err() };
Some(0)
draw_window(handle, false, state_ptr)
}
fn handle_close_msg(handle: HWND, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
let mut lock = state_ptr.state.borrow_mut();
let output = if let Some(mut callback) = lock.callbacks.should_close.take() {
drop(lock);
let should_close = callback();
state_ptr.state.borrow_mut().callbacks.should_close = Some(callback);
if should_close { None } else { Some(0) }
} else {
None
};
// Workaround as window close animation is not played with `WS_EX_LAYERED` enabled.
if output.is_none() {
unsafe {
let current_style = get_window_long(handle, GWL_EXSTYLE);
set_window_long(
handle,
GWL_EXSTYLE,
current_style & !WS_EX_LAYERED.0 as isize,
);
}
}
output
fn handle_close_msg(state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
let mut callback = state_ptr.state.borrow_mut().callbacks.should_close.take()?;
let should_close = callback();
state_ptr.state.borrow_mut().callbacks.should_close = Some(callback);
if should_close { None } else { Some(0) }
}
fn handle_destroy_msg(handle: HWND, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
@ -1223,6 +1198,53 @@ fn handle_input_language_changed(
Some(0)
}
fn handle_device_change_msg(
handle: HWND,
wparam: WPARAM,
state_ptr: Rc<WindowsWindowStatePtr>,
) -> Option<isize> {
if wparam.0 == DBT_DEVNODES_CHANGED as usize {
// The reason for sending this message is to actually trigger a redraw of the window.
unsafe {
PostMessageW(
Some(handle),
WM_GPUI_FORCE_UPDATE_WINDOW,
WPARAM(0),
LPARAM(0),
)
.log_err();
}
// If the GPU device is lost, this redraw will take care of recreating the device context.
// The WM_GPUI_FORCE_UPDATE_WINDOW message will take care of redrawing the window, after
// the device context has been recreated.
draw_window(handle, true, state_ptr)
} else {
// Other device change messages are not handled.
None
}
}
#[inline]
fn draw_window(
handle: HWND,
force_render: bool,
state_ptr: Rc<WindowsWindowStatePtr>,
) -> Option<isize> {
let mut request_frame = state_ptr
.state
.borrow_mut()
.callbacks
.request_frame
.take()?;
request_frame(RequestFrameOptions {
require_presentation: false,
force_render,
});
state_ptr.state.borrow_mut().callbacks.request_frame = Some(request_frame);
unsafe { ValidateRect(Some(handle), None).ok().log_err() };
Some(0)
}
#[inline]
fn parse_char_message(wparam: WPARAM, state_ptr: &Rc<WindowsWindowStatePtr>) -> Option<String> {
let code_point = wparam.loword();

View file

@ -28,13 +28,12 @@ use windows::{
core::*,
};
use crate::{platform::blade::BladeContext, *};
use crate::*;
pub(crate) struct WindowsPlatform {
state: RefCell<WindowsPlatformState>,
raw_window_handles: RwLock<SmallVec<[HWND; 4]>>,
// The below members will never change throughout the entire lifecycle of the app.
gpu_context: BladeContext,
icon: HICON,
main_receiver: flume::Receiver<Runnable>,
background_executor: BackgroundExecutor,
@ -45,6 +44,7 @@ pub(crate) struct WindowsPlatform {
drop_target_helper: IDropTargetHelper,
validation_number: usize,
main_thread_id_win32: u32,
disable_direct_composition: bool,
}
pub(crate) struct WindowsPlatformState {
@ -94,14 +94,18 @@ impl WindowsPlatform {
main_thread_id_win32,
validation_number,
));
let disable_direct_composition = std::env::var(DISABLE_DIRECT_COMPOSITION)
.is_ok_and(|value| value == "true" || value == "1");
let background_executor = BackgroundExecutor::new(dispatcher.clone());
let foreground_executor = ForegroundExecutor::new(dispatcher);
let directx_devices = DirectXDevices::new(disable_direct_composition)
.context("Unable to init directx devices.")?;
let bitmap_factory = ManuallyDrop::new(unsafe {
CoCreateInstance(&CLSID_WICImagingFactory, None, CLSCTX_INPROC_SERVER)
.context("Error creating bitmap factory.")?
});
let text_system = Arc::new(
DirectWriteTextSystem::new(&bitmap_factory)
DirectWriteTextSystem::new(&directx_devices, &bitmap_factory)
.context("Error creating DirectWriteTextSystem")?,
);
let drop_target_helper: IDropTargetHelper = unsafe {
@ -111,18 +115,17 @@ impl WindowsPlatform {
let icon = load_icon().unwrap_or_default();
let state = RefCell::new(WindowsPlatformState::new());
let raw_window_handles = RwLock::new(SmallVec::new());
let gpu_context = BladeContext::new().context("Unable to init GPU context")?;
let windows_version = WindowsVersion::new().context("Error retrieve windows version")?;
Ok(Self {
state,
raw_window_handles,
gpu_context,
icon,
main_receiver,
background_executor,
foreground_executor,
text_system,
disable_direct_composition,
windows_version,
bitmap_factory,
drop_target_helper,
@ -187,6 +190,7 @@ impl WindowsPlatform {
validation_number: self.validation_number,
main_receiver: self.main_receiver.clone(),
main_thread_id_win32: self.main_thread_id_win32,
disable_direct_composition: self.disable_direct_composition,
}
}
@ -343,27 +347,11 @@ impl Platform for WindowsPlatform {
fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>) {
on_finish_launching();
let vsync_event = unsafe { Owned::new(CreateEventW(None, false, false, None).unwrap()) };
begin_vsync(*vsync_event);
'a: loop {
let wait_result = unsafe {
MsgWaitForMultipleObjects(Some(&[*vsync_event]), false, INFINITE, QS_ALLINPUT)
};
match wait_result {
// compositor clock ticked so we should draw a frame
WAIT_EVENT(0) => self.redraw_all(),
// Windows thread messages are posted
WAIT_EVENT(1) => {
if self.handle_events() {
break 'a;
}
}
_ => {
log::error!("Something went wrong while waiting {:?}", wait_result);
break;
}
loop {
if self.handle_events() {
break;
}
self.redraw_all();
}
if let Some(ref mut callback) = self.state.borrow_mut().callbacks.quit {
@ -455,12 +443,7 @@ impl Platform for WindowsPlatform {
handle: AnyWindowHandle,
options: WindowParams,
) -> Result<Box<dyn PlatformWindow>> {
let window = WindowsWindow::new(
handle,
options,
self.generate_creation_info(),
&self.gpu_context,
)?;
let window = WindowsWindow::new(handle, options, self.generate_creation_info())?;
let handle = window.get_raw_handle();
self.raw_window_handles.write().push(handle);
@ -739,6 +722,7 @@ pub(crate) struct WindowCreationInfo {
pub(crate) validation_number: usize,
pub(crate) main_receiver: flume::Receiver<Runnable>,
pub(crate) main_thread_id_win32: u32,
pub(crate) disable_direct_composition: bool,
}
fn open_target(target: &str) {
@ -846,16 +830,6 @@ fn file_save_dialog(directory: PathBuf, window: Option<HWND>) -> Result<Option<P
Ok(Some(PathBuf::from(file_path_string)))
}
fn begin_vsync(vsync_event: HANDLE) {
let event: SafeHandle = vsync_event.into();
std::thread::spawn(move || unsafe {
loop {
windows::Win32::Graphics::Dwm::DwmFlush().log_err();
SetEvent(*event).log_err();
}
});
}
fn load_icon() -> Result<HICON> {
let module = unsafe { GetModuleHandleW(None).context("unable to get module handle")? };
let handle = unsafe {

File diff suppressed because it is too large Load diff

View file

@ -26,7 +26,6 @@ use windows::{
core::*,
};
use crate::platform::blade::{BladeContext, BladeRenderer};
use crate::*;
pub(crate) struct WindowsWindow(pub Rc<WindowsWindowStatePtr>);
@ -49,7 +48,7 @@ pub struct WindowsWindowState {
pub system_key_handled: bool,
pub hovered: bool,
pub renderer: BladeRenderer,
pub renderer: DirectXRenderer,
pub click_state: ClickState,
pub system_settings: WindowsSystemSettings,
@ -80,13 +79,12 @@ pub(crate) struct WindowsWindowStatePtr {
impl WindowsWindowState {
fn new(
hwnd: HWND,
transparent: bool,
cs: &CREATESTRUCTW,
current_cursor: Option<HCURSOR>,
display: WindowsDisplay,
gpu_context: &BladeContext,
min_size: Option<Size<Pixels>>,
appearance: WindowAppearance,
disable_direct_composition: bool,
) -> Result<Self> {
let scale_factor = {
let monitor_dpi = unsafe { GetDpiForWindow(hwnd) } as f32;
@ -103,7 +101,8 @@ impl WindowsWindowState {
};
let border_offset = WindowBorderOffset::default();
let restore_from_minimized = None;
let renderer = windows_renderer::init(gpu_context, hwnd, transparent)?;
let renderer = DirectXRenderer::new(hwnd, disable_direct_composition)
.context("Creating DirectX renderer")?;
let callbacks = Callbacks::default();
let input_handler = None;
let pending_surrogate = None;
@ -206,13 +205,12 @@ impl WindowsWindowStatePtr {
fn new(context: &WindowCreateContext, hwnd: HWND, cs: &CREATESTRUCTW) -> Result<Rc<Self>> {
let state = RefCell::new(WindowsWindowState::new(
hwnd,
context.transparent,
cs,
context.current_cursor,
context.display,
context.gpu_context,
context.min_size,
context.appearance,
context.disable_direct_composition,
)?);
Ok(Rc::new_cyclic(|this| Self {
@ -329,12 +327,11 @@ pub(crate) struct Callbacks {
pub(crate) appearance_changed: Option<Box<dyn FnMut()>>,
}
struct WindowCreateContext<'a> {
struct WindowCreateContext {
inner: Option<Result<Rc<WindowsWindowStatePtr>>>,
handle: AnyWindowHandle,
hide_title_bar: bool,
display: WindowsDisplay,
transparent: bool,
is_movable: bool,
min_size: Option<Size<Pixels>>,
executor: ForegroundExecutor,
@ -343,9 +340,9 @@ struct WindowCreateContext<'a> {
drop_target_helper: IDropTargetHelper,
validation_number: usize,
main_receiver: flume::Receiver<Runnable>,
gpu_context: &'a BladeContext,
main_thread_id_win32: u32,
appearance: WindowAppearance,
disable_direct_composition: bool,
}
impl WindowsWindow {
@ -353,7 +350,6 @@ impl WindowsWindow {
handle: AnyWindowHandle,
params: WindowParams,
creation_info: WindowCreationInfo,
gpu_context: &BladeContext,
) -> Result<Self> {
let WindowCreationInfo {
icon,
@ -364,6 +360,7 @@ impl WindowsWindow {
validation_number,
main_receiver,
main_thread_id_win32,
disable_direct_composition,
} = creation_info;
let classname = register_wnd_class(icon);
let hide_title_bar = params
@ -379,14 +376,18 @@ impl WindowsWindow {
.map(|title| title.as_ref())
.unwrap_or(""),
);
let (dwexstyle, mut dwstyle) = if params.kind == WindowKind::PopUp {
(WS_EX_TOOLWINDOW | WS_EX_LAYERED, WINDOW_STYLE(0x0))
let (mut dwexstyle, dwstyle) = if params.kind == WindowKind::PopUp {
(WS_EX_TOOLWINDOW, WINDOW_STYLE(0x0))
} else {
(
WS_EX_APPWINDOW | WS_EX_LAYERED,
WS_EX_APPWINDOW,
WS_THICKFRAME | WS_SYSMENU | WS_MAXIMIZEBOX | WS_MINIMIZEBOX,
)
};
if !disable_direct_composition {
dwexstyle |= WS_EX_NOREDIRECTIONBITMAP;
}
let hinstance = get_module_handle();
let display = if let Some(display_id) = params.display_id {
@ -401,7 +402,6 @@ impl WindowsWindow {
handle,
hide_title_bar,
display,
transparent: true,
is_movable: params.is_movable,
min_size: params.window_min_size,
executor,
@ -410,9 +410,9 @@ impl WindowsWindow {
drop_target_helper,
validation_number,
main_receiver,
gpu_context,
main_thread_id_win32,
appearance,
disable_direct_composition,
};
let lpparam = Some(&context as *const _ as *const _);
let creation_result = unsafe {
@ -453,14 +453,6 @@ impl WindowsWindow {
state: WindowOpenState::Windowed,
});
}
// The render pipeline will perform compositing on the GPU when the
// swapchain is configured correctly (see downstream of
// update_transparency).
// The following configuration is a one-time setup to ensure that the
// window is going to be composited with per-pixel alpha, but the render
// pipeline is responsible for effectively calling UpdateLayeredWindow
// at the appropriate time.
unsafe { SetLayeredWindowAttributes(hwnd, COLORREF(0), 255, LWA_ALPHA)? };
Ok(Self(state_ptr))
}
@ -485,7 +477,6 @@ impl rwh::HasDisplayHandle for WindowsWindow {
impl Drop for WindowsWindow {
fn drop(&mut self) {
self.0.state.borrow_mut().renderer.destroy();
// clone this `Rc` to prevent early release of the pointer
let this = self.0.clone();
self.0
@ -705,24 +696,21 @@ impl PlatformWindow for WindowsWindow {
}
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
let mut window_state = self.0.state.borrow_mut();
window_state
.renderer
.update_transparency(background_appearance != WindowBackgroundAppearance::Opaque);
let hwnd = self.0.hwnd;
match background_appearance {
WindowBackgroundAppearance::Opaque => {
// ACCENT_DISABLED
set_window_composition_attribute(window_state.hwnd, None, 0);
set_window_composition_attribute(hwnd, None, 0);
}
WindowBackgroundAppearance::Transparent => {
// Use ACCENT_ENABLE_TRANSPARENTGRADIENT for transparent background
set_window_composition_attribute(window_state.hwnd, None, 2);
set_window_composition_attribute(hwnd, None, 2);
}
WindowBackgroundAppearance::Blurred => {
// Enable acrylic blur
// ACCENT_ENABLE_ACRYLICBLURBEHIND
set_window_composition_attribute(window_state.hwnd, Some((0, 0, 0, 0)), 4);
set_window_composition_attribute(hwnd, Some((0, 0, 0, 0)), 4);
}
}
}
@ -794,11 +782,11 @@ impl PlatformWindow for WindowsWindow {
}
fn draw(&self, scene: &Scene) {
self.0.state.borrow_mut().renderer.draw(scene)
self.0.state.borrow_mut().renderer.draw(scene).log_err();
}
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
self.0.state.borrow().renderer.sprite_atlas().clone()
self.0.state.borrow().renderer.sprite_atlas()
}
fn get_raw_handle(&self) -> HWND {
@ -806,11 +794,11 @@ impl PlatformWindow for WindowsWindow {
}
fn gpu_specs(&self) -> Option<GpuSpecs> {
Some(self.0.state.borrow().renderer.gpu_specs())
self.0.state.borrow().renderer.gpu_specs().log_err()
}
fn update_ime_position(&self, _bounds: Bounds<ScaledPixels>) {
// todo(windows)
// There is no such thing on Windows.
}
}
@ -1306,52 +1294,6 @@ fn set_window_composition_attribute(hwnd: HWND, color: Option<Color>, state: u32
}
}
mod windows_renderer {
use crate::platform::blade::{BladeContext, BladeRenderer, BladeSurfaceConfig};
use raw_window_handle as rwh;
use std::num::NonZeroIsize;
use windows::Win32::{Foundation::HWND, UI::WindowsAndMessaging::GWLP_HINSTANCE};
use crate::{get_window_long, show_error};
pub(super) fn init(
context: &BladeContext,
hwnd: HWND,
transparent: bool,
) -> anyhow::Result<BladeRenderer> {
let raw = RawWindow { hwnd };
let config = BladeSurfaceConfig {
size: Default::default(),
transparent,
};
BladeRenderer::new(context, &raw, config)
.inspect_err(|err| show_error("Failed to initialize BladeRenderer", err.to_string()))
}
struct RawWindow {
hwnd: HWND,
}
impl rwh::HasWindowHandle for RawWindow {
fn window_handle(&self) -> Result<rwh::WindowHandle<'_>, rwh::HandleError> {
Ok(unsafe {
let hwnd = NonZeroIsize::new_unchecked(self.hwnd.0 as isize);
let mut handle = rwh::Win32WindowHandle::new(hwnd);
let hinstance = get_window_long(self.hwnd, GWLP_HINSTANCE);
handle.hinstance = NonZeroIsize::new(hinstance);
rwh::WindowHandle::borrow_raw(handle.into())
})
}
}
impl rwh::HasDisplayHandle for RawWindow {
fn display_handle(&self) -> Result<rwh::DisplayHandle<'_>, rwh::HandleError> {
let handle = rwh::WindowsDisplayHandle::new();
Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) })
}
}
}
#[cfg(test)]
mod tests {
use super::ClickState;

View file

@ -1020,7 +1020,7 @@ impl Window {
|| (active.get()
&& last_input_timestamp.get().elapsed() < Duration::from_secs(1));
if invalidator.is_dirty() {
if invalidator.is_dirty() || request_frame_options.force_render {
measure("frame duration", || {
handle
.update(&mut cx, |_, window, cx| {

View file

@ -23,6 +23,7 @@ futures.workspace = true
http.workspace = true
http-body.workspace = true
log.workspace = true
parking_lot.workspace = true
serde.workspace = true
serde_json.workspace = true
url.workspace = true

View file

@ -9,12 +9,10 @@ pub use http::{self, Method, Request, Response, StatusCode, Uri};
use futures::future::BoxFuture;
use http::request::Builder;
use parking_lot::Mutex;
#[cfg(feature = "test-support")]
use std::fmt;
use std::{
any::type_name,
sync::{Arc, Mutex},
};
use std::{any::type_name, sync::Arc};
pub use url::Url;
#[derive(Default, Debug, Clone, PartialEq, Eq, Hash)]
@ -86,6 +84,11 @@ pub trait HttpClient: 'static + Send + Sync {
}
fn proxy(&self) -> Option<&Url>;
#[cfg(feature = "test-support")]
fn as_fake(&self) -> &FakeHttpClient {
panic!("called as_fake on {}", type_name::<Self>())
}
}
/// An [`HttpClient`] that may have a proxy.
@ -132,6 +135,11 @@ impl HttpClient for HttpClientWithProxy {
fn type_name(&self) -> &'static str {
self.client.type_name()
}
#[cfg(feature = "test-support")]
fn as_fake(&self) -> &FakeHttpClient {
self.client.as_fake()
}
}
impl HttpClient for Arc<HttpClientWithProxy> {
@ -153,6 +161,11 @@ impl HttpClient for Arc<HttpClientWithProxy> {
fn type_name(&self) -> &'static str {
self.client.type_name()
}
#[cfg(feature = "test-support")]
fn as_fake(&self) -> &FakeHttpClient {
self.client.as_fake()
}
}
/// An [`HttpClient`] that has a base URL.
@ -199,20 +212,13 @@ impl HttpClientWithUrl {
/// Returns the base URL.
pub fn base_url(&self) -> String {
self.base_url
.lock()
.map_or_else(|_| Default::default(), |url| url.clone())
self.base_url.lock().clone()
}
/// Sets the base URL.
pub fn set_base_url(&self, base_url: impl Into<String>) {
let base_url = base_url.into();
self.base_url
.lock()
.map(|mut url| {
*url = base_url;
})
.ok();
*self.base_url.lock() = base_url;
}
/// Builds a URL using the given path.
@ -236,6 +242,22 @@ impl HttpClientWithUrl {
)?)
}
/// Builds a Zed Cloud URL using the given path.
pub fn build_zed_cloud_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
let base_url = self.base_url();
let base_api_url = match base_url.as_ref() {
"https://zed.dev" => "https://cloud.zed.dev",
"https://staging.zed.dev" => "https://cloud.zed.dev",
"http://localhost:3000" => "http://localhost:8787",
other => other,
};
Ok(Url::parse_with_params(
&format!("{}{}", base_api_url, path),
query,
)?)
}
/// Builds a Zed LLM URL using the given path.
pub fn build_zed_llm_url(&self, path: &str, query: &[(&str, &str)]) -> Result<Url> {
let base_url = self.base_url();
@ -272,6 +294,11 @@ impl HttpClient for Arc<HttpClientWithUrl> {
fn type_name(&self) -> &'static str {
self.client.type_name()
}
#[cfg(feature = "test-support")]
fn as_fake(&self) -> &FakeHttpClient {
self.client.as_fake()
}
}
impl HttpClient for HttpClientWithUrl {
@ -293,6 +320,11 @@ impl HttpClient for HttpClientWithUrl {
fn type_name(&self) -> &'static str {
self.client.type_name()
}
#[cfg(feature = "test-support")]
fn as_fake(&self) -> &FakeHttpClient {
self.client.as_fake()
}
}
pub fn read_proxy_from_env() -> Option<Url> {
@ -344,10 +376,15 @@ impl HttpClient for BlockedHttpClient {
fn type_name(&self) -> &'static str {
type_name::<Self>()
}
#[cfg(feature = "test-support")]
fn as_fake(&self) -> &FakeHttpClient {
panic!("called as_fake on {}", type_name::<Self>())
}
}
#[cfg(feature = "test-support")]
type FakeHttpHandler = Box<
type FakeHttpHandler = Arc<
dyn Fn(Request<AsyncBody>) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>>
+ Send
+ Sync
@ -356,7 +393,7 @@ type FakeHttpHandler = Box<
#[cfg(feature = "test-support")]
pub struct FakeHttpClient {
handler: FakeHttpHandler,
handler: Mutex<Option<FakeHttpHandler>>,
user_agent: HeaderValue,
}
@ -371,7 +408,7 @@ impl FakeHttpClient {
base_url: Mutex::new("http://test.example".into()),
client: HttpClientWithProxy {
client: Arc::new(Self {
handler: Box::new(move |req| Box::pin(handler(req))),
handler: Mutex::new(Some(Arc::new(move |req| Box::pin(handler(req))))),
user_agent: HeaderValue::from_static(type_name::<Self>()),
}),
proxy: None,
@ -396,6 +433,18 @@ impl FakeHttpClient {
.unwrap())
})
}
pub fn replace_handler<Fut, F>(&self, new_handler: F)
where
Fut: futures::Future<Output = anyhow::Result<Response<AsyncBody>>> + Send + 'static,
F: Fn(FakeHttpHandler, Request<AsyncBody>) -> Fut + Send + Sync + 'static,
{
let mut handler = self.handler.lock();
let old_handler = handler.take().unwrap();
*handler = Some(Arc::new(move |req| {
Box::pin(new_handler(old_handler.clone(), req))
}));
}
}
#[cfg(feature = "test-support")]
@ -411,7 +460,7 @@ impl HttpClient for FakeHttpClient {
&self,
req: Request<AsyncBody>,
) -> BoxFuture<'static, anyhow::Result<Response<AsyncBody>>> {
let future = (self.handler)(req);
let future = (self.handler.lock().as_ref().unwrap())(req);
future
}
@ -426,4 +475,8 @@ impl HttpClient for FakeHttpClient {
fn type_name(&self) -> &'static str {
type_name::<Self>()
}
fn as_fake(&self) -> &FakeHttpClient {
self
}
}

View file

@ -107,6 +107,12 @@ pub enum IconName {
Disconnected,
DocumentText,
Download,
EditorAtom,
EditorCursor,
EditorEmacs,
EditorJetBrains,
EditorSublime,
EditorVsCode,
Ellipsis,
EllipsisVertical,
Envelope,
@ -229,6 +235,7 @@ pub enum IconName {
Server,
Settings,
SettingsAlt,
ShieldCheck,
Shift,
Slash,
SlashSquare,

View file

@ -246,12 +246,15 @@ impl Render for InlineCompletionButton {
};
if zeta::should_show_upsell_modal(&self.user_store, cx) {
let tooltip_meta =
match self.user_store.read(cx).current_user_has_accepted_terms() {
Some(true) => "Choose a Plan",
Some(false) => "Accept the Terms of Service",
None => "Sign In",
};
let tooltip_meta = if self.user_store.read(cx).current_user().is_some() {
if self.user_store.read(cx).has_accepted_terms_of_service() {
"Choose a Plan"
} else {
"Accept the Terms of Service"
}
} else {
"Sign In"
};
return div().child(
IconButton::new("zed-predict-pending-button", zeta_icon)
@ -387,9 +390,9 @@ impl InlineCompletionButton {
language: None,
file: None,
edit_prediction_provider: None,
user_store,
popover_menu_handle,
fs,
user_store,
}
}

View file

@ -3,10 +3,11 @@ use std::sync::Arc;
use anyhow::Result;
use client::Client;
use cloud_llm_client::Plan;
use gpui::{
App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Global, ReadGlobal as _,
};
use proto::{Plan, TypedEnvelope};
use proto::TypedEnvelope;
use smol::lock::{RwLock, RwLockUpgradableReadGuard, RwLockWriteGuard};
use thiserror::Error;
@ -30,7 +31,7 @@ pub struct ModelRequestLimitReachedError {
impl fmt::Display for ModelRequestLimitReachedError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let message = match self.plan {
Plan::Free => "Model request limit reached. Upgrade to Zed Pro for more requests.",
Plan::ZedFree => "Model request limit reached. Upgrade to Zed Pro for more requests.",
Plan::ZedPro => {
"Model request limit reached. Upgrade to usage-based billing for more requests."
}
@ -64,9 +65,14 @@ impl LlmApiToken {
mut lock: RwLockWriteGuard<'_, Option<String>>,
client: &Arc<Client>,
) -> Result<String> {
let response = client.request(proto::GetLlmToken {}).await?;
*lock = Some(response.token.clone());
Ok(response.token.clone())
let system_id = client
.telemetry()
.system_id()
.map(|system_id| system_id.to_string());
let response = client.cloud_client().create_llm_token(system_id).await?;
*lock = Some(response.token.0.clone());
Ok(response.token.0.clone())
}
}

View file

@ -44,7 +44,6 @@ ollama = { workspace = true, features = ["schemars"] }
open_ai = { workspace = true, features = ["schemars"] }
open_router = { workspace = true, features = ["schemars"] }
partial-json-fixer.workspace = true
proto.workspace = true
release_channel.workspace = true
schemars.workspace = true
serde.workspace = true

View file

@ -6,7 +6,7 @@ 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,
EXPIRED_LLM_TOKEN_HEADER_NAME, ListModelsResponse, MODEL_REQUESTS_RESOURCE_HEADER_VALUE, Plan,
SERVER_SUPPORTS_STATUS_MESSAGES_HEADER_NAME, SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME,
TOOL_USE_LIMIT_REACHED_HEADER_NAME, ZED_VERSION_HEADER_NAME,
};
@ -27,7 +27,6 @@ use language_model::{
LanguageModelToolChoice, LanguageModelToolSchemaFormat, LlmApiToken,
ModelRequestLimitReachedError, PaymentRequiredError, RateLimiter, RefreshLlmTokenListener,
};
use proto::Plan;
use release_channel::AppVersion;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
@ -137,11 +136,10 @@ impl State {
cx: &mut Context<Self>,
) -> Self {
let refresh_llm_token_listener = RefreshLlmTokenListener::global(cx);
Self {
client: client.clone(),
llm_api_token: LlmApiToken::default(),
user_store,
user_store: user_store.clone(),
status,
accept_terms_of_service_task: None,
models: Vec::new(),
@ -154,8 +152,9 @@ impl State {
.read_with(cx, |this, _cx| (client.clone(), this.llm_api_token.clone()))?;
loop {
let status = this.read_with(cx, |this, _cx| this.status)?;
if matches!(status, client::Status::Connected { .. }) {
let is_authenticated = user_store
.read_with(cx, |user_store, _cx| user_store.current_user().is_some())?;
if is_authenticated {
break;
}
@ -194,26 +193,20 @@ impl State {
}
}
fn is_signed_out(&self) -> bool {
self.status.is_signed_out()
fn is_signed_out(&self, cx: &App) -> bool {
self.user_store.read(cx).current_user().is_none()
}
fn authenticate(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
let client = self.client.clone();
cx.spawn(async move |state, cx| {
client
.authenticate_and_connect(true, &cx)
.await
.into_response()?;
client.sign_in_with_optional_connect(true, &cx).await?;
state.update(cx, |_, cx| cx.notify())
})
}
fn has_accepted_terms_of_service(&self, cx: &App) -> bool {
self.user_store
.read(cx)
.current_user_has_accepted_terms()
.unwrap_or(false)
self.user_store.read(cx).has_accepted_terms_of_service()
}
fn accept_terms_of_service(&mut self, cx: &mut Context<Self>) {
@ -398,7 +391,7 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
fn is_authenticated(&self, cx: &App) -> bool {
let state = self.state.read(cx);
!state.is_signed_out() && state.has_accepted_terms_of_service(cx)
!state.is_signed_out(cx) && state.has_accepted_terms_of_service(cx)
}
fn authenticate(&self, _cx: &mut App) -> Task<Result<(), AuthenticateError>> {
@ -613,11 +606,6 @@ impl CloudLanguageModel {
.and_then(|plan| plan.to_str().ok())
.and_then(|plan| cloud_llm_client::Plan::from_str(plan).ok())
{
let plan = match plan {
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 }));
}
}
@ -1118,7 +1106,7 @@ fn response_lines<T: DeserializeOwned>(
#[derive(IntoElement, RegisterComponent)]
struct ZedAiConfiguration {
is_connected: bool,
plan: Option<proto::Plan>,
plan: Option<Plan>,
subscription_period: Option<(DateTime<Utc>, DateTime<Utc>)>,
eligible_for_trial: bool,
has_accepted_terms_of_service: bool,
@ -1132,15 +1120,15 @@ impl RenderOnce for ZedAiConfiguration {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
let young_account_banner = YoungAccountBanner;
let is_pro = self.plan == Some(proto::Plan::ZedPro);
let is_pro = self.plan == Some(Plan::ZedPro);
let subscription_text = match (self.plan, self.subscription_period) {
(Some(proto::Plan::ZedPro), Some(_)) => {
(Some(Plan::ZedPro), Some(_)) => {
"You have access to Zed's hosted models through your Pro subscription."
}
(Some(proto::Plan::ZedProTrial), Some(_)) => {
(Some(Plan::ZedProTrial), Some(_)) => {
"You have access to Zed's hosted models through your Pro trial."
}
(Some(proto::Plan::Free), Some(_)) => {
(Some(Plan::ZedFree), Some(_)) => {
"You have basic access to Zed's hosted models through the Free plan."
}
_ => {
@ -1265,8 +1253,8 @@ impl Render for ConfigurationView {
let user_store = state.user_store.read(cx);
ZedAiConfiguration {
is_connected: !state.is_signed_out(),
plan: user_store.current_plan(),
is_connected: !state.is_signed_out(cx),
plan: user_store.plan(),
subscription_period: user_store.subscription_period(),
eligible_for_trial: user_store.trial_started_at().is_none(),
has_accepted_terms_of_service: state.has_accepted_terms_of_service(cx),
@ -1286,7 +1274,7 @@ impl Component for ZedAiConfiguration {
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
fn configuration(
is_connected: bool,
plan: Option<proto::Plan>,
plan: Option<Plan>,
eligible_for_trial: bool,
account_too_young: bool,
has_accepted_terms_of_service: bool,
@ -1330,15 +1318,15 @@ impl Component for ZedAiConfiguration {
),
single_example(
"Free Plan",
configuration(true, Some(proto::Plan::Free), true, false, true),
configuration(true, Some(Plan::ZedFree), true, false, true),
),
single_example(
"Zed Pro Trial Plan",
configuration(true, Some(proto::Plan::ZedProTrial), true, false, true),
configuration(true, Some(Plan::ZedProTrial), true, false, true),
),
single_example(
"Zed Pro Plan",
configuration(true, Some(proto::Plan::ZedPro), true, false, true),
configuration(true, Some(Plan::ZedPro), true, false, true),
),
])
.into_any_element(),

View file

@ -1015,7 +1015,7 @@ impl Render for LspTool {
.anchor(Corner::BottomLeft)
.with_handle(self.popover_menu_handle.clone())
.trigger_with_tooltip(
IconButton::new("zed-lsp-tool-button", IconName::BoltFilledAlt)
IconButton::new("zed-lsp-tool-button", IconName::Bolt)
.when_some(indicator, IconButton::indicator)
.icon_size(IconSize::Small)
.indicator_border_color(Some(cx.theme().colors().status_bar_background)),

View file

@ -40,8 +40,8 @@ util.workspace = true
workspace-hack.workspace = true
[target.'cfg(not(any(all(target_os = "windows", target_env = "gnu"), target_os = "freebsd")))'.dependencies]
libwebrtc = { rev = "383e5377f8b7de1f8627ee16f0cf11c5293337bd", git = "https://github.com/zed-industries/livekit-rust-sdks" }
livekit = { rev = "383e5377f8b7de1f8627ee16f0cf11c5293337bd", git = "https://github.com/zed-industries/livekit-rust-sdks", features = [
libwebrtc = { rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d", git = "https://github.com/zed-industries/livekit-rust-sdks" }
livekit = { rev = "5f04705ac3f356350ae31534ffbc476abc9ea83d", git = "https://github.com/zed-industries/livekit-rust-sdks", features = [
"__rustls-tls"
] }

View file

@ -421,14 +421,14 @@ impl LanguageServer {
.map(|stderr| {
let io_handlers = io_handlers.clone();
let stderr_captures = stderr_capture.clone();
cx.spawn(async move |_| {
cx.background_spawn(async move {
Self::handle_stderr(stderr, io_handlers, stderr_captures)
.log_err()
.await
})
})
.unwrap_or_else(|| Task::ready(None));
let input_task = cx.spawn(async move |_| {
let input_task = cx.background_spawn(async move {
let (stdout, stderr) = futures::join!(stdout_input_task, stderr_input_task);
stdout.or(stderr)
});
@ -846,7 +846,7 @@ impl LanguageServer {
configuration: Arc<DidChangeConfigurationParams>,
cx: &App,
) -> Task<Result<Arc<Self>>> {
cx.spawn(async move |_| {
cx.background_spawn(async move {
let response = self
.request::<request::Initialize>(params)
.await

View file

@ -167,10 +167,10 @@ impl Anchor {
if *self == Anchor::min() || *self == Anchor::max() {
true
} else if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
excerpt.contains(self)
&& (self.text_anchor == excerpt.range.context.start
|| self.text_anchor == excerpt.range.context.end
|| self.text_anchor.is_valid(&excerpt.buffer))
(self.text_anchor == excerpt.range.context.start
|| self.text_anchor == excerpt.range.context.end
|| self.text_anchor.is_valid(&excerpt.buffer))
&& excerpt.contains(self)
} else {
false
}

View file

@ -16,17 +16,29 @@ default = []
[dependencies]
anyhow.workspace = true
ai_onboarding.workspace = true
client.workspace = true
command_palette_hooks.workspace = true
component.workspace = true
documented.workspace = true
db.workspace = true
editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
gpui.workspace = true
itertools.workspace = true
language.workspace = true
language_model.workspace = true
menu.workspace = true
project.workspace = true
schemars.workspace = true
serde.workspace = true
settings.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
vim_mode_setting.workspace = true
workspace-hack.workspace = true
workspace.workspace = true
zed_actions.workspace = true
zlog.workspace = true

View file

@ -0,0 +1,359 @@
use std::sync::Arc;
use ai_onboarding::{AiUpsellCard, SignInStatus};
use client::DisableAiSettings;
use fs::Fs;
use gpui::{
Action, AnyView, App, DismissEvent, EventEmitter, FocusHandle, Focusable, Window, prelude::*,
};
use itertools;
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
use settings::{Settings, update_settings_file};
use ui::{
Badge, ButtonLike, Divider, Modal, ModalFooter, ModalHeader, Section, SwitchField, ToggleState,
prelude::*,
};
use workspace::ModalView;
use util::ResultExt;
use zed_actions::agent::OpenSettings;
use crate::Onboarding;
const FEATURED_PROVIDERS: [&'static str; 4] = ["anthropic", "google", "openai", "ollama"];
fn render_llm_provider_section(
onboarding: &Onboarding,
disabled: bool,
window: &mut Window,
cx: &mut App,
) -> impl IntoElement {
v_flex()
.gap_4()
.child(
v_flex()
.child(Label::new("Or use other LLM providers").size(LabelSize::Large))
.child(
Label::new("Bring your API keys to use the available providers with Zed's UI for free.")
.color(Color::Muted),
),
)
.child(render_llm_provider_card(onboarding, disabled, window, cx))
}
fn render_privacy_card(disabled: bool, cx: &mut App) -> impl IntoElement {
let privacy_badge = || Badge::new("Privacy").icon(IconName::ShieldCheck);
v_flex()
.relative()
.pt_2()
.pb_2p5()
.pl_3()
.pr_2()
.border_1()
.border_dashed()
.border_color(cx.theme().colors().border.opacity(0.5))
.bg(cx.theme().colors().surface_background.opacity(0.3))
.rounded_lg()
.overflow_hidden()
.map(|this| {
if disabled {
this.child(
h_flex()
.gap_2()
.justify_between()
.child(
h_flex()
.gap_1()
.child(Label::new("AI is disabled across Zed"))
.child(
Icon::new(IconName::Check)
.color(Color::Success)
.size(IconSize::XSmall),
),
)
.child(privacy_badge()),
)
.child(
Label::new("Re-enable it any time in Settings.")
.size(LabelSize::Small)
.color(Color::Muted),
)
} else {
this.child(
h_flex()
.gap_2()
.justify_between()
.child(Label::new("We don't train models using your data"))
.child(
h_flex().gap_1().child(privacy_badge()).child(
Button::new("learn_more", "Learn More")
.style(ButtonStyle::Outlined)
.label_size(LabelSize::Small)
.icon(IconName::ArrowUpRight)
.icon_size(IconSize::XSmall)
.icon_color(Color::Muted)
.on_click(|_, _, cx| {
cx.open_url("https://zed.dev/docs/ai/privacy-and-security");
}),
),
),
)
.child(
Label::new(
"Feel confident in the security and privacy of your projects using Zed.",
)
.size(LabelSize::Small)
.color(Color::Muted),
)
}
})
}
fn render_llm_provider_card(
onboarding: &Onboarding,
disabled: bool,
_: &mut Window,
cx: &mut App,
) -> impl IntoElement {
let registry = LanguageModelRegistry::read_global(cx);
v_flex()
.border_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().surface_background.opacity(0.5))
.rounded_lg()
.overflow_hidden()
.children(itertools::intersperse_with(
FEATURED_PROVIDERS
.into_iter()
.flat_map(|provider_name| {
registry.provider(&LanguageModelProviderId::new(provider_name))
})
.enumerate()
.map(|(index, provider)| {
let group_name = SharedString::new(format!("onboarding-hover-group-{}", index));
let is_authenticated = provider.is_authenticated(cx);
ButtonLike::new(("onboarding-ai-setup-buttons", index))
.size(ButtonSize::Large)
.child(
h_flex()
.group(&group_name)
.px_0p5()
.w_full()
.gap_2()
.justify_between()
.child(
h_flex()
.gap_1()
.child(
Icon::new(provider.icon())
.color(Color::Muted)
.size(IconSize::XSmall),
)
.child(Label::new(provider.name().0)),
)
.child(
h_flex()
.gap_1()
.when(!is_authenticated, |el| {
el.visible_on_hover(group_name.clone())
.child(
Icon::new(IconName::Settings)
.color(Color::Muted)
.size(IconSize::XSmall),
)
.child(
Label::new("Configure")
.color(Color::Muted)
.size(LabelSize::Small),
)
})
.when(is_authenticated && !disabled, |el| {
el.child(
Icon::new(IconName::Check)
.color(Color::Success)
.size(IconSize::XSmall),
)
.child(
Label::new("Configured")
.color(Color::Muted)
.size(LabelSize::Small),
)
}),
),
)
.on_click({
let workspace = onboarding.workspace.clone();
move |_, window, cx| {
workspace
.update(cx, |workspace, cx| {
workspace.toggle_modal(window, cx, |window, cx| {
let modal = AiConfigurationModal::new(
provider.clone(),
window,
cx,
);
window.focus(&modal.focus_handle(cx));
modal
});
})
.log_err();
}
})
.into_any_element()
}),
|| Divider::horizontal().into_any_element(),
))
.child(Divider::horizontal())
.child(
Button::new("agent_settings", "Add Many Others")
.size(ButtonSize::Large)
.icon(IconName::Plus)
.icon_position(IconPosition::Start)
.icon_color(Color::Muted)
.icon_size(IconSize::XSmall)
.on_click(|_event, window, cx| {
window.dispatch_action(OpenSettings.boxed_clone(), cx)
}),
)
}
pub(crate) fn render_ai_setup_page(
onboarding: &Onboarding,
window: &mut Window,
cx: &mut App,
) -> impl IntoElement {
let is_ai_disabled = DisableAiSettings::get_global(cx).disable_ai;
let backdrop = div()
.id("backdrop")
.size_full()
.absolute()
.inset_0()
.bg(cx.theme().colors().editor_background)
.opacity(0.8)
.block_mouse_except_scroll();
v_flex()
.gap_2()
.child(SwitchField::new(
"enable_ai",
"Enable AI features",
None,
if is_ai_disabled {
ToggleState::Unselected
} else {
ToggleState::Selected
},
|toggle_state, _, cx| {
let enabled = match toggle_state {
ToggleState::Indeterminate => {
return;
}
ToggleState::Unselected => false,
ToggleState::Selected => true,
};
let fs = <dyn Fs>::global(cx);
update_settings_file::<DisableAiSettings>(
fs,
cx,
move |ai_settings: &mut Option<bool>, _| {
*ai_settings = Some(!enabled);
},
);
},
))
.child(render_privacy_card(is_ai_disabled, cx))
.child(
v_flex()
.mt_2()
.gap_6()
.child(AiUpsellCard {
sign_in_status: SignInStatus::SignedIn,
sign_in: Arc::new(|_, _| {}),
user_plan: onboarding.user_store.read(cx).plan(),
})
.child(render_llm_provider_section(
onboarding,
is_ai_disabled,
window,
cx,
))
.when(is_ai_disabled, |this| this.child(backdrop)),
)
}
struct AiConfigurationModal {
focus_handle: FocusHandle,
selected_provider: Arc<dyn LanguageModelProvider>,
configuration_view: AnyView,
}
impl AiConfigurationModal {
fn new(
selected_provider: Arc<dyn LanguageModelProvider>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let focus_handle = cx.focus_handle();
let configuration_view = selected_provider.configuration_view(window, cx);
Self {
focus_handle,
configuration_view,
selected_provider,
}
}
}
impl ModalView for AiConfigurationModal {}
impl EventEmitter<DismissEvent> for AiConfigurationModal {}
impl Focusable for AiConfigurationModal {
fn focus_handle(&self, _cx: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl Render for AiConfigurationModal {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.w(rems(34.))
.elevation_3(cx)
.track_focus(&self.focus_handle)
.child(
Modal::new("onboarding-ai-setup-modal", None)
.header(
ModalHeader::new()
.icon(
Icon::new(self.selected_provider.icon())
.color(Color::Muted)
.size(IconSize::Small),
)
.headline(self.selected_provider.name().0),
)
.section(Section::new().child(self.configuration_view.clone()))
.footer(
ModalFooter::new().end_slot(
h_flex()
.gap_1()
.child(
Button::new("onboarding-closing-cancel", "Cancel")
.on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
)
.child(Button::new("save-btn", "Done").on_click(cx.listener(
|_, _, window, cx| {
window.dispatch_action(menu::Confirm.boxed_clone(), cx);
cx.emit(DismissEvent);
},
))),
),
),
)
}
}

View file

@ -0,0 +1,351 @@
use client::TelemetrySettings;
use fs::Fs;
use gpui::{App, Entity, IntoElement, Window};
use settings::{BaseKeymap, Settings, update_settings_file};
use theme::{Appearance, ThemeMode, ThemeName, ThemeRegistry, ThemeSelection, ThemeSettings};
use ui::{
ParentElement as _, StatefulInteractiveElement, SwitchField, ToggleButtonGroup,
ToggleButtonSimple, ToggleButtonWithIcon, prelude::*, rems_from_px,
};
use vim_mode_setting::VimModeSetting;
use crate::theme_preview::ThemePreviewTile;
/// separates theme "mode" ("dark" | "light" | "system") into two separate states
/// - appearance = "dark" | "light"
/// - "system" true/false
/// when system selected:
/// - toggling between light and dark does not change theme.mode, just which variant will be changed
/// when system not selected:
/// - toggling between light and dark does change theme.mode
/// selecting a theme preview will always change theme.["light" | "dark"] to the selected theme,
///
/// this allows for selecting a dark and light theme option regardless of whether the mode is set to system or not
/// it does not support setting theme to a static value
fn render_theme_section(window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme_selection = ThemeSettings::get_global(cx).theme_selection.clone();
let system_appearance = theme::SystemAppearance::global(cx);
let appearance_state = window.use_state(cx, |_, _cx| {
theme_selection
.as_ref()
.and_then(|selection| selection.mode())
.and_then(|mode| match mode {
ThemeMode::System => None,
ThemeMode::Light => Some(Appearance::Light),
ThemeMode::Dark => Some(Appearance::Dark),
})
.unwrap_or(*system_appearance)
});
let appearance = *appearance_state.read(cx);
let theme_selection = theme_selection.unwrap_or_else(|| ThemeSelection::Dynamic {
mode: match *system_appearance {
Appearance::Light => ThemeMode::Light,
Appearance::Dark => ThemeMode::Dark,
},
light: ThemeName("One Light".into()),
dark: ThemeName("One Dark".into()),
});
let theme_registry = ThemeRegistry::global(cx);
let current_theme_name = theme_selection.theme(appearance);
let theme_mode = theme_selection.mode().unwrap_or_default();
// let theme_mode = theme_selection.mode();
// TODO: Clean this up once the "System" button inside the
// toggle button group is done
let selected_index = match appearance {
Appearance::Light => 0,
Appearance::Dark => 1,
};
let theme_seed = 0xBEEF as f32;
const LIGHT_THEMES: [&'static str; 3] = ["One Light", "Ayu Light", "Gruvbox Light"];
const DARK_THEMES: [&'static str; 3] = ["One Dark", "Ayu Dark", "Gruvbox Dark"];
let theme_names = match appearance {
Appearance::Light => LIGHT_THEMES,
Appearance::Dark => DARK_THEMES,
};
let themes = theme_names
.map(|theme_name| theme_registry.get(theme_name))
.map(Result::unwrap);
let theme_previews = themes.map(|theme| {
let is_selected = theme.name == current_theme_name;
let name = theme.name.clone();
let colors = cx.theme().colors();
v_flex()
.id(name.clone())
.w_full()
.items_center()
.gap_1()
.child(
div()
.w_full()
.border_2()
.border_color(colors.border_transparent)
.rounded(ThemePreviewTile::CORNER_RADIUS)
.map(|this| {
if is_selected {
this.border_color(colors.border_selected)
} else {
this.opacity(0.8).hover(|s| s.border_color(colors.border))
}
})
.child(ThemePreviewTile::new(theme.clone(), theme_seed)),
)
.child(Label::new(name).color(Color::Muted).size(LabelSize::Small))
.on_click({
let theme_name = theme.name.clone();
move |_, _, cx| {
let fs = <dyn Fs>::global(cx);
let theme_name = theme_name.clone();
update_settings_file::<ThemeSettings>(fs, cx, move |settings, _| {
settings.set_theme(theme_name, appearance);
});
}
})
});
return v_flex()
.gap_2()
.child(
h_flex().justify_between().child(Label::new("Theme")).child(
ToggleButtonGroup::single_row(
"theme-selector-onboarding-dark-light",
[
ToggleButtonSimple::new("Light", {
let appearance_state = appearance_state.clone();
move |_, _, cx| {
write_appearance_change(&appearance_state, Appearance::Light, cx);
}
}),
ToggleButtonSimple::new("Dark", {
let appearance_state = appearance_state.clone();
move |_, _, cx| {
write_appearance_change(&appearance_state, Appearance::Dark, cx);
}
}),
// TODO: Properly put the System back as a button within this group
// Currently, given "System" is not an option in the Appearance enum,
// this button doesn't get selected
ToggleButtonSimple::new("System", {
let theme = theme_selection.clone();
move |_, _, cx| {
toggle_system_theme_mode(theme.clone(), appearance, cx);
}
})
.selected(theme_mode == ThemeMode::System),
],
)
.selected_index(selected_index)
.style(ui::ToggleButtonGroupStyle::Outlined)
.button_width(rems_from_px(64.)),
),
)
.child(h_flex().gap_4().justify_between().children(theme_previews));
fn write_appearance_change(
appearance_state: &Entity<Appearance>,
new_appearance: Appearance,
cx: &mut App,
) {
let fs = <dyn Fs>::global(cx);
appearance_state.write(cx, new_appearance);
update_settings_file::<ThemeSettings>(fs, cx, move |settings, _| {
if settings.theme.as_ref().and_then(ThemeSelection::mode) == Some(ThemeMode::System) {
return;
}
let new_mode = match new_appearance {
Appearance::Light => ThemeMode::Light,
Appearance::Dark => ThemeMode::Dark,
};
settings.set_mode(new_mode);
});
}
fn toggle_system_theme_mode(
theme_selection: ThemeSelection,
appearance: Appearance,
cx: &mut App,
) {
let fs = <dyn Fs>::global(cx);
update_settings_file::<ThemeSettings>(fs, cx, move |settings, _| {
settings.theme = Some(match theme_selection {
ThemeSelection::Static(theme_name) => ThemeSelection::Dynamic {
mode: ThemeMode::System,
light: theme_name.clone(),
dark: theme_name.clone(),
},
ThemeSelection::Dynamic {
mode: ThemeMode::System,
light,
dark,
} => {
let mode = match appearance {
Appearance::Light => ThemeMode::Light,
Appearance::Dark => ThemeMode::Dark,
};
ThemeSelection::Dynamic { mode, light, dark }
}
ThemeSelection::Dynamic {
mode: _,
light,
dark,
} => ThemeSelection::Dynamic {
mode: ThemeMode::System,
light,
dark,
},
});
});
}
}
fn write_keymap_base(keymap_base: BaseKeymap, cx: &App) {
let fs = <dyn Fs>::global(cx);
update_settings_file::<BaseKeymap>(fs, cx, move |setting, _| {
*setting = Some(keymap_base);
});
}
fn render_telemetry_section(cx: &App) -> impl IntoElement {
let fs = <dyn Fs>::global(cx);
v_flex()
.gap_4()
.child(Label::new("Telemetry").size(LabelSize::Large))
.child(SwitchField::new(
"onboarding-telemetry-metrics",
"Help Improve Zed",
Some("Sending anonymous usage data helps us build the right features and create the best experience.".into()),
if TelemetrySettings::get_global(cx).metrics {
ui::ToggleState::Selected
} else {
ui::ToggleState::Unselected
},
{
let fs = fs.clone();
move |selection, _, cx| {
let enabled = match selection {
ToggleState::Selected => true,
ToggleState::Unselected => false,
ToggleState::Indeterminate => { return; },
};
update_settings_file::<TelemetrySettings>(
fs.clone(),
cx,
move |setting, _| setting.metrics = Some(enabled),
);
}},
))
.child(SwitchField::new(
"onboarding-telemetry-crash-reports",
"Help Fix Zed",
Some("Send crash reports so we can fix critical issues fast.".into()),
if TelemetrySettings::get_global(cx).diagnostics {
ui::ToggleState::Selected
} else {
ui::ToggleState::Unselected
},
{
let fs = fs.clone();
move |selection, _, cx| {
let enabled = match selection {
ToggleState::Selected => true,
ToggleState::Unselected => false,
ToggleState::Indeterminate => { return; },
};
update_settings_file::<TelemetrySettings>(
fs.clone(),
cx,
move |setting, _| setting.diagnostics = Some(enabled),
);
}
}
))
}
pub(crate) fn render_basics_page(window: &mut Window, cx: &mut App) -> impl IntoElement {
let base_keymap = match BaseKeymap::get_global(cx) {
BaseKeymap::VSCode => Some(0),
BaseKeymap::JetBrains => Some(1),
BaseKeymap::SublimeText => Some(2),
BaseKeymap::Atom => Some(3),
BaseKeymap::Emacs => Some(4),
BaseKeymap::Cursor => Some(5),
BaseKeymap::TextMate | BaseKeymap::None => None,
};
v_flex()
.gap_6()
.child(render_theme_section(window, cx))
.child(
v_flex().gap_2().child(Label::new("Base Keymap")).child(
ToggleButtonGroup::two_rows(
"multiple_row_test",
[
ToggleButtonWithIcon::new("VS Code", IconName::EditorVsCode, |_, _, cx| {
write_keymap_base(BaseKeymap::VSCode, cx);
}),
ToggleButtonWithIcon::new("Jetbrains", IconName::EditorJetBrains, |_, _, cx| {
write_keymap_base(BaseKeymap::JetBrains, cx);
}),
ToggleButtonWithIcon::new("Sublime Text", IconName::EditorSublime, |_, _, cx| {
write_keymap_base(BaseKeymap::SublimeText, cx);
}),
],
[
ToggleButtonWithIcon::new("Atom", IconName::EditorAtom, |_, _, cx| {
write_keymap_base(BaseKeymap::Atom, cx);
}),
ToggleButtonWithIcon::new("Emacs", IconName::EditorEmacs, |_, _, cx| {
write_keymap_base(BaseKeymap::Emacs, cx);
}),
ToggleButtonWithIcon::new("Cursor (Beta)", IconName::EditorCursor, |_, _, cx| {
write_keymap_base(BaseKeymap::Cursor, cx);
}),
],
)
.when_some(base_keymap, |this, base_keymap| this.selected_index(base_keymap))
.button_width(rems_from_px(216.))
.size(ui::ToggleButtonGroupSize::Medium)
.style(ui::ToggleButtonGroupStyle::Outlined)
),
)
.child(SwitchField::new(
"onboarding-vim-mode",
"Vim Mode",
Some("Coming from Neovim? Zed's first-class implementation of Vim Mode has got your back.".into()),
if VimModeSetting::get_global(cx).0 {
ui::ToggleState::Selected
} else {
ui::ToggleState::Unselected
},
{
let fs = <dyn Fs>::global(cx);
move |selection, _, cx| {
let enabled = match selection {
ToggleState::Selected => true,
ToggleState::Unselected => false,
ToggleState::Indeterminate => { return; },
};
update_settings_file::<VimModeSetting>(
fs.clone(),
cx,
move |setting, _| *setting = Some(enabled),
);
}
},
))
.child(render_telemetry_section(cx))
}

View file

@ -1,16 +1,17 @@
use editor::{EditorSettings, ShowMinimap};
use fs::Fs;
use gpui::{App, IntoElement, Pixels, Window};
use gpui::{Action, 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,
ButtonLike, ContextMenu, DropdownMenu, NumericStepper, SwitchField, ToggleButtonGroup,
ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, prelude::*,
};
use crate::{ImportCursorSettings, ImportVsCodeSettings};
fn read_show_mini_map(cx: &App) -> ShowMinimap {
editor::EditorSettings::get_global(cx).minimap.show
}
@ -18,6 +19,14 @@ fn read_show_mini_map(cx: &App) -> ShowMinimap {
fn write_show_mini_map(show: ShowMinimap, cx: &mut App) {
let fs = <dyn Fs>::global(cx);
// This is used to speed up the UI
// the UI reads the current values to get what toggle state to show on buttons
// there's a slight delay if we just call update_settings_file so we manually set
// the value here then call update_settings file to get around the delay
let mut curr_settings = EditorSettings::get_global(cx).clone();
curr_settings.minimap.show = show;
EditorSettings::override_global(curr_settings, cx);
update_settings_file::<EditorSettings>(fs, cx, move |editor_settings, _| {
editor_settings.minimap.get_or_insert_default().show = Some(show);
});
@ -33,6 +42,10 @@ fn read_inlay_hints(cx: &App) -> bool {
fn write_inlay_hints(enabled: bool, cx: &mut App) {
let fs = <dyn Fs>::global(cx);
let mut curr_settings = AllLanguageSettings::get_global(cx).clone();
curr_settings.defaults.inlay_hints.enabled = enabled;
AllLanguageSettings::override_global(curr_settings, cx);
update_settings_file::<AllLanguageSettings>(fs, cx, move |all_language_settings, cx| {
all_language_settings
.defaults
@ -54,6 +67,14 @@ fn read_git_blame(cx: &App) -> bool {
fn set_git_blame(enabled: bool, cx: &mut App) {
let fs = <dyn Fs>::global(cx);
let mut curr_settings = ProjectSettings::get_global(cx).clone();
curr_settings
.git
.inline_blame
.get_or_insert_default()
.enabled = enabled;
ProjectSettings::override_global(curr_settings, cx);
update_settings_file::<ProjectSettings>(fs, cx, move |project_settings, _| {
project_settings
.git
@ -95,139 +116,212 @@ fn write_buffer_font_family(font_family: SharedString, cx: &mut App) {
});
}
pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement {
fn render_import_settings_section() -> impl IntoElement {
v_flex()
.gap_4()
.child(
v_flex()
.child(Label::new("Import Settings").size(LabelSize::Large))
.child(
Label::new("Automatically pull your settings from other editors.")
.color(Color::Muted),
),
)
.child(
h_flex()
.w_full()
.gap_4()
.child(
h_flex().w_full().child(
ButtonLike::new("import_vs_code")
.full_width()
.style(ButtonStyle::Outlined)
.size(ButtonSize::Large)
.child(
h_flex()
.w_full()
.gap_1p5()
.px_1()
.child(
Icon::new(IconName::EditorVsCode)
.color(Color::Muted)
.size(IconSize::XSmall),
)
.child(Label::new("VS Code")),
)
.on_click(|_, window, cx| {
window.dispatch_action(
ImportVsCodeSettings::default().boxed_clone(),
cx,
)
}),
),
)
.child(
h_flex().w_full().child(
ButtonLike::new("import_cursor")
.full_width()
.style(ButtonStyle::Outlined)
.size(ButtonSize::Large)
.child(
h_flex()
.w_full()
.gap_1p5()
.px_1()
.child(
Icon::new(IconName::EditorCursor)
.color(Color::Muted)
.size(IconSize::XSmall),
)
.child(Label::new("Cursor")),
)
.on_click(|_, window, cx| {
window.dispatch_action(
ImportCursorSettings::default().boxed_clone(),
cx,
)
}),
),
),
)
}
fn render_font_customization_section(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()
h_flex()
.w_full()
.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()
v_flex()
.w_full()
.gap_1()
.child(Label::new("UI Font"))
.child(
v_flex()
h_flex()
.w_full()
.justify_between()
.gap_1()
.child(Label::new("UI Font"))
.gap_2()
.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);
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);
}
},
)
}
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"))
menu
}),
)
.style(ui::DropdownStyle::Outlined)
.full_width(true),
)
.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);
},
)),
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);
},
)
.style(ui::NumericStepperStyle::Outlined),
),
),
)
.child(
v_flex()
.w_full()
.gap_1()
.child(Label::new("Editor Font"))
.child(
h_flex()
.w_full()
.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
}),
)
.style(ui::DropdownStyle::Outlined)
.full_width(true),
)
.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);
},
)
.style(ui::NumericStepperStyle::Outlined),
),
),
)
}
fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl IntoElement {
v_flex()
.gap_5()
.child(Label::new("Popular Settings").size(LabelSize::Large).mt_8())
.child(render_font_customization_section(window, cx))
.child(
h_flex()
.items_start()
.justify_between()
.child(Label::new("Mini Map"))
.child(
v_flex().child(Label::new("Mini Map")).child(
Label::new("See a high-level overview of your source code.")
.color(Color::Muted),
),
)
.child(
ToggleButtonGroup::single_row(
"onboarding-show-mini-map",
@ -252,36 +346,37 @@ pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl Int
.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),
)
.child(SwitchField::new(
"onboarding-enable-inlay-hints",
"Inlay Hints",
Some("See parameter names for function and method calls inline.".into()),
if read_inlay_hints(cx) {
ui::ToggleState::Selected
} else {
ui::ToggleState::Unselected
},
|toggle_state, _, cx| {
write_inlay_hints(toggle_state == &ToggleState::Selected, cx);
},
))
.child(SwitchField::new(
"onboarding-git-blame-switch",
"Git Blame",
Some("See who committed each line on a given file.".into()),
if read_git_blame(cx) {
ui::ToggleState::Selected
} else {
ui::ToggleState::Unselected
},
|toggle_state, _, cx| {
set_git_blame(toggle_state == &ToggleState::Selected, cx);
},
))
}
pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement {
v_flex()
.gap_4()
.child(render_import_settings_section())
.child(render_popular_settings_section(window, cx))
}

View file

@ -1,27 +1,34 @@
use crate::welcome::{ShowWelcome, WelcomePage};
use client::{Client, UserStore};
use command_palette_hooks::CommandPaletteFilter;
use db::kvp::KEY_VALUE_STORE;
use feature_flags::{FeatureFlag, FeatureFlagViewExt as _};
use fs::Fs;
use gpui::{
AnyElement, App, AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable,
IntoElement, Render, SharedString, Subscription, Task, WeakEntity, Window, actions,
Action, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity, EventEmitter,
FocusHandle, Focusable, IntoElement, KeyContext, Render, SharedString, Subscription, Task,
WeakEntity, Window, actions,
};
use settings::{Settings, SettingsStore, update_settings_file};
use schemars::JsonSchema;
use serde::Deserialize;
use settings::{SettingsStore, VsCodeSettingsSource};
use std::sync::Arc;
use theme::{ThemeMode, ThemeSettings};
use ui::{
Divider, FluentBuilder, Headline, KeyBinding, ParentElement as _, StatefulInteractiveElement,
ToggleButtonGroup, ToggleButtonSimple, Vector, VectorName, prelude::*, rems_from_px,
Avatar, ButtonLike, FluentBuilder, Headline, KeyBinding, ParentElement as _,
StatefulInteractiveElement, Vector, VectorName, prelude::*, rems_from_px,
};
use workspace::{
AppState, Workspace, WorkspaceId,
dock::DockPosition,
item::{Item, ItemEvent},
open_new, with_active_or_new_workspace,
notifications::NotifyResultExt as _,
open_new, register_serializable_item, with_active_or_new_workspace,
};
mod ai_setup_page;
mod basics_page;
mod editing_page;
mod theme_preview;
mod welcome;
pub struct OnBoardingFeatureFlag {}
@ -30,6 +37,24 @@ impl FeatureFlag for OnBoardingFeatureFlag {
const NAME: &'static str = "onboarding";
}
/// Imports settings from Visual Studio Code.
#[derive(Copy, Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)]
#[action(namespace = zed)]
#[serde(deny_unknown_fields)]
pub struct ImportVsCodeSettings {
#[serde(default)]
pub skip_prompt: bool,
}
/// Imports settings from Cursor editor.
#[derive(Copy, Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)]
#[action(namespace = zed)]
#[serde(deny_unknown_fields)]
pub struct ImportCursorSettings {
#[serde(default)]
pub skip_prompt: bool,
}
pub const FIRST_OPEN: &str = "first_open";
actions!(
@ -40,6 +65,18 @@ actions!(
]
);
actions!(
onboarding,
[
/// Activates the Basics page.
ActivateBasicsPage,
/// Activates the Editing page.
ActivateEditingPage,
/// Activates the AI Setup page.
ActivateAISetupPage,
]
);
pub fn init(cx: &mut App) {
cx.on_action(|_: &OpenOnboarding, cx| {
with_active_or_new_workspace(cx, |workspace, window, cx| {
@ -54,7 +91,7 @@ pub fn init(cx: &mut App) {
if let Some(existing) = existing {
workspace.activate_item(&existing, true, true, window, cx);
} else {
let settings_page = Onboarding::new(workspace.weak_handle(), cx);
let settings_page = Onboarding::new(workspace, cx);
workspace.add_item_to_active_pane(
Box::new(settings_page),
None,
@ -81,7 +118,7 @@ pub fn init(cx: &mut App) {
if let Some(existing) = existing {
workspace.activate_item(&existing, true, true, window, cx);
} else {
let settings_page = WelcomePage::new(cx);
let settings_page = WelcomePage::new(window, cx);
workspace.add_item_to_active_pane(
Box::new(settings_page),
None,
@ -95,6 +132,43 @@ pub fn init(cx: &mut App) {
});
});
cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
workspace.register_action(|_workspace, action: &ImportVsCodeSettings, window, cx| {
let fs = <dyn Fs>::global(cx);
let action = *action;
window
.spawn(cx, async move |cx: &mut AsyncWindowContext| {
handle_import_vscode_settings(
VsCodeSettingsSource::VsCode,
action.skip_prompt,
fs,
cx,
)
.await
})
.detach();
});
workspace.register_action(|_workspace, action: &ImportCursorSettings, window, cx| {
let fs = <dyn Fs>::global(cx);
let action = *action;
window
.spawn(cx, async move |cx: &mut AsyncWindowContext| {
handle_import_vscode_settings(
VsCodeSettingsSource::Cursor,
action.skip_prompt,
fs,
cx,
)
.await
})
.detach();
});
})
.detach();
cx.observe_new::<Workspace>(|_, window, cx| {
let Some(window) = window else {
return;
@ -123,6 +197,7 @@ pub fn init(cx: &mut App) {
.detach();
})
.detach();
register_serializable_item::<Onboarding>(cx);
}
pub fn show_onboarding_view(app_state: Arc<AppState>, cx: &mut App) -> Task<anyhow::Result<()>> {
@ -133,7 +208,7 @@ pub fn show_onboarding_view(app_state: Arc<AppState>, cx: &mut App) -> Task<anyh
|workspace, window, cx| {
{
workspace.toggle_dock(DockPosition::Left, window, cx);
let onboarding_page = Onboarding::new(workspace.weak_handle(), cx);
let onboarding_page = Onboarding::new(workspace, cx);
workspace.add_item_to_center(Box::new(onboarding_page.clone()), window, cx);
window.focus(&onboarding_page.focus_handle(cx));
@ -147,23 +222,6 @@ pub fn show_onboarding_view(app_state: Arc<AppState>, cx: &mut App) -> Task<anyh
)
}
fn read_theme_selection(cx: &App) -> ThemeMode {
let settings = ThemeSettings::get_global(cx);
settings
.theme_selection
.as_ref()
.and_then(|selection| selection.mode())
.unwrap_or_default()
}
fn write_theme_selection(theme_mode: ThemeMode, cx: &App) {
let fs = <dyn Fs>::global(cx);
update_settings_file::<ThemeSettings>(fs, cx, move |settings, _| {
settings.set_mode(theme_mode);
});
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SelectedPage {
Basics,
@ -175,163 +233,278 @@ struct Onboarding {
workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle,
selected_page: SelectedPage,
user_store: Entity<UserStore>,
_settings_subscription: Subscription,
}
impl Onboarding {
fn new(workspace: WeakEntity<Workspace>, cx: &mut App) -> Entity<Self> {
fn new(workspace: &Workspace, cx: &mut App) -> Entity<Self> {
cx.new(|cx| Self {
workspace,
workspace: workspace.weak_handle(),
focus_handle: cx.focus_handle(),
selected_page: SelectedPage::Basics,
user_store: workspace.user_store().clone(),
_settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
})
}
fn render_page_nav(
fn set_page(&mut self, page: SelectedPage, cx: &mut Context<Self>) {
self.selected_page = page;
cx.notify();
cx.emit(ItemEvent::UpdateTab);
}
fn render_nav_buttons(
&mut self,
page: SelectedPage,
_: &mut Window,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let text = match page {
SelectedPage::Basics => "Basics",
SelectedPage::Editing => "Editing",
SelectedPage::AiSetup => "AI Setup",
};
let binding = match page {
SelectedPage::Basics => {
KeyBinding::new(vec![gpui::Keystroke::parse("cmd-1").unwrap()], cx)
}
SelectedPage::Editing => {
KeyBinding::new(vec![gpui::Keystroke::parse("cmd-2").unwrap()], cx)
}
SelectedPage::AiSetup => {
KeyBinding::new(vec![gpui::Keystroke::parse("cmd-3").unwrap()], cx)
}
};
let selected = self.selected_page == page;
h_flex()
.id(text)
.rounded_sm()
.child(text)
.child(binding)
.h_8()
.gap_2()
.px_2()
.py_0p5()
.w_full()
) -> [impl IntoElement; 3] {
let pages = [
SelectedPage::Basics,
SelectedPage::Editing,
SelectedPage::AiSetup,
];
let text = ["Basics", "Editing", "AI Setup"];
let actions: [&dyn Action; 3] = [
&ActivateBasicsPage,
&ActivateEditingPage,
&ActivateAISetupPage,
];
let mut binding = actions.map(|action| {
KeyBinding::for_action_in(action, &self.focus_handle, window, cx)
.map(|kb| kb.size(rems_from_px(12.)))
});
pages.map(|page| {
let i = page as usize;
let selected = self.selected_page == page;
h_flex()
.id(text[i])
.relative()
.w_full()
.gap_2()
.px_2()
.py_0p5()
.justify_between()
.rounded_sm()
.when(selected, |this| {
this.child(
div()
.h_4()
.w_px()
.bg(cx.theme().colors().text_accent)
.absolute()
.left_0(),
)
})
.hover(|style| style.bg(cx.theme().colors().element_hover))
.child(Label::new(text[i]).map(|this| {
if selected {
this.color(Color::Default)
} else {
this.color(Color::Muted)
}
}))
.child(binding[i].take().map_or(
gpui::Empty.into_any_element(),
IntoElement::into_any_element,
))
.on_click(cx.listener(move |this, _, _, cx| {
this.set_page(page, cx);
}))
})
}
fn render_nav(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.h_full()
.w(rems_from_px(220.))
.flex_shrink_0()
.gap_4()
.justify_between()
.map(|this| {
if selected {
this.bg(Color::Selected.color(cx))
.border_l_1()
.border_color(Color::Accent.color(cx))
.child(
v_flex()
.gap_6()
.child(
h_flex()
.px_2()
.gap_4()
.child(Vector::square(VectorName::ZedLogo, rems(2.5)))
.child(
v_flex()
.child(
Headline::new("Welcome to Zed").size(HeadlineSize::Small),
)
.child(
Label::new("The editor for what's next")
.color(Color::Muted)
.size(LabelSize::Small)
.italic(),
),
),
)
.child(
v_flex()
.gap_4()
.child(
v_flex()
.py_4()
.border_y_1()
.border_color(cx.theme().colors().border_variant.opacity(0.5))
.gap_1()
.children(self.render_nav_buttons(window, cx)),
)
.child(
ButtonLike::new("skip_all")
.child(Label::new("Skip All").ml_1())
.on_click(|_, _, cx| {
with_active_or_new_workspace(
cx,
|workspace, window, cx| {
let Some((onboarding_id, onboarding_idx)) =
workspace
.active_pane()
.read(cx)
.items()
.enumerate()
.find_map(|(idx, item)| {
let _ =
item.downcast::<Onboarding>()?;
Some((item.item_id(), idx))
})
else {
return;
};
workspace.active_pane().update(cx, |pane, cx| {
// Get the index here to get around the borrow checker
let idx = pane.items().enumerate().find_map(
|(idx, item)| {
let _ =
item.downcast::<WelcomePage>()?;
Some(idx)
},
);
if let Some(idx) = idx {
pane.activate_item(
idx, true, true, window, cx,
);
} else {
let item =
Box::new(WelcomePage::new(window, cx));
pane.add_item(
item,
true,
true,
Some(onboarding_idx),
window,
cx,
);
}
pane.remove_item(
onboarding_id,
false,
false,
window,
cx,
);
});
},
);
}),
),
),
)
.child(
if let Some(user) = self.user_store.read(cx).current_user() {
h_flex()
.pl_1p5()
.gap_2()
.child(Avatar::new(user.avatar_uri.clone()))
.child(Label::new(user.github_login.clone()))
.into_any_element()
} else {
this.text_color(Color::Muted.color(cx))
}
})
.hover(|style| {
if selected {
style.bg(Color::Selected.color(cx).opacity(0.6))
} else {
style.bg(Color::Selected.color(cx).opacity(0.3))
}
})
.on_click(cx.listener(move |this, _, _, cx| {
this.selected_page = page;
cx.notify();
}))
Button::new("sign_in", "Sign In")
.style(ButtonStyle::Outlined)
.full_width()
.on_click(|_, window, cx| {
let client = Client::global(cx);
window
.spawn(cx, async move |cx| {
client
.sign_in_with_optional_connect(true, &cx)
.await
.notify_async_err(cx);
})
.detach();
})
.into_any_element()
},
)
}
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::Basics => {
crate::basics_page::render_basics_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(),
SelectedPage::AiSetup => {
crate::ai_setup_page::render_ai_setup_page(&self, window, cx).into_any_element()
}
}
}
fn render_basics_page(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme_mode = read_theme_selection(cx);
v_flex().child(
h_flex().justify_between().child(Label::new("Theme")).child(
ToggleButtonGroup::single_row(
"theme-selector-onboarding",
[
ToggleButtonSimple::new("Light", |_, _, cx| {
write_theme_selection(ThemeMode::Light, cx)
}),
ToggleButtonSimple::new("Dark", |_, _, cx| {
write_theme_selection(ThemeMode::Dark, cx)
}),
ToggleButtonSimple::new("System", |_, _, cx| {
write_theme_selection(ThemeMode::System, cx)
}),
],
)
.selected_index(match theme_mode {
ThemeMode::Light => 0,
ThemeMode::Dark => 1,
ThemeMode::System => 2,
})
.style(ui::ToggleButtonGroupStyle::Outlined)
.button_width(rems_from_px(64.)),
),
)
}
fn render_ai_setup_page(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
div().child("ai setup page")
}
}
impl Render for Onboarding {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
h_flex()
.image_cache(gpui::retain_all("onboarding-page"))
.key_context("onboarding-page")
.px_24()
.py_12()
.items_start()
.key_context({
let mut ctx = KeyContext::new_with_defaults();
ctx.add("Onboarding");
ctx
})
.track_focus(&self.focus_handle)
.size_full()
.bg(cx.theme().colors().editor_background)
.on_action(cx.listener(|this, _: &ActivateBasicsPage, _, cx| {
this.set_page(SelectedPage::Basics, cx);
}))
.on_action(cx.listener(|this, _: &ActivateEditingPage, _, cx| {
this.set_page(SelectedPage::Editing, cx);
}))
.on_action(cx.listener(|this, _: &ActivateAISetupPage, _, cx| {
this.set_page(SelectedPage::AiSetup, cx);
}))
.child(
v_flex()
.w_1_3()
.h_full()
h_flex()
.max_w(rems_from_px(1100.))
.size_full()
.m_auto()
.py_20()
.px_12()
.items_start()
.gap_12()
.child(self.render_nav(window, cx))
.child(
h_flex()
.pt_0p5()
.child(Vector::square(VectorName::ZedLogo, rems(2.)))
.child(
v_flex()
.left_1()
.items_center()
.child(Headline::new("Welcome to Zed"))
.child(
Label::new("The editor for what's next")
.color(Color::Muted)
.italic(),
),
),
)
.p_1()
.child(Divider::horizontal_dashed())
.child(
v_flex().gap_1().children([
self.render_page_nav(SelectedPage::Basics, window, cx)
.into_element(),
self.render_page_nav(SelectedPage::Editing, window, cx)
.into_element(),
self.render_page_nav(SelectedPage::AiSetup, window, cx)
.into_element(),
]),
v_flex()
.max_w_full()
.min_w_0()
.pl_12()
.border_l_1()
.border_color(cx.theme().colors().border_variant.opacity(0.5))
.size_full()
.child(self.render_page(window, cx)),
),
)
// .child(Divider::vertical_dashed())
.child(div().w_2_3().h_full().child(self.render_page(window, cx)))
}
}
@ -364,10 +537,185 @@ impl Item for Onboarding {
_: &mut Window,
cx: &mut Context<Self>,
) -> Option<Entity<Self>> {
Some(Onboarding::new(self.workspace.clone(), cx))
self.workspace
.update(cx, |workspace, cx| Onboarding::new(workspace, cx))
.ok()
}
fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
f(*event)
}
}
pub async fn handle_import_vscode_settings(
source: VsCodeSettingsSource,
skip_prompt: bool,
fs: Arc<dyn Fs>,
cx: &mut AsyncWindowContext,
) {
use util::truncate_and_remove_front;
let vscode_settings =
match settings::VsCodeSettings::load_user_settings(source, fs.clone()).await {
Ok(vscode_settings) => vscode_settings,
Err(err) => {
zlog::error!("{err}");
let _ = cx.prompt(
gpui::PromptLevel::Info,
&format!("Could not find or load a {source} settings file"),
None,
&["Ok"],
);
return;
}
};
if !skip_prompt {
let prompt = cx.prompt(
gpui::PromptLevel::Warning,
&format!(
"Importing {} settings may overwrite your existing settings. \
Will import settings from {}",
vscode_settings.source,
truncate_and_remove_front(&vscode_settings.path.to_string_lossy(), 128),
),
None,
&["Ok", "Cancel"],
);
let result = cx.spawn(async move |_| prompt.await.ok()).await;
if result != Some(0) {
return;
}
};
cx.update(|_, cx| {
let source = vscode_settings.source;
let path = vscode_settings.path.clone();
cx.global::<SettingsStore>()
.import_vscode_settings(fs, vscode_settings);
zlog::info!("Imported {source} settings from {}", path.display());
})
.ok();
}
impl workspace::SerializableItem for Onboarding {
fn serialized_item_kind() -> &'static str {
"OnboardingPage"
}
fn cleanup(
workspace_id: workspace::WorkspaceId,
alive_items: Vec<workspace::ItemId>,
_window: &mut Window,
cx: &mut App,
) -> gpui::Task<gpui::Result<()>> {
workspace::delete_unloaded_items(
alive_items,
workspace_id,
"onboarding_pages",
&persistence::ONBOARDING_PAGES,
cx,
)
}
fn deserialize(
_project: Entity<project::Project>,
workspace: WeakEntity<Workspace>,
workspace_id: workspace::WorkspaceId,
item_id: workspace::ItemId,
window: &mut Window,
cx: &mut App,
) -> gpui::Task<gpui::Result<Entity<Self>>> {
window.spawn(cx, async move |cx| {
if let Some(page_number) =
persistence::ONBOARDING_PAGES.get_onboarding_page(item_id, workspace_id)?
{
let page = match page_number {
0 => Some(SelectedPage::Basics),
1 => Some(SelectedPage::Editing),
2 => Some(SelectedPage::AiSetup),
_ => None,
};
workspace.update(cx, |workspace, cx| {
let onboarding_page = Onboarding::new(workspace, cx);
if let Some(page) = page {
zlog::info!("Onboarding page {page:?} loaded");
onboarding_page.update(cx, |onboarding_page, cx| {
onboarding_page.set_page(page, cx);
})
}
onboarding_page
})
} else {
Err(anyhow::anyhow!("No onboarding page to deserialize"))
}
})
}
fn serialize(
&mut self,
workspace: &mut Workspace,
item_id: workspace::ItemId,
_closing: bool,
_window: &mut Window,
cx: &mut ui::Context<Self>,
) -> Option<gpui::Task<gpui::Result<()>>> {
let workspace_id = workspace.database_id()?;
let page_number = self.selected_page as u16;
Some(cx.background_spawn(async move {
persistence::ONBOARDING_PAGES
.save_onboarding_page(item_id, workspace_id, page_number)
.await
}))
}
fn should_serialize(&self, event: &Self::Event) -> bool {
event == &ItemEvent::UpdateTab
}
}
mod persistence {
use db::{define_connection, query, sqlez_macros::sql};
use workspace::WorkspaceDb;
define_connection! {
pub static ref ONBOARDING_PAGES: OnboardingPagesDb<WorkspaceDb> =
&[
sql!(
CREATE TABLE onboarding_pages (
workspace_id INTEGER,
item_id INTEGER UNIQUE,
page_number INTEGER,
PRIMARY KEY(workspace_id, item_id),
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
) STRICT;
),
];
}
impl OnboardingPagesDb {
query! {
pub async fn save_onboarding_page(
item_id: workspace::ItemId,
workspace_id: workspace::WorkspaceId,
page_number: u16
) -> Result<()> {
INSERT OR REPLACE INTO onboarding_pages(item_id, workspace_id, page_number)
VALUES (?, ?, ?)
}
}
query! {
pub fn get_onboarding_page(
item_id: workspace::ItemId,
workspace_id: workspace::WorkspaceId
) -> Result<Option<u16>> {
SELECT page_number
FROM onboarding_pages
WHERE item_id = ? AND workspace_id = ?
}
}
}
}

View file

@ -11,22 +11,14 @@ use ui::{
#[derive(IntoElement, RegisterComponent, Documented)]
pub struct ThemePreviewTile {
theme: Arc<Theme>,
selected: bool,
seed: f32,
}
impl ThemePreviewTile {
pub fn new(theme: Arc<Theme>, selected: bool, seed: f32) -> Self {
Self {
theme,
selected,
seed,
}
}
pub const CORNER_RADIUS: Pixels = px(8.0);
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
pub fn new(theme: Arc<Theme>, seed: f32) -> Self {
Self { theme, seed }
}
}
@ -34,7 +26,7 @@ impl RenderOnce for ThemePreviewTile {
fn render(self, _window: &mut ui::Window, _cx: &mut ui::App) -> impl IntoElement {
let color = self.theme.colors();
let root_radius = px(8.0);
let root_radius = Self::CORNER_RADIUS;
let root_border = px(2.0);
let root_padding = px(2.0);
let child_border = px(1.0);
@ -43,7 +35,7 @@ impl RenderOnce for ThemePreviewTile {
let item_skeleton = |w: Length, h: Pixels, bg: Hsla| div().w(w).h(h).rounded_full().bg(bg);
let skeleton_height = px(4.);
let skeleton_height = px(2.);
let sidebar_seeded_width = |seed: f32, index: usize| {
let value = (seed * 1000.0 + index as f32 * 10.0).sin() * 0.5 + 0.5;
@ -70,12 +62,10 @@ impl RenderOnce for ThemePreviewTile {
.border_color(color.border_transparent)
.bg(color.panel_background)
.child(
div()
v_flex()
.p_2()
.flex()
.flex_col()
.size_full()
.gap(px(4.))
.gap_1()
.children(sidebar_skeleton),
);
@ -151,32 +141,19 @@ impl RenderOnce for ThemePreviewTile {
v_flex()
.size_full()
.p_1()
.gap(px(6.))
.gap_1p5()
.children(lines)
.into_any_element()
};
let pane = div()
.h_full()
.flex_grow()
.flex()
.flex_col()
// .child(
// div()
// .w_full()
// .border_color(color.border)
// .border_b(px(1.))
// .h(relative(0.1))
// .bg(color.tab_bar_background),
// )
.child(
div()
.size_full()
.overflow_hidden()
.bg(color.editor_background)
.p_2()
.child(pseudo_code_skeleton(self.theme.clone(), self.seed)),
);
let pane = v_flex().h_full().flex_grow().child(
div()
.size_full()
.overflow_hidden()
.bg(color.editor_background)
.p_2()
.child(pseudo_code_skeleton(self.theme.clone(), self.seed)),
);
let content = div().size_full().flex().child(sidebar).child(pane);
@ -184,11 +161,6 @@ impl RenderOnce for ThemePreviewTile {
.size_full()
.rounded(root_radius)
.p(root_padding)
.border(root_border)
.border_color(color.border_transparent)
.when(self.selected, |this| {
this.border_color(color.border_selected)
})
.child(
div()
.size_full()
@ -230,24 +202,14 @@ impl Component for ThemePreviewTile {
.p_4()
.children({
if let Some(one_dark) = one_dark.ok() {
vec![example_group(vec![
single_example(
"Default",
div()
.w(px(240.))
.h(px(180.))
.child(ThemePreviewTile::new(one_dark.clone(), false, 0.42))
.into_any_element(),
),
single_example(
"Selected",
div()
.w(px(240.))
.h(px(180.))
.child(ThemePreviewTile::new(one_dark, true, 0.42))
.into_any_element(),
),
])]
vec![example_group(vec![single_example(
"Default",
div()
.w(px(240.))
.h(px(180.))
.child(ThemePreviewTile::new(one_dark.clone(), 0.42))
.into_any_element(),
)])]
} else {
vec![]
}
@ -261,12 +223,11 @@ impl Component for ThemePreviewTile {
themes_to_preview
.iter()
.enumerate()
.map(|(i, theme)| {
div().w(px(200.)).h(px(140.)).child(ThemePreviewTile::new(
theme.clone(),
false,
0.42,
))
.map(|(_, theme)| {
div()
.w(px(200.))
.h(px(140.))
.child(ThemePreviewTile::new(theme.clone(), 0.42))
})
.collect::<Vec<_>>(),
)

View file

@ -4,10 +4,13 @@ use gpui::{
};
use ui::{ButtonLike, Divider, DividerColor, KeyBinding, Vector, VectorName, prelude::*};
use workspace::{
NewFile, Open, Workspace, WorkspaceId,
NewFile, Open, WorkspaceId,
item::{Item, ItemEvent},
with_active_or_new_workspace,
};
use zed_actions::{Extensions, OpenSettings, command_palette};
use zed_actions::{Extensions, OpenSettings, agent, command_palette};
use crate::{Onboarding, OpenOnboarding};
actions!(
zed,
@ -55,8 +58,7 @@ const CONTENT: (Section<4>, Section<3>) = (
SectionEntry {
icon: IconName::ZedAssistant,
title: "View AI Settings",
// TODO: use proper action
action: &NoAction,
action: &agent::OpenSettings,
},
SectionEntry {
icon: IconName::Blocks,
@ -217,7 +219,64 @@ impl Render for WelcomePage {
div().child(
Button::new("welcome-exit", "Return to Setup")
.full_width()
.label_size(LabelSize::XSmall),
.label_size(LabelSize::XSmall)
.on_click(|_, window, cx| {
window.dispatch_action(
OpenOnboarding.boxed_clone(),
cx,
);
with_active_or_new_workspace(cx, |workspace, window, cx| {
let Some((welcome_id, welcome_idx)) = workspace
.active_pane()
.read(cx)
.items()
.enumerate()
.find_map(|(idx, item)| {
let _ = item.downcast::<WelcomePage>()?;
Some((item.item_id(), idx))
})
else {
return;
};
workspace.active_pane().update(cx, |pane, cx| {
// Get the index here to get around the borrow checker
let idx = pane.items().enumerate().find_map(
|(idx, item)| {
let _ =
item.downcast::<Onboarding>()?;
Some(idx)
},
);
if let Some(idx) = idx {
pane.activate_item(
idx, true, true, window, cx,
);
} else {
let item =
Box::new(Onboarding::new(workspace, cx));
pane.add_item(
item,
true,
true,
Some(welcome_idx),
window,
cx,
);
}
pane.remove_item(
welcome_id,
false,
false,
window,
cx,
);
});
});
}),
),
),
),
@ -228,12 +287,14 @@ impl Render for WelcomePage {
}
impl WelcomePage {
pub fn new(cx: &mut Context<Workspace>) -> Entity<Self> {
let this = cx.new(|cx| WelcomePage {
focus_handle: cx.focus_handle(),
});
pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
cx.new(|cx| {
let focus_handle = cx.focus_handle();
cx.on_focus(&focus_handle, window, |_, _, cx| cx.notify())
.detach();
this
WelcomePage { focus_handle }
})
}
}

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