Add language toolchains (#19576)

Piotr Osiewicz created

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)

Change summary

.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             |  25 
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 
crates/toolchain_selector/src/active_toolchain.rs   | 173 +++++
crates/toolchain_selector/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, 2,222 insertions(+), 134 deletions(-)

Detailed changes

.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:

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",

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"

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.

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<Self>,
         delegate: &Arc<dyn LspAdapterDelegate>,
+        _: Arc<dyn LanguageToolchainStore>,
         _cx: &mut AsyncAppContext,
     ) -> Result<Value> {
         let delegate = delegate.clone();

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<_>)
-                        });
-
-                    Ok((config, queries, tasks))
+                    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(LoadedLanguage {
+                        config,
+                        queries,
+                        context_provider,
+                        toolchain_provider: None,
+                    })
                 },
             );
         }

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<Self>,
         _: &Arc<dyn LspAdapterDelegate>,
+        _: Arc<dyn LanguageToolchainStore>,
         _cx: &mut AsyncAppContext,
     ) -> Result<Value> {
         Ok(serde_json::json!({}))
@@ -855,6 +858,7 @@ pub struct Language {
     pub(crate) config: LanguageConfig,
     pub(crate) grammar: Option<Arc<Grammar>>,
     pub(crate) context_provider: Option<Arc<dyn ContextProvider>>,
+    pub(crate) toolchain: Option<Arc<dyn ToolchainLister>>,
 }
 
 #[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<Arc<dyn ToolchainLister>>) -> Self {
+        self.toolchain = provider;
+        self
+    }
+
     pub fn with_queries(mut self, queries: LanguageQueries) -> Result<Self> {
         if let Some(query) = queries.highlights {
             self = self
@@ -1361,6 +1371,10 @@ impl Language {
         self.context_provider.clone()
     }
 
+    pub fn toolchain_lister(&self) -> Option<Arc<dyn ToolchainLister>> {
+        self.toolchain.clone()
+    }
+
     pub fn highlight_text<'a>(
         self: &'a Arc<Self>,
         text: &'a Rope,

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<LanguageName> for String {
+    fn from(value: LanguageName) -> Self {
+        let value: &str = &value.0;
+        Self::from(value)
+    }
+}
+
 pub struct LanguageRegistry {
     state: RwLock<LanguageRegistryState>,
     language_server_download_dir: Option<Arc<Path>>,
@@ -123,16 +130,7 @@ pub struct AvailableLanguage {
     name: LanguageName,
     grammar: Option<Arc<str>>,
     matcher: LanguageMatcher,
-    load: Arc<
-        dyn Fn() -> Result<(
-                LanguageConfig,
-                LanguageQueries,
-                Option<Arc<dyn ContextProvider>>,
-            )>
-            + 'static
-            + Send
-            + Sync,
-    >,
+    load: Arc<dyn Fn() -> Result<LoadedLanguage> + 'static + Send + Sync>,
     loaded: bool,
 }
 
@@ -200,6 +198,13 @@ struct LspBinaryStatusSender {
     txs: Arc<Mutex<Vec<mpsc::UnboundedSender<(LanguageServerName, LanguageServerBinaryStatus)>>>>,
 }
 
+pub struct LoadedLanguage {
+    pub config: LanguageConfig,
+    pub queries: LanguageQueries,
+    pub context_provider: Option<Arc<dyn ContextProvider>>,
+    pub toolchain_provider: Option<Arc<dyn ToolchainLister>>,
+}
+
 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<Arc<str>>,
         matcher: LanguageMatcher,
-        load: impl Fn() -> Result<(
-                LanguageConfig,
-                LanguageQueries,
-                Option<Arc<dyn ContextProvider>>,
-            )>
-            + 'static
-            + Send
-            + Sync,
+        load: impl Fn() -> Result<LoadedLanguage> + '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;

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<Self>,
+        worktree_id: WorktreeId,
+        language_name: LanguageName,
+        cx: &mut AsyncAppContext,
+    ) -> Option<Toolchain>;
+}
+
+type DefaultIndex = usize;
+#[derive(Default, Clone)]
+pub struct ToolchainList {
+    pub toolchains: Vec<Toolchain>,
+    pub default: Option<DefaultIndex>,
+    pub groups: Box<[(usize, SharedString)]>,
+}
+
+impl ToolchainList {
+    pub fn toolchains(&self) -> &[Toolchain] {
+        &self.toolchains
+    }
+    pub fn default_toolchain(&self) -> Option<Toolchain> {
+        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()
+    }
+}

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

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<Self>,
         _: &Arc<dyn LspAdapterDelegate>,
+        _: Arc<dyn LanguageToolchainStore>,
         cx: &mut AsyncAppContext,
     ) -> Result<Value> {
         cx.update(|cx| {

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<LanguageRegistry>, 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<LanguageRegistry>, 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<LanguageRegistry>, 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<Arc<dyn LspAdapter>> = $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<LanguageRegistry>, node_runtime: NodeRuntime, cx: &mu
         vec![Arc::new(python::PythonLspAdapter::new(
             node_runtime.clone(),
         ))],
-        PythonContextProvider
+        PythonContextProvider,
+        Arc::new(PythonToolchainProvider::default()) as Arc<dyn ToolchainLister>
     );
     language!(
         "rust",

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<Self>,
         adapter: &Arc<dyn LspAdapterDelegate>,
+        toolchains: Arc<dyn LanguageToolchainStore>,
         cx: &mut AsyncAppContext,
     ) -> Result<Value> {
-        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<PythonEnvironmentKind>) -> 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};

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<Self>,
         delegate: &Arc<dyn LspAdapterDelegate>,
+        _: Arc<dyn LanguageToolchainStore>,
         cx: &mut AsyncAppContext,
     ) -> Result<Value> {
         let tailwind_user_settings = cx.update(|cx| {

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<Self>,
         delegate: &Arc<dyn LspAdapterDelegate>,
+        _: Arc<dyn LanguageToolchainStore>,
         cx: &mut AsyncAppContext,
     ) -> Result<Value> {
         let override_options = cx.update(|cx| {
@@ -325,6 +326,7 @@ impl LspAdapter for EsLintLspAdapter {
     async fn workspace_configuration(
         self: Arc<Self>,
         delegate: &Arc<dyn LspAdapterDelegate>,
+        _: Arc<dyn LanguageToolchainStore>,
         cx: &mut AsyncAppContext,
     ) -> Result<Value> {
         let workspace_root = delegate.worktree_root_path();

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<Self>,
         delegate: &Arc<dyn LspAdapterDelegate>,
+        _: Arc<dyn LanguageToolchainStore>,
         cx: &mut AsyncAppContext,
     ) -> Result<Value> {
         let tsdk_path = Self::tsdk_path(delegate).await;

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<Self>,
         delegate: &Arc<dyn LspAdapterDelegate>,
+        _: Arc<dyn LanguageToolchainStore>,
         cx: &mut AsyncAppContext,
     ) -> Result<Value> {
         let location = SettingsLocation {

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<BufferStore>,
     worktree_store: Model<WorktreeStore>,
+    toolchain_store: Option<Model<ToolchainStore>>,
     buffer_snapshots: HashMap<BufferId, HashMap<LanguageServerId, Vec<LspBufferSnapshot>>>, // buffer_id -> server_id -> vec of snapshots
     pub languages: Arc<LanguageRegistry>,
     language_server_ids: HashMap<(WorktreeId, LanguageServerName), LanguageServerId>,
     pub language_server_statuses: BTreeMap<LanguageServerId, LanguageServerStatus>,
     active_entry: Option<ProjectEntryId>,
-    _maintain_workspace_config: Task<Result<()>>,
+    _maintain_workspace_config: (Task<Result<()>>, watch::Sender<()>),
     _maintain_buffer_languages: Task<()>,
     next_diagnostic_group_id: usize,
     diagnostic_summaries:
@@ -871,6 +873,7 @@ impl LspStore {
         buffer_store: Model<BufferStore>,
         worktree_store: Model<WorktreeStore>,
         prettier_store: Model<PrettierStore>,
+        toolchain_store: Model<ToolchainStore>,
         environment: Model<ProjectEnvironment>,
         languages: Arc<LanguageRegistry>,
         http_client: Arc<dyn HttpClient>,
@@ -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::<SettingsStore>(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<BufferStore>,
         worktree_store: Model<WorktreeStore>,
+        toolchain_store: Option<Model<ToolchainStore>>,
         languages: Arc<LanguageRegistry>,
         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<ToolchainStore>,
+        event: &ToolchainStoreEvent,
+        _: &mut ModelContext<Self>,
+    ) {
+        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<Model<PrettierStore>> {
         self.as_local().map(|local| local.prettier_store.clone())
@@ -3029,17 +3060,13 @@ impl LspStore {
         None
     }
 
-    fn maintain_workspace_config(cx: &mut ModelContext<Self>) -> Task<Result<()>> {
-        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::<SettingsStore>(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<Self>,
+        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::<Vec<_>>()
-                })?;
+                })
+                .ok()?;
+
+            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()?;
 
-                for (adapter, server, delegate) in servers {
-                    let settings = adapter.workspace_configuration(&delegate, &mut cx).await?;
+                server
+                    .notify::<lsp::notification::DidChangeConfiguration>(
+                        lsp::DidChangeConfigurationParams { settings },
+                    )
+                    .ok();
+            }
+            Some(())
+        })
+        .await;
+    }
 
-                    server
-                        .notify::<lsp::notification::DidChangeConfiguration>(
-                            lsp::DidChangeConfigurationParams { settings },
-                        )
-                        .ok();
-                }
+    fn toolchain_store(&self, cx: &AppContext) -> Arc<dyn LanguageToolchainStore> {
+        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<Self>,
+    ) -> Task<Result<()>> {
+        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::<SettingsStore>(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::<lsp::request::WorkspaceConfiguration, _, _>({
                 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()

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<SnippetProvider>,
     environment: Model<ProjectEnvironment>,
     settings_observer: Model<SettingsObserver>,
+    toolchain_store: Option<Model<ToolchainStore>>,
 }
 
 #[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<Option<ToolchainList>> {
+        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<Option<()>> {
+        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<Option<Toolchain>> {
+        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,

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<LocalToolchainStore>, #[allow(dead_code)] Subscription),
+    Remote(Model<RemoteToolchainStore>),
+}
+
+impl EventEmitter<ToolchainStoreEvent> 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<LanguageRegistry>,
+        worktree_store: Model<WorktreeStore>,
+        cx: &mut ModelContext<Self>,
+    ) -> 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<Option<()>> {
+        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<Option<ToolchainList>> {
+        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<Option<Toolchain>> {
+        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<Self>,
+        envelope: TypedEnvelope<proto::ActivateToolchain>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::Ack> {
+        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<Self>,
+        envelope: TypedEnvelope<proto::ActiveToolchain>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::ActiveToolchainResponse> {
+        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<Self>,
+        envelope: TypedEnvelope<proto::ListToolchains>,
+        mut cx: AsyncAppContext,
+    ) -> Result<proto::ListToolchainsResponse> {
+        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::<Vec<_>>()
+        } else {
+            vec![]
+        };
+
+        Ok(proto::ListToolchainsResponse {
+            has_values,
+            toolchains,
+            groups,
+        })
+    }
+    pub(crate) fn as_language_toolchain_store(&self) -> Arc<dyn LanguageToolchainStore> {
+        match &self.0 {
+            ToolchainStoreInner::Local(local, _) => Arc::new(LocalStore(local.downgrade())),
+            ToolchainStoreInner::Remote(remote) => Arc::new(RemoteStore(remote.downgrade())),
+        }
+    }
+}
+
+struct LocalToolchainStore {
+    languages: Arc<LanguageRegistry>,
+    worktree_store: Model<WorktreeStore>,
+    active_toolchains: BTreeMap<(WorktreeId, LanguageName), Toolchain>,
+}
+
+#[async_trait(?Send)]
+impl language::LanguageToolchainStore for LocalStore {
+    async fn active_toolchain(
+        self: Arc<Self>,
+        worktree_id: WorktreeId,
+        language_name: LanguageName,
+        cx: &mut AsyncAppContext,
+    ) -> Option<Toolchain> {
+        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<Self>,
+        worktree_id: WorktreeId,
+        language_name: LanguageName,
+        cx: &mut AsyncAppContext,
+    ) -> Option<Toolchain> {
+        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<Self>,
+        _: WorktreeId,
+        _: LanguageName,
+        _: &mut AsyncAppContext,
+    ) -> Option<Toolchain> {
+        None
+    }
+}
+struct LocalStore(WeakModel<LocalToolchainStore>);
+struct RemoteStore(WeakModel<RemoteToolchainStore>);
+
+#[derive(Clone)]
+pub(crate) enum ToolchainStoreEvent {
+    ToolchainActivated,
+}
+
+impl EventEmitter<ToolchainStoreEvent> for LocalToolchainStore {}
+
+impl LocalToolchainStore {
+    pub(crate) fn activate_toolchain(
+        &self,
+        worktree_id: WorktreeId,
+        toolchain: Toolchain,
+        cx: &mut ModelContext<Self>,
+    ) -> Task<Option<()>> {
+        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<Option<ToolchainList>> {
+        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<Option<Toolchain>> {
+        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<Option<()>> {
+        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<Option<ToolchainList>> {
+        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<Option<Toolchain>> {
+        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(),
+            })
+        })
+    }
+}

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;
+
 }

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!(

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,

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

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<Toolchain>,
+    workspace: WeakView<Workspace>,
+    active_buffer: Option<(WorktreeId, WeakModel<Buffer>, Subscription)>,
+    _observe_language_changes: Subscription,
+    _update_toolchain_task: Task<Option<()>>,
+}
+
+struct LanguageChanged;
+
+impl EventEmitter<LanguageChanged> for ActiveToolchain {}
+
+impl ActiveToolchain {
+    pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> 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<Self>) -> Task<Option<()>> {
+        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<Editor>, cx: &mut ViewContext<Self>) {
+        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<Workspace>,
+        worktree_id: WorktreeId,
+        language_name: LanguageName,
+        cx: AsyncWindowContext,
+    ) -> Task<Option<Toolchain>> {
+        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<Self>) -> 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<Self>,
+    ) {
+        if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(cx)) {
+            self.active_toolchain.take();
+            self.update_lister(editor, cx);
+        }
+        cx.notify();
+    }
+}

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<Picker<ToolchainSelectorDelegate>>,
+}
+
+impl ToolchainSelector {
+    fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
+        workspace.register_action(move |workspace, _: &Select, cx| {
+            Self::toggle(workspace, cx);
+        });
+    }
+
+    fn toggle(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<()> {
+        let (_, buffer, _) = workspace
+            .active_item(cx)?
+            .act_as::<Editor>(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<Workspace>,
+        project: Model<Project>,
+        active_toolchain: Option<Toolchain>,
+        worktree_id: WorktreeId,
+        worktree_root: Arc<Path>,
+        language_name: LanguageName,
+        cx: &mut ViewContext<Self>,
+    ) -> 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<Self>) -> 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<DismissEvent> for ToolchainSelector {}
+impl ModalView for ToolchainSelector {}
+
+pub struct ToolchainSelectorDelegate {
+    toolchain_selector: WeakView<ToolchainSelector>,
+    candidates: ToolchainList,
+    matches: Vec<StringMatch>,
+    selected_index: usize,
+    workspace: WeakView<Workspace>,
+    worktree_id: WorktreeId,
+    worktree_abs_path_root: Arc<Path>,
+    _fetch_candidates_task: Task<Option<()>>,
+}
+
+impl ToolchainSelectorDelegate {
+    #[allow(clippy::too_many_arguments)]
+    fn new(
+        active_toolchain: Option<Toolchain>,
+        language_selector: WeakView<ToolchainSelector>,
+        workspace: WeakView<Workspace>,
+        worktree_id: WorktreeId,
+        worktree_abs_path_root: Arc<Path>,
+        project: Model<Project>,
+        language_name: LanguageName,
+        cx: &mut ViewContext<Picker<Self>>,
+    ) -> 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<str> {
+        "Select a toolchain...".into()
+    }
+
+    fn match_count(&self) -> usize {
+        self.matches.len()
+    }
+
+    fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
+        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<Picker<Self>>) {
+        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<Picker<Self>>) {
+        self.selected_index = ix;
+    }
+
+    fn update_matches(
+        &mut self,
+        query: String,
+        cx: &mut ViewContext<Picker<Self>>,
+    ) -> 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::<Vec<_>>();
+                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<Picker<Self>>,
+    ) -> Option<Self::ListItem> {
+        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::<Vec<_>, _>(|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),
+                ),
+        )
+    }
+}

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<Option<Toolchain>> {
+        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<Vec<(Toolchain, WorktreeId)>> {
+        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)]

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![];
 

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

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);

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);
         });

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'