Piotr OsiewiczRust 1.85 (#25272)e4e758d1 year agoHistory
1140 lines · 40.1 KB · Rust
RawDownload
use anyhow::ensure;use anyhow::{anyhow, Result};use async_trait::async_trait;use collections::HashMap;use gpui::{App, Task};use gpui::{AsyncApp, SharedString};use language::language_settings::language_settings;use language::LanguageName;use language::LanguageToolchainStore;use language::Toolchain;use language::ToolchainList;use language::ToolchainLister;use language::{ContextProvider, LspAdapter, LspAdapterDelegate};use lsp::LanguageServerBinary;use lsp::LanguageServerName;use node_runtime::NodeRuntime;use pet_core::os_environment::Environment;use pet_core::python_environment::PythonEnvironmentKind;use pet_core::Configuration;use project::lsp_store::language_server_settings;use project::Fs;use serde_json::{json, Value};use smol::lock::OnceCell;use std::cmp::Ordering; use std::str::FromStr;use std::sync::Mutex;use std::{    any::Any,    borrow::Cow,    ffi::OsString,    path::{Path, PathBuf},    sync::Arc,};use task::{TaskTemplate, TaskTemplates, VariableName};use util::ResultExt; const SERVER_PATH: &str = "node_modules/pyright/langserver.index.js";const NODE_MODULE_RELATIVE_SERVER_PATH: &str = "pyright/langserver.index.js"; 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(()),        }    }} fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {    vec![server_path.into(), "--stdio".into()]} pub struct PythonLspAdapter {    node: NodeRuntime,} impl PythonLspAdapter {    const SERVER_NAME: LanguageServerName = LanguageServerName::new_static("pyright");     pub fn new(node: NodeRuntime) -> Self {        PythonLspAdapter { node }    }} #[async_trait(?Send)]impl LspAdapter for PythonLspAdapter {    fn name(&self) -> LanguageServerName {        Self::SERVER_NAME.clone()    }     async fn check_if_user_installed(        &self,        delegate: &dyn LspAdapterDelegate,        _: Arc<dyn LanguageToolchainStore>,        _: &AsyncApp,    ) -> Option<LanguageServerBinary> {        if let Some(pyright_bin) = delegate.which("pyright-langserver".as_ref()).await {            let env = delegate.shell_env().await;            Some(LanguageServerBinary {                path: pyright_bin,                env: Some(env),                arguments: vec!["--stdio".into()],            })        } else {            let node = delegate.which("node".as_ref()).await?;            let (node_modules_path, _) = delegate                .npm_package_installed_version(Self::SERVER_NAME.as_ref())                .await                .log_err()??;             let path = node_modules_path.join(NODE_MODULE_RELATIVE_SERVER_PATH);             Some(LanguageServerBinary {                path: node,                env: None,                arguments: server_binary_arguments(&path),            })        }    }     async fn fetch_latest_server_version(        &self,        _: &dyn LspAdapterDelegate,    ) -> Result<Box<dyn 'static + Any + Send>> {        Ok(Box::new(            self.node                .npm_package_latest_version(Self::SERVER_NAME.as_ref())                .await?,        ) as Box<_>)    }     async fn fetch_server_binary(        &self,        latest_version: Box<dyn 'static + Send + Any>,        container_dir: PathBuf,        _: &dyn LspAdapterDelegate,    ) -> Result<LanguageServerBinary> {        let latest_version = latest_version.downcast::<String>().unwrap();        let server_path = container_dir.join(SERVER_PATH);         self.node            .npm_install_packages(                &container_dir,                &[(Self::SERVER_NAME.as_ref(), latest_version.as_str())],            )            .await?;         Ok(LanguageServerBinary {            path: self.node.binary_path().await?,            env: None,            arguments: server_binary_arguments(&server_path),        })    }     async fn check_if_version_installed(        &self,        version: &(dyn 'static + Send + Any),        container_dir: &PathBuf,        _: &dyn LspAdapterDelegate,    ) -> Option<LanguageServerBinary> {        let version = version.downcast_ref::<String>().unwrap();        let server_path = container_dir.join(SERVER_PATH);         let should_install_language_server = self            .node            .should_install_npm_package(                Self::SERVER_NAME.as_ref(),                &server_path,                &container_dir,                &version,            )            .await;         if should_install_language_server {            None        } else {            Some(LanguageServerBinary {                path: self.node.binary_path().await.ok()?,                env: None,                arguments: server_binary_arguments(&server_path),            })        }    }     async fn cached_server_binary(        &self,        container_dir: PathBuf,        _: &dyn LspAdapterDelegate,    ) -> Option<LanguageServerBinary> {        get_cached_server_binary(container_dir, &self.node).await    }     async fn process_completions(&self, items: &mut [lsp::CompletionItem]) {        // 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.        //        // Because the symbol name is included, there generally are not ties when        // sorting by the `sortText`, so the symbol's fuzzy match score is not taken        // into account. Here, we remove the symbol name from the sortText in order        // to allow our own fuzzy score to be used to break ties.        //        // see https://github.com/microsoft/pyright/blob/95ef4e103b9b2f129c9320427e51b73ea7cf78bd/packages/pyright-internal/src/languageService/completionProvider.ts#LL2873        for item in items {            let Some(sort_text) = &mut item.sort_text else {                continue;            };            let mut parts = sort_text.split('.');            let Some(first) = parts.next() else { continue };            let Some(second) = parts.next() else { continue };            let Some(_) = parts.next() else { continue };            sort_text.replace_range(first.len() + second.len() + 1.., "");        }    }     async fn label_for_completion(        &self,        item: &lsp::CompletionItem,        language: &Arc<language::Language>,    ) -> Option<language::CodeLabel> {        let label = &item.label;        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")?,            _ => return None,        };        Some(language::CodeLabel {            text: label.clone(),            runs: vec![(0..label.len(), highlight_id)],            filter_range: 0..label.len(),        })    }     async fn label_for_symbol(        &self,        name: &str,        kind: lsp::SymbolKind,        language: &Arc<language::Language>,    ) -> Option<language::CodeLabel> {        let (text, filter_range, display_range) = match 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 {            runs: language.highlight_text(&text.as_str().into(), display_range.clone()),            text: text[display_range].to_string(),            filter_range,        })    }     async fn workspace_configuration(        self: Arc<Self>,        _: &dyn Fs,        adapter: &Arc<dyn LspAdapterDelegate>,        toolchains: Arc<dyn LanguageToolchainStore>,        cx: &mut AsyncApp,    ) -> Result<Value> {        let toolchain = toolchains            .active_toolchain(adapter.worktree_id(), LanguageName::new("Python"), cx)            .await;        cx.update(move |cx| {            let mut user_settings =                language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)                    .and_then(|s| s.settings.clone())                    .unwrap_or_default();             // If python.pythonPath is not set in user config, do so using our toolchain picker.            if let Some(toolchain) = toolchain {                if user_settings.is_null() {                    user_settings = Value::Object(serde_json::Map::default());                }                let object = user_settings.as_object_mut().unwrap();                if let Some(python) = object                    .entry("python")                    .or_insert(Value::Object(serde_json::Map::default()))                    .as_object_mut()                {                    python                        .entry("pythonPath")                        .or_insert(Value::String(toolchain.path.into()));                }            }            user_settings        })    }} async fn get_cached_server_binary(    container_dir: PathBuf,    node: &NodeRuntime,) -> Option<LanguageServerBinary> {    let server_path = container_dir.join(SERVER_PATH);    if server_path.exists() {        Some(LanguageServerBinary {            path: node.binary_path().await.log_err()?,            env: None,            arguments: server_binary_arguments(&server_path),        })    } else {        log::error!("missing executable in directory {:?}", server_path);        None    }} pub(crate) struct PythonContextProvider; const PYTHON_TEST_TARGET_TASK_VARIABLE: VariableName =    VariableName::Custom(Cow::Borrowed("PYTHON_TEST_TARGET")); const PYTHON_ACTIVE_TOOLCHAIN_PATH: VariableName =    VariableName::Custom(Cow::Borrowed("PYTHON_ACTIVE_ZED_TOOLCHAIN"));impl ContextProvider for PythonContextProvider {    fn build_context(        &self,        variables: &task::TaskVariables,        location: &project::Location,        _: Option<HashMap<String, String>>,        toolchains: Arc<dyn LanguageToolchainStore>,        cx: &mut gpui::App,    ) -> Task<Result<task::TaskVariables>> {        let test_target = {            let test_runner = selected_test_runner(location.buffer.read(cx).file(), cx);             let runner = match test_runner {                TestRunner::UNITTEST => self.build_unittest_target(variables),                TestRunner::PYTEST => self.build_pytest_target(variables),            };            runner        };         let worktree_id = location.buffer.read(cx).file().map(|f| f.worktree_id(cx));        cx.spawn(move |mut cx| async move {            let active_toolchain = if let Some(worktree_id) = worktree_id {                toolchains                    .active_toolchain(worktree_id, "Python".into(), &mut cx)                    .await                    .map_or_else(                        || "python3".to_owned(),                        |toolchain| format!("\"{}\"", toolchain.path),                    )            } else {                String::from("python3")            };            let toolchain = (PYTHON_ACTIVE_TOOLCHAIN_PATH, active_toolchain);            Ok(task::TaskVariables::from_iter([test_target?, toolchain]))        })    }     fn associated_tasks(        &self,        file: Option<Arc<dyn language::File>>,        cx: &App,    ) -> Option<TaskTemplates> {        let test_runner = selected_test_runner(file.as_ref(), cx);         let mut tasks = vec![            // Execute a selection            TaskTemplate {                label: "execute selection".to_owned(),                command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),                args: vec![                    "-c".to_owned(),                    VariableName::SelectedText.template_value_with_whitespace(),                ],                ..TaskTemplate::default()            },            // Execute an entire file            TaskTemplate {                label: format!("run '{}'", VariableName::File.template_value()),                command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),                args: vec![VariableName::File.template_value_with_whitespace()],                ..TaskTemplate::default()            },        ];         tasks.extend(match test_runner {            TestRunner::UNITTEST => {                [                    // Run tests for an entire file                    TaskTemplate {                        label: format!("unittest '{}'", VariableName::File.template_value()),                        command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),                        args: vec![                            "-m".to_owned(),                            "unittest".to_owned(),                            VariableName::File.template_value_with_whitespace(),                        ],                        ..TaskTemplate::default()                    },                    // Run test(s) for a specific target within a file                    TaskTemplate {                        label: "unittest $ZED_CUSTOM_PYTHON_TEST_TARGET".to_owned(),                        command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),                        args: vec![                            "-m".to_owned(),                            "unittest".to_owned(),                            PYTHON_TEST_TARGET_TASK_VARIABLE.template_value_with_whitespace(),                        ],                        tags: vec![                            "python-unittest-class".to_owned(),                            "python-unittest-method".to_owned(),                        ],                        ..TaskTemplate::default()                    },                ]            }            TestRunner::PYTEST => {                [                    // Run tests for an entire file                    TaskTemplate {                        label: format!("pytest '{}'", VariableName::File.template_value()),                        command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),                        args: vec![                            "-m".to_owned(),                            "pytest".to_owned(),                            VariableName::File.template_value_with_whitespace(),                        ],                        ..TaskTemplate::default()                    },                    // Run test(s) for a specific target within a file                    TaskTemplate {                        label: "pytest $ZED_CUSTOM_PYTHON_TEST_TARGET".to_owned(),                        command: PYTHON_ACTIVE_TOOLCHAIN_PATH.template_value(),                        args: vec![                            "-m".to_owned(),                            "pytest".to_owned(),                            PYTHON_TEST_TARGET_TASK_VARIABLE.template_value_with_whitespace(),                        ],                        tags: vec![                            "python-pytest-class".to_owned(),                            "python-pytest-method".to_owned(),                        ],                        ..TaskTemplate::default()                    },                ]            }        });         Some(TaskTemplates(tasks))    }} fn selected_test_runner(location: Option<&Arc<dyn language::File>>, cx: &App) -> TestRunner {    const TEST_RUNNER_VARIABLE: &str = "TEST_RUNNER";    language_settings(Some(LanguageName::new("Python")), location, cx)        .tasks        .variables        .get(TEST_RUNNER_VARIABLE)        .and_then(|val| TestRunner::from_str(val).ok())        .unwrap_or(TestRunner::PYTEST)} impl PythonContextProvider {    fn build_unittest_target(        &self,        variables: &task::TaskVariables,    ) -> Result<(VariableName, String)> {        let python_module_name = python_module_name_from_relative_path(            variables.get(&VariableName::RelativeFile).unwrap_or(""),        );         let unittest_class_name =            variables.get(&VariableName::Custom(Cow::Borrowed("_unittest_class_name")));         let unittest_method_name = variables.get(&VariableName::Custom(Cow::Borrowed(            "_unittest_method_name",        )));         let unittest_target_str = match (unittest_class_name, unittest_method_name) {            (Some(class_name), Some(method_name)) => {                format!("{}.{}.{}", python_module_name, class_name, method_name)            }            (Some(class_name), None) => format!("{}.{}", python_module_name, class_name),            (None, None) => python_module_name,            (None, Some(_)) => return Ok((VariableName::Custom(Cow::Borrowed("")), String::new())), // should never happen, a TestCase class is the unit of testing        };         let unittest_target = (            PYTHON_TEST_TARGET_TASK_VARIABLE.clone(),            unittest_target_str,        );         Ok(unittest_target)    }     fn build_pytest_target(        &self,        variables: &task::TaskVariables,    ) -> Result<(VariableName, String)> {        let file_path = variables

Lines 1–500 of 1140.