Compare commits

..

25 Commits

Author SHA1 Message Date
Ben Brandt
a2ce038352 eval: retry in more scenarios 2025-07-31 11:21:05 +02:00
Joseph T. Lyons
47af878ebb Do not sort settings profiles (#35389)
After playing with this for a bit, I realize it does not feel good to
not have control over the order of profiles. I find myself wanting to
group similar profiles together and not being able to.

Release Notes:

- N/A
2025-07-31 07:34:35 +00:00
Danilo Leal
5488398986 onboarding: Refine page and component designs (#35387)
Includes adding new variants to the Dropdown and Numeric Stepper
components.

Release Notes:

- N/A
2025-07-31 05:32:18 +00:00
Marshall Bowers
b1a7993544 cloud_api_types: Add more data to the GetAuthenticatedUserResponse (#35384)
This PR adds more data to the `GetAuthenticatedUserResponse`.

We now return more information about the authenticated user, as well as
their plan information.

Release Notes:

- N/A
2025-07-30 23:38:51 -04:00
Marshall Bowers
b90fd4287f client: Don't fetch the authenticated user once we have them (#35385)
This PR makes it so we don't keep fetching the authenticated user once
we have them.

Release Notes:

- N/A
2025-07-31 03:37:02 +00:00
Ben Kunkle
e1e2775b80 docs: Run lychee link check on generated docs output (#35381)
Closes #ISSUE

Following #35310, . This PR makes it so the lychee link check is ran
before building the docs on the md files to catch basic errors, and then
after building on the html output to catch generation errors, including
regressions like the one #35380 fixes.

Release Notes:

- N/A
2025-07-31 02:01:40 +00:00
Joseph T. Lyons
ed104ec5e0 Ensure settings are being adjusted via settings profile selector (#35382)
This PR just pins down the behavior of the settings profile selector by
checking a single setting, `buffer_font_size`, as options in the
selector are changed / selected.

Release Notes:

- N/A
2025-07-31 01:52:02 +00:00
Kainoa Kanter
67a491df50 Use outlined bolt icon for the LSP tool (#35373)
| Before | After |
|--------|--------|
| <img width="266" height="67" alt="image"
src="https://github.com/user-attachments/assets/bbfc75b6-6747-4eb1-ab94-ab098eba5335"
/> | <img width="266" height="67" alt="image"
src="https://github.com/user-attachments/assets/4631be9d-3d5e-4eb6-bf2f-596403fdf014"
/> |

Release Notes:

- Changed the icon of the language servers entry in the status bar.
2025-07-30 21:37:10 -04:00
Marshall Bowers
f003036aec docs: Pin mdbook to v0.4.40 (#35380)
This PR pins `mdbook` to v0.4.40 to fix an issue with sidebar links
having some of their path segments duplicated (e.g.,
`http://localhost:3000/extensions/extensions/developing-extensions.html`.

For reference:

-
https://zed-industries.slack.com/archives/C04S5TU0RSN/p1745439470378339?thread_ts=1745428671.190059&cid=C04S5TU0RSN
-
https://zed-industries.slack.com/archives/C04S5TU0RSN/p1753922478290399

Release Notes:

- N/A
2025-07-31 01:34:26 +00:00
Marshall Bowers
fbc784d323 Use the user from the CloudUserStore to drive the user menu (#35375)
This PR updates the user menu in the title bar to base the "signed in"
state on the user in the `CloudUserStore` rather than the `UserStore`.

This makes it possible to be signed-in—at least, as far as the user menu
is concerned—even when disconnected from Collab.

Release Notes:

- N/A
2025-07-30 20:31:22 -04:00
Piotr Osiewicz
296bb66b65 chore: Move a few more tasks into background_spawn (#35374)
Release Notes:

- N/A
2025-07-30 23:56:47 +00:00
Marshall Bowers
bb1a7ccbba client: Add CloudUserStore (#35370)
This PR adds a new `CloudUserStore` for storing information about the
user retrieved from Cloud instead of Collab.

Release Notes:

- N/A
2025-07-30 18:43:10 -04:00
Marshall Bowers
289f420504 Sort crate members in Cargo.toml (#35371)
This PR sorts the crate members in the `Cargo.toml` file, as they had
gotten unsorted.

Release Notes:

- N/A
2025-07-30 22:35:17 +00:00
张小白
15ad986329 windows: Port to DirectX 11 (#34374)
Closes #16713
Closes #19739
Closes #33191
Closes #26692
Closes #17374
Closes #35077
Closes https://github.com/zed-industries/zed/issues/35205
Closes https://github.com/zed-industries/zed/issues/35262


Compared to the current Vulkan implementation, this PR brings several
improvements:

- Fewer weird bugs
- Better hardware compatibility
- VSync support
- More accurate colors
- Lower memory usage
- Graceful handling of device loss

---

**TODO:**

- [x] Don’t use AGS binaries directly
- [ ] The message loop is using too much CPU when ths app is idle
- [x] There’s a
[bug](https://github.com/zed-industries/zed/issues/33191#issuecomment-3109306630)
in how `Path` is being rendered.

---

Release Notes:

- N/A

---------

Co-authored-by: Kate <kate@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2025-07-30 15:27:58 -07:00
Finn Evers
0d9715325c docs: Add section about terminal contrast adjustments (#35369)
Closes #35146

This change adds documentation for the `terminal.minimum_contrast`
setting to the docs as we've had a lot of reports regarding the contrast
adjustments, yet are missing proper documentation (aside from that in
the `defaults.json`) for it.

Release Notes:

- N/A
2025-07-31 00:19:56 +02:00
Joseph T. Lyons
5ef5f3c5ca Introduce settings profiles (#35339)
Settings Profiles

- [X] Allow profiles to be defined, where each profile can be any of
Zed's settings
    - [X] Autocompletion of all settings
    - [X] Errors on invalid keys
- [X] Action brings up modal that shows user-defined profiles
- [X] Alphabetize profiles
- [X] Ability to filter down via keyboard, and navigate via arrow up and
down
- [X] Auto select Disabled option by default (first in list, after
alphabetizing user-defined profiles)
- [X] Automatically select active profile on next picker summoning
- [X] Persist settings until toggled off
- [X] Show live preview as you select from the profile picker
- [X] Tweaking a setting, while in a profile, updates the profile live
- [X] Make sure actions that live update Zed, such as `cmd-0`, `cmd-+`,
and `cmd--`, work while in a profile
- [X] Add a test to track state

Release Notes:

- Added the ability to configure settings profiles, via the "profiles"
key. Example:

```json
{
  "profiles": {
    "Streaming": {
      "agent_font_size": 20,
      "buffer_font_size": 20,
      "theme": "One Light",
      "ui_font_size": 20
    }
  }
}
```

To set a profile, use `settings profile selector: toggle`
2025-07-30 21:48:24 +00:00
Anthony Eid
2d4afd2119 Polish onboarding page (#35367)
- Added borders to the numeric stepper.
- Changed the hover mouse style for SwitchField.
- Made the edit page toggle buttons more responsive.

Release Notes:

- N/A
2025-07-30 20:35:21 +00:00
Ben Kunkle
afcb8f2a3f onboarding: Wire up settings import (#35366)
Closes #ISSUE

Release Notes:

- N/A *or* Added/Fixed/Improved ...
2025-07-30 20:09:11 +00:00
Smit Barmase
cdce3b3620 linux: Fix caps lock not working consistently for certain X11 systems (#35361)
Closes #35316

Bug in https://github.com/zed-industries/zed/pull/34514

Turns out you are not supposed to call `update_key` for modifiers on
`KeyPress`/`KeyRelease`, as modifiers are already updated in
`XkbStateNotify` events. Not sure why this only causes issues on a few
systems and works on others.

Tested on Ubuntu 24.04.2 LTS (initial bug) and Kubuntu 25.04 (worked
fine before too).

Release Notes:

- Fixed an issue where caps lock stopped working consistently on some
Linux X11 systems.
2025-07-31 00:56:36 +05:30
Marshall Bowers
bc6bb42745 Add cloud_api_client and cloud_api_types crates (#35357)
This PR adds two new crates for interacting with Cloud:

- `cloud_api_client` - The client that will be used to talk to Cloud.
- `cloud_api_types` - The types for the Cloud API that are shared
between Zed and Cloud.

Release Notes:

- N/A
2025-07-30 18:57:51 +00:00
Marshall Bowers
7695c4b82e collab: Temporarily add back GET /user endpoint for local development (#35358)
This PR temporarily adds back the `GET /user` endpoint to Collab since
we're still using it for local development.

Will remove it again once we update the local development process to
leverage Cloud.

Release Notes:

- N/A
2025-07-30 18:54:44 +00:00
Smit Barmase
794ade8b6d ui_prompt: Fix prompt dialog is hard to see on large screen (#35348)
Closes #18516

Release Notes:

- Improved visibility of prompt dialog on Linux by dimming the
background.
2025-07-30 23:03:53 +05:30
Smit Barmase
f4bd524d7f ui_prompt: Fix copy version number from About Zed (#35346)
Closes #29361

Release Notes:

- Fixed not selectable version number in About Zed prompt on Linux.
2025-07-30 22:51:59 +05:30
Joseph T. Lyons
9d82e148de Bump Zed to v0.199 (#35343)
Release Notes:

-N/A
2025-07-30 16:53:53 +00:00
Finn Evers
f8d1062484 onboarding: Fix keybindings showing up after a delay (#35342)
This fixes an issue where keybinds would only show up after a delay on
the welcome page upon re-opening it. It also binds one of the buttons to
the corresponding action.

Release Notes:

- N/A
2025-07-30 16:18:14 +00:00
67 changed files with 5820 additions and 999 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

224
Cargo.lock generated
View File

@@ -153,7 +153,6 @@ version = "0.1.0"
dependencies = [
"acp_thread",
"agent-client-protocol",
"agent_settings",
"agentic-coding-protocol",
"anyhow",
"collections",
@@ -2977,6 +2976,7 @@ dependencies = [
"base64 0.22.1",
"chrono",
"clock",
"cloud_api_client",
"cloud_llm_client",
"cocoa 0.26.0",
"collections",
@@ -3032,6 +3032,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"
@@ -4270,41 +4295,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"
@@ -4520,37 +4510,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"
@@ -5929,7 +5888,7 @@ dependencies = [
"ignore",
"libc",
"log",
"notify",
"notify 8.0.0",
"objc",
"parking_lot",
"paths",
@@ -7486,18 +7445,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]]
@@ -8168,12 +8125,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"
@@ -8392,6 +8343,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"
@@ -8545,7 +8507,7 @@ dependencies = [
"fnv",
"lazy_static",
"libc",
"mio",
"mio 1.0.3",
"rand 0.8.5",
"serde",
"tempfile",
@@ -9985,9 +9947,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",
@@ -9997,12 +9959,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",
@@ -10011,7 +9972,6 @@ dependencies = [
"regex",
"serde",
"serde_json",
"sha2",
"shlex",
"tempfile",
"tokio",
@@ -10154,6 +10114,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"
@@ -10500,6 +10472,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"
@@ -10508,11 +10499,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",
@@ -10520,14 +10511,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]]
@@ -10667,21 +10657,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"
@@ -10961,12 +10936,16 @@ dependencies = [
"gpui",
"language",
"project",
"schemars",
"serde",
"settings",
"theme",
"ui",
"util",
"workspace",
"workspace-hack",
"zed_actions",
"zlog",
]
[[package]]
@@ -14730,6 +14709,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"
@@ -14752,7 +14752,6 @@ dependencies = [
"notifications",
"paths",
"project",
"schemars",
"search",
"serde",
"serde_json",
@@ -16573,7 +16572,7 @@ dependencies = [
"backtrace",
"bytes 1.10.1",
"libc",
"mio",
"mio 1.0.3",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
@@ -19750,7 +19749,7 @@ dependencies = [
"md-5",
"memchr",
"miniz_oxide",
"mio",
"mio 1.0.3",
"naga",
"nix 0.29.0",
"nom",
@@ -20194,7 +20193,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.198.0"
version = "0.199.0"
dependencies = [
"activity_indicator",
"agent",
@@ -20297,6 +20296,7 @@ dependencies = [
"serde_json",
"session",
"settings",
"settings_profile_selector",
"settings_ui",
"shellexpand 2.1.2",
"smol",

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",
@@ -251,6 +254,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 +343,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" }
@@ -674,8 +680,13 @@ features = [
"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",

View File

@@ -1877,5 +1877,8 @@
"save_breakpoints": true,
"dock": "bottom",
"button": true
}
},
// Configures any number of settings profiles that are temporarily applied
// when selected from `settings profile selector: toggle`.
"profiles": []
}

View File

@@ -19,7 +19,6 @@ doctest = false
[dependencies]
acp_thread.workspace = true
agent-client-protocol.workspace = true
agent_settings.workspace = true
agentic-coding-protocol.workspace = true
anyhow.workspace = true
collections.workspace = true

View File

@@ -3,7 +3,6 @@ use std::path::PathBuf;
use crate::claude::tools::{ClaudeTool, EditToolParams, ReadToolParams};
use acp_thread::AcpThread;
use agent_client_protocol as acp;
use agent_settings::AgentSettings;
use anyhow::{Context, Result};
use collections::HashMap;
use context_server::listener::{McpServerTool, ToolResponse};
@@ -14,7 +13,6 @@ use context_server::types::{
use gpui::{App, AsyncApp, Task, WeakEntity};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
pub struct ClaudeZedMcpServer {
server: context_server::listener::McpServer,
@@ -116,7 +114,6 @@ pub struct PermissionToolParams {
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(test, derive(serde::Deserialize))]
pub struct PermissionToolResponse {
behavior: PermissionToolBehavior,
updated_input: serde_json::Value,
@@ -124,8 +121,7 @@ pub struct PermissionToolResponse {
#[derive(Serialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(test, derive(serde::Deserialize))]
pub enum PermissionToolBehavior {
enum PermissionToolBehavior {
Allow,
Deny,
}
@@ -145,26 +141,6 @@ impl McpServerTool for PermissionTool {
input: Self::Input,
cx: &mut AsyncApp,
) -> Result<ToolResponse<Self::Output>> {
// Check if we should automatically allow tool actions
let always_allow =
cx.update(|cx| AgentSettings::get_global(cx).always_allow_tool_actions)?;
if always_allow {
// If always_allow_tool_actions is true, immediately return Allow without prompting
let response = PermissionToolResponse {
behavior: PermissionToolBehavior::Allow,
updated_input: input.input,
};
return Ok(ToolResponse {
content: vec![ToolResponseContent::Text {
text: serde_json::to_string(&response)?,
}],
structured_content: (),
});
}
// Otherwise, proceed with the normal permission flow
let mut thread_rx = self.thread_rx.clone();
let Some(thread) = thread_rx.recv().await?.upgrade() else {
anyhow::bail!("Thread closed");
@@ -324,78 +300,3 @@ impl McpServerTool for EditTool {
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use gpui::TestAppContext;
use project::Project;
use settings::{Settings, SettingsStore};
#[gpui::test]
async fn test_permission_tool_respects_always_allow_setting(cx: &mut TestAppContext) {
// Initialize settings
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
agent_settings::init(cx);
});
// Create a test thread
let project = cx.update(|cx| gpui::Entity::new(cx, |_cx| Project::local()));
let thread = cx.update(|cx| {
gpui::Entity::new(cx, |_cx| {
acp_thread::AcpThread::new(
acp::ConnectionId("test".into()),
project,
std::path::Path::new("/tmp"),
)
})
});
let (tx, rx) = watch::channel(thread.downgrade());
let tool = PermissionTool { thread_rx: rx };
// Test with always_allow_tool_actions = true
cx.update(|cx| {
AgentSettings::override_global(
AgentSettings {
always_allow_tool_actions: true,
..Default::default()
},
cx,
);
});
let input = PermissionToolParams {
tool_name: "test_tool".to_string(),
input: serde_json::json!({"test": "data"}),
tool_use_id: Some("test_id".to_string()),
};
let result = tool.run(input.clone(), &mut cx.to_async()).await.unwrap();
// Should return Allow without prompting
assert_eq!(result.content.len(), 1);
if let ToolResponseContent::Text { text } = &result.content[0] {
let response: PermissionToolResponse = serde_json::from_str(text).unwrap();
assert!(matches!(response.behavior, PermissionToolBehavior::Allow));
} else {
panic!("Expected text response");
}
// Test with always_allow_tool_actions = false
cx.update(|cx| {
AgentSettings::override_global(
AgentSettings {
always_allow_tool_actions: false,
..Default::default()
},
cx,
);
});
// This test would require mocking the permission prompt response
// In the real scenario, it would wait for user input
}
}

View File

@@ -7,7 +7,6 @@ use std::{
use crate::{AgentServer, AgentServerSettings, AllAgentServersSettings};
use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus};
use agent_client_protocol as acp;
use agent_settings::AgentSettings;
use futures::{FutureExt, StreamExt, channel::mpsc, select};
use gpui::{Entity, TestAppContext};
@@ -242,57 +241,6 @@ pub async fn test_tool_call_with_confirmation(
});
}
pub async fn test_tool_call_always_allow(
server: impl AgentServer + 'static,
cx: &mut TestAppContext,
) {
let fs = init_test(cx).await;
let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await;
// Enable always_allow_tool_actions
cx.update(|cx| {
AgentSettings::override_global(
AgentSettings {
always_allow_tool_actions: true,
..Default::default()
},
cx,
);
});
let thread = new_test_thread(server, project.clone(), "/private/tmp", cx).await;
let full_turn = thread.update(cx, |thread, cx| {
thread.send_raw(
r#"Run `touch hello.txt && echo "Hello, world!" | tee hello.txt`"#,
cx,
)
});
// Wait for the tool call to complete
full_turn.await.unwrap();
thread.read_with(cx, |thread, _cx| {
// With always_allow_tool_actions enabled, the tool call should be immediately allowed
// without waiting for confirmation
let tool_call_entry = thread
.entries()
.iter()
.find(|entry| matches!(entry, AgentThreadEntry::ToolCall(_)))
.expect("Expected a tool call entry");
let AgentThreadEntry::ToolCall(tool_call) = tool_call_entry else {
panic!("Expected tool call entry");
};
// Should be allowed, not waiting for confirmation
assert!(
matches!(tool_call.status, ToolCallStatus::Allowed { .. }),
"Expected tool call to be allowed automatically, but got {:?}",
tool_call.status
);
});
}
pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppContext) {
let fs = init_test(cx).await;
@@ -403,12 +351,6 @@ macro_rules! common_e2e_tests {
async fn cancel(cx: &mut ::gpui::TestAppContext) {
$crate::e2e_tests::test_cancel($server, cx).await;
}
#[::gpui::test]
#[cfg_attr(not(feature = "e2e"), ignore)]
async fn tool_call_always_allow(cx: &mut ::gpui::TestAppContext) {
$crate::e2e_tests::test_tool_call_always_allow($server, cx).await;
}
}
};
}

View File

@@ -1663,47 +1663,68 @@ async fn retry_on_rate_limit<R>(mut request: impl AsyncFnMut() -> Result<R>) ->
attempt += 1;
match request().await {
Ok(result) => return Ok(result),
Err(err) => match err.downcast::<LanguageModelCompletionError>() {
Ok(err) => match &err {
LanguageModelCompletionError::RateLimitExceeded { retry_after, .. }
| LanguageModelCompletionError::ServerOverloaded { retry_after, .. } => {
let retry_after = retry_after.unwrap_or(Duration::from_secs(5));
// Wait for the duration supplied, with some jitter to avoid all requests being made at the same time.
let jitter = retry_after.mul_f64(rand::thread_rng().gen_range(0.0..1.0));
eprintln!(
"Attempt #{attempt}: {err}. Retry after {retry_after:?} + jitter of {jitter:?}"
);
Timer::after(retry_after + jitter).await;
continue;
}
LanguageModelCompletionError::UpstreamProviderError {
status,
retry_after,
..
} => {
// Only retry for specific status codes
let should_retry = matches!(
*status,
StatusCode::TOO_MANY_REQUESTS | StatusCode::SERVICE_UNAVAILABLE
) || status.as_u16() == 529;
Err(err) => {
if attempt > 20 {
return Err(err);
}
if !should_retry {
return Err(err.into());
match err.downcast::<LanguageModelCompletionError>() {
Ok(err) => match &err {
LanguageModelCompletionError::RateLimitExceeded { retry_after, .. }
| LanguageModelCompletionError::ServerOverloaded { retry_after, .. } => {
let retry_after = retry_after.unwrap_or(Duration::from_secs(5));
// Wait for the duration supplied, with some jitter to avoid all requests being made at the same time.
let jitter =
retry_after.mul_f64(rand::thread_rng().gen_range(0.0..1.0));
eprintln!(
"Attempt #{attempt}: {err}. Retry after {retry_after:?} + jitter of {jitter:?}"
);
Timer::after(retry_after + jitter).await;
continue;
}
LanguageModelCompletionError::UpstreamProviderError {
status,
retry_after,
..
} => {
// Only retry for specific status codes
let should_retry = matches!(
*status,
StatusCode::TOO_MANY_REQUESTS | StatusCode::SERVICE_UNAVAILABLE
) || status.as_u16() == 529;
// Use server-provided retry_after if available, otherwise use default
let retry_after = retry_after.unwrap_or(Duration::from_secs(5));
let jitter = retry_after.mul_f64(rand::thread_rng().gen_range(0.0..1.0));
eprintln!(
"Attempt #{attempt}: {err}. Retry after {retry_after:?} + jitter of {jitter:?}"
);
Timer::after(retry_after + jitter).await;
continue;
}
_ => return Err(err.into()),
},
Err(err) => return Err(err),
},
if !should_retry {
return Err(err.into());
}
// Use server-provided retry_after if available, otherwise use default
let retry_after = retry_after.unwrap_or(Duration::from_secs(5));
let jitter =
retry_after.mul_f64(rand::thread_rng().gen_range(0.0..1.0));
eprintln!(
"Attempt #{attempt}: {err}. Retry after {retry_after:?} + jitter of {jitter:?}"
);
Timer::after(retry_after + jitter).await;
continue;
}
LanguageModelCompletionError::ApiInternalServerError { .. }
| LanguageModelCompletionError::ApiReadResponseError { .. }
| LanguageModelCompletionError::DeserializeResponse { .. }
| LanguageModelCompletionError::HttpSend { .. } => {
let retry_after = Duration::from_secs(attempt);
let jitter =
retry_after.mul_f64(rand::thread_rng().gen_range(0.0..1.0));
eprintln!(
"Attempt #{attempt}: {err}. Retry after {retry_after:?} + jitter of {jitter:?}"
);
Timer::after(retry_after + jitter).await;
continue;
}
_ => return Err(err.into()),
},
Err(err) => return Err(err),
}
}
}
}
}

View File

@@ -22,6 +22,7 @@ async-tungstenite = { workspace = true, features = ["tokio", "tokio-rustls-manua
base64.workspace = true
chrono = { workspace = true, features = ["serde"] }
clock.workspace = true
cloud_api_client.workspace = true
cloud_llm_client.workspace = true
collections.workspace = true
credentials_provider.workspace = true

View File

@@ -1,6 +1,7 @@
#[cfg(any(test, feature = "test-support"))]
pub mod test;
mod cloud;
mod proxy;
pub mod telemetry;
pub mod user;
@@ -15,6 +16,7 @@ use async_tungstenite::tungstenite::{
};
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,
@@ -51,6 +53,7 @@ use tokio::net::TcpStream;
use url::Url;
use util::{ConnectionResult, ResultExt};
pub use cloud::*;
pub use rpc::*;
pub use telemetry_events::Event;
pub use user::*;
@@ -213,6 +216,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>,
@@ -586,6 +590,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 +623,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
@@ -930,6 +939,8 @@ impl Client {
}
let credentials = credentials.unwrap();
self.set_id(credentials.user_id);
self.cloud_client
.set_credentials(credentials.user_id as u32, credentials.access_token.clone());
if was_disconnected {
self.set_status(Status::Connecting, cx);
@@ -1480,6 +1491,7 @@ impl Client {
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 {

View File

@@ -0,0 +1,3 @@
mod user_store;
pub use user_store::*;

View File

@@ -0,0 +1,69 @@
use std::sync::Arc;
use std::time::Duration;
use anyhow::Context as _;
use cloud_api_client::{AuthenticatedUser, CloudApiClient};
use gpui::{Context, Task};
use util::{ResultExt as _, maybe};
pub struct CloudUserStore {
authenticated_user: Option<Arc<AuthenticatedUser>>,
_maintain_authenticated_user_task: Task<()>,
}
impl CloudUserStore {
pub fn new(cloud_client: Arc<CloudApiClient>, cx: &mut Context<Self>) -> Self {
Self {
authenticated_user: None,
_maintain_authenticated_user_task: cx.spawn(async move |this, cx| {
maybe!(async move {
loop {
let Some(this) = this.upgrade() else {
return anyhow::Ok(());
};
if cloud_client.has_credentials() {
let already_fetched_authenticated_user = this
.read_with(cx, |this, _cx| this.authenticated_user().is_some())
.unwrap_or(false);
if already_fetched_authenticated_user {
// We already fetched the authenticated user; nothing to do.
} else {
let authenticated_user_result = cloud_client
.get_authenticated_user()
.await
.context("failed to fetch authenticated user");
if let Some(response) = authenticated_user_result.log_err() {
this.update(cx, |this, _cx| {
this.authenticated_user = Some(Arc::new(response.user));
})
.ok();
}
}
} else {
this.update(cx, |this, _cx| {
this.authenticated_user = None;
})
.ok();
}
cx.background_executor()
.timer(Duration::from_millis(100))
.await;
}
})
.await
.log_err();
}),
}
}
pub fn is_authenticated(&self) -> bool {
self.authenticated_user.is_some()
}
pub fn authenticated_user(&self) -> Option<Arc<AuthenticatedUser>> {
self.authenticated_user.clone()
}
}

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,83 @@
use std::sync::Arc;
use anyhow::{Result, anyhow};
pub use cloud_api_types::*;
use futures::AsyncReadExt as _;
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
))
}
pub async fn get_authenticated_user(&self) -> Result<GetAuthenticatedUserResponse> {
let request = Request::builder()
.method(Method::GET)
.uri(
self.http_client
.build_zed_cloud_url("/client/users/me", &[])?
.as_ref(),
)
.header("Content-Type", "application/json")
.header("Authorization", self.authorization_header()?)
.body(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)?)
}
}

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,40 @@
mod timestamp;
use serde::{Deserialize, Serialize};
pub use crate::timestamp::Timestamp;
#[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,
}

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

@@ -100,6 +100,7 @@ impl std::fmt::Display for SystemIdHeader {
pub fn routes(rpc_server: Arc<rpc::Server>) -> Router<(), Body> {
Router::new()
.route("/user", get(legacy_update_or_create_authenticated_user))
.route("/users/look_up", get(look_up_user))
.route("/users/:id/access_tokens", post(create_access_token))
.route("/users/:id/refresh_llm_tokens", post(refresh_llm_tokens))
@@ -144,6 +145,51 @@ pub async fn validate_api_token<B>(req: Request<B>, next: Next<B>) -> impl IntoR
Ok::<_, Error>(next.run(req).await)
}
#[derive(Debug, Deserialize)]
struct AuthenticatedUserParams {
github_user_id: i32,
github_login: String,
github_email: Option<String>,
github_name: Option<String>,
github_user_created_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Serialize)]
struct AuthenticatedUserResponse {
user: User,
metrics_id: String,
feature_flags: Vec<String>,
}
/// This is a legacy endpoint that is no longer used in production.
///
/// It currently only exists to be used when developing Collab locally.
async fn legacy_update_or_create_authenticated_user(
Query(params): Query<AuthenticatedUserParams>,
Extension(app): Extension<Arc<AppState>>,
) -> Result<Json<AuthenticatedUserResponse>> {
let initial_channel_id = app.config.auto_join_channel_id;
let user = app
.db
.update_or_create_user_by_github_account(
&params.github_login,
params.github_user_id,
params.github_email.as_deref(),
params.github_name.as_deref(),
params.github_user_created_at,
initial_channel_id,
)
.await?;
let metrics_id = app.db.get_user_metrics_id(user.id).await?;
let feature_flags = app.db.get_user_flags(user.id).await?;
Ok(Json(AuthenticatedUserResponse {
user,
metrics_id,
feature_flags,
}))
}
#[derive(Debug, Deserialize)]
struct LookUpUserParams {
identifier: String,

View File

@@ -8,6 +8,7 @@ use crate::{
use anyhow::anyhow;
use call::ActiveCall;
use channel::{ChannelBuffer, ChannelStore};
use client::CloudUserStore;
use client::{
self, ChannelId, Client, Connection, Credentials, EstablishConnectionError, UserStore,
proto::PeerId,
@@ -281,12 +282,14 @@ impl TestServer {
.register_hosting_provider(Arc::new(git_hosting_providers::Github::public_instance()));
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
let cloud_user_store = cx.new(|cx| CloudUserStore::new(client.cloud_client(), cx));
let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
let language_registry = Arc::new(LanguageRegistry::test(cx.executor()));
let session = cx.new(|cx| AppSession::new(Session::test(), cx));
let app_state = Arc::new(workspace::AppState {
client: client.clone(),
user_store: user_store.clone(),
cloud_user_store,
workspace_store,
languages: language_registry,
fs: fs.clone(),

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

@@ -216,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
@@ -240,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,203 @@ 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,
);
}
}
/// 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

@@ -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 {

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

@@ -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,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::{DXGI_FORMAT_A8_UNORM, DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_SAMPLE_DESC},
};
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_A8_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,
@@ -111,13 +110,11 @@ 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,
@@ -343,27 +340,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 +436,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);
@@ -846,16 +822,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,
@@ -379,14 +375,20 @@ 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 disable_direct_composition = std::env::var(DISABLE_DIRECT_COMPOSITION)
.is_ok_and(|value| value == "true" || value == "1");
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 +403,6 @@ impl WindowsWindow {
handle,
hide_title_bar,
display,
transparent: true,
is_movable: params.is_movable,
min_size: params.window_min_size,
executor,
@@ -410,9 +411,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 +454,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 +478,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 +697,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 +783,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 +795,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 +1295,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

@@ -236,6 +236,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();

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

@@ -24,9 +24,13 @@ fs.workspace = true
gpui.workspace = true
language.workspace = true
project.workspace = true
schemars.workspace = true
serde.workspace = true
settings.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true
workspace-hack.workspace = true
workspace.workspace = true
zed_actions.workspace = true
zlog.workspace = true

View File

@@ -0,0 +1,103 @@
use fs::Fs;
use gpui::{App, IntoElement, Window};
use settings::{Settings, update_settings_file};
use theme::{ThemeMode, ThemeSettings};
use ui::{SwitchField, ToggleButtonGroup, ToggleButtonSimple, ToggleButtonWithIcon, prelude::*};
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);
});
}
fn render_theme_section(cx: &mut App) -> impl IntoElement {
let theme_mode = read_theme_selection(cx);
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_telemetry_section() -> impl IntoElement {
v_flex()
.gap_3()
.child(Label::new("Telemetry").size(LabelSize::Large))
.child(SwitchField::new(
"vim_mode",
"Help Improve Zed",
"Sending anonymous usage data helps us build the right features and create the best experience.",
ui::ToggleState::Selected,
|_, _, _| {},
))
.child(SwitchField::new(
"vim_mode",
"Help Fix Zed",
"Send crash reports so we can fix critical issues fast.",
ui::ToggleState::Selected,
|_, _, _| {},
))
}
pub(crate) fn render_basics_page(_: &mut Window, cx: &mut App) -> impl IntoElement {
v_flex()
.gap_6()
.child(render_theme_section(cx))
.child(
v_flex().gap_2().child(Label::new("Base Keymap")).child(
ToggleButtonGroup::two_rows(
"multiple_row_test",
[
ToggleButtonWithIcon::new("VS Code", IconName::AiZed, |_, _, _| {}),
ToggleButtonWithIcon::new("Jetbrains", IconName::AiZed, |_, _, _| {}),
ToggleButtonWithIcon::new("Sublime Text", IconName::AiZed, |_, _, _| {}),
],
[
ToggleButtonWithIcon::new("Atom", IconName::AiZed, |_, _, _| {}),
ToggleButtonWithIcon::new("Emacs", IconName::AiZed, |_, _, _| {}),
ToggleButtonWithIcon::new("Cursor (Beta)", IconName::AiZed, |_, _, _| {}),
],
)
.button_width(rems_from_px(230.))
.style(ui::ToggleButtonGroupStyle::Outlined)
),
)
.child(v_flex().justify_center().child(div().h_0().child("hack").invisible()).child(SwitchField::new(
"vim_mode",
"Vim Mode",
"Coming from Neovim? Zed's first-class implementation of Vim Mode has got your back.",
ui::ToggleState::Selected,
|_, _, _| {},
)))
.child(render_telemetry_section())
}

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::Sparkle)
.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::Sparkle)
.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",
"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);
},
))
.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);
},
))
}
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

@@ -4,16 +4,15 @@ 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, 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,
};
use ui::{FluentBuilder, KeyBinding, Vector, VectorName, prelude::*, rems_from_px};
use workspace::{
AppState, Workspace, WorkspaceId,
dock::DockPosition,
@@ -21,6 +20,7 @@ use workspace::{
open_new, with_active_or_new_workspace,
};
mod basics_page;
mod editing_page;
mod welcome;
@@ -30,6 +30,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!(
@@ -81,7 +99,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 +113,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;
@@ -147,23 +202,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,
@@ -188,7 +226,7 @@ impl Onboarding {
})
}
fn render_page_nav(
fn render_nav_button(
&mut self,
page: SelectedPage,
_: &mut Window,
@@ -199,54 +237,119 @@ impl Onboarding {
SelectedPage::Editing => "Editing",
SelectedPage::AiSetup => "AI Setup",
};
let binding = match page {
SelectedPage::Basics => {
KeyBinding::new(vec![gpui::Keystroke::parse("cmd-1").unwrap()], cx)
.map(|kb| kb.size(rems_from_px(12.)))
}
SelectedPage::Editing => {
KeyBinding::new(vec![gpui::Keystroke::parse("cmd-2").unwrap()], cx)
.map(|kb| kb.size(rems_from_px(12.)))
}
SelectedPage::AiSetup => {
KeyBinding::new(vec![gpui::Keystroke::parse("cmd-3").unwrap()], cx)
.map(|kb| kb.size(rems_from_px(12.)))
}
};
let selected = self.selected_page == page;
h_flex()
.id(text)
.rounded_sm()
.child(text)
.child(binding)
.h_8()
.relative()
.w_full()
.gap_2()
.px_2()
.py_0p5()
.w_full()
.justify_between()
.map(|this| {
if selected {
this.bg(Color::Selected.color(cx))
.border_l_1()
.border_color(Color::Accent.color(cx))
} else {
this.text_color(Color::Muted.color(cx))
}
.rounded_sm()
.when(selected, |this| {
this.child(
div()
.h_4()
.w_px()
.bg(cx.theme().colors().text_accent)
.absolute()
.left_0(),
)
})
.hover(|style| {
.hover(|style| style.bg(cx.theme().colors().element_hover))
.child(Label::new(text).map(|this| {
if selected {
style.bg(Color::Selected.color(cx).opacity(0.6))
this.color(Color::Default)
} else {
style.bg(Color::Selected.color(cx).opacity(0.3))
this.color(Color::Muted)
}
})
}))
.child(binding)
.on_click(cx.listener(move |this, _, _, cx| {
this.selected_page = page;
cx.notify();
}))
}
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()
.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_button(SelectedPage::Basics, window, cx)
.into_element(),
self.render_nav_button(SelectedPage::Editing, window, cx)
.into_element(),
self.render_nav_button(SelectedPage::AiSetup, window, cx)
.into_element(),
]),
)
.child(Button::new("skip_all", "Skip All")),
),
)
.child(
Button::new("sign_in", "Sign In")
.style(ButtonStyle::Outlined)
.full_width(),
)
}
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()
}
@@ -254,36 +357,6 @@ impl Onboarding {
}
}
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")
}
@@ -294,44 +367,27 @@ impl Render for Onboarding {
h_flex()
.image_cache(gpui::retain_all("onboarding-page"))
.key_context("onboarding-page")
.px_24()
.py_12()
.items_start()
.size_full()
.bg(cx.theme().colors().editor_background)
.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(),
]),
div()
.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)))
}
}
@@ -371,3 +427,54 @@ impl Item for Onboarding {
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();
}

View File

@@ -7,7 +7,7 @@ use workspace::{
NewFile, Open, Workspace, WorkspaceId,
item::{Item, ItemEvent},
};
use zed_actions::{Extensions, OpenSettings, command_palette};
use zed_actions::{Extensions, OpenSettings, agent, command_palette};
actions!(
zed,
@@ -55,8 +55,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,
@@ -228,12 +227,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 Context<Workspace>) -> 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 }
})
}
}

View File

@@ -4911,7 +4911,7 @@ impl LspStore {
language_server_id: server_id.0 as u64,
hint: Some(InlayHints::project_to_proto_hint(hint.clone())),
};
cx.spawn(async move |_, _| {
cx.background_spawn(async move {
let response = upstream_client
.request(request)
.await
@@ -5125,7 +5125,7 @@ impl LspStore {
trigger,
version: serialize_version(&buffer.read(cx).version()),
};
cx.spawn(async move |_, _| {
cx.background_spawn(async move {
client
.request(request)
.await?
@@ -5284,7 +5284,7 @@ impl LspStore {
GetDefinitions { position },
cx,
);
cx.spawn(async move |_, _| {
cx.background_spawn(async move {
Ok(definitions_task
.await
.into_iter()
@@ -5357,7 +5357,7 @@ impl LspStore {
GetDeclarations { position },
cx,
);
cx.spawn(async move |_, _| {
cx.background_spawn(async move {
Ok(declarations_task
.await
.into_iter()
@@ -5430,7 +5430,7 @@ impl LspStore {
GetTypeDefinitions { position },
cx,
);
cx.spawn(async move |_, _| {
cx.background_spawn(async move {
Ok(type_definitions_task
.await
.into_iter()
@@ -5503,7 +5503,7 @@ impl LspStore {
GetImplementations { position },
cx,
);
cx.spawn(async move |_, _| {
cx.background_spawn(async move {
Ok(implementations_task
.await
.into_iter()
@@ -5576,7 +5576,7 @@ impl LspStore {
GetReferences { position },
cx,
);
cx.spawn(async move |_, _| {
cx.background_spawn(async move {
Ok(references_task
.await
.into_iter()
@@ -5660,7 +5660,7 @@ impl LspStore {
},
cx,
);
cx.spawn(async move |_, _| {
cx.background_spawn(async move {
Ok(all_actions_task
.await
.into_iter()
@@ -6854,7 +6854,7 @@ impl LspStore {
} else {
let document_colors_task =
self.request_multiple_lsp_locally(buffer, None::<usize>, GetDocumentColor, cx);
cx.spawn(async move |_, _| {
cx.background_spawn(async move {
Ok(document_colors_task
.await
.into_iter()
@@ -6933,7 +6933,7 @@ impl LspStore {
GetSignatureHelp { position },
cx,
);
cx.spawn(async move |_, _| {
cx.background_spawn(async move {
all_actions_task
.await
.into_iter()
@@ -7010,7 +7010,7 @@ impl LspStore {
GetHover { position },
cx,
);
cx.spawn(async move |_, _| {
cx.background_spawn(async move {
all_actions_task
.await
.into_iter()
@@ -8013,7 +8013,7 @@ impl LspStore {
})
.collect::<FuturesUnordered<_>>();
cx.spawn(async move |_, _| {
cx.background_spawn(async move {
let mut responses = Vec::with_capacity(response_results.len());
while let Some((server_id, response_result)) = response_results.next().await {
if let Some(response) = response_result.log_err() {

View File

@@ -3372,7 +3372,7 @@ impl Project {
let task = self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.definitions(buffer, position, cx)
});
cx.spawn(async move |_, _| {
cx.background_spawn(async move {
let result = task.await;
drop(guard);
result
@@ -3390,7 +3390,7 @@ impl Project {
let task = self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.declarations(buffer, position, cx)
});
cx.spawn(async move |_, _| {
cx.background_spawn(async move {
let result = task.await;
drop(guard);
result
@@ -3408,7 +3408,7 @@ impl Project {
let task = self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.type_definitions(buffer, position, cx)
});
cx.spawn(async move |_, _| {
cx.background_spawn(async move {
let result = task.await;
drop(guard);
result
@@ -3426,7 +3426,7 @@ impl Project {
let task = self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.implementations(buffer, position, cx)
});
cx.spawn(async move |_, _| {
cx.background_spawn(async move {
let result = task.await;
drop(guard);
result
@@ -3444,7 +3444,7 @@ impl Project {
let task = self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.references(buffer, position, cx)
});
cx.spawn(async move |_, _| {
cx.background_spawn(async move {
let result = task.await;
drop(guard);
result
@@ -3996,7 +3996,7 @@ impl Project {
let task = self.lsp_store.update(cx, |lsp_store, cx| {
lsp_store.request_lsp(buffer_handle, server, request, cx)
});
cx.spawn(async move |_, _| {
cx.background_spawn(async move {
let result = task.await;
drop(guard);
result

View File

@@ -7,7 +7,7 @@ mod settings_json;
mod settings_store;
mod vscode_import;
use gpui::App;
use gpui::{App, Global};
use rust_embed::RustEmbed;
use std::{borrow::Cow, fmt, str};
use util::asset_str;
@@ -27,6 +27,11 @@ pub use settings_store::{
};
pub use vscode_import::{VsCodeSettings, VsCodeSettingsSource};
#[derive(Clone, Debug, PartialEq)]
pub struct ActiveSettingsProfileName(pub String);
impl Global for ActiveSettingsProfileName {}
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
pub struct WorktreeId(usize);
@@ -74,6 +79,7 @@ pub fn init(cx: &mut App) {
.unwrap();
cx.set_global(settings);
BaseKeymap::register(cx);
SettingsStore::observe_active_settings_profile_name(cx).detach();
}
pub fn default_settings() -> Cow<'static, str> {

View File

@@ -26,8 +26,8 @@ use util::{
pub type EditorconfigProperties = ec4rs::Properties;
use crate::{
ParameterizedJsonSchema, SettingsJsonSchemaParams, VsCodeSettings, WorktreeId,
parse_json_with_comments, update_value_in_json_text,
ActiveSettingsProfileName, ParameterizedJsonSchema, SettingsJsonSchemaParams, VsCodeSettings,
WorktreeId, parse_json_with_comments, update_value_in_json_text,
};
/// A value that can be defined as a user setting.
@@ -122,6 +122,8 @@ pub struct SettingsSources<'a, T> {
pub user: Option<&'a T>,
/// The user settings for the current release channel.
pub release_channel: Option<&'a T>,
/// The settings associated with an enabled settings profile
pub profile: Option<&'a T>,
/// The server's settings.
pub server: Option<&'a T>,
/// The project settings, ordered from least specific to most specific.
@@ -141,6 +143,7 @@ impl<'a, T: Serialize> SettingsSources<'a, T> {
.chain(self.extensions)
.chain(self.user)
.chain(self.release_channel)
.chain(self.profile)
.chain(self.server)
.chain(self.project.iter().copied())
}
@@ -282,6 +285,14 @@ impl SettingsStore {
}
}
pub fn observe_active_settings_profile_name(cx: &mut App) -> gpui::Subscription {
cx.observe_global::<ActiveSettingsProfileName>(|cx| {
Self::update_global(cx, |store, cx| {
store.recompute_values(None, cx).log_err();
});
})
}
pub fn update<C, R>(cx: &mut C, f: impl FnOnce(&mut Self, &mut C) -> R) -> R
where
C: BorrowAppContext,
@@ -321,6 +332,17 @@ impl SettingsStore {
.log_err();
}
let mut profile_value = None;
if let Some(active_profile) = cx.try_global::<ActiveSettingsProfileName>() {
if let Some(profiles) = self.raw_user_settings.get("profiles") {
if let Some(profile_settings) = profiles.get(&active_profile.0) {
profile_value = setting_value
.deserialize_setting(profile_settings)
.log_err();
}
}
}
let server_value = self
.raw_server_settings
.as_ref()
@@ -340,6 +362,7 @@ impl SettingsStore {
extensions: extension_value.as_ref(),
user: user_value.as_ref(),
release_channel: release_channel_value.as_ref(),
profile: profile_value.as_ref(),
server: server_value.as_ref(),
project: &[],
},
@@ -402,6 +425,16 @@ impl SettingsStore {
&self.raw_user_settings
}
/// Get the configured settings profile names.
pub fn configured_settings_profiles(&self) -> impl Iterator<Item = &str> {
self.raw_user_settings
.get("profiles")
.and_then(|v| v.as_object())
.into_iter()
.flat_map(|obj| obj.keys())
.map(|s| s.as_str())
}
/// Access the raw JSON value of the global settings.
pub fn raw_global_settings(&self) -> Option<&Value> {
self.raw_global_settings.as_ref()
@@ -532,7 +565,9 @@ impl SettingsStore {
}))
.ok();
}
}
impl SettingsStore {
/// Updates the value of a setting in a JSON file, returning the new text
/// for that JSON file.
pub fn new_text_for_update<T: Settings>(
@@ -1001,18 +1036,18 @@ impl SettingsStore {
const ZED_SETTINGS: &str = "ZedSettings";
let zed_settings_ref = add_new_subschema(&mut generator, ZED_SETTINGS, combined_schema);
// add `ZedReleaseStageSettings` which is the same as `ZedSettings` except that unknown
// fields are rejected.
let mut zed_release_stage_settings = zed_settings_ref.clone();
zed_release_stage_settings.insert("unevaluatedProperties".to_string(), false.into());
let zed_release_stage_settings_ref = add_new_subschema(
// add `ZedSettingsOverride` which is the same as `ZedSettings` except that unknown
// fields are rejected. This is used for release stage settings and profiles.
let mut zed_settings_override = zed_settings_ref.clone();
zed_settings_override.insert("unevaluatedProperties".to_string(), false.into());
let zed_settings_override_ref = add_new_subschema(
&mut generator,
"ZedReleaseStageSettings",
zed_release_stage_settings.to_value(),
"ZedSettingsOverride",
zed_settings_override.to_value(),
);
// Remove `"additionalProperties": false` added by `DefaultDenyUnknownFields` so that
// unknown fields can be handled by the root schema and `ZedReleaseStageSettings`.
// unknown fields can be handled by the root schema and `ZedSettingsOverride`.
let mut definitions = generator.take_definitions(true);
definitions
.get_mut(ZED_SETTINGS)
@@ -1032,15 +1067,20 @@ impl SettingsStore {
"$schema": meta_schema,
"title": "Zed Settings",
"unevaluatedProperties": false,
// ZedSettings + settings overrides for each release stage
// ZedSettings + settings overrides for each release stage / profiles
"allOf": [
zed_settings_ref,
{
"properties": {
"dev": zed_release_stage_settings_ref,
"nightly": zed_release_stage_settings_ref,
"stable": zed_release_stage_settings_ref,
"preview": zed_release_stage_settings_ref,
"dev": zed_settings_override_ref,
"nightly": zed_settings_override_ref,
"stable": zed_settings_override_ref,
"preview": zed_settings_override_ref,
"profiles": {
"type": "object",
"description": "Configures any number of settings profiles that are temporarily applied when selected from `settings profile selector: toggle`.",
"additionalProperties": zed_settings_override_ref
}
}
}
],
@@ -1099,6 +1139,16 @@ impl SettingsStore {
}
}
let mut profile_settings = None;
if let Some(active_profile) = cx.try_global::<ActiveSettingsProfileName>() {
if let Some(profiles) = self.raw_user_settings.get("profiles") {
if let Some(profile_json) = profiles.get(&active_profile.0) {
profile_settings =
setting_value.deserialize_setting(profile_json).log_err();
}
}
}
// If the global settings file changed, reload the global value for the field.
if changed_local_path.is_none() {
if let Some(value) = setting_value
@@ -1109,6 +1159,7 @@ impl SettingsStore {
extensions: extension_settings.as_ref(),
user: user_settings.as_ref(),
release_channel: release_channel_settings.as_ref(),
profile: profile_settings.as_ref(),
server: server_settings.as_ref(),
project: &[],
},
@@ -1161,6 +1212,7 @@ impl SettingsStore {
extensions: extension_settings.as_ref(),
user: user_settings.as_ref(),
release_channel: release_channel_settings.as_ref(),
profile: profile_settings.as_ref(),
server: server_settings.as_ref(),
project: &project_settings_stack.iter().collect::<Vec<_>>(),
},
@@ -1286,6 +1338,9 @@ impl<T: Settings> AnySettingValue for SettingValue<T> {
release_channel: values
.release_channel
.map(|value| value.0.downcast_ref::<T::FileContent>().unwrap()),
profile: values
.profile
.map(|value| value.0.downcast_ref::<T::FileContent>().unwrap()),
server: values
.server
.map(|value| value.0.downcast_ref::<T::FileContent>().unwrap()),

View File

@@ -0,0 +1,35 @@
[package]
name = "settings_profile_selector"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/settings_profile_selector.rs"
doctest = false
[dependencies]
fuzzy.workspace = true
gpui.workspace = true
picker.workspace = true
settings.workspace = true
ui.workspace = true
workspace-hack.workspace = true
workspace.workspace = true
zed_actions.workspace = true
[dev-dependencies]
client = { workspace = true, features = ["test-support"] }
editor = { workspace = true, features = ["test-support"] }
gpui = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] }
menu.workspace = true
project = { workspace = true, features = ["test-support"] }
serde_json.workspace = true
settings = { workspace = true, features = ["test-support"] }
theme = { workspace = true, features = ["test-support"] }
workspace = { workspace = true, features = ["test-support"] }

View File

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

View File

@@ -0,0 +1,582 @@
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
use gpui::{
App, Context, DismissEvent, Entity, EventEmitter, Focusable, Render, Task, WeakEntity, Window,
};
use picker::{Picker, PickerDelegate};
use settings::{ActiveSettingsProfileName, SettingsStore};
use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*};
use workspace::{ModalView, Workspace};
pub fn init(cx: &mut App) {
cx.on_action(|_: &zed_actions::settings_profile_selector::Toggle, cx| {
workspace::with_active_or_new_workspace(cx, |workspace, window, cx| {
toggle_settings_profile_selector(workspace, window, cx);
});
});
}
fn toggle_settings_profile_selector(
workspace: &mut Workspace,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
workspace.toggle_modal(window, cx, |window, cx| {
let delegate = SettingsProfileSelectorDelegate::new(cx.entity().downgrade(), window, cx);
SettingsProfileSelector::new(delegate, window, cx)
});
}
pub struct SettingsProfileSelector {
picker: Entity<Picker<SettingsProfileSelectorDelegate>>,
}
impl ModalView for SettingsProfileSelector {}
impl EventEmitter<DismissEvent> for SettingsProfileSelector {}
impl Focusable for SettingsProfileSelector {
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for SettingsProfileSelector {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
v_flex().w(rems(34.)).child(self.picker.clone())
}
}
impl SettingsProfileSelector {
pub fn new(
delegate: SettingsProfileSelectorDelegate,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
Self { picker }
}
}
pub struct SettingsProfileSelectorDelegate {
matches: Vec<StringMatch>,
profile_names: Vec<Option<String>>,
original_profile_name: Option<String>,
selected_profile_name: Option<String>,
selected_index: usize,
selection_completed: bool,
selector: WeakEntity<SettingsProfileSelector>,
}
impl SettingsProfileSelectorDelegate {
fn new(
selector: WeakEntity<SettingsProfileSelector>,
_: &mut Window,
cx: &mut Context<SettingsProfileSelector>,
) -> Self {
let settings_store = cx.global::<SettingsStore>();
let mut profile_names: Vec<Option<String>> = settings_store
.configured_settings_profiles()
.map(|s| Some(s.to_string()))
.collect();
profile_names.insert(0, None);
let matches = profile_names
.iter()
.enumerate()
.map(|(ix, profile_name)| StringMatch {
candidate_id: ix,
score: 0.0,
positions: Default::default(),
string: display_name(profile_name),
})
.collect();
let profile_name = cx
.try_global::<ActiveSettingsProfileName>()
.map(|p| p.0.clone());
let mut this = Self {
matches,
profile_names,
original_profile_name: profile_name.clone(),
selected_profile_name: None,
selected_index: 0,
selection_completed: false,
selector,
};
if let Some(profile_name) = profile_name {
this.select_if_matching(&profile_name);
}
this
}
fn select_if_matching(&mut self, profile_name: &str) {
self.selected_index = self
.matches
.iter()
.position(|mat| mat.string == profile_name)
.unwrap_or(self.selected_index);
}
fn set_selected_profile(
&self,
cx: &mut Context<Picker<SettingsProfileSelectorDelegate>>,
) -> Option<String> {
let mat = self.matches.get(self.selected_index)?;
let profile_name = self.profile_names.get(mat.candidate_id)?;
return Self::update_active_profile_name_global(profile_name.clone(), cx);
}
fn update_active_profile_name_global(
profile_name: Option<String>,
cx: &mut Context<Picker<SettingsProfileSelectorDelegate>>,
) -> Option<String> {
if let Some(profile_name) = profile_name {
cx.set_global(ActiveSettingsProfileName(profile_name.clone()));
return Some(profile_name.clone());
}
if cx.has_global::<ActiveSettingsProfileName>() {
cx.remove_global::<ActiveSettingsProfileName>();
}
None
}
}
impl PickerDelegate for SettingsProfileSelectorDelegate {
type ListItem = ListItem;
fn placeholder_text(&self, _: &mut Window, _: &mut App) -> std::sync::Arc<str> {
"Select a settings profile...".into()
}
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(
&mut self,
ix: usize,
_: &mut Window,
cx: &mut Context<Picker<SettingsProfileSelectorDelegate>>,
) {
self.selected_index = ix;
self.selected_profile_name = self.set_selected_profile(cx);
}
fn update_matches(
&mut self,
query: String,
window: &mut Window,
cx: &mut Context<Picker<SettingsProfileSelectorDelegate>>,
) -> Task<()> {
let background = cx.background_executor().clone();
let candidates = self
.profile_names
.iter()
.enumerate()
.map(|(id, profile_name)| StringMatchCandidate::new(id, &display_name(profile_name)))
.collect::<Vec<_>>();
cx.spawn_in(window, async move |this, cx| {
let matches = if query.is_empty() {
candidates
.into_iter()
.enumerate()
.map(|(index, candidate)| StringMatch {
candidate_id: index,
string: candidate.string,
positions: Vec::new(),
score: 0.0,
})
.collect()
} else {
match_strings(
&candidates,
&query,
false,
true,
100,
&Default::default(),
background,
)
.await
};
this.update_in(cx, |this, _, cx| {
this.delegate.matches = matches;
this.delegate.selected_index = this
.delegate
.selected_index
.min(this.delegate.matches.len().saturating_sub(1));
this.delegate.selected_profile_name = this.delegate.set_selected_profile(cx);
})
.ok();
})
}
fn confirm(
&mut self,
_: bool,
_: &mut Window,
cx: &mut Context<Picker<SettingsProfileSelectorDelegate>>,
) {
self.selection_completed = true;
self.selector
.update(cx, |_, cx| {
cx.emit(DismissEvent);
})
.ok();
}
fn dismissed(
&mut self,
_: &mut Window,
cx: &mut Context<Picker<SettingsProfileSelectorDelegate>>,
) {
if !self.selection_completed {
SettingsProfileSelectorDelegate::update_active_profile_name_global(
self.original_profile_name.clone(),
cx,
);
}
self.selector.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_: &mut Window,
_: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let mat = &self.matches[ix];
let profile_name = &self.profile_names[mat.candidate_id];
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(HighlightedLabel::new(
display_name(profile_name),
mat.positions.clone(),
)),
)
}
}
fn display_name(profile_name: &Option<String>) -> String {
profile_name.clone().unwrap_or("Disabled".into())
}
#[cfg(test)]
mod tests {
use super::*;
use client;
use editor;
use gpui::{TestAppContext, UpdateGlobal, VisualTestContext};
use language;
use menu::{Cancel, Confirm, SelectNext, SelectPrevious};
use project::{FakeFs, Project};
use serde_json::json;
use settings::Settings;
use theme::{self, ThemeSettings};
use workspace::{self, AppState};
use zed_actions::settings_profile_selector;
async fn init_test(
profiles_json: serde_json::Value,
cx: &mut TestAppContext,
) -> (Entity<Workspace>, &mut VisualTestContext) {
cx.update(|cx| {
let state = AppState::test(cx);
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
settings::init(cx);
theme::init(theme::LoadThemes::JustBase, cx);
ThemeSettings::register(cx);
client::init_settings(cx);
language::init(cx);
super::init(cx);
editor::init(cx);
workspace::init_settings(cx);
Project::init_settings(cx);
state
});
cx.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
let settings_json = json!({
"buffer_font_size": 10.0,
"profiles": profiles_json,
});
store
.set_user_settings(&settings_json.to_string(), cx)
.unwrap();
});
});
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, ["/test".as_ref()], cx).await;
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
cx.update(|_, cx| {
assert!(!cx.has_global::<ActiveSettingsProfileName>());
let theme_settings = ThemeSettings::get_global(cx);
assert_eq!(theme_settings.buffer_font_size(cx).0, 10.0);
});
(workspace, cx)
}
#[track_caller]
fn active_settings_profile_picker(
workspace: &Entity<Workspace>,
cx: &mut VisualTestContext,
) -> Entity<Picker<SettingsProfileSelectorDelegate>> {
workspace.update(cx, |workspace, cx| {
workspace
.active_modal::<SettingsProfileSelector>(cx)
.expect("settings profile selector is not open")
.read(cx)
.picker
.clone()
})
}
#[gpui::test]
async fn test_settings_profile_selector_state(cx: &mut TestAppContext) {
let classroom_and_streaming_profile_name = "Classroom / Streaming".to_string();
let demo_videos_profile_name = "Demo Videos".to_string();
let profiles_json = json!({
classroom_and_streaming_profile_name.clone(): {
"buffer_font_size": 20.0,
},
demo_videos_profile_name.clone(): {
"buffer_font_size": 15.0
}
});
let (workspace, cx) = init_test(profiles_json.clone(), cx).await;
cx.dispatch_action(settings_profile_selector::Toggle);
let picker = active_settings_profile_picker(&workspace, cx);
picker.read_with(cx, |picker, cx| {
assert_eq!(picker.delegate.matches.len(), 3);
assert_eq!(picker.delegate.matches[0].string, display_name(&None));
assert_eq!(
picker.delegate.matches[1].string,
classroom_and_streaming_profile_name
);
assert_eq!(picker.delegate.matches[2].string, demo_videos_profile_name);
assert_eq!(picker.delegate.matches.get(3), None);
assert_eq!(picker.delegate.selected_index, 0);
assert_eq!(picker.delegate.selected_profile_name, None);
assert_eq!(cx.try_global::<ActiveSettingsProfileName>(), None);
assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx).0, 10.0);
});
cx.dispatch_action(Confirm);
cx.update(|_, cx| {
assert_eq!(cx.try_global::<ActiveSettingsProfileName>(), None);
});
cx.dispatch_action(settings_profile_selector::Toggle);
let picker = active_settings_profile_picker(&workspace, cx);
cx.dispatch_action(SelectNext);
picker.read_with(cx, |picker, cx| {
assert_eq!(picker.delegate.selected_index, 1);
assert_eq!(
picker.delegate.selected_profile_name,
Some(classroom_and_streaming_profile_name.clone())
);
assert_eq!(
cx.try_global::<ActiveSettingsProfileName>()
.map(|p| p.0.clone()),
Some(classroom_and_streaming_profile_name.clone())
);
assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx).0, 20.0);
});
cx.dispatch_action(Cancel);
cx.update(|_, cx| {
assert_eq!(cx.try_global::<ActiveSettingsProfileName>(), None);
assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx).0, 10.0);
});
cx.dispatch_action(settings_profile_selector::Toggle);
let picker = active_settings_profile_picker(&workspace, cx);
cx.dispatch_action(SelectNext);
picker.read_with(cx, |picker, cx| {
assert_eq!(picker.delegate.selected_index, 1);
assert_eq!(
picker.delegate.selected_profile_name,
Some(classroom_and_streaming_profile_name.clone())
);
assert_eq!(
cx.try_global::<ActiveSettingsProfileName>()
.map(|p| p.0.clone()),
Some(classroom_and_streaming_profile_name.clone())
);
assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx).0, 20.0);
});
cx.dispatch_action(SelectNext);
picker.read_with(cx, |picker, cx| {
assert_eq!(picker.delegate.selected_index, 2);
assert_eq!(
picker.delegate.selected_profile_name,
Some(demo_videos_profile_name.clone())
);
assert_eq!(
cx.try_global::<ActiveSettingsProfileName>()
.map(|p| p.0.clone()),
Some(demo_videos_profile_name.clone())
);
assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx).0, 15.0);
});
cx.dispatch_action(Confirm);
cx.update(|_, cx| {
assert_eq!(
cx.try_global::<ActiveSettingsProfileName>()
.map(|p| p.0.clone()),
Some(demo_videos_profile_name.clone())
);
assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx).0, 15.0);
});
cx.dispatch_action(settings_profile_selector::Toggle);
let picker = active_settings_profile_picker(&workspace, cx);
picker.read_with(cx, |picker, cx| {
assert_eq!(picker.delegate.selected_index, 2);
assert_eq!(
picker.delegate.selected_profile_name,
Some(demo_videos_profile_name.clone())
);
assert_eq!(
cx.try_global::<ActiveSettingsProfileName>()
.map(|p| p.0.clone()),
Some(demo_videos_profile_name.clone())
);
assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx).0, 15.0);
});
cx.dispatch_action(SelectPrevious);
picker.read_with(cx, |picker, cx| {
assert_eq!(picker.delegate.selected_index, 1);
assert_eq!(
picker.delegate.selected_profile_name,
Some(classroom_and_streaming_profile_name.clone())
);
assert_eq!(
cx.try_global::<ActiveSettingsProfileName>()
.map(|p| p.0.clone()),
Some(classroom_and_streaming_profile_name.clone())
);
assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx).0, 20.0);
});
cx.dispatch_action(Cancel);
cx.update(|_, cx| {
assert_eq!(
cx.try_global::<ActiveSettingsProfileName>()
.map(|p| p.0.clone()),
Some(demo_videos_profile_name.clone())
);
assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx).0, 15.0);
});
cx.dispatch_action(settings_profile_selector::Toggle);
let picker = active_settings_profile_picker(&workspace, cx);
picker.read_with(cx, |picker, cx| {
assert_eq!(picker.delegate.selected_index, 2);
assert_eq!(
picker.delegate.selected_profile_name,
Some(demo_videos_profile_name.clone())
);
assert_eq!(
cx.try_global::<ActiveSettingsProfileName>()
.map(|p| p.0.clone()),
Some(demo_videos_profile_name)
);
assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx).0, 15.0);
});
cx.dispatch_action(SelectPrevious);
picker.read_with(cx, |picker, cx| {
assert_eq!(picker.delegate.selected_index, 1);
assert_eq!(
picker.delegate.selected_profile_name,
Some(classroom_and_streaming_profile_name.clone())
);
assert_eq!(
cx.try_global::<ActiveSettingsProfileName>()
.map(|p| p.0.clone()),
Some(classroom_and_streaming_profile_name)
);
assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx).0, 20.0);
});
cx.dispatch_action(SelectPrevious);
picker.read_with(cx, |picker, cx| {
assert_eq!(picker.delegate.selected_index, 0);
assert_eq!(picker.delegate.selected_profile_name, None);
assert_eq!(
cx.try_global::<ActiveSettingsProfileName>()
.map(|p| p.0.clone()),
None
);
assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx).0, 10.0);
});
cx.dispatch_action(Confirm);
cx.update(|_, cx| {
assert_eq!(cx.try_global::<ActiveSettingsProfileName>(), None);
assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx).0, 10.0);
});
}
}

View File

@@ -30,7 +30,6 @@ menu.workspace = true
notifications.workspace = true
paths.workspace = true
project.workspace = true
schemars.workspace = true
search.workspace = true
serde.workspace = true
serde_json.workspace = true

View File

@@ -1,20 +1,12 @@
mod appearance_settings_controls;
use std::any::TypeId;
use std::sync::Arc;
use command_palette_hooks::CommandPaletteFilter;
use editor::EditorSettingsControls;
use feature_flags::{FeatureFlag, FeatureFlagViewExt};
use fs::Fs;
use gpui::{
Action, App, AsyncWindowContext, Entity, EventEmitter, FocusHandle, Focusable, Task, actions,
};
use schemars::JsonSchema;
use serde::Deserialize;
use settings::{SettingsStore, VsCodeSettingsSource};
use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, actions};
use ui::prelude::*;
use util::truncate_and_remove_front;
use workspace::item::{Item, ItemEvent};
use workspace::{Workspace, with_active_or_new_workspace};
@@ -29,23 +21,6 @@ impl FeatureFlag for SettingsUiFeatureFlag {
const NAME: &'static str = "settings-ui";
}
/// 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,
}
actions!(
zed,
[
@@ -72,45 +47,11 @@ pub fn init(cx: &mut App) {
});
});
cx.observe_new(|workspace: &mut Workspace, window, cx| {
cx.observe_new(|_workspace: &mut Workspace, window, cx| {
let Some(window) = window else {
return;
};
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();
});
let settings_ui_actions = [TypeId::of::<OpenSettingsEditor>()];
CommandPaletteFilter::update_global(cx, |filter, _cx| {
@@ -138,57 +79,6 @@ pub fn init(cx: &mut App) {
keybindings::init(cx);
}
async fn handle_import_vscode_settings(
source: VsCodeSettingsSource,
skip_prompt: bool,
fs: Arc<dyn Fs>,
cx: &mut AsyncWindowContext,
) {
let vscode_settings =
match settings::VsCodeSettings::load_user_settings(source, fs.clone()).await {
Ok(vscode_settings) => vscode_settings,
Err(err) => {
log::error!("{err}");
let _ = cx.prompt(
gpui::PromptLevel::Info,
&format!("Could not find or load a {source} settings file"),
None,
&["Ok"],
);
return;
}
};
let prompt = if skip_prompt {
Task::ready(Some(0))
} else {
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"],
);
cx.spawn(async move |_| prompt.await.ok())
};
if prompt.await != 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);
log::info!("Imported {source} settings from {}", path.display());
})
.ok();
}
pub struct SettingsPage {
focus_handle: FocusHandle,
}

View File

@@ -867,6 +867,7 @@ impl settings::Settings for ThemeSettings {
.user
.into_iter()
.chain(sources.release_channel)
.chain(sources.profile)
.chain(sources.server)
{
if let Some(value) = value.ui_density {

View File

@@ -20,7 +20,7 @@ use crate::application_menu::{
use auto_update::AutoUpdateStatus;
use call::ActiveCall;
use client::{Client, UserStore, zed_urls};
use client::{Client, CloudUserStore, UserStore, zed_urls};
use gpui::{
Action, AnyElement, App, Context, Corner, Element, Entity, Focusable, InteractiveElement,
IntoElement, MouseButton, ParentElement, Render, StatefulInteractiveElement, Styled,
@@ -126,6 +126,7 @@ pub struct TitleBar {
platform_titlebar: Entity<PlatformTitleBar>,
project: Entity<Project>,
user_store: Entity<UserStore>,
cloud_user_store: Entity<CloudUserStore>,
client: Arc<Client>,
workspace: WeakEntity<Workspace>,
application_menu: Option<Entity<ApplicationMenu>>,
@@ -179,24 +180,25 @@ impl Render for TitleBar {
children.push(self.banner.clone().into_any_element())
}
let is_authenticated = self.cloud_user_store.read(cx).is_authenticated();
let status = self.client.status();
let status = &*status.borrow();
let show_sign_in = !is_authenticated || !matches!(status, client::Status::Connected { .. });
children.push(
h_flex()
.gap_1()
.pr_1()
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
.children(self.render_call_controls(window, cx))
.map(|el| {
let status = self.client.status();
let status = &*status.borrow();
if matches!(status, client::Status::Connected { .. }) {
el.child(self.render_user_menu_button(cx))
} else {
el.children(self.render_connection_status(status, cx))
.when(TitleBarSettings::get_global(cx).show_sign_in, |el| {
el.child(self.render_sign_in_button(cx))
})
.child(self.render_user_menu_button(cx))
}
.children(self.render_connection_status(status, cx))
.when(
show_sign_in && TitleBarSettings::get_global(cx).show_sign_in,
|el| el.child(self.render_sign_in_button(cx)),
)
.when(is_authenticated, |parent| {
parent.child(self.render_user_menu_button(cx))
})
.into_any_element(),
);
@@ -246,6 +248,7 @@ impl TitleBar {
) -> Self {
let project = workspace.project().clone();
let user_store = workspace.app_state().user_store.clone();
let cloud_user_store = workspace.app_state().cloud_user_store.clone();
let client = workspace.app_state().client.clone();
let active_call = ActiveCall::global(cx);
@@ -293,6 +296,7 @@ impl TitleBar {
workspace: workspace.weak_handle(),
project,
user_store,
cloud_user_store,
client,
_subscriptions: subscriptions,
banner,
@@ -628,15 +632,15 @@ impl TitleBar {
}
pub fn render_user_menu_button(&mut self, cx: &mut Context<Self>) -> impl Element {
let user_store = self.user_store.read(cx);
if let Some(user) = user_store.current_user() {
let cloud_user_store = self.cloud_user_store.read(cx);
if let Some(user) = cloud_user_store.authenticated_user() {
let has_subscription_period = self.user_store.read(cx).subscription_period().is_some();
let plan = self.user_store.read(cx).current_plan().filter(|_| {
// Since the user might be on the legacy free plan we filter based on whether we have a subscription period.
has_subscription_period
});
let user_avatar = user.avatar_uri.clone();
let user_avatar = user.avatar_url.clone();
let free_chip_bg = cx
.theme()
.colors()

View File

@@ -8,6 +8,7 @@ use super::PopoverMenuHandle;
pub enum DropdownStyle {
#[default]
Solid,
Outlined,
Ghost,
}
@@ -147,6 +148,23 @@ impl Component for DropdownMenu {
),
],
),
example_group_with_title(
"Styles",
vec![
single_example(
"Outlined",
DropdownMenu::new("outlined", "Outlined Dropdown", menu.clone())
.style(DropdownStyle::Outlined)
.into_any_element(),
),
single_example(
"Ghost",
DropdownMenu::new("ghost", "Ghost Dropdown", menu.clone())
.style(DropdownStyle::Ghost)
.into_any_element(),
),
],
),
example_group_with_title(
"States",
vec![single_example(
@@ -170,10 +188,13 @@ pub struct DropdownTriggerStyle {
impl DropdownTriggerStyle {
pub fn for_style(style: DropdownStyle, cx: &App) -> Self {
let colors = cx.theme().colors();
let bg = match style {
DropdownStyle::Solid => colors.editor_background,
DropdownStyle::Outlined => colors.surface_background,
DropdownStyle::Ghost => colors.ghost_element_background,
};
Self { bg }
}
}
@@ -244,17 +265,24 @@ impl RenderOnce for DropdownMenuTrigger {
let disabled = self.disabled;
let style = DropdownTriggerStyle::for_style(self.style, cx);
let is_outlined = matches!(self.style, DropdownStyle::Outlined);
h_flex()
.id("dropdown-menu-trigger")
.justify_between()
.rounded_sm()
.bg(style.bg)
.min_w_20()
.pl_2()
.pr_1p5()
.py_0p5()
.gap_2()
.min_w_20()
.justify_between()
.rounded_sm()
.bg(style.bg)
.hover(|s| s.bg(cx.theme().colors().element_hover))
.when(is_outlined, |this| {
this.border_1()
.border_color(cx.theme().colors().border)
.overflow_hidden()
})
.map(|el| {
if self.full_width {
el.w_full()

View File

@@ -2,10 +2,18 @@ use gpui::ClickEvent;
use crate::{IconButtonShape, prelude::*};
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub enum NumericStepperStyle {
Outlined,
#[default]
Ghost,
}
#[derive(IntoElement, RegisterComponent)]
pub struct NumericStepper {
id: ElementId,
value: SharedString,
style: NumericStepperStyle,
on_decrement: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
on_increment: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
/// Whether to reserve space for the reset button.
@@ -23,6 +31,7 @@ impl NumericStepper {
Self {
id: id.into(),
value: value.into(),
style: NumericStepperStyle::default(),
on_decrement: Box::new(on_decrement),
on_increment: Box::new(on_increment),
reserve_space_for_reset: false,
@@ -30,6 +39,11 @@ impl NumericStepper {
}
}
pub fn style(mut self, style: NumericStepperStyle) -> Self {
self.style = style;
self
}
pub fn reserve_space_for_reset(mut self, reserve_space_for_reset: bool) -> Self {
self.reserve_space_for_reset = reserve_space_for_reset;
self
@@ -49,6 +63,8 @@ impl RenderOnce for NumericStepper {
let shape = IconButtonShape::Square;
let icon_size = IconSize::Small;
let is_outlined = matches!(self.style, NumericStepperStyle::Outlined);
h_flex()
.id(self.id)
.gap_1()
@@ -74,22 +90,65 @@ impl RenderOnce for NumericStepper {
.child(
h_flex()
.gap_1()
.px_1()
.rounded_xs()
.bg(cx.theme().colors().editor_background)
.child(
IconButton::new("decrement", IconName::Dash)
.shape(shape)
.icon_size(icon_size)
.on_click(self.on_decrement),
)
.child(Label::new(self.value))
.child(
IconButton::new("increment", IconName::Plus)
.shape(shape)
.icon_size(icon_size)
.on_click(self.on_increment),
),
.rounded_sm()
.map(|this| {
if is_outlined {
this.overflow_hidden()
.bg(cx.theme().colors().surface_background)
.border_1()
.border_color(cx.theme().colors().border)
} else {
this.px_1().bg(cx.theme().colors().editor_background)
}
})
.map(|decrement| {
if is_outlined {
decrement.child(
h_flex()
.id("decrement_button")
.p_1p5()
.size_full()
.justify_center()
.hover(|s| s.bg(cx.theme().colors().element_hover))
.border_r_1()
.border_color(cx.theme().colors().border)
.child(Icon::new(IconName::Dash).size(IconSize::Small))
.on_click(self.on_decrement),
)
} else {
decrement.child(
IconButton::new("decrement", IconName::Dash)
.shape(shape)
.icon_size(icon_size)
.on_click(self.on_decrement),
)
}
})
.when(is_outlined, |this| this)
.child(Label::new(self.value).mx_3())
.map(|increment| {
if is_outlined {
increment.child(
h_flex()
.id("increment_button")
.p_1p5()
.size_full()
.justify_center()
.hover(|s| s.bg(cx.theme().colors().element_hover))
.border_l_1()
.border_color(cx.theme().colors().border)
.child(Icon::new(IconName::Plus).size(IconSize::Small))
.on_click(self.on_increment),
)
} else {
increment.child(
IconButton::new("increment", IconName::Dash)
.shape(shape)
.icon_size(icon_size)
.on_click(self.on_increment),
)
}
}),
)
}
}
@@ -100,7 +159,7 @@ impl Component for NumericStepper {
}
fn name() -> &'static str {
"NumericStepper"
"Numeric Stepper"
}
fn sort_name() -> &'static str {
@@ -108,18 +167,39 @@ impl Component for NumericStepper {
}
fn description() -> Option<&'static str> {
Some("A button used to increment or decrement a numeric value. ")
Some("A button used to increment or decrement a numeric value.")
}
fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
Some(
div()
.child(NumericStepper::new(
"numeric-stepper-component-preview",
"10",
move |_, _, _| {},
move |_, _, _| {},
))
v_flex()
.gap_6()
.children(vec![example_group_with_title(
"Styles",
vec![
single_example(
"Default",
NumericStepper::new(
"numeric-stepper-component-preview",
"10",
move |_, _, _| {},
move |_, _, _| {},
)
.into_any_element(),
),
single_example(
"Outlined",
NumericStepper::new(
"numeric-stepper-with-border-component-preview",
"10",
move |_, _, _| {},
move |_, _, _| {},
)
.style(NumericStepperStyle::Outlined)
.into_any_element(),
),
],
)])
.into_any_element(),
)
}

View File

@@ -1,6 +1,6 @@
use gpui::{
AnyElement, AnyView, ClickEvent, ElementId, Hsla, IntoElement, Styled, Window, div, hsla,
prelude::*,
AnyElement, AnyView, ClickEvent, CursorStyle, ElementId, Hsla, IntoElement, Styled, Window,
div, hsla, prelude::*,
};
use std::sync::Arc;
@@ -609,6 +609,9 @@ impl RenderOnce for SwitchField {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
h_flex()
.id(SharedString::from(format!("{}-container", self.id)))
.when(!self.disabled, |this| {
this.hover(|this| this.cursor(CursorStyle::PointingHand))
})
.w_full()
.gap_4()
.justify_between()

View File

@@ -43,7 +43,7 @@ fn zed_prompt_renderer(
let renderer = cx.new({
|cx| ZedPromptRenderer {
_level: level,
message: message.to_string(),
message: cx.new(|cx| Markdown::new(SharedString::new(message), None, None, cx)),
actions: actions.iter().map(|a| a.label().to_string()).collect(),
focus: cx.focus_handle(),
active_action_id: 0,
@@ -58,7 +58,7 @@ fn zed_prompt_renderer(
pub struct ZedPromptRenderer {
_level: PromptLevel,
message: String,
message: Entity<Markdown>,
actions: Vec<String>,
focus: FocusHandle,
active_action_id: usize,
@@ -114,7 +114,7 @@ impl ZedPromptRenderer {
impl Render for ZedPromptRenderer {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let settings = ThemeSettings::get_global(cx);
let font_family = settings.ui_font.family.clone();
let font_size = settings.ui_font_size(cx).into();
let prompt = v_flex()
.key_context("Prompt")
.cursor_default()
@@ -130,24 +130,38 @@ impl Render for ZedPromptRenderer {
.overflow_hidden()
.p_4()
.gap_4()
.font_family(font_family)
.font_family(settings.ui_font.family.clone())
.child(
div()
.w_full()
.font_weight(FontWeight::BOLD)
.child(self.message.clone())
.text_color(ui::Color::Default.color(cx)),
.child(MarkdownElement::new(self.message.clone(), {
let mut base_text_style = window.text_style();
base_text_style.refine(&TextStyleRefinement {
font_family: Some(settings.ui_font.family.clone()),
font_size: Some(font_size),
font_weight: Some(FontWeight::BOLD),
color: Some(ui::Color::Default.color(cx)),
..Default::default()
});
MarkdownStyle {
base_text_style,
selection_background_color: cx
.theme()
.colors()
.element_selection_background,
..Default::default()
}
})),
)
.children(self.detail.clone().map(|detail| {
div()
.w_full()
.text_xs()
.child(MarkdownElement::new(detail, {
let settings = ThemeSettings::get_global(cx);
let mut base_text_style = window.text_style();
base_text_style.refine(&TextStyleRefinement {
font_family: Some(settings.ui_font.family.clone()),
font_size: Some(settings.ui_font_size(cx).into()),
font_size: Some(font_size),
color: Some(ui::Color::Muted.color(cx)),
..Default::default()
});
@@ -176,24 +190,28 @@ impl Render for ZedPromptRenderer {
}),
));
div().size_full().occlude().child(
div()
.size_full()
.absolute()
.top_0()
.left_0()
.flex()
.flex_col()
.justify_around()
.child(
div()
.w_full()
.flex()
.flex_row()
.justify_around()
.child(prompt),
),
)
div()
.size_full()
.occlude()
.bg(gpui::black().opacity(0.2))
.child(
div()
.size_full()
.absolute()
.top_0()
.left_0()
.flex()
.flex_col()
.justify_around()
.child(
div()
.w_full()
.flex()
.flex_row()
.justify_around()
.child(prompt),
),
)
}
}

View File

@@ -15,6 +15,7 @@ mod toast_layer;
mod toolbar;
mod workspace_settings;
use client::CloudUserStore;
pub use toast_layer::{ToastAction, ToastLayer, ToastView};
use anyhow::{Context as _, Result, anyhow};
@@ -839,6 +840,7 @@ pub struct AppState {
pub languages: Arc<LanguageRegistry>,
pub client: Arc<Client>,
pub user_store: Entity<UserStore>,
pub cloud_user_store: Entity<CloudUserStore>,
pub workspace_store: Entity<WorkspaceStore>,
pub fs: Arc<dyn fs::Fs>,
pub build_window_options: fn(Option<Uuid>, &mut App) -> WindowOptions,
@@ -911,6 +913,7 @@ impl AppState {
let client = Client::new(clock, http_client.clone(), cx);
let session = cx.new(|cx| AppSession::new(Session::test(), cx));
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
let cloud_user_store = cx.new(|cx| CloudUserStore::new(client.cloud_client(), cx));
let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
theme::init(theme::LoadThemes::JustBase, cx);
@@ -922,6 +925,7 @@ impl AppState {
fs,
languages,
user_store,
cloud_user_store,
workspace_store,
node_runtime: NodeRuntime::unavailable(),
build_window_options: |_, _| Default::default(),
@@ -5689,6 +5693,7 @@ impl Workspace {
let client = project.read(cx).client();
let user_store = project.read(cx).user_store();
let cloud_user_store = cx.new(|cx| CloudUserStore::new(client.cloud_client(), cx));
let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
let session = cx.new(|cx| AppSession::new(Session::test(), cx));
@@ -5696,6 +5701,7 @@ impl Workspace {
let app_state = Arc::new(AppState {
languages: project.read(cx).languages().clone(),
workspace_store,
cloud_user_store,
client,
user_store,
fs: project.read(cx).fs().clone(),

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition.workspace = true
name = "zed"
version = "0.198.0"
version = "0.199.0"
publish.workspace = true
license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.dev>"]
@@ -106,6 +106,7 @@ outline_panel.workspace = true
parking_lot.workspace = true
paths.workspace = true
picker.workspace = true
settings_profile_selector.workspace = true
profiling.workspace = true
project.workspace = true
project_panel.workspace = true

View File

@@ -62,6 +62,7 @@ Source: "{#ResourcesDir}\Zed.exe"; DestDir: "{code:GetInstallDir}"; Flags: ignor
Source: "{#ResourcesDir}\bin\*"; DestDir: "{code:GetInstallDir}\bin"; Flags: ignoreversion
Source: "{#ResourcesDir}\tools\*"; DestDir: "{app}\tools"; Flags: ignoreversion
Source: "{#ResourcesDir}\appx\*"; DestDir: "{app}\appx"; BeforeInstall: RemoveAppxPackage; AfterInstall: AddAppxPackage; Flags: ignoreversion; Check: IsWindows11OrLater
Source: "{#ResourcesDir}\amd_ags_x64.dll"; DestDir: "{app}"; Flags: ignoreversion
[Icons]
Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}.exe"; AppUserModelID: "{#AppUserId}"

View File

@@ -5,7 +5,7 @@ use agent_ui::AgentPanel;
use anyhow::{Context as _, Result};
use clap::{Parser, command};
use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
use client::{Client, ProxySettings, UserStore, parse_zed_link};
use client::{Client, CloudUserStore, ProxySettings, UserStore, parse_zed_link};
use collab_ui::channel_view::ChannelView;
use collections::HashMap;
use db::kvp::{GLOBAL_KEY_VALUE_STORE, KEY_VALUE_STORE};
@@ -457,6 +457,7 @@ pub fn main() {
language::init(cx);
languages::init(languages.clone(), node_runtime.clone(), cx);
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
let cloud_user_store = cx.new(|cx| CloudUserStore::new(client.cloud_client(), cx));
let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
language_extension::init(
@@ -516,6 +517,7 @@ pub fn main() {
languages: languages.clone(),
client: client.clone(),
user_store: user_store.clone(),
cloud_user_store,
fs: fs.clone(),
build_window_options,
workspace_store,
@@ -613,6 +615,7 @@ pub fn main() {
language_selector::init(cx);
toolchain_selector::init(cx);
theme_selector::init(cx);
settings_profile_selector::init(cx);
language_tools::init(cx);
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx);

View File

@@ -4366,6 +4366,7 @@ mod tests {
"repl",
"rules_library",
"search",
"settings_profile_selector",
"snippets",
"supermaven",
"svg",

View File

@@ -260,6 +260,16 @@ pub mod icon_theme_selector {
}
}
pub mod settings_profile_selector {
use gpui::Action;
use schemars::JsonSchema;
use serde::Deserialize;
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
#[action(namespace = settings_profile_selector)]
pub struct Toggle;
}
pub mod agent {
use gpui::actions;

View File

@@ -2588,6 +2588,7 @@ List of `integer` column numbers
"font_features": null,
"font_size": null,
"line_height": "comfortable",
"minimum_contrast": 45,
"option_as_meta": false,
"button": true,
"shell": "system",
@@ -2883,6 +2884,30 @@ See Buffer Font Features
}
```
### Terminal: Minimum Contrast
- Description: Controls the minimum contrast between foreground and background colors in the terminal. Uses the APCA (Accessible Perceptual Contrast Algorithm) for color adjustments. Set this to 0 to disable this feature.
- Setting: `minimum_contrast`
- Default: `45`
**Options**
`integer` values from 0 to 106. Common recommended values:
- `0`: No contrast adjustment
- `45`: Minimum for large fluent text (default)
- `60`: Minimum for other content text
- `75`: Minimum for body text
- `90`: Preferred for body text
```json
{
"terminal": {
"minimum_contrast": 45
}
}
```
### Terminal: Option As Meta
- Description: Re-interprets the option keys to act like a 'meta' key, like in Emacs.

View File

@@ -136,11 +136,22 @@ function SignZedAndItsFriends {
& "$innoDir\sign.ps1" $files
}
function DownloadAMDGpuServices {
# If you update the AGS SDK version, please also update the version in `crates/gpui/src/platform/windows/directx_renderer.rs`
$url = "https://codeload.github.com/GPUOpen-LibrariesAndSDKs/AGS_SDK/zip/refs/tags/v6.3.0"
$zipPath = ".\AGS_SDK_v6.3.0.zip"
# Download the AGS SDK zip file
Invoke-WebRequest -Uri $url -OutFile $zipPath
# Extract the AGS SDK zip file
Expand-Archive -Path $zipPath -DestinationPath "." -Force
}
function CollectFiles {
Move-Item -Path "$innoDir\zed_explorer_command_injector.appx" -Destination "$innoDir\appx\zed_explorer_command_injector.appx" -Force
Move-Item -Path "$innoDir\zed_explorer_command_injector.dll" -Destination "$innoDir\appx\zed_explorer_command_injector.dll" -Force
Move-Item -Path "$innoDir\cli.exe" -Destination "$innoDir\bin\zed.exe" -Force
Move-Item -Path "$innoDir\auto_update_helper.exe" -Destination "$innoDir\tools\auto_update_helper.exe" -Force
Move-Item -Path ".\AGS_SDK-6.3.0\ags_lib\lib\amd_ags_x64.dll" -Destination "$innoDir\amd_ags_x64.dll" -Force
}
function BuildInstaller {
@@ -211,7 +222,6 @@ function BuildInstaller {
# Windows runner 2022 default has iscc in PATH, https://github.com/actions/runner-images/blob/main/images/windows/Windows2022-Readme.md
# Currently, we are using Windows 2022 runner.
# Windows runner 2025 doesn't have iscc in PATH for now, https://github.com/actions/runner-images/issues/11228
# $innoSetupPath = "iscc.exe"
$innoSetupPath = "C:\Program Files (x86)\Inno Setup 6\ISCC.exe"
$definitions = @{
@@ -268,6 +278,7 @@ BuildZedAndItsFriends
MakeAppx
SignZedAndItsFriends
ZipZedAndItsFriendsDebug
DownloadAMDGpuServices
CollectFiles
BuildInstaller

View File

@@ -558,7 +558,6 @@ getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-f
getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] }
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
naga = { version = "25", features = ["spv-out", "wgsl-in"] }
ring = { version = "0.17", features = ["std"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event"] }
scopeguard = { version = "1" }
@@ -572,7 +571,7 @@ windows-core = { version = "0.61" }
windows-numerics = { version = "0.2" }
windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_Globalization", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
windows-sys-b21d60becc0929df = { package = "windows-sys", version = "0.52", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Wdk_System_IO", "Win32_Foundation", "Win32_Networking_WinSock", "Win32_Security_Authorization", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_IO", "Win32_System_Memory", "Win32_System_Pipes", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_WindowsProgramming"] }
windows-sys-c8eced492e86ede7 = { package = "windows-sys", version = "0.48", features = ["Win32_Foundation", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_Threading", "Win32_System_Time", "Win32_UI_Shell"] }
windows-sys-c8eced492e86ede7 = { package = "windows-sys", version = "0.48", features = ["Win32_Foundation", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Shell"] }
[target.x86_64-pc-windows-msvc.build-dependencies]
codespan-reporting = { version = "0.12" }
@@ -582,7 +581,6 @@ getrandom-468e82937335b1c9 = { package = "getrandom", version = "0.3", default-f
getrandom-6f8ce4dd05d13bba = { package = "getrandom", version = "0.2", default-features = false, features = ["js", "rdrand"] }
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
itertools-5ef9efb8ec2df382 = { package = "itertools", version = "0.12" }
naga = { version = "25", features = ["spv-out", "wgsl-in"] }
proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] }
ring = { version = "0.17", features = ["std"] }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", features = ["event"] }
@@ -597,7 +595,7 @@ windows-core = { version = "0.61" }
windows-numerics = { version = "0.2" }
windows-sys-73dcd821b1037cfd = { package = "windows-sys", version = "0.59", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_Globalization", "Win32_NetworkManagement_IpHelper", "Win32_Networking_WinSock", "Win32_Security_Authentication_Identity", "Win32_Security_Credentials", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Console", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Ioctl", "Win32_System_Kernel", "Win32_System_LibraryLoader", "Win32_System_Memory", "Win32_System_Performance", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_SystemInformation", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }
windows-sys-b21d60becc0929df = { package = "windows-sys", version = "0.52", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Wdk_System_IO", "Win32_Foundation", "Win32_Networking_WinSock", "Win32_Security_Authorization", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_IO", "Win32_System_Memory", "Win32_System_Pipes", "Win32_System_SystemServices", "Win32_System_Threading", "Win32_System_WindowsProgramming"] }
windows-sys-c8eced492e86ede7 = { package = "windows-sys", version = "0.48", features = ["Win32_Foundation", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_Threading", "Win32_System_Time", "Win32_UI_Shell"] }
windows-sys-c8eced492e86ede7 = { package = "windows-sys", version = "0.48", features = ["Win32_Foundation", "Win32_Globalization", "Win32_Networking_WinSock", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Diagnostics_Debug", "Win32_System_IO", "Win32_System_Pipes", "Win32_System_Registry", "Win32_System_Threading", "Win32_System_Time", "Win32_System_WindowsProgramming", "Win32_UI_Shell"] }
[target.x86_64-unknown-linux-musl.dependencies]
aes = { version = "0.8", default-features = false, features = ["zeroize"] }

View File

@@ -71,6 +71,10 @@ extend-ignore-re = [
# Not an actual typo but an intentionally invalid color, in `color_extractor`
"#fof",
# Stripped version of reserved keyword `type`
"typ"
"typ",
# AMD GPU Services
"ags",
# AMD GPU Services
"AGS"
]
check-filename = true