diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b6e014d25900931a02926ec69d64f211590c99e..39036ef5649e699ffda1636f304629fce6184371 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/Cargo.lock b/Cargo.lock index 51e5e9371213b9815f58e5f1efe0c9170dacb57b..42c2e2559e6669178a09c47f8ad6c36e0d61ba73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 45ed0e1dbe83f12193b1c36440d18969a399fe81..52ec99e969f4bc3ee11d234aea461bfa3add88b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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] diff --git a/Dockerfile-collab b/Dockerfile-collab index 48854af4dad4b1d19f8060582f19f187a8112b97..2dafe296c7c8bb46c758d6c5f67ce6feed055d2b 100644 --- a/Dockerfile-collab +++ b/Dockerfile-collab @@ -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 . . diff --git a/assets/icons/arrow_down10.svg b/assets/icons/arrow_down10.svg new file mode 100644 index 0000000000000000000000000000000000000000..97ce967a8b03311dfe9df75da6ee4b26e44ba72a --- /dev/null +++ b/assets/icons/arrow_down10.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/bolt_filled_alt.svg b/assets/icons/bolt_filled_alt.svg new file mode 100644 index 0000000000000000000000000000000000000000..3c8938736279684981b03d168b11272d4e196d24 --- /dev/null +++ b/assets/icons/bolt_filled_alt.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/lsp_debug.svg b/assets/icons/lsp_debug.svg new file mode 100644 index 0000000000000000000000000000000000000000..aa49fcb6a214de6c5361d641d8236fbe4a0f6fc0 --- /dev/null +++ b/assets/icons/lsp_debug.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/lsp_restart.svg b/assets/icons/lsp_restart.svg new file mode 100644 index 0000000000000000000000000000000000000000..dfc68e7a9ea1b431a68e82d138d404fa656c3190 --- /dev/null +++ b/assets/icons/lsp_restart.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/lsp_stop.svg b/assets/icons/lsp_stop.svg new file mode 100644 index 0000000000000000000000000000000000000000..c6311d215582d38e865d6fbc56ac01c3d27fc28d --- /dev/null +++ b/assets/icons/lsp_stop.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/scroll_text.svg b/assets/icons/scroll_text.svg new file mode 100644 index 0000000000000000000000000000000000000000..f066c8a84e71ad209e2ebb6b9f7404182ee63552 --- /dev/null +++ b/assets/icons/scroll_text.svg @@ -0,0 +1 @@ + diff --git a/assets/icons/split_alt.svg b/assets/icons/split_alt.svg new file mode 100644 index 0000000000000000000000000000000000000000..3f7622701de82f4e960b3608575d59f83aca44ea --- /dev/null +++ b/assets/icons/split_alt.svg @@ -0,0 +1 @@ + diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index e21005816b84aca4e0c8e8c4e4bea2cb2003d4a6..c88df28b99d2408c4b2f99ef98928bac4cb266c8 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -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" + } } ] diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 51f4ffe23f255f31becbec25e15d20955ec058f9..7d6ce6e80a981cd8606c7b4e5f2b51d35205c41c 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -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" + } } ] diff --git a/assets/keymaps/linux/emacs.json b/assets/keymaps/linux/emacs.json index d1453da4850226d9168410f55c0743b17a16ed1f..26482f66f5054235022db8ebd748bd9b94aac799 100755 --- a/assets/keymaps/linux/emacs.json +++ b/assets/keymaps/linux/emacs.json @@ -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 } }, { diff --git a/assets/keymaps/macos/emacs.json b/assets/keymaps/macos/emacs.json index d1453da4850226d9168410f55c0743b17a16ed1f..26482f66f5054235022db8ebd748bd9b94aac799 100755 --- a/assets/keymaps/macos/emacs.json +++ b/assets/keymaps/macos/emacs.json @@ -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 } }, { diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 6b95839e2aecf404b0fcbc7d5267e863b2a2bc29..59b21ae3457eba5d3d9338ccf340e21e70a26050 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -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" } }, { diff --git a/crates/agent/src/agent_profile.rs b/crates/agent/src/agent_profile.rs index 07030c744fc085914ed5d085afd3699482fc6739..2c3b457dc2eef593085bb63ccb42fd70082163b3 100644 --- a/crates/agent/src/agent_profile.rs +++ b/crates/agent/src/agent_profile.rs @@ -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), } } } diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 68624d7c3b9d304c5b27c79f1bcdb072117b7dcd..028dabbd912ab1e58273bea6302d77baf4e635b8 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -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::() + } else if let Some(completion_error) = + error.downcast_ref::() { - 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, intent: CompletionIntent, @@ -2091,9 +2053,10 @@ impl Thread { cx: &mut Context, ) { // 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, intent: CompletionIntent, window: Option, cx: &mut Context, ) -> 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, model: Arc, 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()) diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index 294d793e79ec534e2318f03db5fbc9a75821ecc0..f3087765de072f2043fb7f87fd8369a2eab39d25 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -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, pub inline_assistant_model: Option, pub commit_message_model: Option, pub thread_summary_model: Option, @@ -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, @@ -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() diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index 4da959d36e9f77ba06e71d722400a60d9f5be25b..fa6d3144b519aa823171a9e86971d5ed96cf7b06 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -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, @@ -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::() .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, ) -> Stateful
{ - 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( diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index e91a0f7ebe590d1f0480741f6eec3ebda220ccea..4aa9c3fc38b2555e2675d668aa534d9c0125da7e 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -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, diff --git a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs index 30fad51cfcbc100bdf469278c0210a220c7e2833..299f3cee34b1c7635c3c0a8f46a52cc730993b01 100644 --- a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs +++ b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs @@ -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 { diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index b8e67512e2b069f2a4f19c4903512f385c4eeab7..1a0f3ff27d83a98d343985b3f827aab26afd192a 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -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)]) }); }); diff --git a/crates/agent_ui/src/agent_model_selector.rs b/crates/agent_ui/src/agent_model_selector.rs index c8b628c938e5be37ee3e10ca2a46dd2e59c78e84..f7b9157bbb9c07abac6a80dddfc014443165a712 100644 --- a/crates/agent_ui/src/agent_model_selector.rs +++ b/crates/agent_ui/src/agent_model_selector.rs @@ -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, @@ -94,20 +94,35 @@ impl Render for AgentModelSelector { fn render(&mut self, window: &mut Window, cx: &mut Context) -> 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", diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 938c1771bad1bb6d6ac56f2cf41d77aa5a8de308..d07521f3ee0fbe2c5310e4a232b6213039bed6bd 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -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) -> 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, + cx: &mut Context, + ) -> 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, + cx: &mut Context, + ) -> 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) -> Hsla { + cx.theme().status().error.opacity(0.08) + } + fn render_payment_required_error( &self, thread: &Entity, cx: &mut Context, ) -> 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, ) -> 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, ) -> 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) -> 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() diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 62f1eb7bf6294f58cb48a3368b7df05cd0804a6c..55a1cba94f436de386ba9621d0c359fd1523c781 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -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, @@ -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, @@ -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::>(); 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); diff --git a/crates/agent_ui/src/buffer_codegen.rs b/crates/agent_ui/src/buffer_codegen.rs index f3919a958f8cdcc7f1114406350de4cec5afd77a..117dcf4f8e17bc99c4bd6ed75af070d84e5b1015 100644 --- a/crates/agent_ui/src/buffer_codegen.rs +++ b/crates/agent_ui/src/buffer_codegen.rs @@ -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); diff --git a/crates/agent_ui/src/max_mode_tooltip.rs b/crates/agent_ui/src/burn_mode_tooltip.rs similarity index 95% rename from crates/agent_ui/src/max_mode_tooltip.rs rename to crates/agent_ui/src/burn_mode_tooltip.rs index a3100d4367c7a7b43edc3cc2b9b3a821941074e6..6354c07760f5aa0261b69e8dd08ce1f1b1be6023 100644 --- a/crates/agent_ui/src/max_mode_tooltip.rs +++ b/crates/agent_ui/src/burn_mode_tooltip.rs @@ -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) -> impl IntoElement { let (icon, color) = if self.selected { (IconName::ZedBurnModeOn, Color::Error) diff --git a/crates/agent_ui/src/context_picker.rs b/crates/agent_ui/src/context_picker.rs index b0069a2446bdce30968518d2af7f6d60ab0ad59e..f303f34a52856a068f1d2da33cf1f0a4fb5813a5 100644 --- a/crates/agent_ui/src/context_picker.rs +++ b/crates/agent_ui/src/context_picker.rs @@ -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, diff --git a/crates/agent_ui/src/inline_assistant.rs b/crates/agent_ui/src/inline_assistant.rs index 6e77e764a5ed172f0948d7d76f476377cafd04b7..c9c173a68be5191e77690e826378ca52d3db9684 100644 --- a/crates/agent_ui/src/inline_assistant.rs +++ b/crates/agent_ui/src/inline_assistant.rs @@ -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]) }); diff --git a/crates/agent_ui/src/language_model_selector.rs b/crates/agent_ui/src/language_model_selector.rs index d9d11231edbe128fcfd9a486278ed8e1542b4397..55c0974fc1d2bdbd65e0b6d746abf7f4ef10654d 100644 --- a/crates/agent_ui/src/language_model_selector.rs +++ b/crates/agent_ui/src/language_model_selector.rs @@ -399,7 +399,7 @@ impl PickerDelegate for LanguageModelPickerDelegate { cx: &mut Context>, ) -> 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(); }) diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index a5b7537da0a78e271b5eab3df23cd576576c1632..d8d97853dc94bbcceffe9e956842931123e2f37f 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -576,7 +576,7 @@ impl MessageEditor { fn render_burn_mode_toggle(&self, cx: &mut Context) -> Option { 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) -> Option
{ diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index 0a1013a6f29f86ede4580565d2f5df67ac9e263d..d11deb790820ba18a7437ac50ed3d5b2e8d4c9c0 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -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::(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) -> Option { + fn render_burn_mode_toggle(&self, cx: &mut Context) -> Option { 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>, - model_summary_editor: Entity, -} - -impl ContextEditorToolbarItem {} - pub fn render_remaining_tokens( context_editor: &Entity, 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) -> 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, - ) -> ToolbarItemLocation { - self.active_context_editor = active_pane_item - .and_then(|item| item.act_as::(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, - ) { - cx.notify(); - } -} - -impl EventEmitter for ContextEditorToolbarItem {} - -pub enum ContextEditorToolbarItemEvent { - RegenerateSummary, -} -impl EventEmitter 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); diff --git a/crates/agent_ui/src/ui.rs b/crates/agent_ui/src/ui.rs index c9adc2a63177bfad71203f37f79a140639605702..c076d113b8946c8bc9d85dd89672f3417f4bc15a 100644 --- a/crates/agent_ui/src/ui.rs +++ b/crates/agent_ui/src/ui.rs @@ -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::*; diff --git a/crates/agent_ui/src/ui/max_mode_tooltip.rs b/crates/agent_ui/src/ui/burn_mode_tooltip.rs similarity index 100% rename from crates/agent_ui/src/ui/max_mode_tooltip.rs rename to crates/agent_ui/src/ui/burn_mode_tooltip.rs diff --git a/crates/anthropic/src/anthropic.rs b/crates/anthropic/src/anthropic.rs index 7f0ab7550d8df12ea08f0bd955e83aa72d25e6b3..c73f6060458783f22b4d846dbe6d4a619d7e791c 100644 --- a/crates/anthropic/src/anthropic.rs +++ b/crates/anthropic/src/anthropic.rs @@ -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::().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) -> Option { + headers + .get("retry-after") + .and_then(|v| v.to_str().ok()) + .and_then(|v| v.parse::().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::(&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 }, + /// API returned an error response ApiError(ApiError), - - /// Unexpected response format - UnexpectedResponseFormat(String), } #[derive(Debug, Serialize, Deserialize, Error)] diff --git a/crates/assistant_context/src/assistant_context.rs b/crates/assistant_context/src/assistant_context.rs index cef9d2f0fd60c842883fcff80766416ca3db66de..aaaef152503e477c0bff4e8036c6460d6e9fde46 100644 --- a/crates/assistant_context/src/assistant_context.rs +++ b/crates/assistant_context/src/assistant_context.rs @@ -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 diff --git a/crates/assistant_slash_commands/src/delta_command.rs b/crates/assistant_slash_commands/src/delta_command.rs index 047d2899082891ad5e1cfc5e8ec9188dd1aa4e4f..8c840c17b2c7fe9d8c8995b21c35cb35980dd71b 100644 --- a/crates/assistant_slash_commands/src/delta_command.rs +++ b/crates/assistant_slash_commands/src/delta_command.rs @@ -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(), diff --git a/crates/assistant_tools/src/edit_agent/evals.rs b/crates/assistant_tools/src/edit_agent/evals.rs index 7beb2ec9190c4e6e65ed7d48211328dc51073ea4..8df8f677f20861c2cd5834bdcec6ac3ba414cdb0 100644 --- a/crates/assistant_tools/src/edit_agent/evals.rs +++ b/crates/assistant_tools/src/edit_agent/evals.rs @@ -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(mut request: impl AsyncFnMut() -> Result) -> match request().await { Ok(result) => return Ok(result), Err(err) => match err.downcast::() { - 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; diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index fde697b00eb2177bf3ac0382fd7d0c78daa0907e..8c7728b4b72c9aa52c717e58fbdd63591dd88f0f 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -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() } } diff --git a/crates/assistant_tools/src/schema.rs b/crates/assistant_tools/src/schema.rs index 4a71d47d2cdba4d92711c9dd4549d036df58b0ad..888e11de4e83df853d5d1c252d30cecf84c701a2 100644 --- a/crates/assistant_tools/src/schema.rs +++ b/crates/assistant_tools/src/schema.rs @@ -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( @@ -13,7 +14,7 @@ pub fn json_schema_for( } fn schema_to_json( - schema: &RootSchema, + schema: &Schema, format: LanguageModelToolSchemaFormat, ) -> Result { let mut value = serde_json::to_value(schema)?; @@ -21,58 +22,42 @@ fn schema_to_json( Ok(value) } -fn root_schema_for(format: LanguageModelToolSchemaFormat) -> RootSchema { +fn root_schema_for(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::() } #[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, 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); } } diff --git a/crates/assistant_tools/src/terminal_tool.rs b/crates/assistant_tools/src/terminal_tool.rs index 5ec0ce7b8f0a4f5f5a34efd01069cd0487104b58..2c582a531069eb9a81340af7eb07731e8df8a96e 100644 --- a/crates/assistant_tools/src/terminal_tool.rs +++ b/crates/assistant_tools/src/terminal_tool.rs @@ -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() } } diff --git a/crates/auto_update_ui/src/auto_update_ui.rs b/crates/auto_update_ui/src/auto_update_ui.rs index 30c1cddec2935d82f2ecc9fe0cfc569999d80d7b..afb135bc974f56d04db93e2a902fe48a64ab8ea7 100644 --- a/crates/auto_update_ui/src/auto_update_ui.rs +++ b/crates/auto_update_ui/src/auto_update_ui.rs @@ -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, ); diff --git a/crates/bedrock/Cargo.toml b/crates/bedrock/Cargo.toml index 84fd58460185eafef2196aabf15380b2abbbe390..3000af50bb71be18784a8e6a8f6da0ca8a66d7f9 100644 --- a/crates/bedrock/Cargo.toml +++ b/crates/bedrock/Cargo.toml @@ -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 diff --git a/crates/bedrock/src/bedrock.rs b/crates/bedrock/src/bedrock.rs index e32a456dbae184c41c1d4268db05af08a9fc6f4d..1c6a9bd0a1e745da1dd4577741fc7cb4cab771ad 100644 --- a/crates/bedrock/src/bedrock.rs +++ b/crates/bedrock/src/bedrock.rs @@ -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>, 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> - + 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 { diff --git a/crates/buffer_diff/src/buffer_diff.rs b/crates/buffer_diff/src/buffer_diff.rs index 3812d48bf7a064017b654ea82cdf3e19a7810fb2..ee09fda46e008c903120eb0430ff18fae57dc3da 100644 --- a/crates/buffer_diff/src/buffer_diff.rs +++ b/crates/buffer_diff/src/buffer_diff.rs @@ -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); diff --git a/crates/call/src/call_settings.rs b/crates/call/src/call_settings.rs index dd6999a17090bc970fe46cb49acc13c3e16cd57c..c8f51e0c1a2019dd2c266210e469989946ed8a35 100644 --- a/crates/call/src/call_settings.rs +++ b/crates/call/src/call_settings.rs @@ -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. /// diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index a4899f408d9d60d8222e558cd12964597cf4228d..9a370bb73b91b6f6434fc53d6a024e4cd00dae1e 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -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, diff --git a/crates/collab/src/db/tests/embedding_tests.rs b/crates/collab/src/db/tests/embedding_tests.rs index 8659d4b4a1165ab9d3a2ed591fc9a6dfbd727c56..bfc238dd9ab7027cb2506b4c2d7130e070da8a04 100644 --- a/crates/collab/src/db/tests/embedding_tests.rs +++ b/crates/collab/src/db/tests/embedding_tests.rs @@ -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" diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 22daab491c499bf568f155cd6e049868c58192ce..753e591914f45ea962367130d1ecce9a4fd2620f 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -179,7 +179,7 @@ struct Session { } impl Session { - async fn db(&self) -> tokio::sync::MutexGuard { + 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) -> ServerSnapshot { + pub async fn snapshot(self: &Arc) -> ServerSnapshot<'_> { ServerSnapshot { connection_pool: ConnectionPoolGuard { guard: self.connection_pool.lock(), diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index 4069f61f90b48bfedfd4780f0865a061e4ab6971..0b331ff1e66279f5e2f5e52f9d83f0eaca6cfcdb 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -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]); }); }); diff --git a/crates/collab/src/tests/editor_tests.rs b/crates/collab/src/tests/editor_tests.rs index 7a51caefa1c2f7f6a3e7f702ae9594b790760d7d..2cc3ca76d1b639cc479cb44cde93a73570d5eb7f 100644 --- a/crates/collab/src/tests/editor_tests.rs +++ b/crates/collab/src/tests/editor_tests.rs @@ -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(); diff --git a/crates/collab/src/tests/following_tests.rs b/crates/collab/src/tests/following_tests.rs index 99f9b3350512f8d7eb126cb7a427979ab360d509..a77112213f195190e613c2382300bfbbeca70066 100644 --- a/crates/collab/src/tests/following_tests.rs +++ b/crates/collab/src/tests/following_tests.rs @@ -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)]); }) }); diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 145a31a179c38eba5ad0312b24ba96afcaa69b49..55427b1aa70fe59dd330e274ddade4839c73affd 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -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::(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::(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() diff --git a/crates/collab/src/tests/remote_editing_collaboration_tests.rs b/crates/collab/src/tests/remote_editing_collaboration_tests.rs index 217273a38787730c1cf6d5535e3e8e456cffb64a..0e9b25dc380fee929274d2a84607e55ee6832cb8 100644 --- a/crates/collab/src/tests/remote_editing_collaboration_tests.rs +++ b/crates/collab/src/tests/remote_editing_collaboration_tests.rs @@ -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::(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() diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index 80cc504308b30579d80e42e35e3267117a8bc456..c872f99aa10ee160ed499621d9aceb2aa7c06a05 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -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; } diff --git a/crates/collab_ui/src/panel_settings.rs b/crates/collab_ui/src/panel_settings.rs index 497b403019bfddf2c90be501a8e85c175accc8c7..652d9eb67f6ce1f0ab583e20e4feab05cfb743e3 100644 --- a/crates/collab_ui/src/panel_settings.rs +++ b/crates/collab_ui/src/panel_settings.rs @@ -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 `👋`. diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 2e411fd139c4d6410bee6512dd3537a9592f2420..abb8978d5a103fb66f862af6c5ee69beee0f6251 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -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::>(); - 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>, ) -> Option { - 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" ); } diff --git a/crates/context_server/src/context_server.rs b/crates/context_server/src/context_server.rs index f774deef170086f4b17e8f83d8fd5ec0557528e0..905435fcce57dc8ce8719e5056b28118168e9a04 100644 --- a/crates/context_server/src/context_server.rs +++ b/crates/context_server/src/context_server.rs @@ -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, pub env: Option>, diff --git a/crates/copilot/src/copilot_chat.rs b/crates/copilot/src/copilot_chat.rs index 19cff56c911b2d85f91b5075f238f8320f846e3f..b1fa1565f30ed79fdff763964708fe01c62d023f 100644 --- a/crates/copilot/src/copilot_chat.rs +++ b/crates/copilot/src/copilot_chat.rs @@ -698,16 +698,16 @@ async fn stream_completion( completion_url: Arc, request: Request, ) -> Result>> { - 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; diff --git a/crates/copilot/src/copilot_completion_provider.rs b/crates/copilot/src/copilot_completion_provider.rs index ff636178753b11bbe3be920a27a27a5c467cef5e..8dc04622f9020c2fe175304764157b409c7936c1 100644 --- a/crates/copilot/src/copilot_completion_provider.rs +++ b/crates/copilot/src/copilot_completion_provider.rs @@ -264,7 +264,8 @@ fn common_prefix, T2: Iterator>(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); diff --git a/crates/dap/src/adapters.rs b/crates/dap/src/adapters.rs index 8e1c84083f18835dee6c4bc3bea4ce7c45147499..d9f26b3b348985f2e52423cb217b1c1446960bbf 100644 --- a/crates/dap/src/adapters.rs +++ b/crates/dap/src/adapters.rs @@ -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; } -#[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 { diff --git a/crates/dap_adapters/Cargo.toml b/crates/dap_adapters/Cargo.toml index e2e922bd56ca3edcede5184358bfca443905b50e..65544fbb6a1b7565c4fe641058e4e6c725b21016 100644 --- a/crates/dap_adapters/Cargo.toml +++ b/crates/dap_adapters/Cargo.toml @@ -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 diff --git a/crates/dap_adapters/src/codelldb.rs b/crates/dap_adapters/src/codelldb.rs index 5d14cc87475c814639ab8e15b54df46d9a01dd4c..5b88db4432d3823e8a85228a82ca064cfacad23c 100644 --- a/crates/dap_adapters/src/codelldb.rs +++ b/crates/dap_adapters/src/codelldb.rs @@ -22,17 +22,16 @@ impl CodeLldbDebugAdapter { async fn request_args( &self, delegate: &Arc, - task_definition: &DebugTaskDefinition, + mut configuration: Value, + label: &str, ) -> Result { - // 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, }) diff --git a/crates/dap_adapters/src/dap_adapters.rs b/crates/dap_adapters/src/dap_adapters.rs index 79c56fdf25583e6cbe3a182b3abf464ac449eb27..c254302e7144b53500fd2a3b84be06e8ec30c2a0 100644 --- a/crates/dap_adapters/src/dap_adapters.rs +++ b/crates/dap_adapters/src/dap_adapters.rs @@ -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)); diff --git a/crates/dap_adapters/src/go.rs b/crates/dap_adapters/src/go.rs index bc3f5007454adee4cfcbc8a3cf09c87ae0100b97..d32f5cbf3426f1b669132e74e389862e7944267b 100644 --- a/crates/dap_adapters/src/go.rs +++ b/crates/dap_adapters/src/go.rs @@ -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, + envs: &mut HashMap, + cwd: Option<&Path>, + fs: Arc, +) -> Option<()> { + let env_files = match config.get("envFile")? { + Value::Array(arr) => arr.iter().map(|v| v.as_str()).collect::>(), + 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(()) +} diff --git a/crates/dap_adapters/src/javascript.rs b/crates/dap_adapters/src/javascript.rs index d5d78186acc9c76fc2dda5d096b099bd52aaf2a4..67adb5629bdb3d32730fe0bbb24d3c1ee6893ab1 100644 --- a/crates/dap_adapters/src/javascript.rs +++ b/crates/dap_adapters/src/javascript.rs @@ -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::>().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, diff --git a/crates/dap_adapters/src/ruby.rs b/crates/dap_adapters/src/ruby.rs deleted file mode 100644 index 28f1fb1f5ff155329a0629889cfb7d197dd6ce68..0000000000000000000000000000000000000000 --- a/crates/dap_adapters/src/ruby.rs +++ /dev/null @@ -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, - script: Option, - command: Option, - #[serde(default)] - args: Vec, - #[serde(default)] - env: FxHashMap, - cwd: Option, -} - -#[async_trait(?Send)] -impl DebugAdapter for RubyDebugAdapter { - fn name(&self) -> DebugAdapterName { - DebugAdapterName(Self::ADAPTER_NAME.into()) - } - - fn adapter_language_name(&self) -> Option { - Some(SharedString::new_static("Ruby").into()) - } - - async fn request_kind( - &self, - _: &serde_json::Value, - ) -> Result { - 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 { - 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, - definition: &DebugTaskDefinition, - _user_installed_path: Option, - _user_args: Option>, - _cx: &mut AsyncApp, - ) -> Result { - 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::(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, - }, - }) - } -} diff --git a/crates/debugger_tools/src/dap_log.rs b/crates/debugger_tools/src/dap_log.rs index fb5a345725dbcd93a08676a7cd85de6c10088bb5..532107f63302e9e057d2f90c28a2b32bcd0622d7 100644 --- a/crates/debugger_tools/src/dap_log.rs +++ b/crates/debugger_tools/src/dap_log.rs @@ -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, focus_handle: FocusHandle, @@ -49,14 +42,34 @@ struct DapLogView { _subscriptions: Vec, } +struct LogStoreEntryIdentifier<'a> { + session_id: SessionId, + project: Cow<'a, WeakEntity>, +} +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, + message: SharedString, +} + pub struct LogStore { projects: HashMap, ProjectState>, - debug_sessions: VecDeque, - rpc_tx: UnboundedSender<(SessionId, IoKind, Option, SharedString)>, - adapter_log_tx: UnboundedSender<(SessionId, IoKind, Option, SharedString)>, + rpc_tx: UnboundedSender, + adapter_log_tx: UnboundedSender, } struct ProjectState { + debug_sessions: BTreeMap, _subscriptions: [gpui::Subscription; 2], } @@ -122,13 +135,12 @@ impl DebugAdapterState { impl LogStore { pub fn new(cx: &Context) -> Self { - let (rpc_tx, mut rpc_rx) = - unbounded::<(SessionId, IoKind, Option, SharedString)>(); + let (rpc_tx, mut rpc_rx) = unbounded::(); 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)>(); + let (adapter_log_tx, mut adapter_log_rx) = unbounded::(); 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, cx: &mut Context) { - 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, - message: SharedString, + LogStoreMessage { + id, + kind: io_kind, + command, + message, + }: LogStoreMessage, cx: &mut Context, ) { - 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, ) { - 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, - id: SessionId, + id: LogStoreEntryIdentifier<'static>, message: SharedString, kind: LogKind, cx: &mut Context, ) -> 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, cx: &mut Context, ) { - 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) { - 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> { - 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> { - 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> { - 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> { + 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 { 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::>() }) - .collect::>() } fn show_rpc_trace_for_server( &mut self, - session_id: SessionId, + id: &LogStoreEntryIdentifier<'_>, window: &mut Window, cx: &mut Context, ) { 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, ) { 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, ) { 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 for DapLogView {} #[cfg(any(test, feature = "test-support"))] impl LogStore { - pub fn contained_session_ids(&self) -> Vec { - 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 { - 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) -> Vec { + 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 { - 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, + session_id: SessionId, + ) -> Vec { + 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() + }) } } diff --git a/crates/debugger_ui/Cargo.toml b/crates/debugger_ui/Cargo.toml index 91f9acad3c73334980036880143df9c7b410b3b6..ba71e50a0830c7fbab60aa75ba14bb63d58bac07 100644 --- a/crates/debugger_ui/Cargo.toml +++ b/crates/debugger_ui/Cargo.toml @@ -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 diff --git a/crates/debugger_ui/src/debugger_panel.rs b/crates/debugger_ui/src/debugger_panel.rs index b7f3be0426e9c189eb0edf203859c7d2489c75d9..795b4caf9e43a28c8bf115755332fa9976d89d93 100644 --- a/crates/debugger_ui/src/debugger_panel.rs +++ b/crates/debugger_ui/src/debugger_panel.rs @@ -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) + } + }), + ), ) } }) diff --git a/crates/debugger_ui/src/session/running.rs b/crates/debugger_ui/src/session/running.rs index 6a3535fe0ebc43eb49066f0e3a81887c10ad51bc..91e49059e92609d2639d043220fa042cc70b708b 100644 --- a/crates/debugger_ui/src/session/running.rs +++ b/crates/debugger_ui/src/session/running.rs @@ -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 { diff --git a/crates/debugger_ui/src/session/running/breakpoint_list.rs b/crates/debugger_ui/src/session/running/breakpoint_list.rs index 8077b289a7d111cbbd1ed189206904d753ae412c..5576435a0875ae298a7a7f5fb9d509a6a7ea16f1 100644 --- a/crates/debugger_ui/src/session/running/breakpoint_list.rs +++ b/crates/debugger_ui/src/session/running/breakpoint_list.rs @@ -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, + input: Entity, + strip_mode: Option, } 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>, workspace: WeakEntity, project: &Entity, + window: &mut Window, cx: &mut App, ) -> Entity { 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, row: u32, action: BreakpointEditAction, - cx: &mut Context, + cx: &mut App, + ) { + Self::edit_line_breakpoint_inner(&self.breakpoint_store, path, row, action, cx); + } + fn edit_line_breakpoint_inner( + breakpoint_store: &Entity, + path: Arc, + 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, cx: &mut Context) { + 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, window: &mut Window, cx: &mut Context) { 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) { + fn select_next(&mut self, _: &menu::SelectNext, window: &mut Window, cx: &mut Context) { + 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, ) { + 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, - ) { + fn select_first(&mut self, _: &menu::SelectFirst, window: &mut Window, cx: &mut Context) { + 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) { + fn select_last(&mut self, _: &menu::SelectLast, window: &mut Window, cx: &mut Context) { + 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) { + 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) { 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, ) { 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, + ) { + 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, + ) { + 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) { 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) -> impl IntoElement { + fn render_list(&mut self, cx: &mut Context) -> 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, window, cx| { + cx.processor(move |this, range: Range, _, _| { 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) -> 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, ix: usize, + is_selected: bool, focus_handle: FocusHandle, weak: WeakEntity, ) -> 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::(); + 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, ix: usize, + is_selected: bool, focus_handle: FocusHandle, list: WeakEntity, ) -> 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, + 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, + 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() + }), + ) + } +} diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index 83d2d46547ada9da328cc44443813a87a6f681f1..aaac63640188b2b277d1ff8bfb9b75b114f5554b 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -114,7 +114,7 @@ impl Console { } fn is_running(&self, cx: &Context) -> bool { - self.session.read(cx).is_running() + self.session.read(cx).is_started() } fn handle_stack_frame_list_events( diff --git a/crates/debugger_ui/src/stack_trace_view.rs b/crates/debugger_ui/src/stack_trace_view.rs index 675522e99996b276b5f62eeb88297dfe7d592579..aef053df4a1ea930fb09a779e08afecfa08ddde9 100644 --- a/crates/debugger_ui/src/stack_trace_view.rs +++ b/crates/debugger_ui/src/stack_trace_view.rs @@ -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 { diff --git a/crates/debugger_ui/src/tests/dap_logger.rs b/crates/debugger_ui/src/tests/dap_logger.rs index 0427a5c4ac41161739295dad194f7d1e94d1dec9..ff2b0f695f6a2e7f0ca65b49938e0129efb04326 100644 --- a/crates/debugger_ui/src/tests/dap_logger.rs +++ b/crates/debugger_ui/src/tests/dap_logger.rs @@ -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" ); diff --git a/crates/debugger_ui/src/tests/new_process_modal.rs b/crates/debugger_ui/src/tests/new_process_modal.rs index 201b149746a918a461701920bf7be4dc85510aa7..eb8c7f8063f23b8097efca4a16c071b1649cb903 100644 --- a/crates/debugger_ui/src/tests/new_process_modal.rs +++ b/crates/debugger_ui/src/tests/new_process_modal.rs @@ -267,7 +267,6 @@ async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppConte "Debugpy", "PHP", "JavaScript", - "Ruby", "Delve", "GDB", "fake-adapter", diff --git a/crates/diagnostics/src/diagnostic_renderer.rs b/crates/diagnostics/src/diagnostic_renderer.rs index 9524f97ff1e14599576df549844ee7c164d6d017..77bb249733f612ede3017e1cff592927b40e8d43 100644 --- a/crates/diagnostics/src/diagnostic_renderer.rs +++ b/crates/diagnostics/src/diagnostic_renderer.rs @@ -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)); diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 4f66a5a8839ddd8a3a2405a2b57114b73a1cf9f8..8b49c536245a2509cb73254eca8de6d1be1cfd75 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -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]); }) }); diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index c42b58729e04edd9138002209d7c8db305c853a0..bea83b1df826a3dac1cf4afe14a0dd7b417b972b 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -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 diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index fd371e20cbf22585d1dd2640f01104dd16428750..3352d21ef878835987e0227a926dfb61c893a182 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -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, + widths: impl IntoIterator, cx: &mut Context, ) -> 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); } } diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 92456836a9766b1ab6fb5e3d4dfc406dc0bc393b..f37e7063e7228176b0f5455c278f331ed31d6ba0 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -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, + new_widths: impl IntoIterator, ) -> (FoldSnapshot, Vec) { 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 for ElementId { fn from(val: FoldId) -> Self { @@ -1265,11 +1270,17 @@ pub struct Chunk<'a> { pub renderer: Option, } +#[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 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, }); } diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 33fc5540d63f20e5108e438f38c3cba4703ad927..49b5ce1d26916de0ec79ab80ec21f1bcf8b335e3 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -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, +} + impl InlayChunks<'_> { pub fn seek(&mut self, new_range: Range) { 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 { 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::(); assert_eq!( actual_text, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ea30cc6fab94d7a80e8855efd3832b21a945b6c1..fe904ab4ec09faba7ce91fd3600363bde05339a0 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -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, + 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, completions: bool, scroll: Option, } @@ -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( &mut self, - effects: impl Into, + effects: SelectionEffects, window: &mut Window, cx: &mut Context, 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::(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::(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>>, cx: &mut Context, @@ -6708,6 +6741,77 @@ impl Editor { }) } + fn refresh_single_line_folds(&mut self, window: &mut Window, cx: &mut Context) { + struct NewlineFold; + let type_id = std::any::TypeId::of::(); + 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::>(); + 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::>(); + + (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::>() }); 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 = 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.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::(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::(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, 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::(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::(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, Option) { + 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::(); + 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::>::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::>::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::(); - + let indent_prefix = indent_size.chars().collect::(); + 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::(); 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::, _>>() .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.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::(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.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.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.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.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; @@ -12246,7 +12401,7 @@ impl Editor { pub fn select_up(&mut self, _: &SelectUp, window: &mut Window, cx: &mut Context) { 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(map, head, goal, false, text_layout_details) }) @@ -12267,7 +12422,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; @@ -12303,7 +12458,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::down_by_rows(map, head, row_count, goal, false, text_layout_details) }) @@ -12341,14 +12496,14 @@ 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; @@ -12369,7 +12524,7 @@ impl Editor { pub fn select_down(&mut self, _: &SelectDown, window: &mut Window, cx: &mut Context) { 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(map, head, goal, false, text_layout_details) }) @@ -12427,7 +12582,7 @@ impl Editor { cx: &mut Context, ) { 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_cursors_with(|map, head, _| { ( movement::previous_word_start(map, head), @@ -12444,7 +12599,7 @@ impl Editor { cx: &mut Context, ) { 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_cursors_with(|map, head, _| { ( movement::previous_subword_start(map, head), @@ -12461,7 +12616,7 @@ impl Editor { cx: &mut Context, ) { 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::previous_word_start(map, head), @@ -12478,7 +12633,7 @@ impl Editor { cx: &mut Context, ) { 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::previous_subword_start(map, head), @@ -12497,7 +12652,7 @@ impl Editor { self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { this.select_autoclose_pair(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 = if action.ignore_newlines { @@ -12522,7 +12677,7 @@ impl Editor { self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx); self.transact(window, cx, |this, window, cx| { this.select_autoclose_pair(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::previous_subword_start(map, selection.head()); @@ -12541,7 +12696,7 @@ impl Editor { cx: &mut Context, ) { 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_cursors_with(|map, head, _| { (movement::next_word_end(map, head), SelectionGoal::None) }); @@ -12555,7 +12710,7 @@ impl Editor { cx: &mut Context, ) { 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_cursors_with(|map, head, _| { (movement::next_subword_end(map, head), SelectionGoal::None) }); @@ -12569,7 +12724,7 @@ impl Editor { cx: &mut Context, ) { 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::next_word_end(map, head), SelectionGoal::None) }); @@ -12583,7 +12738,7 @@ impl Editor { cx: &mut Context, ) { 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::next_subword_end(map, head), SelectionGoal::None) }); @@ -12598,7 +12753,7 @@ impl Editor { ) { 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 = if action.ignore_newlines { @@ -12622,7 +12777,7 @@ impl Editor { ) { 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::next_subword_end(map, selection.head()); @@ -12641,7 +12796,7 @@ impl Editor { cx: &mut Context, ) { 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_cursors_with(|map, head, _| { ( movement::indented_line_beginning( @@ -12663,7 +12818,7 @@ impl Editor { cx: &mut Context, ) { 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::indented_line_beginning( @@ -12686,7 +12841,7 @@ impl Editor { ) { 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(|_, selection| { selection.reversed = true; }); @@ -12711,7 +12866,7 @@ impl Editor { cx: &mut Context, ) { 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_cursors_with(|map, head, _| { ( movement::line_end(map, head, action.stop_at_soft_wraps), @@ -12728,7 +12883,7 @@ impl Editor { cx: &mut Context, ) { 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::line_end(map, head, action.stop_at_soft_wraps), @@ -12787,7 +12942,7 @@ impl Editor { return; } 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| { selection.collapse_to( movement::start_of_paragraph(map, selection.head(), 1), @@ -12808,7 +12963,7 @@ impl Editor { return; } 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| { selection.collapse_to( movement::end_of_paragraph(map, selection.head(), 1), @@ -12829,7 +12984,7 @@ impl Editor { return; } 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::start_of_paragraph(map, head, 1), @@ -12850,7 +13005,7 @@ impl Editor { return; } 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::end_of_paragraph(map, head, 1), @@ -12871,7 +13026,7 @@ impl Editor { return; } 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| { selection.collapse_to( movement::start_of_excerpt( @@ -12896,7 +13051,7 @@ impl Editor { return; } - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.change_selections(Default::default(), window, cx, |s| { s.move_with(|map, selection| { selection.collapse_to( movement::start_of_excerpt( @@ -12921,7 +13076,7 @@ impl Editor { return; } 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| { selection.collapse_to( movement::end_of_excerpt( @@ -12946,7 +13101,7 @@ impl Editor { return; } 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| { selection.collapse_to( movement::end_of_excerpt( @@ -12971,7 +13126,7 @@ impl Editor { return; } 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::start_of_excerpt(map, head, workspace::searchable::Direction::Prev), @@ -12992,7 +13147,7 @@ impl Editor { return; } 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::start_of_excerpt(map, head, workspace::searchable::Direction::Next), @@ -13013,7 +13168,7 @@ impl Editor { return; } 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::end_of_excerpt(map, head, workspace::searchable::Direction::Next), @@ -13034,7 +13189,7 @@ impl Editor { return; } 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::end_of_excerpt(map, head, workspace::searchable::Direction::Prev), @@ -13055,7 +13210,7 @@ impl Editor { return; } 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.select_ranges(vec![0..0]); }); } @@ -13069,7 +13224,7 @@ impl Editor { let mut selection = self.selections.last::(cx); selection.set_head(Point::zero(), SelectionGoal::None); 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.select(vec![selection]); }); } @@ -13081,7 +13236,7 @@ impl Editor { } self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let cursor = self.buffer.read(cx).read(cx).len(); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.change_selections(Default::default(), window, cx, |s| { s.select_ranges(vec![cursor..cursor]) }); } @@ -13095,7 +13250,13 @@ impl Editor { } pub fn create_nav_history_entry(&mut self, cx: &mut Context) { - self.push_to_nav_history(self.selections.newest_anchor().head(), None, false, cx); + self.push_to_nav_history( + self.selections.newest_anchor().head(), + None, + false, + true, + cx, + ); } fn push_to_nav_history( @@ -13103,6 +13264,7 @@ impl Editor { cursor_anchor: Anchor, new_position: Option, is_deactivate: bool, + always: bool, cx: &mut Context, ) { if let Some(nav_history) = self.nav_history.as_mut() { @@ -13114,7 +13276,7 @@ impl Editor { if let Some(new_position) = new_position { let row_delta = (new_position.row as i64 - cursor_position.row as i64).abs(); - if row_delta < MIN_NAVIGATION_HISTORY_ROW_DELTA { + if row_delta == 0 || (row_delta < MIN_NAVIGATION_HISTORY_ROW_DELTA && !always) { return; } } @@ -13140,7 +13302,7 @@ impl Editor { let buffer = self.buffer.read(cx).snapshot(cx); let mut selection = self.selections.first::(cx); selection.set_head(buffer.len(), SelectionGoal::None); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.change_selections(Default::default(), window, cx, |s| { s.select(vec![selection]); }); } @@ -13148,7 +13310,7 @@ impl Editor { pub fn select_all(&mut self, _: &SelectAll, window: &mut Window, cx: &mut Context) { self.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); let end = self.buffer.read(cx).read(cx).len(); - self.change_selections(None, window, cx, |s| { + self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges(vec![0..end]); }); } @@ -13164,7 +13326,7 @@ impl Editor { selection.end = cmp::min(max_point, Point::new(rows.end.0, 0)); selection.reversed = false; } - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.change_selections(Default::default(), window, cx, |s| { s.select(selections); }); } @@ -13201,7 +13363,7 @@ impl Editor { } } } - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.change_selections(Default::default(), window, cx, |s| { s.select_ranges(new_selection_ranges); }); } @@ -13349,7 +13511,7 @@ impl Editor { } } - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.change_selections(Default::default(), window, cx, |s| { s.select(final_selections); }); @@ -13381,8 +13543,18 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.unfold_ranges(&[range.clone()], false, auto_scroll.is_some(), cx); - self.change_selections(auto_scroll, window, cx, |s| { + self.unfold_ranges( + std::slice::from_ref(&range), + false, + auto_scroll.is_some(), + cx, + ); + let effects = if let Some(scroll) = auto_scroll { + SelectionEffects::scroll(scroll) + } else { + SelectionEffects::no_scroll() + }; + self.change_selections(effects, window, cx, |s| { if replace_newest { s.delete(s.newest_anchor().id); } @@ -13594,7 +13766,7 @@ impl Editor { } self.unfold_ranges(&new_selections.clone(), false, false, cx); - self.change_selections(None, window, cx, |selections| { + self.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { selections.select_ranges(new_selections) }); @@ -13765,7 +13937,7 @@ impl Editor { let selections = self.selections.disjoint_anchors(); match selections.first() { Some(first) if selections.len() >= 2 => { - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.change_selections(Default::default(), window, cx, |s| { s.select_ranges([first.range()]); }); } @@ -13789,7 +13961,7 @@ impl Editor { let selections = self.selections.disjoint_anchors(); match selections.last() { Some(last) if selections.len() >= 2 => { - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.change_selections(Default::default(), window, cx, |s| { s.select_ranges([last.range()]); }); } @@ -14068,9 +14240,7 @@ impl Editor { } drop(snapshot); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(selections) - }); + this.change_selections(Default::default(), window, cx, |s| s.select(selections)); let selections = this.selections.all::(cx); let selections_on_single_row = selections.windows(2).all(|selections| { @@ -14089,7 +14259,7 @@ impl Editor { if advance_downwards { let snapshot = this.buffer.read(cx).snapshot(cx); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + this.change_selections(Default::default(), window, cx, |s| { s.move_cursors_with(|display_snapshot, display_point, _| { let mut point = display_point.to_point(display_snapshot); point.row += 1; @@ -14156,7 +14326,7 @@ impl Editor { .collect::>(); if selected_larger_symbol { - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.change_selections(Default::default(), window, cx, |s| { s.select(new_selections); }); } @@ -14256,7 +14426,7 @@ impl Editor { if selected_larger_node { self.select_syntax_node_history.disable_clearing = true; - self.change_selections(None, window, cx, |s| { + self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select(new_selections.clone()); }); self.select_syntax_node_history.disable_clearing = false; @@ -14302,7 +14472,7 @@ impl Editor { } self.select_syntax_node_history.disable_clearing = true; - self.change_selections(None, window, cx, |s| { + self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select(selections.to_vec()); }); self.select_syntax_node_history.disable_clearing = false; @@ -14567,7 +14737,7 @@ impl Editor { cx: &mut Context, ) { 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_offsets_with(|snapshot, selection| { let Some(enclosing_bracket_ranges) = snapshot.enclosing_bracket_ranges(selection.start..selection.end) @@ -14628,9 +14798,12 @@ impl Editor { self.selection_history.mode = SelectionHistoryMode::Undoing; self.with_selection_effects_deferred(window, cx, |this, window, cx| { this.end_selection(window, cx); - this.change_selections(Some(Autoscroll::newest()), window, cx, |s| { - s.select_anchors(entry.selections.to_vec()) - }); + this.change_selections( + SelectionEffects::scroll(Autoscroll::newest()), + window, + cx, + |s| s.select_anchors(entry.selections.to_vec()), + ); }); self.selection_history.mode = SelectionHistoryMode::Normal; @@ -14651,9 +14824,12 @@ impl Editor { self.selection_history.mode = SelectionHistoryMode::Redoing; self.with_selection_effects_deferred(window, cx, |this, window, cx| { this.end_selection(window, cx); - this.change_selections(Some(Autoscroll::newest()), window, cx, |s| { - s.select_anchors(entry.selections.to_vec()) - }); + this.change_selections( + SelectionEffects::scroll(Autoscroll::newest()), + window, + cx, + |s| s.select_anchors(entry.selections.to_vec()), + ); }); self.selection_history.mode = SelectionHistoryMode::Normal; @@ -14781,9 +14957,12 @@ impl Editor { let Some(end) = multibuffer.buffer_point_to_anchor(&buffer, range.end, cx) else { return; }; - self.change_selections(Some(Autoscroll::center()), window, cx, |s| { - s.select_anchor_ranges([start..end]) - }); + self.change_selections( + SelectionEffects::default().nav_history(true), + window, + cx, + |s| s.select_anchor_ranges([start..end]), + ); } pub fn go_to_diagnostic( @@ -14883,7 +15062,7 @@ impl Editor { let Some(buffer_id) = buffer.anchor_after(next_diagnostic.range.start).buffer_id else { return; }; - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.change_selections(Default::default(), window, cx, |s| { s.select_ranges(vec![ next_diagnostic.range.start..next_diagnostic.range.start, ]) @@ -14925,7 +15104,7 @@ impl Editor { let autoscroll = Autoscroll::center(); self.unfold_ranges(&[destination..destination], false, false, cx); - self.change_selections(Some(autoscroll), window, cx, |s| { + self.change_selections(SelectionEffects::scroll(autoscroll), window, cx, |s| { s.select_ranges([destination..destination]); }); } @@ -14988,7 +15167,7 @@ impl Editor { .next_change(1, Direction::Next) .map(|s| s.to_vec()) { - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.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); @@ -15009,7 +15188,7 @@ impl Editor { .next_change(1, Direction::Prev) .map(|s| s.to_vec()) { - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + self.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); @@ -15629,10 +15808,16 @@ impl Editor { match multibuffer_selection_mode { MultibufferSelectionMode::First => { if let Some(first_range) = ranges.first() { - editor.change_selections(None, window, cx, |selections| { - selections.clear_disjoint(); - selections.select_anchor_ranges(std::iter::once(first_range.clone())); - }); + editor.change_selections( + SelectionEffects::no_scroll(), + window, + cx, + |selections| { + selections.clear_disjoint(); + selections + .select_anchor_ranges(std::iter::once(first_range.clone())); + }, + ); } editor.highlight_background::( &ranges, @@ -15641,10 +15826,15 @@ impl Editor { ); } MultibufferSelectionMode::All => { - editor.change_selections(None, window, cx, |selections| { - selections.clear_disjoint(); - selections.select_anchor_ranges(ranges); - }); + editor.change_selections( + SelectionEffects::no_scroll(), + window, + cx, + |selections| { + selections.clear_disjoint(); + selections.select_anchor_ranges(ranges); + }, + ); } } editor.register_buffers_with_language_servers(cx); @@ -15778,7 +15968,7 @@ impl Editor { if rename_selection_range.end > old_name.len() { editor.select_all(&SelectAll, window, cx); } else { - editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + editor.change_selections(Default::default(), window, cx, |s| { s.select_ranges([rename_selection_range]); }); } @@ -15951,7 +16141,7 @@ impl Editor { .min(rename_range.end); drop(snapshot); - self.change_selections(None, window, cx, |s| { + self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges(vec![cursor_in_editor..cursor_in_editor]) }); } else { @@ -16634,7 +16824,7 @@ impl Editor { pub fn set_mark(&mut self, _: &actions::SetMark, window: &mut Window, cx: &mut Context) { if self.selection_mark_mode { - self.change_selections(None, window, cx, |s| { + self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.move_with(|_, sel| { sel.collapse_to(sel.head(), SelectionGoal::None); }); @@ -16650,7 +16840,7 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - self.change_selections(None, window, cx, |s| { + self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.move_with(|_, sel| { if sel.start != sel.end { sel.reversed = !sel.reversed @@ -17076,16 +17266,6 @@ impl Editor { return; } - let mut buffers_affected = HashSet::default(); - let multi_buffer = self.buffer().read(cx); - for crease in &creases { - if let Some((_, buffer, _)) = - multi_buffer.excerpt_containing(crease.range().start.clone(), cx) - { - buffers_affected.insert(buffer.read(cx).remote_id()); - }; - } - self.display_map.update(cx, |map, cx| map.fold(creases, cx)); if auto_scroll { @@ -17201,9 +17381,9 @@ impl Editor { self.active_indent_guides_state.dirty = true; } - pub fn update_fold_widths( + pub fn update_renderer_widths( &mut self, - widths: impl IntoIterator, + widths: impl IntoIterator, cx: &mut Context, ) -> bool { self.display_map @@ -17399,7 +17579,7 @@ impl Editor { let autoscroll = Autoscroll::center(); self.unfold_ranges(&[destination..destination], false, false, cx); - self.change_selections(Some(autoscroll), window, cx, |s| { + self.change_selections(SelectionEffects::scroll(autoscroll), window, cx, |s| { s.select_ranges([destination..destination]); }); } @@ -19411,6 +19591,7 @@ impl Editor { self.refresh_active_diagnostics(cx); self.refresh_code_actions(window, cx); self.refresh_selected_text_highlights(true, window, cx); + self.refresh_single_line_folds(window, cx); refresh_matching_bracket_highlights(self, window, cx); if self.has_active_inline_completion() { self.update_visible_inline_completion(window, cx); @@ -19431,7 +19612,7 @@ impl Editor { cx.emit(SearchEvent::MatchesInvalidated); if let Some(buffer) = edited_buffer { - self.update_lsp_data(None, Some(buffer.read(cx).remote_id()), window, cx); + self.update_lsp_data(false, Some(buffer.read(cx).remote_id()), window, cx); } if *singleton_buffer_edited { @@ -19496,7 +19677,7 @@ impl Editor { .detach(); } } - self.update_lsp_data(None, Some(buffer_id), window, cx); + self.update_lsp_data(false, Some(buffer_id), window, cx); cx.emit(EditorEvent::ExcerptsAdded { buffer: buffer.clone(), predecessor: *predecessor, @@ -19682,7 +19863,7 @@ impl Editor { if !inlay_splice.to_insert.is_empty() || !inlay_splice.to_remove.is_empty() { self.splice_inlays(&inlay_splice.to_remove, inlay_splice.to_insert, cx); } - self.refresh_colors(None, None, window, cx); + self.refresh_colors(false, None, window, cx); } cx.notify(); @@ -19933,9 +20114,14 @@ impl Editor { None => Autoscroll::newest(), }; let nav_history = editor.nav_history.take(); - editor.change_selections(Some(autoscroll), window, cx, |s| { - s.select_ranges(ranges); - }); + editor.change_selections( + SelectionEffects::scroll(autoscroll), + window, + cx, + |s| { + s.select_ranges(ranges); + }, + ); editor.nav_history = nav_history; }); } @@ -20136,7 +20322,7 @@ impl Editor { } if let Some(relative_utf16_range) = relative_utf16_range { let selections = self.selections.all::(cx); - self.change_selections(None, window, cx, |s| { + self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { let new_ranges = selections.into_iter().map(|range| { let start = OffsetUtf16( range @@ -20279,7 +20465,7 @@ impl Editor { .iter() .map(|selection| (selection.end..selection.end, pending.clone())); this.edit(edits, cx); - this.change_selections(None, window, cx, |s| { + this.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges(selections.into_iter().enumerate().map(|(ix, sel)| { sel.start + ix * pending.len()..sel.end + ix * pending.len() })); @@ -20435,7 +20621,9 @@ impl Editor { } }) .detach(); - self.change_selections(None, window, cx, |selections| selections.refresh()); + self.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { + selections.refresh() + }); } pub fn to_pixel_point( @@ -20502,15 +20690,20 @@ impl Editor { .and_then(|item| item.to_any_mut()?.downcast_mut::()) } - fn character_size(&self, window: &mut Window) -> gpui::Size { + fn character_dimensions(&self, window: &mut Window) -> CharacterDimensions { let text_layout_details = self.text_layout_details(window); let style = &text_layout_details.editor_style; let font_id = window.text_system().resolve_font(&style.text.font()); let font_size = style.text.font_size.to_pixels(window.rem_size()); let line_height = style.text.line_height_in_pixels(window.rem_size()); let em_width = window.text_system().em_width(font_id, font_size).unwrap(); + let em_advance = window.text_system().em_advance(font_id, font_size).unwrap(); - gpui::Size::new(em_width, line_height) + CharacterDimensions { + em_width, + em_advance, + line_height, + } } pub fn wait_for_diff_to_load(&self) -> Option>> { @@ -20555,7 +20748,7 @@ impl Editor { buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx)); // skip adding the initial selection to selection history self.selection_history.mode = SelectionHistoryMode::Skipping; - self.change_selections(None, window, cx, |s| { + self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges(selections.into_iter().map(|(start, end)| { snapshot.clip_offset(start, Bias::Left) ..snapshot.clip_offset(end, Bias::Right) @@ -20571,13 +20764,13 @@ impl Editor { fn update_lsp_data( &mut self, - for_server_id: Option, + ignore_cache: bool, for_buffer: Option, window: &mut Window, cx: &mut Context<'_, Self>, ) { self.pull_diagnostics(for_buffer, window, cx); - self.refresh_colors(for_server_id, for_buffer, window, cx); + self.refresh_colors(ignore_cache, for_buffer, window, cx); } } @@ -21055,18 +21248,22 @@ fn test_word_breaking_tokenizer() { } fn wrap_with_prefix( - line_prefix: String, + first_line_prefix: String, + subsequent_lines_prefix: String, unwrapped_text: String, wrap_column: usize, tab_size: NonZeroU32, preserve_existing_whitespace: bool, ) -> String { - let line_prefix_len = char_len_with_expanded_tabs(0, &line_prefix, tab_size); + let first_line_prefix_len = char_len_with_expanded_tabs(0, &first_line_prefix, tab_size); + let subsequent_lines_prefix_len = + char_len_with_expanded_tabs(0, &subsequent_lines_prefix, tab_size); let mut wrapped_text = String::new(); - let mut current_line = line_prefix.clone(); + let mut current_line = first_line_prefix.clone(); + let mut is_first_line = true; let tokenizer = WordBreakingTokenizer::new(&unwrapped_text); - let mut current_line_len = line_prefix_len; + let mut current_line_len = first_line_prefix_len; let mut in_whitespace = false; for token in tokenizer { let have_preceding_whitespace = in_whitespace; @@ -21076,13 +21273,19 @@ fn wrap_with_prefix( grapheme_len, } => { in_whitespace = false; + let current_prefix_len = if is_first_line { + first_line_prefix_len + } else { + subsequent_lines_prefix_len + }; if current_line_len + grapheme_len > wrap_column - && current_line_len != line_prefix_len + && current_line_len != current_prefix_len { wrapped_text.push_str(current_line.trim_end()); wrapped_text.push('\n'); - current_line.truncate(line_prefix.len()); - current_line_len = line_prefix_len; + is_first_line = false; + current_line = subsequent_lines_prefix.clone(); + current_line_len = subsequent_lines_prefix_len; } current_line.push_str(token); current_line_len += grapheme_len; @@ -21099,32 +21302,46 @@ fn wrap_with_prefix( token = " "; grapheme_len = 1; } + let current_prefix_len = if is_first_line { + first_line_prefix_len + } else { + subsequent_lines_prefix_len + }; if current_line_len + grapheme_len > wrap_column { wrapped_text.push_str(current_line.trim_end()); wrapped_text.push('\n'); - current_line.truncate(line_prefix.len()); - current_line_len = line_prefix_len; - } else if current_line_len != line_prefix_len || preserve_existing_whitespace { + is_first_line = false; + current_line = subsequent_lines_prefix.clone(); + current_line_len = subsequent_lines_prefix_len; + } else if current_line_len != current_prefix_len || preserve_existing_whitespace { current_line.push_str(token); current_line_len += grapheme_len; } } WordBreakToken::Newline => { in_whitespace = true; + let current_prefix_len = if is_first_line { + first_line_prefix_len + } else { + subsequent_lines_prefix_len + }; if preserve_existing_whitespace { wrapped_text.push_str(current_line.trim_end()); wrapped_text.push('\n'); - current_line.truncate(line_prefix.len()); - current_line_len = line_prefix_len; + is_first_line = false; + current_line = subsequent_lines_prefix.clone(); + current_line_len = subsequent_lines_prefix_len; } else if have_preceding_whitespace { continue; - } else if current_line_len + 1 > wrap_column && current_line_len != line_prefix_len + } else if current_line_len + 1 > wrap_column + && current_line_len != current_prefix_len { wrapped_text.push_str(current_line.trim_end()); wrapped_text.push('\n'); - current_line.truncate(line_prefix.len()); - current_line_len = line_prefix_len; - } else if current_line_len != line_prefix_len { + is_first_line = false; + current_line = subsequent_lines_prefix.clone(); + current_line_len = subsequent_lines_prefix_len; + } else if current_line_len != current_prefix_len { current_line.push(' '); current_line_len += 1; } @@ -21142,6 +21359,7 @@ fn wrap_with_prefix( fn test_wrap_with_prefix() { assert_eq!( wrap_with_prefix( + "# ".to_string(), "# ".to_string(), "abcdefg".to_string(), 4, @@ -21152,6 +21370,7 @@ fn test_wrap_with_prefix() { ); assert_eq!( wrap_with_prefix( + "".to_string(), "".to_string(), "\thello world".to_string(), 8, @@ -21162,6 +21381,7 @@ fn test_wrap_with_prefix() { ); assert_eq!( wrap_with_prefix( + "// ".to_string(), "// ".to_string(), "xx \nyy zz aa bb cc".to_string(), 12, @@ -21172,6 +21392,7 @@ fn test_wrap_with_prefix() { ); assert_eq!( wrap_with_prefix( + String::new(), String::new(), "这是什么 \n 钢笔".to_string(), 3, @@ -22262,6 +22483,7 @@ impl Render for Editor { &cx.entity(), EditorStyle { background, + border: cx.theme().colors().border, local_player: cx.theme().players().local(), text: text_style, scrollbar_width: EditorElement::SCROLLBAR_WIDTH, @@ -22369,7 +22591,7 @@ impl EntityInputHandler for Editor { }); if let Some(new_selected_ranges) = new_selected_ranges { - this.change_selections(None, window, cx, |selections| { + this.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { selections.select_ranges(new_selected_ranges) }); this.backspace(&Default::default(), window, cx); @@ -22444,7 +22666,9 @@ impl EntityInputHandler for Editor { }); if let Some(ranges) = ranges_to_replace { - this.change_selections(None, window, cx, |s| s.select_ranges(ranges)); + this.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges(ranges) + }); } let marked_ranges = { @@ -22498,7 +22722,7 @@ impl EntityInputHandler for Editor { .collect::>(); drop(snapshot); - this.change_selections(None, window, cx, |selections| { + this.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { selections.select_ranges(new_selected_ranges) }); } @@ -22524,19 +22748,19 @@ impl EntityInputHandler for Editor { cx: &mut Context, ) -> Option> { let text_layout_details = self.text_layout_details(window); - let gpui::Size { - width: em_width, - height: line_height, - } = self.character_size(window); + let CharacterDimensions { + em_width, + em_advance, + line_height, + } = self.character_dimensions(window); let snapshot = self.snapshot(window, cx); let scroll_position = snapshot.scroll_position(); - let scroll_left = scroll_position.x * em_width; + let scroll_left = scroll_position.x * em_advance; let start = OffsetUtf16(range_utf16.start).to_display_point(&snapshot); let x = snapshot.x_for_display_point(start, &text_layout_details) - scroll_left - + self.gutter_dimensions.width - + self.gutter_dimensions.margin; + + self.gutter_dimensions.full_width(); let y = line_height * (start.row().as_f32() - scroll_position.y); Some(Bounds { diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index f2cb41793c9bfd08f57d2a3734f3fa321479bb13..d7b8bac359abe21ef3cc977518e828899a57e299 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -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. /// diff --git a/crates/editor/src/editor_settings_controls.rs b/crates/editor/src/editor_settings_controls.rs index 54bb865520568ef2f5d0291c19a061a5c87d3568..dc5557b05277da972ea36ba43ffdf08a565edda9 100644 --- a/crates/editor/src/editor_settings_controls.rs +++ b/crates/editor/src/editor_settings_controls.rs @@ -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())); } } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 5b9a2ef773f6deb5ef2f26e573125021445cbcfd..4f3a9bcd35b2918feb8d82adca21a2a556003776 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -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, ); @@ -6061,7 +6136,7 @@ fn test_select_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), @@ -6188,7 +6263,7 @@ async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestA }); _ = 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), @@ -6207,7 +6282,7 @@ async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestA .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ"); _ = 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(5), 0)..DisplayPoint::new(DisplayRow(0), 1) ]) @@ -6953,7 +7028,7 @@ async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) { // Move cursor to a different position cx.update_editor(|editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]); }); }); @@ -7058,7 +7133,7 @@ async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext "}); cx.update_editor(|editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]); }); }); @@ -7318,7 +7393,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) { .await; 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), 25)..DisplayPoint::new(DisplayRow(0), 25), DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12), @@ -7500,7 +7575,7 @@ async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContex // Test case 1: Cursor at end of word 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), 5)..DisplayPoint::new(DisplayRow(0), 5) ]); @@ -7524,7 +7599,7 @@ async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContex // Test case 2: Cursor at end of statement 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), 11)..DisplayPoint::new(DisplayRow(0), 11) ]); @@ -7569,7 +7644,7 @@ async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppConte // Test 1: Cursor on a letter of a string word 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), 17)..DisplayPoint::new(DisplayRow(3), 17) ]); @@ -7603,7 +7678,7 @@ async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppConte // Test 2: Partial selection within a word 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), 17)..DisplayPoint::new(DisplayRow(3), 19) ]); @@ -7637,7 +7712,7 @@ async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppConte // Test 3: Complete word already selected 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), 16)..DisplayPoint::new(DisplayRow(3), 21) ]); @@ -7671,7 +7746,7 @@ async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppConte // Test 4: Selection spanning across words 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), 19)..DisplayPoint::new(DisplayRow(3), 24) ]); @@ -7873,7 +7948,9 @@ async fn test_autoindent(cx: &mut TestAppContext) { .await; editor.update_in(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9])); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([5..5, 8..8, 9..9]) + }); editor.newline(&Newline, window, cx); assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n"); assert_eq!( @@ -8655,7 +8732,7 @@ async fn test_surround_with_pair(cx: &mut TestAppContext) { .await; 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), 1), DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1), @@ -8805,7 +8882,7 @@ async fn test_delete_autoclose_pair(cx: &mut TestAppContext) { .await; 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([ Point::new(0, 1)..Point::new(0, 1), Point::new(1, 1)..Point::new(1, 1), @@ -9328,7 +9405,7 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) { // Set rust language override and assert overridden tabsize is sent to language server update_test_language_settings(cx, |settings| { - settings.languages.insert( + settings.languages.0.insert( "Rust".into(), LanguageSettingsContent { tab_size: NonZeroU32::new(8), @@ -9487,16 +9564,22 @@ async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) { }); multi_buffer_editor.update_in(cx, |editor, window, cx| { - editor.change_selections(Some(Autoscroll::Next), window, cx, |s| { - s.select_ranges(Some(1..2)) - }); + editor.change_selections( + SelectionEffects::scroll(Autoscroll::Next), + window, + cx, + |s| s.select_ranges(Some(1..2)), + ); editor.insert("|one|two|three|", window, cx); }); assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx))); multi_buffer_editor.update_in(cx, |editor, window, cx| { - editor.change_selections(Some(Autoscroll::Next), window, cx, |s| { - s.select_ranges(Some(60..70)) - }); + editor.change_selections( + SelectionEffects::scroll(Autoscroll::Next), + window, + cx, + |s| s.select_ranges(Some(60..70)), + ); editor.insert("|four|five|six|", window, cx); }); assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx))); @@ -9659,9 +9742,12 @@ async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) { // Edit only the first buffer editor.update_in(cx, |editor, window, cx| { - editor.change_selections(Some(Autoscroll::Next), window, cx, |s| { - s.select_ranges(Some(10..10)) - }); + editor.change_selections( + SelectionEffects::scroll(Autoscroll::Next), + window, + cx, + |s| s.select_ranges(Some(10..10)), + ); editor.insert("// edited", window, cx); }); @@ -9883,7 +9969,7 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { // Set Rust language override and assert overridden tabsize is sent to language server update_test_language_settings(cx, |settings| { - settings.languages.insert( + settings.languages.0.insert( "Rust".into(), LanguageSettingsContent { tab_size: NonZeroU32::new(8), @@ -9926,9 +10012,9 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) { #[gpui::test] async fn test_document_format_manual_trigger(cx: &mut TestAppContext) { init_test(cx, |settings| { - settings.defaults.formatter = Some(language_settings::SelectedFormatter::List( - FormatterList(vec![Formatter::LanguageServer { name: None }].into()), - )) + settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(vec![ + Formatter::LanguageServer { name: None }, + ])) }); let fs = FakeFs::new(cx.executor()); @@ -10055,21 +10141,17 @@ async fn test_document_format_manual_trigger(cx: &mut TestAppContext) { async fn test_multiple_formatters(cx: &mut TestAppContext) { init_test(cx, |settings| { settings.defaults.remove_trailing_whitespace_on_save = Some(true); - settings.defaults.formatter = - Some(language_settings::SelectedFormatter::List(FormatterList( - vec![ - Formatter::LanguageServer { name: None }, - Formatter::CodeActions( - [ - ("code-action-1".into(), true), - ("code-action-2".into(), true), - ] - .into_iter() - .collect(), - ), + settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(vec![ + Formatter::LanguageServer { name: None }, + Formatter::CodeActions( + [ + ("code-action-1".into(), true), + ("code-action-2".into(), true), ] - .into(), - ))) + .into_iter() + .collect(), + ), + ])) }); let fs = FakeFs::new(cx.executor()); @@ -10321,9 +10403,9 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) { #[gpui::test] async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) { init_test(cx, |settings| { - settings.defaults.formatter = Some(language_settings::SelectedFormatter::List( - FormatterList(vec![Formatter::LanguageServer { name: None }].into()), - )) + settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(vec![ + Formatter::LanguageServer { name: None }, + ])) }); let fs = FakeFs::new(cx.executor()); @@ -11073,7 +11155,9 @@ async fn test_signature_help(cx: &mut TestAppContext) { "}); cx.update_editor(|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]) + }); }); let mocked_response = lsp::SignatureHelp { @@ -11160,7 +11244,7 @@ async fn test_signature_help(cx: &mut TestAppContext) { // When selecting a range, the popover is gone. // Avoid using `cx.set_state` to not actually edit the document, just change its selections. cx.update_editor(|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, 25)..Point::new(1, 19))); }) }); @@ -11177,7 +11261,7 @@ async fn test_signature_help(cx: &mut TestAppContext) { // When unselecting again, the popover is back if within the brackets. cx.update_editor(|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, 19)..Point::new(1, 19))); }) }); @@ -11197,7 +11281,7 @@ async fn test_signature_help(cx: &mut TestAppContext) { // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape. cx.update_editor(|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(0, 0)..Point::new(0, 0))); s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19))); }) @@ -11238,7 +11322,7 @@ async fn test_signature_help(cx: &mut TestAppContext) { cx.condition(|editor, _| !editor.signature_help_state.is_shown()) .await; cx.update_editor(|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, 25)..Point::new(1, 19))); }) }); @@ -11250,7 +11334,7 @@ async fn test_signature_help(cx: &mut TestAppContext) { fn sample(param1: u8, param2: u8) {} "}); cx.update_editor(|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, 19)..Point::new(1, 19))); }) }); @@ -11906,7 +11990,7 @@ async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppConte let fake_server = fake_servers.next().await.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([ Point::new(1, 11)..Point::new(1, 11), Point::new(7, 11)..Point::new(7, 11), @@ -13547,7 +13631,7 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) { let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx)); editor.update_in(cx, |editor, window, cx| { assert_eq!(editor.text(cx), "aaaa\nbbbb"); - 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(0, 0), Point::new(1, 0)..Point::new(1, 0), @@ -13565,7 +13649,7 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) { ); // Ensure the cursor's head is respected when deleting across an excerpt boundary. - 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, 2)]) }); editor.backspace(&Default::default(), window, cx); @@ -13575,7 +13659,7 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) { [Point::new(1, 0)..Point::new(1, 0)] ); - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(1, 1)..Point::new(0, 1)]) }); editor.backspace(&Default::default(), window, cx); @@ -13623,7 +13707,9 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { true, ); assert_eq!(editor.text(cx), expected_text); - editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges)); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges(selection_ranges) + }); editor.handle_input("X", window, cx); @@ -13684,7 +13770,7 @@ fn test_refresh_selections(cx: &mut TestAppContext) { let editor = cx.add_window(|window, cx| { let mut editor = build_editor(multibuffer.clone(), window, cx); let snapshot = editor.snapshot(window, cx); - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(1, 3)..Point::new(1, 3)]) }); editor.begin_selection( @@ -13706,7 +13792,7 @@ fn test_refresh_selections(cx: &mut TestAppContext) { // Refreshing selections is a no-op when excerpts haven't changed. _ = editor.update(cx, |editor, window, cx| { - editor.change_selections(None, window, cx, |s| s.refresh()); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh()); assert_eq!( editor.selections.ranges(cx), [ @@ -13731,7 +13817,7 @@ fn test_refresh_selections(cx: &mut TestAppContext) { // Refreshing selections will relocate the first selection to the original buffer // location. - editor.change_selections(None, window, cx, |s| s.refresh()); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh()); assert_eq!( editor.selections.ranges(cx), [ @@ -13793,7 +13879,7 @@ fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) { ); // Ensure we don't panic when selections are refreshed and that the pending selection is finalized. - editor.change_selections(None, window, cx, |s| s.refresh()); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh()); assert_eq!( editor.selections.ranges(cx), [Point::new(0, 3)..Point::new(0, 3)] @@ -13852,7 +13938,7 @@ async fn test_extra_newline_insertion(cx: &mut TestAppContext) { .await; 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), 2)..DisplayPoint::new(DisplayRow(0), 3), DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5), @@ -14031,7 +14117,9 @@ async fn test_following(cx: &mut TestAppContext) { // Update the selections only _ = leader.update(cx, |leader, window, cx| { - leader.change_selections(None, window, cx, |s| s.select_ranges([1..1])); + leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([1..1]) + }); }); follower .update(cx, |follower, window, cx| { @@ -14079,7 +14167,9 @@ async fn test_following(cx: &mut TestAppContext) { // Update the selections and scroll position. The follower's scroll position is updated // via autoscroll, not via the leader's exact scroll position. _ = leader.update(cx, |leader, window, cx| { - leader.change_selections(None, window, cx, |s| s.select_ranges([0..0])); + leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([0..0]) + }); leader.request_autoscroll(Autoscroll::newest(), cx); leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx); }); @@ -14103,7 +14193,9 @@ async fn test_following(cx: &mut TestAppContext) { // Creating a pending selection that precedes another selection _ = leader.update(cx, |leader, window, cx| { - leader.change_selections(None, window, cx, |s| s.select_ranges([1..1])); + leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges([1..1]) + }); leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx); }); follower @@ -14759,7 +14851,7 @@ async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) { editor_handle.update_in(cx, |editor, window, cx| { window.focus(&editor.focus_handle(cx)); - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(0, 21)..Point::new(0, 20)]) }); editor.handle_input("{", window, cx); @@ -14888,7 +14980,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon .unwrap(); let _fake_server = fake_servers.next().await.unwrap(); update_test_language_settings(cx, |language_settings| { - language_settings.languages.insert( + language_settings.languages.0.insert( language_name.clone(), LanguageSettingsContent { tab_size: NonZeroU32::new(8), @@ -15786,9 +15878,9 @@ fn completion_menu_entries(menu: &CompletionsMenu) -> Vec { #[gpui::test] async fn test_document_format_with_prettier(cx: &mut TestAppContext) { init_test(cx, |settings| { - settings.defaults.formatter = Some(language_settings::SelectedFormatter::List( - FormatterList(vec![Formatter::Prettier].into()), - )) + settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(vec![ + Formatter::Prettier, + ])) }); let fs = FakeFs::new(cx.executor()); @@ -16374,7 +16466,7 @@ async fn test_multibuffer_reverts(cx: &mut TestAppContext) { }); 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(0, 0)..Point::new(6, 0))); }); editor.git_restore(&Default::default(), window, cx); @@ -16518,9 +16610,12 @@ async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) { cx.executor().run_until_parked(); multi_buffer_editor.update_in(cx, |editor, window, cx| { - editor.change_selections(Some(Autoscroll::Next), window, cx, |s| { - s.select_ranges(Some(1..2)) - }); + editor.change_selections( + SelectionEffects::scroll(Autoscroll::Next), + window, + cx, + |s| s.select_ranges(Some(1..2)), + ); editor.open_excerpts(&OpenExcerpts, window, cx); }); cx.executor().run_until_parked(); @@ -16570,9 +16665,12 @@ async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) { .unwrap(); multi_buffer_editor.update_in(cx, |editor, window, cx| { - editor.change_selections(Some(Autoscroll::Next), window, cx, |s| { - s.select_ranges(Some(39..40)) - }); + editor.change_selections( + SelectionEffects::scroll(Autoscroll::Next), + window, + cx, + |s| s.select_ranges(Some(39..40)), + ); editor.open_excerpts(&OpenExcerpts, window, cx); }); cx.executor().run_until_parked(); @@ -16626,9 +16724,12 @@ async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) { .unwrap(); multi_buffer_editor.update_in(cx, |editor, window, cx| { - editor.change_selections(Some(Autoscroll::Next), window, cx, |s| { - s.select_ranges(Some(70..70)) - }); + editor.change_selections( + SelectionEffects::scroll(Autoscroll::Next), + window, + cx, + |s| s.select_ranges(Some(70..70)), + ); editor.open_excerpts(&OpenExcerpts, window, cx); }); cx.executor().run_until_parked(); @@ -18230,7 +18331,7 @@ async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) { .await; cx.update_editor(|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, 0)..Point::new(1, 0)]) }); }); @@ -18258,7 +18359,7 @@ async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext .await; cx.update_editor(|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, 0)..Point::new(1, 0)]) }); }); @@ -18274,7 +18375,7 @@ async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext ); cx.update_editor(|editor, window, 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)]) }); }); @@ -18290,7 +18391,7 @@ async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext ); cx.update_editor(|editor, window, cx| { - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]) }); }); @@ -18321,7 +18422,7 @@ async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) { .await; cx.update_editor(|editor, window, 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)]) }); }); @@ -18347,7 +18448,7 @@ async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) { .await; cx.update_editor(|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, 0)..Point::new(1, 0)]) }); }); @@ -19285,14 +19386,14 @@ async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) { ); // Test finding task when cursor is inside function body - 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)]) }); let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap(); assert_eq!(row, 3, "Should find task for cursor inside runnable_1"); // Test finding task when cursor is on function name - editor.change_selections(None, window, cx, |s| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { s.select_ranges([Point::new(8, 4)..Point::new(8, 4)]) }); let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap(); @@ -19446,7 +19547,7 @@ async fn test_folding_buffers(cx: &mut TestAppContext) { .collect::(), "bbbb" ); - editor.change_selections(None, window, cx, |selections| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]); }); editor.handle_input("B", window, cx); @@ -19673,7 +19774,9 @@ async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut Test HighlightStyle::color(Hsla::green()), cx, ); - editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range))); + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { + s.select_ranges(Some(highlight_range)) + }); }); let full_text = format!("\n\n{sample_text}"); @@ -21043,7 +21146,7 @@ println!("5"); }) }); editor_1.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(expected_ranges.clone()); }); }); @@ -21489,7 +21592,7 @@ async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) { let fake_server = fake_servers.next().await.unwrap(); editor.update_in(cx, |editor, window, cx| { editor.set_text("", window, cx); - editor.change_selections(None, window, cx, |selections| { + editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]); }); let Some((buffer, _)) = editor @@ -22495,7 +22598,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) { // Moving cursor should not trigger diagnostic request 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([Point::new(0, 0)..Point::new(0, 0)]) }); }); @@ -22574,8 +22677,8 @@ async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppC ); } -#[gpui::test] -async fn test_mtime_and_document_colors(cx: &mut TestAppContext) { +#[gpui::test(iterations = 10)] +async fn test_document_colors(cx: &mut TestAppContext) { let expected_color = Rgba { r: 0.33, g: 0.33, @@ -22607,6 +22710,18 @@ async fn test_mtime_and_document_colors(cx: &mut TestAppContext) { color_provider: Some(lsp::ColorProviderCapability::Simple(true)), ..lsp::ServerCapabilities::default() }, + name: "rust-analyzer", + ..FakeLspAdapter::default() + }, + ); + let mut fake_servers_without_capabilities = language_registry.register_fake_lsp( + "Rust", + FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + color_provider: Some(lsp::ColorProviderCapability::Simple(false)), + ..lsp::ServerCapabilities::default() + }, + name: "not-rust-analyzer", ..FakeLspAdapter::default() }, ); @@ -22626,6 +22741,8 @@ async fn test_mtime_and_document_colors(cx: &mut TestAppContext) { .downcast::() .unwrap(); let fake_language_server = fake_servers.next().await.unwrap(); + let fake_language_server_without_capabilities = + fake_servers_without_capabilities.next().await.unwrap(); let requests_made = Arc::new(AtomicUsize::new(0)); let closure_requests_made = Arc::clone(&requests_made); let mut color_request_handle = fake_language_server @@ -22637,44 +22754,118 @@ async fn test_mtime_and_document_colors(cx: &mut TestAppContext) { lsp::Url::from_file_path(path!("/a/first.rs")).unwrap() ); requests_made.fetch_add(1, atomic::Ordering::Release); - Ok(vec![lsp::ColorInformation { - range: lsp::Range { - start: lsp::Position { - line: 0, - character: 0, + Ok(vec![ + lsp::ColorInformation { + range: lsp::Range { + start: lsp::Position { + line: 0, + character: 0, + }, + end: lsp::Position { + line: 0, + character: 1, + }, }, - end: lsp::Position { - line: 0, - character: 1, + color: lsp::Color { + red: 0.33, + green: 0.33, + blue: 0.33, + alpha: 0.33, }, }, - color: lsp::Color { - red: 0.33, - green: 0.33, - blue: 0.33, - alpha: 0.33, + lsp::ColorInformation { + range: lsp::Range { + start: lsp::Position { + line: 0, + character: 0, + }, + end: lsp::Position { + line: 0, + character: 1, + }, + }, + color: lsp::Color { + red: 0.33, + green: 0.33, + blue: 0.33, + alpha: 0.33, + }, }, - }]) + ]) } }); - color_request_handle.next().await.unwrap(); - cx.run_until_parked(); + + let _handle = fake_language_server_without_capabilities + .set_request_handler::(move |_, _| async move { + panic!("Should not be called"); + }); + cx.executor().advance_clock(Duration::from_millis(100)); color_request_handle.next().await.unwrap(); cx.run_until_parked(); assert_eq!( - 2, + 1, requests_made.load(atomic::Ordering::Acquire), - "Should query for colors once per editor open and once after the language server startup" + "Should query for colors once per editor open" ); - - cx.executor().advance_clock(Duration::from_millis(500)); - let save = editor.update_in(cx, |editor, window, cx| { + editor.update_in(cx, |editor, _, cx| { assert_eq!( vec![expected_color], extract_color_inlays(editor, cx), "Should have an initial inlay" ); + }); + // opening another file in a split should not influence the LSP query counter + workspace + .update(cx, |workspace, window, cx| { + assert_eq!( + workspace.panes().len(), + 1, + "Should have one pane with one editor" + ); + workspace.move_item_to_pane_in_direction( + &MoveItemToPaneInDirection { + direction: SplitDirection::Right, + focus: false, + clone: true, + }, + window, + cx, + ); + }) + .unwrap(); + cx.run_until_parked(); + workspace + .update(cx, |workspace, _, cx| { + let panes = workspace.panes(); + assert_eq!(panes.len(), 2, "Should have two panes after splitting"); + for pane in panes { + let editor = pane + .read(cx) + .active_item() + .and_then(|item| item.downcast::()) + .expect("Should have opened an editor in each split"); + let editor_file = editor + .read(cx) + .buffer() + .read(cx) + .as_singleton() + .expect("test deals with singleton buffers") + .read(cx) + .file() + .expect("test buffese should have a file") + .path(); + assert_eq!( + editor_file.as_ref(), + Path::new("first.rs"), + "Both editors should be opened for the same file" + ) + } + }) + .unwrap(); + + cx.executor().advance_clock(Duration::from_millis(500)); + let save = editor.update_in(cx, |editor, window, cx| { editor.move_to_end(&MoveToEnd, window, cx); editor.handle_input("dirty", window, cx); editor.save( @@ -22689,12 +22880,10 @@ async fn test_mtime_and_document_colors(cx: &mut TestAppContext) { }); save.await.unwrap(); - color_request_handle.next().await.unwrap(); - cx.run_until_parked(); color_request_handle.next().await.unwrap(); cx.run_until_parked(); assert_eq!( - 4, + 3, requests_made.load(atomic::Ordering::Acquire), "Should query for colors once per save and once per formatting after save" ); @@ -22708,11 +22897,27 @@ async fn test_mtime_and_document_colors(cx: &mut TestAppContext) { }) .unwrap(); close.await.unwrap(); + let close = workspace + .update(cx, |workspace, window, cx| { + workspace.active_pane().update(cx, |pane, cx| { + pane.close_active_item(&CloseActiveItem::default(), window, cx) + }) + }) + .unwrap(); + close.await.unwrap(); assert_eq!( - 4, + 3, requests_made.load(atomic::Ordering::Acquire), - "After saving and closing the editor, no extra requests should be made" + "After saving and closing all editors, no extra requests should be made" ); + workspace + .update(cx, |workspace, _, cx| { + assert!( + workspace.active_item(cx).is_none(), + "Should close all editors" + ) + }) + .unwrap(); workspace .update(cx, |workspace, window, cx| { @@ -22721,13 +22926,8 @@ async fn test_mtime_and_document_colors(cx: &mut TestAppContext) { }) }) .unwrap(); - color_request_handle.next().await.unwrap(); + cx.executor().advance_clock(Duration::from_millis(100)); cx.run_until_parked(); - assert_eq!( - 5, - requests_made.load(atomic::Ordering::Acquire), - "After navigating back to an editor and reopening it, another color request should be made" - ); let editor = workspace .update(cx, |workspace, _, cx| { workspace @@ -22737,6 +22937,12 @@ async fn test_mtime_and_document_colors(cx: &mut TestAppContext) { .expect("Should be an editor") }) .unwrap(); + color_request_handle.next().await.unwrap(); + assert_eq!( + 3, + requests_made.load(atomic::Ordering::Acquire), + "Cache should be reused on buffer close and reopen" + ); editor.update(cx, |editor, cx| { assert_eq!( vec![expected_color], @@ -22746,6 +22952,24 @@ async fn test_mtime_and_document_colors(cx: &mut TestAppContext) { }); } +#[gpui::test] +async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + let (editor, cx) = cx.add_window_view(Editor::single_line); + editor.update_in(cx, |editor, window, cx| { + editor.set_text("oops\n\nwow\n", window, cx) + }); + cx.run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯"); + }); + editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx)); + cx.run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!(editor.display_text(cx), "oop⋯wow⋯"); + }); +} + #[track_caller] fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec { editor diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 602a0579b3a23b4449d08a732580ac261bd841c2..1c55ff2d092835f6162e3321fd4da00a1ad83d59 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -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, size: Size, 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), diff --git a/crates/editor/src/hover_links.rs b/crates/editor/src/hover_links.rs index a716b2e0314223aa81338942da063d87919a71fe..02f93e6829a3f7ac08ec7dfa390cd846560bb7d5 100644 --- a/crates/editor/src/hover_links.rs +++ b/crates/editor/src/hover_links.rs @@ -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) }); }); diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index b174a3ba62ed3924cf4a0da151ad20330a77eafa..cae47895356c4fbd6ffc94779952475ce6f18dd6 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -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| { diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index dcfa8429a0da818679965dac4cdbc6875a16118f..db01cc7ad1d668520f9650c7d396156814c50ba1 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -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 { 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)]) }) }) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 93a80d7764944c82f547894a982b5cd1dbf02b26..fa6bd93ab8558628670cb315e672ddf4fb3ebcab 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -778,7 +778,7 @@ impl Item for Editor { fn deactivated(&mut self, _: &mut Window, cx: &mut Context) { 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) { @@ -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) -> 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::(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.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()) }); } diff --git a/crates/editor/src/jsx_tag_auto_close.rs b/crates/editor/src/jsx_tag_auto_close.rs index f24fe46100879ce885d7bf863e797458c8bac52d..95a792583953e02a77e592ea957b752f0f8042bb 100644 --- a/crates/editor/src/jsx_tag_auto_close.rs +++ b/crates/editor/src/jsx_tag_auto_close.rs @@ -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), diff --git a/crates/editor/src/lsp_colors.rs b/crates/editor/src/lsp_colors.rs index bacd61199efbb1fa988ff0d9b6762d2bd24dc099..ce07dd43fe8ffc2a3705eefec96e2382312301a7 100644 --- a/crates/editor/src/lsp_colors.rs +++ b/crates/editor/src/lsp_colors.rs @@ -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, + render_mode: DocumentColorsRenderMode, +} + +#[derive(Debug, Default)] +struct BufferColors { colors: Vec<(Range, DocumentColor, InlayId)>, inlay_colors: HashMap, - 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, DocumentColor, InlayId)>) -> bool { - if self.colors == colors { + fn set_colors( + &mut self, + buffer_id: BufferId, + colors: Vec<(Range, DocumentColor, InlayId)>, + cache_version: Option, + ) -> 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, + ignore_cache: bool, buffer_id: Option, _: &Window, cx: &mut Context, @@ -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::>(); + 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::>() - }) + 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::>() }); 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, 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, 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()) diff --git a/crates/editor/src/mouse_context_menu.rs b/crates/editor/src/mouse_context_menu.rs index 408cccd33282e76c29817d6d69efc29b5365eff0..4780f1f56582bf675d7cd7deb7b8f8effb98bfae 100644 --- a/crates/editor/src/mouse_context_menu.rs +++ b/crates/editor/src/mouse_context_menu.rs @@ -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, diff --git a/crates/editor/src/proposed_changes_editor.rs b/crates/editor/src/proposed_changes_editor.rs index c5f937f20c3c56b16f42b8e5b501b4a21e0e987f..1ead45b3de89c0705510f8afc55ecf6176a4d7a2 100644 --- a/crates/editor/src/proposed_changes_editor.rs +++ b/crates/editor/src/proposed_changes_editor.rs @@ -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) diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index 6cc483cb650d102ba3a8f569f9ac3e99cb95727c..0642b2b20ebfb7213f74ab6980889a7e07218415 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -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) -> gpui::Point { diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index 9e20d14b61c6413fda35bdc7c3e0f2d0521f7aa4..0a9d5e9535d2b2d29e33ee49a8afa46a387d773e 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -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] diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index 195abbe6d98acafb0fa5a874362dd41a2e0fc630..bdf73da5fbfd5d4c29826859790493fbb8494239 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -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) }) }); diff --git a/crates/eval/Cargo.toml b/crates/eval/Cargo.toml index 7ecba7c1ec91facef139eb0b8e971a12f76361a7..d5db7f71a4593a66ee8218c053109041035428ab 100644 --- a/crates/eval/Cargo.toml +++ b/crates/eval/Cargo.toml @@ -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 diff --git a/crates/eval/src/eval.rs b/crates/eval/src/eval.rs index 5e8dd8961c8c3416fa84303eff722c22c31738e6..39121377bba907dbf38983156e1e0f55d187829a 100644 --- a/crates/eval/src/eval.rs +++ b/crates/eval/src/eval.rs @@ -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(); diff --git a/crates/eval/src/instance.rs b/crates/eval/src/instance.rs index bb66a04e1f07f1f070d9c4c6536f260a05a11bb6..d17dc89d0ba9d3e0a301fd19c4c47ff6f5a531ad 100644 --- a/crates/eval/src/instance.rs +++ b/crates/eval/src/instance.rs @@ -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))); diff --git a/crates/extension_cli/src/main.rs b/crates/extension_cli/src/main.rs index 93148191516bd9810c4a4380ff5b0cbbfea5ac64..45a7e3b6412ea9fba02a6394394b3ca9fc5bc58f 100644 --- a/crates/extension_cli/src/main.rs +++ b/crates/extension_cli/src/main.rs @@ -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(()) } diff --git a/crates/extensions_ui/src/extension_suggest.rs b/crates/extensions_ui/src/extension_suggest.rs index 9b1d1f8cdfc5e3f9201e6513d632c1ec96f15058..ab990881cca337e72361a0a79ce1ded5739595da 100644 --- a/crates/extensions_ui/src/extension_suggest.rs +++ b/crates/extensions_ui/src/extension_suggest.rs @@ -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"]), diff --git a/crates/fs/src/fake_git_repo.rs b/crates/fs/src/fake_git_repo.rs index c6e0afe2948386b44d831475debe85d1d7f5e5f5..40a292e0401df931cc17d04ed71219917292ab1f 100644 --- a/crates/fs/src/fake_git_repo.rs +++ b/crates/fs/src/fake_git_repo.rs @@ -74,7 +74,7 @@ impl FakeGitRepository { impl GitRepository for FakeGitRepository { fn reload_index(&self) {} - fn load_index_text(&self, path: RepoPath) -> BoxFuture> { + fn load_index_text(&self, path: RepoPath) -> BoxFuture<'_, Option> { 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> { + fn load_committed_text(&self, path: RepoPath) -> BoxFuture<'_, Option> { async { self.with_state_async(false, move |state| { state @@ -108,7 +108,7 @@ impl GitRepository for FakeGitRepository { &self, _commit: String, _cx: AsyncApp, - ) -> BoxFuture> { + ) -> BoxFuture<'_, Result> { unimplemented!() } @@ -117,7 +117,7 @@ impl GitRepository for FakeGitRepository { path: RepoPath, content: Option, _env: Arc>, - ) -> BoxFuture> { + ) -> 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) -> BoxFuture>>> { + fn revparse_batch(&self, revs: Vec) -> BoxFuture<'_, Result>>> { self.with_state_async(false, |state| { Ok(revs .into_iter() @@ -143,7 +143,7 @@ impl GitRepository for FakeGitRepository { }) } - fn show(&self, commit: String) -> BoxFuture> { + fn show(&self, commit: String) -> BoxFuture<'_, Result> { async { Ok(CommitDetails { sha: commit.into(), @@ -158,7 +158,7 @@ impl GitRepository for FakeGitRepository { _commit: String, _mode: ResetMode, _env: Arc>, - ) -> BoxFuture> { + ) -> BoxFuture<'_, Result<()>> { unimplemented!() } @@ -167,7 +167,7 @@ impl GitRepository for FakeGitRepository { _commit: String, _paths: Vec, _env: Arc>, - ) -> BoxFuture> { + ) -> BoxFuture<'_, Result<()>> { unimplemented!() } @@ -179,11 +179,11 @@ impl GitRepository for FakeGitRepository { self.common_dir_path.clone() } - fn merge_message(&self) -> BoxFuture> { + fn merge_message(&self) -> BoxFuture<'_, Option> { async move { None }.boxed() } - fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture> { + fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<'_, Result> { 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>> { + fn branches(&self) -> BoxFuture<'_, Result>> { 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> { + 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> { + 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> { + fn blame(&self, path: RepoPath, _content: Rope) -> BoxFuture<'_, Result> { self.with_state_async(false, move |state| { state .blames @@ -358,7 +358,7 @@ impl GitRepository for FakeGitRepository { &self, _paths: Vec, _env: Arc>, - ) -> BoxFuture> { + ) -> BoxFuture<'_, Result<()>> { unimplemented!() } @@ -366,7 +366,7 @@ impl GitRepository for FakeGitRepository { &self, _paths: Vec, _env: Arc>, - ) -> BoxFuture> { + ) -> BoxFuture<'_, Result<()>> { unimplemented!() } @@ -376,7 +376,7 @@ impl GitRepository for FakeGitRepository { _name_and_email: Option<(gpui::SharedString, gpui::SharedString)>, _options: CommitOptions, _env: Arc>, - ) -> BoxFuture> { + ) -> BoxFuture<'_, Result<()>> { unimplemented!() } @@ -388,7 +388,7 @@ impl GitRepository for FakeGitRepository { _askpass: AskPassDelegate, _env: Arc>, _cx: AsyncApp, - ) -> BoxFuture> { + ) -> BoxFuture<'_, Result> { unimplemented!() } @@ -399,7 +399,7 @@ impl GitRepository for FakeGitRepository { _askpass: AskPassDelegate, _env: Arc>, _cx: AsyncApp, - ) -> BoxFuture> { + ) -> BoxFuture<'_, Result> { unimplemented!() } @@ -409,19 +409,19 @@ impl GitRepository for FakeGitRepository { _askpass: AskPassDelegate, _env: Arc>, _cx: AsyncApp, - ) -> BoxFuture> { + ) -> BoxFuture<'_, Result> { unimplemented!() } - fn get_remotes(&self, _branch: Option) -> BoxFuture>> { + fn get_remotes(&self, _branch: Option) -> BoxFuture<'_, Result>> { unimplemented!() } - fn check_for_pushed_commit(&self) -> BoxFuture>> { + fn check_for_pushed_commit(&self) -> BoxFuture<'_, Result>> { future::ready(Ok(Vec::new())).boxed() } - fn diff(&self, _diff: git::repository::DiffType) -> BoxFuture> { + fn diff(&self, _diff: git::repository::DiffType) -> BoxFuture<'_, Result> { unimplemented!() } @@ -429,7 +429,10 @@ impl GitRepository for FakeGitRepository { unimplemented!() } - fn restore_checkpoint(&self, _checkpoint: GitRepositoryCheckpoint) -> BoxFuture> { + fn restore_checkpoint( + &self, + _checkpoint: GitRepositoryCheckpoint, + ) -> BoxFuture<'_, Result<()>> { unimplemented!() } @@ -437,7 +440,7 @@ impl GitRepository for FakeGitRepository { &self, _left: GitRepositoryCheckpoint, _right: GitRepositoryCheckpoint, - ) -> BoxFuture> { + ) -> BoxFuture<'_, Result> { unimplemented!() } @@ -445,7 +448,7 @@ impl GitRepository for FakeGitRepository { &self, _base_checkpoint: GitRepositoryCheckpoint, _target_checkpoint: GitRepositoryCheckpoint, - ) -> BoxFuture> { + ) -> BoxFuture<'_, Result> { unimplemented!() } } diff --git a/crates/git/src/git.rs b/crates/git/src/git.rs index bb8f39f127f7bb3edae553844daf1635fb6b312d..d64d35b789415b2dda821693ee7e9d19193a5de4 100644 --- a/crates/git/src/git.rs +++ b/crates/git/src/git.rs @@ -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, diff --git a/crates/git/src/repository.rs b/crates/git/src/repository.rs index 1254e451fdaed13f07862ec3a379b254da3ee373..2ecd4bb894348cf3fc532a8473e43f0712e61700 100644 --- a/crates/git/src/repository.rs +++ b/crates/git/src/repository.rs @@ -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>; + fn load_index_text(&self, path: RepoPath) -> BoxFuture<'_, Option>; /// 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>; + fn load_committed_text(&self, path: RepoPath) -> BoxFuture<'_, Option>; fn set_index_text( &self, path: RepoPath, content: Option, env: Arc>, - ) -> BoxFuture>; + ) -> BoxFuture<'_, anyhow::Result<()>>; /// Returns the URL of the remote with the given name. fn remote_url(&self, name: &str) -> Option; /// Resolve a list of refs to SHAs. - fn revparse_batch(&self, revs: Vec) -> BoxFuture>>>; + fn revparse_batch(&self, revs: Vec) -> BoxFuture<'_, Result>>>; fn head_sha(&self) -> BoxFuture<'_, Option> { async move { @@ -335,33 +335,33 @@ pub trait GitRepository: Send + Sync { .boxed() } - fn merge_message(&self) -> BoxFuture>; + fn merge_message(&self) -> BoxFuture<'_, Option>; - fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture>; + fn status(&self, path_prefixes: &[RepoPath]) -> BoxFuture<'_, Result>; - fn branches(&self) -> BoxFuture>>; + fn branches(&self) -> BoxFuture<'_, Result>>; - fn change_branch(&self, name: String) -> BoxFuture>; - fn create_branch(&self, name: String) -> BoxFuture>; + 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>, - ) -> BoxFuture>; + ) -> BoxFuture<'_, Result<()>>; fn checkout_files( &self, commit: String, paths: Vec, env: Arc>, - ) -> BoxFuture>; + ) -> BoxFuture<'_, Result<()>>; - fn show(&self, commit: String) -> BoxFuture>; + fn show(&self, commit: String) -> BoxFuture<'_, Result>; - fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture>; - fn blame(&self, path: RepoPath, content: Rope) -> BoxFuture>; + fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<'_, Result>; + fn blame(&self, path: RepoPath, content: Rope) -> BoxFuture<'_, Result>; /// 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/`). @@ -376,7 +376,7 @@ pub trait GitRepository: Send + Sync { &self, paths: Vec, env: Arc>, - ) -> BoxFuture>; + ) -> 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, env: Arc>, - ) -> BoxFuture>; + ) -> BoxFuture<'_, Result<()>>; fn commit( &self, @@ -392,7 +392,7 @@ pub trait GitRepository: Send + Sync { name_and_email: Option<(SharedString, SharedString)>, options: CommitOptions, env: Arc>, - ) -> BoxFuture>; + ) -> 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>; + ) -> BoxFuture<'_, Result>; 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>; + ) -> BoxFuture<'_, Result>; 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>; + ) -> BoxFuture<'_, Result>; - fn get_remotes(&self, branch_name: Option) -> BoxFuture>>; + fn get_remotes(&self, branch_name: Option) -> BoxFuture<'_, Result>>; /// returns a list of remote branches that contain HEAD - fn check_for_pushed_commit(&self) -> BoxFuture>>; + fn check_for_pushed_commit(&self) -> BoxFuture<'_, Result>>; /// Run git diff - fn diff(&self, diff: DiffType) -> BoxFuture>; + fn diff(&self, diff: DiffType) -> BoxFuture<'_, Result>; /// Creates a checkpoint for the repository. fn checkpoint(&self) -> BoxFuture<'static, Result>; /// Resets to a previously-created checkpoint. - fn restore_checkpoint(&self, checkpoint: GitRepositoryCheckpoint) -> BoxFuture>; + 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>; + ) -> BoxFuture<'_, Result>; /// Computes a diff between two checkpoints. fn diff_checkpoints( &self, base_checkpoint: GitRepositoryCheckpoint, target_checkpoint: GitRepositoryCheckpoint, - ) -> BoxFuture>; + ) -> BoxFuture<'_, Result>; } 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> { + 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(); diff --git a/crates/git_ui/src/branch_picker.rs b/crates/git_ui/src/branch_picker.rs index 635876dace889bde4f461a9feee9c8df4d1c24cc..9eac3ce5aff6dd440fd18fde3ea70042e71a4ce7 100644 --- a/crates/git_ui/src/branch_picker.rs +++ b/crates/git_ui/src/branch_picker.rs @@ -245,7 +245,7 @@ impl PickerDelegate for BranchListDelegate { type ListItem = ListItem; fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - "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) diff --git a/crates/git_ui/src/commit_view.rs b/crates/git_ui/src/commit_view.rs index e07f84ba0272cb05572e404106af637788510a6e..c8c237fe90f12f2ac4ead04e0f2f0b4955f8bc1c 100644 --- a/crates/git_ui/src/commit_view.rs +++ b/crates/git_ui/src/commit_view.rs @@ -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]); }); }); diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index dce3a52e0a567301f4b3b387ee71f89014ef5083..51ef90fd38287a0b28debb90baf97c135f4ab9d4 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -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()); diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index 371759bd24eb21ae53995648cf86a794b114e156..f858bea94c288efc5dd24c3c17c63bc4b3c63aa2 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -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) { 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]) }); diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index bba9617975774883ba869e4a6e607cd66cebee5a..1ac933e316bcde24384139c851a8bedb63388611 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -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() }); diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index fb99f7174436d7466d5b29c44c880e709fa76bee..418b7729f4bca1bbbfc5b51b833a262e29343ecb 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -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" diff --git a/crates/gpui/build.rs b/crates/gpui/build.rs index c0ee6037609e487395c1fcc83ed5f12a0da243ab..aed439744044574c87e8873e0d06f1c5cc68ec26 100644 --- a/crates/gpui/build.rs +++ b/crates/gpui/build.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"); diff --git a/crates/gpui/src/action.rs b/crates/gpui/src/action.rs index 24fbd70b63d87f564301153073c5ebe7b7fdaa32..7885497034c1a9b0e3404503d36e9f4fdc24b276 100644 --- a/crates/gpui/src/action.rs +++ b/crates/gpui/src/action.rs @@ -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 + fn action_json_schema(_: &mut schemars::SchemaGenerator) -> Option 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, + pub json_schema: fn(&mut schemars::SchemaGenerator) -> Option, } /// 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, + pub json_schema: fn(&mut schemars::SchemaGenerator) -> Option, 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)> { + generator: &mut schemars::SchemaGenerator, + ) -> Vec<(&'static str, Option)> { // Use the order from all_names so that the resulting schema has sensible order. self.all_names .iter() diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 1853e6e93488e0cba9db2380594eb3f28b4a0132..2ecc3dadaa294783a23cb523b7beb79af3506200 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -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> { + self.keymap.clone() + } + /// Register a global listener for actions invoked via the keyboard. pub fn on_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)> { + generator: &mut schemars::SchemaGenerator, + ) -> Vec<(&'static str, Option)> { self.actions.action_schemas(generator) } diff --git a/crates/gpui/src/arena.rs b/crates/gpui/src/arena.rs index 2448746a8867b88cc7e6b22b27a6ef5eae6c40aa..ee72d0e96425816220094f4cbff86315153afb74 100644 --- a/crates/gpui/src/arena.rs +++ b/crates/gpui/src/arena.rs @@ -214,32 +214,6 @@ impl DerefMut for ArenaBox { } } -pub struct ArenaRef(ArenaBox); - -impl From> for ArenaRef { - fn from(value: ArenaBox) -> Self { - ArenaRef(value) - } -} - -impl Clone for ArenaRef { - fn clone(&self) -> Self { - Self(ArenaBox { - ptr: self.0.ptr, - valid: self.0.valid.clone(), - }) - } -} - -impl Deref for ArenaRef { - type Target = T; - - #[inline(always)] - fn deref(&self) -> &Self::Target { - self.0.deref() - } -} - #[cfg(test)] mod tests { use std::{cell::Cell, rc::Rc}; diff --git a/crates/gpui/src/color.rs b/crates/gpui/src/color.rs index 1115d1c99c8c8edc1a43f92c4746470c800b7bef..7fc9c24393907d3991edcf9ae82b25eee419e766 100644 --- a/crates/gpui/src/color.rs +++ b/crates/gpui/src/color.rs @@ -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 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) } } diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index bbc3454923c488c9b9120a7a762ed5b85fba28ea..6e05b384e15492f6ebd137004f0f13fd4a6d549c 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -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 { + 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 { + 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; pub(crate) type MouseUpListener = @@ -2777,126 +2776,6 @@ impl GroupHitboxes { } } -/// A wrapper around an element that can be focused. -pub struct FocusableWrapper { - /// The element that is focusable - pub element: E, -} - -impl FocusableElement for FocusableWrapper {} - -impl InteractiveElement for FocusableWrapper -where - E: InteractiveElement, -{ - fn interactivity(&mut self) -> &mut Interactivity { - self.element.interactivity() - } -} - -impl StatefulInteractiveElement for FocusableWrapper {} - -impl Styled for FocusableWrapper -where - E: Styled, -{ - fn style(&mut self) -> &mut StyleRefinement { - self.element.style() - } -} - -impl FocusableWrapper
{ - /// 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>, &mut Window, &mut App) + 'static, - ) -> Self { - self.element = self.element.on_children_prepainted(listener); - self - } -} - -impl Element for FocusableWrapper -where - E: Element, -{ - type RequestLayoutState = E::RequestLayoutState; - type PrepaintState = E::PrepaintState; - - fn id(&self) -> Option { - 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, - 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, - 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 IntoElement for FocusableWrapper -where - E: IntoElement, -{ - type Element = E::Element; - - fn into_element(self) -> Self::Element { - self.element.into_element() - } -} - -impl ParentElement for FocusableWrapper -where - E: ParentElement, -{ - fn extend(&mut self, elements: impl IntoIterator) { - self.element.extend(elements) - } -} - /// A wrapper around an element that can store state, produced after assigning an ElementId. pub struct Stateful { pub(crate) element: E, @@ -2927,8 +2806,6 @@ where } } -impl FocusableElement for Stateful {} - impl Element for Stateful where E: Element, diff --git a/crates/gpui/src/elements/img.rs b/crates/gpui/src/elements/img.rs index c6130667776351bf4aa2230a1c37454f598320a3..993b319b697ece386ad8af6d6164c1b85bf3a1c7 100644 --- a/crates/gpui/src/elements/img.rs +++ b/crates/gpui/src/elements/img.rs @@ -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 { diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 6b9df6ab29a3a1680c01ae2bdc5c4cf854f6dbdd..c9a08e968a46774bc36c11d461738196483c30aa 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -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, diff --git a/crates/gpui/src/geometry.rs b/crates/gpui/src/geometry.rs index 30283c8ddedd01744660f5fd08ab8597ed670a56..74be6344f92a2c478318641be5a78eb7bacfe28e 100644 --- a/crates/gpui/src/geometry.rs +++ b/crates/gpui/src/geometry.rs @@ -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|%))$" }) } } diff --git a/crates/gpui/src/keymap/binding.rs b/crates/gpui/src/keymap/binding.rs index ffc4656ff7d30e43c553d7f208e0ee1bb668684d..1d3f612c5bef76d75cb1bd8ee9d9c686190c3fd7 100644 --- a/crates/gpui/src/keymap/binding.rs +++ b/crates/gpui/src/keymap/binding.rs @@ -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>, pub(crate) meta: Option, + /// The json input string used when building the keybinding, if any + pub(crate) action_input: Option, } 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, context_predicate: Option>, key_equivalents: Option<&HashMap>, + action_input: Option, ) -> std::result::Result { 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 { self.meta } + + /// Get the action input associated with the action for this binding + pub fn action_input(&self) -> Option { + self.action_input.clone() + } } impl std::fmt::Debug for KeyBinding { diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index ac21c5dea12c783790d3877735a7074f7dbd9c95..277f2d9ab8762c43473c7c07ef58a3c5188d091b 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -151,7 +151,7 @@ pub fn guess_compositor() -> &'static str { pub(crate) fn current_platform(_headless: bool) -> Rc { 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(), ) } diff --git a/crates/gpui/src/platform/blade/apple_compat.rs b/crates/gpui/src/platform/blade/apple_compat.rs index b1baab8854aca67dd25b70c3f03e288edeeab6dc..a75ddfa69a3daa2e43eaf00673a34d8c22e1cd25 100644 --- a/crates/gpui/src/platform/blade/apple_compat.rs +++ b/crates/gpui/src/platform/blade/apple_compat.rs @@ -29,14 +29,14 @@ pub unsafe fn new_renderer( } impl rwh::HasWindowHandle for RawWindow { - fn window_handle(&self) -> Result { + fn window_handle(&self) -> Result, 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 { + fn display_handle(&self) -> Result, rwh::HandleError> { let handle = rwh::AppKitDisplayHandle::new(); Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) }) } diff --git a/crates/gpui/src/platform/linux/wayland/window.rs b/crates/gpui/src/platform/linux/wayland/window.rs index 9d602130d4c545213175b2bbbd088ec5f9062c1c..36e070b0b0fc03d1dd6cd3402eedd228dbc909e3 100644 --- a/crates/gpui/src/platform/linux/wayland/window.rs +++ b/crates/gpui/src/platform/linux/wayland/window.rs @@ -252,11 +252,11 @@ impl Drop for WaylandWindow { } impl WaylandWindow { - fn borrow(&self) -> Ref { + fn borrow(&self) -> Ref<'_, WaylandWindowState> { self.0.state.borrow() } - fn borrow_mut(&self) -> RefMut { + fn borrow_mut(&self) -> RefMut<'_, WaylandWindowState> { self.0.state.borrow_mut() } diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index 248911a5b97d08d2ceaf48db22c215480ea68db0..1a3c323c35129b9ea56595b7f81775de4b036454 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -288,7 +288,7 @@ pub(crate) struct X11WindowStatePtr { } impl rwh::HasWindowHandle for RawWindow { - fn window_handle(&self) -> Result { + fn window_handle(&self) -> Result, 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 { + fn display_handle(&self) -> Result, 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 { + fn window_handle(&self) -> Result, rwh::HandleError> { unimplemented!() } } impl rwh::HasDisplayHandle for X11Window { - fn display_handle(&self) -> Result { + fn display_handle(&self) -> Result, 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, -} - -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 { diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 82a43eb760bfb9bf1bd411aea455c2e2e864758a..aedf131909a6956e9a4501b107c81ce242b80a49 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -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, + blurred_view: Option, display_link: Option, renderer: renderer::Renderer, request_frame_callback: Option>, @@ -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::::from(size); - if lock.content_size() == new_size { + let old_size = unsafe { + let old_frame: NSRect = msg_send![this, frame]; + Size::::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); + } + } + } +} diff --git a/crates/gpui/src/platform/windows/events.rs b/crates/gpui/src/platform/windows/events.rs index 65565c6b3fc2e43ee9e8ef29cc131cc8c42c1355..d7205580cdc133fccbf97f1287651521ff7bb06b 100644 --- a/crates/gpui/src/platform/windows/events.rs +++ b/crates/gpui/src/platform/windows/events.rs @@ -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 diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index e84840fb25591ed0d25ab257b7db59eb0b7dd1b5..27c843932bb2a38f37f3b01b354a7eed7f8354fe 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/crates/gpui/src/platform/windows/window.rs @@ -1250,11 +1250,13 @@ fn set_window_composition_attribute(hwnd: HWND, color: Option, 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, state: u32 cb_data: std::mem::size_of::(), }; 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 { diff --git a/crates/gpui/src/prelude.rs b/crates/gpui/src/prelude.rs index 270f0a9341181e35669db16b9bddfb3aa6afd519..191d0a0e6d4019df9a6584fc2c15a406bbb08287 100644 --- a/crates/gpui/src/prelude.rs +++ b/crates/gpui/src/prelude.rs @@ -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, }; diff --git a/crates/gpui/src/shared_string.rs b/crates/gpui/src/shared_string.rs index 591bada48d7c0f200a83f5a5319e183e4ce2021f..c325f98cd243121264875d7a9452308772d49e86 100644 --- a/crates/gpui/src/shared_string.rs +++ b/crates/gpui/src/shared_string.rs @@ -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) } } diff --git a/crates/gpui/src/text_system/font_features.rs b/crates/gpui/src/text_system/font_features.rs index 9fca90380791160e31b3771eb061104c91bc2bd9..c1ab72b417a53b86a5b43f7a90aec9e439aa1fab 100644 --- a/crates/gpui/src/text_system/font_features.rs +++ b/crates/gpui/src/text_system/font_features.rs @@ -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 + }) } } diff --git a/crates/gpui/src/text_system/line_layout.rs b/crates/gpui/src/text_system/line_layout.rs index 5e5c2eff1e02e57b69726394722a99b63f35b1d2..5a72080e4809663679483b41b70cf84a69cc5a06 100644 --- a/crates/gpui/src/text_system/line_layout.rs +++ b/crates/gpui/src/text_system/line_layout.rs @@ -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)] diff --git a/crates/gpui/src/util.rs b/crates/gpui/src/util.rs index fda5e81333817b9ce7fc79311b1dc4a628208f58..5e92335fdc86e331d3a469c4384043fd9799b00a 100644 --- a/crates/gpui/src/util.rs +++ b/crates/gpui/src/util.rs @@ -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 { diff --git a/crates/gpui/tests/action_macros.rs b/crates/gpui/tests/action_macros.rs index f601639fc8dbc05afaeed997a0c0b17c5dfa5ea7..7bff3a97b1c3d22e6c0e9841a26f2adf5d7f3a70 100644 --- a/crates/gpui/tests/action_macros.rs +++ b/crates/gpui/tests/action_macros.rs @@ -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); diff --git a/crates/gpui_macros/src/derive_action.rs b/crates/gpui_macros/src/derive_action.rs index c382ddd9c652902e1c444f080a601de16bfeca9a..c32baba6cbacdb809623c43aef7ec362b963d178 100644 --- a/crates/gpui_macros/src/derive_action.rs +++ b/crates/gpui_macros/src/derive_action.rs @@ -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 { + _generator: &mut gpui::private::schemars::SchemaGenerator, + ) -> Option { #json_schema_fn_body } diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index 7e1d7db5753ff1517902880bda4c6c9e24cfe582..332e38b038a51f533f22cccc3c4ffec3d83a4898 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -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, diff --git a/crates/inline_completion_button/src/inline_completion_button.rs b/crates/inline_completion_button/src/inline_completion_button.rs index 4ff793cbaf47a80bff266d21aebd273849c97875..cf1e808f602803c971f2e7c604947ca5b7aee589 100644 --- a/crates/inline_completion_button/src/inline_completion_button.rs +++ b/crates/inline_completion_button/src/inline_completion_button.rs @@ -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::(fs, cx, move |file, _| { file.languages + .0 .entry(language.name()) .or_default() .show_edit_predictions = Some(!show_edit_predictions); diff --git a/crates/journal/src/journal.rs b/crates/journal/src/journal.rs index 0aed317a0b80f0d0bb52095a9d6d5f95489bce2f..08bdb8e04f620518ef7955361979f28d83353718 100644 --- a/crates/journal/src/journal.rs +++ b/crates/journal/src/journal.rs @@ -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::().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); } diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index b0e06c3d65a7bc05df0cb41104a1139353372539..477b978517d56d0f70270a4bf413b285b455ca94 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -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 diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index ebf7558abb28f8641baa0d52ba7f04e2af8289e9..6955cd054925076f8d2678eff58c44e0b82351d0 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -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 { diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index f77afc76d2ffae034e2f0d3d3d4d2507c919b518..1ad057ff41eb3eef961d687d9e7ee097c0364c43 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -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, Arc)>, + /// 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, /// 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, @@ -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, 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, D::Error> { + let sources = Vec::::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::")] pub struct BracketPairConfig { /// A list of character pairs that should be treated as brackets in the context of a given language. pub pairs: Vec, /// 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>, } @@ -1003,10 +1021,6 @@ impl BracketPairConfig { } } -fn bracket_pair_config_json_schema(r#gen: &mut SchemaGenerator) -> Schema { - Option::>::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 diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index b2bb684e1bb10d6edc72a41d3006d114a4b5f371..ff17d6dd9a9d7bb250f15d358d11eb23ef8f188f 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -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, diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 9dda60b6a685b7705563a7d218990af33b2577f2..bb143b38422b0b56c8a4713a94ccc68ed3c6d284 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -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, + pub languages: LanguageToSettingsMap, /// Settings for associating file extensions and filenames /// with languages. #[serde(default)] pub file_types: HashMap, Vec>, } +/// 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); + +inventory::submit! { + ParameterizedJsonSchema { + add_and_get_ref: |generator, params, _cx| { + let language_settings_content_ref = generator + .subschema_for::() + .to_value(); + let schema = json_schema!({ + "type": "object", + "properties": params + .language_names + .iter() + .map(|name| { + ( + name.clone(), + language_settings_content_ref.clone(), + ) + }) + .collect::>() + }); + replace_subschema::(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), } 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 = + let ret: Result, _> = 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 = + let ret: Result, _> = 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 = + let ret: Result, _> = 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), } 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(deserializer: D) -> std::result::Result 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 = + let ret: Result, _> = 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 = + let ret: Result, _> = 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 = + let ret: Result, _> = 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); - -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::(); - - // 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 + ])) ); } diff --git a/crates/language_model/src/language_model.rs b/crates/language_model/src/language_model.rs index f84357bd98936e478a826df9a4d0563f2c857e10..8ecb4056ea582202f0ea83bd81c8cfb051cb4b0b 100644 --- a/crates/language_model/src/language_model.rs +++ b/crates/language_model/src/language_model.rs @@ -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, cx: &mut App) { init_settings(cx); @@ -71,6 +83,12 @@ pub enum LanguageModelCompletionEvent { data: String, }, ToolUse(LanguageModelToolUse), + ToolUseJsonParseError { + id: LanguageModelToolUseId, + tool_name: Arc, + raw_input: Arc, + 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, - raw_input: Arc, - json_parse_error: String, + #[error("prompt too large for context window")] + PromptTooLarge { tokens: Option }, + #[error("missing {provider} API key")] + NoApiKey { provider: LanguageModelProviderName }, + #[error("{provider}'s API rate limit exceeded")] + RateLimitExceeded { + provider: LanguageModelProviderName, + retry_after: Option, + }, + #[error("{provider}'s API servers are overloaded right now")] + ServerOverloaded { + provider: LanguageModelProviderName, + retry_after: Option, + }, + #[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 }, - #[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, + ) -> 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, + ) -> 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 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 for LanguageModelCompletionError { impl From 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 { @@ -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 { - 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 for LanguageModelId { fn from(value: String) -> Self { Self(SharedString::from(value)) diff --git a/crates/language_model/src/registry.rs b/crates/language_model/src/registry.rs index e9f03cc1ff49ad82d109d67236406b5444faf982..840fda38dec4714a32f3397a28dd2d116bb67f5d 100644 --- a/crates/language_model/src/registry.rs +++ b/crates/language_model/src/registry.rs @@ -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 } } diff --git a/crates/language_model/src/telemetry.rs b/crates/language_model/src/telemetry.rs index 9bd9b903c20e1b314a9e1f945ca1ddd1bb608279..ccdcb0ad0cdf0d830d0163f39afad478377fe01d 100644 --- a/crates/language_model/src/telemetry.rs +++ b/crates/language_model/src/telemetry.rs @@ -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>, @@ -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 { diff --git a/crates/language_models/Cargo.toml b/crates/language_models/Cargo.toml index d6aff380aab1696b0f71f0b83ed876cc1e756ecb..0f248edd574819aee9ac1311ed23de30be48b21e 100644 --- a/crates/language_models/Cargo.toml +++ b/crates/language_models/Cargo.toml @@ -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"] } diff --git a/crates/language_models/src/provider/anthropic.rs b/crates/language_models/src/provider/anthropic.rs index 48bea47fec02a0cb5b51b59883492caf00d1c982..6ddb1a438108bd6611d9139a042f297b3481549b 100644 --- a/crates/language_models/src/provider/anthropic.rs +++ b/crates/language_models/src/provider/anthropic.rs @@ -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] diff --git a/crates/language_models/src/provider/bedrock.rs b/crates/language_models/src/provider/bedrock.rs index a55fc5bc1142dcacf1d6cf5193345f6904c76b37..9c0d48160701f82bde79c55ac4b3a3f168a99d3d 100644 --- a/crates/language_models/src/provider/bedrock.rs +++ b/crates/language_models/src/provider/bedrock.rs @@ -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>>, + ) -> BoxFuture< + 'static, + Result>>, > { - 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>>>, - handle: Handle, ) -> impl Stream> { struct RawToolUse { id: String, @@ -892,198 +888,123 @@ pub fn map_to_language_model_completion_events( tool_uses_by_index: HashMap, } - 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 { diff --git a/crates/language_models/src/provider/cloud.rs b/crates/language_models/src/provider/cloud.rs index 58902850ea1d66d843306e9612a0ed2538a29ac9..505caa2e42b27f21e07cda9dc55252dfdde403b1 100644 --- a/crates/language_models/src/provider/cloud.rs +++ b/crates/language_models/src/provider/cloud.rs @@ -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, status: client::Status, - accept_terms: Option>>, + accept_terms_of_service_task: Option>>, models: Vec>, default_model: Option>, default_fast_model: Option>, @@ -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) { 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> { @@ -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 { - 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> { @@ -429,18 +438,12 @@ impl LanguageModelProvider for CloudLanguageModelProvider { } fn render_accept_terms( - state: Entity, view_kind: LanguageModelProviderTosView, - cx: &mut App, -) -> Option { - 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, 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, +} + +impl From 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::() { - 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( ) } -struct ConfigurationView { - state: gpui::Entity, +#[derive(IntoElement, RegisterComponent)] +struct ZedAiConfiguration { + is_connected: bool, + plan: Option, + subscription_period: Option<(DateTime, DateTime)>, + eligible_for_trial: bool, + has_accepted_terms_of_service: bool, + accept_terms_of_service_in_progress: bool, + accept_terms_of_service_callback: Arc, + sign_in_callback: Arc, } -impl ConfigurationView { - fn authenticate(&mut self, cx: &mut Context) { - 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) -> 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, + accept_terms_of_service_callback: Arc, + sign_in_callback: Arc, +} + +impl ConfigurationView { + fn new(state: Entity) -> 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) -> 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 { + fn configuration( + is_connected: bool, + plan: Option, + 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(), + ) + } +} diff --git a/crates/language_models/src/provider/copilot_chat.rs b/crates/language_models/src/provider/copilot_chat.rs index b00ec7570cd65111048f679381bf57d544c1ef03..5411fbc63c10d84d96e2d85bf77c453bf66b5411 100644 --- a/crates/language_models/src/provider/copilot_chat.rs +++ b/crates/language_models/src/provider/copilot_chat.rs @@ -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, @@ -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(), + }, + ), + } }, )); diff --git a/crates/language_models/src/provider/deepseek.rs b/crates/language_models/src/provider/deepseek.rs index 99a1ca70c6e9ced064c76d4ede427e3b2f5ace0f..a568ef4034193b5b1078d2ec4907d18fb0762efa 100644 --- a/crates/language_models/src/provider/deepseek.rs +++ b/crates/language_models/src/provider/deepseek.rs @@ -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> { 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(), diff --git a/crates/language_models/src/provider/google.rs b/crates/language_models/src/provider/google.rs index 597279852307635f85ae2024fdc516c81accbbfa..bb19a3901a10416abc655ae21f0288bc1b6f436c 100644 --- a/crates/language_models/src/provider/google.rs +++ b/crates/language_models/src/provider/google.rs @@ -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))], }) diff --git a/crates/language_models/src/provider/lmstudio.rs b/crates/language_models/src/provider/lmstudio.rs index e0fcf38f38e1eb46f22eed8705344389dcb31848..01600f3646da5091796adacd90db2d18f0042b1e 100644 --- a/crates/language_models/src/provider/lmstudio.rs +++ b/crates/language_models/src/provider/lmstudio.rs @@ -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> { 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 => {} diff --git a/crates/language_models/src/provider/mistral.rs b/crates/language_models/src/provider/mistral.rs index 171ce058968afe2f8bc16326fc841e3ea6b804de..c58622d4e0bddb30981d7edc519ca8c5b7c21513 100644 --- a/crates/language_models/src/provider/mistral.rs +++ b/crates/language_models/src/provider/mistral.rs @@ -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>>>, - ) -> impl futures::Stream> + events: Pin>>>, + ) -> impl Stream> { 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> { 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(), + })) + } } } diff --git a/crates/language_models/src/provider/ollama.rs b/crates/language_models/src/provider/ollama.rs index 205dab6c87d234d09cd425ba8882bb303f3dc4f0..0866cfa4c83f645a28b8052d86c244ed313cd74f 100644 --- a/crates/language_models/src/provider/ollama.rs +++ b/crates/language_models/src/provider/ollama.rs @@ -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)); } }; diff --git a/crates/language_models/src/provider/open_ai.rs b/crates/language_models/src/provider/open_ai.rs index ad4203ff81c5ec28e98bbf6eab0e4f3e23b7f604..476c1715ae2e65971227e86fb2087c99284cf969 100644 --- a/crates/language_models/src/provider/open_ai.rs +++ b/crates/language_models/src/provider/open_ai.rs @@ -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(), }), } diff --git a/crates/language_models/src/provider/open_router.rs b/crates/language_models/src/provider/open_router.rs index b447ee1bd72b4d13bbcf04da15ca5c26037e6405..5883da1e2f7871122e91ced23f41c8e9b75fc59f 100644 --- a/crates/language_models/src/provider/open_router.rs +++ b/crates/language_models/src/provider/open_router.rs @@ -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> { 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(), }), } diff --git a/crates/language_models/src/provider/vercel.rs b/crates/language_models/src/provider/vercel.rs index 2f64115d2096c4bd4214d43d0a010995fb2edd15..037ce467d03eb00a3e1ed272a6d2de80bb51e200 100644 --- a/crates/language_models/src/provider/vercel.rs +++ b/crates/language_models/src/provider/vercel.rs @@ -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 { diff --git a/crates/language_tools/Cargo.toml b/crates/language_tools/Cargo.toml index 3a0f487f7a17ddc3a43550a998590c5aa937a19a..ffdc939809145b319d5421adf5b8a923604e74fe 100644 --- a/crates/language_tools/Cargo.toml +++ b/crates/language_tools/Cargo.toml @@ -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 diff --git a/crates/language_tools/src/lsp_tool.rs b/crates/language_tools/src/lsp_tool.rs index fc1efc7794eb33986cb26ecbd0941075111da700..899aaf0679689c344b3fe6dcac15d76d40009b5c 100644 --- a/crates/language_tools/src/lsp_tool.rs +++ b/crates/language_tools/src/lsp_tool.rs @@ -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, + popover_menu_handle: PopoverMenuHandle>, lsp_picker: Option>>, _subscriptions: Vec, } @@ -31,7 +36,7 @@ struct PickerState { } #[derive(Debug)] -struct LspPickerDelegate { +pub struct LspPickerDelegate { state: Entity, selected_index: usize, items: Vec, @@ -64,6 +69,23 @@ struct LanguageServerBinaryStatus { message: Option, } +#[derive(Debug)] +struct ServerInfo { + name: LanguageServerName, + id: Option, + health: Option, + binary_status: Option, + message: Option, +} + +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 { 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 { + 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, + &'a LanguageServerName, + &'a LanguageServerBinaryStatus, + ), } #[derive(Debug)] @@ -263,15 +359,21 @@ enum LspItem { LanguageServerHealthStatus, Option, ), - WithBinaryStatus(LanguageServerName, LanguageServerBinaryStatus), - Header(SharedString), + WithBinaryStatus( + Option, + 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>) {} + fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { + 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::().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>) { 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>, ) -> Option { - 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::().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::>() - } else { - editor_buffers - .iter() - .flat_map(|buffer_id| { - buffer_store.read(cx).get(*buffer_id) - }) - .collect::>() - }; - 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>) -> Option { - 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 { 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 { + pub fn new( + workspace: &Workspace, + popover_menu_handle: PopoverMenuHandle>, + window: &mut Window, + cx: &mut Context, + ) -> Self { let settings_subscription = cx.observe_global_in::(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) -> 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), ) } diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index 99132ce452e4680c8a7302f4c1afbc9d62b613a9..6f74e76e261b7b5f33463fe7932c7eaf0fa2a9fe 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -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]); diff --git a/crates/languages/src/gitcommit/config.toml b/crates/languages/src/gitcommit/config.toml index ae4b836ed66dc7558cf7b033e555dd2dba3b309c..c2421ce00613e5848aacab5d1230ab839c8b1388 100644 --- a/crates/languages/src/gitcommit/config.toml +++ b/crates/languages/src/gitcommit/config.toml @@ -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+" +] diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 7a52a82f6b07699f7eeea23dfb346ae99b4b8c11..bd950f34f5bda478ea6c6a0a3f0e85b25dd54e49 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -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::(); + .root_schema_for::(); serde_json_lenient::to_value(schema).unwrap() } diff --git a/crates/languages/src/markdown/config.toml b/crates/languages/src/markdown/config.toml index 00c4fafecd28971aa5439113916dadf248da6045..059e52de9444b10cb8d6b089a2bdf8ec6d49485d 100644 --- a/crates/languages/src/markdown/config.toml +++ b/crates/languages/src/markdown/config.toml @@ -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 diff --git a/crates/languages/src/python/highlights.scm b/crates/languages/src/python/highlights.scm index 97d5fb52755c7c9e25d1016f085dc9660a081f30..77db9b2f4c17519e966b68c44fede2aa9bc4c29f 100644 --- a/crates/languages/src/python/highlights.scm +++ b/crates/languages/src/python/highlights.scm @@ -226,6 +226,12 @@ ">>" "|" "~" + "&=" + "<<=" + ">>=" + "@=" + "^=" + "|=" ] @operator [ diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index 0a0326f4f78c6e154d559cbe1bfcad3f436c3e84..4a9626c8b82b49abd54344a53c5ed5177f94393c 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -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| { diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 28ad606132fcc61fc5e801c8442dcc62fad45357..53dc24a21a93fecee9a320a44a9b9c46655f31be 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -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 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)) diff --git a/crates/markdown/examples/markdown.rs b/crates/markdown/examples/markdown.rs index 16387a8000c2bbe1ae38a9979f96e5e1b1dda85d..bf685bd9acfe9f678454dffc538ca66e3ca0910d 100644 --- a/crates/markdown/examples/markdown.rs +++ b/crates/markdown/examples/markdown.rs @@ -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() }; diff --git a/crates/markdown/examples/markdown_as_child.rs b/crates/markdown/examples/markdown_as_child.rs index 62a35629b1dedfb33b08c0ce52d5fab94ee18ccf..862b657c8c50c7adc88642f1af21a4c075ff77f2 100644 --- a/crates/markdown/examples/markdown_as_child.rs +++ b/crates/markdown/examples/markdown_as_child.rs @@ -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() }; diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index ac959d13b5c0236b0d4b3caf99df1970d2f73031..9c057baec97840d81317d828f635a9aad0f6c9fc 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -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) { diff --git a/crates/markdown_preview/src/markdown_preview_view.rs b/crates/markdown_preview/src/markdown_preview_view.rs index 40c1783482f8b2a91126962d58b84b495e96a039..f22671d5dfaf2badafb9a7be5b372c91bd0b1ef6 100644 --- a/crates/markdown_preview/src/markdown_preview_view.rs +++ b/crates/markdown_preview/src/markdown_preview_view.rs @@ -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, selected_block: usize, list_state: ListState, - tab_content_text: Option, language_registry: Arc, parsing_markdown_task: Option>>, 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, workspace: WeakEntity, language_registry: Arc, - tab_content_text: Option, window: &mut Window, cx: &mut Context, ) -> Entity { @@ -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 for MarkdownPreviewView {} +impl EventEmitter<()> for MarkdownPreviewView {} impl Item for MarkdownPreviewView { - type Event = PreviewEvent; + type Event = (); fn tab_icon(&self, _window: &Window, _cx: &App) -> Option { 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")) } diff --git a/crates/migrator/src/migrations.rs b/crates/migrator/src/migrations.rs index d43521faa958782f902151b99233f511732786fb..84ef0f0456e65da7dbb604e7733fec8ab1cdd386 100644 --- a/crates/migrator/src/migrations.rs +++ b/crates/migrator/src/migrations.rs @@ -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; diff --git a/crates/migrator/src/migrations/m_2025_06_25/settings.rs b/crates/migrator/src/migrations/m_2025_06_25/settings.rs deleted file mode 100644 index 5dd6c3093a43b00acff3db6c1e316a3fc6664175..0000000000000000000000000000000000000000 --- a/crates/migrator/src/migrations/m_2025_06_25/settings.rs +++ /dev/null @@ -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, 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, 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, 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())) -} diff --git a/crates/migrator/src/migrations/m_2025_06_27/settings.rs b/crates/migrator/src/migrations/m_2025_06_27/settings.rs new file mode 100644 index 0000000000000000000000000000000000000000..6156308fcec05dfb10b5b258d31077e5d4b09adc --- /dev/null +++ b/crates/migrator/src/migrations/m_2025_06_27/settings.rs @@ -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, 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 +} diff --git a/crates/migrator/src/migrator.rs b/crates/migrator/src/migrator.rs index bcd41836e6f8d1d3dabf1f37c5ba456d475f12e1..06e96a6f865c227579ab2452426b5d8cf46fda7c 100644 --- a/crates/migrator/src/migrator.rs +++ b/crates/migrator/src/migrator.rs @@ -153,8 +153,8 @@ pub fn migrate_settings(text: &str) -> Result> { &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=" + ], + "env": { + "SUPABASE_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=" + ], + "env": { + "SUPABASE_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" + } + } +}"#, + ), ); } } diff --git a/crates/multi_buffer/src/position.rs b/crates/multi_buffer/src/position.rs index 1ed2fe56e4d775a8b55d311262c77ec2397592b6..06508750597b97d7275b964114bcdad0d0e34c79 100644 --- a/crates/multi_buffer/src/position.rs +++ b/crates/multi_buffer/src/position.rs @@ -126,17 +126,17 @@ impl Default for TypedRow { impl PartialOrd for TypedOffset { fn partial_cmp(&self, other: &Self) -> Option { - Some(self.value.cmp(&other.value)) + Some(self.cmp(&other)) } } impl PartialOrd for TypedPoint { fn partial_cmp(&self, other: &Self) -> Option { - Some(self.value.cmp(&other.value)) + Some(self.cmp(&other)) } } impl PartialOrd for TypedRow { fn partial_cmp(&self, other: &Self) -> Option { - Some(self.value.cmp(&other.value)) + Some(self.cmp(&other)) } } diff --git a/crates/open_ai/src/open_ai.rs b/crates/open_ai/src/open_ai.rs index 5b09aa5cbc17a0c48e4a1fadcbdd0b44cba98e1c..12a5cf52d2efe7bf1d94bfc45ed629e38bc94382 100644 --- a/crates/open_ai/src/open_ai.rs +++ b/crates/open_ai/src/open_ai.rs @@ -445,12 +445,14 @@ pub async fn stream_completion( match serde_json::from_str::(&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, ), diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 3fec1d616ab5cbe577d4f3fec7fff1449c62fec6..8c5e78d77bce76e62ef94d2501dbef588cd76f00 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -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::() .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::(); window.focus(&active_editor.focus_handle(cx)); } diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 5bb771c1e9fc8e1e7d605e1583b52137f0181bd4..0be05d458908e3d7b1317ea205664a349eb6ef5f 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -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)), diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index c1ebe25538c4db1f02539f5138c065661be47085..4a122ac7316ed1a7552eda41ef223c62bc3ba910 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -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 Picker { 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)), + ); }); } } diff --git a/crates/project/src/context_server_store.rs b/crates/project/src/context_server_store.rs index 3bde9d6b36b42fe30aaf0f0fce903c3c0e373f3f..6f93238cc9b6f8d6fad08e25baa819eae4ef9b4b 100644 --- a/crates/project/src/context_server_store.rs +++ b/crates/project/src/context_server_store.rs @@ -135,6 +135,7 @@ pub type ContextServerFactory = Box) -> Arc>; pub struct ContextServerStore { + context_server_settings: HashMap, ContextServerSettings>, servers: HashMap, worktree_store: Entity, registry: Entity, @@ -202,6 +203,11 @@ impl ContextServerStore { this.available_context_servers_changed(cx); }), cx.observe_global::(|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, cx: &'a App, ) -> &'a HashMap, 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, 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] diff --git a/crates/project/src/debugger/locators/cargo.rs b/crates/project/src/debugger/locators/cargo.rs index 4cf4233ca382f7fecffc7ddf5fdbd40b008bb2f4..caa563eb6b88b3f1c2692dab36897e7094186987 100644 --- a/crates/project/src/debugger/locators/cargo.rs +++ b/crates/project/src/debugger/locators/cargo.rs @@ -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, }) } diff --git a/crates/project/src/debugger/locators/go.rs b/crates/project/src/debugger/locators/go.rs index 79d7a1721c5f4013443bdbda7970571377c6a65d..61436fce8f3659d4b12c3010b82e0d845654c4e9 100644 --- a/crates/project/src/debugger/locators/go.rs +++ b/crates/project/src/debugger/locators/go.rs @@ -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); diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs index 300c598bfb9e1daa198baecda0ce5ef5c08aa3e7..837bc4b81c5b3bc1fe7e54d9bc4fbbc42eeaede2 100644 --- a/crates/project/src/debugger/session.rs +++ b/crates/project/src/debugger/session.rs @@ -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::>(); + 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(_) => {} diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs index 7002f83ab35bc9f9aa500fd1d96aded03df072c9..9ff3823e0f13a87fdcff944db7ad2d52350a7cce 100644 --- a/crates/project/src/git_store.rs +++ b/crates/project/src/git_store.rs @@ -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 diff --git a/crates/project/src/git_store/conflict_set.rs b/crates/project/src/git_store/conflict_set.rs index e78a70f2754a905ca465ad07ad365b04638e7c5f..27b191f65f896e6488a4d9c52f37e9426cac1c46 100644 --- a/crates/project/src/git_store/conflict_set.rs +++ b/crates/project/src/git_store/conflict_set.rs @@ -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(); diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 18164b4bcb68613987b4c938eaa317f8d6564c7b..cdeb9f71c1ec18dee0b8a21f1cfa84ceb2c8c453 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -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( diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index d6f5d7a3cc98a872a1ce6822c88b6fee8599540e..9e1a38a6d165c74a9ec1d340927b9875f464c5ec 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -170,6 +170,7 @@ pub struct LocalLspStore { _subscription: gpui::Subscription, lsp_tree: Entity, registered_buffers: HashMap, + buffers_opened_in_servers: HashMap>, buffer_pull_diagnostics_result_ids: HashMap>>, } @@ -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, HashMap>>, - lsp_data: Option, + lsp_data: HashMap, } -type DocumentColorTask = Shared, Arc>>>; - -#[derive(Debug)] -struct LspData { - mtime: MTime, - buffer_lsp_data: HashMap>, - colors_update: HashMap, - last_version_queried: HashMap, +#[derive(Debug, Default, Clone)] +pub struct DocumentColors { + pub colors: HashSet, + pub cache_version: Option, } +type DocumentColorTask = Shared>>>; + #[derive(Debug, Default)] -struct BufferLspData { - colors: Option>, +struct DocumentColorData { + colors_for_version: Global, + colors: HashMap>, + cache_version: usize, + colors_update: Option<(Global, DocumentColorTask)>, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum ColorFetchStrategy { + IgnoreCache, + UseCache { known_cache_version: Option }, } #[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, + fetch_strategy: ColorFetchStrategy, buffer: Entity, cx: &mut Context, ) -> Option { - 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::>(); - - 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::>(); + 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, cx: &mut Context, - ) -> Task)>>> { + ) -> Task>>> { 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::>() }); @@ -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); + } } } diff --git a/crates/project/src/lsp_store/lsp_ext_command.rs b/crates/project/src/lsp_store/lsp_ext_command.rs index 2b6d11ceb92aee19240f10b2c140e3d48f3b9586..cb13fa5efcfd753e0ffb12fbcc0f3d84e09ff370 100644 --- a/crates/project/src/lsp_store/lsp_ext_command.rs +++ b/crates/project/src/lsp_store/lsp_ext_command.rs @@ -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, diff --git a/crates/project/src/prettier_store.rs b/crates/project/src/prettier_store.rs index 32cadd7ecf06e83ac411c818d6cf038998f9303b..68a3ae8778c351b9290dccdc5355f0b3eb22562b 100644 --- a/crates/project/src/prettier_store.rs +++ b/crates/project/src/prettier_store.rs @@ -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::>(); 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>, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index cdf66610633178d24637b752ea04de97c205ebca..060e7c0415ba86052e78ba45e5d8fe581f0761ce 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -779,13 +779,42 @@ pub struct DocumentColor { pub color_presentations: Vec, } -#[derive(Clone, Debug, PartialEq)] +impl Eq for DocumentColor {} + +impl std::hash::Hash for DocumentColor { + fn hash(&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, pub additional_text_edits: Vec, } +impl std::hash::Hash for ColorPresentation { + fn hash(&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), @@ -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(_) => {} + }, } } diff --git a/crates/project/src/project_settings.rs b/crates/project/src/project_settings.rs index d2a4e5126c973bf9a1454c0a96c17e17c4c593e2..1c35f1652232113ed83c41fc6dee3d6b32251358 100644 --- a/crates/project/src/project_settings.rs +++ b/crates/project/src/project_settings.rs @@ -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), LocalTasksUpdated(Result), + LocalDebugScenariosUpdated(Result), } impl EventEmitter for SettingsObserver {} @@ -566,6 +565,7 @@ pub struct SettingsObserver { project_id: u64, task_store: Entity, _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, + file_path: PathBuf, + cx: &mut Context, + ) -> 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 { diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 19b88c069554a483c0412bc313dec6f4f0350055..3e9a2c3273c3fa13d45286311fb7ceb80451f9b2 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -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::(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::(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; } diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 00e12a312f860efde4dee562c6efd0f748650843..b4e1093293b6275b9da68075425dd3b75b5bb335 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -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 diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 3bcc881f9d8a39ddbf1285e0deffe6b2907a4aa5..4db83bcf4c897d3a9bddf304ee96b3de600899bb 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -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)); diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index a9ba14264ff4a1c30536f6b400f0336bc49a1631..47aed8f470f3538f34bff0a0accdd55d9f1ac70e 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -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::(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(()) diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index e3aeb557ab3f55307211e9419176a642f3174097..918ac9e93596ce5de102f841ab95073778aab056 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -632,7 +632,7 @@ impl From for SystemTime { impl From 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(), diff --git a/crates/recent_projects/src/ssh_connections.rs b/crates/recent_projects/src/ssh_connections.rs index 070d8dc4e35295f43d3dbad37e4ce6ea3bd9d4be..5a38e1aadb00e75f9139eb4eccdacacb5e967593 100644 --- a/crates/recent_projects/src/ssh_connections.rs +++ b/crates/recent_projects/src/ssh_connections.rs @@ -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() }; diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs index e9805b12a214509d6c9ba2b04c2183d4a7a331e0..9730984f2632be65330203fcd93350cf29233435 100644 --- a/crates/remote_server/src/remote_editing_tests.rs +++ b/crates/remote_server/src/remote_editing_tests.rs @@ -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, ) }); diff --git a/crates/repl/src/session.rs b/crates/repl/src/session.rs index 20518fb12cc39c54993a077decd0ee1ff5f81c8b..18d41f3eae97ce4288d95e1e0eabb57d4b47adec 100644 --- a/crates/repl/src/session.rs +++ b/crates/repl/src/session.rs @@ -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| { diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs index 231647ef5a930da03a50b21eb571d0f19e039e7a..5e249162d3286e777ba28f8c645f8e2918bc9acf 100644 --- a/crates/rules_library/src/rules_library.rs +++ b/crates/rules_library/src/rules_library.rs @@ -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]); + }, + ); }); } _ => {} diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index fa7a3ba915896d52f1d2f60f55d5ab13746edda8..28d61c135772410c8fcf57c2dc501fae24933d99 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -101,7 +101,7 @@ pub struct BufferSearchBar { search_options: SearchOptions, default_options: SearchOptions, configured_options: SearchOptions, - query_contains_error: bool, + query_error: Option, 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), diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 79ae18fcfe1dd6ddb443b0afe00153a7ec472e31..dd440e0639c5d235ef39398a98f8014c30ad06c2 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -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>, _subscriptions: Vec, + query_error: Option, } #[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.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) } diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index 7817169aa299be14902ff83ef8d34b39c4e19049..892d4dea8b2daac7395bcbe273635fbb535a0e53 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -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"] } diff --git a/crates/settings/src/json_schema.rs b/crates/settings/src/json_schema.rs deleted file mode 100644 index 5fd340fffa8f9d3e60d3910cfe9e1d2506fded5a..0000000000000000000000000000000000000000 --- a/crates/settings/src/json_schema.rs +++ /dev/null @@ -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. - } - } - } -} diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 551920c8a038d2b3c3ad2432bbfa0da0b857fcac..fd35cc6116fd32406ad41afa289f52557ed5f5c2 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -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::(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)>, + mut generator: schemars::SchemaGenerator, + action_schemas: Vec<(&'static str, Option)>, deprecations: &HashMap<&'static str, &'static str>, deprecation_messages: &HashMap<&'static str, &'static str>, ) -> serde_json::Value { - fn set(input: I) -> Option - where - I: Into, - { - 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::(); - 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::().to_value() } pub fn sections(&self) -> impl DoubleEndedIterator { @@ -629,9 +591,162 @@ impl KeymapFile { } } } + + pub fn update_keybinding<'a>( + mut operation: KeybindUpdateOperation<'a>, + mut keymap_contents: String, + tab_size: usize, + ) -> Result { + // 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::, _>>() + 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 { + let action_name: Value = self.action_name.into(); + let value = match self.input { + Some(input) => { + let input = serde_json::from_str::(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 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 { + 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(), + ); + } } diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index a01414b0b29f95dbadac88d5f577e5b0809322ff..f690a2ea936c6516b6d4a60701a7cce89fa50cb2 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -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}; diff --git a/crates/settings/src/settings_file.rs b/crates/settings/src/settings_file.rs index 0fcdcde8ad886e6a6969d3c23e958a67396cb399..c43f3e79e8cf2edcee9d49e5dc3268295ff41439 100644 --- a/crates/settings/src/settings_file.rs +++ b/crates/settings/src/settings_file.rs @@ -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::( - crate::default_settings().as_ref(), - ) - .unwrap(); + let mut value = + crate::parse_json_with_comments::(crate::default_settings().as_ref()) + .unwrap(); #[cfg(not(target_os = "windows"))] util::merge_non_null_json_value_into( serde_json::json!({ diff --git a/crates/settings/src/settings_json.rs b/crates/settings/src/settings_json.rs new file mode 100644 index 0000000000000000000000000000000000000000..ebf32c2948ce7d4451ff35cd72fba7ba9c52d368 --- /dev/null +++ b/crates/settings/src/settings_json.rs @@ -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( + 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::(); + 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, 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, String) { + static PAIR_QUERY: LazyLock = 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, 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, 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(content: &str) -> Result { + 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, + 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(), + ); + } +} diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs index f5469adf381896169b4f8599111ac75d4af99f03..0ba516ad7d6d108b431cb3371e936fb82c7e2318 100644 --- a/crates/settings/src/settings_store.rs +++ b/crates/settings/src/settings_store.rs @@ -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::() - } - 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, &dyn Any)>; fn set_global_value(&mut self, value: Box); fn set_local_value(&mut self, root_id: WorktreeId, path: Arc, value: Box); - 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::() { + (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(target: &mut Option, source: Option) { - 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 AnySettingValue for SettingValue { } } - 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 AnySettingValue for SettingValue { } } -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, 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, String) { - static PAIR_QUERY: LazyLock = 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(content: &str) -> Result { - Ok(serde_json_lenient::from_str(content)?) -} - #[cfg(test)] mod tests { use crate::VsCodeSettingsSource; @@ -1784,6 +1582,22 @@ mod tests { ); } + fn check_settings_update( + 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::(&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( - 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::(&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, age: Option, @@ -2200,7 +1997,6 @@ mod tests { } #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)] - #[schemars(deny_unknown_fields)] struct MultiKeySettingsJson { key1: Option, key2: Option, @@ -2239,7 +2035,6 @@ mod tests { } #[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema)] - #[schemars(deny_unknown_fields)] struct JournalSettingsJson { pub path: Option, pub hour_format: Option, @@ -2334,7 +2129,6 @@ mod tests { } #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] - #[schemars(deny_unknown_fields)] struct LanguageSettingEntry { language_setting_1: Option, language_setting_2: Option, diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml index 84d77e3fdcbfcad98e104a068af9bcafade3231f..7b01fcc0e6599dfd619d246a5a0a446e9bed4135 100644 --- a/crates/settings_ui/Cargo.toml +++ b/crates/settings_ui/Cargo.toml @@ -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 diff --git a/crates/settings_ui/src/appearance_settings_controls.rs b/crates/settings_ui/src/appearance_settings_controls.rs index fa7e31c5cdb56719f49fc3f190f53081f0c7221f..141ae131826f43bf39dd1a7fa435753f84801e4f 100644 --- a/crates/settings_ui/src/appearance_settings_controls.rs +++ b/crates/settings_ui/src/appearance_settings_controls.rs @@ -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())); } } diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs new file mode 100644 index 0000000000000000000000000000000000000000..6021f74a4614d43b7ca0451e8988da410a2d4d0f --- /dev/null +++ b/crates/settings_ui/src/keybindings.rs @@ -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::()); + + 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::()]; + + command_palette_hooks::CommandPaletteFilter::update_global(cx, |filter, _cx| { + filter.hide_action_types(&keymap_ui_actions); + }); + + cx.observe_flag::( + 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::(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::() 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, + focus_handle: FocusHandle, + _keymap_subscription: Subscription, + keybindings: Vec, + // corresponds 1 to 1 with keybindings + string_match_candidates: Arc>, + matches: Vec, + table_interaction_state: Entity, + filter_editor: Entity, + selected_index: Option, +} + +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, window: &mut Window, cx: &mut Context) -> Self { + let focus_handle = cx.focus_handle(); + + let _keymap_subscription = + cx.observe_global::(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) { + 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, + ) -> (Vec, Vec) { + 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(|| "".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) { + 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) -> 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) { + 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, + ) { + 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, + ) { + 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) { + 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) { + 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.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, + ) { + 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, + action: SharedString, + action_input: Option, + 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) -> 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, _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, + fs: Arc, + error: Option, +} + +impl ModalView for KeybindingEditorModal {} + +impl EventEmitter 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, + _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) -> 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::().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, + 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, + focus_handle: FocusHandle, +} + +impl KeybindInput { + fn new(cx: &mut Context) -> 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, + ) { + 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, + ) { + 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, + ) { + 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) -> 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, + _window: &mut Window, + cx: &mut App, + ) -> gpui::Task> { + workspace::delete_unloaded_items( + alive_items, + workspace_id, + "keybinding_editors", + &KEYBINDING_EDITORS, + cx, + ) + } + + fn deserialize( + _project: Entity, + workspace: WeakEntity, + workspace_id: workspace::WorkspaceId, + item_id: workspace::ItemId, + window: &mut Window, + cx: &mut App, + ) -> gpui::Task>> { + 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, + ) -> Option>> { + 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 = + &[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> { + SELECT item_id + FROM keybinding_editors + WHERE item_id = ? AND workspace_id = ? + } + } + } +} diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index dd6626a7160fac48ccc3be8bb1387a166aef4692..28ffb1ab4a3ce6b300c16df545daeacba66887a0 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -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( diff --git a/crates/settings_ui/src/ui_components/mod.rs b/crates/settings_ui/src/ui_components/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..13971b0a5df8e3b188de1df94faab3df94aa86da --- /dev/null +++ b/crates/settings_ui/src/ui_components/mod.rs @@ -0,0 +1 @@ +pub mod table; diff --git a/crates/settings_ui/src/ui_components/table.rs b/crates/settings_ui/src/ui_components/table.rs new file mode 100644 index 0000000000000000000000000000000000000000..62f597e148b013ffaf59eca99f5105920701fcb5 --- /dev/null +++ b/crates/settings_ui/src/ui_components/table.rs @@ -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 { + render_item_fn: Box, &mut Window, &mut App) -> Vec<[AnyElement; COLS]>>, + element_id: ElementId, + row_count: usize, +} + +enum TableContents { + Vec(Vec<[AnyElement; COLS]>), + UniformList(UniformListData), +} + +impl TableContents { + 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 { + 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) { + let show_setting = EditorSettings::get_global(cx).scrollbar.show; + + let scroll_handle = self.scroll_handle.0.borrow(); + + let autohide = |show: ShowScrollbar, cx: &mut Context| match show { + ShowScrollbar::Auto => true, + ShowScrollbar::System => cx + .try_global::() + .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.horizontal_scrollbar.hide(window, cx); + self.vertical_scrollbar.hide(window, cx); + } + + // fn listener(this: Entity, fn: F) -> + + pub fn listener( + this: &Entity, + f: impl Fn(&mut Self, &E, &mut Window, &mut Context) + '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, + 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, 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, + 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, + 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 { + striped: bool, + width: Option, + headers: Option<[AnyElement; COLS]>, + rows: TableContents, + interaction_state: Option>, + selected_item_index: Option, + column_widths: Option<[Length; COLS]>, + on_click_row: Option>, +} + +impl Table { + /// 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, + row_count: usize, + render_item_fn: impl Fn(Range, &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) -> 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) -> Self { + self.interaction_state = Some(interaction_state.downgrade()); + self + } + + pub fn selected_item_index(mut self, selected_item_index: Option) -> 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; 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, 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( + row_index: usize, + items: [impl IntoElement; COLS], + table_context: TableRenderContext, + 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( + headers: [impl IntoElement; COLS], + table_context: TableRenderContext, + 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 { + pub striped: bool, + pub total_row_count: usize, + pub selected_item_index: Option, + pub column_widths: Option<[Length; COLS]>, + pub on_click_row: Option>, +} + +impl TableRenderContext { + fn new(table: &Table) -> 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 RenderOnce for Table { + 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, 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>, + state: ScrollbarState, +} + +impl ScrollbarProperties { + // Shows the scrollbar and cancels any pending hide task + fn show(&mut self, cx: &mut Context) { + 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) { + 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 { + 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(), + ) + } +} diff --git a/crates/snippet_provider/src/format.rs b/crates/snippet_provider/src/format.rs index 84bae238b4e62f59a7259f9aefa16c5a0e5aad87..0d06cbbc887bdce83514e9b14516539c86b8ee42 100644 --- a/crates/snippet_provider/src/format.rs +++ b/crates/snippet_provider/src/format.rs @@ -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::(); + .root_schema_for::(); 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::())), - ..Default::default() - })), - ..Default::default() - } - .into() + fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { + let snippet_schema = generator.subschema_for::(); + json_schema!({ + "type": "object", + "additionalProperties": snippet_schema + }) } } diff --git a/crates/storybook/src/stories.rs b/crates/storybook/src/stories.rs index b824235b00b5d49502734515ca14b853ca3be435..63992d259c7a1cb76a3684f53c55fe255522aced 100644 --- a/crates/storybook/src/stories.rs +++ b/crates/storybook/src/stories.rs @@ -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::*; diff --git a/crates/storybook/src/stories/indent_guides.rs b/crates/storybook/src/stories/indent_guides.rs index 068890ae50c524fa9242c53327ed0b929d098363..e83c9ed3837b49c4c701d4434ca1533fef83a5d7 100644 --- a/crates/storybook/src/stories/indent_guides.rs +++ b/crates/storybook/src/stories/indent_guides.rs @@ -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 { + pub fn model(_window: &mut Window, cx: &mut App) -> Entity { 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) -> impl IntoElement { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> 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, _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) diff --git a/crates/storybook/src/story_selector.rs b/crates/storybook/src/story_selector.rs index 1de6191367fb821ffeb41f88db0b9c5b275c499a..fd0be97ff6f8e5ef04126a4de60f41d4f31e2bef 100644 --- a/crates/storybook/src/story_selector.rs +++ b/crates/storybook/src/story_selector.rs @@ -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(), } } } diff --git a/crates/storybook/src/storybook.rs b/crates/storybook/src/storybook.rs index c8b055a67e60a07c87696515013b1a6fd5fefb1d..4c5b6272ef1f26d1fd065f76032e327ce59d1e12 100644 --- a/crates/storybook/src/storybook.rs +++ b/crates/storybook/src/storybook.rs @@ -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)); diff --git a/crates/svg_preview/Cargo.toml b/crates/svg_preview/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..b783d7192cce888218617b46e935f8c689b70a56 --- /dev/null +++ b/crates/svg_preview/Cargo.toml @@ -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 diff --git a/crates/svg_preview/LICENSE-GPL b/crates/svg_preview/LICENSE-GPL new file mode 120000 index 0000000000000000000000000000000000000000..89e542f750cd3860a0598eff0dc34b56d7336dc4 --- /dev/null +++ b/crates/svg_preview/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/svg_preview/src/svg_preview.rs b/crates/svg_preview/src/svg_preview.rs new file mode 100644 index 0000000000000000000000000000000000000000..cbee76be834b6db23860c2a67e8e8030c81a01b7 --- /dev/null +++ b/crates/svg_preview/src/svg_preview.rs @@ -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(); +} diff --git a/crates/svg_preview/src/svg_preview_view.rs b/crates/svg_preview/src/svg_preview_view.rs new file mode 100644 index 0000000000000000000000000000000000000000..327856d74989ba5cabab631486fd27133e3f684e --- /dev/null +++ b/crates/svg_preview/src/svg_preview_view.rs @@ -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, + image_cache: Entity, + _editor_subscription: Subscription, + _workspace_subscription: Option, +} + +#[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.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, + cx: &App, + ) -> Option { + let editor_path = Self::get_svg_path(editor, cx); + pane.items_of_type::() + .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, + ) -> Option> { + let editor = workspace.active_item(cx)?.act_as::(cx)?; + + if Self::is_svg_file(&editor, cx) { + Some(editor) + } else { + None + } + } + + fn create_svg_view( + mode: SvgPreviewMode, + workspace: &mut Workspace, + editor: Entity, + window: &mut Window, + cx: &mut Context, + ) -> Entity { + let workspace_handle = workspace.weak_handle(); + SvgPreviewView::new(mode, editor, workspace_handle, window, cx) + } + + pub fn new( + mode: SvgPreviewMode, + active_editor: Entity, + workspace_handle: WeakEntity, + window: &mut Window, + cx: &mut Context, + ) -> Entity { + 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::() + { + 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(editor: &Entity, cx: &C) -> bool + where + C: std::borrow::Borrow, + { + 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(editor: &Entity, cx: &C) -> Option + where + C: std::borrow::Borrow, + { + 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) -> 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 { + // 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)) {} +} diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs index 0d9733ebfff10c995ddb5181815b1845d33c1636..e336fa1fd7d0d61de323fdfc66de64919ff9186c 100644 --- a/crates/task/src/debug_format.rs +++ b/crates/task/src/debug_format.rs @@ -287,7 +287,8 @@ pub struct DebugTaskFile(pub Vec); 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::(); let mut build_task_value = serde_json_lenient::to_value(&build_task_schema).unwrap_or_default(); diff --git a/crates/task/src/serde_helpers.rs b/crates/task/src/serde_helpers.rs index d7af919fbf2b49b04f93a870021e12a555bb89a1..a95214d8b0903fb154770e0a3c7f7789819683ee 100644 --- a/crates/task/src/serde_helpers.rs +++ b/crates/task/src/serde_helpers.rs @@ -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, D::Error> where diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs index 02310bb1b0208cc2d6f929b0898a6e5ffadd7586..65424eeed4612b3e0f35be509f782b5947e7198f 100644 --- a/crates/task/src/task_template.rs +++ b/crates/task/src/task_template.rs @@ -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, /// Which shell to use when spawning the task. #[serde(default)] @@ -116,10 +115,9 @@ pub struct TaskTemplates(pub Vec); 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::(); + .root_schema_for::(); serde_json_lenient::to_value(schema).unwrap() } diff --git a/crates/task/src/vscode_debug_format.rs b/crates/task/src/vscode_debug_format.rs index 32177a4842e1710e90f2d9146139ac4c88c9aa58..a74401a2c66ea8080cbf5abbe29c211064e256d1 100644 --- a/crates/task/src/vscode_debug_format.rs +++ b/crates/task/src/vscode_debug_format.rs @@ -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() diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs index d3b8d927b3cf9114bc341b795f31e1ee4ad8e6b7..1510f613e34ef7bfc78bbfad23b7843787432491 100644 --- a/crates/tasks_ui/src/modal.rs +++ b/crates/tasks_ui/src/modal.rs @@ -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::(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))) }) }); diff --git a/crates/tasks_ui/src/tasks_ui.rs b/crates/tasks_ui/src/tasks_ui.rs index acdc7d0298490b2765b828c5bc468796deb6b3c3..0b3f70e6bcc5402bae3af09effb5bebc1a574977 100644 --- a/crates/tasks_ui/src/tasks_ui.rs +++ b/crates/tasks_ui/src/tasks_ui.rs @@ -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]) }) }); diff --git a/crates/terminal/src/terminal_hyperlinks.rs b/crates/terminal/src/terminal_hyperlinks.rs index 18675bbe02f94fc20995e90ba98799fbaf0fc92a..e318ae21bdcb755646e069c93ec8786f8197ad6a 100644 --- a/crates/terminal/src/terminal_hyperlinks.rs +++ b/crates/terminal/src/terminal_hyperlinks.rs @@ -52,7 +52,7 @@ pub(super) fn find_from_grid_point( ) -> 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( } } - 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)) diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs index bd93b7e0a67dea83cebd45012f8fefa2541bb5c8..d588d3680bbefbc522245a5ca709c2ef99de83f8 100644 --- a/crates/terminal/src/terminal_settings.rs +++ b/crates/terminal/src/terminal_settings.rs @@ -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, - pub font_family: Option, + pub font_family: Option, pub font_fallbacks: Option, pub font_features: Option, pub font_weight: Option, @@ -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, + pub font_family: Option, /// 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>, + #[schemars(extend("uniqueItems" = true))] + pub font_fallbacks: Option>, /// 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::(); - 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); diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index c0671048f6e54a1da6a54c1a0aea217706a82571..3439a5b7f882eb089b6e104d1b150cd40b5fdacf 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -196,7 +196,6 @@ impl TerminalElement { interactivity: Default::default(), } .track_focus(&focus) - .element } //Vec> -> 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 diff --git a/crates/theme/Cargo.toml b/crates/theme/Cargo.toml index 43d720b5560e3da3e8ebe9a68914e46b19fcfd4f..998d31bb3c5473de324dbfd34a3b276c81d95aba 100644 --- a/crates/theme/Cargo.toml +++ b/crates/theme/Cargo.toml @@ -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 diff --git a/crates/theme/src/default_colors.rs b/crates/theme/src/default_colors.rs index 33d7b86e3db7a2e311acf9c3cf1fc9548185b42d..3424e0fe04cdbc11544fa81018edba4ff2b357c1 100644 --- a/crates/theme/src/default_colors.rs +++ b/crates/theme/src/default_colors.rs @@ -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(), diff --git a/crates/theme/src/fallback_themes.rs b/crates/theme/src/fallback_themes.rs index afc977d7fdd1ad4cba18d63c899746837d79325f..5e9967d4603a5bac8c9f1a7e461c7319f52f82d7 100644 --- a/crates/theme/src/fallback_themes.rs +++ b/crates/theme/src/fallback_themes.rs @@ -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()), diff --git a/crates/theme/src/schema.rs b/crates/theme/src/schema.rs index a071ca26c8b0105828aa0f42ab315c6d67902823..b2a13b54b662f106018667de9635a4c896e1993c 100644 --- a/crates/theme/src/schema.rs +++ b/crates/theme/src/schema.rs @@ -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, + /// Background Color. Used for the background of selections in a UI element. + #[serde(rename = "element.selection_background")] + pub element_selection_background: Option, + /// 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, @@ -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] + }) } } diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs index eedee05592e2c5256a1b3afef46f83183f20b344..42012e080ca82f7fef487916e144ec79a30f9d84 100644 --- a/crates/theme/src/settings.rs +++ b/crates/theme/src/settings.rs @@ -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, /// The name of a font to use for rendering in the UI. #[serde(default)] - pub ui_font_family: Option, + pub ui_font_family: Option, /// The font fallbacks to use for rendering in the UI. #[serde(default)] #[schemars(default = "default_font_fallbacks")] - pub ui_font_fallbacks: Option>, + #[schemars(extend("uniqueItems" = true))] + pub ui_font_fallbacks: Option>, /// 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, /// The name of a font to use for rendering in text buffers. #[serde(default)] - pub buffer_font_family: Option, + pub buffer_font_family: Option, /// The font fallbacks to use for rendering in text buffers. #[serde(default)] - #[schemars(default = "default_font_fallbacks")] - pub buffer_font_fallbacks: Option>, + #[schemars(extend("uniqueItems" = true))] + pub buffer_font_fallbacks: Option>, /// The default font size for rendering in text buffers. #[serde(default)] pub buffer_font_size: Option, @@ -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>, + ) -> Option { + 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::(); - 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); + +inventory::submit! { + ParameterizedJsonSchema { + add_and_get_ref: |generator, _params, cx| { + let schema = json_schema!({ + "type": "string", + "enum": ThemeRegistry::global(cx).list_names(), + }); + replace_subschema::(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); + +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::>(), + }); + replace_subschema::(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); + +inventory::submit! { + ParameterizedJsonSchema { + add_and_get_ref: |generator, params, _cx| { + let schema = json_schema!({ + "type": "string", + "enum": params.font_names, + }); + replace_subschema::(generator, schema) + } + } +} + fn merge(target: &mut T, value: Option) { if let Some(value) = value { *target = value; diff --git a/crates/theme/src/styles/colors.rs b/crates/theme/src/styles/colors.rs index fb821385f54fb72e4f445a0c88b9edc4814574f8..76d18c6d6553edbc57ba2666a433b857141ba05b 100644 --- a/crates/theme/src/styles/colors.rs +++ b/crates/theme/src/styles/colors.rs @@ -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. diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 3b5306c216b8f6a100b75ce9d3e915c39989d620..bdb52693c0bad35107b79fc21bc127d496cec396 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -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(), diff --git a/crates/theme_importer/Cargo.toml b/crates/theme_importer/Cargo.toml index 0fc3206d5c1152c82bdbe09c8ec2e0949dbce6ec..f9f7daa5b3bd7d48ce0631d26d6a3c21767e5d5e 100644 --- a/crates/theme_importer/Cargo.toml +++ b/crates/theme_importer/Cargo.toml @@ -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 diff --git a/crates/theme_importer/src/assets.rs b/crates/theme_importer/src/assets.rs deleted file mode 100644 index 56e6ed46ed5677ff6d82354316b826166dc6f048..0000000000000000000000000000000000000000 --- a/crates/theme_importer/src/assets.rs +++ /dev/null @@ -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>> { - 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> { - Ok(Self::iter() - .filter(|p| p.starts_with(path)) - .map(SharedString::from) - .collect()) - } -} diff --git a/crates/theme_importer/src/main.rs b/crates/theme_importer/src/main.rs index c2ceee4cfcc0318ce1ab0efda4784f7930631737..ebb2840d0401d05b2bcac4e8c001dc30424f0fe5 100644 --- a/crates/theme_importer/src/main.rs +++ b/crates/theme_importer/src/main.rs @@ -1,4 +1,3 @@ -mod assets; mod color; mod vscode; diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs index 6e3c7c78aec2732aeb6cea67b6da5da6686b0f30..237403d4ba053646108e88546242df1f07cdc8ab 100644 --- a/crates/ui/src/components.rs +++ b/crates/ui/src/components.rs @@ -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::*; diff --git a/crates/ui/src/components/callout.rs b/crates/ui/src/components/callout.rs index b3f3758db6ce331eb17f4fe50e579dc148afb1da..d15fa122ed95e5e9a922c8bc694d1c35d975f9a4 100644 --- a/crates/ui/src/components/callout.rs +++ b/crates/ui/src/components/callout.rs @@ -1,4 +1,4 @@ -use gpui::AnyElement; +use gpui::{AnyElement, Hsla}; use crate::prelude::*; @@ -24,7 +24,9 @@ pub struct Callout { description: Option, primary_action: Option, secondary_action: Option, + tertiary_action: Option, line_height: Option, + bg_color: Option, } 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), ) }), ) diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs index b57454d7c130fffe12450b3f81b75515bd1c2930..6da3d03ea1869d1bff0558b510838544c419be1a 100644 --- a/crates/ui/src/components/keybinding.rs +++ b/crates/ui/src/components/keybinding.rs @@ -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, diff --git a/crates/ui/src/components/table.rs b/crates/ui/src/components/table.rs deleted file mode 100644 index 3f1b73e441c4722330f8de57e9b317e734ff2ddf..0000000000000000000000000000000000000000 --- a/crates/ui/src/components/table.rs +++ /dev/null @@ -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, - rows: Vec>, - 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>) -> 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>) -> 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>>) -> 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) -> 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) -> TableCell { - TableCell::String(s.into()) -} - -/// Creates a `TableCell` containing an element. -pub fn element_cell(e: impl Into) -> TableCell { - TableCell::Element(e.into()) -} - -impl From for TableCell -where - E: Into, -{ - 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 { - 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(), - ) - } -} diff --git a/crates/ui_input/src/ui_input.rs b/crates/ui_input/src/ui_input.rs index dfecc08dac203d597cb72e11a11acafe965b9571..bd99814cb30534165ad2bfba3911233e2946271b 100644 --- a/crates/ui_input/src/ui_input.rs +++ b/crates/ui_input/src/ui_input.rs @@ -29,7 +29,7 @@ pub struct SingleLineInput { label: Option, /// The placeholder text for the text field. placeholder: SharedString, - /// Exposes the underlying [`Model`] to allow for customizing the editor beyond the provided API. + /// Exposes the underlying [`Entity`] 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, diff --git a/crates/ui_prompt/src/ui_prompt.rs b/crates/ui_prompt/src/ui_prompt.rs index dc6aee177d72dc5898f6dcc43895d02aa02f7714..2b6a030f26e752401a56a61a3f6a0a881bb89557 100644 --- a/crates/ui_prompt/src/ui_prompt.rs +++ b/crates/ui_prompt/src/ui_prompt.rs @@ -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() } })) diff --git a/crates/vim/src/change_list.rs b/crates/vim/src/change_list.rs index 3332239631ae836111fe34431e807a21381b970f..25da3e09b8f6115273176cdb74e10e52aaeb951c 100644 --- a/crates/vim/src/change_list.rs +++ b/crates/vim/src/change_list.rs @@ -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); diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs index 40e8fcffa3c90be95f1421548a19c3a1a444035c..83df86d0e887f9802e664db79cb8259d83495d1a 100644 --- a/crates/vim/src/command.rs +++ b/crates/vim/src/command.rs @@ -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) { 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) { .disjoint_anchor_ranges() .collect::>() }); - 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) { 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::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::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::(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) diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs index 425280d58bd50ae73a39362bd635f28f1630eb44..42890d7a06093690c3b75175fabba9db58a0be71 100644 --- a/crates/vim/src/helix.rs +++ b/crates/vim/src/helix.rs @@ -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); + } } diff --git a/crates/vim/src/indent.rs b/crates/vim/src/indent.rs index ac708a7e8932f98502a2b969fa9ca68153765e8b..b10fff8b5d1b71a2c69edd3efe878dbb913fd17e 100644 --- a/crates/vim/src/indent.rs +++ b/crates/vim/src/indent.rs @@ -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, window: &mut Window, cx: &mut Context, ) { @@ -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); diff --git a/crates/vim/src/insert.rs b/crates/vim/src/insert.rs index a30af8769fac99ac1d1b8c131b32e8c440e0b180..7b38bed2be087085bf66e632c027af7aa858e6f3 100644 --- a/crates/vim/src/insert.rs +++ b/crates/vim/src/insert.rs @@ -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) diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 6b92246e501092ed547ae7169ac0691f67f4a3a8..2a6e5196bc01da9f8e6f3b6e12a9e0690757580f 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -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 { diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index ff9b347e41c49148f954b13acbb371cc7e23f458..f25467aec454e92dbc77dde2fccecd0ccbf46986 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -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::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) { + pub fn normal_object( + &mut self, + object: Object, + times: Option, + window: &mut Window, + cx: &mut Context, + ) { let mut waiting_operator: Option = 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::>(); 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::>(); - 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::>(); 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, mut positions: HashMap, ) { - 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(); + } } diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index e6ecf309f198891ba05370a9270d52978c73ea52..9485f174771cd1f21f1513e9609008dce8479b14 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -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, window: &mut Window, cx: &mut Context, ) { @@ -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 { diff --git a/crates/vim/src/normal/convert.rs b/crates/vim/src/normal/convert.rs index 31aac771c232cf082a9f63331acf787449cffc10..25b425e847d67eb5bc3d58b1d0a2201581a1e03f 100644 --- a/crates/vim/src/normal/convert.rs +++ b/crates/vim/src/normal/convert.rs @@ -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, window: &mut Window, cx: &mut Context, ) { @@ -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::(); 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) }) }); diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs index f52d9bebe05d517a5dda8d8080d47a9588c9ed9d..ccbb3dd0fd901b515258a34bb9377063e2a84cbd 100644 --- a/crates/vim/src/normal/delete.rs +++ b/crates/vim/src/normal/delete.rs @@ -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, window: &mut Window, cx: &mut Context, ) { @@ -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| { @@ -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) { diff --git a/crates/vim/src/normal/increment.rs b/crates/vim/src/normal/increment.rs index e2a0d282673a6f1ccb96d7c0a2d63f55d3dd78c1..09e6e85a5ccd057111dddca9e1bc76ebfacc1b63 100644 --- a/crates/vim/src/normal/increment.rs +++ b/crates/vim/src/normal/increment.rs @@ -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); diff --git a/crates/vim/src/normal/mark.rs b/crates/vim/src/normal/mark.rs index af4b71f4278a35a1e6462d833d46a247f025fda4..57a6108841e49d0461ff343e000969839287d6c7 100644 --- a/crates/vim/src/normal/mark.rs +++ b/crates/vim/src/normal/mark.rs @@ -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) }); } diff --git a/crates/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs index 41337f07074e56e17b35bc72addf3c0ce3ae0f39..86a5392b87bfe3ede9bc518591c95df00d87f9a1 100644 --- a/crates/vim/src/normal/paste.rs +++ b/crates/vim/src/normal/paste.rs @@ -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::(cx, |settings| { - settings.languages.insert( + settings.languages.0.insert( LanguageName::new("Rust"), LanguageSettingsContent { auto_indent_on_paste: Some(false), diff --git a/crates/vim/src/normal/repeat.rs b/crates/vim/src/normal/repeat.rs index 49f07954ffba04d58863dd25c942ce3d0e2032fc..8799a8b635fa53ee576ce2ff47b3e540eaa87059 100644 --- a/crates/vim/src/normal/repeat.rs +++ b/crates/vim/src/normal/repeat.rs @@ -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 diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs index 1199356995df9be3e8425d7c7d3ad0f1ae4c76b7..96df61e528d3df3a480b978c78154d8c0c3a0150 100644 --- a/crates/vim/src/normal/substitute.rs +++ b/crates/vim/src/normal/substitute.rs @@ -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( diff --git a/crates/vim/src/normal/toggle_comments.rs b/crates/vim/src/normal/toggle_comments.rs index 1df381acbeea2fdc9cc691ebadcc4a429f7745ec..636ea9eec2a04d46b4a9b288590bcf510e6b604f 100644 --- a/crates/vim/src/normal/toggle_comments.rs +++ b/crates/vim/src/normal/toggle_comments.rs @@ -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, window: &mut Window, cx: &mut Context, ) { @@ -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); diff --git a/crates/vim/src/normal/yank.rs b/crates/vim/src/normal/yank.rs index 3525b0d43fbc215fe0d469e1536398177c925653..847eba3143e6e0ba4ad18c3081e54a6759645567 100644 --- a/crates/vim/src/normal/yank.rs +++ b/crates/vim/src/normal/yank.rs @@ -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, window: &mut Window, cx: &mut Context, ) { @@ -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, }); } diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs index 2486619608fa8206d5bd7479ad93681b922081ec..2cec4e254ae3ac49a934be9a1b80842ae4cd3f1b 100644 --- a/crates/vim/src/object.rs +++ b/crates/vim/src/object.rs @@ -373,10 +373,12 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { impl Vim { fn object(&mut self, object: Object, window: &mut Window, cx: &mut Context) { + 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, around: bool, + times: Option, ) -> Option> { 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, around: bool, + times: Option, ) -> 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> { 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); } } diff --git a/crates/vim/src/replace.rs b/crates/vim/src/replace.rs index 5f407db5cb816a30aa83875d19e48bf4bb856473..15753e829003f829cddb93faa85b84104c7d92c8 100644 --- a/crates/vim/src/replace.rs +++ b/crates/vim/src/replace.rs @@ -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); }); diff --git a/crates/vim/src/rewrap.rs b/crates/vim/src/rewrap.rs index b5d69ef0ae73d87deb49dde9d852457d910be075..c1d157accbc0463a79a094a084a86748a122c552 100644 --- a/crates/vim/src/rewrap.rs +++ b/crates/vim/src/rewrap.rs @@ -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) { }, 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, window: &mut Window, cx: &mut Context, ) { @@ -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); diff --git a/crates/vim/src/surrounds.rs b/crates/vim/src/surrounds.rs index 6697742e4d318bb3a790e59e3404cf1f19a8c4ff..1f77ebda4ab02c755aaa704b4d0c772f42b7f84b 100644 --- a/crates/vim/src/surrounds.rs +++ b/crates/vim/src/surrounds.rs @@ -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); diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 346f78c1cabe483ec6305704d0889d70d24f2e99..2db1d4a20cb7c4162ca2e795f880ece500d88e0f 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -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, + ); +} diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 6b5d41f12ebf732781f6cb3234924c6ea48e92b5..2c2d60004e7aae6771906ff718c73b1dc0539723 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -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) diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 29ef3943b57086021844d8f65644fbe24e80392d..c3da5d21438b0734b3e537411ddf3c8d37e53508 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -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.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) { + pub fn visual_object( + &mut self, + object: Object, + count: Option, + window: &mut Window, + cx: &mut Context, + ) { 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.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" + }); + } } diff --git a/crates/vim/test_data/test_jump_list.json b/crates/vim/test_data/test_jump_list.json new file mode 100644 index 0000000000000000000000000000000000000000..833d1adadb91201413482427e4ae03b119645687 --- /dev/null +++ b/crates/vim/test_data/test_jump_list.json @@ -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"}} diff --git a/crates/vim/test_data/test_paragraph_multi_delete.json b/crates/vim/test_data/test_paragraph_multi_delete.json new file mode 100644 index 0000000000000000000000000000000000000000..f706827a24c7d5c903d87c422e1da5ab4ad89f6b --- /dev/null +++ b/crates/vim/test_data/test_paragraph_multi_delete.json @@ -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"}} diff --git a/crates/vim/test_data/test_v2ap.json b/crates/vim/test_data/test_v2ap.json new file mode 100644 index 0000000000000000000000000000000000000000..7b4d31a5dc42e19eea40af74851aba981adb63eb --- /dev/null +++ b/crates/vim/test_data/test_v2ap.json @@ -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"}} diff --git a/crates/web_search_providers/src/cloud.rs b/crates/web_search_providers/src/cloud.rs index 39602e071466d6aff126af075ade782e9da42928..adf79b0ff68c4d569dbf7cd40951c7c6c9761583 100644 --- a/crates/web_search_providers/src/cloud.rs +++ b/crates/web_search_providers/src/cloud.rs @@ -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, @@ -73,32 +71,50 @@ async fn perform_web_search( llm_api_token: LlmApiToken, body: WebSearchBody, ) -> Result { + 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(), + ); + } } } diff --git a/crates/workspace/src/theme_preview.rs b/crates/workspace/src/theme_preview.rs index ded1a08437fcfc7c8d8d47a1fd08072975dfeedd..f9aee26cddca7013bfa2fd1c6fb7c27dcb20e17d 100644 --- a/crates/workspace/src/theme_preview.rs +++ b/crates/workspace/src/theme_preview.rs @@ -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}; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 38532292435fdfa5948835cdf0d332c70bec3aa8..7c5355bfd1be10609ff8bcab9e7cd9cd3e7f7bc6 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -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, diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs index 6b3a0b855f9221c34d2c534cd6f02853d937a8ce..8c407fdd3eab5a6b7189f67ff46b8ce76d1a428d 100644 --- a/crates/worktree/src/worktree.rs +++ b/crates/worktree/src/worktree.rs @@ -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 = { diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 534d79c6ac4fb5ab792482f248021ee71197d082..4e426c3837f969802d0dc75de872412cae0567a5 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -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 } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 0e08b304f7c09d225c1da8449de1fd093512bf74..00a1f150eae5bb87e62fd52f1464101e3a9fd750 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -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); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index c57a9b576aa09139ec039de01b9569438a086f3a..333282611befcc5f71cebf15cb29d5c6713bfbe8 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -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) { "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", diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index 52c4ff3831e4b8ede0d4aea5fe2b22ec9fda3f81..85e28c6ae826b479731e35397d8d4195628a06d4 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -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, diff --git a/crates/zed/src/zed/quick_action_bar/markdown_preview.rs b/crates/zed/src/zed/quick_action_bar/markdown_preview.rs deleted file mode 100644 index 44008f71100bed6a39874cdfa807a3046842ae53..0000000000000000000000000000000000000000 --- a/crates/zed/src/zed/quick_action_bar/markdown_preview.rs +++ /dev/null @@ -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, - cx: &mut Context, - ) -> Option { - 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()) - } -} diff --git a/crates/zed/src/zed/quick_action_bar/preview.rs b/crates/zed/src/zed/quick_action_bar/preview.rs new file mode 100644 index 0000000000000000000000000000000000000000..3772104f39050c53ced37031e2c2f3e052dcb12d --- /dev/null +++ b/crates/zed/src/zed/quick_action_bar/preview.rs @@ -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, + cx: &mut Context, + ) -> Option { + 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, + Box::new(MarkdownOpenPreviewToTheSide) as Box, + &markdown_preview::OpenPreview as &dyn gpui::Action, + ), + PreviewType::Svg => ( + "toggle-svg-preview", + "Preview SVG", + Box::new(SvgOpenPreview) as Box, + Box::new(SvgOpenPreviewToTheSide) as Box, + &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()) + } +} diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs index 8d310653095cdb9104f6e1fe0e545cd543cee372..f039679f1fa4f7577ac9dd57e2643416a2fadfaa 100644 --- a/crates/zed_actions/src/lib.rs +++ b/crates/zed_actions/src/lib.rs @@ -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, diff --git a/docs/src/ai/mcp.md b/docs/src/ai/mcp.md index 8fbea0fef11fa6b18480ea1a1b73501fcea59c89..202b14102209ae8d3dbf338ff11bb8b443432cf9 100644 --- a/docs/src/ai/mcp.md +++ b/docs/src/ai/mcp.md @@ -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": {} } } } diff --git a/docs/src/debugger.md b/docs/src/debugger.md index fc95fb43b5e12c646ed287d6715fc1218336d54a..47d9ffaa042cc7681757867ce88804af79a5f621 100644 --- a/docs/src/debugger.md +++ b/docs/src/debugger.md @@ -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. diff --git a/docs/src/development/freebsd.md b/docs/src/development/freebsd.md index 33ff9a56d94c3f3882d7465d82f236b463fac7d6..199e653a65c5cf3d86881c4c038677d60ac2fec5 100644 --- a/docs/src/development/freebsd.md +++ b/docs/src/development/freebsd.md @@ -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`. diff --git a/docs/src/languages/r.md b/docs/src/languages/r.md index ce123d4606a8199bd7660a45b44648bfb3d0da50..226a6f866846da43a3f32668dd19e1efb3f657ce 100644 --- a/docs/src/languages/r.md +++ b/docs/src/languages/r.md @@ -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. + + + + diff --git a/docs/src/languages/ruby.md b/docs/src/languages/ruby.md index 4c563ca1f41d98f5c9b32fcafe0fd5f151540ed5..8b3094a3b714b12e310b7b30ff7b5d196e1893d8 100644 --- a/docs/src/languages/ruby.md +++ b/docs/src/languages/ruby.md @@ -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", + }, + }, +] +``` diff --git a/docs/src/languages/swift.md b/docs/src/languages/swift.md index c3d5cfaa1a579455d3ef4fb9b354df63f7471199..9b056be5bc8869b18b78e9a2e64ea43db3d8ea90 100644 --- a/docs/src/languages/swift.md +++ b/docs/src/languages/swift.md @@ -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" + } +] +``` diff --git a/docs/src/vim.md b/docs/src/vim.md index 3d3a1bac013f6fb417d297bd9c6587af68699a60..a1c79b531da21533ba6467ae46f65d3eba4bbeb8 100644 --- a/docs/src/vim.md +++ b/docs/src/vim.md @@ -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]` | diff --git a/docs/src/windows.md b/docs/src/windows.md index bf2ecbab9c0a34d8a2b15716fbf51d94bd9cb88a..2594919cce4e72616ca8cfb292d4ecdf016d2dea 100644 --- a/docs/src/windows.md +++ b/docs/src/windows.md @@ -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) diff --git a/docs/theme/css/general.css b/docs/theme/css/general.css index 61b373651a07e8d96499e5aeee90494ade583518..138a3a325b8a975ca36fb63dee09dc7893137a14 100644 --- a/docs/theme/css/general.css +++ b/docs/theme/css/general.css @@ -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, diff --git a/extensions/emmet/extension.toml b/extensions/emmet/extension.toml index 66ed9b3099b61bee844a08f36fdf453e8ef66920..02198a2c822fe41c46e981f2867e051787993169 100644 --- a/extensions/emmet/extension.toml +++ b/extensions/emmet/extension.toml @@ -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 "] 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" diff --git a/flake.lock b/flake.lock index fb5206fe3c5449383b510a48da90d505b7eb438e..fa0d51d90de9a6a9929241f6be212ea32e1432a2 100644 --- a/flake.lock +++ b/flake.lock @@ -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": { diff --git a/rust-toolchain.toml b/rust-toolchain.toml index a7a9ac8295b3aaed6dabc2be50452493f7233f69..f80eab8fbcbd78e5bbf3bf4e8757bc6872146e1b 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,5 +1,5 @@ [toolchain] -channel = "1.87" +channel = "1.88" profile = "minimal" components = [ "rustfmt", "clippy" ] targets = [ diff --git a/script/new-crate b/script/new-crate index 44b5a6e5c8de2444354e6959e4539b3261547aea..df574981e739a465f3f4f92d8a05c8df7cffdb82 100755 --- a/script/new-crate +++ b/script/new-crate @@ -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] diff --git a/tooling/workspace-hack/Cargo.toml b/tooling/workspace-hack/Cargo.toml index 347d59e293130e03b1539a028ea8dc2fa82c061d..84ed00153647dd780d818844dcd6d3cb628859d1 100644 --- a/tooling/workspace-hack/Cargo.toml +++ b/tooling/workspace-hack/Cargo.toml @@ -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"] }