From cdddb4d3603691406ac846f97bc66699c3dc093a Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 28 Oct 2024 15:34:03 +0100 Subject: [PATCH] Add language toolchains (#19576) This PR adds support for selecting toolchains for a given language (e.g. Rust toolchains or Python virtual environments) with support for SSH projects provided out of the box. For Python we piggy-back off of [PET](https://github.com/microsoft/python-environment-tools), a library maintained by Microsoft. Closes #16421 Closes #7646 Release Notes: - Added toolchain selector to the status bar (with initial support for Python virtual environments) --- .github/workflows/ci.yml | 3 + Cargo.lock | 493 ++++++++++++++++-- Cargo.toml | 7 + assets/settings/default.json | 1 + crates/extension/src/extension_lsp_adapter.rs | 4 +- crates/extension/src/extension_store.rs | 23 +- crates/language/src/language.rs | 16 +- crates/language/src/language_registry.rs | 63 ++- crates/language/src/toolchain.rs | 65 +++ crates/languages/Cargo.toml | 5 + crates/languages/src/json.rs | 5 +- crates/languages/src/lib.rs | 55 +- crates/languages/src/python.rs | 115 +++- crates/languages/src/tailwind.rs | 3 +- crates/languages/src/typescript.rs | 4 +- crates/languages/src/vtsls.rs | 3 +- crates/languages/src/yaml.rs | 4 +- crates/project/src/lsp_store.rs | 135 +++-- crates/project/src/project.rs | 65 ++- crates/project/src/toolchain_store.rs | 416 +++++++++++++++ crates/proto/proto/zed.proto | 49 +- crates/proto/src/proto.rs | 17 +- crates/remote_server/src/headless_project.rs | 7 +- crates/toolchain_selector/Cargo.toml | 24 + crates/toolchain_selector/LICENSE-GPL | 1 + .../src/active_toolchain.rs | 173 ++++++ .../src/toolchain_selector.rs | 343 ++++++++++++ crates/workspace/src/persistence.rs | 96 +++- crates/workspace/src/workspace.rs | 16 + crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 1 + crates/zed/src/zed.rs | 3 + script/licenses/zed-licenses.toml | 138 +++++ 33 files changed, 2221 insertions(+), 133 deletions(-) create mode 100644 crates/language/src/toolchain.rs create mode 100644 crates/project/src/toolchain_store.rs create mode 100644 crates/toolchain_selector/Cargo.toml create mode 120000 crates/toolchain_selector/LICENSE-GPL create mode 100644 crates/toolchain_selector/src/active_toolchain.rs create mode 100644 crates/toolchain_selector/src/toolchain_selector.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc38baeae9..84ed0dd5d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -192,6 +192,9 @@ jobs: if: github.repository_owner == 'zed-industries' runs-on: hosted-windows-1 steps: + # more info here:- https://github.com/rust-lang/cargo/issues/13020 + - name: Enable longer pathnames for git + run: git config --system core.longpaths true - name: Checkout repo uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: diff --git a/Cargo.lock b/Cargo.lock index 91b76f33e8..bd9ad91bf7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -291,6 +291,12 @@ dependencies = [ "syn 2.0.76", ] +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + [[package]] name = "arrayref" version = "0.3.8" @@ -385,7 +391,7 @@ dependencies = [ "ctor", "db", "editor", - "env_logger", + "env_logger 0.11.5", "feature_flags", "fs", "futures 0.3.30", @@ -2551,7 +2557,7 @@ dependencies = [ "dashmap 6.0.1", "derive_more", "editor", - "env_logger", + "env_logger 0.11.5", "envy", "file_finder", "fs", @@ -2706,7 +2712,7 @@ dependencies = [ "command_palette_hooks", "ctor", "editor", - "env_logger", + "env_logger 0.11.5", "fuzzy", "go_to_line", "gpui", @@ -3483,7 +3489,7 @@ dependencies = [ "collections", "ctor", "editor", - "env_logger", + "env_logger 0.11.5", "futures 0.3.30", "gpui", "language", @@ -3671,7 +3677,7 @@ dependencies = [ "ctor", "db", "emojis", - "env_logger", + "env_logger 0.11.5", "file_icons", "futures 0.3.30", "fuzzy", @@ -3877,6 +3883,19 @@ dependencies = [ "regex", ] +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + [[package]] name = "env_logger" version = "0.11.5" @@ -3985,7 +4004,7 @@ dependencies = [ "client", "clock", "collections", - "env_logger", + "env_logger 0.11.5", "feature_flags", "fs", "git", @@ -4080,7 +4099,7 @@ dependencies = [ "client", "collections", "ctor", - "env_logger", + "env_logger 0.11.5", "fs", "futures 0.3.30", "gpui", @@ -4122,7 +4141,7 @@ version = "0.1.0" dependencies = [ "anyhow", "clap", - "env_logger", + "env_logger 0.11.5", "extension", "fs", "language", @@ -4281,7 +4300,7 @@ dependencies = [ "collections", "ctor", "editor", - "env_logger", + "env_logger 0.11.5", "file_icons", "futures 0.3.30", "fuzzy", @@ -5036,7 +5055,7 @@ dependencies = [ "ctor", "derive_more", "embed-resource", - "env_logger", + "env_logger 0.11.5", "etagere", "filedescriptor", "flume", @@ -5226,6 +5245,15 @@ dependencies = [ "serde", ] +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.5", +] + [[package]] name = "hashlink" version = "0.9.1" @@ -6184,7 +6212,7 @@ dependencies = [ "collections", "ctor", "ec4rs", - "env_logger", + "env_logger 0.11.5", "futures 0.3.30", "fuzzy", "git", @@ -6241,7 +6269,7 @@ dependencies = [ "copilot", "ctor", "editor", - "env_logger", + "env_logger 0.11.5", "feature_flags", "futures 0.3.30", "google_ai", @@ -6298,7 +6326,7 @@ dependencies = [ "collections", "copilot", "editor", - "env_logger", + "env_logger 0.11.5", "futures 0.3.30", "gpui", "language", @@ -6332,6 +6360,11 @@ dependencies = [ "lsp", "node_runtime", "paths", + "pet", + "pet-conda", + "pet-core", + "pet-poetry", + "pet-reporter", "project", "regex", "rope", @@ -6628,7 +6661,7 @@ dependencies = [ "async-pipe", "collections", "ctor", - "env_logger", + "env_logger 0.11.5", "futures 0.3.30", "gpui", "log", @@ -6711,7 +6744,7 @@ version = "0.1.0" dependencies = [ "anyhow", "assets", - "env_logger", + "env_logger 0.11.5", "futures 0.3.30", "gpui", "language", @@ -6824,7 +6857,7 @@ dependencies = [ "clap", "clap_complete", "elasticlunr-rs", - "env_logger", + "env_logger 0.11.5", "futures-util", "handlebars 5.1.2", "ignore", @@ -7006,6 +7039,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "msvc_spectre_libs" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8661ace213a0a130c7c5b9542df5023aedf092a02008ccf477b39ff108990305" +dependencies = [ + "cc", +] + [[package]] name = "multi_buffer" version = "0.1.0" @@ -7014,7 +7056,7 @@ dependencies = [ "clock", "collections", "ctor", - "env_logger", + "env_logger 0.11.5", "futures 0.3.30", "gpui", "itertools 0.13.0", @@ -7974,6 +8016,366 @@ dependencies = [ "sha2", ] +[[package]] +name = "pet" +version = "0.1.0" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c" +dependencies = [ + "clap", + "env_logger 0.10.2", + "lazy_static", + "log", + "msvc_spectre_libs", + "pet-conda", + "pet-core", + "pet-env-var-path", + "pet-fs", + "pet-global-virtualenvs", + "pet-homebrew", + "pet-jsonrpc", + "pet-linux-global-python", + "pet-mac-commandlinetools", + "pet-mac-python-org", + "pet-mac-xcode", + "pet-pipenv", + "pet-poetry", + "pet-pyenv", + "pet-python-utils", + "pet-reporter", + "pet-telemetry", + "pet-venv", + "pet-virtualenv", + "pet-virtualenvwrapper", + "pet-windows-registry", + "pet-windows-store", + "serde", + "serde_json", +] + +[[package]] +name = "pet-conda" +version = "0.1.0" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c" +dependencies = [ + "env_logger 0.10.2", + "lazy_static", + "log", + "msvc_spectre_libs", + "pet-core", + "pet-fs", + "pet-python-utils", + "pet-reporter", + "regex", + "serde", + "serde_json", + "yaml-rust2", +] + +[[package]] +name = "pet-core" +version = "0.1.0" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c" +dependencies = [ + "clap", + "lazy_static", + "log", + "msvc_spectre_libs", + "pet-fs", + "regex", + "serde", + "serde_json", +] + +[[package]] +name = "pet-env-var-path" +version = "0.1.0" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c" +dependencies = [ + "lazy_static", + "log", + "msvc_spectre_libs", + "pet-conda", + "pet-core", + "pet-fs", + "pet-python-utils", + "pet-virtualenv", + "regex", +] + +[[package]] +name = "pet-fs" +version = "0.1.0" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c" +dependencies = [ + "log", + "msvc_spectre_libs", +] + +[[package]] +name = "pet-global-virtualenvs" +version = "0.1.0" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c" +dependencies = [ + "log", + "msvc_spectre_libs", + "pet-conda", + "pet-core", + "pet-fs", + "pet-virtualenv", +] + +[[package]] +name = "pet-homebrew" +version = "0.1.0" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c" +dependencies = [ + "lazy_static", + "log", + "msvc_spectre_libs", + "pet-conda", + "pet-core", + "pet-fs", + "pet-python-utils", + "pet-virtualenv", + "regex", + "serde", + "serde_json", +] + +[[package]] +name = "pet-jsonrpc" +version = "0.1.0" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c" +dependencies = [ + "env_logger 0.10.2", + "log", + "msvc_spectre_libs", + "pet-core", + "serde", + "serde_json", +] + +[[package]] +name = "pet-linux-global-python" +version = "0.1.0" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c" +dependencies = [ + "log", + "msvc_spectre_libs", + "pet-core", + "pet-fs", + "pet-python-utils", + "pet-virtualenv", +] + +[[package]] +name = "pet-mac-commandlinetools" +version = "0.1.0" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c" +dependencies = [ + "log", + "msvc_spectre_libs", + "pet-core", + "pet-fs", + "pet-python-utils", + "pet-virtualenv", +] + +[[package]] +name = "pet-mac-python-org" +version = "0.1.0" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c" +dependencies = [ + "log", + "msvc_spectre_libs", + "pet-core", + "pet-fs", + "pet-python-utils", + "pet-virtualenv", +] + +[[package]] +name = "pet-mac-xcode" +version = "0.1.0" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c" +dependencies = [ + "log", + "msvc_spectre_libs", + "pet-core", + "pet-fs", + "pet-python-utils", + "pet-virtualenv", +] + +[[package]] +name = "pet-pipenv" +version = "0.1.0" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c" +dependencies = [ + "log", + "msvc_spectre_libs", + "pet-core", + "pet-fs", + "pet-python-utils", + "pet-virtualenv", +] + +[[package]] +name = "pet-poetry" +version = "0.1.0" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c" +dependencies = [ + "base64 0.22.1", + "lazy_static", + "log", + "msvc_spectre_libs", + "pet-core", + "pet-fs", + "pet-python-utils", + "pet-reporter", + "pet-virtualenv", + "regex", + "serde", + "serde_json", + "sha2", + "toml 0.8.19", +] + +[[package]] +name = "pet-pyenv" +version = "0.1.0" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c" +dependencies = [ + "lazy_static", + "log", + "msvc_spectre_libs", + "pet-conda", + "pet-core", + "pet-fs", + "pet-python-utils", + "pet-reporter", + "regex", + "serde", + "serde_json", +] + +[[package]] +name = "pet-python-utils" +version = "0.1.0" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c" +dependencies = [ + "env_logger 0.10.2", + "lazy_static", + "log", + "msvc_spectre_libs", + "pet-core", + "pet-fs", + "regex", + "serde", + "serde_json", + "sha2", +] + +[[package]] +name = "pet-reporter" +version = "0.1.0" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c" +dependencies = [ + "env_logger 0.10.2", + "log", + "msvc_spectre_libs", + "pet-core", + "pet-jsonrpc", + "serde", + "serde_json", +] + +[[package]] +name = "pet-telemetry" +version = "0.1.0" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c" +dependencies = [ + "env_logger 0.10.2", + "lazy_static", + "log", + "msvc_spectre_libs", + "pet-core", + "pet-fs", + "pet-python-utils", + "regex", +] + +[[package]] +name = "pet-venv" +version = "0.1.0" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c" +dependencies = [ + "log", + "msvc_spectre_libs", + "pet-core", + "pet-python-utils", + "pet-virtualenv", +] + +[[package]] +name = "pet-virtualenv" +version = "0.1.0" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c" +dependencies = [ + "log", + "msvc_spectre_libs", + "pet-core", + "pet-fs", + "pet-python-utils", +] + +[[package]] +name = "pet-virtualenvwrapper" +version = "0.1.0" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c" +dependencies = [ + "log", + "msvc_spectre_libs", + "pet-core", + "pet-fs", + "pet-python-utils", + "pet-virtualenv", +] + +[[package]] +name = "pet-windows-registry" +version = "0.1.0" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c" +dependencies = [ + "lazy_static", + "log", + "msvc_spectre_libs", + "pet-conda", + "pet-core", + "pet-fs", + "pet-python-utils", + "pet-virtualenv", + "pet-windows-store", + "regex", + "winreg 0.52.0", +] + +[[package]] +name = "pet-windows-store" +version = "0.1.0" +source = "git+https://github.com/microsoft/python-environment-tools.git?rev=ffcbf3f28c46633abd5448a52b1f396c322e0d6c#ffcbf3f28c46633abd5448a52b1f396c322e0d6c" +dependencies = [ + "lazy_static", + "log", + "msvc_spectre_libs", + "pet-core", + "pet-fs", + "pet-python-utils", + "pet-virtualenv", + "regex", + "winreg 0.52.0", +] + [[package]] name = "petgraph" version = "0.6.5" @@ -8062,7 +8464,7 @@ dependencies = [ "anyhow", "ctor", "editor", - "env_logger", + "env_logger 0.11.5", "gpui", "menu", "serde", @@ -8408,7 +8810,7 @@ dependencies = [ "client", "clock", "collections", - "env_logger", + "env_logger 0.11.5", "fs", "futures 0.3.30", "fuzzy", @@ -9123,7 +9525,7 @@ dependencies = [ "clap", "client", "clock", - "env_logger", + "env_logger 0.11.5", "fork", "fs", "futures 0.3.30", @@ -9174,7 +9576,7 @@ dependencies = [ "collections", "command_palette_hooks", "editor", - "env_logger", + "env_logger 0.11.5", "futures 0.3.30", "gpui", "http_client", @@ -9454,7 +9856,7 @@ dependencies = [ "arrayvec", "criterion", "ctor", - "env_logger", + "env_logger 0.11.5", "gpui", "log", "rand 0.8.5", @@ -9485,7 +9887,7 @@ dependencies = [ "base64 0.22.1", "chrono", "collections", - "env_logger", + "env_logger 0.11.5", "futures 0.3.30", "gpui", "parking_lot", @@ -10074,7 +10476,7 @@ dependencies = [ "client", "clock", "collections", - "env_logger", + "env_logger 0.11.5", "feature_flags", "fs", "futures 0.3.30", @@ -10767,7 +11169,7 @@ dependencies = [ "futures-io", "futures-util", "hashbrown 0.14.5", - "hashlink", + "hashlink 0.9.1", "hex", "indexmap 2.4.0", "log", @@ -11091,7 +11493,7 @@ version = "0.1.0" dependencies = [ "arrayvec", "ctor", - "env_logger", + "env_logger 0.11.5", "log", "rand 0.8.5", "rayon", @@ -11105,7 +11507,7 @@ dependencies = [ "client", "collections", "editor", - "env_logger", + "env_logger 0.11.5", "futures 0.3.30", "gpui", "http_client", @@ -11404,7 +11806,7 @@ dependencies = [ "collections", "ctor", "editor", - "env_logger", + "env_logger 0.11.5", "gpui", "language", "menu", @@ -11611,7 +12013,7 @@ dependencies = [ "clock", "collections", "ctor", - "env_logger", + "env_logger 0.11.5", "gpui", "http_client", "log", @@ -12100,6 +12502,21 @@ dependencies = [ "winnow 0.6.18", ] +[[package]] +name = "toolchain_selector" +version = "0.1.0" +dependencies = [ + "editor", + "fuzzy", + "gpui", + "language", + "picker", + "project", + "ui", + "util", + "workspace", +] + [[package]] name = "topological-sort" version = "0.2.2" @@ -14269,7 +14686,7 @@ dependencies = [ "collections", "db", "derive_more", - "env_logger", + "env_logger 0.11.5", "fs", "futures 0.3.30", "git", @@ -14306,7 +14723,7 @@ dependencies = [ "anyhow", "clock", "collections", - "env_logger", + "env_logger 0.11.5", "fs", "futures 0.3.30", "fuzzy", @@ -14476,6 +14893,17 @@ dependencies = [ "clap", ] +[[package]] +name = "yaml-rust2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink 0.8.4", +] + [[package]] name = "yansi" version = "1.0.1" @@ -14589,7 +15017,7 @@ dependencies = [ "db", "diagnostics", "editor", - "env_logger", + "env_logger 0.11.5", "extension", "extensions_ui", "feature_flags", @@ -14656,6 +15084,7 @@ dependencies = [ "theme", "theme_selector", "time", + "toolchain_selector", "tree-sitter-md", "tree-sitter-rust", "ui", diff --git a/Cargo.toml b/Cargo.toml index 64a2546020..0697cc0c0b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -117,6 +117,7 @@ members = [ "crates/theme_selector", "crates/time_format", "crates/title_bar", + "crates/toolchain_selector", "crates/ui", "crates/ui_input", "crates/ui_macros", @@ -290,6 +291,7 @@ theme_importer = { path = "crates/theme_importer" } theme_selector = { path = "crates/theme_selector" } time_format = { path = "crates/time_format" } title_bar = { path = "crates/title_bar" } +toolchain_selector = { path = "crates/toolchain_selector" } ui = { path = "crates/ui" } ui_input = { path = "crates/ui_input" } ui_macros = { path = "crates/ui_macros" } @@ -376,6 +378,11 @@ ordered-float = "2.1.1" palette = { version = "0.7.5", default-features = false, features = ["std"] } parking_lot = "0.12.1" pathdiff = "0.2" +pet = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" } +pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" } +pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" } +pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" } +pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "ffcbf3f28c46633abd5448a52b1f396c322e0d6c" } postage = { version = "0.5", features = ["futures-traits"] } pretty_assertions = "1.3.0" profiling = "1" diff --git a/assets/settings/default.json b/assets/settings/default.json index cd4e3db15c..879f6bb7fa 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -779,6 +779,7 @@ "tasks": { "variables": {} }, + "toolchain": { "name": "default", "path": "default" }, // An object whose keys are language names, and whose values // are arrays of filenames or extensions of files that should // use those languages. diff --git a/crates/extension/src/extension_lsp_adapter.rs b/crates/extension/src/extension_lsp_adapter.rs index 25179acec6..1557ef2153 100644 --- a/crates/extension/src/extension_lsp_adapter.rs +++ b/crates/extension/src/extension_lsp_adapter.rs @@ -8,7 +8,8 @@ use collections::HashMap; use futures::{Future, FutureExt}; use gpui::AsyncAppContext; use language::{ - CodeLabel, HighlightId, Language, LanguageServerName, LspAdapter, LspAdapterDelegate, + CodeLabel, HighlightId, Language, LanguageServerName, LanguageToolchainStore, LspAdapter, + LspAdapterDelegate, }; use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions}; use serde::Serialize; @@ -194,6 +195,7 @@ impl LspAdapter for ExtensionLspAdapter { async fn workspace_configuration( self: Arc, delegate: &Arc, + _: Arc, _cx: &mut AsyncAppContext, ) -> Result { let delegate = delegate.clone(); diff --git a/crates/extension/src/extension_store.rs b/crates/extension/src/extension_store.rs index 535d68326f..0a9299a8be 100644 --- a/crates/extension/src/extension_store.rs +++ b/crates/extension/src/extension_store.rs @@ -37,7 +37,7 @@ use http_client::{AsyncBody, HttpClient, HttpClientWithUrl}; use indexed_docs::{IndexedDocsRegistry, ProviderId}; use language::{ LanguageConfig, LanguageMatcher, LanguageName, LanguageQueries, LanguageRegistry, - QUERY_FILENAME_PREFIXES, + LoadedLanguage, QUERY_FILENAME_PREFIXES, }; use node_runtime::NodeRuntime; use project::ContextProviderWithTasks; @@ -1102,14 +1102,21 @@ impl ExtensionStore { let config = std::fs::read_to_string(language_path.join("config.toml"))?; let config: LanguageConfig = ::toml::from_str(&config)?; let queries = load_plugin_queries(&language_path); - let tasks = std::fs::read_to_string(language_path.join("tasks.json")) - .ok() - .and_then(|contents| { - let definitions = serde_json_lenient::from_str(&contents).log_err()?; - Some(Arc::new(ContextProviderWithTasks::new(definitions)) as Arc<_>) - }); + let context_provider = + std::fs::read_to_string(language_path.join("tasks.json")) + .ok() + .and_then(|contents| { + let definitions = + serde_json_lenient::from_str(&contents).log_err()?; + Some(Arc::new(ContextProviderWithTasks::new(definitions)) as Arc<_>) + }); - Ok((config, queries, tasks)) + Ok(LoadedLanguage { + config, + queries, + context_provider, + toolchain_provider: None, + }) }, ); } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index c1c9cfebbe..e52794f81f 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -15,6 +15,7 @@ mod outline; pub mod proto; mod syntax_map; mod task_context; +mod toolchain; #[cfg(test)] pub mod buffer_tests; @@ -28,7 +29,7 @@ use futures::Future; use gpui::{AppContext, AsyncAppContext, Model, SharedString, Task}; pub use highlight_map::HighlightMap; use http_client::HttpClient; -pub use language_registry::LanguageName; +pub use language_registry::{LanguageName, LoadedLanguage}; use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerBinaryOptions}; use parking_lot::Mutex; use regex::Regex; @@ -61,6 +62,7 @@ use syntax_map::{QueryCursorHandle, SyntaxSnapshot}; use task::RunnableTag; pub use task_context::{ContextProvider, RunnableRange}; use theme::SyntaxTheme; +pub use toolchain::{LanguageToolchainStore, Toolchain, ToolchainList, ToolchainLister}; use tree_sitter::{self, wasmtime, Query, QueryCursor, WasmStore}; use util::serde::default_true; @@ -502,6 +504,7 @@ pub trait LspAdapter: 'static + Send + Sync { async fn workspace_configuration( self: Arc, _: &Arc, + _: Arc, _cx: &mut AsyncAppContext, ) -> Result { Ok(serde_json::json!({})) @@ -855,6 +858,7 @@ pub struct Language { pub(crate) config: LanguageConfig, pub(crate) grammar: Option>, pub(crate) context_provider: Option>, + pub(crate) toolchain: Option>, } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] @@ -983,6 +987,7 @@ impl Language { }) }), context_provider: None, + toolchain: None, } } @@ -991,6 +996,11 @@ impl Language { self } + pub fn with_toolchain_lister(mut self, provider: Option>) -> Self { + self.toolchain = provider; + self + } + pub fn with_queries(mut self, queries: LanguageQueries) -> Result { if let Some(query) = queries.highlights { self = self @@ -1361,6 +1371,10 @@ impl Language { self.context_provider.clone() } + pub fn toolchain_lister(&self) -> Option> { + self.toolchain.clone() + } + pub fn highlight_text<'a>( self: &'a Arc, text: &'a Rope, diff --git a/crates/language/src/language_registry.rs b/crates/language/src/language_registry.rs index 880ae3b611..caea801ce5 100644 --- a/crates/language/src/language_registry.rs +++ b/crates/language/src/language_registry.rs @@ -4,7 +4,7 @@ use crate::{ }, task_context::ContextProvider, with_parser, CachedLspAdapter, File, Language, LanguageConfig, LanguageId, LanguageMatcher, - LanguageServerName, LspAdapter, PLAIN_TEXT, + LanguageServerName, LspAdapter, ToolchainLister, PLAIN_TEXT, }; use anyhow::{anyhow, Context, Result}; use collections::{hash_map, HashMap, HashSet}; @@ -75,6 +75,13 @@ impl<'a> From<&'a str> for LanguageName { } } +impl From for String { + fn from(value: LanguageName) -> Self { + let value: &str = &value.0; + Self::from(value) + } +} + pub struct LanguageRegistry { state: RwLock, language_server_download_dir: Option>, @@ -123,16 +130,7 @@ pub struct AvailableLanguage { name: LanguageName, grammar: Option>, matcher: LanguageMatcher, - load: Arc< - dyn Fn() -> Result<( - LanguageConfig, - LanguageQueries, - Option>, - )> - + 'static - + Send - + Sync, - >, + load: Arc Result + 'static + Send + Sync>, loaded: bool, } @@ -200,6 +198,13 @@ struct LspBinaryStatusSender { txs: Arc>>>, } +pub struct LoadedLanguage { + pub config: LanguageConfig, + pub queries: LanguageQueries, + pub context_provider: Option>, + pub toolchain_provider: Option>, +} + impl LanguageRegistry { pub fn new(executor: BackgroundExecutor) -> Self { let this = Self { @@ -283,7 +288,14 @@ impl LanguageRegistry { config.name.clone(), config.grammar.clone(), config.matcher.clone(), - move || Ok((config.clone(), Default::default(), None)), + move || { + Ok(LoadedLanguage { + config: config.clone(), + queries: Default::default(), + toolchain_provider: None, + context_provider: None, + }) + }, ) } @@ -424,14 +436,7 @@ impl LanguageRegistry { name: LanguageName, grammar_name: Option>, matcher: LanguageMatcher, - load: impl Fn() -> Result<( - LanguageConfig, - LanguageQueries, - Option>, - )> - + 'static - + Send - + Sync, + load: impl Fn() -> Result + 'static + Send + Sync, ) { let load = Arc::new(load); let state = &mut *self.state.write(); @@ -726,16 +731,18 @@ impl LanguageRegistry { self.executor .spawn(async move { let language = async { - let (config, queries, provider) = (language_load)()?; - - if let Some(grammar) = config.grammar.clone() { + let loaded_language = (language_load)()?; + if let Some(grammar) = loaded_language.config.grammar.clone() { let grammar = Some(this.get_or_load_grammar(grammar).await?); - Language::new_with_id(id, config, grammar) - .with_context_provider(provider) - .with_queries(queries) + + Language::new_with_id(id, loaded_language.config, grammar) + .with_context_provider(loaded_language.context_provider) + .with_toolchain_lister(loaded_language.toolchain_provider) + .with_queries(loaded_language.queries) } else { - Ok(Language::new_with_id(id, config, None) - .with_context_provider(provider)) + Ok(Language::new_with_id(id, loaded_language.config, None) + .with_context_provider(loaded_language.context_provider) + .with_toolchain_lister(loaded_language.toolchain_provider)) } } .await; diff --git a/crates/language/src/toolchain.rs b/crates/language/src/toolchain.rs new file mode 100644 index 0000000000..efb27008d0 --- /dev/null +++ b/crates/language/src/toolchain.rs @@ -0,0 +1,65 @@ +//! Provides support for language toolchains. +//! +//! A language can have associated toolchains, +//! which is a set of tools used to interact with the projects written in said language. +//! For example, a Python project can have an associated virtual environment; a Rust project can have a toolchain override. + +use std::{path::PathBuf, sync::Arc}; + +use async_trait::async_trait; +use gpui::{AsyncAppContext, SharedString}; +use settings::WorktreeId; + +use crate::LanguageName; + +/// Represents a single toolchain. +#[derive(Clone, Debug, PartialEq)] +pub struct Toolchain { + /// User-facing label + pub name: SharedString, + pub path: SharedString, + pub language_name: LanguageName, +} + +#[async_trait(?Send)] +pub trait ToolchainLister: Send + Sync { + async fn list(&self, _: PathBuf) -> ToolchainList; +} + +#[async_trait(?Send)] +pub trait LanguageToolchainStore { + async fn active_toolchain( + self: Arc, + worktree_id: WorktreeId, + language_name: LanguageName, + cx: &mut AsyncAppContext, + ) -> Option; +} + +type DefaultIndex = usize; +#[derive(Default, Clone)] +pub struct ToolchainList { + pub toolchains: Vec, + pub default: Option, + pub groups: Box<[(usize, SharedString)]>, +} + +impl ToolchainList { + pub fn toolchains(&self) -> &[Toolchain] { + &self.toolchains + } + pub fn default_toolchain(&self) -> Option { + self.default.and_then(|ix| self.toolchains.get(ix)).cloned() + } + pub fn group_for_index(&self, index: usize) -> Option<(usize, SharedString)> { + if index >= self.toolchains.len() { + return None; + } + let first_equal_or_greater = self + .groups + .partition_point(|(group_lower_bound, _)| group_lower_bound <= &index); + self.groups + .get(first_equal_or_greater.checked_sub(1)?) + .cloned() + } +} diff --git a/crates/languages/Cargo.toml b/crates/languages/Cargo.toml index d6746575f3..29c52ba301 100644 --- a/crates/languages/Cargo.toml +++ b/crates/languages/Cargo.toml @@ -47,6 +47,11 @@ log.workspace = true lsp.workspace = true node_runtime.workspace = true paths.workspace = true +pet.workspace = true +pet-core.workspace = true +pet-conda.workspace = true +pet-poetry.workspace = true +pet-reporter.workspace = true project.workspace = true regex.workspace = true rope.workspace = true diff --git a/crates/languages/src/json.rs b/crates/languages/src/json.rs index 95c4070b13..28ee884307 100644 --- a/crates/languages/src/json.rs +++ b/crates/languages/src/json.rs @@ -7,7 +7,9 @@ use feature_flags::FeatureFlagAppExt; use futures::StreamExt; use gpui::{AppContext, AsyncAppContext}; use http_client::github::{latest_github_release, GitHubLspBinaryVersion}; -use language::{LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate}; +use language::{ + LanguageRegistry, LanguageServerName, LanguageToolchainStore, LspAdapter, LspAdapterDelegate, +}; use lsp::LanguageServerBinary; use node_runtime::NodeRuntime; use project::ContextProviderWithTasks; @@ -198,6 +200,7 @@ impl LspAdapter for JsonLspAdapter { async fn workspace_configuration( self: Arc, _: &Arc, + _: Arc, cx: &mut AsyncAppContext, ) -> Result { cx.update(|cx| { diff --git a/crates/languages/src/lib.rs b/crates/languages/src/lib.rs index 03c4735d6d..2fd8ffa633 100644 --- a/crates/languages/src/lib.rs +++ b/crates/languages/src/lib.rs @@ -3,7 +3,7 @@ use gpui::{AppContext, UpdateGlobal}; use json::json_task_context; pub use language::*; use node_runtime::NodeRuntime; -use python::PythonContextProvider; +use python::{PythonContextProvider, PythonToolchainProvider}; use rust_embed::RustEmbed; use settings::SettingsStore; use smol::stream::StreamExt; @@ -61,7 +61,14 @@ pub fn init(languages: Arc, node_runtime: NodeRuntime, cx: &mu config.name.clone(), config.grammar.clone(), config.matcher.clone(), - move || Ok((config.clone(), load_queries($name), None)), + move || { + Ok(LoadedLanguage { + config: config.clone(), + queries: load_queries($name), + context_provider: None, + toolchain_provider: None, + }) + }, ); }; ($name:literal, $adapters:expr) => { @@ -75,7 +82,14 @@ pub fn init(languages: Arc, node_runtime: NodeRuntime, cx: &mu config.name.clone(), config.grammar.clone(), config.matcher.clone(), - move || Ok((config.clone(), load_queries($name), None)), + move || { + Ok(LoadedLanguage { + config: config.clone(), + queries: load_queries($name), + context_provider: None, + toolchain_provider: None, + }) + }, ); }; ($name:literal, $adapters:expr, $context_provider:expr) => { @@ -90,11 +104,33 @@ pub fn init(languages: Arc, node_runtime: NodeRuntime, cx: &mu config.grammar.clone(), config.matcher.clone(), move || { - Ok(( - config.clone(), - load_queries($name), - Some(Arc::new($context_provider)), - )) + Ok(LoadedLanguage { + config: config.clone(), + queries: load_queries($name), + context_provider: Some(Arc::new($context_provider)), + toolchain_provider: None, + }) + }, + ); + }; + ($name:literal, $adapters:expr, $context_provider:expr, $toolchain_provider:expr) => { + let config = load_config($name); + // typeck helper + let adapters: Vec> = $adapters; + for adapter in adapters { + languages.register_lsp_adapter(config.name.clone(), adapter); + } + languages.register_language( + config.name.clone(), + config.grammar.clone(), + config.matcher.clone(), + move || { + Ok(LoadedLanguage { + config: config.clone(), + queries: load_queries($name), + context_provider: Some(Arc::new($context_provider)), + toolchain_provider: Some($toolchain_provider), + }) }, ); }; @@ -141,7 +177,8 @@ pub fn init(languages: Arc, node_runtime: NodeRuntime, cx: &mu vec![Arc::new(python::PythonLspAdapter::new( node_runtime.clone(), ))], - PythonContextProvider + PythonContextProvider, + Arc::new(PythonToolchainProvider::default()) as Arc ); language!( "rust", diff --git a/crates/languages/src/python.rs b/crates/languages/src/python.rs index 4b5fe3d277..e73e3c8682 100644 --- a/crates/languages/src/python.rs +++ b/crates/languages/src/python.rs @@ -3,9 +3,16 @@ use async_trait::async_trait; use collections::HashMap; use gpui::AppContext; use gpui::AsyncAppContext; +use language::LanguageName; +use language::LanguageToolchainStore; +use language::Toolchain; +use language::ToolchainList; +use language::ToolchainLister; use language::{ContextProvider, LanguageServerName, LspAdapter, LspAdapterDelegate}; use lsp::LanguageServerBinary; use node_runtime::NodeRuntime; +use pet_core::python_environment::PythonEnvironmentKind; +use pet_core::Configuration; use project::lsp_store::language_server_settings; use serde_json::Value; @@ -200,12 +207,35 @@ impl LspAdapter for PythonLspAdapter { async fn workspace_configuration( self: Arc, adapter: &Arc, + toolchains: Arc, cx: &mut AsyncAppContext, ) -> Result { - cx.update(|cx| { - language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx) - .and_then(|s| s.settings.clone()) - .unwrap_or_default() + let toolchain = toolchains + .active_toolchain(adapter.worktree_id(), LanguageName::new("Python"), cx) + .await; + cx.update(move |cx| { + let mut user_settings = + language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx) + .and_then(|s| s.settings.clone()) + .unwrap_or_default(); + + // If python.pythonPath is not set in user config, do so using our toolchain picker. + if let Some(toolchain) = toolchain { + if user_settings.is_null() { + user_settings = Value::Object(serde_json::Map::default()); + } + let object = user_settings.as_object_mut().unwrap(); + if let Some(python) = object + .entry("python") + .or_insert(Value::Object(serde_json::Map::default())) + .as_object_mut() + { + python + .entry("pythonPath") + .or_insert(Value::String(toolchain.path.into())); + } + } + user_settings }) } } @@ -320,6 +350,83 @@ fn python_module_name_from_relative_path(relative_path: &str) -> String { .to_string() } +#[derive(Default)] +pub(crate) struct PythonToolchainProvider {} + +static ENV_PRIORITY_LIST: &'static [PythonEnvironmentKind] = &[ + // Prioritize non-Conda environments. + PythonEnvironmentKind::Poetry, + PythonEnvironmentKind::Pipenv, + PythonEnvironmentKind::VirtualEnvWrapper, + PythonEnvironmentKind::Venv, + PythonEnvironmentKind::VirtualEnv, + PythonEnvironmentKind::Conda, + PythonEnvironmentKind::Pyenv, + PythonEnvironmentKind::GlobalPaths, + PythonEnvironmentKind::Homebrew, +]; + +fn env_priority(kind: Option) -> usize { + if let Some(kind) = kind { + ENV_PRIORITY_LIST + .iter() + .position(|blessed_env| blessed_env == &kind) + .unwrap_or(ENV_PRIORITY_LIST.len()) + } else { + // Unknown toolchains are less useful than non-blessed ones. + ENV_PRIORITY_LIST.len() + 1 + } +} + +#[async_trait(?Send)] +impl ToolchainLister for PythonToolchainProvider { + async fn list(&self, worktree_root: PathBuf) -> ToolchainList { + let environment = pet_core::os_environment::EnvironmentApi::new(); + let locators = pet::locators::create_locators( + Arc::new(pet_conda::Conda::from(&environment)), + Arc::new(pet_poetry::Poetry::from(&environment)), + &environment, + ); + let mut config = Configuration::default(); + config.workspace_directories = Some(vec![worktree_root]); + let reporter = pet_reporter::collect::create_reporter(); + pet::find::find_and_report_envs(&reporter, config, &locators, &environment, None); + + let mut toolchains = reporter + .environments + .lock() + .ok() + .map_or(Vec::new(), |mut guard| std::mem::take(&mut guard)); + toolchains.sort_by(|lhs, rhs| { + env_priority(lhs.kind) + .cmp(&env_priority(rhs.kind)) + .then_with(|| lhs.executable.cmp(&rhs.executable)) + }); + let mut toolchains: Vec<_> = toolchains + .into_iter() + .filter_map(|toolchain| { + let name = if let Some(version) = &toolchain.version { + format!("Python {version} ({:?})", toolchain.kind?) + } else { + format!("{:?}", toolchain.kind?) + } + .into(); + Some(Toolchain { + name, + path: toolchain.executable?.to_str()?.to_owned().into(), + language_name: LanguageName::new("Python"), + }) + }) + .collect(); + toolchains.dedup(); + ToolchainList { + toolchains, + default: None, + groups: Default::default(), + } + } +} + #[cfg(test)] mod tests { use gpui::{BorrowAppContext, Context, ModelContext, TestAppContext}; diff --git a/crates/languages/src/tailwind.rs b/crates/languages/src/tailwind.rs index 4ed5c742a9..6d4416c7d9 100644 --- a/crates/languages/src/tailwind.rs +++ b/crates/languages/src/tailwind.rs @@ -3,7 +3,7 @@ use async_trait::async_trait; use collections::HashMap; use futures::StreamExt; use gpui::AsyncAppContext; -use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use language::{LanguageServerName, LanguageToolchainStore, LspAdapter, LspAdapterDelegate}; use lsp::LanguageServerBinary; use node_runtime::NodeRuntime; use project::lsp_store::language_server_settings; @@ -111,6 +111,7 @@ impl LspAdapter for TailwindLspAdapter { async fn workspace_configuration( self: Arc, delegate: &Arc, + _: Arc, cx: &mut AsyncAppContext, ) -> Result { let tailwind_user_settings = cx.update(|cx| { diff --git a/crates/languages/src/typescript.rs b/crates/languages/src/typescript.rs index cfd7e04bc6..345a5f0694 100644 --- a/crates/languages/src/typescript.rs +++ b/crates/languages/src/typescript.rs @@ -5,7 +5,7 @@ use async_trait::async_trait; use collections::HashMap; use gpui::AsyncAppContext; use http_client::github::{build_asset_url, AssetKind, GitHubLspBinaryVersion}; -use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use language::{LanguageServerName, LanguageToolchainStore, LspAdapter, LspAdapterDelegate}; use lsp::{CodeActionKind, LanguageServerBinary}; use node_runtime::NodeRuntime; use project::lsp_store::language_server_settings; @@ -230,6 +230,7 @@ impl LspAdapter for TypeScriptLspAdapter { async fn workspace_configuration( self: Arc, delegate: &Arc, + _: Arc, cx: &mut AsyncAppContext, ) -> Result { let override_options = cx.update(|cx| { @@ -325,6 +326,7 @@ impl LspAdapter for EsLintLspAdapter { async fn workspace_configuration( self: Arc, delegate: &Arc, + _: Arc, cx: &mut AsyncAppContext, ) -> Result { let workspace_root = delegate.worktree_root_path(); diff --git a/crates/languages/src/vtsls.rs b/crates/languages/src/vtsls.rs index ff8637dc28..ae65488a38 100644 --- a/crates/languages/src/vtsls.rs +++ b/crates/languages/src/vtsls.rs @@ -2,7 +2,7 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use collections::HashMap; use gpui::AsyncAppContext; -use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use language::{LanguageServerName, LanguageToolchainStore, LspAdapter, LspAdapterDelegate}; use lsp::{CodeActionKind, LanguageServerBinary}; use node_runtime::NodeRuntime; use project::lsp_store::language_server_settings; @@ -183,6 +183,7 @@ impl LspAdapter for VtslsLspAdapter { async fn workspace_configuration( self: Arc, delegate: &Arc, + _: Arc, cx: &mut AsyncAppContext, ) -> Result { let tsdk_path = Self::tsdk_path(delegate).await; diff --git a/crates/languages/src/yaml.rs b/crates/languages/src/yaml.rs index 9f1c468b87..d8f927b770 100644 --- a/crates/languages/src/yaml.rs +++ b/crates/languages/src/yaml.rs @@ -3,7 +3,8 @@ use async_trait::async_trait; use futures::StreamExt; use gpui::AsyncAppContext; use language::{ - language_settings::AllLanguageSettings, LanguageServerName, LspAdapter, LspAdapterDelegate, + language_settings::AllLanguageSettings, LanguageServerName, LanguageToolchainStore, LspAdapter, + LspAdapterDelegate, }; use lsp::LanguageServerBinary; use node_runtime::NodeRuntime; @@ -92,6 +93,7 @@ impl LspAdapter for YamlLspAdapter { async fn workspace_configuration( self: Arc, delegate: &Arc, + _: Arc, cx: &mut AsyncAppContext, ) -> Result { let location = SettingsLocation { diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 8152ddb3c0..40e87b55e5 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -7,10 +7,11 @@ use crate::{ prettier_store::{self, PrettierStore, PrettierStoreEvent}, project_settings::{LspSettings, ProjectSettings}, relativize_path, resolve_path, + toolchain_store::{EmptyToolchainStore, ToolchainStoreEvent}, worktree_store::{WorktreeStore, WorktreeStoreEvent}, yarn::YarnPathStore, CodeAction, Completion, CoreCompletion, Hover, InlayHint, Item as _, ProjectPath, - ProjectTransaction, ResolveState, Symbol, + ProjectTransaction, ResolveState, Symbol, ToolchainStore, }; use anyhow::{anyhow, Context as _, Result}; use async_trait::async_trait; @@ -36,9 +37,9 @@ use language::{ proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, range_from_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeLabel, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Documentation, File as _, Language, LanguageName, - LanguageRegistry, LanguageServerBinaryStatus, LanguageServerName, LocalFile, LspAdapter, - LspAdapterDelegate, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, - Unclipped, + LanguageRegistry, LanguageServerBinaryStatus, LanguageServerName, LanguageToolchainStore, + LocalFile, LspAdapter, LspAdapterDelegate, Patch, PointUtf16, TextBufferSnapshot, ToOffset, + ToPointUtf16, Transaction, Unclipped, }; use lsp::{ CodeActionKind, CompletionContext, DiagnosticSeverity, DiagnosticTag, @@ -707,12 +708,13 @@ pub struct LspStore { nonce: u128, buffer_store: Model, worktree_store: Model, + toolchain_store: Option>, buffer_snapshots: HashMap>>, // buffer_id -> server_id -> vec of snapshots pub languages: Arc, language_server_ids: HashMap<(WorktreeId, LanguageServerName), LanguageServerId>, pub language_server_statuses: BTreeMap, active_entry: Option, - _maintain_workspace_config: Task>, + _maintain_workspace_config: (Task>, watch::Sender<()>), _maintain_buffer_languages: Task<()>, next_diagnostic_group_id: usize, diagnostic_summaries: @@ -871,6 +873,7 @@ impl LspStore { buffer_store: Model, worktree_store: Model, prettier_store: Model, + toolchain_store: Model, environment: Model, languages: Arc, http_client: Arc, @@ -884,9 +887,15 @@ impl LspStore { .detach(); cx.subscribe(&prettier_store, Self::on_prettier_store_event) .detach(); + cx.subscribe(&toolchain_store, Self::on_toolchain_store_event) + .detach(); cx.observe_global::(Self::on_settings_changed) .detach(); + let _maintain_workspace_config = { + let (sender, receiver) = watch::channel(); + (Self::maintain_workspace_config(receiver, cx), sender) + }; Self { mode: LspStoreMode::Local(LocalLspStore { supplementary_language_servers: Default::default(), @@ -909,6 +918,7 @@ impl LspStore { downstream_client: None, buffer_store, worktree_store, + toolchain_store: Some(toolchain_store), languages: languages.clone(), language_server_ids: Default::default(), language_server_statuses: Default::default(), @@ -919,7 +929,7 @@ impl LspStore { diagnostics: Default::default(), active_entry: None, - _maintain_workspace_config: Self::maintain_workspace_config(cx), + _maintain_workspace_config, _maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx), } } @@ -942,9 +952,10 @@ impl LspStore { }) } - pub fn new_remote( + pub(super) fn new_remote( buffer_store: Model, worktree_store: Model, + toolchain_store: Option>, languages: Arc, upstream_client: AnyProtoClient, project_id: u64, @@ -954,7 +965,10 @@ impl LspStore { .detach(); cx.subscribe(&worktree_store, Self::on_worktree_store_event) .detach(); - + let _maintain_workspace_config = { + let (sender, receiver) = watch::channel(); + (Self::maintain_workspace_config(receiver, cx), sender) + }; Self { mode: LspStoreMode::Remote(RemoteLspStore { upstream_client: Some(upstream_client), @@ -972,7 +986,8 @@ impl LspStore { diagnostic_summaries: Default::default(), diagnostics: Default::default(), active_entry: None, - _maintain_workspace_config: Self::maintain_workspace_config(cx), + toolchain_store, + _maintain_workspace_config, _maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx), } } @@ -1063,6 +1078,22 @@ impl LspStore { } } + fn on_toolchain_store_event( + &mut self, + _: Model, + event: &ToolchainStoreEvent, + _: &mut ModelContext, + ) { + match event { + ToolchainStoreEvent::ToolchainActivated { .. } => { + self.request_workspace_config_refresh() + } + } + } + + fn request_workspace_config_refresh(&mut self) { + *self._maintain_workspace_config.1.borrow_mut() = (); + } // todo! pub fn prettier_store(&self) -> Option> { self.as_local().map(|local| local.prettier_store.clone()) @@ -3029,17 +3060,13 @@ impl LspStore { None } - fn maintain_workspace_config(cx: &mut ModelContext) -> Task> { - let (mut settings_changed_tx, mut settings_changed_rx) = watch::channel(); - let _ = postage::stream::Stream::try_recv(&mut settings_changed_rx); - - let settings_observation = cx.observe_global::(move |_, _| { - *settings_changed_tx.borrow_mut() = (); - }); - - cx.spawn(move |this, mut cx| async move { - while let Some(()) = settings_changed_rx.next().await { - let servers = this.update(&mut cx, |this, cx| { + pub(crate) async fn refresh_workspace_configurations( + this: &WeakModel, + mut cx: AsyncAppContext, + ) { + maybe!(async move { + let servers = this + .update(&mut cx, |this, cx| { this.language_server_ids .iter() .filter_map(|((worktree_id, _), server_id)| { @@ -3061,17 +3088,52 @@ impl LspStore { } }) .collect::>() - })?; + }) + .ok()?; - for (adapter, server, delegate) in servers { - let settings = adapter.workspace_configuration(&delegate, &mut cx).await?; + let toolchain_store = this + .update(&mut cx, |this, cx| this.toolchain_store(cx)) + .ok()?; + for (adapter, server, delegate) in servers { + let settings = adapter + .workspace_configuration(&delegate, toolchain_store.clone(), &mut cx) + .await + .ok()?; - server - .notify::( - lsp::DidChangeConfigurationParams { settings }, - ) - .ok(); - } + server + .notify::( + lsp::DidChangeConfigurationParams { settings }, + ) + .ok(); + } + Some(()) + }) + .await; + } + + fn toolchain_store(&self, cx: &AppContext) -> Arc { + if let Some(toolchain_store) = self.toolchain_store.as_ref() { + toolchain_store.read(cx).as_language_toolchain_store() + } else { + Arc::new(EmptyToolchainStore) + } + } + fn maintain_workspace_config( + external_refresh_requests: watch::Receiver<()>, + cx: &mut ModelContext, + ) -> Task> { + let (mut settings_changed_tx, mut settings_changed_rx) = watch::channel(); + let _ = postage::stream::Stream::try_recv(&mut settings_changed_rx); + + let settings_observation = cx.observe_global::(move |_, _| { + *settings_changed_tx.borrow_mut() = (); + }); + + let mut joint_future = + futures::stream::select(settings_changed_rx, external_refresh_requests); + cx.spawn(move |this, cx| async move { + while let Some(()) = joint_future.next().await { + Self::refresh_workspace_configurations(&this, cx.clone()).await; } drop(settings_observation); @@ -5517,6 +5579,9 @@ impl LspStore { let delegate = delegate.clone(); let adapter = adapter.clone(); let this = this.clone(); + let toolchains = this + .update(&mut cx, |this, cx| this.toolchain_store(cx)) + .ok()?; let mut cx = cx.clone(); async move { let language_server = pending_server.await?; @@ -5524,7 +5589,7 @@ impl LspStore { let workspace_config = adapter .adapter .clone() - .workspace_configuration(&delegate, &mut cx) + .workspace_configuration(&delegate, toolchains.clone(), &mut cx) .await?; let mut initialization_options = adapter @@ -5864,17 +5929,21 @@ impl LspStore { } }) .detach(); - language_server .on_request::({ let adapter = adapter.adapter.clone(); let delegate = delegate.clone(); + let this = this.clone(); move |params, mut cx| { let adapter = adapter.clone(); let delegate = delegate.clone(); + let this = this.clone(); async move { - let workspace_config = - adapter.workspace_configuration(&delegate, &mut cx).await?; + let toolchains = + this.update(&mut cx, |this, cx| this.toolchain_store(cx))?; + let workspace_config = adapter + .workspace_configuration(&delegate, toolchains, &mut cx) + .await?; Ok(params .items .into_iter() diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 49f4b7c6f3..7a57e048c8 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -11,6 +11,7 @@ pub mod search; mod task_inventory; pub mod task_store; pub mod terminals; +pub mod toolchain_store; pub mod worktree_store; #[cfg(test)] @@ -44,8 +45,8 @@ use itertools::Itertools; use language::{ language_settings::InlayHintKind, proto::split_operations, Buffer, BufferEvent, CachedLspAdapter, Capability, CodeLabel, DiagnosticEntry, Documentation, File as _, Language, - LanguageRegistry, LanguageServerName, PointUtf16, ToOffset, ToPointUtf16, Transaction, - Unclipped, + LanguageName, LanguageRegistry, LanguageServerName, PointUtf16, ToOffset, ToPointUtf16, + Toolchain, ToolchainList, Transaction, Unclipped, }; use lsp::{ CompletionContext, CompletionItemKind, DocumentHighlightKind, LanguageServer, LanguageServerId, @@ -101,7 +102,7 @@ pub use lsp_store::{ LanguageServerStatus, LanguageServerToQuery, LspStore, LspStoreEvent, SERVER_PROGRESS_THROTTLE_TIMEOUT, }; - +pub use toolchain_store::ToolchainStore; const MAX_PROJECT_SEARCH_HISTORY_SIZE: usize = 500; const MAX_SEARCH_RESULT_FILES: usize = 5_000; const MAX_SEARCH_RESULT_RANGES: usize = 10_000; @@ -158,6 +159,7 @@ pub struct Project { snippets: Model, environment: Model, settings_observer: Model, + toolchain_store: Option>, } #[derive(Default)] @@ -579,6 +581,7 @@ impl Project { LspStore::init(&client); SettingsObserver::init(&client); TaskStore::init(Some(&client)); + ToolchainStore::init(&client); } pub fn local( @@ -635,12 +638,15 @@ impl Project { }); cx.subscribe(&settings_observer, Self::on_settings_observer_event) .detach(); - + let toolchain_store = cx.new_model(|cx| { + ToolchainStore::local(languages.clone(), worktree_store.clone(), cx) + }); let lsp_store = cx.new_model(|cx| { LspStore::new_local( buffer_store.clone(), worktree_store.clone(), prettier_store.clone(), + toolchain_store.clone(), environment.clone(), languages.clone(), client.http_client(), @@ -681,6 +687,8 @@ impl Project { search_included_history: Self::new_search_history(), search_excluded_history: Self::new_search_history(), + + toolchain_store: Some(toolchain_store), } }) } @@ -737,10 +745,14 @@ impl Project { .detach(); let environment = ProjectEnvironment::new(&worktree_store, None, cx); + let toolchain_store = Some(cx.new_model(|cx| { + ToolchainStore::remote(SSH_PROJECT_ID, ssh.read(cx).proto_client(), cx) + })); let lsp_store = cx.new_model(|cx| { LspStore::new_remote( buffer_store.clone(), worktree_store.clone(), + toolchain_store.clone(), languages.clone(), ssh_proto.clone(), SSH_PROJECT_ID, @@ -798,6 +810,8 @@ impl Project { search_included_history: Self::new_search_history(), search_excluded_history: Self::new_search_history(), + + toolchain_store, }; let ssh = ssh.read(cx); @@ -818,6 +832,7 @@ impl Project { LspStore::init(&ssh_proto); SettingsObserver::init(&ssh_proto); TaskStore::init(Some(&ssh_proto)); + ToolchainStore::init(&ssh_proto); this }) @@ -905,6 +920,7 @@ impl Project { let mut lsp_store = LspStore::new_remote( buffer_store.clone(), worktree_store.clone(), + None, languages.clone(), client.clone().into(), remote_id, @@ -993,6 +1009,7 @@ impl Project { search_excluded_history: Self::new_search_history(), environment: ProjectEnvironment::new(&worktree_store, None, cx), remotely_created_models: Arc::new(Mutex::new(RemotelyCreatedModels::default())), + toolchain_store: None, }; this.set_role(role, cx); for worktree in worktrees { @@ -2346,6 +2363,46 @@ impl Project { .map_err(|e| anyhow!(e)) } + pub fn available_toolchains( + &self, + worktree_id: WorktreeId, + language_name: LanguageName, + cx: &AppContext, + ) -> Task> { + if let Some(toolchain_store) = self.toolchain_store.as_ref() { + toolchain_store + .read(cx) + .list_toolchains(worktree_id, language_name, cx) + } else { + Task::ready(None) + } + } + pub fn activate_toolchain( + &self, + worktree_id: WorktreeId, + toolchain: Toolchain, + cx: &mut AppContext, + ) -> Task> { + let Some(toolchain_store) = self.toolchain_store.clone() else { + return Task::ready(None); + }; + toolchain_store.update(cx, |this, cx| { + this.activate_toolchain(worktree_id, toolchain, cx) + }) + } + pub fn active_toolchain( + &self, + worktree_id: WorktreeId, + language_name: LanguageName, + cx: &AppContext, + ) -> Task> { + let Some(toolchain_store) = self.toolchain_store.clone() else { + return Task::ready(None); + }; + toolchain_store + .read(cx) + .active_toolchain(worktree_id, language_name, cx) + } pub fn language_server_statuses<'a>( &'a self, cx: &'a AppContext, diff --git a/crates/project/src/toolchain_store.rs b/crates/project/src/toolchain_store.rs new file mode 100644 index 0000000000..a3f27d731b --- /dev/null +++ b/crates/project/src/toolchain_store.rs @@ -0,0 +1,416 @@ +use std::sync::Arc; + +use anyhow::{bail, Result}; + +use async_trait::async_trait; +use collections::BTreeMap; +use gpui::{ + AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task, + WeakModel, +}; +use language::{LanguageName, LanguageRegistry, LanguageToolchainStore, Toolchain, ToolchainList}; +use rpc::{proto, AnyProtoClient, TypedEnvelope}; +use settings::WorktreeId; +use util::ResultExt as _; + +use crate::worktree_store::WorktreeStore; + +pub struct ToolchainStore(ToolchainStoreInner); +enum ToolchainStoreInner { + Local(Model, #[allow(dead_code)] Subscription), + Remote(Model), +} + +impl EventEmitter for ToolchainStore {} +impl ToolchainStore { + pub fn init(client: &AnyProtoClient) { + client.add_model_request_handler(Self::handle_activate_toolchain); + client.add_model_request_handler(Self::handle_list_toolchains); + client.add_model_request_handler(Self::handle_active_toolchain); + } + + pub fn local( + languages: Arc, + worktree_store: Model, + cx: &mut ModelContext, + ) -> Self { + let model = cx.new_model(|_| LocalToolchainStore { + languages, + worktree_store, + active_toolchains: Default::default(), + }); + let subscription = cx.subscribe(&model, |_, _, e: &ToolchainStoreEvent, cx| { + cx.emit(e.clone()) + }); + Self(ToolchainStoreInner::Local(model, subscription)) + } + pub(super) fn remote(project_id: u64, client: AnyProtoClient, cx: &mut AppContext) -> Self { + Self(ToolchainStoreInner::Remote( + cx.new_model(|_| RemoteToolchainStore { client, project_id }), + )) + } + pub(crate) fn activate_toolchain( + &self, + worktree_id: WorktreeId, + toolchain: Toolchain, + cx: &mut AppContext, + ) -> Task> { + match &self.0 { + ToolchainStoreInner::Local(local, _) => local.update(cx, |this, cx| { + this.activate_toolchain(worktree_id, toolchain, cx) + }), + ToolchainStoreInner::Remote(remote) => { + remote + .read(cx) + .activate_toolchain(worktree_id, toolchain, cx) + } + } + } + pub(crate) fn list_toolchains( + &self, + worktree_id: WorktreeId, + language_name: LanguageName, + cx: &AppContext, + ) -> Task> { + match &self.0 { + ToolchainStoreInner::Local(local, _) => { + local + .read(cx) + .list_toolchains(worktree_id, language_name, cx) + } + ToolchainStoreInner::Remote(remote) => { + remote + .read(cx) + .list_toolchains(worktree_id, language_name, cx) + } + } + } + pub(crate) fn active_toolchain( + &self, + worktree_id: WorktreeId, + language_name: LanguageName, + cx: &AppContext, + ) -> Task> { + match &self.0 { + ToolchainStoreInner::Local(local, _) => { + local + .read(cx) + .active_toolchain(worktree_id, language_name, cx) + } + ToolchainStoreInner::Remote(remote) => { + remote + .read(cx) + .active_toolchain(worktree_id, language_name, cx) + } + } + } + async fn handle_activate_toolchain( + this: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result { + this.update(&mut cx, |this, cx| { + let language_name = LanguageName::from_proto(envelope.payload.language_name); + let Some(toolchain) = envelope.payload.toolchain else { + bail!("Missing `toolchain` in payload"); + }; + let toolchain = Toolchain { + name: toolchain.name.into(), + path: toolchain.path.into(), + language_name, + }; + let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); + Ok(this.activate_toolchain(worktree_id, toolchain, cx)) + })?? + .await; + Ok(proto::Ack {}) + } + async fn handle_active_toolchain( + this: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result { + let toolchain = this + .update(&mut cx, |this, cx| { + let language_name = LanguageName::from_proto(envelope.payload.language_name); + let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); + this.active_toolchain(worktree_id, language_name, cx) + })? + .await; + + Ok(proto::ActiveToolchainResponse { + toolchain: toolchain.map(|toolchain| proto::Toolchain { + name: toolchain.name.into(), + path: toolchain.path.into(), + }), + }) + } + + async fn handle_list_toolchains( + this: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result { + let toolchains = this + .update(&mut cx, |this, cx| { + let language_name = LanguageName::from_proto(envelope.payload.language_name); + let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); + this.list_toolchains(worktree_id, language_name, cx) + })? + .await; + let has_values = toolchains.is_some(); + let groups = if let Some(toolchains) = &toolchains { + toolchains + .groups + .iter() + .filter_map(|group| { + Some(proto::ToolchainGroup { + start_index: u64::try_from(group.0).ok()?, + name: String::from(group.1.as_ref()), + }) + }) + .collect() + } else { + vec![] + }; + let toolchains = if let Some(toolchains) = toolchains { + toolchains + .toolchains + .into_iter() + .map(|toolchain| proto::Toolchain { + name: toolchain.name.to_string(), + path: toolchain.path.to_string(), + }) + .collect::>() + } else { + vec![] + }; + + Ok(proto::ListToolchainsResponse { + has_values, + toolchains, + groups, + }) + } + pub(crate) fn as_language_toolchain_store(&self) -> Arc { + match &self.0 { + ToolchainStoreInner::Local(local, _) => Arc::new(LocalStore(local.downgrade())), + ToolchainStoreInner::Remote(remote) => Arc::new(RemoteStore(remote.downgrade())), + } + } +} + +struct LocalToolchainStore { + languages: Arc, + worktree_store: Model, + active_toolchains: BTreeMap<(WorktreeId, LanguageName), Toolchain>, +} + +#[async_trait(?Send)] +impl language::LanguageToolchainStore for LocalStore { + async fn active_toolchain( + self: Arc, + worktree_id: WorktreeId, + language_name: LanguageName, + cx: &mut AsyncAppContext, + ) -> Option { + self.0 + .update(cx, |this, cx| { + this.active_toolchain(worktree_id, language_name, cx) + }) + .ok()? + .await + } +} + +#[async_trait(?Send)] +impl language::LanguageToolchainStore for RemoteStore { + async fn active_toolchain( + self: Arc, + worktree_id: WorktreeId, + language_name: LanguageName, + cx: &mut AsyncAppContext, + ) -> Option { + self.0 + .update(cx, |this, cx| { + this.active_toolchain(worktree_id, language_name, cx) + }) + .ok()? + .await + } +} + +pub(crate) struct EmptyToolchainStore; +#[async_trait(?Send)] +impl language::LanguageToolchainStore for EmptyToolchainStore { + async fn active_toolchain( + self: Arc, + _: WorktreeId, + _: LanguageName, + _: &mut AsyncAppContext, + ) -> Option { + None + } +} +struct LocalStore(WeakModel); +struct RemoteStore(WeakModel); + +#[derive(Clone)] +pub(crate) enum ToolchainStoreEvent { + ToolchainActivated, +} + +impl EventEmitter for LocalToolchainStore {} + +impl LocalToolchainStore { + pub(crate) fn activate_toolchain( + &self, + worktree_id: WorktreeId, + toolchain: Toolchain, + cx: &mut ModelContext, + ) -> Task> { + cx.spawn(move |this, mut cx| async move { + this.update(&mut cx, |this, cx| { + this.active_toolchains.insert( + (worktree_id, toolchain.language_name.clone()), + toolchain.clone(), + ); + cx.emit(ToolchainStoreEvent::ToolchainActivated); + }) + .ok(); + Some(()) + }) + } + pub(crate) fn list_toolchains( + &self, + worktree_id: WorktreeId, + language_name: LanguageName, + cx: &AppContext, + ) -> Task> { + let registry = self.languages.clone(); + let Some(root) = self + .worktree_store + .read(cx) + .worktree_for_id(worktree_id, cx) + .map(|worktree| worktree.read(cx).abs_path()) + else { + return Task::ready(None); + }; + cx.spawn(|_| async move { + let language = registry.language_for_name(&language_name.0).await.ok()?; + let toolchains = language.toolchain_lister()?.list(root.to_path_buf()).await; + Some(toolchains) + }) + } + pub(crate) fn active_toolchain( + &self, + worktree_id: WorktreeId, + language_name: LanguageName, + _: &AppContext, + ) -> Task> { + Task::ready( + self.active_toolchains + .get(&(worktree_id, language_name)) + .cloned(), + ) + } +} +struct RemoteToolchainStore { + client: AnyProtoClient, + project_id: u64, +} + +impl RemoteToolchainStore { + pub(crate) fn activate_toolchain( + &self, + worktree_id: WorktreeId, + toolchain: Toolchain, + cx: &AppContext, + ) -> Task> { + let project_id = self.project_id; + let client = self.client.clone(); + cx.spawn(move |_| async move { + let _ = client + .request(proto::ActivateToolchain { + project_id, + worktree_id: worktree_id.to_proto(), + language_name: toolchain.language_name.into(), + toolchain: Some(proto::Toolchain { + name: toolchain.name.into(), + path: toolchain.path.into(), + }), + }) + .await + .log_err()?; + Some(()) + }) + } + pub(crate) fn list_toolchains( + &self, + worktree_id: WorktreeId, + language_name: LanguageName, + cx: &AppContext, + ) -> Task> { + let project_id = self.project_id; + let client = self.client.clone(); + cx.spawn(move |_| async move { + let response = client + .request(proto::ListToolchains { + project_id, + worktree_id: worktree_id.to_proto(), + language_name: language_name.clone().into(), + }) + .await + .log_err()?; + if !response.has_values { + return None; + } + let toolchains = response + .toolchains + .into_iter() + .map(|toolchain| Toolchain { + language_name: language_name.clone(), + name: toolchain.name.into(), + path: toolchain.path.into(), + }) + .collect(); + let groups = response + .groups + .into_iter() + .filter_map(|group| { + Some((usize::try_from(group.start_index).ok()?, group.name.into())) + }) + .collect(); + Some(ToolchainList { + toolchains, + default: None, + groups, + }) + }) + } + pub(crate) fn active_toolchain( + &self, + worktree_id: WorktreeId, + language_name: LanguageName, + cx: &AppContext, + ) -> Task> { + let project_id = self.project_id; + let client = self.client.clone(); + cx.spawn(move |_| async move { + let response = client + .request(proto::ActiveToolchain { + project_id, + worktree_id: worktree_id.to_proto(), + language_name: language_name.clone().into(), + }) + .await + .log_err()?; + + response.toolchain.map(|toolchain| Toolchain { + language_name: language_name.clone(), + name: toolchain.name.into(), + path: toolchain.path.into(), + }) + }) + } +} diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto index 53aaa6ef6d..95a54c3d5c 100644 --- a/crates/proto/proto/zed.proto +++ b/crates/proto/proto/zed.proto @@ -280,11 +280,15 @@ message Envelope { LanguageServerPromptRequest language_server_prompt_request = 268; LanguageServerPromptResponse language_server_prompt_response = 269; - GitBranches git_branches = 270; GitBranchesResponse git_branches_response = 271; - UpdateGitBranch update_git_branch = 272; // current max + UpdateGitBranch update_git_branch = 272; + ListToolchains list_toolchains = 273; + ListToolchainsResponse list_toolchains_response = 274; + ActivateToolchain activate_toolchain = 275; + ActiveToolchain active_toolchain = 276; + ActiveToolchainResponse active_toolchain_response = 277; // current max } @@ -2393,7 +2397,6 @@ message GetPermalinkToLine { message GetPermalinkToLineResponse { string permalink = 1; } - message FlushBufferedMessages {} message FlushBufferedMessagesResponse {} @@ -2419,6 +2422,45 @@ message LanguageServerPromptResponse { optional uint64 action_response = 1; } +message ListToolchains { + uint64 project_id = 1; + uint64 worktree_id = 2; + string language_name = 3; +} + +message Toolchain { + string name = 1; + string path = 2; +} + +message ToolchainGroup { + uint64 start_index = 1; + string name = 2; +} + +message ListToolchainsResponse { + repeated Toolchain toolchains = 1; + bool has_values = 2; + repeated ToolchainGroup groups = 3; +} + +message ActivateToolchain { + uint64 project_id = 1; + uint64 worktree_id = 2; + Toolchain toolchain = 3; + string language_name = 4; +} + +message ActiveToolchain { + uint64 project_id = 1; + uint64 worktree_id = 2; + string language_name = 3; +} + +message ActiveToolchainResponse { + optional Toolchain toolchain = 1; +} + message Branch { bool is_head = 1; string name = 2; @@ -2438,4 +2480,5 @@ message UpdateGitBranch { uint64 project_id = 1; string branch_name = 2; ProjectPath repository = 3; + } diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs index a7140cc7ed..7fcebf0513 100644 --- a/crates/proto/src/proto.rs +++ b/crates/proto/src/proto.rs @@ -358,7 +358,12 @@ messages!( (LanguageServerPromptResponse, Foreground), (GitBranches, Background), (GitBranchesResponse, Background), - (UpdateGitBranch, Background) + (UpdateGitBranch, Background), + (ListToolchains, Foreground), + (ListToolchainsResponse, Foreground), + (ActivateToolchain, Foreground), + (ActiveToolchain, Foreground), + (ActiveToolchainResponse, Foreground) ); request_messages!( @@ -475,7 +480,10 @@ request_messages!( (FlushBufferedMessages, Ack), (LanguageServerPromptRequest, LanguageServerPromptResponse), (GitBranches, GitBranchesResponse), - (UpdateGitBranch, Ack) + (UpdateGitBranch, Ack), + (ListToolchains, ListToolchainsResponse), + (ActivateToolchain, Ack), + (ActiveToolchain, ActiveToolchainResponse) ); entity_messages!( @@ -555,7 +563,10 @@ entity_messages!( GetPermalinkToLine, LanguageServerPromptRequest, GitBranches, - UpdateGitBranch + UpdateGitBranch, + ListToolchains, + ActivateToolchain, + ActiveToolchain ); entity_messages!( diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs index 81be01b6a6..ce34af247f 100644 --- a/crates/remote_server/src/headless_project.rs +++ b/crates/remote_server/src/headless_project.rs @@ -10,7 +10,7 @@ use project::{ search::SearchQuery, task_store::TaskStore, worktree_store::WorktreeStore, - LspStore, LspStoreEvent, PrettierStore, ProjectPath, WorktreeId, + LspStore, LspStoreEvent, PrettierStore, ProjectPath, ToolchainStore, WorktreeId, }; use remote::ssh_session::ChannelClient; use rpc::{ @@ -108,11 +108,14 @@ impl HeadlessProject { observer.shared(SSH_PROJECT_ID, session.clone().into(), cx); observer }); + let toolchain_store = + cx.new_model(|cx| ToolchainStore::local(languages.clone(), worktree_store.clone(), cx)); let lsp_store = cx.new_model(|cx| { let mut lsp_store = LspStore::new_local( buffer_store.clone(), worktree_store.clone(), prettier_store.clone(), + toolchain_store.clone(), environment, languages.clone(), http_client, @@ -143,6 +146,7 @@ impl HeadlessProject { session.subscribe_to_entity(SSH_PROJECT_ID, &cx.handle()); session.subscribe_to_entity(SSH_PROJECT_ID, &lsp_store); session.subscribe_to_entity(SSH_PROJECT_ID, &task_store); + session.subscribe_to_entity(SSH_PROJECT_ID, &toolchain_store); session.subscribe_to_entity(SSH_PROJECT_ID, &settings_observer); client.add_request_handler(cx.weak_model(), Self::handle_list_remote_directory); @@ -166,6 +170,7 @@ impl HeadlessProject { SettingsObserver::init(&client); LspStore::init(&client); TaskStore::init(Some(&client)); + ToolchainStore::init(&client); HeadlessProject { session: client, diff --git a/crates/toolchain_selector/Cargo.toml b/crates/toolchain_selector/Cargo.toml new file mode 100644 index 0000000000..ed80bd0dc9 --- /dev/null +++ b/crates/toolchain_selector/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "toolchain_selector" +version = "0.1.0" +edition = "2021" +publish = false +license = "GPL-3.0-or-later" + +[dependencies] +editor.workspace = true +fuzzy.workspace = true +gpui.workspace = true +language.workspace = true +picker.workspace = true +project.workspace = true +ui.workspace = true +util.workspace = true +workspace.workspace = true + +[lints] +workspace = true + +[lib] +path = "src/toolchain_selector.rs" +doctest = false diff --git a/crates/toolchain_selector/LICENSE-GPL b/crates/toolchain_selector/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/toolchain_selector/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/toolchain_selector/src/active_toolchain.rs b/crates/toolchain_selector/src/active_toolchain.rs new file mode 100644 index 0000000000..74a6bd7107 --- /dev/null +++ b/crates/toolchain_selector/src/active_toolchain.rs @@ -0,0 +1,173 @@ +use editor::Editor; +use gpui::{ + div, AsyncWindowContext, EventEmitter, IntoElement, ParentElement, Render, Subscription, Task, + View, ViewContext, WeakModel, WeakView, +}; +use language::{Buffer, BufferEvent, LanguageName, Toolchain}; +use project::WorktreeId; +use ui::{Button, ButtonCommon, Clickable, FluentBuilder, LabelSize, Tooltip}; +use workspace::{item::ItemHandle, StatusItemView, Workspace}; + +use crate::ToolchainSelector; + +pub struct ActiveToolchain { + active_toolchain: Option, + workspace: WeakView, + active_buffer: Option<(WorktreeId, WeakModel, Subscription)>, + _observe_language_changes: Subscription, + _update_toolchain_task: Task>, +} + +struct LanguageChanged; + +impl EventEmitter for ActiveToolchain {} + +impl ActiveToolchain { + pub fn new(workspace: &Workspace, cx: &mut ViewContext) -> Self { + let view = cx.view().clone(); + Self { + active_toolchain: None, + active_buffer: None, + workspace: workspace.weak_handle(), + _observe_language_changes: cx.subscribe(&view, |this, _, _: &LanguageChanged, cx| { + this._update_toolchain_task = Self::spawn_tracker_task(cx); + }), + _update_toolchain_task: Self::spawn_tracker_task(cx), + } + } + fn spawn_tracker_task(cx: &mut ViewContext) -> Task> { + cx.spawn(|this, mut cx| async move { + let active_file = this + .update(&mut cx, |this, _| { + this.active_buffer + .as_ref() + .map(|(_, buffer, _)| buffer.clone()) + }) + .ok() + .flatten()?; + let workspace = this + .update(&mut cx, |this, _| this.workspace.clone()) + .ok()?; + + let language_name = active_file + .update(&mut cx, |this, _| Some(this.language()?.name())) + .ok() + .flatten()?; + + let worktree_id = active_file + .update(&mut cx, |this, cx| Some(this.file()?.worktree_id(cx))) + .ok() + .flatten()?; + let toolchain = + Self::active_toolchain(workspace, worktree_id, language_name, cx.clone()).await?; + let _ = this.update(&mut cx, |this, cx| { + this.active_toolchain = Some(toolchain); + + cx.notify(); + }); + Some(()) + }) + } + + fn update_lister(&mut self, editor: View, cx: &mut ViewContext) { + let editor = editor.read(cx); + if let Some((_, buffer, _)) = editor.active_excerpt(cx) { + if let Some(worktree_id) = buffer.read(cx).file().map(|file| file.worktree_id(cx)) { + let subscription = cx.subscribe(&buffer, |_, _, event: &BufferEvent, cx| { + if let BufferEvent::LanguageChanged = event { + cx.emit(LanguageChanged) + } + }); + self.active_buffer = Some((worktree_id, buffer.downgrade(), subscription)); + cx.emit(LanguageChanged); + } + } + + cx.notify(); + } + + fn active_toolchain( + workspace: WeakView, + worktree_id: WorktreeId, + language_name: LanguageName, + cx: AsyncWindowContext, + ) -> Task> { + cx.spawn(move |mut cx| async move { + let workspace_id = workspace + .update(&mut cx, |this, _| this.database_id()) + .ok() + .flatten()?; + let selected_toolchain = workspace + .update(&mut cx, |this, cx| { + this.project() + .read(cx) + .active_toolchain(worktree_id, language_name.clone(), cx) + }) + .ok()? + .await; + if let Some(toolchain) = selected_toolchain { + Some(toolchain) + } else { + let project = workspace + .update(&mut cx, |this, _| this.project().clone()) + .ok()?; + let toolchains = cx + .update(|cx| { + project + .read(cx) + .available_toolchains(worktree_id, language_name, cx) + }) + .ok()? + .await?; + if let Some(toolchain) = toolchains.toolchains.first() { + // Since we don't have a selected toolchain, pick one for user here. + workspace::WORKSPACE_DB + .set_toolchain(workspace_id, worktree_id, toolchain.clone()) + .await + .ok()?; + project + .update(&mut cx, |this, cx| { + this.activate_toolchain(worktree_id, toolchain.clone(), cx) + }) + .ok()? + .await; + } + + toolchains.toolchains.first().cloned() + } + }) + } +} + +impl Render for ActiveToolchain { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + div().when_some(self.active_toolchain.as_ref(), |el, active_toolchain| { + el.child( + Button::new("change-toolchain", active_toolchain.name.clone()) + .label_size(LabelSize::Small) + .on_click(cx.listener(|this, _, cx| { + if let Some(workspace) = this.workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + ToolchainSelector::toggle(workspace, cx) + }); + } + })) + .tooltip(|cx| Tooltip::text("Select Toolchain", cx)), + ) + }) + } +} + +impl StatusItemView for ActiveToolchain { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut ViewContext, + ) { + if let Some(editor) = active_pane_item.and_then(|item| item.act_as::(cx)) { + self.active_toolchain.take(); + self.update_lister(editor, cx); + } + cx.notify(); + } +} diff --git a/crates/toolchain_selector/src/toolchain_selector.rs b/crates/toolchain_selector/src/toolchain_selector.rs new file mode 100644 index 0000000000..8a3368f816 --- /dev/null +++ b/crates/toolchain_selector/src/toolchain_selector.rs @@ -0,0 +1,343 @@ +mod active_toolchain; + +pub use active_toolchain::ActiveToolchain; +use editor::Editor; +use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; +use gpui::{ + actions, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model, + ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView, +}; +use language::{LanguageName, Toolchain, ToolchainList}; +use picker::{Picker, PickerDelegate}; +use project::{Project, WorktreeId}; +use std::{path::Path, sync::Arc}; +use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing}; +use util::ResultExt; +use workspace::{ModalView, Workspace}; + +actions!(toolchain, [Select]); + +pub fn init(cx: &mut AppContext) { + cx.observe_new_views(ToolchainSelector::register).detach(); +} + +pub struct ToolchainSelector { + picker: View>, +} + +impl ToolchainSelector { + fn register(workspace: &mut Workspace, _: &mut ViewContext) { + workspace.register_action(move |workspace, _: &Select, cx| { + Self::toggle(workspace, cx); + }); + } + + fn toggle(workspace: &mut Workspace, cx: &mut ViewContext) -> Option<()> { + let (_, buffer, _) = workspace + .active_item(cx)? + .act_as::(cx)? + .read(cx) + .active_excerpt(cx)?; + let project = workspace.project().clone(); + + let language_name = buffer.read(cx).language()?.name(); + let worktree_id = buffer.read(cx).file()?.worktree_id(cx); + let worktree_root_path = project + .read(cx) + .worktree_for_id(worktree_id, cx)? + .read(cx) + .abs_path(); + let workspace_id = workspace.database_id()?; + let weak = workspace.weak_handle(); + cx.spawn(move |workspace, mut cx| async move { + let active_toolchain = workspace::WORKSPACE_DB + .toolchain(workspace_id, worktree_id, language_name.clone()) + .await + .ok() + .flatten(); + workspace + .update(&mut cx, |this, cx| { + this.toggle_modal(cx, move |cx| { + ToolchainSelector::new( + weak, + project, + active_toolchain, + worktree_id, + worktree_root_path, + language_name, + cx, + ) + }); + }) + .ok(); + }) + .detach(); + + Some(()) + } + + fn new( + workspace: WeakView, + project: Model, + active_toolchain: Option, + worktree_id: WorktreeId, + worktree_root: Arc, + language_name: LanguageName, + cx: &mut ViewContext, + ) -> Self { + let view = cx.view().downgrade(); + let picker = cx.new_view(|cx| { + let delegate = ToolchainSelectorDelegate::new( + active_toolchain, + view, + workspace, + worktree_id, + worktree_root, + project, + language_name, + cx, + ); + Picker::uniform_list(delegate, cx) + }); + Self { picker } + } +} + +impl Render for ToolchainSelector { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + v_flex().w(rems(34.)).child(self.picker.clone()) + } +} + +impl FocusableView for ToolchainSelector { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.picker.focus_handle(cx) + } +} + +impl EventEmitter for ToolchainSelector {} +impl ModalView for ToolchainSelector {} + +pub struct ToolchainSelectorDelegate { + toolchain_selector: WeakView, + candidates: ToolchainList, + matches: Vec, + selected_index: usize, + workspace: WeakView, + worktree_id: WorktreeId, + worktree_abs_path_root: Arc, + _fetch_candidates_task: Task>, +} + +impl ToolchainSelectorDelegate { + #[allow(clippy::too_many_arguments)] + fn new( + active_toolchain: Option, + language_selector: WeakView, + workspace: WeakView, + worktree_id: WorktreeId, + worktree_abs_path_root: Arc, + project: Model, + language_name: LanguageName, + cx: &mut ViewContext>, + ) -> Self { + let _fetch_candidates_task = cx.spawn({ + let project = project.clone(); + move |this, mut cx| async move { + let available_toolchains = project + .update(&mut cx, |this, cx| { + this.available_toolchains(worktree_id, language_name, cx) + }) + .ok()? + .await?; + + let _ = this.update(&mut cx, move |this, cx| { + this.delegate.candidates = available_toolchains; + if let Some(active_toolchain) = active_toolchain { + if let Some(position) = this + .delegate + .candidates + .toolchains + .iter() + .position(|toolchain| *toolchain == active_toolchain) + { + this.delegate.set_selected_index(position, cx); + } + } + this.update_matches(this.query(cx), cx); + }); + + Some(()) + } + }); + + Self { + toolchain_selector: language_selector, + candidates: Default::default(), + matches: vec![], + selected_index: 0, + workspace, + worktree_id, + worktree_abs_path_root, + _fetch_candidates_task, + } + } + fn relativize_path(path: SharedString, worktree_root: &Path) -> SharedString { + Path::new(&path.as_ref()) + .strip_prefix(&worktree_root) + .ok() + .map(|suffix| Path::new(".").join(suffix)) + .and_then(|path| path.to_str().map(String::from).map(SharedString::from)) + .unwrap_or(path) + } +} + +impl PickerDelegate for ToolchainSelectorDelegate { + type ListItem = ListItem; + + fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc { + "Select a toolchain...".into() + } + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { + if let Some(string_match) = self.matches.get(self.selected_index) { + let toolchain = self.candidates.toolchains[string_match.candidate_id].clone(); + if let Some(workspace_id) = self + .workspace + .update(cx, |this, _| this.database_id()) + .ok() + .flatten() + { + let workspace = self.workspace.clone(); + let worktree_id = self.worktree_id; + cx.spawn(|_, mut cx| async move { + workspace::WORKSPACE_DB + .set_toolchain(workspace_id, worktree_id, toolchain.clone()) + .await + .log_err(); + workspace + .update(&mut cx, |this, cx| { + this.project().update(cx, |this, cx| { + this.activate_toolchain(worktree_id, toolchain, cx) + }) + }) + .ok()? + .await; + Some(()) + }) + .detach(); + } + } + self.dismissed(cx); + } + + fn dismissed(&mut self, cx: &mut ViewContext>) { + self.toolchain_selector + .update(cx, |_, cx| cx.emit(DismissEvent)) + .log_err(); + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext>) { + self.selected_index = ix; + } + + fn update_matches( + &mut self, + query: String, + cx: &mut ViewContext>, + ) -> gpui::Task<()> { + let background = cx.background_executor().clone(); + let candidates = self.candidates.clone(); + let worktree_root_path = self.worktree_abs_path_root.clone(); + cx.spawn(|this, mut cx| async move { + let matches = if query.is_empty() { + candidates + .toolchains + .into_iter() + .enumerate() + .map(|(index, candidate)| { + let path = Self::relativize_path(candidate.path, &worktree_root_path); + let string = format!("{}{}", candidate.name, path); + StringMatch { + candidate_id: index, + string, + positions: Vec::new(), + score: 0.0, + } + }) + .collect() + } else { + let candidates = candidates + .toolchains + .into_iter() + .enumerate() + .map(|(candidate_id, toolchain)| { + let path = Self::relativize_path(toolchain.path, &worktree_root_path); + let string = format!("{}{}", toolchain.name, path); + StringMatchCandidate::new(candidate_id, string) + }) + .collect::>(); + match_strings( + &candidates, + &query, + false, + 100, + &Default::default(), + background, + ) + .await + }; + + this.update(&mut cx, |this, cx| { + let delegate = &mut this.delegate; + delegate.matches = matches; + delegate.selected_index = delegate + .selected_index + .min(delegate.matches.len().saturating_sub(1)); + cx.notify(); + }) + .log_err(); + }) + } + + fn render_match( + &self, + ix: usize, + selected: bool, + _: &mut ViewContext>, + ) -> Option { + let mat = &self.matches[ix]; + let toolchain = &self.candidates.toolchains[mat.candidate_id]; + + let label = toolchain.name.clone(); + let path = Self::relativize_path(toolchain.path.clone(), &self.worktree_abs_path_root); + let (name_highlights, mut path_highlights) = mat + .positions + .iter() + .cloned() + .partition::, _>(|index| *index < label.len()); + path_highlights.iter_mut().for_each(|index| { + *index -= label.len(); + }); + Some( + ListItem::new(ix) + .inset(true) + .spacing(ListItemSpacing::Sparse) + .selected(selected) + .child(HighlightedLabel::new(label, name_highlights)) + .child( + HighlightedLabel::new(path, path_highlights) + .size(LabelSize::Small) + .color(Color::Muted), + ), + ) + } +} diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 7c4fb93ba1..925d56a921 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -7,6 +7,8 @@ use client::DevServerProjectId; use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql}; use gpui::{point, size, Axis, Bounds, WindowBounds, WindowId}; +use language::{LanguageName, Toolchain}; +use project::WorktreeId; use remote::ssh_session::SshProjectId; use sqlez::{ bindable::{Bind, Column, StaticColumnCount}, @@ -204,7 +206,8 @@ define_connection! { // preview: bool // Indicates if this item is a preview item // ) pub static ref DB: WorkspaceDb<()> = - &[sql!( + &[ + sql!( CREATE TABLE workspaces( workspace_id INTEGER PRIMARY KEY, workspace_location BLOB UNIQUE, @@ -367,6 +370,16 @@ define_connection! { sql!( ALTER TABLE ssh_projects RENAME COLUMN path TO paths; ), + sql!( + CREATE TABLE toolchains ( + workspace_id INTEGER, + worktree_id INTEGER, + language_name TEXT NOT NULL, + name TEXT NOT NULL, + path TEXT NOT NULL, + PRIMARY KEY (workspace_id, worktree_id, language_name) + ); + ), ]; } @@ -528,6 +541,7 @@ impl WorkspaceDb { match workspace.location { SerializedWorkspaceLocation::Local(local_paths, local_paths_order) => { conn.exec_bound(sql!( + DELETE FROM toolchains WHERE workspace_id = ?1; DELETE FROM workspaces WHERE local_paths = ? AND workspace_id != ? ))?((&local_paths, workspace.id)) .context("clearing out old locations")?; @@ -576,6 +590,7 @@ impl WorkspaceDb { } SerializedWorkspaceLocation::Ssh(ssh_project) => { conn.exec_bound(sql!( + DELETE FROM toolchains WHERE workspace_id = ?1; DELETE FROM workspaces WHERE ssh_project_id = ? AND workspace_id != ? ))?((ssh_project.id.0, workspace.id)) .context("clearing out old locations")?; @@ -737,6 +752,7 @@ impl WorkspaceDb { query! { pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> { + DELETE FROM toolchains WHERE workspace_id = ?1; DELETE FROM workspaces WHERE workspace_id IS ? } @@ -751,6 +767,7 @@ impl WorkspaceDb { DELETE FROM dev_server_projects WHERE id = ? ))?(id.0)?; conn.exec_bound(sql!( + DELETE FROM toolchains WHERE workspace_id = ?1; DELETE FROM workspaces WHERE dev_server_project_id IS ? ))?(id.0) @@ -1053,6 +1070,83 @@ impl WorkspaceDb { WHERE workspace_id = ?1 } } + + pub async fn toolchain( + &self, + workspace_id: WorkspaceId, + worktree_id: WorktreeId, + language_name: LanguageName, + ) -> Result> { + self.write(move |this| { + let mut select = this + .select_bound(sql!( + SELECT name, path FROM toolchains WHERE workspace_id = ? AND language_name = ? AND worktree_id = ? + )) + .context("Preparing insertion")?; + + let toolchain: Vec<(String, String)> = + select((workspace_id, language_name.0.to_owned(), worktree_id.to_usize()))?; + + Ok(toolchain.into_iter().next().map(|(name, path)| Toolchain { + name: name.into(), + path: path.into(), + language_name, + })) + }) + .await + } + + pub(crate) async fn toolchains( + &self, + workspace_id: WorkspaceId, + ) -> Result> { + self.write(move |this| { + let mut select = this + .select_bound(sql!( + SELECT name, path, worktree_id, language_name FROM toolchains WHERE workspace_id = ? + )) + .context("Preparing insertion")?; + + let toolchain: Vec<(String, String, u64, String)> = + select(workspace_id)?; + + Ok(toolchain.into_iter().map(|(name, path, worktree_id, language_name)| (Toolchain { + name: name.into(), + path: path.into(), + language_name: LanguageName::new(&language_name), + }, WorktreeId::from_proto(worktree_id))).collect()) + }) + .await + } + pub async fn set_toolchain( + &self, + workspace_id: WorkspaceId, + worktree_id: WorktreeId, + toolchain: Toolchain, + ) -> Result<()> { + self.write(move |conn| { + let mut insert = conn + .exec_bound(sql!( + INSERT INTO toolchains(workspace_id, worktree_id, language_name, name, path) VALUES (?, ?, ?, ?, ?) + ON CONFLICT DO + UPDATE SET + name = ?4, + path = ?5 + + )) + .context("Preparing insertion")?; + + insert(( + workspace_id, + worktree_id.to_usize(), + toolchain.language_name.0.as_ref(), + toolchain.name.as_ref(), + toolchain.path.as_ref(), + ))?; + + Ok(()) + }).await + } } #[cfg(test)] diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index b92417b293..de2c985f34 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1153,6 +1153,14 @@ impl Workspace { DB.next_id().await.unwrap_or_else(|_| Default::default()) }; + let toolchains = DB.toolchains(workspace_id).await?; + for (toolchain, worktree_id) in toolchains { + project_handle + .update(&mut cx, |this, cx| { + this.activate_toolchain(worktree_id, toolchain, cx) + })? + .await; + } let window = if let Some(window) = requesting_window { cx.update_window(window.into(), |_, cx| { cx.replace_root_view(|cx| { @@ -5522,6 +5530,14 @@ pub fn open_ssh_project( ) })?; + let toolchains = DB.toolchains(workspace_id).await?; + for (toolchain, worktree_id) in toolchains { + project + .update(&mut cx, |this, cx| { + this.activate_toolchain(worktree_id, toolchain, cx) + })? + .await; + } let mut project_paths_to_open = vec![]; let mut project_path_errors = vec![]; diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 58728d504b..e2a3f2be36 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -104,6 +104,7 @@ terminal_view.workspace = true theme.workspace = true theme_selector.workspace = true time.workspace = true +toolchain_selector.workspace = true ui.workspace = true reqwest_client.workspace = true url.workspace = true diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 3cb717d24f..89ff72b5a9 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -441,6 +441,7 @@ fn main() { terminal_view::init(cx); journal::init(app_state.clone(), cx); language_selector::init(cx); + toolchain_selector::init(cx); theme_selector::init(cx); language_tools::init(cx); call::init(app_state.client.clone(), app_state.user_store.clone(), cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 8965a1755a..7b630489cf 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -208,6 +208,8 @@ pub fn initialize_workspace( activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx); let active_buffer_language = cx.new_view(|_| language_selector::ActiveBufferLanguage::new(workspace)); + let active_toolchain_language = + cx.new_view(|cx| toolchain_selector::ActiveToolchain::new(workspace, cx)); let vim_mode_indicator = cx.new_view(vim::ModeIndicator::new); let cursor_position = cx.new_view(|_| go_to_line::cursor_position::CursorPosition::new(workspace)); @@ -216,6 +218,7 @@ pub fn initialize_workspace( status_bar.add_left_item(activity_indicator, cx); status_bar.add_right_item(inline_completion_button, cx); status_bar.add_right_item(active_buffer_language, cx); + status_bar.add_right_item(active_toolchain_language, cx); status_bar.add_right_item(vim_mode_indicator, cx); status_bar.add_right_item(cursor_position, cx); }); diff --git a/script/licenses/zed-licenses.toml b/script/licenses/zed-licenses.toml index 3459fee3e5..15c98c6702 100644 --- a/script/licenses/zed-licenses.toml +++ b/script/licenses/zed-licenses.toml @@ -36,3 +36,141 @@ license = "BSD-3-Clause" [[fuchsia-cprng.clarify.files]] path = 'LICENSE' checksum = '03b114f53e6587a398931762ee11e2395bfdba252a329940e2c8c9e81813845b' + +[pet.clarify] +license = "MIT" +[[pet.clarify.git]] +path = 'LICENSE' +checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' + +[pet-conda.clarify] +license = "MIT" +[[pet-conda.clarify.git]] +path = 'LICENSE' +checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' + +[pet-core.clarify] +license = "MIT" +[[pet-core.clarify.git]] +path = 'LICENSE' +checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' + +[pet-env-var-path.clarify] +license = "MIT" +[[pet-env-var-path.clarify.git]] +path = 'LICENSE' +checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' + +[pet-fs.clarify] +license = "MIT" +[[pet-fs.clarify.git]] +path = 'LICENSE' +checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' + +[pet-global-virtualenvs.clarify] +license = "MIT" +[[pet-global-virtualenvs.clarify.git]] +path = 'LICENSE' +checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' + +[pet-homebrew.clarify] +license = "MIT" +[[pet-homebrew.clarify.git]] +path = 'LICENSE' +checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' + +[pet-jsonrpc.clarify] +license = "MIT" +[[pet-jsonrpc.clarify.git]] +path = 'LICENSE' +checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' + +[pet-linux-global-python.clarify] +license = "MIT" +[[pet-linux-global-python.clarify.git]] +path = 'LICENSE' +checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' + +[pet-mac-commandlinetools.clarify] +license = "MIT" +[[pet-mac-commandlinetools.clarify.git]] +path = 'LICENSE' +checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' + +[pet-mac-python-org.clarify] +license = "MIT" +[[pet-mac-python-org.clarify.git]] +path = 'LICENSE' +checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' + +[pet-mac-xcode.clarify] +license = "MIT" +[[pet-mac-xcode.clarify.git]] +path = 'LICENSE' +checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' + +[pet-pipenv.clarify] +license = "MIT" +[[pet-pipenv.clarify.git]] +path = 'LICENSE' +checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' + +[pet-poetry.clarify] +license = "MIT" +[[pet-poetry.clarify.git]] +path = 'LICENSE' +checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' + +[pet-pyenv.clarify] +license = "MIT" +[[pet-pyenv.clarify.git]] +path = 'LICENSE' +checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' + +[pet-python-utils.clarify] +license = "MIT" +[[pet-python-utils.clarify.git]] +path = 'LICENSE' +checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' + +[pet-reporter.clarify] +license = "MIT" +[[pet-reporter.clarify.git]] +path = 'LICENSE' +checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' + +[pet-telemetry.clarify] +license = "MIT" +[[pet-telemetry.clarify.git]] +path = 'LICENSE' +checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' + +[pet-venv.clarify] +license = "MIT" +[[pet-venv.clarify.git]] +path = 'LICENSE' +checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' + +[pet-virtualenv.clarify] +license = "MIT" +[[pet-virtualenv.clarify.git]] +path = 'LICENSE' +checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' + +[pet-virtualenvwrapper.clarify] +license = "MIT" +[[pet-virtualenvwrapper.clarify.git]] +path = 'LICENSE' +checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' + +[pet-windows-registry.clarify] +license = "MIT" +[[pet-windows-registry.clarify.git]] +path = 'LICENSE' +checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383' + +[pet-windows-store.clarify] +license = "MIT" +[[pet-windows-store.clarify.git]] +path = 'LICENSE' +checksum = 'c2cfccb812fe482101a8f04597dfc5a9991a6b2748266c47ac91b6a5aae15383'