python.rs

   1use anyhow::{Context as _, ensure};
   2use anyhow::{Result, anyhow};
   3use async_trait::async_trait;
   4use collections::HashMap;
   5use futures::{AsyncBufReadExt, StreamExt as _};
   6use gpui::{App, AsyncApp, SharedString, Task};
   7use http_client::github::{AssetKind, GitHubLspBinaryVersion, latest_github_release};
   8use language::language_settings::language_settings;
   9use language::{ContextLocation, LanguageToolchainStore, LspInstaller};
  10use language::{ContextProvider, LspAdapter, LspAdapterDelegate};
  11use language::{LanguageName, ManifestName, ManifestProvider, ManifestQuery};
  12use language::{Toolchain, ToolchainList, ToolchainLister, ToolchainMetadata};
  13use lsp::LanguageServerBinary;
  14use lsp::LanguageServerName;
  15use node_runtime::{NodeRuntime, VersionStrategy};
  16use pet_core::Configuration;
  17use pet_core::os_environment::Environment;
  18use pet_core::python_environment::{PythonEnvironment, PythonEnvironmentKind};
  19use project::Fs;
  20use project::lsp_store::language_server_settings;
  21use serde_json::{Value, json};
  22use smol::lock::OnceCell;
  23use std::cmp::Ordering;
  24use std::env::consts;
  25use util::fs::{make_file_executable, remove_matching};
  26
  27use parking_lot::Mutex;
  28use std::str::FromStr;
  29use std::{
  30    borrow::Cow,
  31    ffi::OsString,
  32    fmt::Write,
  33    path::{Path, PathBuf},
  34    sync::Arc,
  35};
  36use task::{ShellKind, TaskTemplate, TaskTemplates, VariableName};
  37use util::{ResultExt, maybe};
  38
  39use crate::github_download::{GithubBinaryMetadata, download_server_binary};
  40
  41pub(crate) struct PyprojectTomlManifestProvider;
  42
  43impl ManifestProvider for PyprojectTomlManifestProvider {
  44    fn name(&self) -> ManifestName {
  45        SharedString::new_static("pyproject.toml").into()
  46    }
  47
  48    fn search(
  49        &self,
  50        ManifestQuery {
  51            path,
  52            depth,
  53            delegate,
  54        }: ManifestQuery,
  55    ) -> Option<Arc<Path>> {
  56        for path in path.ancestors().take(depth) {
  57            let p = path.join("pyproject.toml");
  58            if delegate.exists(&p, Some(false)) {
  59                return Some(path.into());
  60            }
  61        }
  62
  63        None
  64    }
  65}
  66
  67const SERVER_PATH: &str = "node_modules/pyright/langserver.index.js";
  68const NODE_MODULE_RELATIVE_SERVER_PATH: &str = "pyright/langserver.index.js";
  69
  70enum TestRunner {
  71    UNITTEST,
  72    PYTEST,
  73}
  74
  75impl FromStr for TestRunner {
  76    type Err = ();
  77
  78    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
  79        match s {
  80            "unittest" => Ok(Self::UNITTEST),
  81            "pytest" => Ok(Self::PYTEST),
  82            _ => Err(()),
  83        }
  84    }
  85}
  86
  87fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
  88    vec![server_path.into(), "--stdio".into()]
  89}
  90
  91/// Pyright assigns each completion item a `sortText` of the form `XX.YYYY.name`.
  92/// Where `XX` is the sorting category, `YYYY` is based on most recent usage,
  93/// and `name` is the symbol name itself.
  94///
  95/// The problem with it is that Pyright adjusts the sort text based on previous resolutions (items for which we've issued `completion/resolve` call have their sortText adjusted),
  96/// which - long story short - makes completion items list non-stable. Pyright probably relies on VSCode's implementation detail.
  97/// see https://github.com/microsoft/pyright/blob/95ef4e103b9b2f129c9320427e51b73ea7cf78bd/packages/pyright-internal/src/languageService/completionProvider.ts#LL2873
  98fn process_pyright_completions(items: &mut [lsp::CompletionItem]) {
  99    for item in items {
 100        item.sort_text.take();
 101    }
 102}
 103
 104pub struct TyLspAdapter {
 105    fs: Arc<dyn Fs>,
 106}
 107
 108#[cfg(target_os = "macos")]
 109impl TyLspAdapter {
 110    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Gz;
 111    const ARCH_SERVER_NAME: &str = "apple-darwin";
 112}
 113
 114#[cfg(target_os = "linux")]
 115impl TyLspAdapter {
 116    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Gz;
 117    const ARCH_SERVER_NAME: &str = "unknown-linux-gnu";
 118}
 119
 120#[cfg(target_os = "freebsd")]
 121impl TyLspAdapter {
 122    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Gz;
 123    const ARCH_SERVER_NAME: &str = "unknown-freebsd";
 124}
 125
 126#[cfg(target_os = "windows")]
 127impl TyLspAdapter {
 128    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;
 129    const ARCH_SERVER_NAME: &str = "pc-windows-msvc";
 130}
 131
 132impl TyLspAdapter {
 133    const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("ty");
 134
 135    pub fn new(fs: Arc<dyn Fs>) -> TyLspAdapter {
 136        TyLspAdapter { fs }
 137    }
 138
 139    fn build_asset_name() -> Result<(String, String)> {
 140        let arch = match consts::ARCH {
 141            "x86" => "i686",
 142            _ => consts::ARCH,
 143        };
 144        let os = Self::ARCH_SERVER_NAME;
 145        let suffix = match consts::OS {
 146            "windows" => "zip",
 147            _ => "tar.gz",
 148        };
 149        let asset_name = format!("ty-{arch}-{os}.{suffix}");
 150        let asset_stem = format!("ty-{arch}-{os}");
 151        Ok((asset_stem, asset_name))
 152    }
 153}
 154
 155#[async_trait(?Send)]
 156impl LspAdapter for TyLspAdapter {
 157    fn name(&self) -> LanguageServerName {
 158        Self::SERVER_NAME
 159    }
 160
 161    async fn workspace_configuration(
 162        self: Arc<Self>,
 163        _: &Arc<dyn LspAdapterDelegate>,
 164        toolchain: Option<Toolchain>,
 165        _cx: &mut AsyncApp,
 166    ) -> Result<Value> {
 167        let mut ret = json!({});
 168        if let Some(toolchain) = toolchain.and_then(|toolchain| {
 169            serde_json::from_value::<PythonEnvironment>(toolchain.as_json).ok()
 170        }) {
 171            _ = maybe!({
 172                let uri = url::Url::from_file_path(toolchain.executable?).ok()?;
 173                let sys_prefix = toolchain.prefix.clone()?;
 174                let environment = json!({
 175                    "executable": {
 176                        "uri": uri,
 177                        "sysPrefix": sys_prefix
 178                    }
 179                });
 180                ret.as_object_mut()?.insert(
 181                    "pythonExtension".into(),
 182                    json!({ "activeEnvironment": environment }),
 183                );
 184                Some(())
 185            });
 186        }
 187        Ok(json!({"ty": ret}))
 188    }
 189}
 190
 191impl LspInstaller for TyLspAdapter {
 192    type BinaryVersion = GitHubLspBinaryVersion;
 193    async fn fetch_latest_server_version(
 194        &self,
 195        delegate: &dyn LspAdapterDelegate,
 196        _: bool,
 197        _: &mut AsyncApp,
 198    ) -> Result<Self::BinaryVersion> {
 199        let release =
 200            latest_github_release("astral-sh/ty", true, true, delegate.http_client()).await?;
 201        let (_, asset_name) = Self::build_asset_name()?;
 202        let asset = release
 203            .assets
 204            .into_iter()
 205            .find(|asset| asset.name == asset_name)
 206            .with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
 207        Ok(GitHubLspBinaryVersion {
 208            name: release.tag_name,
 209            url: asset.browser_download_url,
 210            digest: asset.digest,
 211        })
 212    }
 213
 214    async fn fetch_server_binary(
 215        &self,
 216        latest_version: Self::BinaryVersion,
 217        container_dir: PathBuf,
 218        delegate: &dyn LspAdapterDelegate,
 219    ) -> Result<LanguageServerBinary> {
 220        let GitHubLspBinaryVersion {
 221            name,
 222            url,
 223            digest: expected_digest,
 224        } = latest_version;
 225        let destination_path = container_dir.join(format!("ty-{name}"));
 226        let server_path = match Self::GITHUB_ASSET_KIND {
 227            AssetKind::TarGz | AssetKind::Gz => destination_path.clone(), // Tar and gzip extract in place.
 228            AssetKind::Zip => destination_path.clone().join("ty.exe"),    // zip contains a .exe
 229        };
 230
 231        let binary = LanguageServerBinary {
 232            path: server_path.clone(),
 233            env: None,
 234            arguments: Default::default(),
 235        };
 236
 237        let metadata_path = destination_path.with_extension("metadata");
 238        let metadata = GithubBinaryMetadata::read_from_file(&metadata_path)
 239            .await
 240            .ok();
 241        if let Some(metadata) = metadata {
 242            let validity_check = async || {
 243                delegate
 244                    .try_exec(LanguageServerBinary {
 245                        path: server_path.clone(),
 246                        arguments: vec!["--version".into()],
 247                        env: None,
 248                    })
 249                    .await
 250                    .inspect_err(|err| {
 251                        log::warn!("Unable to run {server_path:?} asset, redownloading: {err}",)
 252                    })
 253            };
 254            if let (Some(actual_digest), Some(expected_digest)) =
 255                (&metadata.digest, &expected_digest)
 256            {
 257                if actual_digest == expected_digest {
 258                    if validity_check().await.is_ok() {
 259                        return Ok(binary);
 260                    }
 261                } else {
 262                    log::info!(
 263                        "SHA-256 mismatch for {destination_path:?} asset, downloading new asset. Expected: {expected_digest}, Got: {actual_digest}"
 264                    );
 265                }
 266            } else if validity_check().await.is_ok() {
 267                return Ok(binary);
 268            }
 269        }
 270
 271        download_server_binary(
 272            delegate,
 273            &url,
 274            expected_digest.as_deref(),
 275            &destination_path,
 276            Self::GITHUB_ASSET_KIND,
 277        )
 278        .await?;
 279        make_file_executable(&server_path).await?;
 280        remove_matching(&container_dir, |path| path != destination_path).await;
 281        GithubBinaryMetadata::write_to_file(
 282            &GithubBinaryMetadata {
 283                metadata_version: 1,
 284                digest: expected_digest,
 285            },
 286            &metadata_path,
 287        )
 288        .await?;
 289
 290        Ok(LanguageServerBinary {
 291            path: server_path,
 292            env: None,
 293            arguments: Default::default(),
 294        })
 295    }
 296
 297    async fn cached_server_binary(
 298        &self,
 299        container_dir: PathBuf,
 300        _: &dyn LspAdapterDelegate,
 301    ) -> Option<LanguageServerBinary> {
 302        maybe!(async {
 303            let mut last = None;
 304            let mut entries = self.fs.read_dir(&container_dir).await?;
 305            while let Some(entry) = entries.next().await {
 306                let path = entry?;
 307                if path.extension().is_some_and(|ext| ext == "metadata") {
 308                    continue;
 309                }
 310                last = Some(path);
 311            }
 312
 313            let path = last.context("no cached binary")?;
 314            let path = match TyLspAdapter::GITHUB_ASSET_KIND {
 315                AssetKind::TarGz | AssetKind::Gz => path, // Tar and gzip extract in place.
 316                AssetKind::Zip => path.join("ty.exe"),    // zip contains a .exe
 317            };
 318
 319            anyhow::Ok(LanguageServerBinary {
 320                path,
 321                env: None,
 322                arguments: Default::default(),
 323            })
 324        })
 325        .await
 326        .log_err()
 327    }
 328}
 329
 330pub struct PyrightLspAdapter {
 331    node: NodeRuntime,
 332}
 333
 334impl PyrightLspAdapter {
 335    const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("pyright");
 336
 337    pub fn new(node: NodeRuntime) -> Self {
 338        PyrightLspAdapter { node }
 339    }
 340}
 341
 342#[async_trait(?Send)]
 343impl LspAdapter for PyrightLspAdapter {
 344    fn name(&self) -> LanguageServerName {
 345        Self::SERVER_NAME
 346    }
 347
 348    async fn initialization_options(
 349        self: Arc<Self>,
 350        _: &Arc<dyn LspAdapterDelegate>,
 351    ) -> Result<Option<Value>> {
 352        // Provide minimal initialization options
 353        // Virtual environment configuration will be handled through workspace configuration
 354        Ok(Some(json!({
 355            "python": {
 356                "analysis": {
 357                    "autoSearchPaths": true,
 358                    "useLibraryCodeForTypes": true,
 359                    "autoImportCompletions": true
 360                }
 361            }
 362        })))
 363    }
 364
 365    async fn process_completions(&self, items: &mut [lsp::CompletionItem]) {
 366        process_pyright_completions(items);
 367    }
 368
 369    async fn label_for_completion(
 370        &self,
 371        item: &lsp::CompletionItem,
 372        language: &Arc<language::Language>,
 373    ) -> Option<language::CodeLabel> {
 374        let label = &item.label;
 375        let grammar = language.grammar()?;
 376        let highlight_id = match item.kind? {
 377            lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method"),
 378            lsp::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function"),
 379            lsp::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type"),
 380            lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant"),
 381            lsp::CompletionItemKind::VARIABLE => grammar.highlight_id_for_name("variable"),
 382            _ => {
 383                return None;
 384            }
 385        };
 386        let filter_range = item
 387            .filter_text
 388            .as_deref()
 389            .and_then(|filter| label.find(filter).map(|ix| ix..ix + filter.len()))
 390            .unwrap_or(0..label.len());
 391        let mut text = label.clone();
 392        if let Some(completion_details) = item
 393            .label_details
 394            .as_ref()
 395            .and_then(|details| details.description.as_ref())
 396        {
 397            write!(&mut text, " {}", completion_details).ok();
 398        }
 399        Some(language::CodeLabel {
 400            runs: highlight_id
 401                .map(|id| (0..label.len(), id))
 402                .into_iter()
 403                .collect(),
 404            text,
 405            filter_range,
 406        })
 407    }
 408
 409    async fn label_for_symbol(
 410        &self,
 411        name: &str,
 412        kind: lsp::SymbolKind,
 413        language: &Arc<language::Language>,
 414    ) -> Option<language::CodeLabel> {
 415        let (text, filter_range, display_range) = match kind {
 416            lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
 417                let text = format!("def {}():\n", name);
 418                let filter_range = 4..4 + name.len();
 419                let display_range = 0..filter_range.end;
 420                (text, filter_range, display_range)
 421            }
 422            lsp::SymbolKind::CLASS => {
 423                let text = format!("class {}:", name);
 424                let filter_range = 6..6 + name.len();
 425                let display_range = 0..filter_range.end;
 426                (text, filter_range, display_range)
 427            }
 428            lsp::SymbolKind::CONSTANT => {
 429                let text = format!("{} = 0", name);
 430                let filter_range = 0..name.len();
 431                let display_range = 0..filter_range.end;
 432                (text, filter_range, display_range)
 433            }
 434            _ => return None,
 435        };
 436
 437        Some(language::CodeLabel {
 438            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
 439            text: text[display_range].to_string(),
 440            filter_range,
 441        })
 442    }
 443
 444    async fn workspace_configuration(
 445        self: Arc<Self>,
 446
 447        adapter: &Arc<dyn LspAdapterDelegate>,
 448        toolchain: Option<Toolchain>,
 449        cx: &mut AsyncApp,
 450    ) -> Result<Value> {
 451        cx.update(move |cx| {
 452            let mut user_settings =
 453                language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
 454                    .and_then(|s| s.settings.clone())
 455                    .unwrap_or_default();
 456
 457            // If we have a detected toolchain, configure Pyright to use it
 458            if let Some(toolchain) = toolchain
 459                && let Ok(env) = serde_json::from_value::<
 460                    pet_core::python_environment::PythonEnvironment,
 461                >(toolchain.as_json.clone())
 462            {
 463                if user_settings.is_null() {
 464                    user_settings = Value::Object(serde_json::Map::default());
 465                }
 466                let object = user_settings.as_object_mut().unwrap();
 467
 468                let interpreter_path = toolchain.path.to_string();
 469                if let Some(venv_dir) = env.prefix {
 470                    // Set venvPath and venv at the root level
 471                    // This matches the format of a pyrightconfig.json file
 472                    if let Some(parent) = venv_dir.parent() {
 473                        // Use relative path if the venv is inside the workspace
 474                        let venv_path = if parent == adapter.worktree_root_path() {
 475                            ".".to_string()
 476                        } else {
 477                            parent.to_string_lossy().into_owned()
 478                        };
 479                        object.insert("venvPath".to_string(), Value::String(venv_path));
 480                    }
 481
 482                    if let Some(venv_name) = venv_dir.file_name() {
 483                        object.insert(
 484                            "venv".to_owned(),
 485                            Value::String(venv_name.to_string_lossy().into_owned()),
 486                        );
 487                    }
 488                }
 489
 490                // Always set the python interpreter path
 491                // Get or create the python section
 492                let python = object
 493                    .entry("python")
 494                    .or_insert(Value::Object(serde_json::Map::default()))
 495                    .as_object_mut()
 496                    .unwrap();
 497
 498                // Set both pythonPath and defaultInterpreterPath for compatibility
 499                python.insert(
 500                    "pythonPath".to_owned(),
 501                    Value::String(interpreter_path.clone()),
 502                );
 503                python.insert(
 504                    "defaultInterpreterPath".to_owned(),
 505                    Value::String(interpreter_path),
 506                );
 507            }
 508
 509            user_settings
 510        })
 511    }
 512}
 513
 514impl LspInstaller for PyrightLspAdapter {
 515    type BinaryVersion = String;
 516
 517    async fn fetch_latest_server_version(
 518        &self,
 519        _: &dyn LspAdapterDelegate,
 520        _: bool,
 521        _: &mut AsyncApp,
 522    ) -> Result<String> {
 523        self.node
 524            .npm_package_latest_version(Self::SERVER_NAME.as_ref())
 525            .await
 526    }
 527
 528    async fn check_if_user_installed(
 529        &self,
 530        delegate: &dyn LspAdapterDelegate,
 531        _: Option<Toolchain>,
 532        _: &AsyncApp,
 533    ) -> Option<LanguageServerBinary> {
 534        if let Some(pyright_bin) = delegate.which("pyright-langserver".as_ref()).await {
 535            let env = delegate.shell_env().await;
 536            Some(LanguageServerBinary {
 537                path: pyright_bin,
 538                env: Some(env),
 539                arguments: vec!["--stdio".into()],
 540            })
 541        } else {
 542            let node = delegate.which("node".as_ref()).await?;
 543            let (node_modules_path, _) = delegate
 544                .npm_package_installed_version(Self::SERVER_NAME.as_ref())
 545                .await
 546                .log_err()??;
 547
 548            let path = node_modules_path.join(NODE_MODULE_RELATIVE_SERVER_PATH);
 549
 550            let env = delegate.shell_env().await;
 551            Some(LanguageServerBinary {
 552                path: node,
 553                env: Some(env),
 554                arguments: server_binary_arguments(&path),
 555            })
 556        }
 557    }
 558
 559    async fn fetch_server_binary(
 560        &self,
 561        latest_version: Self::BinaryVersion,
 562        container_dir: PathBuf,
 563        delegate: &dyn LspAdapterDelegate,
 564    ) -> Result<LanguageServerBinary> {
 565        let server_path = container_dir.join(SERVER_PATH);
 566
 567        self.node
 568            .npm_install_packages(
 569                &container_dir,
 570                &[(Self::SERVER_NAME.as_ref(), latest_version.as_str())],
 571            )
 572            .await?;
 573
 574        let env = delegate.shell_env().await;
 575        Ok(LanguageServerBinary {
 576            path: self.node.binary_path().await?,
 577            env: Some(env),
 578            arguments: server_binary_arguments(&server_path),
 579        })
 580    }
 581
 582    async fn check_if_version_installed(
 583        &self,
 584        version: &Self::BinaryVersion,
 585        container_dir: &PathBuf,
 586        delegate: &dyn LspAdapterDelegate,
 587    ) -> Option<LanguageServerBinary> {
 588        let server_path = container_dir.join(SERVER_PATH);
 589
 590        let should_install_language_server = self
 591            .node
 592            .should_install_npm_package(
 593                Self::SERVER_NAME.as_ref(),
 594                &server_path,
 595                container_dir,
 596                VersionStrategy::Latest(version),
 597            )
 598            .await;
 599
 600        if should_install_language_server {
 601            None
 602        } else {
 603            let env = delegate.shell_env().await;
 604            Some(LanguageServerBinary {
 605                path: self.node.binary_path().await.ok()?,
 606                env: Some(env),
 607                arguments: server_binary_arguments(&server_path),
 608            })
 609        }
 610    }
 611
 612    async fn cached_server_binary(
 613        &self,
 614        container_dir: PathBuf,
 615        delegate: &dyn LspAdapterDelegate,
 616    ) -> Option<LanguageServerBinary> {
 617        let mut binary = get_cached_server_binary(container_dir, &self.node).await?;
 618        binary.env = Some(delegate.shell_env().await);
 619        Some(binary)
 620    }
 621}
 622
 623async fn get_cached_server_binary(
 624    container_dir: PathBuf,
 625    node: &NodeRuntime,
 626) -> Option<LanguageServerBinary> {
 627    let server_path = container_dir.join(SERVER_PATH);
 628    if server_path.exists() {
 629        Some(LanguageServerBinary {
 630            path: node.binary_path().await.log_err()?,
 631            env: None,
 632            arguments: server_binary_arguments(&server_path),
 633        })
 634    } else {
 635        log::error!("missing executable in directory {:?}", server_path);
 636        None
 637    }
 638}
 639
 640pub(crate) struct PythonContextProvider;
 641
 642const PYTHON_TEST_TARGET_TASK_VARIABLE: VariableName =
 643    VariableName::Custom(Cow::Borrowed("PYTHON_TEST_TARGET"));
 644
 645const PYTHON_ACTIVE_TOOLCHAIN_PATH: VariableName =
 646    VariableName::Custom(Cow::Borrowed("PYTHON_ACTIVE_ZED_TOOLCHAIN"));
 647
 648const PYTHON_MODULE_NAME_TASK_VARIABLE: VariableName =
 649    VariableName::Custom(Cow::Borrowed("PYTHON_MODULE_NAME"));
 650
 651impl ContextProvider for PythonContextProvider {
 652    fn build_context(
 653        &self,
 654        variables: &task::TaskVariables,
 655        location: ContextLocation<'_>,
 656        _: Option<HashMap<String, String>>,
 657        toolchains: Arc<dyn LanguageToolchainStore>,
 658        cx: &mut gpui::App,
 659    ) -> Task<Result<task::TaskVariables>> {
 660        let test_target =
 661            match selected_test_runner(location.file_location.buffer.read(cx).file(), cx) {
 662                TestRunner::UNITTEST => self.build_unittest_target(variables),
 663                TestRunner::PYTEST => self.build_pytest_target(variables),
 664            };
 665
 666        let module_target = self.build_module_target(variables);
 667        let location_file = location.file_location.buffer.read(cx).file().cloned();
 668        let worktree_id = location_file.as_ref().map(|f| f.worktree_id(cx));
 669
 670        cx.spawn(async move |cx| {
 671            let active_toolchain = if let Some(worktree_id) = worktree_id {
 672                let file_path = location_file
 673                    .as_ref()
 674                    .and_then(|f| f.path().parent())
 675                    .map(Arc::from)
 676                    .unwrap_or_else(|| Arc::from("".as_ref()));
 677
 678                toolchains
 679                    .active_toolchain(worktree_id, file_path, "Python".into(), cx)
 680                    .await
 681                    .map_or_else(
 682                        || String::from("python3"),
 683                        |toolchain| toolchain.path.to_string(),
 684                    )
 685            } else {
 686                String::from("python3")
 687            };
 688
 689            let toolchain = (PYTHON_ACTIVE_TOOLCHAIN_PATH, active_toolchain);
 690
 691            Ok(task::TaskVariables::from_iter(
 692                test_target
 693                    .into_iter()
 694                    .chain(module_target.into_iter())
 695                    .chain([toolchain]),
 696            ))
 697        })
 698    }
 699
 700    fn associated_tasks(
 701        &self,
 702        file: Option<Arc<dyn language::File>>,
 703        cx: &App,
 704    ) -> Task<Option<TaskTemplates>> {
 705        let test_runner = selected_test_runner(file.as_ref(), cx);
 706
 707        let mut tasks = vec![
 708            // Execute a selection
 709            TaskTemplate {
 710                label: "execute selection".to_owned(),
 711                command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
 712                args: vec![
 713                    "-c".to_owned(),
 714                    VariableName::SelectedText.template_value_with_whitespace(),
 715                ],
 716                cwd: Some(VariableName::WorktreeRoot.template_value()),
 717                ..TaskTemplate::default()
 718            },
 719            // Execute an entire file
 720            TaskTemplate {
 721                label: format!("run '{}'", VariableName::File.template_value()),
 722                command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
 723                args: vec![VariableName::File.template_value_with_whitespace()],
 724                cwd: Some(VariableName::WorktreeRoot.template_value()),
 725                ..TaskTemplate::default()
 726            },
 727            // Execute a file as module
 728            TaskTemplate {
 729                label: format!("run module '{}'", VariableName::File.template_value()),
 730                command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
 731                args: vec![
 732                    "-m".to_owned(),
 733                    PYTHON_MODULE_NAME_TASK_VARIABLE.template_value(),
 734                ],
 735                cwd: Some(VariableName::WorktreeRoot.template_value()),
 736                tags: vec!["python-module-main-method".to_owned()],
 737                ..TaskTemplate::default()
 738            },
 739        ];
 740
 741        tasks.extend(match test_runner {
 742            TestRunner::UNITTEST => {
 743                [
 744                    // Run tests for an entire file
 745                    TaskTemplate {
 746                        label: format!("unittest '{}'", VariableName::File.template_value()),
 747                        command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
 748                        args: vec![
 749                            "-m".to_owned(),
 750                            "unittest".to_owned(),
 751                            VariableName::File.template_value_with_whitespace(),
 752                        ],
 753                        cwd: Some(VariableName::WorktreeRoot.template_value()),
 754                        ..TaskTemplate::default()
 755                    },
 756                    // Run test(s) for a specific target within a file
 757                    TaskTemplate {
 758                        label: "unittest $ZED_CUSTOM_PYTHON_TEST_TARGET".to_owned(),
 759                        command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
 760                        args: vec![
 761                            "-m".to_owned(),
 762                            "unittest".to_owned(),
 763                            PYTHON_TEST_TARGET_TASK_VARIABLE.template_value_with_whitespace(),
 764                        ],
 765                        tags: vec![
 766                            "python-unittest-class".to_owned(),
 767                            "python-unittest-method".to_owned(),
 768                        ],
 769                        cwd: Some(VariableName::WorktreeRoot.template_value()),
 770                        ..TaskTemplate::default()
 771                    },
 772                ]
 773            }
 774            TestRunner::PYTEST => {
 775                [
 776                    // Run tests for an entire file
 777                    TaskTemplate {
 778                        label: format!("pytest '{}'", VariableName::File.template_value()),
 779                        command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
 780                        args: vec![
 781                            "-m".to_owned(),
 782                            "pytest".to_owned(),
 783                            VariableName::File.template_value_with_whitespace(),
 784                        ],
 785                        cwd: Some(VariableName::WorktreeRoot.template_value()),
 786                        ..TaskTemplate::default()
 787                    },
 788                    // Run test(s) for a specific target within a file
 789                    TaskTemplate {
 790                        label: "pytest $ZED_CUSTOM_PYTHON_TEST_TARGET".to_owned(),
 791                        command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),
 792                        args: vec![
 793                            "-m".to_owned(),
 794                            "pytest".to_owned(),
 795                            PYTHON_TEST_TARGET_TASK_VARIABLE.template_value_with_whitespace(),
 796                        ],
 797                        cwd: Some(VariableName::WorktreeRoot.template_value()),
 798                        tags: vec![
 799                            "python-pytest-class".to_owned(),
 800                            "python-pytest-method".to_owned(),
 801                        ],
 802                        ..TaskTemplate::default()
 803                    },
 804                ]
 805            }
 806        });
 807
 808        Task::ready(Some(TaskTemplates(tasks)))
 809    }
 810}
 811
 812fn selected_test_runner(location: Option<&Arc<dyn language::File>>, cx: &App) -> TestRunner {
 813    const TEST_RUNNER_VARIABLE: &str = "TEST_RUNNER";
 814    language_settings(Some(LanguageName::new("Python")), location, cx)
 815        .tasks
 816        .variables
 817        .get(TEST_RUNNER_VARIABLE)
 818        .and_then(|val| TestRunner::from_str(val).ok())
 819        .unwrap_or(TestRunner::PYTEST)
 820}
 821
 822impl PythonContextProvider {
 823    fn build_unittest_target(
 824        &self,
 825        variables: &task::TaskVariables,
 826    ) -> Option<(VariableName, String)> {
 827        let python_module_name =
 828            python_module_name_from_relative_path(variables.get(&VariableName::RelativeFile)?);
 829
 830        let unittest_class_name =
 831            variables.get(&VariableName::Custom(Cow::Borrowed("_unittest_class_name")));
 832
 833        let unittest_method_name = variables.get(&VariableName::Custom(Cow::Borrowed(
 834            "_unittest_method_name",
 835        )));
 836
 837        let unittest_target_str = match (unittest_class_name, unittest_method_name) {
 838            (Some(class_name), Some(method_name)) => {
 839                format!("{python_module_name}.{class_name}.{method_name}")
 840            }
 841            (Some(class_name), None) => format!("{python_module_name}.{class_name}"),
 842            (None, None) => python_module_name,
 843            // should never happen, a TestCase class is the unit of testing
 844            (None, Some(_)) => return None,
 845        };
 846
 847        Some((
 848            PYTHON_TEST_TARGET_TASK_VARIABLE.clone(),
 849            unittest_target_str,
 850        ))
 851    }
 852
 853    fn build_pytest_target(
 854        &self,
 855        variables: &task::TaskVariables,
 856    ) -> Option<(VariableName, String)> {
 857        let file_path = variables.get(&VariableName::RelativeFile)?;
 858
 859        let pytest_class_name =
 860            variables.get(&VariableName::Custom(Cow::Borrowed("_pytest_class_name")));
 861
 862        let pytest_method_name =
 863            variables.get(&VariableName::Custom(Cow::Borrowed("_pytest_method_name")));
 864
 865        let pytest_target_str = match (pytest_class_name, pytest_method_name) {
 866            (Some(class_name), Some(method_name)) => {
 867                format!("{file_path}::{class_name}::{method_name}")
 868            }
 869            (Some(class_name), None) => {
 870                format!("{file_path}::{class_name}")
 871            }
 872            (None, Some(method_name)) => {
 873                format!("{file_path}::{method_name}")
 874            }
 875            (None, None) => file_path.to_string(),
 876        };
 877
 878        Some((PYTHON_TEST_TARGET_TASK_VARIABLE.clone(), pytest_target_str))
 879    }
 880
 881    fn build_module_target(
 882        &self,
 883        variables: &task::TaskVariables,
 884    ) -> Result<(VariableName, String)> {
 885        let python_module_name = python_module_name_from_relative_path(
 886            variables.get(&VariableName::RelativeFile).unwrap_or(""),
 887        );
 888
 889        let module_target = (PYTHON_MODULE_NAME_TASK_VARIABLE.clone(), python_module_name);
 890
 891        Ok(module_target)
 892    }
 893}
 894
 895fn python_module_name_from_relative_path(relative_path: &str) -> String {
 896    let path_with_dots = relative_path.replace('/', ".");
 897    path_with_dots
 898        .strip_suffix(".py")
 899        .unwrap_or(&path_with_dots)
 900        .to_string()
 901}
 902
 903fn python_env_kind_display(k: &PythonEnvironmentKind) -> &'static str {
 904    match k {
 905        PythonEnvironmentKind::Conda => "Conda",
 906        PythonEnvironmentKind::Pixi => "pixi",
 907        PythonEnvironmentKind::Homebrew => "Homebrew",
 908        PythonEnvironmentKind::Pyenv => "global (Pyenv)",
 909        PythonEnvironmentKind::GlobalPaths => "global",
 910        PythonEnvironmentKind::PyenvVirtualEnv => "Pyenv",
 911        PythonEnvironmentKind::Pipenv => "Pipenv",
 912        PythonEnvironmentKind::Poetry => "Poetry",
 913        PythonEnvironmentKind::MacPythonOrg => "global (Python.org)",
 914        PythonEnvironmentKind::MacCommandLineTools => "global (Command Line Tools for Xcode)",
 915        PythonEnvironmentKind::LinuxGlobal => "global",
 916        PythonEnvironmentKind::MacXCode => "global (Xcode)",
 917        PythonEnvironmentKind::Venv => "venv",
 918        PythonEnvironmentKind::VirtualEnv => "virtualenv",
 919        PythonEnvironmentKind::VirtualEnvWrapper => "virtualenvwrapper",
 920        PythonEnvironmentKind::WindowsStore => "global (Windows Store)",
 921        PythonEnvironmentKind::WindowsRegistry => "global (Windows Registry)",
 922    }
 923}
 924
 925pub(crate) struct PythonToolchainProvider;
 926
 927static ENV_PRIORITY_LIST: &[PythonEnvironmentKind] = &[
 928    // Prioritize non-Conda environments.
 929    PythonEnvironmentKind::Poetry,
 930    PythonEnvironmentKind::Pipenv,
 931    PythonEnvironmentKind::VirtualEnvWrapper,
 932    PythonEnvironmentKind::Venv,
 933    PythonEnvironmentKind::VirtualEnv,
 934    PythonEnvironmentKind::PyenvVirtualEnv,
 935    PythonEnvironmentKind::Pixi,
 936    PythonEnvironmentKind::Conda,
 937    PythonEnvironmentKind::Pyenv,
 938    PythonEnvironmentKind::GlobalPaths,
 939    PythonEnvironmentKind::Homebrew,
 940];
 941
 942fn env_priority(kind: Option<PythonEnvironmentKind>) -> usize {
 943    if let Some(kind) = kind {
 944        ENV_PRIORITY_LIST
 945            .iter()
 946            .position(|blessed_env| blessed_env == &kind)
 947            .unwrap_or(ENV_PRIORITY_LIST.len())
 948    } else {
 949        // Unknown toolchains are less useful than non-blessed ones.
 950        ENV_PRIORITY_LIST.len() + 1
 951    }
 952}
 953
 954/// Return the name of environment declared in <worktree-root/.venv.
 955///
 956/// https://virtualfish.readthedocs.io/en/latest/plugins.html#auto-activation-auto-activation
 957async fn get_worktree_venv_declaration(worktree_root: &Path) -> Option<String> {
 958    let file = async_fs::File::open(worktree_root.join(".venv"))
 959        .await
 960        .ok()?;
 961    let mut venv_name = String::new();
 962    smol::io::BufReader::new(file)
 963        .read_line(&mut venv_name)
 964        .await
 965        .ok()?;
 966    Some(venv_name.trim().to_string())
 967}
 968
 969#[async_trait]
 970impl ToolchainLister for PythonToolchainProvider {
 971    async fn list(
 972        &self,
 973        worktree_root: PathBuf,
 974        subroot_relative_path: Arc<Path>,
 975        project_env: Option<HashMap<String, String>>,
 976    ) -> ToolchainList {
 977        let env = project_env.unwrap_or_default();
 978        let environment = EnvironmentApi::from_env(&env);
 979        let locators = pet::locators::create_locators(
 980            Arc::new(pet_conda::Conda::from(&environment)),
 981            Arc::new(pet_poetry::Poetry::from(&environment)),
 982            &environment,
 983        );
 984        let mut config = Configuration::default();
 985
 986        debug_assert!(subroot_relative_path.is_relative());
 987        // `.ancestors()` will yield at least one path, so in case of empty `subroot_relative_path`, we'll just use
 988        // worktree root as the workspace directory.
 989        config.workspace_directories = Some(
 990            subroot_relative_path
 991                .ancestors()
 992                .map(|ancestor| worktree_root.join(ancestor))
 993                .collect(),
 994        );
 995        for locator in locators.iter() {
 996            locator.configure(&config);
 997        }
 998
 999        let reporter = pet_reporter::collect::create_reporter();
1000        pet::find::find_and_report_envs(&reporter, config, &locators, &environment, None);
1001
1002        let mut toolchains = reporter
1003            .environments
1004            .lock()
1005            .map_or(Vec::new(), |mut guard| std::mem::take(&mut guard));
1006
1007        let wr = worktree_root;
1008        let wr_venv = get_worktree_venv_declaration(&wr).await;
1009        // Sort detected environments by:
1010        //     environment name matching activation file (<workdir>/.venv)
1011        //     environment project dir matching worktree_root
1012        //     general env priority
1013        //     environment path matching the CONDA_PREFIX env var
1014        //     executable path
1015        toolchains.sort_by(|lhs, rhs| {
1016            // Compare venv names against worktree .venv file
1017            let venv_ordering =
1018                wr_venv
1019                    .as_ref()
1020                    .map_or(Ordering::Equal, |venv| match (&lhs.name, &rhs.name) {
1021                        (Some(l), Some(r)) => (r == venv).cmp(&(l == venv)),
1022                        (Some(l), None) if l == venv => Ordering::Less,
1023                        (None, Some(r)) if r == venv => Ordering::Greater,
1024                        _ => Ordering::Equal,
1025                    });
1026
1027            // Compare project paths against worktree root
1028            let proj_ordering = || match (&lhs.project, &rhs.project) {
1029                (Some(l), Some(r)) => (r == &wr).cmp(&(l == &wr)),
1030                (Some(l), None) if l == &wr => Ordering::Less,
1031                (None, Some(r)) if r == &wr => Ordering::Greater,
1032                _ => Ordering::Equal,
1033            };
1034
1035            // Compare environment priorities
1036            let priority_ordering = || env_priority(lhs.kind).cmp(&env_priority(rhs.kind));
1037
1038            // Compare conda prefixes
1039            let conda_ordering = || {
1040                if lhs.kind == Some(PythonEnvironmentKind::Conda) {
1041                    environment
1042                        .get_env_var("CONDA_PREFIX".to_string())
1043                        .map(|conda_prefix| {
1044                            let is_match = |exe: &Option<PathBuf>| {
1045                                exe.as_ref().is_some_and(|e| e.starts_with(&conda_prefix))
1046                            };
1047                            match (is_match(&lhs.executable), is_match(&rhs.executable)) {
1048                                (true, false) => Ordering::Less,
1049                                (false, true) => Ordering::Greater,
1050                                _ => Ordering::Equal,
1051                            }
1052                        })
1053                        .unwrap_or(Ordering::Equal)
1054                } else {
1055                    Ordering::Equal
1056                }
1057            };
1058
1059            // Compare Python executables
1060            let exe_ordering = || lhs.executable.cmp(&rhs.executable);
1061
1062            venv_ordering
1063                .then_with(proj_ordering)
1064                .then_with(priority_ordering)
1065                .then_with(conda_ordering)
1066                .then_with(exe_ordering)
1067        });
1068
1069        let mut toolchains: Vec<_> = toolchains
1070            .into_iter()
1071            .filter_map(venv_to_toolchain)
1072            .collect();
1073        toolchains.dedup();
1074        ToolchainList {
1075            toolchains,
1076            default: None,
1077            groups: Default::default(),
1078        }
1079    }
1080    fn meta(&self) -> ToolchainMetadata {
1081        ToolchainMetadata {
1082            term: SharedString::new_static("Virtual Environment"),
1083            new_toolchain_placeholder: SharedString::new_static(
1084                "A path to the python3 executable within a virtual environment, or path to virtual environment itself",
1085            ),
1086            manifest_name: ManifestName::from(SharedString::new_static("pyproject.toml")),
1087        }
1088    }
1089
1090    async fn resolve(
1091        &self,
1092        path: PathBuf,
1093        env: Option<HashMap<String, String>>,
1094    ) -> anyhow::Result<Toolchain> {
1095        let env = env.unwrap_or_default();
1096        let environment = EnvironmentApi::from_env(&env);
1097        let locators = pet::locators::create_locators(
1098            Arc::new(pet_conda::Conda::from(&environment)),
1099            Arc::new(pet_poetry::Poetry::from(&environment)),
1100            &environment,
1101        );
1102        let toolchain = pet::resolve::resolve_environment(&path, &locators, &environment)
1103            .context("Could not find a virtual environment in provided path")?;
1104        let venv = toolchain.resolved.unwrap_or(toolchain.discovered);
1105        venv_to_toolchain(venv).context("Could not convert a venv into a toolchain")
1106    }
1107
1108    async fn activation_script(
1109        &self,
1110        toolchain: &Toolchain,
1111        shell: ShellKind,
1112        fs: &dyn Fs,
1113    ) -> Vec<String> {
1114        let Ok(toolchain) = serde_json::from_value::<pet_core::python_environment::PythonEnvironment>(
1115            toolchain.as_json.clone(),
1116        ) else {
1117            return vec![];
1118        };
1119        let mut activation_script = vec![];
1120
1121        match toolchain.kind {
1122            Some(PythonEnvironmentKind::Conda) => {
1123                if let Some(name) = &toolchain.name {
1124                    activation_script.push(format!("conda activate {name}"));
1125                } else {
1126                    activation_script.push("conda activate".to_string());
1127                }
1128            }
1129            Some(PythonEnvironmentKind::Venv | PythonEnvironmentKind::VirtualEnv) => {
1130                if let Some(prefix) = &toolchain.prefix {
1131                    let activate_keyword = match shell {
1132                        ShellKind::Cmd => ".",
1133                        ShellKind::Nushell => "overlay use",
1134                        ShellKind::PowerShell => ".",
1135                        ShellKind::Fish => "source",
1136                        ShellKind::Csh => "source",
1137                        ShellKind::Posix => "source",
1138                    };
1139                    let activate_script_name = match shell {
1140                        ShellKind::Posix => "activate",
1141                        ShellKind::Csh => "activate.csh",
1142                        ShellKind::Fish => "activate.fish",
1143                        ShellKind::Nushell => "activate.nu",
1144                        ShellKind::PowerShell => "activate.ps1",
1145                        ShellKind::Cmd => "activate.bat",
1146                    };
1147                    let path = prefix.join(BINARY_DIR).join(activate_script_name);
1148
1149                    if let Ok(quoted) =
1150                        shlex::try_quote(&path.to_string_lossy()).map(Cow::into_owned)
1151                        && fs.is_file(&path).await
1152                    {
1153                        activation_script.push(format!("{activate_keyword} {quoted}"));
1154                    }
1155                }
1156            }
1157            Some(PythonEnvironmentKind::Pyenv) => {
1158                let Some(manager) = toolchain.manager else {
1159                    return vec![];
1160                };
1161                let version = toolchain.version.as_deref().unwrap_or("system");
1162                let pyenv = manager.executable;
1163                let pyenv = pyenv.display();
1164                activation_script.extend(match shell {
1165                    ShellKind::Fish => Some(format!("\"{pyenv}\" shell - fish {version}")),
1166                    ShellKind::Posix => Some(format!("\"{pyenv}\" shell - sh {version}")),
1167                    ShellKind::Nushell => Some(format!("\"{pyenv}\" shell - nu {version}")),
1168                    ShellKind::PowerShell => None,
1169                    ShellKind::Csh => None,
1170                    ShellKind::Cmd => None,
1171                })
1172            }
1173            _ => {}
1174        }
1175        activation_script
1176    }
1177}
1178
1179fn venv_to_toolchain(venv: PythonEnvironment) -> Option<Toolchain> {
1180    let mut name = String::from("Python");
1181    if let Some(ref version) = venv.version {
1182        _ = write!(name, " {version}");
1183    }
1184
1185    let name_and_kind = match (&venv.name, &venv.kind) {
1186        (Some(name), Some(kind)) => Some(format!("({name}; {})", python_env_kind_display(kind))),
1187        (Some(name), None) => Some(format!("({name})")),
1188        (None, Some(kind)) => Some(format!("({})", python_env_kind_display(kind))),
1189        (None, None) => None,
1190    };
1191
1192    if let Some(nk) = name_and_kind {
1193        _ = write!(name, " {nk}");
1194    }
1195
1196    Some(Toolchain {
1197        name: name.into(),
1198        path: venv.executable.as_ref()?.to_str()?.to_owned().into(),
1199        language_name: LanguageName::new("Python"),
1200        as_json: serde_json::to_value(venv).ok()?,
1201    })
1202}
1203
1204pub struct EnvironmentApi<'a> {
1205    global_search_locations: Arc<Mutex<Vec<PathBuf>>>,
1206    project_env: &'a HashMap<String, String>,
1207    pet_env: pet_core::os_environment::EnvironmentApi,
1208}
1209
1210impl<'a> EnvironmentApi<'a> {
1211    pub fn from_env(project_env: &'a HashMap<String, String>) -> Self {
1212        let paths = project_env
1213            .get("PATH")
1214            .map(|p| std::env::split_paths(p).collect())
1215            .unwrap_or_default();
1216
1217        EnvironmentApi {
1218            global_search_locations: Arc::new(Mutex::new(paths)),
1219            project_env,
1220            pet_env: pet_core::os_environment::EnvironmentApi::new(),
1221        }
1222    }
1223
1224    fn user_home(&self) -> Option<PathBuf> {
1225        self.project_env
1226            .get("HOME")
1227            .or_else(|| self.project_env.get("USERPROFILE"))
1228            .map(|home| pet_fs::path::norm_case(PathBuf::from(home)))
1229            .or_else(|| self.pet_env.get_user_home())
1230    }
1231}
1232
1233impl pet_core::os_environment::Environment for EnvironmentApi<'_> {
1234    fn get_user_home(&self) -> Option<PathBuf> {
1235        self.user_home()
1236    }
1237
1238    fn get_root(&self) -> Option<PathBuf> {
1239        None
1240    }
1241
1242    fn get_env_var(&self, key: String) -> Option<String> {
1243        self.project_env
1244            .get(&key)
1245            .cloned()
1246            .or_else(|| self.pet_env.get_env_var(key))
1247    }
1248
1249    fn get_know_global_search_locations(&self) -> Vec<PathBuf> {
1250        if self.global_search_locations.lock().is_empty() {
1251            let mut paths =
1252                std::env::split_paths(&self.get_env_var("PATH".to_string()).unwrap_or_default())
1253                    .collect::<Vec<PathBuf>>();
1254
1255            log::trace!("Env PATH: {:?}", paths);
1256            for p in self.pet_env.get_know_global_search_locations() {
1257                if !paths.contains(&p) {
1258                    paths.push(p);
1259                }
1260            }
1261
1262            let mut paths = paths
1263                .into_iter()
1264                .filter(|p| p.exists())
1265                .collect::<Vec<PathBuf>>();
1266
1267            self.global_search_locations.lock().append(&mut paths);
1268        }
1269        self.global_search_locations.lock().clone()
1270    }
1271}
1272
1273pub(crate) struct PyLspAdapter {
1274    python_venv_base: OnceCell<Result<Arc<Path>, String>>,
1275}
1276impl PyLspAdapter {
1277    const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("pylsp");
1278    pub(crate) fn new() -> Self {
1279        Self {
1280            python_venv_base: OnceCell::new(),
1281        }
1282    }
1283    async fn ensure_venv(delegate: &dyn LspAdapterDelegate) -> Result<Arc<Path>> {
1284        let python_path = Self::find_base_python(delegate)
1285            .await
1286            .context("Could not find Python installation for PyLSP")?;
1287        let work_dir = delegate
1288            .language_server_download_dir(&Self::SERVER_NAME)
1289            .await
1290            .context("Could not get working directory for PyLSP")?;
1291        let mut path = PathBuf::from(work_dir.as_ref());
1292        path.push("pylsp-venv");
1293        if !path.exists() {
1294            util::command::new_smol_command(python_path)
1295                .arg("-m")
1296                .arg("venv")
1297                .arg("pylsp-venv")
1298                .current_dir(work_dir)
1299                .spawn()?
1300                .output()
1301                .await?;
1302        }
1303
1304        Ok(path.into())
1305    }
1306    // Find "baseline", user python version from which we'll create our own venv.
1307    async fn find_base_python(delegate: &dyn LspAdapterDelegate) -> Option<PathBuf> {
1308        for path in ["python3", "python"] {
1309            if let Some(path) = delegate.which(path.as_ref()).await {
1310                return Some(path);
1311            }
1312        }
1313        None
1314    }
1315
1316    async fn base_venv(&self, delegate: &dyn LspAdapterDelegate) -> Result<Arc<Path>, String> {
1317        self.python_venv_base
1318            .get_or_init(move || async move {
1319                Self::ensure_venv(delegate)
1320                    .await
1321                    .map_err(|e| format!("{e}"))
1322            })
1323            .await
1324            .clone()
1325    }
1326}
1327
1328const BINARY_DIR: &str = if cfg!(target_os = "windows") {
1329    "Scripts"
1330} else {
1331    "bin"
1332};
1333
1334#[async_trait(?Send)]
1335impl LspAdapter for PyLspAdapter {
1336    fn name(&self) -> LanguageServerName {
1337        Self::SERVER_NAME
1338    }
1339
1340    async fn process_completions(&self, _items: &mut [lsp::CompletionItem]) {}
1341
1342    async fn label_for_completion(
1343        &self,
1344        item: &lsp::CompletionItem,
1345        language: &Arc<language::Language>,
1346    ) -> Option<language::CodeLabel> {
1347        let label = &item.label;
1348        let grammar = language.grammar()?;
1349        let highlight_id = match item.kind? {
1350            lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method")?,
1351            lsp::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function")?,
1352            lsp::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type")?,
1353            lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?,
1354            _ => return None,
1355        };
1356        let filter_range = item
1357            .filter_text
1358            .as_deref()
1359            .and_then(|filter| label.find(filter).map(|ix| ix..ix + filter.len()))
1360            .unwrap_or(0..label.len());
1361        Some(language::CodeLabel {
1362            text: label.clone(),
1363            runs: vec![(0..label.len(), highlight_id)],
1364            filter_range,
1365        })
1366    }
1367
1368    async fn label_for_symbol(
1369        &self,
1370        name: &str,
1371        kind: lsp::SymbolKind,
1372        language: &Arc<language::Language>,
1373    ) -> Option<language::CodeLabel> {
1374        let (text, filter_range, display_range) = match kind {
1375            lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
1376                let text = format!("def {}():\n", name);
1377                let filter_range = 4..4 + name.len();
1378                let display_range = 0..filter_range.end;
1379                (text, filter_range, display_range)
1380            }
1381            lsp::SymbolKind::CLASS => {
1382                let text = format!("class {}:", name);
1383                let filter_range = 6..6 + name.len();
1384                let display_range = 0..filter_range.end;
1385                (text, filter_range, display_range)
1386            }
1387            lsp::SymbolKind::CONSTANT => {
1388                let text = format!("{} = 0", name);
1389                let filter_range = 0..name.len();
1390                let display_range = 0..filter_range.end;
1391                (text, filter_range, display_range)
1392            }
1393            _ => return None,
1394        };
1395
1396        Some(language::CodeLabel {
1397            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
1398            text: text[display_range].to_string(),
1399            filter_range,
1400        })
1401    }
1402
1403    async fn workspace_configuration(
1404        self: Arc<Self>,
1405        adapter: &Arc<dyn LspAdapterDelegate>,
1406        toolchain: Option<Toolchain>,
1407        cx: &mut AsyncApp,
1408    ) -> Result<Value> {
1409        cx.update(move |cx| {
1410            let mut user_settings =
1411                language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
1412                    .and_then(|s| s.settings.clone())
1413                    .unwrap_or_else(|| {
1414                        json!({
1415                            "plugins": {
1416                                "pycodestyle": {"enabled": false},
1417                                "rope_autoimport": {"enabled": true, "memory": true},
1418                                "pylsp_mypy": {"enabled": false}
1419                            },
1420                            "rope": {
1421                                "ropeFolder": null
1422                            },
1423                        })
1424                    });
1425
1426            // If user did not explicitly modify their python venv, use one from picker.
1427            if let Some(toolchain) = toolchain {
1428                if user_settings.is_null() {
1429                    user_settings = Value::Object(serde_json::Map::default());
1430                }
1431                let object = user_settings.as_object_mut().unwrap();
1432                if let Some(python) = object
1433                    .entry("plugins")
1434                    .or_insert(Value::Object(serde_json::Map::default()))
1435                    .as_object_mut()
1436                {
1437                    if let Some(jedi) = python
1438                        .entry("jedi")
1439                        .or_insert(Value::Object(serde_json::Map::default()))
1440                        .as_object_mut()
1441                    {
1442                        jedi.entry("environment".to_string())
1443                            .or_insert_with(|| Value::String(toolchain.path.clone().into()));
1444                    }
1445                    if let Some(pylint) = python
1446                        .entry("pylsp_mypy")
1447                        .or_insert(Value::Object(serde_json::Map::default()))
1448                        .as_object_mut()
1449                    {
1450                        pylint.entry("overrides".to_string()).or_insert_with(|| {
1451                            Value::Array(vec![
1452                                Value::String("--python-executable".into()),
1453                                Value::String(toolchain.path.into()),
1454                                Value::String("--cache-dir=/dev/null".into()),
1455                                Value::Bool(true),
1456                            ])
1457                        });
1458                    }
1459                }
1460            }
1461            user_settings = Value::Object(serde_json::Map::from_iter([(
1462                "pylsp".to_string(),
1463                user_settings,
1464            )]));
1465
1466            user_settings
1467        })
1468    }
1469}
1470
1471impl LspInstaller for PyLspAdapter {
1472    type BinaryVersion = ();
1473    async fn check_if_user_installed(
1474        &self,
1475        delegate: &dyn LspAdapterDelegate,
1476        toolchain: Option<Toolchain>,
1477        _: &AsyncApp,
1478    ) -> Option<LanguageServerBinary> {
1479        if let Some(pylsp_bin) = delegate.which(Self::SERVER_NAME.as_ref()).await {
1480            let env = delegate.shell_env().await;
1481            Some(LanguageServerBinary {
1482                path: pylsp_bin,
1483                env: Some(env),
1484                arguments: vec![],
1485            })
1486        } else {
1487            let toolchain = toolchain?;
1488            let pylsp_path = Path::new(toolchain.path.as_ref()).parent()?.join("pylsp");
1489            pylsp_path.exists().then(|| LanguageServerBinary {
1490                path: toolchain.path.to_string().into(),
1491                arguments: vec![pylsp_path.into()],
1492                env: None,
1493            })
1494        }
1495    }
1496
1497    async fn fetch_latest_server_version(
1498        &self,
1499        _: &dyn LspAdapterDelegate,
1500        _: bool,
1501        _: &mut AsyncApp,
1502    ) -> Result<()> {
1503        Ok(())
1504    }
1505
1506    async fn fetch_server_binary(
1507        &self,
1508        _: (),
1509        _: PathBuf,
1510        delegate: &dyn LspAdapterDelegate,
1511    ) -> Result<LanguageServerBinary> {
1512        let venv = self.base_venv(delegate).await.map_err(|e| anyhow!(e))?;
1513        let pip_path = venv.join(BINARY_DIR).join("pip3");
1514        ensure!(
1515            util::command::new_smol_command(pip_path.as_path())
1516                .arg("install")
1517                .arg("python-lsp-server")
1518                .arg("-U")
1519                .output()
1520                .await?
1521                .status
1522                .success(),
1523            "python-lsp-server installation failed"
1524        );
1525        ensure!(
1526            util::command::new_smol_command(pip_path.as_path())
1527                .arg("install")
1528                .arg("python-lsp-server[all]")
1529                .arg("-U")
1530                .output()
1531                .await?
1532                .status
1533                .success(),
1534            "python-lsp-server[all] installation failed"
1535        );
1536        ensure!(
1537            util::command::new_smol_command(pip_path)
1538                .arg("install")
1539                .arg("pylsp-mypy")
1540                .arg("-U")
1541                .output()
1542                .await?
1543                .status
1544                .success(),
1545            "pylsp-mypy installation failed"
1546        );
1547        let pylsp = venv.join(BINARY_DIR).join("pylsp");
1548        Ok(LanguageServerBinary {
1549            path: pylsp,
1550            env: None,
1551            arguments: vec![],
1552        })
1553    }
1554
1555    async fn cached_server_binary(
1556        &self,
1557        _: PathBuf,
1558        delegate: &dyn LspAdapterDelegate,
1559    ) -> Option<LanguageServerBinary> {
1560        let venv = self.base_venv(delegate).await.ok()?;
1561        let pylsp = venv.join(BINARY_DIR).join("pylsp");
1562        Some(LanguageServerBinary {
1563            path: pylsp,
1564            env: None,
1565            arguments: vec![],
1566        })
1567    }
1568}
1569
1570pub(crate) struct BasedPyrightLspAdapter {
1571    python_venv_base: OnceCell<Result<Arc<Path>, String>>,
1572}
1573
1574impl BasedPyrightLspAdapter {
1575    const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("basedpyright");
1576    const BINARY_NAME: &'static str = "basedpyright-langserver";
1577
1578    pub(crate) fn new() -> Self {
1579        Self {
1580            python_venv_base: OnceCell::new(),
1581        }
1582    }
1583
1584    async fn ensure_venv(delegate: &dyn LspAdapterDelegate) -> Result<Arc<Path>> {
1585        let python_path = Self::find_base_python(delegate)
1586            .await
1587            .context("Could not find Python installation for basedpyright")?;
1588        let work_dir = delegate
1589            .language_server_download_dir(&Self::SERVER_NAME)
1590            .await
1591            .context("Could not get working directory for basedpyright")?;
1592        let mut path = PathBuf::from(work_dir.as_ref());
1593        path.push("basedpyright-venv");
1594        if !path.exists() {
1595            util::command::new_smol_command(python_path)
1596                .arg("-m")
1597                .arg("venv")
1598                .arg("basedpyright-venv")
1599                .current_dir(work_dir)
1600                .spawn()?
1601                .output()
1602                .await?;
1603        }
1604
1605        Ok(path.into())
1606    }
1607
1608    // Find "baseline", user python version from which we'll create our own venv.
1609    async fn find_base_python(delegate: &dyn LspAdapterDelegate) -> Option<PathBuf> {
1610        for path in ["python3", "python"] {
1611            if let Some(path) = delegate.which(path.as_ref()).await {
1612                return Some(path);
1613            }
1614        }
1615        None
1616    }
1617
1618    async fn base_venv(&self, delegate: &dyn LspAdapterDelegate) -> Result<Arc<Path>, String> {
1619        self.python_venv_base
1620            .get_or_init(move || async move {
1621                Self::ensure_venv(delegate)
1622                    .await
1623                    .map_err(|e| format!("{e}"))
1624            })
1625            .await
1626            .clone()
1627    }
1628}
1629
1630#[async_trait(?Send)]
1631impl LspAdapter for BasedPyrightLspAdapter {
1632    fn name(&self) -> LanguageServerName {
1633        Self::SERVER_NAME
1634    }
1635
1636    async fn initialization_options(
1637        self: Arc<Self>,
1638        _: &Arc<dyn LspAdapterDelegate>,
1639    ) -> Result<Option<Value>> {
1640        // Provide minimal initialization options
1641        // Virtual environment configuration will be handled through workspace configuration
1642        Ok(Some(json!({
1643            "python": {
1644                "analysis": {
1645                    "autoSearchPaths": true,
1646                    "useLibraryCodeForTypes": true,
1647                    "autoImportCompletions": true
1648                }
1649            }
1650        })))
1651    }
1652
1653    async fn process_completions(&self, items: &mut [lsp::CompletionItem]) {
1654        process_pyright_completions(items);
1655    }
1656
1657    async fn label_for_completion(
1658        &self,
1659        item: &lsp::CompletionItem,
1660        language: &Arc<language::Language>,
1661    ) -> Option<language::CodeLabel> {
1662        let label = &item.label;
1663        let grammar = language.grammar()?;
1664        let highlight_id = match item.kind? {
1665            lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method"),
1666            lsp::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function"),
1667            lsp::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type"),
1668            lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant"),
1669            lsp::CompletionItemKind::VARIABLE => grammar.highlight_id_for_name("variable"),
1670            _ => {
1671                return None;
1672            }
1673        };
1674        let filter_range = item
1675            .filter_text
1676            .as_deref()
1677            .and_then(|filter| label.find(filter).map(|ix| ix..ix + filter.len()))
1678            .unwrap_or(0..label.len());
1679        let mut text = label.clone();
1680        if let Some(completion_details) = item
1681            .label_details
1682            .as_ref()
1683            .and_then(|details| details.description.as_ref())
1684        {
1685            write!(&mut text, " {}", completion_details).ok();
1686        }
1687        Some(language::CodeLabel {
1688            runs: highlight_id
1689                .map(|id| (0..label.len(), id))
1690                .into_iter()
1691                .collect(),
1692            text,
1693            filter_range,
1694        })
1695    }
1696
1697    async fn label_for_symbol(
1698        &self,
1699        name: &str,
1700        kind: lsp::SymbolKind,
1701        language: &Arc<language::Language>,
1702    ) -> Option<language::CodeLabel> {
1703        let (text, filter_range, display_range) = match kind {
1704            lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {
1705                let text = format!("def {}():\n", name);
1706                let filter_range = 4..4 + name.len();
1707                let display_range = 0..filter_range.end;
1708                (text, filter_range, display_range)
1709            }
1710            lsp::SymbolKind::CLASS => {
1711                let text = format!("class {}:", name);
1712                let filter_range = 6..6 + name.len();
1713                let display_range = 0..filter_range.end;
1714                (text, filter_range, display_range)
1715            }
1716            lsp::SymbolKind::CONSTANT => {
1717                let text = format!("{} = 0", name);
1718                let filter_range = 0..name.len();
1719                let display_range = 0..filter_range.end;
1720                (text, filter_range, display_range)
1721            }
1722            _ => return None,
1723        };
1724
1725        Some(language::CodeLabel {
1726            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),
1727            text: text[display_range].to_string(),
1728            filter_range,
1729        })
1730    }
1731
1732    async fn workspace_configuration(
1733        self: Arc<Self>,
1734        adapter: &Arc<dyn LspAdapterDelegate>,
1735        toolchain: Option<Toolchain>,
1736        cx: &mut AsyncApp,
1737    ) -> Result<Value> {
1738        cx.update(move |cx| {
1739            let mut user_settings =
1740                language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
1741                    .and_then(|s| s.settings.clone())
1742                    .unwrap_or_default();
1743
1744            // If we have a detected toolchain, configure Pyright to use it
1745            if let Some(toolchain) = toolchain
1746                && let Ok(env) = serde_json::from_value::<
1747                    pet_core::python_environment::PythonEnvironment,
1748                >(toolchain.as_json.clone())
1749            {
1750                if user_settings.is_null() {
1751                    user_settings = Value::Object(serde_json::Map::default());
1752                }
1753                let object = user_settings.as_object_mut().unwrap();
1754
1755                let interpreter_path = toolchain.path.to_string();
1756                if let Some(venv_dir) = env.prefix {
1757                    // Set venvPath and venv at the root level
1758                    // This matches the format of a pyrightconfig.json file
1759                    if let Some(parent) = venv_dir.parent() {
1760                        // Use relative path if the venv is inside the workspace
1761                        let venv_path = if parent == adapter.worktree_root_path() {
1762                            ".".to_string()
1763                        } else {
1764                            parent.to_string_lossy().into_owned()
1765                        };
1766                        object.insert("venvPath".to_string(), Value::String(venv_path));
1767                    }
1768
1769                    if let Some(venv_name) = venv_dir.file_name() {
1770                        object.insert(
1771                            "venv".to_owned(),
1772                            Value::String(venv_name.to_string_lossy().into_owned()),
1773                        );
1774                    }
1775                }
1776
1777                // Set both pythonPath and defaultInterpreterPath for compatibility
1778                if let Some(python) = object
1779                    .entry("python")
1780                    .or_insert(Value::Object(serde_json::Map::default()))
1781                    .as_object_mut()
1782                {
1783                    python.insert(
1784                        "pythonPath".to_owned(),
1785                        Value::String(interpreter_path.clone()),
1786                    );
1787                    python.insert(
1788                        "defaultInterpreterPath".to_owned(),
1789                        Value::String(interpreter_path),
1790                    );
1791                }
1792                // Basedpyright by default uses `strict` type checking, we tone it down as to not surpris users
1793                maybe!({
1794                    let basedpyright = object
1795                        .entry("basedpyright")
1796                        .or_insert(Value::Object(serde_json::Map::default()));
1797                    let analysis = basedpyright
1798                        .as_object_mut()?
1799                        .entry("analysis")
1800                        .or_insert(Value::Object(serde_json::Map::default()));
1801                    if let serde_json::map::Entry::Vacant(v) =
1802                        analysis.as_object_mut()?.entry("typeCheckingMode")
1803                    {
1804                        v.insert(Value::String("standard".to_owned()));
1805                    }
1806                    Some(())
1807                });
1808            }
1809
1810            user_settings
1811        })
1812    }
1813}
1814
1815impl LspInstaller for BasedPyrightLspAdapter {
1816    type BinaryVersion = ();
1817
1818    async fn fetch_latest_server_version(
1819        &self,
1820        _: &dyn LspAdapterDelegate,
1821        _: bool,
1822        _: &mut AsyncApp,
1823    ) -> Result<()> {
1824        Ok(())
1825    }
1826
1827    async fn check_if_user_installed(
1828        &self,
1829        delegate: &dyn LspAdapterDelegate,
1830        toolchain: Option<Toolchain>,
1831        _: &AsyncApp,
1832    ) -> Option<LanguageServerBinary> {
1833        if let Some(bin) = delegate.which(Self::BINARY_NAME.as_ref()).await {
1834            let env = delegate.shell_env().await;
1835            Some(LanguageServerBinary {
1836                path: bin,
1837                env: Some(env),
1838                arguments: vec!["--stdio".into()],
1839            })
1840        } else {
1841            let path = Path::new(toolchain?.path.as_ref())
1842                .parent()?
1843                .join(Self::BINARY_NAME);
1844            path.exists().then(|| LanguageServerBinary {
1845                path,
1846                arguments: vec!["--stdio".into()],
1847                env: None,
1848            })
1849        }
1850    }
1851
1852    async fn fetch_server_binary(
1853        &self,
1854        _latest_version: (),
1855        _container_dir: PathBuf,
1856        delegate: &dyn LspAdapterDelegate,
1857    ) -> Result<LanguageServerBinary> {
1858        let venv = self.base_venv(delegate).await.map_err(|e| anyhow!(e))?;
1859        let pip_path = venv.join(BINARY_DIR).join("pip3");
1860        ensure!(
1861            util::command::new_smol_command(pip_path.as_path())
1862                .arg("install")
1863                .arg("basedpyright")
1864                .arg("-U")
1865                .output()
1866                .await?
1867                .status
1868                .success(),
1869            "basedpyright installation failed"
1870        );
1871        let pylsp = venv.join(BINARY_DIR).join(Self::BINARY_NAME);
1872        Ok(LanguageServerBinary {
1873            path: pylsp,
1874            env: None,
1875            arguments: vec!["--stdio".into()],
1876        })
1877    }
1878
1879    async fn cached_server_binary(
1880        &self,
1881        _container_dir: PathBuf,
1882        delegate: &dyn LspAdapterDelegate,
1883    ) -> Option<LanguageServerBinary> {
1884        let venv = self.base_venv(delegate).await.ok()?;
1885        let pylsp = venv.join(BINARY_DIR).join(Self::BINARY_NAME);
1886        Some(LanguageServerBinary {
1887            path: pylsp,
1888            env: None,
1889            arguments: vec!["--stdio".into()],
1890        })
1891    }
1892}
1893
1894pub(crate) struct RuffLspAdapter {
1895    fs: Arc<dyn Fs>,
1896}
1897
1898#[cfg(target_os = "macos")]
1899impl RuffLspAdapter {
1900    const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
1901    const ARCH_SERVER_NAME: &str = "apple-darwin";
1902}
1903
1904#[cfg(target_os = "linux")]
1905impl RuffLspAdapter {
1906    const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
1907    const ARCH_SERVER_NAME: &str = "unknown-linux-gnu";
1908}
1909
1910#[cfg(target_os = "freebsd")]
1911impl RuffLspAdapter {
1912    const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
1913    const ARCH_SERVER_NAME: &str = "unknown-freebsd";
1914}
1915
1916#[cfg(target_os = "windows")]
1917impl RuffLspAdapter {
1918    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;
1919    const ARCH_SERVER_NAME: &str = "pc-windows-msvc";
1920}
1921
1922impl RuffLspAdapter {
1923    const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("ruff");
1924
1925    pub fn new(fs: Arc<dyn Fs>) -> RuffLspAdapter {
1926        RuffLspAdapter { fs }
1927    }
1928
1929    fn build_asset_name() -> Result<(String, String)> {
1930        let arch = match consts::ARCH {
1931            "x86" => "i686",
1932            _ => consts::ARCH,
1933        };
1934        let os = Self::ARCH_SERVER_NAME;
1935        let suffix = match consts::OS {
1936            "windows" => "zip",
1937            _ => "tar.gz",
1938        };
1939        let asset_name = format!("ruff-{arch}-{os}.{suffix}");
1940        let asset_stem = format!("ruff-{arch}-{os}");
1941        Ok((asset_stem, asset_name))
1942    }
1943}
1944
1945#[async_trait(?Send)]
1946impl LspAdapter for RuffLspAdapter {
1947    fn name(&self) -> LanguageServerName {
1948        Self::SERVER_NAME
1949    }
1950}
1951
1952impl LspInstaller for RuffLspAdapter {
1953    type BinaryVersion = GitHubLspBinaryVersion;
1954    async fn check_if_user_installed(
1955        &self,
1956        delegate: &dyn LspAdapterDelegate,
1957        toolchain: Option<Toolchain>,
1958        _: &AsyncApp,
1959    ) -> Option<LanguageServerBinary> {
1960        let ruff_in_venv = if let Some(toolchain) = toolchain
1961            && toolchain.language_name.as_ref() == "Python"
1962        {
1963            Path::new(toolchain.path.as_str())
1964                .parent()
1965                .map(|path| path.join("ruff"))
1966        } else {
1967            None
1968        };
1969
1970        for path in ruff_in_venv.into_iter().chain(["ruff".into()]) {
1971            if let Some(ruff_bin) = delegate.which(path.as_os_str()).await {
1972                let env = delegate.shell_env().await;
1973                return Some(LanguageServerBinary {
1974                    path: ruff_bin,
1975                    env: Some(env),
1976                    arguments: vec!["server".into()],
1977                });
1978            }
1979        }
1980
1981        None
1982    }
1983
1984    async fn fetch_latest_server_version(
1985        &self,
1986        delegate: &dyn LspAdapterDelegate,
1987        _: bool,
1988        _: &mut AsyncApp,
1989    ) -> Result<GitHubLspBinaryVersion> {
1990        let release =
1991            latest_github_release("astral-sh/ruff", true, false, delegate.http_client()).await?;
1992        let (_, asset_name) = Self::build_asset_name()?;
1993        let asset = release
1994            .assets
1995            .into_iter()
1996            .find(|asset| asset.name == asset_name)
1997            .with_context(|| format!("no asset found matching `{asset_name:?}`"))?;
1998        Ok(GitHubLspBinaryVersion {
1999            name: release.tag_name,
2000            url: asset.browser_download_url,
2001            digest: asset.digest,
2002        })
2003    }
2004
2005    async fn fetch_server_binary(
2006        &self,
2007        latest_version: GitHubLspBinaryVersion,
2008        container_dir: PathBuf,
2009        delegate: &dyn LspAdapterDelegate,
2010    ) -> Result<LanguageServerBinary> {
2011        let GitHubLspBinaryVersion {
2012            name,
2013            url,
2014            digest: expected_digest,
2015        } = latest_version;
2016        let destination_path = container_dir.join(format!("ruff-{name}"));
2017        let server_path = match Self::GITHUB_ASSET_KIND {
2018            AssetKind::TarGz | AssetKind::Gz => destination_path
2019                .join(Self::build_asset_name()?.0)
2020                .join("ruff"),
2021            AssetKind::Zip => destination_path.clone().join("ruff.exe"),
2022        };
2023
2024        let binary = LanguageServerBinary {
2025            path: server_path.clone(),
2026            env: None,
2027            arguments: vec!["server".into()],
2028        };
2029
2030        let metadata_path = destination_path.with_extension("metadata");
2031        let metadata = GithubBinaryMetadata::read_from_file(&metadata_path)
2032            .await
2033            .ok();
2034        if let Some(metadata) = metadata {
2035            let validity_check = async || {
2036                delegate
2037                    .try_exec(LanguageServerBinary {
2038                        path: server_path.clone(),
2039                        arguments: vec!["--version".into()],
2040                        env: None,
2041                    })
2042                    .await
2043                    .inspect_err(|err| {
2044                        log::warn!("Unable to run {server_path:?} asset, redownloading: {err}",)
2045                    })
2046            };
2047            if let (Some(actual_digest), Some(expected_digest)) =
2048                (&metadata.digest, &expected_digest)
2049            {
2050                if actual_digest == expected_digest {
2051                    if validity_check().await.is_ok() {
2052                        return Ok(binary);
2053                    }
2054                } else {
2055                    log::info!(
2056                        "SHA-256 mismatch for {destination_path:?} asset, downloading new asset. Expected: {expected_digest}, Got: {actual_digest}"
2057                    );
2058                }
2059            } else if validity_check().await.is_ok() {
2060                return Ok(binary);
2061            }
2062        }
2063
2064        download_server_binary(
2065            delegate,
2066            &url,
2067            expected_digest.as_deref(),
2068            &destination_path,
2069            Self::GITHUB_ASSET_KIND,
2070        )
2071        .await?;
2072        make_file_executable(&server_path).await?;
2073        remove_matching(&container_dir, |path| path != destination_path).await;
2074        GithubBinaryMetadata::write_to_file(
2075            &GithubBinaryMetadata {
2076                metadata_version: 1,
2077                digest: expected_digest,
2078            },
2079            &metadata_path,
2080        )
2081        .await?;
2082
2083        Ok(LanguageServerBinary {
2084            path: server_path,
2085            env: None,
2086            arguments: vec!["server".into()],
2087        })
2088    }
2089
2090    async fn cached_server_binary(
2091        &self,
2092        container_dir: PathBuf,
2093        _: &dyn LspAdapterDelegate,
2094    ) -> Option<LanguageServerBinary> {
2095        maybe!(async {
2096            let mut last = None;
2097            let mut entries = self.fs.read_dir(&container_dir).await?;
2098            while let Some(entry) = entries.next().await {
2099                let path = entry?;
2100                if path.extension().is_some_and(|ext| ext == "metadata") {
2101                    continue;
2102                }
2103                last = Some(path);
2104            }
2105
2106            let path = last.context("no cached binary")?;
2107            let path = match Self::GITHUB_ASSET_KIND {
2108                AssetKind::TarGz | AssetKind::Gz => {
2109                    path.join(Self::build_asset_name()?.0).join("ruff")
2110                }
2111                AssetKind::Zip => path.join("ruff.exe"),
2112            };
2113
2114            anyhow::Ok(LanguageServerBinary {
2115                path,
2116                env: None,
2117                arguments: vec!["server".into()],
2118            })
2119        })
2120        .await
2121        .log_err()
2122    }
2123}
2124
2125#[cfg(test)]
2126mod tests {
2127    use gpui::{AppContext as _, BorrowAppContext, Context, TestAppContext};
2128    use language::{AutoindentMode, Buffer, language_settings::AllLanguageSettings};
2129    use settings::SettingsStore;
2130    use std::num::NonZeroU32;
2131
2132    #[gpui::test]
2133    async fn test_python_autoindent(cx: &mut TestAppContext) {
2134        cx.executor().set_block_on_ticks(usize::MAX..=usize::MAX);
2135        let language = crate::language("python", tree_sitter_python::LANGUAGE.into());
2136        cx.update(|cx| {
2137            let test_settings = SettingsStore::test(cx);
2138            cx.set_global(test_settings);
2139            language::init(cx);
2140            cx.update_global::<SettingsStore, _>(|store, cx| {
2141                store.update_user_settings::<AllLanguageSettings>(cx, |s| {
2142                    s.defaults.tab_size = NonZeroU32::new(2);
2143                });
2144            });
2145        });
2146
2147        cx.new(|cx| {
2148            let mut buffer = Buffer::local("", cx).with_language(language, cx);
2149            let append = |buffer: &mut Buffer, text: &str, cx: &mut Context<Buffer>| {
2150                let ix = buffer.len();
2151                buffer.edit([(ix..ix, text)], Some(AutoindentMode::EachLine), cx);
2152            };
2153
2154            // indent after "def():"
2155            append(&mut buffer, "def a():\n", cx);
2156            assert_eq!(buffer.text(), "def a():\n  ");
2157
2158            // preserve indent after blank line
2159            append(&mut buffer, "\n  ", cx);
2160            assert_eq!(buffer.text(), "def a():\n  \n  ");
2161
2162            // indent after "if"
2163            append(&mut buffer, "if a:\n  ", cx);
2164            assert_eq!(buffer.text(), "def a():\n  \n  if a:\n    ");
2165
2166            // preserve indent after statement
2167            append(&mut buffer, "b()\n", cx);
2168            assert_eq!(buffer.text(), "def a():\n  \n  if a:\n    b()\n    ");
2169
2170            // preserve indent after statement
2171            append(&mut buffer, "else", cx);
2172            assert_eq!(buffer.text(), "def a():\n  \n  if a:\n    b()\n    else");
2173
2174            // dedent "else""
2175            append(&mut buffer, ":", cx);
2176            assert_eq!(buffer.text(), "def a():\n  \n  if a:\n    b()\n  else:");
2177
2178            // indent lines after else
2179            append(&mut buffer, "\n", cx);
2180            assert_eq!(
2181                buffer.text(),
2182                "def a():\n  \n  if a:\n    b()\n  else:\n    "
2183            );
2184
2185            // indent after an open paren. the closing paren is not indented
2186            // because there is another token before it on the same line.
2187            append(&mut buffer, "foo(\n1)", cx);
2188            assert_eq!(
2189                buffer.text(),
2190                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n      1)"
2191            );
2192
2193            // dedent the closing paren if it is shifted to the beginning of the line
2194            let argument_ix = buffer.text().find('1').unwrap();
2195            buffer.edit(
2196                [(argument_ix..argument_ix + 1, "")],
2197                Some(AutoindentMode::EachLine),
2198                cx,
2199            );
2200            assert_eq!(
2201                buffer.text(),
2202                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n    )"
2203            );
2204
2205            // preserve indent after the close paren
2206            append(&mut buffer, "\n", cx);
2207            assert_eq!(
2208                buffer.text(),
2209                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n    )\n    "
2210            );
2211
2212            // manually outdent the last line
2213            let end_whitespace_ix = buffer.len() - 4;
2214            buffer.edit(
2215                [(end_whitespace_ix..buffer.len(), "")],
2216                Some(AutoindentMode::EachLine),
2217                cx,
2218            );
2219            assert_eq!(
2220                buffer.text(),
2221                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n    )\n"
2222            );
2223
2224            // preserve the newly reduced indentation on the next newline
2225            append(&mut buffer, "\n", cx);
2226            assert_eq!(
2227                buffer.text(),
2228                "def a():\n  \n  if a:\n    b()\n  else:\n    foo(\n    )\n\n"
2229            );
2230
2231            // reset to a for loop statement
2232            let statement = "for i in range(10):\n  print(i)\n";
2233            buffer.edit([(0..buffer.len(), statement)], None, cx);
2234
2235            // insert single line comment after each line
2236            let eol_ixs = statement
2237                .char_indices()
2238                .filter_map(|(ix, c)| if c == '\n' { Some(ix) } else { None })
2239                .collect::<Vec<usize>>();
2240            let editions = eol_ixs
2241                .iter()
2242                .enumerate()
2243                .map(|(i, &eol_ix)| (eol_ix..eol_ix, format!(" # comment {}", i + 1)))
2244                .collect::<Vec<(std::ops::Range<usize>, String)>>();
2245            buffer.edit(editions, Some(AutoindentMode::EachLine), cx);
2246            assert_eq!(
2247                buffer.text(),
2248                "for i in range(10): # comment 1\n  print(i) # comment 2\n"
2249            );
2250
2251            // reset to a simple if statement
2252            buffer.edit([(0..buffer.len(), "if a:\n  b(\n  )")], None, cx);
2253
2254            // dedent "else" on the line after a closing paren
2255            append(&mut buffer, "\n  else:\n", cx);
2256            assert_eq!(buffer.text(), "if a:\n  b(\n  )\nelse:\n  ");
2257
2258            buffer
2259        });
2260    }
2261}