Compare commits
25 Commits
v0.198.2
...
more-retri
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2ce038352 | ||
|
|
47af878ebb | ||
|
|
5488398986 | ||
|
|
b1a7993544 | ||
|
|
b90fd4287f | ||
|
|
e1e2775b80 | ||
|
|
ed104ec5e0 | ||
|
|
67a491df50 | ||
|
|
f003036aec | ||
|
|
fbc784d323 | ||
|
|
296bb66b65 | ||
|
|
bb1a7ccbba | ||
|
|
289f420504 | ||
|
|
15ad986329 | ||
|
|
0d9715325c | ||
|
|
5ef5f3c5ca | ||
|
|
2d4afd2119 | ||
|
|
afcb8f2a3f | ||
|
|
cdce3b3620 | ||
|
|
bc6bb42745 | ||
|
|
7695c4b82e | ||
|
|
794ade8b6d | ||
|
|
f4bd524d7f | ||
|
|
9d82e148de | ||
|
|
f8d1062484 |
1
.github/actionlint.yml
vendored
1
.github/actionlint.yml
vendored
@@ -24,6 +24,7 @@ self-hosted-runner:
|
||||
- buildjet-8vcpu-ubuntu-2204-arm
|
||||
- buildjet-16vcpu-ubuntu-2204-arm
|
||||
- buildjet-32vcpu-ubuntu-2204-arm
|
||||
- buildjet-64vcpu-ubuntu-2204-arm
|
||||
# Self Hosted Runners
|
||||
- self-mini-macos
|
||||
- self-32vcpu-windows-2022
|
||||
|
||||
8
.github/actions/build_docs/action.yml
vendored
8
.github/actions/build_docs/action.yml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@@ -649,7 +649,7 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
name: Linux arm64 release bundle
|
||||
runs-on:
|
||||
- buildjet-32vcpu-ubuntu-2204-arm
|
||||
- buildjet-16vcpu-ubuntu-2204-arm
|
||||
if: |
|
||||
startsWith(github.ref, 'refs/tags/v')
|
||||
|| contains(github.event.pull_request.labels.*.name, 'run-bundling')
|
||||
|
||||
2
.github/workflows/release_nightly.yml
vendored
2
.github/workflows/release_nightly.yml
vendored
@@ -167,7 +167,7 @@ jobs:
|
||||
name: Create a Linux *.tar.gz bundle for ARM
|
||||
if: github.repository_owner == 'zed-industries'
|
||||
runs-on:
|
||||
- buildjet-32vcpu-ubuntu-2204-arm
|
||||
- buildjet-16vcpu-ubuntu-2204-arm
|
||||
needs: tests
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
|
||||
225
Cargo.lock
generated
225
Cargo.lock
generated
@@ -2976,6 +2976,7 @@ dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"clock",
|
||||
"cloud_api_client",
|
||||
"cloud_llm_client",
|
||||
"cocoa 0.26.0",
|
||||
"collections",
|
||||
@@ -3031,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"
|
||||
@@ -4269,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"
|
||||
@@ -4519,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"
|
||||
@@ -4960,7 +4920,6 @@ dependencies = [
|
||||
"theme",
|
||||
"time",
|
||||
"tree-sitter-bash",
|
||||
"tree-sitter-c",
|
||||
"tree-sitter-html",
|
||||
"tree-sitter-python",
|
||||
"tree-sitter-rust",
|
||||
@@ -5929,7 +5888,7 @@ dependencies = [
|
||||
"ignore",
|
||||
"libc",
|
||||
"log",
|
||||
"notify",
|
||||
"notify 8.0.0",
|
||||
"objc",
|
||||
"parking_lot",
|
||||
"paths",
|
||||
@@ -6363,6 +6322,7 @@ dependencies = [
|
||||
"buffer_diff",
|
||||
"call",
|
||||
"chrono",
|
||||
"client",
|
||||
"cloud_llm_client",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
@@ -7485,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]]
|
||||
@@ -8167,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"
|
||||
@@ -8391,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"
|
||||
@@ -8544,7 +8507,7 @@ dependencies = [
|
||||
"fnv",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"mio",
|
||||
"mio 1.0.3",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"tempfile",
|
||||
@@ -9984,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",
|
||||
@@ -9996,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",
|
||||
@@ -10010,7 +9972,6 @@ dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"shlex",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
@@ -10153,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"
|
||||
@@ -10499,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"
|
||||
@@ -10507,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",
|
||||
@@ -10519,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]]
|
||||
@@ -10666,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"
|
||||
@@ -10960,12 +10936,16 @@ dependencies = [
|
||||
"gpui",
|
||||
"language",
|
||||
"project",
|
||||
"schemars",
|
||||
"serde",
|
||||
"settings",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
"zlog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -14729,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"
|
||||
@@ -14751,7 +14752,6 @@ dependencies = [
|
||||
"notifications",
|
||||
"paths",
|
||||
"project",
|
||||
"schemars",
|
||||
"search",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -16572,7 +16572,7 @@ dependencies = [
|
||||
"backtrace",
|
||||
"bytes 1.10.1",
|
||||
"libc",
|
||||
"mio",
|
||||
"mio 1.0.3",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
@@ -19749,7 +19749,7 @@ dependencies = [
|
||||
"md-5",
|
||||
"memchr",
|
||||
"miniz_oxide",
|
||||
"mio",
|
||||
"mio 1.0.3",
|
||||
"naga",
|
||||
"nix 0.29.0",
|
||||
"nom",
|
||||
@@ -20193,7 +20193,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.198.2"
|
||||
version = "0.199.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"agent",
|
||||
@@ -20296,6 +20296,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"session",
|
||||
"settings",
|
||||
"settings_profile_selector",
|
||||
"settings_ui",
|
||||
"shellexpand 2.1.2",
|
||||
"smol",
|
||||
|
||||
23
Cargo.toml
23
Cargo.toml
@@ -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",
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
"ctrl-shift-r": ["pane::DeploySearch", { "replace_enabled": true }],
|
||||
"alt-shift-f10": "task::Spawn",
|
||||
"ctrl-e": "file_finder::Toggle",
|
||||
// "ctrl-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
|
||||
"ctrl-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
|
||||
"ctrl-shift-n": "file_finder::Toggle",
|
||||
"ctrl-shift-a": "command_palette::Toggle",
|
||||
"shift shift": "command_palette::Toggle",
|
||||
@@ -166,7 +166,7 @@
|
||||
{ "context": "Diagnostics > Editor", "bindings": { "alt-6": "pane::CloseActiveItem" } },
|
||||
{ "context": "OutlinePanel", "bindings": { "alt-7": "workspace::CloseActiveDock" } },
|
||||
{
|
||||
"context": "Dock || Workspace || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)",
|
||||
"context": "Dock || Workspace || Terminal || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)",
|
||||
"bindings": {
|
||||
"escape": "editor::ToggleFocus",
|
||||
"shift-escape": "workspace::CloseActiveDock"
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
"cmd-shift-r": ["pane::DeploySearch", { "replace_enabled": true }],
|
||||
"ctrl-alt-r": "task::Spawn",
|
||||
"cmd-e": "file_finder::Toggle",
|
||||
// "cmd-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
|
||||
"cmd-k": "git_panel::ToggleFocus", // bug: This should also focus commit editor
|
||||
"cmd-shift-o": "file_finder::Toggle",
|
||||
"cmd-shift-a": "command_palette::Toggle",
|
||||
"shift shift": "command_palette::Toggle",
|
||||
@@ -167,7 +167,7 @@
|
||||
{ "context": "Diagnostics > Editor", "bindings": { "cmd-6": "pane::CloseActiveItem" } },
|
||||
{ "context": "OutlinePanel", "bindings": { "cmd-7": "workspace::CloseActiveDock" } },
|
||||
{
|
||||
"context": "Dock || Workspace || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)",
|
||||
"context": "Dock || Workspace || Terminal || OutlinePanel || ProjectPanel || CollabPanel || (Editor && mode == auto_height)",
|
||||
"bindings": {
|
||||
"escape": "editor::ToggleFocus",
|
||||
"shift-escape": "workspace::CloseActiveDock"
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ use anyhow::{Result, anyhow};
|
||||
use assistant_context::{AssistantContext, ContextEvent, ContextSummary};
|
||||
use assistant_slash_command::SlashCommandWorkingSet;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use client::{UserStore, zed_urls};
|
||||
use client::{DisableAiSettings, UserStore, zed_urls};
|
||||
use cloud_llm_client::{CompletionIntent, UsageLimit};
|
||||
use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
|
||||
use feature_flags::{self, FeatureFlagAppExt};
|
||||
@@ -58,7 +58,7 @@ use language::LanguageRegistry;
|
||||
use language_model::{
|
||||
ConfigurationError, ConfiguredModel, LanguageModelProviderTosView, LanguageModelRegistry,
|
||||
};
|
||||
use project::{DisableAiSettings, Project, ProjectPath, Worktree};
|
||||
use project::{Project, ProjectPath, Worktree};
|
||||
use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
|
||||
use proto::Plan;
|
||||
use rules_library::{RulesLibrary, open_rules_library};
|
||||
|
||||
@@ -31,7 +31,7 @@ use std::sync::Arc;
|
||||
use agent::{Thread, ThreadId};
|
||||
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection};
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use client::Client;
|
||||
use client::{Client, DisableAiSettings};
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
use fs::Fs;
|
||||
@@ -40,7 +40,6 @@ use language::LanguageRegistry;
|
||||
use language_model::{
|
||||
ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
|
||||
};
|
||||
use project::DisableAiSettings;
|
||||
use prompt_store::PromptBuilder;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -16,7 +16,7 @@ use agent::{
|
||||
};
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::{Context as _, Result};
|
||||
use client::telemetry::Telemetry;
|
||||
use client::{DisableAiSettings, telemetry::Telemetry};
|
||||
use collections::{HashMap, HashSet, VecDeque, hash_map};
|
||||
use editor::SelectionEffects;
|
||||
use editor::{
|
||||
@@ -39,7 +39,7 @@ use language_model::{
|
||||
};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use parking_lot::Mutex;
|
||||
use project::{CodeAction, DisableAiSettings, LspAction, Project, ProjectTransaction};
|
||||
use project::{CodeAction, LspAction, Project, ProjectTransaction};
|
||||
use prompt_store::{PromptBuilder, PromptStore};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
|
||||
|
||||
@@ -36,18 +36,11 @@ pub enum AnthropicModelMode {
|
||||
pub enum Model {
|
||||
#[serde(rename = "claude-opus-4", alias = "claude-opus-4-latest")]
|
||||
ClaudeOpus4,
|
||||
#[serde(rename = "claude-opus-4-1", alias = "claude-opus-4-1-latest")]
|
||||
ClaudeOpus4_1,
|
||||
#[serde(
|
||||
rename = "claude-opus-4-thinking",
|
||||
alias = "claude-opus-4-thinking-latest"
|
||||
)]
|
||||
ClaudeOpus4Thinking,
|
||||
#[serde(
|
||||
rename = "claude-opus-4-1-thinking",
|
||||
alias = "claude-opus-4-1-thinking-latest"
|
||||
)]
|
||||
ClaudeOpus4_1Thinking,
|
||||
#[default]
|
||||
#[serde(rename = "claude-sonnet-4", alias = "claude-sonnet-4-latest")]
|
||||
ClaudeSonnet4,
|
||||
@@ -98,18 +91,10 @@ impl Model {
|
||||
}
|
||||
|
||||
pub fn from_id(id: &str) -> Result<Self> {
|
||||
if id.starts_with("claude-opus-4-1-thinking") {
|
||||
return Ok(Self::ClaudeOpus4_1Thinking);
|
||||
}
|
||||
|
||||
if id.starts_with("claude-opus-4-thinking") {
|
||||
return Ok(Self::ClaudeOpus4Thinking);
|
||||
}
|
||||
|
||||
if id.starts_with("claude-opus-4-1") {
|
||||
return Ok(Self::ClaudeOpus4_1);
|
||||
}
|
||||
|
||||
if id.starts_with("claude-opus-4") {
|
||||
return Ok(Self::ClaudeOpus4);
|
||||
}
|
||||
@@ -156,9 +141,7 @@ impl Model {
|
||||
pub fn id(&self) -> &str {
|
||||
match self {
|
||||
Self::ClaudeOpus4 => "claude-opus-4-latest",
|
||||
Self::ClaudeOpus4_1 => "claude-opus-4-1-latest",
|
||||
Self::ClaudeOpus4Thinking => "claude-opus-4-thinking-latest",
|
||||
Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-thinking-latest",
|
||||
Self::ClaudeSonnet4 => "claude-sonnet-4-latest",
|
||||
Self::ClaudeSonnet4Thinking => "claude-sonnet-4-thinking-latest",
|
||||
Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
|
||||
@@ -176,7 +159,6 @@ impl Model {
|
||||
pub fn request_id(&self) -> &str {
|
||||
match self {
|
||||
Self::ClaudeOpus4 | Self::ClaudeOpus4Thinking => "claude-opus-4-20250514",
|
||||
Self::ClaudeOpus4_1 | Self::ClaudeOpus4_1Thinking => "claude-opus-4-1-20250805",
|
||||
Self::ClaudeSonnet4 | Self::ClaudeSonnet4Thinking => "claude-sonnet-4-20250514",
|
||||
Self::Claude3_5Sonnet => "claude-3-5-sonnet-latest",
|
||||
Self::Claude3_7Sonnet | Self::Claude3_7SonnetThinking => "claude-3-7-sonnet-latest",
|
||||
@@ -191,9 +173,7 @@ impl Model {
|
||||
pub fn display_name(&self) -> &str {
|
||||
match self {
|
||||
Self::ClaudeOpus4 => "Claude Opus 4",
|
||||
Self::ClaudeOpus4_1 => "Claude Opus 4.1",
|
||||
Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
|
||||
Self::ClaudeOpus4_1Thinking => "Claude Opus 4.1 Thinking",
|
||||
Self::ClaudeSonnet4 => "Claude Sonnet 4",
|
||||
Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
|
||||
Self::Claude3_7Sonnet => "Claude 3.7 Sonnet",
|
||||
@@ -212,9 +192,7 @@ impl Model {
|
||||
pub fn cache_configuration(&self) -> Option<AnthropicModelCacheConfiguration> {
|
||||
match self {
|
||||
Self::ClaudeOpus4
|
||||
| Self::ClaudeOpus4_1
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeOpus4_1Thinking
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4Thinking
|
||||
| Self::Claude3_5Sonnet
|
||||
@@ -237,9 +215,7 @@ impl Model {
|
||||
pub fn max_token_count(&self) -> u64 {
|
||||
match self {
|
||||
Self::ClaudeOpus4
|
||||
| Self::ClaudeOpus4_1
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeOpus4_1Thinking
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4Thinking
|
||||
| Self::Claude3_5Sonnet
|
||||
@@ -256,9 +232,7 @@ impl Model {
|
||||
pub fn max_output_tokens(&self) -> u64 {
|
||||
match self {
|
||||
Self::ClaudeOpus4
|
||||
| Self::ClaudeOpus4_1
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeOpus4_1Thinking
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4Thinking
|
||||
| Self::Claude3_5Sonnet
|
||||
@@ -275,9 +249,7 @@ impl Model {
|
||||
pub fn default_temperature(&self) -> f32 {
|
||||
match self {
|
||||
Self::ClaudeOpus4
|
||||
| Self::ClaudeOpus4_1
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeOpus4_1Thinking
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4Thinking
|
||||
| Self::Claude3_5Sonnet
|
||||
@@ -297,7 +269,6 @@ impl Model {
|
||||
pub fn mode(&self) -> AnthropicModelMode {
|
||||
match self {
|
||||
Self::ClaudeOpus4
|
||||
| Self::ClaudeOpus4_1
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::Claude3_5Sonnet
|
||||
| Self::Claude3_7Sonnet
|
||||
@@ -306,7 +277,6 @@ impl Model {
|
||||
| Self::Claude3Sonnet
|
||||
| Self::Claude3Haiku => AnthropicModelMode::Default,
|
||||
Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeOpus4_1Thinking
|
||||
| Self::ClaudeSonnet4Thinking
|
||||
| Self::Claude3_7SonnetThinking => AnthropicModelMode::Thinking {
|
||||
budget_tokens: Some(4_096),
|
||||
|
||||
@@ -630,11 +630,6 @@ impl ActionLog {
|
||||
false
|
||||
}
|
||||
});
|
||||
if tracked_buffer.unreviewed_edits.is_empty() {
|
||||
if let TrackedBufferStatus::Created { .. } = &mut tracked_buffer.status {
|
||||
tracked_buffer.status = TrackedBufferStatus::Modified;
|
||||
}
|
||||
}
|
||||
tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
|
||||
}
|
||||
}
|
||||
@@ -780,9 +775,6 @@ impl ActionLog {
|
||||
.retain(|_buffer, tracked_buffer| match tracked_buffer.status {
|
||||
TrackedBufferStatus::Deleted => false,
|
||||
_ => {
|
||||
if let TrackedBufferStatus::Created { .. } = &mut tracked_buffer.status {
|
||||
tracked_buffer.status = TrackedBufferStatus::Modified;
|
||||
}
|
||||
tracked_buffer.unreviewed_edits.clear();
|
||||
tracked_buffer.diff_base = tracked_buffer.snapshot.as_rope().clone();
|
||||
tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
|
||||
@@ -2083,134 +2075,6 @@ mod tests {
|
||||
assert_eq!(content, "ai content\nuser added this line");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_reject_after_accepting_hunk_on_created_file(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
|
||||
let file_path = project
|
||||
.read_with(cx, |project, cx| {
|
||||
project.find_project_path("dir/new_file", cx)
|
||||
})
|
||||
.unwrap();
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_buffer(file_path.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// AI creates file with initial content
|
||||
cx.update(|cx| {
|
||||
action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
|
||||
buffer.update(cx, |buffer, cx| buffer.set_text("ai content v1", cx));
|
||||
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
|
||||
});
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
assert_ne!(unreviewed_hunks(&action_log, cx), vec![]);
|
||||
|
||||
// User accepts the single hunk
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.keep_edits_in_range(buffer.clone(), Anchor::MIN..Anchor::MAX, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
|
||||
assert!(fs.is_file(path!("/dir/new_file").as_ref()).await);
|
||||
|
||||
// AI modifies the file
|
||||
cx.update(|cx| {
|
||||
buffer.update(cx, |buffer, cx| buffer.set_text("ai content v2", cx));
|
||||
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
|
||||
});
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
assert_ne!(unreviewed_hunks(&action_log, cx), vec![]);
|
||||
|
||||
// User rejects the hunk
|
||||
action_log
|
||||
.update(cx, |log, cx| {
|
||||
log.reject_edits_in_ranges(buffer.clone(), vec![Anchor::MIN..Anchor::MAX], cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
assert!(fs.is_file(path!("/dir/new_file").as_ref()).await,);
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, _| buffer.text()),
|
||||
"ai content v1"
|
||||
);
|
||||
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_reject_edits_on_previously_accepted_created_file(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
|
||||
let file_path = project
|
||||
.read_with(cx, |project, cx| {
|
||||
project.find_project_path("dir/new_file", cx)
|
||||
})
|
||||
.unwrap();
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_buffer(file_path.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// AI creates file with initial content
|
||||
cx.update(|cx| {
|
||||
action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx));
|
||||
buffer.update(cx, |buffer, cx| buffer.set_text("ai content v1", cx));
|
||||
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
|
||||
});
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
// User clicks "Accept All"
|
||||
action_log.update(cx, |log, cx| log.keep_all_edits(cx));
|
||||
cx.run_until_parked();
|
||||
assert!(fs.is_file(path!("/dir/new_file").as_ref()).await);
|
||||
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); // Hunks are cleared
|
||||
|
||||
// AI modifies file again
|
||||
cx.update(|cx| {
|
||||
buffer.update(cx, |buffer, cx| buffer.set_text("ai content v2", cx));
|
||||
action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
|
||||
});
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
assert_ne!(unreviewed_hunks(&action_log, cx), vec![]);
|
||||
|
||||
// User clicks "Reject All"
|
||||
action_log
|
||||
.update(cx, |log, cx| log.reject_all_edits(cx))
|
||||
.await;
|
||||
cx.run_until_parked();
|
||||
assert!(fs.is_file(path!("/dir/new_file").as_ref()).await);
|
||||
assert_eq!(
|
||||
buffer.read_with(cx, |buffer, _| buffer.text()),
|
||||
"ai content v1"
|
||||
);
|
||||
assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
async fn test_random_diffs(mut rng: StdRng, cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,18 +32,11 @@ pub enum Model {
|
||||
ClaudeSonnet4Thinking,
|
||||
#[serde(rename = "claude-opus-4", alias = "claude-opus-4-latest")]
|
||||
ClaudeOpus4,
|
||||
#[serde(rename = "claude-opus-4-1", alias = "claude-opus-4-1-latest")]
|
||||
ClaudeOpus4_1,
|
||||
#[serde(
|
||||
rename = "claude-opus-4-thinking",
|
||||
alias = "claude-opus-4-thinking-latest"
|
||||
)]
|
||||
ClaudeOpus4Thinking,
|
||||
#[serde(
|
||||
rename = "claude-opus-4-1-thinking",
|
||||
alias = "claude-opus-4-1-thinking-latest"
|
||||
)]
|
||||
ClaudeOpus4_1Thinking,
|
||||
#[serde(rename = "claude-3-5-sonnet-v2", alias = "claude-3-5-sonnet-latest")]
|
||||
Claude3_5SonnetV2,
|
||||
#[serde(rename = "claude-3-7-sonnet", alias = "claude-3-7-sonnet-latest")]
|
||||
@@ -154,9 +147,7 @@ impl Model {
|
||||
Model::ClaudeSonnet4 => "claude-4-sonnet",
|
||||
Model::ClaudeSonnet4Thinking => "claude-4-sonnet-thinking",
|
||||
Model::ClaudeOpus4 => "claude-4-opus",
|
||||
Model::ClaudeOpus4_1 => "claude-4-opus-1",
|
||||
Model::ClaudeOpus4Thinking => "claude-4-opus-thinking",
|
||||
Model::ClaudeOpus4_1Thinking => "claude-4-opus-1-thinking",
|
||||
Model::Claude3_5SonnetV2 => "claude-3-5-sonnet-v2",
|
||||
Model::Claude3_5Sonnet => "claude-3-5-sonnet",
|
||||
Model::Claude3Opus => "claude-3-opus",
|
||||
@@ -217,9 +208,6 @@ impl Model {
|
||||
Model::ClaudeOpus4 | Model::ClaudeOpus4Thinking => {
|
||||
"anthropic.claude-opus-4-20250514-v1:0"
|
||||
}
|
||||
Model::ClaudeOpus4_1 | Model::ClaudeOpus4_1Thinking => {
|
||||
"anthropic.claude-opus-4-1-20250805-v1:0"
|
||||
}
|
||||
Model::Claude3_5SonnetV2 => "anthropic.claude-3-5-sonnet-20241022-v2:0",
|
||||
Model::Claude3_5Sonnet => "anthropic.claude-3-5-sonnet-20240620-v1:0",
|
||||
Model::Claude3Opus => "anthropic.claude-3-opus-20240229-v1:0",
|
||||
@@ -278,9 +266,7 @@ impl Model {
|
||||
Self::ClaudeSonnet4 => "Claude Sonnet 4",
|
||||
Self::ClaudeSonnet4Thinking => "Claude Sonnet 4 Thinking",
|
||||
Self::ClaudeOpus4 => "Claude Opus 4",
|
||||
Self::ClaudeOpus4_1 => "Claude Opus 4.1",
|
||||
Self::ClaudeOpus4Thinking => "Claude Opus 4 Thinking",
|
||||
Self::ClaudeOpus4_1Thinking => "Claude Opus 4.1 Thinking",
|
||||
Self::Claude3_5SonnetV2 => "Claude 3.5 Sonnet v2",
|
||||
Self::Claude3_5Sonnet => "Claude 3.5 Sonnet",
|
||||
Self::Claude3Opus => "Claude 3 Opus",
|
||||
@@ -344,10 +330,8 @@ impl Model {
|
||||
| Self::Claude3_7Sonnet
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeOpus4
|
||||
| Self::ClaudeOpus4_1
|
||||
| Self::ClaudeSonnet4Thinking
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeOpus4_1Thinking => 200_000,
|
||||
| Self::ClaudeOpus4Thinking => 200_000,
|
||||
Self::AmazonNovaPremier => 1_000_000,
|
||||
Self::PalmyraWriterX5 => 1_000_000,
|
||||
Self::PalmyraWriterX4 => 128_000,
|
||||
@@ -364,9 +348,7 @@ impl Model {
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4Thinking
|
||||
| Self::ClaudeOpus4
|
||||
| Model::ClaudeOpus4Thinking
|
||||
| Self::ClaudeOpus4_1
|
||||
| Model::ClaudeOpus4_1Thinking => 128_000,
|
||||
| Model::ClaudeOpus4Thinking => 128_000,
|
||||
Self::Claude3_5SonnetV2 | Self::PalmyraWriterX4 | Self::PalmyraWriterX5 => 8_192,
|
||||
Self::Custom {
|
||||
max_output_tokens, ..
|
||||
@@ -384,8 +366,6 @@ impl Model {
|
||||
| Self::Claude3_7Sonnet
|
||||
| Self::ClaudeOpus4
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeOpus4_1
|
||||
| Self::ClaudeOpus4_1Thinking
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4Thinking => 1.0,
|
||||
Self::Custom {
|
||||
@@ -407,8 +387,6 @@ impl Model {
|
||||
| Self::Claude3_7SonnetThinking
|
||||
| Self::ClaudeOpus4
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeOpus4_1
|
||||
| Self::ClaudeOpus4_1Thinking
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4Thinking
|
||||
| Self::Claude3_5Haiku => true,
|
||||
@@ -442,9 +420,7 @@ impl Model {
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4Thinking
|
||||
| Self::ClaudeOpus4
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeOpus4_1
|
||||
| Self::ClaudeOpus4_1Thinking => true,
|
||||
| Self::ClaudeOpus4Thinking => true,
|
||||
|
||||
// Custom models - check if they have cache configuration
|
||||
Self::Custom {
|
||||
@@ -464,9 +440,7 @@ impl Model {
|
||||
| Self::ClaudeSonnet4
|
||||
| Self::ClaudeSonnet4Thinking
|
||||
| Self::ClaudeOpus4
|
||||
| Self::ClaudeOpus4Thinking
|
||||
| Self::ClaudeOpus4_1
|
||||
| Self::ClaudeOpus4_1Thinking => Some(BedrockModelCacheConfiguration {
|
||||
| Self::ClaudeOpus4Thinking => Some(BedrockModelCacheConfiguration {
|
||||
max_cache_anchors: 4,
|
||||
min_total_token: 1024,
|
||||
}),
|
||||
@@ -493,11 +467,9 @@ impl Model {
|
||||
Model::ClaudeSonnet4Thinking => BedrockModelMode::Thinking {
|
||||
budget_tokens: Some(4096),
|
||||
},
|
||||
Model::ClaudeOpus4Thinking | Model::ClaudeOpus4_1Thinking => {
|
||||
BedrockModelMode::Thinking {
|
||||
budget_tokens: Some(4096),
|
||||
}
|
||||
}
|
||||
Model::ClaudeOpus4Thinking => BedrockModelMode::Thinking {
|
||||
budget_tokens: Some(4096),
|
||||
},
|
||||
_ => BedrockModelMode::Default,
|
||||
}
|
||||
}
|
||||
@@ -546,8 +518,6 @@ impl Model {
|
||||
| Model::ClaudeSonnet4Thinking
|
||||
| Model::ClaudeOpus4
|
||||
| Model::ClaudeOpus4Thinking
|
||||
| Model::ClaudeOpus4_1
|
||||
| Model::ClaudeOpus4_1Thinking
|
||||
| Model::Claude3Haiku
|
||||
| Model::Claude3Opus
|
||||
| Model::Claude3Sonnet
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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::*;
|
||||
@@ -151,6 +154,7 @@ impl Settings for ProxySettings {
|
||||
|
||||
pub fn init_settings(cx: &mut App) {
|
||||
TelemetrySettings::register(cx);
|
||||
DisableAiSettings::register(cx);
|
||||
ClientSettings::register(cx);
|
||||
ProxySettings::register(cx);
|
||||
}
|
||||
@@ -212,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>,
|
||||
@@ -548,6 +553,33 @@ impl settings::Settings for TelemetrySettings {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether to disable all AI features in Zed.
|
||||
///
|
||||
/// Default: false
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct DisableAiSettings {
|
||||
pub disable_ai: bool,
|
||||
}
|
||||
|
||||
impl settings::Settings for DisableAiSettings {
|
||||
const KEY: Option<&'static str> = Some("disable_ai");
|
||||
|
||||
type FileContent = Option<bool>;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
||||
Ok(Self {
|
||||
disable_ai: sources
|
||||
.user
|
||||
.or(sources.server)
|
||||
.copied()
|
||||
.flatten()
|
||||
.unwrap_or(sources.default.ok_or_else(Self::missing_default)?),
|
||||
})
|
||||
}
|
||||
|
||||
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn new(
|
||||
clock: Arc<dyn SystemClock>,
|
||||
@@ -558,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(),
|
||||
@@ -590,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
|
||||
@@ -902,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);
|
||||
@@ -1452,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 {
|
||||
|
||||
3
crates/client/src/cloud.rs
Normal file
3
crates/client/src/cloud.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
mod user_store;
|
||||
|
||||
pub use user_store::*;
|
||||
69
crates/client/src/cloud/user_store.rs
Normal file
69
crates/client/src/cloud/user_store.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
21
crates/cloud_api_client/Cargo.toml
Normal file
21
crates/cloud_api_client/Cargo.toml
Normal 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
|
||||
1
crates/cloud_api_client/LICENSE-APACHE
Symbolic link
1
crates/cloud_api_client/LICENSE-APACHE
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-APACHE
|
||||
83
crates/cloud_api_client/src/cloud_api_client.rs
Normal file
83
crates/cloud_api_client/src/cloud_api_client.rs
Normal 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)?)
|
||||
}
|
||||
}
|
||||
22
crates/cloud_api_types/Cargo.toml
Normal file
22
crates/cloud_api_types/Cargo.toml
Normal 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
|
||||
1
crates/cloud_api_types/LICENSE-APACHE
Symbolic link
1
crates/cloud_api_types/LICENSE-APACHE
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-APACHE
|
||||
40
crates/cloud_api_types/src/cloud_api_types.rs
Normal file
40
crates/cloud_api_types/src/cloud_api_types.rs
Normal 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,
|
||||
}
|
||||
166
crates/cloud_api_types/src/timestamp.rs
Normal file
166
crates/cloud_api_types/src/timestamp.rs
Normal 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(×tamp).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(×tamp).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(×tamp).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());
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
¶ms.github_login,
|
||||
params.github_user_id,
|
||||
params.github_email.as_deref(),
|
||||
params.github_name.as_deref(),
|
||||
params.github_user_created_at,
|
||||
initial_channel_id,
|
||||
)
|
||||
.await?;
|
||||
let metrics_id = app.db.get_user_metrics_id(user.id).await?;
|
||||
let feature_flags = app.db.get_user_flags(user.id).await?;
|
||||
Ok(Json(AuthenticatedUserResponse {
|
||||
user,
|
||||
metrics_id,
|
||||
feature_flags,
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct LookUpUserParams {
|
||||
identifier: String,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -6,6 +6,7 @@ mod sign_in;
|
||||
use crate::sign_in::initiate_sign_in_within_workspace;
|
||||
use ::fs::Fs;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use client::DisableAiSettings;
|
||||
use collections::{HashMap, HashSet};
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use futures::{Future, FutureExt, TryFutureExt, channel::oneshot, future::Shared};
|
||||
@@ -23,7 +24,6 @@ use language::{
|
||||
use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId, LanguageServerName};
|
||||
use node_runtime::NodeRuntime;
|
||||
use parking_lot::Mutex;
|
||||
use project::DisableAiSettings;
|
||||
use request::StatusNotification;
|
||||
use serde_json::json;
|
||||
use settings::Settings;
|
||||
|
||||
@@ -295,7 +295,7 @@ mod tests {
|
||||
request: dap_types::StartDebuggingRequestArgumentsRequest::Launch,
|
||||
},
|
||||
},
|
||||
Box::new(|_| {}),
|
||||
Box::new(|_| panic!("Did not expect to hit this code path")),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -883,7 +883,6 @@ impl FakeTransport {
|
||||
break Err(anyhow!("exit in response to request"));
|
||||
}
|
||||
};
|
||||
let success = response.success;
|
||||
let message =
|
||||
serde_json::to_string(&Message::Response(response)).unwrap();
|
||||
|
||||
@@ -894,25 +893,6 @@ impl FakeTransport {
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if request.command == dap_types::requests::Initialize::COMMAND
|
||||
&& success
|
||||
{
|
||||
let message = serde_json::to_string(&Message::Event(Box::new(
|
||||
dap_types::messages::Events::Initialized(Some(
|
||||
Default::default(),
|
||||
)),
|
||||
)))
|
||||
.unwrap();
|
||||
writer
|
||||
.write_all(
|
||||
TransportDelegate::build_rpc_message(message)
|
||||
.as_bytes(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
writer.flush().await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -22,7 +22,6 @@ test-support = [
|
||||
"theme/test-support",
|
||||
"util/test-support",
|
||||
"workspace/test-support",
|
||||
"tree-sitter-c",
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-typescript",
|
||||
"tree-sitter-html",
|
||||
@@ -77,7 +76,6 @@ telemetry.workspace = true
|
||||
text.workspace = true
|
||||
time.workspace = true
|
||||
theme.workspace = true
|
||||
tree-sitter-c = { workspace = true, optional = true }
|
||||
tree-sitter-html = { workspace = true, optional = true }
|
||||
tree-sitter-rust = { workspace = true, optional = true }
|
||||
tree-sitter-typescript = { workspace = true, optional = true }
|
||||
@@ -108,7 +106,6 @@ settings = { workspace = true, features = ["test-support"] }
|
||||
tempfile.workspace = true
|
||||
text = { workspace = true, features = ["test-support"] }
|
||||
theme = { workspace = true, features = ["test-support"] }
|
||||
tree-sitter-c.workspace = true
|
||||
tree-sitter-html.workspace = true
|
||||
tree-sitter-rust.workspace = true
|
||||
tree-sitter-typescript.workspace = true
|
||||
|
||||
@@ -56,7 +56,7 @@ use aho_corasick::AhoCorasick;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use blink_manager::BlinkManager;
|
||||
use buffer_diff::DiffHunkStatus;
|
||||
use client::{Collaborator, ParticipantIndex};
|
||||
use client::{Collaborator, DisableAiSettings, ParticipantIndex};
|
||||
use clock::{AGENT_REPLICA_ID, ReplicaId};
|
||||
use collections::{BTreeMap, HashMap, HashSet, VecDeque};
|
||||
use convert_case::{Case, Casing};
|
||||
@@ -125,7 +125,7 @@ use markdown::Markdown;
|
||||
use mouse_context_menu::MouseContextMenu;
|
||||
use persistence::DB;
|
||||
use project::{
|
||||
BreakpointWithPosition, CompletionResponse, DisableAiSettings, ProjectPath,
|
||||
BreakpointWithPosition, CompletionResponse, ProjectPath,
|
||||
debugger::{
|
||||
breakpoint_store::{
|
||||
BreakpointEditAction, BreakpointSessionState, BreakpointState, BreakpointStore,
|
||||
@@ -1305,7 +1305,6 @@ impl Default for SelectionHistoryMode {
|
||||
///
|
||||
/// Similarly, you might want to disable scrolling if you don't want the viewport to
|
||||
/// move.
|
||||
#[derive(Clone)]
|
||||
pub struct SelectionEffects {
|
||||
nav_history: Option<bool>,
|
||||
completions: bool,
|
||||
@@ -2945,12 +2944,10 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
let selection_anchors = self.selections.disjoint_anchors();
|
||||
|
||||
if self.focus_handle.is_focused(window) && self.leader_id.is_none() {
|
||||
self.buffer.update(cx, |buffer, cx| {
|
||||
buffer.set_active_selections(
|
||||
&selection_anchors,
|
||||
&self.selections.disjoint_anchors(),
|
||||
self.selections.line_mode,
|
||||
self.cursor_shape,
|
||||
cx,
|
||||
@@ -2967,8 +2964,9 @@ impl Editor {
|
||||
self.select_next_state = None;
|
||||
self.select_prev_state = None;
|
||||
self.select_syntax_node_history.try_clear();
|
||||
self.invalidate_autoclose_regions(&selection_anchors, buffer);
|
||||
self.snippet_stack.invalidate(&selection_anchors, buffer);
|
||||
self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), buffer);
|
||||
self.snippet_stack
|
||||
.invalidate(&self.selections.disjoint_anchors(), buffer);
|
||||
self.take_rename(false, window, cx);
|
||||
|
||||
let newest_selection = self.selections.newest_anchor();
|
||||
@@ -4049,8 +4047,7 @@ impl Editor {
|
||||
// then don't insert that closing bracket again; just move the selection
|
||||
// past the closing bracket.
|
||||
let should_skip = selection.end == region.range.end.to_point(&snapshot)
|
||||
&& text.as_ref() == region.pair.end.as_str()
|
||||
&& snapshot.contains_str_at(region.range.end, text.as_ref());
|
||||
&& text.as_ref() == region.pair.end.as_str();
|
||||
if should_skip {
|
||||
let anchor = snapshot.anchor_after(selection.end);
|
||||
new_selections
|
||||
@@ -4976,17 +4973,13 @@ impl Editor {
|
||||
})
|
||||
}
|
||||
|
||||
/// Remove any autoclose regions that no longer contain their selection or have invalid anchors in ranges.
|
||||
/// Remove any autoclose regions that no longer contain their selection.
|
||||
fn invalidate_autoclose_regions(
|
||||
&mut self,
|
||||
mut selections: &[Selection<Anchor>],
|
||||
buffer: &MultiBufferSnapshot,
|
||||
) {
|
||||
self.autoclose_regions.retain(|state| {
|
||||
if !state.range.start.is_valid(buffer) || !state.range.end.is_valid(buffer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut i = 0;
|
||||
while let Some(selection) = selections.get(i) {
|
||||
if selection.end.cmp(&state.range.start, buffer).is_lt() {
|
||||
@@ -5898,20 +5891,18 @@ impl Editor {
|
||||
text: new_text[common_prefix_len..].into(),
|
||||
});
|
||||
|
||||
self.transact(window, cx, |editor, window, cx| {
|
||||
self.transact(window, cx, |this, window, cx| {
|
||||
if let Some(mut snippet) = snippet {
|
||||
snippet.text = new_text.to_string();
|
||||
editor
|
||||
.insert_snippet(&ranges, snippet, window, cx)
|
||||
.log_err();
|
||||
this.insert_snippet(&ranges, snippet, window, cx).log_err();
|
||||
} else {
|
||||
editor.buffer.update(cx, |multi_buffer, cx| {
|
||||
this.buffer.update(cx, |buffer, cx| {
|
||||
let auto_indent = match completion.insert_text_mode {
|
||||
Some(InsertTextMode::AS_IS) => None,
|
||||
_ => editor.autoindent_mode.clone(),
|
||||
_ => this.autoindent_mode.clone(),
|
||||
};
|
||||
let edits = ranges.into_iter().map(|range| (range, new_text.as_str()));
|
||||
multi_buffer.edit(edits, auto_indent, cx);
|
||||
buffer.edit(edits, auto_indent, cx);
|
||||
});
|
||||
}
|
||||
for (buffer, edits) in linked_edits {
|
||||
@@ -5930,9 +5921,8 @@ impl Editor {
|
||||
})
|
||||
}
|
||||
|
||||
editor.refresh_inline_completion(true, false, window, cx);
|
||||
this.refresh_inline_completion(true, false, window, cx);
|
||||
});
|
||||
self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), &snapshot);
|
||||
|
||||
let show_new_completions_on_confirm = completion
|
||||
.confirm
|
||||
@@ -7003,10 +6993,6 @@ impl Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<()> {
|
||||
if DisableAiSettings::get_global(cx).disable_ai {
|
||||
return None;
|
||||
}
|
||||
|
||||
let provider = self.edit_prediction_provider()?;
|
||||
let cursor = self.selections.newest_anchor().head();
|
||||
let (buffer, cursor_buffer_position) =
|
||||
@@ -7064,7 +7050,6 @@ impl Editor {
|
||||
pub fn update_edit_prediction_settings(&mut self, cx: &mut Context<Self>) {
|
||||
if self.edit_prediction_provider.is_none() || DisableAiSettings::get_global(cx).disable_ai {
|
||||
self.edit_prediction_settings = EditPredictionSettings::Disabled;
|
||||
self.discard_inline_completion(false, cx);
|
||||
} else {
|
||||
let selection = self.selections.newest_anchor();
|
||||
let cursor = selection.head();
|
||||
@@ -7682,10 +7667,6 @@ impl Editor {
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<()> {
|
||||
if DisableAiSettings::get_global(cx).disable_ai {
|
||||
return None;
|
||||
}
|
||||
|
||||
let selection = self.selections.newest_anchor();
|
||||
let cursor = selection.head();
|
||||
let multibuffer = self.buffer.read(cx).snapshot(cx);
|
||||
@@ -9581,46 +9562,27 @@ impl Editor {
|
||||
// Check whether the just-entered snippet ends with an auto-closable bracket.
|
||||
if self.autoclose_regions.is_empty() {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
let mut all_selections = self.selections.all::<Point>(cx);
|
||||
for selection in &mut all_selections {
|
||||
for selection in &mut self.selections.all::<Point>(cx) {
|
||||
let selection_head = selection.head();
|
||||
let Some(scope) = snapshot.language_scope_at(selection_head) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut bracket_pair = None;
|
||||
let max_lookup_length = scope
|
||||
.brackets()
|
||||
.map(|(pair, _)| {
|
||||
pair.start
|
||||
.as_str()
|
||||
.chars()
|
||||
.count()
|
||||
.max(pair.end.as_str().chars().count())
|
||||
})
|
||||
.max();
|
||||
if let Some(max_lookup_length) = max_lookup_length {
|
||||
let next_text = snapshot
|
||||
.chars_at(selection_head)
|
||||
.take(max_lookup_length)
|
||||
.collect::<String>();
|
||||
let prev_text = snapshot
|
||||
.reversed_chars_at(selection_head)
|
||||
.take(max_lookup_length)
|
||||
.collect::<String>();
|
||||
|
||||
for (pair, enabled) in scope.brackets() {
|
||||
if enabled
|
||||
&& pair.close
|
||||
&& prev_text.starts_with(pair.start.as_str())
|
||||
&& next_text.starts_with(pair.end.as_str())
|
||||
{
|
||||
bracket_pair = Some(pair.clone());
|
||||
break;
|
||||
}
|
||||
let next_chars = snapshot.chars_at(selection_head).collect::<String>();
|
||||
let prev_chars = snapshot
|
||||
.reversed_chars_at(selection_head)
|
||||
.collect::<String>();
|
||||
for (pair, enabled) in scope.brackets() {
|
||||
if enabled
|
||||
&& pair.close
|
||||
&& prev_chars.starts_with(pair.start.as_str())
|
||||
&& next_chars.starts_with(pair.end.as_str())
|
||||
{
|
||||
bracket_pair = Some(pair.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(pair) = bracket_pair {
|
||||
let snapshot_settings = snapshot.language_settings_at(selection_head, cx);
|
||||
let autoclose_enabled =
|
||||
|
||||
@@ -13400,178 +13400,6 @@ async fn test_as_is_completions(cx: &mut TestAppContext) {
|
||||
cx.assert_editor_state("fn a() {}\n unsafeˇ");
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
let language =
|
||||
Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
|
||||
let mut cx = EditorLspTestContext::new(
|
||||
language,
|
||||
lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
..lsp::CompletionOptions::default()
|
||||
}),
|
||||
..lsp::ServerCapabilities::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
cx.set_state(
|
||||
"#ifndef BAR_H
|
||||
#define BAR_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
int fn_branch(bool do_branch1, bool do_branch2);
|
||||
|
||||
#endif // BAR_H
|
||||
ˇ",
|
||||
);
|
||||
cx.executor().run_until_parked();
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("#", window, cx);
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("i", window, cx);
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("n", window, cx);
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
cx.assert_editor_state(
|
||||
"#ifndef BAR_H
|
||||
#define BAR_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
int fn_branch(bool do_branch1, bool do_branch2);
|
||||
|
||||
#endif // BAR_H
|
||||
#inˇ",
|
||||
);
|
||||
|
||||
cx.lsp
|
||||
.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
|
||||
Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
|
||||
is_incomplete: false,
|
||||
item_defaults: None,
|
||||
items: vec![lsp::CompletionItem {
|
||||
kind: Some(lsp::CompletionItemKind::SNIPPET),
|
||||
label_details: Some(lsp::CompletionItemLabelDetails {
|
||||
detail: Some("header".to_string()),
|
||||
description: None,
|
||||
}),
|
||||
label: " include".to_string(),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: lsp::Range {
|
||||
start: lsp::Position {
|
||||
line: 8,
|
||||
character: 1,
|
||||
},
|
||||
end: lsp::Position {
|
||||
line: 8,
|
||||
character: 1,
|
||||
},
|
||||
},
|
||||
new_text: "include \"$0\"".to_string(),
|
||||
})),
|
||||
sort_text: Some("40b67681include".to_string()),
|
||||
insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
|
||||
filter_text: Some("include".to_string()),
|
||||
insert_text: Some("include \"$0\"".to_string()),
|
||||
..lsp::CompletionItem::default()
|
||||
}],
|
||||
})))
|
||||
});
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
cx.assert_editor_state(
|
||||
"#ifndef BAR_H
|
||||
#define BAR_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
int fn_branch(bool do_branch1, bool do_branch2);
|
||||
|
||||
#endif // BAR_H
|
||||
#include \"ˇ\"",
|
||||
);
|
||||
|
||||
cx.lsp
|
||||
.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
|
||||
Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
|
||||
is_incomplete: true,
|
||||
item_defaults: None,
|
||||
items: vec![lsp::CompletionItem {
|
||||
kind: Some(lsp::CompletionItemKind::FILE),
|
||||
label: "AGL/".to_string(),
|
||||
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
|
||||
range: lsp::Range {
|
||||
start: lsp::Position {
|
||||
line: 8,
|
||||
character: 10,
|
||||
},
|
||||
end: lsp::Position {
|
||||
line: 8,
|
||||
character: 11,
|
||||
},
|
||||
},
|
||||
new_text: "AGL/".to_string(),
|
||||
})),
|
||||
sort_text: Some("40b67681AGL/".to_string()),
|
||||
insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
|
||||
filter_text: Some("AGL/".to_string()),
|
||||
insert_text: Some("AGL/".to_string()),
|
||||
..lsp::CompletionItem::default()
|
||||
}],
|
||||
})))
|
||||
});
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
cx.assert_editor_state(
|
||||
r##"#ifndef BAR_H
|
||||
#define BAR_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
int fn_branch(bool do_branch1, bool do_branch2);
|
||||
|
||||
#endif // BAR_H
|
||||
#include "AGL/ˇ"##,
|
||||
);
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("\"", window, cx);
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
cx.assert_editor_state(
|
||||
r##"#ifndef BAR_H
|
||||
#define BAR_H
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
int fn_branch(bool do_branch1, bool do_branch2);
|
||||
|
||||
#endif // BAR_H
|
||||
#include "AGL/"ˇ"##,
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
@@ -23,6 +23,7 @@ askpass.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
call.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
cloud_llm_client.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::branch_picker::{self, BranchList};
|
||||
use crate::git_panel::{GitPanel, commit_message_editor};
|
||||
use client::DisableAiSettings;
|
||||
use git::repository::CommitOptions;
|
||||
use git::{Amend, Commit, GenerateCommitMessage, Signoff};
|
||||
use panel::{panel_button, panel_editor_style};
|
||||
use project::DisableAiSettings;
|
||||
use settings::Settings;
|
||||
use ui::{
|
||||
ContextMenu, KeybindingHint, PopoverMenu, PopoverMenuHandle, SplitButton, Tooltip, prelude::*,
|
||||
|
||||
@@ -12,6 +12,7 @@ use crate::{
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::Context as _;
|
||||
use askpass::AskPassDelegate;
|
||||
use client::DisableAiSettings;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::{
|
||||
Editor, EditorElement, EditorMode, EditorSettings, MultiBuffer, ShowScrollbar,
|
||||
@@ -50,9 +51,10 @@ use panel::{
|
||||
PanelHeader, panel_button, panel_editor_container, panel_editor_style, panel_filled_button,
|
||||
panel_icon_button,
|
||||
};
|
||||
use project::git_store::{RepositoryEvent, RepositoryId};
|
||||
use project::{
|
||||
DisableAiSettings, Fs, Project, ProjectPath,
|
||||
git_store::{GitStoreEvent, Repository, RepositoryEvent, RepositoryId},
|
||||
Fs, Project, ProjectPath,
|
||||
git_store::{GitStoreEvent, Repository},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore};
|
||||
@@ -5113,6 +5115,7 @@ mod tests {
|
||||
language::init(cx);
|
||||
editor::init(cx);
|
||||
Project::init_settings(cx);
|
||||
client::DisableAiSettings::register(cx);
|
||||
crate::init(cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -606,7 +606,7 @@ impl BladeRenderer {
|
||||
xy_position: v.xy_position,
|
||||
st_position: v.st_position,
|
||||
color: path.color,
|
||||
bounds: path.clipped_bounds(),
|
||||
bounds: path.bounds.intersect(&path.content_mask.bounds),
|
||||
}));
|
||||
}
|
||||
let vertex_buf = unsafe { self.instance_belt.alloc_typed(&vertices, &self.gpu) };
|
||||
@@ -735,13 +735,13 @@ impl BladeRenderer {
|
||||
paths
|
||||
.iter()
|
||||
.map(|path| PathSprite {
|
||||
bounds: path.clipped_bounds(),
|
||||
bounds: path.bounds,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
let mut bounds = first_path.clipped_bounds();
|
||||
let mut bounds = first_path.bounds;
|
||||
for path in paths.iter().skip(1) {
|
||||
bounds = bounds.union(&path.clipped_bounds());
|
||||
bounds = bounds.union(&path.bounds);
|
||||
}
|
||||
vec![PathSprite { bounds }]
|
||||
};
|
||||
|
||||
@@ -1795,6 +1795,7 @@ impl X11ClientState {
|
||||
drop(state);
|
||||
window.refresh(RequestFrameOptions {
|
||||
require_presentation: expose_event_received,
|
||||
force_render: false,
|
||||
});
|
||||
}
|
||||
xcb_connection
|
||||
|
||||
@@ -791,13 +791,13 @@ impl MetalRenderer {
|
||||
sprites = paths
|
||||
.iter()
|
||||
.map(|path| PathSprite {
|
||||
bounds: path.clipped_bounds(),
|
||||
bounds: path.bounds,
|
||||
})
|
||||
.collect();
|
||||
} else {
|
||||
let mut bounds = first_path.clipped_bounds();
|
||||
let mut bounds = first_path.bounds;
|
||||
for path in paths.iter().skip(1) {
|
||||
bounds = bounds.union(&path.clipped_bounds());
|
||||
bounds = bounds.union(&path.bounds);
|
||||
}
|
||||
sprites = vec![PathSprite { bounds }];
|
||||
}
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
309
crates/gpui/src/platform/windows/directx_atlas.rs
Normal file
309
crates/gpui/src/platform/windows/directx_atlas.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
1777
crates/gpui/src/platform/windows/directx_renderer.rs
Normal file
1777
crates/gpui/src/platform/windows/directx_renderer.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
1160
crates/gpui/src/platform/windows/shaders.hlsl
Normal file
1160
crates/gpui/src/platform/windows/shaders.hlsl
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
@@ -8,12 +8,7 @@ use crate::{
|
||||
AtlasTextureId, AtlasTile, Background, Bounds, ContentMask, Corners, Edges, Hsla, Pixels,
|
||||
Point, Radians, ScaledPixels, Size, bounds_tree::BoundsTree, point,
|
||||
};
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
iter::Peekable,
|
||||
ops::{Add, Range, Sub},
|
||||
slice,
|
||||
};
|
||||
use std::{fmt::Debug, iter::Peekable, ops::Range, slice};
|
||||
|
||||
#[allow(non_camel_case_types, unused)]
|
||||
pub(crate) type PathVertex_ScaledPixels = PathVertex<ScaledPixels>;
|
||||
@@ -798,16 +793,6 @@ impl Path<Pixels> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Path<T>
|
||||
where
|
||||
T: Clone + Debug + Default + PartialEq + PartialOrd + Add<T, Output = T> + Sub<Output = T>,
|
||||
{
|
||||
#[allow(unused)]
|
||||
pub(crate) fn clipped_bounds(&self) -> Bounds<T> {
|
||||
self.bounds.intersect(&self.content_mask.bounds)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Path<ScaledPixels>> for Primitive {
|
||||
fn from(path: Path<ScaledPixels>) -> Self {
|
||||
Primitive::Path(path)
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -25,7 +25,6 @@ indoc.workspace = true
|
||||
inline_completion.workspace = true
|
||||
language.workspace = true
|
||||
paths.workspace = true
|
||||
project.workspace = true
|
||||
regex.workspace = true
|
||||
settings.workspace = true
|
||||
supermaven.workspace = true
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use client::{UserStore, zed_urls};
|
||||
use client::{DisableAiSettings, UserStore, zed_urls};
|
||||
use cloud_llm_client::UsageLimit;
|
||||
use copilot::{Copilot, Status};
|
||||
use editor::{
|
||||
@@ -19,7 +19,6 @@ use language::{
|
||||
EditPredictionsMode, File, Language,
|
||||
language_settings::{self, AllLanguageSettings, EditPredictionProvider, all_language_settings},
|
||||
};
|
||||
use project::DisableAiSettings;
|
||||
use regex::Regex;
|
||||
use settings::{Settings, SettingsStore, update_settings_file};
|
||||
use std::{
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -167,10 +167,10 @@ impl Anchor {
|
||||
if *self == Anchor::min() || *self == Anchor::max() {
|
||||
true
|
||||
} else if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) {
|
||||
(self.text_anchor == excerpt.range.context.start
|
||||
|| self.text_anchor == excerpt.range.context.end
|
||||
|| self.text_anchor.is_valid(&excerpt.buffer))
|
||||
&& excerpt.contains(self)
|
||||
excerpt.contains(self)
|
||||
&& (self.text_anchor == excerpt.range.context.start
|
||||
|| self.text_anchor == excerpt.range.context.end
|
||||
|| self.text_anchor.is_valid(&excerpt.buffer))
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
103
crates/onboarding/src/basics_page.rs
Normal file
103
crates/onboarding/src/basics_page.rs
Normal 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())
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::{path::Path, sync::Arc};
|
||||
|
||||
use dap::client::DebugAdapterClient;
|
||||
use gpui::{App, Subscription};
|
||||
use gpui::{App, AppContext, Subscription};
|
||||
|
||||
use super::session::{Session, SessionStateEvent};
|
||||
|
||||
@@ -19,6 +19,14 @@ pub fn intercept_debug_sessions<T: Fn(&Arc<DebugAdapterClient>) + 'static>(
|
||||
let client = session.adapter_client().unwrap();
|
||||
register_default_handlers(session, &client, cx);
|
||||
configure(&client);
|
||||
cx.background_spawn(async move {
|
||||
client
|
||||
.fake_event(dap::messages::Events::Initialized(
|
||||
Some(Default::default()),
|
||||
))
|
||||
.await
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
@@ -2269,7 +2269,7 @@ impl LspCommand for GetCompletions {
|
||||
// the range based on the syntax tree.
|
||||
None => {
|
||||
if self.position != clipped_position {
|
||||
log::info!("completion out of expected range ");
|
||||
log::info!("completion out of expected range");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2483,9 +2483,7 @@ pub(crate) fn parse_completion_text_edit(
|
||||
let start = snapshot.clip_point_utf16(range.start, Bias::Left);
|
||||
let end = snapshot.clip_point_utf16(range.end, Bias::Left);
|
||||
if start != range.start.0 || end != range.end.0 {
|
||||
log::info!(
|
||||
"completion out of expected range, start: {start:?}, end: {end:?}, range: {range:?}"
|
||||
);
|
||||
log::info!("completion out of expected range");
|
||||
return None;
|
||||
}
|
||||
snapshot.anchor_before(start)..snapshot.anchor_after(end)
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -97,7 +97,7 @@ use rpc::{
|
||||
};
|
||||
use search::{SearchInputKind, SearchQuery, SearchResult};
|
||||
use search_history::SearchHistory;
|
||||
use settings::{InvalidSettingsError, Settings, SettingsLocation, SettingsSources, SettingsStore};
|
||||
use settings::{InvalidSettingsError, Settings, SettingsLocation, SettingsStore};
|
||||
use smol::channel::Receiver;
|
||||
use snippet::Snippet;
|
||||
use snippet_provider::SnippetProvider;
|
||||
@@ -942,38 +942,10 @@ pub enum PulledDiagnostics {
|
||||
},
|
||||
}
|
||||
|
||||
/// Whether to disable all AI features in Zed.
|
||||
///
|
||||
/// Default: false
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct DisableAiSettings {
|
||||
pub disable_ai: bool,
|
||||
}
|
||||
|
||||
impl settings::Settings for DisableAiSettings {
|
||||
const KEY: Option<&'static str> = Some("disable_ai");
|
||||
|
||||
type FileContent = Option<bool>;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
||||
Ok(Self {
|
||||
disable_ai: sources
|
||||
.user
|
||||
.or(sources.server)
|
||||
.copied()
|
||||
.flatten()
|
||||
.unwrap_or(sources.default.ok_or_else(Self::missing_default)?),
|
||||
})
|
||||
}
|
||||
|
||||
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
|
||||
}
|
||||
|
||||
impl Project {
|
||||
pub fn init_settings(cx: &mut App) {
|
||||
WorktreeSettings::register(cx);
|
||||
ProjectSettings::register(cx);
|
||||
DisableAiSettings::register(cx);
|
||||
}
|
||||
|
||||
pub fn init(client: &Arc<Client>, cx: &mut App) {
|
||||
@@ -3400,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
|
||||
@@ -3418,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
|
||||
@@ -3436,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
|
||||
@@ -3454,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
|
||||
@@ -3472,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
|
||||
@@ -4024,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
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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()),
|
||||
|
||||
35
crates/settings_profile_selector/Cargo.toml
Normal file
35
crates/settings_profile_selector/Cargo.toml
Normal 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"] }
|
||||
1
crates/settings_profile_selector/LICENSE-GPL
Symbolic link
1
crates/settings_profile_selector/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -99,9 +99,7 @@ impl Anchor {
|
||||
} else if self.buffer_id != Some(buffer.remote_id) {
|
||||
false
|
||||
} else {
|
||||
let Some(fragment_id) = buffer.try_fragment_id_for_anchor(self) else {
|
||||
return false;
|
||||
};
|
||||
let fragment_id = buffer.fragment_id_for_anchor(self);
|
||||
let mut fragment_cursor = buffer.fragments.cursor::<(Option<&Locator>, usize)>(&None);
|
||||
fragment_cursor.seek(&Some(fragment_id), Bias::Left);
|
||||
fragment_cursor
|
||||
|
||||
@@ -2330,19 +2330,10 @@ impl BufferSnapshot {
|
||||
}
|
||||
|
||||
fn fragment_id_for_anchor(&self, anchor: &Anchor) -> &Locator {
|
||||
self.try_fragment_id_for_anchor(anchor).unwrap_or_else(|| {
|
||||
panic!(
|
||||
"invalid anchor {:?}. buffer id: {}, version: {:?}",
|
||||
anchor, self.remote_id, self.version,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn try_fragment_id_for_anchor(&self, anchor: &Anchor) -> Option<&Locator> {
|
||||
if *anchor == Anchor::MIN {
|
||||
Some(Locator::min_ref())
|
||||
Locator::min_ref()
|
||||
} else if *anchor == Anchor::MAX {
|
||||
Some(Locator::max_ref())
|
||||
Locator::max_ref()
|
||||
} else {
|
||||
let anchor_key = InsertionFragmentKey {
|
||||
timestamp: anchor.timestamp,
|
||||
@@ -2363,12 +2354,20 @@ impl BufferSnapshot {
|
||||
insertion_cursor.prev();
|
||||
}
|
||||
|
||||
insertion_cursor
|
||||
.item()
|
||||
.filter(|insertion| {
|
||||
!cfg!(debug_assertions) || insertion.timestamp == anchor.timestamp
|
||||
})
|
||||
.map(|insertion| &insertion.fragment_id)
|
||||
let Some(insertion) = insertion_cursor.item().filter(|insertion| {
|
||||
if cfg!(debug_assertions) {
|
||||
insertion.timestamp == anchor.timestamp
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}) else {
|
||||
panic!(
|
||||
"invalid anchor {:?}. buffer id: {}, version: {:?}",
|
||||
anchor, self.remote_id, self.version
|
||||
);
|
||||
};
|
||||
|
||||
&insertion.fragment_id
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use client::{TelemetrySettings, telemetry::Telemetry};
|
||||
use client::{DisableAiSettings, TelemetrySettings, telemetry::Telemetry};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use gpui::{
|
||||
Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
|
||||
ParentElement, Render, Styled, Subscription, Task, WeakEntity, Window, actions, svg,
|
||||
};
|
||||
use language::language_settings::{EditPredictionProvider, all_language_settings};
|
||||
use project::DisableAiSettings;
|
||||
use settings::{Settings, SettingsStore};
|
||||
use std::sync::Arc;
|
||||
use ui::{CheckboxWithLabel, ElevationIndex, Tooltip, prelude::*};
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "The fast, collaborative code editor."
|
||||
edition.workspace = true
|
||||
name = "zed"
|
||||
version = "0.198.2"
|
||||
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
|
||||
|
||||
@@ -1 +1 @@
|
||||
stable
|
||||
dev
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -4366,6 +4366,7 @@ mod tests {
|
||||
"repl",
|
||||
"rules_library",
|
||||
"search",
|
||||
"settings_profile_selector",
|
||||
"snippets",
|
||||
"supermaven",
|
||||
"svg",
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use client::{Client, UserStore};
|
||||
use client::{Client, DisableAiSettings, UserStore};
|
||||
use collections::HashMap;
|
||||
use copilot::{Copilot, CopilotCompletionProvider};
|
||||
use editor::Editor;
|
||||
use gpui::{AnyWindowHandle, App, AppContext as _, Context, Entity, WeakEntity};
|
||||
use language::language_settings::{EditPredictionProvider, all_language_settings};
|
||||
use project::DisableAiSettings;
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use smol::stream::StreamExt;
|
||||
use std::{cell::RefCell, rc::Rc, sync::Arc};
|
||||
|
||||
@@ -2,6 +2,7 @@ mod preview;
|
||||
mod repl_menu;
|
||||
|
||||
use agent_settings::AgentSettings;
|
||||
use client::DisableAiSettings;
|
||||
use editor::actions::{
|
||||
AddSelectionAbove, AddSelectionBelow, CodeActionSource, DuplicateLineDown, GoToDiagnostic,
|
||||
GoToHunk, GoToPreviousDiagnostic, GoToPreviousHunk, MoveLineDown, MoveLineUp, SelectAll,
|
||||
@@ -15,7 +16,6 @@ use gpui::{
|
||||
FocusHandle, Focusable, InteractiveElement, ParentElement, Render, Styled, Subscription,
|
||||
WeakEntity, Window, anchored, deferred, point,
|
||||
};
|
||||
use project::DisableAiSettings;
|
||||
use project::project_settings::DiagnosticSeverity;
|
||||
use search::{BufferSearchBar, buffer_search};
|
||||
use settings::{Settings, SettingsStore};
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
use std::any::{Any, TypeId};
|
||||
|
||||
use client::DisableAiSettings;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use feature_flags::{FeatureFlagAppExt as _, PredictEditsRateCompletionsFeatureFlag};
|
||||
use gpui::actions;
|
||||
use language::language_settings::{AllLanguageSettings, EditPredictionProvider};
|
||||
use project::DisableAiSettings;
|
||||
use settings::{Settings, SettingsStore, update_settings_file};
|
||||
use ui::App;
|
||||
use workspace::Workspace;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user