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