Make python run local worktree LSPs (#18353)

Mikayla Maki and conrad created

Release Notes:

- Python: made it possible to use locally installed `pyright` if
available

---------

Co-authored-by: conrad <conrad@zed.dev>

Change summary

crates/language/src/language.rs         |  4 ++
crates/languages/src/python.rs          | 21 ++++++++++++
crates/node_runtime/src/node_runtime.rs |  3 +
crates/project/src/lsp_store.rs         | 46 +++++++++++++++++++++++++++
4 files changed, 73 insertions(+), 1 deletion(-)

Detailed changes

crates/language/src/language.rs 🔗

@@ -313,6 +313,10 @@ pub trait LspAdapterDelegate: Send + Sync {
     fn update_status(&self, language: LanguageServerName, status: LanguageServerBinaryStatus);
     async fn language_server_download_dir(&self, name: &LanguageServerName) -> Option<Arc<Path>>;
 
+    async fn npm_package_installed_version(
+        &self,
+        package_name: &str,
+    ) -> Result<Option<(PathBuf, String)>>;
     async fn which(&self, command: &OsStr) -> Option<PathBuf>;
     async fn shell_env(&self) -> HashMap<String, String>;
     async fn read_text_file(&self, path: PathBuf) -> Result<String>;

crates/languages/src/python.rs 🔗

@@ -20,6 +20,7 @@ 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";
 
 fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {
     vec![server_path.into(), "--stdio".into()]
@@ -43,6 +44,26 @@ impl LspAdapter for PythonLspAdapter {
         Self::SERVER_NAME.clone()
     }
 
+    async fn check_if_user_installed(
+        &self,
+        delegate: &dyn LspAdapterDelegate,
+        _: &AsyncAppContext,
+    ) -> Option<LanguageServerBinary> {
+        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,

crates/node_runtime/src/node_runtime.rs 🔗

@@ -177,6 +177,7 @@ impl NodeRuntime {
             "5000",
         ]);
 
+        // This is also wrong because the directory is wrong.
         self.run_npm_subcommand(directory, "install", &arguments)
             .await?;
         Ok(())
@@ -576,7 +577,7 @@ impl NodeRuntimeTrait for SystemNodeRuntime {
     }
 }
 
-async fn read_package_installed_version(
+pub async fn read_package_installed_version(
     node_module_directory: PathBuf,
     name: &str,
 ) -> Result<Option<String>> {

crates/project/src/lsp_store.rs 🔗

@@ -48,6 +48,7 @@ use lsp::{
     LspRequestFuture, MessageActionItem, MessageType, OneOf, ServerHealthStatus, ServerStatus,
     SymbolKind, TextEdit, Url, WorkDoneProgressCancelParams, WorkspaceFolder,
 };
+use node_runtime::read_package_installed_version;
 use parking_lot::{Mutex, RwLock};
 use postage::watch;
 use rand::prelude::*;
@@ -7801,6 +7802,44 @@ impl LspAdapterDelegate for LocalLspAdapterDelegate {
         task.await.unwrap_or_default()
     }
 
+    async fn npm_package_installed_version(
+        &self,
+        package_name: &str,
+    ) -> Result<Option<(PathBuf, String)>> {
+        let local_package_directory = self.worktree_root_path();
+        let node_modules_directory = local_package_directory.join("node_modules");
+
+        if let Some(version) =
+            read_package_installed_version(node_modules_directory.clone(), package_name).await?
+        {
+            return Ok(Some((node_modules_directory, version)));
+        }
+        let Some(npm) = self.which("npm".as_ref()).await else {
+            log::warn!(
+                "Failed to find npm executable for {:?}",
+                local_package_directory
+            );
+            return Ok(None);
+        };
+
+        let env = self.shell_env().await;
+        let output = smol::process::Command::new(&npm)
+            .args(["root", "-g"])
+            .envs(env)
+            .current_dir(local_package_directory)
+            .output()
+            .await?;
+        let global_node_modules =
+            PathBuf::from(String::from_utf8_lossy(&output.stdout).to_string());
+
+        if let Some(version) =
+            read_package_installed_version(global_node_modules.clone(), package_name).await?
+        {
+            return Ok(Some((global_node_modules, version)));
+        }
+        return Ok(None);
+    }
+
     #[cfg(not(target_os = "windows"))]
     async fn which(&self, command: &OsStr) -> Option<PathBuf> {
         let worktree_abs_path = self.worktree.abs_path();
@@ -7883,6 +7922,13 @@ impl LspAdapterDelegate for SshLspAdapterDelegate {
             .ok();
     }
 
+    async fn npm_package_installed_version(
+        &self,
+        _package_name: &str,
+    ) -> Result<Option<(PathBuf, String)>> {
+        Ok(None)
+    }
+
     fn http_client(&self) -> Arc<dyn HttpClient> {
         Arc::new(BlockedHttpClient)
     }