Detailed changes
@@ -30,6 +30,7 @@ jobs:
run_tests: ${{ steps.filter.outputs.run_tests }}
run_license: ${{ steps.filter.outputs.run_license }}
run_docs: ${{ steps.filter.outputs.run_docs }}
+ run_nix: ${{ steps.filter.outputs.run_nix }}
runs-on:
- ubuntu-latest
steps:
@@ -69,6 +70,12 @@ jobs:
else
echo "run_license=false" >> $GITHUB_OUTPUT
fi
+ NIX_REGEX='^(nix/|flake\.|Cargo\.|rust-toolchain.toml|\.cargo/config.toml)'
+ if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep "$NIX_REGEX") ]]; then
+ echo "run_nix=true" >> $GITHUB_OUTPUT
+ else
+ echo "run_nix=false" >> $GITHUB_OUTPUT
+ fi
migration_checks:
name: Check Postgres and Protobuf migrations, mergability
@@ -746,7 +753,10 @@ jobs:
nix-build:
name: Build with Nix
uses: ./.github/workflows/nix.yml
- if: github.repository_owner == 'zed-industries' && contains(github.event.pull_request.labels.*.name, 'run-nix')
+ needs: [job_spec]
+ if: github.repository_owner == 'zed-industries' &&
+ (contains(github.event.pull_request.labels.*.name, 'run-nix') ||
+ needs.job_spec.outputs.run_nix == 'true')
secrets: inherit
with:
flake-output: debug
@@ -115,7 +115,7 @@ dependencies = [
"rand 0.8.5",
"ref-cast",
"rope",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"settings",
@@ -144,7 +144,7 @@ dependencies = [
"gpui",
"language_model",
"paths",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"serde_json_lenient",
@@ -211,7 +211,7 @@ dependencies = [
"release_channel",
"rope",
"rules_library",
- "schemars 0.8.22",
+ "schemars",
"search",
"serde",
"serde_json",
@@ -250,7 +250,7 @@ dependencies = [
"futures 0.3.31",
"log",
"parking_lot",
- "schemars 1.0.1",
+ "schemars",
"serde",
"serde_json",
]
@@ -451,7 +451,7 @@ dependencies = [
"chrono",
"futures 0.3.31",
"http_client",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"strum 0.27.1",
@@ -778,7 +778,7 @@ dependencies = [
"regex",
"reqwest_client",
"rust-embed",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"settings",
@@ -1239,7 +1239,7 @@ dependencies = [
"log",
"paths",
"release_channel",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"settings",
@@ -1949,12 +1949,11 @@ dependencies = [
"aws-sdk-bedrockruntime",
"aws-smithy-types",
"futures 0.3.31",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"strum 0.27.1",
"thiserror 2.0.12",
- "tokio",
"workspace-hack",
]
@@ -2463,7 +2462,7 @@ dependencies = [
"log",
"postage",
"project",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_derive",
"settings",
@@ -2919,7 +2918,7 @@ dependencies = [
"release_channel",
"rpc",
"rustls-pki-types",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"settings",
@@ -3194,7 +3193,7 @@ dependencies = [
"release_channel",
"rich_text",
"rpc",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_derive",
"serde_json",
@@ -3383,7 +3382,7 @@ dependencies = [
"log",
"parking_lot",
"postage",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"smol",
@@ -4158,7 +4157,7 @@ dependencies = [
"parking_lot",
"paths",
"proto",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"settings",
@@ -4176,9 +4175,9 @@ dependencies = [
[[package]]
name = "dap-types"
version = "0.0.1"
-source = "git+https://github.com/zed-industries/dap-types?rev=b40956a7f4d1939da67429d941389ee306a3a308#b40956a7f4d1939da67429d941389ee306a3a308"
+source = "git+https://github.com/zed-industries/dap-types?rev=7f39295b441614ca9dbf44293e53c32f666897f9#7f39295b441614ca9dbf44293e53c32f666897f9"
dependencies = [
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
]
@@ -4191,6 +4190,8 @@ dependencies = [
"async-trait",
"collections",
"dap",
+ "dotenvy",
+ "fs",
"futures 0.3.31",
"gpui",
"json_dotpath",
@@ -4199,6 +4200,7 @@ dependencies = [
"paths",
"serde",
"serde_json",
+ "shlex",
"task",
"util",
"workspace-hack",
@@ -4352,6 +4354,7 @@ version = "0.1.0"
dependencies = [
"alacritty_terminal",
"anyhow",
+ "bitflags 2.9.0",
"client",
"collections",
"command_palette_hooks",
@@ -4404,7 +4407,7 @@ dependencies = [
"anyhow",
"futures 0.3.31",
"http_client",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"workspace-hack",
@@ -4717,12 +4720,6 @@ dependencies = [
"syn 2.0.101",
]
-[[package]]
-name = "dotenv"
-version = "0.15.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
-
[[package]]
name = "dotenvy"
version = "0.15.7"
@@ -4855,9 +4852,10 @@ dependencies = [
"pretty_assertions",
"project",
"rand 0.8.5",
+ "regex",
"release_channel",
"rpc",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"settings",
@@ -5155,7 +5153,7 @@ dependencies = [
"collections",
"debug_adapter_extension",
"dirs 4.0.0",
- "dotenv",
+ "dotenvy",
"env_logger 0.11.8",
"extension",
"fs",
@@ -5331,7 +5329,7 @@ dependencies = [
"release_channel",
"remote",
"reqwest_client",
- "schemars 0.8.22",
+ "schemars",
"semantic_version",
"serde",
"serde_json",
@@ -5532,7 +5530,7 @@ dependencies = [
"picker",
"pretty_assertions",
"project",
- "schemars 0.8.22",
+ "schemars",
"search",
"serde",
"serde_derive",
@@ -6193,7 +6191,7 @@ dependencies = [
"pretty_assertions",
"regex",
"rope",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"smol",
@@ -6235,7 +6233,7 @@ dependencies = [
"indoc",
"pretty_assertions",
"regex",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"settings",
@@ -6278,7 +6276,7 @@ dependencies = [
"postage",
"pretty_assertions",
"project",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_derive",
"serde_json",
@@ -7115,7 +7113,7 @@ dependencies = [
"menu",
"project",
"rope",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"settings",
@@ -7136,7 +7134,7 @@ dependencies = [
"anyhow",
"futures 0.3.31",
"http_client",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"strum 0.27.1",
@@ -7237,7 +7235,7 @@ dependencies = [
"reqwest_client",
"resvg",
"scap",
- "schemars 0.8.22",
+ "schemars",
"seahash",
"semantic_version",
"serde",
@@ -8145,7 +8143,7 @@ dependencies = [
"language",
"log",
"project",
- "schemars 0.8.22",
+ "schemars",
"serde",
"settings",
"theme",
@@ -8702,7 +8700,7 @@ dependencies = [
"editor",
"gpui",
"log",
- "schemars 0.8.22",
+ "schemars",
"serde",
"settings",
"shellexpand 2.1.2",
@@ -8888,6 +8886,7 @@ dependencies = [
"http_client",
"imara-diff",
"indoc",
+ "inventory",
"itertools 0.14.0",
"log",
"lsp",
@@ -8897,7 +8896,7 @@ dependencies = [
"rand 0.8.5",
"regex",
"rpc",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"settings",
@@ -8965,7 +8964,7 @@ dependencies = [
"log",
"parking_lot",
"proto",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"smol",
@@ -8986,8 +8985,10 @@ dependencies = [
"aws-credential-types",
"aws_http_client",
"bedrock",
+ "chrono",
"client",
"collections",
+ "component",
"copilot",
"credentials_provider",
"deepseek",
@@ -9011,7 +9012,7 @@ dependencies = [
"project",
"proto",
"release_channel",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"settings",
@@ -9058,6 +9059,7 @@ dependencies = [
"collections",
"copilot",
"editor",
+ "feature_flags",
"futures 0.3.31",
"gpui",
"itertools 0.14.0",
@@ -9109,7 +9111,7 @@ dependencies = [
"regex",
"rope",
"rust-embed",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"serde_json_lenient",
@@ -9500,7 +9502,7 @@ dependencies = [
"anyhow",
"futures 0.3.31",
"http_client",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"workspace-hack",
@@ -9606,7 +9608,7 @@ dependencies = [
"parking_lot",
"postage",
"release_channel",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"smol",
@@ -10065,7 +10067,7 @@ dependencies = [
"anyhow",
"futures 0.3.31",
"http_client",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"strum 0.27.1",
@@ -10848,7 +10850,7 @@ dependencies = [
"anyhow",
"futures 0.3.31",
"http_client",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"workspace-hack",
@@ -10919,7 +10921,7 @@ dependencies = [
"anyhow",
"futures 0.3.31",
"http_client",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"strum 0.27.1",
@@ -10933,7 +10935,7 @@ dependencies = [
"anyhow",
"futures 0.3.31",
"http_client",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"workspace-hack",
@@ -11109,7 +11111,7 @@ dependencies = [
"outline",
"pretty_assertions",
"project",
- "schemars 0.8.22",
+ "schemars",
"search",
"serde",
"serde_json",
@@ -11882,7 +11884,7 @@ dependencies = [
"env_logger 0.11.8",
"gpui",
"menu",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"ui",
@@ -12311,7 +12313,7 @@ dependencies = [
"release_channel",
"remote",
"rpc",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"settings",
@@ -12354,7 +12356,7 @@ dependencies = [
"menu",
"pretty_assertions",
"project",
- "schemars 0.8.22",
+ "schemars",
"search",
"serde",
"serde_derive",
@@ -13010,7 +13012,7 @@ dependencies = [
"project",
"release_channel",
"remote",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"settings",
@@ -13198,7 +13200,7 @@ dependencies = [
"prost 0.9.0",
"release_channel",
"rpc",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"shlex",
@@ -13314,7 +13316,7 @@ dependencies = [
"picker",
"project",
"runtimelib",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"settings",
@@ -14084,26 +14086,13 @@ dependencies = [
"anyhow",
"clap",
"env_logger 0.11.8",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"theme",
"workspace-hack",
]
-[[package]]
-name = "schemars"
-version = "0.8.22"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
-dependencies = [
- "dyn-clone",
- "indexmap",
- "schemars_derive 0.8.22",
- "serde",
- "serde_json",
-]
-
[[package]]
name = "schemars"
version = "1.0.1"
@@ -14112,24 +14101,13 @@ checksum = "fe8c9d1c68d67dd9f97ecbc6f932b60eb289c5dbddd8aa1405484a8fd2fcd984"
dependencies = [
"chrono",
"dyn-clone",
+ "indexmap",
"ref-cast",
- "schemars_derive 1.0.1",
+ "schemars_derive",
"serde",
"serde_json",
]
-[[package]]
-name = "schemars_derive"
-version = "0.8.22"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
-dependencies = [
- "proc-macro2",
- "quote",
- "serde_derive_internals",
- "syn 2.0.101",
-]
-
[[package]]
name = "schemars_derive"
version = "1.0.1"
@@ -14314,7 +14292,7 @@ dependencies = [
"language",
"menu",
"project",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"settings",
@@ -14615,31 +14593,40 @@ dependencies = [
"pretty_assertions",
"release_channel",
"rust-embed",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_derive",
"serde_json",
"serde_json_lenient",
"smallvec",
- "streaming-iterator",
"tree-sitter",
"tree-sitter-json",
"unindent",
"util",
"workspace-hack",
+ "zlog",
]
[[package]]
name = "settings_ui"
version = "0.1.0"
dependencies = [
+ "collections",
+ "command_palette",
"command_palette_hooks",
+ "component",
+ "db",
"editor",
"feature_flags",
"fs",
+ "fuzzy",
"gpui",
"log",
- "schemars 0.8.22",
+ "menu",
+ "paths",
+ "project",
+ "schemars",
+ "search",
"serde",
"settings",
"theme",
@@ -14943,7 +14930,7 @@ dependencies = [
"indoc",
"parking_lot",
"paths",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json_lenient",
"snippet",
@@ -15593,6 +15580,18 @@ version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb"
+[[package]]
+name = "svg_preview"
+version = "0.1.0"
+dependencies = [
+ "editor",
+ "file_icons",
+ "gpui",
+ "ui",
+ "workspace",
+ "workspace-hack",
+]
+
[[package]]
name = "svgtypes"
version = "0.15.3"
@@ -15779,7 +15778,7 @@ dependencies = [
"menu",
"picker",
"project",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"settings",
@@ -15860,7 +15859,7 @@ dependencies = [
"parking_lot",
"pretty_assertions",
"proto",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"serde_json_lenient",
@@ -15966,7 +15965,7 @@ dependencies = [
"rand 0.8.5",
"regex",
"release_channel",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_derive",
"settings",
@@ -16013,7 +16012,7 @@ dependencies = [
"project",
"rand 0.8.5",
"regex",
- "schemars 0.8.22",
+ "schemars",
"search",
"serde",
"serde_json",
@@ -16064,11 +16063,12 @@ dependencies = [
"futures 0.3.31",
"gpui",
"indexmap",
+ "inventory",
"log",
"palette",
"parking_lot",
"refineable",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_derive",
"serde_json",
@@ -16104,7 +16104,6 @@ dependencies = [
"indexmap",
"log",
"palette",
- "rust-embed",
"serde",
"serde_json",
"serde_json_lenient",
@@ -16355,7 +16354,7 @@ dependencies = [
"project",
"remote",
"rpc",
- "schemars 0.8.22",
+ "schemars",
"serde",
"settings",
"smallvec",
@@ -17496,7 +17495,7 @@ name = "vercel"
version = "0.1.0"
dependencies = [
"anyhow",
- "schemars 0.8.22",
+ "schemars",
"serde",
"strum 0.27.1",
"workspace-hack",
@@ -17543,7 +17542,7 @@ dependencies = [
"project_panel",
"regex",
"release_channel",
- "schemars 0.8.22",
+ "schemars",
"search",
"serde",
"serde_derive",
@@ -18396,7 +18395,7 @@ dependencies = [
"language",
"picker",
"project",
- "schemars 0.8.22",
+ "schemars",
"serde",
"settings",
"telemetry",
@@ -19438,7 +19437,7 @@ dependencies = [
"postage",
"project",
"remote",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"session",
@@ -19670,7 +19669,7 @@ dependencies = [
"pretty_assertions",
"rand 0.8.5",
"rpc",
- "schemars 0.8.22",
+ "schemars",
"serde",
"serde_json",
"settings",
@@ -20089,6 +20088,7 @@ dependencies = [
"snippet_provider",
"snippets_ui",
"supermaven",
+ "svg_preview",
"sysinfo",
"tab_switcher",
"task",
@@ -20132,7 +20132,7 @@ name = "zed_actions"
version = "0.1.0"
dependencies = [
"gpui",
- "schemars 0.8.22",
+ "schemars",
"serde",
"uuid",
"workspace-hack",
@@ -20181,9 +20181,9 @@ dependencies = [
[[package]]
name = "zed_llm_client"
-version = "0.8.4"
+version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "de7d9523255f4e00ee3d0918e5407bd252d798a4a8e71f6d37f23317a1588203"
+checksum = "c740e29260b8797ad252c202ea09a255b3cbc13f30faaf92fb6b2490336106e0"
dependencies = [
"anyhow",
"serde",
@@ -20459,7 +20459,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"gpui",
- "schemars 0.8.22",
+ "schemars",
"serde",
"settings",
"workspace-hack",
@@ -96,6 +96,7 @@ members = [
"crates/markdown_preview",
"crates/media",
"crates/menu",
+ "crates/svg_preview",
"crates/migrator",
"crates/mistral",
"crates/multi_buffer",
@@ -306,6 +307,7 @@ lmstudio = { path = "crates/lmstudio" }
lsp = { path = "crates/lsp" }
markdown = { path = "crates/markdown" }
markdown_preview = { path = "crates/markdown_preview" }
+svg_preview = { path = "crates/svg_preview" }
media = { path = "crates/media" }
menu = { path = "crates/menu" }
migrator = { path = "crates/migrator" }
@@ -445,12 +447,12 @@ core-video = { version = "0.4.3", features = ["metal"] }
cpal = "0.16"
criterion = { version = "0.5", features = ["html_reports"] }
ctor = "0.4.0"
-dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "b40956a7f4d1939da67429d941389ee306a3a308" }
+dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "7f39295b441614ca9dbf44293e53c32f666897f9" }
dashmap = "6.0"
derive_more = "0.99.17"
dirs = "4.0"
documented = "0.9.1"
-dotenv = "0.15.0"
+dotenvy = "0.15.0"
ec4rs = "1.1"
emojis = "0.6.1"
env_logger = "0.11"
@@ -541,7 +543,7 @@ rustc-hash = "2.1.0"
rustls = { version = "0.23.26" }
rustls-platform-verifier = "0.5.0"
scap = { git = "https://github.com/zed-industries/scap", rev = "08f0a01417505cc0990b9931a37e5120db92e0d0", default-features = false }
-schemars = { version = "0.8", features = ["impl_json_schema", "indexmap2"] }
+schemars = { version = "1.0", features = ["indexmap2"] }
semver = "1.0"
serde = { version = "1.0", features = ["derive", "rc"] }
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
@@ -626,7 +628,7 @@ wasmtime = { version = "29", default-features = false, features = [
wasmtime-wasi = "29"
which = "6.0.0"
workspace-hack = "0.1.0"
-zed_llm_client = "0.8.4"
+zed_llm_client = "0.8.5"
zstd = "0.11"
[workspace.dependencies.async-stripe]
@@ -1,6 +1,6 @@
# syntax = docker/dockerfile:1.2
-FROM rust:1.87-bookworm as builder
+FROM rust:1.88-bookworm as builder
WORKDIR app
COPY . .
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-arrow-down10-icon lucide-arrow-down-1-0"><path d="m3 16 4 4 4-4"/><path d="M7 20V4"/><path d="M17 10V4h-2"/><path d="M15 10h4"/><rect x="15" y="14" width="4" height="6" ry="2"/></svg>
@@ -0,0 +1,3 @@
+<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M6.75776 5.50003H8.49988C8.70769 5.50003 8.89518 5.62971 8.95455 5.82346C9.04049 6.01876 8.9858 6.23906 8.82956 6.37656L4.82971 9.87643C4.65315 10.0295 4.39488 10.042 4.20614 9.90455C4.01724 9.76705 3.94849 9.51706 4.04052 9.30301L5.24219 6.49999H3.48601C3.2918 6.49999 3.10524 6.37031 3.03197 6.17657C2.9587 5.98126 3.014 5.76096 3.1708 5.62346L7.17018 2.12375C7.34674 1.97001 7.60454 1.95829 7.7936 2.09547C7.98265 2.23275 8.0514 2.48218 7.95922 2.69695L6.75776 5.50003Z" fill="black"/>
+</svg>
@@ -0,0 +1,12 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M6 3L7 4" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9 4L10 3" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M6.002 6V5.51658C5.98992 5.32067 6.03266 5.12502 6.12762 4.94143C6.22259 4.75784 6.36781 4.59012 6.55453 4.44839C6.74125 4.30666 6.9656 4.19386 7.21403 4.1168C7.46246 4.03973 7.72983 4 8 4C8.27017 4 8.53754 4.03973 8.78597 4.1168C9.0344 4.19386 9.25875 4.30666 9.44547 4.44839C9.63219 4.59012 9.77741 4.75784 9.87238 4.94143C9.96734 5.12502 10.0101 5.32067 9.998 5.51658V6" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8 13C6.35 13 5 11.5462 5 9.76923V8.15385C5 7.58261 5.21071 7.03477 5.58579 6.63085C5.96086 6.22692 6.46957 6 7 6H9C9.53043 6 10.0391 6.22692 10.4142 6.63085C10.7893 7.03477 11 7.58261 11 8.15385V9.76923C11 11.5462 9.65 13 8 13Z" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5 6.16663C3.90652 6.06663 3 5.21663 3 4.16663" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5 9H3" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3 13C3 11.95 3.89474 11.05 5 11" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13 4C13 5.05 12.0857 5.9 11 6" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M13 9H11" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11 11C12.1053 11.05 13 11.95 13 13" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -0,0 +1,4 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M3.84265 10.7778C4.39206 11.6001 5.17295 12.241 6.08658 12.6194C7.00021 12.9978 8.00555 13.0969 8.97545 12.9039C9.94535 12.711 10.8363 12.2348 11.5355 11.5355C12.2348 10.8363 12.711 9.94535 12.9039 8.97545C13.0969 8.00555 12.9978 7.00021 12.6194 6.08658C12.241 5.17295 11.6001 4.39206 10.7778 3.84265C9.9556 3.29324 8.9889 3 8 3C6.60219 3.00526 5.26054 3.55068 4.25556 4.52222L3 5.77778" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M3 3V6H6" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -0,0 +1,4 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M8 13C10.7614 13 13 10.7614 13 8C13 5.23858 10.7614 3 8 3C5.23858 3 3 5.23858 3 8C3 10.7614 5.23858 13 8 13Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M5 5L11 11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-scroll-text-icon lucide-scroll-text"><path d="M15 12h-5"/><path d="M15 8h-5"/><path d="M19 17V5a2 2 0 0 0-2-2H4"/><path d="M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3"/></svg>
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-split-icon lucide-split"><path d="M16 3h5v5"/><path d="M8 3H3v5"/><path d="M12 22v-8.3a4 4 0 0 0-1.172-2.872L3 3"/><path d="m15 9 6-6"/></svg>
@@ -491,13 +491,27 @@
"ctrl-k r": "editor::RevealInFileManager",
"ctrl-k p": "editor::CopyPath",
"ctrl-\\": "pane::SplitRight",
- "ctrl-k v": "markdown::OpenPreviewToTheSide",
- "ctrl-shift-v": "markdown::OpenPreview",
"ctrl-alt-shift-c": "editor::DisplayCursorNames",
"alt-.": "editor::GoToHunk",
"alt-,": "editor::GoToPreviousHunk"
}
},
+ {
+ "context": "Editor && extension == md",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-k v": "markdown::OpenPreviewToTheSide",
+ "ctrl-shift-v": "markdown::OpenPreview"
+ }
+ },
+ {
+ "context": "Editor && extension == svg",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-k v": "svg::OpenPreviewToTheSide",
+ "ctrl-shift-v": "svg::OpenPreview"
+ }
+ },
{
"context": "Editor && mode == full",
"bindings": {
@@ -905,7 +919,9 @@
"context": "BreakpointList",
"bindings": {
"space": "debugger::ToggleEnableBreakpoint",
- "backspace": "debugger::UnsetBreakpoint"
+ "backspace": "debugger::UnsetBreakpoint",
+ "left": "debugger::PreviousBreakpointProperty",
+ "right": "debugger::NextBreakpointProperty"
}
},
{
@@ -1051,5 +1067,12 @@
"ctrl-tab": "pane::ActivateNextItem",
"ctrl-shift-tab": "pane::ActivatePreviousItem"
}
+ },
+ {
+ "context": "KeymapEditor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-f": "search::FocusSearch"
+ }
}
]
@@ -545,11 +545,25 @@
"cmd-k r": "editor::RevealInFileManager",
"cmd-k p": "editor::CopyPath",
"cmd-\\": "pane::SplitRight",
- "cmd-k v": "markdown::OpenPreviewToTheSide",
- "cmd-shift-v": "markdown::OpenPreview",
"ctrl-cmd-c": "editor::DisplayCursorNames"
}
},
+ {
+ "context": "Editor && extension == md",
+ "use_key_equivalents": true,
+ "bindings": {
+ "cmd-k v": "markdown::OpenPreviewToTheSide",
+ "cmd-shift-v": "markdown::OpenPreview"
+ }
+ },
+ {
+ "context": "Editor && extension == svg",
+ "use_key_equivalents": true,
+ "bindings": {
+ "cmd-k v": "svg::OpenPreviewToTheSide",
+ "cmd-shift-v": "svg::OpenPreview"
+ }
+ },
{
"context": "Editor && mode == full",
"use_key_equivalents": true,
@@ -966,7 +980,9 @@
"context": "BreakpointList",
"bindings": {
"space": "debugger::ToggleEnableBreakpoint",
- "backspace": "debugger::UnsetBreakpoint"
+ "backspace": "debugger::UnsetBreakpoint",
+ "left": "debugger::PreviousBreakpointProperty",
+ "right": "debugger::NextBreakpointProperty"
}
},
{
@@ -1151,5 +1167,12 @@
"ctrl-tab": "pane::ActivateNextItem",
"ctrl-shift-tab": "pane::ActivatePreviousItem"
}
+ },
+ {
+ "context": "KeymapEditor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "cmd-f": "search::FocusSearch"
+ }
}
]
@@ -59,7 +59,8 @@
"alt->": "editor::MoveToEnd", // end-of-buffer
"ctrl-l": "editor::ScrollCursorCenterTopBottom", // recenter-top-bottom
"ctrl-s": "buffer_search::Deploy", // isearch-forward
- "alt-^": "editor::JoinLines" // join-line
+ "alt-^": "editor::JoinLines", // join-line
+ "alt-q": "editor::Rewrap" // fill-paragraph
}
},
{
@@ -59,7 +59,8 @@
"alt->": "editor::MoveToEnd", // end-of-buffer
"ctrl-l": "editor::ScrollCursorCenterTopBottom", // recenter-top-bottom
"ctrl-s": "buffer_search::Deploy", // isearch-forward
- "alt-^": "editor::JoinLines" // join-line
+ "alt-^": "editor::JoinLines", // join-line
+ "alt-q": "editor::Rewrap" // fill-paragraph
}
},
{
@@ -210,7 +210,8 @@
"ctrl-w space": "editor::OpenExcerptsSplit",
"ctrl-w g space": "editor::OpenExcerptsSplit",
"ctrl-6": "pane::AlternateFile",
- "ctrl-^": "pane::AlternateFile"
+ "ctrl-^": "pane::AlternateFile",
+ ".": "vim::Repeat"
}
},
{
@@ -219,7 +220,6 @@
"ctrl-[": "editor::Cancel",
"escape": "editor::Cancel",
":": "command_palette::Toggle",
- ".": "vim::Repeat",
"c": "vim::PushChange",
"shift-c": "vim::ChangeToEndOfLine",
"d": "vim::PushDelete",
@@ -849,6 +849,25 @@
"shift-u": "git::UnstageAll"
}
},
+ {
+ "context": "Editor && mode == auto_height && VimControl",
+ "bindings": {
+ // TODO: Implement search
+ "/": null,
+ "?": null,
+ "#": null,
+ "*": null,
+ "n": null,
+ "shift-n": null
+ }
+ },
+ {
+ "context": "GitCommit > Editor && VimControl && vim_mode == normal",
+ "bindings": {
+ "ctrl-c": "menu::Cancel",
+ "escape": "menu::Cancel"
+ }
+ },
{
"context": "Editor && edit_prediction",
"bindings": {
@@ -860,14 +879,7 @@
{
"context": "MessageEditor > Editor && VimControl",
"bindings": {
- "enter": "agent::Chat",
- // TODO: Implement search
- "/": null,
- "?": null,
- "#": null,
- "*": null,
- "n": null,
- "shift-n": null
+ "enter": "agent::Chat"
}
},
{
@@ -96,16 +96,11 @@ impl AgentProfile {
fn is_enabled(settings: &AgentProfileSettings, source: ToolSource, name: String) -> bool {
match source {
ToolSource::Native => *settings.tools.get(name.as_str()).unwrap_or(&false),
- ToolSource::ContextServer { id } => {
- if settings.enable_all_context_servers {
- return true;
- }
-
- let Some(preset) = settings.context_servers.get(id.as_ref()) else {
- return false;
- };
- *preset.tools.get(name.as_str()).unwrap_or(&false)
- }
+ ToolSource::ContextServer { id } => settings
+ .context_servers
+ .get(id.as_ref())
+ .and_then(|preset| preset.tools.get(name.as_str()).copied())
+ .unwrap_or(settings.enable_all_context_servers),
}
}
}
@@ -23,11 +23,10 @@ use gpui::{
};
use language_model::{
ConfiguredModel, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
- LanguageModelId, LanguageModelKnownError, LanguageModelRegistry, LanguageModelRequest,
- LanguageModelRequestMessage, LanguageModelRequestTool, LanguageModelToolResult,
- LanguageModelToolResultContent, LanguageModelToolUseId, MessageContent,
- ModelRequestLimitReachedError, PaymentRequiredError, Role, SelectedModel, StopReason,
- TokenUsage,
+ LanguageModelId, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
+ LanguageModelRequestTool, LanguageModelToolResult, LanguageModelToolResultContent,
+ LanguageModelToolUseId, MessageContent, ModelRequestLimitReachedError, PaymentRequiredError,
+ Role, SelectedModel, StopReason, TokenUsage,
};
use postage::stream::Stream as _;
use project::{
@@ -1343,6 +1342,7 @@ impl Thread {
for segment in &message.segments {
match segment {
MessageSegment::Text(text) => {
+ let text = text.trim_end();
if !text.is_empty() {
request_message
.content
@@ -1419,7 +1419,7 @@ impl Thread {
}
request.tools = available_tools;
- request.mode = if model.supports_max_mode() {
+ request.mode = if model.supports_burn_mode() {
Some(self.completion_mode.into())
} else {
Some(CompletionMode::Normal.into())
@@ -1530,82 +1530,7 @@ impl Thread {
}
thread.update(cx, |thread, cx| {
- let event = match event {
- Ok(event) => event,
- Err(error) => {
- match error {
- LanguageModelCompletionError::RateLimitExceeded { retry_after } => {
- anyhow::bail!(LanguageModelKnownError::RateLimitExceeded { retry_after });
- }
- LanguageModelCompletionError::Overloaded => {
- anyhow::bail!(LanguageModelKnownError::Overloaded);
- }
- LanguageModelCompletionError::ApiInternalServerError =>{
- anyhow::bail!(LanguageModelKnownError::ApiInternalServerError);
- }
- LanguageModelCompletionError::PromptTooLarge { tokens } => {
- let tokens = tokens.unwrap_or_else(|| {
- // We didn't get an exact token count from the API, so fall back on our estimate.
- thread.total_token_usage()
- .map(|usage| usage.total)
- .unwrap_or(0)
- // We know the context window was exceeded in practice, so if our estimate was
- // lower than max tokens, the estimate was wrong; return that we exceeded by 1.
- .max(model.max_token_count().saturating_add(1))
- });
-
- anyhow::bail!(LanguageModelKnownError::ContextWindowLimitExceeded { tokens })
- }
- LanguageModelCompletionError::ApiReadResponseError(io_error) => {
- anyhow::bail!(LanguageModelKnownError::ReadResponseError(io_error));
- }
- LanguageModelCompletionError::UnknownResponseFormat(error) => {
- anyhow::bail!(LanguageModelKnownError::UnknownResponseFormat(error));
- }
- LanguageModelCompletionError::HttpResponseError { status, ref body } => {
- if let Some(known_error) = LanguageModelKnownError::from_http_response(status, body) {
- anyhow::bail!(known_error);
- } else {
- return Err(error.into());
- }
- }
- LanguageModelCompletionError::DeserializeResponse(error) => {
- anyhow::bail!(LanguageModelKnownError::DeserializeResponse(error));
- }
- LanguageModelCompletionError::BadInputJson {
- id,
- tool_name,
- raw_input: invalid_input_json,
- json_parse_error,
- } => {
- thread.receive_invalid_tool_json(
- id,
- tool_name,
- invalid_input_json,
- json_parse_error,
- window,
- cx,
- );
- return Ok(());
- }
- // These are all errors we can't automatically attempt to recover from (e.g. by retrying)
- err @ LanguageModelCompletionError::BadRequestFormat |
- err @ LanguageModelCompletionError::AuthenticationError |
- err @ LanguageModelCompletionError::PermissionError |
- err @ LanguageModelCompletionError::ApiEndpointNotFound |
- err @ LanguageModelCompletionError::SerializeRequest(_) |
- err @ LanguageModelCompletionError::BuildRequestBody(_) |
- err @ LanguageModelCompletionError::HttpSend(_) => {
- anyhow::bail!(err);
- }
- LanguageModelCompletionError::Other(error) => {
- return Err(error);
- }
- }
- }
- };
-
- match event {
+ match event? {
LanguageModelCompletionEvent::StartMessage { .. } => {
request_assistant_message_id =
Some(thread.insert_assistant_message(
@@ -1682,9 +1607,7 @@ impl Thread {
};
}
}
- LanguageModelCompletionEvent::RedactedThinking {
- data
- } => {
+ LanguageModelCompletionEvent::RedactedThinking { data } => {
thread.received_chunk();
if let Some(last_message) = thread.messages.last_mut() {
@@ -1733,6 +1656,21 @@ impl Thread {
});
}
}
+ LanguageModelCompletionEvent::ToolUseJsonParseError {
+ id,
+ tool_name,
+ raw_input: invalid_input_json,
+ json_parse_error,
+ } => {
+ thread.receive_invalid_tool_json(
+ id,
+ tool_name,
+ invalid_input_json,
+ json_parse_error,
+ window,
+ cx,
+ );
+ }
LanguageModelCompletionEvent::StatusUpdate(status_update) => {
if let Some(completion) = thread
.pending_completions
@@ -1740,23 +1678,34 @@ impl Thread {
.find(|completion| completion.id == pending_completion_id)
{
match status_update {
- CompletionRequestStatus::Queued {
- position,
- } => {
- completion.queue_state = QueueState::Queued { position };
+ CompletionRequestStatus::Queued { position } => {
+ completion.queue_state =
+ QueueState::Queued { position };
}
CompletionRequestStatus::Started => {
- completion.queue_state = QueueState::Started;
+ completion.queue_state = QueueState::Started;
}
CompletionRequestStatus::Failed {
- code, message, request_id
+ code,
+ message,
+ request_id: _,
+ retry_after,
} => {
- anyhow::bail!("completion request failed. request_id: {request_id}, code: {code}, message: {message}");
+ return Err(
+ LanguageModelCompletionError::from_cloud_failure(
+ model.upstream_provider_name(),
+ code,
+ message,
+ retry_after.map(Duration::from_secs_f64),
+ ),
+ );
}
- CompletionRequestStatus::UsageUpdated {
- amount, limit
- } => {
- thread.update_model_request_usage(amount as u32, limit, cx);
+ CompletionRequestStatus::UsageUpdated { amount, limit } => {
+ thread.update_model_request_usage(
+ amount as u32,
+ limit,
+ cx,
+ );
}
CompletionRequestStatus::ToolUseLimitReached => {
thread.tool_use_limit_reached = true;
@@ -1807,10 +1756,11 @@ impl Thread {
Ok(stop_reason) => {
match stop_reason {
StopReason::ToolUse => {
- let tool_uses = thread.use_pending_tools(window, model.clone(), cx);
+ let tool_uses =
+ thread.use_pending_tools(window, model.clone(), cx);
cx.emit(ThreadEvent::UsePendingTools { tool_uses });
}
- StopReason::EndTurn | StopReason::MaxTokens => {
+ StopReason::EndTurn | StopReason::MaxTokens => {
thread.project.update(cx, |project, cx| {
project.set_agent_location(None, cx);
});
@@ -1826,7 +1776,9 @@ impl Thread {
{
let mut messages_to_remove = Vec::new();
- for (ix, message) in thread.messages.iter().enumerate().rev() {
+ for (ix, message) in
+ thread.messages.iter().enumerate().rev()
+ {
messages_to_remove.push(message.id);
if message.role == Role::User {
@@ -1834,7 +1786,9 @@ impl Thread {
break;
}
- if let Some(prev_message) = thread.messages.get(ix - 1) {
+ if let Some(prev_message) =
+ thread.messages.get(ix - 1)
+ {
if prev_message.role == Role::Assistant {
break;
}
@@ -1849,14 +1803,16 @@ impl Thread {
cx.emit(ThreadEvent::ShowError(ThreadError::Message {
header: "Language model refusal".into(),
- message: "Model refused to generate content for safety reasons.".into(),
+ message:
+ "Model refused to generate content for safety reasons."
+ .into(),
}));
}
}
// We successfully completed, so cancel any remaining retries.
thread.retry_state = None;
- },
+ }
Err(error) => {
thread.project.update(cx, |project, cx| {
project.set_agent_location(None, cx);
@@ -1882,26 +1838,38 @@ impl Thread {
cx.emit(ThreadEvent::ShowError(
ThreadError::ModelRequestLimitReached { plan: error.plan },
));
- } else if let Some(known_error) =
- error.downcast_ref::<LanguageModelKnownError>()
+ } else if let Some(completion_error) =
+ error.downcast_ref::<LanguageModelCompletionError>()
{
- match known_error {
- LanguageModelKnownError::ContextWindowLimitExceeded { tokens } => {
+ use LanguageModelCompletionError::*;
+ match &completion_error {
+ PromptTooLarge { tokens, .. } => {
+ let tokens = tokens.unwrap_or_else(|| {
+ // We didn't get an exact token count from the API, so fall back on our estimate.
+ thread
+ .total_token_usage()
+ .map(|usage| usage.total)
+ .unwrap_or(0)
+ // We know the context window was exceeded in practice, so if our estimate was
+ // lower than max tokens, the estimate was wrong; return that we exceeded by 1.
+ .max(model.max_token_count().saturating_add(1))
+ });
thread.exceeded_window_error = Some(ExceededWindowError {
model_id: model.id(),
- token_count: *tokens,
+ token_count: tokens,
});
cx.notify();
}
- LanguageModelKnownError::RateLimitExceeded { retry_after } => {
- let provider_name = model.provider_name();
- let error_message = format!(
- "{}'s API rate limit exceeded",
- provider_name.0.as_ref()
- );
-
+ RateLimitExceeded {
+ retry_after: Some(retry_after),
+ ..
+ }
+ | ServerOverloaded {
+ retry_after: Some(retry_after),
+ ..
+ } => {
thread.handle_rate_limit_error(
- &error_message,
+ &completion_error,
*retry_after,
model.clone(),
intent,
@@ -1910,15 +1878,9 @@ impl Thread {
);
retry_scheduled = true;
}
- LanguageModelKnownError::Overloaded => {
- let provider_name = model.provider_name();
- let error_message = format!(
- "{}'s API servers are overloaded right now",
- provider_name.0.as_ref()
- );
-
+ RateLimitExceeded { .. } | ServerOverloaded { .. } => {
retry_scheduled = thread.handle_retryable_error(
- &error_message,
+ &completion_error,
model.clone(),
intent,
window,
@@ -1928,15 +1890,11 @@ impl Thread {
emit_generic_error(error, cx);
}
}
- LanguageModelKnownError::ApiInternalServerError => {
- let provider_name = model.provider_name();
- let error_message = format!(
- "{}'s API server reported an internal server error",
- provider_name.0.as_ref()
- );
-
+ ApiInternalServerError { .. }
+ | ApiReadResponseError { .. }
+ | HttpSend { .. } => {
retry_scheduled = thread.handle_retryable_error(
- &error_message,
+ &completion_error,
model.clone(),
intent,
window,
@@ -1946,12 +1904,16 @@ impl Thread {
emit_generic_error(error, cx);
}
}
- LanguageModelKnownError::ReadResponseError(_) |
- LanguageModelKnownError::DeserializeResponse(_) |
- LanguageModelKnownError::UnknownResponseFormat(_) => {
- // In the future we will attempt to re-roll response, but only once
- emit_generic_error(error, cx);
- }
+ NoApiKey { .. }
+ | HttpResponseError { .. }
+ | BadRequestFormat { .. }
+ | AuthenticationError { .. }
+ | PermissionError { .. }
+ | ApiEndpointNotFound { .. }
+ | SerializeRequest { .. }
+ | BuildRequestBody { .. }
+ | DeserializeResponse { .. }
+ | Other { .. } => emit_generic_error(error, cx),
}
} else {
emit_generic_error(error, cx);
@@ -2083,7 +2045,7 @@ impl Thread {
fn handle_rate_limit_error(
&mut self,
- error_message: &str,
+ error: &LanguageModelCompletionError,
retry_after: Duration,
model: Arc<dyn LanguageModel>,
intent: CompletionIntent,
@@ -2091,9 +2053,10 @@ impl Thread {
cx: &mut Context<Self>,
) {
// For rate limit errors, we only retry once with the specified duration
- let retry_message = format!(
- "{error_message}. Retrying in {} secondsβ¦",
- retry_after.as_secs()
+ let retry_message = format!("{error}. Retrying in {} secondsβ¦", retry_after.as_secs());
+ log::warn!(
+ "Retrying completion request in {} seconds: {error:?}",
+ retry_after.as_secs(),
);
// Add a UI-only message instead of a regular message
@@ -2126,18 +2089,18 @@ impl Thread {
fn handle_retryable_error(
&mut self,
- error_message: &str,
+ error: &LanguageModelCompletionError,
model: Arc<dyn LanguageModel>,
intent: CompletionIntent,
window: Option<AnyWindowHandle>,
cx: &mut Context<Self>,
) -> bool {
- self.handle_retryable_error_with_delay(error_message, None, model, intent, window, cx)
+ self.handle_retryable_error_with_delay(error, None, model, intent, window, cx)
}
fn handle_retryable_error_with_delay(
&mut self,
- error_message: &str,
+ error: &LanguageModelCompletionError,
custom_delay: Option<Duration>,
model: Arc<dyn LanguageModel>,
intent: CompletionIntent,
@@ -2167,8 +2130,12 @@ impl Thread {
// Add a transient message to inform the user
let delay_secs = delay.as_secs();
let retry_message = format!(
- "{}. Retrying (attempt {} of {}) in {} seconds...",
- error_message, attempt, max_attempts, delay_secs
+ "{error}. Retrying (attempt {attempt} of {max_attempts}) \
+ in {delay_secs} seconds..."
+ );
+ log::warn!(
+ "Retrying completion request (attempt {attempt} of {max_attempts}) \
+ in {delay_secs} seconds: {error:?}",
);
// Add a UI-only message instead of a regular message
@@ -4138,9 +4105,15 @@ fn main() {{
>,
> {
let error = match self.error_type {
- TestError::Overloaded => LanguageModelCompletionError::Overloaded,
+ TestError::Overloaded => LanguageModelCompletionError::ServerOverloaded {
+ provider: self.provider_name(),
+ retry_after: None,
+ },
TestError::InternalServerError => {
- LanguageModelCompletionError::ApiInternalServerError
+ LanguageModelCompletionError::ApiInternalServerError {
+ provider: self.provider_name(),
+ message: "I'm a teapot orbiting the sun".to_string(),
+ }
}
};
async move {
@@ -4648,9 +4621,13 @@ fn main() {{
> {
if !*self.failed_once.lock() {
*self.failed_once.lock() = true;
+ let provider = self.provider_name();
// Return error on first attempt
let stream = futures::stream::once(async move {
- Err(LanguageModelCompletionError::Overloaded)
+ Err(LanguageModelCompletionError::ServerOverloaded {
+ provider,
+ retry_after: None,
+ })
});
async move { Ok(stream.boxed()) }.boxed()
} else {
@@ -4813,9 +4790,13 @@ fn main() {{
> {
if !*self.failed_once.lock() {
*self.failed_once.lock() = true;
+ let provider = self.provider_name();
// Return error on first attempt
let stream = futures::stream::once(async move {
- Err(LanguageModelCompletionError::Overloaded)
+ Err(LanguageModelCompletionError::ServerOverloaded {
+ provider,
+ retry_after: None,
+ })
});
async move { Ok(stream.boxed()) }.boxed()
} else {
@@ -4968,10 +4949,12 @@ fn main() {{
LanguageModelCompletionError,
>,
> {
+ let provider = self.provider_name();
async move {
let stream = futures::stream::once(async move {
Err(LanguageModelCompletionError::RateLimitExceeded {
- retry_after: Duration::from_secs(TEST_RATE_LIMIT_RETRY_SECS),
+ provider,
+ retry_after: Some(Duration::from_secs(TEST_RATE_LIMIT_RETRY_SECS)),
})
});
Ok(stream.boxed())
@@ -6,9 +6,10 @@ use anyhow::{Result, bail};
use collections::IndexMap;
use gpui::{App, Pixels, SharedString};
use language_model::LanguageModel;
-use schemars::{JsonSchema, schema::Schema};
+use schemars::{JsonSchema, json_schema};
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
+use std::borrow::Cow;
pub use crate::agent_profile::*;
@@ -49,7 +50,7 @@ pub struct AgentSettings {
pub dock: AgentDockPosition,
pub default_width: Pixels,
pub default_height: Pixels,
- pub default_model: LanguageModelSelection,
+ pub default_model: Option<LanguageModelSelection>,
pub inline_assistant_model: Option<LanguageModelSelection>,
pub commit_message_model: Option<LanguageModelSelection>,
pub thread_summary_model: Option<LanguageModelSelection>,
@@ -211,7 +212,6 @@ impl AgentSettingsContent {
}
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)]
-#[schemars(deny_unknown_fields)]
pub struct AgentSettingsContent {
/// Whether the Agent is enabled.
///
@@ -321,29 +321,27 @@ pub struct LanguageModelSelection {
pub struct LanguageModelProviderSetting(pub String);
impl JsonSchema for LanguageModelProviderSetting {
- fn schema_name() -> String {
+ fn schema_name() -> Cow<'static, str> {
"LanguageModelProviderSetting".into()
}
- fn json_schema(_: &mut schemars::r#gen::SchemaGenerator) -> Schema {
- schemars::schema::SchemaObject {
- enum_values: Some(vec![
- "anthropic".into(),
- "amazon-bedrock".into(),
- "google".into(),
- "lmstudio".into(),
- "ollama".into(),
- "openai".into(),
- "zed.dev".into(),
- "copilot_chat".into(),
- "deepseek".into(),
- "openrouter".into(),
- "mistral".into(),
- "vercel".into(),
- ]),
- ..Default::default()
- }
- .into()
+ fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
+ json_schema!({
+ "enum": [
+ "anthropic",
+ "amazon-bedrock",
+ "google",
+ "lmstudio",
+ "ollama",
+ "openai",
+ "zed.dev",
+ "copilot_chat",
+ "deepseek",
+ "openrouter",
+ "mistral",
+ "vercel"
+ ]
+ })
}
}
@@ -359,15 +357,6 @@ impl From<&str> for LanguageModelProviderSetting {
}
}
-impl Default for LanguageModelSelection {
- fn default() -> Self {
- Self {
- provider: LanguageModelProviderSetting("openai".to_string()),
- model: "gpt-4".to_string(),
- }
- }
-}
-
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
pub struct AgentProfileContent {
pub name: Arc<str>,
@@ -411,7 +400,10 @@ impl Settings for AgentSettings {
&mut settings.default_height,
value.default_height.map(Into::into),
);
- merge(&mut settings.default_model, value.default_model.clone());
+ settings.default_model = value
+ .default_model
+ .clone()
+ .or(settings.default_model.take());
settings.inline_assistant_model = value
.inline_assistant_model
.clone()
@@ -19,7 +19,7 @@ use audio::{Audio, Sound};
use collections::{HashMap, HashSet};
use editor::actions::{MoveUp, Paste};
use editor::scroll::Autoscroll;
-use editor::{Editor, EditorElement, EditorEvent, EditorStyle, MultiBuffer};
+use editor::{Editor, EditorElement, EditorEvent, EditorStyle, MultiBuffer, SelectionEffects};
use gpui::{
AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent, ClipboardEntry,
ClipboardItem, DefiniteLength, EdgesRefinement, Empty, Entity, EventEmitter, Focusable, Hsla,
@@ -47,8 +47,8 @@ use std::time::Duration;
use text::ToPoint;
use theme::ThemeSettings;
use ui::{
- Disclosure, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState, TextSize, Tooltip,
- prelude::*,
+ Banner, Disclosure, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState, TextSize,
+ Tooltip, prelude::*,
};
use util::ResultExt as _;
use util::markdown::MarkdownCodeBlock;
@@ -58,6 +58,7 @@ use zed_llm_client::CompletionIntent;
const CODEBLOCK_CONTAINER_GROUP: &str = "codeblock_container";
const EDIT_PREVIOUS_MESSAGE_MIN_LINES: usize = 1;
+const RESPONSE_PADDING_X: Pixels = px(19.);
pub struct ActiveThread {
context_store: Entity<ContextStore>,
@@ -204,7 +205,7 @@ pub(crate) fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle
MarkdownStyle {
base_text_style: text_style.clone(),
syntax: cx.theme().syntax().clone(),
- selection_background_color: cx.theme().players().local().selection,
+ selection_background_color: cx.theme().colors().element_selection_background,
code_block_overflow_x_scroll: true,
table_overflow_x_scroll: true,
heading_level_styles: Some(HeadingLevelStyles {
@@ -301,7 +302,7 @@ fn tool_use_markdown_style(window: &Window, cx: &mut App) -> MarkdownStyle {
MarkdownStyle {
base_text_style: text_style,
syntax: cx.theme().syntax().clone(),
- selection_background_color: cx.theme().players().local().selection,
+ selection_background_color: cx.theme().colors().element_selection_background,
code_block_overflow_x_scroll: false,
code_block: StyleRefinement {
margin: EdgesRefinement::default(),
@@ -689,9 +690,12 @@ fn open_markdown_link(
})
.context("Could not find matching symbol")?;
- editor.change_selections(Some(Autoscroll::center()), window, cx, |s| {
- s.select_anchor_ranges([symbol_range.start..symbol_range.start])
- });
+ editor.change_selections(
+ SelectionEffects::scroll(Autoscroll::center()),
+ window,
+ cx,
+ |s| s.select_anchor_ranges([symbol_range.start..symbol_range.start]),
+ );
anyhow::Ok(())
})
})
@@ -708,10 +712,15 @@ fn open_markdown_link(
.downcast::<Editor>()
.context("Item is not an editor")?;
active_editor.update_in(cx, |editor, window, cx| {
- editor.change_selections(Some(Autoscroll::center()), window, cx, |s| {
- s.select_ranges([Point::new(line_range.start as u32, 0)
- ..Point::new(line_range.start as u32, 0)])
- });
+ editor.change_selections(
+ SelectionEffects::scroll(Autoscroll::center()),
+ window,
+ cx,
+ |s| {
+ s.select_ranges([Point::new(line_range.start as u32, 0)
+ ..Point::new(line_range.start as u32, 0)])
+ },
+ );
anyhow::Ok(())
})
})
@@ -1866,9 +1875,6 @@ impl ActiveThread {
this.scroll_to_top(cx);
}));
- // For all items that should be aligned with the LLM's response.
- const RESPONSE_PADDING_X: Pixels = px(19.);
-
let show_feedback = thread.is_turn_end(ix);
let feedback_container = h_flex()
.group("feedback_container")
@@ -2529,34 +2535,18 @@ impl ActiveThread {
ix: usize,
cx: &mut Context<Self>,
) -> Stateful<Div> {
- let colors = cx.theme().colors();
- div().id(("message-container", ix)).py_1().px_2().child(
- v_flex()
- .w_full()
- .bg(colors.editor_background)
- .rounded_sm()
- .child(
- h_flex()
- .w_full()
- .p_2()
- .gap_2()
- .child(
- div().flex_none().child(
- Icon::new(IconName::Warning)
- .size(IconSize::Small)
- .color(Color::Warning),
- ),
- )
- .child(
- v_flex()
- .flex_1()
- .min_w_0()
- .text_size(TextSize::Small.rems(cx))
- .text_color(cx.theme().colors().text_muted)
- .children(message_content),
- ),
- ),
- )
+ let message = div()
+ .flex_1()
+ .min_w_0()
+ .text_size(TextSize::XSmall.rems(cx))
+ .text_color(cx.theme().colors().text_muted)
+ .children(message_content);
+
+ div()
+ .id(("message-container", ix))
+ .py_1()
+ .px_2p5()
+ .child(Banner::new().severity(ui::Severity::Warning).child(message))
}
fn render_message_thinking_segment(
@@ -16,7 +16,9 @@ use gpui::{
Focusable, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage,
};
use language::LanguageRegistry;
-use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
+use language_model::{
+ LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
+};
use notifications::status_toast::{StatusToast, ToastIcon};
use project::{
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
@@ -86,6 +88,14 @@ impl AgentConfiguration {
let scroll_handle = ScrollHandle::new();
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
+ let mut expanded_provider_configurations = HashMap::default();
+ if LanguageModelRegistry::read_global(cx)
+ .provider(&ZED_CLOUD_PROVIDER_ID)
+ .map_or(false, |cloud_provider| cloud_provider.must_accept_terms(cx))
+ {
+ expanded_provider_configurations.insert(ZED_CLOUD_PROVIDER_ID, true);
+ }
+
let mut this = Self {
fs,
language_registry,
@@ -94,7 +104,7 @@ impl AgentConfiguration {
configuration_views_by_provider: HashMap::default(),
context_server_store,
expanded_context_server_tools: HashMap::default(),
- expanded_provider_configurations: HashMap::default(),
+ expanded_provider_configurations,
tools,
_registry_subscription: registry_subscription,
scroll_handle,
@@ -180,7 +180,7 @@ impl ConfigurationSource {
}
fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand)>) -> String {
- let (name, path, args, env) = match existing {
+ let (name, command, args, env) = match existing {
Some((id, cmd)) => {
let args = serde_json::to_string(&cmd.args).unwrap();
let env = serde_json::to_string(&cmd.env.unwrap_or_default()).unwrap();
@@ -198,14 +198,12 @@ fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand)
r#"{{
/// The name of your MCP server
"{name}": {{
- "command": {{
- /// The path to the executable
- "path": "{path}",
- /// The arguments to pass to the executable
- "args": {args},
- /// The environment variables to set for the executable
- "env": {env}
- }}
+ /// The command which runs the MCP server
+ "command": "{command}",
+ /// The arguments to pass to the MCP server
+ "args": {args},
+ /// The environment variables to set
+ "env": {env}
}}
}}"#
)
@@ -439,8 +437,7 @@ fn parse_input(text: &str) -> Result<(ContextServerId, ContextServerCommand)> {
let object = value.as_object().context("Expected object")?;
anyhow::ensure!(object.len() == 1, "Expected exactly one key-value pair");
let (context_server_name, value) = object.into_iter().next().unwrap();
- let command = value.get("command").context("Expected command")?;
- let command: ContextServerCommand = serde_json::from_value(command.clone())?;
+ let command: ContextServerCommand = serde_json::from_value(value.clone())?;
Ok((ContextServerId(context_server_name.clone().into()), command))
}
@@ -748,7 +745,7 @@ pub(crate) fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle
MarkdownStyle {
base_text_style: text_style.clone(),
- selection_background_color: cx.theme().players().local().selection,
+ selection_background_color: colors.element_selection_background,
link: TextStyleRefinement {
background_color: Some(colors.editor_foreground.opacity(0.025)),
underline: Some(UnderlineStyle {
@@ -5,7 +5,8 @@ use anyhow::Result;
use buffer_diff::DiffHunkStatus;
use collections::{HashMap, HashSet};
use editor::{
- Direction, Editor, EditorEvent, EditorSettings, MultiBuffer, MultiBufferSnapshot, ToPoint,
+ Direction, Editor, EditorEvent, EditorSettings, MultiBuffer, MultiBufferSnapshot,
+ SelectionEffects, ToPoint,
actions::{GoToHunk, GoToPreviousHunk},
scroll::Autoscroll,
};
@@ -171,15 +172,9 @@ impl AgentDiffPane {
if let Some(first_hunk) = first_hunk {
let first_hunk_start = first_hunk.multi_buffer_range().start;
- editor.change_selections(
- Some(Autoscroll::fit()),
- window,
- cx,
- |selections| {
- selections
- .select_anchor_ranges([first_hunk_start..first_hunk_start]);
- },
- )
+ editor.change_selections(Default::default(), window, cx, |selections| {
+ selections.select_anchor_ranges([first_hunk_start..first_hunk_start]);
+ })
}
}
@@ -242,7 +237,7 @@ impl AgentDiffPane {
if let Some(first_hunk) = first_hunk {
let first_hunk_start = first_hunk.multi_buffer_range().start;
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
+ editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_anchor_ranges([first_hunk_start..first_hunk_start]);
})
}
@@ -416,7 +411,7 @@ fn update_editor_selection(
};
if let Some(target_hunk) = target_hunk {
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
+ editor.change_selections(Default::default(), window, cx, |selections| {
let next_hunk_start = target_hunk.multi_buffer_range().start;
selections.select_anchor_ranges([next_hunk_start..next_hunk_start]);
})
@@ -1544,7 +1539,7 @@ impl AgentDiff {
let first_hunk_start = first_hunk.multi_buffer_range().start;
editor.change_selections(
- Some(Autoscroll::center()),
+ SelectionEffects::scroll(Autoscroll::center()),
window,
cx,
|selections| {
@@ -1868,7 +1863,7 @@ mod tests {
// Rejecting a hunk also moves the cursor to the next hunk, possibly cycling if it's at the end.
editor.update_in(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |selections| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
selections.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
});
});
@@ -2124,7 +2119,7 @@ mod tests {
// Rejecting a hunk also moves the cursor to the next hunk, possibly cycling if it's at the end.
editor1.update_in(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |selections| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
selections.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
});
});
@@ -11,7 +11,7 @@ use language_model::{ConfiguredModel, LanguageModelRegistry};
use picker::popover_menu::PickerPopoverMenu;
use settings::update_settings_file;
use std::sync::Arc;
-use ui::{PopoverMenuHandle, Tooltip, prelude::*};
+use ui::{ButtonLike, PopoverMenuHandle, Tooltip, prelude::*};
pub struct AgentModelSelector {
selector: Entity<LanguageModelSelector>,
@@ -94,20 +94,35 @@ impl Render for AgentModelSelector {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let model = self.selector.read(cx).delegate.active_model(cx);
let model_name = model
+ .as_ref()
.map(|model| model.model.name().0)
.unwrap_or_else(|| SharedString::from("No model selected"));
+ let provider_icon = model
+ .as_ref()
+ .map(|model| model.provider.icon())
+ .unwrap_or_else(|| IconName::Ai);
let focus_handle = self.focus_handle.clone();
PickerPopoverMenu::new(
self.selector.clone(),
- Button::new("active-model", model_name)
- .label_size(LabelSize::Small)
- .color(Color::Muted)
- .icon(IconName::ChevronDown)
- .icon_size(IconSize::XSmall)
- .icon_position(IconPosition::End)
- .icon_color(Color::Muted),
+ ButtonLike::new("active-model")
+ .child(
+ Icon::new(provider_icon)
+ .color(Color::Muted)
+ .size(IconSize::XSmall),
+ )
+ .child(
+ Label::new(model_name)
+ .color(Color::Muted)
+ .size(LabelSize::Small)
+ .ml_0p5(),
+ )
+ .child(
+ Icon::new(IconName::ChevronDown)
+ .color(Color::Muted)
+ .size(IconSize::XSmall),
+ ),
move |window, cx| {
Tooltip::for_action_in(
"Change Model",
@@ -43,7 +43,7 @@ use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
use fs::Fs;
use gpui::{
Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
- Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, FontWeight,
+ Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, Hsla,
KeyContext, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, linear_color_stop,
linear_gradient, prelude::*, pulsating_between,
};
@@ -61,7 +61,7 @@ use theme::ThemeSettings;
use time::UtcOffset;
use ui::utils::WithRemSize;
use ui::{
- Banner, CheckboxWithLabel, ContextMenu, ElevationIndex, KeyBinding, PopoverMenu,
+ Banner, Callout, CheckboxWithLabel, ContextMenu, ElevationIndex, KeyBinding, PopoverMenu,
PopoverMenuHandle, ProgressBar, Tab, Tooltip, Vector, VectorName, prelude::*,
};
use util::ResultExt as _;
@@ -2124,9 +2124,7 @@ impl AgentPanel {
.thread()
.read(cx)
.configured_model()
- .map_or(false, |model| {
- model.provider.id().0 == ZED_CLOUD_PROVIDER_ID
- });
+ .map_or(false, |model| model.provider.id() == ZED_CLOUD_PROVIDER_ID);
if !is_using_zed_provider {
return false;
@@ -2703,7 +2701,7 @@ impl AgentPanel {
Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
parent.child(Banner::new().severity(ui::Severity::Warning).child(
h_flex().w_full().children(provider.render_accept_terms(
- LanguageModelProviderTosView::ThreadtEmptyState,
+ LanguageModelProviderTosView::ThreadEmptyState,
cx,
)),
))
@@ -2763,7 +2761,7 @@ impl AgentPanel {
this.continue_conversation(window, cx);
})),
)
- .when(model.supports_max_mode(), |this| {
+ .when(model.supports_burn_mode(), |this| {
this.child(
Button::new("continue-burn-mode", "Continue with Burn Mode")
.style(ButtonStyle::Filled)
@@ -2798,58 +2796,90 @@ impl AgentPanel {
Some(div().px_2().pb_2().child(banner).into_any_element())
}
+ fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
+ let message = message.into();
+
+ IconButton::new("copy", IconName::Copy)
+ .icon_size(IconSize::Small)
+ .icon_color(Color::Muted)
+ .tooltip(Tooltip::text("Copy Error Message"))
+ .on_click(move |_, _, cx| {
+ cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
+ })
+ }
+
+ fn dismiss_error_button(
+ &self,
+ thread: &Entity<ActiveThread>,
+ cx: &mut Context<Self>,
+ ) -> impl IntoElement {
+ IconButton::new("dismiss", IconName::Close)
+ .icon_size(IconSize::Small)
+ .icon_color(Color::Muted)
+ .tooltip(Tooltip::text("Dismiss Error"))
+ .on_click(cx.listener({
+ let thread = thread.clone();
+ move |_, _, _, cx| {
+ thread.update(cx, |this, _cx| {
+ this.clear_last_error();
+ });
+
+ cx.notify();
+ }
+ }))
+ }
+
+ fn upgrade_button(
+ &self,
+ thread: &Entity<ActiveThread>,
+ cx: &mut Context<Self>,
+ ) -> impl IntoElement {
+ Button::new("upgrade", "Upgrade")
+ .label_size(LabelSize::Small)
+ .style(ButtonStyle::Tinted(ui::TintColor::Accent))
+ .on_click(cx.listener({
+ let thread = thread.clone();
+ move |_, _, _, cx| {
+ thread.update(cx, |this, _cx| {
+ this.clear_last_error();
+ });
+
+ cx.open_url(&zed_urls::account_url(cx));
+ cx.notify();
+ }
+ }))
+ }
+
+ fn error_callout_bg(&self, cx: &Context<Self>) -> Hsla {
+ cx.theme().status().error.opacity(0.08)
+ }
+
fn render_payment_required_error(
&self,
thread: &Entity<ActiveThread>,
cx: &mut Context<Self>,
) -> AnyElement {
- const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
-
- v_flex()
- .gap_0p5()
- .child(
- h_flex()
- .gap_1p5()
- .items_center()
- .child(Icon::new(IconName::XCircle).color(Color::Error))
- .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
- )
- .child(
- div()
- .id("error-message")
- .max_h_24()
- .overflow_y_scroll()
- .child(Label::new(ERROR_MESSAGE)),
- )
- .child(
- h_flex()
- .justify_end()
- .mt_1()
- .gap_1()
- .child(self.create_copy_button(ERROR_MESSAGE))
- .child(Button::new("subscribe", "Subscribe").on_click(cx.listener({
- let thread = thread.clone();
- move |_, _, _, cx| {
- thread.update(cx, |this, _cx| {
- this.clear_last_error();
- });
+ const ERROR_MESSAGE: &str =
+ "You reached your free usage limit. Upgrade to Zed Pro for more prompts.";
- cx.open_url(&zed_urls::account_url(cx));
- cx.notify();
- }
- })))
- .child(Button::new("dismiss", "Dismiss").on_click(cx.listener({
- let thread = thread.clone();
- move |_, _, _, cx| {
- thread.update(cx, |this, _cx| {
- this.clear_last_error();
- });
+ let icon = Icon::new(IconName::XCircle)
+ .size(IconSize::Small)
+ .color(Color::Error);
- cx.notify();
- }
- }))),
+ div()
+ .border_t_1()
+ .border_color(cx.theme().colors().border)
+ .child(
+ Callout::new()
+ .icon(icon)
+ .title("Free Usage Exceeded")
+ .description(ERROR_MESSAGE)
+ .tertiary_action(self.upgrade_button(thread, cx))
+ .secondary_action(self.create_copy_button(ERROR_MESSAGE))
+ .primary_action(self.dismiss_error_button(thread, cx))
+ .bg_color(self.error_callout_bg(cx)),
)
- .into_any()
+ .into_any_element()
}
fn render_model_request_limit_reached_error(
@@ -2859,67 +2889,28 @@ impl AgentPanel {
cx: &mut Context<Self>,
) -> AnyElement {
let error_message = match plan {
- Plan::ZedPro => {
- "Model request limit reached. Upgrade to usage-based billing for more requests."
- }
- Plan::ZedProTrial => {
- "Model request limit reached. Upgrade to Zed Pro for more requests."
- }
- Plan::Free => "Model request limit reached. Upgrade to Zed Pro for more requests.",
- };
- let call_to_action = match plan {
- Plan::ZedPro => "Upgrade to usage-based billing",
- Plan::ZedProTrial => "Upgrade to Zed Pro",
- Plan::Free => "Upgrade to Zed Pro",
+ Plan::ZedPro => "Upgrade to usage-based billing for more prompts.",
+ Plan::ZedProTrial | Plan::Free => "Upgrade to Zed Pro for more prompts.",
};
- v_flex()
- .gap_0p5()
- .child(
- h_flex()
- .gap_1p5()
- .items_center()
- .child(Icon::new(IconName::XCircle).color(Color::Error))
- .child(Label::new("Model Request Limit Reached").weight(FontWeight::MEDIUM)),
- )
- .child(
- div()
- .id("error-message")
- .max_h_24()
- .overflow_y_scroll()
- .child(Label::new(error_message)),
- )
- .child(
- h_flex()
- .justify_end()
- .mt_1()
- .gap_1()
- .child(self.create_copy_button(error_message))
- .child(
- Button::new("subscribe", call_to_action).on_click(cx.listener({
- let thread = thread.clone();
- move |_, _, _, cx| {
- thread.update(cx, |this, _cx| {
- this.clear_last_error();
- });
-
- cx.open_url(&zed_urls::account_url(cx));
- cx.notify();
- }
- })),
- )
- .child(Button::new("dismiss", "Dismiss").on_click(cx.listener({
- let thread = thread.clone();
- move |_, _, _, cx| {
- thread.update(cx, |this, _cx| {
- this.clear_last_error();
- });
+ let icon = Icon::new(IconName::XCircle)
+ .size(IconSize::Small)
+ .color(Color::Error);
- cx.notify();
- }
- }))),
+ div()
+ .border_t_1()
+ .border_color(cx.theme().colors().border)
+ .child(
+ Callout::new()
+ .icon(icon)
+ .title("Model Prompt Limit Reached")
+ .description(error_message)
+ .tertiary_action(self.upgrade_button(thread, cx))
+ .secondary_action(self.create_copy_button(error_message))
+ .primary_action(self.dismiss_error_button(thread, cx))
+ .bg_color(self.error_callout_bg(cx)),
)
- .into_any()
+ .into_any_element()
}
fn render_error_message(
@@ -2930,40 +2921,24 @@ impl AgentPanel {
cx: &mut Context<Self>,
) -> AnyElement {
let message_with_header = format!("{}\n{}", header, message);
- v_flex()
- .gap_0p5()
- .child(
- h_flex()
- .gap_1p5()
- .items_center()
- .child(Icon::new(IconName::XCircle).color(Color::Error))
- .child(Label::new(header).weight(FontWeight::MEDIUM)),
- )
- .child(
- div()
- .id("error-message")
- .max_h_32()
- .overflow_y_scroll()
- .child(Label::new(message.clone())),
- )
- .child(
- h_flex()
- .justify_end()
- .mt_1()
- .gap_1()
- .child(self.create_copy_button(message_with_header))
- .child(Button::new("dismiss", "Dismiss").on_click(cx.listener({
- let thread = thread.clone();
- move |_, _, _, cx| {
- thread.update(cx, |this, _cx| {
- this.clear_last_error();
- });
- cx.notify();
- }
- }))),
+ let icon = Icon::new(IconName::XCircle)
+ .size(IconSize::Small)
+ .color(Color::Error);
+
+ div()
+ .border_t_1()
+ .border_color(cx.theme().colors().border)
+ .child(
+ Callout::new()
+ .icon(icon)
+ .title(header)
+ .description(message.clone())
+ .primary_action(self.dismiss_error_button(thread, cx))
+ .secondary_action(self.create_copy_button(message_with_header))
+ .bg_color(self.error_callout_bg(cx)),
)
- .into_any()
+ .into_any_element()
}
fn render_prompt_editor(
@@ -3111,15 +3086,6 @@ impl AgentPanel {
}
}
- fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
- let message = message.into();
- IconButton::new("copy", IconName::Copy)
- .on_click(move |_, _, cx| {
- cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
- })
- .tooltip(Tooltip::text("Copy Error Message"))
- }
-
fn key_context(&self) -> KeyContext {
let mut key_context = KeyContext::new_with_defaults();
key_context.add("AgentPanel");
@@ -3204,18 +3170,9 @@ impl Render for AgentPanel {
thread.clone().into_any_element()
})
.children(self.render_tool_use_limit_reached(window, cx))
- .child(h_flex().child(message_editor.clone()))
.when_some(thread.read(cx).last_error(), |this, last_error| {
this.child(
div()
- .absolute()
- .right_3()
- .bottom_12()
- .max_w_96()
- .py_2()
- .px_3()
- .elevation_2(cx)
- .occlude()
.child(match last_error {
ThreadError::PaymentRequired => {
self.render_payment_required_error(thread, cx)
@@ -3229,6 +3186,7 @@ impl Render for AgentPanel {
.into_any(),
)
})
+ .child(h_flex().child(message_editor.clone()))
.child(self.render_drag_target(cx)),
ActiveView::AcpThread { thread_element, .. } => parent
.relative()
@@ -4,6 +4,7 @@ mod agent_diff;
mod agent_model_selector;
mod agent_panel;
mod buffer_codegen;
+mod burn_mode_tooltip;
mod context_picker;
mod context_server_configuration;
mod context_strip;
@@ -11,7 +12,6 @@ mod debug;
mod inline_assistant;
mod inline_prompt_editor;
mod language_model_selector;
-mod max_mode_tooltip;
mod message_editor;
mod profile_selector;
mod slash_command;
@@ -92,6 +92,7 @@ actions!(
#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)]
#[action(namespace = agent)]
+#[serde(deny_unknown_fields)]
pub struct NewThread {
#[serde(default)]
from_thread_id: Option<ThreadId>,
@@ -99,6 +100,7 @@ pub struct NewThread {
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
#[action(namespace = agent)]
+#[serde(deny_unknown_fields)]
pub struct ManageProfiles {
#[serde(default)]
pub customize_tools: Option<AgentProfileId>,
@@ -209,7 +211,7 @@ fn update_active_language_model_from_settings(cx: &mut App) {
}
}
- let default = to_selected_model(&settings.default_model);
+ let default = settings.default_model.as_ref().map(to_selected_model);
let inline_assistant = settings
.inline_assistant_model
.as_ref()
@@ -229,7 +231,7 @@ fn update_active_language_model_from_settings(cx: &mut App) {
.collect::<Vec<_>>();
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
- registry.select_default_model(Some(&default), cx);
+ registry.select_default_model(default.as_ref(), cx);
registry.select_inline_assistant_model(inline_assistant.as_ref(), cx);
registry.select_commit_message_model(commit_message.as_ref(), cx);
registry.select_thread_summary_model(thread_summary.as_ref(), cx);
@@ -1094,15 +1094,9 @@ mod tests {
};
use language_model::{LanguageModelRegistry, TokenUsage};
use rand::prelude::*;
- use serde::Serialize;
use settings::SettingsStore;
use std::{future, sync::Arc};
- #[derive(Serialize)]
- pub struct DummyCompletionRequest {
- pub name: String,
- }
-
#[gpui::test(iterations = 10)]
async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) {
init_test(cx);
@@ -1,11 +1,11 @@
use gpui::{Context, FontWeight, IntoElement, Render, Window};
use ui::{prelude::*, tooltip_container};
-pub struct MaxModeTooltip {
+pub struct BurnModeTooltip {
selected: bool,
}
-impl MaxModeTooltip {
+impl BurnModeTooltip {
pub fn new() -> Self {
Self { selected: false }
}
@@ -16,7 +16,7 @@ impl MaxModeTooltip {
}
}
-impl Render for MaxModeTooltip {
+impl Render for BurnModeTooltip {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let (icon, color) = if self.selected {
(IconName::ZedBurnModeOn, Color::Error)
@@ -930,8 +930,8 @@ impl MentionLink {
format!(
"[@{} ({}-{})]({}:{}:{}-{})",
file_name,
- line_range.start,
- line_range.end,
+ line_range.start + 1,
+ line_range.end + 1,
Self::SELECTION,
full_path,
line_range.start,
@@ -18,6 +18,7 @@ use agent_settings::AgentSettings;
use anyhow::{Context as _, Result};
use client::telemetry::Telemetry;
use collections::{HashMap, HashSet, VecDeque, hash_map};
+use editor::SelectionEffects;
use editor::{
Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorEvent, ExcerptId, ExcerptRange,
MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint,
@@ -1159,7 +1160,7 @@ impl InlineAssistant {
let position = assist.range.start;
editor.update(cx, |editor, cx| {
- editor.change_selections(None, window, cx, |selections| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
selections.select_anchor_ranges([position..position])
});
@@ -399,7 +399,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let all_models = self.all_models.clone();
- let current_index = self.selected_index;
+ let active_model = (self.get_active_model)(cx);
let bg_executor = cx.background_executor();
let language_model_registry = LanguageModelRegistry::global(cx);
@@ -441,12 +441,9 @@ impl PickerDelegate for LanguageModelPickerDelegate {
cx.spawn_in(window, async move |this, cx| {
this.update_in(cx, |this, window, cx| {
this.delegate.filtered_entries = filtered_models.entries();
- // Preserve selection focus
- let new_index = if current_index >= this.delegate.filtered_entries.len() {
- 0
- } else {
- current_index
- };
+ // Finds the currently selected model in the list
+ let new_index =
+ Self::get_active_model_index(&this.delegate.filtered_entries, active_model);
this.set_selected_index(new_index, Some(picker::Direction::Down), true, window, cx);
cx.notify();
})
@@ -576,7 +576,7 @@ impl MessageEditor {
fn render_burn_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
let thread = self.thread.read(cx);
let model = thread.configured_model();
- if !model?.model.supports_max_mode() {
+ if !model?.model.supports_burn_mode() {
return None;
}
@@ -1251,9 +1251,7 @@ impl MessageEditor {
self.thread
.read(cx)
.configured_model()
- .map_or(false, |model| {
- model.provider.id().0 == ZED_CLOUD_PROVIDER_ID
- })
+ .map_or(false, |model| model.provider.id() == ZED_CLOUD_PROVIDER_ID)
}
fn render_usage_callout(&self, line_height: Pixels, cx: &mut Context<Self>) -> Option<Div> {
@@ -1,8 +1,8 @@
use crate::{
+ burn_mode_tooltip::BurnModeTooltip,
language_model_selector::{
LanguageModelSelector, ToggleModelSelector, language_model_selector,
},
- max_mode_tooltip::MaxModeTooltip,
};
use agent_settings::{AgentSettings, CompletionMode};
use anyhow::Result;
@@ -21,7 +21,6 @@ use editor::{
BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata, CustomBlockId, FoldId,
RenderBlock, ToDisplayPoint,
},
- scroll::Autoscroll,
};
use editor::{FoldPlaceholder, display_map::CreaseId};
use fs::Fs;
@@ -69,7 +68,7 @@ use workspace::{
searchable::{Direction, SearchableItemHandle},
};
use workspace::{
- Save, Toast, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
+ Save, Toast, Workspace,
item::{self, FollowableItem, Item, ItemHandle},
notifications::NotificationId,
pane,
@@ -389,7 +388,7 @@ impl TextThreadEditor {
cursor..cursor
};
self.editor.update(cx, |editor, cx| {
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
+ editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges([new_selection])
});
});
@@ -449,8 +448,7 @@ impl TextThreadEditor {
if let Some(command) = self.slash_commands.command(name, cx) {
self.editor.update(cx, |editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
- editor
- .change_selections(Some(Autoscroll::fit()), window, cx, |s| s.try_cancel());
+ editor.change_selections(Default::default(), window, cx, |s| s.try_cancel());
let snapshot = editor.buffer().read(cx).snapshot(cx);
let newest_cursor = editor.selections.newest::<Point>(cx).head();
if newest_cursor.column > 0
@@ -1583,7 +1581,7 @@ impl TextThreadEditor {
self.editor.update(cx, |editor, cx| {
editor.transact(window, cx, |this, window, cx| {
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ this.change_selections(Default::default(), window, cx, |s| {
s.select(selections);
});
this.insert("", window, cx);
@@ -2075,12 +2073,12 @@ impl TextThreadEditor {
)
}
- fn render_max_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
+ fn render_burn_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
let context = self.context().read(cx);
let active_model = LanguageModelRegistry::read_global(cx)
.default_model()
.map(|default| default.model)?;
- if !active_model.supports_max_mode() {
+ if !active_model.supports_burn_mode() {
return None;
}
@@ -2107,7 +2105,7 @@ impl TextThreadEditor {
});
}))
.tooltip(move |_window, cx| {
- cx.new(|_| MaxModeTooltip::new().selected(burn_mode_enabled))
+ cx.new(|_| BurnModeTooltip::new().selected(burn_mode_enabled))
.into()
})
.into_any_element(),
@@ -2122,12 +2120,21 @@ impl TextThreadEditor {
let active_model = LanguageModelRegistry::read_global(cx)
.default_model()
.map(|default| default.model);
- let focus_handle = self.editor().focus_handle(cx).clone();
let model_name = match active_model {
Some(model) => model.name().0,
None => SharedString::from("No model selected"),
};
+ let active_provider = LanguageModelRegistry::read_global(cx)
+ .default_model()
+ .map(|default| default.provider);
+ let provider_icon = match active_provider {
+ Some(provider) => provider.icon(),
+ None => IconName::Ai,
+ };
+
+ let focus_handle = self.editor().focus_handle(cx).clone();
+
PickerPopoverMenu::new(
self.language_model_selector.clone(),
ButtonLike::new("active-model")
@@ -2135,10 +2142,16 @@ impl TextThreadEditor {
.child(
h_flex()
.gap_0p5()
+ .child(
+ Icon::new(provider_icon)
+ .color(Color::Muted)
+ .size(IconSize::XSmall),
+ )
.child(
Label::new(model_name)
+ .color(Color::Muted)
.size(LabelSize::Small)
- .color(Color::Muted),
+ .ml_0p5(),
)
.child(
Icon::new(IconName::ChevronDown)
@@ -2575,7 +2588,7 @@ impl Render for TextThreadEditor {
};
let language_model_selector = self.language_model_selector_menu_handle.clone();
- let max_mode_toggle = self.render_max_mode_toggle(cx);
+ let burn_mode_toggle = self.render_burn_mode_toggle(cx);
v_flex()
.key_context("ContextEditor")
@@ -2630,7 +2643,7 @@ impl Render for TextThreadEditor {
h_flex()
.gap_0p5()
.child(self.render_inject_context_menu(cx))
- .when_some(max_mode_toggle, |this, element| this.child(element)),
+ .when_some(burn_mode_toggle, |this, element| this.child(element)),
)
.child(
h_flex()
@@ -2924,13 +2937,6 @@ impl FollowableItem for TextThreadEditor {
}
}
-pub struct ContextEditorToolbarItem {
- active_context_editor: Option<WeakEntity<TextThreadEditor>>,
- model_summary_editor: Entity<Editor>,
-}
-
-impl ContextEditorToolbarItem {}
-
pub fn render_remaining_tokens(
context_editor: &Entity<TextThreadEditor>,
cx: &App,
@@ -2983,98 +2989,6 @@ pub fn render_remaining_tokens(
)
}
-impl Render for ContextEditorToolbarItem {
- fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
- let left_side = h_flex()
- .group("chat-title-group")
- .gap_1()
- .items_center()
- .flex_grow()
- .child(
- div()
- .w_full()
- .when(self.active_context_editor.is_some(), |left_side| {
- left_side.child(self.model_summary_editor.clone())
- }),
- )
- .child(
- div().visible_on_hover("chat-title-group").child(
- IconButton::new("regenerate-context", IconName::RefreshTitle)
- .shape(ui::IconButtonShape::Square)
- .tooltip(Tooltip::text("Regenerate Title"))
- .on_click(cx.listener(move |_, _, _window, cx| {
- cx.emit(ContextEditorToolbarItemEvent::RegenerateSummary)
- })),
- ),
- );
-
- let right_side = h_flex()
- .gap_2()
- // TODO display this in a nicer way, once we have a design for it.
- // .children({
- // let project = self
- // .workspace
- // .upgrade()
- // .map(|workspace| workspace.read(cx).project().downgrade());
- //
- // let scan_items_remaining = cx.update_global(|db: &mut SemanticDb, cx| {
- // project.and_then(|project| db.remaining_summaries(&project, cx))
- // });
- // scan_items_remaining
- // .map(|remaining_items| format!("Files to scan: {}", remaining_items))
- // })
- .children(
- self.active_context_editor
- .as_ref()
- .and_then(|editor| editor.upgrade())
- .and_then(|editor| render_remaining_tokens(&editor, cx)),
- );
-
- h_flex()
- .px_0p5()
- .size_full()
- .gap_2()
- .justify_between()
- .child(left_side)
- .child(right_side)
- }
-}
-
-impl ToolbarItemView for ContextEditorToolbarItem {
- fn set_active_pane_item(
- &mut self,
- active_pane_item: Option<&dyn ItemHandle>,
- _window: &mut Window,
- cx: &mut Context<Self>,
- ) -> ToolbarItemLocation {
- self.active_context_editor = active_pane_item
- .and_then(|item| item.act_as::<TextThreadEditor>(cx))
- .map(|editor| editor.downgrade());
- cx.notify();
- if self.active_context_editor.is_none() {
- ToolbarItemLocation::Hidden
- } else {
- ToolbarItemLocation::PrimaryRight
- }
- }
-
- fn pane_focus_update(
- &mut self,
- _pane_focused: bool,
- _window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- cx.notify();
- }
-}
-
-impl EventEmitter<ToolbarItemEvent> for ContextEditorToolbarItem {}
-
-pub enum ContextEditorToolbarItemEvent {
- RegenerateSummary,
-}
-impl EventEmitter<ContextEditorToolbarItemEvent> for ContextEditorToolbarItem {}
-
enum PendingSlashCommand {}
fn invoked_slash_command_fold_placeholder(
@@ -3240,6 +3154,7 @@ pub fn make_lsp_adapter_delegate(
#[cfg(test)]
mod tests {
use super::*;
+ use editor::SelectionEffects;
use fs::FakeFs;
use gpui::{App, TestAppContext, VisualTestContext};
use indoc::indoc;
@@ -3465,7 +3380,9 @@ mod tests {
) {
context_editor.update_in(cx, |context_editor, window, cx| {
context_editor.editor.update(cx, |editor, cx| {
- editor.change_selections(None, window, cx, |s| s.select_ranges([range]));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([range])
+ });
});
context_editor.copy(&Default::default(), window, cx);
@@ -1,13 +1,13 @@
mod agent_notification;
mod animated_label;
+mod burn_mode_tooltip;
mod context_pill;
-mod max_mode_tooltip;
mod onboarding_modal;
pub mod preview;
mod upsell;
pub use agent_notification::*;
pub use animated_label::*;
+pub use burn_mode_tooltip::*;
pub use context_pill::*;
-pub use max_mode_tooltip::*;
pub use onboarding_modal::*;
@@ -6,7 +6,7 @@ use anyhow::{Context as _, Result, anyhow};
use chrono::{DateTime, Utc};
use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream};
use http_client::http::{self, HeaderMap, HeaderValue};
-use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
+use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest, StatusCode};
use serde::{Deserialize, Serialize};
use strum::{EnumIter, EnumString};
use thiserror::Error;
@@ -356,7 +356,7 @@ pub async fn complete(
.send(request)
.await
.map_err(AnthropicError::HttpSend)?;
- let status = response.status();
+ let status_code = response.status();
let mut body = String::new();
response
.body_mut()
@@ -364,12 +364,12 @@ pub async fn complete(
.await
.map_err(AnthropicError::ReadResponse)?;
- if status.is_success() {
+ if status_code.is_success() {
Ok(serde_json::from_str(&body).map_err(AnthropicError::DeserializeResponse)?)
} else {
Err(AnthropicError::HttpResponseError {
- status: status.as_u16(),
- body,
+ status_code,
+ message: body,
})
}
}
@@ -444,11 +444,7 @@ impl RateLimitInfo {
}
Self {
- retry_after: headers
- .get("retry-after")
- .and_then(|v| v.to_str().ok())
- .and_then(|v| v.parse::<u64>().ok())
- .map(Duration::from_secs),
+ retry_after: parse_retry_after(headers),
requests: RateLimit::from_headers("requests", headers).ok(),
tokens: RateLimit::from_headers("tokens", headers).ok(),
input_tokens: RateLimit::from_headers("input-tokens", headers).ok(),
@@ -457,6 +453,17 @@ impl RateLimitInfo {
}
}
+/// Parses the Retry-After header value as an integer number of seconds (anthropic always uses
+/// seconds). Note that other services might specify an HTTP date or some other format for this
+/// header. Returns `None` if the header is not present or cannot be parsed.
+pub fn parse_retry_after(headers: &HeaderMap<HeaderValue>) -> Option<Duration> {
+ headers
+ .get("retry-after")
+ .and_then(|v| v.to_str().ok())
+ .and_then(|v| v.parse::<u64>().ok())
+ .map(Duration::from_secs)
+}
+
fn get_header<'a>(key: &str, headers: &'a HeaderMap) -> anyhow::Result<&'a str> {
Ok(headers
.get(key)
@@ -520,6 +527,10 @@ pub async fn stream_completion_with_rate_limit_info(
})
.boxed();
Ok((stream, Some(rate_limits)))
+ } else if response.status().as_u16() == 529 {
+ Err(AnthropicError::ServerOverloaded {
+ retry_after: rate_limits.retry_after,
+ })
} else if let Some(retry_after) = rate_limits.retry_after {
Err(AnthropicError::RateLimit { retry_after })
} else {
@@ -532,10 +543,9 @@ pub async fn stream_completion_with_rate_limit_info(
match serde_json::from_str::<Event>(&body) {
Ok(Event::Error { error }) => Err(AnthropicError::ApiError(error)),
- Ok(_) => Err(AnthropicError::UnexpectedResponseFormat(body)),
- Err(_) => Err(AnthropicError::HttpResponseError {
- status: response.status().as_u16(),
- body: body,
+ Ok(_) | Err(_) => Err(AnthropicError::HttpResponseError {
+ status_code: response.status(),
+ message: body,
}),
}
}
@@ -801,16 +811,19 @@ pub enum AnthropicError {
ReadResponse(io::Error),
/// HTTP error response from the API
- HttpResponseError { status: u16, body: String },
+ HttpResponseError {
+ status_code: StatusCode,
+ message: String,
+ },
/// Rate limit exceeded
RateLimit { retry_after: Duration },
+ /// Server overloaded
+ ServerOverloaded { retry_after: Option<Duration> },
+
/// API returned an error response
ApiError(ApiError),
-
- /// Unexpected response format
- UnexpectedResponseFormat(String),
}
#[derive(Debug, Serialize, Deserialize, Error)]
@@ -2140,7 +2140,8 @@ impl AssistantContext {
);
}
LanguageModelCompletionEvent::ToolUse(_) |
- LanguageModelCompletionEvent::UsageUpdate(_) => {}
+ LanguageModelCompletionEvent::ToolUseJsonParseError { .. } |
+ LanguageModelCompletionEvent::UsageUpdate(_) => {}
}
});
@@ -2346,13 +2347,13 @@ impl AssistantContext {
completion_request.messages.push(request_message);
}
}
- let supports_max_mode = if let Some(model) = model {
- model.supports_max_mode()
+ let supports_burn_mode = if let Some(model) = model {
+ model.supports_burn_mode()
} else {
false
};
- if supports_max_mode {
+ if supports_burn_mode {
completion_request.mode = Some(self.completion_mode.into());
}
completion_request
@@ -74,7 +74,7 @@ impl SlashCommand for DeltaSlashCommand {
.slice(section.range.to_offset(&context_buffer)),
);
file_command_new_outputs.push(Arc::new(FileSlashCommand).run(
- &[metadata.path.clone()],
+ std::slice::from_ref(&metadata.path),
context_slash_command_output_sections,
context_buffer.clone(),
workspace.clone(),
@@ -29,6 +29,7 @@ use std::{
path::Path,
str::FromStr,
sync::mpsc,
+ time::Duration,
};
use util::path;
@@ -1658,12 +1659,14 @@ async fn retry_on_rate_limit<R>(mut request: impl AsyncFnMut() -> Result<R>) ->
match request().await {
Ok(result) => return Ok(result),
Err(err) => match err.downcast::<LanguageModelCompletionError>() {
- Ok(err) => match err {
- LanguageModelCompletionError::RateLimitExceeded { retry_after } => {
+ 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}: Rate limit exceeded. Retry after {retry_after:?} + jitter of {jitter:?}"
+ "Attempt #{attempt}: {err}. Retry after {retry_after:?} + jitter of {jitter:?}"
);
Timer::after(retry_after + jitter).await;
continue;
@@ -10,7 +10,7 @@ use assistant_tool::{
ToolUseStatus,
};
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
-use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey, scroll::Autoscroll};
+use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey};
use futures::StreamExt;
use gpui::{
Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task,
@@ -823,7 +823,7 @@ impl ToolCard for EditFileToolCard {
let first_hunk_start =
first_hunk.multi_buffer_range().start;
editor.change_selections(
- Some(Autoscroll::fit()),
+ Default::default(),
window,
cx,
|selections| {
@@ -1065,7 +1065,7 @@ fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
MarkdownStyle {
base_text_style: text_style.clone(),
- selection_background_color: cx.theme().players().local().selection,
+ selection_background_color: cx.theme().colors().element_selection_background,
..Default::default()
}
}
@@ -1,8 +1,9 @@
use anyhow::Result;
use language_model::LanguageModelToolSchemaFormat;
use schemars::{
- JsonSchema,
- schema::{RootSchema, Schema, SchemaObject},
+ JsonSchema, Schema,
+ generate::SchemaSettings,
+ transform::{Transform, transform_subschemas},
};
pub fn json_schema_for<T: JsonSchema>(
@@ -13,7 +14,7 @@ pub fn json_schema_for<T: JsonSchema>(
}
fn schema_to_json(
- schema: &RootSchema,
+ schema: &Schema,
format: LanguageModelToolSchemaFormat,
) -> Result<serde_json::Value> {
let mut value = serde_json::to_value(schema)?;
@@ -21,58 +22,42 @@ fn schema_to_json(
Ok(value)
}
-fn root_schema_for<T: JsonSchema>(format: LanguageModelToolSchemaFormat) -> RootSchema {
+fn root_schema_for<T: JsonSchema>(format: LanguageModelToolSchemaFormat) -> Schema {
let mut generator = match format {
- LanguageModelToolSchemaFormat::JsonSchema => schemars::SchemaGenerator::default(),
- LanguageModelToolSchemaFormat::JsonSchemaSubset => {
- schemars::r#gen::SchemaSettings::default()
- .with(|settings| {
- settings.meta_schema = None;
- settings.inline_subschemas = true;
- settings
- .visitors
- .push(Box::new(TransformToJsonSchemaSubsetVisitor));
- })
- .into_generator()
- }
+ LanguageModelToolSchemaFormat::JsonSchema => SchemaSettings::draft07().into_generator(),
+ // TODO: Gemini docs mention using a subset of OpenAPI 3, so this may benefit from using
+ // `SchemaSettings::openapi3()`.
+ LanguageModelToolSchemaFormat::JsonSchemaSubset => SchemaSettings::draft07()
+ .with(|settings| {
+ settings.meta_schema = None;
+ settings.inline_subschemas = true;
+ })
+ .with_transform(ToJsonSchemaSubsetTransform)
+ .into_generator(),
};
generator.root_schema_for::<T>()
}
#[derive(Debug, Clone)]
-struct TransformToJsonSchemaSubsetVisitor;
-
-impl schemars::visit::Visitor for TransformToJsonSchemaSubsetVisitor {
- fn visit_root_schema(&mut self, root: &mut RootSchema) {
- schemars::visit::visit_root_schema(self, root)
- }
+struct ToJsonSchemaSubsetTransform;
- fn visit_schema(&mut self, schema: &mut Schema) {
- schemars::visit::visit_schema(self, schema)
- }
-
- fn visit_schema_object(&mut self, schema: &mut SchemaObject) {
+impl Transform for ToJsonSchemaSubsetTransform {
+ fn transform(&mut self, schema: &mut Schema) {
// Ensure that the type field is not an array, this happens when we use
// Option<T>, the type will be [T, "null"].
- if let Some(instance_type) = schema.instance_type.take() {
- schema.instance_type = match instance_type {
- schemars::schema::SingleOrVec::Single(t) => {
- Some(schemars::schema::SingleOrVec::Single(t))
+ if let Some(type_field) = schema.get_mut("type") {
+ if let Some(types) = type_field.as_array() {
+ if let Some(first_type) = types.first() {
+ *type_field = first_type.clone();
}
- schemars::schema::SingleOrVec::Vec(items) => items
- .into_iter()
- .next()
- .map(schemars::schema::SingleOrVec::from),
- };
+ }
}
- // One of is not supported, use anyOf instead.
- if let Some(subschema) = schema.subschemas.as_mut() {
- if let Some(one_of) = subschema.one_of.take() {
- subschema.any_of = Some(one_of);
- }
+ // oneOf is not supported, use anyOf instead
+ if let Some(one_of) = schema.remove("oneOf") {
+ schema.insert("anyOf".to_string(), one_of);
}
- schemars::visit::visit_schema_object(self, schema)
+ transform_subschemas(self, schema);
}
}
@@ -691,7 +691,7 @@ fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
MarkdownStyle {
base_text_style: text_style.clone(),
- selection_background_color: cx.theme().players().local().selection,
+ selection_background_color: cx.theme().colors().element_selection_background,
..Default::default()
}
}
@@ -1,7 +1,7 @@
use auto_update::AutoUpdater;
use client::proto::UpdateNotification;
use editor::{Editor, MultiBuffer};
-use gpui::{App, Context, DismissEvent, Entity, SharedString, Window, actions, prelude::*};
+use gpui::{App, Context, DismissEvent, Entity, Window, actions, prelude::*};
use http_client::HttpClient;
use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
use release_channel::{AppVersion, ReleaseChannel};
@@ -94,7 +94,6 @@ fn view_release_notes_locally(
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
- let tab_content = Some(SharedString::from(body.title.to_string()));
let editor = cx.new(|cx| {
Editor::for_multibuffer(buffer, Some(project), window, cx)
});
@@ -105,7 +104,6 @@ fn view_release_notes_locally(
editor,
workspace_handle,
language_registry,
- tab_content,
window,
cx,
);
@@ -25,5 +25,4 @@ serde.workspace = true
serde_json.workspace = true
strum.workspace = true
thiserror.workspace = true
-tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
workspace-hack.workspace = true
@@ -1,9 +1,6 @@
mod models;
-use std::collections::HashMap;
-use std::pin::Pin;
-
-use anyhow::{Context as _, Error, Result, anyhow};
+use anyhow::{Context, Error, Result, anyhow};
use aws_sdk_bedrockruntime as bedrock;
pub use aws_sdk_bedrockruntime as bedrock_client;
pub use aws_sdk_bedrockruntime::types::{
@@ -24,9 +21,10 @@ pub use bedrock::types::{
ToolResultContentBlock as BedrockToolResultContentBlock,
ToolResultStatus as BedrockToolResultStatus, ToolUseBlock as BedrockToolUseBlock,
};
-use futures::stream::{self, BoxStream, Stream};
+use futures::stream::{self, BoxStream};
use serde::{Deserialize, Serialize};
use serde_json::{Number, Value};
+use std::collections::HashMap;
use thiserror::Error;
pub use crate::models::*;
@@ -34,70 +32,59 @@ pub use crate::models::*;
pub async fn stream_completion(
client: bedrock::Client,
request: Request,
- handle: tokio::runtime::Handle,
) -> Result<BoxStream<'static, Result<BedrockStreamingResponse, BedrockError>>, Error> {
- handle
- .spawn(async move {
- let mut response = bedrock::Client::converse_stream(&client)
- .model_id(request.model.clone())
- .set_messages(request.messages.into());
+ let mut response = bedrock::Client::converse_stream(&client)
+ .model_id(request.model.clone())
+ .set_messages(request.messages.into());
- if let Some(Thinking::Enabled {
- budget_tokens: Some(budget_tokens),
- }) = request.thinking
- {
- response =
- response.additional_model_request_fields(Document::Object(HashMap::from([(
- "thinking".to_string(),
- Document::from(HashMap::from([
- ("type".to_string(), Document::String("enabled".to_string())),
- (
- "budget_tokens".to_string(),
- Document::Number(AwsNumber::PosInt(budget_tokens)),
- ),
- ])),
- )])));
- }
+ if let Some(Thinking::Enabled {
+ budget_tokens: Some(budget_tokens),
+ }) = request.thinking
+ {
+ let thinking_config = HashMap::from([
+ ("type".to_string(), Document::String("enabled".to_string())),
+ (
+ "budget_tokens".to_string(),
+ Document::Number(AwsNumber::PosInt(budget_tokens)),
+ ),
+ ]);
+ response = response.additional_model_request_fields(Document::Object(HashMap::from([(
+ "thinking".to_string(),
+ Document::from(thinking_config),
+ )])));
+ }
- if request.tools.is_some() && !request.tools.as_ref().unwrap().tools.is_empty() {
- response = response.set_tool_config(request.tools);
- }
+ if request
+ .tools
+ .as_ref()
+ .map_or(false, |t| !t.tools.is_empty())
+ {
+ response = response.set_tool_config(request.tools);
+ }
- let response = response.send().await;
+ let output = response
+ .send()
+ .await
+ .context("Failed to send API request to Bedrock");
- match response {
- Ok(output) => {
- let stream: Pin<
- Box<
- dyn Stream<Item = Result<BedrockStreamingResponse, BedrockError>>
- + Send,
- >,
- > = Box::pin(stream::unfold(output.stream, |mut stream| async move {
- match stream.recv().await {
- Ok(Some(output)) => Some(({ Ok(output) }, stream)),
- Ok(None) => None,
- Err(err) => {
- Some((
- // TODO: Figure out how we can capture Throttling Exceptions
- Err(BedrockError::ClientError(anyhow!(
- "{:?}",
- aws_sdk_bedrockruntime::error::DisplayErrorContext(err)
- ))),
- stream,
- ))
- }
- }
- }));
- Ok(stream)
- }
- Err(err) => Err(anyhow!(
- "{:?}",
- aws_sdk_bedrockruntime::error::DisplayErrorContext(err)
+ let stream = Box::pin(stream::unfold(
+ output?.stream,
+ move |mut stream| async move {
+ match stream.recv().await {
+ Ok(Some(output)) => Some((Ok(output), stream)),
+ Ok(None) => None,
+ Err(err) => Some((
+ Err(BedrockError::ClientError(anyhow!(
+ "{:?}",
+ aws_sdk_bedrockruntime::error::DisplayErrorContext(err)
+ ))),
+ stream,
)),
}
- })
- .await
- .context("spawning a task")?
+ },
+ ));
+
+ Ok(stream)
}
pub fn aws_document_to_value(document: &Document) -> Value {
@@ -1867,7 +1867,7 @@ mod tests {
let hunk = diff.hunks(&buffer, cx).next().unwrap();
let new_index_text = diff
- .stage_or_unstage_hunks(true, &[hunk.clone()], &buffer, true, cx)
+ .stage_or_unstage_hunks(true, std::slice::from_ref(&hunk), &buffer, true, cx)
.unwrap()
.to_string();
assert_eq!(new_index_text, buffer_text);
@@ -12,7 +12,6 @@ pub struct CallSettings {
/// Configuration of voice calls in Zed.
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
-#[schemars(deny_unknown_fields)]
pub struct CallSettingsContent {
/// Whether the microphone should be muted when joining a channel or a call.
///
@@ -734,8 +734,8 @@ impl Database {
users.push(proto::User {
id: user.id.to_proto(),
avatar_url: format!(
- "https://github.com/{}.png?size=128",
- user.github_login
+ "https://avatars.githubusercontent.com/u/{}?s=128&v=4",
+ user.github_user_id
),
github_login: user.github_login,
name: user.name,
@@ -76,7 +76,10 @@ async fn test_purge_old_embeddings(cx: &mut gpui::TestAppContext) {
db.purge_old_embeddings().await.unwrap();
// Try to retrieve the purged embeddings
- let retrieved_embeddings = db.get_embeddings(model, &[digest.clone()]).await.unwrap();
+ let retrieved_embeddings = db
+ .get_embeddings(model, std::slice::from_ref(&digest))
+ .await
+ .unwrap();
assert!(
retrieved_embeddings.is_empty(),
"Old embeddings should have been purged"
@@ -179,7 +179,7 @@ struct Session {
}
impl Session {
- async fn db(&self) -> tokio::sync::MutexGuard<DbHandle> {
+ async fn db(&self) -> tokio::sync::MutexGuard<'_, DbHandle> {
#[cfg(test)]
tokio::task::yield_now().await;
let guard = self.db.lock().await;
@@ -1037,7 +1037,7 @@ impl Server {
}
}
- pub async fn snapshot(self: &Arc<Self>) -> ServerSnapshot {
+ pub async fn snapshot(self: &Arc<Self>) -> ServerSnapshot<'_> {
ServerSnapshot {
connection_pool: ConnectionPoolGuard {
guard: self.connection_pool.lock(),
@@ -178,7 +178,7 @@ async fn test_channel_notes_participant_indices(
channel_view_a.update_in(cx_a, |notes, window, cx| {
notes.editor.update(cx, |editor, cx| {
editor.insert("a", window, cx);
- editor.change_selections(None, window, cx, |selections| {
+ editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges(vec![0..1]);
});
});
@@ -188,7 +188,7 @@ async fn test_channel_notes_participant_indices(
notes.editor.update(cx, |editor, cx| {
editor.move_down(&Default::default(), window, cx);
editor.insert("b", window, cx);
- editor.change_selections(None, window, cx, |selections| {
+ editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges(vec![1..2]);
});
});
@@ -198,7 +198,7 @@ async fn test_channel_notes_participant_indices(
notes.editor.update(cx, |editor, cx| {
editor.move_down(&Default::default(), window, cx);
editor.insert("c", window, cx);
- editor.change_selections(None, window, cx, |selections| {
+ editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges(vec![2..3]);
});
});
@@ -273,12 +273,12 @@ async fn test_channel_notes_participant_indices(
.unwrap();
editor_a.update_in(cx_a, |editor, window, cx| {
- editor.change_selections(None, window, cx, |selections| {
+ editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges(vec![0..1]);
});
});
editor_b.update_in(cx_b, |editor, window, cx| {
- editor.change_selections(None, window, cx, |selections| {
+ editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges(vec![2..3]);
});
});
@@ -4,7 +4,7 @@ use crate::{
};
use call::ActiveCall;
use editor::{
- DocumentColorsRenderMode, Editor, EditorSettings, RowInfo,
+ DocumentColorsRenderMode, Editor, EditorSettings, RowInfo, SelectionEffects,
actions::{
ConfirmCodeAction, ConfirmCompletion, ConfirmRename, ContextMenuFirst,
ExpandMacroRecursively, MoveToEnd, Redo, Rename, SelectAll, ToggleCodeActions, Undo,
@@ -348,7 +348,9 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
// Type a completion trigger character as the guest.
editor_b.update_in(cx_b, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([13..13])
+ });
editor.handle_input(".", window, cx);
});
cx_b.focus(&editor_b);
@@ -461,7 +463,9 @@ async fn test_collaborating_with_completion(cx_a: &mut TestAppContext, cx_b: &mu
// Now we do a second completion, this time to ensure that documentation/snippets are
// resolved
editor_b.update_in(cx_b, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| s.select_ranges([46..46]));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([46..46])
+ });
editor.handle_input("; a", window, cx);
editor.handle_input(".", window, cx);
});
@@ -613,7 +617,7 @@ async fn test_collaborating_with_code_actions(
// Move cursor to a location that contains code actions.
editor_b.update_in(cx_b, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::new(1, 31)..Point::new(1, 31)])
});
});
@@ -817,7 +821,9 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
// Move cursor to a location that can be renamed.
let prepare_rename = editor_b.update_in(cx_b, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| s.select_ranges([7..7]));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([7..7])
+ });
editor.rename(&Rename, window, cx).unwrap()
});
@@ -863,7 +869,9 @@ async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut T
editor.cancel(&editor::actions::Cancel, window, cx);
});
let prepare_rename = editor_b.update_in(cx_b, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| s.select_ranges([7..8]));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([7..8])
+ });
editor.rename(&Rename, window, cx).unwrap()
});
@@ -1364,7 +1372,9 @@ async fn test_on_input_format_from_host_to_guest(
// Type a on type formatting trigger character as the guest.
cx_a.focus(&editor_a);
editor_a.update_in(cx_a, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([13..13])
+ });
editor.handle_input(">", window, cx);
});
@@ -1460,7 +1470,9 @@ async fn test_on_input_format_from_guest_to_host(
// Type a on type formatting trigger character as the guest.
cx_b.focus(&editor_b);
editor_b.update_in(cx_b, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([13..13])
+ });
editor.handle_input(":", window, cx);
});
@@ -1697,7 +1709,9 @@ async fn test_mutual_editor_inlay_hint_cache_update(
let after_client_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
editor_b.update_in(cx_b, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| s.select_ranges([13..13].clone()));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([13..13].clone())
+ });
editor.handle_input(":", window, cx);
});
cx_b.focus(&editor_b);
@@ -1718,7 +1732,9 @@ async fn test_mutual_editor_inlay_hint_cache_update(
let after_host_edit = edits_made.fetch_add(1, atomic::Ordering::Release) + 1;
editor_a.update_in(cx_a, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([13..13])
+ });
editor.handle_input("a change to increment both buffers' versions", window, cx);
});
cx_a.focus(&editor_a);
@@ -2121,7 +2137,9 @@ async fn test_lsp_document_color(cx_a: &mut TestAppContext, cx_b: &mut TestAppCo
});
editor_a.update_in(cx_a, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| s.select_ranges([13..13].clone()));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([13..13].clone())
+ });
editor.handle_input(":", window, cx);
});
color_request_handle.next().await.unwrap();
@@ -6,7 +6,7 @@ use collab_ui::{
channel_view::ChannelView,
notifications::project_shared_notification::ProjectSharedNotification,
};
-use editor::{Editor, MultiBuffer, PathKey};
+use editor::{Editor, MultiBuffer, PathKey, SelectionEffects};
use gpui::{
AppContext as _, BackgroundExecutor, BorrowAppContext, Entity, SharedString, TestAppContext,
VisualContext, VisualTestContext, point,
@@ -376,7 +376,9 @@ async fn test_basic_following(
// Changes to client A's editor are reflected on client B.
editor_a1.update_in(cx_a, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2]));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([1..1, 2..2])
+ });
});
executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
executor.run_until_parked();
@@ -393,7 +395,9 @@ async fn test_basic_following(
editor_b1.update(cx_b, |editor, cx| assert_eq!(editor.text(cx), "TWO"));
editor_a1.update_in(cx_a, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([3..3])
+ });
editor.set_scroll_position(point(0., 100.), window, cx);
});
executor.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
@@ -1647,7 +1651,9 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T
// b should follow a to position 1
editor_a.update_in(cx_a, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]))
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([1..1])
+ })
});
cx_a.executor()
.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
@@ -1667,7 +1673,9 @@ async fn test_following_stops_on_unshare(cx_a: &mut TestAppContext, cx_b: &mut T
// b should not follow a to position 2
editor_a.update_in(cx_a, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]))
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([2..2])
+ })
});
cx_a.executor()
.advance_clock(workspace::item::LEADER_UPDATE_THROTTLE);
@@ -1968,7 +1976,7 @@ async fn test_following_to_channel_notes_without_a_shared_project(
assert_eq!(notes.channel(cx).unwrap().name, "channel-1");
notes.editor.update(cx, |editor, cx| {
editor.insert("Hello from A.", window, cx);
- editor.change_selections(None, window, cx, |selections| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
selections.select_ranges(vec![3..4]);
});
});
@@ -2109,7 +2117,7 @@ async fn test_following_after_replacement(cx_a: &mut TestAppContext, cx_b: &mut
workspace.add_item_to_center(Box::new(editor.clone()) as _, window, cx)
});
editor.update_in(cx_a, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::row_range(4..4)]);
})
});
@@ -22,9 +22,7 @@ use gpui::{
use language::{
Diagnostic, DiagnosticEntry, DiagnosticSourceKind, FakeLspAdapter, Language, LanguageConfig,
LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope,
- language_settings::{
- AllLanguageSettings, Formatter, FormatterList, PrettierSettings, SelectedFormatter,
- },
+ language_settings::{AllLanguageSettings, Formatter, PrettierSettings, SelectedFormatter},
tree_sitter_rust, tree_sitter_typescript,
};
use lsp::{LanguageServerId, OneOf};
@@ -4591,15 +4589,13 @@ async fn test_formatting_buffer(
cx_a.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
- file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
- vec![Formatter::External {
+ file.defaults.formatter =
+ Some(SelectedFormatter::List(vec![Formatter::External {
command: "awk".into(),
arguments: Some(
vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(),
),
- }]
- .into(),
- )));
+ }]));
});
});
});
@@ -4699,9 +4695,10 @@ async fn test_prettier_formatting_buffer(
cx_b.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
- file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
- vec![Formatter::LanguageServer { name: None }].into(),
- )));
+ file.defaults.formatter =
+ Some(SelectedFormatter::List(vec![Formatter::LanguageServer {
+ name: None,
+ }]));
file.defaults.prettier = Some(PrettierSettings {
allowed: true,
..PrettierSettings::default()
@@ -14,8 +14,7 @@ use http_client::BlockedHttpClient;
use language::{
FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry,
language_settings::{
- AllLanguageSettings, Formatter, FormatterList, PrettierSettings, SelectedFormatter,
- language_settings,
+ AllLanguageSettings, Formatter, PrettierSettings, SelectedFormatter, language_settings,
},
tree_sitter_typescript,
};
@@ -505,9 +504,10 @@ async fn test_ssh_collaboration_formatting_with_prettier(
cx_b.update(|cx| {
SettingsStore::update_global(cx, |store, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
- file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
- vec![Formatter::LanguageServer { name: None }].into(),
- )));
+ file.defaults.formatter =
+ Some(SelectedFormatter::List(vec![Formatter::LanguageServer {
+ name: None,
+ }]));
file.defaults.prettier = Some(PrettierSettings {
allowed: true,
..PrettierSettings::default()
@@ -7,8 +7,8 @@ use client::{
};
use collections::HashMap;
use editor::{
- CollaborationHub, DisplayPoint, Editor, EditorEvent, display_map::ToDisplayPoint,
- scroll::Autoscroll,
+ CollaborationHub, DisplayPoint, Editor, EditorEvent, SelectionEffects,
+ display_map::ToDisplayPoint, scroll::Autoscroll,
};
use gpui::{
AnyView, App, ClipboardItem, Context, Entity, EventEmitter, Focusable, Pixels, Point, Render,
@@ -260,9 +260,16 @@ impl ChannelView {
.find(|item| &Channel::slug(&item.text).to_lowercase() == &position)
{
self.editor.update(cx, |editor, cx| {
- editor.change_selections(Some(Autoscroll::focused()), window, cx, |s| {
- s.replace_cursors_with(|map| vec![item.range.start.to_display_point(map)])
- })
+ editor.change_selections(
+ SelectionEffects::scroll(Autoscroll::focused()),
+ window,
+ cx,
+ |s| {
+ s.replace_cursors_with(|map| {
+ vec![item.range.start.to_display_point(map)]
+ })
+ },
+ )
});
return;
}
@@ -28,7 +28,6 @@ pub struct ChatPanelSettings {
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
-#[schemars(deny_unknown_fields)]
pub struct ChatPanelSettingsContent {
/// When to show the panel button in the status bar.
///
@@ -52,7 +51,6 @@ pub struct NotificationPanelSettings {
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
-#[schemars(deny_unknown_fields)]
pub struct PanelSettingsContent {
/// Whether to show the panel button in the status bar.
///
@@ -69,7 +67,6 @@ pub struct PanelSettingsContent {
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
-#[schemars(deny_unknown_fields)]
pub struct MessageEditorSettings {
/// Whether to automatically replace emoji shortcodes with emoji characters.
/// For example: typing `:wave:` gets replaced with `π`.
@@ -41,7 +41,7 @@ pub struct CommandPalette {
/// Removes subsequent whitespace characters and double colons from the query.
///
/// This improves the likelihood of a match by either humanized name or keymap-style name.
-fn normalize_query(input: &str) -> String {
+pub fn normalize_action_query(input: &str) -> String {
let mut result = String::with_capacity(input.len());
let mut last_char = None;
@@ -297,7 +297,7 @@ impl PickerDelegate for CommandPaletteDelegate {
let mut commands = self.all_commands.clone();
let hit_counts = self.hit_counts();
let executor = cx.background_executor().clone();
- let query = normalize_query(query.as_str());
+ let query = normalize_action_query(query.as_str());
async move {
commands.sort_by_key(|action| {
(
@@ -311,29 +311,17 @@ impl PickerDelegate for CommandPaletteDelegate {
.enumerate()
.map(|(ix, command)| StringMatchCandidate::new(ix, &command.name))
.collect::<Vec<_>>();
- 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 {
- fuzzy::match_strings(
- &candidates,
- &query,
- true,
- true,
- 10000,
- &Default::default(),
- executor,
- )
- .await
- };
+
+ let matches = fuzzy::match_strings(
+ &candidates,
+ &query,
+ true,
+ true,
+ 10000,
+ &Default::default(),
+ executor,
+ )
+ .await;
tx.send((commands, matches)).await.log_err();
}
@@ -422,8 +410,8 @@ impl PickerDelegate for CommandPaletteDelegate {
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
- let r#match = self.matches.get(ix)?;
- let command = self.commands.get(r#match.candidate_id)?;
+ let matching_command = self.matches.get(ix)?;
+ let command = self.commands.get(matching_command.candidate_id)?;
Some(
ListItem::new(ix)
.inset(true)
@@ -436,7 +424,7 @@ impl PickerDelegate for CommandPaletteDelegate {
.justify_between()
.child(HighlightedLabel::new(
command.name.clone(),
- r#match.positions.clone(),
+ matching_command.positions.clone(),
))
.children(KeyBinding::for_action_in(
&*command.action,
@@ -512,19 +500,28 @@ mod tests {
#[test]
fn test_normalize_query() {
- assert_eq!(normalize_query("editor: backspace"), "editor: backspace");
- assert_eq!(normalize_query("editor: backspace"), "editor: backspace");
- assert_eq!(normalize_query("editor: backspace"), "editor: backspace");
assert_eq!(
- normalize_query("editor::GoToDefinition"),
+ normalize_action_query("editor: backspace"),
+ "editor: backspace"
+ );
+ assert_eq!(
+ normalize_action_query("editor: backspace"),
+ "editor: backspace"
+ );
+ assert_eq!(
+ normalize_action_query("editor: backspace"),
+ "editor: backspace"
+ );
+ assert_eq!(
+ normalize_action_query("editor::GoToDefinition"),
"editor:GoToDefinition"
);
assert_eq!(
- normalize_query("editor::::GoToDefinition"),
+ normalize_action_query("editor::::GoToDefinition"),
"editor:GoToDefinition"
);
assert_eq!(
- normalize_query("editor: :GoToDefinition"),
+ normalize_action_query("editor: :GoToDefinition"),
"editor: :GoToDefinition"
);
}
@@ -29,6 +29,7 @@ impl Display for ContextServerId {
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
pub struct ContextServerCommand {
+ #[serde(rename = "command")]
pub path: String,
pub args: Vec<String>,
pub env: Option<HashMap<String, String>>,
@@ -698,16 +698,16 @@ async fn stream_completion(
completion_url: Arc<str>,
request: Request,
) -> Result<BoxStream<'static, Result<ResponseEvent>>> {
- let is_vision_request = request.messages.last().map_or(false, |message| match message {
- ChatMessage::User { content }
- | ChatMessage::Assistant { content, .. }
- | ChatMessage::Tool { content, .. } => {
- matches!(content, ChatMessageContent::Multipart(parts) if parts.iter().any(|part| matches!(part, ChatMessagePart::Image { .. })))
- }
- _ => false,
- });
-
- let request_builder = HttpRequest::builder()
+ let is_vision_request = request.messages.iter().any(|message| match message {
+ ChatMessage::User { content }
+ | ChatMessage::Assistant { content, .. }
+ | ChatMessage::Tool { content, .. } => {
+ matches!(content, ChatMessageContent::Multipart(parts) if parts.iter().any(|part| matches!(part, ChatMessagePart::Image { .. })))
+ }
+ _ => false,
+ });
+
+ let mut request_builder = HttpRequest::builder()
.method(Method::POST)
.uri(completion_url.as_ref())
.header(
@@ -719,8 +719,12 @@ async fn stream_completion(
)
.header("Authorization", format!("Bearer {}", api_key))
.header("Content-Type", "application/json")
- .header("Copilot-Integration-Id", "vscode-chat")
- .header("Copilot-Vision-Request", is_vision_request.to_string());
+ .header("Copilot-Integration-Id", "vscode-chat");
+
+ if is_vision_request {
+ request_builder =
+ request_builder.header("Copilot-Vision-Request", is_vision_request.to_string());
+ }
let is_streaming = request.stream;
@@ -264,7 +264,8 @@ fn common_prefix<T1: Iterator<Item = char>, T2: Iterator<Item = char>>(a: T1, b:
mod tests {
use super::*;
use editor::{
- Editor, ExcerptRange, MultiBuffer, test::editor_lsp_test_context::EditorLspTestContext,
+ Editor, ExcerptRange, MultiBuffer, SelectionEffects,
+ test::editor_lsp_test_context::EditorLspTestContext,
};
use fs::FakeFs;
use futures::StreamExt;
@@ -478,7 +479,7 @@ mod tests {
// Reset the editor to verify how suggestions behave when tabbing on leading indentation.
cx.update_editor(|editor, window, cx| {
editor.set_text("fn foo() {\n \n}", window, cx);
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::new(1, 2)..Point::new(1, 2)])
});
});
@@ -767,7 +768,7 @@ mod tests {
);
_ = editor.update(cx, |editor, window, cx| {
// Ensure copilot suggestions are shown for the first excerpt.
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::new(1, 5)..Point::new(1, 5)])
});
editor.next_edit_prediction(&Default::default(), window, cx);
@@ -793,7 +794,7 @@ mod tests {
);
_ = editor.update(cx, |editor, window, cx| {
// Move to another excerpt, ensuring the suggestion gets cleared.
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
});
assert!(!editor.has_active_inline_completion());
@@ -1019,7 +1020,7 @@ mod tests {
);
_ = editor.update(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |selections| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
selections.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
});
editor.refresh_inline_completion(true, false, window, cx);
@@ -1029,7 +1030,7 @@ mod tests {
assert!(copilot_requests.try_next().is_err());
_ = editor.update(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::new(5, 0)..Point::new(5, 0)])
});
editor.refresh_inline_completion(true, false, window, cx);
@@ -10,6 +10,7 @@ use gpui::{AsyncApp, SharedString};
pub use http_client::{HttpClient, github::latest_github_release};
use language::{LanguageName, LanguageToolchainStore};
use node_runtime::NodeRuntime;
+use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::WorktreeId;
use smol::fs::File;
@@ -47,7 +48,10 @@ pub trait DapDelegate: Send + Sync + 'static {
async fn shell_env(&self) -> collections::HashMap<String, String>;
}
-#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
+#[derive(
+ Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, JsonSchema,
+)]
+#[serde(transparent)]
pub struct DebugAdapterName(pub SharedString);
impl Deref for DebugAdapterName {
@@ -25,7 +25,9 @@ anyhow.workspace = true
async-trait.workspace = true
collections.workspace = true
dap.workspace = true
+dotenvy.workspace = true
futures.workspace = true
+fs.workspace = true
gpui.workspace = true
json_dotpath.workspace = true
language.workspace = true
@@ -33,6 +35,7 @@ log.workspace = true
paths.workspace = true
serde.workspace = true
serde_json.workspace = true
+shlex.workspace = true
task.workspace = true
util.workspace = true
workspace-hack.workspace = true
@@ -22,17 +22,16 @@ impl CodeLldbDebugAdapter {
async fn request_args(
&self,
delegate: &Arc<dyn DapDelegate>,
- task_definition: &DebugTaskDefinition,
+ mut configuration: Value,
+ label: &str,
) -> Result<dap::StartDebuggingRequestArguments> {
- // CodeLLDB uses `name` for a terminal label.
- let mut configuration = task_definition.config.clone();
-
let obj = configuration
.as_object_mut()
.context("CodeLLDB is not a valid json object")?;
+ // CodeLLDB uses `name` for a terminal label.
obj.entry("name")
- .or_insert(Value::String(String::from(task_definition.label.as_ref())));
+ .or_insert(Value::String(String::from(label)));
obj.entry("cwd")
.or_insert(delegate.worktree_root_path().to_string_lossy().into());
@@ -361,17 +360,31 @@ impl DebugAdapter for CodeLldbDebugAdapter {
self.path_to_codelldb.set(path.clone()).ok();
command = Some(path);
};
-
+ let mut json_config = config.config.clone();
Ok(DebugAdapterBinary {
command: Some(command.unwrap()),
cwd: Some(delegate.worktree_root_path().to_path_buf()),
arguments: user_args.unwrap_or_else(|| {
- vec![
- "--settings".into(),
- json!({"sourceLanguages": ["cpp", "rust"]}).to_string(),
- ]
+ if let Some(config) = json_config.as_object_mut()
+ && let Some(source_languages) = config.get("sourceLanguages").filter(|value| {
+ value
+ .as_array()
+ .map_or(false, |array| array.iter().all(Value::is_string))
+ })
+ {
+ let ret = vec![
+ "--settings".into(),
+ json!({"sourceLanguages": source_languages}).to_string(),
+ ];
+ config.remove("sourceLanguages");
+ ret
+ } else {
+ vec![]
+ }
}),
- request_args: self.request_args(delegate, &config).await?,
+ request_args: self
+ .request_args(delegate, json_config, &config.label)
+ .await?,
envs: HashMap::default(),
connection: None,
})
@@ -4,7 +4,6 @@ mod go;
mod javascript;
mod php;
mod python;
-mod ruby;
use std::sync::Arc;
@@ -25,7 +24,6 @@ use gpui::{App, BorrowAppContext};
use javascript::JsDebugAdapter;
use php::PhpDebugAdapter;
use python::PythonDebugAdapter;
-use ruby::RubyDebugAdapter;
use serde_json::json;
use task::{DebugScenario, ZedDebugConfig};
@@ -35,7 +33,6 @@ pub fn init(cx: &mut App) {
registry.add_adapter(Arc::from(PythonDebugAdapter::default()));
registry.add_adapter(Arc::from(PhpDebugAdapter::default()));
registry.add_adapter(Arc::from(JsDebugAdapter::default()));
- registry.add_adapter(Arc::from(RubyDebugAdapter));
registry.add_adapter(Arc::from(GoDebugAdapter::default()));
registry.add_adapter(Arc::from(GdbDebugAdapter));
@@ -7,13 +7,22 @@ use dap::{
latest_github_release,
},
};
-
+use fs::Fs;
use gpui::{AsyncApp, SharedString};
use language::LanguageName;
-use std::{env::consts, ffi::OsStr, path::PathBuf, sync::OnceLock};
+use log::warn;
+use serde_json::{Map, Value};
use task::TcpArgumentsTemplate;
use util;
+use std::{
+ env::consts,
+ ffi::OsStr,
+ path::{Path, PathBuf},
+ str::FromStr,
+ sync::OnceLock,
+};
+
use crate::*;
#[derive(Default, Debug)]
@@ -437,22 +446,34 @@ impl DebugAdapter for GoDebugAdapter {
adapter_path.join("dlv").to_string_lossy().to_string()
};
- let cwd = task_definition
- .config
- .get("cwd")
- .and_then(|s| s.as_str())
- .map(PathBuf::from)
- .unwrap_or_else(|| delegate.worktree_root_path().to_path_buf());
+ let cwd = Some(
+ task_definition
+ .config
+ .get("cwd")
+ .and_then(|s| s.as_str())
+ .map(PathBuf::from)
+ .unwrap_or_else(|| delegate.worktree_root_path().to_path_buf()),
+ );
let arguments;
let command;
let connection;
let mut configuration = task_definition.config.clone();
+ let mut envs = HashMap::default();
+
if let Some(configuration) = configuration.as_object_mut() {
configuration
.entry("cwd")
.or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
+
+ handle_envs(
+ configuration,
+ &mut envs,
+ cwd.as_deref(),
+ delegate.fs().clone(),
+ )
+ .await;
}
if let Some(connection_options) = &task_definition.tcp_connection {
@@ -494,8 +515,8 @@ impl DebugAdapter for GoDebugAdapter {
Ok(DebugAdapterBinary {
command,
arguments,
- cwd: Some(cwd),
- envs: HashMap::default(),
+ cwd,
+ envs,
connection,
request_args: StartDebuggingRequestArguments {
configuration,
@@ -504,3 +525,44 @@ impl DebugAdapter for GoDebugAdapter {
})
}
}
+
+// delve doesn't do anything with the envFile setting, so we intercept it
+async fn handle_envs(
+ config: &mut Map<String, Value>,
+ envs: &mut HashMap<String, String>,
+ cwd: Option<&Path>,
+ fs: Arc<dyn Fs>,
+) -> Option<()> {
+ let env_files = match config.get("envFile")? {
+ Value::Array(arr) => arr.iter().map(|v| v.as_str()).collect::<Vec<_>>(),
+ Value::String(s) => vec![Some(s.as_str())],
+ _ => return None,
+ };
+
+ let rebase_path = |path: PathBuf| {
+ if path.is_absolute() {
+ Some(path)
+ } else {
+ cwd.map(|p| p.join(path))
+ }
+ };
+
+ for path in env_files {
+ let Some(path) = path
+ .and_then(|s| PathBuf::from_str(s).ok())
+ .and_then(rebase_path)
+ else {
+ continue;
+ };
+
+ if let Ok(file) = fs.open_sync(&path).await {
+ envs.extend(dotenvy::from_read_iter(file).filter_map(Result::ok))
+ } else {
+ warn!("While starting Go debug session: failed to read env file {path:?}");
+ };
+ }
+
+ // remove envFile now that it's been handled
+ config.remove("entry");
+ Some(())
+}
@@ -5,7 +5,7 @@ use gpui::AsyncApp;
use serde_json::Value;
use std::{collections::HashMap, path::PathBuf, sync::OnceLock};
use task::DebugRequest;
-use util::ResultExt;
+use util::{ResultExt, maybe};
use crate::*;
@@ -72,6 +72,24 @@ impl JsDebugAdapter {
let mut configuration = task_definition.config.clone();
if let Some(configuration) = configuration.as_object_mut() {
+ maybe!({
+ configuration
+ .get("type")
+ .filter(|value| value == &"node-terminal")?;
+ let command = configuration.get("command")?.as_str()?.to_owned();
+ let mut args = shlex::split(&command)?.into_iter();
+ let program = args.next()?;
+ configuration.insert("program".to_owned(), program.into());
+ configuration.insert(
+ "args".to_owned(),
+ args.map(Value::from).collect::<Vec<_>>().into(),
+ );
+ configuration.insert("console".to_owned(), "externalTerminal".into());
+ Some(())
+ });
+
+ configuration.entry("type").and_modify(normalize_task_type);
+
if let Some(program) = configuration
.get("program")
.cloned()
@@ -96,7 +114,6 @@ impl JsDebugAdapter {
.entry("cwd")
.or_insert(delegate.worktree_root_path().to_string_lossy().into());
- configuration.entry("type").and_modify(normalize_task_type);
configuration
.entry("console")
.or_insert("externalTerminal".into());
@@ -265,6 +282,10 @@ impl DebugAdapter for JsDebugAdapter {
"description": "Automatically stop program after launch",
"default": false
},
+ "attachSimplePort": {
+ "type": "number",
+ "description": "If set, attaches to the process via the given port. This is generally no longer necessary for Node.js programs and loses the ability to debug child processes, but can be useful in more esoteric scenarios such as with Deno and Docker launches. If set to 0, a random port will be chosen and --inspect-brk added to the launch arguments automatically."
+ },
"runtimeExecutable": {
"type": ["string", "null"],
"description": "Runtime to use, an absolute path or the name of a runtime available on PATH",
@@ -512,7 +533,7 @@ fn normalize_task_type(task_type: &mut Value) {
};
let new_name = match task_type_str {
- "node" | "pwa-node" => "pwa-node",
+ "node" | "pwa-node" | "node-terminal" => "pwa-node",
"chrome" | "pwa-chrome" => "pwa-chrome",
"edge" | "msedge" | "pwa-edge" | "pwa-msedge" => "pwa-msedge",
_ => task_type_str,
@@ -1,208 +0,0 @@
-use anyhow::{Result, bail};
-use async_trait::async_trait;
-use collections::FxHashMap;
-use dap::{
- DebugRequest, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
- adapters::{
- DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
- },
-};
-use gpui::{AsyncApp, SharedString};
-use language::LanguageName;
-use serde::{Deserialize, Serialize};
-use serde_json::json;
-use std::path::PathBuf;
-use std::{ffi::OsStr, sync::Arc};
-use task::{DebugScenario, ZedDebugConfig};
-use util::command::new_smol_command;
-
-#[derive(Default)]
-pub(crate) struct RubyDebugAdapter;
-
-impl RubyDebugAdapter {
- const ADAPTER_NAME: &'static str = "Ruby";
-}
-
-#[derive(Serialize, Deserialize)]
-struct RubyDebugConfig {
- script_or_command: Option<String>,
- script: Option<String>,
- command: Option<String>,
- #[serde(default)]
- args: Vec<String>,
- #[serde(default)]
- env: FxHashMap<String, String>,
- cwd: Option<PathBuf>,
-}
-
-#[async_trait(?Send)]
-impl DebugAdapter for RubyDebugAdapter {
- fn name(&self) -> DebugAdapterName {
- DebugAdapterName(Self::ADAPTER_NAME.into())
- }
-
- fn adapter_language_name(&self) -> Option<LanguageName> {
- Some(SharedString::new_static("Ruby").into())
- }
-
- async fn request_kind(
- &self,
- _: &serde_json::Value,
- ) -> Result<StartDebuggingRequestArgumentsRequest> {
- Ok(StartDebuggingRequestArgumentsRequest::Launch)
- }
-
- fn dap_schema(&self) -> serde_json::Value {
- json!({
- "type": "object",
- "properties": {
- "command": {
- "type": "string",
- "description": "Command name (ruby, rake, bin/rails, bundle exec ruby, etc)",
- },
- "script": {
- "type": "string",
- "description": "Absolute path to a Ruby file."
- },
- "cwd": {
- "type": "string",
- "description": "Directory to execute the program in",
- "default": "${ZED_WORKTREE_ROOT}"
- },
- "args": {
- "type": "array",
- "description": "Command line arguments passed to the program",
- "items": {
- "type": "string"
- },
- "default": []
- },
- "env": {
- "type": "object",
- "description": "Additional environment variables to pass to the debugging (and debugged) process",
- "default": {}
- },
- }
- })
- }
-
- async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
- match zed_scenario.request {
- DebugRequest::Launch(launch) => {
- let config = RubyDebugConfig {
- script_or_command: Some(launch.program),
- script: None,
- command: None,
- args: launch.args,
- env: launch.env,
- cwd: launch.cwd.clone(),
- };
-
- let config = serde_json::to_value(config)?;
-
- Ok(DebugScenario {
- adapter: zed_scenario.adapter,
- label: zed_scenario.label,
- config,
- tcp_connection: None,
- build: None,
- })
- }
- DebugRequest::Attach(_) => {
- anyhow::bail!("Attach requests are unsupported");
- }
- }
- }
-
- async fn get_binary(
- &self,
- delegate: &Arc<dyn DapDelegate>,
- definition: &DebugTaskDefinition,
- _user_installed_path: Option<PathBuf>,
- _user_args: Option<Vec<String>>,
- _cx: &mut AsyncApp,
- ) -> Result<DebugAdapterBinary> {
- let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
- let mut rdbg_path = adapter_path.join("rdbg");
- if !delegate.fs().is_file(&rdbg_path).await {
- match delegate.which("rdbg".as_ref()).await {
- Some(path) => rdbg_path = path,
- None => {
- delegate.output_to_console(
- "rdbg not found on path, trying `gem install debug`".to_string(),
- );
- let output = new_smol_command("gem")
- .arg("install")
- .arg("--no-document")
- .arg("--bindir")
- .arg(adapter_path)
- .arg("debug")
- .output()
- .await?;
- anyhow::ensure!(
- output.status.success(),
- "Failed to install rdbg:\n{}",
- String::from_utf8_lossy(&output.stderr).to_string()
- );
- }
- }
- }
-
- let tcp_connection = definition.tcp_connection.clone().unwrap_or_default();
- let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
- let ruby_config = serde_json::from_value::<RubyDebugConfig>(definition.config.clone())?;
-
- let mut arguments = vec![
- "--open".to_string(),
- format!("--port={}", port),
- format!("--host={}", host),
- ];
-
- if let Some(script) = &ruby_config.script {
- arguments.push(script.clone());
- } else if let Some(command) = &ruby_config.command {
- arguments.push("--command".to_string());
- arguments.push(command.clone());
- } else if let Some(command_or_script) = &ruby_config.script_or_command {
- if delegate
- .which(OsStr::new(&command_or_script))
- .await
- .is_some()
- {
- arguments.push("--command".to_string());
- }
- arguments.push(command_or_script.clone());
- } else {
- bail!("Ruby debug config must have 'script' or 'command' args");
- }
-
- arguments.extend(ruby_config.args);
-
- let mut configuration = definition.config.clone();
- if let Some(configuration) = configuration.as_object_mut() {
- configuration
- .entry("cwd")
- .or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
- }
-
- Ok(DebugAdapterBinary {
- command: Some(rdbg_path.to_string_lossy().to_string()),
- arguments,
- connection: Some(dap::adapters::TcpArguments {
- host,
- port,
- timeout,
- }),
- cwd: Some(
- ruby_config
- .cwd
- .unwrap_or(delegate.worktree_root_path().to_owned()),
- ),
- envs: ruby_config.env.into_iter().collect(),
- request_args: StartDebuggingRequestArguments {
- request: self.request_kind(&definition.config).await?,
- configuration,
- },
- })
- }
-}
@@ -21,7 +21,7 @@ use project::{
use settings::Settings as _;
use std::{
borrow::Cow,
- collections::{HashMap, VecDeque},
+ collections::{BTreeMap, HashMap, VecDeque},
sync::Arc,
};
use util::maybe;
@@ -32,13 +32,6 @@ use workspace::{
ui::{Button, Clickable, ContextMenu, Label, LabelCommon, PopoverMenu, h_flex},
};
-// TODO:
-// - [x] stop sorting by session ID
-// - [x] pick the most recent session by default (logs if available, RPC messages otherwise)
-// - [ ] dump the launch/attach request somewhere (logs?)
-
-const MAX_SESSIONS: usize = 10;
-
struct DapLogView {
editor: Entity<Editor>,
focus_handle: FocusHandle,
@@ -49,14 +42,34 @@ struct DapLogView {
_subscriptions: Vec<Subscription>,
}
+struct LogStoreEntryIdentifier<'a> {
+ session_id: SessionId,
+ project: Cow<'a, WeakEntity<Project>>,
+}
+impl LogStoreEntryIdentifier<'_> {
+ fn to_owned(&self) -> LogStoreEntryIdentifier<'static> {
+ LogStoreEntryIdentifier {
+ session_id: self.session_id,
+ project: Cow::Owned(self.project.as_ref().clone()),
+ }
+ }
+}
+
+struct LogStoreMessage {
+ id: LogStoreEntryIdentifier<'static>,
+ kind: IoKind,
+ command: Option<SharedString>,
+ message: SharedString,
+}
+
pub struct LogStore {
projects: HashMap<WeakEntity<Project>, ProjectState>,
- debug_sessions: VecDeque<DebugAdapterState>,
- rpc_tx: UnboundedSender<(SessionId, IoKind, Option<SharedString>, SharedString)>,
- adapter_log_tx: UnboundedSender<(SessionId, IoKind, Option<SharedString>, SharedString)>,
+ rpc_tx: UnboundedSender<LogStoreMessage>,
+ adapter_log_tx: UnboundedSender<LogStoreMessage>,
}
struct ProjectState {
+ debug_sessions: BTreeMap<SessionId, DebugAdapterState>,
_subscriptions: [gpui::Subscription; 2],
}
@@ -122,13 +135,12 @@ impl DebugAdapterState {
impl LogStore {
pub fn new(cx: &Context<Self>) -> Self {
- let (rpc_tx, mut rpc_rx) =
- unbounded::<(SessionId, IoKind, Option<SharedString>, SharedString)>();
+ let (rpc_tx, mut rpc_rx) = unbounded::<LogStoreMessage>();
cx.spawn(async move |this, cx| {
- while let Some((session_id, io_kind, command, message)) = rpc_rx.next().await {
+ while let Some(message) = rpc_rx.next().await {
if let Some(this) = this.upgrade() {
this.update(cx, |this, cx| {
- this.add_debug_adapter_message(session_id, io_kind, command, message, cx);
+ this.add_debug_adapter_message(message, cx);
})?;
}
@@ -138,13 +150,12 @@ impl LogStore {
})
.detach_and_log_err(cx);
- let (adapter_log_tx, mut adapter_log_rx) =
- unbounded::<(SessionId, IoKind, Option<SharedString>, SharedString)>();
+ let (adapter_log_tx, mut adapter_log_rx) = unbounded::<LogStoreMessage>();
cx.spawn(async move |this, cx| {
- while let Some((session_id, io_kind, _, message)) = adapter_log_rx.next().await {
+ while let Some(message) = adapter_log_rx.next().await {
if let Some(this) = this.upgrade() {
this.update(cx, |this, cx| {
- this.add_debug_adapter_log(session_id, io_kind, message, cx);
+ this.add_debug_adapter_log(message, cx);
})?;
}
@@ -157,57 +168,76 @@ impl LogStore {
rpc_tx,
adapter_log_tx,
projects: HashMap::new(),
- debug_sessions: Default::default(),
}
}
pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) {
- let weak_project = project.downgrade();
self.projects.insert(
project.downgrade(),
ProjectState {
_subscriptions: [
- cx.observe_release(project, move |this, _, _| {
- this.projects.remove(&weak_project);
+ cx.observe_release(project, {
+ let weak_project = project.downgrade();
+ move |this, _, _| {
+ this.projects.remove(&weak_project);
+ }
}),
- cx.subscribe(
- &project.read(cx).dap_store(),
- |this, dap_store, event, cx| match event {
+ cx.subscribe(&project.read(cx).dap_store(), {
+ let weak_project = project.downgrade();
+ move |this, dap_store, event, cx| match event {
dap_store::DapStoreEvent::DebugClientStarted(session_id) => {
let session = dap_store.read(cx).session_by_id(session_id);
if let Some(session) = session {
- this.add_debug_session(*session_id, session, cx);
+ this.add_debug_session(
+ LogStoreEntryIdentifier {
+ project: Cow::Owned(weak_project.clone()),
+ session_id: *session_id,
+ },
+ session,
+ cx,
+ );
}
}
dap_store::DapStoreEvent::DebugClientShutdown(session_id) => {
- this.get_debug_adapter_state(*session_id)
- .iter_mut()
- .for_each(|state| state.is_terminated = true);
+ let id = LogStoreEntryIdentifier {
+ project: Cow::Borrowed(&weak_project),
+ session_id: *session_id,
+ };
+ if let Some(state) = this.get_debug_adapter_state(&id) {
+ state.is_terminated = true;
+ }
+
this.clean_sessions(cx);
}
_ => {}
- },
- ),
+ }
+ }),
],
+ debug_sessions: Default::default(),
},
);
}
- fn get_debug_adapter_state(&mut self, id: SessionId) -> Option<&mut DebugAdapterState> {
- self.debug_sessions
- .iter_mut()
- .find(|adapter_state| adapter_state.id == id)
+ fn get_debug_adapter_state(
+ &mut self,
+ id: &LogStoreEntryIdentifier<'_>,
+ ) -> Option<&mut DebugAdapterState> {
+ self.projects
+ .get_mut(&id.project)
+ .and_then(|state| state.debug_sessions.get_mut(&id.session_id))
}
fn add_debug_adapter_message(
&mut self,
- id: SessionId,
- io_kind: IoKind,
- command: Option<SharedString>,
- message: SharedString,
+ LogStoreMessage {
+ id,
+ kind: io_kind,
+ command,
+ message,
+ }: LogStoreMessage,
cx: &mut Context<Self>,
) {
- let Some(debug_client_state) = self.get_debug_adapter_state(id) else {
+ let Some(debug_client_state) = self.get_debug_adapter_state(&id) else {
return;
};
@@ -229,7 +259,7 @@ impl LogStore {
if rpc_messages.last_message_kind != Some(kind) {
Self::get_debug_adapter_entry(
&mut rpc_messages.messages,
- id,
+ id.to_owned(),
kind.label().into(),
LogKind::Rpc,
cx,
@@ -239,7 +269,7 @@ impl LogStore {
let entry = Self::get_debug_adapter_entry(
&mut rpc_messages.messages,
- id,
+ id.to_owned(),
message,
LogKind::Rpc,
cx,
@@ -260,12 +290,15 @@ impl LogStore {
fn add_debug_adapter_log(
&mut self,
- id: SessionId,
- io_kind: IoKind,
- message: SharedString,
+ LogStoreMessage {
+ id,
+ kind: io_kind,
+ message,
+ ..
+ }: LogStoreMessage,
cx: &mut Context<Self>,
) {
- let Some(debug_adapter_state) = self.get_debug_adapter_state(id) else {
+ let Some(debug_adapter_state) = self.get_debug_adapter_state(&id) else {
return;
};
@@ -276,7 +309,7 @@ impl LogStore {
Self::get_debug_adapter_entry(
&mut debug_adapter_state.log_messages,
- id,
+ id.to_owned(),
message,
LogKind::Adapter,
cx,
@@ -286,13 +319,17 @@ impl LogStore {
fn get_debug_adapter_entry(
log_lines: &mut VecDeque<SharedString>,
- id: SessionId,
+ id: LogStoreEntryIdentifier<'static>,
message: SharedString,
kind: LogKind,
cx: &mut Context<Self>,
) -> SharedString {
- while log_lines.len() >= RpcMessages::MESSAGE_QUEUE_LIMIT {
- log_lines.pop_front();
+ if let Some(excess) = log_lines
+ .len()
+ .checked_sub(RpcMessages::MESSAGE_QUEUE_LIMIT)
+ && excess > 0
+ {
+ log_lines.drain(..excess);
}
let format_messages = DebuggerSettings::get_global(cx).format_dap_log_messages;
@@ -322,118 +359,116 @@ impl LogStore {
fn add_debug_session(
&mut self,
- session_id: SessionId,
+ id: LogStoreEntryIdentifier<'static>,
session: Entity<Session>,
cx: &mut Context<Self>,
) {
- if self
- .debug_sessions
- .iter_mut()
- .any(|adapter_state| adapter_state.id == session_id)
- {
- return;
- }
-
- let (adapter_name, has_adapter_logs) = session.read_with(cx, |session, _| {
- (
- session.adapter(),
- session
- .adapter_client()
- .map(|client| client.has_adapter_logs())
- .unwrap_or(false),
- )
- });
-
- self.debug_sessions.push_back(DebugAdapterState::new(
- session_id,
- adapter_name,
- has_adapter_logs,
- ));
-
- self.clean_sessions(cx);
-
- let io_tx = self.rpc_tx.clone();
-
- let Some(client) = session.read(cx).adapter_client() else {
- return;
- };
+ maybe!({
+ let project_entry = self.projects.get_mut(&id.project)?;
+ let std::collections::btree_map::Entry::Vacant(state) =
+ project_entry.debug_sessions.entry(id.session_id)
+ else {
+ return None;
+ };
+
+ let (adapter_name, has_adapter_logs) = session.read_with(cx, |session, _| {
+ (
+ session.adapter(),
+ session
+ .adapter_client()
+ .map_or(false, |client| client.has_adapter_logs()),
+ )
+ });
- client.add_log_handler(
- move |io_kind, command, message| {
- io_tx
- .unbounded_send((
- session_id,
- io_kind,
- command.map(|command| command.to_owned().into()),
- message.to_owned().into(),
- ))
- .ok();
- },
- LogKind::Rpc,
- );
+ state.insert(DebugAdapterState::new(
+ id.session_id,
+ adapter_name,
+ has_adapter_logs,
+ ));
+
+ self.clean_sessions(cx);
+
+ let io_tx = self.rpc_tx.clone();
+
+ let client = session.read(cx).adapter_client()?;
+ let project = id.project.clone();
+ let session_id = id.session_id;
+ client.add_log_handler(
+ move |kind, command, message| {
+ io_tx
+ .unbounded_send(LogStoreMessage {
+ id: LogStoreEntryIdentifier {
+ session_id,
+ project: project.clone(),
+ },
+ kind,
+ command: command.map(|command| command.to_owned().into()),
+ message: message.to_owned().into(),
+ })
+ .ok();
+ },
+ LogKind::Rpc,
+ );
- let log_io_tx = self.adapter_log_tx.clone();
- client.add_log_handler(
- move |io_kind, command, message| {
- log_io_tx
- .unbounded_send((
- session_id,
- io_kind,
- command.map(|command| command.to_owned().into()),
- message.to_owned().into(),
- ))
- .ok();
- },
- LogKind::Adapter,
- );
+ let log_io_tx = self.adapter_log_tx.clone();
+ let project = id.project;
+ client.add_log_handler(
+ move |kind, command, message| {
+ log_io_tx
+ .unbounded_send(LogStoreMessage {
+ id: LogStoreEntryIdentifier {
+ session_id,
+ project: project.clone(),
+ },
+ kind,
+ command: command.map(|command| command.to_owned().into()),
+ message: message.to_owned().into(),
+ })
+ .ok();
+ },
+ LogKind::Adapter,
+ );
+ Some(())
+ });
}
fn clean_sessions(&mut self, cx: &mut Context<Self>) {
- let mut to_remove = self.debug_sessions.len().saturating_sub(MAX_SESSIONS);
- self.debug_sessions.retain(|session| {
- if to_remove > 0 && session.is_terminated {
- to_remove -= 1;
- return false;
- }
- true
+ self.projects.values_mut().for_each(|project| {
+ let mut allowed_terminated_sessions = 10u32;
+ project.debug_sessions.retain(|_, session| {
+ if !session.is_terminated {
+ return true;
+ }
+ allowed_terminated_sessions = allowed_terminated_sessions.saturating_sub(1);
+ allowed_terminated_sessions > 0
+ });
});
+
cx.notify();
}
fn log_messages_for_session(
&mut self,
- session_id: SessionId,
+ id: &LogStoreEntryIdentifier<'_>,
) -> Option<&mut VecDeque<SharedString>> {
- self.debug_sessions
- .iter_mut()
- .find(|session| session.id == session_id)
+ self.get_debug_adapter_state(id)
.map(|state| &mut state.log_messages)
}
fn rpc_messages_for_session(
&mut self,
- session_id: SessionId,
+ id: &LogStoreEntryIdentifier<'_>,
) -> Option<&mut VecDeque<SharedString>> {
- self.debug_sessions.iter_mut().find_map(|state| {
- if state.id == session_id {
- Some(&mut state.rpc_messages.messages)
- } else {
- None
- }
- })
+ self.get_debug_adapter_state(id)
+ .map(|state| &mut state.rpc_messages.messages)
}
fn initialization_sequence_for_session(
&mut self,
- session_id: SessionId,
- ) -> Option<&mut Vec<SharedString>> {
- self.debug_sessions.iter_mut().find_map(|state| {
- if state.id == session_id {
- Some(&mut state.rpc_messages.initialization_sequence)
- } else {
- None
- }
- })
+ id: &LogStoreEntryIdentifier<'_>,
+ ) -> Option<&Vec<SharedString>> {
+ self.get_debug_adapter_state(&id)
+ .map(|state| &state.rpc_messages.initialization_sequence)
}
}
@@ -453,10 +488,11 @@ impl Render for DapLogToolbarItemView {
return Empty.into_any_element();
};
- let (menu_rows, current_session_id) = log_view.update(cx, |log_view, cx| {
+ let (menu_rows, current_session_id, project) = log_view.update(cx, |log_view, cx| {
(
log_view.menu_items(cx),
log_view.current_view.map(|(session_id, _)| session_id),
+ log_view.project.downgrade(),
)
});
@@ -484,6 +520,7 @@ impl Render for DapLogToolbarItemView {
.menu(move |mut window, cx| {
let log_view = log_view.clone();
let menu_rows = menu_rows.clone();
+ let project = project.clone();
ContextMenu::build(&mut window, cx, move |mut menu, window, _cx| {
for row in menu_rows.into_iter() {
menu = menu.custom_row(move |_window, _cx| {
@@ -509,8 +546,15 @@ impl Render for DapLogToolbarItemView {
.child(Label::new(ADAPTER_LOGS))
.into_any_element()
},
- window.handler_for(&log_view, move |view, window, cx| {
- view.show_log_messages_for_adapter(row.session_id, window, cx);
+ window.handler_for(&log_view, {
+ let project = project.clone();
+ let id = LogStoreEntryIdentifier {
+ project: Cow::Owned(project),
+ session_id: row.session_id,
+ };
+ move |view, window, cx| {
+ view.show_log_messages_for_adapter(&id, window, cx);
+ }
}),
);
}
@@ -524,8 +568,15 @@ impl Render for DapLogToolbarItemView {
.child(Label::new(RPC_MESSAGES))
.into_any_element()
},
- window.handler_for(&log_view, move |view, window, cx| {
- view.show_rpc_trace_for_server(row.session_id, window, cx);
+ window.handler_for(&log_view, {
+ let project = project.clone();
+ let id = LogStoreEntryIdentifier {
+ project: Cow::Owned(project),
+ session_id: row.session_id,
+ };
+ move |view, window, cx| {
+ view.show_rpc_trace_for_server(&id, window, cx);
+ }
}),
)
.custom_entry(
@@ -536,12 +587,17 @@ impl Render for DapLogToolbarItemView {
.child(Label::new(INITIALIZATION_SEQUENCE))
.into_any_element()
},
- window.handler_for(&log_view, move |view, window, cx| {
- view.show_initialization_sequence_for_server(
- row.session_id,
- window,
- cx,
- );
+ window.handler_for(&log_view, {
+ let project = project.clone();
+ let id = LogStoreEntryIdentifier {
+ project: Cow::Owned(project),
+ session_id: row.session_id,
+ };
+ move |view, window, cx| {
+ view.show_initialization_sequence_for_server(
+ &id, window, cx,
+ );
+ }
}),
);
}
@@ -613,7 +669,9 @@ impl DapLogView {
let events_subscriptions = cx.subscribe(&log_store, |log_view, _, event, cx| match event {
Event::NewLogEntry { id, entry, kind } => {
- if log_view.current_view == Some((*id, *kind)) {
+ if log_view.current_view == Some((id.session_id, *kind))
+ && log_view.project == *id.project
+ {
log_view.editor.update(cx, |editor, cx| {
editor.set_read_only(false);
let last_point = editor.buffer().read(cx).len(cx);
@@ -629,12 +687,18 @@ impl DapLogView {
}
}
});
-
+ let weak_project = project.downgrade();
let state_info = log_store
.read(cx)
- .debug_sessions
- .back()
- .map(|session| (session.id, session.has_adapter_logs));
+ .projects
+ .get(&weak_project)
+ .and_then(|project| {
+ project
+ .debug_sessions
+ .values()
+ .next_back()
+ .map(|session| (session.id, session.has_adapter_logs))
+ });
let mut this = Self {
editor,
@@ -647,10 +711,14 @@ impl DapLogView {
};
if let Some((session_id, have_adapter_logs)) = state_info {
+ let id = LogStoreEntryIdentifier {
+ session_id,
+ project: Cow::Owned(weak_project),
+ };
if have_adapter_logs {
- this.show_log_messages_for_adapter(session_id, window, cx);
+ this.show_log_messages_for_adapter(&id, window, cx);
} else {
- this.show_rpc_trace_for_server(session_id, window, cx);
+ this.show_rpc_trace_for_server(&id, window, cx);
}
}
@@ -690,31 +758,38 @@ impl DapLogView {
fn menu_items(&self, cx: &App) -> Vec<DapMenuItem> {
self.log_store
.read(cx)
- .debug_sessions
- .iter()
- .rev()
- .map(|state| DapMenuItem {
- session_id: state.id,
- adapter_name: state.adapter_name.clone(),
- has_adapter_logs: state.has_adapter_logs,
- selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind),
+ .projects
+ .get(&self.project.downgrade())
+ .map_or_else(Vec::new, |state| {
+ state
+ .debug_sessions
+ .values()
+ .rev()
+ .map(|state| DapMenuItem {
+ session_id: state.id,
+ adapter_name: state.adapter_name.clone(),
+ has_adapter_logs: state.has_adapter_logs,
+ selected_entry: self
+ .current_view
+ .map_or(LogKind::Adapter, |(_, kind)| kind),
+ })
+ .collect::<Vec<_>>()
})
- .collect::<Vec<_>>()
}
fn show_rpc_trace_for_server(
&mut self,
- session_id: SessionId,
+ id: &LogStoreEntryIdentifier<'_>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let rpc_log = self.log_store.update(cx, |log_store, _| {
log_store
- .rpc_messages_for_session(session_id)
+ .rpc_messages_for_session(id)
.map(|state| log_contents(state.iter().cloned()))
});
if let Some(rpc_log) = rpc_log {
- self.current_view = Some((session_id, LogKind::Rpc));
+ self.current_view = Some((id.session_id, LogKind::Rpc));
let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
let language = self.project.read(cx).languages().language_for_name("JSON");
editor
@@ -725,8 +800,7 @@ impl DapLogView {
.expect("log buffer should be a singleton")
.update(cx, |_, cx| {
cx.spawn({
- let buffer = cx.entity();
- async move |_, cx| {
+ async move |buffer, cx| {
let language = language.await.ok();
buffer.update(cx, |buffer, cx| {
buffer.set_language(language, cx);
@@ -746,17 +820,17 @@ impl DapLogView {
fn show_log_messages_for_adapter(
&mut self,
- session_id: SessionId,
+ id: &LogStoreEntryIdentifier<'_>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let message_log = self.log_store.update(cx, |log_store, _| {
log_store
- .log_messages_for_session(session_id)
+ .log_messages_for_session(id)
.map(|state| log_contents(state.iter().cloned()))
});
if let Some(message_log) = message_log {
- self.current_view = Some((session_id, LogKind::Adapter));
+ self.current_view = Some((id.session_id, LogKind::Adapter));
let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, window, cx);
editor
.read(cx)
@@ -775,17 +849,17 @@ impl DapLogView {
fn show_initialization_sequence_for_server(
&mut self,
- session_id: SessionId,
+ id: &LogStoreEntryIdentifier<'_>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let rpc_log = self.log_store.update(cx, |log_store, _| {
log_store
- .initialization_sequence_for_session(session_id)
+ .initialization_sequence_for_session(id)
.map(|state| log_contents(state.iter().cloned()))
});
if let Some(rpc_log) = rpc_log {
- self.current_view = Some((session_id, LogKind::Rpc));
+ self.current_view = Some((id.session_id, LogKind::Rpc));
let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
let language = self.project.read(cx).languages().language_for_name("JSON");
editor
@@ -993,9 +1067,9 @@ impl Focusable for DapLogView {
}
}
-pub enum Event {
+enum Event {
NewLogEntry {
- id: SessionId,
+ id: LogStoreEntryIdentifier<'static>,
entry: SharedString,
kind: LogKind,
},
@@ -1008,31 +1082,30 @@ impl EventEmitter<SearchEvent> for DapLogView {}
#[cfg(any(test, feature = "test-support"))]
impl LogStore {
- pub fn contained_session_ids(&self) -> Vec<SessionId> {
- self.debug_sessions
- .iter()
- .map(|session| session.id)
- .collect()
+ pub fn has_projects(&self) -> bool {
+ !self.projects.is_empty()
}
- pub fn rpc_messages_for_session_id(&self, session_id: SessionId) -> Vec<SharedString> {
- self.debug_sessions
- .iter()
- .find(|adapter_state| adapter_state.id == session_id)
- .expect("This session should exist if a test is calling")
- .rpc_messages
- .messages
- .clone()
- .into()
+ pub fn contained_session_ids(&self, project: &WeakEntity<Project>) -> Vec<SessionId> {
+ self.projects.get(project).map_or(vec![], |state| {
+ state.debug_sessions.keys().copied().collect()
+ })
}
- pub fn log_messages_for_session_id(&self, session_id: SessionId) -> Vec<SharedString> {
- self.debug_sessions
- .iter()
- .find(|adapter_state| adapter_state.id == session_id)
- .expect("This session should exist if a test is calling")
- .log_messages
- .clone()
- .into()
+ pub fn rpc_messages_for_session_id(
+ &self,
+ project: &WeakEntity<Project>,
+ session_id: SessionId,
+ ) -> Vec<SharedString> {
+ self.projects.get(&project).map_or(vec![], |state| {
+ state
+ .debug_sessions
+ .get(&session_id)
+ .expect("This session should exist if a test is calling")
+ .rpc_messages
+ .messages
+ .clone()
+ .into()
+ })
}
}
@@ -28,6 +28,7 @@ test-support = [
[dependencies]
alacritty_terminal.workspace = true
anyhow.workspace = true
+bitflags.workspace = true
client.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
@@ -100,7 +100,13 @@ impl DebugPanel {
sessions: vec![],
active_session: None,
focus_handle,
- breakpoint_list: BreakpointList::new(None, workspace.weak_handle(), &project, cx),
+ breakpoint_list: BreakpointList::new(
+ None,
+ workspace.weak_handle(),
+ &project,
+ window,
+ cx,
+ ),
project,
workspace: workspace.weak_handle(),
context_menu: None,
@@ -862,7 +868,7 @@ impl DebugPanel {
let threads =
running_state.update(cx, |running_state, cx| {
let session = running_state.session();
- session.read(cx).is_running().then(|| {
+ session.read(cx).is_started().then(|| {
session.update(cx, |session, cx| {
session.threads(cx)
})
@@ -1292,6 +1298,11 @@ impl Render for DebugPanel {
}
v_flex()
+ .when_else(
+ self.position(window, cx) == DockPosition::Bottom,
+ |this| this.max_h(self.size),
+ |this| this.max_w(self.size),
+ )
.size_full()
.key_context("DebugPanel")
.child(h_flex().children(self.top_controls_strip(window, cx)))
@@ -1462,6 +1473,94 @@ impl Render for DebugPanel {
if has_sessions {
this.children(self.active_session.clone())
} else {
+ let docked_to_bottom = self.position(window, cx) == DockPosition::Bottom;
+ let welcome_experience = v_flex()
+ .when_else(
+ docked_to_bottom,
+ |this| this.w_2_3().h_full().pr_8(),
+ |this| this.w_full().h_1_3(),
+ )
+ .items_center()
+ .justify_center()
+ .gap_2()
+ .child(
+ Button::new("spawn-new-session-empty-state", "New Session")
+ .icon(IconName::Plus)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Muted)
+ .icon_position(IconPosition::Start)
+ .on_click(|_, window, cx| {
+ window.dispatch_action(crate::Start.boxed_clone(), cx);
+ }),
+ )
+ .child(
+ Button::new("edit-debug-settings", "Edit debug.json")
+ .icon(IconName::Code)
+ .icon_size(IconSize::XSmall)
+ .color(Color::Muted)
+ .icon_color(Color::Muted)
+ .icon_position(IconPosition::Start)
+ .on_click(|_, window, cx| {
+ window.dispatch_action(
+ zed_actions::OpenProjectDebugTasks.boxed_clone(),
+ cx,
+ );
+ }),
+ )
+ .child(
+ Button::new("open-debugger-docs", "Debugger Docs")
+ .icon(IconName::Book)
+ .color(Color::Muted)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Muted)
+ .icon_position(IconPosition::Start)
+ .on_click(|_, _, cx| cx.open_url("https://zed.dev/docs/debugger")),
+ )
+ .child(
+ Button::new(
+ "spawn-new-session-install-extensions",
+ "Debugger Extensions",
+ )
+ .icon(IconName::Blocks)
+ .color(Color::Muted)
+ .icon_size(IconSize::XSmall)
+ .icon_color(Color::Muted)
+ .icon_position(IconPosition::Start)
+ .on_click(|_, window, cx| {
+ window.dispatch_action(
+ zed_actions::Extensions {
+ category_filter: Some(
+ zed_actions::ExtensionCategoryFilter::DebugAdapters,
+ ),
+ }
+ .boxed_clone(),
+ cx,
+ );
+ }),
+ );
+ let breakpoint_list =
+ v_flex()
+ .group("base-breakpoint-list")
+ .items_start()
+ .when_else(
+ docked_to_bottom,
+ |this| this.min_w_1_3().h_full(),
+ |this| this.w_full().h_2_3(),
+ )
+ .p_1()
+ .child(
+ h_flex()
+ .pl_1()
+ .w_full()
+ .justify_between()
+ .child(Label::new("Breakpoints").size(LabelSize::Small))
+ .child(h_flex().visible_on_hover("base-breakpoint-list").child(
+ self.breakpoint_list.read(cx).render_control_strip(),
+ ))
+ .track_focus(&self.breakpoint_list.focus_handle(cx)),
+ )
+ .child(Divider::horizontal())
+ .child(self.breakpoint_list.clone());
this.child(
v_flex()
.h_full()
@@ -1469,65 +1568,23 @@ impl Render for DebugPanel {
.items_center()
.justify_center()
.child(
- h_flex().size_full()
- .items_start()
-
- .child(v_flex().group("base-breakpoint-list").items_start().min_w_1_3().h_full().p_1()
- .child(h_flex().pl_1().w_full().justify_between()
- .child(Label::new("Breakpoints").size(LabelSize::Small))
- .child(h_flex().visible_on_hover("base-breakpoint-list").child(self.breakpoint_list.read(cx).render_control_strip())))
- .child(Divider::horizontal())
- .child(self.breakpoint_list.clone()))
- .child(Divider::vertical())
- .child(
- v_flex().w_2_3().h_full().items_center().justify_center()
- .gap_2()
- .pr_8()
- .child(
- Button::new("spawn-new-session-empty-state", "New Session")
- .icon(IconName::Plus)
- .icon_size(IconSize::XSmall)
- .icon_color(Color::Muted)
- .icon_position(IconPosition::Start)
- .on_click(|_, window, cx| {
- window.dispatch_action(crate::Start.boxed_clone(), cx);
- })
- )
- .child(
- Button::new("edit-debug-settings", "Edit debug.json")
- .icon(IconName::Code)
- .icon_size(IconSize::XSmall)
- .color(Color::Muted)
- .icon_color(Color::Muted)
- .icon_position(IconPosition::Start)
- .on_click(|_, window, cx| {
- window.dispatch_action(zed_actions::OpenProjectDebugTasks.boxed_clone(), cx);
- })
- )
- .child(
- Button::new("open-debugger-docs", "Debugger Docs")
- .icon(IconName::Book)
- .color(Color::Muted)
- .icon_size(IconSize::XSmall)
- .icon_color(Color::Muted)
- .icon_position(IconPosition::Start)
- .on_click(|_, _, cx| {
- cx.open_url("https://zed.dev/docs/debugger")
- })
- )
- .child(
- Button::new("spawn-new-session-install-extensions", "Debugger Extensions")
- .icon(IconName::Blocks)
- .color(Color::Muted)
- .icon_size(IconSize::XSmall)
- .icon_color(Color::Muted)
- .icon_position(IconPosition::Start)
- .on_click(|_, window, cx| {
- window.dispatch_action(zed_actions::Extensions { category_filter: Some(zed_actions::ExtensionCategoryFilter::DebugAdapters)}.boxed_clone(), cx);
- })
- )
- )
- )
+ div()
+ .when_else(docked_to_bottom, Div::h_flex, Div::v_flex)
+ .size_full()
+ .map(|this| {
+ if docked_to_bottom {
+ this.items_start()
+ .child(breakpoint_list)
+ .child(Divider::vertical())
+ .child(welcome_experience)
+ } else {
+ this.items_end()
+ .child(welcome_experience)
+ .child(Divider::horizontal())
+ .child(breakpoint_list)
+ }
+ }),
+ ),
)
}
})
@@ -697,8 +697,13 @@ impl RunningState {
)
});
- let breakpoint_list =
- BreakpointList::new(Some(session.clone()), workspace.clone(), &project, cx);
+ let breakpoint_list = BreakpointList::new(
+ Some(session.clone()),
+ workspace.clone(),
+ &project,
+ window,
+ cx,
+ );
let _subscriptions = vec![
cx.on_app_quit(move |this, cx| {
@@ -895,7 +900,7 @@ impl RunningState {
let config_is_valid = request_type.is_ok();
-
+ let mut extra_config = Value::Null;
let build_output = if let Some(build) = build {
let (task_template, locator_name) = match build {
BuildTaskDefinition::Template {
@@ -925,6 +930,7 @@ impl RunningState {
};
let locator_name = if let Some(locator_name) = locator_name {
+ extra_config = config.clone();
debug_assert!(!config_is_valid);
Some(locator_name)
} else if !config_is_valid {
@@ -940,6 +946,7 @@ impl RunningState {
});
if let Ok(t) = task {
t.await.and_then(|scenario| {
+ extra_config = scenario.config;
match scenario.build {
Some(BuildTaskDefinition::Template {
locator_name, ..
@@ -1003,13 +1010,13 @@ impl RunningState {
if !exit_status.success() {
anyhow::bail!("Build failed");
}
- Some((task.resolved.clone(), locator_name))
+ Some((task.resolved.clone(), locator_name, extra_config))
} else {
None
};
if config_is_valid {
- } else if let Some((task, locator_name)) = build_output {
+ } else if let Some((task, locator_name, extra_config)) = build_output {
let locator_name =
locator_name.with_context(|| {
format!("Could not find a valid locator for a build task and configure is invalid with error: {}", request_type.err()
@@ -1034,6 +1041,8 @@ impl RunningState {
.with_context(|| anyhow!("{}: is not a valid adapter name", &adapter))?.config_from_zed_format(zed_config)
.await?;
config = scenario.config;
+ util::merge_non_null_json_value_into(extra_config, &mut config);
+
Self::substitute_variables_in_config(&mut config, &task_context);
} else {
let Err(e) = request_type else {
@@ -5,11 +5,11 @@ use std::{
time::Duration,
};
-use dap::ExceptionBreakpointsFilter;
+use dap::{Capabilities, ExceptionBreakpointsFilter};
use editor::Editor;
use gpui::{
- Action, AppContext, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy, Stateful,
- Task, UniformListScrollHandle, WeakEntity, uniform_list,
+ Action, AppContext, ClickEvent, Entity, FocusHandle, Focusable, MouseButton, ScrollStrategy,
+ Stateful, Task, UniformListScrollHandle, WeakEntity, actions, uniform_list,
};
use language::Point;
use project::{
@@ -21,16 +21,20 @@ use project::{
worktree_store::WorktreeStore,
};
use ui::{
- AnyElement, App, ButtonCommon, Clickable, Color, Context, Disableable, Div, FluentBuilder as _,
- Icon, IconButton, IconName, IconSize, Indicator, InteractiveElement, IntoElement, Label,
- LabelCommon, LabelSize, ListItem, ParentElement, Render, Scrollbar, ScrollbarState,
- SharedString, StatefulInteractiveElement, Styled, Toggleable, Tooltip, Window, div, h_flex, px,
- v_flex,
+ ActiveTheme, AnyElement, App, ButtonCommon, Clickable, Color, Context, Disableable, Div,
+ Divider, FluentBuilder as _, Icon, IconButton, IconName, IconSize, Indicator,
+ InteractiveElement, IntoElement, Label, LabelCommon, LabelSize, ListItem, ParentElement,
+ Render, RenderOnce, Scrollbar, ScrollbarState, SharedString, StatefulInteractiveElement,
+ Styled, Toggleable, Tooltip, Window, div, h_flex, px, v_flex,
};
use util::ResultExt;
use workspace::Workspace;
use zed_actions::{ToggleEnableBreakpoint, UnsetBreakpoint};
+actions!(
+ debugger,
+ [PreviousBreakpointProperty, NextBreakpointProperty]
+);
#[derive(Clone, Copy, PartialEq)]
pub(crate) enum SelectedBreakpointKind {
Source,
@@ -48,6 +52,8 @@ pub(crate) struct BreakpointList {
focus_handle: FocusHandle,
scroll_handle: UniformListScrollHandle,
selected_ix: Option<usize>,
+ input: Entity<Editor>,
+ strip_mode: Option<ActiveBreakpointStripMode>,
}
impl Focusable for BreakpointList {
@@ -56,11 +62,19 @@ impl Focusable for BreakpointList {
}
}
+#[derive(Clone, Copy, PartialEq)]
+enum ActiveBreakpointStripMode {
+ Log,
+ Condition,
+ HitCondition,
+}
+
impl BreakpointList {
pub(crate) fn new(
session: Option<Entity<Session>>,
workspace: WeakEntity<Workspace>,
project: &Entity<Project>,
+ window: &mut Window,
cx: &mut App,
) -> Entity<Self> {
let project = project.read(cx);
@@ -70,7 +84,7 @@ impl BreakpointList {
let scroll_handle = UniformListScrollHandle::new();
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
- cx.new(|_| Self {
+ cx.new(|cx| Self {
breakpoint_store,
worktree_store,
scrollbar_state,
@@ -82,17 +96,28 @@ impl BreakpointList {
focus_handle,
scroll_handle,
selected_ix: None,
+ input: cx.new(|cx| Editor::single_line(window, cx)),
+ strip_mode: None,
})
}
fn edit_line_breakpoint(
- &mut self,
+ &self,
path: Arc<Path>,
row: u32,
action: BreakpointEditAction,
- cx: &mut Context<Self>,
+ cx: &mut App,
+ ) {
+ Self::edit_line_breakpoint_inner(&self.breakpoint_store, path, row, action, cx);
+ }
+ fn edit_line_breakpoint_inner(
+ breakpoint_store: &Entity<BreakpointStore>,
+ path: Arc<Path>,
+ row: u32,
+ action: BreakpointEditAction,
+ cx: &mut App,
) {
- self.breakpoint_store.update(cx, |breakpoint_store, cx| {
+ breakpoint_store.update(cx, |breakpoint_store, cx| {
if let Some((buffer, breakpoint)) = breakpoint_store.breakpoint_at_row(&path, row, cx) {
breakpoint_store.toggle_breakpoint(buffer, breakpoint, action, cx);
} else {
@@ -148,16 +173,63 @@ impl BreakpointList {
})
}
- fn select_ix(&mut self, ix: Option<usize>, cx: &mut Context<Self>) {
+ fn set_active_breakpoint_property(
+ &mut self,
+ prop: ActiveBreakpointStripMode,
+ window: &mut Window,
+ cx: &mut App,
+ ) {
+ self.strip_mode = Some(prop);
+ let placeholder = match prop {
+ ActiveBreakpointStripMode::Log => "Set Log Message",
+ ActiveBreakpointStripMode::Condition => "Set Condition",
+ ActiveBreakpointStripMode::HitCondition => "Set Hit Condition",
+ };
+ let mut is_exception_breakpoint = true;
+ let active_value = self.selected_ix.and_then(|ix| {
+ self.breakpoints.get(ix).and_then(|bp| {
+ if let BreakpointEntryKind::LineBreakpoint(bp) = &bp.kind {
+ is_exception_breakpoint = false;
+ match prop {
+ ActiveBreakpointStripMode::Log => bp.breakpoint.message.clone(),
+ ActiveBreakpointStripMode::Condition => bp.breakpoint.condition.clone(),
+ ActiveBreakpointStripMode::HitCondition => {
+ bp.breakpoint.hit_condition.clone()
+ }
+ }
+ } else {
+ None
+ }
+ })
+ });
+
+ self.input.update(cx, |this, cx| {
+ this.set_placeholder_text(placeholder, cx);
+ this.set_read_only(is_exception_breakpoint);
+ this.set_text(active_value.as_deref().unwrap_or(""), window, cx);
+ });
+ }
+
+ fn select_ix(&mut self, ix: Option<usize>, window: &mut Window, cx: &mut Context<Self>) {
self.selected_ix = ix;
if let Some(ix) = ix {
self.scroll_handle
.scroll_to_item(ix, ScrollStrategy::Center);
}
+ if let Some(mode) = self.strip_mode {
+ self.set_active_breakpoint_property(mode, window, cx);
+ }
+
cx.notify();
}
- fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context<Self>) {
+ fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
+ if self.strip_mode.is_some() {
+ if self.input.focus_handle(cx).contains_focused(window, cx) {
+ cx.propagate();
+ return;
+ }
+ }
let ix = match self.selected_ix {
_ if self.breakpoints.len() == 0 => None,
None => Some(0),
@@ -169,15 +241,21 @@ impl BreakpointList {
}
}
};
- self.select_ix(ix, cx);
+ self.select_ix(ix, window, cx);
}
fn select_previous(
&mut self,
_: &menu::SelectPrevious,
- _window: &mut Window,
+ window: &mut Window,
cx: &mut Context<Self>,
) {
+ if self.strip_mode.is_some() {
+ if self.input.focus_handle(cx).contains_focused(window, cx) {
+ cx.propagate();
+ return;
+ }
+ }
let ix = match self.selected_ix {
_ if self.breakpoints.len() == 0 => None,
None => Some(self.breakpoints.len() - 1),
@@ -189,37 +267,105 @@ impl BreakpointList {
}
}
};
- self.select_ix(ix, cx);
+ self.select_ix(ix, window, cx);
}
- fn select_first(
- &mut self,
- _: &menu::SelectFirst,
- _window: &mut Window,
- cx: &mut Context<Self>,
- ) {
+ fn select_first(&mut self, _: &menu::SelectFirst, window: &mut Window, cx: &mut Context<Self>) {
+ if self.strip_mode.is_some() {
+ if self.input.focus_handle(cx).contains_focused(window, cx) {
+ cx.propagate();
+ return;
+ }
+ }
let ix = if self.breakpoints.len() > 0 {
Some(0)
} else {
None
};
- self.select_ix(ix, cx);
+ self.select_ix(ix, window, cx);
}
- fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
+ fn select_last(&mut self, _: &menu::SelectLast, window: &mut Window, cx: &mut Context<Self>) {
+ if self.strip_mode.is_some() {
+ if self.input.focus_handle(cx).contains_focused(window, cx) {
+ cx.propagate();
+ return;
+ }
+ }
let ix = if self.breakpoints.len() > 0 {
Some(self.breakpoints.len() - 1)
} else {
None
};
- self.select_ix(ix, cx);
+ self.select_ix(ix, window, cx);
}
+ fn dismiss(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
+ if self.input.focus_handle(cx).contains_focused(window, cx) {
+ self.focus_handle.focus(window);
+ } else if self.strip_mode.is_some() {
+ self.strip_mode.take();
+ cx.notify();
+ } else {
+ cx.propagate();
+ }
+ }
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
return;
};
+ if let Some(mode) = self.strip_mode {
+ let handle = self.input.focus_handle(cx);
+ if handle.is_focused(window) {
+ // Go back to the main strip. Save the result as well.
+ let text = self.input.read(cx).text(cx);
+
+ match mode {
+ ActiveBreakpointStripMode::Log => match &entry.kind {
+ BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
+ Self::edit_line_breakpoint_inner(
+ &self.breakpoint_store,
+ line_breakpoint.breakpoint.path.clone(),
+ line_breakpoint.breakpoint.row,
+ BreakpointEditAction::EditLogMessage(Arc::from(text)),
+ cx,
+ );
+ }
+ _ => {}
+ },
+ ActiveBreakpointStripMode::Condition => match &entry.kind {
+ BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
+ Self::edit_line_breakpoint_inner(
+ &self.breakpoint_store,
+ line_breakpoint.breakpoint.path.clone(),
+ line_breakpoint.breakpoint.row,
+ BreakpointEditAction::EditCondition(Arc::from(text)),
+ cx,
+ );
+ }
+ _ => {}
+ },
+ ActiveBreakpointStripMode::HitCondition => match &entry.kind {
+ BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
+ Self::edit_line_breakpoint_inner(
+ &self.breakpoint_store,
+ line_breakpoint.breakpoint.path.clone(),
+ line_breakpoint.breakpoint.row,
+ BreakpointEditAction::EditHitCondition(Arc::from(text)),
+ cx,
+ );
+ }
+ _ => {}
+ },
+ }
+ self.focus_handle.focus(window);
+ } else {
+ handle.focus(window);
+ }
+
+ return;
+ }
match &mut entry.kind {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
let path = line_breakpoint.breakpoint.path.clone();
@@ -233,12 +379,18 @@ impl BreakpointList {
fn toggle_enable_breakpoint(
&mut self,
_: &ToggleEnableBreakpoint,
- _window: &mut Window,
+ window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(entry) = self.selected_ix.and_then(|ix| self.breakpoints.get_mut(ix)) else {
return;
};
+ if self.strip_mode.is_some() {
+ if self.input.focus_handle(cx).contains_focused(window, cx) {
+ cx.propagate();
+ return;
+ }
+ }
match &mut entry.kind {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
@@ -279,6 +431,50 @@ impl BreakpointList {
cx.notify();
}
+ fn previous_breakpoint_property(
+ &mut self,
+ _: &PreviousBreakpointProperty,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let next_mode = match self.strip_mode {
+ Some(ActiveBreakpointStripMode::Log) => None,
+ Some(ActiveBreakpointStripMode::Condition) => Some(ActiveBreakpointStripMode::Log),
+ Some(ActiveBreakpointStripMode::HitCondition) => {
+ Some(ActiveBreakpointStripMode::Condition)
+ }
+ None => Some(ActiveBreakpointStripMode::HitCondition),
+ };
+ if let Some(mode) = next_mode {
+ self.set_active_breakpoint_property(mode, window, cx);
+ } else {
+ self.strip_mode.take();
+ }
+
+ cx.notify();
+ }
+ fn next_breakpoint_property(
+ &mut self,
+ _: &NextBreakpointProperty,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let next_mode = match self.strip_mode {
+ Some(ActiveBreakpointStripMode::Log) => Some(ActiveBreakpointStripMode::Condition),
+ Some(ActiveBreakpointStripMode::Condition) => {
+ Some(ActiveBreakpointStripMode::HitCondition)
+ }
+ Some(ActiveBreakpointStripMode::HitCondition) => None,
+ None => Some(ActiveBreakpointStripMode::Log),
+ };
+ if let Some(mode) = next_mode {
+ self.set_active_breakpoint_property(mode, window, cx);
+ } else {
+ self.strip_mode.take();
+ }
+ cx.notify();
+ }
+
fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |panel, cx| {
@@ -294,20 +490,31 @@ impl BreakpointList {
}))
}
- fn render_list(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ fn render_list(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let selected_ix = self.selected_ix;
let focus_handle = self.focus_handle.clone();
+ let supported_breakpoint_properties = self
+ .session
+ .as_ref()
+ .map(|session| SupportedBreakpointProperties::from(session.read(cx).capabilities()))
+ .unwrap_or_else(SupportedBreakpointProperties::empty);
+ let strip_mode = self.strip_mode;
uniform_list(
"breakpoint-list",
self.breakpoints.len(),
- cx.processor(move |this, range: Range<usize>, window, cx| {
+ cx.processor(move |this, range: Range<usize>, _, _| {
range
.clone()
.zip(&mut this.breakpoints[range])
.map(|(ix, breakpoint)| {
breakpoint
- .render(ix, focus_handle.clone(), window, cx)
- .toggle_state(Some(ix) == selected_ix)
+ .render(
+ strip_mode,
+ supported_breakpoint_properties,
+ ix,
+ Some(ix) == selected_ix,
+ focus_handle.clone(),
+ )
.into_any_element()
})
.collect()
@@ -443,7 +650,6 @@ impl BreakpointList {
impl Render for BreakpointList {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
- // let old_len = self.breakpoints.len();
let breakpoints = self.breakpoint_store.read(cx).all_source_breakpoints(cx);
self.breakpoints.clear();
let weak = cx.weak_entity();
@@ -523,15 +729,46 @@ impl Render for BreakpointList {
.on_action(cx.listener(Self::select_previous))
.on_action(cx.listener(Self::select_first))
.on_action(cx.listener(Self::select_last))
+ .on_action(cx.listener(Self::dismiss))
.on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::toggle_enable_breakpoint))
.on_action(cx.listener(Self::unset_breakpoint))
+ .on_action(cx.listener(Self::next_breakpoint_property))
+ .on_action(cx.listener(Self::previous_breakpoint_property))
.size_full()
.m_0p5()
- .child(self.render_list(window, cx))
- .children(self.render_vertical_scrollbar(cx))
+ .child(
+ v_flex()
+ .size_full()
+ .child(self.render_list(cx))
+ .children(self.render_vertical_scrollbar(cx)),
+ )
+ .when_some(self.strip_mode, |this, _| {
+ this.child(Divider::horizontal()).child(
+ h_flex()
+ // .w_full()
+ .m_0p5()
+ .p_0p5()
+ .border_1()
+ .rounded_sm()
+ .when(
+ self.input.focus_handle(cx).contains_focused(window, cx),
+ |this| {
+ let colors = cx.theme().colors();
+ let border = if self.input.read(cx).read_only(cx) {
+ colors.border_disabled
+ } else {
+ colors.border_focused
+ };
+ this.border_color(border)
+ },
+ )
+ .child(self.input.clone()),
+ )
+ })
}
}
+
#[derive(Clone, Debug)]
struct LineBreakpoint {
name: SharedString,
@@ -543,7 +780,10 @@ struct LineBreakpoint {
impl LineBreakpoint {
fn render(
&mut self,
+ props: SupportedBreakpointProperties,
+ strip_mode: Option<ActiveBreakpointStripMode>,
ix: usize,
+ is_selected: bool,
focus_handle: FocusHandle,
weak: WeakEntity<BreakpointList>,
) -> ListItem {
@@ -594,15 +834,16 @@ impl LineBreakpoint {
})
.child(Indicator::icon(Icon::new(icon_name)).color(Color::Debugger))
.on_mouse_down(MouseButton::Left, move |_, _, _| {});
+
ListItem::new(SharedString::from(format!(
"breakpoint-ui-item-{:?}/{}:{}",
self.dir, self.name, self.line
)))
.on_click({
let weak = weak.clone();
- move |_, _, cx| {
+ move |_, window, cx| {
weak.update(cx, |breakpoint_list, cx| {
- breakpoint_list.select_ix(Some(ix), cx);
+ breakpoint_list.select_ix(Some(ix), window, cx);
})
.ok();
}
@@ -613,39 +854,67 @@ impl LineBreakpoint {
cx.stop_propagation();
})
.child(
- v_flex()
- .py_1()
+ h_flex()
+ .w_full()
+ .mr_4()
+ .py_0p5()
.gap_1()
.min_h(px(26.))
- .justify_center()
+ .justify_between()
.id(SharedString::from(format!(
"breakpoint-ui-on-click-go-to-line-{:?}/{}:{}",
self.dir, self.name, self.line
)))
- .on_click(move |_, window, cx| {
- weak.update(cx, |breakpoint_list, cx| {
- breakpoint_list.select_ix(Some(ix), cx);
- breakpoint_list.go_to_line_breakpoint(path.clone(), row, window, cx);
- })
- .ok();
+ .on_click({
+ let weak = weak.clone();
+ move |_, window, cx| {
+ weak.update(cx, |breakpoint_list, cx| {
+ breakpoint_list.select_ix(Some(ix), window, cx);
+ breakpoint_list.go_to_line_breakpoint(path.clone(), row, window, cx);
+ })
+ .ok();
+ }
})
.cursor_pointer()
.child(
h_flex()
- .gap_1()
+ .gap_0p5()
.child(
Label::new(format!("{}:{}", self.name, self.line))
.size(LabelSize::Small)
.line_height_style(ui::LineHeightStyle::UiLabel),
)
- .children(self.dir.clone().map(|dir| {
- Label::new(dir)
- .color(Color::Muted)
- .size(LabelSize::Small)
- .line_height_style(ui::LineHeightStyle::UiLabel)
+ .children(self.dir.as_ref().and_then(|dir| {
+ let path_without_root = Path::new(dir.as_ref())
+ .components()
+ .skip(1)
+ .collect::<PathBuf>();
+ path_without_root.components().next()?;
+ Some(
+ Label::new(path_without_root.to_string_lossy().into_owned())
+ .color(Color::Muted)
+ .size(LabelSize::Small)
+ .line_height_style(ui::LineHeightStyle::UiLabel)
+ .truncate(),
+ )
})),
- ),
+ )
+ .when_some(self.dir.as_ref(), |this, parent_dir| {
+ this.tooltip(Tooltip::text(format!("Worktree parent path: {parent_dir}")))
+ })
+ .child(BreakpointOptionsStrip {
+ props,
+ breakpoint: BreakpointEntry {
+ kind: BreakpointEntryKind::LineBreakpoint(self.clone()),
+ weak: weak,
+ },
+ is_selected,
+ focus_handle,
+ strip_mode,
+ index: ix,
+ }),
)
+ .toggle_state(is_selected)
}
}
#[derive(Clone, Debug)]
@@ -658,7 +927,10 @@ struct ExceptionBreakpoint {
impl ExceptionBreakpoint {
fn render(
&mut self,
+ props: SupportedBreakpointProperties,
+ strip_mode: Option<ActiveBreakpointStripMode>,
ix: usize,
+ is_selected: bool,
focus_handle: FocusHandle,
list: WeakEntity<BreakpointList>,
) -> ListItem {
@@ -669,15 +941,15 @@ impl ExceptionBreakpoint {
};
let id = SharedString::from(&self.id);
let is_enabled = self.is_enabled;
-
+ let weak = list.clone();
ListItem::new(SharedString::from(format!(
"exception-breakpoint-ui-item-{}",
self.id
)))
.on_click({
let list = list.clone();
- move |_, _, cx| {
- list.update(cx, |list, cx| list.select_ix(Some(ix), cx))
+ move |_, window, cx| {
+ list.update(cx, |list, cx| list.select_ix(Some(ix), window, cx))
.ok();
}
})
@@ -691,18 +963,21 @@ impl ExceptionBreakpoint {
"exception-breakpoint-ui-item-{}-click-handler",
self.id
)))
- .tooltip(move |window, cx| {
- Tooltip::for_action_in(
- if is_enabled {
- "Disable Exception Breakpoint"
- } else {
- "Enable Exception Breakpoint"
- },
- &ToggleEnableBreakpoint,
- &focus_handle,
- window,
- cx,
- )
+ .tooltip({
+ let focus_handle = focus_handle.clone();
+ move |window, cx| {
+ Tooltip::for_action_in(
+ if is_enabled {
+ "Disable Exception Breakpoint"
+ } else {
+ "Enable Exception Breakpoint"
+ },
+ &ToggleEnableBreakpoint,
+ &focus_handle,
+ window,
+ cx,
+ )
+ }
})
.on_click({
let list = list.clone();
@@ -722,21 +997,40 @@ impl ExceptionBreakpoint {
.child(Indicator::icon(Icon::new(IconName::Flame)).color(color)),
)
.child(
- v_flex()
- .py_1()
- .gap_1()
- .min_h(px(26.))
- .justify_center()
- .id(("exception-breakpoint-label", ix))
+ h_flex()
+ .w_full()
+ .mr_4()
+ .py_0p5()
+ .justify_between()
.child(
- Label::new(self.data.label.clone())
- .size(LabelSize::Small)
- .line_height_style(ui::LineHeightStyle::UiLabel),
+ v_flex()
+ .py_1()
+ .gap_1()
+ .min_h(px(26.))
+ .justify_center()
+ .id(("exception-breakpoint-label", ix))
+ .child(
+ Label::new(self.data.label.clone())
+ .size(LabelSize::Small)
+ .line_height_style(ui::LineHeightStyle::UiLabel),
+ )
+ .when_some(self.data.description.clone(), |el, description| {
+ el.tooltip(Tooltip::text(description))
+ }),
)
- .when_some(self.data.description.clone(), |el, description| {
- el.tooltip(Tooltip::text(description))
+ .child(BreakpointOptionsStrip {
+ props,
+ breakpoint: BreakpointEntry {
+ kind: BreakpointEntryKind::ExceptionBreakpoint(self.clone()),
+ weak: weak,
+ },
+ is_selected,
+ focus_handle,
+ strip_mode,
+ index: ix,
}),
)
+ .toggle_state(is_selected)
}
}
#[derive(Clone, Debug)]
@@ -754,18 +1048,267 @@ struct BreakpointEntry {
impl BreakpointEntry {
fn render(
&mut self,
+ strip_mode: Option<ActiveBreakpointStripMode>,
+ props: SupportedBreakpointProperties,
ix: usize,
+ is_selected: bool,
focus_handle: FocusHandle,
- _: &mut Window,
- _: &mut App,
) -> ListItem {
match &mut self.kind {
+ BreakpointEntryKind::LineBreakpoint(line_breakpoint) => line_breakpoint.render(
+ props,
+ strip_mode,
+ ix,
+ is_selected,
+ focus_handle,
+ self.weak.clone(),
+ ),
+ BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => exception_breakpoint
+ .render(
+ props.for_exception_breakpoints(),
+ strip_mode,
+ ix,
+ is_selected,
+ focus_handle,
+ self.weak.clone(),
+ ),
+ }
+ }
+
+ fn id(&self) -> SharedString {
+ match &self.kind {
+ BreakpointEntryKind::LineBreakpoint(line_breakpoint) => format!(
+ "source-breakpoint-control-strip-{:?}:{}",
+ line_breakpoint.breakpoint.path, line_breakpoint.breakpoint.row
+ )
+ .into(),
+ BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => format!(
+ "exception-breakpoint-control-strip--{}",
+ exception_breakpoint.id
+ )
+ .into(),
+ }
+ }
+
+ fn has_log(&self) -> bool {
+ match &self.kind {
BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
- line_breakpoint.render(ix, focus_handle, self.weak.clone())
+ line_breakpoint.breakpoint.message.is_some()
}
- BreakpointEntryKind::ExceptionBreakpoint(exception_breakpoint) => {
- exception_breakpoint.render(ix, focus_handle, self.weak.clone())
+ _ => false,
+ }
+ }
+
+ fn has_condition(&self) -> bool {
+ match &self.kind {
+ BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
+ line_breakpoint.breakpoint.condition.is_some()
+ }
+ // We don't support conditions on exception breakpoints
+ BreakpointEntryKind::ExceptionBreakpoint(_) => false,
+ }
+ }
+
+ fn has_hit_condition(&self) -> bool {
+ match &self.kind {
+ BreakpointEntryKind::LineBreakpoint(line_breakpoint) => {
+ line_breakpoint.breakpoint.hit_condition.is_some()
}
+ _ => false,
}
}
}
+bitflags::bitflags! {
+ #[derive(Clone, Copy)]
+ pub struct SupportedBreakpointProperties: u32 {
+ const LOG = 1 << 0;
+ const CONDITION = 1 << 1;
+ const HIT_CONDITION = 1 << 2;
+ // Conditions for exceptions can be set only when exception filters are supported.
+ const EXCEPTION_FILTER_OPTIONS = 1 << 3;
+ }
+}
+
+impl From<&Capabilities> for SupportedBreakpointProperties {
+ fn from(caps: &Capabilities) -> Self {
+ let mut this = Self::empty();
+ for (prop, offset) in [
+ (caps.supports_log_points, Self::LOG),
+ (caps.supports_conditional_breakpoints, Self::CONDITION),
+ (
+ caps.supports_hit_conditional_breakpoints,
+ Self::HIT_CONDITION,
+ ),
+ (
+ caps.supports_exception_options,
+ Self::EXCEPTION_FILTER_OPTIONS,
+ ),
+ ] {
+ if prop.unwrap_or_default() {
+ this.insert(offset);
+ }
+ }
+ this
+ }
+}
+
+impl SupportedBreakpointProperties {
+ fn for_exception_breakpoints(self) -> Self {
+ // TODO: we don't yet support conditions for exception breakpoints at the data layer, hence all props are disabled here.
+ Self::empty()
+ }
+}
+#[derive(IntoElement)]
+struct BreakpointOptionsStrip {
+ props: SupportedBreakpointProperties,
+ breakpoint: BreakpointEntry,
+ is_selected: bool,
+ focus_handle: FocusHandle,
+ strip_mode: Option<ActiveBreakpointStripMode>,
+ index: usize,
+}
+
+impl BreakpointOptionsStrip {
+ fn is_toggled(&self, expected_mode: ActiveBreakpointStripMode) -> bool {
+ self.is_selected && self.strip_mode == Some(expected_mode)
+ }
+ fn on_click_callback(
+ &self,
+ mode: ActiveBreakpointStripMode,
+ ) -> impl for<'a> Fn(&ClickEvent, &mut Window, &'a mut App) + use<> {
+ let list = self.breakpoint.weak.clone();
+ let ix = self.index;
+ move |_, window, cx| {
+ list.update(cx, |this, cx| {
+ if this.strip_mode != Some(mode) {
+ this.set_active_breakpoint_property(mode, window, cx);
+ } else if this.selected_ix == Some(ix) {
+ this.strip_mode.take();
+ } else {
+ cx.propagate();
+ }
+ })
+ .ok();
+ }
+ }
+ fn add_border(
+ &self,
+ kind: ActiveBreakpointStripMode,
+ available: bool,
+ window: &Window,
+ cx: &App,
+ ) -> impl Fn(Div) -> Div {
+ move |this: Div| {
+ // Avoid layout shifts in case there's no colored border
+ let this = this.border_2().rounded_sm();
+ if self.is_selected && self.strip_mode == Some(kind) {
+ let theme = cx.theme().colors();
+ if self.focus_handle.is_focused(window) {
+ this.border_color(theme.border_selected)
+ } else {
+ this.border_color(theme.border_disabled)
+ }
+ } else if !available {
+ this.border_color(cx.theme().colors().border_disabled)
+ } else {
+ this
+ }
+ }
+ }
+}
+impl RenderOnce for BreakpointOptionsStrip {
+ fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
+ let id = self.breakpoint.id();
+ let supports_logs = self.props.contains(SupportedBreakpointProperties::LOG);
+ let supports_condition = self
+ .props
+ .contains(SupportedBreakpointProperties::CONDITION);
+ let supports_hit_condition = self
+ .props
+ .contains(SupportedBreakpointProperties::HIT_CONDITION);
+ let has_logs = self.breakpoint.has_log();
+ let has_condition = self.breakpoint.has_condition();
+ let has_hit_condition = self.breakpoint.has_hit_condition();
+ let style_for_toggle = |mode, is_enabled| {
+ if is_enabled && self.strip_mode == Some(mode) && self.is_selected {
+ ui::ButtonStyle::Filled
+ } else {
+ ui::ButtonStyle::Subtle
+ }
+ };
+ let color_for_toggle = |is_enabled| {
+ if is_enabled {
+ ui::Color::Default
+ } else {
+ ui::Color::Muted
+ }
+ };
+
+ h_flex()
+ .gap_1()
+ .child(
+ div().map(self.add_border(ActiveBreakpointStripMode::Log, supports_logs, window, cx))
+ .child(
+ IconButton::new(
+ SharedString::from(format!("{id}-log-toggle")),
+ IconName::ScrollText,
+ )
+ .icon_size(IconSize::XSmall)
+ .style(style_for_toggle(ActiveBreakpointStripMode::Log, has_logs))
+ .icon_color(color_for_toggle(has_logs))
+ .disabled(!supports_logs)
+ .toggle_state(self.is_toggled(ActiveBreakpointStripMode::Log))
+ .on_click(self.on_click_callback(ActiveBreakpointStripMode::Log)).tooltip(|window, cx| Tooltip::with_meta("Set Log Message", None, "Set log message to display (instead of stopping) when a breakpoint is hit", window, cx))
+ )
+ .when(!has_logs && !self.is_selected, |this| this.invisible()),
+ )
+ .child(
+ div().map(self.add_border(
+ ActiveBreakpointStripMode::Condition,
+ supports_condition,
+ window, cx
+ ))
+ .child(
+ IconButton::new(
+ SharedString::from(format!("{id}-condition-toggle")),
+ IconName::SplitAlt,
+ )
+ .icon_size(IconSize::XSmall)
+ .style(style_for_toggle(
+ ActiveBreakpointStripMode::Condition,
+ has_condition
+ ))
+ .icon_color(color_for_toggle(has_condition))
+ .disabled(!supports_condition)
+ .toggle_state(self.is_toggled(ActiveBreakpointStripMode::Condition))
+ .on_click(self.on_click_callback(ActiveBreakpointStripMode::Condition))
+ .tooltip(|window, cx| Tooltip::with_meta("Set Condition", None, "Set condition to evaluate when a breakpoint is hit. Program execution will stop only when the condition is met", window, cx))
+ )
+ .when(!has_condition && !self.is_selected, |this| this.invisible()),
+ )
+ .child(
+ div().map(self.add_border(
+ ActiveBreakpointStripMode::HitCondition,
+ supports_hit_condition,window, cx
+ ))
+ .child(
+ IconButton::new(
+ SharedString::from(format!("{id}-hit-condition-toggle")),
+ IconName::ArrowDown10,
+ )
+ .icon_size(IconSize::XSmall)
+ .style(style_for_toggle(
+ ActiveBreakpointStripMode::HitCondition,
+ has_hit_condition,
+ ))
+ .icon_color(color_for_toggle(has_hit_condition))
+ .disabled(!supports_hit_condition)
+ .toggle_state(self.is_toggled(ActiveBreakpointStripMode::HitCondition))
+ .on_click(self.on_click_callback(ActiveBreakpointStripMode::HitCondition)).tooltip(|window, cx| Tooltip::with_meta("Set Hit Condition", None, "Set expression that controls how many hits of the breakpoint are ignored.", window, cx))
+ )
+ .when(!has_hit_condition && !self.is_selected, |this| {
+ this.invisible()
+ }),
+ )
+ }
+}
@@ -114,7 +114,7 @@ impl Console {
}
fn is_running(&self, cx: &Context<Self>) -> bool {
- self.session.read(cx).is_running()
+ self.session.read(cx).is_started()
}
fn handle_stack_frame_list_events(
@@ -4,7 +4,7 @@ use collections::HashMap;
use dap::StackFrameId;
use editor::{
Anchor, Bias, DebugStackFrameLine, Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer,
- RowHighlightOptions, ToPoint, scroll::Autoscroll,
+ RowHighlightOptions, SelectionEffects, ToPoint, scroll::Autoscroll,
};
use gpui::{
AnyView, App, AppContext, Entity, EventEmitter, Focusable, IntoElement, Render, SharedString,
@@ -99,10 +99,11 @@ impl StackTraceView {
if frame_anchor.excerpt_id
!= editor.selections.newest_anchor().head().excerpt_id
{
- let auto_scroll =
- Some(Autoscroll::center().for_anchor(frame_anchor));
+ let effects = SelectionEffects::scroll(
+ Autoscroll::center().for_anchor(frame_anchor),
+ );
- editor.change_selections(auto_scroll, window, cx, |selections| {
+ editor.change_selections(effects, window, cx, |selections| {
let selection_id = selections.new_selection_id();
let selection = Selection {
@@ -37,15 +37,23 @@ async fn test_dap_logger_captures_all_session_rpc_messages(
.await;
assert!(
- log_store.read_with(cx, |log_store, _| log_store
- .contained_session_ids()
- .is_empty()),
- "log_store shouldn't contain any session IDs before any sessions were created"
+ log_store.read_with(cx, |log_store, _| !log_store.has_projects()),
+ "log_store shouldn't contain any projects before any projects were created"
);
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
let workspace = init_test_workspace(&project, cx).await;
+ assert!(
+ log_store.read_with(cx, |log_store, _| log_store.has_projects()),
+ "log_store shouldn't contain any projects before any projects were created"
+ );
+ assert!(
+ log_store.read_with(cx, |log_store, _| log_store
+ .contained_session_ids(&project.downgrade())
+ .is_empty()),
+ "log_store shouldn't contain any projects before any projects were created"
+ );
let cx = &mut VisualTestContext::from_window(*workspace, cx);
// Start a debug session
@@ -54,20 +62,22 @@ async fn test_dap_logger_captures_all_session_rpc_messages(
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
assert_eq!(
- log_store.read_with(cx, |log_store, _| log_store.contained_session_ids().len()),
+ log_store.read_with(cx, |log_store, _| log_store
+ .contained_session_ids(&project.downgrade())
+ .len()),
1,
);
assert!(
log_store.read_with(cx, |log_store, _| log_store
- .contained_session_ids()
+ .contained_session_ids(&project.downgrade())
.contains(&session_id)),
"log_store should contain the session IDs of the started session"
);
assert!(
!log_store.read_with(cx, |log_store, _| log_store
- .rpc_messages_for_session_id(session_id)
+ .rpc_messages_for_session_id(&project.downgrade(), session_id)
.is_empty()),
"We should have the initialization sequence in the log store"
);
@@ -267,7 +267,6 @@ async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppConte
"Debugpy",
"PHP",
"JavaScript",
- "Ruby",
"Delve",
"GDB",
"fake-adapter",
@@ -4,7 +4,6 @@ use editor::{
Anchor, Editor, EditorSnapshot, ToOffset,
display_map::{BlockContext, BlockPlacement, BlockProperties, BlockStyle},
hover_popover::diagnostics_markdown_style,
- scroll::Autoscroll,
};
use gpui::{AppContext, Entity, Focusable, WeakEntity};
use language::{BufferId, Diagnostic, DiagnosticEntry};
@@ -311,7 +310,7 @@ impl DiagnosticBlock {
let range = range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot);
editor.unfold_ranges(&[range.start..range.end], true, false, cx);
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.select_ranges([range.start..range.start]);
});
window.focus(&editor.focus_handle(cx));
@@ -12,7 +12,6 @@ use diagnostic_renderer::DiagnosticBlock;
use editor::{
DEFAULT_MULTIBUFFER_CONTEXT, Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,
display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
- scroll::Autoscroll,
};
use futures::future::join_all;
use gpui::{
@@ -626,7 +625,7 @@ impl ProjectDiagnosticsEditor {
if let Some(anchor_range) = anchor_ranges.first() {
let range_to_select = anchor_range.start..anchor_range.start;
this.editor.update(cx, |editor, cx| {
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.select_anchor_ranges([range_to_select]);
})
});
@@ -61,6 +61,7 @@ parking_lot.workspace = true
pretty_assertions.workspace = true
project.workspace = true
rand.workspace = true
+regex.workspace = true
rpc.workspace = true
schemars.workspace = true
serde.workspace = true
@@ -37,7 +37,9 @@ pub use block_map::{
use block_map::{BlockRow, BlockSnapshot};
use collections::{HashMap, HashSet};
pub use crease_map::*;
-pub use fold_map::{ChunkRenderer, ChunkRendererContext, Fold, FoldId, FoldPlaceholder, FoldPoint};
+pub use fold_map::{
+ ChunkRenderer, ChunkRendererContext, ChunkRendererId, Fold, FoldId, FoldPlaceholder, FoldPoint,
+};
use fold_map::{FoldMap, FoldSnapshot};
use gpui::{App, Context, Entity, Font, HighlightStyle, LineLayout, Pixels, UnderlineStyle};
pub use inlay_map::Inlay;
@@ -538,7 +540,7 @@ impl DisplayMap {
pub fn update_fold_widths(
&mut self,
- widths: impl IntoIterator<Item = (FoldId, Pixels)>,
+ widths: impl IntoIterator<Item = (ChunkRendererId, Pixels)>,
cx: &mut Context<Self>,
) -> bool {
let snapshot = self.buffer.read(cx).snapshot(cx);
@@ -966,10 +968,22 @@ impl DisplaySnapshot {
.and_then(|id| id.style(&editor_style.syntax));
if let Some(chunk_highlight) = chunk.highlight_style {
+ // For color inlays, blend the color with the editor background
+ let mut processed_highlight = chunk_highlight;
+ if chunk.is_inlay {
+ if let Some(inlay_color) = chunk_highlight.color {
+ // Only blend if the color has transparency (alpha < 1.0)
+ if inlay_color.a < 1.0 {
+ let blended_color = editor_style.background.blend(inlay_color);
+ processed_highlight.color = Some(blended_color);
+ }
+ }
+ }
+
if let Some(highlight_style) = highlight_style.as_mut() {
- highlight_style.highlight(chunk_highlight);
+ highlight_style.highlight(processed_highlight);
} else {
- highlight_style = Some(chunk_highlight);
+ highlight_style = Some(processed_highlight);
}
}
@@ -1,3 +1,5 @@
+use crate::{InlayId, display_map::inlay_map::InlayChunk};
+
use super::{
Highlights,
inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot},
@@ -275,13 +277,16 @@ impl FoldMapWriter<'_> {
pub(crate) fn update_fold_widths(
&mut self,
- new_widths: impl IntoIterator<Item = (FoldId, Pixels)>,
+ new_widths: impl IntoIterator<Item = (ChunkRendererId, Pixels)>,
) -> (FoldSnapshot, Vec<FoldEdit>) {
let mut edits = Vec::new();
let inlay_snapshot = self.0.snapshot.inlay_snapshot.clone();
let buffer = &inlay_snapshot.buffer;
for (id, new_width) in new_widths {
+ let ChunkRendererId::Fold(id) = id else {
+ continue;
+ };
if let Some(metadata) = self.0.snapshot.fold_metadata_by_id.get(&id).cloned() {
if Some(new_width) != metadata.width {
let buffer_start = metadata.range.start.to_offset(buffer);
@@ -527,7 +532,7 @@ impl FoldMap {
placeholder: Some(TransformPlaceholder {
text: ELLIPSIS,
renderer: ChunkRenderer {
- id: fold.id,
+ id: ChunkRendererId::Fold(fold.id),
render: Arc::new(move |cx| {
(fold.placeholder.render)(
fold_id,
@@ -1060,7 +1065,7 @@ impl sum_tree::Summary for TransformSummary {
}
#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, Ord, PartialOrd, Hash)]
-pub struct FoldId(usize);
+pub struct FoldId(pub(super) usize);
impl From<FoldId> for ElementId {
fn from(val: FoldId) -> Self {
@@ -1265,11 +1270,17 @@ pub struct Chunk<'a> {
pub renderer: Option<ChunkRenderer>,
}
+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+pub enum ChunkRendererId {
+ Fold(FoldId),
+ Inlay(InlayId),
+}
+
/// A recipe for how the chunk should be presented.
#[derive(Clone)]
pub struct ChunkRenderer {
- /// The id of the fold associated with this chunk.
- pub id: FoldId,
+ /// The id of the renderer associated with this chunk.
+ pub id: ChunkRendererId,
/// Creates a custom element to represent this chunk.
pub render: Arc<dyn Send + Sync + Fn(&mut ChunkRendererContext) -> AnyElement>,
/// If true, the element is constrained to the shaped width of the text.
@@ -1311,7 +1322,7 @@ impl DerefMut for ChunkRendererContext<'_, '_> {
pub struct FoldChunks<'a> {
transform_cursor: Cursor<'a, Transform, (FoldOffset, InlayOffset)>,
inlay_chunks: InlayChunks<'a>,
- inlay_chunk: Option<(InlayOffset, language::Chunk<'a>)>,
+ inlay_chunk: Option<(InlayOffset, InlayChunk<'a>)>,
inlay_offset: InlayOffset,
output_offset: FoldOffset,
max_output_offset: FoldOffset,
@@ -1403,7 +1414,8 @@ impl<'a> Iterator for FoldChunks<'a> {
}
// Otherwise, take a chunk from the buffer's text.
- if let Some((buffer_chunk_start, mut chunk)) = self.inlay_chunk.clone() {
+ if let Some((buffer_chunk_start, mut inlay_chunk)) = self.inlay_chunk.clone() {
+ let chunk = &mut inlay_chunk.chunk;
let buffer_chunk_end = buffer_chunk_start + InlayOffset(chunk.text.len());
let transform_end = self.transform_cursor.end(&()).1;
let chunk_end = buffer_chunk_end.min(transform_end);
@@ -1428,7 +1440,7 @@ impl<'a> Iterator for FoldChunks<'a> {
is_tab: chunk.is_tab,
is_inlay: chunk.is_inlay,
underline: chunk.underline,
- renderer: None,
+ renderer: inlay_chunk.renderer,
});
}
@@ -1,4 +1,4 @@
-use crate::{HighlightStyles, InlayId};
+use crate::{ChunkRenderer, HighlightStyles, InlayId};
use collections::BTreeSet;
use gpui::{Hsla, Rgba};
use language::{Chunk, Edit, Point, TextSummary};
@@ -8,11 +8,13 @@ use multi_buffer::{
use std::{
cmp,
ops::{Add, AddAssign, Range, Sub, SubAssign},
+ sync::Arc,
};
use sum_tree::{Bias, Cursor, SumTree};
use text::{Patch, Rope};
+use ui::{ActiveTheme, IntoElement as _, ParentElement as _, Styled as _, div};
-use super::{Highlights, custom_highlights::CustomHighlightsChunks};
+use super::{Highlights, custom_highlights::CustomHighlightsChunks, fold_map::ChunkRendererId};
/// Decides where the [`Inlay`]s should be displayed.
///
@@ -252,6 +254,13 @@ pub struct InlayChunks<'a> {
snapshot: &'a InlaySnapshot,
}
+#[derive(Clone)]
+pub struct InlayChunk<'a> {
+ pub chunk: Chunk<'a>,
+ /// Whether the inlay should be customly rendered.
+ pub renderer: Option<ChunkRenderer>,
+}
+
impl InlayChunks<'_> {
pub fn seek(&mut self, new_range: Range<InlayOffset>) {
self.transforms.seek(&new_range.start, Bias::Right, &());
@@ -271,7 +280,7 @@ impl InlayChunks<'_> {
}
impl<'a> Iterator for InlayChunks<'a> {
- type Item = Chunk<'a>;
+ type Item = InlayChunk<'a>;
fn next(&mut self) -> Option<Self::Item> {
if self.output_offset == self.max_output_offset {
@@ -296,9 +305,12 @@ impl<'a> Iterator for InlayChunks<'a> {
chunk.text = suffix;
self.output_offset.0 += prefix.len();
- Chunk {
- text: prefix,
- ..chunk.clone()
+ InlayChunk {
+ chunk: Chunk {
+ text: prefix,
+ ..chunk.clone()
+ },
+ renderer: None,
}
}
Transform::Inlay(inlay) => {
@@ -313,6 +325,7 @@ impl<'a> Iterator for InlayChunks<'a> {
}
}
+ let mut renderer = None;
let mut highlight_style = match inlay.id {
InlayId::InlineCompletion(_) => {
self.highlight_styles.inline_completion.map(|s| {
@@ -325,14 +338,31 @@ impl<'a> Iterator for InlayChunks<'a> {
}
InlayId::Hint(_) => self.highlight_styles.inlay_hint,
InlayId::DebuggerValue(_) => self.highlight_styles.inlay_hint,
- InlayId::Color(_) => match inlay.color {
- Some(color) => {
- let style = self.highlight_styles.inlay_hint.get_or_insert_default();
- style.color = Some(color);
- Some(*style)
+ InlayId::Color(_) => {
+ if let Some(color) = inlay.color {
+ renderer = Some(ChunkRenderer {
+ id: ChunkRendererId::Inlay(inlay.id),
+ render: Arc::new(move |cx| {
+ div()
+ .relative()
+ .size_3p5()
+ .child(
+ div()
+ .absolute()
+ .right_1()
+ .size_3()
+ .border_1()
+ .border_color(cx.theme().colors().border)
+ .bg(color),
+ )
+ .into_any_element()
+ }),
+ constrain_width: false,
+ measured_width: None,
+ });
}
- None => self.highlight_styles.inlay_hint,
- },
+ self.highlight_styles.inlay_hint
+ }
};
let next_inlay_highlight_endpoint;
let offset_in_inlay = self.output_offset - self.transforms.start().0;
@@ -370,11 +400,14 @@ impl<'a> Iterator for InlayChunks<'a> {
self.output_offset.0 += chunk.len();
- Chunk {
- text: chunk,
- highlight_style,
- is_inlay: true,
- ..Default::default()
+ InlayChunk {
+ chunk: Chunk {
+ text: chunk,
+ highlight_style,
+ is_inlay: true,
+ ..Chunk::default()
+ },
+ renderer,
}
}
};
@@ -1066,7 +1099,7 @@ impl InlaySnapshot {
#[cfg(test)]
pub fn text(&self) -> String {
self.chunks(Default::default()..self.len(), false, Highlights::default())
- .map(|chunk| chunk.text)
+ .map(|chunk| chunk.chunk.text)
.collect()
}
@@ -1704,7 +1737,7 @@ mod tests {
..Highlights::default()
},
)
- .map(|chunk| chunk.text)
+ .map(|chunk| chunk.chunk.text)
.collect::<String>();
assert_eq!(
actual_text,
@@ -547,6 +547,7 @@ pub enum SoftWrap {
#[derive(Clone)]
pub struct EditorStyle {
pub background: Hsla,
+ pub border: Hsla,
pub local_player: PlayerColor,
pub text: TextStyle,
pub scrollbar_width: Pixels,
@@ -562,6 +563,7 @@ impl Default for EditorStyle {
fn default() -> Self {
Self {
background: Hsla::default(),
+ border: Hsla::default(),
local_player: PlayerColor::default(),
text: TextStyle::default(),
scrollbar_width: Pixels::default(),
@@ -1143,6 +1145,7 @@ pub struct Editor {
drag_and_drop_selection_enabled: bool,
next_color_inlay_id: usize,
colors: Option<LspColorData>,
+ folding_newlines: Task<()>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
@@ -1215,6 +1218,12 @@ impl GutterDimensions {
}
}
+struct CharacterDimensions {
+ em_width: Pixels,
+ em_advance: Pixels,
+ line_height: Pixels,
+}
+
#[derive(Debug)]
pub struct RemoteSelection {
pub replica_id: ReplicaId,
@@ -1255,8 +1264,21 @@ impl Default for SelectionHistoryMode {
}
#[derive(Debug)]
+/// SelectionEffects controls the side-effects of updating the selection.
+///
+/// The default behaviour does "what you mostly want":
+/// - it pushes to the nav history if the cursor moved by >10 lines
+/// - it re-triggers completion requests
+/// - it scrolls to fit
+///
+/// You might want to modify these behaviours. For example when doing a "jump"
+/// like go to definition, we always want to add to nav history; but when scrolling
+/// in vim mode we never do.
+///
+/// Similarly, you might want to disable scrolling if you don't want the viewport to
+/// move.
pub struct SelectionEffects {
- nav_history: bool,
+ nav_history: Option<bool>,
completions: bool,
scroll: Option<Autoscroll>,
}
@@ -1264,7 +1286,7 @@ pub struct SelectionEffects {
impl Default for SelectionEffects {
fn default() -> Self {
Self {
- nav_history: true,
+ nav_history: None,
completions: true,
scroll: Some(Autoscroll::fit()),
}
@@ -1294,7 +1316,7 @@ impl SelectionEffects {
pub fn nav_history(self, nav_history: bool) -> Self {
Self {
- nav_history,
+ nav_history: Some(nav_history),
..self
}
}
@@ -1825,13 +1847,13 @@ impl Editor {
editor
.refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx);
}
- project::Event::LanguageServerAdded(server_id, ..)
- | project::Event::LanguageServerRemoved(server_id) => {
+ project::Event::LanguageServerAdded(..)
+ | project::Event::LanguageServerRemoved(..) => {
if editor.tasks_update_task.is_none() {
editor.tasks_update_task =
Some(editor.refresh_runnables(window, cx));
}
- editor.update_lsp_data(Some(*server_id), None, window, cx);
+ editor.update_lsp_data(true, None, window, cx);
}
project::Event::SnippetEdit(id, snippet_edits) => {
if let Some(buffer) = editor.buffer.read(cx).buffer(*id) {
@@ -2153,6 +2175,7 @@ impl Editor {
mode,
selection_drag_state: SelectionDragState::None,
drag_and_drop_selection_enabled: EditorSettings::get_global(cx).drag_and_drop_selection,
+ folding_newlines: Task::ready(()),
};
if let Some(breakpoints) = editor.breakpoint_store.as_ref() {
editor
@@ -2270,7 +2293,7 @@ impl Editor {
editor.minimap =
editor.create_minimap(EditorSettings::get_global(cx).minimap, window, cx);
editor.colors = Some(LspColorData::new(cx));
- editor.update_lsp_data(None, None, window, cx);
+ editor.update_lsp_data(false, None, window, cx);
}
editor.report_editor_event("Editor Opened", None, cx);
@@ -2909,11 +2932,12 @@ impl Editor {
let new_cursor_position = newest_selection.head();
let selection_start = newest_selection.start;
- if effects.nav_history {
+ if effects.nav_history.is_none() || effects.nav_history == Some(true) {
self.push_to_nav_history(
*old_cursor_position,
Some(new_cursor_position.to_point(buffer)),
false,
+ effects.nav_history == Some(true),
cx,
);
}
@@ -3155,16 +3179,15 @@ impl Editor {
/// effects of selection change occur at the end of the transaction.
pub fn change_selections<R>(
&mut self,
- effects: impl Into<SelectionEffects>,
+ effects: SelectionEffects,
window: &mut Window,
cx: &mut Context<Self>,
change: impl FnOnce(&mut MutableSelectionsCollection<'_>) -> R,
) -> R {
- let effects = effects.into();
if let Some(state) = &mut self.deferred_selection_effects_state {
state.effects.scroll = effects.scroll.or(state.effects.scroll);
state.effects.completions = effects.completions;
- state.effects.nav_history |= effects.nav_history;
+ state.effects.nav_history = effects.nav_history.or(state.effects.nav_history);
let (changed, result) = self.selections.change_with(cx, change);
state.changed |= changed;
return result;
@@ -3440,8 +3463,13 @@ impl Editor {
};
let selections_count = self.selections.count();
+ let effects = if auto_scroll {
+ SelectionEffects::default()
+ } else {
+ SelectionEffects::no_scroll()
+ };
- self.change_selections(auto_scroll.then(Autoscroll::newest), window, cx, |s| {
+ self.change_selections(effects, window, cx, |s| {
if let Some(point_to_delete) = point_to_delete {
s.delete(point_to_delete);
@@ -3479,13 +3507,18 @@ impl Editor {
.buffer_snapshot
.anchor_before(position.to_point(&display_map));
- self.change_selections(Some(Autoscroll::newest()), window, cx, |s| {
- s.clear_disjoint();
- s.set_pending_anchor_range(
- pointer_position..pointer_position,
- SelectMode::Character,
- );
- });
+ self.change_selections(
+ SelectionEffects::scroll(Autoscroll::newest()),
+ window,
+ cx,
+ |s| {
+ s.clear_disjoint();
+ s.set_pending_anchor_range(
+ pointer_position..pointer_position,
+ SelectMode::Character,
+ );
+ },
+ );
};
let tail = self.selections.newest::<Point>(cx).tail();
@@ -3600,7 +3633,7 @@ impl Editor {
pending.reversed = false;
}
- self.change_selections(None, window, cx, |s| {
+ self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.set_pending(pending, mode);
});
} else {
@@ -3616,7 +3649,7 @@ impl Editor {
self.columnar_selection_state.take();
if self.selections.pending_anchor().is_some() {
let selections = self.selections.all::<usize>(cx);
- self.change_selections(None, window, cx, |s| {
+ self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select(selections);
s.clear_pending();
});
@@ -3690,7 +3723,7 @@ impl Editor {
_ => selection_ranges,
};
- self.change_selections(None, window, cx, |s| {
+ self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges(ranges);
});
cx.notify();
@@ -3730,7 +3763,7 @@ impl Editor {
}
if self.mode.is_full()
- && self.change_selections(Some(Autoscroll::fit()), window, cx, |s| s.try_cancel())
+ && self.change_selections(Default::default(), window, cx, |s| s.try_cancel())
{
return;
}
@@ -3897,8 +3930,10 @@ impl Editor {
bracket_pair_matching_end = Some(pair.clone());
}
}
- if bracket_pair.is_none() && bracket_pair_matching_end.is_some() {
- bracket_pair = Some(bracket_pair_matching_end.unwrap());
+ if let Some(end) = bracket_pair_matching_end
+ && bracket_pair.is_none()
+ {
+ bracket_pair = Some(end);
is_bracket_pair_end = true;
}
}
@@ -4531,9 +4566,7 @@ impl Editor {
})
.collect();
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
- s.select(new_selections)
- });
+ this.change_selections(Default::default(), window, cx, |s| s.select(new_selections));
this.refresh_inline_completion(true, false, window, cx);
});
}
@@ -4562,7 +4595,7 @@ impl Editor {
self.transact(window, cx, |editor, window, cx| {
editor.edit(edits, cx);
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
let mut index = 0;
s.move_cursors_with(|map, _, _| {
let row = rows[index];
@@ -4624,7 +4657,7 @@ impl Editor {
self.transact(window, cx, |editor, window, cx| {
editor.edit(edits, cx);
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
let mut index = 0;
s.move_cursors_with(|map, _, _| {
let row = rows[index];
@@ -4701,7 +4734,7 @@ impl Editor {
anchors
});
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ this.change_selections(Default::default(), window, cx, |s| {
s.select_anchors(selection_anchors);
});
@@ -4845,7 +4878,7 @@ impl Editor {
.collect();
drop(buffer);
- self.change_selections(None, window, cx, |selections| {
+ self.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
selections.select(new_selections)
});
}
@@ -5072,7 +5105,7 @@ impl Editor {
to_insert,
}) = self.inlay_hint_cache.spawn_hint_refresh(
reason_description,
- self.excerpts_for_inlay_hints_query(required_languages.as_ref(), cx),
+ self.visible_excerpts(required_languages.as_ref(), cx),
invalidate_cache,
ignore_debounce,
cx,
@@ -5090,7 +5123,7 @@ impl Editor {
.collect()
}
- pub fn excerpts_for_inlay_hints_query(
+ pub fn visible_excerpts(
&self,
restrict_to_languages: Option<&HashSet<Arc<Language>>>,
cx: &mut Context<Editor>,
@@ -6708,6 +6741,77 @@ impl Editor {
})
}
+ fn refresh_single_line_folds(&mut self, window: &mut Window, cx: &mut Context<Editor>) {
+ struct NewlineFold;
+ let type_id = std::any::TypeId::of::<NewlineFold>();
+ if !self.mode.is_single_line() {
+ return;
+ }
+ let snapshot = self.snapshot(window, cx);
+ if snapshot.buffer_snapshot.max_point().row == 0 {
+ return;
+ }
+ let task = cx.background_spawn(async move {
+ let new_newlines = snapshot
+ .buffer_chars_at(0)
+ .filter_map(|(c, i)| {
+ if c == '\n' {
+ Some(
+ snapshot.buffer_snapshot.anchor_after(i)
+ ..snapshot.buffer_snapshot.anchor_before(i + 1),
+ )
+ } else {
+ None
+ }
+ })
+ .collect::<Vec<_>>();
+ let existing_newlines = snapshot
+ .folds_in_range(0..snapshot.buffer_snapshot.len())
+ .filter_map(|fold| {
+ if fold.placeholder.type_tag == Some(type_id) {
+ Some(fold.range.start..fold.range.end)
+ } else {
+ None
+ }
+ })
+ .collect::<Vec<_>>();
+
+ (new_newlines, existing_newlines)
+ });
+ self.folding_newlines = cx.spawn(async move |this, cx| {
+ let (new_newlines, existing_newlines) = task.await;
+ if new_newlines == existing_newlines {
+ return;
+ }
+ let placeholder = FoldPlaceholder {
+ render: Arc::new(move |_, _, cx| {
+ div()
+ .bg(cx.theme().status().hint_background)
+ .border_b_1()
+ .size_full()
+ .font(ThemeSettings::get_global(cx).buffer_font.clone())
+ .border_color(cx.theme().status().hint)
+ .child("\\n")
+ .into_any()
+ }),
+ constrain_width: false,
+ merge_adjacent: false,
+ type_tag: Some(type_id),
+ };
+ let creases = new_newlines
+ .into_iter()
+ .map(|range| Crease::simple(range, placeholder.clone()))
+ .collect();
+ this.update(cx, |this, cx| {
+ this.display_map.update(cx, |display_map, cx| {
+ display_map.remove_folds_with_type(existing_newlines, type_id, cx);
+ display_map.fold(creases, cx);
+ });
+ })
+ .ok();
+ });
+ }
+
fn refresh_selected_text_highlights(
&mut self,
on_buffer_edit: bool,
@@ -7078,7 +7182,7 @@ impl Editor {
self.unfold_ranges(&[target..target], true, false, cx);
// Note that this is also done in vim's handler of the Tab action.
self.change_selections(
- Some(Autoscroll::newest()),
+ SelectionEffects::scroll(Autoscroll::newest()),
window,
cx,
|selections| {
@@ -7123,7 +7227,7 @@ impl Editor {
buffer.edit(edits.iter().cloned(), None, cx)
});
- self.change_selections(None, window, cx, |s| {
+ self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_anchor_ranges([last_edit_end..last_edit_end]);
});
@@ -7170,9 +7274,14 @@ impl Editor {
match &active_inline_completion.completion {
InlineCompletion::Move { target, .. } => {
let target = *target;
- self.change_selections(Some(Autoscroll::newest()), window, cx, |selections| {
- selections.select_anchor_ranges([target..target]);
- });
+ self.change_selections(
+ SelectionEffects::scroll(Autoscroll::newest()),
+ window,
+ cx,
+ |selections| {
+ selections.select_anchor_ranges([target..target]);
+ },
+ );
}
InlineCompletion::Edit { edits, .. } => {
// Find an insertion that starts at the cursor position.
@@ -7773,9 +7882,12 @@ impl Editor {
this.entry("Run to cursor", None, move |window, cx| {
weak_editor
.update(cx, |editor, cx| {
- editor.change_selections(None, window, cx, |s| {
- s.select_ranges([Point::new(row, 0)..Point::new(row, 0)])
- });
+ editor.change_selections(
+ SelectionEffects::no_scroll(),
+ window,
+ cx,
+ |s| s.select_ranges([Point::new(row, 0)..Point::new(row, 0)]),
+ );
})
.ok();
@@ -9316,7 +9428,7 @@ impl Editor {
.collect::<Vec<_>>()
});
if let Some(tabstop) = tabstops.first() {
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
// Reverse order so that the first range is the newest created selection.
// Completions will use it and autoscroll will prioritize it.
s.select_ranges(tabstop.ranges.iter().rev().cloned());
@@ -9434,7 +9546,7 @@ impl Editor {
}
}
if let Some(current_ranges) = snippet.ranges.get(snippet.active_index) {
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
// Reverse order so that the first range is the newest created selection.
// Completions will use it and autoscroll will prioritize it.
s.select_ranges(current_ranges.iter().rev().cloned())
@@ -9524,9 +9636,7 @@ impl Editor {
}
}
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
- s.select(selections)
- });
+ this.change_selections(Default::default(), window, cx, |s| s.select(selections));
this.insert("", window, cx);
let empty_str: Arc<str> = Arc::from("");
for (buffer, edits) in linked_ranges {
@@ -9562,7 +9672,7 @@ impl Editor {
pub fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
self.transact(window, cx, |this, window, cx| {
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ this.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
if selection.is_empty() {
let cursor = movement::right(map, selection.head());
@@ -9705,9 +9815,7 @@ impl Editor {
self.transact(window, cx, |this, window, cx| {
this.buffer.update(cx, |b, cx| b.edit(edits, None, cx));
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
- s.select(selections)
- });
+ this.change_selections(Default::default(), window, cx, |s| s.select(selections));
this.refresh_inline_completion(true, false, window, cx);
});
}
@@ -9740,9 +9848,7 @@ impl Editor {
self.transact(window, cx, |this, window, cx| {
this.buffer.update(cx, |b, cx| b.edit(edits, None, cx));
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
- s.select(selections)
- });
+ this.change_selections(Default::default(), window, cx, |s| s.select(selections));
});
}
@@ -9895,9 +10001,7 @@ impl Editor {
);
});
let selections = this.selections.all::<usize>(cx);
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
- s.select(selections)
- });
+ this.change_selections(Default::default(), window, cx, |s| s.select(selections));
});
}
@@ -9922,9 +10026,7 @@ impl Editor {
buffer.autoindent_ranges(selections, cx);
});
let selections = this.selections.all::<usize>(cx);
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
- s.select(selections)
- });
+ this.change_selections(Default::default(), window, cx, |s| s.select(selections));
});
}
@@ -10005,7 +10107,7 @@ impl Editor {
})
.collect();
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ this.change_selections(Default::default(), window, cx, |s| {
s.select(new_selections);
});
});
@@ -10071,7 +10173,7 @@ impl Editor {
}
}
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ this.change_selections(Default::default(), window, cx, |s| {
s.select_anchor_ranges(cursor_positions)
});
});
@@ -10658,7 +10760,7 @@ impl Editor {
})
.collect();
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ this.change_selections(Default::default(), window, cx, |s| {
s.select(new_selections);
});
@@ -11009,7 +11111,7 @@ impl Editor {
buffer.edit(edits, None, cx);
});
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ this.change_selections(Default::default(), window, cx, |s| {
s.select(new_selections);
});
@@ -11045,7 +11147,7 @@ impl Editor {
this.buffer.update(cx, |buffer, cx| {
buffer.edit(edits, None, cx);
});
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ this.change_selections(Default::default(), window, cx, |s| {
s.select_anchor_ranges([last_edit_start..last_edit_end]);
});
});
@@ -11247,7 +11349,7 @@ impl Editor {
}
});
this.fold_creases(refold_creases, true, window, cx);
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ this.change_selections(Default::default(), window, cx, |s| {
s.select(new_selections);
})
});
@@ -11348,9 +11450,7 @@ impl Editor {
}
});
this.fold_creases(refold_creases, true, window, cx);
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
- s.select(new_selections)
- });
+ this.change_selections(Default::default(), window, cx, |s| s.select(new_selections));
});
}
@@ -11358,7 +11458,7 @@ impl Editor {
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
let text_layout_details = &self.text_layout_details(window);
self.transact(window, cx, |this, window, cx| {
- let edits = this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ let edits = this.change_selections(Default::default(), window, cx, |s| {
let mut edits: Vec<(Range<usize>, String)> = Default::default();
s.move_with(|display_map, selection| {
if !selection.is_empty() {
@@ -11406,7 +11506,7 @@ impl Editor {
this.buffer
.update(cx, |buffer, cx| buffer.edit(edits, None, cx));
let selections = this.selections.all::<usize>(cx);
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ this.change_selections(Default::default(), window, cx, |s| {
s.select(selections);
});
});
@@ -11426,42 +11526,106 @@ impl Editor {
let buffer = self.buffer.read(cx).snapshot(cx);
let selections = self.selections.all::<Point>(cx);
- // Shrink and split selections to respect paragraph boundaries.
- let ranges = selections.into_iter().flat_map(|selection| {
+ // Split selections to respect paragraph, indent, and comment prefix boundaries.
+ let wrap_ranges = selections.into_iter().flat_map(|selection| {
+ let mut non_blank_rows_iter = (selection.start.row..=selection.end.row)
+ .filter(|row| !buffer.is_line_blank(MultiBufferRow(*row)))
+ .peekable();
+
+ let first_row = if let Some(&row) = non_blank_rows_iter.peek() {
+ row
+ } else {
+ return Vec::new();
+ };
+
let language_settings = buffer.language_settings_at(selection.head(), cx);
let language_scope = buffer.language_scope_at(selection.head());
- let Some(start_row) = (selection.start.row..=selection.end.row)
- .find(|row| !buffer.is_line_blank(MultiBufferRow(*row)))
- else {
- return vec![];
- };
- let Some(end_row) = (selection.start.row..=selection.end.row)
- .rev()
- .find(|row| !buffer.is_line_blank(MultiBufferRow(*row)))
- else {
- return vec![];
- };
+ let indent_and_prefix_for_row =
+ |row: u32| -> (IndentSize, Option<String>, Option<String>) {
+ let indent = buffer.indent_size_for_line(MultiBufferRow(row));
+ let (comment_prefix, rewrap_prefix) =
+ if let Some(language_scope) = &language_scope {
+ let indent_end = Point::new(row, indent.len);
+ let comment_prefix = language_scope
+ .line_comment_prefixes()
+ .iter()
+ .find(|prefix| buffer.contains_str_at(indent_end, prefix))
+ .map(|prefix| prefix.to_string());
+ let line_end = Point::new(row, buffer.line_len(MultiBufferRow(row)));
+ let line_text_after_indent = buffer
+ .text_for_range(indent_end..line_end)
+ .collect::<String>();
+ let rewrap_prefix = language_scope
+ .rewrap_prefixes()
+ .iter()
+ .find_map(|prefix_regex| {
+ prefix_regex.find(&line_text_after_indent).map(|mat| {
+ if mat.start() == 0 {
+ Some(mat.as_str().to_string())
+ } else {
+ None
+ }
+ })
+ })
+ .flatten();
+ (comment_prefix, rewrap_prefix)
+ } else {
+ (None, None)
+ };
+ (indent, comment_prefix, rewrap_prefix)
+ };
- let mut row = start_row;
let mut ranges = Vec::new();
- while let Some(blank_row) =
- (row..end_row).find(|row| buffer.is_line_blank(MultiBufferRow(*row)))
- {
- let next_paragraph_start = (blank_row + 1..=end_row)
- .find(|row| !buffer.is_line_blank(MultiBufferRow(*row)))
- .unwrap();
- ranges.push((
- language_settings.clone(),
- language_scope.clone(),
- Point::new(row, 0)..Point::new(blank_row - 1, 0),
- ));
- row = next_paragraph_start;
+ let from_empty_selection = selection.is_empty();
+
+ let mut current_range_start = first_row;
+ let mut prev_row = first_row;
+ let (
+ mut current_range_indent,
+ mut current_range_comment_prefix,
+ mut current_range_rewrap_prefix,
+ ) = indent_and_prefix_for_row(first_row);
+
+ for row in non_blank_rows_iter.skip(1) {
+ let has_paragraph_break = row > prev_row + 1;
+
+ let (row_indent, row_comment_prefix, row_rewrap_prefix) =
+ indent_and_prefix_for_row(row);
+
+ let has_indent_change = row_indent != current_range_indent;
+ let has_comment_change = row_comment_prefix != current_range_comment_prefix;
+
+ let has_boundary_change = has_comment_change
+ || row_rewrap_prefix.is_some()
+ || (has_indent_change && current_range_comment_prefix.is_some());
+
+ if has_paragraph_break || has_boundary_change {
+ ranges.push((
+ language_settings.clone(),
+ Point::new(current_range_start, 0)
+ ..Point::new(prev_row, buffer.line_len(MultiBufferRow(prev_row))),
+ current_range_indent,
+ current_range_comment_prefix.clone(),
+ current_range_rewrap_prefix.clone(),
+ from_empty_selection,
+ ));
+ current_range_start = row;
+ current_range_indent = row_indent;
+ current_range_comment_prefix = row_comment_prefix;
+ current_range_rewrap_prefix = row_rewrap_prefix;
+ }
+ prev_row = row;
}
+
ranges.push((
language_settings.clone(),
- language_scope.clone(),
- Point::new(row, 0)..Point::new(end_row, 0),
+ Point::new(current_range_start, 0)
+ ..Point::new(prev_row, buffer.line_len(MultiBufferRow(prev_row))),
+ current_range_indent,
+ current_range_comment_prefix,
+ current_range_rewrap_prefix,
+ from_empty_selection,
));
ranges
@@ -11470,9 +11634,17 @@ impl Editor {
let mut edits = Vec::new();
let mut rewrapped_row_ranges = Vec::<RangeInclusive<u32>>::new();
- for (language_settings, language_scope, range) in ranges {
- let mut start_row = range.start.row;
- let mut end_row = range.end.row;
+ for (
+ language_settings,
+ wrap_range,
+ indent_size,
+ comment_prefix,
+ rewrap_prefix,
+ from_empty_selection,
+ ) in wrap_ranges
+ {
+ let mut start_row = wrap_range.start.row;
+ let mut end_row = wrap_range.end.row;
// Skip selections that overlap with a range that has already been rewrapped.
let selection_range = start_row..end_row;
@@ -11485,49 +11657,20 @@ impl Editor {
let tab_size = language_settings.tab_size;
- // Since not all lines in the selection may be at the same indent
- // level, choose the indent size that is the most common between all
- // of the lines.
- //
- // If there is a tie, we use the deepest indent.
- let (indent_size, indent_end) = {
- let mut indent_size_occurrences = HashMap::default();
- let mut rows_by_indent_size = HashMap::<IndentSize, Vec<u32>>::default();
-
- for row in start_row..=end_row {
- let indent = buffer.indent_size_for_line(MultiBufferRow(row));
- rows_by_indent_size.entry(indent).or_default().push(row);
- *indent_size_occurrences.entry(indent).or_insert(0) += 1;
- }
-
- let indent_size = indent_size_occurrences
- .into_iter()
- .max_by_key(|(indent, count)| (*count, indent.len_with_expanded_tabs(tab_size)))
- .map(|(indent, _)| indent)
- .unwrap_or_default();
- let row = rows_by_indent_size[&indent_size][0];
- let indent_end = Point::new(row, indent_size.len);
-
- (indent_size, indent_end)
- };
-
- let mut line_prefix = indent_size.chars().collect::<String>();
-
+ let indent_prefix = indent_size.chars().collect::<String>();
+ let mut line_prefix = indent_prefix.clone();
let mut inside_comment = false;
- if let Some(comment_prefix) = language_scope.and_then(|language| {
- language
- .line_comment_prefixes()
- .iter()
- .find(|prefix| buffer.contains_str_at(indent_end, prefix))
- .cloned()
- }) {
- line_prefix.push_str(&comment_prefix);
+ if let Some(prefix) = &comment_prefix {
+ line_prefix.push_str(prefix);
inside_comment = true;
}
+ if let Some(prefix) = &rewrap_prefix {
+ line_prefix.push_str(prefix);
+ }
let allow_rewrap_based_on_language = match language_settings.allow_rewrap {
RewrapBehavior::InComments => inside_comment,
- RewrapBehavior::InSelections => !range.is_empty(),
+ RewrapBehavior::InSelections => !wrap_range.is_empty(),
RewrapBehavior::Anywhere => true,
};
@@ -11538,7 +11681,7 @@ impl Editor {
continue;
}
- if range.is_empty() {
+ if from_empty_selection {
'expand_upwards: while start_row > 0 {
let prev_row = start_row - 1;
if buffer.contains_str_at(Point::new(prev_row, 0), &line_prefix)
@@ -11570,12 +11713,18 @@ impl Editor {
let selection_text = buffer.text_for_range(start..end).collect::<String>();
let Some(lines_without_prefixes) = selection_text
.lines()
- .map(|line| {
- line.strip_prefix(&line_prefix)
- .or_else(|| line.trim_start().strip_prefix(&line_prefix.trim_start()))
- .with_context(|| {
- format!("line did not start with prefix {line_prefix:?}: {line:?}")
- })
+ .enumerate()
+ .map(|(ix, line)| {
+ let line_trimmed = line.trim_start();
+ if rewrap_prefix.is_some() && ix > 0 {
+ Ok(line_trimmed)
+ } else {
+ line_trimmed
+ .strip_prefix(&line_prefix.trim_start())
+ .with_context(|| {
+ format!("line did not start with prefix {line_prefix:?}: {line:?}")
+ })
+ }
})
.collect::<Result<Vec<_>, _>>()
.log_err()
@@ -11588,8 +11737,16 @@ impl Editor {
.language_settings_at(Point::new(start_row, 0), cx)
.preferred_line_length as usize
});
+
+ let subsequent_lines_prefix = if let Some(rewrap_prefix_str) = &rewrap_prefix {
+ format!("{}{}", indent_prefix, " ".repeat(rewrap_prefix_str.len()))
+ } else {
+ line_prefix.clone()
+ };
+
let wrapped_text = wrap_with_prefix(
line_prefix,
+ subsequent_lines_prefix,
lines_without_prefixes.join("\n"),
wrap_column,
tab_size,
@@ -11662,7 +11819,7 @@ impl Editor {
}
self.transact(window, cx, |this, window, cx| {
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ this.change_selections(Default::default(), window, cx, |s| {
s.select(selections);
});
this.insert("", window, cx);
@@ -11678,7 +11835,7 @@ impl Editor {
pub fn kill_ring_cut(&mut self, _: &KillRingCut, window: &mut Window, cx: &mut Context<Self>) {
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
- self.change_selections(None, window, cx, |s| {
+ self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|snapshot, sel| {
if sel.is_empty() {
sel.end = DisplayPoint::new(sel.end.row(), snapshot.line_len(sel.end.row()))
@@ -11882,9 +12039,7 @@ impl Editor {
});
let selections = this.selections.all::<usize>(cx);
- this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
- s.select(selections)
- });
+ this.change_selections(Default::default(), window, cx, |s| s.select(selections));
} else {
this.insert(&clipboard_text, window, cx);
}
@@ -11923,7 +12078,7 @@ impl Editor {
if let Some((selections, _)) =
self.selection_history.transaction(transaction_id).cloned()
{
- self.change_selections(None, window, cx, |s| {
+ self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_anchors(selections.to_vec());
});
} else {
@@ -11953,7 +12108,7 @@ impl Editor {
if let Some((_, Some(selections))) =
self.selection_history.transaction(transaction_id).cloned()
{
- self.change_selections(None, window, cx, |s| {
+ self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_anchors(selections.to_vec());
});
} else {
@@ -11983,7 +12138,7 @@ impl Editor {
pub fn move_left(&mut self, _: &MoveLeft, window: &mut Window, cx: &mut Context<Self>) {
self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let cursor = if selection.is_empty() {
movement::left(map, selection.start)
@@ -11997,14 +12152,14 @@ impl Editor {
pub fn select_left(&mut self, _: &SelectLeft, window: &mut Window, cx: &mut Context<Self>) {
self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
s.move_heads_with(|map, head, _| (movement::left(map, head), SelectionGoal::None));
})
}
pub fn move_right(&mut self, _: &MoveRight, window: &mut Window, cx: &mut Context<Self>) {
self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let cursor = if selection.is_empty() {
movement::right(map, selection.end)
@@ -12018,7 +12173,7 @@ impl Editor {
pub fn select_right(&mut self, _: &SelectRight, window: &mut Window, cx: &mut Context<Self>) {
self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
s.move_heads_with(|map, head, _| (movement::right(map, head), SelectionGoal::None));
})
}
@@ -12039,7 +12194,7 @@ impl Editor {
let selection_count = self.selections.count();
let first_selection = self.selections.first_anchor();
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
if !selection.is_empty() {
selection.goal = SelectionGoal::None;
@@ -12080,7 +12235,7 @@ impl Editor {
let text_layout_details = &self.text_layout_details(window);
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
if !selection.is_empty() {
selection.goal = SelectionGoal::None;
@@ -12117,7 +12272,7 @@ impl Editor {
let text_layout_details = &self.text_layout_details(window);
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
if !selection.is_empty() {
selection.goal = SelectionGoal::None;
@@ -12143,7 +12298,7 @@ impl Editor {
) {
self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
let text_layout_details = &self.text_layout_details(window);
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
s.move_heads_with(|map, head, goal| {
movement::down_by_rows(map, head, action.lines, goal, false, text_layout_details)
})
@@ -12158,7 +12313,7 @@ impl Editor {
) {
self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
let text_layout_details = &self.text_layout_details(window);
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
s.move_heads_with(|map, head, goal| {
movement::up_by_rows(map, head, action.lines, goal, false, text_layout_details)
})
@@ -12179,7 +12334,7 @@ impl Editor {
let text_layout_details = &self.text_layout_details(window);
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
s.move_heads_with(|map, head, goal| {
movement::up_by_rows(map, head, row_count, goal, false, text_layout_details)
})
@@ -12217,15 +12372,15 @@ impl Editor {
self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx);
- let autoscroll = if action.center_cursor {
- Autoscroll::center()
+ let effects = if action.center_cursor {
+ SelectionEffects::scroll(Autoscroll::center())
} else {
- Autoscroll::fit()
+ SelectionEffects::default()
};
let text_layout_details = &self.text_layout_details(window);
- self.change_selections(Some(autoscroll), window, cx, |s| {
+ self.change_selections(effects, window, cx, |s| {
s.move_with(|map, selection| {
if !selection.is_empty() {
selection.goal = SelectionGoal::None;
@@ -378,7 +378,6 @@ pub enum SnippetSortOrder {
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
-#[schemars(deny_unknown_fields)]
pub struct EditorSettingsContent {
/// Whether the cursor blinks in the editor.
///
@@ -3,7 +3,7 @@ use std::sync::Arc;
use gpui::{App, FontFeatures, FontWeight};
use project::project_settings::{InlineBlameSettings, ProjectSettings};
use settings::{EditableSettingControl, Settings};
-use theme::{FontFamilyCache, ThemeSettings};
+use theme::{FontFamilyCache, FontFamilyName, ThemeSettings};
use ui::{
CheckboxWithLabel, ContextMenu, DropdownMenu, NumericStepper, SettingsContainer, SettingsGroup,
prelude::*,
@@ -75,7 +75,7 @@ impl EditableSettingControl for BufferFontFamilyControl {
value: Self::Value,
_cx: &App,
) {
- settings.buffer_font_family = Some(value.to_string());
+ settings.buffer_font_family = Some(FontFamilyName(value.into()));
}
}
@@ -30,7 +30,7 @@ use language::{
},
tree_sitter_python,
};
-use language_settings::{Formatter, FormatterList, IndentGuideSettings};
+use language_settings::{Formatter, IndentGuideSettings};
use lsp::CompletionParams;
use multi_buffer::{IndentGuide, PathKey};
use parking_lot::Mutex;
@@ -55,7 +55,8 @@ use util::{
uri,
};
use workspace::{
- CloseActiveItem, CloseAllItems, CloseInactiveItems, NavigationEntry, OpenOptions, ViewId,
+ CloseActiveItem, CloseAllItems, CloseInactiveItems, MoveItemToPaneInDirection, NavigationEntry,
+ OpenOptions, ViewId,
item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
};
@@ -179,7 +180,9 @@ fn test_edit_events(cx: &mut TestAppContext) {
// No event is emitted when the mutation is a no-op.
_ = editor2.update(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([0..0])
+ });
editor.backspace(&Backspace, window, cx);
});
@@ -202,7 +205,9 @@ fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
_ = editor.update(cx, |editor, window, cx| {
editor.start_transaction_at(now, window, cx);
- editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([2..4])
+ });
editor.insert("cd", window, cx);
editor.end_transaction_at(now, cx);
@@ -210,14 +215,18 @@ fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
assert_eq!(editor.selections.ranges(cx), vec![4..4]);
editor.start_transaction_at(now, window, cx);
- editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([4..5])
+ });
editor.insert("e", window, cx);
editor.end_transaction_at(now, cx);
assert_eq!(editor.text(cx), "12cde6");
assert_eq!(editor.selections.ranges(cx), vec![5..5]);
now += group_interval + Duration::from_millis(1);
- editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([2..2])
+ });
// Simulate an edit in another editor
buffer.update(cx, |buffer, cx| {
@@ -325,7 +334,7 @@ fn test_ime_composition(cx: &mut TestAppContext) {
assert_eq!(editor.marked_text_ranges(cx), None);
// Start a new IME composition with multiple cursors.
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([
OffsetUtf16(1)..OffsetUtf16(1),
OffsetUtf16(3)..OffsetUtf16(3),
@@ -623,7 +632,7 @@ fn test_clone(cx: &mut TestAppContext) {
});
_ = editor.update(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges(selection_ranges.clone())
});
editor.fold_creases(
@@ -709,12 +718,12 @@ async fn test_navigation_history(cx: &mut TestAppContext) {
// Move the cursor a small distance.
// Nothing is added to the navigation history.
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
])
});
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
])
@@ -723,7 +732,7 @@ async fn test_navigation_history(cx: &mut TestAppContext) {
// Move the cursor a large distance.
// The history can jump back to the previous position.
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
])
@@ -893,7 +902,7 @@ fn test_fold_action(cx: &mut TestAppContext) {
});
_ = editor.update(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
]);
@@ -984,7 +993,7 @@ fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
});
_ = editor.update(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
]);
@@ -1069,7 +1078,7 @@ fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
});
_ = editor.update(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
]);
@@ -1301,7 +1310,7 @@ fn test_move_cursor(cx: &mut TestAppContext) {
&[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
);
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
]);
@@ -1446,7 +1455,7 @@ fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
build_editor(buffer.clone(), window, cx)
});
_ = editor.update(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([empty_range(0, "βββββ".len())]);
});
@@ -1536,7 +1545,7 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) {
build_editor(buffer, window, cx)
});
_ = editor.update(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
@@ -1731,7 +1740,7 @@ fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
// First, let's assert behavior on the first line, that was not soft-wrapped.
// Start the cursor at the `k` on the first line
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
]);
@@ -1753,7 +1762,7 @@ fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
// Now, let's assert behavior on the second line, that ended up being soft-wrapped.
// Start the cursor at the last line (`y` that was wrapped to a new line)
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
]);
@@ -1819,7 +1828,7 @@ fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
});
_ = editor.update(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
@@ -1901,7 +1910,7 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
build_editor(buffer, window, cx)
});
_ = editor.update(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
@@ -1971,7 +1980,7 @@ fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
"use one::{\n two::three::\n four::five\n};"
);
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
]);
@@ -2234,7 +2243,7 @@ async fn test_autoscroll(cx: &mut TestAppContext) {
// on screen, the editor autoscrolls to reveal the newest cursor, and
// allows the vertical scroll margin below that cursor.
cx.update_editor(|editor, window, cx| {
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
+ editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges([
Point::new(0, 0)..Point::new(0, 0),
Point::new(6, 0)..Point::new(6, 0),
@@ -2262,7 +2271,7 @@ async fn test_autoscroll(cx: &mut TestAppContext) {
// Add a cursor above the visible area. Since both cursors fit on screen,
// the editor scrolls to show both.
cx.update_editor(|editor, window, cx| {
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
+ editor.change_selections(Default::default(), window, cx, |selections| {
selections.select_ranges([
Point::new(1, 0)..Point::new(1, 0),
Point::new(6, 0)..Point::new(6, 0),
@@ -2429,7 +2438,7 @@ fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
});
_ = editor.update(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
// an empty selection - the preceding word fragment is deleted
DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
@@ -2448,7 +2457,7 @@ fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
});
_ = editor.update(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
// an empty selection - the following word fragment is deleted
DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
@@ -2483,7 +2492,7 @@ fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
};
_ = editor.update(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
])
@@ -2519,7 +2528,7 @@ fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
};
_ = editor.update(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
])
@@ -2558,7 +2567,7 @@ fn test_newline(cx: &mut TestAppContext) {
});
_ = editor.update(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
@@ -2591,7 +2600,7 @@ fn test_newline_with_old_selections(cx: &mut TestAppContext) {
cx,
);
let mut editor = build_editor(buffer.clone(), window, cx);
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([
Point::new(2, 4)..Point::new(2, 5),
Point::new(5, 4)..Point::new(5, 5),
@@ -3078,7 +3087,7 @@ fn test_insert_with_old_selections(cx: &mut TestAppContext) {
let editor = cx.add_window(|window, cx| {
let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
let mut editor = build_editor(buffer.clone(), window, cx);
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([3..4, 11..12, 19..20])
});
editor
@@ -3558,7 +3567,7 @@ async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
#[gpui::test]
fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
init_test(cx, |settings| {
- settings.languages.extend([
+ settings.languages.0.extend([
(
"TOML".into(),
LanguageSettingsContent {
@@ -3727,7 +3736,7 @@ fn test_delete_line(cx: &mut TestAppContext) {
build_editor(buffer, window, cx)
});
_ = editor.update(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
@@ -3750,7 +3759,7 @@ fn test_delete_line(cx: &mut TestAppContext) {
build_editor(buffer, window, cx)
});
_ = editor.update(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
])
@@ -3787,7 +3796,7 @@ fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
);
// When multiple lines are selected, remove newlines that are spanned by the selection
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
});
editor.join_lines(&JoinLines, window, cx);
@@ -3806,7 +3815,7 @@ fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
);
// When joining an empty line don't insert a space
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
});
editor.join_lines(&JoinLines, window, cx);
@@ -3846,7 +3855,7 @@ fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
// We remove any leading spaces
assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
});
editor.join_lines(&JoinLines, window, cx);
@@ -3873,7 +3882,7 @@ fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
let mut editor = build_editor(buffer.clone(), window, cx);
let buffer = buffer.read(cx).as_singleton().unwrap();
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([
Point::new(0, 2)..Point::new(1, 1),
Point::new(1, 2)..Point::new(1, 2),
@@ -4335,48 +4344,60 @@ async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
cx.update_editor(|e, window, cx| {
e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
});
- cx.assert_editor_state(indoc! {"
- Β«
- abc // No indentation
- abc // 1 tab
- abc // 2 tabs
- abc // Tab followed by space
- abc // Space followed by tab (3 spaces should be the result)
- abc // Mixed indentation (tab conversion depends on the column)
- abc // Already space indented
-
- abc\tdef // Only the leading tab is manipulatedΛΒ»
- "});
+ cx.assert_editor_state(
+ indoc! {"
+ Β«
+ abc // No indentation
+ abc // 1 tab
+ abc // 2 tabs
+ abc // Tab followed by space
+ abc // Space followed by tab (3 spaces should be the result)
+ abc // Mixed indentation (tab conversion depends on the column)
+ abc // Already space indented
+ Β·
+ abc\tdef // Only the leading tab is manipulatedΛΒ»
+ "}
+ .replace("Β·", "")
+ .as_str(), // Β· used as placeholder to prevent format-on-save from removing whitespace
+ );
// Test on just a few lines, the others should remain unchanged
// Only lines (3, 5, 10, 11) should change
- cx.set_state(indoc! {"
-
- abc // No indentation
- \tabcΛ // 1 tab
- \t\tabc // 2 tabs
- \t abcΛ // Tab followed by space
- \tabc // Space followed by tab (3 spaces should be the result)
- \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
- abc // Already space indented
- Β«\t
- \tabc\tdef // Only the leading tab is manipulatedΛΒ»
- "});
+ cx.set_state(
+ indoc! {"
+ Β·
+ abc // No indentation
+ \tabcΛ // 1 tab
+ \t\tabc // 2 tabs
+ \t abcΛ // Tab followed by space
+ \tabc // Space followed by tab (3 spaces should be the result)
+ \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
+ abc // Already space indented
+ Β«\t
+ \tabc\tdef // Only the leading tab is manipulatedΛΒ»
+ "}
+ .replace("Β·", "")
+ .as_str(), // Β· used as placeholder to prevent format-on-save from removing whitespace
+ );
cx.update_editor(|e, window, cx| {
e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
});
- cx.assert_editor_state(indoc! {"
-
- abc // No indentation
- Β« abc // 1 tabΛΒ»
- \t\tabc // 2 tabs
- Β« abc // Tab followed by spaceΛΒ»
- \tabc // Space followed by tab (3 spaces should be the result)
- \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
- abc // Already space indented
- Β«
- abc\tdef // Only the leading tab is manipulatedΛΒ»
- "});
+ cx.assert_editor_state(
+ indoc! {"
+ Β·
+ abc // No indentation
+ Β« abc // 1 tabΛΒ»
+ \t\tabc // 2 tabs
+ Β« abc // Tab followed by spaceΛΒ»
+ \tabc // Space followed by tab (3 spaces should be the result)
+ \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
+ abc // Already space indented
+ Β« Β·
+ abc\tdef // Only the leading tab is manipulatedΛΒ»
+ "}
+ .replace("Β·", "")
+ .as_str(), // Β· used as placeholder to prevent format-on-save from removing whitespace
+ );
// SINGLE SELECTION
// Ln.1 "Β«" tests empty lines
@@ -4396,18 +4417,22 @@ async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
cx.update_editor(|e, window, cx| {
e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
});
- cx.assert_editor_state(indoc! {"
- Β«
- abc // No indentation
- abc // 1 tab
- abc // 2 tabs
- abc // Tab followed by space
- abc // Space followed by tab (3 spaces should be the result)
- abc // Mixed indentation (tab conversion depends on the column)
- abc // Already space indented
-
- abc\tdef // Only the leading tab is manipulatedΛΒ»
- "});
+ cx.assert_editor_state(
+ indoc! {"
+ Β«
+ abc // No indentation
+ abc // 1 tab
+ abc // 2 tabs
+ abc // Tab followed by space
+ abc // Space followed by tab (3 spaces should be the result)
+ abc // Mixed indentation (tab conversion depends on the column)
+ abc // Already space indented
+ Β·
+ abc\tdef // Only the leading tab is manipulatedΛΒ»
+ "}
+ .replace("Β·", "")
+ .as_str(), // Β· used as placeholder to prevent format-on-save from removing whitespace
+ );
}
#[gpui::test]
@@ -4455,39 +4480,47 @@ async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
// Test on just a few lines, the other should remain unchanged
// Only lines (4, 8, 11, 12) should change
- cx.set_state(indoc! {"
-
- abc // No indentation
- abc // 1 space (< 3 so dont convert)
- abc // 2 spaces (< 3 so dont convert)
- Β« abc // 3 spaces (convert)ΛΒ»
- abc // 5 spaces (1 tab + 2 spaces)
- \t\t\tabc // Already tab indented
- \t abc // Tab followed by space
- \tabc Λ // Space followed by tab (should be consumed due to tab)
- \t\t \tabc // Mixed indentation
- \t \t \t \tabc // Mixed indentation
- \t \tΛ
- Β« abc \t // Only the leading spaces should be convertedΛΒ»
- "});
+ cx.set_state(
+ indoc! {"
+ Β·
+ abc // No indentation
+ abc // 1 space (< 3 so dont convert)
+ abc // 2 spaces (< 3 so dont convert)
+ Β« abc // 3 spaces (convert)ΛΒ»
+ abc // 5 spaces (1 tab + 2 spaces)
+ \t\t\tabc // Already tab indented
+ \t abc // Tab followed by space
+ \tabc Λ // Space followed by tab (should be consumed due to tab)
+ \t\t \tabc // Mixed indentation
+ \t \t \t \tabc // Mixed indentation
+ \t \tΛ
+ Β« abc \t // Only the leading spaces should be convertedΛΒ»
+ "}
+ .replace("Β·", "")
+ .as_str(), // Β· used as placeholder to prevent format-on-save from removing whitespace
+ );
cx.update_editor(|e, window, cx| {
e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
});
- cx.assert_editor_state(indoc! {"
-
- abc // No indentation
- abc // 1 space (< 3 so dont convert)
- abc // 2 spaces (< 3 so dont convert)
- Β«\tabc // 3 spaces (convert)ΛΒ»
- abc // 5 spaces (1 tab + 2 spaces)
- \t\t\tabc // Already tab indented
- \t abc // Tab followed by space
- Β«\tabc // Space followed by tab (should be consumed due to tab)ΛΒ»
- \t\t \tabc // Mixed indentation
- \t \t \t \tabc // Mixed indentation
- Β«\t\t\t
- \tabc \t // Only the leading spaces should be convertedΛΒ»
- "});
+ cx.assert_editor_state(
+ indoc! {"
+ Β·
+ abc // No indentation
+ abc // 1 space (< 3 so dont convert)
+ abc // 2 spaces (< 3 so dont convert)
+ Β«\tabc // 3 spaces (convert)ΛΒ»
+ abc // 5 spaces (1 tab + 2 spaces)
+ \t\t\tabc // Already tab indented
+ \t abc // Tab followed by space
+ Β«\tabc // Space followed by tab (should be consumed due to tab)ΛΒ»
+ \t\t \tabc // Mixed indentation
+ \t \t \t \tabc // Mixed indentation
+ Β«\t\t\t
+ \tabc \t // Only the leading spaces should be convertedΛΒ»
+ "}
+ .replace("Β·", "")
+ .as_str(), // Β· used as placeholder to prevent format-on-save from removing whitespace
+ );
// SINGLE SELECTION
// Ln.1 "Β«" tests empty lines
@@ -4689,7 +4722,7 @@ fn test_duplicate_line(cx: &mut TestAppContext) {
build_editor(buffer, window, cx)
});
_ = editor.update(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
@@ -4715,7 +4748,7 @@ fn test_duplicate_line(cx: &mut TestAppContext) {
build_editor(buffer, window, cx)
});
_ = editor.update(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
@@ -4739,7 +4772,7 @@ fn test_duplicate_line(cx: &mut TestAppContext) {
build_editor(buffer, window, cx)
});
_ = editor.update(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
@@ -4765,7 +4798,7 @@ fn test_duplicate_line(cx: &mut TestAppContext) {
build_editor(buffer, window, cx)
});
_ = editor.update(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
@@ -4787,7 +4820,7 @@ fn test_duplicate_line(cx: &mut TestAppContext) {
build_editor(buffer, window, cx)
});
_ = editor.update(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
@@ -4824,7 +4857,7 @@ fn test_move_line_up_down(cx: &mut TestAppContext) {
window,
cx,
);
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
@@ -4927,7 +4960,7 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
Some(Autoscroll::fit()),
cx,
);
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
});
editor.move_line_down(&MoveLineDown, window, cx);
@@ -5012,7 +5045,9 @@ fn test_transpose(cx: &mut TestAppContext) {
_ = cx.add_window(|window, cx| {
let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
editor.set_style(EditorStyle::default(), window, cx);
- editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([1..1])
+ });
editor.transpose(&Default::default(), window, cx);
assert_eq!(editor.text(cx), "bac");
assert_eq!(editor.selections.ranges(cx), [2..2]);
@@ -5031,12 +5066,16 @@ fn test_transpose(cx: &mut TestAppContext) {
_ = cx.add_window(|window, cx| {
let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
editor.set_style(EditorStyle::default(), window, cx);
- editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([3..3])
+ });
editor.transpose(&Default::default(), window, cx);
assert_eq!(editor.text(cx), "acb\nde");
assert_eq!(editor.selections.ranges(cx), [3..3]);
- editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([4..4])
+ });
editor.transpose(&Default::default(), window, cx);
assert_eq!(editor.text(cx), "acbd\ne");
assert_eq!(editor.selections.ranges(cx), [5..5]);
@@ -5055,7 +5094,9 @@ fn test_transpose(cx: &mut TestAppContext) {
_ = cx.add_window(|window, cx| {
let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
editor.set_style(EditorStyle::default(), window, cx);
- editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([1..1, 2..2, 4..4])
+ });
editor.transpose(&Default::default(), window, cx);
assert_eq!(editor.text(cx), "bacd\ne");
assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
@@ -5082,7 +5123,9 @@ fn test_transpose(cx: &mut TestAppContext) {
_ = cx.add_window(|window, cx| {
let mut editor = build_editor(MultiBuffer::build_simple("ππβ", cx), window, cx);
editor.set_style(EditorStyle::default(), window, cx);
- editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([4..4])
+ });
editor.transpose(&Default::default(), window, cx);
assert_eq!(editor.text(cx), "ππβ");
assert_eq!(editor.selections.ranges(cx), [8..8]);
@@ -5102,11 +5145,12 @@ fn test_transpose(cx: &mut TestAppContext) {
#[gpui::test]
async fn test_rewrap(cx: &mut TestAppContext) {
init_test(cx, |settings| {
- settings.languages.extend([
+ settings.languages.0.extend([
(
"Markdown".into(),
LanguageSettingsContent {
allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
+ preferred_line_length: Some(40),
..Default::default()
},
),
@@ -5114,6 +5158,31 @@ async fn test_rewrap(cx: &mut TestAppContext) {
"Plain Text".into(),
LanguageSettingsContent {
allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
+ preferred_line_length: Some(40),
+ ..Default::default()
+ },
+ ),
+ (
+ "C++".into(),
+ LanguageSettingsContent {
+ allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
+ preferred_line_length: Some(40),
+ ..Default::default()
+ },
+ ),
+ (
+ "Python".into(),
+ LanguageSettingsContent {
+ allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
+ preferred_line_length: Some(40),
+ ..Default::default()
+ },
+ ),
+ (
+ "Rust".into(),
+ LanguageSettingsContent {
+ allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
+ preferred_line_length: Some(40),
..Default::default()
},
),
@@ -5122,15 +5191,17 @@ async fn test_rewrap(cx: &mut TestAppContext) {
let mut cx = EditorTestContext::new(cx).await;
- let language_with_c_comments = Arc::new(Language::new(
+ let cpp_language = Arc::new(Language::new(
LanguageConfig {
+ name: "C++".into(),
line_comments: vec!["// ".into()],
..LanguageConfig::default()
},
None,
));
- let language_with_pound_comments = Arc::new(Language::new(
+ let python_language = Arc::new(Language::new(
LanguageConfig {
+ name: "Python".into(),
line_comments: vec!["# ".into()],
..LanguageConfig::default()
},
@@ -5139,12 +5210,17 @@ async fn test_rewrap(cx: &mut TestAppContext) {
let markdown_language = Arc::new(Language::new(
LanguageConfig {
name: "Markdown".into(),
+ rewrap_prefixes: vec![
+ regex::Regex::new("\\d+\\.\\s+").unwrap(),
+ regex::Regex::new("[-*+]\\s+").unwrap(),
+ ],
..LanguageConfig::default()
},
None,
));
- let language_with_doc_comments = Arc::new(Language::new(
+ let rust_language = Arc::new(Language::new(
LanguageConfig {
+ name: "Rust".into(),
line_comments: vec!["// ".into(), "/// ".into()],
..LanguageConfig::default()
},
@@ -5159,296 +5235,295 @@ async fn test_rewrap(cx: &mut TestAppContext) {
None,
));
+ // Test basic rewrapping of a long line with a cursor
assert_rewrap(
indoc! {"
- // ΛLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
+ // ΛThis is a long comment that needs to be wrapped.
"},
indoc! {"
- // ΛLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
- // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
- // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
- // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
- // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
- // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
- // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
- // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
- // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
- // porttitor id. Aliquam id accumsan eros.
+ // ΛThis is a long comment that needs to
+ // be wrapped.
"},
- language_with_c_comments.clone(),
+ cpp_language.clone(),
&mut cx,
);
- // Test that rewrapping works inside of a selection
+ // Test rewrapping a full selection
assert_rewrap(
indoc! {"
- Β«// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.ΛΒ»
- "},
+ Β«// This selected long comment needs to be wrapped.ΛΒ»"
+ },
indoc! {"
- Β«// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
- // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
- // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
- // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
- // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
- // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
- // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
- // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
- // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
- // porttitor id. Aliquam id accumsan eros.ΛΒ»
- "},
- language_with_c_comments.clone(),
+ Β«// This selected long comment needs to
+ // be wrapped.ΛΒ»"
+ },
+ cpp_language.clone(),
&mut cx,
);
- // Test that cursors that expand to the same region are collapsed.
+ // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
assert_rewrap(
indoc! {"
- // ΛLorem ipsum dolor sit amet, consectetur adipiscing elit.
- // ΛVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
- // ΛVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
- // Λblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
+ // ΛThis is the first line.
+ // ThisΛ is the second line.
+ // This is the thirdΛ line, all part of one paragraph.
+ "},
+ indoc! {"
+ // ΛThis is the first line. ThisΛ is the
+ // second line. This is the thirdΛ line,
+ // all part of one paragraph.
+ "},
+ cpp_language.clone(),
+ &mut cx,
+ );
+
+ // Test multiple cursors in different paragraphs trigger separate rewraps
+ assert_rewrap(
+ indoc! {"
+ // ΛThis is the first paragraph, first line.
+ // ΛThis is the first paragraph, second line.
+
+ // ΛThis is the second paragraph, first line.
+ // ΛThis is the second paragraph, second line.
"},
indoc! {"
- // ΛLorem ipsum dolor sit amet, consectetur adipiscing elit. ΛVivamus mollis elit
- // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
- // auctor, eu lacinia sapien scelerisque. ΛVivamus sit amet neque et quam
- // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
- // Pellentesque odio lectus, iaculis ac volutpat et, Λblandit quis urna. Sed
- // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
- // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
- // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
- // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
- // porttitor id. Aliquam id accumsan eros.
+ // ΛThis is the first paragraph, first
+ // line. ΛThis is the first paragraph,
+ // second line.
+
+ // ΛThis is the second paragraph, first
+ // line. ΛThis is the second paragraph,
+ // second line.
"},
- language_with_c_comments.clone(),
+ cpp_language.clone(),
&mut cx,
);
- // Test that non-contiguous selections are treated separately.
+ // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
assert_rewrap(
indoc! {"
- // ΛLorem ipsum dolor sit amet, consectetur adipiscing elit.
- // ΛVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
- //
- // ΛVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
- // Λblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
+ Β«// A regular long long comment to be wrapped.
+ /// A documentation long comment to be wrapped.ΛΒ»
+ "},
+ indoc! {"
+ Β«// A regular long long comment to be
+ // wrapped.
+ /// A documentation long comment to be
+ /// wrapped.ΛΒ»
+ "},
+ rust_language.clone(),
+ &mut cx,
+ );
+
+ // Test that change in indentation level trigger seperate rewraps
+ assert_rewrap(
+ indoc! {"
+ fn foo() {
+ Β«// This is a long comment at the base indent.
+ // This is a long comment at the next indent.ΛΒ»
+ }
"},
indoc! {"
- // ΛLorem ipsum dolor sit amet, consectetur adipiscing elit. ΛVivamus mollis elit
- // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
- // auctor, eu lacinia sapien scelerisque.
- //
- // ΛVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
- // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
- // Λblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
- // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
- // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
- // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
- // vulputate turpis porttitor id. Aliquam id accumsan eros.
+ fn foo() {
+ Β«// This is a long comment at the
+ // base indent.
+ // This is a long comment at the
+ // next indent.ΛΒ»
+ }
"},
- language_with_c_comments.clone(),
+ rust_language.clone(),
&mut cx,
);
- // Test that different comment prefixes are supported.
+ // Test that different comment prefix characters (e.g., '#') are handled correctly
assert_rewrap(
indoc! {"
- # ΛLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
+ # ΛThis is a long comment using a pound sign.
"},
indoc! {"
- # ΛLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
- # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
- # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
- # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
- # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
- # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
- # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
- # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
- # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
- # accumsan eros.
+ # ΛThis is a long comment using a pound
+ # sign.
"},
- language_with_pound_comments.clone(),
+ python_language.clone(),
&mut cx,
);
- // Test that rewrapping is ignored outside of comments in most languages.
+ // Test rewrapping only affects comments, not code even when selected
assert_rewrap(
indoc! {"
- /// Adds two numbers.
- /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.Λ
- fn add(a: u32, b: u32) -> u32 {
- a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bΛ
- }
+ Β«/// This doc comment is long and should be wrapped.
+ fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ΛΒ»
"},
indoc! {"
- /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
- /// Vivamus mollis elit purus, a ornare lacus gravida vitae.Λ
- fn add(a: u32, b: u32) -> u32 {
- a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bΛ
- }
+ Β«/// This doc comment is long and should
+ /// be wrapped.
+ fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ΛΒ»
"},
- language_with_doc_comments.clone(),
+ rust_language.clone(),
&mut cx,
);
- // Test that rewrapping works in Markdown and Plain Text languages.
+ // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
assert_rewrap(
indoc! {"
- # Hello
+ # Header
+
+ A long long long line of markdown text to wrap.Λ
+ "},
+ indoc! {"
+ # Header
- Lorem ipsum dolor sit amet, Λconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
+ A long long long line of markdown text
+ to wrap.Λ
+ "},
+ markdown_language.clone(),
+ &mut cx,
+ );
+
+ // Test that rewrapping boundary works and preserves relative indent for Markdown documents
+ assert_rewrap(
+ indoc! {"
+ Β«1. This is a numbered list item that is very long and needs to be wrapped properly.
+ 2. This is a numbered list item that is very long and needs to be wrapped properly.
+ - This is an unordered list item that is also very long and should not merge with the numbered item.ΛΒ»
"},
indoc! {"
- # Hello
-
- Lorem ipsum dolor sit amet, Λconsectetur adipiscing elit. Vivamus mollis elit
- purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
- eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
- hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
- lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
- nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
- Integer sit amet scelerisque nisi.
+ Β«1. This is a numbered list item that is
+ very long and needs to be wrapped
+ properly.
+ 2. This is a numbered list item that is
+ very long and needs to be wrapped
+ properly.
+ - This is an unordered list item that is
+ also very long and should not merge
+ with the numbered item.ΛΒ»
"},
- markdown_language,
+ markdown_language.clone(),
&mut cx,
);
+ // Test that rewrapping add indents for rewrapping boundary if not exists already.
assert_rewrap(
indoc! {"
- Lorem ipsum dolor sit amet, Λconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
+ Β«1. This is a numbered list item that is
+ very long and needs to be wrapped
+ properly.
+ 2. This is a numbered list item that is
+ very long and needs to be wrapped
+ properly.
+ - This is an unordered list item that is
+ also very long and should not merge with
+ the numbered item.ΛΒ»
"},
indoc! {"
- Lorem ipsum dolor sit amet, Λconsectetur adipiscing elit. Vivamus mollis elit
- purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
- eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
- hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
- lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
- nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
- Integer sit amet scelerisque nisi.
+ Β«1. This is a numbered list item that is
+ very long and needs to be wrapped
+ properly.
+ 2. This is a numbered list item that is
+ very long and needs to be wrapped
+ properly.
+ - This is an unordered list item that is
+ also very long and should not merge
+ with the numbered item.ΛΒ»
"},
- plaintext_language.clone(),
+ markdown_language.clone(),
&mut cx,
);
- // Test rewrapping unaligned comments in a selection.
+ // Test that rewrapping maintain indents even when they already exists.
assert_rewrap(
indoc! {"
- fn foo() {
- if true {
- Β« // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
- // Praesent semper egestas tellus id dignissim.ΛΒ»
- do_something();
- } else {
- //
- }
- }
+ Β«1. This is a numbered list
+ item that is very long and needs to be wrapped properly.
+ 2. This is a numbered list
+ item that is very long and needs to be wrapped properly.
+ - This is an unordered list item that is also very long and
+ should not merge with the numbered item.ΛΒ»
"},
indoc! {"
- fn foo() {
- if true {
- Β« // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
- // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
- // egestas tellus id dignissim.ΛΒ»
- do_something();
- } else {
- //
- }
- }
+ Β«1. This is a numbered list item that is
+ very long and needs to be wrapped
+ properly.
+ 2. This is a numbered list item that is
+ very long and needs to be wrapped
+ properly.
+ - This is an unordered list item that is
+ also very long and should not merge
+ with the numbered item.ΛΒ»
"},
- language_with_doc_comments.clone(),
+ markdown_language.clone(),
&mut cx,
);
+ // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
assert_rewrap(
indoc! {"
- fn foo() {
- if true {
- Β«Λ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
- // Praesent semper egestas tellus id dignissim.Β»
- do_something();
- } else {
- //
- }
-
- }
+ ΛThis is a very long line of plain text that will be wrapped.
"},
indoc! {"
- fn foo() {
- if true {
- Β«Λ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
- // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
- // egestas tellus id dignissim.Β»
- do_something();
- } else {
- //
- }
-
- }
+ ΛThis is a very long line of plain text
+ that will be wrapped.
"},
- language_with_doc_comments.clone(),
+ plaintext_language.clone(),
&mut cx,
);
+ // Test that non-commented code acts as a paragraph boundary within a selection
assert_rewrap(
indoc! {"
- Β«Λone one one one one one one one one one one one one one one one one one one one one one one one one
-
- twoΒ»
-
- three
-
- Β«Λ\t
-
- four four four four four four four four four four four four four four four four four four four fourΒ»
-
- Β«Λfive five five five five five five five five five five five five five five five five five five five
- \tΒ»
- six six six six six six six six six six six six six six six six six six six six six six six six six
- "},
+ Β«// This is the first long comment block to be wrapped.
+ fn my_func(a: u32);
+ // This is the second long comment block to be wrapped.ΛΒ»
+ "},
indoc! {"
- Β«Λone one one one one one one one one one one one one one one one one one one one
- one one one one one
+ Β«// This is the first long comment block
+ // to be wrapped.
+ fn my_func(a: u32);
+ // This is the second long comment block
+ // to be wrapped.ΛΒ»
+ "},
+ rust_language.clone(),
+ &mut cx,
+ );
- twoΒ»
+ // Test rewrapping multiple selections, including ones with blank lines or tabs
+ assert_rewrap(
+ indoc! {"
+ Β«ΛThis is a very long line that will be wrapped.
- three
+ This is another paragraph in the same selection.Β»
- Β«Λ\t
+ Β«\tThis is a very long indented line that will be wrapped.ΛΒ»
+ "},
+ indoc! {"
+ Β«ΛThis is a very long line that will be
+ wrapped.
- four four four four four four four four four four four four four four four four
- four four four fourΒ»
+ This is another paragraph in the same
+ selection.Β»
- Β«Λfive five five five five five five five five five five five five five five five
- five five five five
- \tΒ»
- six six six six six six six six six six six six six six six six six six six six six six six six six
- "},
+ Β«\tThis is a very long indented line
+ \tthat will be wrapped.ΛΒ»
+ "},
plaintext_language.clone(),
&mut cx,
);
+ // Test that an empty comment line acts as a paragraph boundary
assert_rewrap(
indoc! {"
- //Λ long long long long long long long long long long long long long long long long long long long long long long long long long long long long
- //Λ
- //Λ long long long long long long long long long long long long long long long long long long long long long long long long long long long long
- //Λ short short short
- int main(void) {
- return 17;
- }
- "},
+ // ΛThis is a long comment that will be wrapped.
+ //
+ // And this is another long comment that will also be wrapped.Λ
+ "},
indoc! {"
- //Λ long long long long long long long long long long long long long long long
- // long long long long long long long long long long long long long
- //Λ
- //Λ long long long long long long long long long long long long long long long
- //Λ long long long long long long long long long long long long long short short
- // short
- int main(void) {
- return 17;
- }
- "},
- language_with_c_comments,
+ // ΛThis is a long comment that will be
+ // wrapped.
+ //
+ // And this is another long comment that
+ // will also be wrapped.Λ
+ "},
+ cpp_language,
&mut cx,
);
@@ -12,8 +12,8 @@ use crate::{
ToggleFold,
code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
display_map::{
- Block, BlockContext, BlockStyle, DisplaySnapshot, EditorMargins, FoldId, HighlightKey,
- HighlightedChunk, ToDisplayPoint,
+ Block, BlockContext, BlockStyle, ChunkRendererId, DisplaySnapshot, EditorMargins,
+ HighlightKey, HighlightedChunk, ToDisplayPoint,
},
editor_settings::{
CurrentLineHighlight, DocumentColorsRenderMode, DoubleClickInMultibuffer, Minimap,
@@ -7119,7 +7119,7 @@ pub(crate) struct LineWithInvisibles {
enum LineFragment {
Text(ShapedLine),
Element {
- id: FoldId,
+ id: ChunkRendererId,
element: Option<AnyElement>,
size: Size<Pixels>,
len: usize,
@@ -8297,7 +8297,7 @@ impl Element for EditorElement {
window,
cx,
);
- let new_fold_widths = line_layouts
+ let new_renrerer_widths = line_layouts
.iter()
.flat_map(|layout| &layout.fragments)
.filter_map(|fragment| {
@@ -8308,7 +8308,7 @@ impl Element for EditorElement {
}
});
if self.editor.update(cx, |editor, cx| {
- editor.update_fold_widths(new_fold_widths, cx)
+ editor.update_renderer_widths(new_renrerer_widths, cx)
}) {
// If the fold widths have changed, we need to prepaint
// the element again to account for any changes in
@@ -10051,7 +10051,7 @@ fn compute_auto_height_layout(
mod tests {
use super::*;
use crate::{
- Editor, MultiBuffer,
+ Editor, MultiBuffer, SelectionEffects,
display_map::{BlockPlacement, BlockProperties},
editor_tests::{init_test, update_test_language_settings},
};
@@ -10176,7 +10176,7 @@ mod tests {
window
.update(cx, |editor, window, cx| {
editor.cursor_shape = CursorShape::Block;
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([
Point::new(0, 0)..Point::new(1, 0),
Point::new(3, 2)..Point::new(3, 3),
@@ -1257,7 +1257,7 @@ mod tests {
let snapshot = editor.buffer().read(cx).snapshot(cx);
let anchor_range = snapshot.anchor_before(selection_range.start)
..snapshot.anchor_after(selection_range.end);
- editor.change_selections(Some(crate::Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.set_pending_anchor_range(anchor_range, crate::SelectMode::Character)
});
});
@@ -3,7 +3,7 @@ use crate::{
EditorSnapshot, GlobalDiagnosticRenderer, Hover,
display_map::{InlayOffset, ToDisplayPoint, invisibles::is_invisible},
hover_links::{InlayHighlight, RangeInEditor},
- scroll::{Autoscroll, ScrollAmount},
+ scroll::ScrollAmount,
};
use anyhow::Context as _;
use gpui::{
@@ -648,7 +648,7 @@ pub fn hover_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
..Default::default()
},
syntax: cx.theme().syntax().clone(),
- selection_background_color: { cx.theme().players().local().selection },
+ selection_background_color: cx.theme().colors().element_selection_background,
heading: StyleRefinement::default()
.font_weight(FontWeight::BOLD)
.text_base()
@@ -697,7 +697,7 @@ pub fn diagnostics_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
..Default::default()
},
syntax: cx.theme().syntax().clone(),
- selection_background_color: { cx.theme().players().local().selection },
+ selection_background_color: cx.theme().colors().element_selection_background,
height_is_multiple_of_line_height: true,
heading: StyleRefinement::default()
.font_weight(FontWeight::BOLD)
@@ -746,7 +746,7 @@ pub fn open_markdown_url(link: SharedString, window: &mut Window, cx: &mut App)
};
editor.update_in(cx, |editor, window, cx| {
editor.change_selections(
- Some(Autoscroll::fit()),
+ Default::default(),
window,
cx,
|selections| {
@@ -956,7 +956,7 @@ fn fetch_and_update_hints(
.update(cx, |editor, cx| {
if got_throttled {
let query_not_around_visible_range = match editor
- .excerpts_for_inlay_hints_query(None, cx)
+ .visible_excerpts(None, cx)
.remove(&query.excerpt_id)
{
Some((_, _, current_visible_range)) => {
@@ -1302,6 +1302,7 @@ fn apply_hint_update(
#[cfg(test)]
pub mod tests {
+ use crate::SelectionEffects;
use crate::editor_tests::update_test_language_settings;
use crate::scroll::ScrollAmount;
use crate::{ExcerptRange, scroll::Autoscroll, test::editor_lsp_test_context::rust_lang};
@@ -1384,7 +1385,9 @@ pub mod tests {
editor
.update(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([13..13])
+ });
editor.handle_input("some change", window, cx);
})
.unwrap();
@@ -1698,7 +1701,9 @@ pub mod tests {
rs_editor
.update(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([13..13])
+ });
editor.handle_input("some rs change", window, cx);
})
.unwrap();
@@ -1733,7 +1738,9 @@ pub mod tests {
md_editor
.update(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([13..13])
+ });
editor.handle_input("some md change", window, cx);
})
.unwrap();
@@ -2155,7 +2162,9 @@ pub mod tests {
] {
editor
.update(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([13..13])
+ });
editor.handle_input(change_after_opening, window, cx);
})
.unwrap();
@@ -2199,7 +2208,9 @@ pub mod tests {
edits.push(cx.spawn(|mut cx| async move {
task_editor
.update(&mut cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| s.select_ranges([13..13]));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([13..13])
+ });
editor.handle_input(async_later_change, window, cx);
})
.unwrap();
@@ -2447,9 +2458,12 @@ pub mod tests {
editor
.update(cx, |editor, window, cx| {
- editor.change_selections(Some(Autoscroll::center()), window, cx, |s| {
- s.select_ranges([selection_in_cached_range..selection_in_cached_range])
- });
+ editor.change_selections(
+ SelectionEffects::scroll(Autoscroll::center()),
+ window,
+ cx,
+ |s| s.select_ranges([selection_in_cached_range..selection_in_cached_range]),
+ );
})
.unwrap();
cx.executor().advance_clock(Duration::from_millis(
@@ -2511,9 +2525,7 @@ pub mod tests {
cx: &mut gpui::TestAppContext,
) -> Range<Point> {
let ranges = editor
- .update(cx, |editor, _window, cx| {
- editor.excerpts_for_inlay_hints_query(None, cx)
- })
+ .update(cx, |editor, _window, cx| editor.visible_excerpts(None, cx))
.unwrap();
assert_eq!(
ranges.len(),
@@ -2712,15 +2724,24 @@ pub mod tests {
editor
.update(cx, |editor, window, cx| {
- editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
- s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
- });
- editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
- s.select_ranges([Point::new(22, 0)..Point::new(22, 0)])
- });
- editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
- s.select_ranges([Point::new(50, 0)..Point::new(50, 0)])
- });
+ editor.change_selections(
+ SelectionEffects::scroll(Autoscroll::Next),
+ window,
+ cx,
+ |s| s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]),
+ );
+ editor.change_selections(
+ SelectionEffects::scroll(Autoscroll::Next),
+ window,
+ cx,
+ |s| s.select_ranges([Point::new(22, 0)..Point::new(22, 0)]),
+ );
+ editor.change_selections(
+ SelectionEffects::scroll(Autoscroll::Next),
+ window,
+ cx,
+ |s| s.select_ranges([Point::new(50, 0)..Point::new(50, 0)]),
+ );
})
.unwrap();
cx.executor().run_until_parked();
@@ -2745,9 +2766,12 @@ pub mod tests {
editor
.update(cx, |editor, window, cx| {
- editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
- s.select_ranges([Point::new(100, 0)..Point::new(100, 0)])
- });
+ editor.change_selections(
+ SelectionEffects::scroll(Autoscroll::Next),
+ window,
+ cx,
+ |s| s.select_ranges([Point::new(100, 0)..Point::new(100, 0)]),
+ );
})
.unwrap();
cx.executor().advance_clock(Duration::from_millis(
@@ -2778,9 +2802,12 @@ pub mod tests {
editor
.update(cx, |editor, window, cx| {
- editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
- s.select_ranges([Point::new(4, 0)..Point::new(4, 0)])
- });
+ editor.change_selections(
+ SelectionEffects::scroll(Autoscroll::Next),
+ window,
+ cx,
+ |s| s.select_ranges([Point::new(4, 0)..Point::new(4, 0)]),
+ );
})
.unwrap();
cx.executor().advance_clock(Duration::from_millis(
@@ -2812,7 +2839,7 @@ pub mod tests {
editor_edited.store(true, Ordering::Release);
editor
.update(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::new(57, 0)..Point::new(57, 0)])
});
editor.handle_input("++++more text++++", window, cx);
@@ -3130,7 +3157,7 @@ pub mod tests {
cx.executor().run_until_parked();
editor
.update(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
})
})
@@ -3412,7 +3439,7 @@ pub mod tests {
cx.executor().run_until_parked();
editor
.update(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([Point::new(10, 0)..Point::new(10, 0)])
})
})
@@ -778,7 +778,7 @@ impl Item for Editor {
fn deactivated(&mut self, _: &mut Window, cx: &mut Context<Self>) {
let selection = self.selections.newest_anchor();
- self.push_to_nav_history(selection.head(), None, true, cx);
+ self.push_to_nav_history(selection.head(), None, true, false, cx);
}
fn workspace_deactivated(&mut self, _: &mut Window, cx: &mut Context<Self>) {
@@ -1352,7 +1352,7 @@ impl ProjectItem for Editor {
cx,
);
if !restoration_data.selections.is_empty() {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges(clip_ranges(&restoration_data.selections, &snapshot));
});
}
@@ -1521,7 +1521,7 @@ impl SearchableItem for Editor {
fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
let setting = EditorSettings::get_global(cx).seed_search_query_from_cursor;
let snapshot = &self.snapshot(window, cx).buffer_snapshot;
- let selection = self.selections.newest::<usize>(cx);
+ let selection = self.selections.newest_adjusted(cx);
match setting {
SeedQuerySetting::Never => String::new(),
@@ -1558,7 +1558,7 @@ impl SearchableItem for Editor {
) {
self.unfold_ranges(&[matches[index].clone()], false, true, cx);
let range = self.range_for_match(&matches[index]);
- self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ self.change_selections(Default::default(), window, cx, |s| {
s.select_ranges([range]);
})
}
@@ -1570,7 +1570,7 @@ impl SearchableItem for Editor {
cx: &mut Context<Self>,
) {
self.unfold_ranges(matches, false, false, cx);
- self.change_selections(None, window, cx, |s| {
+ self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges(matches.iter().cloned())
});
}
@@ -843,7 +843,7 @@ mod jsx_tag_autoclose_tests {
let mut cx = EditorTestContext::for_editor(editor, cx).await;
cx.update_editor(|editor, window, cx| {
- editor.change_selections(None, window, cx, |selections| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
selections.select(vec![
Selection::from_offset(4),
Selection::from_offset(9),
@@ -3,10 +3,10 @@ use std::{cmp, ops::Range};
use collections::HashMap;
use futures::future::join_all;
use gpui::{Hsla, Rgba};
+use itertools::Itertools;
use language::point_from_lsp;
-use lsp::LanguageServerId;
use multi_buffer::Anchor;
-use project::DocumentColor;
+use project::{DocumentColor, lsp_store::ColorFetchStrategy};
use settings::Settings as _;
use text::{Bias, BufferId, OffsetRangeExt as _};
use ui::{App, Context, Window};
@@ -19,16 +19,21 @@ use crate::{
#[derive(Debug)]
pub(super) struct LspColorData {
+ buffer_colors: HashMap<BufferId, BufferColors>,
+ render_mode: DocumentColorsRenderMode,
+}
+
+#[derive(Debug, Default)]
+struct BufferColors {
colors: Vec<(Range<Anchor>, DocumentColor, InlayId)>,
inlay_colors: HashMap<InlayId, usize>,
- render_mode: DocumentColorsRenderMode,
+ cache_version_used: usize,
}
impl LspColorData {
pub fn new(cx: &App) -> Self {
Self {
- colors: Vec::new(),
- inlay_colors: HashMap::default(),
+ buffer_colors: HashMap::default(),
render_mode: EditorSettings::get_global(cx).lsp_document_colors,
}
}
@@ -45,8 +50,9 @@ impl LspColorData {
DocumentColorsRenderMode::Inlay => Some(InlaySplice {
to_remove: Vec::new(),
to_insert: self
- .colors
+ .buffer_colors
.iter()
+ .flat_map(|(_, buffer_colors)| buffer_colors.colors.iter())
.map(|(range, color, id)| {
Inlay::color(
id.id(),
@@ -61,33 +67,49 @@ impl LspColorData {
})
.collect(),
}),
- DocumentColorsRenderMode::None => {
- self.colors.clear();
- Some(InlaySplice {
- to_remove: self.inlay_colors.drain().map(|(id, _)| id).collect(),
- to_insert: Vec::new(),
- })
- }
+ DocumentColorsRenderMode::None => Some(InlaySplice {
+ to_remove: self
+ .buffer_colors
+ .drain()
+ .flat_map(|(_, buffer_colors)| buffer_colors.inlay_colors)
+ .map(|(id, _)| id)
+ .collect(),
+ to_insert: Vec::new(),
+ }),
DocumentColorsRenderMode::Border | DocumentColorsRenderMode::Background => {
Some(InlaySplice {
- to_remove: self.inlay_colors.drain().map(|(id, _)| id).collect(),
+ to_remove: self
+ .buffer_colors
+ .iter_mut()
+ .flat_map(|(_, buffer_colors)| buffer_colors.inlay_colors.drain())
+ .map(|(id, _)| id)
+ .collect(),
to_insert: Vec::new(),
})
}
}
}
- fn set_colors(&mut self, colors: Vec<(Range<Anchor>, DocumentColor, InlayId)>) -> bool {
- if self.colors == colors {
+ fn set_colors(
+ &mut self,
+ buffer_id: BufferId,
+ colors: Vec<(Range<Anchor>, DocumentColor, InlayId)>,
+ cache_version: Option<usize>,
+ ) -> bool {
+ let buffer_colors = self.buffer_colors.entry(buffer_id).or_default();
+ if let Some(cache_version) = cache_version {
+ buffer_colors.cache_version_used = cache_version;
+ }
+ if buffer_colors.colors == colors {
return false;
}
- self.inlay_colors = colors
+ buffer_colors.inlay_colors = colors
.iter()
.enumerate()
.map(|(i, (_, _, id))| (*id, i))
.collect();
- self.colors = colors;
+ buffer_colors.colors = colors;
true
}
@@ -101,8 +123,9 @@ impl LspColorData {
{
Vec::new()
} else {
- self.colors
+ self.buffer_colors
.iter()
+ .flat_map(|(_, buffer_colors)| &buffer_colors.colors)
.map(|(range, color, _)| {
let display_range = range.clone().to_display_points(snapshot);
let color = Hsla::from(Rgba {
@@ -122,7 +145,7 @@ impl LspColorData {
impl Editor {
pub(super) fn refresh_colors(
&mut self,
- for_server_id: Option<LanguageServerId>,
+ ignore_cache: bool,
buffer_id: Option<BufferId>,
_: &Window,
cx: &mut Context<Self>,
@@ -141,29 +164,40 @@ impl Editor {
return;
}
+ let visible_buffers = self
+ .visible_excerpts(None, cx)
+ .into_values()
+ .map(|(buffer, ..)| buffer)
+ .filter(|editor_buffer| {
+ buffer_id.is_none_or(|buffer_id| buffer_id == editor_buffer.read(cx).remote_id())
+ })
+ .unique_by(|buffer| buffer.read(cx).remote_id())
+ .collect::<Vec<_>>();
+
let all_colors_task = project.read(cx).lsp_store().update(cx, |lsp_store, cx| {
- self.buffer()
- .update(cx, |multi_buffer, cx| {
- multi_buffer
- .all_buffers()
- .into_iter()
- .filter(|editor_buffer| {
- buffer_id.is_none_or(|buffer_id| {
- buffer_id == editor_buffer.read(cx).remote_id()
- })
- })
- .collect::<Vec<_>>()
- })
+ visible_buffers
.into_iter()
.filter_map(|buffer| {
let buffer_id = buffer.read(cx).remote_id();
- let colors_task = lsp_store.document_colors(for_server_id, buffer, cx)?;
+ let fetch_strategy = if ignore_cache {
+ ColorFetchStrategy::IgnoreCache
+ } else {
+ ColorFetchStrategy::UseCache {
+ known_cache_version: self.colors.as_ref().and_then(|colors| {
+ Some(colors.buffer_colors.get(&buffer_id)?.cache_version_used)
+ }),
+ }
+ };
+ let colors_task = lsp_store.document_colors(fetch_strategy, buffer, cx)?;
Some(async move { (buffer_id, colors_task.await) })
})
.collect::<Vec<_>>()
});
cx.spawn(async move |editor, cx| {
let all_colors = join_all(all_colors_task).await;
+ if all_colors.is_empty() {
+ return;
+ }
let Ok((multi_buffer_snapshot, editor_excerpts)) = editor.update(cx, |editor, cx| {
let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
let editor_excerpts = multi_buffer_snapshot.excerpts().fold(
@@ -187,14 +221,14 @@ impl Editor {
return;
};
- let mut new_editor_colors = Vec::<(Range<Anchor>, DocumentColor)>::new();
+ let mut new_editor_colors = HashMap::default();
for (buffer_id, colors) in all_colors {
let Some(excerpts) = editor_excerpts.get(&buffer_id) else {
continue;
};
match colors {
Ok(colors) => {
- for color in colors {
+ for color in colors.colors {
let color_start = point_from_lsp(color.lsp_range.start);
let color_end = point_from_lsp(color.lsp_range.end);
@@ -227,8 +261,15 @@ impl Editor {
continue;
};
+ let new_entry =
+ new_editor_colors.entry(buffer_id).or_insert_with(|| {
+ (Vec::<(Range<Anchor>, DocumentColor)>::new(), None)
+ });
+ new_entry.1 = colors.cache_version;
+ let new_buffer_colors = &mut new_entry.0;
+
let (Ok(i) | Err(i)) =
- new_editor_colors.binary_search_by(|(probe, _)| {
+ new_buffer_colors.binary_search_by(|(probe, _)| {
probe
.start
.cmp(&color_start_anchor, &multi_buffer_snapshot)
@@ -238,7 +279,7 @@ impl Editor {
.cmp(&color_end_anchor, &multi_buffer_snapshot)
})
});
- new_editor_colors
+ new_buffer_colors
.insert(i, (color_start_anchor..color_end_anchor, color));
break;
}
@@ -251,45 +292,70 @@ impl Editor {
editor
.update(cx, |editor, cx| {
let mut colors_splice = InlaySplice::default();
- let mut new_color_inlays = Vec::with_capacity(new_editor_colors.len());
let Some(colors) = &mut editor.colors else {
return;
};
- let mut existing_colors = colors.colors.iter().peekable();
- for (new_range, new_color) in new_editor_colors {
- let rgba_color = Rgba {
- r: new_color.color.red,
- g: new_color.color.green,
- b: new_color.color.blue,
- a: new_color.color.alpha,
- };
+ let mut updated = false;
+ for (buffer_id, (new_buffer_colors, new_cache_version)) in new_editor_colors {
+ let mut new_buffer_color_inlays =
+ Vec::with_capacity(new_buffer_colors.len());
+ let mut existing_buffer_colors = colors
+ .buffer_colors
+ .entry(buffer_id)
+ .or_default()
+ .colors
+ .iter()
+ .peekable();
+ for (new_range, new_color) in new_buffer_colors {
+ let rgba_color = Rgba {
+ r: new_color.color.red,
+ g: new_color.color.green,
+ b: new_color.color.blue,
+ a: new_color.color.alpha,
+ };
- loop {
- match existing_colors.peek() {
- Some((existing_range, existing_color, existing_inlay_id)) => {
- match existing_range
- .start
- .cmp(&new_range.start, &multi_buffer_snapshot)
- .then_with(|| {
- existing_range
- .end
- .cmp(&new_range.end, &multi_buffer_snapshot)
- }) {
- cmp::Ordering::Less => {
- colors_splice.to_remove.push(*existing_inlay_id);
- existing_colors.next();
- continue;
- }
- cmp::Ordering::Equal => {
- if existing_color == &new_color {
- new_color_inlays.push((
- new_range,
- new_color,
- *existing_inlay_id,
- ));
- } else {
+ loop {
+ match existing_buffer_colors.peek() {
+ Some((existing_range, existing_color, existing_inlay_id)) => {
+ match existing_range
+ .start
+ .cmp(&new_range.start, &multi_buffer_snapshot)
+ .then_with(|| {
+ existing_range
+ .end
+ .cmp(&new_range.end, &multi_buffer_snapshot)
+ }) {
+ cmp::Ordering::Less => {
colors_splice.to_remove.push(*existing_inlay_id);
+ existing_buffer_colors.next();
+ continue;
+ }
+ cmp::Ordering::Equal => {
+ if existing_color == &new_color {
+ new_buffer_color_inlays.push((
+ new_range,
+ new_color,
+ *existing_inlay_id,
+ ));
+ } else {
+ colors_splice
+ .to_remove
+ .push(*existing_inlay_id);
+ let inlay = Inlay::color(
+ post_inc(&mut editor.next_color_inlay_id),
+ new_range.start,
+ rgba_color,
+ );
+ let inlay_id = inlay.id;
+ colors_splice.to_insert.push(inlay);
+ new_buffer_color_inlays
+ .push((new_range, new_color, inlay_id));
+ }
+ existing_buffer_colors.next();
+ break;
+ }
+ cmp::Ordering::Greater => {
let inlay = Inlay::color(
post_inc(&mut editor.next_color_inlay_id),
new_range.start,
@@ -297,46 +363,40 @@ impl Editor {
);
let inlay_id = inlay.id;
colors_splice.to_insert.push(inlay);
- new_color_inlays
+ new_buffer_color_inlays
.push((new_range, new_color, inlay_id));
+ break;
}
- existing_colors.next();
- break;
- }
- cmp::Ordering::Greater => {
- let inlay = Inlay::color(
- post_inc(&mut editor.next_color_inlay_id),
- new_range.start,
- rgba_color,
- );
- let inlay_id = inlay.id;
- colors_splice.to_insert.push(inlay);
- new_color_inlays.push((new_range, new_color, inlay_id));
- break;
}
}
- }
- None => {
- let inlay = Inlay::color(
- post_inc(&mut editor.next_color_inlay_id),
- new_range.start,
- rgba_color,
- );
- let inlay_id = inlay.id;
- colors_splice.to_insert.push(inlay);
- new_color_inlays.push((new_range, new_color, inlay_id));
- break;
+ None => {
+ let inlay = Inlay::color(
+ post_inc(&mut editor.next_color_inlay_id),
+ new_range.start,
+ rgba_color,
+ );
+ let inlay_id = inlay.id;
+ colors_splice.to_insert.push(inlay);
+ new_buffer_color_inlays
+ .push((new_range, new_color, inlay_id));
+ break;
+ }
}
}
}
- }
- if existing_colors.peek().is_some() {
- colors_splice
- .to_remove
- .extend(existing_colors.map(|(_, _, id)| *id));
+
+ if existing_buffer_colors.peek().is_some() {
+ colors_splice
+ .to_remove
+ .extend(existing_buffer_colors.map(|(_, _, id)| *id));
+ }
+ updated |= colors.set_colors(
+ buffer_id,
+ new_buffer_color_inlays,
+ new_cache_version,
+ );
}
- let mut updated = colors.set_colors(new_color_inlays);
if colors.render_mode == DocumentColorsRenderMode::Inlay
&& (!colors_splice.to_insert.is_empty()
|| !colors_splice.to_remove.is_empty())
@@ -1,8 +1,8 @@
use crate::{
Copy, CopyAndTrim, CopyPermalinkToLine, Cut, DisplayPoint, DisplaySnapshot, Editor,
EvaluateSelectedText, FindAllReferences, GoToDeclaration, GoToDefinition, GoToImplementation,
- GoToTypeDefinition, Paste, Rename, RevealInFileManager, SelectMode, SelectionExt,
- ToDisplayPoint, ToggleCodeActions,
+ GoToTypeDefinition, Paste, Rename, RevealInFileManager, SelectMode, SelectionEffects,
+ SelectionExt, ToDisplayPoint, ToggleCodeActions,
actions::{Format, FormatSelections},
selections_collection::SelectionsCollection,
};
@@ -177,7 +177,7 @@ pub fn deploy_context_menu(
let anchor = buffer.anchor_before(point.to_point(&display_map));
if !display_ranges(&display_map, &editor.selections).any(|r| r.contains(&point)) {
// Move the cursor to the clicked location so that dispatched actions make sense
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.clear_disjoint();
s.set_pending_anchor_range(anchor..anchor, SelectMode::Character);
});
@@ -275,10 +275,10 @@ pub fn deploy_context_menu(
cx,
),
None => {
- let character_size = editor.character_size(window);
+ let character_size = editor.character_dimensions(window);
let menu_position = MenuPosition::PinnedToEditor {
source: source_anchor,
- offset: gpui::point(character_size.width, character_size.height),
+ offset: gpui::point(character_size.em_width, character_size.line_height),
};
Some(MouseContextMenu::new(
editor,
@@ -1,4 +1,4 @@
-use crate::{ApplyAllDiffHunks, Editor, EditorEvent, SemanticsProvider};
+use crate::{ApplyAllDiffHunks, Editor, EditorEvent, SelectionEffects, SemanticsProvider};
use buffer_diff::BufferDiff;
use collections::HashSet;
use futures::{channel::mpsc, future::join_all};
@@ -213,7 +213,9 @@ impl ProposedChangesEditor {
self.buffer_entries = buffer_entries;
self.editor.update(cx, |editor, cx| {
- editor.change_selections(None, window, cx, |selections| selections.refresh());
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
+ selections.refresh()
+ });
editor.buffer.update(cx, |buffer, cx| {
for diff in new_diffs {
buffer.add_diff(diff, cx)
@@ -487,8 +487,9 @@ impl Editor {
if opened_first_time {
cx.spawn_in(window, async move |editor, cx| {
editor
- .update(cx, |editor, cx| {
- editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx)
+ .update_in(cx, |editor, window, cx| {
+ editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
+ editor.refresh_colors(false, None, window, cx);
})
.ok()
})
@@ -599,6 +600,7 @@ impl Editor {
);
self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
+ self.refresh_colors(false, None, window, cx);
}
pub fn scroll_position(&self, cx: &mut Context<Self>) -> gpui::Point<f32> {
@@ -5,7 +5,7 @@ use std::{rc::Rc, sync::LazyLock};
pub use crate::rust_analyzer_ext::expand_macro_recursively;
use crate::{
- DisplayPoint, Editor, EditorMode, FoldPlaceholder, MultiBuffer,
+ DisplayPoint, Editor, EditorMode, FoldPlaceholder, MultiBuffer, SelectionEffects,
display_map::{
Block, BlockPlacement, CustomBlockId, DisplayMap, DisplayRow, DisplaySnapshot,
ToDisplayPoint,
@@ -93,7 +93,9 @@ pub fn select_ranges(
) {
let (unmarked_text, text_ranges) = marked_text_ranges(marked_text, true);
assert_eq!(editor.text(cx), unmarked_text);
- editor.change_selections(None, window, cx, |s| s.select_ranges(text_ranges));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges(text_ranges)
+ });
}
#[track_caller]
@@ -1,5 +1,5 @@
use crate::{
- AnchorRangeExt, Autoscroll, DisplayPoint, Editor, MultiBuffer, RowExt,
+ AnchorRangeExt, DisplayPoint, Editor, MultiBuffer, RowExt,
display_map::{HighlightKey, ToDisplayPoint},
};
use buffer_diff::DiffHunkStatusKind;
@@ -362,7 +362,7 @@ impl EditorTestContext {
let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
self.editor.update_in(&mut self.cx, |editor, window, cx| {
editor.set_text(unmarked_text, window, cx);
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.select_ranges(selection_ranges)
})
});
@@ -379,7 +379,7 @@ impl EditorTestContext {
let (unmarked_text, selection_ranges) = marked_text_ranges(marked_text, true);
self.editor.update_in(&mut self.cx, |editor, window, cx| {
assert_eq!(editor.text(cx), unmarked_text);
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.select_ranges(selection_ranges)
})
});
@@ -32,7 +32,7 @@ client.workspace = true
collections.workspace = true
debug_adapter_extension.workspace = true
dirs.workspace = true
-dotenv.workspace = true
+dotenvy.workspace = true
env_logger.workspace = true
extension.workspace = true
fs.workspace = true
@@ -63,7 +63,7 @@ struct Args {
}
fn main() {
- dotenv::from_filename(CARGO_MANIFEST_DIR.join(".env")).ok();
+ dotenvy::from_filename(CARGO_MANIFEST_DIR.join(".env")).ok();
env_logger::init();
@@ -1054,6 +1054,15 @@ pub fn response_events_to_markdown(
| LanguageModelCompletionEvent::StartMessage { .. }
| LanguageModelCompletionEvent::StatusUpdate { .. },
) => {}
+ Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
+ json_parse_error, ..
+ }) => {
+ flush_buffers(&mut response, &mut text_buffer, &mut thinking_buffer);
+ response.push_str(&format!(
+ "**Error**: parse error in tool use JSON: {}\n\n",
+ json_parse_error
+ ));
+ }
Err(error) => {
flush_buffers(&mut response, &mut text_buffer, &mut thinking_buffer);
response.push_str(&format!("**Error**: {}\n\n", error));
@@ -1132,6 +1141,17 @@ impl ThreadDialog {
| Ok(LanguageModelCompletionEvent::StartMessage { .. })
| Ok(LanguageModelCompletionEvent::Stop(_)) => {}
+ Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
+ json_parse_error,
+ ..
+ }) => {
+ flush_text(&mut current_text, &mut content);
+ content.push(MessageContent::Text(format!(
+ "ERROR: parse error in tool use JSON: {}",
+ json_parse_error
+ )));
+ }
+
Err(error) => {
flush_text(&mut current_text, &mut content);
content.push(MessageContent::Text(format!("ERROR: {}", error)));
@@ -259,6 +259,36 @@ async fn copy_extension_resources(
}
}
+ if !manifest.debug_adapters.is_empty() {
+ for (debug_adapter, entry) in &manifest.debug_adapters {
+ let schema_path = entry.schema_path.clone().unwrap_or_else(|| {
+ PathBuf::from("debug_adapter_schemas".to_owned())
+ .join(debug_adapter.as_ref())
+ .with_extension("json")
+ });
+ let parent = schema_path
+ .parent()
+ .with_context(|| format!("invalid empty schema path for {debug_adapter}"))?;
+ fs::create_dir_all(output_dir.join(parent))?;
+ copy_recursive(
+ fs.as_ref(),
+ &extension_path.join(&schema_path),
+ &output_dir.join(&schema_path),
+ CopyOptions {
+ overwrite: true,
+ ignore_if_exists: false,
+ },
+ )
+ .await
+ .with_context(|| {
+ format!(
+ "failed to copy debug adapter schema '{}'",
+ schema_path.display()
+ )
+ })?;
+ }
+ }
+
Ok(())
}
@@ -70,6 +70,7 @@ const SUGGESTIONS_BY_EXTENSION_ID: &[(&str, &[&str])] = &[
("templ", &["templ"]),
("terraform", &["tf", "tfvars", "hcl"]),
("toml", &["Cargo.lock", "toml"]),
+ ("typst", &["typ"]),
("vue", &["vue"]),
("wgsl", &["wgsl"]),
("wit", &["wit"]),
@@ -74,7 +74,7 @@ impl FakeGitRepository {
impl GitRepository for FakeGitRepository {
fn reload_index(&self) {}
- fn load_index_text(&self, path: RepoPath) -> BoxFuture<Option<String>> {
+ fn load_index_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>> {
async {
self.with_state_async(false, move |state| {
state
@@ -89,7 +89,7 @@ impl GitRepository for FakeGitRepository {
.boxed()
}
- fn load_committed_text(&self, path: RepoPath) -> BoxFuture<Option<String>> {
+ fn load_committed_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>> {
async {
self.with_state_async(false, move |state| {
state
@@ -108,7 +108,7 @@ impl GitRepository for FakeGitRepository {
&self,
_commit: String,
_cx: AsyncApp,
- ) -> BoxFuture<Result<git::repository::CommitDiff>> {
+ ) -> BoxFuture<'_, Result<git::repository::CommitDiff>> {
unimplemented!()
}
@@ -117,7 +117,7 @@ impl GitRepository for FakeGitRepository {
path: RepoPath,
content: Option<String>,
_env: Arc<HashMap<String, String>>,
- ) -> BoxFuture<anyhow::Result<()>> {
+ ) -> BoxFuture<'_, anyhow::Result<()>> {
self.with_state_async(true, move |state| {
if let Some(message) = &state.simulated_index_write_error_message {
anyhow::bail!("{message}");
@@ -134,7 +134,7 @@ impl GitRepository for FakeGitRepository {
None
}
- fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<Result<Vec<Option<String>>>> {
+ fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<'_, Result<Vec<Option<String>>>> {
self.with_state_async(false, |state| {
Ok(revs
.into_iter()
@@ -143,7 +143,7 @@ impl GitRepository for FakeGitRepository {
})
}
- fn show(&self, commit: String) -> BoxFuture<Result<CommitDetails>> {
+ fn show(&self, commit: String) -> BoxFuture<'_, Result<CommitDetails>> {
async {
Ok(CommitDetails {
sha: commit.into(),
@@ -158,7 +158,7 @@ impl GitRepository for FakeGitRepository {
_commit: String,
_mode: ResetMode,
_env: Arc<HashMap<String, String>>,
- ) -> BoxFuture<Result<()>> {
+ ) -> BoxFuture<'_, Result<()>> {
unimplemented!()
}
@@ -167,7 +167,7 @@ impl GitRepository for FakeGitRepository {
_commit: String,
_paths: Vec<RepoPath>,
_env: Arc<HashMap<String, String>>,
- ) -> BoxFuture<Result<()>> {
+ ) -> BoxFuture<'_, Result<()>> {
unimplemented!()
}
@@ -179,11 +179,11 @@ impl GitRepository for FakeGitRepository {
self.common_dir_path.clone()
}
- fn merge_message(&self) -> BoxFuture<Option<String>> {
+ fn merge_message(&self) -> BoxFuture<'_, Option<String>> {
async move { None }.boxed()
}
- fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<Result<GitStatus>> {
+ fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<'_, Result<GitStatus>> {
let workdir_path = self.dot_git_path.parent().unwrap();
// Load gitignores
@@ -314,7 +314,7 @@ impl GitRepository for FakeGitRepository {
async move { result? }.boxed()
}
- fn branches(&self) -> BoxFuture<Result<Vec<Branch>>> {
+ fn branches(&self) -> BoxFuture<'_, Result<Vec<Branch>>> {
self.with_state_async(false, move |state| {
let current_branch = &state.current_branch_name;
Ok(state
@@ -330,21 +330,21 @@ impl GitRepository for FakeGitRepository {
})
}
- fn change_branch(&self, name: String) -> BoxFuture<Result<()>> {
+ fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
self.with_state_async(true, |state| {
state.current_branch_name = Some(name);
Ok(())
})
}
- fn create_branch(&self, name: String) -> BoxFuture<Result<()>> {
+ fn create_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
self.with_state_async(true, move |state| {
state.branches.insert(name.to_owned());
Ok(())
})
}
- fn blame(&self, path: RepoPath, _content: Rope) -> BoxFuture<Result<git::blame::Blame>> {
+ fn blame(&self, path: RepoPath, _content: Rope) -> BoxFuture<'_, Result<git::blame::Blame>> {
self.with_state_async(false, move |state| {
state
.blames
@@ -358,7 +358,7 @@ impl GitRepository for FakeGitRepository {
&self,
_paths: Vec<RepoPath>,
_env: Arc<HashMap<String, String>>,
- ) -> BoxFuture<Result<()>> {
+ ) -> BoxFuture<'_, Result<()>> {
unimplemented!()
}
@@ -366,7 +366,7 @@ impl GitRepository for FakeGitRepository {
&self,
_paths: Vec<RepoPath>,
_env: Arc<HashMap<String, String>>,
- ) -> BoxFuture<Result<()>> {
+ ) -> BoxFuture<'_, Result<()>> {
unimplemented!()
}
@@ -376,7 +376,7 @@ impl GitRepository for FakeGitRepository {
_name_and_email: Option<(gpui::SharedString, gpui::SharedString)>,
_options: CommitOptions,
_env: Arc<HashMap<String, String>>,
- ) -> BoxFuture<Result<()>> {
+ ) -> BoxFuture<'_, Result<()>> {
unimplemented!()
}
@@ -388,7 +388,7 @@ impl GitRepository for FakeGitRepository {
_askpass: AskPassDelegate,
_env: Arc<HashMap<String, String>>,
_cx: AsyncApp,
- ) -> BoxFuture<Result<git::repository::RemoteCommandOutput>> {
+ ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
unimplemented!()
}
@@ -399,7 +399,7 @@ impl GitRepository for FakeGitRepository {
_askpass: AskPassDelegate,
_env: Arc<HashMap<String, String>>,
_cx: AsyncApp,
- ) -> BoxFuture<Result<git::repository::RemoteCommandOutput>> {
+ ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
unimplemented!()
}
@@ -409,19 +409,19 @@ impl GitRepository for FakeGitRepository {
_askpass: AskPassDelegate,
_env: Arc<HashMap<String, String>>,
_cx: AsyncApp,
- ) -> BoxFuture<Result<git::repository::RemoteCommandOutput>> {
+ ) -> BoxFuture<'_, Result<git::repository::RemoteCommandOutput>> {
unimplemented!()
}
- fn get_remotes(&self, _branch: Option<String>) -> BoxFuture<Result<Vec<Remote>>> {
+ fn get_remotes(&self, _branch: Option<String>) -> BoxFuture<'_, Result<Vec<Remote>>> {
unimplemented!()
}
- fn check_for_pushed_commit(&self) -> BoxFuture<Result<Vec<gpui::SharedString>>> {
+ fn check_for_pushed_commit(&self) -> BoxFuture<'_, Result<Vec<gpui::SharedString>>> {
future::ready(Ok(Vec::new())).boxed()
}
- fn diff(&self, _diff: git::repository::DiffType) -> BoxFuture<Result<String>> {
+ fn diff(&self, _diff: git::repository::DiffType) -> BoxFuture<'_, Result<String>> {
unimplemented!()
}
@@ -429,7 +429,10 @@ impl GitRepository for FakeGitRepository {
unimplemented!()
}
- fn restore_checkpoint(&self, _checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>> {
+ fn restore_checkpoint(
+ &self,
+ _checkpoint: GitRepositoryCheckpoint,
+ ) -> BoxFuture<'_, Result<()>> {
unimplemented!()
}
@@ -437,7 +440,7 @@ impl GitRepository for FakeGitRepository {
&self,
_left: GitRepositoryCheckpoint,
_right: GitRepositoryCheckpoint,
- ) -> BoxFuture<Result<bool>> {
+ ) -> BoxFuture<'_, Result<bool>> {
unimplemented!()
}
@@ -445,7 +448,7 @@ impl GitRepository for FakeGitRepository {
&self,
_base_checkpoint: GitRepositoryCheckpoint,
_target_checkpoint: GitRepositoryCheckpoint,
- ) -> BoxFuture<Result<String>> {
+ ) -> BoxFuture<'_, Result<String>> {
unimplemented!()
}
}
@@ -65,6 +65,7 @@ actions!(
#[derive(Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)]
#[action(namespace = git, deprecated_aliases = ["editor::RevertFile"])]
+#[serde(deny_unknown_fields)]
pub struct RestoreFile {
#[serde(default)]
pub skip_prompt: bool,
@@ -303,25 +303,25 @@ pub trait GitRepository: Send + Sync {
/// Returns the contents of an entry in the repository's index, or None if there is no entry for the given path.
///
/// Also returns `None` for symlinks.
- fn load_index_text(&self, path: RepoPath) -> BoxFuture<Option<String>>;
+ fn load_index_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>>;
/// Returns the contents of an entry in the repository's HEAD, or None if HEAD does not exist or has no entry for the given path.
///
/// Also returns `None` for symlinks.
- fn load_committed_text(&self, path: RepoPath) -> BoxFuture<Option<String>>;
+ fn load_committed_text(&self, path: RepoPath) -> BoxFuture<'_, Option<String>>;
fn set_index_text(
&self,
path: RepoPath,
content: Option<String>,
env: Arc<HashMap<String, String>>,
- ) -> BoxFuture<anyhow::Result<()>>;
+ ) -> BoxFuture<'_, anyhow::Result<()>>;
/// Returns the URL of the remote with the given name.
fn remote_url(&self, name: &str) -> Option<String>;
/// Resolve a list of refs to SHAs.
- fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<Result<Vec<Option<String>>>>;
+ fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<'_, Result<Vec<Option<String>>>>;
fn head_sha(&self) -> BoxFuture<'_, Option<String>> {
async move {
@@ -335,33 +335,33 @@ pub trait GitRepository: Send + Sync {
.boxed()
}
- fn merge_message(&self) -> BoxFuture<Option<String>>;
+ fn merge_message(&self) -> BoxFuture<'_, Option<String>>;
- fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<Result<GitStatus>>;
+ fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<'_, Result<GitStatus>>;
- fn branches(&self) -> BoxFuture<Result<Vec<Branch>>>;
+ fn branches(&self) -> BoxFuture<'_, Result<Vec<Branch>>>;
- fn change_branch(&self, name: String) -> BoxFuture<Result<()>>;
- fn create_branch(&self, name: String) -> BoxFuture<Result<()>>;
+ fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>>;
+ fn create_branch(&self, name: String) -> BoxFuture<'_, Result<()>>;
fn reset(
&self,
commit: String,
mode: ResetMode,
env: Arc<HashMap<String, String>>,
- ) -> BoxFuture<Result<()>>;
+ ) -> BoxFuture<'_, Result<()>>;
fn checkout_files(
&self,
commit: String,
paths: Vec<RepoPath>,
env: Arc<HashMap<String, String>>,
- ) -> BoxFuture<Result<()>>;
+ ) -> BoxFuture<'_, Result<()>>;
- fn show(&self, commit: String) -> BoxFuture<Result<CommitDetails>>;
+ fn show(&self, commit: String) -> BoxFuture<'_, Result<CommitDetails>>;
- fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<Result<CommitDiff>>;
- fn blame(&self, path: RepoPath, content: Rope) -> BoxFuture<Result<crate::blame::Blame>>;
+ fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<'_, Result<CommitDiff>>;
+ fn blame(&self, path: RepoPath, content: Rope) -> BoxFuture<'_, Result<crate::blame::Blame>>;
/// Returns the absolute path to the repository. For worktrees, this will be the path to the
/// worktree's gitdir within the main repository (typically `.git/worktrees/<name>`).
@@ -376,7 +376,7 @@ pub trait GitRepository: Send + Sync {
&self,
paths: Vec<RepoPath>,
env: Arc<HashMap<String, String>>,
- ) -> BoxFuture<Result<()>>;
+ ) -> BoxFuture<'_, Result<()>>;
/// Updates the index to match HEAD at the given paths.
///
/// If any of the paths were previously staged but do not exist in HEAD, they will be removed from the index.
@@ -384,7 +384,7 @@ pub trait GitRepository: Send + Sync {
&self,
paths: Vec<RepoPath>,
env: Arc<HashMap<String, String>>,
- ) -> BoxFuture<Result<()>>;
+ ) -> BoxFuture<'_, Result<()>>;
fn commit(
&self,
@@ -392,7 +392,7 @@ pub trait GitRepository: Send + Sync {
name_and_email: Option<(SharedString, SharedString)>,
options: CommitOptions,
env: Arc<HashMap<String, String>>,
- ) -> BoxFuture<Result<()>>;
+ ) -> BoxFuture<'_, Result<()>>;
fn push(
&self,
@@ -404,7 +404,7 @@ pub trait GitRepository: Send + Sync {
// This method takes an AsyncApp to ensure it's invoked on the main thread,
// otherwise git-credentials-manager won't work.
cx: AsyncApp,
- ) -> BoxFuture<Result<RemoteCommandOutput>>;
+ ) -> BoxFuture<'_, Result<RemoteCommandOutput>>;
fn pull(
&self,
@@ -415,7 +415,7 @@ pub trait GitRepository: Send + Sync {
// This method takes an AsyncApp to ensure it's invoked on the main thread,
// otherwise git-credentials-manager won't work.
cx: AsyncApp,
- ) -> BoxFuture<Result<RemoteCommandOutput>>;
+ ) -> BoxFuture<'_, Result<RemoteCommandOutput>>;
fn fetch(
&self,
@@ -425,35 +425,35 @@ pub trait GitRepository: Send + Sync {
// This method takes an AsyncApp to ensure it's invoked on the main thread,
// otherwise git-credentials-manager won't work.
cx: AsyncApp,
- ) -> BoxFuture<Result<RemoteCommandOutput>>;
+ ) -> BoxFuture<'_, Result<RemoteCommandOutput>>;
- fn get_remotes(&self, branch_name: Option<String>) -> BoxFuture<Result<Vec<Remote>>>;
+ fn get_remotes(&self, branch_name: Option<String>) -> BoxFuture<'_, Result<Vec<Remote>>>;
/// returns a list of remote branches that contain HEAD
- fn check_for_pushed_commit(&self) -> BoxFuture<Result<Vec<SharedString>>>;
+ fn check_for_pushed_commit(&self) -> BoxFuture<'_, Result<Vec<SharedString>>>;
/// Run git diff
- fn diff(&self, diff: DiffType) -> BoxFuture<Result<String>>;
+ fn diff(&self, diff: DiffType) -> BoxFuture<'_, Result<String>>;
/// Creates a checkpoint for the repository.
fn checkpoint(&self) -> BoxFuture<'static, Result<GitRepositoryCheckpoint>>;
/// Resets to a previously-created checkpoint.
- fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>>;
+ fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture<'_, Result<()>>;
/// Compares two checkpoints, returning true if they are equal
fn compare_checkpoints(
&self,
left: GitRepositoryCheckpoint,
right: GitRepositoryCheckpoint,
- ) -> BoxFuture<Result<bool>>;
+ ) -> BoxFuture<'_, Result<bool>>;
/// Computes a diff between two checkpoints.
fn diff_checkpoints(
&self,
base_checkpoint: GitRepositoryCheckpoint,
target_checkpoint: GitRepositoryCheckpoint,
- ) -> BoxFuture<Result<String>>;
+ ) -> BoxFuture<'_, Result<String>>;
}
pub enum DiffType {
@@ -1032,32 +1032,39 @@ impl GitRepository for RealGitRepository {
fn change_branch(&self, name: String) -> BoxFuture<'_, Result<()>> {
let repo = self.repository.clone();
+ let working_directory = self.working_directory();
+ let git_binary_path = self.git_binary_path.clone();
+ let executor = self.executor.clone();
+ let branch = self.executor.spawn(async move {
+ let repo = repo.lock();
+ let branch = if let Ok(branch) = repo.find_branch(&name, BranchType::Local) {
+ branch
+ } else if let Ok(revision) = repo.find_branch(&name, BranchType::Remote) {
+ let (_, branch_name) = name.split_once("/").context("Unexpected branch format")?;
+ let revision = revision.get();
+ let branch_commit = revision.peel_to_commit()?;
+ let mut branch = repo.branch(&branch_name, &branch_commit, false)?;
+ branch.set_upstream(Some(&name))?;
+ branch
+ } else {
+ anyhow::bail!("Branch not found");
+ };
+
+ Ok(branch
+ .name()?
+ .context("cannot checkout anonymous branch")?
+ .to_string())
+ });
+
self.executor
.spawn(async move {
- let repo = repo.lock();
- let branch = if let Ok(branch) = repo.find_branch(&name, BranchType::Local) {
- branch
- } else if let Ok(revision) = repo.find_branch(&name, BranchType::Remote) {
- let (_, branch_name) =
- name.split_once("/").context("Unexpected branch format")?;
- let revision = revision.get();
- let branch_commit = revision.peel_to_commit()?;
- let mut branch = repo.branch(&branch_name, &branch_commit, false)?;
- branch.set_upstream(Some(&name))?;
- branch
- } else {
- anyhow::bail!("Branch not found");
- };
+ let branch = branch.await?;
- let revision = branch.get();
- let as_tree = revision.peel_to_tree()?;
- repo.checkout_tree(as_tree.as_object(), None)?;
- repo.set_head(
- revision
- .name()
- .context("Branch name could not be retrieved")?,
- )?;
- Ok(())
+ GitBinary::new(git_binary_path, working_directory?, executor)
+ .run(&["checkout", &branch])
+ .await?;
+
+ anyhow::Ok(())
})
.boxed()
}
@@ -2268,7 +2275,7 @@ mod tests {
impl RealGitRepository {
/// Force a Git garbage collection on the repository.
- fn gc(&self) -> BoxFuture<Result<()>> {
+ fn gc(&self) -> BoxFuture<'_, Result<()>> {
let working_directory = self.working_directory();
let git_binary_path = self.git_binary_path.clone();
let executor = self.executor.clone();
@@ -245,7 +245,7 @@ impl PickerDelegate for BranchListDelegate {
type ListItem = ListItem;
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
- "Select branch...".into()
+ "Select branchβ¦".into()
}
fn editor_position(&self) -> PickerEditorPosition {
@@ -439,44 +439,43 @@ impl PickerDelegate for BranchListDelegate {
})
.unwrap_or_else(|| (None, None));
+ let branch_name = if entry.is_new {
+ h_flex()
+ .gap_1()
+ .child(
+ Icon::new(IconName::Plus)
+ .size(IconSize::Small)
+ .color(Color::Muted),
+ )
+ .child(
+ Label::new(format!("Create branch \"{}\"β¦", entry.branch.name()))
+ .single_line()
+ .truncate(),
+ )
+ .into_any_element()
+ } else {
+ HighlightedLabel::new(entry.branch.name().to_owned(), entry.positions.clone())
+ .truncate()
+ .into_any_element()
+ };
+
Some(
ListItem::new(SharedString::from(format!("vcs-menu-{ix}")))
.inset(true)
- .spacing(match self.style {
- BranchListStyle::Modal => ListItemSpacing::default(),
- BranchListStyle::Popover => ListItemSpacing::ExtraDense,
- })
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
.child(
v_flex()
.w_full()
+ .overflow_hidden()
.child(
h_flex()
- .w_full()
- .flex_shrink()
- .overflow_x_hidden()
- .gap_2()
+ .gap_6()
.justify_between()
- .child(div().flex_shrink().overflow_x_hidden().child(
- if entry.is_new {
- Label::new(format!(
- "Create branch \"{}\"β¦",
- entry.branch.name()
- ))
- .single_line()
- .into_any_element()
- } else {
- HighlightedLabel::new(
- entry.branch.name().to_owned(),
- entry.positions.clone(),
- )
- .truncate()
- .into_any_element()
- },
- ))
- .when_some(commit_time, |el, commit_time| {
- el.child(
+ .overflow_x_hidden()
+ .child(branch_name)
+ .when_some(commit_time, |label, commit_time| {
+ label.child(
Label::new(commit_time)
.size(LabelSize::Small)
.color(Color::Muted)
@@ -1,6 +1,6 @@
use anyhow::{Context as _, Result};
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
-use editor::{Editor, EditorEvent, MultiBuffer};
+use editor::{Editor, EditorEvent, MultiBuffer, SelectionEffects};
use git::repository::{CommitDetails, CommitDiff, CommitSummary, RepoPath};
use gpui::{
AnyElement, AnyView, App, AppContext as _, AsyncApp, Context, Entity, EventEmitter,
@@ -154,7 +154,7 @@ impl CommitView {
});
editor.update(cx, |editor, cx| {
editor.disable_header_for_buffer(metadata_buffer_id.unwrap(), cx);
- editor.change_selections(None, window, cx, |selections| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
selections.select_ranges(vec![0..0]);
});
});
@@ -388,6 +388,7 @@ pub(crate) fn commit_message_editor(
commit_editor.set_collaboration_hub(Box::new(project));
commit_editor.set_use_autoclose(false);
commit_editor.set_show_gutter(false, cx);
+ commit_editor.set_use_modal_editing(true);
commit_editor.set_show_wrap_guides(false, cx);
commit_editor.set_show_indent_guides(false, cx);
let placeholder = placeholder.unwrap_or("Enter commit message".into());
@@ -8,7 +8,7 @@ use anyhow::Result;
use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus};
use collections::HashSet;
use editor::{
- Editor, EditorEvent,
+ Editor, EditorEvent, SelectionEffects,
actions::{GoToHunk, GoToPreviousHunk},
scroll::Autoscroll,
};
@@ -255,9 +255,14 @@ impl ProjectDiff {
fn move_to_path(&mut self, path_key: PathKey, window: &mut Window, cx: &mut Context<Self>) {
if let Some(position) = self.multibuffer.read(cx).location_for_path(&path_key, cx) {
self.editor.update(cx, |editor, cx| {
- editor.change_selections(Some(Autoscroll::focused()), window, cx, |s| {
- s.select_ranges([position..position]);
- })
+ editor.change_selections(
+ SelectionEffects::scroll(Autoscroll::focused()),
+ window,
+ cx,
+ |s| {
+ s.select_ranges([position..position]);
+ },
+ )
});
} else {
self.pending_scroll = Some(path_key);
@@ -463,7 +468,7 @@ impl ProjectDiff {
self.editor.update(cx, |editor, cx| {
if was_empty {
- editor.change_selections(None, window, cx, |selections| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
// TODO select the very beginning (possibly inside a deletion)
selections.select_ranges([0..0])
});
@@ -2,8 +2,8 @@ pub mod cursor_position;
use cursor_position::{LineIndicatorFormat, UserCaretPosition};
use editor::{
- Anchor, Editor, MultiBufferSnapshot, RowHighlightOptions, ToOffset, ToPoint, actions::Tab,
- scroll::Autoscroll,
+ Anchor, Editor, MultiBufferSnapshot, RowHighlightOptions, SelectionEffects, ToOffset, ToPoint,
+ actions::Tab, scroll::Autoscroll,
};
use gpui::{
App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, SharedString, Styled,
@@ -249,9 +249,12 @@ impl GoToLine {
let Some(start) = self.anchor_from_query(&snapshot, cx) else {
return;
};
- editor.change_selections(Some(Autoscroll::center()), window, cx, |s| {
- s.select_anchor_ranges([start..start])
- });
+ editor.change_selections(
+ SelectionEffects::scroll(Autoscroll::center()),
+ window,
+ cx,
+ |s| s.select_anchor_ranges([start..start]),
+ );
editor.focus_handle(cx).focus(window);
cx.notify()
});
@@ -12,7 +12,7 @@ license = "Apache-2.0"
workspace = true
[features]
-default = ["http_client", "font-kit", "wayland", "x11"]
+default = ["http_client", "font-kit", "wayland", "x11", "windows-manifest"]
test-support = [
"leak-detection",
"collections/test-support",
@@ -69,7 +69,7 @@ x11 = [
"open",
"scap",
]
-
+windows-manifest = []
[lib]
path = "src/gpui.rs"
@@ -17,7 +17,7 @@ fn main() {
#[cfg(target_os = "macos")]
macos::build();
}
- #[cfg(target_os = "windows")]
+ #[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");
@@ -125,9 +125,7 @@ pub trait Action: Any + Send {
Self: Sized;
/// Optional JSON schema for the action's input data.
- fn action_json_schema(
- _: &mut schemars::r#gen::SchemaGenerator,
- ) -> Option<schemars::schema::Schema>
+ fn action_json_schema(_: &mut schemars::SchemaGenerator) -> Option<schemars::Schema>
where
Self: Sized,
{
@@ -238,7 +236,7 @@ impl Default for ActionRegistry {
struct ActionData {
pub build: ActionBuilder,
- pub json_schema: fn(&mut schemars::r#gen::SchemaGenerator) -> Option<schemars::schema::Schema>,
+ pub json_schema: fn(&mut schemars::SchemaGenerator) -> Option<schemars::Schema>,
}
/// This type must be public so that our macros can build it in other crates.
@@ -253,7 +251,7 @@ pub struct MacroActionData {
pub name: &'static str,
pub type_id: TypeId,
pub build: ActionBuilder,
- pub json_schema: fn(&mut schemars::r#gen::SchemaGenerator) -> Option<schemars::schema::Schema>,
+ pub json_schema: fn(&mut schemars::SchemaGenerator) -> Option<schemars::Schema>,
pub deprecated_aliases: &'static [&'static str],
pub deprecation_message: Option<&'static str>,
}
@@ -357,8 +355,8 @@ impl ActionRegistry {
pub fn action_schemas(
&self,
- generator: &mut schemars::r#gen::SchemaGenerator,
- ) -> Vec<(&'static str, Option<schemars::schema::Schema>)> {
+ generator: &mut schemars::SchemaGenerator,
+ ) -> Vec<(&'static str, Option<schemars::Schema>)> {
// Use the order from all_names so that the resulting schema has sensible order.
self.all_names
.iter()
@@ -1334,6 +1334,11 @@ impl App {
self.pending_effects.push_back(Effect::RefreshWindows);
}
+ /// Get all key bindings in the app.
+ pub fn key_bindings(&self) -> Rc<RefCell<Keymap>> {
+ self.keymap.clone()
+ }
+
/// Register a global listener for actions invoked via the keyboard.
pub fn on_action<A: Action>(&mut self, listener: impl Fn(&A, &mut Self) + 'static) {
self.global_action_listeners
@@ -1388,8 +1393,8 @@ impl App {
/// Get all non-internal actions that have been registered, along with their schemas.
pub fn action_schemas(
&self,
- generator: &mut schemars::r#gen::SchemaGenerator,
- ) -> Vec<(&'static str, Option<schemars::schema::Schema>)> {
+ generator: &mut schemars::SchemaGenerator,
+ ) -> Vec<(&'static str, Option<schemars::Schema>)> {
self.actions.action_schemas(generator)
}
@@ -214,32 +214,6 @@ impl<T: ?Sized> DerefMut for ArenaBox<T> {
}
}
-pub struct ArenaRef<T: ?Sized>(ArenaBox<T>);
-
-impl<T: ?Sized> From<ArenaBox<T>> for ArenaRef<T> {
- fn from(value: ArenaBox<T>) -> Self {
- ArenaRef(value)
- }
-}
-
-impl<T: ?Sized> Clone for ArenaRef<T> {
- fn clone(&self) -> Self {
- Self(ArenaBox {
- ptr: self.0.ptr,
- valid: self.0.valid.clone(),
- })
- }
-}
-
-impl<T: ?Sized> Deref for ArenaRef<T> {
- type Target = T;
-
- #[inline(always)]
- fn deref(&self) -> &Self::Target {
- self.0.deref()
- }
-}
-
#[cfg(test)]
mod tests {
use std::{cell::Cell, rc::Rc};
@@ -1,9 +1,10 @@
use anyhow::{Context as _, bail};
-use schemars::{JsonSchema, SchemaGenerator, schema::Schema};
+use schemars::{JsonSchema, json_schema};
use serde::{
Deserialize, Deserializer, Serialize, Serializer,
de::{self, Visitor},
};
+use std::borrow::Cow;
use std::{
fmt::{self, Display, Formatter},
hash::{Hash, Hasher},
@@ -99,22 +100,14 @@ impl Visitor<'_> for RgbaVisitor {
}
impl JsonSchema for Rgba {
- fn schema_name() -> String {
- "Rgba".to_string()
+ fn schema_name() -> Cow<'static, str> {
+ "Rgba".into()
}
- fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
- use schemars::schema::{InstanceType, SchemaObject, StringValidation};
-
- Schema::Object(SchemaObject {
- instance_type: Some(InstanceType::String.into()),
- string: Some(Box::new(StringValidation {
- pattern: Some(
- r"^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$".to_string(),
- ),
- ..Default::default()
- })),
- ..Default::default()
+ fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
+ json_schema!({
+ "type": "string",
+ "pattern": "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$"
})
}
}
@@ -629,11 +622,11 @@ impl From<Rgba> for Hsla {
}
impl JsonSchema for Hsla {
- fn schema_name() -> String {
+ fn schema_name() -> Cow<'static, str> {
Rgba::schema_name()
}
- fn json_schema(generator: &mut SchemaGenerator) -> Schema {
+ fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
Rgba::json_schema(generator)
}
}
@@ -613,10 +613,10 @@ pub trait InteractiveElement: Sized {
/// Track the focus state of the given focus handle on this element.
/// If the focus handle is focused by the application, this element will
/// apply its focused styles.
- fn track_focus(mut self, focus_handle: &FocusHandle) -> FocusableWrapper<Self> {
+ fn track_focus(mut self, focus_handle: &FocusHandle) -> Self {
self.interactivity().focusable = true;
self.interactivity().tracked_focus_handle = Some(focus_handle.clone());
- FocusableWrapper { element: self }
+ self
}
/// Set the keymap context for this element. This will be used to determine
@@ -980,15 +980,35 @@ pub trait InteractiveElement: Sized {
self.interactivity().block_mouse_except_scroll();
self
}
+
+ /// Set the given styles to be applied when this element, specifically, is focused.
+ /// Requires that the element is focusable. Elements can be made focusable using [`InteractiveElement::track_focus`].
+ fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
+ where
+ Self: Sized,
+ {
+ self.interactivity().focus_style = Some(Box::new(f(StyleRefinement::default())));
+ self
+ }
+
+ /// Set the given styles to be applied when this element is inside another element that is focused.
+ /// Requires that the element is focusable. Elements can be made focusable using [`InteractiveElement::track_focus`].
+ fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
+ where
+ Self: Sized,
+ {
+ self.interactivity().in_focus_style = Some(Box::new(f(StyleRefinement::default())));
+ self
+ }
}
/// A trait for elements that want to use the standard GPUI interactivity features
/// that require state.
pub trait StatefulInteractiveElement: InteractiveElement {
/// Set this element to focusable.
- fn focusable(mut self) -> FocusableWrapper<Self> {
+ fn focusable(mut self) -> Self {
self.interactivity().focusable = true;
- FocusableWrapper { element: self }
+ self
}
/// Set the overflow x and y to scroll.
@@ -1118,27 +1138,6 @@ pub trait StatefulInteractiveElement: InteractiveElement {
}
}
-/// A trait for providing focus related APIs to interactive elements
-pub trait FocusableElement: InteractiveElement {
- /// Set the given styles to be applied when this element, specifically, is focused.
- fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
- where
- Self: Sized,
- {
- self.interactivity().focus_style = Some(Box::new(f(StyleRefinement::default())));
- self
- }
-
- /// Set the given styles to be applied when this element is inside another element that is focused.
- fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
- where
- Self: Sized,
- {
- self.interactivity().in_focus_style = Some(Box::new(f(StyleRefinement::default())));
- self
- }
-}
-
pub(crate) type MouseDownListener =
Box<dyn Fn(&MouseDownEvent, DispatchPhase, &Hitbox, &mut Window, &mut App) + 'static>;
pub(crate) type MouseUpListener =
@@ -2777,126 +2776,6 @@ impl GroupHitboxes {
}
}
-/// A wrapper around an element that can be focused.
-pub struct FocusableWrapper<E> {
- /// The element that is focusable
- pub element: E,
-}
-
-impl<E: InteractiveElement> FocusableElement for FocusableWrapper<E> {}
-
-impl<E> InteractiveElement for FocusableWrapper<E>
-where
- E: InteractiveElement,
-{
- fn interactivity(&mut self) -> &mut Interactivity {
- self.element.interactivity()
- }
-}
-
-impl<E: StatefulInteractiveElement> StatefulInteractiveElement for FocusableWrapper<E> {}
-
-impl<E> Styled for FocusableWrapper<E>
-where
- E: Styled,
-{
- fn style(&mut self) -> &mut StyleRefinement {
- self.element.style()
- }
-}
-
-impl FocusableWrapper<Div> {
- /// Add a listener to be called when the children of this `Div` are prepainted.
- /// This allows you to store the [`Bounds`] of the children for later use.
- pub fn on_children_prepainted(
- mut self,
- listener: impl Fn(Vec<Bounds<Pixels>>, &mut Window, &mut App) + 'static,
- ) -> Self {
- self.element = self.element.on_children_prepainted(listener);
- self
- }
-}
-
-impl<E> Element for FocusableWrapper<E>
-where
- E: Element,
-{
- type RequestLayoutState = E::RequestLayoutState;
- type PrepaintState = E::PrepaintState;
-
- fn id(&self) -> Option<ElementId> {
- self.element.id()
- }
-
- fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
- self.element.source_location()
- }
-
- fn request_layout(
- &mut self,
- id: Option<&GlobalElementId>,
- inspector_id: Option<&InspectorElementId>,
- window: &mut Window,
- cx: &mut App,
- ) -> (LayoutId, Self::RequestLayoutState) {
- self.element.request_layout(id, inspector_id, window, cx)
- }
-
- fn prepaint(
- &mut self,
- id: Option<&GlobalElementId>,
- inspector_id: Option<&InspectorElementId>,
- bounds: Bounds<Pixels>,
- state: &mut Self::RequestLayoutState,
- window: &mut Window,
- cx: &mut App,
- ) -> E::PrepaintState {
- self.element
- .prepaint(id, inspector_id, bounds, state, window, cx)
- }
-
- fn paint(
- &mut self,
- id: Option<&GlobalElementId>,
- inspector_id: Option<&InspectorElementId>,
- bounds: Bounds<Pixels>,
- request_layout: &mut Self::RequestLayoutState,
- prepaint: &mut Self::PrepaintState,
- window: &mut Window,
- cx: &mut App,
- ) {
- self.element.paint(
- id,
- inspector_id,
- bounds,
- request_layout,
- prepaint,
- window,
- cx,
- )
- }
-}
-
-impl<E> IntoElement for FocusableWrapper<E>
-where
- E: IntoElement,
-{
- type Element = E::Element;
-
- fn into_element(self) -> Self::Element {
- self.element.into_element()
- }
-}
-
-impl<E> ParentElement for FocusableWrapper<E>
-where
- E: ParentElement,
-{
- fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
- self.element.extend(elements)
- }
-}
-
/// A wrapper around an element that can store state, produced after assigning an ElementId.
pub struct Stateful<E> {
pub(crate) element: E,
@@ -2927,8 +2806,6 @@ where
}
}
-impl<E: FocusableElement> FocusableElement for Stateful<E> {}
-
impl<E> Element for Stateful<E>
where
E: Element,
@@ -25,7 +25,7 @@ use std::{
use thiserror::Error;
use util::ResultExt;
-use super::{FocusableElement, Stateful, StatefulInteractiveElement};
+use super::{Stateful, StatefulInteractiveElement};
/// The delay before showing the loading state.
pub const LOADING_DELAY: Duration = Duration::from_millis(200);
@@ -509,8 +509,6 @@ impl IntoElement for Img {
}
}
-impl FocusableElement for Img {}
-
impl StatefulInteractiveElement for Img {}
impl ImageSource {
@@ -10,8 +10,8 @@
use crate::{
AnyElement, App, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges, Element, EntityId,
FocusHandle, GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId, IntoElement,
- Overflow, Pixels, Point, ScrollWheelEvent, Size, Style, StyleRefinement, Styled, Window, point,
- px, size,
+ Overflow, Pixels, Point, ScrollDelta, ScrollWheelEvent, Size, Style, StyleRefinement, Styled,
+ Window, point, px, size,
};
use collections::VecDeque;
use refineable::Refineable as _;
@@ -962,12 +962,15 @@ impl Element for List {
let height = bounds.size.height;
let scroll_top = prepaint.layout.scroll_top;
let hitbox_id = prepaint.hitbox.id;
+ let mut accumulated_scroll_delta = ScrollDelta::default();
window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| {
if phase == DispatchPhase::Bubble && hitbox_id.should_handle_scroll(window) {
+ accumulated_scroll_delta = accumulated_scroll_delta.coalesce(event.delta);
+ let pixel_delta = accumulated_scroll_delta.pixel_delta(px(20.));
list_state.0.borrow_mut().scroll(
&scroll_top,
height,
- event.delta.pixel_delta(px(20.)),
+ pixel_delta,
current_view,
window,
cx,
@@ -6,8 +6,9 @@ use anyhow::{Context as _, anyhow};
use core::fmt::Debug;
use derive_more::{Add, AddAssign, Div, DivAssign, Mul, Neg, Sub, SubAssign};
use refineable::Refineable;
-use schemars::{JsonSchema, SchemaGenerator, schema::Schema};
+use schemars::{JsonSchema, json_schema};
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
+use std::borrow::Cow;
use std::{
cmp::{self, PartialOrd},
fmt::{self, Display},
@@ -3229,20 +3230,14 @@ impl TryFrom<&'_ str> for AbsoluteLength {
}
impl JsonSchema for AbsoluteLength {
- fn schema_name() -> String {
- "AbsoluteLength".to_string()
+ fn schema_name() -> Cow<'static, str> {
+ "AbsoluteLength".into()
}
- fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
- use schemars::schema::{InstanceType, SchemaObject, StringValidation};
-
- Schema::Object(SchemaObject {
- instance_type: Some(InstanceType::String.into()),
- string: Some(Box::new(StringValidation {
- pattern: Some(r"^-?\d+(\.\d+)?(px|rem)$".to_string()),
- ..Default::default()
- })),
- ..Default::default()
+ fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
+ json_schema!({
+ "type": "string",
+ "pattern": r"^-?\d+(\.\d+)?(px|rem)$"
})
}
}
@@ -3366,20 +3361,14 @@ impl TryFrom<&'_ str> for DefiniteLength {
}
impl JsonSchema for DefiniteLength {
- fn schema_name() -> String {
- "DefiniteLength".to_string()
+ fn schema_name() -> Cow<'static, str> {
+ "DefiniteLength".into()
}
- fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
- use schemars::schema::{InstanceType, SchemaObject, StringValidation};
-
- Schema::Object(SchemaObject {
- instance_type: Some(InstanceType::String.into()),
- string: Some(Box::new(StringValidation {
- pattern: Some(r"^-?\d+(\.\d+)?(px|rem|%)$".to_string()),
- ..Default::default()
- })),
- ..Default::default()
+ fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
+ json_schema!({
+ "type": "string",
+ "pattern": r"^-?\d+(\.\d+)?(px|rem|%)$"
})
}
}
@@ -3480,20 +3469,14 @@ impl TryFrom<&'_ str> for Length {
}
impl JsonSchema for Length {
- fn schema_name() -> String {
- "Length".to_string()
+ fn schema_name() -> Cow<'static, str> {
+ "Length".into()
}
- fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
- use schemars::schema::{InstanceType, SchemaObject, StringValidation};
-
- Schema::Object(SchemaObject {
- instance_type: Some(InstanceType::String.into()),
- string: Some(Box::new(StringValidation {
- pattern: Some(r"^(auto|-?\d+(\.\d+)?(px|rem|%))$".to_string()),
- ..Default::default()
- })),
- ..Default::default()
+ fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
+ json_schema!({
+ "type": "string",
+ "pattern": r"^(auto|-?\d+(\.\d+)?(px|rem|%))$"
})
}
}
@@ -2,7 +2,7 @@ use std::rc::Rc;
use collections::HashMap;
-use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke};
+use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke, SharedString};
use smallvec::SmallVec;
/// A keybinding and its associated metadata, from the keymap.
@@ -11,6 +11,8 @@ pub struct KeyBinding {
pub(crate) keystrokes: SmallVec<[Keystroke; 2]>,
pub(crate) context_predicate: Option<Rc<KeyBindingContextPredicate>>,
pub(crate) meta: Option<KeyBindingMetaIndex>,
+ /// The json input string used when building the keybinding, if any
+ pub(crate) action_input: Option<SharedString>,
}
impl Clone for KeyBinding {
@@ -20,6 +22,7 @@ impl Clone for KeyBinding {
keystrokes: self.keystrokes.clone(),
context_predicate: self.context_predicate.clone(),
meta: self.meta,
+ action_input: self.action_input.clone(),
}
}
}
@@ -32,7 +35,7 @@ impl KeyBinding {
} else {
None
};
- Self::load(keystrokes, Box::new(action), context_predicate, None).unwrap()
+ Self::load(keystrokes, Box::new(action), context_predicate, None, None).unwrap()
}
/// Load a keybinding from the given raw data.
@@ -41,6 +44,7 @@ impl KeyBinding {
action: Box<dyn Action>,
context_predicate: Option<Rc<KeyBindingContextPredicate>>,
key_equivalents: Option<&HashMap<char, char>>,
+ action_input: Option<SharedString>,
) -> std::result::Result<Self, InvalidKeystrokeError> {
let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes
.split_whitespace()
@@ -62,6 +66,7 @@ impl KeyBinding {
action,
context_predicate,
meta: None,
+ action_input,
})
}
@@ -110,6 +115,11 @@ impl KeyBinding {
pub fn meta(&self) -> Option<KeyBindingMetaIndex> {
self.meta
}
+
+ /// Get the action input associated with the action for this binding
+ pub fn action_input(&self) -> Option<SharedString> {
+ self.action_input.clone()
+ }
}
impl std::fmt::Debug for KeyBinding {
@@ -151,7 +151,7 @@ pub fn guess_compositor() -> &'static str {
pub(crate) fn current_platform(_headless: bool) -> Rc<dyn Platform> {
Rc::new(
WindowsPlatform::new()
- .inspect_err(|err| show_error("Error: Zed failed to launch", err.to_string()))
+ .inspect_err(|err| show_error("Failed to launch", err.to_string()))
.unwrap(),
)
}
@@ -29,14 +29,14 @@ pub unsafe fn new_renderer(
}
impl rwh::HasWindowHandle for RawWindow {
- fn window_handle(&self) -> Result<rwh::WindowHandle, rwh::HandleError> {
+ fn window_handle(&self) -> Result<rwh::WindowHandle<'_>, rwh::HandleError> {
let view = NonNull::new(self.view).unwrap();
let handle = rwh::AppKitWindowHandle::new(view);
Ok(unsafe { rwh::WindowHandle::borrow_raw(handle.into()) })
}
}
impl rwh::HasDisplayHandle for RawWindow {
- fn display_handle(&self) -> Result<rwh::DisplayHandle, rwh::HandleError> {
+ fn display_handle(&self) -> Result<rwh::DisplayHandle<'_>, rwh::HandleError> {
let handle = rwh::AppKitDisplayHandle::new();
Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) })
}
@@ -252,11 +252,11 @@ impl Drop for WaylandWindow {
}
impl WaylandWindow {
- fn borrow(&self) -> Ref<WaylandWindowState> {
+ fn borrow(&self) -> Ref<'_, WaylandWindowState> {
self.0.state.borrow()
}
- fn borrow_mut(&self) -> RefMut<WaylandWindowState> {
+ fn borrow_mut(&self) -> RefMut<'_, WaylandWindowState> {
self.0.state.borrow_mut()
}
@@ -288,7 +288,7 @@ pub(crate) struct X11WindowStatePtr {
}
impl rwh::HasWindowHandle for RawWindow {
- fn window_handle(&self) -> Result<rwh::WindowHandle, rwh::HandleError> {
+ fn window_handle(&self) -> Result<rwh::WindowHandle<'_>, rwh::HandleError> {
let Some(non_zero) = NonZeroU32::new(self.window_id) else {
log::error!("RawWindow.window_id zero when getting window handle.");
return Err(rwh::HandleError::Unavailable);
@@ -299,7 +299,7 @@ impl rwh::HasWindowHandle for RawWindow {
}
}
impl rwh::HasDisplayHandle for RawWindow {
- fn display_handle(&self) -> Result<rwh::DisplayHandle, rwh::HandleError> {
+ fn display_handle(&self) -> Result<rwh::DisplayHandle<'_>, rwh::HandleError> {
let Some(non_zero) = NonNull::new(self.connection) else {
log::error!("Null RawWindow.connection when getting display handle.");
return Err(rwh::HandleError::Unavailable);
@@ -310,12 +310,12 @@ impl rwh::HasDisplayHandle for RawWindow {
}
impl rwh::HasWindowHandle for X11Window {
- fn window_handle(&self) -> Result<rwh::WindowHandle, rwh::HandleError> {
+ fn window_handle(&self) -> Result<rwh::WindowHandle<'_>, rwh::HandleError> {
unimplemented!()
}
}
impl rwh::HasDisplayHandle for X11Window {
- fn display_handle(&self) -> Result<rwh::DisplayHandle, rwh::HandleError> {
+ fn display_handle(&self) -> Result<rwh::DisplayHandle<'_>, rwh::HandleError> {
unimplemented!()
}
}
@@ -679,26 +679,6 @@ impl X11WindowState {
}
}
-/// A handle to an X11 window which destroys it on Drop.
-pub struct X11WindowHandle {
- id: xproto::Window,
- xcb: Rc<XCBConnection>,
-}
-
-impl Drop for X11WindowHandle {
- fn drop(&mut self) {
- maybe!({
- check_reply(
- || "X11 DestroyWindow failed while dropping X11WindowHandle.",
- self.xcb.destroy_window(self.id),
- )?;
- xcb_flush(&self.xcb);
- anyhow::Ok(())
- })
- .log_err();
- }
-}
-
pub(crate) struct X11Window(pub X11WindowStatePtr);
impl Drop for X11Window {
@@ -10,10 +10,12 @@ use crate::{
use block::ConcreteBlock;
use cocoa::{
appkit::{
- NSApplication, NSBackingStoreBuffered, NSColor, NSEvent, NSEventModifierFlags,
- NSFilenamesPboardType, NSPasteboard, NSScreen, NSView, NSViewHeightSizable,
- NSViewWidthSizable, NSWindow, NSWindowButton, NSWindowCollectionBehavior,
- NSWindowOcclusionState, NSWindowStyleMask, NSWindowTitleVisibility,
+ NSAppKitVersionNumber, NSAppKitVersionNumber12_0, NSApplication, NSBackingStoreBuffered,
+ NSColor, NSEvent, NSEventModifierFlags, NSFilenamesPboardType, NSPasteboard, NSScreen,
+ NSView, NSViewHeightSizable, NSViewWidthSizable, NSVisualEffectMaterial,
+ NSVisualEffectState, NSVisualEffectView, NSWindow, NSWindowButton,
+ NSWindowCollectionBehavior, NSWindowOcclusionState, NSWindowOrderingMode,
+ NSWindowStyleMask, NSWindowTitleVisibility,
},
base::{id, nil},
foundation::{
@@ -53,6 +55,7 @@ const WINDOW_STATE_IVAR: &str = "windowState";
static mut WINDOW_CLASS: *const Class = ptr::null();
static mut PANEL_CLASS: *const Class = ptr::null();
static mut VIEW_CLASS: *const Class = ptr::null();
+static mut BLURRED_VIEW_CLASS: *const Class = ptr::null();
#[allow(non_upper_case_globals)]
const NSWindowStyleMaskNonactivatingPanel: NSWindowStyleMask =
@@ -241,6 +244,20 @@ unsafe fn build_classes() {
}
decl.register()
};
+ BLURRED_VIEW_CLASS = {
+ let mut decl = ClassDecl::new("BlurredView", class!(NSVisualEffectView)).unwrap();
+ unsafe {
+ decl.add_method(
+ sel!(initWithFrame:),
+ blurred_view_init_with_frame as extern "C" fn(&Object, Sel, NSRect) -> id,
+ );
+ decl.add_method(
+ sel!(updateLayer),
+ blurred_view_update_layer as extern "C" fn(&Object, Sel),
+ );
+ decl.register()
+ }
+ };
}
}
@@ -335,6 +352,7 @@ struct MacWindowState {
executor: ForegroundExecutor,
native_window: id,
native_view: NonNull<Object>,
+ blurred_view: Option<id>,
display_link: Option<DisplayLink>,
renderer: renderer::Renderer,
request_frame_callback: Option<Box<dyn FnMut(RequestFrameOptions)>>,
@@ -600,8 +618,9 @@ impl MacWindow {
setReleasedWhenClosed: NO
];
+ let content_view = native_window.contentView();
let native_view: id = msg_send![VIEW_CLASS, alloc];
- let native_view = NSView::init(native_view);
+ let native_view = NSView::initWithFrame_(native_view, NSView::bounds(content_view));
assert!(!native_view.is_null());
let mut window = Self(Arc::new(Mutex::new(MacWindowState {
@@ -609,6 +628,7 @@ impl MacWindow {
executor,
native_window,
native_view: NonNull::new_unchecked(native_view),
+ blurred_view: None,
display_link: None,
renderer: renderer::new_renderer(
renderer_context,
@@ -683,11 +703,11 @@ impl MacWindow {
// itself and break the association with its context.
native_view.setWantsLayer(YES);
let _: () = msg_send![
- native_view,
- setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize
+ native_view,
+ setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize
];
- native_window.setContentView_(native_view.autorelease());
+ content_view.addSubview_(native_view.autorelease());
native_window.makeFirstResponder_(native_view);
match kind {
@@ -1035,28 +1055,57 @@ impl PlatformWindow for MacWindow {
fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
let mut this = self.0.as_ref().lock();
- this.renderer
- .update_transparency(background_appearance != WindowBackgroundAppearance::Opaque);
- let blur_radius = if background_appearance == WindowBackgroundAppearance::Blurred {
- 80
- } else {
- 0
- };
- let opaque = (background_appearance == WindowBackgroundAppearance::Opaque).to_objc();
+ let opaque = background_appearance == WindowBackgroundAppearance::Opaque;
+ this.renderer.update_transparency(!opaque);
unsafe {
- this.native_window.setOpaque_(opaque);
- // Shadows for transparent windows cause artifacts and performance issues
- this.native_window.setHasShadow_(opaque);
- let clear_color = if opaque == YES {
+ this.native_window.setOpaque_(opaque as BOOL);
+ let background_color = if opaque {
NSColor::colorWithSRGBRed_green_blue_alpha_(nil, 0f64, 0f64, 0f64, 1f64)
} else {
- NSColor::clearColor(nil)
+ // Not using `+[NSColor clearColor]` to avoid broken shadow.
+ NSColor::colorWithSRGBRed_green_blue_alpha_(nil, 0f64, 0f64, 0f64, 0.0001)
};
- this.native_window.setBackgroundColor_(clear_color);
- let window_number = this.native_window.windowNumber();
- CGSSetWindowBackgroundBlurRadius(CGSMainConnectionID(), window_number, blur_radius);
+ this.native_window.setBackgroundColor_(background_color);
+
+ if NSAppKitVersionNumber < NSAppKitVersionNumber12_0 {
+ // Whether `-[NSVisualEffectView respondsToSelector:@selector(_updateProxyLayer)]`.
+ // On macOS Catalina/Big Sur `NSVisualEffectView` doesnβt own concrete sublayers
+ // but uses a `CAProxyLayer`. Use the legacy WindowServer API.
+ let blur_radius = if background_appearance == WindowBackgroundAppearance::Blurred {
+ 80
+ } else {
+ 0
+ };
+
+ let window_number = this.native_window.windowNumber();
+ CGSSetWindowBackgroundBlurRadius(CGSMainConnectionID(), window_number, blur_radius);
+ } else {
+ // On newer macOS `NSVisualEffectView` manages the effect layer directly. Using it
+ // could have a better performance (it downsamples the backdrop) and more control
+ // over the effect layer.
+ if background_appearance != WindowBackgroundAppearance::Blurred {
+ if let Some(blur_view) = this.blurred_view {
+ NSView::removeFromSuperview(blur_view);
+ this.blurred_view = None;
+ }
+ } else if this.blurred_view == None {
+ let content_view = this.native_window.contentView();
+ let frame = NSView::bounds(content_view);
+ let mut blur_view: id = msg_send![BLURRED_VIEW_CLASS, alloc];
+ blur_view = NSView::initWithFrame_(blur_view, frame);
+ blur_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable);
+
+ let _: () = msg_send![
+ content_view,
+ addSubview: blur_view
+ positioned: NSWindowOrderingMode::NSWindowBelow
+ relativeTo: nil
+ ];
+ this.blurred_view = Some(blur_view.autorelease());
+ }
+ }
}
}
@@ -1763,7 +1812,12 @@ extern "C" fn set_frame_size(this: &Object, _: Sel, size: NSSize) {
let mut lock = window_state.as_ref().lock();
let new_size = Size::<Pixels>::from(size);
- if lock.content_size() == new_size {
+ let old_size = unsafe {
+ let old_frame: NSRect = msg_send![this, frame];
+ Size::<Pixels>::from(old_frame.size)
+ };
+
+ if old_size == new_size {
return;
}
@@ -2148,3 +2202,75 @@ unsafe fn display_id_for_screen(screen: id) -> CGDirectDisplayID {
screen_number as CGDirectDisplayID
}
}
+
+extern "C" fn blurred_view_init_with_frame(this: &Object, _: Sel, frame: NSRect) -> id {
+ unsafe {
+ let view = msg_send![super(this, class!(NSVisualEffectView)), initWithFrame: frame];
+ // Use a colorless semantic material. The default value `AppearanceBased`, though not
+ // manually set, is deprecated.
+ NSVisualEffectView::setMaterial_(view, NSVisualEffectMaterial::Selection);
+ NSVisualEffectView::setState_(view, NSVisualEffectState::Active);
+ view
+ }
+}
+
+extern "C" fn blurred_view_update_layer(this: &Object, _: Sel) {
+ unsafe {
+ let _: () = msg_send![super(this, class!(NSVisualEffectView)), updateLayer];
+ let layer: id = msg_send![this, layer];
+ if !layer.is_null() {
+ remove_layer_background(layer);
+ }
+ }
+}
+
+unsafe fn remove_layer_background(layer: id) {
+ unsafe {
+ let _: () = msg_send![layer, setBackgroundColor:nil];
+
+ let class_name: id = msg_send![layer, className];
+ if class_name.isEqualToString("CAChameleonLayer") {
+ // Remove the desktop tinting effect.
+ let _: () = msg_send![layer, setHidden: YES];
+ return;
+ }
+
+ let filters: id = msg_send![layer, filters];
+ if !filters.is_null() {
+ // Remove the increased saturation.
+ // The effect of a `CAFilter` or `CIFilter` is determined by its name, and the
+ // `description` reflects its name and some parameters. Currently `NSVisualEffectView`
+ // uses a `CAFilter` named "colorSaturate". If one day they switch to `CIFilter`, the
+ // `description` will still contain "Saturat" ("... inputSaturation = ...").
+ let test_string: id = NSString::alloc(nil).init_str("Saturat").autorelease();
+ let count = NSArray::count(filters);
+ for i in 0..count {
+ let description: id = msg_send![filters.objectAtIndex(i), description];
+ let hit: BOOL = msg_send![description, containsString: test_string];
+ if hit == NO {
+ continue;
+ }
+
+ let all_indices = NSRange {
+ location: 0,
+ length: count,
+ };
+ let indices: id = msg_send![class!(NSMutableIndexSet), indexSet];
+ let _: () = msg_send![indices, addIndexesInRange: all_indices];
+ let _: () = msg_send![indices, removeIndex:i];
+ let filtered: id = msg_send![filters, objectsAtIndexes: indices];
+ let _: () = msg_send![layer, setFilters: filtered];
+ break;
+ }
+ }
+
+ let sublayers: id = msg_send![layer, sublayers];
+ if !sublayers.is_null() {
+ let count = NSArray::count(sublayers);
+ for i in 0..count {
+ let sublayer = sublayers.objectAtIndex(i);
+ remove_layer_background(sublayer);
+ }
+ }
+ }
+}
@@ -1074,8 +1074,10 @@ fn handle_nc_mouse_up_msg(
}
let last_pressed = state_ptr.state.borrow_mut().nc_button_pressed.take();
- if button == MouseButton::Left && last_pressed.is_some() {
- let handled = match (wparam.0 as u32, last_pressed.unwrap()) {
+ if button == MouseButton::Left
+ && let Some(last_pressed) = last_pressed
+ {
+ let handled = match (wparam.0 as u32, last_pressed) {
(HTMINBUTTON, HTMINBUTTON) => {
unsafe { ShowWindowAsync(handle, SW_MINIMIZE).ok().log_err() };
true
@@ -1250,11 +1250,13 @@ fn set_window_composition_attribute(hwnd: HWND, color: Option<Color>, state: u32
type SetWindowCompositionAttributeType =
unsafe extern "system" fn(HWND, *mut WINDOWCOMPOSITIONATTRIBDATA) -> BOOL;
let module_name = PCSTR::from_raw(c"user32.dll".as_ptr() as *const u8);
- let user32 = GetModuleHandleA(module_name);
- if user32.is_ok() {
+ if let Some(user32) = GetModuleHandleA(module_name)
+ .context("Unable to get user32.dll handle")
+ .log_err()
+ {
let func_name = PCSTR::from_raw(c"SetWindowCompositionAttribute".as_ptr() as *const u8);
let set_window_composition_attribute: SetWindowCompositionAttributeType =
- std::mem::transmute(GetProcAddress(user32.unwrap(), func_name));
+ std::mem::transmute(GetProcAddress(user32, func_name));
let mut color = color.unwrap_or_default();
let is_acrylic = state == 4;
if is_acrylic && color.3 == 0 {
@@ -1275,10 +1277,6 @@ fn set_window_composition_attribute(hwnd: HWND, color: Option<Color>, state: u32
cb_data: std::mem::size_of::<AccentPolicy>(),
};
let _ = set_window_composition_attribute(hwnd, &mut data as *mut _ as _);
- } else {
- let _ = user32
- .inspect_err(|e| log::error!("Error getting module: {e}"))
- .ok();
}
}
}
@@ -1301,12 +1299,8 @@ mod windows_renderer {
size: Default::default(),
transparent,
};
- BladeRenderer::new(context, &raw, config).inspect_err(|err| {
- show_error(
- "Error: Zed failed to initialize BladeRenderer",
- err.to_string(),
- )
- })
+ BladeRenderer::new(context, &raw, config)
+ .inspect_err(|err| show_error("Failed to initialize BladeRenderer", err.to_string()))
}
struct RawWindow {
@@ -3,7 +3,7 @@
//! application to avoid having to import each trait individually.
pub use crate::{
- AppContext as _, BorrowAppContext, Context, Element, FocusableElement, InteractiveElement,
- IntoElement, ParentElement, Refineable, Render, RenderOnce, StatefulInteractiveElement, Styled,
- StyledImage, VisualContext, util::FluentBuilder,
+ AppContext as _, BorrowAppContext, Context, Element, InteractiveElement, IntoElement,
+ ParentElement, Refineable, Render, RenderOnce, StatefulInteractiveElement, Styled, StyledImage,
+ VisualContext, util::FluentBuilder,
};
@@ -2,7 +2,10 @@ use derive_more::{Deref, DerefMut};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
-use std::{borrow::Borrow, sync::Arc};
+use std::{
+ borrow::{Borrow, Cow},
+ sync::Arc,
+};
use util::arc_cow::ArcCow;
/// A shared string is an immutable string that can be cheaply cloned in GPUI
@@ -23,12 +26,16 @@ impl SharedString {
}
impl JsonSchema for SharedString {
- fn schema_name() -> String {
+ fn inline_schema() -> bool {
+ String::inline_schema()
+ }
+
+ fn schema_name() -> Cow<'static, str> {
String::schema_name()
}
- fn json_schema(r#gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
- String::json_schema(r#gen)
+ fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
+ String::json_schema(generator)
}
}
@@ -1,6 +1,7 @@
+use std::borrow::Cow;
use std::sync::Arc;
-use schemars::schema::{InstanceType, SchemaObject};
+use schemars::{JsonSchema, json_schema};
/// The OpenType features that can be configured for a given font.
#[derive(Default, Clone, Eq, PartialEq, Hash)]
@@ -128,36 +129,23 @@ impl serde::Serialize for FontFeatures {
}
}
-impl schemars::JsonSchema for FontFeatures {
- fn schema_name() -> String {
+impl JsonSchema for FontFeatures {
+ fn schema_name() -> Cow<'static, str> {
"FontFeatures".into()
}
- fn json_schema(_: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
- let mut schema = SchemaObject::default();
- schema.instance_type = Some(schemars::schema::SingleOrVec::Single(Box::new(
- InstanceType::Object,
- )));
- {
- let mut property = SchemaObject {
- instance_type: Some(schemars::schema::SingleOrVec::Vec(vec![
- InstanceType::Boolean,
- InstanceType::Integer,
- ])),
- ..Default::default()
- };
-
- {
- let mut number_constraints = property.number();
- number_constraints.multiple_of = Some(1.0);
- number_constraints.minimum = Some(0.0);
- }
- schema
- .object()
- .pattern_properties
- .insert("[0-9a-zA-Z]{4}$".into(), property.into());
- }
- schema.into()
+ fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
+ json_schema!({
+ "type": "object",
+ "patternProperties": {
+ "[0-9a-zA-Z]{4}$": {
+ "type": ["boolean", "integer"],
+ "minimum": 0,
+ "multipleOf": 1
+ }
+ },
+ "additionalProperties": false
+ })
}
}
@@ -582,7 +582,7 @@ pub struct FontRun {
}
trait AsCacheKeyRef {
- fn as_cache_key_ref(&self) -> CacheKeyRef;
+ fn as_cache_key_ref(&self) -> CacheKeyRef<'_>;
}
#[derive(Clone, Debug, Eq)]
@@ -83,34 +83,6 @@ where
timer.race(future).await
}
-#[cfg(any(test, feature = "test-support"))]
-pub struct CwdBacktrace<'a>(pub &'a backtrace::Backtrace);
-
-#[cfg(any(test, feature = "test-support"))]
-impl std::fmt::Debug for CwdBacktrace<'_> {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- use backtrace::{BacktraceFmt, BytesOrWideString};
-
- let cwd = std::env::current_dir().unwrap();
- let cwd = cwd.parent().unwrap();
- let mut print_path = |fmt: &mut std::fmt::Formatter<'_>, path: BytesOrWideString<'_>| {
- std::fmt::Display::fmt(&path, fmt)
- };
- let mut fmt = BacktraceFmt::new(f, backtrace::PrintFmt::Full, &mut print_path);
- for frame in self.0.frames() {
- let mut formatted_frame = fmt.frame();
- if frame
- .symbols()
- .iter()
- .any(|s| s.filename().map_or(false, |f| f.starts_with(cwd)))
- {
- formatted_frame.backtrace_frame(frame)?;
- }
- }
- fmt.finish()
- }
-}
-
/// Increment the given atomic counter if it is not zero.
/// Return the new value of the counter.
pub(crate) fn atomic_incr_if_not_zero(counter: &AtomicUsize) -> usize {
@@ -16,9 +16,11 @@ fn test_action_macros() {
#[derive(PartialEq, Clone, Deserialize, JsonSchema, Action)]
#[action(namespace = test_only)]
- struct AnotherSomeAction;
+ #[serde(deny_unknown_fields)]
+ struct AnotherAction;
#[derive(PartialEq, Clone, gpui::private::serde_derive::Deserialize)]
+ #[serde(deny_unknown_fields)]
struct RegisterableAction {}
register_action!(RegisterableAction);
@@ -159,8 +159,8 @@ pub(crate) fn derive_action(input: TokenStream) -> TokenStream {
}
fn action_json_schema(
- _generator: &mut gpui::private::schemars::r#gen::SchemaGenerator,
- ) -> Option<gpui::private::schemars::schema::Schema> {
+ _generator: &mut gpui::private::schemars::SchemaGenerator,
+ ) -> Option<gpui::private::schemars::Schema> {
#json_schema_fn_body
}
@@ -23,6 +23,7 @@ pub enum IconName {
AiZed,
ArrowCircle,
ArrowDown,
+ ArrowDown10,
ArrowDownFromLine,
ArrowDownRight,
ArrowLeft,
@@ -45,6 +46,7 @@ pub enum IconName {
Blocks,
Bolt,
BoltFilled,
+ BoltFilledAlt,
Book,
BookCopy,
BookPlus,
@@ -163,6 +165,9 @@ pub enum IconName {
ListX,
LoadCircle,
LockOutlined,
+ LspDebug,
+ LspRestart,
+ LspStop,
MagnifyingGlass,
MailOpen,
Maximize,
@@ -208,6 +213,7 @@ pub enum IconName {
Save,
Scissors,
Screen,
+ ScrollText,
SearchCode,
SearchSelection,
SelectAll,
@@ -227,6 +233,7 @@ pub enum IconName {
SparkleFilled,
Spinner,
Split,
+ SplitAlt,
SquareDot,
SquareMinus,
SquarePlus,
@@ -2,7 +2,7 @@ use anyhow::Result;
use client::{UserStore, zed_urls};
use copilot::{Copilot, Status};
use editor::{
- Editor,
+ Editor, SelectionEffects,
actions::{ShowEditPrediction, ToggleEditPrediction},
scroll::Autoscroll,
};
@@ -929,9 +929,14 @@ async fn open_disabled_globs_setting_in_editor(
.map(|inner_match| inner_match.start()..inner_match.end())
});
if let Some(range) = range {
- item.change_selections(Some(Autoscroll::newest()), window, cx, |selections| {
- selections.select_ranges(vec![range]);
- });
+ item.change_selections(
+ SelectionEffects::scroll(Autoscroll::newest()),
+ window,
+ cx,
+ |selections| {
+ selections.select_ranges(vec![range]);
+ },
+ );
}
})?;
@@ -962,6 +967,7 @@ fn toggle_show_inline_completions_for_language(
all_language_settings(None, cx).show_edit_predictions(Some(&language), cx);
update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
file.languages
+ .0
.entry(language.name())
.or_default()
.show_edit_predictions = Some(!show_edit_predictions);
@@ -1,7 +1,7 @@
use anyhow::Result;
use chrono::{Datelike, Local, NaiveTime, Timelike};
-use editor::Editor;
use editor::scroll::Autoscroll;
+use editor::{Editor, SelectionEffects};
use gpui::{App, AppContext as _, Context, Window, actions};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -168,9 +168,12 @@ pub fn new_journal_entry(workspace: &Workspace, window: &mut Window, cx: &mut Ap
if let Some(editor) = item.downcast::<Editor>().map(|editor| editor.downgrade()) {
editor.update_in(cx, |editor, window, cx| {
let len = editor.buffer().read(cx).len(cx);
- editor.change_selections(Some(Autoscroll::center()), window, cx, |s| {
- s.select_ranges([len..len])
- });
+ editor.change_selections(
+ SelectionEffects::scroll(Autoscroll::center()),
+ window,
+ cx,
+ |s| s.select_ranges([len..len]),
+ );
if len > 0 {
editor.insert("\n\n", window, cx);
}
@@ -39,6 +39,7 @@ globset.workspace = true
gpui.workspace = true
http_client.workspace = true
imara-diff.workspace = true
+inventory.workspace = true
itertools.workspace = true
log.workspace = true
lsp.workspace = true
@@ -2006,7 +2006,7 @@ fn test_autoindent_language_without_indents_query(cx: &mut App) {
#[gpui::test]
fn test_autoindent_with_injected_languages(cx: &mut App) {
init_settings(cx, |settings| {
- settings.languages.extend([
+ settings.languages.0.extend([
(
"HTML".into(),
LanguageSettingsContent {
@@ -39,11 +39,7 @@ use lsp::{CodeActionKind, InitializeParams, LanguageServerBinary, LanguageServer
pub use manifest::{ManifestDelegate, ManifestName, ManifestProvider, ManifestQuery};
use parking_lot::Mutex;
use regex::Regex;
-use schemars::{
- JsonSchema,
- r#gen::SchemaGenerator,
- schema::{InstanceType, Schema, SchemaObject},
-};
+use schemars::{JsonSchema, SchemaGenerator, json_schema};
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use serde_json::Value;
use settings::WorktreeId;
@@ -694,7 +690,6 @@ pub struct LanguageConfig {
pub matcher: LanguageMatcher,
/// List of bracket types in a language.
#[serde(default)]
- #[schemars(schema_with = "bracket_pair_config_json_schema")]
pub brackets: BracketPairConfig,
/// If set to true, auto indentation uses last non empty line to determine
/// the indentation level for a new line.
@@ -735,6 +730,13 @@ pub struct LanguageConfig {
/// Starting and closing characters of a block comment.
#[serde(default)]
pub block_comment: Option<(Arc<str>, Arc<str>)>,
+ /// A list of additional regex patterns that should be treated as prefixes
+ /// for creating boundaries during rewrapping, ensuring content from one
+ /// prefixed section doesn't merge with another (e.g., markdown list items).
+ /// By default, Zed treats as paragraph and comment prefixes as boundaries.
+ #[serde(default, deserialize_with = "deserialize_regex_vec")]
+ #[schemars(schema_with = "regex_vec_json_schema")]
+ pub rewrap_prefixes: Vec<Regex>,
/// A list of language servers that are allowed to run on subranges of a given language.
#[serde(default)]
pub scope_opt_in_language_servers: Vec<LanguageServerName>,
@@ -914,6 +916,7 @@ impl Default for LanguageConfig {
autoclose_before: Default::default(),
line_comments: Default::default(),
block_comment: Default::default(),
+ rewrap_prefixes: Default::default(),
scope_opt_in_language_servers: Default::default(),
overrides: Default::default(),
word_characters: Default::default(),
@@ -944,10 +947,9 @@ fn deserialize_regex<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Regex>, D
}
}
-fn regex_json_schema(_: &mut SchemaGenerator) -> Schema {
- Schema::Object(SchemaObject {
- instance_type: Some(InstanceType::String.into()),
- ..Default::default()
+fn regex_json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
+ json_schema!({
+ "type": "string"
})
}
@@ -961,6 +963,22 @@ where
}
}
+fn deserialize_regex_vec<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<Regex>, D::Error> {
+ let sources = Vec::<String>::deserialize(d)?;
+ let mut regexes = Vec::new();
+ for source in sources {
+ regexes.push(regex::Regex::new(&source).map_err(de::Error::custom)?);
+ }
+ Ok(regexes)
+}
+
+fn regex_vec_json_schema(_: &mut SchemaGenerator) -> schemars::Schema {
+ json_schema!({
+ "type": "array",
+ "items": { "type": "string" }
+ })
+}
+
#[doc(hidden)]
#[cfg(any(test, feature = "test-support"))]
pub struct FakeLspAdapter {
@@ -988,12 +1006,12 @@ pub struct FakeLspAdapter {
/// This struct includes settings for defining which pairs of characters are considered brackets and
/// also specifies any language-specific scopes where these pairs should be ignored for bracket matching purposes.
#[derive(Clone, Debug, Default, JsonSchema)]
+#[schemars(with = "Vec::<BracketPairContent>")]
pub struct BracketPairConfig {
/// A list of character pairs that should be treated as brackets in the context of a given language.
pub pairs: Vec<BracketPair>,
/// A list of tree-sitter scopes for which a given bracket should not be active.
/// N-th entry in `[Self::disabled_scopes_by_bracket_ix]` contains a list of disabled scopes for an n-th entry in `[Self::pairs]`
- #[serde(skip)]
pub disabled_scopes_by_bracket_ix: Vec<Vec<String>>,
}
@@ -1003,10 +1021,6 @@ impl BracketPairConfig {
}
}
-fn bracket_pair_config_json_schema(r#gen: &mut SchemaGenerator) -> Schema {
- Option::<Vec<BracketPairContent>>::json_schema(r#gen)
-}
-
#[derive(Deserialize, JsonSchema)]
pub struct BracketPairContent {
#[serde(flatten)]
@@ -1841,6 +1855,14 @@ impl LanguageScope {
.map(|e| (&e.0, &e.1))
}
+ /// Returns additional regex patterns that act as prefix markers for creating
+ /// boundaries during rewrapping.
+ ///
+ /// By default, Zed treats as paragraph and comment prefixes as boundaries.
+ pub fn rewrap_prefixes(&self) -> &[Regex] {
+ &self.language.config.rewrap_prefixes
+ }
+
/// Returns a list of language-specific word characters.
///
/// By default, Zed treats alphanumeric characters (and '_') as word characters for
@@ -1170,7 +1170,7 @@ impl LanguageRegistryState {
if let Some(theme) = self.theme.as_ref() {
language.set_theme(theme.syntax());
}
- self.language_settings.languages.insert(
+ self.language_settings.languages.0.insert(
language.name(),
LanguageSettingsContent {
tab_size: language.config.tab_size,
@@ -3,7 +3,6 @@
use crate::{File, Language, LanguageName, LanguageServerName};
use anyhow::Result;
use collections::{FxHashMap, HashMap, HashSet};
-use core::slice;
use ec4rs::{
Properties as EditorconfigProperties,
property::{FinalNewline, IndentSize, IndentStyle, TabWidth, TrimTrailingWs},
@@ -11,17 +10,15 @@ use ec4rs::{
use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
use gpui::{App, Modifiers};
use itertools::{Either, Itertools};
-use schemars::{
- JsonSchema,
- schema::{InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec},
-};
+use schemars::{JsonSchema, json_schema};
use serde::{
Deserialize, Deserializer, Serialize,
de::{self, IntoDeserializer, MapAccess, SeqAccess, Visitor},
};
-use serde_json::Value;
+
use settings::{
- Settings, SettingsLocation, SettingsSources, SettingsStore, add_references_to_properties,
+ ParameterizedJsonSchema, Settings, SettingsLocation, SettingsSources, SettingsStore,
+ replace_subschema,
};
use shellexpand;
use std::{borrow::Cow, num::NonZeroU32, path::Path, sync::Arc};
@@ -306,13 +303,42 @@ pub struct AllLanguageSettingsContent {
pub defaults: LanguageSettingsContent,
/// The settings for individual languages.
#[serde(default)]
- pub languages: HashMap<LanguageName, LanguageSettingsContent>,
+ pub languages: LanguageToSettingsMap,
/// Settings for associating file extensions and filenames
/// with languages.
#[serde(default)]
pub file_types: HashMap<Arc<str>, Vec<String>>,
}
+/// Map from language name to settings. Its `ParameterizedJsonSchema` allows only known language
+/// names in the keys.
+#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
+pub struct LanguageToSettingsMap(pub HashMap<LanguageName, LanguageSettingsContent>);
+
+inventory::submit! {
+ ParameterizedJsonSchema {
+ add_and_get_ref: |generator, params, _cx| {
+ let language_settings_content_ref = generator
+ .subschema_for::<LanguageSettingsContent>()
+ .to_value();
+ let schema = json_schema!({
+ "type": "object",
+ "properties": params
+ .language_names
+ .iter()
+ .map(|name| {
+ (
+ name.clone(),
+ language_settings_content_ref.clone(),
+ )
+ })
+ .collect::<serde_json::Map<_, _>>()
+ });
+ replace_subschema::<LanguageToSettingsMap>(generator, schema)
+ }
+ }
+}
+
/// Controls how completions are processed for this language.
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
@@ -384,7 +410,6 @@ fn default_lsp_fetch_timeout_ms() -> u64 {
/// The settings for a particular language.
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
-#[schemars(deny_unknown_fields)]
pub struct LanguageSettingsContent {
/// How many columns a tab should occupy.
///
@@ -648,45 +673,30 @@ pub enum FormatOnSave {
On,
/// Files should not be formatted on save.
Off,
- List(FormatterList),
+ List(Vec<Formatter>),
}
impl JsonSchema for FormatOnSave {
- fn schema_name() -> String {
+ fn schema_name() -> Cow<'static, str> {
"OnSaveFormatter".into()
}
- fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> Schema {
- let mut schema = SchemaObject::default();
+ fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
let formatter_schema = Formatter::json_schema(generator);
- schema.instance_type = Some(
- vec![
- InstanceType::Object,
- InstanceType::String,
- InstanceType::Array,
- ]
- .into(),
- );
-
- let valid_raw_values = SchemaObject {
- enum_values: Some(vec![
- Value::String("on".into()),
- Value::String("off".into()),
- Value::String("prettier".into()),
- Value::String("language_server".into()),
- ]),
- ..Default::default()
- };
- let mut nested_values = SchemaObject::default();
- nested_values.array().items = Some(formatter_schema.clone().into());
-
- schema.subschemas().any_of = Some(vec![
- nested_values.into(),
- valid_raw_values.into(),
- formatter_schema,
- ]);
- schema.into()
+ json_schema!({
+ "oneOf": [
+ {
+ "type": "array",
+ "items": formatter_schema
+ },
+ {
+ "type": "string",
+ "enum": ["on", "off", "prettier", "language_server"]
+ },
+ formatter_schema
+ ]
+ })
}
}
@@ -725,11 +735,11 @@ impl<'de> Deserialize<'de> for FormatOnSave {
} else if v == "off" {
Ok(Self::Value::Off)
} else if v == "language_server" {
- Ok(Self::Value::List(FormatterList(
- Formatter::LanguageServer { name: None }.into(),
- )))
+ Ok(Self::Value::List(vec![Formatter::LanguageServer {
+ name: None,
+ }]))
} else {
- let ret: Result<FormatterList, _> =
+ let ret: Result<Vec<Formatter>, _> =
Deserialize::deserialize(v.into_deserializer());
ret.map(Self::Value::List)
}
@@ -738,7 +748,7 @@ impl<'de> Deserialize<'de> for FormatOnSave {
where
A: MapAccess<'d>,
{
- let ret: Result<FormatterList, _> =
+ let ret: Result<Vec<Formatter>, _> =
Deserialize::deserialize(de::value::MapAccessDeserializer::new(map));
ret.map(Self::Value::List)
}
@@ -746,7 +756,7 @@ impl<'de> Deserialize<'de> for FormatOnSave {
where
A: SeqAccess<'d>,
{
- let ret: Result<FormatterList, _> =
+ let ret: Result<Vec<Formatter>, _> =
Deserialize::deserialize(de::value::SeqAccessDeserializer::new(map));
ret.map(Self::Value::List)
}
@@ -783,45 +793,30 @@ pub enum SelectedFormatter {
/// or falling back to formatting via language server.
#[default]
Auto,
- List(FormatterList),
+ List(Vec<Formatter>),
}
impl JsonSchema for SelectedFormatter {
- fn schema_name() -> String {
+ fn schema_name() -> Cow<'static, str> {
"Formatter".into()
}
- fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> Schema {
- let mut schema = SchemaObject::default();
+ fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
let formatter_schema = Formatter::json_schema(generator);
- schema.instance_type = Some(
- vec![
- InstanceType::Object,
- InstanceType::String,
- InstanceType::Array,
- ]
- .into(),
- );
-
- let valid_raw_values = SchemaObject {
- enum_values: Some(vec![
- Value::String("auto".into()),
- Value::String("prettier".into()),
- Value::String("language_server".into()),
- ]),
- ..Default::default()
- };
-
- let mut nested_values = SchemaObject::default();
- nested_values.array().items = Some(formatter_schema.clone().into());
-
- schema.subschemas().any_of = Some(vec![
- nested_values.into(),
- valid_raw_values.into(),
- formatter_schema,
- ]);
- schema.into()
+ json_schema!({
+ "oneOf": [
+ {
+ "type": "array",
+ "items": formatter_schema
+ },
+ {
+ "type": "string",
+ "enum": ["auto", "prettier", "language_server"]
+ },
+ formatter_schema
+ ]
+ })
}
}
@@ -836,6 +831,7 @@ impl Serialize for SelectedFormatter {
}
}
}
+
impl<'de> Deserialize<'de> for SelectedFormatter {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
@@ -856,11 +852,11 @@ impl<'de> Deserialize<'de> for SelectedFormatter {
if v == "auto" {
Ok(Self::Value::Auto)
} else if v == "language_server" {
- Ok(Self::Value::List(FormatterList(
- Formatter::LanguageServer { name: None }.into(),
- )))
+ Ok(Self::Value::List(vec![Formatter::LanguageServer {
+ name: None,
+ }]))
} else {
- let ret: Result<FormatterList, _> =
+ let ret: Result<Vec<Formatter>, _> =
Deserialize::deserialize(v.into_deserializer());
ret.map(SelectedFormatter::List)
}
@@ -869,7 +865,7 @@ impl<'de> Deserialize<'de> for SelectedFormatter {
where
A: MapAccess<'d>,
{
- let ret: Result<FormatterList, _> =
+ let ret: Result<Vec<Formatter>, _> =
Deserialize::deserialize(de::value::MapAccessDeserializer::new(map));
ret.map(SelectedFormatter::List)
}
@@ -877,7 +873,7 @@ impl<'de> Deserialize<'de> for SelectedFormatter {
where
A: SeqAccess<'d>,
{
- let ret: Result<FormatterList, _> =
+ let ret: Result<Vec<Formatter>, _> =
Deserialize::deserialize(de::value::SeqAccessDeserializer::new(map));
ret.map(SelectedFormatter::List)
}
@@ -885,19 +881,6 @@ impl<'de> Deserialize<'de> for SelectedFormatter {
deserializer.deserialize_any(FormatDeserializer)
}
}
-/// Controls which formatter should be used when formatting code.
-#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
-#[serde(rename_all = "snake_case", transparent)]
-pub struct FormatterList(pub SingleOrVec<Formatter>);
-
-impl AsRef<[Formatter]> for FormatterList {
- fn as_ref(&self) -> &[Formatter] {
- match &self.0 {
- SingleOrVec::Single(single) => slice::from_ref(single),
- SingleOrVec::Vec(v) => v,
- }
- }
-}
/// Controls which formatter should be used when formatting code. If there are multiple formatters, they are executed in the order of declaration.
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@@ -1209,7 +1192,7 @@ impl settings::Settings for AllLanguageSettings {
serde_json::from_value(serde_json::to_value(&default_value.defaults)?)?;
let mut languages = HashMap::default();
- for (language_name, settings) in &default_value.languages {
+ for (language_name, settings) in &default_value.languages.0 {
let mut language_settings = defaults.clone();
merge_settings(&mut language_settings, settings);
languages.insert(language_name.clone(), language_settings);
@@ -1310,7 +1293,7 @@ impl settings::Settings for AllLanguageSettings {
}
// A user's language-specific settings override default language-specific settings.
- for (language_name, user_language_settings) in &user_settings.languages {
+ for (language_name, user_language_settings) in &user_settings.languages.0 {
merge_settings(
languages
.entry(language_name.clone())
@@ -1366,51 +1349,6 @@ impl settings::Settings for AllLanguageSettings {
})
}
- fn json_schema(
- generator: &mut schemars::r#gen::SchemaGenerator,
- params: &settings::SettingsJsonSchemaParams,
- _: &App,
- ) -> schemars::schema::RootSchema {
- let mut root_schema = generator.root_schema_for::<Self::FileContent>();
-
- // Create a schema for a 'languages overrides' object, associating editor
- // settings with specific languages.
- assert!(
- root_schema
- .definitions
- .contains_key("LanguageSettingsContent")
- );
-
- let languages_object_schema = SchemaObject {
- instance_type: Some(InstanceType::Object.into()),
- object: Some(Box::new(ObjectValidation {
- properties: params
- .language_names
- .iter()
- .map(|name| {
- (
- name.clone(),
- Schema::new_ref("#/definitions/LanguageSettingsContent".into()),
- )
- })
- .collect(),
- ..Default::default()
- })),
- ..Default::default()
- };
-
- root_schema
- .definitions
- .extend([("Languages".into(), languages_object_schema.into())]);
-
- add_references_to_properties(
- &mut root_schema,
- &[("languages", "#/definitions/Languages")],
- );
-
- root_schema
- }
-
fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
let d = &mut current.defaults;
if let Some(size) = vscode
@@ -1674,29 +1612,26 @@ mod tests {
let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap();
assert_eq!(
settings.formatter,
- Some(SelectedFormatter::List(FormatterList(
- Formatter::LanguageServer { name: None }.into()
- )))
+ Some(SelectedFormatter::List(vec![Formatter::LanguageServer {
+ name: None
+ }]))
);
let raw = "{\"formatter\": [{\"language_server\": {\"name\": null}}]}";
let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap();
assert_eq!(
settings.formatter,
- Some(SelectedFormatter::List(FormatterList(
- vec![Formatter::LanguageServer { name: None }].into()
- )))
+ Some(SelectedFormatter::List(vec![Formatter::LanguageServer {
+ name: None
+ }]))
);
let raw = "{\"formatter\": [{\"language_server\": {\"name\": null}}, \"prettier\"]}";
let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap();
assert_eq!(
settings.formatter,
- Some(SelectedFormatter::List(FormatterList(
- vec![
- Formatter::LanguageServer { name: None },
- Formatter::Prettier
- ]
- .into()
- )))
+ Some(SelectedFormatter::List(vec![
+ Formatter::LanguageServer { name: None },
+ Formatter::Prettier
+ ]))
);
}
@@ -9,17 +9,18 @@ mod telemetry;
pub mod fake_provider;
use anthropic::{AnthropicError, parse_prompt_too_long};
-use anyhow::Result;
+use anyhow::{Result, anyhow};
use client::Client;
use futures::FutureExt;
use futures::{StreamExt, future::BoxFuture, stream::BoxStream};
use gpui::{AnyElement, AnyView, App, AsyncApp, SharedString, Task, Window};
-use http_client::http;
+use http_client::{StatusCode, http};
use icons::IconName;
use parking_lot::Mutex;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use std::ops::{Add, Sub};
+use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use std::{fmt, io};
@@ -34,11 +35,22 @@ pub use crate::request::*;
pub use crate::role::*;
pub use crate::telemetry::*;
-pub const ZED_CLOUD_PROVIDER_ID: &str = "zed.dev";
+pub const ANTHROPIC_PROVIDER_ID: LanguageModelProviderId =
+ LanguageModelProviderId::new("anthropic");
+pub const ANTHROPIC_PROVIDER_NAME: LanguageModelProviderName =
+ LanguageModelProviderName::new("Anthropic");
-/// If we get a rate limit error that doesn't tell us when we can retry,
-/// default to waiting this long before retrying.
-const DEFAULT_RATE_LIMIT_RETRY_AFTER: Duration = Duration::from_secs(4);
+pub const GOOGLE_PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("google");
+pub const GOOGLE_PROVIDER_NAME: LanguageModelProviderName =
+ LanguageModelProviderName::new("Google AI");
+
+pub const OPEN_AI_PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("openai");
+pub const OPEN_AI_PROVIDER_NAME: LanguageModelProviderName =
+ LanguageModelProviderName::new("OpenAI");
+
+pub const ZED_CLOUD_PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("zed.dev");
+pub const ZED_CLOUD_PROVIDER_NAME: LanguageModelProviderName =
+ LanguageModelProviderName::new("Zed");
pub fn init(client: Arc<Client>, cx: &mut App) {
init_settings(cx);
@@ -71,6 +83,12 @@ pub enum LanguageModelCompletionEvent {
data: String,
},
ToolUse(LanguageModelToolUse),
+ ToolUseJsonParseError {
+ id: LanguageModelToolUseId,
+ tool_name: Arc<str>,
+ raw_input: Arc<str>,
+ json_parse_error: String,
+ },
StartMessage {
message_id: String,
},
@@ -79,61 +97,179 @@ pub enum LanguageModelCompletionEvent {
#[derive(Error, Debug)]
pub enum LanguageModelCompletionError {
- #[error("rate limit exceeded, retry after {retry_after:?}")]
- RateLimitExceeded { retry_after: Duration },
- #[error("received bad input JSON")]
- BadInputJson {
- id: LanguageModelToolUseId,
- tool_name: Arc<str>,
- raw_input: Arc<str>,
- json_parse_error: String,
+ #[error("prompt too large for context window")]
+ PromptTooLarge { tokens: Option<u64> },
+ #[error("missing {provider} API key")]
+ NoApiKey { provider: LanguageModelProviderName },
+ #[error("{provider}'s API rate limit exceeded")]
+ RateLimitExceeded {
+ provider: LanguageModelProviderName,
+ retry_after: Option<Duration>,
+ },
+ #[error("{provider}'s API servers are overloaded right now")]
+ ServerOverloaded {
+ provider: LanguageModelProviderName,
+ retry_after: Option<Duration>,
+ },
+ #[error("{provider}'s API server reported an internal server error: {message}")]
+ ApiInternalServerError {
+ provider: LanguageModelProviderName,
+ message: String,
+ },
+ #[error("HTTP response error from {provider}'s API: status {status_code} - {message:?}")]
+ HttpResponseError {
+ provider: LanguageModelProviderName,
+ status_code: StatusCode,
+ message: String,
+ },
+
+ // Client errors
+ #[error("invalid request format to {provider}'s API: {message}")]
+ BadRequestFormat {
+ provider: LanguageModelProviderName,
+ message: String,
},
- #[error("language model provider's API is overloaded")]
- Overloaded,
+ #[error("authentication error with {provider}'s API: {message}")]
+ AuthenticationError {
+ provider: LanguageModelProviderName,
+ message: String,
+ },
+ #[error("permission error with {provider}'s API: {message}")]
+ PermissionError {
+ provider: LanguageModelProviderName,
+ message: String,
+ },
+ #[error("language model provider API endpoint not found")]
+ ApiEndpointNotFound { provider: LanguageModelProviderName },
+ #[error("I/O error reading response from {provider}'s API")]
+ ApiReadResponseError {
+ provider: LanguageModelProviderName,
+ #[source]
+ error: io::Error,
+ },
+ #[error("error serializing request to {provider} API")]
+ SerializeRequest {
+ provider: LanguageModelProviderName,
+ #[source]
+ error: serde_json::Error,
+ },
+ #[error("error building request body to {provider} API")]
+ BuildRequestBody {
+ provider: LanguageModelProviderName,
+ #[source]
+ error: http::Error,
+ },
+ #[error("error sending HTTP request to {provider} API")]
+ HttpSend {
+ provider: LanguageModelProviderName,
+ #[source]
+ error: anyhow::Error,
+ },
+ #[error("error deserializing {provider} API response")]
+ DeserializeResponse {
+ provider: LanguageModelProviderName,
+ #[source]
+ error: serde_json::Error,
+ },
+
+ // TODO: Ideally this would be removed in favor of having a comprehensive list of errors.
#[error(transparent)]
Other(#[from] anyhow::Error),
- #[error("invalid request format to language model provider's API")]
- BadRequestFormat,
- #[error("authentication error with language model provider's API")]
- AuthenticationError,
- #[error("permission error with language model provider's API")]
- PermissionError,
- #[error("language model provider API endpoint not found")]
- ApiEndpointNotFound,
- #[error("prompt too large for context window")]
- PromptTooLarge { tokens: Option<u64> },
- #[error("internal server error in language model provider's API")]
- ApiInternalServerError,
- #[error("I/O error reading response from language model provider's API: {0:?}")]
- ApiReadResponseError(io::Error),
- #[error("HTTP response error from language model provider's API: status {status} - {body:?}")]
- HttpResponseError { status: u16, body: String },
- #[error("error serializing request to language model provider API: {0}")]
- SerializeRequest(serde_json::Error),
- #[error("error building request body to language model provider API: {0}")]
- BuildRequestBody(http::Error),
- #[error("error sending HTTP request to language model provider API: {0}")]
- HttpSend(anyhow::Error),
- #[error("error deserializing language model provider API response: {0}")]
- DeserializeResponse(serde_json::Error),
- #[error("unexpected language model provider API response format: {0}")]
- UnknownResponseFormat(String),
+}
+
+impl LanguageModelCompletionError {
+ pub fn from_cloud_failure(
+ upstream_provider: LanguageModelProviderName,
+ code: String,
+ message: String,
+ retry_after: Option<Duration>,
+ ) -> Self {
+ if let Some(tokens) = parse_prompt_too_long(&message) {
+ // TODO: currently Anthropic PAYLOAD_TOO_LARGE response may cause INTERNAL_SERVER_ERROR
+ // to be reported. This is a temporary workaround to handle this in the case where the
+ // token limit has been exceeded.
+ Self::PromptTooLarge {
+ tokens: Some(tokens),
+ }
+ } else if let Some(status_code) = code
+ .strip_prefix("upstream_http_")
+ .and_then(|code| StatusCode::from_str(code).ok())
+ {
+ Self::from_http_status(upstream_provider, status_code, message, retry_after)
+ } else if let Some(status_code) = code
+ .strip_prefix("http_")
+ .and_then(|code| StatusCode::from_str(code).ok())
+ {
+ Self::from_http_status(ZED_CLOUD_PROVIDER_NAME, status_code, message, retry_after)
+ } else {
+ anyhow!("completion request failed, code: {code}, message: {message}").into()
+ }
+ }
+
+ pub fn from_http_status(
+ provider: LanguageModelProviderName,
+ status_code: StatusCode,
+ message: String,
+ retry_after: Option<Duration>,
+ ) -> Self {
+ match status_code {
+ StatusCode::BAD_REQUEST => Self::BadRequestFormat { provider, message },
+ StatusCode::UNAUTHORIZED => Self::AuthenticationError { provider, message },
+ StatusCode::FORBIDDEN => Self::PermissionError { provider, message },
+ StatusCode::NOT_FOUND => Self::ApiEndpointNotFound { provider },
+ StatusCode::PAYLOAD_TOO_LARGE => Self::PromptTooLarge {
+ tokens: parse_prompt_too_long(&message),
+ },
+ StatusCode::TOO_MANY_REQUESTS => Self::RateLimitExceeded {
+ provider,
+ retry_after,
+ },
+ StatusCode::INTERNAL_SERVER_ERROR => Self::ApiInternalServerError { provider, message },
+ StatusCode::SERVICE_UNAVAILABLE => Self::ServerOverloaded {
+ provider,
+ retry_after,
+ },
+ _ if status_code.as_u16() == 529 => Self::ServerOverloaded {
+ provider,
+ retry_after,
+ },
+ _ => Self::HttpResponseError {
+ provider,
+ status_code,
+ message,
+ },
+ }
+ }
}
impl From<AnthropicError> for LanguageModelCompletionError {
fn from(error: AnthropicError) -> Self {
+ let provider = ANTHROPIC_PROVIDER_NAME;
match error {
- AnthropicError::SerializeRequest(error) => Self::SerializeRequest(error),
- AnthropicError::BuildRequestBody(error) => Self::BuildRequestBody(error),
- AnthropicError::HttpSend(error) => Self::HttpSend(error),
- AnthropicError::DeserializeResponse(error) => Self::DeserializeResponse(error),
- AnthropicError::ReadResponse(error) => Self::ApiReadResponseError(error),
- AnthropicError::HttpResponseError { status, body } => {
- Self::HttpResponseError { status, body }
+ AnthropicError::SerializeRequest(error) => Self::SerializeRequest { provider, error },
+ AnthropicError::BuildRequestBody(error) => Self::BuildRequestBody { provider, error },
+ AnthropicError::HttpSend(error) => Self::HttpSend { provider, error },
+ AnthropicError::DeserializeResponse(error) => {
+ Self::DeserializeResponse { provider, error }
}
- AnthropicError::RateLimit { retry_after } => Self::RateLimitExceeded { retry_after },
+ AnthropicError::ReadResponse(error) => Self::ApiReadResponseError { provider, error },
+ AnthropicError::HttpResponseError {
+ status_code,
+ message,
+ } => Self::HttpResponseError {
+ provider,
+ status_code,
+ message,
+ },
+ AnthropicError::RateLimit { retry_after } => Self::RateLimitExceeded {
+ provider,
+ retry_after: Some(retry_after),
+ },
+ AnthropicError::ServerOverloaded { retry_after } => Self::ServerOverloaded {
+ provider,
+ retry_after: retry_after,
+ },
AnthropicError::ApiError(api_error) => api_error.into(),
- AnthropicError::UnexpectedResponseFormat(error) => Self::UnknownResponseFormat(error),
}
}
}
@@ -141,23 +277,39 @@ impl From<AnthropicError> for LanguageModelCompletionError {
impl From<anthropic::ApiError> for LanguageModelCompletionError {
fn from(error: anthropic::ApiError) -> Self {
use anthropic::ApiErrorCode::*;
-
+ let provider = ANTHROPIC_PROVIDER_NAME;
match error.code() {
Some(code) => match code {
- InvalidRequestError => LanguageModelCompletionError::BadRequestFormat,
- AuthenticationError => LanguageModelCompletionError::AuthenticationError,
- PermissionError => LanguageModelCompletionError::PermissionError,
- NotFoundError => LanguageModelCompletionError::ApiEndpointNotFound,
- RequestTooLarge => LanguageModelCompletionError::PromptTooLarge {
+ InvalidRequestError => Self::BadRequestFormat {
+ provider,
+ message: error.message,
+ },
+ AuthenticationError => Self::AuthenticationError {
+ provider,
+ message: error.message,
+ },
+ PermissionError => Self::PermissionError {
+ provider,
+ message: error.message,
+ },
+ NotFoundError => Self::ApiEndpointNotFound { provider },
+ RequestTooLarge => Self::PromptTooLarge {
tokens: parse_prompt_too_long(&error.message),
},
- RateLimitError => LanguageModelCompletionError::RateLimitExceeded {
- retry_after: DEFAULT_RATE_LIMIT_RETRY_AFTER,
+ RateLimitError => Self::RateLimitExceeded {
+ provider,
+ retry_after: None,
+ },
+ ApiError => Self::ApiInternalServerError {
+ provider,
+ message: error.message,
+ },
+ OverloadedError => Self::ServerOverloaded {
+ provider,
+ retry_after: None,
},
- ApiError => LanguageModelCompletionError::ApiInternalServerError,
- OverloadedError => LanguageModelCompletionError::Overloaded,
},
- None => LanguageModelCompletionError::Other(error.into()),
+ None => Self::Other(error.into()),
}
}
}
@@ -278,6 +430,13 @@ pub trait LanguageModel: Send + Sync {
fn name(&self) -> LanguageModelName;
fn provider_id(&self) -> LanguageModelProviderId;
fn provider_name(&self) -> LanguageModelProviderName;
+ fn upstream_provider_id(&self) -> LanguageModelProviderId {
+ self.provider_id()
+ }
+ fn upstream_provider_name(&self) -> LanguageModelProviderName {
+ self.provider_name()
+ }
+
fn telemetry_id(&self) -> String;
fn api_key(&self, _cx: &App) -> Option<String> {
@@ -294,7 +453,7 @@ pub trait LanguageModel: Send + Sync {
fn supports_tool_choice(&self, choice: LanguageModelToolChoice) -> bool;
/// Returns whether this model supports "burn mode";
- fn supports_max_mode(&self) -> bool {
+ fn supports_burn_mode(&self) -> bool {
false
}
@@ -365,6 +524,9 @@ pub trait LanguageModel: Send + Sync {
Ok(LanguageModelCompletionEvent::RedactedThinking { .. }) => None,
Ok(LanguageModelCompletionEvent::Stop(_)) => None,
Ok(LanguageModelCompletionEvent::ToolUse(_)) => None,
+ Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
+ ..
+ }) => None,
Ok(LanguageModelCompletionEvent::UsageUpdate(token_usage)) => {
*last_token_usage.lock() = token_usage;
None
@@ -395,39 +557,6 @@ pub trait LanguageModel: Send + Sync {
}
}
-#[derive(Debug, Error)]
-pub enum LanguageModelKnownError {
- #[error("Context window limit exceeded ({tokens})")]
- ContextWindowLimitExceeded { tokens: u64 },
- #[error("Language model provider's API is currently overloaded")]
- Overloaded,
- #[error("Language model provider's API encountered an internal server error")]
- ApiInternalServerError,
- #[error("I/O error while reading response from language model provider's API: {0:?}")]
- ReadResponseError(io::Error),
- #[error("Error deserializing response from language model provider's API: {0:?}")]
- DeserializeResponse(serde_json::Error),
- #[error("Language model provider's API returned a response in an unknown format")]
- UnknownResponseFormat(String),
- #[error("Rate limit exceeded for language model provider's API; retry in {retry_after:?}")]
- RateLimitExceeded { retry_after: Duration },
-}
-
-impl LanguageModelKnownError {
- /// Attempts to map an HTTP response status code to a known error type.
- /// Returns None if the status code doesn't map to a specific known error.
- pub fn from_http_response(status: u16, _body: &str) -> Option<Self> {
- match status {
- 429 => Some(Self::RateLimitExceeded {
- retry_after: DEFAULT_RATE_LIMIT_RETRY_AFTER,
- }),
- 503 => Some(Self::Overloaded),
- 500..=599 => Some(Self::ApiInternalServerError),
- _ => None,
- }
- }
-}
-
pub trait LanguageModelTool: 'static + DeserializeOwned + JsonSchema {
fn name() -> String;
fn description() -> String;
@@ -473,7 +602,7 @@ pub trait LanguageModelProvider: 'static {
#[derive(PartialEq, Eq)]
pub enum LanguageModelProviderTosView {
/// When there are some past interactions in the Agent Panel.
- ThreadtEmptyState,
+ ThreadEmptyState,
/// When there are no past interactions in the Agent Panel.
ThreadFreshStart,
PromptEditorPopup,
@@ -509,12 +638,30 @@ pub struct LanguageModelProviderId(pub SharedString);
#[derive(Clone, Eq, PartialEq, Hash, Debug, Ord, PartialOrd)]
pub struct LanguageModelProviderName(pub SharedString);
+impl LanguageModelProviderId {
+ pub const fn new(id: &'static str) -> Self {
+ Self(SharedString::new_static(id))
+ }
+}
+
+impl LanguageModelProviderName {
+ pub const fn new(id: &'static str) -> Self {
+ Self(SharedString::new_static(id))
+ }
+}
+
impl fmt::Display for LanguageModelProviderId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
+impl fmt::Display for LanguageModelProviderName {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}", self.0)
+ }
+}
+
impl From<String> for LanguageModelId {
fn from(value: String) -> Self {
Self(SharedString::from(value))
@@ -98,7 +98,7 @@ impl ConfiguredModel {
}
pub fn is_provided_by_zed(&self) -> bool {
- self.provider.id().0 == crate::ZED_CLOUD_PROVIDER_ID
+ self.provider.id() == crate::ZED_CLOUD_PROVIDER_ID
}
}
@@ -1,3 +1,4 @@
+use crate::ANTHROPIC_PROVIDER_ID;
use anthropic::ANTHROPIC_API_URL;
use anyhow::{Context as _, anyhow};
use client::telemetry::Telemetry;
@@ -8,8 +9,6 @@ use std::sync::Arc;
use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
use util::ResultExt;
-pub const ANTHROPIC_PROVIDER_ID: &str = "anthropic";
-
pub fn report_assistant_event(
event: AssistantEventData,
telemetry: Option<Arc<Telemetry>>,
@@ -19,7 +18,7 @@ pub fn report_assistant_event(
) {
if let Some(telemetry) = telemetry.as_ref() {
telemetry.report_assistant_event(event.clone());
- if telemetry.metrics_enabled() && event.model_provider == ANTHROPIC_PROVIDER_ID {
+ if telemetry.metrics_enabled() && event.model_provider == ANTHROPIC_PROVIDER_ID.0 {
if let Some(api_key) = model_api_key {
executor
.spawn(async move {
@@ -20,8 +20,10 @@ aws-credential-types = { workspace = true, features = [
] }
aws_http_client.workspace = true
bedrock.workspace = true
+chrono.workspace = true
client.workspace = true
collections.workspace = true
+component.workspace = true
credentials_provider.workspace = true
copilot.workspace = true
deepseek = { workspace = true, features = ["schemars"] }
@@ -33,8 +33,8 @@ use theme::ThemeSettings;
use ui::{Icon, IconName, List, Tooltip, prelude::*};
use util::ResultExt;
-const PROVIDER_ID: &str = language_model::ANTHROPIC_PROVIDER_ID;
-const PROVIDER_NAME: &str = "Anthropic";
+const PROVIDER_ID: LanguageModelProviderId = language_model::ANTHROPIC_PROVIDER_ID;
+const PROVIDER_NAME: LanguageModelProviderName = language_model::ANTHROPIC_PROVIDER_NAME;
#[derive(Default, Clone, Debug, PartialEq)]
pub struct AnthropicSettings {
@@ -218,11 +218,11 @@ impl LanguageModelProviderState for AnthropicLanguageModelProvider {
impl LanguageModelProvider for AnthropicLanguageModelProvider {
fn id(&self) -> LanguageModelProviderId {
- LanguageModelProviderId(PROVIDER_ID.into())
+ PROVIDER_ID
}
fn name(&self) -> LanguageModelProviderName {
- LanguageModelProviderName(PROVIDER_NAME.into())
+ PROVIDER_NAME
}
fn icon(&self) -> IconName {
@@ -403,7 +403,11 @@ impl AnthropicModel {
};
async move {
- let api_key = api_key.context("Missing Anthropic API Key")?;
+ let Some(api_key) = api_key else {
+ return Err(LanguageModelCompletionError::NoApiKey {
+ provider: PROVIDER_NAME,
+ });
+ };
let request =
anthropic::stream_completion(http_client.as_ref(), &api_url, &api_key, request);
request.await.map_err(Into::into)
@@ -422,11 +426,11 @@ impl LanguageModel for AnthropicModel {
}
fn provider_id(&self) -> LanguageModelProviderId {
- LanguageModelProviderId(PROVIDER_ID.into())
+ PROVIDER_ID
}
fn provider_name(&self) -> LanguageModelProviderName {
- LanguageModelProviderName(PROVIDER_NAME.into())
+ PROVIDER_NAME
}
fn supports_tools(&self) -> bool {
@@ -528,6 +532,11 @@ pub fn into_anthropic(
.into_iter()
.filter_map(|content| match content {
MessageContent::Text(text) => {
+ let text = if text.chars().last().map_or(false, |c| c.is_whitespace()) {
+ text.trim_end().to_string()
+ } else {
+ text
+ };
if !text.is_empty() {
Some(anthropic::RequestContent::Text {
text,
@@ -801,12 +810,14 @@ impl AnthropicEventMapper {
raw_input: tool_use.input_json.clone(),
},
)),
- Err(json_parse_err) => Err(LanguageModelCompletionError::BadInputJson {
- id: tool_use.id.into(),
- tool_name: tool_use.name.into(),
- raw_input: input_json.into(),
- json_parse_error: json_parse_err.to_string(),
- }),
+ Err(json_parse_err) => {
+ Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
+ id: tool_use.id.into(),
+ tool_name: tool_use.name.into(),
+ raw_input: input_json.into(),
+ json_parse_error: json_parse_err.to_string(),
+ })
+ }
};
vec![event_result]
@@ -46,14 +46,13 @@ use settings::{Settings, SettingsStore};
use smol::lock::OnceCell;
use strum::{EnumIter, IntoEnumIterator, IntoStaticStr};
use theme::ThemeSettings;
-use tokio::runtime::Handle;
use ui::{Icon, IconName, List, Tooltip, prelude::*};
use util::ResultExt;
use crate::AllLanguageModelSettings;
-const PROVIDER_ID: &str = "amazon-bedrock";
-const PROVIDER_NAME: &str = "Amazon Bedrock";
+const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("amazon-bedrock");
+const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("Amazon Bedrock");
#[derive(Default, Clone, Deserialize, Serialize, PartialEq, Debug)]
pub struct BedrockCredentials {
@@ -285,11 +284,11 @@ impl BedrockLanguageModelProvider {
impl LanguageModelProvider for BedrockLanguageModelProvider {
fn id(&self) -> LanguageModelProviderId {
- LanguageModelProviderId(PROVIDER_ID.into())
+ PROVIDER_ID
}
fn name(&self) -> LanguageModelProviderName {
- LanguageModelProviderName(PROVIDER_NAME.into())
+ PROVIDER_NAME
}
fn icon(&self) -> IconName {
@@ -460,22 +459,22 @@ impl BedrockModel {
&self,
request: bedrock::Request,
cx: &AsyncApp,
- ) -> Result<
- BoxFuture<'static, BoxStream<'static, Result<BedrockStreamingResponse, BedrockError>>>,
+ ) -> BoxFuture<
+ 'static,
+ Result<BoxStream<'static, Result<BedrockStreamingResponse, BedrockError>>>,
> {
- let runtime_client = self
- .get_or_init_client(cx)
+ let Ok(runtime_client) = self
+ .get_or_init_client(&cx)
.cloned()
- .context("Bedrock client not initialized")?;
- let owned_handle = self.handler.clone();
+ .context("Bedrock client not initialized")
+ else {
+ return futures::future::ready(Err(anyhow!("App state dropped"))).boxed();
+ };
- Ok(async move {
- let request = bedrock::stream_completion(runtime_client, request, owned_handle);
- request.await.unwrap_or_else(|e| {
- futures::stream::once(async move { Err(BedrockError::ClientError(e)) }).boxed()
- })
+ match Tokio::spawn(cx, bedrock::stream_completion(runtime_client, request)) {
+ Ok(res) => async { res.await.map_err(|err| anyhow!(err))? }.boxed(),
+ Err(err) => futures::future::ready(Err(anyhow!(err))).boxed(),
}
- .boxed())
}
}
@@ -489,11 +488,11 @@ impl LanguageModel for BedrockModel {
}
fn provider_id(&self) -> LanguageModelProviderId {
- LanguageModelProviderId(PROVIDER_ID.into())
+ PROVIDER_ID
}
fn provider_name(&self) -> LanguageModelProviderName {
- LanguageModelProviderName(PROVIDER_NAME.into())
+ PROVIDER_NAME
}
fn supports_tools(&self) -> bool {
@@ -570,12 +569,10 @@ impl LanguageModel for BedrockModel {
Err(err) => return futures::future::ready(Err(err.into())).boxed(),
};
- let owned_handle = self.handler.clone();
-
let request = self.stream_completion(request, cx);
let future = self.request_limiter.stream(async move {
- let response = request.map_err(|err| anyhow!(err))?.await;
- let events = map_to_language_model_completion_events(response, owned_handle);
+ let response = request.await.map_err(|err| anyhow!(err))?;
+ let events = map_to_language_model_completion_events(response);
if deny_tool_calls {
Ok(deny_tool_use_events(events).boxed())
@@ -879,7 +876,6 @@ pub fn get_bedrock_tokens(
pub fn map_to_language_model_completion_events(
events: Pin<Box<dyn Send + Stream<Item = Result<BedrockStreamingResponse, BedrockError>>>>,
- handle: Handle,
) -> impl Stream<Item = Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
struct RawToolUse {
id: String,
@@ -892,198 +888,123 @@ pub fn map_to_language_model_completion_events(
tool_uses_by_index: HashMap<i32, RawToolUse>,
}
- futures::stream::unfold(
- State {
- events,
- tool_uses_by_index: HashMap::default(),
- },
- move |mut state: State| {
- let inner_handle = handle.clone();
- async move {
- inner_handle
- .spawn(async {
- while let Some(event) = state.events.next().await {
- match event {
- Ok(event) => match event {
- ConverseStreamOutput::ContentBlockDelta(cb_delta) => {
- match cb_delta.delta {
- Some(ContentBlockDelta::Text(text_out)) => {
- let completion_event =
- LanguageModelCompletionEvent::Text(text_out);
- return Some((Some(Ok(completion_event)), state));
- }
-
- Some(ContentBlockDelta::ToolUse(text_out)) => {
- if let Some(tool_use) = state
- .tool_uses_by_index
- .get_mut(&cb_delta.content_block_index)
- {
- tool_use.input_json.push_str(text_out.input());
- }
- }
-
- Some(ContentBlockDelta::ReasoningContent(thinking)) => {
- match thinking {
- ReasoningContentBlockDelta::RedactedContent(
- redacted,
- ) => {
- let thinking_event =
- LanguageModelCompletionEvent::Thinking {
- text: String::from_utf8(
- redacted.into_inner(),
- )
- .unwrap_or("REDACTED".to_string()),
- signature: None,
- };
-
- return Some((
- Some(Ok(thinking_event)),
- state,
- ));
- }
- ReasoningContentBlockDelta::Signature(
- signature,
- ) => {
- return Some((
- Some(Ok(LanguageModelCompletionEvent::Thinking {
- text: "".to_string(),
- signature: Some(signature)
- })),
- state,
- ));
- }
- ReasoningContentBlockDelta::Text(thoughts) => {
- let thinking_event =
- LanguageModelCompletionEvent::Thinking {
- text: thoughts.to_string(),
- signature: None
- };
-
- return Some((
- Some(Ok(thinking_event)),
- state,
- ));
- }
- _ => {}
- }
- }
- _ => {}
- }
- }
- ConverseStreamOutput::ContentBlockStart(cb_start) => {
- if let Some(ContentBlockStart::ToolUse(text_out)) =
- cb_start.start
- {
- let tool_use = RawToolUse {
- id: text_out.tool_use_id,
- name: text_out.name,
- input_json: String::new(),
- };
-
- state
- .tool_uses_by_index
- .insert(cb_start.content_block_index, tool_use);
- }
- }
- ConverseStreamOutput::ContentBlockStop(cb_stop) => {
- if let Some(tool_use) = state
- .tool_uses_by_index
- .remove(&cb_stop.content_block_index)
- {
- let tool_use_event = LanguageModelToolUse {
- id: tool_use.id.into(),
- name: tool_use.name.into(),
- is_input_complete: true,
- raw_input: tool_use.input_json.clone(),
- input: if tool_use.input_json.is_empty() {
- Value::Null
- } else {
- serde_json::Value::from_str(
- &tool_use.input_json,
- )
- .map_err(|err| anyhow!(err))
- .unwrap()
- },
- };
-
- return Some((
- Some(Ok(LanguageModelCompletionEvent::ToolUse(
- tool_use_event,
- ))),
- state,
- ));
- }
- }
-
- ConverseStreamOutput::Metadata(cb_meta) => {
- if let Some(metadata) = cb_meta.usage {
- let completion_event =
- LanguageModelCompletionEvent::UsageUpdate(
- TokenUsage {
- input_tokens: metadata.input_tokens as u64,
- output_tokens: metadata.output_tokens as u64,
- cache_creation_input_tokens:
- metadata.cache_write_input_tokens.unwrap_or_default() as u64,
- cache_read_input_tokens:
- metadata.cache_read_input_tokens.unwrap_or_default() as u64,
- },
- );
- return Some((Some(Ok(completion_event)), state));
- }
- }
- ConverseStreamOutput::MessageStop(message_stop) => {
- let reason = match message_stop.stop_reason {
- StopReason::ContentFiltered => {
- LanguageModelCompletionEvent::Stop(
- language_model::StopReason::EndTurn,
- )
- }
- StopReason::EndTurn => {
- LanguageModelCompletionEvent::Stop(
- language_model::StopReason::EndTurn,
- )
- }
- StopReason::GuardrailIntervened => {
- LanguageModelCompletionEvent::Stop(
- language_model::StopReason::EndTurn,
- )
- }
- StopReason::MaxTokens => {
- LanguageModelCompletionEvent::Stop(
- language_model::StopReason::EndTurn,
- )
- }
- StopReason::StopSequence => {
- LanguageModelCompletionEvent::Stop(
- language_model::StopReason::EndTurn,
- )
- }
- StopReason::ToolUse => {
- LanguageModelCompletionEvent::Stop(
- language_model::StopReason::ToolUse,
- )
- }
- _ => LanguageModelCompletionEvent::Stop(
- language_model::StopReason::EndTurn,
- ),
- };
- return Some((Some(Ok(reason)), state));
- }
- _ => {}
- },
+ let initial_state = State {
+ events,
+ tool_uses_by_index: HashMap::default(),
+ };
- Err(err) => return Some((Some(Err(anyhow!(err).into())), state)),
+ futures::stream::unfold(initial_state, |mut state| async move {
+ match state.events.next().await {
+ Some(event_result) => match event_result {
+ Ok(event) => {
+ let result = match event {
+ ConverseStreamOutput::ContentBlockDelta(cb_delta) => match cb_delta.delta {
+ Some(ContentBlockDelta::Text(text)) => {
+ Some(Ok(LanguageModelCompletionEvent::Text(text)))
+ }
+ Some(ContentBlockDelta::ToolUse(tool_output)) => {
+ if let Some(tool_use) = state
+ .tool_uses_by_index
+ .get_mut(&cb_delta.content_block_index)
+ {
+ tool_use.input_json.push_str(tool_output.input());
+ }
+ None
}
+ Some(ContentBlockDelta::ReasoningContent(thinking)) => match thinking {
+ ReasoningContentBlockDelta::Text(thoughts) => {
+ Some(Ok(LanguageModelCompletionEvent::Thinking {
+ text: thoughts.clone(),
+ signature: None,
+ }))
+ }
+ ReasoningContentBlockDelta::Signature(sig) => {
+ Some(Ok(LanguageModelCompletionEvent::Thinking {
+ text: "".into(),
+ signature: Some(sig),
+ }))
+ }
+ ReasoningContentBlockDelta::RedactedContent(redacted) => {
+ let content = String::from_utf8(redacted.into_inner())
+ .unwrap_or("REDACTED".to_string());
+ Some(Ok(LanguageModelCompletionEvent::Thinking {
+ text: content,
+ signature: None,
+ }))
+ }
+ _ => None,
+ },
+ _ => None,
+ },
+ ConverseStreamOutput::ContentBlockStart(cb_start) => {
+ if let Some(ContentBlockStart::ToolUse(tool_start)) = cb_start.start {
+ state.tool_uses_by_index.insert(
+ cb_start.content_block_index,
+ RawToolUse {
+ id: tool_start.tool_use_id,
+ name: tool_start.name,
+ input_json: String::new(),
+ },
+ );
+ }
+ None
}
- None
- })
- .await
- .log_err()
- .flatten()
- }
- },
- )
- .filter_map(|event| async move { event })
+ ConverseStreamOutput::ContentBlockStop(cb_stop) => state
+ .tool_uses_by_index
+ .remove(&cb_stop.content_block_index)
+ .map(|tool_use| {
+ let input = if tool_use.input_json.is_empty() {
+ Value::Null
+ } else {
+ serde_json::Value::from_str(&tool_use.input_json)
+ .unwrap_or(Value::Null)
+ };
+
+ Ok(LanguageModelCompletionEvent::ToolUse(
+ LanguageModelToolUse {
+ id: tool_use.id.into(),
+ name: tool_use.name.into(),
+ is_input_complete: true,
+ raw_input: tool_use.input_json.clone(),
+ input,
+ },
+ ))
+ }),
+ ConverseStreamOutput::Metadata(cb_meta) => cb_meta.usage.map(|metadata| {
+ Ok(LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
+ input_tokens: metadata.input_tokens as u64,
+ output_tokens: metadata.output_tokens as u64,
+ cache_creation_input_tokens: metadata
+ .cache_write_input_tokens
+ .unwrap_or_default()
+ as u64,
+ cache_read_input_tokens: metadata
+ .cache_read_input_tokens
+ .unwrap_or_default()
+ as u64,
+ }))
+ }),
+ ConverseStreamOutput::MessageStop(message_stop) => {
+ let stop_reason = match message_stop.stop_reason {
+ StopReason::ToolUse => language_model::StopReason::ToolUse,
+ _ => language_model::StopReason::EndTurn,
+ };
+ Some(Ok(LanguageModelCompletionEvent::Stop(stop_reason)))
+ }
+ _ => None,
+ };
+
+ Some((result, state))
+ }
+ Err(err) => Some((
+ Some(Err(LanguageModelCompletionError::Other(anyhow!(err)))),
+ state,
+ )),
+ },
+ None => None,
+ }
+ })
+ .filter_map(|result| async move { result })
}
struct ConfigurationView {
@@ -1,5 +1,6 @@
-use anthropic::{AnthropicModelMode, parse_prompt_too_long};
+use anthropic::AnthropicModelMode;
use anyhow::{Context as _, Result, anyhow};
+use chrono::{DateTime, Utc};
use client::{Client, ModelRequestUsage, UserStore, zed_urls};
use futures::{
AsyncBufReadExt, FutureExt, Stream, StreamExt, future::BoxFuture, stream::BoxStream,
@@ -8,25 +9,21 @@ use google_ai::GoogleModelMode;
use gpui::{
AnyElement, AnyView, App, AsyncApp, Context, Entity, SemanticVersion, Subscription, Task,
};
+use http_client::http::{HeaderMap, HeaderValue};
use http_client::{AsyncBody, HttpClient, Method, Response, StatusCode};
use language_model::{
AuthenticateError, LanguageModel, LanguageModelCacheConfiguration,
- LanguageModelCompletionError, LanguageModelId, LanguageModelKnownError, LanguageModelName,
- LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
- LanguageModelProviderTosView, LanguageModelRequest, LanguageModelToolChoice,
- LanguageModelToolSchemaFormat, ModelRequestLimitReachedError, RateLimiter,
- ZED_CLOUD_PROVIDER_ID,
-};
-use language_model::{
- LanguageModelCompletionEvent, LanguageModelProvider, LlmApiToken, PaymentRequiredError,
- RefreshLlmTokenListener,
+ LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelId, LanguageModelName,
+ LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
+ LanguageModelProviderState, LanguageModelProviderTosView, LanguageModelRequest,
+ LanguageModelToolChoice, LanguageModelToolSchemaFormat, LlmApiToken,
+ ModelRequestLimitReachedError, PaymentRequiredError, RateLimiter, RefreshLlmTokenListener,
};
use proto::Plan;
use release_channel::AppVersion;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use settings::SettingsStore;
-use smol::Timer;
use smol::io::{AsyncReadExt, BufReader};
use std::pin::Pin;
use std::str::FromStr as _;
@@ -47,7 +44,8 @@ use crate::provider::anthropic::{AnthropicEventMapper, count_anthropic_tokens, i
use crate::provider::google::{GoogleEventMapper, into_google};
use crate::provider::open_ai::{OpenAiEventMapper, count_open_ai_tokens, into_open_ai};
-pub const PROVIDER_NAME: &str = "Zed";
+const PROVIDER_ID: LanguageModelProviderId = language_model::ZED_CLOUD_PROVIDER_ID;
+const PROVIDER_NAME: LanguageModelProviderName = language_model::ZED_CLOUD_PROVIDER_NAME;
#[derive(Default, Clone, Debug, PartialEq)]
pub struct ZedDotDevSettings {
@@ -120,7 +118,7 @@ pub struct State {
llm_api_token: LlmApiToken,
user_store: Entity<UserStore>,
status: client::Status,
- accept_terms: Option<Task<Result<()>>>,
+ accept_terms_of_service_task: Option<Task<Result<()>>>,
models: Vec<Arc<zed_llm_client::LanguageModel>>,
default_model: Option<Arc<zed_llm_client::LanguageModel>>,
default_fast_model: Option<Arc<zed_llm_client::LanguageModel>>,
@@ -144,7 +142,7 @@ impl State {
llm_api_token: LlmApiToken::default(),
user_store,
status,
- accept_terms: None,
+ accept_terms_of_service_task: None,
models: Vec::new(),
default_model: None,
default_fast_model: None,
@@ -253,12 +251,12 @@ impl State {
fn accept_terms_of_service(&mut self, cx: &mut Context<Self>) {
let user_store = self.user_store.clone();
- self.accept_terms = Some(cx.spawn(async move |this, cx| {
+ self.accept_terms_of_service_task = Some(cx.spawn(async move |this, cx| {
let _ = user_store
.update(cx, |store, cx| store.accept_terms_of_service(cx))?
.await;
this.update(cx, |this, cx| {
- this.accept_terms = None;
+ this.accept_terms_of_service_task = None;
cx.notify()
})
}));
@@ -351,11 +349,11 @@ impl LanguageModelProviderState for CloudLanguageModelProvider {
impl LanguageModelProvider for CloudLanguageModelProvider {
fn id(&self) -> LanguageModelProviderId {
- LanguageModelProviderId(ZED_CLOUD_PROVIDER_ID.into())
+ PROVIDER_ID
}
fn name(&self) -> LanguageModelProviderName {
- LanguageModelProviderName(PROVIDER_NAME.into())
+ PROVIDER_NAME
}
fn icon(&self) -> IconName {
@@ -397,7 +395,8 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
}
fn is_authenticated(&self, cx: &App) -> bool {
- !self.state.read(cx).is_signed_out()
+ let state = self.state.read(cx);
+ !state.is_signed_out() && state.has_accepted_terms_of_service(cx)
}
fn authenticate(&self, _cx: &mut App) -> Task<Result<(), AuthenticateError>> {
@@ -405,10 +404,8 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
}
fn configuration_view(&self, _: &mut Window, cx: &mut App) -> AnyView {
- cx.new(|_| ConfigurationView {
- state: self.state.clone(),
- })
- .into()
+ cx.new(|_| ConfigurationView::new(self.state.clone()))
+ .into()
}
fn must_accept_terms(&self, cx: &App) -> bool {
@@ -420,7 +417,19 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
view: LanguageModelProviderTosView,
cx: &mut App,
) -> Option<AnyElement> {
- render_accept_terms(self.state.clone(), view, cx)
+ let state = self.state.read(cx);
+ if state.has_accepted_terms_of_service(cx) {
+ return None;
+ }
+ Some(
+ render_accept_terms(view, state.accept_terms_of_service_task.is_some(), {
+ let state = self.state.clone();
+ move |_window, cx| {
+ state.update(cx, |state, cx| state.accept_terms_of_service(cx));
+ }
+ })
+ .into_any_element(),
+ )
}
fn reset_credentials(&self, _cx: &mut App) -> Task<Result<()>> {
@@ -429,18 +438,12 @@ impl LanguageModelProvider for CloudLanguageModelProvider {
}
fn render_accept_terms(
- state: Entity<State>,
view_kind: LanguageModelProviderTosView,
- cx: &mut App,
-) -> Option<AnyElement> {
- if state.read(cx).has_accepted_terms_of_service(cx) {
- return None;
- }
-
- let accept_terms_disabled = state.read(cx).accept_terms.is_some();
-
+ accept_terms_of_service_in_progress: bool,
+ accept_terms_callback: impl Fn(&mut Window, &mut App) + 'static,
+) -> impl IntoElement {
let thread_fresh_start = matches!(view_kind, LanguageModelProviderTosView::ThreadFreshStart);
- let thread_empty_state = matches!(view_kind, LanguageModelProviderTosView::ThreadtEmptyState);
+ let thread_empty_state = matches!(view_kind, LanguageModelProviderTosView::ThreadEmptyState);
let terms_button = Button::new("terms_of_service", "Terms of Service")
.style(ButtonStyle::Subtle)
@@ -463,18 +466,11 @@ fn render_accept_terms(
this.style(ButtonStyle::Tinted(TintColor::Warning))
.label_size(LabelSize::Small)
})
- .disabled(accept_terms_disabled)
- .on_click({
- let state = state.downgrade();
- move |_, _window, cx| {
- state
- .update(cx, |state, cx| state.accept_terms_of_service(cx))
- .ok();
- }
- }),
+ .disabled(accept_terms_of_service_in_progress)
+ .on_click(move |_, window, cx| (accept_terms_callback)(window, cx)),
);
- let form = if thread_empty_state {
+ if thread_empty_state {
h_flex()
.w_full()
.flex_wrap()
@@ -512,12 +508,10 @@ fn render_accept_terms(
LanguageModelProviderTosView::ThreadFreshStart => {
button_container.w_full().justify_center()
}
- LanguageModelProviderTosView::ThreadtEmptyState => div().w_0(),
+ LanguageModelProviderTosView::ThreadEmptyState => div().w_0(),
}
})
- };
-
- Some(form.into_any())
+ }
}
pub struct CloudLanguageModel {
@@ -536,8 +530,6 @@ struct PerformLlmCompletionResponse {
}
impl CloudLanguageModel {
- const MAX_RETRIES: usize = 3;
-
async fn perform_llm_completion(
client: Arc<Client>,
llm_api_token: LlmApiToken,
@@ -547,8 +539,7 @@ impl CloudLanguageModel {
let http_client = &client.http_client();
let mut token = llm_api_token.acquire(&client).await?;
- let mut retries_remaining = Self::MAX_RETRIES;
- let mut retry_delay = Duration::from_secs(1);
+ let mut refreshed_token = false;
loop {
let request_builder = http_client::Request::builder()
@@ -590,14 +581,20 @@ impl CloudLanguageModel {
includes_status_messages,
tool_use_limit_reached,
});
- } else if response
- .headers()
- .get(EXPIRED_LLM_TOKEN_HEADER_NAME)
- .is_some()
+ }
+
+ if !refreshed_token
+ && response
+ .headers()
+ .get(EXPIRED_LLM_TOKEN_HEADER_NAME)
+ .is_some()
{
- retries_remaining -= 1;
token = llm_api_token.refresh(&client).await?;
- } else if status == StatusCode::FORBIDDEN
+ refreshed_token = true;
+ continue;
+ }
+
+ if status == StatusCode::FORBIDDEN
&& response
.headers()
.get(SUBSCRIPTION_LIMIT_RESOURCE_HEADER_NAME)
@@ -622,35 +619,18 @@ impl CloudLanguageModel {
return Err(anyhow!(ModelRequestLimitReachedError { plan }));
}
}
-
- anyhow::bail!("Forbidden");
- } else if status.as_u16() >= 500 && status.as_u16() < 600 {
- // If we encounter an error in the 500 range, retry after a delay.
- // We've seen at least these in the wild from API providers:
- // * 500 Internal Server Error
- // * 502 Bad Gateway
- // * 529 Service Overloaded
-
- if retries_remaining == 0 {
- let mut body = String::new();
- response.body_mut().read_to_string(&mut body).await?;
- anyhow::bail!(
- "cloud language model completion failed after {} retries with status {status}: {body}",
- Self::MAX_RETRIES
- );
- }
-
- Timer::after(retry_delay).await;
-
- retries_remaining -= 1;
- retry_delay *= 2; // If it fails again, wait longer.
} else if status == StatusCode::PAYMENT_REQUIRED {
return Err(anyhow!(PaymentRequiredError));
- } else {
- let mut body = String::new();
- response.body_mut().read_to_string(&mut body).await?;
- return Err(anyhow!(ApiError { status, body }));
}
+
+ let mut body = String::new();
+ let headers = response.headers().clone();
+ response.body_mut().read_to_string(&mut body).await?;
+ return Err(anyhow!(ApiError {
+ status,
+ body,
+ headers
+ }));
}
}
}
@@ -660,6 +640,19 @@ impl CloudLanguageModel {
struct ApiError {
status: StatusCode,
body: String,
+ headers: HeaderMap<HeaderValue>,
+}
+
+impl From<ApiError> for LanguageModelCompletionError {
+ fn from(error: ApiError) -> Self {
+ let retry_after = None;
+ LanguageModelCompletionError::from_http_status(
+ PROVIDER_NAME,
+ error.status,
+ error.body,
+ retry_after,
+ )
+ }
}
impl LanguageModel for CloudLanguageModel {
@@ -672,11 +665,29 @@ impl LanguageModel for CloudLanguageModel {
}
fn provider_id(&self) -> LanguageModelProviderId {
- LanguageModelProviderId(ZED_CLOUD_PROVIDER_ID.into())
+ PROVIDER_ID
}
fn provider_name(&self) -> LanguageModelProviderName {
- LanguageModelProviderName(PROVIDER_NAME.into())
+ PROVIDER_NAME
+ }
+
+ fn upstream_provider_id(&self) -> LanguageModelProviderId {
+ use zed_llm_client::LanguageModelProvider::*;
+ match self.model.provider {
+ Anthropic => language_model::ANTHROPIC_PROVIDER_ID,
+ OpenAi => language_model::OPEN_AI_PROVIDER_ID,
+ Google => language_model::GOOGLE_PROVIDER_ID,
+ }
+ }
+
+ fn upstream_provider_name(&self) -> LanguageModelProviderName {
+ use zed_llm_client::LanguageModelProvider::*;
+ match self.model.provider {
+ Anthropic => language_model::ANTHROPIC_PROVIDER_NAME,
+ OpenAi => language_model::OPEN_AI_PROVIDER_NAME,
+ Google => language_model::GOOGLE_PROVIDER_NAME,
+ }
}
fn supports_tools(&self) -> bool {
@@ -695,7 +706,7 @@ impl LanguageModel for CloudLanguageModel {
}
}
- fn supports_max_mode(&self) -> bool {
+ fn supports_burn_mode(&self) -> bool {
self.model.supports_max_mode
}
@@ -776,6 +787,7 @@ impl LanguageModel for CloudLanguageModel {
.body(serde_json::to_string(&request_body)?.into())?;
let mut response = http_client.send(request).await?;
let status = response.status();
+ let headers = response.headers().clone();
let mut response_body = String::new();
response
.body_mut()
@@ -790,7 +802,8 @@ impl LanguageModel for CloudLanguageModel {
} else {
Err(anyhow!(ApiError {
status,
- body: response_body
+ body: response_body,
+ headers
}))
}
}
@@ -855,18 +868,7 @@ impl LanguageModel for CloudLanguageModel {
)
.await
.map_err(|err| match err.downcast::<ApiError>() {
- Ok(api_err) => {
- if api_err.status == StatusCode::BAD_REQUEST {
- if let Some(tokens) = parse_prompt_too_long(&api_err.body) {
- return anyhow!(
- LanguageModelKnownError::ContextWindowLimitExceeded {
- tokens
- }
- );
- }
- }
- anyhow!(api_err)
- }
+ Ok(api_err) => anyhow!(LanguageModelCompletionError::from(api_err)),
Err(err) => anyhow!(err),
})?;
@@ -995,7 +997,7 @@ where
.flat_map(move |event| {
futures::stream::iter(match event {
Err(error) => {
- vec![Err(LanguageModelCompletionError::Other(error))]
+ vec![Err(LanguageModelCompletionError::from(error))]
}
Ok(CloudCompletionEvent::Status(event)) => {
vec![Ok(LanguageModelCompletionEvent::StatusUpdate(event))]
@@ -1054,32 +1056,24 @@ fn response_lines<T: DeserializeOwned>(
)
}
-struct ConfigurationView {
- state: gpui::Entity<State>,
+#[derive(IntoElement, RegisterComponent)]
+struct ZedAiConfiguration {
+ is_connected: bool,
+ plan: Option<proto::Plan>,
+ subscription_period: Option<(DateTime<Utc>, DateTime<Utc>)>,
+ eligible_for_trial: bool,
+ has_accepted_terms_of_service: bool,
+ accept_terms_of_service_in_progress: bool,
+ accept_terms_of_service_callback: Arc<dyn Fn(&mut Window, &mut App) + Send + Sync>,
+ sign_in_callback: Arc<dyn Fn(&mut Window, &mut App) + Send + Sync>,
}
-impl ConfigurationView {
- fn authenticate(&mut self, cx: &mut Context<Self>) {
- self.state.update(cx, |state, cx| {
- state.authenticate(cx).detach_and_log_err(cx);
- });
- cx.notify();
- }
-}
-
-impl Render for ConfigurationView {
- fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+impl RenderOnce for ZedAiConfiguration {
+ fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
const ZED_PRICING_URL: &str = "https://zed.dev/pricing";
- let is_connected = !self.state.read(cx).is_signed_out();
- let user_store = self.state.read(cx).user_store.read(cx);
- let plan = user_store.current_plan();
- let subscription_period = user_store.subscription_period();
- let eligible_for_trial = user_store.trial_started_at().is_none();
- let has_accepted_terms = self.state.read(cx).has_accepted_terms_of_service(cx);
-
- let is_pro = plan == Some(proto::Plan::ZedPro);
- let subscription_text = match (plan, subscription_period) {
+ let is_pro = self.plan == Some(proto::Plan::ZedPro);
+ let subscription_text = match (self.plan, self.subscription_period) {
(Some(proto::Plan::ZedPro), Some(_)) => {
"You have access to Zed's hosted LLMs through your Zed Pro subscription."
}
@@ -1090,7 +1084,7 @@ impl Render for ConfigurationView {
"You have basic access to Zed's hosted LLMs through your Zed Free subscription."
}
_ => {
- if eligible_for_trial {
+ if self.eligible_for_trial {
"Subscribe for access to Zed's hosted LLMs. Start with a 14 day free trial."
} else {
"Subscribe for access to Zed's hosted LLMs."
@@ -1101,7 +1095,7 @@ impl Render for ConfigurationView {
h_flex().child(
Button::new("manage_settings", "Manage Subscription")
.style(ButtonStyle::Tinted(TintColor::Accent))
- .on_click(cx.listener(|_, _, _, cx| cx.open_url(&zed_urls::account_url(cx)))),
+ .on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))),
)
} else {
h_flex()
@@ -1109,28 +1103,38 @@ impl Render for ConfigurationView {
.child(
Button::new("learn_more", "Learn more")
.style(ButtonStyle::Subtle)
- .on_click(cx.listener(|_, _, _, cx| cx.open_url(ZED_PRICING_URL))),
+ .on_click(|_, _, cx| cx.open_url(ZED_PRICING_URL)),
)
.child(
- Button::new("upgrade", "Upgrade")
- .style(ButtonStyle::Subtle)
- .color(Color::Accent)
- .on_click(
- cx.listener(|_, _, _, cx| cx.open_url(&zed_urls::account_url(cx))),
- ),
+ Button::new(
+ "upgrade",
+ if self.plan.is_none() && self.eligible_for_trial {
+ "Start Trial"
+ } else {
+ "Upgrade"
+ },
+ )
+ .style(ButtonStyle::Subtle)
+ .color(Color::Accent)
+ .on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))),
)
};
- if is_connected {
+ if self.is_connected {
v_flex()
.gap_3()
.w_full()
- .children(render_accept_terms(
- self.state.clone(),
- LanguageModelProviderTosView::Configuration,
- cx,
- ))
- .when(has_accepted_terms, |this| {
+ .when(!self.has_accepted_terms_of_service, |this| {
+ this.child(render_accept_terms(
+ LanguageModelProviderTosView::Configuration,
+ self.accept_terms_of_service_in_progress,
+ {
+ let callback = self.accept_terms_of_service_callback.clone();
+ move |window, cx| (callback)(window, cx)
+ },
+ ))
+ })
+ .when(self.has_accepted_terms_of_service, |this| {
this.child(subscription_text)
.child(manage_subscription_buttons)
})
@@ -1143,8 +1147,126 @@ impl Render for ConfigurationView {
.icon_color(Color::Muted)
.icon(IconName::Github)
.icon_position(IconPosition::Start)
- .on_click(cx.listener(move |this, _, _, cx| this.authenticate(cx))),
+ .on_click({
+ let callback = self.sign_in_callback.clone();
+ move |_, window, cx| (callback)(window, cx)
+ }),
)
}
}
}
+
+struct ConfigurationView {
+ state: Entity<State>,
+ accept_terms_of_service_callback: Arc<dyn Fn(&mut Window, &mut App) + Send + Sync>,
+ sign_in_callback: Arc<dyn Fn(&mut Window, &mut App) + Send + Sync>,
+}
+
+impl ConfigurationView {
+ fn new(state: Entity<State>) -> Self {
+ let accept_terms_of_service_callback = Arc::new({
+ let state = state.clone();
+ move |_window: &mut Window, cx: &mut App| {
+ state.update(cx, |state, cx| {
+ state.accept_terms_of_service(cx);
+ });
+ }
+ });
+
+ let sign_in_callback = Arc::new({
+ let state = state.clone();
+ move |_window: &mut Window, cx: &mut App| {
+ state.update(cx, |state, cx| {
+ state.authenticate(cx).detach_and_log_err(cx);
+ });
+ }
+ });
+
+ Self {
+ state,
+ accept_terms_of_service_callback,
+ sign_in_callback,
+ }
+ }
+}
+
+impl Render for ConfigurationView {
+ fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ let state = self.state.read(cx);
+ let user_store = state.user_store.read(cx);
+
+ ZedAiConfiguration {
+ is_connected: !state.is_signed_out(),
+ plan: user_store.current_plan(),
+ subscription_period: user_store.subscription_period(),
+ eligible_for_trial: user_store.trial_started_at().is_none(),
+ has_accepted_terms_of_service: state.has_accepted_terms_of_service(cx),
+ accept_terms_of_service_in_progress: state.accept_terms_of_service_task.is_some(),
+ accept_terms_of_service_callback: self.accept_terms_of_service_callback.clone(),
+ sign_in_callback: self.sign_in_callback.clone(),
+ }
+ }
+}
+
+impl Component for ZedAiConfiguration {
+ fn scope() -> ComponentScope {
+ ComponentScope::Agent
+ }
+
+ fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
+ fn configuration(
+ is_connected: bool,
+ plan: Option<proto::Plan>,
+ eligible_for_trial: bool,
+ has_accepted_terms_of_service: bool,
+ ) -> AnyElement {
+ ZedAiConfiguration {
+ is_connected,
+ plan,
+ subscription_period: plan
+ .is_some()
+ .then(|| (Utc::now(), Utc::now() + chrono::Duration::days(7))),
+ eligible_for_trial,
+ has_accepted_terms_of_service,
+ accept_terms_of_service_in_progress: false,
+ accept_terms_of_service_callback: Arc::new(|_, _| {}),
+ sign_in_callback: Arc::new(|_, _| {}),
+ }
+ .into_any_element()
+ }
+
+ Some(
+ v_flex()
+ .p_4()
+ .gap_4()
+ .children(vec![
+ single_example("Not connected", configuration(false, None, false, true)),
+ single_example(
+ "Accept Terms of Service",
+ configuration(true, None, true, false),
+ ),
+ single_example(
+ "No Plan - Not eligible for trial",
+ configuration(true, None, false, true),
+ ),
+ single_example(
+ "No Plan - Eligible for trial",
+ configuration(true, None, true, true),
+ ),
+ single_example(
+ "Free Plan",
+ configuration(true, Some(proto::Plan::Free), true, true),
+ ),
+ single_example(
+ "Zed Pro Trial Plan",
+ configuration(true, Some(proto::Plan::ZedProTrial), true, true),
+ ),
+ single_example(
+ "Zed Pro Plan",
+ configuration(true, Some(proto::Plan::ZedPro), true, true),
+ ),
+ ])
+ .into_any_element(),
+ )
+ }
+}
@@ -35,8 +35,9 @@ use super::anthropic::count_anthropic_tokens;
use super::google::count_google_tokens;
use super::open_ai::count_open_ai_tokens;
-const PROVIDER_ID: &str = "copilot_chat";
-const PROVIDER_NAME: &str = "GitHub Copilot Chat";
+const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("copilot_chat");
+const PROVIDER_NAME: LanguageModelProviderName =
+ LanguageModelProviderName::new("GitHub Copilot Chat");
pub struct CopilotChatLanguageModelProvider {
state: Entity<State>,
@@ -102,11 +103,11 @@ impl LanguageModelProviderState for CopilotChatLanguageModelProvider {
impl LanguageModelProvider for CopilotChatLanguageModelProvider {
fn id(&self) -> LanguageModelProviderId {
- LanguageModelProviderId(PROVIDER_ID.into())
+ PROVIDER_ID
}
fn name(&self) -> LanguageModelProviderName {
- LanguageModelProviderName(PROVIDER_NAME.into())
+ PROVIDER_NAME
}
fn icon(&self) -> IconName {
@@ -201,11 +202,11 @@ impl LanguageModel for CopilotChatLanguageModel {
}
fn provider_id(&self) -> LanguageModelProviderId {
- LanguageModelProviderId(PROVIDER_ID.into())
+ PROVIDER_ID
}
fn provider_name(&self) -> LanguageModelProviderName {
- LanguageModelProviderName(PROVIDER_NAME.into())
+ PROVIDER_NAME
}
fn supports_tools(&self) -> bool {
@@ -391,24 +392,24 @@ pub fn map_to_language_model_completion_events(
serde_json::Value::from_str(&tool_call.arguments)
};
match arguments {
- Ok(input) => Ok(LanguageModelCompletionEvent::ToolUse(
- LanguageModelToolUse {
- id: tool_call.id.clone().into(),
- name: tool_call.name.as_str().into(),
- is_input_complete: true,
- input,
- raw_input: tool_call.arguments.clone(),
- },
- )),
- Err(error) => {
- Err(LanguageModelCompletionError::BadInputJson {
- id: tool_call.id.into(),
- tool_name: tool_call.name.as_str().into(),
- raw_input: tool_call.arguments.into(),
- json_parse_error: error.to_string(),
- })
- }
- }
+ Ok(input) => Ok(LanguageModelCompletionEvent::ToolUse(
+ LanguageModelToolUse {
+ id: tool_call.id.clone().into(),
+ name: tool_call.name.as_str().into(),
+ is_input_complete: true,
+ input,
+ raw_input: tool_call.arguments.clone(),
+ },
+ )),
+ Err(error) => Ok(
+ LanguageModelCompletionEvent::ToolUseJsonParseError {
+ id: tool_call.id.into(),
+ tool_name: tool_call.name.as_str().into(),
+ raw_input: tool_call.arguments.into(),
+ json_parse_error: error.to_string(),
+ },
+ ),
+ }
},
));
@@ -28,8 +28,8 @@ use util::ResultExt;
use crate::{AllLanguageModelSettings, ui::InstructionListItem};
-const PROVIDER_ID: &str = "deepseek";
-const PROVIDER_NAME: &str = "DeepSeek";
+const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("deepseek");
+const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("DeepSeek");
const DEEPSEEK_API_KEY_VAR: &str = "DEEPSEEK_API_KEY";
#[derive(Default)]
@@ -174,11 +174,11 @@ impl LanguageModelProviderState for DeepSeekLanguageModelProvider {
impl LanguageModelProvider for DeepSeekLanguageModelProvider {
fn id(&self) -> LanguageModelProviderId {
- LanguageModelProviderId(PROVIDER_ID.into())
+ PROVIDER_ID
}
fn name(&self) -> LanguageModelProviderName {
- LanguageModelProviderName(PROVIDER_NAME.into())
+ PROVIDER_NAME
}
fn icon(&self) -> IconName {
@@ -283,11 +283,11 @@ impl LanguageModel for DeepSeekLanguageModel {
}
fn provider_id(&self) -> LanguageModelProviderId {
- LanguageModelProviderId(PROVIDER_ID.into())
+ PROVIDER_ID
}
fn provider_name(&self) -> LanguageModelProviderName {
- LanguageModelProviderName(PROVIDER_NAME.into())
+ PROVIDER_NAME
}
fn supports_tools(&self) -> bool {
@@ -466,7 +466,7 @@ impl DeepSeekEventMapper {
events.flat_map(move |event| {
futures::stream::iter(match event {
Ok(event) => self.map_event(event),
- Err(error) => vec![Err(LanguageModelCompletionError::Other(anyhow!(error)))],
+ Err(error) => vec![Err(LanguageModelCompletionError::from(error))],
})
})
}
@@ -476,7 +476,7 @@ impl DeepSeekEventMapper {
event: deepseek::StreamResponse,
) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
let Some(choice) = event.choices.first() else {
- return vec![Err(LanguageModelCompletionError::Other(anyhow!(
+ return vec![Err(LanguageModelCompletionError::from(anyhow!(
"Response contained no choices"
)))];
};
@@ -538,8 +538,8 @@ impl DeepSeekEventMapper {
raw_input: tool_call.arguments.clone(),
},
)),
- Err(error) => Err(LanguageModelCompletionError::BadInputJson {
- id: tool_call.id.into(),
+ Err(error) => Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
+ id: tool_call.id.clone().into(),
tool_name: tool_call.name.as_str().into(),
raw_input: tool_call.arguments.into(),
json_parse_error: error.to_string(),
@@ -37,8 +37,8 @@ use util::ResultExt;
use crate::AllLanguageModelSettings;
use crate::ui::InstructionListItem;
-const PROVIDER_ID: &str = "google";
-const PROVIDER_NAME: &str = "Google AI";
+const PROVIDER_ID: LanguageModelProviderId = language_model::GOOGLE_PROVIDER_ID;
+const PROVIDER_NAME: LanguageModelProviderName = language_model::GOOGLE_PROVIDER_NAME;
#[derive(Default, Clone, Debug, PartialEq)]
pub struct GoogleSettings {
@@ -207,11 +207,11 @@ impl LanguageModelProviderState for GoogleLanguageModelProvider {
impl LanguageModelProvider for GoogleLanguageModelProvider {
fn id(&self) -> LanguageModelProviderId {
- LanguageModelProviderId(PROVIDER_ID.into())
+ PROVIDER_ID
}
fn name(&self) -> LanguageModelProviderName {
- LanguageModelProviderName(PROVIDER_NAME.into())
+ PROVIDER_NAME
}
fn icon(&self) -> IconName {
@@ -334,11 +334,11 @@ impl LanguageModel for GoogleLanguageModel {
}
fn provider_id(&self) -> LanguageModelProviderId {
- LanguageModelProviderId(PROVIDER_ID.into())
+ PROVIDER_ID
}
fn provider_name(&self) -> LanguageModelProviderName {
- LanguageModelProviderName(PROVIDER_NAME.into())
+ PROVIDER_NAME
}
fn supports_tools(&self) -> bool {
@@ -423,9 +423,7 @@ impl LanguageModel for GoogleLanguageModel {
);
let request = self.stream_completion(request, cx);
let future = self.request_limiter.stream(async move {
- let response = request
- .await
- .map_err(|err| LanguageModelCompletionError::Other(anyhow!(err)))?;
+ let response = request.await.map_err(LanguageModelCompletionError::from)?;
Ok(GoogleEventMapper::new().map_stream(response))
});
async move { Ok(future.await?.boxed()) }.boxed()
@@ -622,7 +620,7 @@ impl GoogleEventMapper {
futures::stream::iter(match event {
Some(Ok(event)) => self.map_event(event),
Some(Err(error)) => {
- vec![Err(LanguageModelCompletionError::Other(anyhow!(error)))]
+ vec![Err(LanguageModelCompletionError::from(error))]
}
None => vec![Ok(LanguageModelCompletionEvent::Stop(self.stop_reason))],
})
@@ -31,8 +31,8 @@ const LMSTUDIO_DOWNLOAD_URL: &str = "https://lmstudio.ai/download";
const LMSTUDIO_CATALOG_URL: &str = "https://lmstudio.ai/models";
const LMSTUDIO_SITE: &str = "https://lmstudio.ai/";
-const PROVIDER_ID: &str = "lmstudio";
-const PROVIDER_NAME: &str = "LM Studio";
+const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("lmstudio");
+const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("LM Studio");
#[derive(Default, Debug, Clone, PartialEq)]
pub struct LmStudioSettings {
@@ -156,11 +156,11 @@ impl LanguageModelProviderState for LmStudioLanguageModelProvider {
impl LanguageModelProvider for LmStudioLanguageModelProvider {
fn id(&self) -> LanguageModelProviderId {
- LanguageModelProviderId(PROVIDER_ID.into())
+ PROVIDER_ID
}
fn name(&self) -> LanguageModelProviderName {
- LanguageModelProviderName(PROVIDER_NAME.into())
+ PROVIDER_NAME
}
fn icon(&self) -> IconName {
@@ -386,11 +386,11 @@ impl LanguageModel for LmStudioLanguageModel {
}
fn provider_id(&self) -> LanguageModelProviderId {
- LanguageModelProviderId(PROVIDER_ID.into())
+ PROVIDER_ID
}
fn provider_name(&self) -> LanguageModelProviderName {
- LanguageModelProviderName(PROVIDER_NAME.into())
+ PROVIDER_NAME
}
fn supports_tools(&self) -> bool {
@@ -474,7 +474,7 @@ impl LmStudioEventMapper {
events.flat_map(move |event| {
futures::stream::iter(match event {
Ok(event) => self.map_event(event),
- Err(error) => vec![Err(LanguageModelCompletionError::Other(anyhow!(error)))],
+ Err(error) => vec![Err(LanguageModelCompletionError::from(error))],
})
})
}
@@ -484,7 +484,7 @@ impl LmStudioEventMapper {
event: lmstudio::ResponseStreamEvent,
) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
let Some(choice) = event.choices.into_iter().next() else {
- return vec![Err(LanguageModelCompletionError::Other(anyhow!(
+ return vec![Err(LanguageModelCompletionError::from(anyhow!(
"Response contained no choices"
)))];
};
@@ -553,7 +553,7 @@ impl LmStudioEventMapper {
raw_input: tool_call.arguments,
},
)),
- Err(error) => Err(LanguageModelCompletionError::BadInputJson {
+ Err(error) => Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
id: tool_call.id.into(),
tool_name: tool_call.name.into(),
raw_input: tool_call.arguments.into(),
@@ -565,7 +565,7 @@ impl LmStudioEventMapper {
events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::ToolUse)));
}
Some(stop_reason) => {
- log::error!("Unexpected OpenAI stop_reason: {stop_reason:?}",);
+ log::error!("Unexpected LMStudio stop_reason: {stop_reason:?}",);
events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)));
}
None => {}
@@ -2,8 +2,7 @@ use anyhow::{Context as _, Result, anyhow};
use collections::BTreeMap;
use credentials_provider::CredentialsProvider;
use editor::{Editor, EditorElement, EditorStyle};
-use futures::stream::BoxStream;
-use futures::{FutureExt, StreamExt, future::BoxFuture};
+use futures::{FutureExt, Stream, StreamExt, future::BoxFuture, stream::BoxStream};
use gpui::{
AnyView, App, AsyncApp, Context, Entity, FontStyle, Subscription, Task, TextStyle, WhiteSpace,
};
@@ -15,6 +14,7 @@ use language_model::{
LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolUse, MessageContent,
RateLimiter, Role, StopReason, TokenUsage,
};
+use mistral::StreamResponse;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
@@ -29,8 +29,8 @@ use util::ResultExt;
use crate::{AllLanguageModelSettings, ui::InstructionListItem};
-const PROVIDER_ID: &str = "mistral";
-const PROVIDER_NAME: &str = "Mistral";
+const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("mistral");
+const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("Mistral");
#[derive(Default, Clone, Debug, PartialEq)]
pub struct MistralSettings {
@@ -171,11 +171,11 @@ impl LanguageModelProviderState for MistralLanguageModelProvider {
impl LanguageModelProvider for MistralLanguageModelProvider {
fn id(&self) -> LanguageModelProviderId {
- LanguageModelProviderId(PROVIDER_ID.into())
+ PROVIDER_ID
}
fn name(&self) -> LanguageModelProviderName {
- LanguageModelProviderName(PROVIDER_NAME.into())
+ PROVIDER_NAME
}
fn icon(&self) -> IconName {
@@ -298,11 +298,11 @@ impl LanguageModel for MistralLanguageModel {
}
fn provider_id(&self) -> LanguageModelProviderId {
- LanguageModelProviderId(PROVIDER_ID.into())
+ PROVIDER_ID
}
fn provider_name(&self) -> LanguageModelProviderName {
- LanguageModelProviderName(PROVIDER_NAME.into())
+ PROVIDER_NAME
}
fn supports_tools(&self) -> bool {
@@ -579,13 +579,13 @@ impl MistralEventMapper {
pub fn map_stream(
mut self,
- events: Pin<Box<dyn Send + futures::Stream<Item = Result<mistral::StreamResponse>>>>,
- ) -> impl futures::Stream<Item = Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>
+ events: Pin<Box<dyn Send + Stream<Item = Result<StreamResponse>>>>,
+ ) -> impl Stream<Item = Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>
{
events.flat_map(move |event| {
futures::stream::iter(match event {
Ok(event) => self.map_event(event),
- Err(error) => vec![Err(LanguageModelCompletionError::Other(anyhow!(error)))],
+ Err(error) => vec![Err(LanguageModelCompletionError::from(error))],
})
})
}
@@ -595,7 +595,7 @@ impl MistralEventMapper {
event: mistral::StreamResponse,
) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
let Some(choice) = event.choices.first() else {
- return vec![Err(LanguageModelCompletionError::Other(anyhow!(
+ return vec![Err(LanguageModelCompletionError::from(anyhow!(
"Response contained no choices"
)))];
};
@@ -660,7 +660,7 @@ impl MistralEventMapper {
for (_, tool_call) in self.tool_calls_by_index.drain() {
if tool_call.id.is_empty() || tool_call.name.is_empty() {
- results.push(Err(LanguageModelCompletionError::Other(anyhow!(
+ results.push(Err(LanguageModelCompletionError::from(anyhow!(
"Received incomplete tool call: missing id or name"
))));
continue;
@@ -676,12 +676,14 @@ impl MistralEventMapper {
raw_input: tool_call.arguments,
},
))),
- Err(error) => results.push(Err(LanguageModelCompletionError::BadInputJson {
- id: tool_call.id.into(),
- tool_name: tool_call.name.into(),
- raw_input: tool_call.arguments.into(),
- json_parse_error: error.to_string(),
- })),
+ Err(error) => {
+ results.push(Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
+ id: tool_call.id.into(),
+ tool_name: tool_call.name.into(),
+ raw_input: tool_call.arguments.into(),
+ json_parse_error: error.to_string(),
+ }))
+ }
}
}
@@ -30,8 +30,8 @@ const OLLAMA_DOWNLOAD_URL: &str = "https://ollama.com/download";
const OLLAMA_LIBRARY_URL: &str = "https://ollama.com/library";
const OLLAMA_SITE: &str = "https://ollama.com/";
-const PROVIDER_ID: &str = "ollama";
-const PROVIDER_NAME: &str = "Ollama";
+const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("ollama");
+const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("Ollama");
#[derive(Default, Debug, Clone, PartialEq)]
pub struct OllamaSettings {
@@ -181,11 +181,11 @@ impl LanguageModelProviderState for OllamaLanguageModelProvider {
impl LanguageModelProvider for OllamaLanguageModelProvider {
fn id(&self) -> LanguageModelProviderId {
- LanguageModelProviderId(PROVIDER_ID.into())
+ PROVIDER_ID
}
fn name(&self) -> LanguageModelProviderName {
- LanguageModelProviderName(PROVIDER_NAME.into())
+ PROVIDER_NAME
}
fn icon(&self) -> IconName {
@@ -350,11 +350,11 @@ impl LanguageModel for OllamaLanguageModel {
}
fn provider_id(&self) -> LanguageModelProviderId {
- LanguageModelProviderId(PROVIDER_ID.into())
+ PROVIDER_ID
}
fn provider_name(&self) -> LanguageModelProviderName {
- LanguageModelProviderName(PROVIDER_NAME.into())
+ PROVIDER_NAME
}
fn supports_tools(&self) -> bool {
@@ -453,7 +453,7 @@ fn map_to_language_model_completion_events(
let delta = match response {
Ok(delta) => delta,
Err(e) => {
- let event = Err(LanguageModelCompletionError::Other(anyhow!(e)));
+ let event = Err(LanguageModelCompletionError::from(anyhow!(e)));
return Some((vec![event], state));
}
};
@@ -31,8 +31,8 @@ use util::ResultExt;
use crate::OpenAiSettingsContent;
use crate::{AllLanguageModelSettings, ui::InstructionListItem};
-const PROVIDER_ID: &str = "openai";
-const PROVIDER_NAME: &str = "OpenAI";
+const PROVIDER_ID: LanguageModelProviderId = language_model::OPEN_AI_PROVIDER_ID;
+const PROVIDER_NAME: LanguageModelProviderName = language_model::OPEN_AI_PROVIDER_NAME;
#[derive(Default, Clone, Debug, PartialEq)]
pub struct OpenAiSettings {
@@ -173,11 +173,11 @@ impl LanguageModelProviderState for OpenAiLanguageModelProvider {
impl LanguageModelProvider for OpenAiLanguageModelProvider {
fn id(&self) -> LanguageModelProviderId {
- LanguageModelProviderId(PROVIDER_ID.into())
+ PROVIDER_ID
}
fn name(&self) -> LanguageModelProviderName {
- LanguageModelProviderName(PROVIDER_NAME.into())
+ PROVIDER_NAME
}
fn icon(&self) -> IconName {
@@ -267,7 +267,11 @@ impl OpenAiLanguageModel {
};
let future = self.request_limiter.stream(async move {
- let api_key = api_key.context("Missing OpenAI API Key")?;
+ let Some(api_key) = api_key else {
+ return Err(LanguageModelCompletionError::NoApiKey {
+ provider: PROVIDER_NAME,
+ });
+ };
let request = stream_completion(http_client.as_ref(), &api_url, &api_key, request);
let response = request.await?;
Ok(response)
@@ -287,11 +291,11 @@ impl LanguageModel for OpenAiLanguageModel {
}
fn provider_id(&self) -> LanguageModelProviderId {
- LanguageModelProviderId(PROVIDER_ID.into())
+ PROVIDER_ID
}
fn provider_name(&self) -> LanguageModelProviderName {
- LanguageModelProviderName(PROVIDER_NAME.into())
+ PROVIDER_NAME
}
fn supports_tools(&self) -> bool {
@@ -525,7 +529,7 @@ impl OpenAiEventMapper {
events.flat_map(move |event| {
futures::stream::iter(match event {
Ok(event) => self.map_event(event),
- Err(error) => vec![Err(LanguageModelCompletionError::Other(anyhow!(error)))],
+ Err(error) => vec![Err(LanguageModelCompletionError::from(anyhow!(error)))],
})
})
}
@@ -588,10 +592,10 @@ impl OpenAiEventMapper {
raw_input: tool_call.arguments.clone(),
},
)),
- Err(error) => Err(LanguageModelCompletionError::BadInputJson {
+ Err(error) => Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
id: tool_call.id.into(),
- tool_name: tool_call.name.as_str().into(),
- raw_input: tool_call.arguments.into(),
+ tool_name: tool_call.name.into(),
+ raw_input: tool_call.arguments.clone().into(),
json_parse_error: error.to_string(),
}),
}
@@ -11,8 +11,8 @@ use language_model::{
AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent,
LanguageModelId, LanguageModelName, LanguageModelProvider, LanguageModelProviderId,
LanguageModelProviderName, LanguageModelProviderState, LanguageModelRequest,
- LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolUse, MessageContent,
- RateLimiter, Role, StopReason, TokenUsage,
+ LanguageModelToolChoice, LanguageModelToolResultContent, LanguageModelToolSchemaFormat,
+ LanguageModelToolUse, MessageContent, RateLimiter, Role, StopReason, TokenUsage,
};
use open_router::{
Model, ModelMode as OpenRouterModelMode, ResponseStreamEvent, list_models, stream_completion,
@@ -29,8 +29,8 @@ use util::ResultExt;
use crate::{AllLanguageModelSettings, ui::InstructionListItem};
-const PROVIDER_ID: &str = "openrouter";
-const PROVIDER_NAME: &str = "OpenRouter";
+const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("openrouter");
+const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("OpenRouter");
#[derive(Default, Clone, Debug, PartialEq)]
pub struct OpenRouterSettings {
@@ -244,11 +244,11 @@ impl LanguageModelProviderState for OpenRouterLanguageModelProvider {
impl LanguageModelProvider for OpenRouterLanguageModelProvider {
fn id(&self) -> LanguageModelProviderId {
- LanguageModelProviderId(PROVIDER_ID.into())
+ PROVIDER_ID
}
fn name(&self) -> LanguageModelProviderName {
- LanguageModelProviderName(PROVIDER_NAME.into())
+ PROVIDER_NAME
}
fn icon(&self) -> IconName {
@@ -363,17 +363,26 @@ impl LanguageModel for OpenRouterLanguageModel {
}
fn provider_id(&self) -> LanguageModelProviderId {
- LanguageModelProviderId(PROVIDER_ID.into())
+ PROVIDER_ID
}
fn provider_name(&self) -> LanguageModelProviderName {
- LanguageModelProviderName(PROVIDER_NAME.into())
+ PROVIDER_NAME
}
fn supports_tools(&self) -> bool {
self.model.supports_tool_calls()
}
+ fn tool_input_format(&self) -> LanguageModelToolSchemaFormat {
+ let model_id = self.model.id().trim().to_lowercase();
+ if model_id.contains("gemini") {
+ LanguageModelToolSchemaFormat::JsonSchemaSubset
+ } else {
+ LanguageModelToolSchemaFormat::JsonSchema
+ }
+ }
+
fn telemetry_id(&self) -> String {
format!("openrouter/{}", self.model.id())
}
@@ -598,7 +607,7 @@ impl OpenRouterEventMapper {
events.flat_map(move |event| {
futures::stream::iter(match event {
Ok(event) => self.map_event(event),
- Err(error) => vec![Err(LanguageModelCompletionError::Other(anyhow!(error)))],
+ Err(error) => vec![Err(LanguageModelCompletionError::from(anyhow!(error)))],
})
})
}
@@ -608,7 +617,7 @@ impl OpenRouterEventMapper {
event: ResponseStreamEvent,
) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
let Some(choice) = event.choices.first() else {
- return vec![Err(LanguageModelCompletionError::Other(anyhow!(
+ return vec![Err(LanguageModelCompletionError::from(anyhow!(
"Response contained no choices"
)))];
};
@@ -674,10 +683,10 @@ impl OpenRouterEventMapper {
raw_input: tool_call.arguments.clone(),
},
)),
- Err(error) => Err(LanguageModelCompletionError::BadInputJson {
- id: tool_call.id.into(),
+ Err(error) => Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
+ id: tool_call.id.clone().into(),
tool_name: tool_call.name.as_str().into(),
- raw_input: tool_call.arguments.into(),
+ raw_input: tool_call.arguments.clone().into(),
json_parse_error: error.to_string(),
}),
}
@@ -25,8 +25,8 @@ use util::ResultExt;
use crate::{AllLanguageModelSettings, ui::InstructionListItem};
-const PROVIDER_ID: &str = "vercel";
-const PROVIDER_NAME: &str = "Vercel";
+const PROVIDER_ID: LanguageModelProviderId = LanguageModelProviderId::new("vercel");
+const PROVIDER_NAME: LanguageModelProviderName = LanguageModelProviderName::new("Vercel");
#[derive(Default, Clone, Debug, PartialEq)]
pub struct VercelSettings {
@@ -172,11 +172,11 @@ impl LanguageModelProviderState for VercelLanguageModelProvider {
impl LanguageModelProvider for VercelLanguageModelProvider {
fn id(&self) -> LanguageModelProviderId {
- LanguageModelProviderId(PROVIDER_ID.into())
+ PROVIDER_ID
}
fn name(&self) -> LanguageModelProviderName {
- LanguageModelProviderName(PROVIDER_NAME.into())
+ PROVIDER_NAME
}
fn icon(&self) -> IconName {
@@ -269,7 +269,11 @@ impl VercelLanguageModel {
};
let future = self.request_limiter.stream(async move {
- let api_key = api_key.context("Missing Vercel API Key")?;
+ let Some(api_key) = api_key else {
+ return Err(LanguageModelCompletionError::NoApiKey {
+ provider: PROVIDER_NAME,
+ });
+ };
let request =
open_ai::stream_completion(http_client.as_ref(), &api_url, &api_key, request);
let response = request.await?;
@@ -290,11 +294,11 @@ impl LanguageModel for VercelLanguageModel {
}
fn provider_id(&self) -> LanguageModelProviderId {
- LanguageModelProviderId(PROVIDER_ID.into())
+ PROVIDER_ID
}
fn provider_name(&self) -> LanguageModelProviderName {
- LanguageModelProviderName(PROVIDER_NAME.into())
+ PROVIDER_NAME
}
fn supports_tools(&self) -> bool {
@@ -18,6 +18,7 @@ client.workspace = true
collections.workspace = true
copilot.workspace = true
editor.workspace = true
+feature_flags.workspace = true
futures.workspace = true
gpui.workspace = true
itertools.workspace = true
@@ -3,13 +3,17 @@ use std::{collections::hash_map, path::PathBuf, sync::Arc, time::Duration};
use client::proto;
use collections::{HashMap, HashSet};
use editor::{Editor, EditorEvent};
-use gpui::{Corner, DismissEvent, Entity, Focusable as _, Subscription, Task, WeakEntity, actions};
+use feature_flags::FeatureFlagAppExt as _;
+use gpui::{
+ Corner, DismissEvent, Entity, Focusable as _, MouseButton, Subscription, Task, WeakEntity,
+ actions,
+};
use language::{BinaryStatus, BufferId, LocalFile, ServerHealth};
use lsp::{LanguageServerId, LanguageServerName, LanguageServerSelector};
use picker::{Picker, PickerDelegate, popover_menu::PickerPopoverMenu};
use project::{LspStore, LspStoreEvent, project_settings::ProjectSettings};
use settings::{Settings as _, SettingsStore};
-use ui::{Context, IconButtonShape, Indicator, Tooltip, Window, prelude::*};
+use ui::{Context, Indicator, PopoverMenuHandle, Tooltip, Window, prelude::*};
use workspace::{StatusItemView, Workspace};
@@ -19,6 +23,7 @@ actions!(lsp_tool, [ToggleMenu]);
pub struct LspTool {
state: Entity<PickerState>,
+ popover_menu_handle: PopoverMenuHandle<Picker<LspPickerDelegate>>,
lsp_picker: Option<Entity<Picker<LspPickerDelegate>>>,
_subscriptions: Vec<Subscription>,
}
@@ -31,7 +36,7 @@ struct PickerState {
}
#[derive(Debug)]
-struct LspPickerDelegate {
+pub struct LspPickerDelegate {
state: Entity<PickerState>,
selected_index: usize,
items: Vec<LspItem>,
@@ -64,6 +69,23 @@ struct LanguageServerBinaryStatus {
message: Option<SharedString>,
}
+#[derive(Debug)]
+struct ServerInfo {
+ name: LanguageServerName,
+ id: Option<LanguageServerId>,
+ health: Option<ServerHealth>,
+ binary_status: Option<LanguageServerBinaryStatus>,
+ message: Option<SharedString>,
+}
+
+impl ServerInfo {
+ fn server_selector(&self) -> LanguageServerSelector {
+ self.id
+ .map(LanguageServerSelector::Id)
+ .unwrap_or_else(|| LanguageServerSelector::Name(self.name.clone()))
+ }
+}
+
impl LanguageServerHealthStatus {
fn health(&self) -> Option<ServerHealth> {
self.health.as_ref().map(|(_, health)| *health)
@@ -158,45 +180,111 @@ impl LspPickerDelegate {
}
}
+ let mut can_stop_all = false;
+ let mut can_restart_all = true;
+
for (server_name, status) in state
.language_servers
.binary_statuses
.iter()
.filter(|(name, _)| !servers_with_health_checks.contains(name))
{
- let has_matching_server = state
+ match status.status {
+ BinaryStatus::None => {
+ can_restart_all = false;
+ can_stop_all = true;
+ }
+ BinaryStatus::CheckingForUpdate => {
+ can_restart_all = false;
+ }
+ BinaryStatus::Downloading => {
+ can_restart_all = false;
+ }
+ BinaryStatus::Starting => {
+ can_restart_all = false;
+ }
+ BinaryStatus::Stopping => {
+ can_restart_all = false;
+ }
+ BinaryStatus::Stopped => {}
+ BinaryStatus::Failed { .. } => {}
+ }
+
+ let matching_server_id = state
.language_servers
.servers_per_buffer_abs_path
.iter()
.filter(|(path, _)| editor_buffer_paths.contains(path))
.flat_map(|(_, server_associations)| server_associations.iter())
- .any(|(_, name)| name.as_ref() == Some(server_name));
- if has_matching_server {
- buffer_servers.push(ServerData::WithBinaryStatus(server_name, status));
+ .find_map(|(id, name)| {
+ if name.as_ref() == Some(server_name) {
+ Some(*id)
+ } else {
+ None
+ }
+ });
+ if let Some(server_id) = matching_server_id {
+ buffer_servers.push(ServerData::WithBinaryStatus(
+ Some(server_id),
+ server_name,
+ status,
+ ));
} else {
- other_servers.push(ServerData::WithBinaryStatus(server_name, status));
+ other_servers.push(ServerData::WithBinaryStatus(None, server_name, status));
}
}
buffer_servers.sort_by_key(|data| data.name().clone());
other_servers.sort_by_key(|data| data.name().clone());
+
let mut other_servers_start_index = None;
let mut new_lsp_items =
- Vec::with_capacity(buffer_servers.len() + other_servers.len() + 2);
- if !buffer_servers.is_empty() {
- new_lsp_items.push(LspItem::Header(SharedString::new("Current Buffer")));
- new_lsp_items.extend(buffer_servers.into_iter().map(ServerData::into_lsp_item));
- }
- if !other_servers.is_empty() {
+ Vec::with_capacity(buffer_servers.len() + other_servers.len() + 1);
+ new_lsp_items.extend(buffer_servers.into_iter().map(ServerData::into_lsp_item));
+ if !new_lsp_items.is_empty() {
other_servers_start_index = Some(new_lsp_items.len());
- new_lsp_items.push(LspItem::Header(SharedString::new("Other Active Servers")));
- new_lsp_items.extend(other_servers.into_iter().map(ServerData::into_lsp_item));
+ }
+ new_lsp_items.extend(other_servers.into_iter().map(ServerData::into_lsp_item));
+ if !new_lsp_items.is_empty() {
+ if can_stop_all {
+ new_lsp_items.push(LspItem::ToggleServersButton { restart: false });
+ } else if can_restart_all {
+ new_lsp_items.push(LspItem::ToggleServersButton { restart: true });
+ }
}
self.items = new_lsp_items;
self.other_servers_start_index = other_servers_start_index;
});
}
+
+ fn server_info(&self, ix: usize) -> Option<ServerInfo> {
+ match self.items.get(ix)? {
+ LspItem::ToggleServersButton { .. } => None,
+ LspItem::WithHealthCheck(
+ language_server_id,
+ language_server_health_status,
+ language_server_binary_status,
+ ) => Some(ServerInfo {
+ name: language_server_health_status.name.clone(),
+ id: Some(*language_server_id),
+ health: language_server_health_status.health(),
+ binary_status: language_server_binary_status.clone(),
+ message: language_server_health_status.message(),
+ }),
+ LspItem::WithBinaryStatus(
+ server_id,
+ language_server_name,
+ language_server_binary_status,
+ ) => Some(ServerInfo {
+ name: language_server_name.clone(),
+ id: *server_id,
+ health: None,
+ binary_status: Some(language_server_binary_status.clone()),
+ message: language_server_binary_status.message.clone(),
+ }),
+ }
+ }
}
impl LanguageServers {
@@ -244,6 +332,10 @@ impl LanguageServers {
);
}
}
+
+ fn is_empty(&self) -> bool {
+ self.binary_statuses.is_empty() && self.health_statuses.is_empty()
+ }
}
#[derive(Debug)]
@@ -253,7 +345,11 @@ enum ServerData<'a> {
&'a LanguageServerHealthStatus,
Option<&'a LanguageServerBinaryStatus>,
),
- WithBinaryStatus(&'a LanguageServerName, &'a LanguageServerBinaryStatus),
+ WithBinaryStatus(
+ Option<LanguageServerId>,
+ &'a LanguageServerName,
+ &'a LanguageServerBinaryStatus,
+ ),
}
#[derive(Debug)]
@@ -263,15 +359,21 @@ enum LspItem {
LanguageServerHealthStatus,
Option<LanguageServerBinaryStatus>,
),
- WithBinaryStatus(LanguageServerName, LanguageServerBinaryStatus),
- Header(SharedString),
+ WithBinaryStatus(
+ Option<LanguageServerId>,
+ LanguageServerName,
+ LanguageServerBinaryStatus,
+ ),
+ ToggleServersButton {
+ restart: bool,
+ },
}
impl ServerData<'_> {
fn name(&self) -> &LanguageServerName {
match self {
Self::WithHealthCheck(_, state, _) => &state.name,
- Self::WithBinaryStatus(name, ..) => name,
+ Self::WithBinaryStatus(_, name, ..) => name,
}
}
@@ -280,8 +382,8 @@ impl ServerData<'_> {
Self::WithHealthCheck(id, name, status) => {
LspItem::WithHealthCheck(id, name.clone(), status.cloned())
}
- Self::WithBinaryStatus(name, status) => {
- LspItem::WithBinaryStatus(name.clone(), status.clone())
+ Self::WithBinaryStatus(server_id, name, status) => {
+ LspItem::WithBinaryStatus(server_id, name.clone(), status.clone())
}
}
}
@@ -325,7 +427,81 @@ impl PickerDelegate for LspPickerDelegate {
Arc::default()
}
- fn confirm(&mut self, _: bool, _: &mut Window, _: &mut Context<Picker<Self>>) {}
+ fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
+ if let Some(LspItem::ToggleServersButton { restart }) = self.items.get(self.selected_index)
+ {
+ let lsp_store = self.state.read(cx).lsp_store.clone();
+ lsp_store
+ .update(cx, |lsp_store, cx| {
+ if *restart {
+ let Some(workspace) = self.state.read(cx).workspace.upgrade() else {
+ return;
+ };
+ let project = workspace.read(cx).project().clone();
+ let buffer_store = project.read(cx).buffer_store().clone();
+ let worktree_store = project.read(cx).worktree_store();
+
+ let buffers = self
+ .state
+ .read(cx)
+ .language_servers
+ .servers_per_buffer_abs_path
+ .keys()
+ .filter_map(|abs_path| {
+ worktree_store.read(cx).find_worktree(abs_path, cx)
+ })
+ .filter_map(|(worktree, relative_path)| {
+ let entry = worktree.read(cx).entry_for_path(&relative_path)?;
+ project.read(cx).path_for_entry(entry.id, cx)
+ })
+ .filter_map(|project_path| {
+ buffer_store.read(cx).get_by_path(&project_path)
+ })
+ .collect();
+ let selectors = self
+ .items
+ .iter()
+ // Do not try to use IDs as we have stopped all servers already, when allowing to restart them all
+ .flat_map(|item| match item {
+ LspItem::ToggleServersButton { .. } => None,
+ LspItem::WithHealthCheck(_, status, ..) => {
+ Some(LanguageServerSelector::Name(status.name.clone()))
+ }
+ LspItem::WithBinaryStatus(_, server_name, ..) => {
+ Some(LanguageServerSelector::Name(server_name.clone()))
+ }
+ })
+ .collect();
+ lsp_store.restart_language_servers_for_buffers(buffers, selectors, cx);
+ } else {
+ lsp_store.stop_all_language_servers(cx);
+ }
+ })
+ .ok();
+ }
+
+ let Some(server_selector) = self
+ .server_info(self.selected_index)
+ .map(|info| info.server_selector())
+ else {
+ return;
+ };
+ let lsp_logs = cx.global::<GlobalLogStore>().0.clone();
+ let lsp_store = self.state.read(cx).lsp_store.clone();
+ let workspace = self.state.read(cx).workspace.clone();
+ lsp_logs
+ .update(cx, |lsp_logs, cx| {
+ let has_logs = lsp_store
+ .update(cx, |lsp_store, _| {
+ lsp_store.as_local().is_some() && lsp_logs.has_server_logs(&server_selector)
+ })
+ .unwrap_or(false);
+ if has_logs {
+ lsp_logs.open_server_trace(workspace, server_selector, window, cx);
+ }
+ })
+ .ok();
+ }
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
cx.emit(DismissEvent);
@@ -334,61 +510,47 @@ impl PickerDelegate for LspPickerDelegate {
fn render_match(
&self,
ix: usize,
- _: bool,
+ selected: bool,
_: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
- let is_other_server = self
- .other_servers_start_index
- .map_or(false, |start| ix >= start);
- let server_binary_status;
- let server_health;
- let server_message;
- let server_id;
- let server_name;
- match self.items.get(ix)? {
- LspItem::WithHealthCheck(
- language_server_id,
- language_server_health_status,
- language_server_binary_status,
- ) => {
- server_binary_status = language_server_binary_status.as_ref();
- server_health = language_server_health_status.health();
- server_message = language_server_health_status.message();
- server_id = Some(*language_server_id);
- server_name = language_server_health_status.name.clone();
- }
- LspItem::WithBinaryStatus(language_server_name, language_server_binary_status) => {
- server_binary_status = Some(language_server_binary_status);
- server_health = None;
- server_message = language_server_binary_status.message.clone();
- server_id = None;
- server_name = language_server_name.clone();
- }
- LspItem::Header(header) => {
- return Some(
- h_flex()
- .justify_center()
- .child(Label::new(header.clone()))
- .into_any_element(),
- );
- }
- };
+ let rendered_match = h_flex().px_1().gap_1();
+ let rendered_match_contents = h_flex()
+ .id(("lsp-item", ix))
+ .w_full()
+ .px_2()
+ .gap_2()
+ .when(selected, |server_entry| {
+ server_entry.bg(cx.theme().colors().element_hover)
+ })
+ .hover(|s| s.bg(cx.theme().colors().element_hover));
+ if let Some(LspItem::ToggleServersButton { restart }) = self.items.get(ix) {
+ let label = Label::new(if *restart {
+ "Restart All Servers"
+ } else {
+ "Stop All Servers"
+ });
+ return Some(
+ rendered_match
+ .child(rendered_match_contents.child(label))
+ .into_any_element(),
+ );
+ }
+
+ let server_info = self.server_info(ix)?;
let workspace = self.state.read(cx).workspace.clone();
let lsp_logs = cx.global::<GlobalLogStore>().0.upgrade()?;
let lsp_store = self.state.read(cx).lsp_store.upgrade()?;
- let server_selector = server_id
- .map(LanguageServerSelector::Id)
- .unwrap_or_else(|| LanguageServerSelector::Name(server_name.clone()));
- let can_stop = server_binary_status.is_none_or(|status| {
- matches!(status.status, BinaryStatus::None | BinaryStatus::Starting)
- });
+ let server_selector = server_info.server_selector();
+
// TODO currently, Zed remote does not work well with the LSP logs
// https://github.com/zed-industries/zed/issues/28557
let has_logs = lsp_store.read(cx).as_local().is_some()
&& lsp_logs.read(cx).has_server_logs(&server_selector);
- let status_color = server_binary_status
+
+ let status_color = server_info
+ .binary_status
.and_then(|binary_status| match binary_status.status {
BinaryStatus::None => None,
BinaryStatus::CheckingForUpdate
@@ -399,7 +561,7 @@ impl PickerDelegate for LspPickerDelegate {
BinaryStatus::Failed { .. } => Some(Color::Error),
})
.or_else(|| {
- Some(match server_health? {
+ Some(match server_info.health? {
ServerHealth::Ok => Color::Success,
ServerHealth::Warning => Color::Warning,
ServerHealth::Error => Color::Error,
@@ -408,152 +570,41 @@ impl PickerDelegate for LspPickerDelegate {
.unwrap_or(Color::Success);
Some(
- h_flex()
- .w_full()
- .justify_between()
- .gap_2()
+ rendered_match
.child(
- h_flex()
- .id("server-status-indicator")
- .gap_2()
+ rendered_match_contents
.child(Indicator::dot().color(status_color))
- .child(Label::new(server_name.0.clone()))
- .when_some(server_message.clone(), |div, server_message| {
- div.tooltip(move |_, cx| Tooltip::simple(server_message.clone(), cx))
- }),
+ .child(Label::new(server_info.name.0.clone()))
+ .when_some(
+ server_info.message.clone(),
+ |server_entry, server_message| {
+ server_entry.tooltip(Tooltip::text(server_message.clone()))
+ },
+ ),
)
- .child(
- h_flex()
- .gap_1()
- .when(has_logs, |div| {
- div.child(
- IconButton::new("debug-language-server", IconName::MessageBubbles)
- .icon_size(IconSize::XSmall)
- .tooltip(|_, cx| Tooltip::simple("Debug Language Server", cx))
- .on_click({
- let workspace = workspace.clone();
- let lsp_logs = lsp_logs.downgrade();
- let server_selector = server_selector.clone();
- move |_, window, cx| {
- lsp_logs
- .update(cx, |lsp_logs, cx| {
- lsp_logs.open_server_trace(
- workspace.clone(),
- server_selector.clone(),
- window,
- cx,
- );
- })
- .ok();
- }
- }),
- )
- })
- .when(can_stop, |div| {
- div.child(
- IconButton::new("stop-server", IconName::Stop)
- .icon_size(IconSize::Small)
- .tooltip(|_, cx| Tooltip::simple("Stop server", cx))
- .on_click({
- let lsp_store = lsp_store.downgrade();
- let server_selector = server_selector.clone();
- move |_, _, cx| {
- lsp_store
- .update(cx, |lsp_store, cx| {
- lsp_store.stop_language_servers_for_buffers(
- Vec::new(),
- HashSet::from_iter([
- server_selector.clone()
- ]),
- cx,
- );
- })
- .ok();
- }
- }),
- )
+ .when_else(
+ has_logs,
+ |server_entry| {
+ server_entry.on_mouse_down(MouseButton::Left, {
+ let workspace = workspace.clone();
+ let lsp_logs = lsp_logs.downgrade();
+ let server_selector = server_selector.clone();
+ move |_, window, cx| {
+ lsp_logs
+ .update(cx, |lsp_logs, cx| {
+ lsp_logs.open_server_trace(
+ workspace.clone(),
+ server_selector.clone(),
+ window,
+ cx,
+ );
+ })
+ .ok();
+ }
})
- .child(
- IconButton::new("restart-server", IconName::Rerun)
- .icon_size(IconSize::XSmall)
- .tooltip(|_, cx| Tooltip::simple("Restart server", cx))
- .on_click({
- let state = self.state.clone();
- let workspace = workspace.clone();
- let lsp_store = lsp_store.downgrade();
- let editor_buffers = state
- .read(cx)
- .active_editor
- .as_ref()
- .map(|active_editor| active_editor.editor_buffers.clone())
- .unwrap_or_default();
- let server_selector = server_selector.clone();
- move |_, _, cx| {
- if let Some(workspace) = workspace.upgrade() {
- let project = workspace.read(cx).project().clone();
- let buffer_store =
- project.read(cx).buffer_store().clone();
- let buffers = if is_other_server {
- let worktree_store =
- project.read(cx).worktree_store();
- state
- .read(cx)
- .language_servers
- .servers_per_buffer_abs_path
- .iter()
- .filter_map(|(abs_path, servers)| {
- if servers.values().any(|server| {
- server.as_ref() == Some(&server_name)
- }) {
- worktree_store
- .read(cx)
- .find_worktree(abs_path, cx)
- } else {
- None
- }
- })
- .filter_map(|(worktree, relative_path)| {
- let entry = worktree
- .read(cx)
- .entry_for_path(&relative_path)?;
- project
- .read(cx)
- .path_for_entry(entry.id, cx)
- })
- .filter_map(|project_path| {
- buffer_store
- .read(cx)
- .get_by_path(&project_path)
- })
- .collect::<Vec<_>>()
- } else {
- editor_buffers
- .iter()
- .flat_map(|buffer_id| {
- buffer_store.read(cx).get(*buffer_id)
- })
- .collect::<Vec<_>>()
- };
- if !buffers.is_empty() {
- lsp_store
- .update(cx, |lsp_store, cx| {
- lsp_store
- .restart_language_servers_for_buffers(
- buffers,
- HashSet::from_iter([
- server_selector.clone(),
- ]),
- cx,
- );
- })
- .ok();
- }
- }
- }
- }),
- ),
+ },
+ |div| div.cursor_default(),
)
- .cursor_default()
.into_any_element(),
)
}
@@ -567,56 +618,28 @@ impl PickerDelegate for LspPickerDelegate {
div().child(div().track_focus(&editor.focus_handle(cx)))
}
- fn render_footer(&self, _: &mut Window, cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
- if self.items.is_empty() {
- Some(
- h_flex()
- .w_full()
- .border_color(cx.theme().colors().border_variant)
- .child(
- Button::new("stop-all-servers", "Stop all servers")
- .disabled(true)
- .on_click(move |_, _, _| {})
- .full_width(),
- )
- .into_any_element(),
- )
- } else {
- let lsp_store = self.state.read(cx).lsp_store.clone();
- Some(
- h_flex()
- .w_full()
- .border_color(cx.theme().colors().border_variant)
- .child(
- Button::new("stop-all-servers", "Stop all servers")
- .on_click({
- move |_, _, cx| {
- lsp_store
- .update(cx, |lsp_store, cx| {
- lsp_store.stop_all_language_servers(cx);
- })
- .ok();
- }
- })
- .full_width(),
- )
- .into_any_element(),
- )
- }
- }
-
fn separators_after_indices(&self) -> Vec<usize> {
if self.items.is_empty() {
- Vec::new()
- } else {
- vec![self.items.len() - 1]
+ return Vec::new();
}
+ let mut indices = vec![self.items.len().saturating_sub(2)];
+ if let Some(other_servers_start_index) = self.other_servers_start_index {
+ if other_servers_start_index > 0 {
+ indices.insert(0, other_servers_start_index - 1);
+ indices.dedup();
+ }
+ }
+ indices
}
}
-// TODO kb keyboard story
impl LspTool {
- pub fn new(workspace: &Workspace, window: &mut Window, cx: &mut Context<Self>) -> Self {
+ pub fn new(
+ workspace: &Workspace,
+ popover_menu_handle: PopoverMenuHandle<Picker<LspPickerDelegate>>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Self {
let settings_subscription =
cx.observe_global_in::<SettingsStore>(window, move |lsp_tool, window, cx| {
if ProjectSettings::get_global(cx).global_lsp_settings.button {
@@ -646,6 +669,7 @@ impl LspTool {
Self {
state,
+ popover_menu_handle,
lsp_picker: None,
_subscriptions: vec![settings_subscription, lsp_store_subscription],
}
@@ -865,6 +889,10 @@ impl StatusItemView for LspTool {
impl Render for LspTool {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
+ if !cx.is_staff() || self.state.read(cx).language_servers.is_empty() {
+ return div();
+ }
+
let Some(lsp_picker) = self.lsp_picker.clone() else {
return div();
};
@@ -902,15 +930,15 @@ impl Render for LspTool {
div().child(
PickerPopoverMenu::new(
lsp_picker.clone(),
- IconButton::new("zed-lsp-tool-button", IconName::Bolt)
+ IconButton::new("zed-lsp-tool-button", IconName::BoltFilledAlt)
.when_some(indicator, IconButton::indicator)
- .shape(IconButtonShape::Square)
- .icon_size(IconSize::XSmall)
+ .icon_size(IconSize::Small)
.indicator_border_color(Some(cx.theme().colors().status_bar_background)),
- move |_, cx| Tooltip::simple("Language servers", cx),
- Corner::BottomRight,
+ move |window, cx| Tooltip::for_action("Language Servers", &ToggleMenu, window, cx),
+ Corner::BottomLeft,
cx,
)
+ .with_handle(self.popover_menu_handle.clone())
.render(window, cx),
)
}
@@ -1,4 +1,4 @@
-use editor::{Anchor, Editor, ExcerptId, scroll::Autoscroll};
+use editor::{Anchor, Editor, ExcerptId, SelectionEffects, scroll::Autoscroll};
use gpui::{
App, AppContext as _, Context, Div, Entity, EventEmitter, FocusHandle, Focusable, Hsla,
InteractiveElement, IntoElement, MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement,
@@ -340,7 +340,7 @@ impl Render for SyntaxTreeView {
mem::swap(&mut range.start, &mut range.end);
editor.change_selections(
- Some(Autoscroll::newest()),
+ SelectionEffects::scroll(Autoscroll::newest()),
window, cx,
|selections| {
selections.select_ranges(vec![range]);
@@ -16,3 +16,9 @@ brackets = [
{ start = "{", end = "}", close = true, newline = false },
{ start = "[", end = "]", close = true, newline = false },
]
+rewrap_prefixes = [
+ "[-*+]\\s+",
+ "\\d+\\.\\s+",
+ ">\\s*",
+ "[-*+]\\s+\\[[\\sx]\\]\\s+"
+]
@@ -269,10 +269,9 @@ impl JsonLspAdapter {
#[cfg(debug_assertions)]
fn generate_inspector_style_schema() -> serde_json_lenient::Value {
- let schema = schemars::r#gen::SchemaSettings::draft07()
- .with(|settings| settings.option_add_null_type = false)
+ let schema = schemars::generate::SchemaSettings::draft2019_09()
.into_generator()
- .into_root_schema_for::<gpui::StyleRefinement>();
+ .root_schema_for::<gpui::StyleRefinement>();
serde_json_lenient::to_value(schema).unwrap()
}
@@ -3,7 +3,7 @@ grammar = "markdown"
path_suffixes = ["md", "mdx", "mdwn", "markdown", "MD"]
completion_query_characters = ["-"]
block_comment = ["<!-- ", " -->"]
-autoclose_before = "}])>"
+autoclose_before = ";:.,=}])>"
brackets = [
{ start = "{", end = "}", close = true, newline = true },
{ start = "[", end = "]", close = true, newline = true },
@@ -13,6 +13,12 @@ brackets = [
{ start = "'", end = "'", close = false, newline = false },
{ start = "`", end = "`", close = false, newline = false },
]
+rewrap_prefixes = [
+ "[-*+]\\s+",
+ "\\d+\\.\\s+",
+ ">\\s*",
+ "[-*+]\\s+\\[[\\sx]\\]\\s+"
+]
auto_indent_on_paste = false
auto_indent_using_last_non_empty_line = false
@@ -226,6 +226,12 @@
">>"
"|"
"~"
+ "&="
+ "<<="
+ ">>="
+ "@="
+ "^="
+ "|="
] @operator
[
@@ -767,8 +767,8 @@ pub struct EsLintLspAdapter {
}
impl EsLintLspAdapter {
- const CURRENT_VERSION: &'static str = "3.0.10";
- const CURRENT_VERSION_TAG_NAME: &'static str = "release/3.0.10";
+ const CURRENT_VERSION: &'static str = "2.4.4";
+ const CURRENT_VERSION_TAG_NAME: &'static str = "release/2.4.4";
#[cfg(not(windows))]
const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
@@ -846,7 +846,9 @@ impl LspAdapter for EsLintLspAdapter {
"enable": true
}
},
- "useFlatConfig": use_flat_config,
+ "experimental": {
+ "useFlatConfig": use_flat_config,
+ },
});
let override_options = cx.update(|cx| {
@@ -15,11 +15,7 @@ use gpui::{App, AppContext as _, AsyncApp, BackgroundExecutor, SharedString, Tas
use notification::DidChangeWorkspaceFolders;
use parking_lot::{Mutex, RwLock};
use postage::{barrier, prelude::Stream};
-use schemars::{
- JsonSchema,
- r#gen::SchemaGenerator,
- schema::{InstanceType, Schema, SchemaObject},
-};
+use schemars::JsonSchema;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use serde_json::{Value, json, value::RawValue};
use smol::{
@@ -130,7 +126,10 @@ impl LanguageServerId {
}
/// A name of a language server.
-#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
+#[derive(
+ Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, JsonSchema,
+)]
+#[serde(transparent)]
pub struct LanguageServerName(pub SharedString);
impl std::fmt::Display for LanguageServerName {
@@ -151,20 +150,6 @@ impl AsRef<OsStr> for LanguageServerName {
}
}
-impl JsonSchema for LanguageServerName {
- fn schema_name() -> String {
- "LanguageServerName".into()
- }
-
- fn json_schema(_: &mut SchemaGenerator) -> Schema {
- SchemaObject {
- instance_type: Some(InstanceType::String.into()),
- ..Default::default()
- }
- .into()
- }
-}
-
impl LanguageServerName {
pub const fn new_static(s: &'static str) -> Self {
Self(SharedString::new_static(s))
@@ -107,11 +107,7 @@ impl Render for MarkdownExample {
..Default::default()
},
syntax: cx.theme().syntax().clone(),
- selection_background_color: {
- let mut selection = cx.theme().players().local().selection;
- selection.fade_out(0.7);
- selection
- },
+ selection_background_color: cx.theme().colors().element_selection_background,
..Default::default()
};
@@ -91,11 +91,7 @@ impl Render for HelloWorld {
..Default::default()
},
syntax: cx.theme().syntax().clone(),
- selection_background_color: {
- let mut selection = cx.theme().players().local().selection;
- selection.fade_out(0.7);
- selection
- },
+ selection_background_color: cx.theme().colors().element_selection_background,
heading: Default::default(),
..Default::default()
};
@@ -504,7 +504,6 @@ impl MarkdownElement {
let selection = self.markdown.read(cx).selection;
let selection_start = rendered_text.position_for_source_index(selection.start);
let selection_end = rendered_text.position_for_source_index(selection.end);
-
if let Some(((start_position, start_line_height), (end_position, end_line_height))) =
selection_start.zip(selection_end)
{
@@ -4,7 +4,7 @@ use std::{ops::Range, path::PathBuf};
use anyhow::Result;
use editor::scroll::Autoscroll;
-use editor::{Editor, EditorEvent};
+use editor::{Editor, EditorEvent, SelectionEffects};
use gpui::{
App, ClickEvent, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
IntoElement, ListState, ParentElement, Render, RetainAllImageCache, Styled, Subscription, Task,
@@ -17,10 +17,9 @@ use ui::prelude::*;
use workspace::item::{Item, ItemHandle};
use workspace::{Pane, Workspace};
-use crate::OpenPreviewToTheSide;
use crate::markdown_elements::ParsedMarkdownElement;
use crate::{
- OpenFollowingPreview, OpenPreview,
+ OpenFollowingPreview, OpenPreview, OpenPreviewToTheSide,
markdown_elements::ParsedMarkdown,
markdown_parser::parse_markdown,
markdown_renderer::{RenderContext, render_markdown_block},
@@ -36,7 +35,6 @@ pub struct MarkdownPreviewView {
contents: Option<ParsedMarkdown>,
selected_block: usize,
list_state: ListState,
- tab_content_text: Option<SharedString>,
language_registry: Arc<LanguageRegistry>,
parsing_markdown_task: Option<Task<Result<()>>>,
mode: MarkdownPreviewMode,
@@ -173,7 +171,6 @@ impl MarkdownPreviewView {
editor,
workspace_handle,
language_registry,
- None,
window,
cx,
)
@@ -192,7 +189,6 @@ impl MarkdownPreviewView {
editor,
workspace_handle,
language_registry,
- None,
window,
cx,
)
@@ -203,7 +199,6 @@ impl MarkdownPreviewView {
active_editor: Entity<Editor>,
workspace: WeakEntity<Workspace>,
language_registry: Arc<LanguageRegistry>,
- tab_content_text: Option<SharedString>,
window: &mut Window,
cx: &mut Context<Workspace>,
) -> Entity<Self> {
@@ -324,7 +319,6 @@ impl MarkdownPreviewView {
workspace: workspace.clone(),
contents: None,
list_state,
- tab_content_text,
language_registry,
parsing_markdown_task: None,
image_cache: RetainAllImageCache::new(cx),
@@ -405,12 +399,6 @@ impl MarkdownPreviewView {
},
);
- let tab_content = editor.read(cx).tab_content_text(0, cx);
-
- if self.tab_content_text.is_none() {
- self.tab_content_text = Some(format!("Preview {}", tab_content).into());
- }
-
self.active_editor = Some(EditorState {
editor,
_subscription: subscription,
@@ -480,9 +468,12 @@ impl MarkdownPreviewView {
) {
if let Some(state) = &self.active_editor {
state.editor.update(cx, |editor, cx| {
- editor.change_selections(Some(Autoscroll::center()), window, cx, |selections| {
- selections.select_ranges(vec![selection])
- });
+ editor.change_selections(
+ SelectionEffects::scroll(Autoscroll::center()),
+ window,
+ cx,
+ |selections| selections.select_ranges(vec![selection]),
+ );
window.focus(&editor.focus_handle(cx));
});
}
@@ -547,21 +538,28 @@ impl Focusable for MarkdownPreviewView {
}
}
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub enum PreviewEvent {}
-
-impl EventEmitter<PreviewEvent> for MarkdownPreviewView {}
+impl EventEmitter<()> for MarkdownPreviewView {}
impl Item for MarkdownPreviewView {
- type Event = PreviewEvent;
+ type Event = ();
fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
Some(Icon::new(IconName::FileDoc))
}
- fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
- self.tab_content_text
- .clone()
+ fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
+ self.active_editor
+ .as_ref()
+ .and_then(|editor_state| {
+ let buffer = editor_state.editor.read(cx).buffer().read(cx);
+ let buffer = buffer.as_singleton()?;
+ let file = buffer.read(cx).file()?;
+ let local_file = file.as_local()?;
+ local_file
+ .abs_path(cx)
+ .file_name()
+ .map(|name| format!("Preview {}", name.to_string_lossy()).into())
+ })
.unwrap_or_else(|| SharedString::from("Markdown Preview"))
}
@@ -82,7 +82,7 @@ pub(crate) mod m_2025_06_16 {
pub(crate) use settings::SETTINGS_PATTERNS;
}
-pub(crate) mod m_2025_06_25 {
+pub(crate) mod m_2025_06_27 {
mod settings;
pub(crate) use settings::SETTINGS_PATTERNS;
@@ -1,133 +0,0 @@
-use std::ops::Range;
-use tree_sitter::{Query, QueryMatch};
-
-use crate::MigrationPatterns;
-
-pub const SETTINGS_PATTERNS: MigrationPatterns = &[
- (SETTINGS_VERSION_PATTERN, remove_version_fields),
- (
- SETTINGS_NESTED_VERSION_PATTERN,
- remove_nested_version_fields,
- ),
-];
-
-const SETTINGS_VERSION_PATTERN: &str = r#"(document
- (object
- (pair
- key: (string (string_content) @key)
- value: (object
- (pair
- key: (string (string_content) @version_key)
- value: (_) @version_value
- ) @version_pair
- )
- )
- )
- (#eq? @key "agent")
- (#eq? @version_key "version")
-)"#;
-
-const SETTINGS_NESTED_VERSION_PATTERN: &str = r#"(document
- (object
- (pair
- key: (string (string_content) @language_models)
- value: (object
- (pair
- key: (string (string_content) @provider)
- value: (object
- (pair
- key: (string (string_content) @version_key)
- value: (_) @version_value
- ) @version_pair
- )
- )
- )
- )
- )
- (#eq? @language_models "language_models")
- (#match? @provider "^(anthropic|openai)$")
- (#eq? @version_key "version")
-)"#;
-
-fn remove_version_fields(
- contents: &str,
- mat: &QueryMatch,
- query: &Query,
-) -> Option<(Range<usize>, String)> {
- let version_pair_ix = query.capture_index_for_name("version_pair")?;
- let version_pair_node = mat.nodes_for_capture_index(version_pair_ix).next()?;
-
- remove_pair_with_whitespace(contents, version_pair_node)
-}
-
-fn remove_nested_version_fields(
- contents: &str,
- mat: &QueryMatch,
- query: &Query,
-) -> Option<(Range<usize>, String)> {
- let version_pair_ix = query.capture_index_for_name("version_pair")?;
- let version_pair_node = mat.nodes_for_capture_index(version_pair_ix).next()?;
-
- remove_pair_with_whitespace(contents, version_pair_node)
-}
-
-fn remove_pair_with_whitespace(
- contents: &str,
- pair_node: tree_sitter::Node,
-) -> Option<(Range<usize>, String)> {
- let mut range_to_remove = pair_node.byte_range();
-
- // Check if there's a comma after this pair
- if let Some(next_sibling) = pair_node.next_sibling() {
- if next_sibling.kind() == "," {
- range_to_remove.end = next_sibling.end_byte();
- }
- } else {
- // If no next sibling, check if there's a comma before
- if let Some(prev_sibling) = pair_node.prev_sibling() {
- if prev_sibling.kind() == "," {
- range_to_remove.start = prev_sibling.start_byte();
- }
- }
- }
-
- // Include any leading whitespace/newline, including comments
- let text_before = &contents[..range_to_remove.start];
- if let Some(last_newline) = text_before.rfind('\n') {
- let whitespace_start = last_newline + 1;
- let potential_whitespace = &contents[whitespace_start..range_to_remove.start];
-
- // Check if it's only whitespace or comments
- let mut is_whitespace_or_comment = true;
- let mut in_comment = false;
- let mut chars = potential_whitespace.chars().peekable();
-
- while let Some(ch) = chars.next() {
- if in_comment {
- if ch == '\n' {
- in_comment = false;
- }
- } else if ch == '/' && chars.peek() == Some(&'/') {
- in_comment = true;
- chars.next(); // Skip the second '/'
- } else if !ch.is_whitespace() {
- is_whitespace_or_comment = false;
- break;
- }
- }
-
- if is_whitespace_or_comment {
- range_to_remove.start = whitespace_start;
- }
- }
-
- // Also check if we need to include trailing whitespace up to the next line
- let text_after = &contents[range_to_remove.end..];
- if let Some(newline_pos) = text_after.find('\n') {
- if text_after[..newline_pos].chars().all(|c| c.is_whitespace()) {
- range_to_remove.end += newline_pos + 1;
- }
- }
-
- Some((range_to_remove, String::new()))
-}
@@ -0,0 +1,133 @@
+use std::ops::Range;
+use tree_sitter::{Query, QueryMatch};
+
+use crate::MigrationPatterns;
+
+pub const SETTINGS_PATTERNS: MigrationPatterns = &[(
+ SETTINGS_CONTEXT_SERVER_PATTERN,
+ flatten_context_server_command,
+)];
+
+const SETTINGS_CONTEXT_SERVER_PATTERN: &str = r#"(document
+ (object
+ (pair
+ key: (string (string_content) @context-servers)
+ value: (object
+ (pair
+ key: (string (string_content) @server-name)
+ value: (object
+ (pair
+ key: (string (string_content) @source-key)
+ value: (string (string_content) @source-value)
+ )
+ (pair
+ key: (string (string_content) @command-key)
+ value: (object) @command-object
+ ) @command-pair
+ ) @server-settings
+ )
+ )
+ )
+ )
+ (#eq? @context-servers "context_servers")
+ (#eq? @source-key "source")
+ (#eq? @source-value "custom")
+ (#eq? @command-key "command")
+)"#;
+
+fn flatten_context_server_command(
+ contents: &str,
+ mat: &QueryMatch,
+ query: &Query,
+) -> Option<(Range<usize>, String)> {
+ let command_pair_index = query.capture_index_for_name("command-pair")?;
+ let command_pair = mat.nodes_for_capture_index(command_pair_index).next()?;
+
+ let command_object_index = query.capture_index_for_name("command-object")?;
+ let command_object = mat.nodes_for_capture_index(command_object_index).next()?;
+
+ let server_settings_index = query.capture_index_for_name("server-settings")?;
+ let _server_settings = mat.nodes_for_capture_index(server_settings_index).next()?;
+
+ // Parse the command object to extract path, args, and env
+ let mut path_value = None;
+ let mut args_value = None;
+ let mut env_value = None;
+
+ let mut cursor = command_object.walk();
+ for child in command_object.children(&mut cursor) {
+ if child.kind() == "pair" {
+ if let Some(key_node) = child.child_by_field_name("key") {
+ if let Some(string_content) = key_node.child(1) {
+ let key = &contents[string_content.byte_range()];
+ if let Some(value_node) = child.child_by_field_name("value") {
+ let value_range = value_node.byte_range();
+ match key {
+ "path" => path_value = Some(&contents[value_range]),
+ "args" => args_value = Some(&contents[value_range]),
+ "env" => env_value = Some(&contents[value_range]),
+ _ => {}
+ }
+ }
+ }
+ }
+ }
+ }
+
+ let path = path_value?;
+
+ // Get the proper indentation from the command pair
+ let command_pair_start = command_pair.start_byte();
+ let line_start = contents[..command_pair_start]
+ .rfind('\n')
+ .map(|pos| pos + 1)
+ .unwrap_or(0);
+ let indent = &contents[line_start..command_pair_start];
+
+ // Build the replacement string
+ let mut replacement = format!("\"command\": {}", path);
+
+ // Add args if present - need to reduce indentation
+ if let Some(args) = args_value {
+ replacement.push_str(",\n");
+ replacement.push_str(indent);
+ replacement.push_str("\"args\": ");
+ let reduced_args = reduce_indentation(args, 4);
+ replacement.push_str(&reduced_args);
+ }
+
+ // Add env if present - need to reduce indentation
+ if let Some(env) = env_value {
+ replacement.push_str(",\n");
+ replacement.push_str(indent);
+ replacement.push_str("\"env\": ");
+ replacement.push_str(&reduce_indentation(env, 4));
+ }
+
+ let range_to_replace = command_pair.byte_range();
+ Some((range_to_replace, replacement))
+}
+
+fn reduce_indentation(text: &str, spaces: usize) -> String {
+ let lines: Vec<&str> = text.lines().collect();
+ let mut result = String::new();
+
+ for (i, line) in lines.iter().enumerate() {
+ if i > 0 {
+ result.push('\n');
+ }
+
+ // Count leading spaces
+ let leading_spaces = line.chars().take_while(|&c| c == ' ').count();
+
+ if leading_spaces >= spaces {
+ // Reduce indentation
+ result.push_str(&line[spaces..]);
+ } else {
+ // Keep line as is if it doesn't have enough indentation
+ result.push_str(line);
+ }
+ }
+
+ result
+}
@@ -153,8 +153,8 @@ pub fn migrate_settings(text: &str) -> Result<Option<String>> {
&SETTINGS_QUERY_2025_06_16,
),
(
- migrations::m_2025_06_25::SETTINGS_PATTERNS,
- &SETTINGS_QUERY_2025_06_25,
+ migrations::m_2025_06_27::SETTINGS_PATTERNS,
+ &SETTINGS_QUERY_2025_06_27,
),
];
run_migrations(text, migrations)
@@ -259,8 +259,8 @@ define_query!(
migrations::m_2025_06_16::SETTINGS_PATTERNS
);
define_query!(
- SETTINGS_QUERY_2025_06_25,
- migrations::m_2025_06_25::SETTINGS_PATTERNS
+ SETTINGS_QUERY_2025_06_27,
+ migrations::m_2025_06_27::SETTINGS_PATTERNS
);
// custom query
@@ -286,6 +286,15 @@ mod tests {
pretty_assertions::assert_eq!(migrated.as_deref(), output);
}
+ fn assert_migrate_settings_with_migrations(
+ migrations: &[(MigrationPatterns, &Query)],
+ input: &str,
+ output: Option<&str>,
+ ) {
+ let migrated = run_migrations(input, migrations).unwrap();
+ pretty_assertions::assert_eq!(migrated.as_deref(), output);
+ }
+
#[test]
fn test_replace_array_with_single_string() {
assert_migrate_keymap(
@@ -873,7 +882,11 @@ mod tests {
#[test]
fn test_mcp_settings_migration() {
- assert_migrate_settings(
+ assert_migrate_settings_with_migrations(
+ &[(
+ migrations::m_2025_06_16::SETTINGS_PATTERNS,
+ &SETTINGS_QUERY_2025_06_16,
+ )],
r#"{
"context_servers": {
"empty_server": {},
@@ -1058,77 +1071,109 @@ mod tests {
}
}
}"#;
- assert_migrate_settings(settings, None);
+ assert_migrate_settings_with_migrations(
+ &[(
+ migrations::m_2025_06_16::SETTINGS_PATTERNS,
+ &SETTINGS_QUERY_2025_06_16,
+ )],
+ settings,
+ None,
+ );
}
#[test]
- fn test_remove_version_fields() {
+ fn test_flatten_context_server_command() {
assert_migrate_settings(
r#"{
- "language_models": {
- "anthropic": {
- "version": "1",
- "api_url": "https://api.anthropic.com"
- },
- "openai": {
- "version": "1",
- "api_url": "https://api.openai.com/v1"
- }
- },
- "agent": {
- "version": "2",
- "enabled": true,
- "preferred_completion_mode": "normal",
- "button": true,
- "dock": "right",
- "default_width": 640,
- "default_height": 320,
- "default_model": {
- "provider": "zed.dev",
- "model": "claude-sonnet-4"
+ "context_servers": {
+ "some-mcp-server": {
+ "source": "custom",
+ "command": {
+ "path": "npx",
+ "args": [
+ "-y",
+ "@supabase/mcp-server-supabase@latest",
+ "--read-only",
+ "--project-ref=<project-ref>"
+ ],
+ "env": {
+ "SUPABASE_ACCESS_TOKEN": "<personal-access-token>"
+ }
+ }
}
}
}"#,
Some(
r#"{
- "language_models": {
- "anthropic": {
- "api_url": "https://api.anthropic.com"
- },
- "openai": {
- "api_url": "https://api.openai.com/v1"
+ "context_servers": {
+ "some-mcp-server": {
+ "source": "custom",
+ "command": "npx",
+ "args": [
+ "-y",
+ "@supabase/mcp-server-supabase@latest",
+ "--read-only",
+ "--project-ref=<project-ref>"
+ ],
+ "env": {
+ "SUPABASE_ACCESS_TOKEN": "<personal-access-token>"
+ }
}
- },
- "agent": {
- "enabled": true,
- "preferred_completion_mode": "normal",
- "button": true,
- "dock": "right",
- "default_width": 640,
- "default_height": 320,
- "default_model": {
- "provider": "zed.dev",
- "model": "claude-sonnet-4"
+ }
+}"#,
+ ),
+ );
+
+ // Test with additional keys in server object
+ assert_migrate_settings(
+ r#"{
+ "context_servers": {
+ "server-with-extras": {
+ "source": "custom",
+ "command": {
+ "path": "/usr/bin/node",
+ "args": ["server.js"]
+ },
+ "settings": {}
+ }
+ }
+}"#,
+ Some(
+ r#"{
+ "context_servers": {
+ "server-with-extras": {
+ "source": "custom",
+ "command": "/usr/bin/node",
+ "args": ["server.js"],
+ "settings": {}
}
}
}"#,
),
);
- // Test that version fields in other contexts are not removed
+ // Test command without args or env
assert_migrate_settings(
r#"{
- "language_models": {
- "other_provider": {
- "version": "1",
- "api_url": "https://api.example.com"
+ "context_servers": {
+ "simple-server": {
+ "source": "custom",
+ "command": {
+ "path": "simple-mcp-server"
+ }
}
- },
- "other_section": {
- "version": "1"
}
}"#,
- None,
+ Some(
+ r#"{
+ "context_servers": {
+ "simple-server": {
+ "source": "custom",
+ "command": "simple-mcp-server"
+ }
+ }
+}"#,
+ ),
);
}
}
@@ -126,17 +126,17 @@ impl<T> Default for TypedRow<T> {
impl<T> PartialOrd for TypedOffset<T> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
- Some(self.value.cmp(&other.value))
+ Some(self.cmp(&other))
}
}
impl<T> PartialOrd for TypedPoint<T> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
- Some(self.value.cmp(&other.value))
+ Some(self.cmp(&other))
}
}
impl<T> PartialOrd for TypedRow<T> {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
- Some(self.value.cmp(&other.value))
+ Some(self.cmp(&other))
}
}
@@ -445,12 +445,14 @@ pub async fn stream_completion(
match serde_json::from_str::<OpenAiResponse>(&body) {
Ok(response) if !response.error.message.is_empty() => Err(anyhow!(
- "Failed to connect to OpenAI API: {}",
+ "API request to {} failed: {}",
+ api_url,
response.error.message,
)),
_ => anyhow::bail!(
- "Failed to connect to OpenAI API: {} {}",
+ "API request to {} failed with status {}: {}",
+ api_url,
response.status(),
body,
),
@@ -4,8 +4,8 @@ use std::{
sync::Arc,
};
-use editor::RowHighlightOptions;
use editor::{Anchor, AnchorRangeExt, Editor, scroll::Autoscroll};
+use editor::{RowHighlightOptions, SelectionEffects};
use fuzzy::StringMatch;
use gpui::{
App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, HighlightStyle,
@@ -288,9 +288,12 @@ impl PickerDelegate for OutlineViewDelegate {
.highlighted_rows::<OutlineRowHighlights>()
.next();
if let Some((rows, _)) = highlight {
- active_editor.change_selections(Some(Autoscroll::center()), window, cx, |s| {
- s.select_ranges([rows.start..rows.start])
- });
+ active_editor.change_selections(
+ SelectionEffects::scroll(Autoscroll::center()),
+ window,
+ cx,
+ |s| s.select_ranges([rows.start..rows.start]),
+ );
active_editor.clear_row_highlights::<OutlineRowHighlights>();
window.focus(&active_editor.focus_handle(cx));
}
@@ -19,10 +19,10 @@ use collections::{BTreeSet, HashMap, HashSet, hash_map};
use db::kvp::KEY_VALUE_STORE;
use editor::{
AnchorRangeExt, Bias, DisplayPoint, Editor, EditorEvent, EditorSettings, ExcerptId,
- ExcerptRange, MultiBufferSnapshot, RangeToAnchorExt, ShowScrollbar,
+ ExcerptRange, MultiBufferSnapshot, RangeToAnchorExt, SelectionEffects, ShowScrollbar,
display_map::ToDisplayPoint,
items::{entry_git_aware_label_color, entry_label_color},
- scroll::{Autoscroll, AutoscrollStrategy, ScrollAnchor, ScrollbarAutoHide},
+ scroll::{Autoscroll, ScrollAnchor, ScrollbarAutoHide},
};
use file_icons::FileIcons;
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
@@ -1099,7 +1099,7 @@ impl OutlinePanel {
if change_selection {
active_editor.update(cx, |editor, cx| {
editor.change_selections(
- Some(Autoscroll::Strategy(AutoscrollStrategy::Center, None)),
+ SelectionEffects::scroll(Autoscroll::center()),
window,
cx,
|s| s.select_ranges(Some(anchor..anchor)),
@@ -4,7 +4,7 @@ pub mod popover_menu;
use anyhow::Result;
use editor::{
- Editor,
+ Editor, SelectionEffects,
actions::{MoveDown, MoveUp},
scroll::Autoscroll,
};
@@ -695,9 +695,12 @@ impl<D: PickerDelegate> Picker<D> {
editor.update(cx, |editor, cx| {
editor.set_text(query, window, cx);
let editor_offset = editor.buffer().read(cx).len(cx);
- editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
- s.select_ranges(Some(editor_offset..editor_offset))
- });
+ editor.change_selections(
+ SelectionEffects::scroll(Autoscroll::Next),
+ window,
+ cx,
+ |s| s.select_ranges(Some(editor_offset..editor_offset)),
+ );
});
}
}
@@ -135,6 +135,7 @@ pub type ContextServerFactory =
Box<dyn Fn(ContextServerId, Arc<ContextServerConfiguration>) -> Arc<ContextServer>>;
pub struct ContextServerStore {
+ context_server_settings: HashMap<Arc<str>, ContextServerSettings>,
servers: HashMap<ContextServerId, ContextServerState>,
worktree_store: Entity<WorktreeStore>,
registry: Entity<ContextServerDescriptorRegistry>,
@@ -202,6 +203,11 @@ impl ContextServerStore {
this.available_context_servers_changed(cx);
}),
cx.observe_global::<SettingsStore>(|this, cx| {
+ let settings = Self::resolve_context_server_settings(&this.worktree_store, cx);
+ if &this.context_server_settings == settings {
+ return;
+ }
+ this.context_server_settings = settings.clone();
this.available_context_servers_changed(cx);
}),
]
@@ -211,6 +217,8 @@ impl ContextServerStore {
let mut this = Self {
_subscriptions: subscriptions,
+ context_server_settings: Self::resolve_context_server_settings(&worktree_store, cx)
+ .clone(),
worktree_store,
registry,
needs_server_update: false,
@@ -268,10 +276,8 @@ impl ContextServerStore {
cx.spawn(async move |this, cx| {
let this = this.upgrade().context("Context server store dropped")?;
let settings = this
- .update(cx, |this, cx| {
- this.context_server_settings(cx)
- .get(&server.id().0)
- .cloned()
+ .update(cx, |this, _| {
+ this.context_server_settings.get(&server.id().0).cloned()
})
.ok()
.flatten()
@@ -439,12 +445,11 @@ impl ContextServerStore {
}
}
- fn context_server_settings<'a>(
- &'a self,
+ fn resolve_context_server_settings<'a>(
+ worktree_store: &'a Entity<WorktreeStore>,
cx: &'a App,
) -> &'a HashMap<Arc<str>, ContextServerSettings> {
- let location = self
- .worktree_store
+ let location = worktree_store
.read(cx)
.visible_worktrees(cx)
.next()
@@ -492,9 +497,9 @@ impl ContextServerStore {
}
async fn maintain_servers(this: WeakEntity<Self>, cx: &mut AsyncApp) -> Result<()> {
- let (mut configured_servers, registry, worktree_store) = this.update(cx, |this, cx| {
+ let (mut configured_servers, registry, worktree_store) = this.update(cx, |this, _| {
(
- this.context_server_settings(cx).clone(),
+ this.context_server_settings.clone(),
this.registry.clone(),
this.worktree_store.clone(),
)
@@ -990,6 +995,33 @@ mod tests {
assert_eq!(store.read(cx).status_for_server(&server_2_id), None);
});
}
+
+ // Ensure that nothing happens if the settings do not change
+ {
+ let _server_events = assert_server_events(&store, vec![], cx);
+ set_context_server_configuration(
+ vec![(
+ server_1_id.0.clone(),
+ ContextServerSettings::Extension {
+ enabled: true,
+ settings: json!({
+ "somevalue": false
+ }),
+ },
+ )],
+ cx,
+ );
+
+ cx.run_until_parked();
+
+ cx.update(|cx| {
+ assert_eq!(
+ store.read(cx).status_for_server(&server_1_id),
+ Some(ContextServerStatus::Running)
+ );
+ assert_eq!(store.read(cx).status_for_server(&server_2_id), None);
+ });
+ }
}
#[gpui::test]
@@ -2,7 +2,7 @@ use anyhow::{Context as _, Result};
use async_trait::async_trait;
use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName};
use gpui::SharedString;
-use serde_json::Value;
+use serde_json::{Value, json};
use smol::{
io::AsyncReadExt,
process::{Command, Stdio},
@@ -76,6 +76,13 @@ impl DapLocator for CargoLocator {
_ => {}
}
+ let config = if adapter.as_ref() == "CodeLLDB" {
+ json!({
+ "sourceLanguages": ["rust"]
+ })
+ } else {
+ Value::Null
+ };
Some(DebugScenario {
adapter: adapter.0.clone(),
label: resolved_label.to_string().into(),
@@ -83,7 +90,7 @@ impl DapLocator for CargoLocator {
task_template,
locator_name: Some(self.name()),
}),
- config: serde_json::Value::Null,
+ config,
tcp_connection: None,
})
}
@@ -117,7 +117,20 @@ impl DapLocator for GoLocator {
// HACK: tasks assume that they are run in a shell context,
// so the -run regex has escaped specials. Delve correctly
// handles escaping, so we undo that here.
- if arg.starts_with("\\^") && arg.ends_with("\\$") {
+ if let Some((left, right)) = arg.split_once("/")
+ && left.starts_with("\\^")
+ && left.ends_with("\\$")
+ && right.starts_with("\\^")
+ && right.ends_with("\\$")
+ {
+ let mut left = left[1..left.len() - 2].to_string();
+ left.push('$');
+
+ let mut right = right[1..right.len() - 2].to_string();
+ right.push('$');
+
+ args.push(format!("{left}/{right}"));
+ } else if arg.starts_with("\\^") && arg.ends_with("\\$") {
let mut arg = arg[1..arg.len() - 2].to_string();
arg.push('$');
args.push(arg);
@@ -1037,10 +1037,6 @@ impl Session {
matches!(self.mode, Mode::Building)
}
- pub fn is_running(&self) -> bool {
- matches!(self.mode, Mode::Running(_))
- }
-
pub fn as_running_mut(&mut self) -> Option<&mut RunningMode> {
match &mut self.mode {
Mode::Running(local_mode) => Some(local_mode),
@@ -1483,6 +1479,28 @@ impl Session {
}
Events::Capabilities(event) => {
self.capabilities = self.capabilities.merge(event.capabilities);
+
+ // The adapter might've enabled new exception breakpoints (or disabled existing ones).
+ let recent_filters = self
+ .capabilities
+ .exception_breakpoint_filters
+ .iter()
+ .flatten()
+ .map(|filter| (filter.filter.clone(), filter.clone()))
+ .collect::<BTreeMap<_, _>>();
+ for filter in recent_filters.values() {
+ let default = filter.default.unwrap_or_default();
+ self.exception_breakpoints
+ .entry(filter.filter.clone())
+ .or_insert_with(|| (filter.clone(), default));
+ }
+ self.exception_breakpoints
+ .retain(|k, _| recent_filters.contains_key(k));
+ if self.is_started() {
+ self.send_exception_breakpoints(cx);
+ }
+
+ // Remove the ones that no longer exist.
cx.notify();
}
Events::Memory(_) => {}
@@ -4556,7 +4556,9 @@ async fn compute_snapshot(
let mut events = Vec::new();
let branches = backend.branches().await?;
let branch = branches.into_iter().find(|branch| branch.is_head);
- let statuses = backend.status(&[WORK_DIRECTORY_REPO_PATH.clone()]).await?;
+ let statuses = backend
+ .status(std::slice::from_ref(&WORK_DIRECTORY_REPO_PATH))
+ .await?;
let statuses_by_path = SumTree::from_iter(
statuses
.entries
@@ -565,7 +565,7 @@ mod tests {
conflict_set.snapshot().conflicts[0].clone()
});
cx.update(|cx| {
- conflict.resolve(buffer.clone(), &[conflict.theirs.clone()], cx);
+ conflict.resolve(buffer.clone(), std::slice::from_ref(&conflict.theirs), cx);
});
cx.run_until_parked();
@@ -107,9 +107,7 @@ pub trait LspCommand: 'static + Sized + Send + std::fmt::Debug {
}
/// When false, `to_lsp_params_or_response` default implementation will return the default response.
- fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool {
- true
- }
+ fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool;
fn to_lsp(
&self,
@@ -277,6 +275,16 @@ impl LspCommand for PrepareRename {
"Prepare rename"
}
+ fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool {
+ capabilities
+ .server_capabilities
+ .rename_provider
+ .is_some_and(|capability| match capability {
+ OneOf::Left(enabled) => enabled,
+ OneOf::Right(options) => options.prepare_provider.unwrap_or(false),
+ })
+ }
+
fn to_lsp_params_or_response(
&self,
path: &Path,
@@ -459,6 +467,16 @@ impl LspCommand for PerformRename {
"Rename"
}
+ fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool {
+ capabilities
+ .server_capabilities
+ .rename_provider
+ .is_some_and(|capability| match capability {
+ OneOf::Left(enabled) => enabled,
+ OneOf::Right(_options) => true,
+ })
+ }
+
fn to_lsp(
&self,
path: &Path,
@@ -583,7 +601,10 @@ impl LspCommand for GetDefinition {
capabilities
.server_capabilities
.definition_provider
- .is_some()
+ .is_some_and(|capability| match capability {
+ OneOf::Left(supported) => supported,
+ OneOf::Right(_options) => true,
+ })
}
fn to_lsp(
@@ -682,7 +703,11 @@ impl LspCommand for GetDeclaration {
capabilities
.server_capabilities
.declaration_provider
- .is_some()
+ .is_some_and(|capability| match capability {
+ lsp::DeclarationCapability::Simple(supported) => supported,
+ lsp::DeclarationCapability::RegistrationOptions(..) => true,
+ lsp::DeclarationCapability::Options(..) => true,
+ })
}
fn to_lsp(
@@ -777,6 +802,16 @@ impl LspCommand for GetImplementation {
"Get implementation"
}
+ fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool {
+ capabilities
+ .server_capabilities
+ .implementation_provider
+ .is_some_and(|capability| match capability {
+ lsp::ImplementationProviderCapability::Simple(enabled) => enabled,
+ lsp::ImplementationProviderCapability::Options(_options) => true,
+ })
+ }
+
fn to_lsp(
&self,
path: &Path,
@@ -1437,7 +1472,10 @@ impl LspCommand for GetDocumentHighlights {
capabilities
.server_capabilities
.document_highlight_provider
- .is_some()
+ .is_some_and(|capability| match capability {
+ OneOf::Left(supported) => supported,
+ OneOf::Right(_options) => true,
+ })
}
fn to_lsp(
@@ -1590,7 +1628,10 @@ impl LspCommand for GetDocumentSymbols {
capabilities
.server_capabilities
.document_symbol_provider
- .is_some()
+ .is_some_and(|capability| match capability {
+ OneOf::Left(supported) => supported,
+ OneOf::Right(_options) => true,
+ })
}
fn to_lsp(
@@ -2116,6 +2157,13 @@ impl LspCommand for GetCompletions {
"Get completion"
}
+ fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool {
+ capabilities
+ .server_capabilities
+ .completion_provider
+ .is_some()
+ }
+
fn to_lsp(
&self,
path: &Path,
@@ -4161,7 +4209,11 @@ impl LspCommand for GetDocumentColor {
server_capabilities
.server_capabilities
.color_provider
- .is_some()
+ .is_some_and(|capability| match capability {
+ lsp::ColorProviderCapability::Simple(supported) => supported,
+ lsp::ColorProviderCapability::ColorProvider(..) => true,
+ lsp::ColorProviderCapability::Options(..) => true,
+ })
}
fn to_lsp(
@@ -170,6 +170,7 @@ pub struct LocalLspStore {
_subscription: gpui::Subscription,
lsp_tree: Entity<LanguageServerTree>,
registered_buffers: HashMap<BufferId, usize>,
+ buffers_opened_in_servers: HashMap<BufferId, HashSet<LanguageServerId>>,
buffer_pull_diagnostics_result_ids: HashMap<LanguageServerId, HashMap<PathBuf, Option<String>>>,
}
@@ -1404,7 +1405,7 @@ impl LocalLspStore {
let formatters = match (trigger, &settings.format_on_save) {
(FormatTrigger::Save, FormatOnSave::Off) => &[],
- (FormatTrigger::Save, FormatOnSave::List(formatters)) => formatters.as_ref(),
+ (FormatTrigger::Save, FormatOnSave::List(formatters)) => formatters.as_slice(),
(FormatTrigger::Manual, _) | (FormatTrigger::Save, FormatOnSave::On) => {
match &settings.formatter {
SelectedFormatter::Auto => {
@@ -1416,7 +1417,7 @@ impl LocalLspStore {
std::slice::from_ref(&Formatter::LanguageServer { name: None })
}
}
- SelectedFormatter::List(formatter_list) => formatter_list.as_ref(),
+ SelectedFormatter::List(formatter_list) => formatter_list.as_slice(),
}
}
};
@@ -2484,11 +2485,11 @@ impl LocalLspStore {
}
}
};
- let lsp_tool = self.weak.clone();
+ let lsp_store = self.weak.clone();
let server_name = server_node.name();
let buffer_abs_path = abs_path.to_string_lossy().to_string();
cx.defer(move |cx| {
- lsp_tool.update(cx, |_, cx| cx.emit(LspStoreEvent::LanguageServerUpdate {
+ lsp_store.update(cx, |_, cx| cx.emit(LspStoreEvent::LanguageServerUpdate {
language_server_id: server_id,
name: server_name,
message: proto::update_language_server::Variant::RegisteredForBuffer(proto::RegisteredForBuffer {
@@ -2546,6 +2547,10 @@ impl LocalLspStore {
vec![snapshot]
});
+ self.buffers_opened_in_servers
+ .entry(buffer_id)
+ .or_default()
+ .insert(server.server_id());
cx.emit(LspStoreEvent::LanguageServerUpdate {
language_server_id: server.server_id(),
name: None,
@@ -3208,6 +3213,9 @@ impl LocalLspStore {
self.language_servers.remove(server_id_to_remove);
self.buffer_pull_diagnostics_result_ids
.remove(server_id_to_remove);
+ for buffer_servers in self.buffers_opened_in_servers.values_mut() {
+ buffer_servers.remove(server_id_to_remove);
+ }
cx.emit(LspStoreEvent::LanguageServerRemoved(*server_id_to_remove));
}
servers_to_remove.into_keys().collect()
@@ -3542,22 +3550,29 @@ pub struct LspStore {
_maintain_buffer_languages: Task<()>,
diagnostic_summaries:
HashMap<WorktreeId, HashMap<Arc<Path>, HashMap<LanguageServerId, DiagnosticSummary>>>,
- lsp_data: Option<LspData>,
+ lsp_data: HashMap<BufferId, DocumentColorData>,
}
-type DocumentColorTask = Shared<Task<std::result::Result<Vec<DocumentColor>, Arc<anyhow::Error>>>>;
-
-#[derive(Debug)]
-struct LspData {
- mtime: MTime,
- buffer_lsp_data: HashMap<LanguageServerId, HashMap<PathBuf, BufferLspData>>,
- colors_update: HashMap<PathBuf, DocumentColorTask>,
- last_version_queried: HashMap<PathBuf, Global>,
+#[derive(Debug, Default, Clone)]
+pub struct DocumentColors {
+ pub colors: HashSet<DocumentColor>,
+ pub cache_version: Option<usize>,
}
+type DocumentColorTask = Shared<Task<std::result::Result<DocumentColors, Arc<anyhow::Error>>>>;
+
#[derive(Debug, Default)]
-struct BufferLspData {
- colors: Option<Vec<DocumentColor>>,
+struct DocumentColorData {
+ colors_for_version: Global,
+ colors: HashMap<LanguageServerId, HashSet<DocumentColor>>,
+ cache_version: usize,
+ colors_update: Option<(Global, DocumentColorTask)>,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum ColorFetchStrategy {
+ IgnoreCache,
+ UseCache { known_cache_version: Option<usize> },
}
#[derive(Debug)]
@@ -3780,6 +3795,7 @@ impl LspStore {
}),
lsp_tree: LanguageServerTree::new(manifest_tree, languages.clone(), cx),
registered_buffers: HashMap::default(),
+ buffers_opened_in_servers: HashMap::default(),
buffer_pull_diagnostics_result_ids: HashMap::default(),
}),
last_formatting_failure: None,
@@ -3791,7 +3807,7 @@ impl LspStore {
language_server_statuses: Default::default(),
nonce: StdRng::from_entropy().r#gen(),
diagnostic_summaries: HashMap::default(),
- lsp_data: None,
+ lsp_data: HashMap::default(),
active_entry: None,
_maintain_workspace_config,
_maintain_buffer_languages: Self::maintain_buffer_languages(languages, cx),
@@ -3848,7 +3864,7 @@ impl LspStore {
language_server_statuses: Default::default(),
nonce: StdRng::from_entropy().r#gen(),
diagnostic_summaries: HashMap::default(),
- lsp_data: None,
+ lsp_data: HashMap::default(),
active_entry: None,
toolchain_store,
_maintain_workspace_config,
@@ -4137,16 +4153,22 @@ impl LspStore {
local.register_buffer_with_language_servers(buffer, only_register_servers, cx);
}
if !ignore_refcounts {
- cx.observe_release(&handle, move |this, buffer, cx| {
- let local = this.as_local_mut().unwrap();
- let Some(refcount) = local.registered_buffers.get_mut(&buffer_id) else {
- debug_panic!("bad refcounting");
- return;
- };
+ cx.observe_release(&handle, move |lsp_store, buffer, cx| {
+ let refcount = {
+ let local = lsp_store.as_local_mut().unwrap();
+ let Some(refcount) = local.registered_buffers.get_mut(&buffer_id) else {
+ debug_panic!("bad refcounting");
+ return;
+ };
- *refcount -= 1;
- if *refcount == 0 {
+ *refcount -= 1;
+ *refcount
+ };
+ if refcount == 0 {
+ lsp_store.lsp_data.remove(&buffer_id);
+ let local = lsp_store.as_local_mut().unwrap();
local.registered_buffers.remove(&buffer_id);
+ local.buffers_opened_in_servers.remove(&buffer_id);
if let Some(file) = File::from_dyn(buffer.read(cx).file()).cloned() {
local.unregister_old_buffer_from_language_servers(&buffer, &file, cx);
}
@@ -5011,7 +5033,7 @@ impl LspStore {
.presentations
.into_iter()
.map(|presentation| ColorPresentation {
- label: presentation.label,
+ label: SharedString::from(presentation.label),
text_edit: presentation.text_edit.and_then(deserialize_lsp_edit),
additional_text_edits: presentation
.additional_text_edits
@@ -5054,7 +5076,7 @@ impl LspStore {
.context("color presentation resolve LSP request")?
.into_iter()
.map(|presentation| ColorPresentation {
- label: presentation.label,
+ label: SharedString::from(presentation.label),
text_edit: presentation.text_edit,
additional_text_edits: presentation
.additional_text_edits
@@ -5743,7 +5765,10 @@ impl LspStore {
match language {
Some(language) => {
adapter
- .labels_for_completions(&[completion_item.clone()], language)
+ .labels_for_completions(
+ std::slice::from_ref(&completion_item),
+ language,
+ )
.await?
}
None => Vec::new(),
@@ -6206,152 +6231,137 @@ impl LspStore {
pub fn document_colors(
&mut self,
- for_server_id: Option<LanguageServerId>,
+ fetch_strategy: ColorFetchStrategy,
buffer: Entity<Buffer>,
cx: &mut Context<Self>,
) -> Option<DocumentColorTask> {
- let buffer_mtime = buffer.read(cx).saved_mtime()?;
- let buffer_version = buffer.read(cx).version();
- let abs_path = File::from_dyn(buffer.read(cx).file())?.abs_path(cx);
-
- let mut received_colors_data = false;
- let buffer_lsp_data = self
- .lsp_data
- .as_ref()
- .into_iter()
- .filter(|lsp_data| {
- if buffer_mtime == lsp_data.mtime {
- lsp_data
- .last_version_queried
- .get(&abs_path)
- .is_none_or(|version_queried| {
- !buffer_version.changed_since(version_queried)
- })
- } else {
- !buffer_mtime.bad_is_greater_than(lsp_data.mtime)
- }
- })
- .flat_map(|lsp_data| lsp_data.buffer_lsp_data.values())
- .filter_map(|buffer_data| buffer_data.get(&abs_path))
- .filter_map(|buffer_data| {
- let colors = buffer_data.colors.as_deref()?;
- received_colors_data = true;
- Some(colors)
- })
- .flatten()
- .cloned()
- .collect::<Vec<_>>();
-
- if buffer_lsp_data.is_empty() || for_server_id.is_some() {
- if received_colors_data && for_server_id.is_none() {
- return None;
- }
-
- let mut outdated_lsp_data = false;
- if self.lsp_data.is_none()
- || self.lsp_data.as_ref().is_some_and(|lsp_data| {
- if buffer_mtime == lsp_data.mtime {
- lsp_data
- .last_version_queried
- .get(&abs_path)
- .is_none_or(|version_queried| {
- buffer_version.changed_since(version_queried)
- })
- } else {
- buffer_mtime.bad_is_greater_than(lsp_data.mtime)
- }
- })
- {
- self.lsp_data = Some(LspData {
- mtime: buffer_mtime,
- buffer_lsp_data: HashMap::default(),
- colors_update: HashMap::default(),
- last_version_queried: HashMap::default(),
- });
- outdated_lsp_data = true;
- }
+ let version_queried_for = buffer.read(cx).version();
+ let buffer_id = buffer.read(cx).remote_id();
- {
- let lsp_data = self.lsp_data.as_mut()?;
- match for_server_id {
- Some(for_server_id) if !outdated_lsp_data => {
- lsp_data.buffer_lsp_data.remove(&for_server_id);
- }
- None | Some(_) => {
- let existing_task = lsp_data.colors_update.get(&abs_path).cloned();
- if !outdated_lsp_data && existing_task.is_some() {
- return existing_task;
- }
- for buffer_data in lsp_data.buffer_lsp_data.values_mut() {
- if let Some(buffer_data) = buffer_data.get_mut(&abs_path) {
- buffer_data.colors = None;
+ match fetch_strategy {
+ ColorFetchStrategy::IgnoreCache => {}
+ ColorFetchStrategy::UseCache {
+ known_cache_version,
+ } => {
+ if let Some(cached_data) = self.lsp_data.get(&buffer_id) {
+ if !version_queried_for.changed_since(&cached_data.colors_for_version) {
+ let has_different_servers = self.as_local().is_some_and(|local| {
+ local
+ .buffers_opened_in_servers
+ .get(&buffer_id)
+ .cloned()
+ .unwrap_or_default()
+ != cached_data.colors.keys().copied().collect()
+ });
+ if !has_different_servers {
+ if Some(cached_data.cache_version) == known_cache_version {
+ return None;
+ } else {
+ return Some(
+ Task::ready(Ok(DocumentColors {
+ colors: cached_data
+ .colors
+ .values()
+ .flatten()
+ .cloned()
+ .collect(),
+ cache_version: Some(cached_data.cache_version),
+ }))
+ .shared(),
+ );
}
}
}
}
}
+ }
- let task_abs_path = abs_path.clone();
- let new_task = cx
- .spawn(async move |lsp_store, cx| {
- cx.background_executor().timer(Duration::from_millis(50)).await;
- let fetched_colors = match lsp_store
- .update(cx, |lsp_store, cx| {
- lsp_store.fetch_document_colors(buffer, cx)
- }) {
- Ok(fetch_task) => fetch_task.await
- .with_context(|| {
- format!(
- "Fetching document colors for buffer with path {task_abs_path:?}"
- )
- }),
- Err(e) => return Err(Arc::new(e)),
- };
- let fetched_colors = match fetched_colors {
- Ok(fetched_colors) => fetched_colors,
- Err(e) => return Err(Arc::new(e)),
- };
-
- let lsp_colors = lsp_store.update(cx, |lsp_store, _| {
- let lsp_data = lsp_store.lsp_data.as_mut().with_context(|| format!(
- "Document lsp data got updated between fetch and update for path {task_abs_path:?}"
- ))?;
- let mut lsp_colors = Vec::new();
- anyhow::ensure!(lsp_data.mtime == buffer_mtime, "Buffer lsp data got updated between fetch and update for path {task_abs_path:?}");
- for (server_id, colors) in fetched_colors {
- let colors_lsp_data = &mut lsp_data.buffer_lsp_data.entry(server_id).or_default().entry(task_abs_path.clone()).or_default().colors;
- *colors_lsp_data = Some(colors.clone());
- lsp_colors.extend(colors);
+ let lsp_data = self.lsp_data.entry(buffer_id).or_default();
+ if let Some((updating_for, running_update)) = &lsp_data.colors_update {
+ if !version_queried_for.changed_since(&updating_for) {
+ return Some(running_update.clone());
+ }
+ }
+ let query_version_queried_for = version_queried_for.clone();
+ let new_task = cx
+ .spawn(async move |lsp_store, cx| {
+ cx.background_executor()
+ .timer(Duration::from_millis(30))
+ .await;
+ let fetched_colors = lsp_store
+ .update(cx, |lsp_store, cx| {
+ lsp_store.fetch_document_colors_for_buffer(buffer.clone(), cx)
+ })?
+ .await
+ .context("fetching document colors")
+ .map_err(Arc::new);
+ let fetched_colors = match fetched_colors {
+ Ok(fetched_colors) => {
+ if fetch_strategy != ColorFetchStrategy::IgnoreCache
+ && Some(true)
+ == buffer
+ .update(cx, |buffer, _| {
+ buffer.version() != query_version_queried_for
+ })
+ .ok()
+ {
+ return Ok(DocumentColors::default());
}
- Ok(lsp_colors)
- });
-
- match lsp_colors {
- Ok(Ok(lsp_colors)) => Ok(lsp_colors),
- Ok(Err(e)) => Err(Arc::new(e)),
- Err(e) => Err(Arc::new(e)),
+ fetched_colors
}
- })
- .shared();
- let lsp_data = self.lsp_data.as_mut()?;
- lsp_data
- .colors_update
- .insert(abs_path.clone(), new_task.clone());
- lsp_data
- .last_version_queried
- .insert(abs_path, buffer_version);
- lsp_data.mtime = buffer_mtime;
- Some(new_task)
- } else {
- Some(Task::ready(Ok(buffer_lsp_data)).shared())
- }
+ Err(e) => {
+ lsp_store
+ .update(cx, |lsp_store, _| {
+ lsp_store
+ .lsp_data
+ .entry(buffer_id)
+ .or_default()
+ .colors_update = None;
+ })
+ .ok();
+ return Err(e);
+ }
+ };
+
+ lsp_store
+ .update(cx, |lsp_store, _| {
+ let lsp_data = lsp_store.lsp_data.entry(buffer_id).or_default();
+
+ if lsp_data.colors_for_version == query_version_queried_for {
+ lsp_data.colors.extend(fetched_colors.clone());
+ lsp_data.cache_version += 1;
+ } else if !lsp_data
+ .colors_for_version
+ .changed_since(&query_version_queried_for)
+ {
+ lsp_data.colors_for_version = query_version_queried_for;
+ lsp_data.colors = fetched_colors.clone();
+ lsp_data.cache_version += 1;
+ }
+ lsp_data.colors_update = None;
+ let colors = lsp_data
+ .colors
+ .values()
+ .flatten()
+ .cloned()
+ .collect::<HashSet<_>>();
+ DocumentColors {
+ colors,
+ cache_version: Some(lsp_data.cache_version),
+ }
+ })
+ .map_err(Arc::new)
+ })
+ .shared();
+ lsp_data.colors_update = Some((version_queried_for, new_task.clone()));
+ Some(new_task)
}
- fn fetch_document_colors(
+ fn fetch_document_colors_for_buffer(
&mut self,
buffer: Entity<Buffer>,
cx: &mut Context<Self>,
- ) -> Task<anyhow::Result<Vec<(LanguageServerId, Vec<DocumentColor>)>>> {
+ ) -> Task<anyhow::Result<HashMap<LanguageServerId, HashSet<DocumentColor>>>> {
if let Some((client, project_id)) = self.upstream_client() {
let request_task = client.request(proto::MultiLspQuery {
project_id,
@@ -6366,7 +6376,7 @@ impl LspStore {
});
cx.spawn(async move |project, cx| {
let Some(project) = project.upgrade() else {
- return Ok(Vec::new());
+ return Ok(HashMap::default());
};
let colors = join_all(
request_task
@@ -6400,11 +6410,11 @@ impl LspStore {
.await
.into_iter()
.fold(HashMap::default(), |mut acc, (server_id, colors)| {
- acc.entry(server_id).or_insert_with(Vec::new).extend(colors);
+ acc.entry(server_id)
+ .or_insert_with(HashSet::default)
+ .extend(colors);
acc
- })
- .into_iter()
- .collect();
+ });
Ok(colors)
})
} else {
@@ -6415,7 +6425,9 @@ impl LspStore {
.await
.into_iter()
.fold(HashMap::default(), |mut acc, (server_id, colors)| {
- acc.entry(server_id).or_insert_with(Vec::new).extend(colors);
+ acc.entry(server_id)
+ .or_insert_with(HashSet::default)
+ .extend(colors);
acc
})
.into_iter()
@@ -7530,6 +7542,14 @@ impl LspStore {
.unwrap_or(true)
})
.map(|(_, server)| server.server_id())
+ .filter(|server_id| {
+ self.as_local().is_none_or(|local| {
+ local
+ .buffers_opened_in_servers
+ .get(&snapshot.remote_id())
+ .is_some_and(|servers| servers.contains(server_id))
+ })
+ })
.collect::<Vec<_>>()
});
@@ -8951,7 +8971,7 @@ impl LspStore {
.color_presentations
.into_iter()
.map(|presentation| proto::ColorPresentation {
- label: presentation.label,
+ label: presentation.label.to_string(),
text_edit: presentation.text_edit.map(serialize_lsp_edit),
additional_text_edits: presentation
.additional_text_edits
@@ -10092,6 +10112,7 @@ impl LspStore {
}
// Tell the language server about every open buffer in the worktree that matches the language.
+ let mut buffer_paths_registered = Vec::new();
self.buffer_store.clone().update(cx, |buffer_store, cx| {
for buffer_handle in buffer_store.buffers() {
let buffer = buffer_handle.read(cx);
@@ -10150,6 +10171,12 @@ impl LspStore {
version,
initial_snapshot.text(),
);
+ buffer_paths_registered.push(file.abs_path(cx));
+ local
+ .buffers_opened_in_servers
+ .entry(buffer.remote_id())
+ .or_default()
+ .insert(server_id);
}
buffer_handle.update(cx, |buffer, cx| {
buffer.set_completion_triggers(
@@ -10171,6 +10198,18 @@ impl LspStore {
}
});
+ for abs_path in buffer_paths_registered {
+ cx.emit(LspStoreEvent::LanguageServerUpdate {
+ language_server_id: server_id,
+ name: Some(adapter.name()),
+ message: proto::update_language_server::Variant::RegisteredForBuffer(
+ proto::RegisteredForBuffer {
+ buffer_abs_path: abs_path.to_string_lossy().to_string(),
+ },
+ ),
+ });
+ }
+
cx.notify();
}
@@ -10614,11 +10653,15 @@ impl LspStore {
}
fn cleanup_lsp_data(&mut self, for_server: LanguageServerId) {
- if let Some(lsp_data) = &mut self.lsp_data {
- lsp_data.buffer_lsp_data.remove(&for_server);
+ for buffer_lsp_data in self.lsp_data.values_mut() {
+ buffer_lsp_data.colors.remove(&for_server);
+ buffer_lsp_data.cache_version += 1;
}
if let Some(local) = self.as_local_mut() {
local.buffer_pull_diagnostics_result_ids.remove(&for_server);
+ for buffer_servers in local.buffers_opened_in_servers.values_mut() {
+ buffer_servers.remove(&for_server);
+ }
}
}
@@ -16,7 +16,7 @@ use language::{
Buffer, point_to_lsp,
proto::{deserialize_anchor, serialize_anchor},
};
-use lsp::{LanguageServer, LanguageServerId};
+use lsp::{AdapterServerCapabilities, LanguageServer, LanguageServerId};
use rpc::proto::{self, PeerId};
use serde::{Deserialize, Serialize};
use std::{
@@ -68,6 +68,10 @@ impl LspCommand for ExpandMacro {
"Expand macro"
}
+ fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool {
+ true
+ }
+
fn to_lsp(
&self,
path: &Path,
@@ -196,6 +200,10 @@ impl LspCommand for OpenDocs {
"Open docs"
}
+ fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool {
+ true
+ }
+
fn to_lsp(
&self,
path: &Path,
@@ -326,6 +334,10 @@ impl LspCommand for SwitchSourceHeader {
"Switch source header"
}
+ fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool {
+ true
+ }
+
fn to_lsp(
&self,
path: &Path,
@@ -404,6 +416,10 @@ impl LspCommand for GoToParentModule {
"Go to parent module"
}
+ fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool {
+ true
+ }
+
fn to_lsp(
&self,
path: &Path,
@@ -578,6 +594,10 @@ impl LspCommand for GetLspRunnables {
"LSP Runnables"
}
+ fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool {
+ true
+ }
+
fn to_lsp(
&self,
path: &Path,
@@ -2,6 +2,7 @@ use std::{
ops::ControlFlow,
path::{Path, PathBuf},
sync::Arc,
+ time::Duration,
};
use anyhow::{Context as _, Result, anyhow};
@@ -527,26 +528,6 @@ impl PrettierStore {
let mut new_plugins = plugins.collect::<HashSet<_>>();
let node = self.node.clone();
- let fs = Arc::clone(&self.fs);
- let locate_prettier_installation = match worktree.and_then(|worktree_id| {
- self.worktree_store
- .read(cx)
- .worktree_for_id(worktree_id, cx)
- .map(|worktree| worktree.read(cx).abs_path())
- }) {
- Some(locate_from) => {
- let installed_prettiers = self.prettier_instances.keys().cloned().collect();
- cx.background_spawn(async move {
- Prettier::locate_prettier_installation(
- fs.as_ref(),
- &installed_prettiers,
- locate_from.as_ref(),
- )
- .await
- })
- }
- None => Task::ready(Ok(ControlFlow::Continue(None))),
- };
new_plugins.retain(|plugin| !self.default_prettier.installed_plugins.contains(plugin));
let mut installation_attempt = 0;
let previous_installation_task = match &mut self.default_prettier.prettier {
@@ -574,15 +555,34 @@ impl PrettierStore {
}
};
- log::info!("Initializing default prettier with plugins {new_plugins:?}");
let plugins_to_install = new_plugins.clone();
let fs = Arc::clone(&self.fs);
let new_installation_task = cx
- .spawn(async move |project, cx| {
- match locate_prettier_installation
+ .spawn(async move |prettier_store, cx| {
+ cx.background_executor().timer(Duration::from_millis(30)).await;
+ let location_data = prettier_store.update(cx, |prettier_store, cx| {
+ worktree.and_then(|worktree_id| {
+ prettier_store.worktree_store
+ .read(cx)
+ .worktree_for_id(worktree_id, cx)
+ .map(|worktree| worktree.read(cx).abs_path())
+ }).map(|locate_from| {
+ let installed_prettiers = prettier_store.prettier_instances.keys().cloned().collect();
+ (locate_from, installed_prettiers)
+ })
+ })?;
+ let locate_prettier_installation = match location_data {
+ Some((locate_from, installed_prettiers)) => Prettier::locate_prettier_installation(
+ fs.as_ref(),
+ &installed_prettiers,
+ locate_from.as_ref(),
+ )
.await
- .context("locate prettier installation")
- .map_err(Arc::new)?
+ .context("locate prettier installation").map_err(Arc::new)?,
+ None => ControlFlow::Continue(None),
+ };
+
+ match locate_prettier_installation
{
ControlFlow::Break(()) => return Ok(()),
ControlFlow::Continue(prettier_path) => {
@@ -593,8 +593,8 @@ impl PrettierStore {
if let Some(previous_installation_task) = previous_installation_task {
if let Err(e) = previous_installation_task.await {
log::error!("Failed to install default prettier: {e:#}");
- project.update(cx, |project, _| {
- if let PrettierInstallation::NotInstalled { attempts, not_installed_plugins, .. } = &mut project.default_prettier.prettier {
+ prettier_store.update(cx, |prettier_store, _| {
+ if let PrettierInstallation::NotInstalled { attempts, not_installed_plugins, .. } = &mut prettier_store.default_prettier.prettier {
*attempts += 1;
new_plugins.extend(not_installed_plugins.iter().cloned());
installation_attempt = *attempts;
@@ -604,8 +604,8 @@ impl PrettierStore {
}
};
if installation_attempt > prettier::FAIL_THRESHOLD {
- project.update(cx, |project, _| {
- if let PrettierInstallation::NotInstalled { installation_task, .. } = &mut project.default_prettier.prettier {
+ prettier_store.update(cx, |prettier_store, _| {
+ if let PrettierInstallation::NotInstalled { installation_task, .. } = &mut prettier_store.default_prettier.prettier {
*installation_task = None;
};
})?;
@@ -614,19 +614,20 @@ impl PrettierStore {
);
return Ok(());
}
- project.update(cx, |project, _| {
+ prettier_store.update(cx, |prettier_store, _| {
new_plugins.retain(|plugin| {
- !project.default_prettier.installed_plugins.contains(plugin)
+ !prettier_store.default_prettier.installed_plugins.contains(plugin)
});
- if let PrettierInstallation::NotInstalled { not_installed_plugins, .. } = &mut project.default_prettier.prettier {
+ if let PrettierInstallation::NotInstalled { not_installed_plugins, .. } = &mut prettier_store.default_prettier.prettier {
not_installed_plugins.retain(|plugin| {
- !project.default_prettier.installed_plugins.contains(plugin)
+ !prettier_store.default_prettier.installed_plugins.contains(plugin)
});
not_installed_plugins.extend(new_plugins.iter().cloned());
}
needs_install |= !new_plugins.is_empty();
})?;
if needs_install {
+ log::info!("Initializing default prettier with plugins {new_plugins:?}");
let installed_plugins = new_plugins.clone();
cx.background_spawn(async move {
install_prettier_packages(fs.as_ref(), new_plugins, node).await?;
@@ -637,17 +638,27 @@ impl PrettierStore {
.await
.context("prettier & plugins install")
.map_err(Arc::new)?;
- log::info!("Initialized prettier with plugins: {installed_plugins:?}");
- project.update(cx, |project, _| {
- project.default_prettier.prettier =
+ log::info!("Initialized default prettier with plugins: {installed_plugins:?}");
+ prettier_store.update(cx, |prettier_store, _| {
+ prettier_store.default_prettier.prettier =
PrettierInstallation::Installed(PrettierInstance {
attempt: 0,
prettier: None,
});
- project.default_prettier
+ prettier_store.default_prettier
.installed_plugins
.extend(installed_plugins);
})?;
+ } else {
+ prettier_store.update(cx, |prettier_store, _| {
+ if let PrettierInstallation::NotInstalled { .. } = &mut prettier_store.default_prettier.prettier {
+ prettier_store.default_prettier.prettier =
+ PrettierInstallation::Installed(PrettierInstance {
+ attempt: 0,
+ prettier: None,
+ });
+ }
+ })?;
}
}
}
@@ -694,7 +705,6 @@ pub fn prettier_plugins_for_language(
SelectedFormatter::Auto => Some(&language_settings.prettier.plugins),
SelectedFormatter::List(list) => list
- .as_ref()
.contains(&Formatter::Prettier)
.then_some(&language_settings.prettier.plugins),
}
@@ -767,6 +777,7 @@ pub(super) async fn format_with_prettier(
}
}
+#[derive(Debug)]
pub struct DefaultPrettier {
prettier: PrettierInstallation,
installed_plugins: HashSet<Arc<str>>,
@@ -779,13 +779,42 @@ pub struct DocumentColor {
pub color_presentations: Vec<ColorPresentation>,
}
-#[derive(Clone, Debug, PartialEq)]
+impl Eq for DocumentColor {}
+
+impl std::hash::Hash for DocumentColor {
+ fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+ self.lsp_range.hash(state);
+ self.color.red.to_bits().hash(state);
+ self.color.green.to_bits().hash(state);
+ self.color.blue.to_bits().hash(state);
+ self.color.alpha.to_bits().hash(state);
+ self.resolved.hash(state);
+ self.color_presentations.hash(state);
+ }
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ColorPresentation {
- pub label: String,
+ pub label: SharedString,
pub text_edit: Option<lsp::TextEdit>,
pub additional_text_edits: Vec<lsp::TextEdit>,
}
+impl std::hash::Hash for ColorPresentation {
+ fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+ self.label.hash(state);
+ if let Some(ref edit) = self.text_edit {
+ edit.range.hash(state);
+ edit.new_text.hash(state);
+ }
+ self.additional_text_edits.len().hash(state);
+ for edit in &self.additional_text_edits {
+ edit.range.hash(state);
+ edit.new_text.hash(state);
+ }
+ }
+}
+
#[derive(Clone)]
pub enum DirectoryLister {
Project(Entity<Project>),
@@ -2946,6 +2975,20 @@ impl Project {
}),
Err(_) => {}
},
+ SettingsObserverEvent::LocalDebugScenariosUpdated(result) => match result {
+ Err(InvalidSettingsError::Debug { message, path }) => {
+ let message =
+ format!("Failed to set local debug scenarios in {path:?}:\n{message}");
+ cx.emit(Event::Toast {
+ notification_id: format!("local-debug-scenarios-{path:?}").into(),
+ message,
+ });
+ }
+ Ok(path) => cx.emit(Event::HideToast {
+ notification_id: format!("local-debug-scenarios-{path:?}").into(),
+ }),
+ Err(_) => {}
+ },
}
}
@@ -36,7 +36,6 @@ use crate::{
};
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
-#[schemars(deny_unknown_fields)]
pub struct ProjectSettings {
/// Configuration for language servers.
///
@@ -97,9 +96,8 @@ pub enum ContextServerSettings {
/// Whether the context server is enabled.
#[serde(default = "default_true")]
enabled: bool,
- /// The command to run this context server.
- ///
- /// This will override the command set by an extension.
+
+ #[serde(flatten)]
command: ContextServerCommand,
},
Extension {
@@ -555,6 +553,7 @@ pub enum SettingsObserverMode {
pub enum SettingsObserverEvent {
LocalSettingsUpdated(Result<PathBuf, InvalidSettingsError>),
LocalTasksUpdated(Result<PathBuf, InvalidSettingsError>),
+ LocalDebugScenariosUpdated(Result<PathBuf, InvalidSettingsError>),
}
impl EventEmitter<SettingsObserverEvent> for SettingsObserver {}
@@ -566,6 +565,7 @@ pub struct SettingsObserver {
project_id: u64,
task_store: Entity<TaskStore>,
_global_task_config_watcher: Task<()>,
+ _global_debug_config_watcher: Task<()>,
}
/// SettingsObserver observers changes to .zed/{settings, task}.json files in local worktrees
@@ -598,6 +598,11 @@ impl SettingsObserver {
paths::tasks_file().clone(),
cx,
),
+ _global_debug_config_watcher: Self::subscribe_to_global_debug_scenarios_changes(
+ fs.clone(),
+ paths::debug_scenarios_file().clone(),
+ cx,
+ ),
}
}
@@ -618,6 +623,11 @@ impl SettingsObserver {
paths::tasks_file().clone(),
cx,
),
+ _global_debug_config_watcher: Self::subscribe_to_global_debug_scenarios_changes(
+ fs.clone(),
+ paths::debug_scenarios_file().clone(),
+ cx,
+ ),
}
}
@@ -1048,6 +1058,61 @@ impl SettingsObserver {
}
})
}
+ fn subscribe_to_global_debug_scenarios_changes(
+ fs: Arc<dyn Fs>,
+ file_path: PathBuf,
+ cx: &mut Context<Self>,
+ ) -> Task<()> {
+ let mut user_tasks_file_rx =
+ watch_config_file(&cx.background_executor(), fs, file_path.clone());
+ let user_tasks_content = cx.background_executor().block(user_tasks_file_rx.next());
+ let weak_entry = cx.weak_entity();
+ cx.spawn(async move |settings_observer, cx| {
+ let Ok(task_store) = settings_observer.read_with(cx, |settings_observer, _| {
+ settings_observer.task_store.clone()
+ }) else {
+ return;
+ };
+ if let Some(user_tasks_content) = user_tasks_content {
+ let Ok(()) = task_store.update(cx, |task_store, cx| {
+ task_store
+ .update_user_debug_scenarios(
+ TaskSettingsLocation::Global(&file_path),
+ Some(&user_tasks_content),
+ cx,
+ )
+ .log_err();
+ }) else {
+ return;
+ };
+ }
+ while let Some(user_tasks_content) = user_tasks_file_rx.next().await {
+ let Ok(result) = task_store.update(cx, |task_store, cx| {
+ task_store.update_user_debug_scenarios(
+ TaskSettingsLocation::Global(&file_path),
+ Some(&user_tasks_content),
+ cx,
+ )
+ }) else {
+ break;
+ };
+
+ weak_entry
+ .update(cx, |_, cx| match result {
+ Ok(()) => cx.emit(SettingsObserverEvent::LocalDebugScenariosUpdated(Ok(
+ file_path.clone(),
+ ))),
+ Err(err) => cx.emit(SettingsObserverEvent::LocalDebugScenariosUpdated(
+ Err(InvalidSettingsError::Tasks {
+ path: file_path.clone(),
+ message: err.to_string(),
+ }),
+ )),
+ })
+ .ok();
+ }
+ })
+ }
}
pub fn local_settings_kind_from_proto(kind: proto::LocalSettingsKind) -> LocalSettingsKind {
@@ -2023,7 +2023,7 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) {
cx.update(|cx| {
SettingsStore::update_global(cx, |settings, cx| {
settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
- settings.languages.insert(
+ settings.languages.0.insert(
"Rust".into(),
LanguageSettingsContent {
enable_language_server: Some(false),
@@ -2042,14 +2042,14 @@ async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) {
cx.update(|cx| {
SettingsStore::update_global(cx, |settings, cx| {
settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
- settings.languages.insert(
+ settings.languages.0.insert(
LanguageName::new("Rust"),
LanguageSettingsContent {
enable_language_server: Some(true),
..Default::default()
},
);
- settings.languages.insert(
+ settings.languages.0.insert(
LanguageName::new("JavaScript"),
LanguageSettingsContent {
enable_language_server: Some(false),
@@ -7502,13 +7502,13 @@ async fn test_staging_random_hunks(
if hunk.status().has_secondary_hunk() {
log::info!("staging hunk at {row}");
uncommitted_diff.update(cx, |diff, cx| {
- diff.stage_or_unstage_hunks(true, &[hunk.clone()], &snapshot, true, cx);
+ diff.stage_or_unstage_hunks(true, std::slice::from_ref(hunk), &snapshot, true, cx);
});
hunk.secondary_status = SecondaryHunkRemovalPending;
} else {
log::info!("unstaging hunk at {row}");
uncommitted_diff.update(cx, |diff, cx| {
- diff.stage_or_unstage_hunks(false, &[hunk.clone()], &snapshot, true, cx);
+ diff.stage_or_unstage_hunks(false, std::slice::from_ref(hunk), &snapshot, true, cx);
});
hunk.secondary_status = SecondaryHunkAdditionPending;
}
@@ -148,7 +148,7 @@ impl Project {
let ssh_details = self.ssh_details(cx);
let settings = self.terminal_settings(&path, cx).clone();
- let builder = ShellBuilder::new(ssh_details.is_none(), &settings.shell);
+ let builder = ShellBuilder::new(ssh_details.is_none(), &settings.shell).non_interactive();
let (command, args) = builder.build(command, &Vec::new());
let mut env = self
@@ -12,7 +12,7 @@ use editor::{
entry_diagnostic_aware_icon_decoration_and_color,
entry_diagnostic_aware_icon_name_and_color, entry_git_aware_label_color,
},
- scroll::{Autoscroll, ScrollbarAutoHide},
+ scroll::ScrollbarAutoHide,
};
use file_icons::FileIcons;
use git::status::GitSummary;
@@ -1589,7 +1589,7 @@ impl ProjectPanel {
});
self.filename_editor.update(cx, |editor, cx| {
editor.set_text(file_name, window, cx);
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.select_ranges([selection])
});
window.focus(&editor.focus_handle(cx));
@@ -1,4 +1,4 @@
-use editor::{Bias, Editor, scroll::Autoscroll, styled_runs_for_code_label};
+use editor::{Bias, Editor, SelectionEffects, scroll::Autoscroll, styled_runs_for_code_label};
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
App, Context, DismissEvent, Entity, FontWeight, ParentElement, StyledText, Task, WeakEntity,
@@ -136,9 +136,12 @@ impl PickerDelegate for ProjectSymbolsDelegate {
workspace.open_project_item::<Editor>(pane, buffer, true, true, window, cx);
editor.update(cx, |editor, cx| {
- editor.change_selections(Some(Autoscroll::center()), window, cx, |s| {
- s.select_ranges([position..position])
- });
+ editor.change_selections(
+ SelectionEffects::scroll(Autoscroll::center()),
+ window,
+ cx,
+ |s| s.select_ranges([position..position]),
+ );
});
})?;
anyhow::Ok(())
@@ -632,7 +632,7 @@ impl From<Timestamp> for SystemTime {
impl From<SystemTime> for Timestamp {
fn from(time: SystemTime) -> Self {
- let duration = time.duration_since(UNIX_EPOCH).unwrap();
+ let duration = time.duration_since(UNIX_EPOCH).unwrap_or_default();
Self {
seconds: duration.as_secs(),
nanos: duration.subsec_nanos(),
@@ -248,7 +248,7 @@ impl Render for SshPrompt {
text_style.refine(&refinement);
let markdown_style = MarkdownStyle {
base_text_style: text_style,
- selection_background_color: cx.theme().players().local().selection,
+ selection_background_color: cx.theme().colors().element_selection_background,
..Default::default()
};
@@ -422,7 +422,12 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext
"Rust",
FakeLspAdapter {
name: "rust-analyzer",
- ..Default::default()
+ capabilities: lsp::ServerCapabilities {
+ completion_provider: Some(lsp::CompletionOptions::default()),
+ rename_provider: Some(lsp::OneOf::Left(true)),
+ ..lsp::ServerCapabilities::default()
+ },
+ ..FakeLspAdapter::default()
},
)
});
@@ -430,7 +435,11 @@ async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext
let mut fake_lsp = server_cx.update(|cx| {
headless.read(cx).languages.register_fake_language_server(
LanguageServerName("rust-analyzer".into()),
- Default::default(),
+ lsp::ServerCapabilities {
+ completion_provider: Some(lsp::CompletionOptions::default()),
+ rename_provider: Some(lsp::OneOf::Left(true)),
+ ..lsp::ServerCapabilities::default()
+ },
None,
)
});
@@ -8,6 +8,7 @@ use crate::{
};
use anyhow::Context as _;
use collections::{HashMap, HashSet};
+use editor::SelectionEffects;
use editor::{
Anchor, AnchorRangeExt as _, Editor, MultiBuffer, ToPoint,
display_map::{
@@ -477,7 +478,7 @@ impl Session {
if move_down {
editor.update(cx, move |editor, cx| {
editor.change_selections(
- Some(Autoscroll::top_relative(8)),
+ SelectionEffects::scroll(Autoscroll::top_relative(8)),
window,
cx,
|selections| {
@@ -1,6 +1,6 @@
use anyhow::Result;
use collections::{HashMap, HashSet};
-use editor::CompletionProvider;
+use editor::{CompletionProvider, SelectionEffects};
use editor::{CurrentLineHighlight, Editor, EditorElement, EditorEvent, EditorStyle, actions::Tab};
use gpui::{
Action, App, Bounds, Entity, EventEmitter, Focusable, PromptLevel, Subscription, Task,
@@ -895,10 +895,15 @@ impl RulesLibrary {
}
EditorEvent::Blurred => {
title_editor.update(cx, |title_editor, cx| {
- title_editor.change_selections(None, window, cx, |selections| {
- let cursor = selections.oldest_anchor().head();
- selections.select_anchor_ranges([cursor..cursor]);
- });
+ title_editor.change_selections(
+ SelectionEffects::no_scroll(),
+ window,
+ cx,
+ |selections| {
+ let cursor = selections.oldest_anchor().head();
+ selections.select_anchor_ranges([cursor..cursor]);
+ },
+ );
});
}
_ => {}
@@ -920,10 +925,15 @@ impl RulesLibrary {
}
EditorEvent::Blurred => {
body_editor.update(cx, |body_editor, cx| {
- body_editor.change_selections(None, window, cx, |selections| {
- let cursor = selections.oldest_anchor().head();
- selections.select_anchor_ranges([cursor..cursor]);
- });
+ body_editor.change_selections(
+ SelectionEffects::no_scroll(),
+ window,
+ cx,
+ |selections| {
+ let cursor = selections.oldest_anchor().head();
+ selections.select_anchor_ranges([cursor..cursor]);
+ },
+ );
});
}
_ => {}
@@ -101,7 +101,7 @@ pub struct BufferSearchBar {
search_options: SearchOptions,
default_options: SearchOptions,
configured_options: SearchOptions,
- query_contains_error: bool,
+ query_error: Option<String>,
dismissed: bool,
search_history: SearchHistory,
search_history_cursor: SearchHistoryCursor,
@@ -217,7 +217,7 @@ impl Render for BufferSearchBar {
if in_replace {
key_context.add("in_replace");
}
- let editor_border = if self.query_contains_error {
+ let editor_border = if self.query_error.is_some() {
Color::Error.color(cx)
} else {
cx.theme().colors().border
@@ -469,6 +469,14 @@ impl Render for BufferSearchBar {
)
});
+ let query_error_line = self.query_error.as_ref().map(|error| {
+ Label::new(error)
+ .size(LabelSize::Small)
+ .color(Color::Error)
+ .mt_neg_1()
+ .ml_2()
+ });
+
v_flex()
.id("buffer_search")
.gap_2()
@@ -524,6 +532,7 @@ impl Render for BufferSearchBar {
.w_full()
},
))
+ .children(query_error_line)
.children(replace_line)
}
}
@@ -728,7 +737,7 @@ impl BufferSearchBar {
configured_options: search_options,
search_options,
pending_search: None,
- query_contains_error: false,
+ query_error: None,
dismissed: true,
search_history: SearchHistory::new(
Some(MAX_BUFFER_SEARCH_HISTORY_SIZE),
@@ -1230,7 +1239,7 @@ impl BufferSearchBar {
self.pending_search.take();
if let Some(active_searchable_item) = self.active_searchable_item.as_ref() {
- self.query_contains_error = false;
+ self.query_error = None;
if query.is_empty() {
self.clear_active_searchable_item_matches(window, cx);
let _ = done_tx.send(());
@@ -1255,8 +1264,8 @@ impl BufferSearchBar {
None,
) {
Ok(query) => query.with_replacement(self.replacement(cx)),
- Err(_) => {
- self.query_contains_error = true;
+ Err(e) => {
+ self.query_error = Some(e.to_string());
self.clear_active_searchable_item_matches(window, cx);
cx.notify();
return done_rx;
@@ -1274,8 +1283,8 @@ impl BufferSearchBar {
None,
) {
Ok(query) => query.with_replacement(self.replacement(cx)),
- Err(_) => {
- self.query_contains_error = true;
+ Err(e) => {
+ self.query_error = Some(e.to_string());
self.clear_active_searchable_item_matches(window, cx);
cx.notify();
return done_rx;
@@ -1540,7 +1549,10 @@ mod tests {
use std::ops::Range;
use super::*;
- use editor::{DisplayPoint, Editor, MultiBuffer, SearchSettings, display_map::DisplayRow};
+ use editor::{
+ DisplayPoint, Editor, MultiBuffer, SearchSettings, SelectionEffects,
+ display_map::DisplayRow,
+ };
use gpui::{Hsla, TestAppContext, UpdateGlobal, VisualTestContext};
use language::{Buffer, Point};
use project::Project;
@@ -1677,7 +1689,7 @@ mod tests {
});
editor.update_in(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
])
@@ -1764,7 +1776,7 @@ mod tests {
// Park the cursor in between matches and ensure that going to the previous match selects
// the closest match to the left.
editor.update_in(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
])
@@ -1785,7 +1797,7 @@ mod tests {
// Park the cursor in between matches and ensure that going to the next match selects the
// closest match to the right.
editor.update_in(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
])
@@ -1806,7 +1818,7 @@ mod tests {
// Park the cursor after the last match and ensure that going to the previous match selects
// the last match.
editor.update_in(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
])
@@ -1827,7 +1839,7 @@ mod tests {
// Park the cursor after the last match and ensure that going to the next match selects the
// first match.
editor.update_in(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60)
])
@@ -1848,7 +1860,7 @@ mod tests {
// Park the cursor before the first match and ensure that going to the previous match
// selects the last match.
editor.update_in(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([
DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
])
@@ -2625,7 +2637,7 @@ mod tests {
});
editor.update_in(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)])
})
});
@@ -2708,7 +2720,7 @@ mod tests {
});
editor.update_in(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges(vec![
Point::new(1, 0)..Point::new(1, 4),
Point::new(5, 3)..Point::new(6, 4),
@@ -7,7 +7,7 @@ use anyhow::Context as _;
use collections::{HashMap, HashSet};
use editor::{
Anchor, Editor, EditorElement, EditorEvent, EditorSettings, EditorStyle, MAX_TAB_TITLE_LEN,
- MultiBuffer, actions::SelectAll, items::active_match_index, scroll::Autoscroll,
+ MultiBuffer, SelectionEffects, actions::SelectAll, items::active_match_index,
};
use futures::{StreamExt, stream::FuturesOrdered};
use gpui::{
@@ -208,6 +208,7 @@ pub struct ProjectSearchView {
included_opened_only: bool,
regex_language: Option<Arc<Language>>,
_subscriptions: Vec<Subscription>,
+ query_error: Option<String>,
}
#[derive(Debug, Clone)]
@@ -876,6 +877,7 @@ impl ProjectSearchView {
included_opened_only: false,
regex_language: None,
_subscriptions: subscriptions,
+ query_error: None,
};
this.entity_changed(window, cx);
this
@@ -1209,14 +1211,16 @@ impl ProjectSearchView {
if should_unmark_error {
cx.notify();
}
+ self.query_error = None;
Some(query)
}
- Err(_e) => {
+ Err(e) => {
let should_mark_error = self.panels_with_errors.insert(InputPanel::Query);
if should_mark_error {
cx.notify();
}
+ self.query_error = Some(e.to_string());
None
}
@@ -1302,8 +1306,8 @@ impl ProjectSearchView {
let range_to_select = match_ranges[new_index].clone();
self.results_editor.update(cx, |editor, cx| {
let range_to_select = editor.range_for_match(&range_to_select);
- editor.unfold_ranges(&[range_to_select.clone()], false, true, cx);
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.unfold_ranges(std::slice::from_ref(&range_to_select), false, true, cx);
+ editor.change_selections(Default::default(), window, cx, |s| {
s.select_ranges([range_to_select])
});
});
@@ -1350,7 +1354,9 @@ impl ProjectSearchView {
fn focus_results_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.query_editor.update(cx, |query_editor, cx| {
let cursor = query_editor.selections.newest_anchor().head();
- query_editor.change_selections(None, window, cx, |s| s.select_ranges([cursor..cursor]));
+ query_editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges([cursor..cursor])
+ });
});
let results_handle = self.results_editor.focus_handle(cx);
window.focus(&results_handle);
@@ -1370,7 +1376,7 @@ impl ProjectSearchView {
let range_to_select = match_ranges
.first()
.map(|range| editor.range_for_match(range));
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.select_ranges(range_to_select)
});
editor.scroll(Point::default(), Some(Axis::Vertical), window, cx);
@@ -2289,6 +2295,14 @@ impl Render for ProjectSearchBar {
key_context.add("in_replace");
}
+ let query_error_line = search.query_error.as_ref().map(|error| {
+ Label::new(error)
+ .size(LabelSize::Small)
+ .color(Color::Error)
+ .mt_neg_1()
+ .ml_2()
+ });
+
v_flex()
.py(px(1.0))
.key_context(key_context)
@@ -2340,6 +2354,7 @@ impl Render for ProjectSearchBar {
.gap_2()
.w_full()
.child(search_line)
+ .children(query_error_line)
.children(replace_line)
.children(filter_line)
}
@@ -33,11 +33,11 @@ serde_derive.workspace = true
serde_json.workspace = true
serde_json_lenient.workspace = true
smallvec.workspace = true
-streaming-iterator.workspace = true
tree-sitter-json.workspace = true
tree-sitter.workspace = true
util.workspace = true
workspace-hack.workspace = true
+zlog.workspace = true
[dev-dependencies]
fs = { workspace = true, features = ["test-support"] }
@@ -1,75 +0,0 @@
-use schemars::schema::{
- ArrayValidation, InstanceType, RootSchema, Schema, SchemaObject, SingleOrVec,
-};
-use serde_json::Value;
-
-pub struct SettingsJsonSchemaParams<'a> {
- pub language_names: &'a [String],
- pub font_names: &'a [String],
-}
-
-impl SettingsJsonSchemaParams<'_> {
- pub fn font_family_schema(&self) -> Schema {
- let available_fonts: Vec<_> = self.font_names.iter().cloned().map(Value::String).collect();
-
- SchemaObject {
- instance_type: Some(InstanceType::String.into()),
- enum_values: Some(available_fonts),
- ..Default::default()
- }
- .into()
- }
-
- pub fn font_fallback_schema(&self) -> Schema {
- SchemaObject {
- instance_type: Some(SingleOrVec::Vec(vec![
- InstanceType::Array,
- InstanceType::Null,
- ])),
- array: Some(Box::new(ArrayValidation {
- items: Some(schemars::schema::SingleOrVec::Single(Box::new(
- self.font_family_schema(),
- ))),
- unique_items: Some(true),
- ..Default::default()
- })),
- ..Default::default()
- }
- .into()
- }
-}
-
-type PropertyName<'a> = &'a str;
-type ReferencePath<'a> = &'a str;
-
-/// Modifies the provided [`RootSchema`] by adding references to all of the specified properties.
-///
-/// # Examples
-///
-/// ```
-/// # let root_schema = RootSchema::default();
-/// add_references_to_properties(&mut root_schema, &[
-/// ("property_a", "#/definitions/DefinitionA"),
-/// ("property_b", "#/definitions/DefinitionB"),
-/// ])
-/// ```
-pub fn add_references_to_properties(
- root_schema: &mut RootSchema,
- properties_with_references: &[(PropertyName, ReferencePath)],
-) {
- for (property, definition) in properties_with_references {
- let Some(schema) = root_schema.schema.object().properties.get_mut(*property) else {
- log::warn!("property '{property}' not found in JSON schema");
- continue;
- };
-
- match schema {
- Schema::Object(schema) => {
- schema.reference = Some(definition.to_string());
- }
- Schema::Bool(_) => {
- // Boolean schemas can't have references.
- }
- }
- }
-}
@@ -1,24 +1,24 @@
-use anyhow::Result;
+use anyhow::{Context as _, Result};
use collections::{BTreeMap, HashMap, IndexMap};
use fs::Fs;
use gpui::{
Action, ActionBuildError, App, InvalidKeystrokeError, KEYSTROKE_PARSE_EXPECTED_MESSAGE,
- KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, NoAction,
-};
-use schemars::{
- JsonSchema,
- r#gen::{SchemaGenerator, SchemaSettings},
- schema::{ArrayValidation, InstanceType, Schema, SchemaObject, SubschemaValidation},
+ KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, Keystroke, NoAction, SharedString,
};
+use schemars::{JsonSchema, json_schema};
use serde::Deserialize;
-use serde_json::Value;
+use serde_json::{Value, json};
+use std::borrow::Cow;
use std::{any::TypeId, fmt::Write, rc::Rc, sync::Arc, sync::LazyLock};
use util::{
asset_str,
markdown::{MarkdownEscaped, MarkdownInlineCode, MarkdownString},
};
-use crate::{SettingsAssets, settings_store::parse_json_with_comments};
+use crate::{
+ SettingsAssets, append_top_level_array_value_in_json_text, parse_json_with_comments,
+ replace_top_level_array_value_in_json_text,
+};
pub trait KeyBindingValidator: Send + Sync {
fn action_type_id(&self) -> TypeId;
@@ -120,14 +120,14 @@ impl std::fmt::Display for KeymapAction {
impl JsonSchema for KeymapAction {
/// This is used when generating the JSON schema for the `KeymapAction` type, so that it can
/// reference the keymap action schema.
- fn schema_name() -> String {
+ fn schema_name() -> Cow<'static, str> {
"KeymapAction".into()
}
/// This schema will be replaced with the full action schema in
/// `KeymapFile::generate_json_schema`.
- fn json_schema(_: &mut SchemaGenerator) -> Schema {
- Schema::Bool(true)
+ fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
+ json_schema!(true)
}
}
@@ -218,7 +218,7 @@ impl KeymapFile {
key_bindings: Vec::new(),
};
}
- let keymap_file = match parse_json_with_comments::<Self>(content) {
+ let keymap_file = match Self::parse(content) {
Ok(keymap_file) => keymap_file,
Err(error) => {
return KeymapFileLoadResult::JsonParseFailure { error };
@@ -399,7 +399,13 @@ impl KeymapFile {
},
};
- let key_binding = match KeyBinding::load(keystrokes, action, context, key_equivalents) {
+ let key_binding = match KeyBinding::load(
+ keystrokes,
+ action,
+ context,
+ key_equivalents,
+ action_input_string.map(SharedString::from),
+ ) {
Ok(key_binding) => key_binding,
Err(InvalidKeystrokeError { keystroke }) => {
return Err(format!(
@@ -421,9 +427,7 @@ impl KeymapFile {
}
pub fn generate_json_schema_for_registered_actions(cx: &mut App) -> Value {
- let mut generator = SchemaSettings::draft07()
- .with(|settings| settings.option_add_null_type = false)
- .into_generator();
+ let mut generator = schemars::generate::SchemaSettings::draft2019_09().into_generator();
let action_schemas = cx.action_schemas(&mut generator);
let deprecations = cx.deprecated_actions_to_preferred_actions();
@@ -437,92 +441,70 @@ impl KeymapFile {
}
fn generate_json_schema(
- generator: SchemaGenerator,
- action_schemas: Vec<(&'static str, Option<Schema>)>,
+ mut generator: schemars::SchemaGenerator,
+ action_schemas: Vec<(&'static str, Option<schemars::Schema>)>,
deprecations: &HashMap<&'static str, &'static str>,
deprecation_messages: &HashMap<&'static str, &'static str>,
) -> serde_json::Value {
- fn set<I, O>(input: I) -> Option<O>
- where
- I: Into<O>,
- {
- Some(input.into())
- }
-
- fn add_deprecation(schema_object: &mut SchemaObject, message: String) {
- schema_object.extensions.insert(
- // deprecationMessage is not part of the JSON Schema spec,
- // but json-language-server recognizes it.
- "deprecationMessage".to_owned(),
+ fn add_deprecation(schema: &mut schemars::Schema, message: String) {
+ schema.insert(
+ // deprecationMessage is not part of the JSON Schema spec, but
+ // json-language-server recognizes it.
+ "deprecationMessage".to_string(),
Value::String(message),
);
}
- fn add_deprecation_preferred_name(schema_object: &mut SchemaObject, new_name: &str) {
- add_deprecation(schema_object, format!("Deprecated, use {new_name}"));
+ fn add_deprecation_preferred_name(schema: &mut schemars::Schema, new_name: &str) {
+ add_deprecation(schema, format!("Deprecated, use {new_name}"));
}
- fn add_description(schema_object: &mut SchemaObject, description: String) {
- schema_object
- .metadata
- .get_or_insert(Default::default())
- .description = Some(description);
+ fn add_description(schema: &mut schemars::Schema, description: String) {
+ schema.insert("description".to_string(), Value::String(description));
}
- let empty_object: SchemaObject = SchemaObject {
- instance_type: set(InstanceType::Object),
- ..Default::default()
- };
+ let empty_object = json_schema!({
+ "type": "object"
+ });
// This is a workaround for a json-language-server issue where it matches the first
// alternative that matches the value's shape and uses that for documentation.
//
// In the case of the array validations, it would even provide an error saying that the name
// must match the name of the first alternative.
- let mut plain_action = SchemaObject {
- instance_type: set(InstanceType::String),
- const_value: Some(Value::String("".to_owned())),
- ..Default::default()
- };
+ let mut plain_action = json_schema!({
+ "type": "string",
+ "const": ""
+ });
let no_action_message = "No action named this.";
add_description(&mut plain_action, no_action_message.to_owned());
add_deprecation(&mut plain_action, no_action_message.to_owned());
- let mut matches_action_name = SchemaObject {
- const_value: Some(Value::String("".to_owned())),
- ..Default::default()
- };
- let no_action_message = "No action named this that takes input.";
- add_description(&mut matches_action_name, no_action_message.to_owned());
- add_deprecation(&mut matches_action_name, no_action_message.to_owned());
- let action_with_input = SchemaObject {
- instance_type: set(InstanceType::Array),
- array: set(ArrayValidation {
- items: set(vec![
- matches_action_name.into(),
- // Accept any value, as we want this to be the preferred match when there is a
- // typo in the name.
- Schema::Bool(true),
- ]),
- min_items: Some(2),
- max_items: Some(2),
- ..Default::default()
- }),
- ..Default::default()
- };
- let mut keymap_action_alternatives = vec![plain_action.into(), action_with_input.into()];
- for (name, action_schema) in action_schemas.into_iter() {
- let schema = if let Some(Schema::Object(schema)) = action_schema {
- Some(schema)
- } else {
- None
- };
+ let mut matches_action_name = json_schema!({
+ "const": ""
+ });
+ let no_action_message_input = "No action named this that takes input.";
+ add_description(&mut matches_action_name, no_action_message_input.to_owned());
+ add_deprecation(&mut matches_action_name, no_action_message_input.to_owned());
- let description = schema.as_ref().and_then(|schema| {
+ let action_with_input = json_schema!({
+ "type": "array",
+ "items": [
+ matches_action_name,
+ true
+ ],
+ "minItems": 2,
+ "maxItems": 2
+ });
+ let mut keymap_action_alternatives = vec![plain_action, action_with_input];
+
+ for (name, action_schema) in action_schemas.into_iter() {
+ let description = action_schema.as_ref().and_then(|schema| {
schema
- .metadata
- .as_ref()
- .and_then(|metadata| metadata.description.clone())
+ .as_object()
+ .and_then(|obj| obj.get("description"))
+ .and_then(|v| v.as_str())
+ .map(|s| s.to_string())
});
let deprecation = if name == NoAction.name() {
@@ -532,84 +514,64 @@ impl KeymapFile {
};
// Add an alternative for plain action names.
- let mut plain_action = SchemaObject {
- instance_type: set(InstanceType::String),
- const_value: Some(Value::String(name.to_string())),
- ..Default::default()
- };
+ let mut plain_action = json_schema!({
+ "type": "string",
+ "const": name
+ });
if let Some(message) = deprecation_messages.get(name) {
add_deprecation(&mut plain_action, message.to_string());
} else if let Some(new_name) = deprecation {
add_deprecation_preferred_name(&mut plain_action, new_name);
}
- if let Some(description) = description.clone() {
- add_description(&mut plain_action, description);
+ if let Some(desc) = description.clone() {
+ add_description(&mut plain_action, desc);
}
- keymap_action_alternatives.push(plain_action.into());
+ keymap_action_alternatives.push(plain_action);
// Add an alternative for actions with data specified as a [name, data] array.
//
- // When a struct with no deserializable fields is added with impl_actions! /
- // impl_actions_as! an empty object schema is produced. The action should be invoked
- // without data in this case.
- if let Some(schema) = schema {
+ // When a struct with no deserializable fields is added by deriving `Action`, an empty
+ // object schema is produced. The action should be invoked without data in this case.
+ if let Some(schema) = action_schema {
if schema != empty_object {
- let mut matches_action_name = SchemaObject {
- const_value: Some(Value::String(name.to_string())),
- ..Default::default()
- };
- if let Some(description) = description.clone() {
- add_description(&mut matches_action_name, description);
+ let mut matches_action_name = json_schema!({
+ "const": name
+ });
+ if let Some(desc) = description.clone() {
+ add_description(&mut matches_action_name, desc);
}
if let Some(message) = deprecation_messages.get(name) {
add_deprecation(&mut matches_action_name, message.to_string());
} else if let Some(new_name) = deprecation {
add_deprecation_preferred_name(&mut matches_action_name, new_name);
}
- let action_with_input = SchemaObject {
- instance_type: set(InstanceType::Array),
- array: set(ArrayValidation {
- items: set(vec![matches_action_name.into(), schema.into()]),
- min_items: Some(2),
- max_items: Some(2),
- ..Default::default()
- }),
- ..Default::default()
- };
- keymap_action_alternatives.push(action_with_input.into());
+ let action_with_input = json_schema!({
+ "type": "array",
+ "items": [matches_action_name, schema],
+ "minItems": 2,
+ "maxItems": 2
+ });
+ keymap_action_alternatives.push(action_with_input);
}
}
}
// Placing null first causes json-language-server to default assuming actions should be
// null, so place it last.
- keymap_action_alternatives.push(
- SchemaObject {
- instance_type: set(InstanceType::Null),
- ..Default::default()
- }
- .into(),
- );
+ keymap_action_alternatives.push(json_schema!({
+ "type": "null"
+ }));
- let action_schema = SchemaObject {
- subschemas: set(SubschemaValidation {
- one_of: Some(keymap_action_alternatives),
- ..Default::default()
+ // The `KeymapSection` schema will reference the `KeymapAction` schema by name, so setting
+ // the definition of `KeymapAction` results in the full action schema being used.
+ generator.definitions_mut().insert(
+ KeymapAction::schema_name().to_string(),
+ json!({
+ "oneOf": keymap_action_alternatives
}),
- ..Default::default()
- }
- .into();
+ );
- // The `KeymapSection` schema will reference the `KeymapAction` schema by name, so replacing
- // the definition of `KeymapAction` results in the full action schema being used.
- let mut root_schema = generator.into_root_schema_for::<KeymapFile>();
- root_schema
- .definitions
- .insert(KeymapAction::schema_name(), action_schema);
-
- // This and other json schemas can be viewed via `dev: open language server logs` ->
- // `json-language-server` -> `Server Info`.
- serde_json::to_value(root_schema).unwrap()
+ generator.root_schema_for::<KeymapFile>().to_value()
}
pub fn sections(&self) -> impl DoubleEndedIterator<Item = &KeymapSection> {
@@ -629,9 +591,162 @@ impl KeymapFile {
}
}
}
+
+ pub fn update_keybinding<'a>(
+ mut operation: KeybindUpdateOperation<'a>,
+ mut keymap_contents: String,
+ tab_size: usize,
+ ) -> Result<String> {
+ // if trying to replace a keybinding that is not user-defined, treat it as an add operation
+ match operation {
+ KeybindUpdateOperation::Replace {
+ target_source,
+ source,
+ ..
+ } if target_source != KeybindSource::User => {
+ operation = KeybindUpdateOperation::Add(source);
+ }
+ _ => {}
+ }
+
+ // Sanity check that keymap contents are valid, even though we only use it for Replace.
+ // We don't want to modify the file if it's invalid.
+ let keymap = Self::parse(&keymap_contents).context("Failed to parse keymap")?;
+
+ if let KeybindUpdateOperation::Replace { source, target, .. } = operation {
+ let mut found_index = None;
+ let target_action_value = target
+ .action_value()
+ .context("Failed to generate target action JSON value")?;
+ let source_action_value = source
+ .action_value()
+ .context("Failed to generate source action JSON value")?;
+ 'sections: for (index, section) in keymap.sections().enumerate() {
+ if section.context != target.context.unwrap_or("") {
+ continue;
+ }
+ if section.use_key_equivalents != target.use_key_equivalents {
+ continue;
+ }
+ let Some(bindings) = §ion.bindings else {
+ continue;
+ };
+ for (keystrokes, action) in bindings {
+ let Ok(keystrokes) = keystrokes
+ .split_whitespace()
+ .map(Keystroke::parse)
+ .collect::<Result<Vec<_>, _>>()
+ else {
+ continue;
+ };
+ if keystrokes != target.keystrokes {
+ continue;
+ }
+ if action.0 != target_action_value {
+ continue;
+ }
+ found_index = Some(index);
+ break 'sections;
+ }
+ }
+
+ if let Some(index) = found_index {
+ let (replace_range, replace_value) = replace_top_level_array_value_in_json_text(
+ &keymap_contents,
+ &["bindings", &target.keystrokes_unparsed()],
+ Some(&source_action_value),
+ Some(&source.keystrokes_unparsed()),
+ index,
+ tab_size,
+ )
+ .context("Failed to replace keybinding")?;
+ keymap_contents.replace_range(replace_range, &replace_value);
+
+ return Ok(keymap_contents);
+ } else {
+ log::warn!(
+ "Failed to find keybinding to update `{:?} -> {}` creating new binding for `{:?} -> {}` instead",
+ target.keystrokes,
+ target_action_value,
+ source.keystrokes,
+ source_action_value,
+ );
+ operation = KeybindUpdateOperation::Add(source);
+ }
+ }
+
+ if let KeybindUpdateOperation::Add(keybinding) = operation {
+ let mut value = serde_json::Map::with_capacity(4);
+ if let Some(context) = keybinding.context {
+ value.insert("context".to_string(), context.into());
+ }
+ if keybinding.use_key_equivalents {
+ value.insert("use_key_equivalents".to_string(), true.into());
+ }
+
+ value.insert("bindings".to_string(), {
+ let mut bindings = serde_json::Map::new();
+ let action = keybinding.action_value()?;
+ bindings.insert(keybinding.keystrokes_unparsed(), action);
+ bindings.into()
+ });
+
+ let (replace_range, replace_value) = append_top_level_array_value_in_json_text(
+ &keymap_contents,
+ &value.into(),
+ tab_size,
+ )?;
+ keymap_contents.replace_range(replace_range, &replace_value);
+ }
+ return Ok(keymap_contents);
+ }
}
-#[derive(Clone, Copy)]
+pub enum KeybindUpdateOperation<'a> {
+ Replace {
+ /// Describes the keybind to create
+ source: KeybindUpdateTarget<'a>,
+ /// Describes the keybind to remove
+ target: KeybindUpdateTarget<'a>,
+ target_source: KeybindSource,
+ },
+ Add(KeybindUpdateTarget<'a>),
+}
+
+pub struct KeybindUpdateTarget<'a> {
+ pub context: Option<&'a str>,
+ pub keystrokes: &'a [Keystroke],
+ pub action_name: &'a str,
+ pub use_key_equivalents: bool,
+ pub input: Option<&'a str>,
+}
+
+impl<'a> KeybindUpdateTarget<'a> {
+ fn action_value(&self) -> Result<Value> {
+ let action_name: Value = self.action_name.into();
+ let value = match self.input {
+ Some(input) => {
+ let input = serde_json::from_str::<Value>(input)
+ .context("Failed to parse action input as JSON")?;
+ serde_json::json!([action_name, input])
+ }
+ None => action_name,
+ };
+ return Ok(value);
+ }
+
+ fn keystrokes_unparsed(&self) -> String {
+ let mut keystrokes = String::with_capacity(self.keystrokes.len() * 8);
+ for keystroke in self.keystrokes {
+ keystrokes.push_str(&keystroke.unparse());
+ keystrokes.push(' ');
+ }
+ keystrokes.pop();
+ keystrokes
+ }
+}
+
+#[derive(Clone, Copy, PartialEq, Eq)]
pub enum KeybindSource {
User,
Default,
@@ -688,7 +803,12 @@ impl From<KeybindSource> for KeyBindingMetaIndex {
#[cfg(test)]
mod tests {
- use crate::KeymapFile;
+ use unindent::Unindent;
+
+ use crate::{
+ KeybindSource, KeymapFile,
+ keymap_file::{KeybindUpdateOperation, KeybindUpdateTarget},
+ };
#[test]
fn can_deserialize_keymap_with_trailing_comma() {
@@ -704,4 +824,326 @@ mod tests {
};
KeymapFile::parse(json).unwrap();
}
+
+ #[test]
+ fn keymap_update() {
+ use gpui::Keystroke;
+
+ zlog::init_test();
+ #[track_caller]
+ fn check_keymap_update(
+ input: impl ToString,
+ operation: KeybindUpdateOperation,
+ expected: impl ToString,
+ ) {
+ let result = KeymapFile::update_keybinding(operation, input.to_string(), 4)
+ .expect("Update succeeded");
+ pretty_assertions::assert_eq!(expected.to_string(), result);
+ }
+
+ #[track_caller]
+ fn parse_keystrokes(keystrokes: &str) -> Vec<Keystroke> {
+ return keystrokes
+ .split(' ')
+ .map(|s| Keystroke::parse(s).expect("Keystrokes valid"))
+ .collect();
+ }
+
+ check_keymap_update(
+ "[]",
+ KeybindUpdateOperation::Add(KeybindUpdateTarget {
+ keystrokes: &parse_keystrokes("ctrl-a"),
+ action_name: "zed::SomeAction",
+ context: None,
+ use_key_equivalents: false,
+ input: None,
+ }),
+ r#"[
+ {
+ "bindings": {
+ "ctrl-a": "zed::SomeAction"
+ }
+ }
+ ]"#
+ .unindent(),
+ );
+
+ check_keymap_update(
+ r#"[
+ {
+ "bindings": {
+ "ctrl-a": "zed::SomeAction"
+ }
+ }
+ ]"#
+ .unindent(),
+ KeybindUpdateOperation::Add(KeybindUpdateTarget {
+ keystrokes: &parse_keystrokes("ctrl-b"),
+ action_name: "zed::SomeOtherAction",
+ context: None,
+ use_key_equivalents: false,
+ input: None,
+ }),
+ r#"[
+ {
+ "bindings": {
+ "ctrl-a": "zed::SomeAction"
+ }
+ },
+ {
+ "bindings": {
+ "ctrl-b": "zed::SomeOtherAction"
+ }
+ }
+ ]"#
+ .unindent(),
+ );
+
+ check_keymap_update(
+ r#"[
+ {
+ "bindings": {
+ "ctrl-a": "zed::SomeAction"
+ }
+ }
+ ]"#
+ .unindent(),
+ KeybindUpdateOperation::Add(KeybindUpdateTarget {
+ keystrokes: &parse_keystrokes("ctrl-b"),
+ action_name: "zed::SomeOtherAction",
+ context: None,
+ use_key_equivalents: false,
+ input: Some(r#"{"foo": "bar"}"#),
+ }),
+ r#"[
+ {
+ "bindings": {
+ "ctrl-a": "zed::SomeAction"
+ }
+ },
+ {
+ "bindings": {
+ "ctrl-b": [
+ "zed::SomeOtherAction",
+ {
+ "foo": "bar"
+ }
+ ]
+ }
+ }
+ ]"#
+ .unindent(),
+ );
+
+ check_keymap_update(
+ r#"[
+ {
+ "bindings": {
+ "ctrl-a": "zed::SomeAction"
+ }
+ }
+ ]"#
+ .unindent(),
+ KeybindUpdateOperation::Add(KeybindUpdateTarget {
+ keystrokes: &parse_keystrokes("ctrl-b"),
+ action_name: "zed::SomeOtherAction",
+ context: Some("Zed > Editor && some_condition = true"),
+ use_key_equivalents: true,
+ input: Some(r#"{"foo": "bar"}"#),
+ }),
+ r#"[
+ {
+ "bindings": {
+ "ctrl-a": "zed::SomeAction"
+ }
+ },
+ {
+ "context": "Zed > Editor && some_condition = true",
+ "use_key_equivalents": true,
+ "bindings": {
+ "ctrl-b": [
+ "zed::SomeOtherAction",
+ {
+ "foo": "bar"
+ }
+ ]
+ }
+ }
+ ]"#
+ .unindent(),
+ );
+
+ check_keymap_update(
+ r#"[
+ {
+ "bindings": {
+ "ctrl-a": "zed::SomeAction"
+ }
+ }
+ ]"#
+ .unindent(),
+ KeybindUpdateOperation::Replace {
+ target: KeybindUpdateTarget {
+ keystrokes: &parse_keystrokes("ctrl-a"),
+ action_name: "zed::SomeAction",
+ context: None,
+ use_key_equivalents: false,
+ input: None,
+ },
+ source: KeybindUpdateTarget {
+ keystrokes: &parse_keystrokes("ctrl-b"),
+ action_name: "zed::SomeOtherAction",
+ context: None,
+ use_key_equivalents: false,
+ input: Some(r#"{"foo": "bar"}"#),
+ },
+ target_source: KeybindSource::Base,
+ },
+ r#"[
+ {
+ "bindings": {
+ "ctrl-a": "zed::SomeAction"
+ }
+ },
+ {
+ "bindings": {
+ "ctrl-b": [
+ "zed::SomeOtherAction",
+ {
+ "foo": "bar"
+ }
+ ]
+ }
+ }
+ ]"#
+ .unindent(),
+ );
+
+ check_keymap_update(
+ r#"[
+ {
+ "bindings": {
+ "ctrl-a": "zed::SomeAction"
+ }
+ }
+ ]"#
+ .unindent(),
+ KeybindUpdateOperation::Replace {
+ target: KeybindUpdateTarget {
+ keystrokes: &parse_keystrokes("ctrl-a"),
+ action_name: "zed::SomeAction",
+ context: None,
+ use_key_equivalents: false,
+ input: None,
+ },
+ source: KeybindUpdateTarget {
+ keystrokes: &parse_keystrokes("ctrl-b"),
+ action_name: "zed::SomeOtherAction",
+ context: None,
+ use_key_equivalents: false,
+ input: Some(r#"{"foo": "bar"}"#),
+ },
+ target_source: KeybindSource::User,
+ },
+ r#"[
+ {
+ "bindings": {
+ "ctrl-b": [
+ "zed::SomeOtherAction",
+ {
+ "foo": "bar"
+ }
+ ]
+ }
+ }
+ ]"#
+ .unindent(),
+ );
+
+ check_keymap_update(
+ r#"[
+ {
+ "bindings": {
+ "ctrl-a": "zed::SomeAction"
+ }
+ }
+ ]"#
+ .unindent(),
+ KeybindUpdateOperation::Replace {
+ target: KeybindUpdateTarget {
+ keystrokes: &parse_keystrokes("ctrl-a"),
+ action_name: "zed::SomeNonexistentAction",
+ context: None,
+ use_key_equivalents: false,
+ input: None,
+ },
+ source: KeybindUpdateTarget {
+ keystrokes: &parse_keystrokes("ctrl-b"),
+ action_name: "zed::SomeOtherAction",
+ context: None,
+ use_key_equivalents: false,
+ input: None,
+ },
+ target_source: KeybindSource::User,
+ },
+ r#"[
+ {
+ "bindings": {
+ "ctrl-a": "zed::SomeAction"
+ }
+ },
+ {
+ "bindings": {
+ "ctrl-b": "zed::SomeOtherAction"
+ }
+ }
+ ]"#
+ .unindent(),
+ );
+
+ check_keymap_update(
+ r#"[
+ {
+ "bindings": {
+ // some comment
+ "ctrl-a": "zed::SomeAction"
+ // some other comment
+ }
+ }
+ ]"#
+ .unindent(),
+ KeybindUpdateOperation::Replace {
+ target: KeybindUpdateTarget {
+ keystrokes: &parse_keystrokes("ctrl-a"),
+ action_name: "zed::SomeAction",
+ context: None,
+ use_key_equivalents: false,
+ input: None,
+ },
+ source: KeybindUpdateTarget {
+ keystrokes: &parse_keystrokes("ctrl-b"),
+ action_name: "zed::SomeOtherAction",
+ context: None,
+ use_key_equivalents: false,
+ input: Some(r#"{"foo": "bar"}"#),
+ },
+ target_source: KeybindSource::User,
+ },
+ r#"[
+ {
+ "bindings": {
+ // some comment
+ "ctrl-b": [
+ "zed::SomeOtherAction",
+ {
+ "foo": "bar"
+ }
+ ]
+ // some other comment
+ }
+ }
+ ]"#
+ .unindent(),
+ );
+ }
}
@@ -1,8 +1,8 @@
mod editable_setting_control;
-mod json_schema;
mod key_equivalents;
mod keymap_file;
mod settings_file;
+mod settings_json;
mod settings_store;
mod vscode_import;
@@ -12,16 +12,16 @@ use std::{borrow::Cow, fmt, str};
use util::asset_str;
pub use editable_setting_control::*;
-pub use json_schema::*;
pub use key_equivalents::*;
pub use keymap_file::{
- KeyBindingValidator, KeyBindingValidatorRegistration, KeybindSource, KeymapFile,
- KeymapFileLoadResult,
+ KeyBindingValidator, KeyBindingValidatorRegistration, KeybindSource, KeybindUpdateOperation,
+ KeybindUpdateTarget, KeymapFile, KeymapFileLoadResult,
};
pub use settings_file::*;
+pub use settings_json::*;
pub use settings_store::{
InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation, SettingsSources,
- SettingsStore, parse_json_with_comments,
+ SettingsStore,
};
pub use vscode_import::{VsCodeSettings, VsCodeSettingsSource};
@@ -9,10 +9,9 @@ pub const EMPTY_THEME_NAME: &str = "empty-theme";
#[cfg(any(test, feature = "test-support"))]
pub fn test_settings() -> String {
- let mut value = crate::settings_store::parse_json_with_comments::<serde_json::Value>(
- crate::default_settings().as_ref(),
- )
- .unwrap();
+ let mut value =
+ crate::parse_json_with_comments::<serde_json::Value>(crate::default_settings().as_ref())
+ .unwrap();
#[cfg(not(target_os = "windows"))]
util::merge_non_null_json_value_into(
serde_json::json!({
@@ -0,0 +1,1653 @@
+use anyhow::Result;
+use gpui::App;
+use schemars::{JsonSchema, Schema, transform::transform_subschemas};
+use serde::{Serialize, de::DeserializeOwned};
+use serde_json::Value;
+use std::{ops::Range, sync::LazyLock};
+use tree_sitter::{Query, StreamingIterator as _};
+use util::RangeExt;
+
+/// Parameters that are used when generating some JSON schemas at runtime.
+pub struct SettingsJsonSchemaParams<'a> {
+ pub language_names: &'a [String],
+ pub font_names: &'a [String],
+}
+
+/// Value registered which specifies JSON schemas that are generated at runtime.
+pub struct ParameterizedJsonSchema {
+ pub add_and_get_ref:
+ fn(&mut schemars::SchemaGenerator, &SettingsJsonSchemaParams, &App) -> schemars::Schema,
+}
+
+inventory::collect!(ParameterizedJsonSchema);
+
+const DEFS_PATH: &str = "#/$defs/";
+
+/// Replaces the JSON schema definition for some type, and returns a reference to it.
+pub fn replace_subschema<T: JsonSchema>(
+ generator: &mut schemars::SchemaGenerator,
+ schema: schemars::Schema,
+) -> schemars::Schema {
+ // The key in definitions may not match T::schema_name() if multiple types have the same name.
+ // This is a workaround for there being no straightforward way to get the key used for a type -
+ // see https://github.com/GREsau/schemars/issues/449
+ let ref_schema = generator.subschema_for::<T>();
+ if let Some(serde_json::Value::String(definition_pointer)) = ref_schema.get("$ref") {
+ if let Some(definition_name) = definition_pointer.strip_prefix(DEFS_PATH) {
+ generator
+ .definitions_mut()
+ .insert(definition_name.to_string(), schema.to_value());
+ return ref_schema;
+ } else {
+ log::error!(
+ "bug: expected `$ref` field to start with {DEFS_PATH}, \
+ got {definition_pointer}"
+ );
+ }
+ } else {
+ log::error!("bug: expected `$ref` field in result of `subschema_for`");
+ }
+ // fallback on just using the schema name, which could collide.
+ let schema_name = T::schema_name();
+ generator
+ .definitions_mut()
+ .insert(schema_name.to_string(), schema.to_value());
+ Schema::new_ref(format!("{DEFS_PATH}{schema_name}"))
+}
+
+/// Adds a new JSON schema definition and returns a reference to it. **Panics** if the name is
+/// already in use.
+pub fn add_new_subschema(
+ generator: &mut schemars::SchemaGenerator,
+ name: &str,
+ schema: Value,
+) -> Schema {
+ let old_definition = generator.definitions_mut().insert(name.to_string(), schema);
+ assert_eq!(old_definition, None);
+ schemars::Schema::new_ref(format!("{DEFS_PATH}{name}"))
+}
+
+/// Defaults `additionalProperties` to `true`, as if `#[schemars(deny_unknown_fields)]` was on every
+/// struct. Skips structs that have `additionalProperties` set (such as if #[serde(flatten)] is used
+/// on a map).
+#[derive(Clone)]
+pub struct DefaultDenyUnknownFields;
+
+impl schemars::transform::Transform for DefaultDenyUnknownFields {
+ fn transform(&mut self, schema: &mut schemars::Schema) {
+ if let Some(object) = schema.as_object_mut() {
+ if object.contains_key("properties")
+ && !object.contains_key("additionalProperties")
+ && !object.contains_key("unevaluatedProperties")
+ {
+ object.insert("additionalProperties".to_string(), false.into());
+ }
+ }
+ transform_subschemas(self, schema);
+ }
+}
+
+pub fn update_value_in_json_text<'a>(
+ text: &mut String,
+ key_path: &mut Vec<&'a str>,
+ tab_size: usize,
+ old_value: &'a Value,
+ new_value: &'a Value,
+ preserved_keys: &[&str],
+ edits: &mut Vec<(Range<usize>, String)>,
+) {
+ // If the old and new values are both objects, then compare them key by key,
+ // preserving the comments and formatting of the unchanged parts. Otherwise,
+ // replace the old value with the new value.
+ if let (Value::Object(old_object), Value::Object(new_object)) = (old_value, new_value) {
+ for (key, old_sub_value) in old_object.iter() {
+ key_path.push(key);
+ if let Some(new_sub_value) = new_object.get(key) {
+ // Key exists in both old and new, recursively update
+ update_value_in_json_text(
+ text,
+ key_path,
+ tab_size,
+ old_sub_value,
+ new_sub_value,
+ preserved_keys,
+ edits,
+ );
+ } else {
+ // Key was removed from new object, remove the entire key-value pair
+ let (range, replacement) =
+ replace_value_in_json_text(text, key_path, 0, None, None);
+ text.replace_range(range.clone(), &replacement);
+ edits.push((range, replacement));
+ }
+ key_path.pop();
+ }
+ for (key, new_sub_value) in new_object.iter() {
+ key_path.push(key);
+ if !old_object.contains_key(key) {
+ update_value_in_json_text(
+ text,
+ key_path,
+ tab_size,
+ &Value::Null,
+ new_sub_value,
+ preserved_keys,
+ edits,
+ );
+ }
+ key_path.pop();
+ }
+ } else if key_path
+ .last()
+ .map_or(false, |key| preserved_keys.contains(key))
+ || old_value != new_value
+ {
+ let mut new_value = new_value.clone();
+ if let Some(new_object) = new_value.as_object_mut() {
+ new_object.retain(|_, v| !v.is_null());
+ }
+ let (range, replacement) =
+ replace_value_in_json_text(text, key_path, tab_size, Some(&new_value), None);
+ text.replace_range(range.clone(), &replacement);
+ edits.push((range, replacement));
+ }
+}
+
+/// * `replace_key` - When an exact key match according to `key_path` is found, replace the key with `replace_key` if `Some`.
+fn replace_value_in_json_text(
+ text: &str,
+ key_path: &[&str],
+ tab_size: usize,
+ new_value: Option<&Value>,
+ replace_key: Option<&str>,
+) -> (Range<usize>, String) {
+ static PAIR_QUERY: LazyLock<Query> = LazyLock::new(|| {
+ Query::new(
+ &tree_sitter_json::LANGUAGE.into(),
+ "(pair key: (string) @key value: (_) @value)",
+ )
+ .expect("Failed to create PAIR_QUERY")
+ });
+
+ let mut parser = tree_sitter::Parser::new();
+ parser
+ .set_language(&tree_sitter_json::LANGUAGE.into())
+ .unwrap();
+ let syntax_tree = parser.parse(text, None).unwrap();
+
+ let mut cursor = tree_sitter::QueryCursor::new();
+
+ let mut depth = 0;
+ let mut last_value_range = 0..0;
+ let mut first_key_start = None;
+ let mut existing_value_range = 0..text.len();
+
+ let mut matches = cursor.matches(&PAIR_QUERY, syntax_tree.root_node(), text.as_bytes());
+ while let Some(mat) = matches.next() {
+ if mat.captures.len() != 2 {
+ continue;
+ }
+
+ let key_range = mat.captures[0].node.byte_range();
+ let value_range = mat.captures[1].node.byte_range();
+
+ // Don't enter sub objects until we find an exact
+ // match for the current keypath
+ if last_value_range.contains_inclusive(&value_range) {
+ continue;
+ }
+
+ last_value_range = value_range.clone();
+
+ if key_range.start > existing_value_range.end {
+ break;
+ }
+
+ first_key_start.get_or_insert(key_range.start);
+
+ let found_key = text
+ .get(key_range.clone())
+ .map(|key_text| {
+ depth < key_path.len() && key_text == format!("\"{}\"", key_path[depth])
+ })
+ .unwrap_or(false);
+
+ if found_key {
+ existing_value_range = value_range;
+ // Reset last value range when increasing in depth
+ last_value_range = existing_value_range.start..existing_value_range.start;
+ depth += 1;
+
+ if depth == key_path.len() {
+ break;
+ }
+
+ first_key_start = None;
+ }
+ }
+
+ // We found the exact key we want
+ if depth == key_path.len() {
+ if let Some(new_value) = new_value {
+ let new_val = to_pretty_json(new_value, tab_size, tab_size * depth);
+ if let Some(replace_key) = replace_key {
+ let new_key = format!("\"{}\": ", replace_key);
+ if let Some(key_start) = text[..existing_value_range.start].rfind('"') {
+ if let Some(prev_key_start) = text[..key_start].rfind('"') {
+ existing_value_range.start = prev_key_start;
+ } else {
+ existing_value_range.start = key_start;
+ }
+ }
+ (existing_value_range, new_key + &new_val)
+ } else {
+ (existing_value_range, new_val)
+ }
+ } else {
+ let mut removal_start = first_key_start.unwrap_or(existing_value_range.start);
+ let mut removal_end = existing_value_range.end;
+
+ // Find the actual key position by looking for the key in the pair
+ // We need to extend the range to include the key, not just the value
+ if let Some(key_start) = text[..existing_value_range.start].rfind('"') {
+ if let Some(prev_key_start) = text[..key_start].rfind('"') {
+ removal_start = prev_key_start;
+ } else {
+ removal_start = key_start;
+ }
+ }
+
+ // Look backward for a preceding comma first
+ let preceding_text = text.get(0..removal_start).unwrap_or("");
+ if let Some(comma_pos) = preceding_text.rfind(',') {
+ // Check if there are only whitespace characters between the comma and our key
+ let between_comma_and_key = text.get(comma_pos + 1..removal_start).unwrap_or("");
+ if between_comma_and_key.trim().is_empty() {
+ removal_start = comma_pos;
+ }
+ }
+
+ if let Some(remaining_text) = text.get(existing_value_range.end..) {
+ let mut chars = remaining_text.char_indices();
+ while let Some((offset, ch)) = chars.next() {
+ if ch == ',' {
+ removal_end = existing_value_range.end + offset + 1;
+ // Also consume whitespace after the comma
+ while let Some((_, next_ch)) = chars.next() {
+ if next_ch.is_whitespace() {
+ removal_end += next_ch.len_utf8();
+ } else {
+ break;
+ }
+ }
+ break;
+ } else if !ch.is_whitespace() {
+ break;
+ }
+ }
+ }
+ (removal_start..removal_end, String::new())
+ }
+ } else {
+ // We have key paths, construct the sub objects
+ let new_key = key_path[depth];
+
+ // We don't have the key, construct the nested objects
+ let mut new_value =
+ serde_json::to_value(new_value.unwrap_or(&serde_json::Value::Null)).unwrap();
+ for key in key_path[(depth + 1)..].iter().rev() {
+ new_value = serde_json::json!({ key.to_string(): new_value });
+ }
+
+ if let Some(first_key_start) = first_key_start {
+ let mut row = 0;
+ let mut column = 0;
+ for (ix, char) in text.char_indices() {
+ if ix == first_key_start {
+ break;
+ }
+ if char == '\n' {
+ row += 1;
+ column = 0;
+ } else {
+ column += char.len_utf8();
+ }
+ }
+
+ if row > 0 {
+ // depth is 0 based, but division needs to be 1 based.
+ let new_val = to_pretty_json(&new_value, column / (depth + 1), column);
+ let space = ' ';
+ let content = format!("\"{new_key}\": {new_val},\n{space:width$}", width = column);
+ (first_key_start..first_key_start, content)
+ } else {
+ let new_val = serde_json::to_string(&new_value).unwrap();
+ let mut content = format!(r#""{new_key}": {new_val},"#);
+ content.push(' ');
+ (first_key_start..first_key_start, content)
+ }
+ } else {
+ new_value = serde_json::json!({ new_key.to_string(): new_value });
+ let indent_prefix_len = 4 * depth;
+ let mut new_val = to_pretty_json(&new_value, 4, indent_prefix_len);
+ if depth == 0 {
+ new_val.push('\n');
+ }
+ // best effort to keep comments with best effort indentation
+ let mut replace_text = &text[existing_value_range.clone()];
+ while let Some(comment_start) = replace_text.rfind("//") {
+ if let Some(comment_end) = replace_text[comment_start..].find('\n') {
+ let mut comment_with_indent_start = replace_text[..comment_start]
+ .rfind('\n')
+ .unwrap_or(comment_start);
+ if !replace_text[comment_with_indent_start..comment_start]
+ .trim()
+ .is_empty()
+ {
+ comment_with_indent_start = comment_start;
+ }
+ new_val.insert_str(
+ 1,
+ &replace_text[comment_with_indent_start..comment_start + comment_end],
+ );
+ }
+ replace_text = &replace_text[..comment_start];
+ }
+
+ (existing_value_range, new_val)
+ }
+ }
+}
+
+const TS_DOCUMENT_KIND: &'static str = "document";
+const TS_ARRAY_KIND: &'static str = "array";
+const TS_COMMENT_KIND: &'static str = "comment";
+
+pub fn replace_top_level_array_value_in_json_text(
+ text: &str,
+ key_path: &[&str],
+ new_value: Option<&Value>,
+ replace_key: Option<&str>,
+ array_index: usize,
+ tab_size: usize,
+) -> Result<(Range<usize>, String)> {
+ let mut parser = tree_sitter::Parser::new();
+ parser
+ .set_language(&tree_sitter_json::LANGUAGE.into())
+ .unwrap();
+ let syntax_tree = parser.parse(text, None).unwrap();
+
+ let mut cursor = syntax_tree.walk();
+
+ if cursor.node().kind() == TS_DOCUMENT_KIND {
+ anyhow::ensure!(
+ cursor.goto_first_child(),
+ "Document empty - No top level array"
+ );
+ }
+
+ while cursor.node().kind() != TS_ARRAY_KIND {
+ anyhow::ensure!(cursor.goto_next_sibling(), "EOF - No top level array");
+ }
+
+ // false if no children
+ //
+ cursor.goto_first_child();
+ debug_assert_eq!(cursor.node().kind(), "[");
+
+ let mut index = 0;
+
+ while index <= array_index {
+ let node = cursor.node();
+ if !matches!(node.kind(), "[" | "]" | TS_COMMENT_KIND | ",")
+ && !node.is_extra()
+ && !node.is_missing()
+ {
+ if index == array_index {
+ break;
+ }
+ index += 1;
+ }
+ if !cursor.goto_next_sibling() {
+ if let Some(new_value) = new_value {
+ return append_top_level_array_value_in_json_text(text, new_value, tab_size);
+ } else {
+ return Ok((0..0, String::new()));
+ }
+ }
+ }
+
+ let range = cursor.node().range();
+ let indent_width = range.start_point.column;
+ let offset = range.start_byte;
+ let value_str = &text[range.start_byte..range.end_byte];
+ let needs_indent = range.start_point.row > 0;
+
+ let (mut replace_range, mut replace_value) =
+ replace_value_in_json_text(value_str, key_path, tab_size, new_value, replace_key);
+
+ replace_range.start += offset;
+ replace_range.end += offset;
+
+ if needs_indent {
+ let increased_indent = format!("\n{space:width$}", space = ' ', width = indent_width);
+ replace_value = replace_value.replace('\n', &increased_indent);
+ // replace_value.push('\n');
+ } else {
+ while let Some(idx) = replace_value.find("\n ") {
+ replace_value.remove(idx + 1);
+ }
+ while let Some(idx) = replace_value.find("\n") {
+ replace_value.replace_range(idx..idx + 1, " ");
+ }
+ }
+
+ return Ok((replace_range, replace_value));
+}
+
+pub fn append_top_level_array_value_in_json_text(
+ text: &str,
+ new_value: &Value,
+ tab_size: usize,
+) -> Result<(Range<usize>, String)> {
+ let mut parser = tree_sitter::Parser::new();
+ parser
+ .set_language(&tree_sitter_json::LANGUAGE.into())
+ .unwrap();
+ let syntax_tree = parser.parse(text, None).unwrap();
+
+ let mut cursor = syntax_tree.walk();
+
+ if cursor.node().kind() == TS_DOCUMENT_KIND {
+ anyhow::ensure!(
+ cursor.goto_first_child(),
+ "Document empty - No top level array"
+ );
+ }
+
+ while cursor.node().kind() != TS_ARRAY_KIND {
+ anyhow::ensure!(cursor.goto_next_sibling(), "EOF - No top level array");
+ }
+
+ anyhow::ensure!(
+ cursor.goto_last_child(),
+ "Malformed JSON syntax tree, expected `]` at end of array"
+ );
+ debug_assert_eq!(cursor.node().kind(), "]");
+ let close_bracket_start = cursor.node().start_byte();
+ cursor.goto_previous_sibling();
+ while (cursor.node().is_extra() || cursor.node().is_missing()) && cursor.goto_previous_sibling()
+ {
+ }
+
+ let mut comma_range = None;
+ let mut prev_item_range = None;
+
+ if cursor.node().kind() == "," {
+ comma_range = Some(cursor.node().byte_range());
+ while cursor.goto_previous_sibling() && cursor.node().is_extra() {}
+
+ debug_assert_ne!(cursor.node().kind(), "[");
+ prev_item_range = Some(cursor.node().range());
+ } else {
+ while (cursor.node().is_extra() || cursor.node().is_missing())
+ && cursor.goto_previous_sibling()
+ {}
+ if cursor.node().kind() != "[" {
+ prev_item_range = Some(cursor.node().range());
+ }
+ }
+
+ let (mut replace_range, mut replace_value) =
+ replace_value_in_json_text("", &[], tab_size, Some(new_value), None);
+
+ replace_range.start = close_bracket_start;
+ replace_range.end = close_bracket_start;
+
+ let space = ' ';
+ if let Some(prev_item_range) = prev_item_range {
+ let needs_newline = prev_item_range.start_point.row > 0;
+ let indent_width = text[..prev_item_range.start_byte].rfind('\n').map_or(
+ prev_item_range.start_point.column,
+ |idx| {
+ prev_item_range.start_point.column
+ - text[idx + 1..prev_item_range.start_byte].trim_start().len()
+ },
+ );
+
+ let prev_item_end = comma_range
+ .as_ref()
+ .map_or(prev_item_range.end_byte, |range| range.end);
+ if text[prev_item_end..replace_range.start].trim().is_empty() {
+ replace_range.start = prev_item_end;
+ }
+
+ if needs_newline {
+ let increased_indent = format!("\n{space:width$}", width = indent_width);
+ replace_value = replace_value.replace('\n', &increased_indent);
+ replace_value.push('\n');
+ replace_value.insert_str(0, &format!("\n{space:width$}", width = indent_width));
+ } else {
+ while let Some(idx) = replace_value.find("\n ") {
+ replace_value.remove(idx + 1);
+ }
+ while let Some(idx) = replace_value.find('\n') {
+ replace_value.replace_range(idx..idx + 1, " ");
+ }
+ replace_value.insert(0, ' ');
+ }
+
+ if comma_range.is_none() {
+ replace_value.insert(0, ',');
+ }
+ } else {
+ if let Some(prev_newline) = text[..replace_range.start].rfind('\n') {
+ if text[prev_newline..replace_range.start].trim().is_empty() {
+ replace_range.start = prev_newline;
+ }
+ }
+ let indent = format!("\n{space:width$}", width = tab_size);
+ replace_value = replace_value.replace('\n', &indent);
+ replace_value.insert_str(0, &indent);
+ replace_value.push('\n');
+ }
+ return Ok((replace_range, replace_value));
+}
+
+pub fn to_pretty_json(
+ value: &impl Serialize,
+ indent_size: usize,
+ indent_prefix_len: usize,
+) -> String {
+ const SPACES: [u8; 32] = [b' '; 32];
+
+ debug_assert!(indent_size <= SPACES.len());
+ debug_assert!(indent_prefix_len <= SPACES.len());
+
+ let mut output = Vec::new();
+ let mut ser = serde_json::Serializer::with_formatter(
+ &mut output,
+ serde_json::ser::PrettyFormatter::with_indent(&SPACES[0..indent_size.min(SPACES.len())]),
+ );
+
+ value.serialize(&mut ser).unwrap();
+ let text = String::from_utf8(output).unwrap();
+
+ let mut adjusted_text = String::new();
+ for (i, line) in text.split('\n').enumerate() {
+ if i > 0 {
+ adjusted_text.push_str(str::from_utf8(&SPACES[0..indent_prefix_len]).unwrap());
+ }
+ adjusted_text.push_str(line);
+ adjusted_text.push('\n');
+ }
+ adjusted_text.pop();
+ adjusted_text
+}
+
+pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
+ Ok(serde_json_lenient::from_str(content)?)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use serde_json::{Value, json};
+ use unindent::Unindent;
+
+ #[test]
+ fn object_replace() {
+ #[track_caller]
+ fn check_object_replace(
+ input: String,
+ key_path: &[&str],
+ value: Option<Value>,
+ expected: String,
+ ) {
+ let result = replace_value_in_json_text(&input, key_path, 4, value.as_ref(), None);
+ let mut result_str = input.to_string();
+ result_str.replace_range(result.0, &result.1);
+ pretty_assertions::assert_eq!(expected, result_str);
+ }
+ check_object_replace(
+ r#"{
+ "a": 1,
+ "b": 2
+ }"#
+ .unindent(),
+ &["b"],
+ Some(json!(3)),
+ r#"{
+ "a": 1,
+ "b": 3
+ }"#
+ .unindent(),
+ );
+ check_object_replace(
+ r#"{
+ "a": 1,
+ "b": 2
+ }"#
+ .unindent(),
+ &["b"],
+ None,
+ r#"{
+ "a": 1
+ }"#
+ .unindent(),
+ );
+ check_object_replace(
+ r#"{
+ "a": 1,
+ "b": 2
+ }"#
+ .unindent(),
+ &["c"],
+ Some(json!(3)),
+ r#"{
+ "c": 3,
+ "a": 1,
+ "b": 2
+ }"#
+ .unindent(),
+ );
+ check_object_replace(
+ r#"{
+ "a": 1,
+ "b": {
+ "c": 2,
+ "d": 3,
+ }
+ }"#
+ .unindent(),
+ &["b", "c"],
+ Some(json!([1, 2, 3])),
+ r#"{
+ "a": 1,
+ "b": {
+ "c": [
+ 1,
+ 2,
+ 3
+ ],
+ "d": 3,
+ }
+ }"#
+ .unindent(),
+ );
+
+ check_object_replace(
+ r#"{
+ "name": "old_name",
+ "id": 123
+ }"#
+ .unindent(),
+ &["name"],
+ Some(json!("new_name")),
+ r#"{
+ "name": "new_name",
+ "id": 123
+ }"#
+ .unindent(),
+ );
+
+ check_object_replace(
+ r#"{
+ "enabled": false,
+ "count": 5
+ }"#
+ .unindent(),
+ &["enabled"],
+ Some(json!(true)),
+ r#"{
+ "enabled": true,
+ "count": 5
+ }"#
+ .unindent(),
+ );
+
+ check_object_replace(
+ r#"{
+ "value": null,
+ "other": "test"
+ }"#
+ .unindent(),
+ &["value"],
+ Some(json!(42)),
+ r#"{
+ "value": 42,
+ "other": "test"
+ }"#
+ .unindent(),
+ );
+
+ check_object_replace(
+ r#"{
+ "config": {
+ "old": true
+ },
+ "name": "test"
+ }"#
+ .unindent(),
+ &["config"],
+ Some(json!({"new": false, "count": 3})),
+ r#"{
+ "config": {
+ "new": false,
+ "count": 3
+ },
+ "name": "test"
+ }"#
+ .unindent(),
+ );
+
+ check_object_replace(
+ r#"{
+ // This is a comment
+ "a": 1,
+ "b": 2 // Another comment
+ }"#
+ .unindent(),
+ &["b"],
+ Some(json!({"foo": "bar"})),
+ r#"{
+ // This is a comment
+ "a": 1,
+ "b": {
+ "foo": "bar"
+ } // Another comment
+ }"#
+ .unindent(),
+ );
+
+ check_object_replace(
+ r#"{}"#.to_string(),
+ &["new_key"],
+ Some(json!("value")),
+ r#"{
+ "new_key": "value"
+ }
+ "#
+ .unindent(),
+ );
+
+ check_object_replace(
+ r#"{
+ "only_key": 123
+ }"#
+ .unindent(),
+ &["only_key"],
+ None,
+ "{\n \n}".to_string(),
+ );
+
+ check_object_replace(
+ r#"{
+ "level1": {
+ "level2": {
+ "level3": {
+ "target": "old"
+ }
+ }
+ }
+ }"#
+ .unindent(),
+ &["level1", "level2", "level3", "target"],
+ Some(json!("new")),
+ r#"{
+ "level1": {
+ "level2": {
+ "level3": {
+ "target": "new"
+ }
+ }
+ }
+ }"#
+ .unindent(),
+ );
+
+ check_object_replace(
+ r#"{
+ "parent": {}
+ }"#
+ .unindent(),
+ &["parent", "child"],
+ Some(json!("value")),
+ r#"{
+ "parent": {
+ "child": "value"
+ }
+ }"#
+ .unindent(),
+ );
+
+ check_object_replace(
+ r#"{
+ "a": 1,
+ "b": 2,
+ }"#
+ .unindent(),
+ &["b"],
+ Some(json!(3)),
+ r#"{
+ "a": 1,
+ "b": 3,
+ }"#
+ .unindent(),
+ );
+
+ check_object_replace(
+ r#"{
+ "items": [1, 2, 3],
+ "count": 3
+ }"#
+ .unindent(),
+ &["items", "1"],
+ Some(json!(5)),
+ r#"{
+ "items": {
+ "1": 5
+ },
+ "count": 3
+ }"#
+ .unindent(),
+ );
+
+ check_object_replace(
+ r#"{
+ "items": [1, 2, 3],
+ "count": 3
+ }"#
+ .unindent(),
+ &["items", "1"],
+ None,
+ r#"{
+ "items": {
+ "1": null
+ },
+ "count": 3
+ }"#
+ .unindent(),
+ );
+
+ check_object_replace(
+ r#"{
+ "items": [1, 2, 3],
+ "count": 3
+ }"#
+ .unindent(),
+ &["items"],
+ Some(json!(["a", "b", "c", "d"])),
+ r#"{
+ "items": [
+ "a",
+ "b",
+ "c",
+ "d"
+ ],
+ "count": 3
+ }"#
+ .unindent(),
+ );
+
+ check_object_replace(
+ r#"{
+ "0": "zero",
+ "1": "one"
+ }"#
+ .unindent(),
+ &["1"],
+ Some(json!("ONE")),
+ r#"{
+ "0": "zero",
+ "1": "ONE"
+ }"#
+ .unindent(),
+ );
+ // Test with comments between object members
+ check_object_replace(
+ r#"{
+ "a": 1,
+ // Comment between members
+ "b": 2,
+ /* Block comment */
+ "c": 3
+ }"#
+ .unindent(),
+ &["b"],
+ Some(json!({"nested": true})),
+ r#"{
+ "a": 1,
+ // Comment between members
+ "b": {
+ "nested": true
+ },
+ /* Block comment */
+ "c": 3
+ }"#
+ .unindent(),
+ );
+
+ // Test with trailing comments on replaced value
+ check_object_replace(
+ r#"{
+ "a": 1, // keep this comment
+ "b": 2 // this should stay
+ }"#
+ .unindent(),
+ &["a"],
+ Some(json!("changed")),
+ r#"{
+ "a": "changed", // keep this comment
+ "b": 2 // this should stay
+ }"#
+ .unindent(),
+ );
+
+ // Test with deep indentation
+ check_object_replace(
+ r#"{
+ "deeply": {
+ "nested": {
+ "value": "old"
+ }
+ }
+ }"#
+ .unindent(),
+ &["deeply", "nested", "value"],
+ Some(json!("new")),
+ r#"{
+ "deeply": {
+ "nested": {
+ "value": "new"
+ }
+ }
+ }"#
+ .unindent(),
+ );
+
+ // Test removing value with comment preservation
+ check_object_replace(
+ r#"{
+ // Header comment
+ "a": 1,
+ // This comment belongs to b
+ "b": 2,
+ // This comment belongs to c
+ "c": 3
+ }"#
+ .unindent(),
+ &["b"],
+ None,
+ r#"{
+ // Header comment
+ "a": 1,
+ // This comment belongs to b
+ // This comment belongs to c
+ "c": 3
+ }"#
+ .unindent(),
+ );
+
+ // Test with multiline block comments
+ check_object_replace(
+ r#"{
+ /*
+ * This is a multiline
+ * block comment
+ */
+ "value": "old",
+ /* Another block */ "other": 123
+ }"#
+ .unindent(),
+ &["value"],
+ Some(json!("new")),
+ r#"{
+ /*
+ * This is a multiline
+ * block comment
+ */
+ "value": "new",
+ /* Another block */ "other": 123
+ }"#
+ .unindent(),
+ );
+
+ check_object_replace(
+ r#"{
+ // This object is empty
+ }"#
+ .unindent(),
+ &["key"],
+ Some(json!("value")),
+ r#"{
+ // This object is empty
+ "key": "value"
+ }
+ "#
+ .unindent(),
+ );
+
+ // Test replacing in object with only comments
+ check_object_replace(
+ r#"{
+ // Comment 1
+ // Comment 2
+ }"#
+ .unindent(),
+ &["new"],
+ Some(json!(42)),
+ r#"{
+ // Comment 1
+ // Comment 2
+ "new": 42
+ }
+ "#
+ .unindent(),
+ );
+
+ // Test with inconsistent spacing
+ check_object_replace(
+ r#"{
+ "a":1,
+ "b" : 2 ,
+ "c": 3
+ }"#
+ .unindent(),
+ &["b"],
+ Some(json!("spaced")),
+ r#"{
+ "a":1,
+ "b" : "spaced" ,
+ "c": 3
+ }"#
+ .unindent(),
+ );
+ }
+
+ #[test]
+ fn array_replace() {
+ #[track_caller]
+ fn check_array_replace(
+ input: impl ToString,
+ index: usize,
+ key_path: &[&str],
+ value: Value,
+ expected: impl ToString,
+ ) {
+ let input = input.to_string();
+ let result = replace_top_level_array_value_in_json_text(
+ &input,
+ key_path,
+ Some(&value),
+ None,
+ index,
+ 4,
+ )
+ .expect("replace succeeded");
+ let mut result_str = input;
+ result_str.replace_range(result.0, &result.1);
+ pretty_assertions::assert_eq!(expected.to_string(), result_str);
+ }
+
+ check_array_replace(r#"[1, 3, 3]"#, 1, &[], json!(2), r#"[1, 2, 3]"#);
+ check_array_replace(r#"[1, 3, 3]"#, 2, &[], json!(2), r#"[1, 3, 2]"#);
+ check_array_replace(r#"[1, 3, 3,]"#, 3, &[], json!(2), r#"[1, 3, 3, 2]"#);
+ check_array_replace(r#"[1, 3, 3,]"#, 100, &[], json!(2), r#"[1, 3, 3, 2]"#);
+ check_array_replace(
+ r#"[
+ 1,
+ 2,
+ 3,
+ ]"#
+ .unindent(),
+ 1,
+ &[],
+ json!({"foo": "bar", "baz": "qux"}),
+ r#"[
+ 1,
+ {
+ "foo": "bar",
+ "baz": "qux"
+ },
+ 3,
+ ]"#
+ .unindent(),
+ );
+ check_array_replace(
+ r#"[1, 3, 3,]"#,
+ 1,
+ &[],
+ json!({"foo": "bar", "baz": "qux"}),
+ r#"[1, { "foo": "bar", "baz": "qux" }, 3,]"#,
+ );
+
+ check_array_replace(
+ r#"[1, { "foo": "bar", "baz": "qux" }, 3,]"#,
+ 1,
+ &["baz"],
+ json!({"qux": "quz"}),
+ r#"[1, { "foo": "bar", "baz": { "qux": "quz" } }, 3,]"#,
+ );
+
+ check_array_replace(
+ r#"[
+ 1,
+ {
+ "foo": "bar",
+ "baz": "qux"
+ },
+ 3
+ ]"#,
+ 1,
+ &["baz"],
+ json!({"qux": "quz"}),
+ r#"[
+ 1,
+ {
+ "foo": "bar",
+ "baz": {
+ "qux": "quz"
+ }
+ },
+ 3
+ ]"#,
+ );
+
+ check_array_replace(
+ r#"[
+ 1,
+ {
+ "foo": "bar",
+ "baz": {
+ "qux": "quz"
+ }
+ },
+ 3
+ ]"#,
+ 1,
+ &["baz"],
+ json!("qux"),
+ r#"[
+ 1,
+ {
+ "foo": "bar",
+ "baz": "qux"
+ },
+ 3
+ ]"#,
+ );
+
+ check_array_replace(
+ r#"[
+ 1,
+ {
+ "foo": "bar",
+ // some comment to keep
+ "baz": {
+ // some comment to remove
+ "qux": "quz"
+ }
+ // some other comment to keep
+ },
+ 3
+ ]"#,
+ 1,
+ &["baz"],
+ json!("qux"),
+ r#"[
+ 1,
+ {
+ "foo": "bar",
+ // some comment to keep
+ "baz": "qux"
+ // some other comment to keep
+ },
+ 3
+ ]"#,
+ );
+
+ // Test with comments between array elements
+ check_array_replace(
+ r#"[
+ 1,
+ // This is element 2
+ 2,
+ /* Block comment */ 3,
+ 4 // Trailing comment
+ ]"#,
+ 2,
+ &[],
+ json!("replaced"),
+ r#"[
+ 1,
+ // This is element 2
+ 2,
+ /* Block comment */ "replaced",
+ 4 // Trailing comment
+ ]"#,
+ );
+
+ // Test empty array with comments
+ check_array_replace(
+ r#"[
+ // Empty array with comment
+ ]"#
+ .unindent(),
+ 0,
+ &[],
+ json!("first"),
+ r#"[
+ // Empty array with comment
+ "first"
+ ]"#
+ .unindent(),
+ );
+ check_array_replace(
+ r#"[]"#.unindent(),
+ 0,
+ &[],
+ json!("first"),
+ r#"[
+ "first"
+ ]"#
+ .unindent(),
+ );
+
+ // Test array with leading comments
+ check_array_replace(
+ r#"[
+ // Leading comment
+ // Another leading comment
+ 1,
+ 2
+ ]"#,
+ 0,
+ &[],
+ json!({"new": "object"}),
+ r#"[
+ // Leading comment
+ // Another leading comment
+ {
+ "new": "object"
+ },
+ 2
+ ]"#,
+ );
+
+ // Test with deep indentation
+ check_array_replace(
+ r#"[
+ 1,
+ 2,
+ 3
+ ]"#,
+ 1,
+ &[],
+ json!("deep"),
+ r#"[
+ 1,
+ "deep",
+ 3
+ ]"#,
+ );
+
+ // Test with mixed spacing
+ check_array_replace(
+ r#"[1,2, 3, 4]"#,
+ 2,
+ &[],
+ json!("spaced"),
+ r#"[1,2, "spaced", 4]"#,
+ );
+
+ // Test replacing nested array element
+ check_array_replace(
+ r#"[
+ [1, 2, 3],
+ [4, 5, 6],
+ [7, 8, 9]
+ ]"#,
+ 1,
+ &[],
+ json!(["a", "b", "c", "d"]),
+ r#"[
+ [1, 2, 3],
+ [
+ "a",
+ "b",
+ "c",
+ "d"
+ ],
+ [7, 8, 9]
+ ]"#,
+ );
+
+ // Test with multiline block comments
+ check_array_replace(
+ r#"[
+ /*
+ * This is a
+ * multiline comment
+ */
+ "first",
+ "second"
+ ]"#,
+ 0,
+ &[],
+ json!("updated"),
+ r#"[
+ /*
+ * This is a
+ * multiline comment
+ */
+ "updated",
+ "second"
+ ]"#,
+ );
+
+ // Test replacing with null
+ check_array_replace(
+ r#"[true, false, true]"#,
+ 1,
+ &[],
+ json!(null),
+ r#"[true, null, true]"#,
+ );
+
+ // Test single element array
+ check_array_replace(
+ r#"[42]"#,
+ 0,
+ &[],
+ json!({"answer": 42}),
+ r#"[{ "answer": 42 }]"#,
+ );
+
+ // Test array with only comments
+ check_array_replace(
+ r#"[
+ // Comment 1
+ // Comment 2
+ // Comment 3
+ ]"#
+ .unindent(),
+ 10,
+ &[],
+ json!(123),
+ r#"[
+ // Comment 1
+ // Comment 2
+ // Comment 3
+ 123
+ ]"#
+ .unindent(),
+ );
+ }
+
+ #[test]
+ fn array_append() {
+ #[track_caller]
+ fn check_array_append(input: impl ToString, value: Value, expected: impl ToString) {
+ let input = input.to_string();
+ let result = append_top_level_array_value_in_json_text(&input, &value, 4)
+ .expect("append succeeded");
+ let mut result_str = input;
+ result_str.replace_range(result.0, &result.1);
+ pretty_assertions::assert_eq!(expected.to_string(), result_str);
+ }
+ check_array_append(r#"[1, 3, 3]"#, json!(4), r#"[1, 3, 3, 4]"#);
+ check_array_append(r#"[1, 3, 3,]"#, json!(4), r#"[1, 3, 3, 4]"#);
+ check_array_append(r#"[1, 3, 3 ]"#, json!(4), r#"[1, 3, 3, 4]"#);
+ check_array_append(r#"[1, 3, 3, ]"#, json!(4), r#"[1, 3, 3, 4]"#);
+ check_array_append(
+ r#"[
+ 1,
+ 2,
+ 3
+ ]"#
+ .unindent(),
+ json!(4),
+ r#"[
+ 1,
+ 2,
+ 3,
+ 4
+ ]"#
+ .unindent(),
+ );
+ check_array_append(
+ r#"[
+ 1,
+ 2,
+ 3,
+ ]"#
+ .unindent(),
+ json!(4),
+ r#"[
+ 1,
+ 2,
+ 3,
+ 4
+ ]"#
+ .unindent(),
+ );
+ check_array_append(
+ r#"[
+ 1,
+ 2,
+ 3,
+ ]"#
+ .unindent(),
+ json!({"foo": "bar", "baz": "qux"}),
+ r#"[
+ 1,
+ 2,
+ 3,
+ {
+ "foo": "bar",
+ "baz": "qux"
+ }
+ ]"#
+ .unindent(),
+ );
+ check_array_append(
+ r#"[ 1, 2, 3, ]"#.unindent(),
+ json!({"foo": "bar", "baz": "qux"}),
+ r#"[ 1, 2, 3, { "foo": "bar", "baz": "qux" }]"#.unindent(),
+ );
+ check_array_append(
+ r#"[]"#,
+ json!({"foo": "bar"}),
+ r#"[
+ {
+ "foo": "bar"
+ }
+ ]"#
+ .unindent(),
+ );
+
+ // Test with comments between array elements
+ check_array_append(
+ r#"[
+ 1,
+ // Comment between elements
+ 2,
+ /* Block comment */ 3
+ ]"#
+ .unindent(),
+ json!(4),
+ r#"[
+ 1,
+ // Comment between elements
+ 2,
+ /* Block comment */ 3,
+ 4
+ ]"#
+ .unindent(),
+ );
+
+ // Test with trailing comment on last element
+ check_array_append(
+ r#"[
+ 1,
+ 2,
+ 3 // Trailing comment
+ ]"#
+ .unindent(),
+ json!("new"),
+ r#"[
+ 1,
+ 2,
+ 3 // Trailing comment
+ ,
+ "new"
+ ]"#
+ .unindent(),
+ );
+
+ // Test empty array with comments
+ check_array_append(
+ r#"[
+ // Empty array with comment
+ ]"#
+ .unindent(),
+ json!("first"),
+ r#"[
+ // Empty array with comment
+ "first"
+ ]"#
+ .unindent(),
+ );
+
+ // Test with multiline block comment at end
+ check_array_append(
+ r#"[
+ 1,
+ 2
+ /*
+ * This is a
+ * multiline comment
+ */
+ ]"#
+ .unindent(),
+ json!(3),
+ r#"[
+ 1,
+ 2
+ /*
+ * This is a
+ * multiline comment
+ */
+ ,
+ 3
+ ]"#
+ .unindent(),
+ );
+
+ // Test with deep indentation
+ check_array_append(
+ r#"[
+ 1,
+ 2,
+ 3
+ ]"#
+ .unindent(),
+ json!("deep"),
+ r#"[
+ 1,
+ 2,
+ 3,
+ "deep"
+ ]"#
+ .unindent(),
+ );
+
+ // Test with no spacing
+ check_array_append(r#"[1,2,3]"#, json!(4), r#"[1,2,3, 4]"#);
+
+ // Test appending complex nested structure
+ check_array_append(
+ r#"[
+ {"a": 1},
+ {"b": 2}
+ ]"#
+ .unindent(),
+ json!({"c": {"nested": [1, 2, 3]}}),
+ r#"[
+ {"a": 1},
+ {"b": 2},
+ {
+ "c": {
+ "nested": [
+ 1,
+ 2,
+ 3
+ ]
+ }
+ }
+ ]"#
+ .unindent(),
+ );
+
+ // Test array ending with comment after bracket
+ check_array_append(
+ r#"[
+ 1,
+ 2,
+ 3
+ ] // Comment after array"#
+ .unindent(),
+ json!(4),
+ r#"[
+ 1,
+ 2,
+ 3,
+ 4
+ ] // Comment after array"#
+ .unindent(),
+ );
+
+ // Test with inconsistent element formatting
+ check_array_append(
+ r#"[1,
+ 2,
+ 3,
+ ]"#
+ .unindent(),
+ json!(4),
+ r#"[1,
+ 2,
+ 3,
+ 4
+ ]"#
+ .unindent(),
+ );
+
+ // Test appending to single-line array with trailing comma
+ check_array_append(
+ r#"[1, 2, 3,]"#,
+ json!({"key": "value"}),
+ r#"[1, 2, 3, { "key": "value" }]"#,
+ );
+
+ // Test appending null value
+ check_array_append(r#"[true, false]"#, json!(null), r#"[true, false, null]"#);
+
+ // Test appending to array with only comments
+ check_array_append(
+ r#"[
+ // Just comments here
+ // More comments
+ ]"#
+ .unindent(),
+ json!(42),
+ r#"[
+ // Just comments here
+ // More comments
+ 42
+ ]"#
+ .unindent(),
+ );
+ }
+}
@@ -6,9 +6,9 @@ use futures::{FutureExt, StreamExt, channel::mpsc, future::LocalBoxFuture};
use gpui::{App, AsyncApp, BorrowAppContext, Global, Task, UpdateGlobal};
use paths::{EDITORCONFIG_NAME, local_settings_file_relative_path, task_file_name};
-use schemars::{JsonSchema, r#gen::SchemaGenerator, schema::RootSchema};
+use schemars::{JsonSchema, json_schema};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
-use serde_json::Value;
+use serde_json::{Value, json};
use smallvec::SmallVec;
use std::{
any::{Any, TypeId, type_name},
@@ -16,17 +16,17 @@ use std::{
ops::Range,
path::{Path, PathBuf},
str::{self, FromStr},
- sync::{Arc, LazyLock},
+ sync::Arc,
};
-use streaming_iterator::StreamingIterator;
-use tree_sitter::Query;
-use util::RangeExt;
use util::{ResultExt as _, merge_non_null_json_value_into};
pub type EditorconfigProperties = ec4rs::Properties;
-use crate::{SettingsJsonSchemaParams, VsCodeSettings, WorktreeId};
+use crate::{
+ DefaultDenyUnknownFields, ParameterizedJsonSchema, SettingsJsonSchemaParams, VsCodeSettings,
+ WorktreeId, add_new_subschema, parse_json_with_comments, update_value_in_json_text,
+};
/// A value that can be defined as a user setting.
///
@@ -57,14 +57,6 @@ pub trait Settings: 'static + Send + Sync {
where
Self: Sized;
- fn json_schema(
- generator: &mut SchemaGenerator,
- _: &SettingsJsonSchemaParams,
- _: &App,
- ) -> RootSchema {
- generator.root_schema_for::<Self::FileContent>()
- }
-
fn missing_default() -> anyhow::Error {
anyhow::anyhow!("missing default")
}
@@ -253,12 +245,7 @@ trait AnySettingValue: 'static + Send + Sync {
fn all_local_values(&self) -> Vec<(WorktreeId, Arc<Path>, &dyn Any)>;
fn set_global_value(&mut self, value: Box<dyn Any>);
fn set_local_value(&mut self, root_id: WorktreeId, path: Arc<Path>, value: Box<dyn Any>);
- fn json_schema(
- &self,
- generator: &mut SchemaGenerator,
- _: &SettingsJsonSchemaParams,
- cx: &App,
- ) -> RootSchema;
+ fn json_schema(&self, generator: &mut schemars::SchemaGenerator) -> schemars::Schema;
fn edits_for_update(
&self,
raw_settings: &serde_json::Value,
@@ -276,11 +263,11 @@ impl SettingsStore {
let (setting_file_updates_tx, mut setting_file_updates_rx) = mpsc::unbounded();
Self {
setting_values: Default::default(),
- raw_default_settings: serde_json::json!({}),
+ raw_default_settings: json!({}),
raw_global_settings: None,
- raw_user_settings: serde_json::json!({}),
+ raw_user_settings: json!({}),
raw_server_settings: None,
- raw_extension_settings: serde_json::json!({}),
+ raw_extension_settings: json!({}),
raw_local_settings: Default::default(),
raw_editorconfig_settings: BTreeMap::default(),
tab_size_callback: Default::default(),
@@ -631,7 +618,7 @@ impl SettingsStore {
));
}
- fn json_tab_size(&self) -> usize {
+ pub fn json_tab_size(&self) -> usize {
const DEFAULT_JSON_TAB_SIZE: usize = 2;
if let Some((setting_type_id, callback)) = &self.tab_size_callback {
@@ -877,106 +864,189 @@ impl SettingsStore {
}
pub fn json_schema(&self, schema_params: &SettingsJsonSchemaParams, cx: &App) -> Value {
- use schemars::{
- r#gen::SchemaSettings,
- schema::{Schema, SchemaObject},
- };
-
- let settings = SchemaSettings::draft07().with(|settings| {
- settings.option_add_null_type = true;
+ let mut generator = schemars::generate::SchemaSettings::draft2019_09()
+ .with_transform(DefaultDenyUnknownFields)
+ .into_generator();
+ let mut combined_schema = json!({
+ "type": "object",
+ "properties": {}
});
- let mut generator = SchemaGenerator::new(settings);
- let mut combined_schema = RootSchema::default();
+ // Merge together settings schemas, similarly to json schema's "allOf". This merging is
+ // recursive, though at time of writing this recursive nature isn't used very much. An
+ // example of it is the schema for `jupyter` having contribution from both `EditorSettings`
+ // and `JupyterSettings`.
+ //
+ // This logic could be removed in favor of "allOf", but then there isn't the opportunity to
+ // validate and fully control the merge.
for setting_value in self.setting_values.values() {
- let setting_schema = setting_value.json_schema(&mut generator, schema_params, cx);
- combined_schema
- .definitions
- .extend(setting_schema.definitions);
-
- let target_schema = if let Some(key) = setting_value.key() {
- let key_schema = combined_schema
- .schema
- .object()
- .properties
- .entry(key.to_string())
- .or_insert_with(|| Schema::Object(SchemaObject::default()));
- if let Schema::Object(key_schema) = key_schema {
- key_schema
- } else {
- continue;
+ let mut setting_schema = setting_value.json_schema(&mut generator);
+
+ if let Some(key) = setting_value.key() {
+ if let Some(properties) = combined_schema.get_mut("properties") {
+ if let Some(properties_obj) = properties.as_object_mut() {
+ if let Some(target) = properties_obj.get_mut(key) {
+ merge_schema(target, setting_schema.to_value());
+ } else {
+ properties_obj.insert(key.to_string(), setting_schema.to_value());
+ }
+ }
}
} else {
- &mut combined_schema.schema
- };
-
- merge_schema(target_schema, setting_schema.schema);
+ setting_schema.remove("description");
+ setting_schema.remove("additionalProperties");
+ merge_schema(&mut combined_schema, setting_schema.to_value());
+ }
}
- fn merge_schema(target: &mut SchemaObject, mut source: SchemaObject) {
- let source_subschemas = source.subschemas();
- let target_subschemas = target.subschemas();
- if let Some(all_of) = source_subschemas.all_of.take() {
- target_subschemas
- .all_of
- .get_or_insert(Vec::new())
- .extend(all_of);
- }
- if let Some(any_of) = source_subschemas.any_of.take() {
- target_subschemas
- .any_of
- .get_or_insert(Vec::new())
- .extend(any_of);
- }
- if let Some(one_of) = source_subschemas.one_of.take() {
- target_subschemas
- .one_of
- .get_or_insert(Vec::new())
- .extend(one_of);
- }
+ fn merge_schema(target: &mut serde_json::Value, source: serde_json::Value) {
+ let (Some(target_obj), serde_json::Value::Object(source_obj)) =
+ (target.as_object_mut(), source)
+ else {
+ return;
+ };
- if let Some(source) = source.object {
- let target_properties = &mut target.object().properties;
- for (key, value) in source.properties {
- match target_properties.entry(key) {
- btree_map::Entry::Vacant(e) => {
- e.insert(value);
+ for (source_key, source_value) in source_obj {
+ match source_key.as_str() {
+ "properties" => {
+ let serde_json::Value::Object(source_properties) = source_value else {
+ log::error!(
+ "bug: expected object for `{}` json schema field, but got: {}",
+ source_key,
+ source_value
+ );
+ continue;
+ };
+ let target_properties =
+ target_obj.entry(source_key.clone()).or_insert(json!({}));
+ let Some(target_properties) = target_properties.as_object_mut() else {
+ log::error!(
+ "bug: expected object for `{}` json schema field, but got: {}",
+ source_key,
+ target_properties
+ );
+ continue;
+ };
+ for (key, value) in source_properties {
+ if let Some(existing) = target_properties.get_mut(&key) {
+ merge_schema(existing, value);
+ } else {
+ target_properties.insert(key, value);
+ }
}
- btree_map::Entry::Occupied(e) => {
- if let (Schema::Object(target), Schema::Object(src)) =
- (e.into_mut(), value)
- {
- merge_schema(target, src);
+ }
+ "allOf" | "anyOf" | "oneOf" => {
+ let serde_json::Value::Array(source_array) = source_value else {
+ log::error!(
+ "bug: expected array for `{}` json schema field, but got: {}",
+ source_key,
+ source_value,
+ );
+ continue;
+ };
+ let target_array =
+ target_obj.entry(source_key.clone()).or_insert(json!([]));
+ let Some(target_array) = target_array.as_array_mut() else {
+ log::error!(
+ "bug: expected array for `{}` json schema field, but got: {}",
+ source_key,
+ target_array,
+ );
+ continue;
+ };
+ target_array.extend(source_array);
+ }
+ "type"
+ | "$ref"
+ | "enum"
+ | "minimum"
+ | "maximum"
+ | "pattern"
+ | "description"
+ | "additionalProperties" => {
+ if let Some(old_value) =
+ target_obj.insert(source_key.clone(), source_value.clone())
+ {
+ if old_value != source_value {
+ log::error!(
+ "bug: while merging JSON schemas, \
+ mismatch `\"{}\": {}` (before was `{}`)",
+ source_key,
+ old_value,
+ source_value
+ );
}
}
}
+ _ => {
+ log::error!(
+ "bug: while merging settings JSON schemas, \
+ encountered unexpected `\"{}\": {}`",
+ source_key,
+ source_value
+ );
+ }
}
}
+ }
+
+ // add schemas which are determined at runtime
+ for parameterized_json_schema in inventory::iter::<ParameterizedJsonSchema>() {
+ (parameterized_json_schema.add_and_get_ref)(&mut generator, schema_params, cx);
+ }
- overwrite(&mut target.instance_type, source.instance_type);
- overwrite(&mut target.string, source.string);
- overwrite(&mut target.number, source.number);
- overwrite(&mut target.reference, source.reference);
- overwrite(&mut target.array, source.array);
- overwrite(&mut target.enum_values, source.enum_values);
+ // add merged settings schema to the definitions
+ 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(
+ &mut generator,
+ "ZedReleaseStageSettings",
+ zed_release_stage_settings.to_value(),
+ );
+
+ // Remove `"additionalProperties": false` added by `DefaultDenyUnknownFields` so that
+ // unknown fields can be handled by the root schema and `ZedReleaseStageSettings`.
+ let mut definitions = generator.take_definitions(true);
+ definitions
+ .get_mut(ZED_SETTINGS)
+ .unwrap()
+ .as_object_mut()
+ .unwrap()
+ .remove("additionalProperties");
+
+ let mut root_schema = if let Some(meta_schema) = generator.settings().meta_schema.as_ref() {
+ json_schema!({ "$schema": meta_schema.to_string() })
+ } else {
+ json_schema!({})
+ };
+
+ // "unevaluatedProperties: false" to report unknown fields.
+ root_schema.insert("unevaluatedProperties".to_string(), false.into());
- fn overwrite<T>(target: &mut Option<T>, source: Option<T>) {
- if let Some(source) = source {
- *target = Some(source);
+ // Settings file contents matches ZedSettings + overrides for each release stage.
+ root_schema.insert(
+ "allOf".to_string(),
+ json!([
+ 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,
+ }
}
- }
- }
+ ]),
+ );
- for release_stage in ["dev", "nightly", "stable", "preview"] {
- let schema = combined_schema.schema.clone();
- combined_schema
- .schema
- .object()
- .properties
- .insert(release_stage.to_string(), schema.into());
- }
+ root_schema.insert("$defs".to_string(), definitions.into());
- serde_json::to_value(&combined_schema).unwrap()
+ root_schema.to_value()
}
fn recompute_values(
@@ -1289,13 +1359,8 @@ impl<T: Settings> AnySettingValue for SettingValue<T> {
}
}
- fn json_schema(
- &self,
- generator: &mut SchemaGenerator,
- params: &SettingsJsonSchemaParams,
- cx: &App,
- ) -> RootSchema {
- T::json_schema(generator, params, cx)
+ fn json_schema(&self, generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
+ T::FileContent::json_schema(generator)
}
fn edits_for_update(
@@ -1334,273 +1399,6 @@ impl<T: Settings> AnySettingValue for SettingValue<T> {
}
}
-fn update_value_in_json_text<'a>(
- text: &mut String,
- key_path: &mut Vec<&'a str>,
- tab_size: usize,
- old_value: &'a Value,
- new_value: &'a Value,
- preserved_keys: &[&str],
- edits: &mut Vec<(Range<usize>, String)>,
-) {
- // If the old and new values are both objects, then compare them key by key,
- // preserving the comments and formatting of the unchanged parts. Otherwise,
- // replace the old value with the new value.
- if let (Value::Object(old_object), Value::Object(new_object)) = (old_value, new_value) {
- for (key, old_sub_value) in old_object.iter() {
- key_path.push(key);
- if let Some(new_sub_value) = new_object.get(key) {
- // Key exists in both old and new, recursively update
- update_value_in_json_text(
- text,
- key_path,
- tab_size,
- old_sub_value,
- new_sub_value,
- preserved_keys,
- edits,
- );
- } else {
- // Key was removed from new object, remove the entire key-value pair
- let (range, replacement) = replace_value_in_json_text(text, key_path, 0, None);
- text.replace_range(range.clone(), &replacement);
- edits.push((range, replacement));
- }
- key_path.pop();
- }
- for (key, new_sub_value) in new_object.iter() {
- key_path.push(key);
- if !old_object.contains_key(key) {
- update_value_in_json_text(
- text,
- key_path,
- tab_size,
- &Value::Null,
- new_sub_value,
- preserved_keys,
- edits,
- );
- }
- key_path.pop();
- }
- } else if key_path
- .last()
- .map_or(false, |key| preserved_keys.contains(key))
- || old_value != new_value
- {
- let mut new_value = new_value.clone();
- if let Some(new_object) = new_value.as_object_mut() {
- new_object.retain(|_, v| !v.is_null());
- }
- let (range, replacement) =
- replace_value_in_json_text(text, key_path, tab_size, Some(&new_value));
- text.replace_range(range.clone(), &replacement);
- edits.push((range, replacement));
- }
-}
-
-fn replace_value_in_json_text(
- text: &str,
- key_path: &[&str],
- tab_size: usize,
- new_value: Option<&Value>,
-) -> (Range<usize>, String) {
- static PAIR_QUERY: LazyLock<Query> = LazyLock::new(|| {
- Query::new(
- &tree_sitter_json::LANGUAGE.into(),
- "(pair key: (string) @key value: (_) @value)",
- )
- .expect("Failed to create PAIR_QUERY")
- });
-
- let mut parser = tree_sitter::Parser::new();
- parser
- .set_language(&tree_sitter_json::LANGUAGE.into())
- .unwrap();
- let syntax_tree = parser.parse(text, None).unwrap();
-
- let mut cursor = tree_sitter::QueryCursor::new();
-
- let mut depth = 0;
- let mut last_value_range = 0..0;
- let mut first_key_start = None;
- let mut existing_value_range = 0..text.len();
- let mut matches = cursor.matches(&PAIR_QUERY, syntax_tree.root_node(), text.as_bytes());
- while let Some(mat) = matches.next() {
- if mat.captures.len() != 2 {
- continue;
- }
-
- let key_range = mat.captures[0].node.byte_range();
- let value_range = mat.captures[1].node.byte_range();
-
- // Don't enter sub objects until we find an exact
- // match for the current keypath
- if last_value_range.contains_inclusive(&value_range) {
- continue;
- }
-
- last_value_range = value_range.clone();
-
- if key_range.start > existing_value_range.end {
- break;
- }
-
- first_key_start.get_or_insert(key_range.start);
-
- let found_key = text
- .get(key_range.clone())
- .map(|key_text| {
- depth < key_path.len() && key_text == format!("\"{}\"", key_path[depth])
- })
- .unwrap_or(false);
-
- if found_key {
- existing_value_range = value_range;
- // Reset last value range when increasing in depth
- last_value_range = existing_value_range.start..existing_value_range.start;
- depth += 1;
-
- if depth == key_path.len() {
- break;
- }
-
- first_key_start = None;
- }
- }
-
- // We found the exact key we want
- if depth == key_path.len() {
- if let Some(new_value) = new_value {
- let new_val = to_pretty_json(new_value, tab_size, tab_size * depth);
- (existing_value_range, new_val)
- } else {
- let mut removal_start = first_key_start.unwrap_or(existing_value_range.start);
- let mut removal_end = existing_value_range.end;
-
- // Find the actual key position by looking for the key in the pair
- // We need to extend the range to include the key, not just the value
- if let Some(key_start) = text[..existing_value_range.start].rfind('"') {
- if let Some(prev_key_start) = text[..key_start].rfind('"') {
- removal_start = prev_key_start;
- } else {
- removal_start = key_start;
- }
- }
-
- // Look backward for a preceding comma first
- let preceding_text = text.get(0..removal_start).unwrap_or("");
- if let Some(comma_pos) = preceding_text.rfind(',') {
- // Check if there are only whitespace characters between the comma and our key
- let between_comma_and_key = text.get(comma_pos + 1..removal_start).unwrap_or("");
- if between_comma_and_key.trim().is_empty() {
- removal_start = comma_pos;
- }
- }
-
- if let Some(remaining_text) = text.get(existing_value_range.end..) {
- let mut chars = remaining_text.char_indices();
- while let Some((offset, ch)) = chars.next() {
- if ch == ',' {
- removal_end = existing_value_range.end + offset + 1;
- // Also consume whitespace after the comma
- while let Some((_, next_ch)) = chars.next() {
- if next_ch.is_whitespace() {
- removal_end += next_ch.len_utf8();
- } else {
- break;
- }
- }
- break;
- } else if !ch.is_whitespace() {
- break;
- }
- }
- }
- (removal_start..removal_end, String::new())
- }
- } else {
- // We have key paths, construct the sub objects
- let new_key = key_path[depth];
-
- // We don't have the key, construct the nested objects
- let mut new_value =
- serde_json::to_value(new_value.unwrap_or(&serde_json::Value::Null)).unwrap();
- for key in key_path[(depth + 1)..].iter().rev() {
- new_value = serde_json::json!({ key.to_string(): new_value });
- }
-
- if let Some(first_key_start) = first_key_start {
- let mut row = 0;
- let mut column = 0;
- for (ix, char) in text.char_indices() {
- if ix == first_key_start {
- break;
- }
- if char == '\n' {
- row += 1;
- column = 0;
- } else {
- column += char.len_utf8();
- }
- }
-
- if row > 0 {
- // depth is 0 based, but division needs to be 1 based.
- let new_val = to_pretty_json(&new_value, column / (depth + 1), column);
- let space = ' ';
- let content = format!("\"{new_key}\": {new_val},\n{space:width$}", width = column);
- (first_key_start..first_key_start, content)
- } else {
- let new_val = serde_json::to_string(&new_value).unwrap();
- let mut content = format!(r#""{new_key}": {new_val},"#);
- content.push(' ');
- (first_key_start..first_key_start, content)
- }
- } else {
- new_value = serde_json::json!({ new_key.to_string(): new_value });
- let indent_prefix_len = 4 * depth;
- let mut new_val = to_pretty_json(&new_value, 4, indent_prefix_len);
- if depth == 0 {
- new_val.push('\n');
- }
-
- (existing_value_range, new_val)
- }
- }
-}
-
-fn to_pretty_json(value: &impl Serialize, indent_size: usize, indent_prefix_len: usize) -> String {
- const SPACES: [u8; 32] = [b' '; 32];
-
- debug_assert!(indent_size <= SPACES.len());
- debug_assert!(indent_prefix_len <= SPACES.len());
-
- let mut output = Vec::new();
- let mut ser = serde_json::Serializer::with_formatter(
- &mut output,
- serde_json::ser::PrettyFormatter::with_indent(&SPACES[0..indent_size.min(SPACES.len())]),
- );
-
- value.serialize(&mut ser).unwrap();
- let text = String::from_utf8(output).unwrap();
-
- let mut adjusted_text = String::new();
- for (i, line) in text.split('\n').enumerate() {
- if i > 0 {
- adjusted_text.push_str(str::from_utf8(&SPACES[0..indent_prefix_len]).unwrap());
- }
- adjusted_text.push_str(line);
- adjusted_text.push('\n');
- }
- adjusted_text.pop();
- adjusted_text
-}
-
-pub fn parse_json_with_comments<T: DeserializeOwned>(content: &str) -> Result<T> {
- Ok(serde_json_lenient::from_str(content)?)
-}
-
#[cfg(test)]
mod tests {
use crate::VsCodeSettingsSource;
@@ -1784,6 +1582,22 @@ mod tests {
);
}
+ fn check_settings_update<T: Settings>(
+ store: &mut SettingsStore,
+ old_json: String,
+ update: fn(&mut T::FileContent),
+ expected_new_json: String,
+ cx: &mut App,
+ ) {
+ store.set_user_settings(&old_json, cx).ok();
+ let edits = store.edits_for_update::<T>(&old_json, update);
+ let mut new_json = old_json;
+ for (range, replacement) in edits.into_iter() {
+ new_json.replace_range(range, &replacement);
+ }
+ pretty_assertions::assert_eq!(new_json, expected_new_json);
+ }
+
#[gpui::test]
fn test_setting_store_update(cx: &mut App) {
let mut store = SettingsStore::new(cx);
@@ -1890,12 +1704,12 @@ mod tests {
&mut store,
r#"{
"user": { "age": 36, "name": "Max", "staff": true }
- }"#
+ }"#
.unindent(),
|settings| settings.age = Some(37),
r#"{
"user": { "age": 37, "name": "Max", "staff": true }
- }"#
+ }"#
.unindent(),
cx,
);
@@ -2118,22 +1932,6 @@ mod tests {
);
}
- fn check_settings_update<T: Settings>(
- store: &mut SettingsStore,
- old_json: String,
- update: fn(&mut T::FileContent),
- expected_new_json: String,
- cx: &mut App,
- ) {
- store.set_user_settings(&old_json, cx).ok();
- let edits = store.edits_for_update::<T>(&old_json, update);
- let mut new_json = old_json;
- for (range, replacement) in edits.into_iter() {
- new_json.replace_range(range, &replacement);
- }
- pretty_assertions::assert_eq!(new_json, expected_new_json);
- }
-
fn check_vscode_import(
store: &mut SettingsStore,
old: String,
@@ -2157,7 +1955,6 @@ mod tests {
}
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema)]
- #[schemars(deny_unknown_fields)]
struct UserSettingsContent {
name: Option<String>,
age: Option<u32>,
@@ -2200,7 +1997,6 @@ mod tests {
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
- #[schemars(deny_unknown_fields)]
struct MultiKeySettingsJson {
key1: Option<String>,
key2: Option<String>,
@@ -2239,7 +2035,6 @@ mod tests {
}
#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
- #[schemars(deny_unknown_fields)]
struct JournalSettingsJson {
pub path: Option<String>,
pub hour_format: Option<HourFormat>,
@@ -2334,7 +2129,6 @@ mod tests {
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
- #[schemars(deny_unknown_fields)]
struct LanguageSettingEntry {
language_setting_1: Option<bool>,
language_setting_2: Option<bool>,
@@ -12,12 +12,21 @@ workspace = true
path = "src/settings_ui.rs"
[dependencies]
+command_palette.workspace = true
command_palette_hooks.workspace = true
+component.workspace = true
+collections.workspace = true
+db.workspace = true
editor.workspace = true
feature_flags.workspace = true
fs.workspace = true
+fuzzy.workspace = true
gpui.workspace = true
log.workspace = true
+menu.workspace = true
+paths.workspace = true
+project.workspace = true
+search.workspace = true
schemars.workspace = true
serde.workspace = true
settings.workspace = true
@@ -2,7 +2,9 @@ use std::sync::Arc;
use gpui::{App, FontFeatures, FontWeight};
use settings::{EditableSettingControl, Settings};
-use theme::{FontFamilyCache, SystemAppearance, ThemeMode, ThemeRegistry, ThemeSettings};
+use theme::{
+ FontFamilyCache, FontFamilyName, SystemAppearance, ThemeMode, ThemeRegistry, ThemeSettings,
+};
use ui::{
CheckboxWithLabel, ContextMenu, DropdownMenu, NumericStepper, SettingsContainer, SettingsGroup,
ToggleButton, prelude::*,
@@ -189,7 +191,7 @@ impl EditableSettingControl for UiFontFamilyControl {
value: Self::Value,
_cx: &App,
) {
- settings.ui_font_family = Some(value.to_string());
+ settings.ui_font_family = Some(FontFamilyName(value.into()));
}
}
@@ -0,0 +1,902 @@
+use std::{ops::Range, sync::Arc};
+
+use collections::HashSet;
+use db::anyhow::anyhow;
+use editor::{Editor, EditorEvent};
+use feature_flags::FeatureFlagViewExt;
+use fs::Fs;
+use fuzzy::{StringMatch, StringMatchCandidate};
+use gpui::{
+ AppContext as _, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
+ FontWeight, Global, KeyContext, Keystroke, ModifiersChangedEvent, ScrollStrategy, Subscription,
+ WeakEntity, actions, div,
+};
+use settings::KeybindSource;
+use util::ResultExt;
+
+use ui::{
+ ActiveTheme as _, App, BorrowAppContext, ParentElement as _, Render, SharedString, Styled as _,
+ Window, prelude::*,
+};
+use workspace::{Item, ModalView, SerializableItem, Workspace, register_serializable_item};
+
+use crate::{
+ SettingsUiFeatureFlag,
+ keybindings::persistence::KEYBINDING_EDITORS,
+ ui_components::table::{Table, TableInteractionState},
+};
+
+actions!(zed, [OpenKeymapEditor]);
+
+pub fn init(cx: &mut App) {
+ let keymap_event_channel = KeymapEventChannel::new();
+ cx.set_global(keymap_event_channel);
+
+ cx.on_action(|_: &OpenKeymapEditor, cx| {
+ workspace::with_active_or_new_workspace(cx, move |workspace, window, cx| {
+ let existing = workspace
+ .active_pane()
+ .read(cx)
+ .items()
+ .find_map(|item| item.downcast::<KeymapEditor>());
+
+ if let Some(existing) = existing {
+ workspace.activate_item(&existing, true, true, window, cx);
+ } else {
+ let keymap_editor =
+ cx.new(|cx| KeymapEditor::new(workspace.weak_handle(), window, cx));
+ workspace.add_item_to_active_pane(Box::new(keymap_editor), None, true, window, cx);
+ }
+ });
+ });
+
+ cx.observe_new(|_workspace: &mut Workspace, window, cx| {
+ let Some(window) = window else { return };
+
+ let keymap_ui_actions = [std::any::TypeId::of::<OpenKeymapEditor>()];
+
+ command_palette_hooks::CommandPaletteFilter::update_global(cx, |filter, _cx| {
+ filter.hide_action_types(&keymap_ui_actions);
+ });
+
+ cx.observe_flag::<SettingsUiFeatureFlag, _>(
+ window,
+ move |is_enabled, _workspace, _, cx| {
+ if is_enabled {
+ command_palette_hooks::CommandPaletteFilter::update_global(
+ cx,
+ |filter, _cx| {
+ filter.show_action_types(keymap_ui_actions.iter());
+ },
+ );
+ } else {
+ command_palette_hooks::CommandPaletteFilter::update_global(
+ cx,
+ |filter, _cx| {
+ filter.hide_action_types(&keymap_ui_actions);
+ },
+ );
+ }
+ },
+ )
+ .detach();
+ })
+ .detach();
+
+ register_serializable_item::<KeymapEditor>(cx);
+}
+
+pub struct KeymapEventChannel {}
+
+impl Global for KeymapEventChannel {}
+
+impl KeymapEventChannel {
+ fn new() -> Self {
+ Self {}
+ }
+
+ pub fn trigger_keymap_changed(cx: &mut App) {
+ let Some(_event_channel) = cx.try_global::<Self>() else {
+ // don't panic if no global defined. This usually happens in tests
+ return;
+ };
+ cx.update_global(|_event_channel: &mut Self, _| {
+ /* triggers observers in KeymapEditors */
+ });
+ }
+}
+
+struct KeymapEditor {
+ workspace: WeakEntity<Workspace>,
+ focus_handle: FocusHandle,
+ _keymap_subscription: Subscription,
+ keybindings: Vec<ProcessedKeybinding>,
+ // corresponds 1 to 1 with keybindings
+ string_match_candidates: Arc<Vec<StringMatchCandidate>>,
+ matches: Vec<StringMatch>,
+ table_interaction_state: Entity<TableInteractionState>,
+ filter_editor: Entity<Editor>,
+ selected_index: Option<usize>,
+}
+
+impl EventEmitter<()> for KeymapEditor {}
+
+impl Focusable for KeymapEditor {
+ fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
+ return self.filter_editor.focus_handle(cx);
+ }
+}
+
+impl KeymapEditor {
+ fn new(workspace: WeakEntity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {
+ let focus_handle = cx.focus_handle();
+
+ let _keymap_subscription =
+ cx.observe_global::<KeymapEventChannel>(Self::update_keybindings);
+ let table_interaction_state = TableInteractionState::new(window, cx);
+
+ let filter_editor = cx.new(|cx| {
+ let mut editor = Editor::single_line(window, cx);
+ editor.set_placeholder_text("Filter action names...", cx);
+ editor
+ });
+
+ cx.subscribe(&filter_editor, |this, _, e: &EditorEvent, cx| {
+ if !matches!(e, EditorEvent::BufferEdited) {
+ return;
+ }
+
+ this.update_matches(cx);
+ })
+ .detach();
+
+ let mut this = Self {
+ workspace,
+ keybindings: vec![],
+ string_match_candidates: Arc::new(vec![]),
+ matches: vec![],
+ focus_handle: focus_handle.clone(),
+ _keymap_subscription,
+ table_interaction_state,
+ filter_editor,
+ selected_index: None,
+ };
+
+ this.update_keybindings(cx);
+
+ this
+ }
+
+ fn update_matches(&mut self, cx: &mut Context<Self>) {
+ let query = self.filter_editor.read(cx).text(cx);
+ let string_match_candidates = self.string_match_candidates.clone();
+ let executor = cx.background_executor().clone();
+ let keybind_count = self.keybindings.len();
+ let query = command_palette::normalize_action_query(&query);
+ let fuzzy_match = cx.background_spawn(async move {
+ fuzzy::match_strings(
+ &string_match_candidates,
+ &query,
+ true,
+ true,
+ keybind_count,
+ &Default::default(),
+ executor,
+ )
+ .await
+ });
+
+ cx.spawn(async move |this, cx| {
+ let matches = fuzzy_match.await;
+ this.update(cx, |this, cx| {
+ this.selected_index.take();
+ this.scroll_to_item(0, ScrollStrategy::Top, cx);
+ this.matches = matches;
+ cx.notify();
+ })
+ })
+ .detach();
+ }
+
+ fn process_bindings(
+ cx: &mut Context<Self>,
+ ) -> (Vec<ProcessedKeybinding>, Vec<StringMatchCandidate>) {
+ let key_bindings_ptr = cx.key_bindings();
+ let lock = key_bindings_ptr.borrow();
+ let key_bindings = lock.bindings();
+ let mut unmapped_action_names = HashSet::from_iter(cx.all_action_names());
+
+ let mut processed_bindings = Vec::new();
+ let mut string_match_candidates = Vec::new();
+
+ for key_binding in key_bindings {
+ let source = key_binding.meta().map(settings::KeybindSource::from_meta);
+
+ let keystroke_text = ui::text_for_keystrokes(key_binding.keystrokes(), cx);
+ let ui_key_binding = Some(
+ ui::KeyBinding::new(key_binding.clone(), cx)
+ .vim_mode(source == Some(settings::KeybindSource::Vim)),
+ );
+
+ let context = key_binding
+ .predicate()
+ .map(|predicate| predicate.to_string())
+ .unwrap_or_else(|| "<global>".to_string());
+
+ let source = source.map(|source| (source, source.name().into()));
+
+ let action_name = key_binding.action().name();
+ unmapped_action_names.remove(&action_name);
+
+ let index = processed_bindings.len();
+ let string_match_candidate = StringMatchCandidate::new(index, &action_name);
+ processed_bindings.push(ProcessedKeybinding {
+ keystroke_text: keystroke_text.into(),
+ ui_key_binding,
+ action: action_name.into(),
+ action_input: key_binding.action_input(),
+ context: context.into(),
+ source,
+ });
+ string_match_candidates.push(string_match_candidate);
+ }
+
+ let empty = SharedString::new_static("");
+ for action_name in unmapped_action_names.into_iter() {
+ let index = processed_bindings.len();
+ let string_match_candidate = StringMatchCandidate::new(index, &action_name);
+ processed_bindings.push(ProcessedKeybinding {
+ keystroke_text: empty.clone(),
+ ui_key_binding: None,
+ action: (*action_name).into(),
+ action_input: None,
+ context: empty.clone(),
+ source: None,
+ });
+ string_match_candidates.push(string_match_candidate);
+ }
+
+ (processed_bindings, string_match_candidates)
+ }
+
+ fn update_keybindings(self: &mut KeymapEditor, cx: &mut Context<KeymapEditor>) {
+ let (key_bindings, string_match_candidates) = Self::process_bindings(cx);
+ self.keybindings = key_bindings;
+ self.string_match_candidates = Arc::new(string_match_candidates);
+ self.matches = self
+ .string_match_candidates
+ .iter()
+ .enumerate()
+ .map(|(ix, candidate)| StringMatch {
+ candidate_id: ix,
+ score: 0.0,
+ positions: vec![],
+ string: candidate.string.clone(),
+ })
+ .collect();
+
+ self.update_matches(cx);
+ cx.notify();
+ }
+
+ fn dispatch_context(&self, _window: &Window, _cx: &Context<Self>) -> KeyContext {
+ let mut dispatch_context = KeyContext::new_with_defaults();
+ dispatch_context.add("KeymapEditor");
+ dispatch_context.add("menu");
+
+ dispatch_context
+ }
+
+ fn scroll_to_item(&self, index: usize, strategy: ScrollStrategy, cx: &mut App) {
+ let index = usize::min(index, self.matches.len().saturating_sub(1));
+ self.table_interaction_state.update(cx, |this, _cx| {
+ this.scroll_handle.scroll_to_item(index, strategy);
+ });
+ }
+
+ fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context<Self>) {
+ if let Some(selected) = self.selected_index {
+ let selected = selected + 1;
+ if selected >= self.matches.len() {
+ self.select_last(&Default::default(), window, cx);
+ } else {
+ self.selected_index = Some(selected);
+ self.scroll_to_item(selected, ScrollStrategy::Center, cx);
+ cx.notify();
+ }
+ } else {
+ self.select_first(&Default::default(), window, cx);
+ }
+ }
+
+ fn select_previous(
+ &mut self,
+ _: &menu::SelectPrevious,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if let Some(selected) = self.selected_index {
+ if selected == 0 {
+ return;
+ }
+
+ let selected = selected - 1;
+
+ if selected >= self.matches.len() {
+ self.select_last(&Default::default(), window, cx);
+ } else {
+ self.selected_index = Some(selected);
+ self.scroll_to_item(selected, ScrollStrategy::Center, cx);
+ cx.notify();
+ }
+ } else {
+ self.select_last(&Default::default(), window, cx);
+ }
+ }
+
+ fn select_first(
+ &mut self,
+ _: &menu::SelectFirst,
+ _window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if self.matches.get(0).is_some() {
+ self.selected_index = Some(0);
+ self.scroll_to_item(0, ScrollStrategy::Center, cx);
+ cx.notify();
+ }
+ }
+
+ fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
+ if self.matches.last().is_some() {
+ let index = self.matches.len() - 1;
+ self.selected_index = Some(index);
+ self.scroll_to_item(index, ScrollStrategy::Center, cx);
+ cx.notify();
+ }
+ }
+
+ fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
+ let Some(index) = self.selected_index else {
+ return;
+ };
+ let keybind = self.keybindings[self.matches[index].candidate_id].clone();
+
+ self.edit_keybinding(keybind, window, cx);
+ }
+
+ fn edit_keybinding(
+ &mut self,
+ keybind: ProcessedKeybinding,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.workspace
+ .update(cx, |workspace, cx| {
+ let fs = workspace.app_state().fs.clone();
+ workspace.toggle_modal(window, cx, |window, cx| {
+ let modal = KeybindingEditorModal::new(keybind, fs, window, cx);
+ window.focus(&modal.focus_handle(cx));
+ modal
+ });
+ })
+ .log_err();
+ }
+
+ fn focus_search(
+ &mut self,
+ _: &search::FocusSearch,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if !self
+ .filter_editor
+ .focus_handle(cx)
+ .contains_focused(window, cx)
+ {
+ window.focus(&self.filter_editor.focus_handle(cx));
+ } else {
+ self.filter_editor.update(cx, |editor, cx| {
+ editor.select_all(&Default::default(), window, cx);
+ });
+ }
+ self.selected_index.take();
+ }
+}
+
+#[derive(Clone)]
+struct ProcessedKeybinding {
+ keystroke_text: SharedString,
+ ui_key_binding: Option<ui::KeyBinding>,
+ action: SharedString,
+ action_input: Option<SharedString>,
+ context: SharedString,
+ source: Option<(KeybindSource, SharedString)>,
+}
+
+impl Item for KeymapEditor {
+ type Event = ();
+
+ fn tab_content_text(&self, _detail: usize, _cx: &App) -> ui::SharedString {
+ "Keymap Editor".into()
+ }
+}
+
+impl Render for KeymapEditor {
+ fn render(&mut self, window: &mut Window, cx: &mut ui::Context<Self>) -> impl ui::IntoElement {
+ let row_count = self.matches.len();
+ let theme = cx.theme();
+
+ div()
+ .key_context(self.dispatch_context(window, cx))
+ .on_action(cx.listener(Self::select_next))
+ .on_action(cx.listener(Self::select_previous))
+ .on_action(cx.listener(Self::select_first))
+ .on_action(cx.listener(Self::select_last))
+ .on_action(cx.listener(Self::focus_search))
+ .on_action(cx.listener(Self::confirm))
+ .size_full()
+ .bg(theme.colors().editor_background)
+ .id("keymap-editor")
+ .track_focus(&self.focus_handle)
+ .px_4()
+ .v_flex()
+ .pb_4()
+ .child(
+ h_flex()
+ .key_context({
+ let mut context = KeyContext::new_with_defaults();
+ context.add("BufferSearchBar");
+ context
+ })
+ .w_full()
+ .h_12()
+ .px_4()
+ .my_4()
+ .border_2()
+ .border_color(theme.colors().border)
+ .child(self.filter_editor.clone()),
+ )
+ .child(
+ Table::new()
+ .interactable(&self.table_interaction_state)
+ .striped()
+ .column_widths([rems(24.), rems(16.), rems(32.), rems(8.)])
+ .header(["Command", "Keystrokes", "Context", "Source"])
+ .selected_item_index(self.selected_index)
+ .on_click_row(cx.processor(|this, row_index, _window, _cx| {
+ this.selected_index = Some(row_index);
+ }))
+ .uniform_list(
+ "keymap-editor-table",
+ row_count,
+ cx.processor(move |this, range: Range<usize>, _window, _cx| {
+ range
+ .filter_map(|index| {
+ let candidate_id = this.matches.get(index)?.candidate_id;
+ let binding = &this.keybindings[candidate_id];
+ let action = h_flex()
+ .items_start()
+ .gap_1()
+ .child(binding.action.clone())
+ .when_some(
+ binding.action_input.clone(),
+ |this, binding_input| this.child(binding_input),
+ );
+ let keystrokes = binding.ui_key_binding.clone().map_or(
+ binding.keystroke_text.clone().into_any_element(),
+ IntoElement::into_any_element,
+ );
+ let context = binding.context.clone();
+ let source = binding
+ .source
+ .clone()
+ .map(|(_source, name)| name)
+ .unwrap_or_default();
+ Some([
+ action.into_any_element(),
+ keystrokes,
+ context.into_any_element(),
+ source.into_any_element(),
+ ])
+ })
+ .collect()
+ }),
+ ),
+ )
+ }
+}
+
+struct KeybindingEditorModal {
+ editing_keybind: ProcessedKeybinding,
+ keybind_editor: Entity<KeybindInput>,
+ fs: Arc<dyn Fs>,
+ error: Option<String>,
+}
+
+impl ModalView for KeybindingEditorModal {}
+
+impl EventEmitter<DismissEvent> for KeybindingEditorModal {}
+
+impl Focusable for KeybindingEditorModal {
+ fn focus_handle(&self, cx: &App) -> FocusHandle {
+ self.keybind_editor.focus_handle(cx)
+ }
+}
+
+impl KeybindingEditorModal {
+ pub fn new(
+ editing_keybind: ProcessedKeybinding,
+ fs: Arc<dyn Fs>,
+ _window: &mut Window,
+ cx: &mut App,
+ ) -> Self {
+ let keybind_editor = cx.new(KeybindInput::new);
+ Self {
+ editing_keybind,
+ fs,
+ keybind_editor,
+ error: None,
+ }
+ }
+}
+
+impl Render for KeybindingEditorModal {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ let theme = cx.theme().colors();
+ return v_flex()
+ .gap_4()
+ .w(rems(36.))
+ .child(
+ v_flex()
+ .items_center()
+ .text_center()
+ .bg(theme.background)
+ .border_color(theme.border)
+ .border_2()
+ .px_4()
+ .py_2()
+ .w_full()
+ .child(
+ div()
+ .text_lg()
+ .font_weight(FontWeight::BOLD)
+ .child("Input desired keybinding, then hit save"),
+ )
+ .child(
+ h_flex()
+ .w_full()
+ .child(self.keybind_editor.clone())
+ .child(
+ IconButton::new("backspace-btn", ui::IconName::Backspace).on_click(
+ cx.listener(|this, _event, _window, cx| {
+ this.keybind_editor.update(cx, |editor, cx| {
+ editor.keystrokes.pop();
+ cx.notify();
+ })
+ }),
+ ),
+ )
+ .child(IconButton::new("clear-btn", ui::IconName::Eraser).on_click(
+ cx.listener(|this, _event, _window, cx| {
+ this.keybind_editor.update(cx, |editor, cx| {
+ editor.keystrokes.clear();
+ cx.notify();
+ })
+ }),
+ )),
+ )
+ .child(
+ h_flex().w_full().items_center().justify_center().child(
+ Button::new("save-btn", "Save")
+ .label_size(LabelSize::Large)
+ .on_click(cx.listener(|this, _event, _window, cx| {
+ let existing_keybind = this.editing_keybind.clone();
+ let fs = this.fs.clone();
+ let new_keystrokes = this
+ .keybind_editor
+ .read_with(cx, |editor, _| editor.keystrokes.clone());
+ if new_keystrokes.is_empty() {
+ this.error = Some("Keystrokes cannot be empty".to_string());
+ cx.notify();
+ return;
+ }
+ let tab_size =
+ cx.global::<settings::SettingsStore>().json_tab_size();
+ cx.spawn(async move |this, cx| {
+ if let Err(err) = save_keybinding_update(
+ existing_keybind,
+ &new_keystrokes,
+ &fs,
+ tab_size,
+ )
+ .await
+ {
+ this.update(cx, |this, cx| {
+ this.error = Some(err);
+ cx.notify();
+ })
+ .log_err();
+ }
+ })
+ .detach();
+ })),
+ ),
+ ),
+ )
+ .when_some(self.error.clone(), |this, error| {
+ this.child(
+ div()
+ .bg(theme.background)
+ .border_color(theme.border)
+ .border_2()
+ .rounded_md()
+ .child(error),
+ )
+ });
+ }
+}
+
+async fn save_keybinding_update(
+ existing: ProcessedKeybinding,
+ new_keystrokes: &[Keystroke],
+ fs: &Arc<dyn Fs>,
+ tab_size: usize,
+) -> Result<(), String> {
+ let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
+ .await
+ .map_err(|err| format!("Failed to load keymap file: {}", err))?;
+ let existing_keystrokes = existing
+ .ui_key_binding
+ .as_ref()
+ .map(|keybinding| keybinding.key_binding.keystrokes())
+ .unwrap_or_default();
+ let operation = if existing.ui_key_binding.is_some() {
+ settings::KeybindUpdateOperation::Replace {
+ target: settings::KeybindUpdateTarget {
+ context: Some(existing.context.as_ref()).filter(|context| !context.is_empty()),
+ keystrokes: existing_keystrokes,
+ action_name: &existing.action,
+ use_key_equivalents: false,
+ input: existing.action_input.as_ref().map(|input| input.as_ref()),
+ },
+ target_source: existing
+ .source
+ .map(|(source, _name)| source)
+ .unwrap_or(KeybindSource::User),
+ source: settings::KeybindUpdateTarget {
+ context: Some(existing.context.as_ref()).filter(|context| !context.is_empty()),
+ keystrokes: new_keystrokes,
+ action_name: &existing.action,
+ use_key_equivalents: false,
+ input: existing.action_input.as_ref().map(|input| input.as_ref()),
+ },
+ }
+ } else {
+ return Err(
+ "Not Implemented: Creating new bindings from unbound actions is not supported yet."
+ .to_string(),
+ );
+ };
+ let updated_keymap_contents =
+ settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
+ .map_err(|err| format!("Failed to update keybinding: {}", err))?;
+ fs.atomic_write(paths::keymap_file().clone(), updated_keymap_contents)
+ .await
+ .map_err(|err| format!("Failed to write keymap file: {}", err))?;
+ Ok(())
+}
+
+struct KeybindInput {
+ keystrokes: Vec<Keystroke>,
+ focus_handle: FocusHandle,
+}
+
+impl KeybindInput {
+ fn new(cx: &mut Context<Self>) -> Self {
+ let focus_handle = cx.focus_handle();
+ Self {
+ keystrokes: Vec::new(),
+ focus_handle,
+ }
+ }
+
+ fn on_modifiers_changed(
+ &mut self,
+ event: &ModifiersChangedEvent,
+ _window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if let Some(last) = self.keystrokes.last_mut()
+ && last.key.is_empty()
+ {
+ if !event.modifiers.modified() {
+ self.keystrokes.pop();
+ } else {
+ last.modifiers = event.modifiers;
+ }
+ } else {
+ self.keystrokes.push(Keystroke {
+ modifiers: event.modifiers,
+ key: "".to_string(),
+ key_char: None,
+ });
+ }
+ cx.stop_propagation();
+ cx.notify();
+ }
+
+ fn on_key_down(
+ &mut self,
+ event: &gpui::KeyDownEvent,
+ _window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if event.is_held {
+ return;
+ }
+ if let Some(last) = self.keystrokes.last_mut()
+ && last.key.is_empty()
+ {
+ *last = event.keystroke.clone();
+ } else {
+ self.keystrokes.push(event.keystroke.clone());
+ }
+ cx.stop_propagation();
+ cx.notify();
+ }
+
+ fn on_key_up(
+ &mut self,
+ event: &gpui::KeyUpEvent,
+ _window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if let Some(last) = self.keystrokes.last_mut()
+ && !last.key.is_empty()
+ && last.modifiers == event.keystroke.modifiers
+ {
+ self.keystrokes.push(Keystroke {
+ modifiers: event.keystroke.modifiers,
+ key: "".to_string(),
+ key_char: None,
+ });
+ }
+ cx.stop_propagation();
+ cx.notify();
+ }
+}
+
+impl Focusable for KeybindInput {
+ fn focus_handle(&self, _cx: &App) -> FocusHandle {
+ self.focus_handle.clone()
+ }
+}
+
+impl Render for KeybindInput {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ let colors = cx.theme().colors();
+ return div()
+ .track_focus(&self.focus_handle)
+ .on_modifiers_changed(cx.listener(Self::on_modifiers_changed))
+ .on_key_down(cx.listener(Self::on_key_down))
+ .on_key_up(cx.listener(Self::on_key_up))
+ .focus(|mut style| {
+ style.border_color = Some(colors.border_focused);
+ style
+ })
+ .h_12()
+ .w_full()
+ .bg(colors.editor_background)
+ .border_2()
+ .border_color(colors.border)
+ .p_4()
+ .flex_row()
+ .text_center()
+ .justify_center()
+ .child(ui::text_for_keystrokes(&self.keystrokes, cx));
+ }
+}
+
+impl SerializableItem for KeymapEditor {
+ fn serialized_item_kind() -> &'static str {
+ "KeymapEditor"
+ }
+
+ fn cleanup(
+ workspace_id: workspace::WorkspaceId,
+ alive_items: Vec<workspace::ItemId>,
+ _window: &mut Window,
+ cx: &mut App,
+ ) -> gpui::Task<gpui::Result<()>> {
+ workspace::delete_unloaded_items(
+ alive_items,
+ workspace_id,
+ "keybinding_editors",
+ &KEYBINDING_EDITORS,
+ cx,
+ )
+ }
+
+ fn deserialize(
+ _project: Entity<project::Project>,
+ workspace: WeakEntity<Workspace>,
+ workspace_id: workspace::WorkspaceId,
+ item_id: workspace::ItemId,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> gpui::Task<gpui::Result<Entity<Self>>> {
+ window.spawn(cx, async move |cx| {
+ if KEYBINDING_EDITORS
+ .get_keybinding_editor(item_id, workspace_id)?
+ .is_some()
+ {
+ cx.update(|window, cx| cx.new(|cx| KeymapEditor::new(workspace, window, cx)))
+ } else {
+ Err(anyhow!("No keybinding editor to deserialize"))
+ }
+ })
+ }
+
+ fn serialize(
+ &mut self,
+ workspace: &mut Workspace,
+ item_id: workspace::ItemId,
+ _closing: bool,
+ _window: &mut Window,
+ cx: &mut ui::Context<Self>,
+ ) -> Option<gpui::Task<gpui::Result<()>>> {
+ let workspace_id = workspace.database_id()?;
+ Some(cx.background_spawn(async move {
+ KEYBINDING_EDITORS
+ .save_keybinding_editor(item_id, workspace_id)
+ .await
+ }))
+ }
+
+ fn should_serialize(&self, _event: &Self::Event) -> bool {
+ false
+ }
+}
+
+mod persistence {
+ use db::{define_connection, query, sqlez_macros::sql};
+ use workspace::WorkspaceDb;
+
+ define_connection! {
+ pub static ref KEYBINDING_EDITORS: KeybindingEditorDb<WorkspaceDb> =
+ &[sql!(
+ CREATE TABLE keybinding_editors (
+ workspace_id INTEGER,
+ item_id INTEGER UNIQUE,
+
+ PRIMARY KEY(workspace_id, item_id),
+ FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
+ ON DELETE CASCADE
+ ) STRICT;
+ )];
+ }
+
+ impl KeybindingEditorDb {
+ query! {
+ pub async fn save_keybinding_editor(
+ item_id: workspace::ItemId,
+ workspace_id: workspace::WorkspaceId
+ ) -> Result<()> {
+ INSERT OR REPLACE INTO keybinding_editors(item_id, workspace_id)
+ VALUES (?, ?)
+ }
+ }
+
+ query! {
+ pub fn get_keybinding_editor(
+ item_id: workspace::ItemId,
+ workspace_id: workspace::WorkspaceId
+ ) -> Result<Option<workspace::ItemId>> {
+ SELECT item_id
+ FROM keybinding_editors
+ WHERE item_id = ? AND workspace_id = ?
+ }
+ }
+ }
+}
@@ -20,6 +20,9 @@ use workspace::{Workspace, with_active_or_new_workspace};
use crate::appearance_settings_controls::AppearanceSettingsControls;
+pub mod keybindings;
+pub mod ui_components;
+
pub struct SettingsUiFeatureFlag;
impl FeatureFlag for SettingsUiFeatureFlag {
@@ -28,6 +31,7 @@ impl FeatureFlag for SettingsUiFeatureFlag {
#[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,
@@ -35,6 +39,7 @@ pub struct ImportVsCodeSettings {
#[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,
@@ -121,6 +126,8 @@ pub fn init(cx: &mut App) {
.detach();
})
.detach();
+
+ keybindings::init(cx);
}
async fn handle_import_vscode_settings(
@@ -0,0 +1 @@
+pub mod table;
@@ -0,0 +1,884 @@
+use std::{ops::Range, rc::Rc, time::Duration};
+
+use editor::{EditorSettings, ShowScrollbar, scroll::ScrollbarAutoHide};
+use gpui::{
+ AppContext, Axis, Context, Entity, FocusHandle, FontWeight, Length,
+ ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton, Task, UniformListScrollHandle,
+ WeakEntity, transparent_black, uniform_list,
+};
+use settings::Settings as _;
+use ui::{
+ ActiveTheme as _, AnyElement, App, Button, ButtonCommon as _, ButtonStyle, Color, Component,
+ ComponentScope, Div, ElementId, FixedWidth as _, FluentBuilder as _, Indicator,
+ InteractiveElement as _, IntoElement, ParentElement, Pixels, RegisterComponent, RenderOnce,
+ Scrollbar, ScrollbarState, StatefulInteractiveElement as _, Styled, StyledExt as _,
+ StyledTypography, Window, div, example_group_with_title, h_flex, px, single_example, v_flex,
+};
+
+struct UniformListData<const COLS: usize> {
+ render_item_fn: Box<dyn Fn(Range<usize>, &mut Window, &mut App) -> Vec<[AnyElement; COLS]>>,
+ element_id: ElementId,
+ row_count: usize,
+}
+
+enum TableContents<const COLS: usize> {
+ Vec(Vec<[AnyElement; COLS]>),
+ UniformList(UniformListData<COLS>),
+}
+
+impl<const COLS: usize> TableContents<COLS> {
+ fn rows_mut(&mut self) -> Option<&mut Vec<[AnyElement; COLS]>> {
+ match self {
+ TableContents::Vec(rows) => Some(rows),
+ TableContents::UniformList(_) => None,
+ }
+ }
+
+ fn len(&self) -> usize {
+ match self {
+ TableContents::Vec(rows) => rows.len(),
+ TableContents::UniformList(data) => data.row_count,
+ }
+ }
+}
+
+pub struct TableInteractionState {
+ pub focus_handle: FocusHandle,
+ pub scroll_handle: UniformListScrollHandle,
+ pub horizontal_scrollbar: ScrollbarProperties,
+ pub vertical_scrollbar: ScrollbarProperties,
+}
+
+impl TableInteractionState {
+ pub fn new(window: &mut Window, cx: &mut App) -> Entity<Self> {
+ cx.new(|cx| {
+ let focus_handle = cx.focus_handle();
+
+ cx.on_focus_out(&focus_handle, window, |this: &mut Self, _, window, cx| {
+ this.hide_scrollbars(window, cx);
+ })
+ .detach();
+
+ let scroll_handle = UniformListScrollHandle::new();
+ let vertical_scrollbar = ScrollbarProperties {
+ axis: Axis::Vertical,
+ state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
+ show_scrollbar: false,
+ show_track: false,
+ auto_hide: false,
+ hide_task: None,
+ };
+
+ let horizontal_scrollbar = ScrollbarProperties {
+ axis: Axis::Horizontal,
+ state: ScrollbarState::new(scroll_handle.clone()).parent_entity(&cx.entity()),
+ show_scrollbar: false,
+ show_track: false,
+ auto_hide: false,
+ hide_task: None,
+ };
+
+ let mut this = Self {
+ focus_handle,
+ scroll_handle,
+ horizontal_scrollbar,
+ vertical_scrollbar,
+ };
+
+ this.update_scrollbar_visibility(cx);
+ this
+ })
+ }
+
+ fn update_scrollbar_visibility(&mut self, cx: &mut Context<Self>) {
+ let show_setting = EditorSettings::get_global(cx).scrollbar.show;
+
+ let scroll_handle = self.scroll_handle.0.borrow();
+
+ let autohide = |show: ShowScrollbar, cx: &mut Context<Self>| match show {
+ ShowScrollbar::Auto => true,
+ ShowScrollbar::System => cx
+ .try_global::<ScrollbarAutoHide>()
+ .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
+ ShowScrollbar::Always => false,
+ ShowScrollbar::Never => false,
+ };
+
+ let longest_item_width = scroll_handle.last_item_size.and_then(|size| {
+ (size.contents.width > size.item.width).then_some(size.contents.width)
+ });
+
+ // is there an item long enough that we should show a horizontal scrollbar?
+ let item_wider_than_container = if let Some(longest_item_width) = longest_item_width {
+ longest_item_width > px(scroll_handle.base_handle.bounds().size.width.0)
+ } else {
+ true
+ };
+
+ let show_scrollbar = match show_setting {
+ ShowScrollbar::Auto | ShowScrollbar::System | ShowScrollbar::Always => true,
+ ShowScrollbar::Never => false,
+ };
+ let show_vertical = show_scrollbar;
+
+ let show_horizontal = item_wider_than_container && show_scrollbar;
+
+ let show_horizontal_track =
+ show_horizontal && matches!(show_setting, ShowScrollbar::Always);
+
+ // TODO: we probably should hide the scroll track when the list doesn't need to scroll
+ let show_vertical_track = show_vertical && matches!(show_setting, ShowScrollbar::Always);
+
+ self.vertical_scrollbar = ScrollbarProperties {
+ axis: self.vertical_scrollbar.axis,
+ state: self.vertical_scrollbar.state.clone(),
+ show_scrollbar: show_vertical,
+ show_track: show_vertical_track,
+ auto_hide: autohide(show_setting, cx),
+ hide_task: None,
+ };
+
+ self.horizontal_scrollbar = ScrollbarProperties {
+ axis: self.horizontal_scrollbar.axis,
+ state: self.horizontal_scrollbar.state.clone(),
+ show_scrollbar: show_horizontal,
+ show_track: show_horizontal_track,
+ auto_hide: autohide(show_setting, cx),
+ hide_task: None,
+ };
+
+ cx.notify();
+ }
+
+ fn hide_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ self.horizontal_scrollbar.hide(window, cx);
+ self.vertical_scrollbar.hide(window, cx);
+ }
+
+ // fn listener(this: Entity<Self>, fn: F) ->
+
+ pub fn listener<E: ?Sized>(
+ this: &Entity<Self>,
+ f: impl Fn(&mut Self, &E, &mut Window, &mut Context<Self>) + 'static,
+ ) -> impl Fn(&E, &mut Window, &mut App) + 'static {
+ let view = this.downgrade();
+ move |e: &E, window: &mut Window, cx: &mut App| {
+ view.update(cx, |view, cx| f(view, e, window, cx)).ok();
+ }
+ }
+
+ fn render_vertical_scrollbar_track(
+ this: &Entity<Self>,
+ parent: Div,
+ scroll_track_size: Pixels,
+ cx: &mut App,
+ ) -> Div {
+ if !this.read(cx).vertical_scrollbar.show_track {
+ return parent;
+ }
+ let child = v_flex()
+ .h_full()
+ .flex_none()
+ .w(scroll_track_size)
+ .bg(cx.theme().colors().background)
+ .child(
+ div()
+ .size_full()
+ .flex_1()
+ .border_l_1()
+ .border_color(cx.theme().colors().border),
+ );
+ parent.child(child)
+ }
+
+ fn render_vertical_scrollbar(this: &Entity<Self>, parent: Div, cx: &mut App) -> Div {
+ if !this.read(cx).vertical_scrollbar.show_scrollbar {
+ return parent;
+ }
+ let child = div()
+ .id(("table-vertical-scrollbar", this.entity_id()))
+ .occlude()
+ .flex_none()
+ .h_full()
+ .cursor_default()
+ .absolute()
+ .right_0()
+ .top_0()
+ .bottom_0()
+ .w(px(12.))
+ .on_mouse_move(Self::listener(this, |_, _, _, cx| {
+ cx.notify();
+ cx.stop_propagation()
+ }))
+ .on_hover(|_, _, cx| {
+ cx.stop_propagation();
+ })
+ .on_mouse_up(
+ MouseButton::Left,
+ Self::listener(this, |this, _, window, cx| {
+ if !this.vertical_scrollbar.state.is_dragging()
+ && !this.focus_handle.contains_focused(window, cx)
+ {
+ this.vertical_scrollbar.hide(window, cx);
+ cx.notify();
+ }
+
+ cx.stop_propagation();
+ }),
+ )
+ .on_any_mouse_down(|_, _, cx| {
+ cx.stop_propagation();
+ })
+ .on_scroll_wheel(Self::listener(&this, |_, _, _, cx| {
+ cx.notify();
+ }))
+ .children(Scrollbar::vertical(
+ this.read(cx).vertical_scrollbar.state.clone(),
+ ));
+ parent.child(child)
+ }
+
+ /// Renders the horizontal scrollbar.
+ ///
+ /// The right offset is used to determine how far to the right the
+ /// scrollbar should extend to, useful for ensuring it doesn't collide
+ /// with the vertical scrollbar when visible.
+ fn render_horizontal_scrollbar(
+ this: &Entity<Self>,
+ parent: Div,
+ right_offset: Pixels,
+ cx: &mut App,
+ ) -> Div {
+ if !this.read(cx).horizontal_scrollbar.show_scrollbar {
+ return parent;
+ }
+ let child = div()
+ .id(("table-horizontal-scrollbar", this.entity_id()))
+ .occlude()
+ .flex_none()
+ .w_full()
+ .cursor_default()
+ .absolute()
+ .bottom_neg_px()
+ .left_0()
+ .right_0()
+ .pr(right_offset)
+ .on_mouse_move(Self::listener(this, |_, _, _, cx| {
+ cx.notify();
+ cx.stop_propagation()
+ }))
+ .on_hover(|_, _, cx| {
+ cx.stop_propagation();
+ })
+ .on_any_mouse_down(|_, _, cx| {
+ cx.stop_propagation();
+ })
+ .on_mouse_up(
+ MouseButton::Left,
+ Self::listener(this, |this, _, window, cx| {
+ if !this.horizontal_scrollbar.state.is_dragging()
+ && !this.focus_handle.contains_focused(window, cx)
+ {
+ this.horizontal_scrollbar.hide(window, cx);
+ cx.notify();
+ }
+
+ cx.stop_propagation();
+ }),
+ )
+ .on_scroll_wheel(Self::listener(this, |_, _, _, cx| {
+ cx.notify();
+ }))
+ .children(Scrollbar::horizontal(
+ // percentage as f32..end_offset as f32,
+ this.read(cx).horizontal_scrollbar.state.clone(),
+ ));
+ parent.child(child)
+ }
+
+ fn render_horizontal_scrollbar_track(
+ this: &Entity<Self>,
+ parent: Div,
+ scroll_track_size: Pixels,
+ cx: &mut App,
+ ) -> Div {
+ if !this.read(cx).horizontal_scrollbar.show_track {
+ return parent;
+ }
+ let child = h_flex()
+ .w_full()
+ .h(scroll_track_size)
+ .flex_none()
+ .relative()
+ .child(
+ div()
+ .w_full()
+ .flex_1()
+ // for some reason the horizontal scrollbar is 1px
+ // taller than the vertical scrollbar??
+ .h(scroll_track_size - px(1.))
+ .bg(cx.theme().colors().background)
+ .border_t_1()
+ .border_color(cx.theme().colors().border),
+ )
+ .when(this.read(cx).vertical_scrollbar.show_track, |parent| {
+ parent
+ .child(
+ div()
+ .flex_none()
+ // -1px prevents a missing pixel between the two container borders
+ .w(scroll_track_size - px(1.))
+ .h_full(),
+ )
+ .child(
+ // HACK: Fill the missing 1px π₯²
+ div()
+ .absolute()
+ .right(scroll_track_size - px(1.))
+ .bottom(scroll_track_size - px(1.))
+ .size_px()
+ .bg(cx.theme().colors().border),
+ )
+ });
+
+ parent.child(child)
+ }
+}
+
+/// A table component
+#[derive(RegisterComponent, IntoElement)]
+pub struct Table<const COLS: usize = 3> {
+ striped: bool,
+ width: Option<Length>,
+ headers: Option<[AnyElement; COLS]>,
+ rows: TableContents<COLS>,
+ interaction_state: Option<WeakEntity<TableInteractionState>>,
+ selected_item_index: Option<usize>,
+ column_widths: Option<[Length; COLS]>,
+ on_click_row: Option<Rc<dyn Fn(usize, &mut Window, &mut App)>>,
+}
+
+impl<const COLS: usize> Table<COLS> {
+ /// number of headers provided.
+ pub fn new() -> Self {
+ Table {
+ striped: false,
+ width: None,
+ headers: None,
+ rows: TableContents::Vec(Vec::new()),
+ interaction_state: None,
+ selected_item_index: None,
+ column_widths: None,
+ on_click_row: None,
+ }
+ }
+
+ /// Enables uniform list rendering.
+ /// The provided function will be passed directly to the `uniform_list` element.
+ /// Therefore, if this method is called, any calls to [`Table::row`] before or after
+ /// this method is called will be ignored.
+ pub fn uniform_list(
+ mut self,
+ id: impl Into<ElementId>,
+ row_count: usize,
+ render_item_fn: impl Fn(Range<usize>, &mut Window, &mut App) -> Vec<[AnyElement; COLS]>
+ + 'static,
+ ) -> Self {
+ self.rows = TableContents::UniformList(UniformListData {
+ element_id: id.into(),
+ row_count: row_count,
+ render_item_fn: Box::new(render_item_fn),
+ });
+ self
+ }
+
+ /// Enables row striping.
+ pub fn striped(mut self) -> Self {
+ self.striped = true;
+ self
+ }
+
+ /// Sets the width of the table.
+ /// Will enable horizontal scrolling if [`Self::interactable`] is also called.
+ pub fn width(mut self, width: impl Into<Length>) -> Self {
+ self.width = Some(width.into());
+ self
+ }
+
+ /// Enables interaction (primarily scrolling) with the table.
+ ///
+ /// Vertical scrolling will be enabled by default if the table is taller than its container.
+ ///
+ /// Horizontal scrolling will only be enabled if [`Self::width`] is also called, otherwise
+ /// the list will always shrink the table columns to fit their contents I.e. If [`Self::uniform_list`]
+ /// is used without a width and with [`Self::interactable`], the [`ListHorizontalSizingBehavior`] will
+ /// be set to [`ListHorizontalSizingBehavior::FitList`].
+ pub fn interactable(mut self, interaction_state: &Entity<TableInteractionState>) -> Self {
+ self.interaction_state = Some(interaction_state.downgrade());
+ self
+ }
+
+ pub fn selected_item_index(mut self, selected_item_index: Option<usize>) -> Self {
+ self.selected_item_index = selected_item_index;
+ self
+ }
+
+ pub fn header(mut self, headers: [impl IntoElement; COLS]) -> Self {
+ self.headers = Some(headers.map(IntoElement::into_any_element));
+ self
+ }
+
+ pub fn row(mut self, items: [impl IntoElement; COLS]) -> Self {
+ if let Some(rows) = self.rows.rows_mut() {
+ rows.push(items.map(IntoElement::into_any_element));
+ }
+ self
+ }
+
+ pub fn column_widths(mut self, widths: [impl Into<Length>; COLS]) -> Self {
+ self.column_widths = Some(widths.map(Into::into));
+ self
+ }
+
+ pub fn on_click_row(
+ mut self,
+ callback: impl Fn(usize, &mut Window, &mut App) + 'static,
+ ) -> Self {
+ self.on_click_row = Some(Rc::new(callback));
+ self
+ }
+}
+
+fn base_cell_style(width: Option<Length>, cx: &App) -> Div {
+ div()
+ .px_1p5()
+ .when_some(width, |this, width| this.w(width))
+ .when(width.is_none(), |this| this.flex_1())
+ .justify_start()
+ .text_ui(cx)
+ .whitespace_nowrap()
+ .text_ellipsis()
+ .overflow_hidden()
+}
+
+pub fn render_row<const COLS: usize>(
+ row_index: usize,
+ items: [impl IntoElement; COLS],
+ table_context: TableRenderContext<COLS>,
+ cx: &App,
+) -> AnyElement {
+ let is_striped = table_context.striped;
+ let is_last = row_index == table_context.total_row_count - 1;
+ let bg = if row_index % 2 == 1 && is_striped {
+ Some(cx.theme().colors().text.opacity(0.05))
+ } else {
+ None
+ };
+ let column_widths = table_context
+ .column_widths
+ .map_or([None; COLS], |widths| widths.map(Some));
+ let is_selected = table_context.selected_item_index == Some(row_index);
+
+ let row = div()
+ .w_full()
+ .border_2()
+ .border_color(transparent_black())
+ .when(is_selected, |row| {
+ row.border_color(cx.theme().colors().panel_focused_border)
+ })
+ .child(
+ div()
+ .w_full()
+ .flex()
+ .flex_row()
+ .items_center()
+ .justify_between()
+ .px_1p5()
+ .py_1()
+ .when_some(bg, |row, bg| row.bg(bg))
+ .when(!is_striped, |row| {
+ row.border_b_1()
+ .border_color(transparent_black())
+ .when(!is_last, |row| row.border_color(cx.theme().colors().border))
+ })
+ .children(
+ items
+ .map(IntoElement::into_any_element)
+ .into_iter()
+ .zip(column_widths)
+ .map(|(cell, width)| base_cell_style(width, cx).child(cell)),
+ ),
+ );
+
+ if let Some(on_click) = table_context.on_click_row {
+ row.id(("table-row", row_index))
+ .on_click(move |_, window, cx| on_click(row_index, window, cx))
+ .into_any_element()
+ } else {
+ row.into_any_element()
+ }
+}
+
+pub fn render_header<const COLS: usize>(
+ headers: [impl IntoElement; COLS],
+ table_context: TableRenderContext<COLS>,
+ cx: &mut App,
+) -> impl IntoElement {
+ let column_widths = table_context
+ .column_widths
+ .map_or([None; COLS], |widths| widths.map(Some));
+ div()
+ .flex()
+ .flex_row()
+ .items_center()
+ .justify_between()
+ .w_full()
+ .p_2()
+ .border_b_1()
+ .border_color(cx.theme().colors().border)
+ .children(headers.into_iter().zip(column_widths).map(|(h, width)| {
+ base_cell_style(width, cx)
+ .font_weight(FontWeight::SEMIBOLD)
+ .child(h)
+ }))
+}
+
+#[derive(Clone)]
+pub struct TableRenderContext<const COLS: usize> {
+ pub striped: bool,
+ pub total_row_count: usize,
+ pub selected_item_index: Option<usize>,
+ pub column_widths: Option<[Length; COLS]>,
+ pub on_click_row: Option<Rc<dyn Fn(usize, &mut Window, &mut App)>>,
+}
+
+impl<const COLS: usize> TableRenderContext<COLS> {
+ fn new(table: &Table<COLS>) -> Self {
+ Self {
+ striped: table.striped,
+ total_row_count: table.rows.len(),
+ column_widths: table.column_widths,
+ selected_item_index: table.selected_item_index,
+ on_click_row: table.on_click_row.clone(),
+ }
+ }
+}
+
+impl<const COLS: usize> RenderOnce for Table<COLS> {
+ fn render(mut self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+ let table_context = TableRenderContext::new(&self);
+ let interaction_state = self.interaction_state.and_then(|state| state.upgrade());
+
+ let scroll_track_size = px(16.);
+ let h_scroll_offset = if interaction_state
+ .as_ref()
+ .is_some_and(|state| state.read(cx).vertical_scrollbar.show_scrollbar)
+ {
+ // magic number
+ px(3.)
+ } else {
+ px(0.)
+ };
+
+ let width = self.width;
+
+ let table = div()
+ .when_some(width, |this, width| this.w(width))
+ .h_full()
+ .v_flex()
+ .when_some(self.headers.take(), |this, headers| {
+ this.child(render_header(headers, table_context.clone(), cx))
+ })
+ .child(
+ div()
+ .flex_grow()
+ .w_full()
+ .relative()
+ .overflow_hidden()
+ .map(|parent| match self.rows {
+ TableContents::Vec(items) => {
+ parent.children(items.into_iter().enumerate().map(|(index, row)| {
+ render_row(index, row, table_context.clone(), cx)
+ }))
+ }
+ TableContents::UniformList(uniform_list_data) => parent.child(
+ uniform_list(
+ uniform_list_data.element_id,
+ uniform_list_data.row_count,
+ {
+ let render_item_fn = uniform_list_data.render_item_fn;
+ move |range: Range<usize>, window, cx| {
+ let elements = render_item_fn(range.clone(), window, cx);
+ elements
+ .into_iter()
+ .zip(range)
+ .map(|(row, row_index)| {
+ render_row(
+ row_index,
+ row,
+ table_context.clone(),
+ cx,
+ )
+ })
+ .collect()
+ }
+ },
+ )
+ .size_full()
+ .flex_grow()
+ .with_sizing_behavior(ListSizingBehavior::Auto)
+ .with_horizontal_sizing_behavior(if width.is_some() {
+ ListHorizontalSizingBehavior::Unconstrained
+ } else {
+ ListHorizontalSizingBehavior::FitList
+ })
+ .when_some(
+ interaction_state.as_ref(),
+ |this, state| {
+ this.track_scroll(
+ state.read_with(cx, |s, _| s.scroll_handle.clone()),
+ )
+ },
+ ),
+ ),
+ })
+ .when_some(interaction_state.as_ref(), |this, interaction_state| {
+ this.map(|this| {
+ TableInteractionState::render_vertical_scrollbar_track(
+ interaction_state,
+ this,
+ scroll_track_size,
+ cx,
+ )
+ })
+ .map(|this| {
+ TableInteractionState::render_vertical_scrollbar(
+ interaction_state,
+ this,
+ cx,
+ )
+ })
+ }),
+ )
+ .when_some(
+ width.and(interaction_state.as_ref()),
+ |this, interaction_state| {
+ this.map(|this| {
+ TableInteractionState::render_horizontal_scrollbar_track(
+ interaction_state,
+ this,
+ scroll_track_size,
+ cx,
+ )
+ })
+ .map(|this| {
+ TableInteractionState::render_horizontal_scrollbar(
+ interaction_state,
+ this,
+ h_scroll_offset,
+ cx,
+ )
+ })
+ },
+ );
+
+ if let Some(interaction_state) = interaction_state.as_ref() {
+ table
+ .track_focus(&interaction_state.read(cx).focus_handle)
+ .id(("table", interaction_state.entity_id()))
+ .on_hover({
+ let interaction_state = interaction_state.downgrade();
+ move |hovered, window, cx| {
+ interaction_state
+ .update(cx, |interaction_state, cx| {
+ if *hovered {
+ interaction_state.horizontal_scrollbar.show(cx);
+ interaction_state.vertical_scrollbar.show(cx);
+ cx.notify();
+ } else if !interaction_state
+ .focus_handle
+ .contains_focused(window, cx)
+ {
+ interaction_state.hide_scrollbars(window, cx);
+ }
+ })
+ .ok();
+ }
+ })
+ .into_any_element()
+ } else {
+ table.into_any_element()
+ }
+ }
+}
+
+// computed state related to how to render scrollbars
+// one per axis
+// on render we just read this off the keymap editor
+// we update it when
+// - settings change
+// - on focus in, on focus out, on hover, etc.
+#[derive(Debug)]
+pub struct ScrollbarProperties {
+ axis: Axis,
+ show_scrollbar: bool,
+ show_track: bool,
+ auto_hide: bool,
+ hide_task: Option<Task<()>>,
+ state: ScrollbarState,
+}
+
+impl ScrollbarProperties {
+ // Shows the scrollbar and cancels any pending hide task
+ fn show(&mut self, cx: &mut Context<TableInteractionState>) {
+ if !self.auto_hide {
+ return;
+ }
+ self.show_scrollbar = true;
+ self.hide_task.take();
+ cx.notify();
+ }
+
+ fn hide(&mut self, window: &mut Window, cx: &mut Context<TableInteractionState>) {
+ const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
+
+ if !self.auto_hide {
+ return;
+ }
+
+ let axis = self.axis;
+ self.hide_task = Some(cx.spawn_in(window, async move |keymap_editor, cx| {
+ cx.background_executor()
+ .timer(SCROLLBAR_SHOW_INTERVAL)
+ .await;
+
+ if let Some(keymap_editor) = keymap_editor.upgrade() {
+ keymap_editor
+ .update(cx, |keymap_editor, cx| {
+ match axis {
+ Axis::Vertical => {
+ keymap_editor.vertical_scrollbar.show_scrollbar = false
+ }
+ Axis::Horizontal => {
+ keymap_editor.horizontal_scrollbar.show_scrollbar = false
+ }
+ }
+ cx.notify();
+ })
+ .ok();
+ }
+ }));
+ }
+}
+
+impl Component for Table<3> {
+ fn scope() -> ComponentScope {
+ ComponentScope::Layout
+ }
+
+ fn description() -> Option<&'static str> {
+ Some("A table component for displaying data in rows and columns with optional styling.")
+ }
+
+ fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
+ Some(
+ v_flex()
+ .gap_6()
+ .children(vec![
+ example_group_with_title(
+ "Basic Tables",
+ vec![
+ single_example(
+ "Simple Table",
+ Table::new()
+ .width(px(400.))
+ .header(["Name", "Age", "City"])
+ .row(["Alice", "28", "New York"])
+ .row(["Bob", "32", "San Francisco"])
+ .row(["Charlie", "25", "London"])
+ .into_any_element(),
+ ),
+ single_example(
+ "Two Column Table",
+ Table::new()
+ .header(["Category", "Value"])
+ .width(px(300.))
+ .row(["Revenue", "$100,000"])
+ .row(["Expenses", "$75,000"])
+ .row(["Profit", "$25,000"])
+ .into_any_element(),
+ ),
+ ],
+ ),
+ example_group_with_title(
+ "Styled Tables",
+ vec![
+ single_example(
+ "Default",
+ Table::new()
+ .width(px(400.))
+ .header(["Product", "Price", "Stock"])
+ .row(["Laptop", "$999", "In Stock"])
+ .row(["Phone", "$599", "Low Stock"])
+ .row(["Tablet", "$399", "Out of Stock"])
+ .into_any_element(),
+ ),
+ single_example(
+ "Striped",
+ Table::new()
+ .width(px(400.))
+ .striped()
+ .header(["Product", "Price", "Stock"])
+ .row(["Laptop", "$999", "In Stock"])
+ .row(["Phone", "$599", "Low Stock"])
+ .row(["Tablet", "$399", "Out of Stock"])
+ .row(["Headphones", "$199", "In Stock"])
+ .into_any_element(),
+ ),
+ ],
+ ),
+ example_group_with_title(
+ "Mixed Content Table",
+ vec![single_example(
+ "Table with Elements",
+ Table::new()
+ .width(px(840.))
+ .header(["Status", "Name", "Priority", "Deadline", "Action"])
+ .row([
+ Indicator::dot().color(Color::Success).into_any_element(),
+ "Project A".into_any_element(),
+ "High".into_any_element(),
+ "2023-12-31".into_any_element(),
+ Button::new("view_a", "View")
+ .style(ButtonStyle::Filled)
+ .full_width()
+ .into_any_element(),
+ ])
+ .row([
+ Indicator::dot().color(Color::Warning).into_any_element(),
+ "Project B".into_any_element(),
+ "Medium".into_any_element(),
+ "2024-03-15".into_any_element(),
+ Button::new("view_b", "View")
+ .style(ButtonStyle::Filled)
+ .full_width()
+ .into_any_element(),
+ ])
+ .row([
+ Indicator::dot().color(Color::Error).into_any_element(),
+ "Project C".into_any_element(),
+ "Low".into_any_element(),
+ "2024-06-30".into_any_element(),
+ Button::new("view_c", "View")
+ .style(ButtonStyle::Filled)
+ .full_width()
+ .into_any_element(),
+ ])
+ .into_any_element(),
+ )],
+ ),
+ ])
+ .into_any_element(),
+ )
+ }
+}
@@ -1,11 +1,8 @@
use collections::HashMap;
-use schemars::{
- JsonSchema,
- r#gen::SchemaSettings,
- schema::{ObjectValidation, Schema, SchemaObject},
-};
+use schemars::{JsonSchema, json_schema};
use serde::Deserialize;
use serde_json_lenient::Value;
+use std::borrow::Cow;
#[derive(Deserialize)]
pub struct VsSnippetsFile {
@@ -15,29 +12,25 @@ pub struct VsSnippetsFile {
impl VsSnippetsFile {
pub fn generate_json_schema() -> Value {
- let schema = SchemaSettings::draft07()
- .with(|settings| settings.option_add_null_type = false)
+ let schema = schemars::generate::SchemaSettings::draft2019_09()
.into_generator()
- .into_root_schema_for::<Self>();
+ .root_schema_for::<Self>();
serde_json_lenient::to_value(schema).unwrap()
}
}
impl JsonSchema for VsSnippetsFile {
- fn schema_name() -> String {
+ fn schema_name() -> Cow<'static, str> {
"VsSnippetsFile".into()
}
- fn json_schema(r#gen: &mut schemars::r#gen::SchemaGenerator) -> Schema {
- SchemaObject {
- object: Some(Box::new(ObjectValidation {
- additional_properties: Some(Box::new(r#gen.subschema_for::<VsCodeSnippet>())),
- ..Default::default()
- })),
- ..Default::default()
- }
- .into()
+ fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
+ let snippet_schema = generator.subschema_for::<VsCodeSnippet>();
+ json_schema!({
+ "type": "object",
+ "additionalProperties": snippet_schema
+ })
}
}
@@ -1,6 +1,7 @@
mod auto_height_editor;
mod cursor;
mod focus;
+mod indent_guides;
mod kitchen_sink;
mod overflow_scroll;
mod picker;
@@ -12,6 +13,7 @@ mod with_rem_size;
pub use auto_height_editor::*;
pub use cursor::*;
pub use focus::*;
+pub use indent_guides::*;
pub use kitchen_sink::*;
pub use overflow_scroll::*;
pub use picker::*;
@@ -1,13 +1,10 @@
-use std::fmt::format;
+use std::ops::Range;
+
+use gpui::{Entity, Render, div, uniform_list};
+use gpui::{prelude::*, *};
+use ui::{AbsoluteLength, Color, DefiniteLength, Label, LabelCommon, px, v_flex};
-use gpui::{
- DefaultColor, DefaultThemeAppearance, Hsla, Render, colors, div, prelude::*, uniform_list,
-};
use story::Story;
-use strum::IntoEnumIterator;
-use ui::{
- AbsoluteLength, ActiveTheme, Color, DefiniteLength, Label, LabelCommon, h_flex, px, v_flex,
-};
const LENGTH: usize = 100;
@@ -16,7 +13,7 @@ pub struct IndentGuidesStory {
}
impl IndentGuidesStory {
- pub fn model(window: &mut Window, cx: &mut AppContext) -> Model<Self> {
+ pub fn model(_window: &mut Window, cx: &mut App) -> Entity<Self> {
let mut depths = Vec::new();
depths.push(0);
depths.push(1);
@@ -33,16 +30,15 @@ impl IndentGuidesStory {
}
impl Render for IndentGuidesStory {
- fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
Story::container(cx)
- .child(Story::title("Indent guides"))
+ .child(Story::title("Indent guides", cx))
.child(
v_flex().size_full().child(
uniform_list(
- cx.entity().clone(),
"some-list",
self.depths.len(),
- |this, range, cx| {
+ cx.processor(move |this, range: Range<usize>, _window, _cx| {
this.depths
.iter()
.enumerate()
@@ -56,7 +52,7 @@ impl Render for IndentGuidesStory {
.child(Label::new(format!("Item {}", i)).color(Color::Info))
})
.collect()
- },
+ }),
)
.with_sizing_behavior(gpui::ListSizingBehavior::Infer)
.with_decoration(ui::indent_guides(
@@ -64,10 +60,10 @@ impl Render for IndentGuidesStory {
px(16.),
ui::IndentGuideColors {
default: Color::Info.color(cx),
- hovered: Color::Accent.color(cx),
+ hover: Color::Accent.color(cx),
active: Color::Accent.color(cx),
},
- |this, range, cx| {
+ |this, range, _cx, _context| {
this.depths
.iter()
.skip(range.start)
@@ -31,6 +31,7 @@ pub enum ComponentStory {
ToggleButton,
ViewportUnits,
WithRemSize,
+ IndentGuides,
}
impl ComponentStory {
@@ -60,6 +61,7 @@ impl ComponentStory {
Self::ToggleButton => cx.new(|_| ui::ToggleButtonStory).into(),
Self::ViewportUnits => cx.new(|_| crate::stories::ViewportUnitsStory).into(),
Self::WithRemSize => cx.new(|_| crate::stories::WithRemSizeStory).into(),
+ Self::IndentGuides => crate::stories::IndentGuidesStory::model(window, cx).into(),
}
}
}
@@ -9,7 +9,9 @@ use std::sync::Arc;
use clap::Parser;
use dialoguer::FuzzySelect;
use gpui::{
- AnyView, App, Bounds, Context, Render, Window, WindowBounds, WindowOptions, div, px, size,
+ AnyView, App, Bounds, Context, Render, Window, WindowBounds, WindowOptions,
+ colors::{Colors, GlobalColors},
+ div, px, size,
};
use log::LevelFilter;
use project::Project;
@@ -68,6 +70,8 @@ fn main() {
gpui::Application::new().with_assets(Assets).run(move |cx| {
load_embedded_fonts(cx).unwrap();
+ cx.set_global(GlobalColors(Arc::new(Colors::default())));
+
let http_client = ReqwestClient::user_agent("zed_storybook").unwrap();
cx.set_http_client(Arc::new(http_client));
@@ -0,0 +1,20 @@
+[package]
+name = "svg_preview"
+version = "0.1.0"
+edition.workspace = true
+publish.workspace = true
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/svg_preview.rs"
+
+[dependencies]
+editor.workspace = true
+file_icons.workspace = true
+gpui.workspace = true
+ui.workspace = true
+workspace.workspace = true
+workspace-hack.workspace = true
@@ -0,0 +1 @@
+../../LICENSE-GPL
@@ -0,0 +1,19 @@
+use gpui::{App, actions};
+use workspace::Workspace;
+
+pub mod svg_preview_view;
+
+actions!(
+ svg,
+ [OpenPreview, OpenPreviewToTheSide, OpenFollowingPreview]
+);
+
+pub fn init(cx: &mut App) {
+ cx.observe_new(|workspace: &mut Workspace, window, cx| {
+ let Some(window) = window else {
+ return;
+ };
+ crate::svg_preview_view::SvgPreviewView::register(workspace, window, cx);
+ })
+ .detach();
+}
@@ -0,0 +1,323 @@
+use std::path::PathBuf;
+
+use editor::{Editor, EditorEvent};
+use file_icons::FileIcons;
+use gpui::{
+ App, Context, Entity, EventEmitter, FocusHandle, Focusable, ImageSource, IntoElement,
+ ParentElement, Render, Resource, RetainAllImageCache, Styled, Subscription, WeakEntity, Window,
+ div, img,
+};
+use ui::prelude::*;
+use workspace::item::Item;
+use workspace::{Pane, Workspace};
+
+use crate::{OpenFollowingPreview, OpenPreview, OpenPreviewToTheSide};
+
+pub struct SvgPreviewView {
+ focus_handle: FocusHandle,
+ svg_path: Option<PathBuf>,
+ image_cache: Entity<RetainAllImageCache>,
+ _editor_subscription: Subscription,
+ _workspace_subscription: Option<Subscription>,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum SvgPreviewMode {
+ /// The preview will always show the contents of the provided editor.
+ Default,
+ /// The preview will "follow" the last active editor of an SVG file.
+ Follow,
+}
+
+impl SvgPreviewView {
+ pub fn register(workspace: &mut Workspace, _window: &mut Window, _cx: &mut Context<Workspace>) {
+ workspace.register_action(move |workspace, _: &OpenPreview, window, cx| {
+ if let Some(editor) = Self::resolve_active_item_as_svg_editor(workspace, cx) {
+ if Self::is_svg_file(&editor, cx) {
+ let view = Self::create_svg_view(
+ SvgPreviewMode::Default,
+ workspace,
+ editor.clone(),
+ window,
+ cx,
+ );
+ workspace.active_pane().update(cx, |pane, cx| {
+ if let Some(existing_view_idx) =
+ Self::find_existing_preview_item_idx(pane, &editor, cx)
+ {
+ pane.activate_item(existing_view_idx, true, true, window, cx);
+ } else {
+ pane.add_item(Box::new(view), true, true, None, window, cx)
+ }
+ });
+ cx.notify();
+ }
+ }
+ });
+
+ workspace.register_action(move |workspace, _: &OpenPreviewToTheSide, window, cx| {
+ if let Some(editor) = Self::resolve_active_item_as_svg_editor(workspace, cx) {
+ if Self::is_svg_file(&editor, cx) {
+ let editor_clone = editor.clone();
+ let view = Self::create_svg_view(
+ SvgPreviewMode::Default,
+ workspace,
+ editor_clone,
+ window,
+ cx,
+ );
+ let pane = workspace
+ .find_pane_in_direction(workspace::SplitDirection::Right, cx)
+ .unwrap_or_else(|| {
+ workspace.split_pane(
+ workspace.active_pane().clone(),
+ workspace::SplitDirection::Right,
+ window,
+ cx,
+ )
+ });
+ pane.update(cx, |pane, cx| {
+ if let Some(existing_view_idx) =
+ Self::find_existing_preview_item_idx(pane, &editor, cx)
+ {
+ pane.activate_item(existing_view_idx, true, true, window, cx);
+ } else {
+ pane.add_item(Box::new(view), false, false, None, window, cx)
+ }
+ });
+ cx.notify();
+ }
+ }
+ });
+
+ workspace.register_action(move |workspace, _: &OpenFollowingPreview, window, cx| {
+ if let Some(editor) = Self::resolve_active_item_as_svg_editor(workspace, cx) {
+ if Self::is_svg_file(&editor, cx) {
+ let view = Self::create_svg_view(
+ SvgPreviewMode::Follow,
+ workspace,
+ editor,
+ window,
+ cx,
+ );
+ workspace.active_pane().update(cx, |pane, cx| {
+ pane.add_item(Box::new(view), true, true, None, window, cx)
+ });
+ cx.notify();
+ }
+ }
+ });
+ }
+
+ fn find_existing_preview_item_idx(
+ pane: &Pane,
+ editor: &Entity<Editor>,
+ cx: &App,
+ ) -> Option<usize> {
+ let editor_path = Self::get_svg_path(editor, cx);
+ pane.items_of_type::<SvgPreviewView>()
+ .find(|view| {
+ let view_read = view.read(cx);
+ view_read.svg_path.is_some() && view_read.svg_path == editor_path
+ })
+ .and_then(|view| pane.index_for_item(&view))
+ }
+
+ pub fn resolve_active_item_as_svg_editor(
+ workspace: &Workspace,
+ cx: &mut Context<Workspace>,
+ ) -> Option<Entity<Editor>> {
+ let editor = workspace.active_item(cx)?.act_as::<Editor>(cx)?;
+
+ if Self::is_svg_file(&editor, cx) {
+ Some(editor)
+ } else {
+ None
+ }
+ }
+
+ fn create_svg_view(
+ mode: SvgPreviewMode,
+ workspace: &mut Workspace,
+ editor: Entity<Editor>,
+ window: &mut Window,
+ cx: &mut Context<Workspace>,
+ ) -> Entity<SvgPreviewView> {
+ let workspace_handle = workspace.weak_handle();
+ SvgPreviewView::new(mode, editor, workspace_handle, window, cx)
+ }
+
+ pub fn new(
+ mode: SvgPreviewMode,
+ active_editor: Entity<Editor>,
+ workspace_handle: WeakEntity<Workspace>,
+ window: &mut Window,
+ cx: &mut Context<Workspace>,
+ ) -> Entity<Self> {
+ cx.new(|cx| {
+ let svg_path = Self::get_svg_path(&active_editor, cx);
+ let image_cache = RetainAllImageCache::new(cx);
+
+ let subscription = cx.subscribe_in(
+ &active_editor,
+ window,
+ |this: &mut SvgPreviewView, _editor, event: &EditorEvent, window, cx| {
+ match event {
+ EditorEvent::Saved => {
+ // Remove cached image to force reload
+ if let Some(svg_path) = &this.svg_path {
+ let resource = Resource::Path(svg_path.clone().into());
+ this.image_cache.update(cx, |cache, cx| {
+ cache.remove(&resource, window, cx);
+ });
+ }
+ cx.notify();
+ }
+ _ => {}
+ }
+ },
+ );
+
+ // Subscribe to workspace active item changes to follow SVG files
+ let workspace_subscription = if mode == SvgPreviewMode::Follow {
+ workspace_handle.upgrade().map(|workspace_handle| {
+ cx.subscribe_in(
+ &workspace_handle,
+ window,
+ |this: &mut SvgPreviewView,
+ workspace,
+ event: &workspace::Event,
+ _window,
+ cx| {
+ match event {
+ workspace::Event::ActiveItemChanged => {
+ let workspace_read = workspace.read(cx);
+ if let Some(active_item) = workspace_read.active_item(cx) {
+ if let Some(editor_entity) =
+ active_item.downcast::<Editor>()
+ {
+ if Self::is_svg_file(&editor_entity, cx) {
+ let new_path =
+ Self::get_svg_path(&editor_entity, cx);
+ if this.svg_path != new_path {
+ this.svg_path = new_path;
+ cx.notify();
+ }
+ }
+ }
+ }
+ }
+ _ => {}
+ }
+ },
+ )
+ })
+ } else {
+ None
+ };
+
+ Self {
+ focus_handle: cx.focus_handle(),
+ svg_path,
+ image_cache,
+ _editor_subscription: subscription,
+ _workspace_subscription: workspace_subscription,
+ }
+ })
+ }
+
+ pub fn is_svg_file<C>(editor: &Entity<Editor>, cx: &C) -> bool
+ where
+ C: std::borrow::Borrow<App>,
+ {
+ let app = cx.borrow();
+ let buffer = editor.read(app).buffer().read(app);
+ if let Some(buffer) = buffer.as_singleton() {
+ if let Some(file) = buffer.read(app).file() {
+ return file
+ .path()
+ .extension()
+ .and_then(|ext| ext.to_str())
+ .map(|ext| ext.eq_ignore_ascii_case("svg"))
+ .unwrap_or(false);
+ }
+ }
+ false
+ }
+
+ fn get_svg_path<C>(editor: &Entity<Editor>, cx: &C) -> Option<PathBuf>
+ where
+ C: std::borrow::Borrow<App>,
+ {
+ let app = cx.borrow();
+ let buffer = editor.read(app).buffer().read(app).as_singleton()?;
+ let file = buffer.read(app).file()?;
+ let local_file = file.as_local()?;
+ Some(local_file.abs_path(app))
+ }
+}
+
+impl Render for SvgPreviewView {
+ fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ v_flex()
+ .id("SvgPreview")
+ .key_context("SvgPreview")
+ .track_focus(&self.focus_handle(cx))
+ .size_full()
+ .bg(cx.theme().colors().editor_background)
+ .flex()
+ .justify_center()
+ .items_center()
+ .child(if let Some(svg_path) = &self.svg_path {
+ img(ImageSource::from(svg_path.clone()))
+ .image_cache(&self.image_cache)
+ .max_w_full()
+ .max_h_full()
+ .with_fallback(|| {
+ div()
+ .p_4()
+ .child("Failed to load SVG file")
+ .into_any_element()
+ })
+ .into_any_element()
+ } else {
+ div().p_4().child("No SVG file selected").into_any_element()
+ })
+ }
+}
+
+impl Focusable for SvgPreviewView {
+ fn focus_handle(&self, _cx: &App) -> FocusHandle {
+ self.focus_handle.clone()
+ }
+}
+
+impl EventEmitter<()> for SvgPreviewView {}
+
+impl Item for SvgPreviewView {
+ type Event = ();
+
+ fn tab_icon(&self, _window: &Window, cx: &App) -> Option<Icon> {
+ // Use the same icon as SVG files in the file tree
+ self.svg_path
+ .as_ref()
+ .and_then(|svg_path| FileIcons::get_icon(svg_path, cx))
+ .map(Icon::from_path)
+ .or_else(|| Some(Icon::new(IconName::Image)))
+ }
+
+ fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
+ self.svg_path
+ .as_ref()
+ .and_then(|svg_path| svg_path.file_name())
+ .map(|name| name.to_string_lossy())
+ .map(|name| format!("Preview {}", name).into())
+ .unwrap_or_else(|| "SVG Preview".into())
+ }
+
+ fn telemetry_event_text(&self) -> Option<&'static str> {
+ Some("svg preview: open")
+ }
+
+ fn to_item_events(_event: &Self::Event, _f: impl FnMut(workspace::item::ItemEvent)) {}
+}
@@ -287,7 +287,8 @@ pub struct DebugTaskFile(pub Vec<DebugScenario>);
impl DebugTaskFile {
pub fn generate_json_schema(schemas: &AdapterSchemas) -> serde_json_lenient::Value {
- let build_task_schema = schemars::schema_for!(BuildTaskDefinition);
+ let mut generator = schemars::generate::SchemaSettings::draft2019_09().into_generator();
+ let build_task_schema = generator.root_schema_for::<BuildTaskDefinition>();
let mut build_task_value =
serde_json_lenient::to_value(&build_task_schema).unwrap_or_default();
@@ -1,33 +1,6 @@
-use schemars::{
- SchemaGenerator,
- schema::{ArrayValidation, InstanceType, Schema, SchemaObject, SingleOrVec, StringValidation},
-};
use serde::de::{self, Deserializer, Visitor};
use std::fmt;
-/// Generates a JSON schema for a non-empty string array.
-pub fn non_empty_string_vec_json_schema(_: &mut SchemaGenerator) -> Schema {
- Schema::Object(SchemaObject {
- instance_type: Some(InstanceType::Array.into()),
- array: Some(Box::new(ArrayValidation {
- unique_items: Some(true),
- items: Some(SingleOrVec::Single(Box::new(Schema::Object(
- SchemaObject {
- instance_type: Some(InstanceType::String.into()),
- string: Some(Box::new(StringValidation {
- min_length: Some(1), // Ensures string in the array is non-empty
- ..Default::default()
- })),
- ..Default::default()
- },
- )))),
- ..Default::default()
- })),
- format: Some("vec-of-non-empty-strings".to_string()), // Use a custom format keyword
- ..Default::default()
- })
-}
-
/// Deserializes a non-empty string array.
pub fn non_empty_string_vec<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where
@@ -1,6 +1,6 @@
use anyhow::{Context as _, bail};
use collections::{HashMap, HashSet};
-use schemars::{JsonSchema, r#gen::SchemaSettings};
+use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::path::PathBuf;
@@ -9,8 +9,7 @@ use util::{ResultExt, truncate_and_remove_front};
use crate::{
AttachRequest, ResolvedTask, RevealTarget, Shell, SpawnInTerminal, TaskContext, TaskId,
- VariableName, ZED_VARIABLE_NAME_PREFIX,
- serde_helpers::{non_empty_string_vec, non_empty_string_vec_json_schema},
+ VariableName, ZED_VARIABLE_NAME_PREFIX, serde_helpers::non_empty_string_vec,
};
/// A template definition of a Zed task to run.
@@ -61,7 +60,7 @@ pub struct TaskTemplate {
/// Represents the tags which this template attaches to.
/// Adding this removes this task from other UI and gives you ability to run it by tag.
#[serde(default, deserialize_with = "non_empty_string_vec")]
- #[schemars(schema_with = "non_empty_string_vec_json_schema")]
+ #[schemars(length(min = 1))]
pub tags: Vec<String>,
/// Which shell to use when spawning the task.
#[serde(default)]
@@ -116,10 +115,9 @@ pub struct TaskTemplates(pub Vec<TaskTemplate>);
impl TaskTemplates {
/// Generates JSON schema of Tasks JSON template format.
pub fn generate_json_schema() -> serde_json_lenient::Value {
- let schema = SchemaSettings::draft07()
- .with(|settings| settings.option_add_null_type = false)
+ let schema = schemars::generate::SchemaSettings::draft2019_09()
.into_generator()
- .into_root_schema_for::<Self>();
+ .root_schema_for::<Self>();
serde_json_lenient::to_value(schema).unwrap()
}
@@ -93,7 +93,7 @@ fn task_type_to_adapter_name(task_type: &str) -> String {
"php" => "PHP",
"cppdbg" | "lldb" => "CodeLLDB",
"debugpy" => "Debugpy",
- "rdbg" => "Ruby",
+ "rdbg" => "rdbg",
_ => task_type,
}
.to_owned()
@@ -751,7 +751,7 @@ fn string_match_candidates<'a>(
mod tests {
use std::{path::PathBuf, sync::Arc};
- use editor::Editor;
+ use editor::{Editor, SelectionEffects};
use gpui::{TestAppContext, VisualTestContext};
use language::{Language, LanguageConfig, LanguageMatcher, Point};
use project::{ContextProviderWithTasks, FakeFs, Project};
@@ -1028,7 +1028,7 @@ mod tests {
.update(|_window, cx| second_item.act_as::<Editor>(cx))
.unwrap();
editor.update_in(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges(Some(Point::new(1, 2)..Point::new(1, 5)))
})
});
@@ -393,7 +393,7 @@ fn worktree_context(worktree_abs_path: &Path) -> TaskContext {
mod tests {
use std::{collections::HashMap, sync::Arc};
- use editor::Editor;
+ use editor::{Editor, SelectionEffects};
use gpui::TestAppContext;
use language::{Language, LanguageConfig};
use project::{BasicContextProvider, FakeFs, Project, task_store::TaskStore};
@@ -538,7 +538,7 @@ mod tests {
// And now, let's select an identifier.
editor2.update_in(cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |selections| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
selections.select_ranges([14..18])
})
});
@@ -52,7 +52,7 @@ pub(super) fn find_from_grid_point<T: EventListener>(
) -> Option<(String, bool, Match)> {
let grid = term.grid();
let link = grid.index(point).hyperlink();
- let found_word = if link.is_some() {
+ let found_word = if let Some(ref url) = link {
let mut min_index = point;
loop {
let new_min_index = min_index.sub(term, Boundary::Cursor, 1);
@@ -73,7 +73,7 @@ pub(super) fn find_from_grid_point<T: EventListener>(
}
}
- let url = link.unwrap().uri().to_owned();
+ let url = url.uri().to_owned();
let url_match = min_index..=max_index;
Some((url, true, url_match))
@@ -2,14 +2,14 @@ use alacritty_terminal::vte::ansi::{
CursorShape as AlacCursorShape, CursorStyle as AlacCursorStyle,
};
use collections::HashMap;
-use gpui::{
- AbsoluteLength, App, FontFallbacks, FontFeatures, FontWeight, Pixels, SharedString, px,
-};
-use schemars::{JsonSchema, r#gen::SchemaGenerator, schema::RootSchema};
+use gpui::{AbsoluteLength, App, FontFallbacks, FontFeatures, FontWeight, Pixels, px};
+use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
-use settings::{SettingsJsonSchemaParams, SettingsSources, add_references_to_properties};
+
+use settings::SettingsSources;
use std::path::PathBuf;
use task::Shell;
+use theme::FontFamilyName;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
@@ -29,7 +29,7 @@ pub struct TerminalSettings {
pub shell: Shell,
pub working_directory: WorkingDirectory,
pub font_size: Option<Pixels>,
- pub font_family: Option<SharedString>,
+ pub font_family: Option<FontFamilyName>,
pub font_fallbacks: Option<FontFallbacks>,
pub font_features: Option<FontFeatures>,
pub font_weight: Option<FontWeight>,
@@ -147,13 +147,14 @@ pub struct TerminalSettingsContent {
///
/// If this option is not included,
/// the terminal will default to matching the buffer's font family.
- pub font_family: Option<String>,
+ pub font_family: Option<FontFamilyName>,
/// Sets the terminal's font fallbacks.
///
/// If this option is not included,
/// the terminal will default to matching the buffer's font fallbacks.
- pub font_fallbacks: Option<Vec<String>>,
+ #[schemars(extend("uniqueItems" = true))]
+ pub font_fallbacks: Option<Vec<FontFamilyName>>,
/// Sets the terminal's line height.
///
@@ -234,33 +235,13 @@ impl settings::Settings for TerminalSettings {
sources.json_merge()
}
- fn json_schema(
- generator: &mut SchemaGenerator,
- params: &SettingsJsonSchemaParams,
- _: &App,
- ) -> RootSchema {
- let mut root_schema = generator.root_schema_for::<Self::FileContent>();
- root_schema.definitions.extend([
- ("FontFamilies".into(), params.font_family_schema()),
- ("FontFallbacks".into(), params.font_fallback_schema()),
- ]);
-
- add_references_to_properties(
- &mut root_schema,
- &[
- ("font_family", "#/definitions/FontFamilies"),
- ("font_fallbacks", "#/definitions/FontFallbacks"),
- ],
- );
-
- root_schema
- }
-
fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
let name = |s| format!("terminal.integrated.{s}");
vscode.f32_setting(&name("fontSize"), &mut current.font_size);
- vscode.string_setting(&name("fontFamily"), &mut current.font_family);
+ if let Some(font_family) = vscode.read_string(&name("fontFamily")) {
+ current.font_family = Some(FontFamilyName(font_family.into()));
+ }
vscode.bool_setting(&name("copyOnSelection"), &mut current.copy_on_select);
vscode.bool_setting("macOptionIsMeta", &mut current.option_as_meta);
vscode.usize_setting("scrollback", &mut current.max_scroll_history_lines);
@@ -196,7 +196,6 @@ impl TerminalElement {
interactivity: Default::default(),
}
.track_focus(&focus)
- .element
}
//Vec<Range<AlacPoint>> -> Clip out the parts of the ranges
@@ -682,11 +681,10 @@ impl Element for TerminalElement {
let terminal_settings = TerminalSettings::get_global(cx);
- let font_family = terminal_settings
- .font_family
- .as_ref()
- .unwrap_or(&settings.buffer_font.family)
- .clone();
+ let font_family = terminal_settings.font_family.as_ref().map_or_else(
+ || settings.buffer_font.family.clone(),
+ |font_family| font_family.0.clone().into(),
+ );
let font_fallbacks = terminal_settings
.font_fallbacks
@@ -24,6 +24,7 @@ fs.workspace = true
futures.workspace = true
gpui.workspace = true
indexmap.workspace = true
+inventory.workspace = true
log.workspace = true
palette = { workspace = true, default-features = false, features = ["std"] }
parking_lot.workspace = true
@@ -52,6 +52,7 @@ impl ThemeColors {
element_active: neutral().light_alpha().step_5(),
element_selected: neutral().light_alpha().step_5(),
element_disabled: neutral().light_alpha().step_3(),
+ element_selection_background: blue().light().step_3().alpha(0.25),
drop_target_background: blue().light_alpha().step_2(),
ghost_element_background: system.transparent,
ghost_element_hover: neutral().light_alpha().step_3(),
@@ -174,6 +175,7 @@ impl ThemeColors {
element_active: neutral().dark_alpha().step_5(),
element_selected: neutral().dark_alpha().step_5(),
element_disabled: neutral().dark_alpha().step_3(),
+ element_selection_background: blue().dark().step_3().alpha(0.25),
drop_target_background: blue().dark_alpha().step_2(),
ghost_element_background: system.transparent,
ghost_element_hover: neutral().dark_alpha().step_4(),
@@ -4,7 +4,8 @@ use gpui::{FontStyle, FontWeight, HighlightStyle, Hsla, WindowBackgroundAppearan
use crate::{
AccentColors, Appearance, PlayerColors, StatusColors, StatusColorsRefinement, SyntaxTheme,
- SystemColors, Theme, ThemeColors, ThemeFamily, ThemeStyles, default_color_scales,
+ SystemColors, Theme, ThemeColors, ThemeColorsRefinement, ThemeFamily, ThemeStyles,
+ default_color_scales,
};
/// The default theme family for Zed.
@@ -41,6 +42,19 @@ pub(crate) fn apply_status_color_defaults(status: &mut StatusColorsRefinement) {
}
}
+pub(crate) fn apply_theme_color_defaults(
+ theme_colors: &mut ThemeColorsRefinement,
+ player_colors: &PlayerColors,
+) {
+ if theme_colors.element_selection_background.is_none() {
+ let mut selection = player_colors.local().selection;
+ if selection.a == 1.0 {
+ selection.a = 0.25;
+ }
+ theme_colors.element_selection_background = Some(selection);
+ }
+}
+
pub(crate) fn zed_default_dark() -> Theme {
let bg = hsla(215. / 360., 12. / 100., 15. / 100., 1.);
let editor = hsla(220. / 360., 12. / 100., 18. / 100., 1.);
@@ -74,6 +88,7 @@ pub(crate) fn zed_default_dark() -> Theme {
a: 1.0,
};
+ let player = PlayerColors::dark();
Theme {
id: "one_dark".to_string(),
name: "One Dark".into(),
@@ -97,6 +112,7 @@ pub(crate) fn zed_default_dark() -> Theme {
element_active: hsla(220.0 / 360., 11.8 / 100., 20.0 / 100., 1.0),
element_selected: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0),
element_disabled: SystemColors::default().transparent,
+ element_selection_background: player.local().selection.alpha(0.25),
drop_target_background: hsla(220.0 / 360., 8.3 / 100., 21.4 / 100., 1.0),
ghost_element_background: SystemColors::default().transparent,
ghost_element_hover: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 1.0),
@@ -258,7 +274,7 @@ pub(crate) fn zed_default_dark() -> Theme {
warning_background: yellow,
warning_border: yellow,
},
- player: PlayerColors::dark(),
+ player,
syntax: Arc::new(SyntaxTheme {
highlights: vec![
("attribute".into(), purple.into()),
@@ -4,12 +4,11 @@ use anyhow::Result;
use gpui::{FontStyle, FontWeight, HighlightStyle, Hsla, WindowBackgroundAppearance};
use indexmap::IndexMap;
use palette::FromColor;
-use schemars::JsonSchema;
-use schemars::r#gen::SchemaGenerator;
-use schemars::schema::{Schema, SchemaObject};
+use schemars::{JsonSchema, json_schema};
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use serde_repr::{Deserialize_repr, Serialize_repr};
+use std::borrow::Cow;
use crate::{StatusColorsRefinement, ThemeColorsRefinement};
@@ -219,6 +218,10 @@ pub struct ThemeColorsContent {
#[serde(rename = "element.disabled")]
pub element_disabled: Option<String>,
+ /// Background Color. Used for the background of selections in a UI element.
+ #[serde(rename = "element.selection_background")]
+ pub element_selection_background: Option<String>,
+
/// Background Color. Used for the area that shows where a dragged element will be dropped.
#[serde(rename = "drop_target.background")]
pub drop_target_background: Option<String>,
@@ -726,6 +729,10 @@ impl ThemeColorsContent {
.element_disabled
.as_ref()
.and_then(|color| try_parse_color(color).ok()),
+ element_selection_background: self
+ .element_selection_background
+ .as_ref()
+ .and_then(|color| try_parse_color(color).ok()),
drop_target_background: self
.drop_target_background
.as_ref()
@@ -1494,30 +1501,15 @@ pub enum FontWeightContent {
}
impl JsonSchema for FontWeightContent {
- fn schema_name() -> String {
- "FontWeightContent".to_owned()
+ fn schema_name() -> Cow<'static, str> {
+ "FontWeightContent".into()
}
- fn is_referenceable() -> bool {
- false
- }
-
- fn json_schema(_: &mut SchemaGenerator) -> Schema {
- SchemaObject {
- enum_values: Some(vec![
- 100.into(),
- 200.into(),
- 300.into(),
- 400.into(),
- 500.into(),
- 600.into(),
- 700.into(),
- 800.into(),
- 900.into(),
- ]),
- ..Default::default()
- }
- .into()
+ fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
+ json_schema!({
+ "type": "integer",
+ "enum": [100, 200, 300, 400, 500, 600, 700, 800, 900]
+ })
}
}
@@ -7,17 +7,12 @@ use anyhow::Result;
use derive_more::{Deref, DerefMut};
use gpui::{
App, Context, Font, FontFallbacks, FontFeatures, FontStyle, FontWeight, Global, Pixels,
- Subscription, Window, px,
+ SharedString, Subscription, Window, px,
};
use refineable::Refineable;
-use schemars::{
- JsonSchema,
- r#gen::SchemaGenerator,
- schema::{InstanceType, Schema, SchemaObject},
-};
+use schemars::{JsonSchema, json_schema};
use serde::{Deserialize, Serialize};
-use serde_json::Value;
-use settings::{Settings, SettingsJsonSchemaParams, SettingsSources, add_references_to_properties};
+use settings::{ParameterizedJsonSchema, Settings, SettingsSources, replace_subschema};
use std::sync::Arc;
use util::ResultExt as _;
@@ -263,25 +258,19 @@ impl Global for AgentFontSize {}
#[serde(untagged)]
pub enum ThemeSelection {
/// A static theme selection, represented by a single theme name.
- Static(#[schemars(schema_with = "theme_name_ref")] String),
+ Static(ThemeName),
/// A dynamic theme selection, which can change based the [ThemeMode].
Dynamic {
/// The mode used to determine which theme to use.
#[serde(default)]
mode: ThemeMode,
/// The theme to use for light mode.
- #[schemars(schema_with = "theme_name_ref")]
- light: String,
+ light: ThemeName,
/// The theme to use for dark mode.
- #[schemars(schema_with = "theme_name_ref")]
- dark: String,
+ dark: ThemeName,
},
}
-fn theme_name_ref(_: &mut SchemaGenerator) -> Schema {
- Schema::new_ref("#/definitions/ThemeName".into())
-}
-
// TODO: Rename ThemeMode -> ThemeAppearanceMode
/// The mode use to select a theme.
///
@@ -306,13 +295,13 @@ impl ThemeSelection {
/// Returns the theme name for the selected [ThemeMode].
pub fn theme(&self, system_appearance: Appearance) -> &str {
match self {
- Self::Static(theme) => theme,
+ Self::Static(theme) => &theme.0,
Self::Dynamic { mode, light, dark } => match mode {
- ThemeMode::Light => light,
- ThemeMode::Dark => dark,
+ ThemeMode::Light => &light.0,
+ ThemeMode::Dark => &dark.0,
ThemeMode::System => match system_appearance {
- Appearance::Light => light,
- Appearance::Dark => dark,
+ Appearance::Light => &light.0,
+ Appearance::Dark => &dark.0,
},
},
}
@@ -327,27 +316,21 @@ impl ThemeSelection {
}
}
-fn icon_theme_name_ref(_: &mut SchemaGenerator) -> Schema {
- Schema::new_ref("#/definitions/IconThemeName".into())
-}
-
/// Represents the selection of an icon theme, which can be either static or dynamic.
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(untagged)]
pub enum IconThemeSelection {
/// A static icon theme selection, represented by a single icon theme name.
- Static(#[schemars(schema_with = "icon_theme_name_ref")] String),
+ Static(IconThemeName),
/// A dynamic icon theme selection, which can change based on the [`ThemeMode`].
Dynamic {
/// The mode used to determine which theme to use.
#[serde(default)]
mode: ThemeMode,
/// The icon theme to use for light mode.
- #[schemars(schema_with = "icon_theme_name_ref")]
- light: String,
+ light: IconThemeName,
/// The icon theme to use for dark mode.
- #[schemars(schema_with = "icon_theme_name_ref")]
- dark: String,
+ dark: IconThemeName,
},
}
@@ -355,13 +338,13 @@ impl IconThemeSelection {
/// Returns the icon theme name based on the given [`Appearance`].
pub fn icon_theme(&self, system_appearance: Appearance) -> &str {
match self {
- Self::Static(theme) => theme,
+ Self::Static(theme) => &theme.0,
Self::Dynamic { mode, light, dark } => match mode {
- ThemeMode::Light => light,
- ThemeMode::Dark => dark,
+ ThemeMode::Light => &light.0,
+ ThemeMode::Dark => &dark.0,
ThemeMode::System => match system_appearance {
- Appearance::Light => light,
- Appearance::Dark => dark,
+ Appearance::Light => &light.0,
+ Appearance::Dark => &dark.0,
},
},
}
@@ -384,11 +367,12 @@ pub struct ThemeSettingsContent {
pub ui_font_size: Option<f32>,
/// The name of a font to use for rendering in the UI.
#[serde(default)]
- pub ui_font_family: Option<String>,
+ pub ui_font_family: Option<FontFamilyName>,
/// The font fallbacks to use for rendering in the UI.
#[serde(default)]
#[schemars(default = "default_font_fallbacks")]
- pub ui_font_fallbacks: Option<Vec<String>>,
+ #[schemars(extend("uniqueItems" = true))]
+ pub ui_font_fallbacks: Option<Vec<FontFamilyName>>,
/// The OpenType features to enable for text in the UI.
#[serde(default)]
#[schemars(default = "default_font_features")]
@@ -398,11 +382,11 @@ pub struct ThemeSettingsContent {
pub ui_font_weight: Option<f32>,
/// The name of a font to use for rendering in text buffers.
#[serde(default)]
- pub buffer_font_family: Option<String>,
+ pub buffer_font_family: Option<FontFamilyName>,
/// The font fallbacks to use for rendering in text buffers.
#[serde(default)]
- #[schemars(default = "default_font_fallbacks")]
- pub buffer_font_fallbacks: Option<Vec<String>>,
+ #[schemars(extend("uniqueItems" = true))]
+ pub buffer_font_fallbacks: Option<Vec<FontFamilyName>>,
/// The default font size for rendering in text buffers.
#[serde(default)]
pub buffer_font_size: Option<f32>,
@@ -467,9 +451,9 @@ impl ThemeSettingsContent {
},
};
- *theme_to_update = theme_name.to_string();
+ *theme_to_update = ThemeName(theme_name.into());
} else {
- self.theme = Some(ThemeSelection::Static(theme_name.to_string()));
+ self.theme = Some(ThemeSelection::Static(ThemeName(theme_name.into())));
}
}
@@ -488,9 +472,11 @@ impl ThemeSettingsContent {
},
};
- *icon_theme_to_update = icon_theme_name.to_string();
+ *icon_theme_to_update = IconThemeName(icon_theme_name.into());
} else {
- self.icon_theme = Some(IconThemeSelection::Static(icon_theme_name.to_string()));
+ self.icon_theme = Some(IconThemeSelection::Static(IconThemeName(
+ icon_theme_name.into(),
+ )));
}
}
@@ -516,8 +502,8 @@ impl ThemeSettingsContent {
} else {
self.theme = Some(ThemeSelection::Dynamic {
mode,
- light: ThemeSettings::DEFAULT_LIGHT_THEME.into(),
- dark: ThemeSettings::DEFAULT_DARK_THEME.into(),
+ light: ThemeName(ThemeSettings::DEFAULT_LIGHT_THEME.into()),
+ dark: ThemeName(ThemeSettings::DEFAULT_DARK_THEME.into()),
});
}
@@ -539,7 +525,9 @@ impl ThemeSettingsContent {
} => *mode_to_update = mode,
}
} else {
- self.icon_theme = Some(IconThemeSelection::Static(DEFAULT_ICON_THEME_NAME.into()));
+ self.icon_theme = Some(IconThemeSelection::Static(IconThemeName(
+ DEFAULT_ICON_THEME_NAME.into(),
+ )));
}
}
}
@@ -815,26 +803,39 @@ impl settings::Settings for ThemeSettings {
let themes = ThemeRegistry::default_global(cx);
let system_appearance = SystemAppearance::default_global(cx);
+ fn font_fallbacks_from_settings(
+ fallbacks: Option<Vec<FontFamilyName>>,
+ ) -> Option<FontFallbacks> {
+ fallbacks.map(|fallbacks| {
+ FontFallbacks::from_fonts(
+ fallbacks
+ .into_iter()
+ .map(|font_family| font_family.0.to_string())
+ .collect(),
+ )
+ })
+ }
+
let defaults = sources.default;
let mut this = Self {
ui_font_size: defaults.ui_font_size.unwrap().into(),
ui_font: Font {
- family: defaults.ui_font_family.as_ref().unwrap().clone().into(),
+ family: defaults.ui_font_family.as_ref().unwrap().0.clone().into(),
features: defaults.ui_font_features.clone().unwrap(),
- fallbacks: defaults
- .ui_font_fallbacks
- .as_ref()
- .map(|fallbacks| FontFallbacks::from_fonts(fallbacks.clone())),
+ fallbacks: font_fallbacks_from_settings(defaults.ui_font_fallbacks.clone()),
weight: defaults.ui_font_weight.map(FontWeight).unwrap(),
style: Default::default(),
},
buffer_font: Font {
- family: defaults.buffer_font_family.as_ref().unwrap().clone().into(),
- features: defaults.buffer_font_features.clone().unwrap(),
- fallbacks: defaults
- .buffer_font_fallbacks
+ family: defaults
+ .buffer_font_family
.as_ref()
- .map(|fallbacks| FontFallbacks::from_fonts(fallbacks.clone())),
+ .unwrap()
+ .0
+ .clone()
+ .into(),
+ features: defaults.buffer_font_features.clone().unwrap(),
+ fallbacks: font_fallbacks_from_settings(defaults.buffer_font_fallbacks.clone()),
weight: defaults.buffer_font_weight.map(FontWeight).unwrap(),
style: FontStyle::default(),
},
@@ -872,26 +873,26 @@ impl settings::Settings for ThemeSettings {
}
if let Some(value) = value.buffer_font_family.clone() {
- this.buffer_font.family = value.into();
+ this.buffer_font.family = value.0.into();
}
if let Some(value) = value.buffer_font_features.clone() {
this.buffer_font.features = value;
}
if let Some(value) = value.buffer_font_fallbacks.clone() {
- this.buffer_font.fallbacks = Some(FontFallbacks::from_fonts(value));
+ this.buffer_font.fallbacks = font_fallbacks_from_settings(Some(value));
}
if let Some(value) = value.buffer_font_weight {
this.buffer_font.weight = clamp_font_weight(value);
}
if let Some(value) = value.ui_font_family.clone() {
- this.ui_font.family = value.into();
+ this.ui_font.family = value.0.into();
}
if let Some(value) = value.ui_font_features.clone() {
this.ui_font.features = value;
}
if let Some(value) = value.ui_font_fallbacks.clone() {
- this.ui_font.fallbacks = Some(FontFallbacks::from_fonts(value));
+ this.ui_font.fallbacks = font_fallbacks_from_settings(Some(value));
}
if let Some(value) = value.ui_font_weight {
this.ui_font.weight = clamp_font_weight(value);
@@ -959,64 +960,73 @@ impl settings::Settings for ThemeSettings {
Ok(this)
}
- fn json_schema(
- generator: &mut SchemaGenerator,
- params: &SettingsJsonSchemaParams,
- cx: &App,
- ) -> schemars::schema::RootSchema {
- let mut root_schema = generator.root_schema_for::<ThemeSettingsContent>();
- let theme_names = ThemeRegistry::global(cx)
- .list_names()
- .into_iter()
- .map(|theme_name| Value::String(theme_name.to_string()))
- .collect();
-
- let theme_name_schema = SchemaObject {
- instance_type: Some(InstanceType::String.into()),
- enum_values: Some(theme_names),
- ..Default::default()
- };
-
- let icon_theme_names = ThemeRegistry::global(cx)
- .list_icon_themes()
- .into_iter()
- .map(|icon_theme| Value::String(icon_theme.name.to_string()))
- .collect();
-
- let icon_theme_name_schema = SchemaObject {
- instance_type: Some(InstanceType::String.into()),
- enum_values: Some(icon_theme_names),
- ..Default::default()
- };
-
- root_schema.definitions.extend([
- ("ThemeName".into(), theme_name_schema.into()),
- ("IconThemeName".into(), icon_theme_name_schema.into()),
- ("FontFamilies".into(), params.font_family_schema()),
- ("FontFallbacks".into(), params.font_fallback_schema()),
- ]);
-
- add_references_to_properties(
- &mut root_schema,
- &[
- ("buffer_font_family", "#/definitions/FontFamilies"),
- ("buffer_font_fallbacks", "#/definitions/FontFallbacks"),
- ("ui_font_family", "#/definitions/FontFamilies"),
- ("ui_font_fallbacks", "#/definitions/FontFallbacks"),
- ],
- );
-
- root_schema
- }
-
fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
vscode.f32_setting("editor.fontWeight", &mut current.buffer_font_weight);
vscode.f32_setting("editor.fontSize", &mut current.buffer_font_size);
- vscode.string_setting("editor.font", &mut current.buffer_font_family);
+ if let Some(font) = vscode.read_string("editor.font") {
+ current.buffer_font_family = Some(FontFamilyName(font.into()));
+ }
// TODO: possibly map editor.fontLigatures to buffer_font_features?
}
}
+/// Newtype for a theme name. Its `ParameterizedJsonSchema` lists the theme names known at runtime.
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[serde(transparent)]
+pub struct ThemeName(pub Arc<str>);
+
+inventory::submit! {
+ ParameterizedJsonSchema {
+ add_and_get_ref: |generator, _params, cx| {
+ let schema = json_schema!({
+ "type": "string",
+ "enum": ThemeRegistry::global(cx).list_names(),
+ });
+ replace_subschema::<ThemeName>(generator, schema)
+ }
+ }
+}
+
+/// Newtype for a icon theme name. Its `ParameterizedJsonSchema` lists the icon theme names known at
+/// runtime.
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[serde(transparent)]
+pub struct IconThemeName(pub Arc<str>);
+
+inventory::submit! {
+ ParameterizedJsonSchema {
+ add_and_get_ref: |generator, _params, cx| {
+ let schema = json_schema!({
+ "type": "string",
+ "enum": ThemeRegistry::global(cx)
+ .list_icon_themes()
+ .into_iter()
+ .map(|icon_theme| icon_theme.name)
+ .collect::<Vec<SharedString>>(),
+ });
+ replace_subschema::<IconThemeName>(generator, schema)
+ }
+ }
+}
+
+/// Newtype for font family name. Its `ParameterizedJsonSchema` lists the font families known at
+/// runtime.
+#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[serde(transparent)]
+pub struct FontFamilyName(pub Arc<str>);
+
+inventory::submit! {
+ ParameterizedJsonSchema {
+ add_and_get_ref: |generator, params, _cx| {
+ let schema = json_schema!({
+ "type": "string",
+ "enum": params.font_names,
+ });
+ replace_subschema::<FontFamilyName>(generator, schema)
+ }
+ }
+}
+
fn merge<T: Copy>(target: &mut T, value: Option<T>) {
if let Some(value) = value {
*target = value;
@@ -51,6 +51,8 @@ pub struct ThemeColors {
///
/// This could include a selected checkbox, a toggleable button that is toggled on, etc.
pub element_selected: Hsla,
+ /// Background Color. Used for the background of selections in a UI element.
+ pub element_selection_background: Hsla,
/// Background Color. Used for the disabled state of an element that should have a different background than the surface it's on.
///
/// Disabled states are shown when a user cannot interact with an element, like a disabled button or input.
@@ -35,6 +35,7 @@ use serde::Deserialize;
use uuid::Uuid;
pub use crate::default_colors::*;
+use crate::fallback_themes::apply_theme_color_defaults;
pub use crate::font_family_cache::*;
pub use crate::icon_theme::*;
pub use crate::icon_theme_schema::*;
@@ -165,12 +166,6 @@ impl ThemeFamily {
AppearanceContent::Dark => Appearance::Dark,
};
- let mut refined_theme_colors = match theme.appearance {
- AppearanceContent::Light => ThemeColors::light(),
- AppearanceContent::Dark => ThemeColors::dark(),
- };
- refined_theme_colors.refine(&theme.style.theme_colors_refinement());
-
let mut refined_status_colors = match theme.appearance {
AppearanceContent::Light => StatusColors::light(),
AppearanceContent::Dark => StatusColors::dark(),
@@ -185,6 +180,14 @@ impl ThemeFamily {
};
refined_player_colors.merge(&theme.style.players);
+ let mut refined_theme_colors = match theme.appearance {
+ AppearanceContent::Light => ThemeColors::light(),
+ AppearanceContent::Dark => ThemeColors::dark(),
+ };
+ let mut theme_colors_refinement = theme.style.theme_colors_refinement();
+ apply_theme_color_defaults(&mut theme_colors_refinement, &refined_player_colors);
+ refined_theme_colors.refine(&theme_colors_refinement);
+
let mut refined_accent_colors = match theme.appearance {
AppearanceContent::Light => AccentColors::light(),
AppearanceContent::Dark => AccentColors::dark(),
@@ -15,7 +15,6 @@ gpui.workspace = true
indexmap.workspace = true
log.workspace = true
palette.workspace = true
-rust-embed.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_json_lenient.workspace = true
@@ -1,27 +0,0 @@
-use std::borrow::Cow;
-
-use anyhow::{Context as _, Result};
-use gpui::{AssetSource, SharedString};
-use rust_embed::RustEmbed;
-
-#[derive(RustEmbed)]
-#[folder = "../../assets"]
-#[include = "fonts/**/*"]
-#[exclude = "*.DS_Store"]
-pub struct Assets;
-
-impl AssetSource for Assets {
- fn load(&self, path: &str) -> Result<Option<Cow<'static, [u8]>>> {
- Self::get(path)
- .map(|f| f.data)
- .with_context(|| format!("could not find asset at path {path:?}"))
- .map(Some)
- }
-
- fn list(&self, path: &str) -> Result<Vec<SharedString>> {
- Ok(Self::iter()
- .filter(|p| p.starts_with(path))
- .map(SharedString::from)
- .collect())
- }
-}
@@ -1,4 +1,3 @@
-mod assets;
mod color;
mod vscode;
@@ -32,7 +32,6 @@ mod settings_group;
mod stack;
mod tab;
mod tab_bar;
-mod table;
mod toggle;
mod tooltip;
@@ -73,7 +72,6 @@ pub use settings_group::*;
pub use stack::*;
pub use tab::*;
pub use tab_bar::*;
-pub use table::*;
pub use toggle::*;
pub use tooltip::*;
@@ -1,4 +1,4 @@
-use gpui::AnyElement;
+use gpui::{AnyElement, Hsla};
use crate::prelude::*;
@@ -24,7 +24,9 @@ pub struct Callout {
description: Option<SharedString>,
primary_action: Option<AnyElement>,
secondary_action: Option<AnyElement>,
+ tertiary_action: Option<AnyElement>,
line_height: Option<Pixels>,
+ bg_color: Option<Hsla>,
}
impl Callout {
@@ -36,7 +38,9 @@ impl Callout {
description: None,
primary_action: None,
secondary_action: None,
+ tertiary_action: None,
line_height: None,
+ bg_color: None,
}
}
@@ -71,64 +75,81 @@ impl Callout {
self
}
+ /// Sets an optional tertiary call-to-action button.
+ pub fn tertiary_action(mut self, action: impl IntoElement) -> Self {
+ self.tertiary_action = Some(action.into_any_element());
+ self
+ }
+
/// Sets a custom line height for the callout content.
pub fn line_height(mut self, line_height: Pixels) -> Self {
self.line_height = Some(line_height);
self
}
+
+ /// Sets a custom background color for the callout content.
+ pub fn bg_color(mut self, color: Hsla) -> Self {
+ self.bg_color = Some(color);
+ self
+ }
}
impl RenderOnce for Callout {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let line_height = self.line_height.unwrap_or(window.line_height());
+ let bg_color = self
+ .bg_color
+ .unwrap_or(cx.theme().colors().panel_background);
+ let has_actions = self.primary_action.is_some()
+ || self.secondary_action.is_some()
+ || self.tertiary_action.is_some();
h_flex()
- .w_full()
.p_2()
.gap_2()
.items_start()
- .bg(cx.theme().colors().panel_background)
+ .bg(bg_color)
.overflow_x_hidden()
.when_some(self.icon, |this, icon| {
this.child(h_flex().h(line_height).justify_center().child(icon))
})
.child(
v_flex()
+ .min_w_0()
.w_full()
.child(
h_flex()
.h(line_height)
.w_full()
.gap_1()
- .flex_wrap()
.justify_between()
.when_some(self.title, |this, title| {
this.child(h_flex().child(Label::new(title).size(LabelSize::Small)))
})
- .when(
- self.primary_action.is_some() || self.secondary_action.is_some(),
- |this| {
- this.child(
- h_flex()
- .gap_0p5()
- .when_some(self.secondary_action, |this, action| {
- this.child(action)
- })
- .when_some(self.primary_action, |this, action| {
- this.child(action)
- }),
- )
- },
- ),
+ .when(has_actions, |this| {
+ this.child(
+ h_flex()
+ .gap_0p5()
+ .when_some(self.tertiary_action, |this, action| {
+ this.child(action)
+ })
+ .when_some(self.secondary_action, |this, action| {
+ this.child(action)
+ })
+ .when_some(self.primary_action, |this, action| {
+ this.child(action)
+ }),
+ )
+ }),
)
.when_some(self.description, |this, description| {
this.child(
div()
.w_full()
.flex_1()
- .child(description)
.text_ui_sm(cx)
- .text_color(cx.theme().colors().text_muted),
+ .text_color(cx.theme().colors().text_muted)
+ .child(description),
)
}),
)
@@ -8,11 +8,12 @@ use itertools::Itertools;
#[derive(Debug, IntoElement, Clone, RegisterComponent)]
pub struct KeyBinding {
- /// A keybinding consists of a key and a set of modifier keys.
- /// More then one keybinding produces a chord.
+ /// A keybinding consists of a set of keystrokes,
+ /// where each keystroke is a key and a set of modifier keys.
+ /// More than one keystroke produces a chord.
///
- /// This should always contain at least one element.
- key_binding: gpui::KeyBinding,
+ /// This should always contain at least one keystroke.
+ pub key_binding: gpui::KeyBinding,
/// The [`PlatformStyle`] to use when displaying this keybinding.
platform_style: PlatformStyle,
@@ -1,271 +0,0 @@
-use crate::{Indicator, prelude::*};
-use gpui::{AnyElement, FontWeight, IntoElement, Length, div};
-
-/// A table component
-#[derive(IntoElement, RegisterComponent)]
-pub struct Table {
- column_headers: Vec<SharedString>,
- rows: Vec<Vec<TableCell>>,
- column_count: usize,
- striped: bool,
- width: Length,
-}
-
-impl Table {
- /// Create a new table with a column count equal to the
- /// number of headers provided.
- pub fn new(headers: Vec<impl Into<SharedString>>) -> Self {
- let column_count = headers.len();
-
- Table {
- column_headers: headers.into_iter().map(Into::into).collect(),
- column_count,
- rows: Vec::new(),
- striped: false,
- width: Length::Auto,
- }
- }
-
- /// Adds a row to the table.
- ///
- /// The row must have the same number of columns as the table.
- pub fn row(mut self, items: Vec<impl Into<TableCell>>) -> Self {
- if items.len() == self.column_count {
- self.rows.push(items.into_iter().map(Into::into).collect());
- } else {
- // TODO: Log error: Row length mismatch
- }
- self
- }
-
- /// Adds multiple rows to the table.
- ///
- /// Each row must have the same number of columns as the table.
- /// Rows that don't match the column count are ignored.
- pub fn rows(mut self, rows: Vec<Vec<impl Into<TableCell>>>) -> Self {
- for row in rows {
- self = self.row(row);
- }
- self
- }
-
- fn base_cell_style(cx: &mut App) -> Div {
- div()
- .px_1p5()
- .flex_1()
- .justify_start()
- .text_ui(cx)
- .whitespace_nowrap()
- .text_ellipsis()
- .overflow_hidden()
- }
-
- /// Enables row striping.
- pub fn striped(mut self) -> Self {
- self.striped = true;
- self
- }
-
- /// Sets the width of the table.
- pub fn width(mut self, width: impl Into<Length>) -> Self {
- self.width = width.into();
- self
- }
-}
-
-impl RenderOnce for Table {
- fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
- let header = div()
- .flex()
- .flex_row()
- .items_center()
- .justify_between()
- .w_full()
- .p_2()
- .border_b_1()
- .border_color(cx.theme().colors().border)
- .children(self.column_headers.into_iter().map(|h| {
- Self::base_cell_style(cx)
- .font_weight(FontWeight::SEMIBOLD)
- .child(h)
- }));
-
- let row_count = self.rows.len();
- let rows = self.rows.into_iter().enumerate().map(|(ix, row)| {
- let is_last = ix == row_count - 1;
- let bg = if ix % 2 == 1 && self.striped {
- Some(cx.theme().colors().text.opacity(0.05))
- } else {
- None
- };
- div()
- .w_full()
- .flex()
- .flex_row()
- .items_center()
- .justify_between()
- .px_1p5()
- .py_1()
- .when_some(bg, |row, bg| row.bg(bg))
- .when(!is_last, |row| {
- row.border_b_1().border_color(cx.theme().colors().border)
- })
- .children(row.into_iter().map(|cell| match cell {
- TableCell::String(s) => Self::base_cell_style(cx).child(s),
- TableCell::Element(e) => Self::base_cell_style(cx).child(e),
- }))
- });
-
- div()
- .w(self.width)
- .overflow_hidden()
- .child(header)
- .children(rows)
- }
-}
-
-/// Represents a cell in a table.
-pub enum TableCell {
- /// A cell containing a string value.
- String(SharedString),
- /// A cell containing a UI element.
- Element(AnyElement),
-}
-
-/// Creates a `TableCell` containing a string value.
-pub fn string_cell(s: impl Into<SharedString>) -> TableCell {
- TableCell::String(s.into())
-}
-
-/// Creates a `TableCell` containing an element.
-pub fn element_cell(e: impl Into<AnyElement>) -> TableCell {
- TableCell::Element(e.into())
-}
-
-impl<E> From<E> for TableCell
-where
- E: Into<SharedString>,
-{
- fn from(e: E) -> Self {
- TableCell::String(e.into())
- }
-}
-
-impl Component for Table {
- fn scope() -> ComponentScope {
- ComponentScope::Layout
- }
-
- fn description() -> Option<&'static str> {
- Some("A table component for displaying data in rows and columns with optional styling.")
- }
-
- fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
- Some(
- v_flex()
- .gap_6()
- .children(vec![
- example_group_with_title(
- "Basic Tables",
- vec![
- single_example(
- "Simple Table",
- Table::new(vec!["Name", "Age", "City"])
- .width(px(400.))
- .row(vec!["Alice", "28", "New York"])
- .row(vec!["Bob", "32", "San Francisco"])
- .row(vec!["Charlie", "25", "London"])
- .into_any_element(),
- ),
- single_example(
- "Two Column Table",
- Table::new(vec!["Category", "Value"])
- .width(px(300.))
- .row(vec!["Revenue", "$100,000"])
- .row(vec!["Expenses", "$75,000"])
- .row(vec!["Profit", "$25,000"])
- .into_any_element(),
- ),
- ],
- ),
- example_group_with_title(
- "Styled Tables",
- vec![
- single_example(
- "Default",
- Table::new(vec!["Product", "Price", "Stock"])
- .width(px(400.))
- .row(vec!["Laptop", "$999", "In Stock"])
- .row(vec!["Phone", "$599", "Low Stock"])
- .row(vec!["Tablet", "$399", "Out of Stock"])
- .into_any_element(),
- ),
- single_example(
- "Striped",
- Table::new(vec!["Product", "Price", "Stock"])
- .width(px(400.))
- .striped()
- .row(vec!["Laptop", "$999", "In Stock"])
- .row(vec!["Phone", "$599", "Low Stock"])
- .row(vec!["Tablet", "$399", "Out of Stock"])
- .row(vec!["Headphones", "$199", "In Stock"])
- .into_any_element(),
- ),
- ],
- ),
- example_group_with_title(
- "Mixed Content Table",
- vec![single_example(
- "Table with Elements",
- Table::new(vec!["Status", "Name", "Priority", "Deadline", "Action"])
- .width(px(840.))
- .row(vec![
- element_cell(
- Indicator::dot().color(Color::Success).into_any_element(),
- ),
- string_cell("Project A"),
- string_cell("High"),
- string_cell("2023-12-31"),
- element_cell(
- Button::new("view_a", "View")
- .style(ButtonStyle::Filled)
- .full_width()
- .into_any_element(),
- ),
- ])
- .row(vec![
- element_cell(
- Indicator::dot().color(Color::Warning).into_any_element(),
- ),
- string_cell("Project B"),
- string_cell("Medium"),
- string_cell("2024-03-15"),
- element_cell(
- Button::new("view_b", "View")
- .style(ButtonStyle::Filled)
- .full_width()
- .into_any_element(),
- ),
- ])
- .row(vec![
- element_cell(
- Indicator::dot().color(Color::Error).into_any_element(),
- ),
- string_cell("Project C"),
- string_cell("Low"),
- string_cell("2024-06-30"),
- element_cell(
- Button::new("view_c", "View")
- .style(ButtonStyle::Filled)
- .full_width()
- .into_any_element(),
- ),
- ])
- .into_any_element(),
- )],
- ),
- ])
- .into_any_element(),
- )
- }
-}
@@ -29,7 +29,7 @@ pub struct SingleLineInput {
label: Option<SharedString>,
/// The placeholder text for the text field.
placeholder: SharedString,
- /// Exposes the underlying [`Model<Editor>`] to allow for customizing the editor beyond the provided API.
+ /// Exposes the underlying [`Entity<Editor>`] to allow for customizing the editor beyond the provided API.
///
/// This likely will only be public in the short term, ideally the API will be expanded to cover necessary use cases.
pub editor: Entity<Editor>,
@@ -153,7 +153,10 @@ impl Render for ZedPromptRenderer {
});
MarkdownStyle {
base_text_style,
- selection_background_color: { cx.theme().players().local().selection },
+ selection_background_color: cx
+ .theme()
+ .colors()
+ .element_selection_background,
..Default::default()
}
}))
@@ -1,4 +1,4 @@
-use editor::{Bias, Direction, Editor, display_map::ToDisplayPoint, movement, scroll::Autoscroll};
+use editor::{Bias, Direction, Editor, display_map::ToDisplayPoint, movement};
use gpui::{Context, Window, actions};
use crate::{Vim, state::Mode};
@@ -29,7 +29,7 @@ impl Vim {
.next_change(count, direction)
.map(|s| s.to_vec())
{
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
let map = s.display_map();
s.select_display_ranges(selections.iter().map(|a| {
let point = a.to_display_point(&map);
@@ -2,10 +2,9 @@ use anyhow::Result;
use collections::{HashMap, HashSet};
use command_palette_hooks::CommandInterceptResult;
use editor::{
- Bias, Editor, ToPoint,
+ Bias, Editor, SelectionEffects, ToPoint,
actions::{SortLinesCaseInsensitive, SortLinesCaseSensitive},
display_map::ToDisplayPoint,
- scroll::Autoscroll,
};
use gpui::{Action, App, AppContext as _, Context, Global, Window, actions};
use itertools::Itertools;
@@ -422,7 +421,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
let target = snapshot
.buffer_snapshot
.clip_point(Point::new(buffer_row.0, current.head().column), Bias::Left);
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.select_ranges([target..target]);
});
@@ -493,7 +492,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
.disjoint_anchor_ranges()
.collect::<Vec<_>>()
});
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
let end = Point::new(range.end.0, s.buffer().line_len(range.end));
s.select_ranges([end..Point::new(range.start.0, 0)]);
});
@@ -503,7 +502,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
window.dispatch_action(action.action.boxed_clone(), cx);
cx.defer_in(window, move |vim, window, cx| {
vim.update_editor(window, cx, |_, editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
if let Some(previous_selections) = previous_selections {
s.select_ranges(previous_selections);
} else {
@@ -1068,6 +1067,7 @@ fn generate_commands(_: &App) -> Vec<VimCommand> {
)
}),
VimCommand::new(("reg", "isters"), ToggleRegistersView).bang(ToggleRegistersView),
+ VimCommand::new(("di", "splay"), ToggleRegistersView).bang(ToggleRegistersView),
VimCommand::new(("marks", ""), ToggleMarksView).bang(ToggleMarksView),
VimCommand::new(("delm", "arks"), ArgumentRequired)
.bang(DeleteMarks::AllLocal)
@@ -1086,6 +1086,7 @@ fn generate_commands(_: &App) -> Vec<VimCommand> {
VimCommand::str(("No", "tifications"), "notification_panel::ToggleFocus"),
VimCommand::str(("A", "I"), "agent::ToggleFocus"),
VimCommand::str(("G", "it"), "git_panel::ToggleFocus"),
+ VimCommand::str(("D", "ebug"), "debug_panel::ToggleFocus"),
VimCommand::new(("noh", "lsearch"), search::buffer_search::Dismiss),
VimCommand::new(("$", ""), EndOfDocument),
VimCommand::new(("%", ""), EndOfDocument),
@@ -1455,15 +1456,20 @@ impl OnMatchingLines {
editor
.update_in(cx, |editor, window, cx| {
editor.start_transaction_at(Instant::now(), window, cx);
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.replace_cursors_with(|_| new_selections);
});
window.dispatch_action(action, cx);
cx.defer_in(window, move |editor, window, cx| {
let newest = editor.selections.newest::<Point>(cx).clone();
- editor.change_selections(None, window, cx, |s| {
- s.select(vec![newest]);
- });
+ editor.change_selections(
+ SelectionEffects::no_scroll(),
+ window,
+ cx,
+ |s| {
+ s.select(vec![newest]);
+ },
+ );
editor.end_transaction_at(Instant::now(), cx);
})
})
@@ -1566,7 +1572,7 @@ impl Vim {
)
.unwrap_or((start.range(), MotionKind::Exclusive));
if range.start != start.start {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([
range.start.to_point(&snapshot)..range.start.to_point(&snapshot)
]);
@@ -1603,10 +1609,10 @@ impl Vim {
let snapshot = editor.snapshot(window, cx);
let start = editor.selections.newest_display(cx);
let range = object
- .range(&snapshot, start.clone(), around)
+ .range(&snapshot, start.clone(), around, None)
.unwrap_or(start.range());
if range.start != start.start {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges([
range.start.to_point(&snapshot)..range.start.to_point(&snapshot)
]);
@@ -1799,7 +1805,7 @@ impl ShellExec {
editor.transact(window, cx, |editor, window, cx| {
editor.edit([(range.clone(), text)], cx);
let snapshot = editor.buffer().read(cx).snapshot(cx);
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
let point = if is_read {
let point = range.end.to_point(&snapshot);
Point::new(point.row.saturating_sub(1), 0)
@@ -1,7 +1,8 @@
-use editor::{DisplayPoint, Editor, movement, scroll::Autoscroll};
+use editor::{DisplayPoint, Editor, movement};
use gpui::{Action, actions};
use gpui::{Context, Window};
use language::{CharClassifier, CharKind};
+use text::SelectionGoal;
use crate::{Vim, motion::Motion, state::Mode};
@@ -46,46 +47,46 @@ impl Vim {
mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool,
) {
self.update_editor(window, cx, |_, editor, window, cx| {
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let times = times.unwrap_or(1);
+ let new_goal = SelectionGoal::None;
+ let mut head = selection.head();
+ let mut tail = selection.tail();
- if selection.head() == map.max_point() {
+ if head == map.max_point() {
return;
}
// collapse to block cursor
- if selection.tail() < selection.head() {
- selection.set_tail(movement::left(map, selection.head()), selection.goal);
+ if tail < head {
+ tail = movement::left(map, head);
} else {
- selection.set_tail(selection.head(), selection.goal);
- selection.set_head(movement::right(map, selection.head()), selection.goal);
+ tail = head;
+ head = movement::right(map, head);
}
// create a classifier
- let classifier = map
- .buffer_snapshot
- .char_classifier_at(selection.head().to_point(map));
+ let classifier = map.buffer_snapshot.char_classifier_at(head.to_point(map));
- let mut last_selection = selection.clone();
for _ in 0..times {
- let (new_tail, new_head) =
- movement::find_boundary_trail(map, selection.head(), |left, right| {
+ let (maybe_next_tail, next_head) =
+ movement::find_boundary_trail(map, head, |left, right| {
is_boundary(left, right, &classifier)
});
- selection.set_head(new_head, selection.goal);
- if let Some(new_tail) = new_tail {
- selection.set_tail(new_tail, selection.goal);
+ if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail {
+ break;
}
- if selection.head() == last_selection.head()
- && selection.tail() == last_selection.tail()
- {
- break;
+ head = next_head;
+ if let Some(next_tail) = maybe_next_tail {
+ tail = next_tail;
}
- last_selection = selection.clone();
}
+
+ selection.set_tail(tail, new_goal);
+ selection.set_head(head, new_goal);
});
});
});
@@ -99,50 +100,53 @@ impl Vim {
mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool,
) {
self.update_editor(window, cx, |_, editor, window, cx| {
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let times = times.unwrap_or(1);
+ let new_goal = SelectionGoal::None;
+ let mut head = selection.head();
+ let mut tail = selection.tail();
- if selection.head() == DisplayPoint::zero() {
+ if head == DisplayPoint::zero() {
return;
}
// collapse to block cursor
- if selection.tail() < selection.head() {
- selection.set_tail(movement::left(map, selection.head()), selection.goal);
+ if tail < head {
+ tail = movement::left(map, head);
} else {
- selection.set_tail(selection.head(), selection.goal);
- selection.set_head(movement::right(map, selection.head()), selection.goal);
+ tail = head;
+ head = movement::right(map, head);
}
+ selection.set_head(head, new_goal);
+ selection.set_tail(tail, new_goal);
// flip the selection
selection.swap_head_tail();
+ head = selection.head();
+ tail = selection.tail();
// create a classifier
- let classifier = map
- .buffer_snapshot
- .char_classifier_at(selection.head().to_point(map));
+ let classifier = map.buffer_snapshot.char_classifier_at(head.to_point(map));
- let mut last_selection = selection.clone();
for _ in 0..times {
- let (new_tail, new_head) = movement::find_preceding_boundary_trail(
- map,
- selection.head(),
- |left, right| is_boundary(left, right, &classifier),
- );
-
- selection.set_head(new_head, selection.goal);
- if let Some(new_tail) = new_tail {
- selection.set_tail(new_tail, selection.goal);
- }
+ let (maybe_next_tail, next_head) =
+ movement::find_preceding_boundary_trail(map, head, |left, right| {
+ is_boundary(left, right, &classifier)
+ });
- if selection.head() == last_selection.head()
- && selection.tail() == last_selection.tail()
- {
+ if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail {
break;
}
- last_selection = selection.clone();
+
+ head = next_head;
+ if let Some(next_tail) = maybe_next_tail {
+ tail = next_tail;
+ }
}
+
+ selection.set_tail(tail, new_goal);
+ selection.set_head(head, new_goal);
});
})
});
@@ -157,7 +161,7 @@ impl Vim {
) {
self.update_editor(window, cx, |_, editor, window, cx| {
let text_layout_details = editor.text_layout_details(window);
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let goal = selection.goal;
let cursor = if selection.is_empty() || selection.reversed {
@@ -188,10 +192,10 @@ impl Vim {
self.helix_find_range_forward(times, window, cx, |left, right, classifier| {
let left_kind = classifier.kind_with(left, ignore_punctuation);
let right_kind = classifier.kind_with(right, ignore_punctuation);
- let at_newline = right == '\n';
+ let at_newline = (left == '\n') ^ (right == '\n');
- let found =
- left_kind != right_kind && right_kind != CharKind::Whitespace || at_newline;
+ let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
+ || at_newline;
found
})
@@ -200,10 +204,10 @@ impl Vim {
self.helix_find_range_forward(times, window, cx, |left, right, classifier| {
let left_kind = classifier.kind_with(left, ignore_punctuation);
let right_kind = classifier.kind_with(right, ignore_punctuation);
- let at_newline = right == '\n';
+ let at_newline = (left == '\n') ^ (right == '\n');
- let found = left_kind != right_kind
- && (left_kind != CharKind::Whitespace || at_newline);
+ let found = (left_kind != right_kind && left_kind != CharKind::Whitespace)
+ || at_newline;
found
})
@@ -212,10 +216,10 @@ impl Vim {
self.helix_find_range_backward(times, window, cx, |left, right, classifier| {
let left_kind = classifier.kind_with(left, ignore_punctuation);
let right_kind = classifier.kind_with(right, ignore_punctuation);
- let at_newline = right == '\n';
+ let at_newline = (left == '\n') ^ (right == '\n');
- let found = left_kind != right_kind
- && (left_kind != CharKind::Whitespace || at_newline);
+ let found = (left_kind != right_kind && left_kind != CharKind::Whitespace)
+ || at_newline;
found
})
@@ -224,11 +228,10 @@ impl Vim {
self.helix_find_range_backward(times, window, cx, |left, right, classifier| {
let left_kind = classifier.kind_with(left, ignore_punctuation);
let right_kind = classifier.kind_with(right, ignore_punctuation);
- let at_newline = right == '\n';
+ let at_newline = (left == '\n') ^ (right == '\n');
- let found = left_kind != right_kind
- && right_kind != CharKind::Whitespace
- && !at_newline;
+ let found = (left_kind != right_kind && right_kind != CharKind::Whitespace)
+ || at_newline;
found
})
@@ -236,7 +239,7 @@ impl Vim {
Motion::FindForward { .. } => {
self.update_editor(window, cx, |_, editor, window, cx| {
let text_layout_details = editor.text_layout_details(window);
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let goal = selection.goal;
let cursor = if selection.is_empty() || selection.reversed {
@@ -263,7 +266,7 @@ impl Vim {
Motion::FindBackward { .. } => {
self.update_editor(window, cx, |_, editor, window, cx| {
let text_layout_details = editor.text_layout_details(window);
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let goal = selection.goal;
let cursor = if selection.is_empty() || selection.reversed {
@@ -299,14 +302,14 @@ mod test {
use crate::{state::Mode, test::VimTestContext};
#[gpui::test]
- async fn test_next_word_start(cx: &mut gpui::TestAppContext) {
+ async fn test_word_motions(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
// Β«
// Λ
// Β»
cx.set_state(
indoc! {"
- The quΛick brown
+ ThΒ«e quiΛΒ»ck brown
fox jumps over
the lazy dog."},
Mode::HelixNormal,
@@ -331,6 +334,32 @@ mod test {
the lazy dog."},
Mode::HelixNormal,
);
+
+ cx.simulate_keystrokes("2 b");
+
+ cx.assert_state(
+ indoc! {"
+ The Β«Λquick Β»brown
+ fox jumps over
+ the lazy dog."},
+ Mode::HelixNormal,
+ );
+
+ cx.simulate_keystrokes("down e up");
+
+ cx.assert_state(
+ indoc! {"
+ The quicΛk brown
+ fox jumps over
+ the lazy dog."},
+ Mode::HelixNormal,
+ );
+
+ cx.set_state("aa\n Β«ΛbbΒ»", Mode::HelixNormal);
+
+ cx.simulate_keystroke("b");
+
+ cx.assert_state("aa\nΒ«Λ Β»bb", Mode::HelixNormal);
}
// #[gpui::test]
@@ -445,4 +474,21 @@ mod test {
Mode::HelixNormal,
);
}
+
+ #[gpui::test]
+ async fn test_newline_char(cx: &mut gpui::TestAppContext) {
+ let mut cx = VimTestContext::new(cx, true).await;
+
+ cx.set_state("aaΒ«\nΛΒ»bb cc", Mode::HelixNormal);
+
+ cx.simulate_keystroke("w");
+
+ cx.assert_state("aa\nΒ«bb ΛΒ»cc", Mode::HelixNormal);
+
+ cx.set_state("aaΒ«\nΛΒ»", Mode::HelixNormal);
+
+ cx.simulate_keystroke("b");
+
+ cx.assert_state("Β«ΛaaΒ»\n", Mode::HelixNormal);
+ }
}
@@ -1,5 +1,6 @@
use crate::{Vim, motion::Motion, object::Object, state::Mode};
use collections::HashMap;
+use editor::SelectionEffects;
use editor::{Bias, Editor, display_map::ToDisplayPoint};
use gpui::actions;
use gpui::{Context, Window};
@@ -88,7 +89,7 @@ impl Vim {
let text_layout_details = editor.text_layout_details(window);
editor.transact(window, cx, |editor, window, cx| {
let mut selection_starts: HashMap<_, _> = Default::default();
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| {
let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
selection_starts.insert(selection.id, anchor);
@@ -106,7 +107,7 @@ impl Vim {
IndentDirection::Out => editor.outdent(&Default::default(), window, cx),
IndentDirection::Auto => editor.autoindent(&Default::default(), window, cx),
}
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| {
let anchor = selection_starts.remove(&selection.id).unwrap();
selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
@@ -121,6 +122,7 @@ impl Vim {
object: Object,
around: bool,
dir: IndentDirection,
+ times: Option<usize>,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -128,11 +130,11 @@ impl Vim {
self.update_editor(window, cx, |_, editor, window, cx| {
editor.transact(window, cx, |editor, window, cx| {
let mut original_positions: HashMap<_, _> = Default::default();
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| {
let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
original_positions.insert(selection.id, anchor);
- object.expand_selection(map, selection, around);
+ object.expand_selection(map, selection, around, times);
});
});
match dir {
@@ -140,7 +142,7 @@ impl Vim {
IndentDirection::Out => editor.outdent(&Default::default(), window, cx),
IndentDirection::Auto => editor.autoindent(&Default::default(), window, cx),
}
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| {
let anchor = original_positions.remove(&selection.id).unwrap();
selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
@@ -1,5 +1,5 @@
use crate::{Vim, state::Mode};
-use editor::{Bias, Editor, scroll::Autoscroll};
+use editor::{Bias, Editor};
use gpui::{Action, Context, Window, actions};
use language::SelectionGoal;
use settings::Settings;
@@ -34,7 +34,7 @@ impl Vim {
editor.dismiss_menus_and_popups(false, window, cx);
if !HelixModeSetting::get_global(cx).0 {
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.move_cursors_with(|map, mut cursor, _| {
*cursor.column_mut() = cursor.column().saturating_sub(1);
(map.clip_point(cursor, Bias::Left), SelectionGoal::None)
@@ -4,7 +4,6 @@ use editor::{
movement::{
self, FindRange, TextLayoutDetails, find_boundary, find_preceding_boundary_display_point,
},
- scroll::Autoscroll,
};
use gpui::{Action, Context, Window, actions, px};
use language::{CharKind, Point, Selection, SelectionGoal};
@@ -626,7 +625,7 @@ impl Vim {
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
if !prior_selections.is_empty() {
self.update_editor(window, cx, |_, editor, window, cx| {
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.select_ranges(prior_selections.iter().cloned())
})
});
@@ -768,6 +767,73 @@ impl Motion {
}
}
+ pub(crate) fn push_to_jump_list(&self) -> bool {
+ use Motion::*;
+ match self {
+ CurrentLine
+ | Down { .. }
+ | EndOfLine { .. }
+ | EndOfLineDownward
+ | FindBackward { .. }
+ | FindForward { .. }
+ | FirstNonWhitespace { .. }
+ | GoToColumn
+ | Left
+ | MiddleOfLine { .. }
+ | NextLineStart
+ | NextSubwordEnd { .. }
+ | NextSubwordStart { .. }
+ | NextWordEnd { .. }
+ | NextWordStart { .. }
+ | PreviousLineStart
+ | PreviousSubwordEnd { .. }
+ | PreviousSubwordStart { .. }
+ | PreviousWordEnd { .. }
+ | PreviousWordStart { .. }
+ | RepeatFind { .. }
+ | RepeatFindReversed { .. }
+ | Right
+ | StartOfLine { .. }
+ | StartOfLineDownward
+ | Up { .. }
+ | WrappingLeft
+ | WrappingRight => false,
+ EndOfDocument
+ | EndOfParagraph
+ | GoToPercentage
+ | Jump { .. }
+ | Matching
+ | NextComment
+ | NextGreaterIndent
+ | NextLesserIndent
+ | NextMethodEnd
+ | NextMethodStart
+ | NextSameIndent
+ | NextSectionEnd
+ | NextSectionStart
+ | PreviousComment
+ | PreviousGreaterIndent
+ | PreviousLesserIndent
+ | PreviousMethodEnd
+ | PreviousMethodStart
+ | PreviousSameIndent
+ | PreviousSectionEnd
+ | PreviousSectionStart
+ | SentenceBackward
+ | SentenceForward
+ | Sneak { .. }
+ | SneakBackward { .. }
+ | StartOfDocument
+ | StartOfParagraph
+ | UnmatchedBackward { .. }
+ | UnmatchedForward { .. }
+ | WindowBottom
+ | WindowMiddle
+ | WindowTop
+ | ZedSearchResult { .. } => true,
+ }
+ }
+
pub fn infallible(&self) -> bool {
use Motion::*;
match self {
@@ -24,13 +24,12 @@ use crate::{
};
use collections::BTreeSet;
use convert::ConvertTarget;
-use editor::Anchor;
use editor::Bias;
use editor::Editor;
-use editor::scroll::Autoscroll;
+use editor::{Anchor, SelectionEffects};
use editor::{display_map::ToDisplayPoint, movement};
use gpui::{Context, Window, actions};
-use language::{Point, SelectionGoal, ToPoint};
+use language::{Point, SelectionGoal};
use log::error;
use multi_buffer::MultiBufferRow;
@@ -103,7 +102,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
Vim::action(editor, cx, |vim, _: &HelixDelete, window, cx| {
vim.record_current_action(cx);
vim.update_editor(window, cx, |_, editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| {
if selection.is_empty() {
selection.end = movement::right(map, selection.end)
@@ -278,40 +277,51 @@ impl Vim {
self.exit_temporary_normal(window, cx);
}
- pub fn normal_object(&mut self, object: Object, window: &mut Window, cx: &mut Context<Self>) {
+ pub fn normal_object(
+ &mut self,
+ object: Object,
+ times: Option<usize>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
let mut waiting_operator: Option<Operator> = None;
match self.maybe_pop_operator() {
Some(Operator::Object { around }) => match self.maybe_pop_operator() {
- Some(Operator::Change) => self.change_object(object, around, window, cx),
- Some(Operator::Delete) => self.delete_object(object, around, window, cx),
- Some(Operator::Yank) => self.yank_object(object, around, window, cx),
+ Some(Operator::Change) => self.change_object(object, around, times, window, cx),
+ Some(Operator::Delete) => self.delete_object(object, around, times, window, cx),
+ Some(Operator::Yank) => self.yank_object(object, around, times, window, cx),
Some(Operator::Indent) => {
- self.indent_object(object, around, IndentDirection::In, window, cx)
+ self.indent_object(object, around, IndentDirection::In, times, window, cx)
}
Some(Operator::Outdent) => {
- self.indent_object(object, around, IndentDirection::Out, window, cx)
+ self.indent_object(object, around, IndentDirection::Out, times, window, cx)
}
Some(Operator::AutoIndent) => {
- self.indent_object(object, around, IndentDirection::Auto, window, cx)
+ self.indent_object(object, around, IndentDirection::Auto, times, window, cx)
}
Some(Operator::ShellCommand) => {
self.shell_command_object(object, around, window, cx);
}
- Some(Operator::Rewrap) => self.rewrap_object(object, around, window, cx),
+ Some(Operator::Rewrap) => self.rewrap_object(object, around, times, window, cx),
Some(Operator::Lowercase) => {
- self.convert_object(object, around, ConvertTarget::LowerCase, window, cx)
+ self.convert_object(object, around, ConvertTarget::LowerCase, times, window, cx)
}
Some(Operator::Uppercase) => {
- self.convert_object(object, around, ConvertTarget::UpperCase, window, cx)
- }
- Some(Operator::OppositeCase) => {
- self.convert_object(object, around, ConvertTarget::OppositeCase, window, cx)
+ self.convert_object(object, around, ConvertTarget::UpperCase, times, window, cx)
}
+ Some(Operator::OppositeCase) => self.convert_object(
+ object,
+ around,
+ ConvertTarget::OppositeCase,
+ times,
+ window,
+ cx,
+ ),
Some(Operator::Rot13) => {
- self.convert_object(object, around, ConvertTarget::Rot13, window, cx)
+ self.convert_object(object, around, ConvertTarget::Rot13, times, window, cx)
}
Some(Operator::Rot47) => {
- self.convert_object(object, around, ConvertTarget::Rot47, window, cx)
+ self.convert_object(object, around, ConvertTarget::Rot47, times, window, cx)
}
Some(Operator::AddSurrounds { target: None }) => {
waiting_operator = Some(Operator::AddSurrounds {
@@ -319,7 +329,7 @@ impl Vim {
});
}
Some(Operator::ToggleComments) => {
- self.toggle_comments_object(object, around, window, cx)
+ self.toggle_comments_object(object, around, times, window, cx)
}
Some(Operator::ReplaceWithRegister) => {
self.replace_with_register_object(object, around, window, cx)
@@ -358,13 +368,18 @@ impl Vim {
) {
self.update_editor(window, cx, |_, editor, window, cx| {
let text_layout_details = editor.text_layout_details(window);
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
- s.move_cursors_with(|map, cursor, goal| {
- motion
- .move_point(map, cursor, goal, times, &text_layout_details)
- .unwrap_or((cursor, goal))
- })
- })
+ editor.change_selections(
+ SelectionEffects::default().nav_history(motion.push_to_jump_list()),
+ window,
+ cx,
+ |s| {
+ s.move_cursors_with(|map, cursor, goal| {
+ motion
+ .move_point(map, cursor, goal, times, &text_layout_details)
+ .unwrap_or((cursor, goal))
+ })
+ },
+ )
});
}
@@ -372,7 +387,7 @@ impl Vim {
self.start_recording(cx);
self.switch_mode(Mode::Insert, false, window, cx);
self.update_editor(window, cx, |_, editor, window, cx| {
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.move_cursors_with(|map, cursor, _| (right(map, cursor, 1), SelectionGoal::None));
});
});
@@ -383,7 +398,7 @@ impl Vim {
if self.mode.is_visual() {
let current_mode = self.mode;
self.update_editor(window, cx, |_, editor, window, cx| {
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
if current_mode == Mode::VisualLine {
let start_of_line = motion::start_of_line(map, false, selection.start);
@@ -407,7 +422,7 @@ impl Vim {
self.start_recording(cx);
self.switch_mode(Mode::Insert, false, window, cx);
self.update_editor(window, cx, |_, editor, window, cx| {
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.move_cursors_with(|map, cursor, _| {
(
first_non_whitespace(map, false, cursor),
@@ -427,7 +442,7 @@ impl Vim {
self.start_recording(cx);
self.switch_mode(Mode::Insert, false, window, cx);
self.update_editor(window, cx, |_, editor, window, cx| {
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.move_cursors_with(|map, cursor, _| {
(next_line_end(map, cursor, 1), SelectionGoal::None)
});
@@ -448,7 +463,7 @@ impl Vim {
return;
};
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.select_anchor_ranges(marks.iter().map(|mark| *mark..*mark))
});
});
@@ -484,7 +499,7 @@ impl Vim {
})
.collect::<Vec<_>>();
editor.edit_with_autoindent(edits, cx);
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.move_cursors_with(|map, cursor, _| {
let previous_line = motion::start_of_relative_buffer_row(map, cursor, -1);
let insert_point = motion::end_of_line(map, false, previous_line, 1);
@@ -525,7 +540,7 @@ impl Vim {
(end_of_line..end_of_line, "\n".to_string() + &indent)
})
.collect::<Vec<_>>();
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.maybe_move_cursors_with(|map, cursor, goal| {
Motion::CurrentLine.move_point(
map,
@@ -602,7 +617,7 @@ impl Vim {
.collect::<Vec<_>>();
editor.edit(edits, cx);
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|_, selection| {
if let Some(position) = original_positions.get(&selection.id) {
selection.collapse_to(*position, SelectionGoal::None);
@@ -658,38 +673,42 @@ impl Vim {
Vim::take_forced_motion(cx);
self.update_editor(window, cx, |vim, editor, _window, cx| {
let selection = editor.selections.newest_anchor();
- if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
- let filename = if let Some(file) = buffer.read(cx).file() {
- if count.is_some() {
- if let Some(local) = file.as_local() {
- local.abs_path(cx).to_string_lossy().to_string()
- } else {
- file.full_path(cx).to_string_lossy().to_string()
- }
+ let Some((buffer, point, _)) = editor
+ .buffer()
+ .read(cx)
+ .point_to_buffer_point(selection.head(), cx)
+ else {
+ return;
+ };
+ let filename = if let Some(file) = buffer.read(cx).file() {
+ if count.is_some() {
+ if let Some(local) = file.as_local() {
+ local.abs_path(cx).to_string_lossy().to_string()
} else {
- file.path().to_string_lossy().to_string()
+ file.full_path(cx).to_string_lossy().to_string()
}
} else {
- "[No Name]".into()
- };
- let buffer = buffer.read(cx);
- let snapshot = buffer.snapshot();
- let lines = buffer.max_point().row + 1;
- let current_line = selection.head().text_anchor.to_point(&snapshot).row;
- let percentage = current_line as f32 / lines as f32;
- let modified = if buffer.is_dirty() { " [modified]" } else { "" };
- vim.status_label = Some(
- format!(
- "{}{} {} lines --{:.0}%--",
- filename,
- modified,
- lines,
- percentage * 100.0,
- )
- .into(),
- );
- cx.notify();
- }
+ file.path().to_string_lossy().to_string()
+ }
+ } else {
+ "[No Name]".into()
+ };
+ let buffer = buffer.read(cx);
+ let lines = buffer.max_point().row + 1;
+ let current_line = point.row;
+ let percentage = current_line as f32 / lines as f32;
+ let modified = if buffer.is_dirty() { " [modified]" } else { "" };
+ vim.status_label = Some(
+ format!(
+ "{}{} {} lines --{:.0}%--",
+ filename,
+ modified,
+ lines,
+ percentage * 100.0,
+ )
+ .into(),
+ );
+ cx.notify();
});
}
@@ -746,7 +765,7 @@ impl Vim {
editor.newline(&editor::actions::Newline, window, cx);
}
editor.set_clip_at_line_ends(true, cx);
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| {
let point = movement::saturating_left(map, selection.head());
selection.collapse_to(point, SelectionGoal::None)
@@ -782,7 +801,7 @@ impl Vim {
cx: &mut Context<Editor>,
mut positions: HashMap<usize, Anchor>,
) {
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
if let Some(anchor) = positions.remove(&selection.id) {
selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
@@ -1799,4 +1818,35 @@ mod test {
fox jΛumps over
the lazy dog"});
}
+
+ #[gpui::test]
+ async fn test_jump_list(cx: &mut gpui::TestAppContext) {
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+
+ cx.set_shared_state(indoc! {"
+ Λfn a() { }
+
+
+
+
+
+ fn b() { }
+
+
+
+
+
+ fn b() { }"})
+ .await;
+ cx.simulate_shared_keystrokes("3 }").await;
+ cx.shared_state().await.assert_matches();
+ cx.simulate_shared_keystrokes("ctrl-o").await;
+ cx.shared_state().await.assert_matches();
+ cx.simulate_shared_keystrokes("ctrl-i").await;
+ cx.shared_state().await.assert_matches();
+ cx.simulate_shared_keystrokes("1 1 k").await;
+ cx.shared_state().await.assert_matches();
+ cx.simulate_shared_keystrokes("ctrl-o").await;
+ cx.shared_state().await.assert_matches();
+ }
}
@@ -8,7 +8,6 @@ use editor::{
Bias, DisplayPoint,
display_map::{DisplaySnapshot, ToDisplayPoint},
movement::TextLayoutDetails,
- scroll::Autoscroll,
};
use gpui::{Context, Window};
use language::Selection;
@@ -40,7 +39,7 @@ impl Vim {
editor.transact(window, cx, |editor, window, cx| {
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
editor.set_clip_at_line_ends(false, cx);
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let kind = match motion {
Motion::NextWordStart { ignore_punctuation }
@@ -106,6 +105,7 @@ impl Vim {
&mut self,
object: Object,
around: bool,
+ times: Option<usize>,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -114,9 +114,9 @@ impl Vim {
// We are swapping to insert mode anyway. Just set the line end clipping behavior now
editor.set_clip_at_line_ends(false, cx);
editor.transact(window, cx, |editor, window, cx| {
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
- objects_found |= object.expand_selection(map, selection, around);
+ objects_found |= object.expand_selection(map, selection, around, times);
});
});
if objects_found {
@@ -1,5 +1,5 @@
use collections::HashMap;
-use editor::{display_map::ToDisplayPoint, scroll::Autoscroll};
+use editor::{SelectionEffects, display_map::ToDisplayPoint};
use gpui::{Context, Window};
use language::{Bias, Point, SelectionGoal};
use multi_buffer::MultiBufferRow;
@@ -36,7 +36,7 @@ impl Vim {
let text_layout_details = editor.text_layout_details(window);
editor.transact(window, cx, |editor, window, cx| {
let mut selection_starts: HashMap<_, _> = Default::default();
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| {
let anchor = map.display_point_to_anchor(selection.head(), Bias::Left);
selection_starts.insert(selection.id, anchor);
@@ -66,7 +66,7 @@ impl Vim {
editor.convert_to_rot47(&Default::default(), window, cx)
}
}
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| {
let anchor = selection_starts.remove(&selection.id).unwrap();
selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
@@ -82,6 +82,7 @@ impl Vim {
object: Object,
around: bool,
mode: ConvertTarget,
+ times: Option<usize>,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -90,9 +91,9 @@ impl Vim {
editor.transact(window, cx, |editor, window, cx| {
editor.set_clip_at_line_ends(false, cx);
let mut original_positions: HashMap<_, _> = Default::default();
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| {
- object.expand_selection(map, selection, around);
+ object.expand_selection(map, selection, around, times);
original_positions.insert(
selection.id,
map.display_point_to_anchor(selection.start, Bias::Left),
@@ -116,7 +117,7 @@ impl Vim {
editor.convert_to_rot47(&Default::default(), window, cx)
}
}
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| {
let anchor = original_positions.remove(&selection.id).unwrap();
selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
@@ -220,7 +221,9 @@ impl Vim {
}
ranges.push(start..end);
- if end.column == snapshot.line_len(MultiBufferRow(end.row)) {
+ if end.column == snapshot.line_len(MultiBufferRow(end.row))
+ && end.column > 0
+ {
end = snapshot.clip_point(end - Point::new(0, 1), Bias::Left);
}
cursor_positions.push(end..end)
@@ -237,7 +240,7 @@ impl Vim {
.collect::<String>();
editor.edit([(range, text)], cx)
}
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.select_ranges(cursor_positions)
})
});
@@ -7,7 +7,6 @@ use collections::{HashMap, HashSet};
use editor::{
Bias, DisplayPoint,
display_map::{DisplaySnapshot, ToDisplayPoint},
- scroll::Autoscroll,
};
use gpui::{Context, Window};
use language::{Point, Selection};
@@ -30,7 +29,7 @@ impl Vim {
let mut original_columns: HashMap<_, _> = Default::default();
let mut motion_kind = None;
let mut ranges_to_copy = Vec::new();
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let original_head = selection.head();
original_columns.insert(selection.id, original_head.column());
@@ -71,7 +70,7 @@ impl Vim {
// Fixup cursor position after the deletion
editor.set_clip_at_line_ends(true, cx);
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let mut cursor = selection.head();
if kind.linewise() {
@@ -92,6 +91,7 @@ impl Vim {
&mut self,
object: Object,
around: bool,
+ times: Option<usize>,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -102,9 +102,9 @@ impl Vim {
// Emulates behavior in vim where if we expanded backwards to include a newline
// the cursor gets set back to the start of the line
let mut should_move_to_start: HashSet<_> = Default::default();
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
- object.expand_selection(map, selection, around);
+ object.expand_selection(map, selection, around, times);
let offset_range = selection.map(|p| p.to_offset(map, Bias::Left)).range();
let mut move_selection_start_to_previous_line =
|map: &DisplaySnapshot, selection: &mut Selection<DisplayPoint>| {
@@ -159,7 +159,7 @@ impl Vim {
// Fixup cursor position after the deletion
editor.set_clip_at_line_ends(true, cx);
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let mut cursor = selection.head();
if should_move_to_start.contains(&selection.id) {
@@ -1,4 +1,4 @@
-use editor::{Editor, MultiBufferSnapshot, ToOffset, ToPoint, scroll::Autoscroll};
+use editor::{Editor, MultiBufferSnapshot, ToOffset, ToPoint};
use gpui::{Action, Context, Window};
use language::{Bias, Point};
use schemars::JsonSchema;
@@ -97,7 +97,7 @@ impl Vim {
editor.edit(edits, cx);
let snapshot = editor.buffer().read(cx).snapshot(cx);
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
let mut new_ranges = Vec::new();
for (visual, anchor) in new_anchors.iter() {
let mut point = anchor.to_point(&snapshot);
@@ -4,7 +4,6 @@ use editor::{
Anchor, Bias, DisplayPoint, Editor, MultiBuffer,
display_map::{DisplaySnapshot, ToDisplayPoint},
movement,
- scroll::Autoscroll,
};
use gpui::{Context, Entity, EntityId, UpdateGlobal, Window};
use language::SelectionGoal;
@@ -116,7 +115,7 @@ impl Vim {
}
}
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.select_anchor_ranges(ranges)
});
})
@@ -169,7 +168,7 @@ impl Vim {
}
})
.collect();
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.select_ranges(points.into_iter().map(|p| p..p))
})
})
@@ -251,7 +250,7 @@ impl Vim {
}
if !should_jump && !ranges.is_empty() {
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.select_anchor_ranges(ranges)
});
}
@@ -1,4 +1,4 @@
-use editor::{DisplayPoint, RowExt, display_map::ToDisplayPoint, movement, scroll::Autoscroll};
+use editor::{DisplayPoint, RowExt, SelectionEffects, display_map::ToDisplayPoint, movement};
use gpui::{Action, Context, Window};
use language::{Bias, SelectionGoal};
use schemars::JsonSchema;
@@ -187,7 +187,7 @@ impl Vim {
// and put the cursor on the first non-blank character of the first inserted line (or at the end if the first line is blank).
// otherwise vim will insert the next text at (or before) the current cursor position,
// the cursor will go to the last (or first, if is_multiline) inserted character.
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.replace_cursors_with(|map| {
let mut cursors = Vec::new();
for (anchor, line_mode, is_multiline) in &new_selections {
@@ -238,9 +238,9 @@ impl Vim {
self.update_editor(window, cx, |_, editor, window, cx| {
editor.transact(window, cx, |editor, window, cx| {
editor.set_clip_at_line_ends(false, cx);
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| {
- object.expand_selection(map, selection, around);
+ object.expand_selection(map, selection, around, None);
});
});
@@ -252,7 +252,7 @@ impl Vim {
};
editor.insert(&text, window, cx);
editor.set_clip_at_line_ends(true, cx);
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| {
selection.start = map.clip_point(selection.start, Bias::Left);
selection.end = selection.start
@@ -276,7 +276,7 @@ impl Vim {
let text_layout_details = editor.text_layout_details(window);
editor.transact(window, cx, |editor, window, cx| {
editor.set_clip_at_line_ends(false, cx);
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| {
motion.expand_selection(
map,
@@ -296,7 +296,7 @@ impl Vim {
};
editor.insert(&text, window, cx);
editor.set_clip_at_line_ends(true, cx);
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| {
selection.start = map.clip_point(selection.start, Bias::Left);
selection.end = selection.start
@@ -711,7 +711,7 @@ mod test {
);
cx.update_global(|store: &mut SettingsStore, cx| {
store.update_user_settings::<AllLanguageSettings>(cx, |settings| {
- settings.languages.insert(
+ settings.languages.0.insert(
LanguageName::new("Rust"),
LanguageSettingsContent {
auto_indent_on_paste: Some(false),
@@ -245,61 +245,63 @@ impl Vim {
}) else {
return;
};
- if let Some(mode) = mode {
- self.switch_mode(mode, false, window, cx)
- }
+ if mode != Some(self.mode) {
+ if let Some(mode) = mode {
+ self.switch_mode(mode, false, window, cx)
+ }
- match selection {
- RecordedSelection::SingleLine { cols } => {
- if cols > 1 {
- self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx)
+ match selection {
+ RecordedSelection::SingleLine { cols } => {
+ if cols > 1 {
+ self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx)
+ }
}
- }
- RecordedSelection::Visual { rows, cols } => {
- self.visual_motion(
- Motion::Down {
- display_lines: false,
- },
- Some(rows as usize),
- window,
- cx,
- );
- self.visual_motion(
- Motion::StartOfLine {
- display_lines: false,
- },
- None,
- window,
- cx,
- );
- if cols > 1 {
- self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx)
+ RecordedSelection::Visual { rows, cols } => {
+ self.visual_motion(
+ Motion::Down {
+ display_lines: false,
+ },
+ Some(rows as usize),
+ window,
+ cx,
+ );
+ self.visual_motion(
+ Motion::StartOfLine {
+ display_lines: false,
+ },
+ None,
+ window,
+ cx,
+ );
+ if cols > 1 {
+ self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx)
+ }
}
- }
- RecordedSelection::VisualBlock { rows, cols } => {
- self.visual_motion(
- Motion::Down {
- display_lines: false,
- },
- Some(rows as usize),
- window,
- cx,
- );
- if cols > 1 {
- self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx);
+ RecordedSelection::VisualBlock { rows, cols } => {
+ self.visual_motion(
+ Motion::Down {
+ display_lines: false,
+ },
+ Some(rows as usize),
+ window,
+ cx,
+ );
+ if cols > 1 {
+ self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx);
+ }
}
+ RecordedSelection::VisualLine { rows } => {
+ self.visual_motion(
+ Motion::Down {
+ display_lines: false,
+ },
+ Some(rows as usize),
+ window,
+ cx,
+ );
+ }
+ RecordedSelection::None => {}
}
- RecordedSelection::VisualLine { rows } => {
- self.visual_motion(
- Motion::Down {
- display_lines: false,
- },
- Some(rows as usize),
- window,
- cx,
- );
- }
- RecordedSelection::None => {}
}
// insert internally uses repeat to handle counts
@@ -1,4 +1,4 @@
-use editor::{Editor, movement};
+use editor::{Editor, SelectionEffects, movement};
use gpui::{Context, Window, actions};
use language::Point;
@@ -41,7 +41,7 @@ impl Vim {
editor.set_clip_at_line_ends(false, cx);
editor.transact(window, cx, |editor, window, cx| {
let text_layout_details = editor.text_layout_details(window);
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| {
if selection.start == selection.end {
Motion::Right.expand_selection(
@@ -1,6 +1,6 @@
use crate::{Vim, motion::Motion, object::Object};
use collections::HashMap;
-use editor::{Bias, display_map::ToDisplayPoint};
+use editor::{Bias, SelectionEffects, display_map::ToDisplayPoint};
use gpui::{Context, Window};
use language::SelectionGoal;
@@ -18,7 +18,7 @@ impl Vim {
let text_layout_details = editor.text_layout_details(window);
editor.transact(window, cx, |editor, window, cx| {
let mut selection_starts: HashMap<_, _> = Default::default();
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| {
let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
selection_starts.insert(selection.id, anchor);
@@ -32,7 +32,7 @@ impl Vim {
});
});
editor.toggle_comments(&Default::default(), window, cx);
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| {
let anchor = selection_starts.remove(&selection.id).unwrap();
selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
@@ -46,6 +46,7 @@ impl Vim {
&mut self,
object: Object,
around: bool,
+ times: Option<usize>,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -53,15 +54,15 @@ impl Vim {
self.update_editor(window, cx, |_, editor, window, cx| {
editor.transact(window, cx, |editor, window, cx| {
let mut original_positions: HashMap<_, _> = Default::default();
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| {
let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
original_positions.insert(selection.id, anchor);
- object.expand_selection(map, selection, around);
+ object.expand_selection(map, selection, around, times);
});
});
editor.toggle_comments(&Default::default(), window, cx);
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| {
let anchor = original_positions.remove(&selection.id).unwrap();
selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None);
@@ -7,7 +7,7 @@ use crate::{
state::{Mode, Register},
};
use collections::HashMap;
-use editor::{ClipboardSelection, Editor};
+use editor::{ClipboardSelection, Editor, SelectionEffects};
use gpui::Context;
use gpui::Window;
use language::Point;
@@ -31,7 +31,7 @@ impl Vim {
editor.set_clip_at_line_ends(false, cx);
let mut original_positions: HashMap<_, _> = Default::default();
let mut kind = None;
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| {
let original_position = (selection.head(), selection.goal);
kind = motion.expand_selection(
@@ -51,7 +51,7 @@ impl Vim {
});
let Some(kind) = kind else { return };
vim.yank_selections_content(editor, kind, window, cx);
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|_, selection| {
let (head, goal) = original_positions.remove(&selection.id).unwrap();
selection.collapse_to(head, goal);
@@ -66,6 +66,7 @@ impl Vim {
&mut self,
object: Object,
around: bool,
+ times: Option<usize>,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -73,15 +74,15 @@ impl Vim {
editor.transact(window, cx, |editor, window, cx| {
editor.set_clip_at_line_ends(false, cx);
let mut start_positions: HashMap<_, _> = Default::default();
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| {
- object.expand_selection(map, selection, around);
+ object.expand_selection(map, selection, around, times);
let start_position = (selection.start, selection.goal);
start_positions.insert(selection.id, start_position);
});
});
vim.yank_selections_content(editor, MotionKind::Exclusive, window, cx);
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|_, selection| {
let (head, goal) = start_positions.remove(&selection.id).unwrap();
selection.collapse_to(head, goal);
@@ -195,7 +196,7 @@ impl Vim {
}
clipboard_selections.push(ClipboardSelection {
len: text.len() - initial_len,
- is_entire_line: kind.linewise(),
+ is_entire_line: false,
first_line_indent: buffer.indent_size_for_line(MultiBufferRow(start.row)).len,
});
}
@@ -373,10 +373,12 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
impl Vim {
fn object(&mut self, object: Object, window: &mut Window, cx: &mut Context<Self>) {
+ let count = Self::take_count(cx);
+
match self.mode {
- Mode::Normal => self.normal_object(object, window, cx),
+ Mode::Normal => self.normal_object(object, count, window, cx),
Mode::Visual | Mode::VisualLine | Mode::VisualBlock => {
- self.visual_object(object, window, cx)
+ self.visual_object(object, count, window, cx)
}
Mode::Insert | Mode::Replace | Mode::HelixNormal => {
// Shouldn't execute a text object in insert mode. Ignoring
@@ -485,6 +487,7 @@ impl Object {
map: &DisplaySnapshot,
selection: Selection<DisplayPoint>,
around: bool,
+ times: Option<usize>,
) -> Option<Range<DisplayPoint>> {
let relative_to = selection.head();
match self {
@@ -503,7 +506,8 @@ impl Object {
}
}
Object::Sentence => sentence(map, relative_to, around),
- Object::Paragraph => paragraph(map, relative_to, around),
+ //change others later
+ Object::Paragraph => paragraph(map, relative_to, around, times.unwrap_or(1)),
Object::Quotes => {
surrounding_markers(map, relative_to, around, self.is_multiline(), '\'', '\'')
}
@@ -692,8 +696,9 @@ impl Object {
map: &DisplaySnapshot,
selection: &mut Selection<DisplayPoint>,
around: bool,
+ times: Option<usize>,
) -> bool {
- if let Some(range) = self.range(map, selection.clone(), around) {
+ if let Some(range) = self.range(map, selection.clone(), around, times) {
selection.start = range.start;
selection.end = range.end;
true
@@ -1399,30 +1404,37 @@ fn paragraph(
map: &DisplaySnapshot,
relative_to: DisplayPoint,
around: bool,
+ times: usize,
) -> Option<Range<DisplayPoint>> {
let mut paragraph_start = start_of_paragraph(map, relative_to);
let mut paragraph_end = end_of_paragraph(map, relative_to);
- let paragraph_end_row = paragraph_end.row();
- let paragraph_ends_with_eof = paragraph_end_row == map.max_point().row();
- let point = relative_to.to_point(map);
- let current_line_is_empty = map.buffer_snapshot.is_line_blank(MultiBufferRow(point.row));
+ for i in 0..times {
+ let paragraph_end_row = paragraph_end.row();
+ let paragraph_ends_with_eof = paragraph_end_row == map.max_point().row();
+ let point = relative_to.to_point(map);
+ let current_line_is_empty = map.buffer_snapshot.is_line_blank(MultiBufferRow(point.row));
- if around {
- if paragraph_ends_with_eof {
- if current_line_is_empty {
- return None;
- }
+ if around {
+ if paragraph_ends_with_eof {
+ if current_line_is_empty {
+ return None;
+ }
- let paragraph_start_row = paragraph_start.row();
- if paragraph_start_row.0 != 0 {
- let previous_paragraph_last_line_start =
- DisplayPoint::new(paragraph_start_row - 1, 0);
- paragraph_start = start_of_paragraph(map, previous_paragraph_last_line_start);
+ let paragraph_start_row = paragraph_start.row();
+ if paragraph_start_row.0 != 0 {
+ let previous_paragraph_last_line_start =
+ Point::new(paragraph_start_row.0 - 1, 0).to_display_point(map);
+ paragraph_start = start_of_paragraph(map, previous_paragraph_last_line_start);
+ }
+ } else {
+ let mut start_row = paragraph_end_row.0 + 1;
+ if i > 0 {
+ start_row += 1;
+ }
+ let next_paragraph_start = Point::new(start_row, 0).to_display_point(map);
+ paragraph_end = end_of_paragraph(map, next_paragraph_start);
}
- } else {
- let next_paragraph_start = DisplayPoint::new(paragraph_end_row + 1, 0);
- paragraph_end = end_of_paragraph(map, next_paragraph_start);
}
}
@@ -5,8 +5,8 @@ use crate::{
state::Mode,
};
use editor::{
- Anchor, Bias, Editor, EditorSnapshot, ToOffset, ToPoint, display_map::ToDisplayPoint,
- scroll::Autoscroll,
+ Anchor, Bias, Editor, EditorSnapshot, SelectionEffects, ToOffset, ToPoint,
+ display_map::ToDisplayPoint,
};
use gpui::{Context, Window, actions};
use language::{Point, SelectionGoal};
@@ -72,7 +72,7 @@ impl Vim {
editor.edit_with_block_indent(edits.clone(), Vec::new(), cx);
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_anchor_ranges(edits.iter().map(|(range, _)| range.end..range.end));
});
editor.set_clip_at_line_ends(true, cx);
@@ -124,7 +124,7 @@ impl Vim {
editor.edit(edits, cx);
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_ranges(new_selections);
});
editor.set_clip_at_line_ends(true, cx);
@@ -144,7 +144,7 @@ impl Vim {
editor.set_clip_at_line_ends(false, cx);
let mut selection = editor.selections.newest_display(cx);
let snapshot = editor.snapshot(window, cx);
- object.expand_selection(&snapshot, &mut selection, around);
+ object.expand_selection(&snapshot, &mut selection, around, None);
let start = snapshot
.buffer_snapshot
.anchor_before(selection.start.to_point(&snapshot));
@@ -251,7 +251,7 @@ impl Vim {
}
if let Some(position) = final_cursor_position {
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|_map, selection| {
selection.collapse_to(position, SelectionGoal::None);
});
@@ -1,6 +1,6 @@
use crate::{Vim, motion::Motion, object::Object, state::Mode};
use collections::HashMap;
-use editor::{Bias, Editor, RewrapOptions, display_map::ToDisplayPoint, scroll::Autoscroll};
+use editor::{Bias, Editor, RewrapOptions, SelectionEffects, display_map::ToDisplayPoint};
use gpui::{Context, Window, actions};
use language::SelectionGoal;
@@ -22,7 +22,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
},
cx,
);
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
if let Some(anchor) = positions.remove(&selection.id) {
let mut point = anchor.to_display_point(map);
@@ -53,7 +53,7 @@ impl Vim {
let text_layout_details = editor.text_layout_details(window);
editor.transact(window, cx, |editor, window, cx| {
let mut selection_starts: HashMap<_, _> = Default::default();
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| {
let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
selection_starts.insert(selection.id, anchor);
@@ -73,7 +73,7 @@ impl Vim {
},
cx,
);
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| {
let anchor = selection_starts.remove(&selection.id).unwrap();
let mut point = anchor.to_display_point(map);
@@ -89,6 +89,7 @@ impl Vim {
&mut self,
object: Object,
around: bool,
+ times: Option<usize>,
window: &mut Window,
cx: &mut Context<Self>,
) {
@@ -96,11 +97,11 @@ impl Vim {
self.update_editor(window, cx, |_, editor, window, cx| {
editor.transact(window, cx, |editor, window, cx| {
let mut original_positions: HashMap<_, _> = Default::default();
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| {
let anchor = map.display_point_to_anchor(selection.head(), Bias::Right);
original_positions.insert(selection.id, anchor);
- object.expand_selection(map, selection, around);
+ object.expand_selection(map, selection, around, times);
});
});
editor.rewrap_impl(
@@ -110,7 +111,7 @@ impl Vim {
},
cx,
);
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| {
let anchor = original_positions.remove(&selection.id).unwrap();
let mut point = anchor.to_display_point(map);
@@ -4,7 +4,7 @@ use crate::{
object::Object,
state::Mode,
};
-use editor::{Bias, movement, scroll::Autoscroll};
+use editor::{Bias, movement};
use gpui::{Context, Window};
use language::BracketPair;
@@ -52,7 +52,7 @@ impl Vim {
for selection in &display_selections {
let range = match &target {
SurroundsType::Object(object, around) => {
- object.range(&display_map, selection.clone(), *around)
+ object.range(&display_map, selection.clone(), *around, None)
}
SurroundsType::Motion(motion) => {
motion
@@ -109,7 +109,7 @@ impl Vim {
editor.edit(edits, cx);
editor.set_clip_at_line_ends(true, cx);
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
if mode == Mode::VisualBlock {
s.select_anchor_ranges(anchors.into_iter().take(1))
} else {
@@ -150,7 +150,9 @@ impl Vim {
for selection in &display_selections {
let start = selection.start.to_offset(&display_map, Bias::Left);
- if let Some(range) = pair_object.range(&display_map, selection.clone(), true) {
+ if let Some(range) =
+ pair_object.range(&display_map, selection.clone(), true, None)
+ {
// If the current parenthesis object is single-line,
// then we need to filter whether it is the current line or not
if !pair_object.is_multiline() {
@@ -207,7 +209,7 @@ impl Vim {
}
}
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.select_ranges(anchors);
});
edits.sort_by_key(|(range, _)| range.start);
@@ -247,7 +249,9 @@ impl Vim {
for selection in &selections {
let start = selection.start.to_offset(&display_map, Bias::Left);
- if let Some(range) = target.range(&display_map, selection.clone(), true) {
+ if let Some(range) =
+ target.range(&display_map, selection.clone(), true, None)
+ {
if !target.is_multiline() {
let is_same_row = selection.start.row() == range.start.row()
&& selection.end.row() == range.end.row();
@@ -317,7 +321,7 @@ impl Vim {
edits.sort_by_key(|(range, _)| range.start);
editor.edit(edits, cx);
editor.set_clip_at_line_ends(true, cx);
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.select_anchor_ranges(stable_anchors);
});
});
@@ -348,7 +352,9 @@ impl Vim {
for selection in &selections {
let start = selection.start.to_offset(&display_map, Bias::Left);
- if let Some(range) = object.range(&display_map, selection.clone(), true) {
+ if let Some(range) =
+ object.range(&display_map, selection.clone(), true, None)
+ {
// If the current parenthesis object is single-line,
// then we need to filter whether it is the current line or not
if object.is_multiline()
@@ -375,7 +381,7 @@ impl Vim {
anchors.push(start..start)
}
}
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.select_ranges(anchors);
});
editor.set_clip_at_line_ends(true, cx);
@@ -2031,3 +2031,82 @@ async fn test_delete_unmatched_brace(cx: &mut gpui::TestAppContext) {
.await
.assert_eq(" oth(wow)\n oth(wow)\n");
}
+
+#[gpui::test]
+async fn test_paragraph_multi_delete(cx: &mut gpui::TestAppContext) {
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+ cx.set_shared_state(indoc! {
+ "
+ Emacs is
+ Λa great
+
+ operating system
+
+ all it lacks
+ is a
+
+ decent text editor
+ "
+ })
+ .await;
+
+ cx.simulate_shared_keystrokes("2 d a p").await;
+ cx.shared_state().await.assert_eq(indoc! {
+ "
+ Λall it lacks
+ is a
+
+ decent text editor
+ "
+ });
+
+ cx.simulate_shared_keystrokes("d a p").await;
+ cx.shared_clipboard()
+ .await
+ .assert_eq("all it lacks\nis a\n\n");
+
+ //reset to initial state
+ cx.simulate_shared_keystrokes("2 u").await;
+
+ cx.simulate_shared_keystrokes("4 d a p").await;
+ cx.shared_state().await.assert_eq(indoc! {"Λ"});
+}
+
+#[gpui::test]
+async fn test_multi_cursor_replay(cx: &mut gpui::TestAppContext) {
+ let mut cx = VimTestContext::new(cx, true).await;
+ cx.set_state(
+ indoc! {
+ "
+ oΛne one one
+
+ two two two
+ "
+ },
+ Mode::Normal,
+ );
+
+ cx.simulate_keystrokes("3 g l s wow escape escape");
+ cx.assert_state(
+ indoc! {
+ "
+ woΛw wow wow
+
+ two two two
+ "
+ },
+ Mode::Normal,
+ );
+
+ cx.simulate_keystrokes("2 j 3 g l .");
+ cx.assert_state(
+ indoc! {
+ "
+ wow wow wow
+
+ woΛw woΛw woΛw
+ "
+ },
+ Mode::Normal,
+ );
+}
@@ -22,7 +22,8 @@ mod visual;
use anyhow::Result;
use collections::HashMap;
use editor::{
- Anchor, Bias, Editor, EditorEvent, EditorSettings, HideMouseCursorOrigin, ToPoint,
+ Anchor, Bias, Editor, EditorEvent, EditorSettings, HideMouseCursorOrigin, SelectionEffects,
+ ToPoint,
movement::{self, FindRange},
};
use gpui::{
@@ -963,7 +964,7 @@ impl Vim {
}
}
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
// we cheat with visual block mode and use multiple cursors.
// the cost of this cheat is we need to convert back to a single
// cursor whenever vim would.
@@ -1163,7 +1164,7 @@ impl Vim {
} else {
self.update_editor(window, cx, |_, editor, window, cx| {
editor.set_clip_at_line_ends(false, cx);
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|_, selection| {
selection.collapse_to(selection.start, selection.goal)
})
@@ -1438,27 +1439,29 @@ impl Vim {
Mode::VisualLine | Mode::VisualBlock | Mode::Visual => {
self.update_editor(window, cx, |vim, editor, window, cx| {
let original_mode = vim.undo_modes.get(transaction_id);
- editor.change_selections(None, window, cx, |s| match original_mode {
- Some(Mode::VisualLine) => {
- s.move_with(|map, selection| {
- selection.collapse_to(
- map.prev_line_boundary(selection.start.to_point(map)).1,
- SelectionGoal::None,
- )
- });
- }
- Some(Mode::VisualBlock) => {
- let mut first = s.first_anchor();
- first.collapse_to(first.start, first.goal);
- s.select_anchors(vec![first]);
- }
- _ => {
- s.move_with(|map, selection| {
- selection.collapse_to(
- map.clip_at_line_end(selection.start),
- selection.goal,
- );
- });
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ match original_mode {
+ Some(Mode::VisualLine) => {
+ s.move_with(|map, selection| {
+ selection.collapse_to(
+ map.prev_line_boundary(selection.start.to_point(map)).1,
+ SelectionGoal::None,
+ )
+ });
+ }
+ Some(Mode::VisualBlock) => {
+ let mut first = s.first_anchor();
+ first.collapse_to(first.start, first.goal);
+ s.select_anchors(vec![first]);
+ }
+ _ => {
+ s.move_with(|map, selection| {
+ selection.collapse_to(
+ map.clip_at_line_end(selection.start),
+ selection.goal,
+ );
+ });
+ }
}
});
});
@@ -1466,7 +1469,7 @@ impl Vim {
}
Mode::Normal => {
self.update_editor(window, cx, |_, editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| {
selection
.collapse_to(map.clip_at_line_end(selection.end), selection.goal)
@@ -2,10 +2,9 @@ use std::sync::Arc;
use collections::HashMap;
use editor::{
- Bias, DisplayPoint, Editor,
+ Bias, DisplayPoint, Editor, SelectionEffects,
display_map::{DisplaySnapshot, ToDisplayPoint},
movement,
- scroll::Autoscroll,
};
use gpui::{Context, Window, actions};
use language::{Point, Selection, SelectionGoal};
@@ -133,7 +132,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
vim.update_editor(window, cx, |_, editor, window, cx| {
editor.set_clip_at_line_ends(false, cx);
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
let map = s.display_map();
let ranges = ranges
.into_iter()
@@ -187,7 +186,7 @@ impl Vim {
motion.move_point(map, point, goal, times, &text_layout_details)
})
} else {
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let was_reversed = selection.reversed;
let mut current_head = selection.head();
@@ -259,7 +258,7 @@ impl Vim {
) -> Option<(DisplayPoint, SelectionGoal)>,
) {
let text_layout_details = editor.text_layout_details(window);
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
let map = &s.display_map();
let mut head = s.newest_anchor().head().to_display_point(map);
let mut tail = s.oldest_anchor().tail().to_display_point(map);
@@ -365,7 +364,13 @@ impl Vim {
})
}
- pub fn visual_object(&mut self, object: Object, window: &mut Window, cx: &mut Context<Vim>) {
+ pub fn visual_object(
+ &mut self,
+ object: Object,
+ count: Option<usize>,
+ window: &mut Window,
+ cx: &mut Context<Vim>,
+ ) {
if let Some(Operator::Object { around }) = self.active_operator() {
self.pop_operator(window, cx);
let current_mode = self.mode;
@@ -375,7 +380,7 @@ impl Vim {
}
self.update_editor(window, cx, |_, editor, window, cx| {
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let mut mut_selection = selection.clone();
@@ -391,7 +396,7 @@ impl Vim {
);
}
- if let Some(range) = object.range(map, mut_selection, around) {
+ if let Some(range) = object.range(map, mut_selection, around, count) {
if !range.is_empty() {
let expand_both_ways = object.always_expands_both_ways()
|| selection.is_empty()
@@ -403,7 +408,7 @@ impl Vim {
&& object.always_expands_both_ways()
{
if let Some(range) =
- object.range(map, selection.clone(), around)
+ object.range(map, selection.clone(), around, count)
{
selection.start = range.start;
selection.end = range.end;
@@ -454,7 +459,7 @@ impl Vim {
) {
self.update_editor(window, cx, |_, editor, window, cx| {
editor.split_selection_into_lines(&Default::default(), window, cx);
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.move_cursors_with(|map, cursor, _| {
(next_line_end(map, cursor, 1), SelectionGoal::None)
});
@@ -472,7 +477,7 @@ impl Vim {
) {
self.update_editor(window, cx, |_, editor, window, cx| {
editor.split_selection_into_lines(&Default::default(), window, cx);
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.move_cursors_with(|map, cursor, _| {
(
first_non_whitespace(map, false, cursor),
@@ -495,7 +500,7 @@ impl Vim {
pub fn other_end(&mut self, _: &OtherEnd, window: &mut Window, cx: &mut Context<Self>) {
self.update_editor(window, cx, |_, editor, window, cx| {
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|_, selection| {
selection.reversed = !selection.reversed;
});
@@ -511,7 +516,7 @@ impl Vim {
) {
let mode = self.mode;
self.update_editor(window, cx, |_, editor, window, cx| {
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|_, selection| {
selection.reversed = !selection.reversed;
});
@@ -530,7 +535,7 @@ impl Vim {
editor.selections.line_mode = false;
editor.transact(window, cx, |editor, window, cx| {
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
if line_mode {
let mut position = selection.head();
@@ -567,7 +572,7 @@ impl Vim {
vim.copy_selections_content(editor, kind, window, cx);
if line_mode && vim.mode != Mode::VisualBlock {
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let end = selection.end.to_point(map);
let start = selection.start.to_point(map);
@@ -587,7 +592,7 @@ impl Vim {
// Fixup cursor position after the deletion
editor.set_clip_at_line_ends(true, cx);
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.move_with(|map, selection| {
let mut cursor = selection.head().to_point(map);
@@ -613,7 +618,7 @@ impl Vim {
// For visual line mode, adjust selections to avoid yanking the next line when on \n
if line_mode && vim.mode != Mode::VisualBlock {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| {
let start = selection.start.to_point(map);
let end = selection.end.to_point(map);
@@ -634,7 +639,7 @@ impl Vim {
MotionKind::Exclusive
};
vim.yank_selections_content(editor, kind, window, cx);
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.move_with(|map, selection| {
if line_mode {
selection.start = start_of_line(map, false, selection.start);
@@ -687,7 +692,9 @@ impl Vim {
}
editor.edit(edits, cx);
- editor.change_selections(None, window, cx, |s| s.select_ranges(stable_anchors));
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges(stable_anchors)
+ });
});
});
self.switch_mode(Mode::Normal, false, window, cx);
@@ -799,7 +806,7 @@ impl Vim {
if direction == Direction::Prev {
std::mem::swap(&mut start_selection, &mut end_selection);
}
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.select_ranges([start_selection..end_selection]);
});
editor.set_collapse_matches(true);
@@ -1760,4 +1767,26 @@ mod test {
});
cx.shared_clipboard().await.assert_eq("quick\n");
}
+
+ #[gpui::test]
+ async fn test_v2ap(cx: &mut gpui::TestAppContext) {
+ let mut cx = NeovimBackedTestContext::new(cx).await;
+
+ cx.set_shared_state(indoc! {
+ "The
+ quicΛk
+
+ brown
+ fox"
+ })
+ .await;
+ cx.simulate_shared_keystrokes("v 2 a p").await;
+ cx.shared_state().await.assert_eq(indoc! {
+ "Β«The
+ quick
+
+ brown
+ fΛΒ»ox"
+ });
+ }
}
@@ -0,0 +1,14 @@
+{"Put":{"state":"Λfn a() { }\n\n\n\n\n\nfn b() { }\n\n\n\n\n\nfn b() { }"}}
+{"Key":"3"}
+{"Key":"}"}
+{"Get":{"state":"fn a() { }\n\n\n\n\n\nfn b() { }\n\n\n\n\n\nfn b() { Λ}","mode":"Normal"}}
+{"Key":"ctrl-o"}
+{"Get":{"state":"Λfn a() { }\n\n\n\n\n\nfn b() { }\n\n\n\n\n\nfn b() { }","mode":"Normal"}}
+{"Key":"ctrl-i"}
+{"Get":{"state":"fn a() { }\n\n\n\n\n\nfn b() { }\n\n\n\n\n\nfn b() { Λ}","mode":"Normal"}}
+{"Key":"1"}
+{"Key":"1"}
+{"Key":"k"}
+{"Get":{"state":"fn a() { }\nΛ\n\n\n\n\nfn b() { }\n\n\n\n\n\nfn b() { }","mode":"Normal"}}
+{"Key":"ctrl-o"}
+{"Get":{"state":"Λfn a() { }\n\n\n\n\n\nfn b() { }\n\n\n\n\n\nfn b() { }","mode":"Normal"}}
@@ -0,0 +1,18 @@
+{"Put":{"state":"Emacs is\nΛa great\n\noperating system\n\nall it lacks\nis a\n\ndecent text editor\n"}}
+{"Key":"2"}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"p"}
+{"Get":{"state":"Λall it lacks\nis a\n\ndecent text editor\n","mode":"Normal"}}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"p"}
+{"Get":{"state":"Λdecent text editor\n","mode":"Normal"}}
+{"ReadRegister":{"name":"\"","value":"all it lacks\nis a\n\n"}}
+{"Key":"2"}
+{"Key":"u"}
+{"Key":"4"}
+{"Key":"d"}
+{"Key":"a"}
+{"Key":"p"}
+{"Get":{"state":"Λ","mode":"Normal"}}
@@ -0,0 +1,6 @@
+{"Put":{"state":"The\nquicΛk\n\nbrown\nfox"}}
+{"Key":"v"}
+{"Key":"2"}
+{"Key":"a"}
+{"Key":"p"}
+{"Get":{"state":"Β«The\nquick\n\nbrown\nfΛΒ»ox","mode":"VisualLine"}}
@@ -7,9 +7,7 @@ use gpui::{App, AppContext, Context, Entity, Subscription, Task};
use http_client::{HttpClient, Method};
use language_model::{LlmApiToken, RefreshLlmTokenListener};
use web_search::{WebSearchProvider, WebSearchProviderId};
-use zed_llm_client::{
- CLIENT_SUPPORTS_EXA_WEB_SEARCH_PROVIDER_HEADER_NAME, WebSearchBody, WebSearchResponse,
-};
+use zed_llm_client::{EXPIRED_LLM_TOKEN_HEADER_NAME, WebSearchBody, WebSearchResponse};
pub struct CloudWebSearchProvider {
state: Entity<State>,
@@ -73,32 +71,50 @@ async fn perform_web_search(
llm_api_token: LlmApiToken,
body: WebSearchBody,
) -> Result<WebSearchResponse> {
+ const MAX_RETRIES: usize = 3;
+
let http_client = &client.http_client();
+ let mut retries_remaining = MAX_RETRIES;
+ let mut token = llm_api_token.acquire(&client).await?;
- let token = llm_api_token.acquire(&client).await?;
+ loop {
+ if retries_remaining == 0 {
+ return Err(anyhow::anyhow!(
+ "error performing web search, max retries exceeded"
+ ));
+ }
- let request = http_client::Request::builder()
- .method(Method::POST)
- .uri(http_client.build_zed_llm_url("/web_search", &[])?.as_ref())
- .header("Content-Type", "application/json")
- .header("Authorization", format!("Bearer {token}"))
- .header(CLIENT_SUPPORTS_EXA_WEB_SEARCH_PROVIDER_HEADER_NAME, "true")
- .body(serde_json::to_string(&body)?.into())?;
- let mut response = http_client
- .send(request)
- .await
- .context("failed to send web search request")?;
+ let request = http_client::Request::builder()
+ .method(Method::POST)
+ .uri(http_client.build_zed_llm_url("/web_search", &[])?.as_ref())
+ .header("Content-Type", "application/json")
+ .header("Authorization", format!("Bearer {token}"))
+ .body(serde_json::to_string(&body)?.into())?;
+ let mut response = http_client
+ .send(request)
+ .await
+ .context("failed to send web search request")?;
- if response.status().is_success() {
- let mut body = String::new();
- response.body_mut().read_to_string(&mut body).await?;
- return Ok(serde_json::from_str(&body)?);
- } else {
- let mut body = String::new();
- response.body_mut().read_to_string(&mut body).await?;
- anyhow::bail!(
- "error performing web search.\nStatus: {:?}\nBody: {body}",
- response.status(),
- );
+ if response.status().is_success() {
+ let mut body = String::new();
+ response.body_mut().read_to_string(&mut body).await?;
+ return Ok(serde_json::from_str(&body)?);
+ } else if response
+ .headers()
+ .get(EXPIRED_LLM_TOKEN_HEADER_NAME)
+ .is_some()
+ {
+ token = llm_api_token.refresh(&client).await?;
+ retries_remaining -= 1;
+ } else {
+ // For now we will only retry if the LLM token is expired,
+ // not if the request failed for any other reason.
+ let mut body = String::new();
+ response.body_mut().read_to_string(&mut body).await?;
+ anyhow::bail!(
+ "error performing web search.\nStatus: {:?}\nBody: {body}",
+ response.status(),
+ );
+ }
}
}
@@ -5,8 +5,8 @@ use theme::all_theme_colors;
use ui::{
AudioStatus, Avatar, AvatarAudioStatusIndicator, AvatarAvailabilityIndicator, ButtonLike,
Checkbox, CheckboxWithLabel, CollaboratorAvailability, ContentGroup, DecoratedIcon,
- ElevationIndex, Facepile, IconDecoration, Indicator, KeybindingHint, Switch, Table, TintColor,
- Tooltip, element_cell, prelude::*, string_cell, utils::calculate_contrast_ratio,
+ ElevationIndex, Facepile, IconDecoration, Indicator, KeybindingHint, Switch, TintColor,
+ Tooltip, prelude::*, utils::calculate_contrast_ratio,
};
use crate::{Item, Workspace};
@@ -288,6 +288,7 @@ actions!(
#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema, Action)]
#[action(namespace = file_finder, name = "Toggle")]
+#[serde(deny_unknown_fields)]
pub struct ToggleFileFinder {
#[serde(default)]
pub separate_history: bool,
@@ -3911,7 +3911,7 @@ impl BackgroundScanner {
let Ok(request) = path_prefix_request else { break };
log::trace!("adding path prefix {:?}", request.path);
- let did_scan = self.forcibly_load_paths(&[request.path.clone()]).await;
+ let did_scan = self.forcibly_load_paths(std::slice::from_ref(&request.path)).await;
if did_scan {
let abs_path =
{
@@ -85,6 +85,7 @@ libc.workspace = true
log.workspace = true
markdown.workspace = true
markdown_preview.workspace = true
+svg_preview.workspace = true
menu.workspace = true
migrator.workspace = true
mimalloc = { version = "0.1", optional = true }
@@ -582,6 +582,7 @@ pub fn main() {
jj_ui::init(cx);
feedback::init(cx);
markdown_preview::init(cx);
+ svg_preview::init(cx);
welcome::init(cx);
settings_ui::init(cx);
extensions_ui::init(cx);
@@ -18,7 +18,7 @@ use client::zed_urls;
use collections::VecDeque;
use debugger_ui::debugger_panel::DebugPanel;
use editor::ProposedChangesEditorToolbar;
-use editor::{Editor, MultiBuffer, scroll::Autoscroll};
+use editor::{Editor, MultiBuffer};
use futures::future::Either;
use futures::{StreamExt, channel::mpsc, select_biased};
use git_ui::git_panel::GitPanel;
@@ -30,7 +30,7 @@ use gpui::{
px, retain_all,
};
use image_viewer::ImageInfo;
-use language_tools::lsp_tool::LspTool;
+use language_tools::lsp_tool::{self, LspTool};
use migrate::{MigrationBanner, MigrationEvent, MigrationNotification, MigrationType};
use migrator::{migrate_keymap, migrate_settings};
pub use open_listener::*;
@@ -294,20 +294,18 @@ pub fn initialize_workspace(
show_software_emulation_warning_if_needed(specs, window, cx);
}
- let popover_menu_handle = PopoverMenuHandle::default();
-
+ let inline_completion_menu_handle = PopoverMenuHandle::default();
let edit_prediction_button = cx.new(|cx| {
inline_completion_button::InlineCompletionButton::new(
app_state.fs.clone(),
app_state.user_store.clone(),
- popover_menu_handle.clone(),
+ inline_completion_menu_handle.clone(),
cx,
)
});
-
workspace.register_action({
move |_, _: &inline_completion_button::ToggleMenu, window, cx| {
- popover_menu_handle.toggle(window, cx);
+ inline_completion_menu_handle.toggle(window, cx);
}
});
@@ -326,14 +324,22 @@ pub fn initialize_workspace(
cx.new(|cx| toolchain_selector::ActiveToolchain::new(workspace, window, cx));
let vim_mode_indicator = cx.new(|cx| vim::ModeIndicator::new(window, cx));
let image_info = cx.new(|_cx| ImageInfo::new(workspace));
- let lsp_tool = cx.new(|cx| LspTool::new(workspace, window, cx));
+
+ let lsp_tool_menu_handle = PopoverMenuHandle::default();
+ let lsp_tool =
+ cx.new(|cx| LspTool::new(workspace, lsp_tool_menu_handle.clone(), window, cx));
+ workspace.register_action({
+ move |_, _: &lsp_tool::ToggleMenu, window, cx| {
+ lsp_tool_menu_handle.toggle(window, cx);
+ }
+ });
let cursor_position =
cx.new(|_| go_to_line::cursor_position::CursorPosition::new(workspace));
workspace.status_bar().update(cx, |status_bar, cx| {
status_bar.add_left_item(search_button, window, cx);
- status_bar.add_left_item(diagnostic_summary, window, cx);
status_bar.add_left_item(lsp_tool, window, cx);
+ status_bar.add_left_item(diagnostic_summary, window, cx);
status_bar.add_left_item(activity_indicator, window, cx);
status_bar.add_right_item(edit_prediction_button, window, cx);
status_bar.add_right_item(active_buffer_language, window, cx);
@@ -1119,7 +1125,7 @@ fn open_log_file(workspace: &mut Workspace, window: &mut Window, cx: &mut Contex
editor.update(cx, |editor, cx| {
let last_multi_buffer_offset = editor.buffer().read(cx).len(cx);
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.select_ranges(Some(
last_multi_buffer_offset..last_multi_buffer_offset,
));
@@ -1423,6 +1429,8 @@ fn reload_keymaps(cx: &mut App, mut user_key_bindings: Vec<KeyBinding>) {
"New Window",
workspace::NewWindow,
)]);
+ // todo: nicer api here?
+ settings_ui::keybindings::KeymapEventChannel::trigger_keymap_changed(cx);
}
pub fn load_default_keymap(cx: &mut App) {
@@ -1768,7 +1776,7 @@ mod tests {
use super::*;
use assets::Assets;
use collections::HashSet;
- use editor::{DisplayPoint, Editor, display_map::DisplayRow, scroll::Autoscroll};
+ use editor::{DisplayPoint, Editor, SelectionEffects, display_map::DisplayRow};
use gpui::{
Action, AnyWindowHandle, App, AssetSource, BorrowAppContext, SemanticVersion,
TestAppContext, UpdateGlobal, VisualTestContext, WindowHandle, actions,
@@ -3342,7 +3350,7 @@ mod tests {
workspace
.update(cx, |_, window, cx| {
editor1.update(cx, |editor, cx| {
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.select_display_ranges([DisplayPoint::new(DisplayRow(10), 0)
..DisplayPoint::new(DisplayRow(10), 0)])
});
@@ -3372,7 +3380,7 @@ mod tests {
workspace
.update(cx, |_, window, cx| {
editor3.update(cx, |editor, cx| {
- editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
+ editor.change_selections(Default::default(), window, cx, |s| {
s.select_display_ranges([DisplayPoint::new(DisplayRow(12), 0)
..DisplayPoint::new(DisplayRow(12), 0)])
});
@@ -3587,7 +3595,7 @@ mod tests {
workspace
.update(cx, |_, window, cx| {
editor1.update(cx, |editor, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([DisplayPoint::new(DisplayRow(15), 0)
..DisplayPoint::new(DisplayRow(15), 0)])
})
@@ -3598,7 +3606,7 @@ mod tests {
workspace
.update(cx, |_, window, cx| {
editor1.update(cx, |editor, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([DisplayPoint::new(DisplayRow(3), 0)
..DisplayPoint::new(DisplayRow(3), 0)])
});
@@ -3609,7 +3617,7 @@ mod tests {
workspace
.update(cx, |_, window, cx| {
editor1.update(cx, |editor, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([DisplayPoint::new(DisplayRow(13), 0)
..DisplayPoint::new(DisplayRow(13), 0)])
})
@@ -3621,7 +3629,7 @@ mod tests {
.update(cx, |_, window, cx| {
editor1.update(cx, |editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([DisplayPoint::new(DisplayRow(2), 0)
..DisplayPoint::new(DisplayRow(14), 0)])
});
@@ -3634,7 +3642,7 @@ mod tests {
workspace
.update(cx, |_, window, cx| {
editor1.update(cx, |editor, cx| {
- editor.change_selections(None, window, cx, |s| {
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select_display_ranges([DisplayPoint::new(DisplayRow(1), 0)
..DisplayPoint::new(DisplayRow(1), 0)])
})
@@ -4323,6 +4331,7 @@ mod tests {
"search",
"snippets",
"supermaven",
+ "svg",
"tab_switcher",
"task",
"terminal",
@@ -1,5 +1,6 @@
-mod markdown_preview;
+mod preview;
mod repl_menu;
+
use agent_settings::AgentSettings;
use editor::actions::{
AddSelectionAbove, AddSelectionBelow, CodeActionSource, DuplicateLineDown, GoToDiagnostic,
@@ -571,7 +572,7 @@ impl Render for QuickActionBar {
.id("quick action bar")
.gap(DynamicSpacing::Base01.rems(cx))
.children(self.render_repl_menu(cx))
- .children(self.render_toggle_markdown_preview(self.workspace.clone(), cx))
+ .children(self.render_preview_button(self.workspace.clone(), cx))
.children(search_button)
.when(
AgentSettings::get_global(cx).enabled && AgentSettings::get_global(cx).button,
@@ -1,63 +0,0 @@
-use gpui::{AnyElement, Modifiers, WeakEntity};
-use markdown_preview::{
- OpenPreview, OpenPreviewToTheSide, markdown_preview_view::MarkdownPreviewView,
-};
-use ui::{IconButtonShape, Tooltip, prelude::*, text_for_keystroke};
-use workspace::Workspace;
-
-use super::QuickActionBar;
-
-impl QuickActionBar {
- pub fn render_toggle_markdown_preview(
- &self,
- workspace: WeakEntity<Workspace>,
- cx: &mut Context<Self>,
- ) -> Option<AnyElement> {
- let mut active_editor_is_markdown = false;
-
- if let Some(workspace) = self.workspace.upgrade() {
- workspace.update(cx, |workspace, cx| {
- active_editor_is_markdown =
- MarkdownPreviewView::resolve_active_item_as_markdown_editor(workspace, cx)
- .is_some();
- });
- }
-
- if !active_editor_is_markdown {
- return None;
- }
-
- let alt_click = gpui::Keystroke {
- key: "click".into(),
- modifiers: Modifiers::alt(),
- ..Default::default()
- };
-
- let button = IconButton::new("toggle-markdown-preview", IconName::Eye)
- .shape(IconButtonShape::Square)
- .icon_size(IconSize::Small)
- .style(ButtonStyle::Subtle)
- .tooltip(move |window, cx| {
- Tooltip::with_meta(
- "Preview Markdown",
- Some(&markdown_preview::OpenPreview),
- format!("{} to open in a split", text_for_keystroke(&alt_click, cx)),
- window,
- cx,
- )
- })
- .on_click(move |_, window, cx| {
- if let Some(workspace) = workspace.upgrade() {
- workspace.update(cx, |_, cx| {
- if window.modifiers().alt {
- window.dispatch_action(Box::new(OpenPreviewToTheSide), cx);
- } else {
- window.dispatch_action(Box::new(OpenPreview), cx);
- }
- });
- }
- });
-
- Some(button.into_any_element())
- }
-}
@@ -0,0 +1,94 @@
+use gpui::{AnyElement, Modifiers, WeakEntity};
+use markdown_preview::{
+ OpenPreview as MarkdownOpenPreview, OpenPreviewToTheSide as MarkdownOpenPreviewToTheSide,
+ markdown_preview_view::MarkdownPreviewView,
+};
+use svg_preview::{
+ OpenPreview as SvgOpenPreview, OpenPreviewToTheSide as SvgOpenPreviewToTheSide,
+ svg_preview_view::SvgPreviewView,
+};
+use ui::{Tooltip, prelude::*, text_for_keystroke};
+use workspace::Workspace;
+
+use super::QuickActionBar;
+
+#[derive(Clone, Copy)]
+enum PreviewType {
+ Markdown,
+ Svg,
+}
+
+impl QuickActionBar {
+ pub fn render_preview_button(
+ &self,
+ workspace_handle: WeakEntity<Workspace>,
+ cx: &mut Context<Self>,
+ ) -> Option<AnyElement> {
+ let mut preview_type = None;
+
+ if let Some(workspace) = self.workspace.upgrade() {
+ workspace.update(cx, |workspace, cx| {
+ if MarkdownPreviewView::resolve_active_item_as_markdown_editor(workspace, cx)
+ .is_some()
+ {
+ preview_type = Some(PreviewType::Markdown);
+ } else if SvgPreviewView::resolve_active_item_as_svg_editor(workspace, cx).is_some()
+ {
+ preview_type = Some(PreviewType::Svg);
+ }
+ });
+ }
+
+ let preview_type = preview_type?;
+
+ let (button_id, tooltip_text, open_action, open_to_side_action, open_action_for_tooltip) =
+ match preview_type {
+ PreviewType::Markdown => (
+ "toggle-markdown-preview",
+ "Preview Markdown",
+ Box::new(MarkdownOpenPreview) as Box<dyn gpui::Action>,
+ Box::new(MarkdownOpenPreviewToTheSide) as Box<dyn gpui::Action>,
+ &markdown_preview::OpenPreview as &dyn gpui::Action,
+ ),
+ PreviewType::Svg => (
+ "toggle-svg-preview",
+ "Preview SVG",
+ Box::new(SvgOpenPreview) as Box<dyn gpui::Action>,
+ Box::new(SvgOpenPreviewToTheSide) as Box<dyn gpui::Action>,
+ &svg_preview::OpenPreview as &dyn gpui::Action,
+ ),
+ };
+
+ let alt_click = gpui::Keystroke {
+ key: "click".into(),
+ modifiers: Modifiers::alt(),
+ ..Default::default()
+ };
+
+ let button = IconButton::new(button_id, IconName::Eye)
+ .icon_size(IconSize::Small)
+ .style(ButtonStyle::Subtle)
+ .tooltip(move |window, cx| {
+ Tooltip::with_meta(
+ tooltip_text,
+ Some(open_action_for_tooltip),
+ format!("{} to open in a split", text_for_keystroke(&alt_click, cx)),
+ window,
+ cx,
+ )
+ })
+ .on_click(move |_, window, cx| {
+ if let Some(workspace) = workspace_handle.upgrade() {
+ workspace.update(cx, |_, cx| {
+ if window.modifiers().alt {
+ window.dispatch_action(open_to_side_action.boxed_clone(), cx);
+ } else {
+ window.dispatch_action(open_action.boxed_clone(), cx);
+ }
+ });
+ }
+ });
+
+ Some(button.into_any_element())
+ }
+}
@@ -58,6 +58,7 @@ pub enum ExtensionCategoryFilter {
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
#[action(namespace = zed)]
+#[serde(deny_unknown_fields)]
pub struct Extensions {
/// Filters the extensions page down to extensions that are in the specified category.
#[serde(default)]
@@ -66,6 +67,7 @@ pub struct Extensions {
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
#[action(namespace = zed)]
+#[serde(deny_unknown_fields)]
pub struct DecreaseBufferFontSize {
#[serde(default)]
pub persist: bool,
@@ -73,6 +75,7 @@ pub struct DecreaseBufferFontSize {
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
#[action(namespace = zed)]
+#[serde(deny_unknown_fields)]
pub struct IncreaseBufferFontSize {
#[serde(default)]
pub persist: bool,
@@ -80,6 +83,7 @@ pub struct IncreaseBufferFontSize {
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
#[action(namespace = zed)]
+#[serde(deny_unknown_fields)]
pub struct ResetBufferFontSize {
#[serde(default)]
pub persist: bool,
@@ -87,6 +91,7 @@ pub struct ResetBufferFontSize {
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
#[action(namespace = zed)]
+#[serde(deny_unknown_fields)]
pub struct DecreaseUiFontSize {
#[serde(default)]
pub persist: bool,
@@ -94,6 +99,7 @@ pub struct DecreaseUiFontSize {
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
#[action(namespace = zed)]
+#[serde(deny_unknown_fields)]
pub struct IncreaseUiFontSize {
#[serde(default)]
pub persist: bool,
@@ -101,6 +107,7 @@ pub struct IncreaseUiFontSize {
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
#[action(namespace = zed)]
+#[serde(deny_unknown_fields)]
pub struct ResetUiFontSize {
#[serde(default)]
pub persist: bool,
@@ -40,13 +40,11 @@ You can connect them by adding their commands directly to your `settings.json`,
```json
{
"context_servers": {
- "some-context-server": {
+ "your-mcp-server": {
"source": "custom",
- "command": {
- "path": "some-command",
- "args": ["arg-1", "arg-2"],
- "env": {}
- }
+ "command": "some-command",
+ "args": ["arg-1", "arg-2"],
+ "env": {}
}
}
}
@@ -18,7 +18,7 @@ Zed supports a variety of debug adapters for different programming languages out
- Python ([debugpy](https://github.com/microsoft/debugpy.git)): Provides debugging capabilities for Python applications, supporting features like remote debugging, multi-threaded debugging, and Django/Flask application debugging.
-- LLDB ([CodeLLDB](https://github.com/vadimcn/codelldb.git)): A powerful debugger for Rust, C, C++, and some other compiled languages, offering low-level debugging features and support for Apple platforms. (For Swift, [see below](#swift).)
+- LLDB ([CodeLLDB](https://github.com/vadimcn/codelldb.git)): A powerful debugger for Rust, C, C++, and some other compiled languages, offering low-level debugging features and support for Apple platforms.
- GDB ([GDB](https://sourceware.org/gdb/)): The GNU Debugger, which supports debugging for multiple programming languages including C, C++, Go, and Rust, across various platforms.
@@ -214,6 +214,8 @@ requirements.txt
#### Rust/C++/C
+> For CodeLLDB, you might want to set `sourceLanguages` in your launch configuration based on the source code language.
+
##### Using pre-built binary
```json
@@ -222,7 +224,7 @@ requirements.txt
"label": "Debug native binary",
"program": "$ZED_WORKTREE_ROOT/build/binary",
"request": "launch",
- "adapter": "CodeLLDB" // GDB is available on non arm macs as well as linux
+ "adapter": "CodeLLDB" // GDB is available on non-ARM Macs as well as Linux
}
]
```
@@ -239,7 +241,23 @@ requirements.txt
},
"program": "$ZED_WORKTREE_ROOT/target/debug/binary",
"request": "launch",
- "adapter": "CodeLLDB" // GDB is available on non arm macs as well as linux
+ "adapter": "CodeLLDB" // GDB is available on non-ARM Macs as well as Linux
+ }
+]
+```
+
+##### Automatically locate a debug target based on build command
+
+```json
+[
+ {
+ "label": "Build & Debug native binary",
+ "adapter": "CodeLLDB" // GDB is available on non-ARM Macs as well as Linux
+ // Zed can infer the path to a debuggee based on the build command
+ "build": {
+ "command": "cargo",
+ "args": ["build"]
+ },
}
]
```
@@ -376,21 +394,6 @@ You might find yourself needing to connect to an existing instance of Delve that
In such case Zed won't spawn a new instance of Delve, as it opts to use an existing one. The consequence of this is that _there will be no terminal_ in Zed; you have to interact with the Delve instance directly, as it handles stdin/stdout of the debuggee.
-#### Swift
-
-Out-of-the-box support for debugging Swift programs will be provided by the Swift extension for Zed in the near future. In the meantime, the builtin CodeLLDB adapter can be used with some customization. On macOS, you'll need to locate the `lldb-dap` binary that's part of Apple's LLVM toolchain by running `which lldb-dap`, then point Zed to it in your project's `.zed/settings.json`:
-
-```json
-{
- "dap": {
- "CodeLLDB": {
- "binary": "/Applications/Xcode.app/Contents/Developer/usr/bin/lldb-dap", // example value, may vary between systems
- "args": []
- }
- }
-}
-```
-
#### Ruby
To run a ruby task in the debugger, you will need to configure it in the `.zed/debug.json` file in your project. We don't yet have automatic detection of ruby tasks, nor do we support connecting to an existing process.
@@ -16,15 +16,36 @@ Clone the [Zed repository](https://github.com/zed-industries/zed).
If preferred, you can inspect [`script/freebsd`](https://github.com/zed-industries/zed/blob/main/script/freebsd) and perform the steps manually.
----
+## Building from source
-### β οΈ WebRTC Notice
+Once the dependencies are installed, you can build Zed using [Cargo](https://doc.rust-lang.org/cargo/).
-Currently, building `webrtc-sys` on FreeBSD fails due to missing upstream support and unavailable prebuilt binaries.
-This is actively being worked on.
+For a debug build of the editor:
-More progress and discussion can be found in [Zedβs GitHub Discussions](https://github.com/zed-industries/zed/discussions/29550).
+```sh
+cargo run
+```
-_Environment:
-FreeBSD 14.2-RELEASE
-Architecture: amd64 (x86_64)_
+And to run the tests:
+
+```sh
+cargo test --workspace
+```
+
+In release mode, the primary user interface is the `cli` crate. You can run it in development with:
+
+```sh
+cargo run -p cli
+```
+
+### WebRTC Notice
+
+Currently, building `webrtc-sys` on FreeBSD fails due to missing upstream support and unavailable prebuilt binaries. As a result, some collaboration features (audio calls and screensharing) that depend on WebRTC are temporarily disabled.
+
+See [Issue #15309: FreeBSD Support] and [Discussion #29550: Unofficial FreeBSD port for Zed] for more.
+
+## Troubleshooting
+
+### Cargo errors claiming that a dependency is using unstable features
+
+Try `cargo clean` and `cargo build`.
@@ -1,9 +1,14 @@
# R
-R support is available through the [R extension](https://github.com/ocsmit/zed-r).
+R support is available via multiple R Zed extensions:
-- Tree-sitter: [r-lib/tree-sitter-r](https://github.com/r-lib/tree-sitter-r)
-- Language-Server: [REditorSupport/languageserver](https://github.com/REditorSupport/languageserver)
+- [ocsmit/zed-r](https://github.com/ocsmit/zed-r)
+
+ - Tree-sitter: [r-lib/tree-sitter-r](https://github.com/r-lib/tree-sitter-r)
+ - Language-Server: [REditorSupport/languageserver](https://github.com/REditorSupport/languageserver)
+
+- [posit-dev/air](https://github.com/posit-dev/air/tree/main/editors/zed)
+ - Language-Server: [posit-dev/air](https://github.com/posit-dev/air)
## Installation
@@ -15,7 +20,7 @@ install.packages("languageserver")
install.packages("lintr")
```
-3. Install the [R Zed extension](https://github.com/ocsmit/zed-r) through Zed's extensions manager.
+3. Install the [ocsmit/zed-r](https://github.com/ocsmit/zed-r) through Zed's extensions manager.
For example on macOS:
@@ -28,7 +33,70 @@ Rscript -e 'packageVersion("languageserver")'
Rscript -e 'packageVersion("lintr")'
```
-## Ark Installation
+## Configuration
+
+### Linting
+
+`REditorSupport/languageserver` bundles support for [r-lib/lintr](https://github.com/r-lib/lintr) as a linter. This can be configured via the use of a `.lintr` inside your project (or in your home directory for global defaults).
+
+```r
+linters: linters_with_defaults(
+ line_length_linter(120),
+ commented_code_linter = NULL
+ )
+exclusions: list(
+ "inst/doc/creating_linters.R" = 1,
+ "inst/example/bad.R",
+ "tests/testthat/exclusions-test"
+ )
+```
+
+Or exclude it from linting anything,
+
+```r
+exclusions: list(".")
+```
+
+See [Using lintr](https://lintr.r-lib.org/articles/lintr.html) for a complete list of options,
+
+### Formatting
+
+`REditorSupport/languageserver` bundles support for [r-lib/styler](https://github.com/r-lib/styler) as a formatter. See [Customizing Styler](https://cran.r-project.org/web/packages/styler/vignettes/customizing_styler.html) for more information on how to customize its behavior.
+
+<!--
+TBD: Get this working
+
+### REditorSupport/languageserver Configuration
+
+You can configure the [R languageserver settings](https://github.com/REditorSupport/languageserver#settings) via Zed Project Settings `.zed/settings.json` or Zed User Settings `~/.config/zed/settings.json`:
+
+For example to disable Lintr linting and suppress code snippet suggestions (both enabled by default):
+
+```json
+{
+ "lsp": {
+ "r_language_server": {
+ "settings": {
+ "r": {
+ "lsp": {
+ "diagnostics": false,
+ "snippet_support": false
+ }
+ }
+ }
+ }
+ }
+}
+```
+
+-->
+
+<!--
+TBD: R REPL Docs
+
+## REPL
+
+### Ark Installation
To use the Zed REPL with R you need to install [Ark](https://github.com/posit-dev/ark), an R Kernel for Jupyter applications.
You can down the latest version from the [Ark GitHub Releases](https://github.com/posit-dev/ark/releases) and then extract the `ark` binary to a directory in your `PATH`.
@@ -56,6 +124,4 @@ unzip ark-latest-linux.zip ark
sudo mv /tmp/ark /usr/local/bin/
```
-<!--
-TBD: R REPL Docs
-->
@@ -340,3 +340,41 @@ Plain minitest does not support running tests by line number, only by name, so w
```
Similar task syntax can be used for other test frameworks such as `quickdraw` or `tldr`.
+
+## Debugging
+
+The Ruby extension provides a debug adapter for debugging Ruby code. Zed's name for the adapter (in the UI and `debug.json`) is `rdbg`, and under the hood, it uses the [`debug`](https://github.com/ruby/debug) gem. The extension uses the [same activation logic](#language-server-activation) as the language servers.
+
+### Examples
+
+#### Debug a Ruby script
+
+```jsonc
+[
+ {
+ "label": "Debug current file",
+ "adapter": "rdbg",
+ "request": "launch",
+ "script": "$ZED_FILE",
+ "cwd": "$ZED_WORKTREE_ROOT",
+ },
+]
+```
+
+#### Debug Rails server
+
+```jsonc
+[
+ {
+ "label": "Debug Rails server",
+ "adapter": "rdbg",
+ "request": "launch",
+ "command": "$ZED_WORKTREE_ROOT/bin/rails",
+ "args": ["server"],
+ "cwd": "$ZED_WORKTREE_ROOT",
+ "env": {
+ "RUBY_DEBUG_OPEN": "true",
+ },
+ },
+]
+```
@@ -5,7 +5,34 @@ Report issues to: [https://github.com/zed-extensions/swift/issues](https://githu
- Tree-sitter: [alex-pinkus/tree-sitter-swift](https://github.com/alex-pinkus/tree-sitter-swift)
- Language Server: [swiftlang/sourcekit-lsp](https://github.com/swiftlang/sourcekit-lsp)
+- Debug Adapter: [`lldb-dap`](https://github.com/swiftlang/llvm-project/blob/next/lldb/tools/lldb-dap/README.md)
-## Configuration
+## Language Server Configuration
You can modify the behavior of SourceKit LSP by creating a `.sourcekit-lsp/config.json` under your home directory or in your project root. See [SourceKit-LSP configuration file](https://github.com/swiftlang/sourcekit-lsp/blob/main/Documentation/Configuration%20File.md) for complete documentation.
+
+## Debugging
+
+The Swift extension provides a debug adapter for debugging Swift code.
+Zed's name for the adapter (in the UI and `debug.json`) is `Swift`, and under the hood it uses [`lldb-dap`](https://github.com/swiftlang/llvm-project/blob/next/lldb/tools/lldb-dap/README.md), as provided by the Swift toolchain.
+The extension tries to find an `lldb-dap` binary using `swiftly`, using `xcrun`, and by searching `$PATH`, in that order of preference.
+The extension doesn't attempt to download `lldb-dap` if it's not found.
+
+### Examples
+
+#### Build and debug a Swift binary
+
+```json
+[
+ {
+ "label": "Debug Swift",
+ "build": {
+ "command": "swift",
+ "args": ["build"]
+ },
+ "program": "$ZED_WORKTREE_ROOT/swift-app/.build/arm64-apple-macosx/debug/swift-app",
+ "request": "launch",
+ "adapter": "Swift"
+ }
+]
+```
@@ -288,6 +288,7 @@ These ex commands open Zed's various panels and windows.
| Open the chat panel | `:Ch[at]` |
| Open the AI panel | `:A[I]` |
| Open the git panel | `:G[it]` |
+| Open the debug panel | `:D[ebug]` |
| Open the notifications panel | `:No[tif]` |
| Open the feedback window | `:fe[edback]` |
| Open the diagnostics window | `:cl[ist]` |
@@ -6,8 +6,6 @@ a build of Zed on Windows, and you can compile it yourself with these instructio
- [Building for Windows](./development/windows.md)
-We are currently hiring a [Windows Lead](https://zed.dev/jobs/windows-lead).
-
For now, we welcome contributions from the community to improve Windows support.
- [GitHub Issues with 'Windows' label](https://github.com/zed-industries/zed/issues?q=is%3Aissue+is%3Aopen+label%3Awindows)
@@ -79,20 +79,34 @@ h6 code {
display: none !important;
}
+h1 {
+ font-size: 3.4rem;
+}
+
h2 {
padding-bottom: 1rem;
border-bottom: 1px solid;
border-color: var(--border-light);
}
-h2,
h3 {
- margin-block-start: 1.5em;
- margin-block-end: 0;
+ font-size: 2rem;
}
+
+h4 {
+ font-size: 1.8rem;
+}
+
+h5 {
+ font-size: 1.6rem;
+}
+
+h2,
+h3,
h4,
h5 {
- margin-block-start: 2em;
+ margin-block-start: 1.5em;
+ margin-block-end: 0;
}
.header + .header h3,
@@ -1,7 +1,7 @@
id = "emmet"
name = "Emmet"
description = "Emmet support"
-version = "0.0.3"
+version = "0.0.4"
schema_version = 1
authors = ["Piotr Osiewicz <piotr@zed.dev>"]
repository = "https://github.com/zed-industries/zed"
@@ -9,7 +9,7 @@ repository = "https://github.com/zed-industries/zed"
[language_servers.emmet-language-server]
name = "Emmet Language Server"
language = "HTML"
-languages = ["HTML", "PHP", "ERB", "JavaScript", "TSX", "CSS"]
+languages = ["HTML", "PHP", "ERB", "JavaScript", "TSX", "CSS", "HEEX", "Elixir"]
[language_servers.emmet-language-server.language_ids]
"HTML" = "html"
@@ -18,3 +18,5 @@ languages = ["HTML", "PHP", "ERB", "JavaScript", "TSX", "CSS"]
"JavaScript" = "javascriptreact"
"TSX" = "typescriptreact"
"CSS" = "css"
+"HEEX" = "heex"
+"Elixir" = "heex"
@@ -2,11 +2,11 @@
"nodes": {
"crane": {
"locked": {
- "lastModified": 1748047550,
- "narHash": "sha256-t0qLLqb4C1rdtiY8IFRH5KIapTY/n3Lqt57AmxEv9mk=",
+ "lastModified": 1750266157,
+ "narHash": "sha256-tL42YoNg9y30u7zAqtoGDNdTyXTi8EALDeCB13FtbQA=",
"owner": "ipetkov",
"repo": "crane",
- "rev": "b718a78696060df6280196a6f992d04c87a16aef",
+ "rev": "e37c943371b73ed87faf33f7583860f81f1d5a48",
"type": "github"
},
"original": {
@@ -33,10 +33,10 @@
"nixpkgs": {
"locked": {
"lastModified": 315532800,
- "narHash": "sha256-3c6Axl3SGIXCixGtpSJaMXLkkSRihHDlLaGewDEgha0=",
- "rev": "3108eaa516ae22c2360928589731a4f1581526ef",
+ "narHash": "sha256-j+zO+IHQ7VwEam0pjPExdbLT2rVioyVS3iq4bLO3GEc=",
+ "rev": "61c0f513911459945e2cb8bf333dc849f1b976ff",
"type": "tarball",
- "url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre806109.3108eaa516ae/nixexprs.tar.xz"
+ "url": "https://releases.nixos.org/nixpkgs/nixpkgs-25.11pre821324.61c0f5139114/nixexprs.tar.xz"
},
"original": {
"type": "tarball",
@@ -58,11 +58,11 @@
]
},
"locked": {
- "lastModified": 1748227081,
- "narHash": "sha256-RLnN7LBxhEdCJ6+rIL9sbhjBVDaR6jG377M/CLP/fmE=",
+ "lastModified": 1750964660,
+ "narHash": "sha256-YQ6EyFetjH1uy5JhdhRdPe6cuNXlYpMAQePFfZj4W7M=",
"owner": "oxalica",
"repo": "rust-overlay",
- "rev": "1cbe817fd8c64a9f77ba4d7861a4839b0b15983e",
+ "rev": "04f0fcfb1a50c63529805a798b4b5c21610ff390",
"type": "github"
},
"original": {
@@ -1,5 +1,5 @@
[toolchain]
-channel = "1.87"
+channel = "1.88"
profile = "minimal"
components = [ "rustfmt", "clippy" ]
targets = [
@@ -27,7 +27,7 @@ elif [[ "$LICENSE_FLAG" == *"agpl"* ]]; then
LICENSE_FILE="LICENSE-AGPL"
else
LICENSE_MODE="GPL-3.0-or-later"
- LICENSE_FILE="LICENSE"
+ LICENSE_FILE="LICENSE-GPL"
fi
if [[ ! "$CRATE_NAME" =~ ^[a-z0-9_]+$ ]]; then
@@ -39,7 +39,7 @@ CRATE_PATH="crates/$CRATE_NAME"
mkdir -p "$CRATE_PATH/src"
# Symlink the license
-ln -sf "../../../$LICENSE_FILE" "$CRATE_PATH/LICENSE"
+ln -sf "../../../$LICENSE_FILE" "$CRATE_PATH/$LICENSE_FILE"
CARGO_TOML_TEMPLATE=$(cat << 'EOF'
[package]
@@ -111,7 +111,7 @@ sea-orm = { version = "1", features = ["runtime-tokio-rustls", "sqlx-postgres",
sea-query-binder = { version = "0.7", default-features = false, features = ["postgres-array", "sqlx-postgres", "sqlx-sqlite", "with-bigdecimal", "with-chrono", "with-json", "with-rust_decimal", "with-time", "with-uuid"] }
semver = { version = "1", features = ["serde"] }
serde = { version = "1", features = ["alloc", "derive", "rc"] }
-serde_json = { version = "1", features = ["preserve_order", "raw_value", "unbounded_depth"] }
+serde_json = { version = "1", features = ["alloc", "preserve_order", "raw_value", "unbounded_depth"] }
sha1 = { version = "0.10", features = ["compress"] }
simd-adler32 = { version = "0.3" }
smallvec = { version = "1", default-features = false, features = ["const_new", "serde", "union", "write"] }
@@ -244,7 +244,7 @@ sea-query-binder = { version = "0.7", default-features = false, features = ["pos
semver = { version = "1", features = ["serde"] }
serde = { version = "1", features = ["alloc", "derive", "rc"] }
serde_derive = { version = "1", features = ["deserialize_in_place"] }
-serde_json = { version = "1", features = ["preserve_order", "raw_value", "unbounded_depth"] }
+serde_json = { version = "1", features = ["alloc", "preserve_order", "raw_value", "unbounded_depth"] }
sha1 = { version = "0.10", features = ["compress"] }
simd-adler32 = { version = "0.3" }
smallvec = { version = "1", default-features = false, features = ["const_new", "serde", "union", "write"] }