From dc557e16478ba0fe570cef208185ac104842bdde Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 17 Jul 2023 11:34:22 +0200 Subject: [PATCH] Add scaffolding of php language server --- crates/language/src/language.rs | 23 +++- crates/lsp/src/lsp.rs | 7 +- crates/project/src/project.rs | 2 +- crates/zed/src/languages.rs | 9 +- crates/zed/src/languages/php.rs | 203 ++++++++++++++++++++++++++++++++ 5 files changed, 232 insertions(+), 12 deletions(-) create mode 100644 crates/zed/src/languages/php.rs diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index e8450344b8..af6a6e5045 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -262,24 +262,31 @@ pub trait LspAdapter: 'static + Send + Sync { container_dir: PathBuf, ) -> Option; - async fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} + async fn process_diagnostics(&self, d: &mut lsp::PublishDiagnosticsParams) { + dbg!(d); + } - async fn process_completion(&self, _: &mut lsp::CompletionItem) {} + async fn process_completion(&self, d: &mut lsp::CompletionItem) { + dbg!(d); + } async fn label_for_completion( &self, - _: &lsp::CompletionItem, + item: &lsp::CompletionItem, _: &Arc, ) -> Option { + dbg!(item); None } async fn label_for_symbol( &self, - _: &str, - _: lsp::SymbolKind, + name: &str, + kind: lsp::SymbolKind, _: &Arc, ) -> Option { + dbg!(name); + dbg!(kind); None } @@ -321,7 +328,7 @@ pub struct CodeLabel { pub filter_range: Range, } -#[derive(Clone, Deserialize)] +#[derive(Clone, Deserialize, Debug)] pub struct LanguageConfig { pub name: Arc, pub path_suffixes: Vec, @@ -810,6 +817,7 @@ impl LanguageRegistry { .spawn(async move { let id = language.id; let queries = (language.get_queries)(&language.path); + dbg!(&language.path); let language = Language::new(language.config, Some(language.grammar)) .with_lsp_adapters(language.lsp_adapters) @@ -819,9 +827,11 @@ impl LanguageRegistry { Ok(language) => { let language = Arc::new(language); let mut state = this.state.write(); + state.add(language.clone()); state.mark_language_loaded(id); if let Some(mut txs) = state.loading_languages.remove(&id) { + dbg!(&name); for tx in txs.drain(..) { let _ = tx.send(Ok(language.clone())); } @@ -829,6 +839,7 @@ impl LanguageRegistry { } Err(err) => { log::error!("failed to load language {name} - {err}"); + dbg!(&name); let mut state = this.state.write(); state.mark_language_loaded(id); if let Some(mut txs) = state.loading_languages.remove(&id) { diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index a01f6e8a49..78c858a90c 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -151,16 +151,17 @@ impl LanguageServer { let stdin = server.stdin.take().unwrap(); let stout = server.stdout.take().unwrap(); let mut server = Self::new_internal( - server_id, + server_id.clone(), stdin, stout, Some(server), root_path, code_action_kinds, cx, - |notification| { + move |notification| { log::info!( - "unhandled notification {}:\n{}", + "{} unhandled notification {}:\n{}", + server_id, notification.method, serde_json::to_string_pretty( ¬ification diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 81db0c7ed7..666503e210 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2680,6 +2680,7 @@ impl Project { key: (WorktreeId, LanguageServerName), cx: &mut AsyncAppContext, ) -> Result>> { + dbg!(language.name()); let setup = Self::setup_pending_language_server( this, initialization_options, @@ -2694,7 +2695,6 @@ impl Project { Some(language_server) => language_server, None => return Ok(None), }; - let this = match this.upgrade(cx) { Some(this) => this, None => return Err(anyhow!("failed to upgrade project handle")), diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 81edd92d37..2a29fd537b 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -13,6 +13,7 @@ mod json; #[cfg(feature = "plugin_runtime")] mod language_plugin; mod lua; +mod php; mod python; mod ruby; mod rust; @@ -135,9 +136,13 @@ pub fn init(languages: Arc, node_runtime: Arc) { language( "yaml", tree_sitter_yaml::language(), - vec![Arc::new(yaml::YamlLspAdapter::new(node_runtime))], + vec![Arc::new(yaml::YamlLspAdapter::new(node_runtime.clone()))], + ); + language( + "php", + tree_sitter_php::language(), + vec![Arc::new(php::IntelephenseLspAdapter::new(node_runtime))], ); - language("php", tree_sitter_php::language(), vec![]); } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/zed/src/languages/php.rs b/crates/zed/src/languages/php.rs new file mode 100644 index 0000000000..b42ad18a53 --- /dev/null +++ b/crates/zed/src/languages/php.rs @@ -0,0 +1,203 @@ +use anyhow::{anyhow, Result}; +use async_compression::futures::bufread::GzipDecoder; +use async_tar::Archive; +use async_trait::async_trait; +use futures::{future::BoxFuture, FutureExt}; +use gpui::AppContext; +use language::{LanguageServerName, LspAdapter, LspAdapterDelegate}; +use lsp::{CodeActionKind, LanguageServerBinary}; +use node_runtime::NodeRuntime; +use serde_json::{json, Value}; +use smol::{fs, io::BufReader, stream::StreamExt}; +use std::{ + any::Any, + ffi::OsString, + future, + path::{Path, PathBuf}, + sync::Arc, +}; +use util::ResultExt; + +fn intelephense_server_binary_arguments(server_path: &Path) -> Vec { + vec![server_path.into(), "--stdio".into()] +} + +pub struct IntelephenseVersion(String); + +pub struct IntelephenseLspAdapter { + node: Arc, +} + +impl IntelephenseLspAdapter { + const SERVER_PATH: &'static str = "node_modules/intelephense/lib/intelephense.js"; + + #[allow(unused)] + pub fn new(node: Arc) -> Self { + Self { node } + } +} + +#[async_trait] +impl LspAdapter for IntelephenseLspAdapter { + async fn name(&self) -> LanguageServerName { + LanguageServerName("intelephense".into()) + } + + async fn fetch_latest_server_version( + &self, + _delegate: &dyn LspAdapterDelegate, + ) -> Result> { + // At the time of writing the latest vscode-eslint release was released in 2020 and requires + // special custom LSP protocol extensions be handled to fully initialize. Download the latest + // prerelease instead to sidestep this issue + dbg!("Strarting fetching server binary version"); + Ok(Box::new(IntelephenseVersion( + self.node.npm_package_latest_version("intelephense").await?, + )) as Box<_>) + } + + async fn fetch_server_binary( + &self, + version: Box, + container_dir: PathBuf, + _delegate: &dyn LspAdapterDelegate, + ) -> Result { + dbg!("Strarting fetching server binary"); + let version = version.downcast::().unwrap(); + let server_path = container_dir.join(Self::SERVER_PATH); + + if fs::metadata(&server_path).await.is_err() { + self.node + .npm_install_packages(&container_dir, [("intelephense", version.0.as_str())]) + .await?; + } + dbg!("Fetched server binary"); + Ok(LanguageServerBinary { + path: dbg!(self.node.binary_path().await)?, + arguments: intelephense_server_binary_arguments(&server_path), + }) + } + + async fn cached_server_binary( + &self, + container_dir: PathBuf, + _: &dyn LspAdapterDelegate, + ) -> Option { + dbg!("cached_server_binary"); + get_cached_server_binary(container_dir, &self.node).await + } + + async fn installation_test_binary( + &self, + container_dir: PathBuf, + ) -> Option { + dbg!("installation_test_binary"); + get_cached_server_binary(container_dir, &self.node).await + } + + async fn label_for_completion( + &self, + _item: &lsp::CompletionItem, + _language: &Arc, + ) -> Option { + dbg!(_item.kind); + None + } + + async fn initialization_options(&self) -> Option { + dbg!("init_options"); + None + } +} + +async fn get_cached_server_binary( + container_dir: PathBuf, + node: &NodeRuntime, +) -> Option { + (|| async move { + let mut last_version_dir = None; + let mut entries = fs::read_dir(&container_dir).await?; + while let Some(entry) = entries.next().await { + let entry = entry?; + if entry.file_type().await?.is_dir() { + last_version_dir = Some(entry.path()); + } + } + let last_version_dir = last_version_dir.ok_or_else(|| anyhow!("no cached binary"))?; + let server_path = last_version_dir.join(IntelephenseLspAdapter::SERVER_PATH); + if server_path.exists() { + Ok(LanguageServerBinary { + path: node.binary_path().await?, + arguments: intelephense_server_binary_arguments(&server_path), + }) + } else { + Err(anyhow!( + "missing executable in directory {:?}", + last_version_dir + )) + } + })() + .await + .log_err() +} + +#[cfg(test)] +mod tests { + use gpui::TestAppContext; + use unindent::Unindent; + + #[gpui::test] + async fn test_outline(cx: &mut TestAppContext) { + let language = crate::languages::language("php", tree_sitter_php::language(), None).await; + + /*let text = r#" + function a() { + // local variables are omitted + let a1 = 1; + // all functions are included + async function a2() {} + } + // top-level variables are included + let b: C + function getB() {} + // exported variables are included + export const d = e; + "# + .unindent();*/ + let text = r#" + function a() { + // local variables are omitted + $a1 = 1; + // all functions are included + function a2() {} + } + class Foo {} + "# + .unindent(); + let buffer = + cx.add_model(|cx| language::Buffer::new(0, text, cx).with_language(language, cx)); + let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None).unwrap()); + panic!( + "{:?}", + outline + .items + .iter() + .map(|item| (item.text.as_str(), item.depth)) + .collect::>() + ); + assert_eq!( + outline + .items + .iter() + .map(|item| (item.text.as_str(), item.depth)) + .collect::>(), + &[ + ("function a()", 0), + ("async function a2()", 1), + ("let b", 0), + ("function getB()", 0), + ("const d", 0), + ] + ); + } +}