3000 lines · 105.8 KB · Rust
RawDownload
use anyhow::{Context as _, ensure};use anyhow::{Result, anyhow};use async_trait::async_trait;use collections::HashMap;use futures::future::BoxFuture;use futures::lock::OwnedMutexGuard;use futures::{AsyncBufReadExt, StreamExt as _};use gpui::{App, AsyncApp, SharedString, Task};use http_client::github::{AssetKind, GitHubLspBinaryVersion, latest_github_release};use language::language_settings::language_settings;use language::{ContextLocation, DynLspInstaller, LanguageToolchainStore, LspInstaller, Symbol};use language::{ContextProvider, LspAdapter, LspAdapterDelegate};use language::{LanguageName, ManifestName, ManifestProvider, ManifestQuery};use language::{Toolchain, ToolchainList, ToolchainLister, ToolchainMetadata};use lsp::{LanguageServerBinary, Uri};use lsp::{LanguageServerBinaryOptions, LanguageServerName};use node_runtime::{NodeRuntime, VersionStrategy};use pet_core::Configuration;use pet_core::os_environment::Environment;use pet_core::python_environment::{PythonEnvironment, PythonEnvironmentKind};use pet_virtualenv::is_virtualenv_dir;use project::Fs;use project::lsp_store::language_server_settings;use semver::Version;use serde::{Deserialize, Serialize};use serde_json::{Value, json};use settings::Settings;use terminal::terminal_settings::TerminalSettings; use smol::lock::OnceCell;use std::cmp::{Ordering, Reverse};use std::env::consts;use util::command::Stdio; use util::command::new_command;use util::fs::{make_file_executable, remove_matching};use util::paths::PathStyle;use util::rel_path::RelPath; use http_client::github_download::{GithubBinaryMetadata, download_server_binary};use parking_lot::Mutex;use std::str::FromStr;use std::{    borrow::Cow,    fmt::Write,    path::{Path, PathBuf},    sync::Arc,};use task::{ShellKind, TaskTemplate, TaskTemplates, VariableName};use util::{ResultExt, maybe}; #[derive(Debug, Serialize, Deserialize)]pub(crate) struct PythonToolchainData {    #[serde(flatten)]    environment: PythonEnvironment,    #[serde(skip_serializing_if = "Option::is_none")]    activation_scripts: Option<HashMap<ShellKind, PathBuf>>,} pub(crate) struct PyprojectTomlManifestProvider; impl ManifestProvider for PyprojectTomlManifestProvider {    fn name(&self) -> ManifestName {        SharedString::new_static("pyproject.toml").into()    }     fn search(        &self,        ManifestQuery {            path,            depth,            delegate,        }: ManifestQuery,    ) -> Option<Arc<RelPath>> {        for path in path.ancestors().take(depth) {            let p = path.join(RelPath::unix("pyproject.toml").unwrap());            if delegate.exists(&p, Some(false)) {                return Some(path.into());            }        }         None    }} enum TestRunner {    UNITTEST,    PYTEST,} impl FromStr for TestRunner {    type Err = ();     fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {        match s {            "unittest" => Ok(Self::UNITTEST),            "pytest" => Ok(Self::PYTEST),            _ => Err(()),        }    }} /// Pyright assigns each completion item a `sortText` of the form `XX.YYYY.name`./// Where `XX` is the sorting category, `YYYY` is based on most recent usage,/// and `name` is the symbol name itself.////// 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),/// which - long story short - makes completion items list non-stable. Pyright probably relies on VSCode's implementation detail./// see https://github.com/microsoft/pyright/blob/95ef4e103b9b2f129c9320427e51b73ea7cf78bd/packages/pyright-internal/src/languageService/completionProvider.ts#LL2873////// upd 02.12.25:/// Decided to ignore Pyright's sortText() completely and to manually sort all entriesfn process_pyright_completions(items: &mut [lsp::CompletionItem]) {    for item in items {        let is_named_argument = item.label.ends_with('=');         let is_dunder = item.label.starts_with("__") && item.label.ends_with("__");         let visibility_priority = if is_dunder {            '3'        } else if item.label.starts_with("__") {            '2' // private non-dunder        } else if item.label.starts_with('_') {            '1' // protected        } else {            '0' // public        };         let is_external = item            .detail            .as_ref()            .is_some_and(|detail| detail == "Auto-import");         let source_priority = if is_external { '1' } else { '0' };         // Kind priority within same visibility level        let kind_priority = match item.kind {            Some(lsp::CompletionItemKind::KEYWORD) => '0',            Some(lsp::CompletionItemKind::ENUM_MEMBER) => '1',            Some(lsp::CompletionItemKind::FIELD) => '2',            Some(lsp::CompletionItemKind::PROPERTY) => '3',            Some(lsp::CompletionItemKind::VARIABLE) => '4',            Some(lsp::CompletionItemKind::CONSTANT) => '5',            Some(lsp::CompletionItemKind::METHOD) => '6',            Some(lsp::CompletionItemKind::FUNCTION) => '6',            Some(lsp::CompletionItemKind::CLASS) => '7',            Some(lsp::CompletionItemKind::MODULE) => '8',             _ => 'z',        };         // Named arguments get higher priority        let argument_priority = if is_named_argument { '0' } else { '1' };         item.sort_text = Some(format!(            "{}{}{}{}{}",            argument_priority, source_priority, visibility_priority, kind_priority, item.label        ));    }} fn label_for_pyright_completion(    item: &lsp::CompletionItem,    language: &Arc<language::Language>,) -> Option<language::CodeLabel> {    let label = &item.label;    let label_len = label.len();    let grammar = language.grammar()?;    let highlight_id = match item.kind? {        lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method"),        lsp::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function"),        lsp::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type"),        lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant"),        lsp::CompletionItemKind::VARIABLE => grammar.highlight_id_for_name("variable"),        _ => {            return None;        }    };    let mut text = label.clone();    if let Some(completion_details) = item        .label_details        .as_ref()        .and_then(|details| details.description.as_ref())    {        write!(&mut text, " {}", completion_details).ok();    }    Some(language::CodeLabel::filtered(        text,        label_len,        item.filter_text.as_deref(),        highlight_id            .map(|id| (0..label_len, id))            .into_iter()            .collect(),    ))} fn label_for_python_symbol(    symbol: &Symbol,    language: &Arc<language::Language>,) -> Option<language::CodeLabel> {    let name = &symbol.name;    let (text, filter_range, display_range) = match symbol.kind {        lsp::SymbolKind::METHOD | lsp::SymbolKind::FUNCTION => {            let text = format!("def {}():\n", name);            let filter_range = 4..4 + name.len();            let display_range = 0..filter_range.end;            (text, filter_range, display_range)        }        lsp::SymbolKind::CLASS => {            let text = format!("class {}:", name);            let filter_range = 6..6 + name.len();            let display_range = 0..filter_range.end;            (text, filter_range, display_range)        }        lsp::SymbolKind::CONSTANT => {            let text = format!("{} = 0", name);            let filter_range = 0..name.len();            let display_range = 0..filter_range.end;            (text, filter_range, display_range)        }        _ => return None,    };    Some(language::CodeLabel::new(        text[display_range.clone()].to_string(),        filter_range,        language.highlight_text(&text.as_str().into(), display_range),    ))} pub struct TyLspAdapter {    fs: Arc<dyn Fs>,} #[cfg(target_os = "macos")]impl TyLspAdapter {    const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;    const ARCH_SERVER_NAME: &str = "apple-darwin";} #[cfg(target_os = "linux")]impl TyLspAdapter {    const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;    const ARCH_SERVER_NAME: &str = "unknown-linux-gnu";} #[cfg(target_os = "freebsd")]impl TyLspAdapter {    const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;    const ARCH_SERVER_NAME: &str = "unknown-freebsd";} #[cfg(target_os = "windows")]impl TyLspAdapter {    const GITHUB_ASSET_KIND: AssetKind = AssetKind::Zip;    const ARCH_SERVER_NAME: &str = "pc-windows-msvc";} impl TyLspAdapter {    const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("ty");     pub fn new(fs: Arc<dyn Fs>) -> TyLspAdapter {        TyLspAdapter { fs }    }     fn build_asset_name() -> Result<(String, String)> {        let arch = match consts::ARCH {            "x86" => "i686",            _ => consts::ARCH,        };        let os = Self::ARCH_SERVER_NAME;        let suffix = match consts::OS {            "windows" => "zip",            _ => "tar.gz",        };        let asset_name = format!("ty-{arch}-{os}.{suffix}");        let asset_stem = format!("ty-{arch}-{os}");        Ok((asset_stem, asset_name))    }} #[async_trait(?Send)]impl LspAdapter for TyLspAdapter {    fn name(&self) -> LanguageServerName {        Self::SERVER_NAME    }     async fn label_for_completion(        &self,        item: &lsp::CompletionItem,        language: &Arc<language::Language>,    ) -> Option<language::CodeLabel> {        let label = &item.label;        let label_len = label.len();        let grammar = language.grammar()?;        let highlight_id = match item.kind? {            lsp::CompletionItemKind::METHOD => grammar.highlight_id_for_name("function.method"),            lsp::CompletionItemKind::FUNCTION => grammar.highlight_id_for_name("function"),            lsp::CompletionItemKind::CLASS => grammar.highlight_id_for_name("type"),            lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant"),            lsp::CompletionItemKind::VARIABLE => grammar.highlight_id_for_name("variable"),            _ => {                return None;            }        };         let mut text = label.clone();        if let Some(completion_details) = item            .label_details            .as_ref()            .and_then(|details| details.detail.as_ref())        {            write!(&mut text, " {}", completion_details).ok();        }         Some(language::CodeLabel::filtered(            text,            label_len,            item.filter_text.as_deref(),            highlight_id                .map(|id| (0..label_len, id))                .into_iter()                .collect(),        ))    }     async fn label_for_symbol(        &self,        symbol: &language::Symbol,        language: &Arc<language::Language>,    ) -> Option<language::CodeLabel> {        label_for_python_symbol(symbol, language)    }     async fn workspace_configuration(        self: Arc<Self>,        delegate: &Arc<dyn LspAdapterDelegate>,        toolchain: Option<Toolchain>,        _: Option<Uri>,        cx: &mut AsyncApp,    ) -> Result<Value> {        let mut ret = cx            .update(|cx| {                language_server_settings(delegate.as_ref(), &self.name(), cx)                    .and_then(|s| s.settings.clone())            })            .unwrap_or_else(|| json!({}));        if let Some(toolchain) = toolchain.and_then(|toolchain| {            serde_json::from_value::<PythonToolchainData>(toolchain.as_json).ok()        }) {            _ = maybe!({                let uri =                    url::Url::from_file_path(toolchain.environment.executable.as_ref()?).ok()?;                let sys_prefix = toolchain.environment.prefix.clone()?;                let environment = json!({                    "executable": {                        "uri": uri,                        "sysPrefix": sys_prefix                    }                });                ret.as_object_mut()?                    .entry("pythonExtension")                    .or_insert_with(|| json!({ "activeEnvironment": environment }));                Some(())            });        }        Ok(json!({"ty": ret}))    }} impl LspInstaller for TyLspAdapter {    type BinaryVersion = GitHubLspBinaryVersion;    async fn fetch_latest_server_version(        &self,        delegate: &dyn LspAdapterDelegate,        _: bool,        _: &mut AsyncApp,    ) -> Result<Self::BinaryVersion> {        let release =            latest_github_release("astral-sh/ty", true, false, delegate.http_client()).await?;        let (_, asset_name) = Self::build_asset_name()?;        let asset = release            .assets            .into_iter()            .find(|asset| asset.name == asset_name)            .with_context(|| format!("no asset found matching `{asset_name:?}`"))?;        Ok(GitHubLspBinaryVersion {            name: release.tag_name,            url: asset.browser_download_url,            digest: asset.digest,        })    }     async fn check_if_user_installed(        &self,        delegate: &dyn LspAdapterDelegate,        toolchain: Option<Toolchain>,        _: &AsyncApp,    ) -> Option<LanguageServerBinary> {        let ty_in_venv = if let Some(toolchain) = toolchain            && toolchain.language_name.as_ref() == "Python"        {            Path::new(toolchain.path.as_str())                .parent()                .map(|path| path.join("ty"))        } else {            None        };         for path in ty_in_venv.into_iter().chain(["ty".into()]) {            if let Some(ty_bin) = delegate.which(path.as_os_str()).await {                let env = delegate.shell_env().await;                return Some(LanguageServerBinary {                    path: ty_bin,                    env: Some(env),                    arguments: vec!["server".into()],                });            }        }         None    }     async fn fetch_server_binary(        &self,        latest_version: Self::BinaryVersion,        container_dir: PathBuf,        delegate: &dyn LspAdapterDelegate,    ) -> Result<LanguageServerBinary> {        let GitHubLspBinaryVersion {            name,            url,            digest: expected_digest,        } = latest_version;        let destination_path = container_dir.join(format!("ty-{name}"));         async_fs::create_dir_all(&destination_path).await?;         let server_path = match Self::GITHUB_ASSET_KIND {            AssetKind::TarGz | AssetKind::Gz => destination_path                .join(Self::build_asset_name()?.0)                .join("ty"),            AssetKind::Zip => destination_path.clone().join("ty.exe"),        };         let binary = LanguageServerBinary {            path: server_path.clone(),            env: None,            arguments: vec!["server".into()],        };         let metadata_path = destination_path.with_extension("metadata");        let metadata = GithubBinaryMetadata::read_from_file(&metadata_path)            .await            .ok();        if let Some(metadata) = metadata {            let validity_check = async || {                delegate                    .try_exec(LanguageServerBinary {                        path: server_path.clone(),                        arguments: vec!["--version".into()],                        env: None,                    })                    .await                    .inspect_err(|err| {                        log::warn!("Unable to run {server_path:?} asset, redownloading: {err:#}",)                    })            };            if let (Some(actual_digest), Some(expected_digest)) =                (&metadata.digest, &expected_digest)            {                if actual_digest == expected_digest {                    if validity_check().await.is_ok() {                        return Ok(binary);                    }                } else {                    log::info!(                        "SHA-256 mismatch for {destination_path:?} asset, downloading new asset. Expected: {expected_digest}, Got: {actual_digest}"                    );                }            } else if validity_check().await.is_ok() {                return Ok(binary);            }        }         download_server_binary(            &*delegate.http_client(),            &url,            expected_digest.as_deref(),            &destination_path,            Self::GITHUB_ASSET_KIND,        )        .await?;        make_file_executable(&server_path).await?;        remove_matching(&container_dir, |path| path != destination_path).await;        GithubBinaryMetadata::write_to_file(            &GithubBinaryMetadata {                metadata_version: 1,                digest: expected_digest,            },

Lines 1–500 of 3000.