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