diff --git a/Cargo.lock b/Cargo.lock index 3db82b3cb3..276fbf1525 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5428,6 +5428,16 @@ dependencies = [ "tree-sitter", ] +[[package]] +name = "tree-sitter-typescript" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e8ed0ecb931cdff13c6a13f45ccd615156e2779d9ffb0395864e05505e6e86d" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "ttf-parser" version = "0.9.0" @@ -6039,6 +6049,7 @@ dependencies = [ "tree-sitter-json", "tree-sitter-markdown", "tree-sitter-rust", + "tree-sitter-typescript", "unindent", "url", "util", diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 89eb2863fc..1135c9b260 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2593,6 +2593,8 @@ impl Editor { } } } + } else { + return Ok(()); } let mut ranges_to_highlight = Vec::new(); @@ -6451,13 +6453,12 @@ pub fn styled_runs_for_code_label<'a>( #[cfg(test)] mod tests { - use super::*; use gpui::{ geometry::rect::RectF, platform::{WindowBounds, WindowOptions}, }; - use language::{LanguageConfig, LanguageServerConfig}; + use language::{FakeLspAdapter, LanguageConfig}; use lsp::FakeLanguageServer; use project::FakeFs; use smol::stream::StreamExt; @@ -8893,26 +8894,27 @@ mod tests { cx.foreground().forbid_parking(); cx.update(populate_settings); - let (mut language_server_config, mut fake_servers) = LanguageServerConfig::fake(); - language_server_config.set_fake_capabilities(lsp::ServerCapabilities { - document_formatting_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }); - let language = Arc::new(Language::new( + let mut language = Language::new( LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), ..Default::default() }, Some(tree_sitter_rust::language()), - )); + ); + let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + document_formatting_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + }); let fs = FakeFs::new(cx.background().clone()); fs.insert_file("/file.rs", Default::default()).await; let project = Project::test(fs, cx); - project.update(cx, |project, _| project.languages().add(language)); + project.update(cx, |project, _| project.languages().add(Arc::new(language))); let worktree_id = project .update(cx, |project, cx| { @@ -8926,7 +8928,9 @@ mod tests { .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx)) .await .unwrap(); - let mut fake_server = fake_servers.next().await.unwrap(); + + cx.foreground().start_waiting(); + let fake_server = fake_servers.next().await.unwrap(); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx)); @@ -8940,13 +8944,14 @@ mod tests { params.text_document.uri, lsp::Url::from_file_path("/file.rs").unwrap() ); - Some(vec![lsp::TextEdit::new( + Ok(Some(vec![lsp::TextEdit::new( lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), ", ".to_string(), - )]) + )])) }) .next() .await; + cx.foreground().start_waiting(); save.await.unwrap(); assert_eq!( editor.read_with(cx, |editor, cx| editor.text(cx)), @@ -8968,6 +8973,7 @@ mod tests { }); let save = cx.update(|cx| editor.save(project.clone(), cx)); cx.foreground().advance_clock(items::FORMAT_TIMEOUT); + cx.foreground().start_waiting(); save.await.unwrap(); assert_eq!( editor.read_with(cx, |editor, cx| editor.text(cx)), @@ -8980,23 +8986,24 @@ mod tests { async fn test_completion(cx: &mut gpui::TestAppContext) { cx.update(populate_settings); - let (mut language_server_config, mut fake_servers) = LanguageServerConfig::fake(); - language_server_config.set_fake_capabilities(lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string(), ":".to_string()]), - ..Default::default() - }), - ..Default::default() - }); - let language = Arc::new(Language::new( + let mut language = Language::new( LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), ..Default::default() }, Some(tree_sitter_rust::language()), - )); + ); + let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string(), ":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + }); let text = " one @@ -9009,7 +9016,7 @@ mod tests { fs.insert_file("/file.rs", text).await; let project = Project::test(fs, cx); - project.update(cx, |project, _| project.languages().add(language)); + project.update(cx, |project, _| project.languages().add(Arc::new(language))); let worktree_id = project .update(cx, |project, cx| { @@ -9168,7 +9175,7 @@ mod tests { params.text_document_position.position, lsp::Position::new(position.row, position.column) ); - Some(lsp::CompletionResponse::Array( + Ok(Some(lsp::CompletionResponse::Array( completions .iter() .map(|(range, new_text)| lsp::CompletionItem { @@ -9183,7 +9190,7 @@ mod tests { ..Default::default() }) .collect(), - )) + ))) } }) .next() @@ -9197,7 +9204,7 @@ mod tests { fake.handle_request::(move |_, _| { let edit = edit.clone(); async move { - lsp::CompletionItem { + Ok(lsp::CompletionItem { additional_text_edits: edit.map(|(range, new_text)| { vec![lsp::TextEdit::new( lsp::Range::new( @@ -9208,7 +9215,7 @@ mod tests { )] }), ..Default::default() - } + }) } }) .next() diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index f2c9d209b1..7294262057 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1,8 +1,7 @@ pub use crate::{ diagnostic_set::DiagnosticSet, highlight_map::{HighlightId, HighlightMap}, - proto, BracketPair, Grammar, Language, LanguageConfig, LanguageRegistry, LanguageServerConfig, - PLAIN_TEXT, + proto, BracketPair, Grammar, Language, LanguageConfig, LanguageRegistry, PLAIN_TEXT, }; use crate::{ diagnostic_set::{DiagnosticEntry, DiagnosticGroup}, diff --git a/crates/language/src/diagnostic_set.rs b/crates/language/src/diagnostic_set.rs index 490789a8c8..51c921e61c 100644 --- a/crates/language/src/diagnostic_set.rs +++ b/crates/language/src/diagnostic_set.rs @@ -34,6 +34,23 @@ pub struct Summary { count: usize, } +impl DiagnosticEntry { + // Used to provide diagnostic context to lsp codeAction request + pub fn to_lsp_diagnostic_stub(&self) -> lsp::Diagnostic { + let code = self + .diagnostic + .code + .clone() + .map(lsp::NumberOrString::String); + + lsp::Diagnostic { + code, + severity: Some(self.diagnostic.severity), + ..Default::default() + } + } +} + impl DiagnosticSet { pub fn from_sorted_entries(iter: I, buffer: &text::BufferSnapshot) -> Self where diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 8e5698a614..322fd19b9e 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -7,8 +7,8 @@ pub mod proto; mod tests; use anyhow::{anyhow, Context, Result}; -use client::http::{self, HttpClient}; -use collections::HashSet; +use client::http::HttpClient; +use collections::HashMap; use futures::{ future::{BoxFuture, Shared}, FutureExt, TryFutureExt, @@ -20,6 +20,7 @@ use parking_lot::{Mutex, RwLock}; use serde::Deserialize; use serde_json::Value; use std::{ + any::Any, cell::RefCell, ops::Range, path::{Path, PathBuf}, @@ -51,7 +52,6 @@ lazy_static! { brackets: Default::default(), autoclose_before: Default::default(), line_comment: None, - language_server: None, }, None, )); @@ -61,20 +61,18 @@ pub trait ToLspPosition { fn to_lsp_position(self) -> lsp::Position; } -pub struct LspBinaryVersion { - pub name: String, - pub url: Option, -} +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct LanguageServerName(pub Arc); pub trait LspAdapter: 'static + Send + Sync { - fn name(&self) -> &'static str; + fn name(&self) -> LanguageServerName; fn fetch_latest_server_version( &self, http: Arc, - ) -> BoxFuture<'static, Result>; + ) -> BoxFuture<'static, Result>>; fn fetch_server_binary( &self, - version: LspBinaryVersion, + version: Box, http: Arc, container_dir: PathBuf, ) -> BoxFuture<'static, Result>; @@ -96,6 +94,14 @@ pub trait LspAdapter: 'static + Send + Sync { fn initialization_options(&self) -> Option { None } + + fn disk_based_diagnostic_sources(&self) -> &'static [&'static str] { + Default::default() + } + + fn disk_based_diagnostics_progress_token(&self) -> Option<&'static str> { + None + } } #[derive(Clone, Debug, PartialEq, Eq)] @@ -113,7 +119,6 @@ pub struct LanguageConfig { #[serde(default)] pub autoclose_before: String, pub line_comment: Option, - pub language_server: Option, } impl Default for LanguageConfig { @@ -124,25 +129,17 @@ impl Default for LanguageConfig { brackets: Default::default(), autoclose_before: Default::default(), line_comment: Default::default(), - language_server: Default::default(), } } } -#[derive(Default, Deserialize)] -pub struct LanguageServerConfig { - pub disk_based_diagnostic_sources: HashSet, - pub disk_based_diagnostics_progress_token: Option, - #[cfg(any(test, feature = "test-support"))] - #[serde(skip)] - fake_config: Option, -} - #[cfg(any(test, feature = "test-support"))] -struct FakeLanguageServerConfig { - servers_tx: mpsc::UnboundedSender, - capabilities: lsp::ServerCapabilities, - initializer: Option>, +pub struct FakeLspAdapter { + pub name: &'static str, + pub capabilities: lsp::ServerCapabilities, + pub initializer: Option>, + pub disk_based_diagnostics_progress_token: Option<&'static str>, + pub disk_based_diagnostics_sources: &'static [&'static str], } #[derive(Clone, Debug, Deserialize)] @@ -157,7 +154,12 @@ pub struct Language { pub(crate) config: LanguageConfig, pub(crate) grammar: Option>, pub(crate) adapter: Option>, - lsp_binary_path: Mutex>>>>>, + + #[cfg(any(test, feature = "test-support"))] + fake_adapter: Option<( + mpsc::UnboundedSender, + Arc, + )>, } pub struct Grammar { @@ -184,6 +186,12 @@ pub struct LanguageRegistry { lsp_binary_statuses_tx: async_broadcast::Sender<(Arc, LanguageServerBinaryStatus)>, lsp_binary_statuses_rx: async_broadcast::Receiver<(Arc, LanguageServerBinaryStatus)>, login_shell_env_loaded: Shared>, + lsp_binary_paths: Mutex< + HashMap< + LanguageServerName, + Shared>>>, + >, + >, } impl LanguageRegistry { @@ -195,6 +203,7 @@ impl LanguageRegistry { lsp_binary_statuses_tx, lsp_binary_statuses_rx, login_shell_env_loaded: login_shell_env_loaded.shared(), + lsp_binary_paths: Default::default(), } } @@ -244,7 +253,7 @@ impl LanguageRegistry { } pub fn start_language_server( - &self, + self: &Arc, server_id: usize, language: Arc, root_path: Arc, @@ -252,34 +261,20 @@ impl LanguageRegistry { cx: &mut MutableAppContext, ) -> Option>> { #[cfg(any(test, feature = "test-support"))] - if language - .config - .language_server - .as_ref() - .and_then(|config| config.fake_config.as_ref()) - .is_some() - { + if language.fake_adapter.is_some() { let language = language.clone(); - return Some(cx.spawn(|mut cx| async move { - let fake_config = language - .config - .language_server - .as_ref() - .unwrap() - .fake_config - .as_ref() - .unwrap(); - let (server, mut fake_server) = cx.update(|cx| { - lsp::LanguageServer::fake_with_capabilities( - fake_config.capabilities.clone(), - cx, - ) - }); - if let Some(initializer) = &fake_config.initializer { + return Some(cx.spawn(|cx| async move { + let (servers_tx, fake_adapter) = language.fake_adapter.as_ref().unwrap(); + let (server, mut fake_server) = lsp::LanguageServer::fake_with_capabilities( + fake_adapter.capabilities.clone(), + cx.clone(), + ); + + if let Some(initializer) = &fake_adapter.initializer { initializer(&mut fake_server); } - let servers_tx = fake_config.servers_tx.clone(); + let servers_tx = servers_tx.clone(); cx.background() .spawn(async move { fake_server @@ -298,16 +293,17 @@ impl LanguageRegistry { .ok_or_else(|| anyhow!("language server download directory has not been assigned")) .log_err()?; + let this = self.clone(); let adapter = language.adapter.clone()?; - let background = cx.background().clone(); let lsp_binary_statuses = self.lsp_binary_statuses_tx.clone(); let login_shell_env_loaded = self.login_shell_env_loaded.clone(); - Some(cx.background().spawn(async move { + Some(cx.spawn(|cx| async move { login_shell_env_loaded.await; - let server_binary_path = language - .lsp_binary_path + let server_binary_path = this + .lsp_binary_paths .lock() - .get_or_insert_with(|| { + .entry(adapter.name()) + .or_insert_with(|| { get_server_binary_path( adapter.clone(), language.clone(), @@ -329,8 +325,7 @@ impl LanguageRegistry { &server_binary_path, server_args, &root_path, - adapter.initialization_options(), - background, + cx, )?; Ok(server) })) @@ -350,7 +345,7 @@ async fn get_server_binary_path( download_dir: Arc, statuses: async_broadcast::Sender<(Arc, LanguageServerBinaryStatus)>, ) -> Result { - let container_dir = download_dir.join(adapter.name()); + let container_dir = download_dir.join(adapter.name().0.as_ref()); if !container_dir.exists() { smol::fs::create_dir_all(&container_dir) .await @@ -423,10 +418,16 @@ impl Language { }) }), adapter: None, - lsp_binary_path: Default::default(), + + #[cfg(any(test, feature = "test-support"))] + fake_adapter: None, } } + pub fn lsp_adapter(&self) -> Option> { + self.adapter.clone() + } + pub fn with_highlights_query(mut self, source: &str) -> Result { let grammar = self .grammar @@ -467,11 +468,23 @@ impl Language { Ok(self) } - pub fn with_lsp_adapter(mut self, lsp_adapter: impl LspAdapter) -> Self { - self.adapter = Some(Arc::new(lsp_adapter)); + pub fn with_lsp_adapter(mut self, lsp_adapter: Arc) -> Self { + self.adapter = Some(lsp_adapter); self } + #[cfg(any(test, feature = "test-support"))] + pub fn set_fake_lsp_adapter( + &mut self, + fake_lsp_adapter: FakeLspAdapter, + ) -> mpsc::UnboundedReceiver { + let (servers_tx, servers_rx) = mpsc::unbounded(); + let adapter = Arc::new(fake_lsp_adapter); + self.fake_adapter = Some((servers_tx, adapter.clone())); + self.adapter = Some(adapter); + servers_rx + } + pub fn name(&self) -> Arc { self.config.name.clone() } @@ -480,18 +493,16 @@ impl Language { self.config.line_comment.as_deref() } - pub fn disk_based_diagnostic_sources(&self) -> Option<&HashSet> { - self.config - .language_server - .as_ref() - .map(|config| &config.disk_based_diagnostic_sources) + pub fn disk_based_diagnostic_sources(&self) -> &'static [&'static str] { + self.adapter.as_ref().map_or(&[] as &[_], |adapter| { + adapter.disk_based_diagnostic_sources() + }) } - pub fn disk_based_diagnostics_progress_token(&self) -> Option<&String> { - self.config - .language_server + pub fn disk_based_diagnostics_progress_token(&self) -> Option<&'static str> { + self.adapter .as_ref() - .and_then(|config| config.disk_based_diagnostics_progress_token.as_ref()) + .and_then(|adapter| adapter.disk_based_diagnostics_progress_token()) } pub fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams) { @@ -598,47 +609,70 @@ impl CodeLabel { } #[cfg(any(test, feature = "test-support"))] -impl LanguageServerConfig { - pub fn fake() -> (Self, mpsc::UnboundedReceiver) { - let (servers_tx, servers_rx) = mpsc::unbounded(); - ( - Self { - fake_config: Some(FakeLanguageServerConfig { - servers_tx, - capabilities: lsp::LanguageServer::full_capabilities(), - initializer: None, - }), - disk_based_diagnostics_progress_token: Some("fakeServer/check".to_string()), - ..Default::default() - }, - servers_rx, - ) - } - - pub fn set_fake_capabilities(&mut self, capabilities: lsp::ServerCapabilities) { - self.fake_config.as_mut().unwrap().capabilities = capabilities; - } - - pub fn set_fake_initializer( - &mut self, - initializer: impl 'static + Send + Sync + Fn(&mut lsp::FakeLanguageServer), - ) { - self.fake_config.as_mut().unwrap().initializer = Some(Box::new(initializer)); +impl Default for FakeLspAdapter { + fn default() -> Self { + Self { + name: "the-fake-language-server", + capabilities: lsp::LanguageServer::full_capabilities(), + initializer: None, + disk_based_diagnostics_progress_token: None, + disk_based_diagnostics_sources: &[], + } } } -impl ToLspPosition for PointUtf16 { - fn to_lsp_position(self) -> lsp::Position { - lsp::Position::new(self.row, self.column) +#[cfg(any(test, feature = "test-support"))] +impl LspAdapter for FakeLspAdapter { + fn name(&self) -> LanguageServerName { + LanguageServerName(self.name.into()) } + + fn fetch_latest_server_version( + &self, + _: Arc, + ) -> BoxFuture<'static, Result>> { + unreachable!(); + } + + fn fetch_server_binary( + &self, + _: Box, + _: Arc, + _: PathBuf, + ) -> BoxFuture<'static, Result> { + unreachable!(); + } + + fn cached_server_binary(&self, _: PathBuf) -> BoxFuture<'static, Option> { + unreachable!(); + } + + fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} + + fn disk_based_diagnostic_sources(&self) -> &'static [&'static str] { + self.disk_based_diagnostics_sources + } + + fn disk_based_diagnostics_progress_token(&self) -> Option<&'static str> { + self.disk_based_diagnostics_progress_token + } +} + +pub fn point_to_lsp(point: PointUtf16) -> lsp::Position { + lsp::Position::new(point.row, point.column) } pub fn point_from_lsp(point: lsp::Position) -> PointUtf16 { PointUtf16::new(point.line, point.character) } -pub fn range_from_lsp(range: lsp::Range) -> Range { - let start = PointUtf16::new(range.start.line, range.start.character); - let end = PointUtf16::new(range.end.line, range.end.character); - start..end +pub fn range_to_lsp(range: Range) -> lsp::Range { + lsp::Range { + start: point_to_lsp(range.start), + end: point_to_lsp(range.end), + } +} + +pub fn range_from_lsp(range: lsp::Range) -> Range { + point_from_lsp(range.start)..point_from_lsp(range.end) } diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index e980865cb4..0acd4d7d2f 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -931,7 +931,6 @@ fn rust_lang() -> Language { LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: None, ..Default::default() }, Some(tree_sitter_rust::language()), diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index de47381c46..f5fc98640d 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -1,15 +1,17 @@ +pub use lsp_types::*; + use anyhow::{anyhow, Context, Result}; use collections::HashMap; use futures::{channel::oneshot, io::BufWriter, AsyncRead, AsyncWrite}; -use gpui::{executor, Task}; -use parking_lot::{Mutex, RwLock}; +use gpui::{executor, AsyncAppContext, Task}; +use parking_lot::Mutex; use postage::{barrier, prelude::Stream}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::{json, value::RawValue, Value}; use smol::{ channel, io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader}, - process::Command, + process, }; use std::{ future::Future, @@ -22,15 +24,12 @@ use std::{ }, }; use std::{path::Path, process::Stdio}; -use util::TryFutureExt; - -pub use lsp_types::*; +use util::{ResultExt, TryFutureExt}; const JSON_RPC_VERSION: &'static str = "2.0"; const CONTENT_LEN_HEADER: &'static str = "Content-Length: "; -type NotificationHandler = - Box, &str, &mut channel::Sender>) -> Result<()>>; +type NotificationHandler = Box, &str, AsyncAppContext)>; type ResponseHandler = Box)>; pub struct LanguageServer { @@ -39,18 +38,17 @@ pub struct LanguageServer { outbound_tx: channel::Sender>, name: String, capabilities: ServerCapabilities, - notification_handlers: Arc>>, + notification_handlers: Arc>>, response_handlers: Arc>>, executor: Arc, io_tasks: Mutex>, Task>)>>, output_done_rx: Mutex>, root_path: PathBuf, - options: Option, } pub struct Subscription { method: &'static str, - notification_handlers: Arc>>, + notification_handlers: Arc>>, } #[derive(Serialize, Deserialize)] @@ -61,18 +59,6 @@ struct Request<'a, T> { params: T, } -#[cfg(any(test, feature = "test-support"))] -#[derive(Deserialize)] -struct AnyRequest<'a> { - id: usize, - #[serde(borrow)] - jsonrpc: &'a str, - #[serde(borrow)] - method: &'a str, - #[serde(borrow)] - params: &'a RawValue, -} - #[derive(Serialize, Deserialize)] struct AnyResponse<'a> { id: usize, @@ -85,7 +71,8 @@ struct AnyResponse<'a> { #[derive(Serialize)] struct Response { id: usize, - result: T, + result: Option, + error: Option, } #[derive(Serialize, Deserialize)] @@ -118,15 +105,14 @@ impl LanguageServer { binary_path: &Path, args: &[&str], root_path: &Path, - options: Option, - background: Arc, + cx: AsyncAppContext, ) -> Result { let working_dir = if root_path.is_dir() { root_path } else { root_path.parent().unwrap_or(Path::new("/")) }; - let mut server = Command::new(binary_path) + let mut server = process::Command::new(binary_path) .current_dir(working_dir) .args(args) .stdin(Stdio::piped()) @@ -136,99 +122,97 @@ impl LanguageServer { let stdin = server.stdin.take().unwrap(); let stdout = server.stdout.take().unwrap(); let mut server = - Self::new_internal(server_id, stdin, stdout, root_path, options, background); + Self::new_internal(server_id, stdin, stdout, root_path, cx, |notification| { + log::info!( + "unhandled notification {}:\n{}", + notification.method, + serde_json::to_string_pretty( + &Value::from_str(notification.params.get()).unwrap() + ) + .unwrap() + ); + }); if let Some(name) = binary_path.file_name() { server.name = name.to_string_lossy().to_string(); } Ok(server) } - fn new_internal( + fn new_internal( server_id: usize, stdin: Stdin, stdout: Stdout, root_path: &Path, - options: Option, - executor: Arc, + cx: AsyncAppContext, + mut on_unhandled_notification: F, ) -> Self where Stdin: AsyncWrite + Unpin + Send + 'static, Stdout: AsyncRead + Unpin + Send + 'static, + F: FnMut(AnyNotification) + 'static + Send, { let mut stdin = BufWriter::new(stdin); let mut stdout = BufReader::new(stdout); let (outbound_tx, outbound_rx) = channel::unbounded::>(); let notification_handlers = - Arc::new(RwLock::new(HashMap::<_, NotificationHandler>::default())); + Arc::new(Mutex::new(HashMap::<_, NotificationHandler>::default())); let response_handlers = Arc::new(Mutex::new(HashMap::<_, ResponseHandler>::default())); - let input_task = executor.spawn( - { - let notification_handlers = notification_handlers.clone(); - let response_handlers = response_handlers.clone(); - let mut outbound_tx = outbound_tx.clone(); - async move { - let _clear_response_handlers = ClearResponseHandlers(response_handlers.clone()); - let mut buffer = Vec::new(); - loop { - buffer.clear(); - stdout.read_until(b'\n', &mut buffer).await?; - stdout.read_until(b'\n', &mut buffer).await?; - let message_len: usize = std::str::from_utf8(&buffer)? - .strip_prefix(CONTENT_LEN_HEADER) - .ok_or_else(|| anyhow!("invalid header"))? - .trim_end() - .parse()?; + let input_task = cx.spawn(|cx| { + let notification_handlers = notification_handlers.clone(); + let response_handlers = response_handlers.clone(); + async move { + let _clear_response_handlers = ClearResponseHandlers(response_handlers.clone()); + let mut buffer = Vec::new(); + loop { + buffer.clear(); + stdout.read_until(b'\n', &mut buffer).await?; + stdout.read_until(b'\n', &mut buffer).await?; + let message_len: usize = std::str::from_utf8(&buffer)? + .strip_prefix(CONTENT_LEN_HEADER) + .ok_or_else(|| anyhow!("invalid header"))? + .trim_end() + .parse()?; - buffer.resize(message_len, 0); - stdout.read_exact(&mut buffer).await?; + buffer.resize(message_len, 0); + stdout.read_exact(&mut buffer).await?; + log::trace!("incoming message:{}", String::from_utf8_lossy(&buffer)); - if let Ok(AnyNotification { id, method, params }) = - serde_json::from_slice(&buffer) - { - if let Some(handler) = notification_handlers.write().get_mut(method) { - if let Err(e) = handler(id, params.get(), &mut outbound_tx) { - log::error!("error handling {} message: {:?}", method, e); - } - } else { - log::info!( - "unhandled notification {}:\n{}", - method, - serde_json::to_string_pretty( - &Value::from_str(params.get()).unwrap() - ) - .unwrap() - ); - } - } else if let Ok(AnyResponse { id, error, result }) = - serde_json::from_slice(&buffer) - { - if let Some(handler) = response_handlers.lock().remove(&id) { - if let Some(error) = error { - handler(Err(error)); - } else if let Some(result) = result { - handler(Ok(result.get())); - } else { - handler(Ok("null")); - } - } + if let Ok(msg) = serde_json::from_slice::(&buffer) { + if let Some(handler) = notification_handlers.lock().get_mut(msg.method) { + handler(msg.id, msg.params.get(), cx.clone()); } else { - return Err(anyhow!( - "failed to deserialize message:\n{}", - std::str::from_utf8(&buffer)? - )); + on_unhandled_notification(msg); } + } else if let Ok(AnyResponse { id, error, result }) = + serde_json::from_slice(&buffer) + { + if let Some(handler) = response_handlers.lock().remove(&id) { + if let Some(error) = error { + handler(Err(error)); + } else if let Some(result) = result { + handler(Ok(result.get())); + } else { + handler(Ok("null")); + } + } + } else { + return Err(anyhow!( + "failed to deserialize message:\n{}", + std::str::from_utf8(&buffer)? + )); } } } - .log_err(), - ); + .log_err() + }); let (output_done_tx, output_done_rx) = barrier::channel(); - let output_task = executor.spawn({ + let output_task = cx.background().spawn({ let response_handlers = response_handlers.clone(); async move { let _clear_response_handlers = ClearResponseHandlers(response_handlers); let mut content_len_buffer = Vec::new(); while let Ok(message) = outbound_rx.recv().await { + log::trace!("outgoing message:{}", String::from_utf8_lossy(&message)); content_len_buffer.clear(); write!(content_len_buffer, "{}", message.len()).unwrap(); stdin.write_all(CONTENT_LEN_HEADER.as_bytes()).await?; @@ -251,18 +235,15 @@ impl LanguageServer { capabilities: Default::default(), next_id: Default::default(), outbound_tx, - executor: executor.clone(), + executor: cx.background().clone(), io_tasks: Mutex::new(Some((input_task, output_task))), output_done_rx: Mutex::new(Some(output_done_rx)), root_path: root_path.to_path_buf(), - options, } } - pub async fn initialize(mut self) -> Result> { - let options = self.options.take(); - let mut this = Arc::new(self); - let root_uri = Url::from_file_path(&this.root_path).unwrap(); + pub async fn initialize(mut self, options: Option) -> Result> { + let root_uri = Url::from_file_path(&self.root_path).unwrap(); #[allow(deprecated)] let params = InitializeParams { process_id: Default::default(), @@ -288,12 +269,13 @@ impl LanguageServer { value_set: vec![ CodeActionKind::REFACTOR.as_str().into(), CodeActionKind::QUICKFIX.as_str().into(), + CodeActionKind::SOURCE.as_str().into(), ], }, }), data_support: Some(true), resolve_support: Some(CodeActionCapabilityResolveSupport { - properties: vec!["edit".to_string()], + properties: vec!["edit".to_string(), "command".to_string()], }), ..Default::default() }), @@ -324,16 +306,14 @@ impl LanguageServer { locale: Default::default(), }; - let response = this.request::(params).await?; - { - let this = Arc::get_mut(&mut this).unwrap(); - if let Some(info) = response.server_info { - this.name = info.name; - } - this.capabilities = response.capabilities; + let response = self.request::(params).await?; + if let Some(info) = response.server_info { + self.name = info.name; } - this.notify::(InitializedParams {})?; - Ok(this) + self.capabilities = response.capabilities; + + self.notify::(InitializedParams {})?; + Ok(Arc::new(self)) } pub fn shutdown(&self) -> Option>> { @@ -368,37 +348,42 @@ impl LanguageServer { } } - pub fn on_notification(&mut self, f: F) -> Subscription + #[must_use] + pub fn on_notification(&self, f: F) -> Subscription where T: notification::Notification, - F: 'static + Send + Sync + FnMut(T::Params), + F: 'static + Send + FnMut(T::Params, AsyncAppContext), { self.on_custom_notification(T::METHOD, f) } - pub fn on_request(&mut self, f: F) -> Subscription + #[must_use] + pub fn on_request(&self, f: F) -> Subscription where T: request::Request, - F: 'static + Send + Sync + FnMut(T::Params) -> Result, + T::Params: 'static + Send, + F: 'static + Send + FnMut(T::Params, AsyncAppContext) -> Fut, + Fut: 'static + Future>, { self.on_custom_request(T::METHOD, f) } - pub fn on_custom_notification( - &mut self, - method: &'static str, - mut f: F, - ) -> Subscription + pub fn remove_request_handler(&self) { + self.notification_handlers.lock().remove(T::METHOD); + } + + #[must_use] + pub fn on_custom_notification(&self, method: &'static str, mut f: F) -> Subscription where - F: 'static + Send + Sync + FnMut(Params), + F: 'static + Send + FnMut(Params, AsyncAppContext), Params: DeserializeOwned, { - let prev_handler = self.notification_handlers.write().insert( + let prev_handler = self.notification_handlers.lock().insert( method, - Box::new(move |_, params, _| { - let params = serde_json::from_str(params)?; - f(params); - Ok(()) + Box::new(move |_, params, cx| { + if let Some(params) = serde_json::from_str(params).log_err() { + f(params, cx); + } }), ); assert!( @@ -411,26 +396,52 @@ impl LanguageServer { } } - pub fn on_custom_request( - &mut self, + #[must_use] + pub fn on_custom_request( + &self, method: &'static str, mut f: F, ) -> Subscription where - F: 'static + Send + Sync + FnMut(Params) -> Result, - Params: DeserializeOwned, + F: 'static + Send + FnMut(Params, AsyncAppContext) -> Fut, + Fut: 'static + Future>, + Params: DeserializeOwned + Send + 'static, Res: Serialize, { - let prev_handler = self.notification_handlers.write().insert( + let outbound_tx = self.outbound_tx.clone(); + let prev_handler = self.notification_handlers.lock().insert( method, - Box::new(move |id, params, tx| { + Box::new(move |id, params, cx| { if let Some(id) = id { - let params = serde_json::from_str(params)?; - let result = f(params)?; - let response = serde_json::to_vec(&Response { id, result })?; - tx.try_send(response)?; + if let Some(params) = serde_json::from_str(params).log_err() { + let response = f(params, cx.clone()); + cx.foreground() + .spawn({ + let outbound_tx = outbound_tx.clone(); + async move { + let response = match response.await { + Ok(result) => Response { + id, + result: Some(result), + error: None, + }, + Err(error) => Response { + id, + result: None, + error: Some(Error { + message: error.to_string(), + }), + }, + }; + if let Some(response) = serde_json::to_vec(&response).log_err() + { + outbound_tx.try_send(response).ok(); + } + } + }) + .detach(); + } } - Ok(()) }), ); assert!( @@ -456,7 +467,7 @@ impl LanguageServer { } pub fn request( - self: &Arc, + &self, params: T::Params, ) -> impl Future> where @@ -547,36 +558,17 @@ impl Subscription { impl Drop for Subscription { fn drop(&mut self) { - self.notification_handlers.write().remove(self.method); + self.notification_handlers.lock().remove(self.method); } } #[cfg(any(test, feature = "test-support"))] +#[derive(Clone)] pub struct FakeLanguageServer { - handlers: FakeLanguageServerHandlers, - outgoing_tx: futures::channel::mpsc::UnboundedSender>, - incoming_rx: futures::channel::mpsc::UnboundedReceiver>, - _input_task: Task>, - _output_task: Task>, + pub server: Arc, + notifications_rx: channel::Receiver<(String, String)>, } -#[cfg(any(test, feature = "test-support"))] -type FakeLanguageServerHandlers = Arc< - Mutex< - HashMap< - &'static str, - Box< - dyn Send - + FnMut( - usize, - &[u8], - gpui::AsyncAppContext, - ) -> futures::future::BoxFuture<'static, Vec>, - >, - >, - >, ->; - #[cfg(any(test, feature = "test-support"))] impl LanguageServer { pub fn full_capabilities() -> ServerCapabilities { @@ -589,177 +581,101 @@ impl LanguageServer { } } - pub fn fake(cx: &mut gpui::MutableAppContext) -> (Self, FakeLanguageServer) { + pub fn fake(cx: AsyncAppContext) -> (Self, FakeLanguageServer) { Self::fake_with_capabilities(Self::full_capabilities(), cx) } pub fn fake_with_capabilities( capabilities: ServerCapabilities, - cx: &mut gpui::MutableAppContext, + cx: AsyncAppContext, ) -> (Self, FakeLanguageServer) { let (stdin_writer, stdin_reader) = async_pipe::pipe(); let (stdout_writer, stdout_reader) = async_pipe::pipe(); + let (notifications_tx, notifications_rx) = channel::unbounded(); - let mut fake = FakeLanguageServer::new(stdin_reader, stdout_writer, cx); - fake.handle_request::({ - let capabilities = capabilities.clone(); - move |_, _| { - let capabilities = capabilities.clone(); - async move { - InitializeResult { - capabilities, - ..Default::default() - } - } - } - }); - - let executor = cx.background().clone(); let server = Self::new_internal( 0, stdin_writer, stdout_reader, Path::new("/"), - None, - executor, + cx.clone(), + |_| {}, ); + let fake = FakeLanguageServer { + server: Arc::new(Self::new_internal( + 0, + stdout_writer, + stdin_reader, + Path::new("/"), + cx.clone(), + move |msg| { + notifications_tx + .try_send((msg.method.to_string(), msg.params.get().to_string())) + .ok(); + }, + )), + notifications_rx, + }; + fake.handle_request::({ + let capabilities = capabilities.clone(); + move |_, _| { + let capabilities = capabilities.clone(); + async move { + Ok(InitializeResult { + capabilities, + ..Default::default() + }) + } + } + }); + (server, fake) } } #[cfg(any(test, feature = "test-support"))] impl FakeLanguageServer { - fn new( - stdin: async_pipe::PipeReader, - stdout: async_pipe::PipeWriter, - cx: &mut gpui::MutableAppContext, - ) -> Self { - use futures::StreamExt as _; - - let (incoming_tx, incoming_rx) = futures::channel::mpsc::unbounded(); - let (outgoing_tx, mut outgoing_rx) = futures::channel::mpsc::unbounded(); - let handlers = FakeLanguageServerHandlers::default(); - - let input_task = cx.spawn(|cx| { - let handlers = handlers.clone(); - let outgoing_tx = outgoing_tx.clone(); - async move { - let mut buffer = Vec::new(); - let mut stdin = smol::io::BufReader::new(stdin); - while Self::receive(&mut stdin, &mut buffer).await.is_ok() { - cx.background().simulate_random_delay().await; - - if let Ok(request) = serde_json::from_slice::(&buffer) { - assert_eq!(request.jsonrpc, JSON_RPC_VERSION); - - let response; - if let Some(handler) = handlers.lock().get_mut(request.method) { - response = - handler(request.id, request.params.get().as_bytes(), cx.clone()) - .await; - log::debug!("handled lsp request. method:{}", request.method); - } else { - response = serde_json::to_vec(&AnyResponse { - id: request.id, - error: Some(Error { - message: "no handler".to_string(), - }), - result: None, - }) - .unwrap(); - log::debug!("unhandled lsp request. method:{}", request.method); - } - outgoing_tx.unbounded_send(response)?; - } else { - incoming_tx.unbounded_send(buffer.clone())?; - } - } - Ok::<_, anyhow::Error>(()) - } - }); - - let output_task = cx.background().spawn(async move { - let mut stdout = smol::io::BufWriter::new(stdout); - while let Some(message) = outgoing_rx.next().await { - stdout.write_all(CONTENT_LEN_HEADER.as_bytes()).await?; - stdout - .write_all((format!("{}", message.len())).as_bytes()) - .await?; - stdout.write_all("\r\n\r\n".as_bytes()).await?; - stdout.write_all(&message).await?; - stdout.flush().await?; - } - Ok(()) - }); - - Self { - outgoing_tx, - incoming_rx, - handlers, - _input_task: input_task, - _output_task: output_task, - } - } - - pub fn notify(&mut self, params: T::Params) { - let message = serde_json::to_vec(&Notification { - jsonrpc: JSON_RPC_VERSION, - method: T::METHOD, - params, - }) - .unwrap(); - self.outgoing_tx.unbounded_send(message).unwrap(); + pub fn notify(&self, params: T::Params) { + self.server.notify::(params).ok(); } pub async fn receive_notification(&mut self) -> T::Params { use futures::StreamExt as _; loop { - let bytes = self.incoming_rx.next().await.unwrap(); - if let Ok(notification) = serde_json::from_slice::>(&bytes) { - assert_eq!(notification.method, T::METHOD); - return notification.params; + let (method, params) = self.notifications_rx.next().await.unwrap(); + if &method == T::METHOD { + return serde_json::from_str::(¶ms).unwrap(); } else { - log::info!( - "skipping message in fake language server {:?}", - std::str::from_utf8(&bytes) - ); + log::info!("skipping message in fake language server {:?}", params); } } } pub fn handle_request( - &mut self, + &self, mut handler: F, ) -> futures::channel::mpsc::UnboundedReceiver<()> where T: 'static + request::Request, + T::Params: 'static + Send, F: 'static + Send + FnMut(T::Params, gpui::AsyncAppContext) -> Fut, - Fut: 'static + Send + Future, + Fut: 'static + Send + Future>, { - use futures::FutureExt as _; - let (responded_tx, responded_rx) = futures::channel::mpsc::unbounded(); - self.handlers.lock().insert( - T::METHOD, - Box::new(move |id, params, cx| { - let result = handler(serde_json::from_slice::(params).unwrap(), cx); + self.server.remove_request_handler::(); + self.server + .on_request::(move |params, cx| { + let result = handler(params, cx.clone()); let responded_tx = responded_tx.clone(); async move { + cx.background().simulate_random_delay().await; let result = result.await; - let result = serde_json::to_string(&result).unwrap(); - let result = serde_json::from_str::<&RawValue>(&result).unwrap(); - let response = AnyResponse { - id, - error: None, - result: Some(result), - }; responded_tx.unbounded_send(()).ok(); - serde_json::to_vec(&response).unwrap() + result } - .boxed() - }), - ); + }) + .detach(); responded_rx } @@ -767,7 +683,7 @@ impl FakeLanguageServer { where T: 'static + request::Request, { - self.handlers.lock().remove(T::METHOD); + self.server.remove_request_handler::(); } pub async fn start_progress(&mut self, token: impl Into) { @@ -783,25 +699,6 @@ impl FakeLanguageServer { value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(Default::default())), }); } - - async fn receive( - stdin: &mut smol::io::BufReader, - buffer: &mut Vec, - ) -> Result<()> { - buffer.clear(); - stdin.read_until(b'\n', buffer).await?; - stdin.read_until(b'\n', buffer).await?; - let message_len: usize = std::str::from_utf8(buffer) - .unwrap() - .strip_prefix(CONTENT_LEN_HEADER) - .ok_or_else(|| anyhow!("invalid content length header"))? - .trim_end() - .parse() - .unwrap(); - buffer.resize(message_len, 0); - stdin.read_exact(buffer).await?; - Ok(()) - } } struct ClearResponseHandlers(Arc>>); @@ -826,22 +723,22 @@ mod tests { #[gpui::test] async fn test_fake(cx: &mut TestAppContext) { - let (mut server, mut fake) = cx.update(LanguageServer::fake); + let (server, mut fake) = LanguageServer::fake(cx.to_async()); let (message_tx, message_rx) = channel::unbounded(); let (diagnostics_tx, diagnostics_rx) = channel::unbounded(); server - .on_notification::(move |params| { + .on_notification::(move |params, _| { message_tx.try_send(params).unwrap() }) .detach(); server - .on_notification::(move |params| { + .on_notification::(move |params, _| { diagnostics_tx.try_send(params).unwrap() }) .detach(); - let server = server.initialize().await.unwrap(); + let server = server.initialize(None).await.unwrap(); server .notify::(DidOpenTextDocumentParams { text_document: TextDocumentItem::new( @@ -876,7 +773,7 @@ mod tests { "file://b/c" ); - fake.handle_request::(|_, _| async move {}); + fake.handle_request::(|_, _| async move { Ok(()) }); drop(server); fake.receive_notification::().await; diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 4867ada7cb..71ad489d07 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -4,9 +4,9 @@ use async_trait::async_trait; use client::{proto, PeerId}; use gpui::{AppContext, AsyncAppContext, ModelHandle}; use language::{ - point_from_lsp, + point_from_lsp, point_to_lsp, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, - range_from_lsp, Anchor, Bias, Buffer, PointUtf16, ToLspPosition, ToPointUtf16, + range_from_lsp, Anchor, Bias, Buffer, PointUtf16, ToPointUtf16, }; use lsp::{DocumentHighlightKind, ServerCapabilities}; use std::{cmp::Reverse, ops::Range, path::Path}; @@ -91,7 +91,7 @@ impl LspCommand for PrepareRename { text_document: lsp::TextDocumentIdentifier { uri: lsp::Url::from_file_path(path).unwrap(), }, - position: self.position.to_lsp_position(), + position: point_to_lsp(self.position), } } @@ -208,7 +208,7 @@ impl LspCommand for PerformRename { text_document: lsp::TextDocumentIdentifier { uri: lsp::Url::from_file_path(path).unwrap(), }, - position: self.position.to_lsp_position(), + position: point_to_lsp(self.position), }, new_name: self.new_name.clone(), work_done_progress_params: Default::default(), @@ -223,22 +223,19 @@ impl LspCommand for PerformRename { mut cx: AsyncAppContext, ) -> Result { if let Some(edit) = message { - let language_server = project + let (lsp_adapter, lsp_server) = project .read_with(&cx, |project, cx| { project .language_server_for_buffer(buffer.read(cx), cx) .cloned() }) .ok_or_else(|| anyhow!("no language server found for buffer"))?; - let language = buffer - .read_with(&cx, |buffer, _| buffer.language().cloned()) - .ok_or_else(|| anyhow!("no language for buffer"))?; Project::deserialize_workspace_edit( project, edit, self.push_to_history, - language.name(), - language_server, + lsp_adapter, + lsp_server, &mut cx, ) .await @@ -328,7 +325,7 @@ impl LspCommand for GetDefinition { text_document: lsp::TextDocumentIdentifier { uri: lsp::Url::from_file_path(path).unwrap(), }, - position: self.position.to_lsp_position(), + position: point_to_lsp(self.position), }, work_done_progress_params: Default::default(), partial_result_params: Default::default(), @@ -343,16 +340,13 @@ impl LspCommand for GetDefinition { mut cx: AsyncAppContext, ) -> Result> { let mut definitions = Vec::new(); - let language_server = project + let (lsp_adapter, language_server) = project .read_with(&cx, |project, cx| { project .language_server_for_buffer(buffer.read(cx), cx) .cloned() }) .ok_or_else(|| anyhow!("no language server found for buffer"))?; - let language = buffer - .read_with(&cx, |buffer, _| buffer.language().cloned()) - .ok_or_else(|| anyhow!("no language for buffer"))?; if let Some(message) = message { let mut unresolved_locations = Vec::new(); @@ -377,7 +371,7 @@ impl LspCommand for GetDefinition { .update(&mut cx, |this, cx| { this.open_local_buffer_via_lsp( target_uri, - language.name(), + lsp_adapter.clone(), language_server.clone(), cx, ) @@ -503,7 +497,7 @@ impl LspCommand for GetReferences { text_document: lsp::TextDocumentIdentifier { uri: lsp::Url::from_file_path(path).unwrap(), }, - position: self.position.to_lsp_position(), + position: point_to_lsp(self.position), }, work_done_progress_params: Default::default(), partial_result_params: Default::default(), @@ -521,16 +515,13 @@ impl LspCommand for GetReferences { mut cx: AsyncAppContext, ) -> Result> { let mut references = Vec::new(); - let language_server = project + let (lsp_adapter, language_server) = project .read_with(&cx, |project, cx| { project .language_server_for_buffer(buffer.read(cx), cx) .cloned() }) .ok_or_else(|| anyhow!("no language server found for buffer"))?; - let language = buffer - .read_with(&cx, |buffer, _| buffer.language().cloned()) - .ok_or_else(|| anyhow!("no language for buffer"))?; if let Some(locations) = locations { for lsp_location in locations { @@ -538,7 +529,7 @@ impl LspCommand for GetReferences { .update(&mut cx, |this, cx| { this.open_local_buffer_via_lsp( lsp_location.uri, - language.name(), + lsp_adapter.clone(), language_server.clone(), cx, ) @@ -668,7 +659,7 @@ impl LspCommand for GetDocumentHighlights { text_document: lsp::TextDocumentIdentifier { uri: lsp::Url::from_file_path(path).unwrap(), }, - position: self.position.to_lsp_position(), + position: point_to_lsp(self.position), }, work_done_progress_params: Default::default(), partial_result_params: Default::default(), diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 2316f4d80e..c9683f39d9 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -15,11 +15,12 @@ use gpui::{ MutableAppContext, Task, UpgradeModelHandle, WeakModelHandle, }; use language::{ + point_to_lsp, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, - range_from_lsp, Anchor, Bias, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, - DiagnosticEntry, DiagnosticSet, Event as BufferEvent, File as _, Language, LanguageRegistry, - LocalFile, OffsetRangeExt, Operation, Patch, PointUtf16, TextBufferSnapshot, ToLspPosition, - ToOffset, ToPointUtf16, Transaction, + range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CodeAction, CodeLabel, Completion, + Diagnostic, DiagnosticEntry, DiagnosticSet, Event as BufferEvent, File as _, Language, + LanguageRegistry, LanguageServerName, LocalFile, LspAdapter, OffsetRangeExt, Operation, Patch, + PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, }; use lsp::{DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer}; use lsp_command::*; @@ -57,10 +58,13 @@ pub struct Project { worktrees: Vec, active_entry: Option, languages: Arc, - language_servers: HashMap<(WorktreeId, Arc), Arc>, - started_language_servers: HashMap<(WorktreeId, Arc), Task>>>, + language_servers: + HashMap<(WorktreeId, LanguageServerName), (Arc, Arc)>, + started_language_servers: + HashMap<(WorktreeId, LanguageServerName), Task>>>, language_server_statuses: BTreeMap, language_server_settings: Arc>, + last_workspace_edits_by_language_server: HashMap, next_language_server_id: usize, client: Arc, next_entry_id: Arc, @@ -128,20 +132,6 @@ pub enum Event { CollaboratorLeft(PeerId), } -enum LanguageServerEvent { - WorkStart { - token: String, - }, - WorkProgress { - token: String, - progress: LanguageServerProgress, - }, - WorkEnd { - token: String, - }, - DiagnosticsUpdate(lsp::PublishDiagnosticsParams), -} - pub struct LanguageServerStatus { pub name: String, pub pending_work: BTreeMap, @@ -185,7 +175,7 @@ pub struct DocumentHighlight { pub struct Symbol { pub source_worktree_id: WorktreeId, pub worktree_id: WorktreeId, - pub language_name: String, + pub language_server_name: LanguageServerName, pub path: PathBuf, pub label: CodeLabel, pub name: String, @@ -343,6 +333,7 @@ impl Project { language_servers: Default::default(), started_language_servers: Default::default(), language_server_statuses: Default::default(), + last_workspace_edits_by_language_server: Default::default(), language_server_settings: Default::default(), next_language_server_id: 0, nonce: StdRng::from_entropy().gen(), @@ -430,6 +421,7 @@ impl Project { ) }) .collect(), + last_workspace_edits_by_language_server: Default::default(), next_language_server_id: 0, opened_buffers: Default::default(), buffer_snapshots: Default::default(), @@ -958,8 +950,8 @@ impl Project { fn open_local_buffer_via_lsp( &mut self, abs_path: lsp::Url, - lang_name: Arc, - lang_server: Arc, + lsp_adapter: Arc, + lsp_server: Arc, cx: &mut ModelContext, ) -> Task>> { cx.spawn(|this, mut cx| async move { @@ -977,8 +969,10 @@ impl Project { }) .await?; this.update(&mut cx, |this, cx| { - this.language_servers - .insert((worktree.read(cx).id(), lang_name), lang_server); + this.language_servers.insert( + (worktree.read(cx).id(), lsp_adapter.name()), + (lsp_adapter, lsp_server), + ); }); (worktree, PathBuf::new()) }; @@ -1121,7 +1115,7 @@ impl Project { } } - if let Some(server) = language_server { + if let Some((_, server)) = language_server { server .notify::( lsp::DidOpenTextDocumentParams { @@ -1154,7 +1148,7 @@ impl Project { if let Some(file) = File::from_dyn(buffer.file()) { if file.is_local() { let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap(); - if let Some(server) = this.language_server_for_buffer(buffer, cx) { + if let Some((_, server)) = this.language_server_for_buffer(buffer, cx) { server .notify::( lsp::DidCloseTextDocumentParams { @@ -1190,7 +1184,7 @@ impl Project { cx.background().spawn(request).detach_and_log_err(cx); } BufferEvent::Edited { .. } => { - let language_server = self + let (_, language_server) = self .language_server_for_buffer(buffer.read(cx), cx)? .clone(); let buffer = buffer.read(cx); @@ -1212,8 +1206,8 @@ impl Project { .collect(); lsp::TextDocumentContentChangeEvent { range: Some(lsp::Range::new( - edit_start.to_lsp_position(), - edit_end.to_lsp_position(), + point_to_lsp(edit_start), + point_to_lsp(edit_end), )), range_length: None, text: new_text, @@ -1263,11 +1257,11 @@ impl Project { fn language_servers_for_worktree( &self, worktree_id: WorktreeId, - ) -> impl Iterator)> { + ) -> impl Iterator, Arc)> { self.language_servers.iter().filter_map( - move |((language_server_worktree_id, language_name), server)| { + move |((language_server_worktree_id, _), server)| { if *language_server_worktree_id == worktree_id { - Some((language_name.as_ref(), server)) + Some(server) } else { None } @@ -1303,7 +1297,12 @@ impl Project { language: Arc, cx: &mut ModelContext, ) { - let key = (worktree_id, language.name()); + let adapter = if let Some(adapter) = language.lsp_adapter() { + adapter + } else { + return; + }; + let key = (worktree_id, adapter.name()); self.started_language_servers .entry(key.clone()) .or_insert_with(|| { @@ -1316,109 +1315,100 @@ impl Project { cx, ); cx.spawn_weak(|this, mut cx| async move { - let mut language_server = language_server?.await.log_err()?; + let language_server = language_server?.await.log_err()?; + let language_server = language_server + .initialize(adapter.initialization_options()) + .await + .log_err()?; let this = this.upgrade(&cx)?; - let (language_server_events_tx, language_server_events_rx) = - smol::channel::unbounded(); + let disk_based_diagnostics_progress_token = + adapter.disk_based_diagnostics_progress_token(); language_server .on_notification::({ - let language_server_events_tx = language_server_events_tx.clone(); - move |params| { - language_server_events_tx - .try_send(LanguageServerEvent::DiagnosticsUpdate(params)) - .ok(); + let this = this.downgrade(); + let adapter = adapter.clone(); + move |params, mut cx| { + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.on_lsp_diagnostics_published( + server_id, + params, + &adapter, + disk_based_diagnostics_progress_token, + cx, + ); + }); + } } }) .detach(); language_server - .on_request::({ + .on_request::({ let settings = this .read_with(&cx, |this, _| this.language_server_settings.clone()); - move |params| { - let settings = settings.lock(); - Ok(params - .items - .into_iter() - .map(|item| { - if let Some(section) = &item.section { - settings - .get(section) - .cloned() - .unwrap_or(serde_json::Value::Null) - } else { - settings.clone() - } - }) - .collect()) + move |params, _| { + let settings = settings.lock().clone(); + async move { + Ok(params + .items + .into_iter() + .map(|item| { + if let Some(section) = &item.section { + settings + .get(section) + .cloned() + .unwrap_or(serde_json::Value::Null) + } else { + settings.clone() + } + }) + .collect()) + } } }) .detach(); language_server - .on_notification::(move |params| { - let token = match params.token { - lsp::NumberOrString::String(token) => token, - lsp::NumberOrString::Number(token) => { - log::info!("skipping numeric progress token {}", token); - return; - } - }; - - match params.value { - lsp::ProgressParamsValue::WorkDone(progress) => match progress { - lsp::WorkDoneProgress::Begin(_) => { - language_server_events_tx - .try_send(LanguageServerEvent::WorkStart { token }) - .ok(); - } - lsp::WorkDoneProgress::Report(report) => { - language_server_events_tx - .try_send(LanguageServerEvent::WorkProgress { - token, - progress: LanguageServerProgress { - message: report.message, - percentage: report - .percentage - .map(|p| p as usize), - last_update_at: Instant::now(), - }, - }) - .ok(); - } - lsp::WorkDoneProgress::End(_) => { - language_server_events_tx - .try_send(LanguageServerEvent::WorkEnd { token }) - .ok(); - } - }, + .on_request::({ + let this = this.downgrade(); + let adapter = adapter.clone(); + let language_server = language_server.clone(); + move |params, cx| { + Self::on_lsp_workspace_edit( + this, + params, + server_id, + adapter.clone(), + language_server.clone(), + cx, + ) } }) .detach(); - // Process all the LSP events. - cx.spawn(|mut cx| { - let this = this.downgrade(); - async move { - while let Ok(event) = language_server_events_rx.recv().await { - let this = this.upgrade(&cx)?; - this.update(&mut cx, |this, cx| { - this.on_lsp_event(server_id, event, &language, cx) - }); - - // Don't starve the main thread when lots of events arrive all at once. - smol::future::yield_now().await; + language_server + .on_notification::({ + let this = this.downgrade(); + move |params, mut cx| { + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.on_lsp_progress( + params, + server_id, + disk_based_diagnostics_progress_token, + cx, + ); + }); + } } - Some(()) - } - }) - .detach(); + }) + .detach(); - let language_server = language_server.initialize().await.log_err()?; this.update(&mut cx, |this, cx| { this.language_servers - .insert(key.clone(), language_server.clone()); + .insert(key.clone(), (adapter, language_server.clone())); this.language_server_statuses.insert( server_id, LanguageServerStatus { @@ -1461,7 +1451,10 @@ impl Project { } else { continue; }; - if (file.worktree.read(cx).id(), language.name()) != key { + if file.worktree.read(cx).id() != key.0 + || language.lsp_adapter().map(|a| a.name()) + != Some(key.1.clone()) + { continue; } @@ -1540,15 +1533,20 @@ impl Project { language: Arc, cx: &mut ModelContext, ) { - let key = (worktree_id, language.name()); + let adapter = if let Some(adapter) = language.lsp_adapter() { + adapter + } else { + return; + }; + let key = (worktree_id, adapter.name()); let server_to_shutdown = self.language_servers.remove(&key); self.started_language_servers.remove(&key); server_to_shutdown .as_ref() - .map(|server| self.language_server_statuses.remove(&server.server_id())); + .map(|(_, server)| self.language_server_statuses.remove(&server.server_id())); cx.spawn_weak(|this, mut cx| async move { if let Some(this) = this.upgrade(&cx) { - if let Some(server_to_shutdown) = server_to_shutdown { + if let Some((_, server_to_shutdown)) = server_to_shutdown { if let Some(shutdown_task) = server_to_shutdown.shutdown() { shutdown_task.await; } @@ -1562,116 +1560,138 @@ impl Project { .detach(); } - fn on_lsp_event( + fn on_lsp_diagnostics_published( &mut self, - language_server_id: usize, - event: LanguageServerEvent, - language: &Arc, + server_id: usize, + mut params: lsp::PublishDiagnosticsParams, + adapter: &Arc, + disk_based_diagnostics_progress_token: Option<&str>, cx: &mut ModelContext, ) { - let disk_diagnostics_token = language.disk_based_diagnostics_progress_token(); - let language_server_status = - if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { - status - } else { + adapter.process_diagnostics(&mut params); + if disk_based_diagnostics_progress_token.is_none() { + self.disk_based_diagnostics_started(cx); + self.broadcast_language_server_update( + server_id, + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating( + proto::LspDiskBasedDiagnosticsUpdating {}, + ), + ); + } + self.update_diagnostics(params, adapter.disk_based_diagnostic_sources(), cx) + .log_err(); + if disk_based_diagnostics_progress_token.is_none() { + self.disk_based_diagnostics_finished(cx); + self.broadcast_language_server_update( + server_id, + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( + proto::LspDiskBasedDiagnosticsUpdated {}, + ), + ); + } + } + + fn on_lsp_progress( + &mut self, + progress: lsp::ProgressParams, + server_id: usize, + disk_based_diagnostics_progress_token: Option<&str>, + cx: &mut ModelContext, + ) { + let token = match progress.token { + lsp::NumberOrString::String(token) => token, + lsp::NumberOrString::Number(token) => { + log::info!("skipping numeric progress token {}", token); return; - }; + } + }; - match event { - LanguageServerEvent::WorkStart { token } => { - if Some(&token) == disk_diagnostics_token { - language_server_status.pending_diagnostic_updates += 1; - if language_server_status.pending_diagnostic_updates == 1 { - self.disk_based_diagnostics_started(cx); + match progress.value { + lsp::ProgressParamsValue::WorkDone(progress) => match progress { + lsp::WorkDoneProgress::Begin(_) => { + let language_server_status = + if let Some(status) = self.language_server_statuses.get_mut(&server_id) { + status + } else { + return; + }; + + if Some(token.as_str()) == disk_based_diagnostics_progress_token { + language_server_status.pending_diagnostic_updates += 1; + if language_server_status.pending_diagnostic_updates == 1 { + self.disk_based_diagnostics_started(cx); + self.broadcast_language_server_update( + server_id, + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating( + proto::LspDiskBasedDiagnosticsUpdating {}, + ), + ); + } + } else { + self.on_lsp_work_start(server_id, token.clone(), cx); self.broadcast_language_server_update( - language_server_id, - proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating( - proto::LspDiskBasedDiagnosticsUpdating {}, + server_id, + proto::update_language_server::Variant::WorkStart( + proto::LspWorkStart { token }, ), ); } - } else { - self.on_lsp_work_start(language_server_id, token.clone(), cx); - self.broadcast_language_server_update( - language_server_id, - proto::update_language_server::Variant::WorkStart(proto::LspWorkStart { - token, - }), - ); } - } - LanguageServerEvent::WorkProgress { token, progress } => { - if Some(&token) != disk_diagnostics_token { - self.on_lsp_work_progress( - language_server_id, - token.clone(), - progress.clone(), - cx, - ); - self.broadcast_language_server_update( - language_server_id, - proto::update_language_server::Variant::WorkProgress( - proto::LspWorkProgress { - token, - message: progress.message, - percentage: progress.percentage.map(|p| p as u32), + lsp::WorkDoneProgress::Report(report) => { + if Some(token.as_str()) != disk_based_diagnostics_progress_token { + self.on_lsp_work_progress( + server_id, + token.clone(), + LanguageServerProgress { + message: report.message.clone(), + percentage: report.percentage.map(|p| p as usize), + last_update_at: Instant::now(), }, - ), - ); - } - } - LanguageServerEvent::WorkEnd { token } => { - if Some(&token) == disk_diagnostics_token { - language_server_status.pending_diagnostic_updates -= 1; - if language_server_status.pending_diagnostic_updates == 0 { - self.disk_based_diagnostics_finished(cx); + cx, + ); self.broadcast_language_server_update( - language_server_id, - proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( - proto::LspDiskBasedDiagnosticsUpdated {}, + server_id, + proto::update_language_server::Variant::WorkProgress( + proto::LspWorkProgress { + token, + message: report.message, + percentage: report.percentage.map(|p| p as u32), + }, ), ); } - } else { - self.on_lsp_work_end(language_server_id, token.clone(), cx); - self.broadcast_language_server_update( - language_server_id, - proto::update_language_server::Variant::WorkEnd(proto::LspWorkEnd { - token, - }), - ); } - } - LanguageServerEvent::DiagnosticsUpdate(mut params) => { - language.process_diagnostics(&mut params); + lsp::WorkDoneProgress::End(_) => { + if Some(token.as_str()) == disk_based_diagnostics_progress_token { + let language_server_status = if let Some(status) = + self.language_server_statuses.get_mut(&server_id) + { + status + } else { + return; + }; - if disk_diagnostics_token.is_none() { - self.disk_based_diagnostics_started(cx); - self.broadcast_language_server_update( - language_server_id, - proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating( - proto::LspDiskBasedDiagnosticsUpdating {}, - ), - ); + language_server_status.pending_diagnostic_updates -= 1; + if language_server_status.pending_diagnostic_updates == 0 { + self.disk_based_diagnostics_finished(cx); + self.broadcast_language_server_update( + server_id, + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( + proto::LspDiskBasedDiagnosticsUpdated {}, + ), + ); + } + } else { + self.on_lsp_work_end(server_id, token.clone(), cx); + self.broadcast_language_server_update( + server_id, + proto::update_language_server::Variant::WorkEnd(proto::LspWorkEnd { + token, + }), + ); + } } - self.update_diagnostics( - params, - language - .disk_based_diagnostic_sources() - .unwrap_or(&Default::default()), - cx, - ) - .log_err(); - if disk_diagnostics_token.is_none() { - self.disk_based_diagnostics_finished(cx); - self.broadcast_language_server_update( - language_server_id, - proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( - proto::LspDiskBasedDiagnosticsUpdated {}, - ), - ); - } - } + }, } } @@ -1719,6 +1739,40 @@ impl Project { } } + async fn on_lsp_workspace_edit( + this: WeakModelHandle, + params: lsp::ApplyWorkspaceEditParams, + server_id: usize, + adapter: Arc, + language_server: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let this = this + .upgrade(&cx) + .ok_or_else(|| anyhow!("project project closed"))?; + let transaction = Self::deserialize_workspace_edit( + this.clone(), + params.edit, + true, + adapter.clone(), + language_server.clone(), + &mut cx, + ) + .await + .log_err(); + this.update(&mut cx, |this, _| { + if let Some(transaction) = transaction { + this.last_workspace_edits_by_language_server + .insert(server_id, transaction); + } + }); + Ok(lsp::ApplyWorkspaceEditResponse { + applied: true, + failed_change: None, + failure_reason: None, + }) + } + fn broadcast_language_server_update( &self, language_server_id: usize, @@ -1736,7 +1790,7 @@ impl Project { } pub fn set_language_server_settings(&mut self, settings: serde_json::Value) { - for server in self.language_servers.values() { + for (_, server) in self.language_servers.values() { server .notify::( lsp::DidChangeConfigurationParams { @@ -1757,7 +1811,7 @@ impl Project { pub fn update_diagnostics( &mut self, params: lsp::PublishDiagnosticsParams, - disk_based_sources: &HashSet, + disk_based_sources: &[&str], cx: &mut ModelContext, ) -> Result<()> { let abs_path = params @@ -1800,8 +1854,9 @@ impl Project { ); } else { let group_id = post_inc(&mut next_group_id); - let is_disk_based = - source.map_or(false, |source| disk_based_sources.contains(source)); + let is_disk_based = source.map_or(false, |source| { + disk_based_sources.contains(&source.as_str()) + }); sources_by_group_id.insert(group_id, source); primary_diagnostic_group_ids @@ -2050,7 +2105,7 @@ impl Project { let buffer = buffer_handle.read(cx); if let Some(file) = File::from_dyn(buffer.file()) { if let Some(buffer_abs_path) = file.as_local().map(|f| f.abs_path(cx)) { - if let Some(server) = self.language_server_for_buffer(buffer, cx) { + if let Some((_, server)) = self.language_server_for_buffer(buffer, cx) { local_buffers.push((buffer_handle, buffer_abs_path, server.clone())); } } else { @@ -2099,7 +2154,12 @@ impl Project { language_server .request::(lsp::DocumentFormattingParams { text_document, - options: Default::default(), + options: lsp::FormattingOptions { + tab_size: 4, + insert_spaces: true, + insert_final_newline: Some(true), + ..Default::default() + }, work_done_progress_params: Default::default(), }) .await? @@ -2109,15 +2169,19 @@ impl Project { .map_or(false, |provider| *provider != lsp::OneOf::Left(false)) { let buffer_start = lsp::Position::new(0, 0); - let buffer_end = buffer - .read_with(&cx, |buffer, _| buffer.max_point_utf16()) - .to_lsp_position(); + let buffer_end = + buffer.read_with(&cx, |buffer, _| point_to_lsp(buffer.max_point_utf16())); language_server .request::( lsp::DocumentRangeFormattingParams { text_document, range: lsp::Range::new(buffer_start, buffer_end), - options: Default::default(), + options: lsp::FormattingOptions { + tab_size: 4, + insert_spaces: true, + insert_final_newline: Some(true), + ..Default::default() + }, work_done_progress_params: Default::default(), }, ) @@ -2187,25 +2251,24 @@ impl Project { pub fn symbols(&self, query: &str, cx: &mut ModelContext) -> Task>> { if self.is_local() { let mut language_servers = HashMap::default(); - for ((worktree_id, language_name), language_server) in self.language_servers.iter() { - if let Some((worktree, language)) = self + for ((worktree_id, _), (lsp_adapter, language_server)) in self.language_servers.iter() { + if let Some(worktree) = self .worktree_for_id(*worktree_id, cx) .and_then(|worktree| worktree.read(cx).as_local()) - .zip(self.languages.get_language(language_name)) { language_servers .entry(Arc::as_ptr(language_server)) .or_insert(( + lsp_adapter.clone(), language_server.clone(), *worktree_id, worktree.abs_path().clone(), - language.clone(), )); } } let mut requests = Vec::new(); - for (language_server, _, _, _) in language_servers.values() { + for (_, language_server, _, _) in language_servers.values() { requests.push(language_server.request::( lsp::WorkspaceSymbolParams { query: query.to_string(), @@ -2220,7 +2283,7 @@ impl Project { let mut symbols = Vec::new(); if let Some(this) = this.upgrade(&cx) { this.read_with(&cx, |this, cx| { - for ((_, source_worktree_id, worktree_abs_path, language), lsp_symbols) in + for ((adapter, _, source_worktree_id, worktree_abs_path), lsp_symbols) in language_servers.into_values().zip(responses) { symbols.extend(lsp_symbols.into_iter().flatten().filter_map( @@ -2237,8 +2300,13 @@ impl Project { path = relativize_path(&worktree_abs_path, &abs_path); } - let label = language - .label_for_symbol(&lsp_symbol.name, lsp_symbol.kind) + let label = this + .languages + .select_language(&path) + .and_then(|language| { + language + .label_for_symbol(&lsp_symbol.name, lsp_symbol.kind) + }) .unwrap_or_else(|| { CodeLabel::plain(lsp_symbol.name.clone(), None) }); @@ -2247,7 +2315,7 @@ impl Project { Some(Symbol { source_worktree_id, worktree_id, - language_name: language.name().to_string(), + language_server_name: adapter.name(), name: lsp_symbol.name, kind: lsp_symbol.kind, label, @@ -2294,9 +2362,9 @@ impl Project { cx: &mut ModelContext, ) -> Task>> { if self.is_local() { - let language_server = if let Some(server) = self.language_servers.get(&( + let (lsp_adapter, language_server) = if let Some(server) = self.language_servers.get(&( symbol.source_worktree_id, - Arc::from(symbol.language_name.as_str()), + symbol.language_server_name.clone(), )) { server.clone() } else { @@ -2321,12 +2389,7 @@ impl Project { return Task::ready(Err(anyhow!("invalid symbol path"))); }; - self.open_local_buffer_via_lsp( - symbol_uri, - Arc::from(symbol.language_name.as_str()), - language_server, - cx, - ) + self.open_local_buffer_via_lsp(symbol_uri, lsp_adapter, language_server, cx) } else if let Some(project_id) = self.remote_id() { let request = self.client.request(proto::OpenBufferForSymbol { project_id, @@ -2367,7 +2430,7 @@ impl Project { if worktree.read(cx).as_local().is_some() { let buffer_abs_path = buffer_abs_path.unwrap(); - let lang_server = + let (_, lang_server) = if let Some(server) = self.language_server_for_buffer(source_buffer, cx) { server.clone() } else { @@ -2381,7 +2444,7 @@ impl Project { lsp::TextDocumentIdentifier::new( lsp::Url::from_file_path(buffer_abs_path).unwrap(), ), - position.to_lsp_position(), + point_to_lsp(position), ), context: Default::default(), work_done_progress_params: Default::default(), @@ -2403,11 +2466,26 @@ impl Project { Ok(completions .into_iter() .filter_map(|lsp_completion| { - let (old_range, new_text) = match lsp_completion.text_edit.as_ref()? { - lsp::CompletionTextEdit::Edit(edit) => { + let (old_range, new_text) = match lsp_completion.text_edit.as_ref() { + Some(lsp::CompletionTextEdit::Edit(edit)) => { (range_from_lsp(edit.range), edit.new_text.clone()) } - lsp::CompletionTextEdit::InsertAndReplace(_) => { + None => { + let clipped_position = + this.clip_point_utf16(position, Bias::Left); + if position != clipped_position { + log::info!("completion out of expected range"); + return None; + } + ( + this.common_prefix_at( + clipped_position, + &lsp_completion.label, + ), + lsp_completion.label.clone(), + ) + } + Some(lsp::CompletionTextEdit::InsertAndReplace(_)) => { log::info!("unsupported insert/replace completion"); return None; } @@ -2432,6 +2510,7 @@ impl Project { lsp_completion, }) } else { + log::info!("completion out of expected range"); None } }) @@ -2479,7 +2558,8 @@ impl Project { let buffer_id = buffer.remote_id(); if self.is_local() { - let lang_server = if let Some(server) = self.language_server_for_buffer(buffer, cx) { + let (_, lang_server) = if let Some(server) = self.language_server_for_buffer(buffer, cx) + { server.clone() } else { return Task::ready(Ok(Default::default())); @@ -2549,7 +2629,7 @@ impl Project { } } - pub fn code_actions( + pub fn code_actions( &self, buffer_handle: &ModelHandle, range: Range, @@ -2557,6 +2637,11 @@ impl Project { ) -> Task>> { let buffer_handle = buffer_handle.clone(); let buffer = buffer_handle.read(cx); + let snapshot = buffer.snapshot(); + let relevant_diagnostics = snapshot + .diagnostics_in_range::(range.to_offset(&snapshot), false) + .map(|entry| entry.to_lsp_diagnostic_stub()) + .collect(); let buffer_id = buffer.remote_id(); let worktree; let buffer_abs_path; @@ -2570,16 +2655,14 @@ impl Project { if worktree.read(cx).as_local().is_some() { let buffer_abs_path = buffer_abs_path.unwrap(); - let lang_server = if let Some(server) = self.language_server_for_buffer(buffer, cx) { + let (_, lang_server) = if let Some(server) = self.language_server_for_buffer(buffer, cx) + { server.clone() } else { return Task::ready(Ok(Default::default())); }; - let lsp_range = lsp::Range::new( - range.start.to_point_utf16(buffer).to_lsp_position(), - range.end.to_point_utf16(buffer).to_lsp_position(), - ); + let lsp_range = range_to_lsp(range.to_point_utf16(buffer)); cx.foreground().spawn(async move { if !lang_server.capabilities().code_action_provider.is_some() { return Ok(Default::default()); @@ -2594,11 +2677,12 @@ impl Project { work_done_progress_params: Default::default(), partial_result_params: Default::default(), context: lsp::CodeActionContext { - diagnostics: Default::default(), + diagnostics: relevant_diagnostics, only: Some(vec![ lsp::CodeActionKind::QUICKFIX, lsp::CodeActionKind::REFACTOR, lsp::CodeActionKind::REFACTOR_EXTRACT, + lsp::CodeActionKind::SOURCE, ]), }, }) @@ -2657,16 +2741,12 @@ impl Project { ) -> Task> { if self.is_local() { let buffer = buffer_handle.read(cx); - let lang_name = if let Some(lang) = buffer.language() { - lang.name() - } else { - return Task::ready(Ok(Default::default())); - }; - let lang_server = if let Some(server) = self.language_server_for_buffer(buffer, cx) { - server.clone() - } else { - return Task::ready(Ok(Default::default())); - }; + let (lsp_adapter, lang_server) = + if let Some(server) = self.language_server_for_buffer(buffer, cx) { + server.clone() + } else { + return Task::ready(Ok(Default::default())); + }; let range = action.range.to_point_utf16(buffer); cx.spawn(|this, mut cx| async move { @@ -2677,11 +2757,7 @@ impl Project { .and_then(|d| d.get_mut("codeActionParams")) .and_then(|d| d.get_mut("range")) { - *lsp_range = serde_json::to_value(&lsp::Range::new( - range.start.to_lsp_position(), - range.end.to_lsp_position(), - )) - .unwrap(); + *lsp_range = serde_json::to_value(&range_to_lsp(range)).unwrap(); action.lsp_action = lang_server .request::(action.lsp_action) .await?; @@ -2703,11 +2779,28 @@ impl Project { this, edit, push_to_history, - lang_name, + lsp_adapter, lang_server, &mut cx, ) .await + } else if let Some(command) = action.lsp_action.command { + this.update(&mut cx, |this, _| { + this.last_workspace_edits_by_language_server + .remove(&lang_server.server_id()); + }); + lang_server + .request::(lsp::ExecuteCommandParams { + command: command.command, + arguments: command.arguments.unwrap_or_default(), + ..Default::default() + }) + .await?; + Ok(this.update(&mut cx, |this, _| { + this.last_workspace_edits_by_language_server + .remove(&lang_server.server_id()) + .unwrap_or_default() + })) } else { Ok(ProjectTransaction::default()) } @@ -2739,7 +2832,7 @@ impl Project { this: ModelHandle, edit: lsp::WorkspaceEdit, push_to_history: bool, - language_name: Arc, + lsp_adapter: Arc, language_server: Arc, cx: &mut AsyncAppContext, ) -> Result { @@ -2816,7 +2909,7 @@ impl Project { .update(cx, |this, cx| { this.open_local_buffer_via_lsp( op.text_document.uri, - language_name.clone(), + lsp_adapter.clone(), language_server.clone(), cx, ) @@ -3111,7 +3204,7 @@ impl Project { let buffer = buffer_handle.read(cx); if self.is_local() { let file = File::from_dyn(buffer.file()).and_then(File::as_local); - if let Some((file, language_server)) = + if let Some((file, (_, language_server))) = file.zip(self.language_server_for_buffer(buffer, cx).cloned()) { let lsp_params = request.to_lsp(&file.abs_path(cx), cx); @@ -4238,9 +4331,8 @@ impl Project { } fn deserialize_symbol(&self, serialized_symbol: proto::Symbol) -> Result { - let language = self - .languages - .get_language(&serialized_symbol.language_name); + let source_worktree_id = WorktreeId::from_proto(serialized_symbol.source_worktree_id); + let worktree_id = WorktreeId::from_proto(serialized_symbol.worktree_id); let start = serialized_symbol .start .ok_or_else(|| anyhow!("invalid start"))?; @@ -4248,15 +4340,17 @@ impl Project { .end .ok_or_else(|| anyhow!("invalid end"))?; let kind = unsafe { mem::transmute(serialized_symbol.kind) }; + let path = PathBuf::from(serialized_symbol.path); + let language = self.languages.select_language(&path); Ok(Symbol { - source_worktree_id: WorktreeId::from_proto(serialized_symbol.source_worktree_id), - worktree_id: WorktreeId::from_proto(serialized_symbol.worktree_id), - language_name: serialized_symbol.language_name.clone(), + source_worktree_id, + worktree_id, + language_server_name: LanguageServerName(serialized_symbol.language_server_name.into()), label: language .and_then(|language| language.label_for_symbol(&serialized_symbol.name, kind)) .unwrap_or_else(|| CodeLabel::plain(serialized_symbol.name.clone(), None)), name: serialized_symbol.name, - path: PathBuf::from(serialized_symbol.path), + path, range: PointUtf16::new(start.row, start.column)..PointUtf16::new(end.row, end.column), kind, signature: serialized_symbol @@ -4501,10 +4595,11 @@ impl Project { &self, buffer: &Buffer, cx: &AppContext, - ) -> Option<&Arc> { + ) -> Option<&(Arc, Arc)> { if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) { let worktree_id = file.worktree_id(cx); - self.language_servers.get(&(worktree_id, language.name())) + self.language_servers + .get(&(worktree_id, language.lsp_adapter()?.name())) } else { None } @@ -4618,7 +4713,7 @@ impl Entity for Project { let shutdown_futures = self .language_servers .drain() - .filter_map(|(_, server)| server.shutdown()) + .filter_map(|(_, (_, server))| server.shutdown()) .collect::>(); Some( async move { @@ -4689,7 +4784,7 @@ fn serialize_symbol(symbol: &Symbol) -> proto::Symbol { proto::Symbol { source_worktree_id: symbol.source_worktree_id.to_proto(), worktree_id: symbol.worktree_id.to_proto(), - language_name: symbol.language_name.clone(), + language_server_name: symbol.language_server_name.0.to_string(), name: symbol.name.clone(), kind: unsafe { mem::transmute(symbol.kind) }, path: symbol.path.to_string_lossy().to_string(), @@ -4747,7 +4842,7 @@ mod tests { use futures::{future, StreamExt}; use gpui::test::subscribe; use language::{ - tree_sitter_rust, Diagnostic, LanguageConfig, LanguageServerConfig, OffsetRangeExt, Point, + tree_sitter_rust, Diagnostic, FakeLspAdapter, LanguageConfig, OffsetRangeExt, Point, ToPoint, }; use lsp::Url; @@ -4824,41 +4919,44 @@ mod tests { async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) { cx.foreground().forbid_parking(); - let (mut rust_lsp_config, mut fake_rust_servers) = LanguageServerConfig::fake(); - let (mut json_lsp_config, mut fake_json_servers) = LanguageServerConfig::fake(); - rust_lsp_config.set_fake_capabilities(lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string(), "::".to_string()]), - ..Default::default() - }), - ..Default::default() - }); - json_lsp_config.set_fake_capabilities(lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![":".to_string()]), - ..Default::default() - }), - ..Default::default() - }); - - let rust_language = Arc::new(Language::new( + let mut rust_language = Language::new( LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(rust_lsp_config), ..Default::default() }, Some(tree_sitter_rust::language()), - )); - let json_language = Arc::new(Language::new( + ); + let mut json_language = Language::new( LanguageConfig { name: "JSON".into(), path_suffixes: vec!["json".to_string()], - language_server: Some(json_lsp_config), ..Default::default() }, None, - )); + ); + let mut fake_rust_servers = rust_language.set_fake_lsp_adapter(FakeLspAdapter { + name: "the-rust-language-server", + capabilities: lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string(), "::".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + }); + let mut fake_json_servers = json_language.set_fake_lsp_adapter(FakeLspAdapter { + name: "the-json-language-server", + capabilities: lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![":".to_string()]), + ..Default::default() + }), + ..Default::default() + }, + ..Default::default() + }); let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -4874,8 +4972,8 @@ mod tests { let project = Project::test(fs, cx); project.update(cx, |project, _| { - project.languages.add(rust_language); - project.languages.add(json_language); + project.languages.add(Arc::new(rust_language)); + project.languages.add(Arc::new(json_language)); }); let worktree_id = project @@ -5033,9 +5131,9 @@ mod tests { }); let mut rust_shutdown_requests = fake_rust_server - .handle_request::(|_, _| future::ready(())); + .handle_request::(|_, _| future::ready(Ok(()))); let mut json_shutdown_requests = fake_json_server - .handle_request::(|_, _| future::ready(())); + .handle_request::(|_, _| future::ready(Ok(()))); futures::join!(rust_shutdown_requests.next(), json_shutdown_requests.next()); let mut fake_rust_server = fake_rust_servers.next().await.unwrap(); @@ -5102,21 +5200,20 @@ mod tests { async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) { cx.foreground().forbid_parking(); - let (language_server_config, mut fake_servers) = LanguageServerConfig::fake(); - let progress_token = language_server_config - .disk_based_diagnostics_progress_token - .clone() - .unwrap(); - - let language = Arc::new(Language::new( + let progress_token = "the-progress-token"; + let mut language = Language::new( LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), ..Default::default() }, Some(tree_sitter_rust::language()), - )); + ); + let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter { + disk_based_diagnostics_progress_token: Some(progress_token), + disk_based_diagnostics_sources: &["disk"], + ..Default::default() + }); let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -5129,7 +5226,7 @@ mod tests { .await; let project = Project::test(fs, cx); - project.update(cx, |project, _| project.languages.add(language)); + project.update(cx, |project, _| project.languages.add(Arc::new(language))); let (tree, _) = project .update(cx, |project, cx| { @@ -5153,15 +5250,15 @@ mod tests { let mut events = subscribe(&project, cx); let mut fake_server = fake_servers.next().await.unwrap(); - fake_server.start_progress(&progress_token).await; + fake_server.start_progress(progress_token).await; assert_eq!( events.next().await.unwrap(), Event::DiskBasedDiagnosticsStarted ); - fake_server.start_progress(&progress_token).await; - fake_server.end_progress(&progress_token).await; - fake_server.start_progress(&progress_token).await; + fake_server.start_progress(progress_token).await; + fake_server.end_progress(progress_token).await; + fake_server.start_progress(progress_token).await; fake_server.notify::( lsp::PublishDiagnosticsParams { @@ -5180,8 +5277,8 @@ mod tests { Event::DiagnosticsUpdated((worktree_id, Path::new("a.rs")).into()) ); - fake_server.end_progress(&progress_token).await; - fake_server.end_progress(&progress_token).await; + fake_server.end_progress(progress_token).await; + fake_server.end_progress(progress_token).await; assert_eq!( events.next().await.unwrap(), Event::DiskBasedDiagnosticsUpdated @@ -5221,19 +5318,18 @@ mod tests { async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { cx.foreground().forbid_parking(); - let (mut lsp_config, mut fake_servers) = LanguageServerConfig::fake(); - lsp_config - .disk_based_diagnostic_sources - .insert("disk".to_string()); - let language = Arc::new(Language::new( + let mut language = Language::new( LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(lsp_config), ..Default::default() }, Some(tree_sitter_rust::language()), - )); + ); + let mut fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter { + disk_based_diagnostics_sources: &["disk"], + ..Default::default() + }); let text = " fn a() { A } @@ -5246,7 +5342,7 @@ mod tests { fs.insert_tree("/dir", json!({ "a.rs": text })).await; let project = Project::test(fs, cx); - project.update(cx, |project, _| project.languages.add(language)); + project.update(cx, |project, _| project.languages.add(Arc::new(language))); let worktree_id = project .update(cx, |project, cx| { @@ -5595,16 +5691,15 @@ mod tests { async fn test_edits_from_lsp_with_past_version(cx: &mut gpui::TestAppContext) { cx.foreground().forbid_parking(); - let (lsp_config, mut fake_servers) = LanguageServerConfig::fake(); - let language = Arc::new(Language::new( + let mut language = Language::new( LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(lsp_config), ..Default::default() }, Some(tree_sitter_rust::language()), - )); + ); + let mut fake_servers = language.set_fake_lsp_adapter(Default::default()); let text = " fn a() { @@ -5629,7 +5724,7 @@ mod tests { .await; let project = Project::test(fs, cx); - project.update(cx, |project, _| project.languages.add(language)); + project.update(cx, |project, _| project.languages.add(Arc::new(language))); let worktree_id = project .update(cx, |project, cx| { @@ -5945,16 +6040,15 @@ mod tests { #[gpui::test] async fn test_definition(cx: &mut gpui::TestAppContext) { - let (language_server_config, mut fake_servers) = LanguageServerConfig::fake(); - let language = Arc::new(Language::new( + let mut language = Language::new( LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), ..Default::default() }, Some(tree_sitter_rust::language()), - )); + ); + let mut fake_servers = language.set_fake_lsp_adapter(Default::default()); let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -5967,9 +6061,7 @@ mod tests { .await; let project = Project::test(fs, cx); - project.update(cx, |project, _| { - Arc::get_mut(&mut project.languages).unwrap().add(language); - }); + project.update(cx, |project, _| project.languages.add(Arc::new(language))); let (tree, _) = project .update(cx, |project, cx| { @@ -5982,19 +6074,11 @@ mod tests { .await; let buffer = project - .update(cx, |project, cx| { - project.open_buffer( - ProjectPath { - worktree_id, - path: Path::new("").into(), - }, - cx, - ) - }) + .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx)) .await .unwrap(); - let mut fake_server = fake_servers.next().await.unwrap(); + let fake_server = fake_servers.next().await.unwrap(); fake_server.handle_request::(|params, _| async move { let params = params.text_document_position_params; assert_eq!( @@ -6003,9 +6087,11 @@ mod tests { ); assert_eq!(params.position, lsp::Position::new(0, 22)); - Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( - lsp::Url::from_file_path("/dir/a.rs").unwrap(), - lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), + Ok(Some(lsp::GotoDefinitionResponse::Scalar( + lsp::Location::new( + lsp::Url::from_file_path("/dir/a.rs").unwrap(), + lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)), + ), ))) }); @@ -6057,6 +6143,133 @@ mod tests { } } + #[gpui::test(iterations = 10)] + async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) { + let mut language = Language::new( + LanguageConfig { + name: "TypeScript".into(), + path_suffixes: vec!["ts".to_string()], + ..Default::default() + }, + None, + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "a.ts": "a", + }), + ) + .await; + + let project = Project::test(fs, cx); + project.update(cx, |project, _| project.languages.add(Arc::new(language))); + + let (tree, _) = project + .update(cx, |project, cx| { + project.find_or_create_local_worktree("/dir", true, cx) + }) + .await + .unwrap(); + let worktree_id = tree.read_with(cx, |tree, _| tree.id()); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + + let buffer = project + .update(cx, |p, cx| p.open_buffer((worktree_id, "a.ts"), cx)) + .await + .unwrap(); + + let fake_server = fake_language_servers.next().await.unwrap(); + + // Language server returns code actions that contain commands, and not edits. + let actions = project.update(cx, |project, cx| project.code_actions(&buffer, 0..0, cx)); + fake_server + .handle_request::(|_, _| async move { + Ok(Some(vec![ + lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction { + title: "The code action".into(), + command: Some(lsp::Command { + title: "The command".into(), + command: "_the/command".into(), + arguments: Some(vec![json!("the-argument")]), + }), + ..Default::default() + }), + lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction { + title: "two".into(), + ..Default::default() + }), + ])) + }) + .next() + .await; + + let action = actions.await.unwrap()[0].clone(); + let apply = project.update(cx, |project, cx| { + project.apply_code_action(buffer.clone(), action, true, cx) + }); + + // Resolving the code action does not populate its edits. In absence of + // edits, we must execute the given command. + fake_server.handle_request::( + |action, _| async move { Ok(action) }, + ); + + // While executing the command, the language server sends the editor + // a `workspaceEdit` request. + fake_server + .handle_request::({ + let fake = fake_server.clone(); + move |params, _| { + assert_eq!(params.command, "_the/command"); + let fake = fake.clone(); + async move { + fake.server + .request::( + lsp::ApplyWorkspaceEditParams { + label: None, + edit: lsp::WorkspaceEdit { + changes: Some( + [( + lsp::Url::from_file_path("/dir/a.ts").unwrap(), + vec![lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(0, 0), + lsp::Position::new(0, 0), + ), + new_text: "X".into(), + }], + )] + .into_iter() + .collect(), + ), + ..Default::default() + }, + }, + ) + .await + .unwrap(); + Ok(Some(json!(null))) + } + } + }) + .next() + .await; + + // Applying the code action returns a project transaction containing the edits + // sent by the language server in its `workspaceEdit` request. + let transaction = apply.await.unwrap(); + assert!(transaction.0.contains_key(&buffer)); + buffer.update(cx, |buffer, cx| { + assert_eq!(buffer.text(), "Xa"); + buffer.undo(cx); + assert_eq!(buffer.text(), "a"); + }); + } + #[gpui::test] async fn test_save_file(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.background()); @@ -6751,9 +6964,7 @@ mod tests { }; project - .update(cx, |p, cx| { - p.update_diagnostics(message, &Default::default(), cx) - }) + .update(cx, |p, cx| p.update_diagnostics(message, &[], cx)) .unwrap(); let buffer = buffer.read_with(cx, |buffer, _| buffer.snapshot()); @@ -6881,16 +7092,15 @@ mod tests { async fn test_rename(cx: &mut gpui::TestAppContext) { cx.foreground().forbid_parking(); - let (language_server_config, mut fake_servers) = LanguageServerConfig::fake(); - let language = Arc::new(Language::new( + let mut language = Language::new( LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), ..Default::default() }, Some(tree_sitter_rust::language()), - )); + ); + let mut fake_servers = language.set_fake_lsp_adapter(Default::default()); let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -6903,9 +7113,7 @@ mod tests { .await; let project = Project::test(fs.clone(), cx); - project.update(cx, |project, _| { - Arc::get_mut(&mut project.languages).unwrap().add(language); - }); + project.update(cx, |project, _| project.languages.add(Arc::new(language))); let (tree, _) = project .update(cx, |project, cx| { @@ -6924,7 +7132,7 @@ mod tests { .await .unwrap(); - let mut fake_server = fake_servers.next().await.unwrap(); + let fake_server = fake_servers.next().await.unwrap(); let response = project.update(cx, |project, cx| { project.prepare_rename(buffer.clone(), 7, cx) @@ -6933,10 +7141,10 @@ mod tests { .handle_request::(|params, _| async move { assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs"); assert_eq!(params.position, lsp::Position::new(0, 7)); - Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( + Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( lsp::Position::new(0, 6), lsp::Position::new(0, 9), - ))) + )))) }) .next() .await @@ -6959,7 +7167,7 @@ mod tests { lsp::Position::new(0, 7) ); assert_eq!(params.new_name, "THREE"); - Some(lsp::WorkspaceEdit { + Ok(Some(lsp::WorkspaceEdit { changes: Some( [ ( @@ -6996,7 +7204,7 @@ mod tests { .collect(), ), ..Default::default() - }) + })) }) .next() .await diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index e86a9a556e..f61d6275c9 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -231,7 +231,7 @@ message GetProjectSymbolsResponse { message Symbol { uint64 source_worktree_id = 1; uint64 worktree_id = 2; - string language_name = 3; + string language_server_name = 3; string name = 4; int32 kind = 5; string path = 6; diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 4d23f61ef7..768432ef75 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -1089,10 +1089,10 @@ mod tests { }; use gpui::{executor, geometry::vector::vec2f, ModelHandle, TestAppContext, ViewHandle}; use language::{ - tree_sitter_rust, Diagnostic, DiagnosticEntry, Language, LanguageConfig, LanguageRegistry, - LanguageServerConfig, OffsetRangeExt, Point, Rope, ToLspPosition, + range_to_lsp, tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language, + LanguageConfig, LanguageRegistry, OffsetRangeExt, Point, Rope, }; - use lsp; + use lsp::{self, FakeLanguageServer}; use parking_lot::Mutex; use postage::barrier; use project::{ @@ -2040,22 +2040,20 @@ mod tests { cx_b: &mut TestAppContext, ) { cx_a.foreground().forbid_parking(); - let mut lang_registry = Arc::new(LanguageRegistry::test()); + let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); // Set up a fake language server. - let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - Arc::get_mut(&mut lang_registry) - .unwrap() - .add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ))); + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); + lang_registry.add(Arc::new(language)); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -2262,29 +2260,29 @@ mod tests { cx_b: &mut TestAppContext, ) { cx_a.foreground().forbid_parking(); - let mut lang_registry = Arc::new(LanguageRegistry::test()); + let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); // Set up a fake language server. - let (mut language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - language_server_config.set_fake_capabilities(lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string()]), + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], ..Default::default() - }), + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string()]), + ..Default::default() + }), + ..Default::default() + }, ..Default::default() }); - Arc::get_mut(&mut lang_registry) - .unwrap() - .add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ))); + lang_registry.add(Arc::new(language)); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -2345,7 +2343,7 @@ mod tests { Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx) }); - let mut fake_language_server = fake_language_servers.next().await.unwrap(); + let fake_language_server = fake_language_servers.next().await.unwrap(); buffer_b .condition(&cx_b, |buffer, _| !buffer.completion_triggers().is_empty()) .await; @@ -2371,7 +2369,7 @@ mod tests { lsp::Position::new(0, 14), ); - Some(lsp::CompletionResponse::Array(vec![ + Ok(Some(lsp::CompletionResponse::Array(vec![ lsp::CompletionItem { label: "first_method(…)".into(), detail: Some("fn(&mut self, B) -> C".into()), @@ -2398,7 +2396,7 @@ mod tests { insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), ..Default::default() }, - ])) + ]))) }) .next() .await @@ -2428,7 +2426,7 @@ mod tests { fake_language_server.handle_request::( |params, _| async move { assert_eq!(params.label, "first_method(…)"); - lsp::CompletionItem { + Ok(lsp::CompletionItem { label: "first_method(…)".into(), detail: Some("fn(&mut self, B) -> C".into()), text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { @@ -2444,7 +2442,7 @@ mod tests { }]), insert_text_format: Some(lsp::InsertTextFormat::SNIPPET), ..Default::default() - } + }) }, ); @@ -2581,22 +2579,20 @@ mod tests { #[gpui::test(iterations = 10)] async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); - let mut lang_registry = Arc::new(LanguageRegistry::test()); + let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); // Set up a fake language server. - let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - Arc::get_mut(&mut lang_registry) - .unwrap() - .add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ))); + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); + lang_registry.add(Arc::new(language)); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -2652,9 +2648,9 @@ mod tests { .await .unwrap(); - let mut fake_language_server = fake_language_servers.next().await.unwrap(); + let fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server.handle_request::(|_, _| async move { - Some(vec![ + Ok(Some(vec![ lsp::TextEdit { range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)), new_text: "h".to_string(), @@ -2663,7 +2659,7 @@ mod tests { range: lsp::Range::new(lsp::Position::new(0, 7), lsp::Position::new(0, 7)), new_text: "y".to_string(), }, - ]) + ])) }); project_b @@ -2681,7 +2677,7 @@ mod tests { #[gpui::test(iterations = 10)] async fn test_definition(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); - let mut lang_registry = Arc::new(LanguageRegistry::test()); + let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); fs.insert_tree( "/root-1", @@ -2700,18 +2696,16 @@ mod tests { .await; // Set up a fake language server. - let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - Arc::get_mut(&mut lang_registry) - .unwrap() - .add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ))); + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); + lang_registry.add(Arc::new(language)); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -2761,12 +2755,14 @@ mod tests { .unwrap(); // Request the definition of a symbol as the guest. - let mut fake_language_server = fake_language_servers.next().await.unwrap(); + let fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server.handle_request::( |_, _| async move { - Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( - lsp::Url::from_file_path("/root-2/b.rs").unwrap(), - lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), + Ok(Some(lsp::GotoDefinitionResponse::Scalar( + lsp::Location::new( + lsp::Url::from_file_path("/root-2/b.rs").unwrap(), + lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), + ), ))) }, ); @@ -2793,9 +2789,11 @@ mod tests { // the previous call to `definition`. fake_language_server.handle_request::( |_, _| async move { - Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( - lsp::Url::from_file_path("/root-2/b.rs").unwrap(), - lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)), + Ok(Some(lsp::GotoDefinitionResponse::Scalar( + lsp::Location::new( + lsp::Url::from_file_path("/root-2/b.rs").unwrap(), + lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)), + ), ))) }, ); @@ -2823,7 +2821,7 @@ mod tests { #[gpui::test(iterations = 10)] async fn test_references(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); - let mut lang_registry = Arc::new(LanguageRegistry::test()); + let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); fs.insert_tree( "/root-1", @@ -2843,18 +2841,16 @@ mod tests { .await; // Set up a fake language server. - let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - Arc::get_mut(&mut lang_registry) - .unwrap() - .add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ))); + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); + lang_registry.add(Arc::new(language)); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -2904,14 +2900,14 @@ mod tests { .unwrap(); // Request references to a symbol as the guest. - let mut fake_language_server = fake_language_servers.next().await.unwrap(); + let fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server.handle_request::( |params, _| async move { assert_eq!( params.text_document_position.text_document.uri.as_str(), "file:///root-1/one.rs" ); - Some(vec![ + Ok(Some(vec![ lsp::Location { uri: lsp::Url::from_file_path("/root-1/two.rs").unwrap(), range: lsp::Range::new( @@ -2933,7 +2929,7 @@ mod tests { lsp::Position::new(0, 40), ), }, - ]) + ])) }, ); @@ -3085,16 +3081,16 @@ mod tests { .await; // Set up a fake language server. - let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - lang_registry.add(Arc::new(Language::new( + let mut language = Language::new( LanguageConfig { name: "Rust".into(), path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), ..Default::default() }, Some(tree_sitter_rust::language()), - ))); + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); + lang_registry.add(Arc::new(language)); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -3144,7 +3140,7 @@ mod tests { .unwrap(); // Request document highlights as the guest. - let mut fake_language_server = fake_language_servers.next().await.unwrap(); + let fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server.handle_request::( |params, _| async move { assert_eq!( @@ -3159,7 +3155,7 @@ mod tests { params.text_document_position_params.position, lsp::Position::new(0, 34) ); - Some(vec![ + Ok(Some(vec![ lsp::DocumentHighlight { kind: Some(lsp::DocumentHighlightKind::WRITE), range: lsp::Range::new( @@ -3181,7 +3177,7 @@ mod tests { lsp::Position::new(0, 47), ), }, - ]) + ])) }, ); @@ -3210,7 +3206,7 @@ mod tests { #[gpui::test(iterations = 10)] async fn test_project_symbols(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); - let mut lang_registry = Arc::new(LanguageRegistry::test()); + let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); fs.insert_tree( "/code", @@ -3230,18 +3226,16 @@ mod tests { .await; // Set up a fake language server. - let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - Arc::get_mut(&mut lang_registry) - .unwrap() - .add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ))); + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); + lang_registry.add(Arc::new(language)); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -3290,11 +3284,11 @@ mod tests { .await .unwrap(); - let mut fake_language_server = fake_language_servers.next().await.unwrap(); + let fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server.handle_request::( |_, _| async move { #[allow(deprecated)] - Some(vec![lsp::SymbolInformation { + Ok(Some(vec![lsp::SymbolInformation { name: "TWO".into(), location: lsp::Location { uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(), @@ -3304,7 +3298,7 @@ mod tests { tags: None, container_name: None, deprecated: None, - }]) + }])) }, ); @@ -3349,7 +3343,7 @@ mod tests { mut rng: StdRng, ) { cx_a.foreground().forbid_parking(); - let mut lang_registry = Arc::new(LanguageRegistry::test()); + let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); fs.insert_tree( "/root", @@ -3362,19 +3356,16 @@ mod tests { .await; // Set up a fake language server. - let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - - Arc::get_mut(&mut lang_registry) - .unwrap() - .add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ))); + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); + lang_registry.add(Arc::new(language)); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -3423,12 +3414,14 @@ mod tests { .await .unwrap(); - let mut fake_language_server = fake_language_servers.next().await.unwrap(); + let fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server.handle_request::( |_, _| async move { - Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( - lsp::Url::from_file_path("/root/b.rs").unwrap(), - lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), + Ok(Some(lsp::GotoDefinitionResponse::Scalar( + lsp::Location::new( + lsp::Url::from_file_path("/root/b.rs").unwrap(), + lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), + ), ))) }, ); @@ -3455,23 +3448,21 @@ mod tests { cx_b: &mut TestAppContext, ) { cx_a.foreground().forbid_parking(); - let mut lang_registry = Arc::new(LanguageRegistry::test()); + let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); cx_b.update(|cx| editor::init(cx)); // Set up a fake language server. - let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - Arc::get_mut(&mut lang_registry) - .unwrap() - .add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ))); + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); + lang_registry.add(Arc::new(language)); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -3546,7 +3537,7 @@ mod tests { ); assert_eq!(params.range.start, lsp::Position::new(0, 0)); assert_eq!(params.range.end, lsp::Position::new(0, 0)); - None + Ok(None) }) .next() .await; @@ -3566,7 +3557,7 @@ mod tests { assert_eq!(params.range.start, lsp::Position::new(1, 31)); assert_eq!(params.range.end, lsp::Position::new(1, 31)); - Some(vec![lsp::CodeActionOrCommand::CodeAction( + Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction( lsp::CodeAction { title: "Inline into all callers".to_string(), edit: Some(lsp::WorkspaceEdit { @@ -3608,7 +3599,7 @@ mod tests { })), ..Default::default() }, - )]) + )])) }) .next() .await; @@ -3631,7 +3622,7 @@ mod tests { .unwrap(); fake_language_server.handle_request::( |_, _| async move { - lsp::CodeAction { + Ok(lsp::CodeAction { title: "Inline into all callers".to_string(), edit: Some(lsp::WorkspaceEdit { changes: Some( @@ -3663,7 +3654,7 @@ mod tests { ..Default::default() }), ..Default::default() - } + }) }, ); @@ -3691,23 +3682,21 @@ mod tests { #[gpui::test(iterations = 10)] async fn test_collaborating_with_renames(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { cx_a.foreground().forbid_parking(); - let mut lang_registry = Arc::new(LanguageRegistry::test()); + let lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); cx_b.update(|cx| editor::init(cx)); // Set up a fake language server. - let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); - Arc::get_mut(&mut lang_registry) - .unwrap() - .add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), - ..Default::default() - }, - Some(tree_sitter_rust::language()), - ))); + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()); + lang_registry.add(Arc::new(language)); // Connect to a server as 2 clients. let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; @@ -3772,7 +3761,7 @@ mod tests { .unwrap() .downcast::() .unwrap(); - let mut fake_language_server = fake_language_servers.next().await.unwrap(); + let fake_language_server = fake_language_servers.next().await.unwrap(); // Move cursor to a location that can be renamed. let prepare_rename = editor_b.update(cx_b, |editor, cx| { @@ -3784,10 +3773,10 @@ mod tests { .handle_request::(|params, _| async move { assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs"); assert_eq!(params.position, lsp::Position::new(0, 7)); - Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( + Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( lsp::Position::new(0, 6), lsp::Position::new(0, 9), - ))) + )))) }) .next() .await @@ -3821,7 +3810,7 @@ mod tests { lsp::Position::new(0, 6) ); assert_eq!(params.new_name, "THREE"); - Some(lsp::WorkspaceEdit { + Ok(Some(lsp::WorkspaceEdit { changes: Some( [ ( @@ -3858,7 +3847,7 @@ mod tests { .collect(), ), ..Default::default() - }) + })) }) .next() .await @@ -4956,7 +4945,7 @@ mod tests { let rng = Arc::new(Mutex::new(rng)); let guest_lang_registry = Arc::new(LanguageRegistry::test()); - let (language_server_config, _fake_language_servers) = LanguageServerConfig::fake(); + let host_language_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx.background()); fs.insert_tree( @@ -4970,6 +4959,7 @@ mod tests { let operations = Rc::new(Cell::new(0)); let mut server = TestServer::start(cx.foreground(), cx.background()).await; let mut clients = Vec::new(); + let files = Arc::new(Mutex::new(Vec::new())); let mut next_entity_id = 100000; let mut host_cx = TestAppContext::new( @@ -4986,7 +4976,7 @@ mod tests { Project::local( host.client.clone(), host.user_store.clone(), - Arc::new(LanguageRegistry::test()), + host_language_registry.clone(), fs.clone(), cx, ) @@ -5009,9 +4999,136 @@ mod tests { .await .unwrap(); + // Set up fake language servers. + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + None, + ); + let _fake_servers = language.set_fake_lsp_adapter(FakeLspAdapter { + name: "the-fake-language-server", + capabilities: lsp::LanguageServer::full_capabilities(), + initializer: Some(Box::new({ + let rng = rng.clone(); + let files = files.clone(); + let project = host_project.downgrade(); + move |fake_server: &mut FakeLanguageServer| { + fake_server.handle_request::( + |_, _| async move { + Ok(Some(lsp::CompletionResponse::Array(vec![ + lsp::CompletionItem { + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(0, 0), + lsp::Position::new(0, 0), + ), + new_text: "the-new-text".to_string(), + })), + ..Default::default() + }, + ]))) + }, + ); + + fake_server.handle_request::( + |_, _| async move { + Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction( + lsp::CodeAction { + title: "the-code-action".to_string(), + ..Default::default() + }, + )])) + }, + ); + + fake_server.handle_request::( + |params, _| async move { + Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( + params.position, + params.position, + )))) + }, + ); + + fake_server.handle_request::({ + let files = files.clone(); + let rng = rng.clone(); + move |_, _| { + let files = files.clone(); + let rng = rng.clone(); + async move { + let files = files.lock(); + let mut rng = rng.lock(); + let count = rng.gen_range::(1..3); + let files = (0..count) + .map(|_| files.choose(&mut *rng).unwrap()) + .collect::>(); + log::info!("LSP: Returning definitions in files {:?}", &files); + Ok(Some(lsp::GotoDefinitionResponse::Array( + files + .into_iter() + .map(|file| lsp::Location { + uri: lsp::Url::from_file_path(file).unwrap(), + range: Default::default(), + }) + .collect(), + ))) + } + } + }); + + fake_server.handle_request::({ + let rng = rng.clone(); + let project = project.clone(); + move |params, mut cx| { + let highlights = if let Some(project) = project.upgrade(&cx) { + project.update(&mut cx, |project, cx| { + let path = params + .text_document_position_params + .text_document + .uri + .to_file_path() + .unwrap(); + let (worktree, relative_path) = + project.find_local_worktree(&path, cx)?; + let project_path = + ProjectPath::from((worktree.read(cx).id(), relative_path)); + let buffer = + project.get_open_buffer(&project_path, cx)?.read(cx); + + let mut highlights = Vec::new(); + let highlight_count = rng.lock().gen_range(1..=5); + let mut prev_end = 0; + for _ in 0..highlight_count { + let range = + buffer.random_byte_range(prev_end, &mut *rng.lock()); + + highlights.push(lsp::DocumentHighlight { + range: range_to_lsp(range.to_point_utf16(buffer)), + kind: Some(lsp::DocumentHighlightKind::READ), + }); + prev_end = range.end; + } + Some(highlights) + }) + } else { + None + }; + async move { Ok(highlights) } + } + }); + } + })), + ..Default::default() + }); + host_language_registry.add(Arc::new(language)); + clients.push(cx.foreground().spawn(host.simulate_host( host_project, - language_server_config, + files, operations.clone(), max_operations, rng.clone(), @@ -5442,264 +5559,128 @@ mod tests { }) } - fn simulate_host( + async fn simulate_host( mut self, project: ModelHandle, - mut language_server_config: LanguageServerConfig, + files: Arc>>, operations: Rc>, max_operations: usize, rng: Arc>, mut cx: TestAppContext, - ) -> impl Future { - let files: Arc>> = Default::default(); + ) -> (Self, TestAppContext) { + let fs = project.read_with(&cx, |project, _| project.fs().clone()); + while operations.get() < max_operations { + operations.set(operations.get() + 1); - // Set up a fake language server. - language_server_config.set_fake_initializer({ - let rng = rng.clone(); - let files = files.clone(); - let project = project.downgrade(); - move |fake_server| { - fake_server.handle_request::( - |_, _| async move { - Some(lsp::CompletionResponse::Array(vec![lsp::CompletionItem { - text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - range: lsp::Range::new( - lsp::Position::new(0, 0), - lsp::Position::new(0, 0), - ), - new_text: "the-new-text".to_string(), - })), - ..Default::default() - }])) - }, - ); - - fake_server.handle_request::( - |_, _| async move { - Some(vec![lsp::CodeActionOrCommand::CodeAction( - lsp::CodeAction { - title: "the-code-action".to_string(), - ..Default::default() - }, - )]) - }, - ); - - fake_server.handle_request::( - |params, _| async move { - Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( - params.position, - params.position, - ))) - }, - ); - - fake_server.handle_request::({ - let files = files.clone(); - let rng = rng.clone(); - move |_, _| { - let files = files.clone(); - let rng = rng.clone(); - async move { - let files = files.lock(); - let mut rng = rng.lock(); - let count = rng.gen_range::(1..3); - let files = (0..count) - .map(|_| files.choose(&mut *rng).unwrap()) - .collect::>(); - log::info!("LSP: Returning definitions in files {:?}", &files); - Some(lsp::GotoDefinitionResponse::Array( - files - .into_iter() - .map(|file| lsp::Location { - uri: lsp::Url::from_file_path(file).unwrap(), - range: Default::default(), - }) - .collect(), - )) - } - } - }); - - fake_server.handle_request::({ - let rng = rng.clone(); - let project = project.clone(); - move |params, mut cx| { - let highlights = if let Some(project) = project.upgrade(&cx) { - project.update(&mut cx, |project, cx| { - let path = params - .text_document_position_params - .text_document - .uri - .to_file_path() - .unwrap(); - let (worktree, relative_path) = - project.find_local_worktree(&path, cx)?; - let project_path = - ProjectPath::from((worktree.read(cx).id(), relative_path)); - let buffer = - project.get_open_buffer(&project_path, cx)?.read(cx); - - let mut highlights = Vec::new(); - let highlight_count = rng.lock().gen_range(1..=5); - let mut prev_end = 0; - for _ in 0..highlight_count { - let range = - buffer.random_byte_range(prev_end, &mut *rng.lock()); - let start = buffer - .offset_to_point_utf16(range.start) - .to_lsp_position(); - let end = buffer - .offset_to_point_utf16(range.end) - .to_lsp_position(); - highlights.push(lsp::DocumentHighlight { - range: lsp::Range::new(start, end), - kind: Some(lsp::DocumentHighlightKind::READ), - }); - prev_end = range.end; - } - Some(highlights) - }) - } else { - None - }; - async move { highlights } - } - }); - } - }); - - project.update(&mut cx, |project, _| { - project.languages().add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - path_suffixes: vec!["rs".to_string()], - language_server: Some(language_server_config), - ..Default::default() - }, - None, - ))); - }); - - async move { - let fs = project.read_with(&cx, |project, _| project.fs().clone()); - while operations.get() < max_operations { - operations.set(operations.get() + 1); - - let distribution = rng.lock().gen_range::(0..100); - match distribution { - 0..=20 if !files.lock().is_empty() => { - let path = files.lock().choose(&mut *rng.lock()).unwrap().clone(); - let mut path = path.as_path(); - while let Some(parent_path) = path.parent() { - path = parent_path; - if rng.lock().gen() { - break; - } - } - - log::info!("Host: find/create local worktree {:?}", path); - let find_or_create_worktree = project.update(&mut cx, |project, cx| { - project.find_or_create_local_worktree(path, true, cx) - }); - let find_or_create_worktree = async move { - find_or_create_worktree.await.unwrap(); - }; + let distribution = rng.lock().gen_range::(0..100); + match distribution { + 0..=20 if !files.lock().is_empty() => { + let path = files.lock().choose(&mut *rng.lock()).unwrap().clone(); + let mut path = path.as_path(); + while let Some(parent_path) = path.parent() { + path = parent_path; if rng.lock().gen() { - cx.background().spawn(find_or_create_worktree).detach(); - } else { - find_or_create_worktree.await; - } - } - 10..=80 if !files.lock().is_empty() => { - let buffer = if self.buffers.is_empty() || rng.lock().gen() { - let file = files.lock().choose(&mut *rng.lock()).unwrap().clone(); - let (worktree, path) = project - .update(&mut cx, |project, cx| { - project.find_or_create_local_worktree( - file.clone(), - true, - cx, - ) - }) - .await - .unwrap(); - let project_path = - worktree.read_with(&cx, |worktree, _| (worktree.id(), path)); - log::info!( - "Host: opening path {:?}, worktree {}, relative_path {:?}", - file, - project_path.0, - project_path.1 - ); - let buffer = project - .update(&mut cx, |project, cx| { - project.open_buffer(project_path, cx) - }) - .await - .unwrap(); - self.buffers.insert(buffer.clone()); - buffer - } else { - self.buffers - .iter() - .choose(&mut *rng.lock()) - .unwrap() - .clone() - }; - - if rng.lock().gen_bool(0.1) { - cx.update(|cx| { - log::info!( - "Host: dropping buffer {:?}", - buffer.read(cx).file().unwrap().full_path(cx) - ); - self.buffers.remove(&buffer); - drop(buffer); - }); - } else { - buffer.update(&mut cx, |buffer, cx| { - log::info!( - "Host: updating buffer {:?} ({})", - buffer.file().unwrap().full_path(cx), - buffer.remote_id() - ); - buffer.randomly_edit(&mut *rng.lock(), 5, cx) - }); - } - } - _ => loop { - let path_component_count = rng.lock().gen_range::(1..=5); - let mut path = PathBuf::new(); - path.push("/"); - for _ in 0..path_component_count { - let letter = rng.lock().gen_range(b'a'..=b'z'); - path.push(std::str::from_utf8(&[letter]).unwrap()); - } - path.set_extension("rs"); - let parent_path = path.parent().unwrap(); - - log::info!("Host: creating file {:?}", path,); - - if fs.create_dir(&parent_path).await.is_ok() - && fs.create_file(&path, Default::default()).await.is_ok() - { - files.lock().push(path); break; - } else { - log::info!("Host: cannot create file"); } - }, - } + } - cx.background().simulate_random_delay().await; + log::info!("Host: find/create local worktree {:?}", path); + let find_or_create_worktree = project.update(&mut cx, |project, cx| { + project.find_or_create_local_worktree(path, true, cx) + }); + let find_or_create_worktree = async move { + find_or_create_worktree.await.unwrap(); + }; + if rng.lock().gen() { + cx.background().spawn(find_or_create_worktree).detach(); + } else { + find_or_create_worktree.await; + } + } + 10..=80 if !files.lock().is_empty() => { + let buffer = if self.buffers.is_empty() || rng.lock().gen() { + let file = files.lock().choose(&mut *rng.lock()).unwrap().clone(); + let (worktree, path) = project + .update(&mut cx, |project, cx| { + project.find_or_create_local_worktree(file.clone(), true, cx) + }) + .await + .unwrap(); + let project_path = + worktree.read_with(&cx, |worktree, _| (worktree.id(), path)); + log::info!( + "Host: opening path {:?}, worktree {}, relative_path {:?}", + file, + project_path.0, + project_path.1 + ); + let buffer = project + .update(&mut cx, |project, cx| { + project.open_buffer(project_path, cx) + }) + .await + .unwrap(); + self.buffers.insert(buffer.clone()); + buffer + } else { + self.buffers + .iter() + .choose(&mut *rng.lock()) + .unwrap() + .clone() + }; + + if rng.lock().gen_bool(0.1) { + cx.update(|cx| { + log::info!( + "Host: dropping buffer {:?}", + buffer.read(cx).file().unwrap().full_path(cx) + ); + self.buffers.remove(&buffer); + drop(buffer); + }); + } else { + buffer.update(&mut cx, |buffer, cx| { + log::info!( + "Host: updating buffer {:?} ({})", + buffer.file().unwrap().full_path(cx), + buffer.remote_id() + ); + buffer.randomly_edit(&mut *rng.lock(), 5, cx) + }); + } + } + _ => loop { + let path_component_count = rng.lock().gen_range::(1..=5); + let mut path = PathBuf::new(); + path.push("/"); + for _ in 0..path_component_count { + let letter = rng.lock().gen_range(b'a'..=b'z'); + path.push(std::str::from_utf8(&[letter]).unwrap()); + } + path.set_extension("rs"); + let parent_path = path.parent().unwrap(); + + log::info!("Host: creating file {:?}", path,); + + if fs.create_dir(&parent_path).await.is_ok() + && fs.create_file(&path, Default::default()).await.is_ok() + { + files.lock().push(path); + break; + } else { + log::info!("Host: cannot create file"); + } + }, } - log::info!("Host done"); - - self.project = Some(project); - (self, cx) + cx.background().simulate_random_delay().await; } + + log::info!("Host done"); + + self.project = Some(project); + (self, cx) } pub async fn simulate_guest( diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index 7961dccd56..9348ff0ba6 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -164,6 +164,55 @@ fn test_line_len() { assert_eq!(buffer.line_len(5), 0); } +#[test] +fn test_common_prefix_at_positionn() { + let text = "a = str; b = δα"; + let buffer = Buffer::new(0, 0, History::new(text.into())); + + let offset1 = offset_after(text, "str"); + let offset2 = offset_after(text, "δα"); + + // the preceding word is a prefix of the suggestion + assert_eq!( + buffer.common_prefix_at(offset1, "string"), + range_of(text, "str"), + ); + // a suffix of the preceding word is a prefix of the suggestion + assert_eq!( + buffer.common_prefix_at(offset1, "tree"), + range_of(text, "tr"), + ); + // the preceding word is a substring of the suggestion, but not a prefix + assert_eq!( + buffer.common_prefix_at(offset1, "astro"), + empty_range_after(text, "str"), + ); + + // prefix matching is case insenstive. + assert_eq!( + buffer.common_prefix_at(offset1, "Strαngε"), + range_of(text, "str"), + ); + assert_eq!( + buffer.common_prefix_at(offset2, "ΔΑΜΝ"), + range_of(text, "δα"), + ); + + fn offset_after(text: &str, part: &str) -> usize { + text.find(part).unwrap() + part.len() + } + + fn empty_range_after(text: &str, part: &str) -> Range { + let offset = offset_after(text, part); + offset..offset + } + + fn range_of(text: &str, part: &str) -> Range { + let start = text.find(part).unwrap(); + start..start + part.len() + } +} + #[test] fn test_text_summary_for_range() { let buffer = Buffer::new(0, 0, History::new("ab\nefg\nhklm\nnopqrs\ntuvwxyz".into())); diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index b811d08c04..1c351079a7 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -1508,6 +1508,30 @@ impl BufferSnapshot { .eq(needle.bytes()) } + pub fn common_prefix_at(&self, position: T, needle: &str) -> Range + where + T: ToOffset + TextDimension, + { + let offset = position.to_offset(self); + let common_prefix_len = needle + .char_indices() + .map(|(index, _)| index) + .chain([needle.len()]) + .take_while(|&len| len <= offset) + .filter(|&len| { + let left = self + .chars_for_range(offset - len..offset) + .flat_map(|c| char::to_lowercase(c)); + let right = needle[..len].chars().flat_map(|c| char::to_lowercase(c)); + left.eq(right) + }) + .last() + .unwrap_or(0); + let start_offset = offset - common_prefix_len; + let start = self.text_summary_for_range(0..start_offset); + start..position + } + pub fn text(&self) -> String { self.visible_text.to_string() } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index ede24aae71..68d23b44f8 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -99,6 +99,7 @@ tree-sitter-c = "0.20.1" tree-sitter-json = "0.19.0" tree-sitter-rust = "0.20.1" tree-sitter-markdown = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "330ecab87a3e3a7211ac69bbadc19eabecdb1cca" } +tree-sitter-typescript = "0.20.1" url = "2.2" [dev-dependencies] diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs new file mode 100644 index 0000000000..75a5030ec6 --- /dev/null +++ b/crates/zed/src/languages.rs @@ -0,0 +1,112 @@ +use gpui::Task; +pub use language::*; +use rust_embed::RustEmbed; +use std::{borrow::Cow, str, sync::Arc}; + +mod c; +mod installation; +mod json; +mod rust; +mod typescript; + +#[derive(RustEmbed)] +#[folder = "src/languages"] +#[exclude = "*.rs"] +struct LanguageDir; + +pub fn build_language_registry(login_shell_env_loaded: Task<()>) -> LanguageRegistry { + let languages = LanguageRegistry::new(login_shell_env_loaded); + for (name, grammar, lsp_adapter) in [ + ( + "c", + tree_sitter_c::language(), + Some(Arc::new(c::CLspAdapter) as Arc), + ), + ( + "json", + tree_sitter_json::language(), + Some(Arc::new(json::JsonLspAdapter)), + ), + ( + "markdown", + tree_sitter_markdown::language(), + None, // + ), + ( + "rust", + tree_sitter_rust::language(), + Some(Arc::new(rust::RustLspAdapter)), + ), + ( + "tsx", + tree_sitter_typescript::language_tsx(), + Some(Arc::new(typescript::TypeScriptLspAdapter)), + ), + ( + "typescript", + tree_sitter_typescript::language_typescript(), + Some(Arc::new(typescript::TypeScriptLspAdapter)), + ), + ] { + languages.add(Arc::new(language(name, grammar, lsp_adapter))); + } + languages +} + +fn language( + name: &str, + grammar: tree_sitter::Language, + lsp_adapter: Option>, +) -> Language { + let config = toml::from_slice( + &LanguageDir::get(&format!("{}/config.toml", name)) + .unwrap() + .data, + ) + .unwrap(); + let mut language = Language::new(config, Some(grammar)); + + if let Some(query) = load_query(name, "/highlights") { + language = language + .with_highlights_query(query.as_ref()) + .expect("failed to evaluate highlights query"); + } + if let Some(query) = load_query(name, "/brackets") { + language = language + .with_brackets_query(query.as_ref()) + .expect("failed to load brackets query"); + } + if let Some(query) = load_query(name, "/indents") { + language = language + .with_indents_query(query.as_ref()) + .expect("failed to load indents query"); + } + if let Some(query) = load_query(name, "/outline") { + language = language + .with_outline_query(query.as_ref()) + .expect("failed to load outline query"); + } + if let Some(lsp_adapter) = lsp_adapter { + language = language.with_lsp_adapter(lsp_adapter) + } + language +} + +fn load_query(name: &str, filename_prefix: &str) -> Option> { + let mut result = None; + for path in LanguageDir::iter() { + if let Some(remainder) = path.strip_prefix(name) { + if remainder.starts_with(filename_prefix) { + let contents = match LanguageDir::get(path.as_ref()).unwrap().data { + Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()), + Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()), + }; + match &mut result { + None => result = Some(contents), + Some(r) => r.to_mut().push_str(contents.as_ref()), + } + } + } + } + result +} diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs new file mode 100644 index 0000000000..f2ce41a237 --- /dev/null +++ b/crates/zed/src/languages/c.rs @@ -0,0 +1,114 @@ +use super::installation::{latest_github_release, GitHubLspBinaryVersion}; +use anyhow::{anyhow, Result}; +use client::http::{HttpClient, Method}; +use futures::{future::BoxFuture, FutureExt, StreamExt}; +pub use language::*; +use smol::fs::{self, File}; +use std::{any::Any, path::PathBuf, sync::Arc}; +use util::{ResultExt, TryFutureExt}; + +pub struct CLspAdapter; + +impl super::LspAdapter for CLspAdapter { + fn name(&self) -> LanguageServerName { + LanguageServerName("clangd".into()) + } + + fn fetch_latest_server_version( + &self, + http: Arc, + ) -> BoxFuture<'static, Result>> { + async move { + let version = latest_github_release("clangd/clangd", http, |release_name| { + format!("clangd-mac-{release_name}.zip") + }) + .await?; + Ok(Box::new(version) as Box<_>) + } + .boxed() + } + + fn fetch_server_binary( + &self, + version: Box, + http: Arc, + container_dir: PathBuf, + ) -> BoxFuture<'static, Result> { + let version = version.downcast::().unwrap(); + async move { + let zip_path = container_dir.join(format!("clangd_{}.zip", version.name)); + let version_dir = container_dir.join(format!("clangd_{}", version.name)); + let binary_path = version_dir.join("bin/clangd"); + + if fs::metadata(&binary_path).await.is_err() { + let response = http + .send( + surf::RequestBuilder::new(Method::Get, version.url) + .middleware(surf::middleware::Redirect::default()) + .build(), + ) + .await + .map_err(|err| anyhow!("error downloading release: {}", err))?; + let mut file = File::create(&zip_path).await?; + if !response.status().is_success() { + Err(anyhow!( + "download failed with status {}", + response.status().to_string() + ))?; + } + futures::io::copy(response, &mut file).await?; + + let unzip_status = smol::process::Command::new("unzip") + .current_dir(&container_dir) + .arg(&zip_path) + .output() + .await? + .status; + if !unzip_status.success() { + Err(anyhow!("failed to unzip clangd archive"))?; + } + + if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { + while let Some(entry) = entries.next().await { + if let Some(entry) = entry.log_err() { + let entry_path = entry.path(); + if entry_path.as_path() != version_dir { + fs::remove_dir_all(&entry_path).await.log_err(); + } + } + } + } + } + + Ok(binary_path) + } + .boxed() + } + + fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option> { + async move { + let mut last_clangd_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_clangd_dir = Some(entry.path()); + } + } + let clangd_dir = last_clangd_dir.ok_or_else(|| anyhow!("no cached binary"))?; + let clangd_bin = clangd_dir.join("bin/clangd"); + if clangd_bin.exists() { + Ok(clangd_bin) + } else { + Err(anyhow!( + "missing clangd binary in directory {:?}", + clangd_dir + )) + } + } + .log_err() + .boxed() + } + + fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} +} diff --git a/crates/zed/languages/c/brackets.scm b/crates/zed/src/languages/c/brackets.scm similarity index 100% rename from crates/zed/languages/c/brackets.scm rename to crates/zed/src/languages/c/brackets.scm diff --git a/crates/zed/languages/c/config.toml b/crates/zed/src/languages/c/config.toml similarity index 88% rename from crates/zed/languages/c/config.toml rename to crates/zed/src/languages/c/config.toml index aeee919ac6..b7e1f07443 100644 --- a/crates/zed/languages/c/config.toml +++ b/crates/zed/src/languages/c/config.toml @@ -9,6 +9,3 @@ brackets = [ { start = "\"", end = "\"", close = true, newline = false }, { start = "/*", end = " */", close = true, newline = false }, ] - -[language_server] -disk_based_diagnostic_sources = [] \ No newline at end of file diff --git a/crates/zed/languages/c/highlights.scm b/crates/zed/src/languages/c/highlights.scm similarity index 100% rename from crates/zed/languages/c/highlights.scm rename to crates/zed/src/languages/c/highlights.scm diff --git a/crates/zed/languages/c/indents.scm b/crates/zed/src/languages/c/indents.scm similarity index 100% rename from crates/zed/languages/c/indents.scm rename to crates/zed/src/languages/c/indents.scm diff --git a/crates/zed/languages/c/outline.scm b/crates/zed/src/languages/c/outline.scm similarity index 100% rename from crates/zed/languages/c/outline.scm rename to crates/zed/src/languages/c/outline.scm diff --git a/crates/zed/src/languages/installation.rs b/crates/zed/src/languages/installation.rs new file mode 100644 index 0000000000..212ff472fc --- /dev/null +++ b/crates/zed/src/languages/installation.rs @@ -0,0 +1,111 @@ +use anyhow::{anyhow, Context, Result}; +use client::http::{self, HttpClient, Method}; +use serde::Deserialize; +use std::{path::Path, sync::Arc}; + +pub struct GitHubLspBinaryVersion { + pub name: String, + pub url: http::Url, +} + +#[derive(Deserialize)] +#[serde(rename_all = "kebab-case")] +struct NpmInfo { + #[serde(default)] + dist_tags: NpmInfoDistTags, + versions: Vec, +} + +#[derive(Deserialize, Default)] +struct NpmInfoDistTags { + latest: Option, +} + +#[derive(Deserialize)] +pub(crate) struct GithubRelease { + name: String, + assets: Vec, +} + +#[derive(Deserialize)] +pub(crate) struct GithubReleaseAsset { + name: String, + browser_download_url: http::Url, +} + +pub async fn npm_package_latest_version(name: &str) -> Result { + let output = smol::process::Command::new("npm") + .args(["info", name, "--json"]) + .output() + .await?; + if !output.status.success() { + Err(anyhow!( + "failed to execute npm info: {:?}", + String::from_utf8_lossy(&output.stderr) + ))?; + } + let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?; + info.dist_tags + .latest + .or_else(|| info.versions.pop()) + .ok_or_else(|| anyhow!("no version found for npm package {}", name)) +} + +pub async fn npm_install_packages( + packages: impl IntoIterator, + directory: &Path, +) -> Result<()> { + let output = smol::process::Command::new("npm") + .arg("install") + .arg("--prefix") + .arg(directory) + .args( + packages + .into_iter() + .map(|(name, version)| format!("{name}@{version}")), + ) + .output() + .await + .context("failed to run npm install")?; + if !output.status.success() { + Err(anyhow!( + "failed to execute npm install: {:?}", + String::from_utf8_lossy(&output.stderr) + ))?; + } + Ok(()) +} + +pub async fn latest_github_release( + repo_name_with_owner: &str, + http: Arc, + asset_name: impl Fn(&str) -> String, +) -> Result { + let release = http + .send( + surf::RequestBuilder::new( + Method::Get, + http::Url::parse(&format!( + "https://api.github.com/repos/{repo_name_with_owner}/releases/latest" + )) + .unwrap(), + ) + .middleware(surf::middleware::Redirect::default()) + .build(), + ) + .await + .map_err(|err| anyhow!("error fetching latest release: {}", err))? + .body_json::() + .await + .map_err(|err| anyhow!("error parsing latest release: {}", err))?; + let asset_name = asset_name(&release.name); + let asset = release + .assets + .iter() + .find(|asset| asset.name == asset_name) + .ok_or_else(|| anyhow!("no asset found matching {:?}", asset_name))?; + Ok(GitHubLspBinaryVersion { + name: release.name, + url: asset.browser_download_url.clone(), + }) +} diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs new file mode 100644 index 0000000000..4069413f11 --- /dev/null +++ b/crates/zed/src/languages/json.rs @@ -0,0 +1,130 @@ +use anyhow::{anyhow, Context, Result}; +use client::http::HttpClient; +use futures::{future::BoxFuture, FutureExt, StreamExt}; +use language::{LanguageServerName, LspAdapter}; +use serde::Deserialize; +use serde_json::json; +use smol::fs; +use std::{any::Any, path::PathBuf, sync::Arc}; +use util::{ResultExt, TryFutureExt}; + +pub struct JsonLspAdapter; + +impl JsonLspAdapter { + const BIN_PATH: &'static str = + "node_modules/vscode-json-languageserver/bin/vscode-json-languageserver"; +} + +impl LspAdapter for JsonLspAdapter { + fn name(&self) -> LanguageServerName { + LanguageServerName("vscode-json-languageserver".into()) + } + + fn server_args(&self) -> &[&str] { + &["--stdio"] + } + + fn fetch_latest_server_version( + &self, + _: Arc, + ) -> BoxFuture<'static, Result>> { + async move { + #[derive(Deserialize)] + struct NpmInfo { + versions: Vec, + } + + let output = smol::process::Command::new("npm") + .args(["info", "vscode-json-languageserver", "--json"]) + .output() + .await?; + if !output.status.success() { + Err(anyhow!("failed to execute npm info"))?; + } + let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?; + + Ok(Box::new( + info.versions + .pop() + .ok_or_else(|| anyhow!("no versions found in npm info"))?, + ) as Box<_>) + } + .boxed() + } + + fn fetch_server_binary( + &self, + version: Box, + _: Arc, + container_dir: PathBuf, + ) -> BoxFuture<'static, Result> { + let version = version.downcast::().unwrap(); + async move { + let version_dir = container_dir.join(version.as_str()); + fs::create_dir_all(&version_dir) + .await + .context("failed to create version directory")?; + let binary_path = version_dir.join(Self::BIN_PATH); + + if fs::metadata(&binary_path).await.is_err() { + let output = smol::process::Command::new("npm") + .current_dir(&version_dir) + .arg("install") + .arg(format!("vscode-json-languageserver@{}", version)) + .output() + .await + .context("failed to run npm install")?; + if !output.status.success() { + Err(anyhow!("failed to install vscode-json-languageserver"))?; + } + + if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { + while let Some(entry) = entries.next().await { + if let Some(entry) = entry.log_err() { + let entry_path = entry.path(); + if entry_path.as_path() != version_dir { + fs::remove_dir_all(&entry_path).await.log_err(); + } + } + } + } + } + + Ok(binary_path) + } + .boxed() + } + + fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, 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 bin_path = last_version_dir.join(Self::BIN_PATH); + if bin_path.exists() { + Ok(bin_path) + } else { + Err(anyhow!( + "missing executable in directory {:?}", + last_version_dir + )) + } + } + .log_err() + .boxed() + } + + fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} + + fn initialization_options(&self) -> Option { + Some(json!({ + "provideFormatter": true + })) + } +} diff --git a/crates/zed/languages/json/brackets.scm b/crates/zed/src/languages/json/brackets.scm similarity index 100% rename from crates/zed/languages/json/brackets.scm rename to crates/zed/src/languages/json/brackets.scm diff --git a/crates/zed/languages/json/config.toml b/crates/zed/src/languages/json/config.toml similarity index 83% rename from crates/zed/languages/json/config.toml rename to crates/zed/src/languages/json/config.toml index 27d1193b0d..ad87dcf633 100644 --- a/crates/zed/languages/json/config.toml +++ b/crates/zed/src/languages/json/config.toml @@ -6,6 +6,3 @@ brackets = [ { start = "[", end = "]", close = true, newline = true }, { start = "\"", end = "\"", close = true, newline = false }, ] - -[language_server] -disk_based_diagnostic_sources = [] \ No newline at end of file diff --git a/crates/zed/languages/json/highlights.scm b/crates/zed/src/languages/json/highlights.scm similarity index 100% rename from crates/zed/languages/json/highlights.scm rename to crates/zed/src/languages/json/highlights.scm diff --git a/crates/zed/languages/json/indents.scm b/crates/zed/src/languages/json/indents.scm similarity index 100% rename from crates/zed/languages/json/indents.scm rename to crates/zed/src/languages/json/indents.scm diff --git a/crates/zed/languages/json/outline.scm b/crates/zed/src/languages/json/outline.scm similarity index 100% rename from crates/zed/languages/json/outline.scm rename to crates/zed/src/languages/json/outline.scm diff --git a/crates/zed/languages/markdown/config.toml b/crates/zed/src/languages/markdown/config.toml similarity index 100% rename from crates/zed/languages/markdown/config.toml rename to crates/zed/src/languages/markdown/config.toml diff --git a/crates/zed/languages/markdown/highlights.scm b/crates/zed/src/languages/markdown/highlights.scm similarity index 100% rename from crates/zed/languages/markdown/highlights.scm rename to crates/zed/src/languages/markdown/highlights.scm diff --git a/crates/zed/src/language.rs b/crates/zed/src/languages/rust.rs similarity index 52% rename from crates/zed/src/language.rs rename to crates/zed/src/languages/rust.rs index 0d69ebee69..f419f59abb 100644 --- a/crates/zed/src/language.rs +++ b/crates/zed/src/languages/rust.rs @@ -1,92 +1,50 @@ -use anyhow::{anyhow, Context, Result}; +use super::installation::{latest_github_release, GitHubLspBinaryVersion}; +use anyhow::{anyhow, Result}; use async_compression::futures::bufread::GzipDecoder; -use client::http::{self, HttpClient, Method}; +use client::http::{HttpClient, Method}; use futures::{future::BoxFuture, FutureExt, StreamExt}; -use gpui::Task; pub use language::*; use lazy_static::lazy_static; use regex::Regex; -use rust_embed::RustEmbed; -use serde::Deserialize; -use serde_json::json; use smol::fs::{self, File}; -use std::{borrow::Cow, env::consts, path::PathBuf, str, sync::Arc}; +use std::{any::Any, borrow::Cow, env::consts, path::PathBuf, str, sync::Arc}; use util::{ResultExt, TryFutureExt}; -#[derive(RustEmbed)] -#[folder = "languages"] -struct LanguageDir; - -struct RustLspAdapter; -struct CLspAdapter; -struct JsonLspAdapter; - -#[derive(Deserialize)] -struct GithubRelease { - name: String, - assets: Vec, -} - -#[derive(Deserialize)] -struct GithubReleaseAsset { - name: String, - browser_download_url: http::Url, -} +pub struct RustLspAdapter; impl LspAdapter for RustLspAdapter { - fn name(&self) -> &'static str { - "rust-analyzer" + fn name(&self) -> LanguageServerName { + LanguageServerName("rust-analyzer".into()) } fn fetch_latest_server_version( &self, http: Arc, - ) -> BoxFuture<'static, Result> { + ) -> BoxFuture<'static, Result>> { async move { - let release = http - .send( - surf::RequestBuilder::new( - Method::Get, - http::Url::parse( - "https://api.github.com/repos/rust-analyzer/rust-analyzer/releases/latest", - ) - .unwrap(), - ) - .middleware(surf::middleware::Redirect::default()) - .build(), - ) - .await - .map_err(|err| anyhow!("error fetching latest release: {}", err))? - .body_json::() - .await - .map_err(|err| anyhow!("error parsing latest release: {}", err))?; - let asset_name = format!("rust-analyzer-{}-apple-darwin.gz", consts::ARCH); - let asset = release - .assets - .iter() - .find(|asset| asset.name == asset_name) - .ok_or_else(|| anyhow!("no release found matching {:?}", asset_name))?; - Ok(LspBinaryVersion { - name: release.name, - url: Some(asset.browser_download_url.clone()), + let version = latest_github_release("rust-analyzer/rust-analyzer", http, |_| { + format!("rust-analyzer-{}-apple-darwin.gz", consts::ARCH) }) + .await?; + Ok(Box::new(version) as Box<_>) } .boxed() } fn fetch_server_binary( &self, - version: LspBinaryVersion, + version: Box, http: Arc, container_dir: PathBuf, ) -> BoxFuture<'static, Result> { async move { + let version = version.downcast::().unwrap(); let destination_path = container_dir.join(format!("rust-analyzer-{}", version.name)); if fs::metadata(&destination_path).await.is_err() { let response = http .send( - surf::RequestBuilder::new(Method::Get, version.url.unwrap()) + surf::RequestBuilder::new(Method::Get, version.url) .middleware(surf::middleware::Redirect::default()) .build(), ) @@ -131,6 +89,14 @@ impl LspAdapter for RustLspAdapter { .boxed() } + fn disk_based_diagnostic_sources(&self) -> &'static [&'static str] { + &["rustc"] + } + + fn disk_based_diagnostics_progress_token(&self) -> Option<&'static str> { + Some("rustAnalyzer/cargo check") + } + fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) { lazy_static! { static ref REGEX: Regex = Regex::new("(?m)`([^`]+)\n`$").unwrap(); @@ -287,325 +253,11 @@ impl LspAdapter for RustLspAdapter { } } -impl LspAdapter for CLspAdapter { - fn name(&self) -> &'static str { - "clangd" - } - - fn fetch_latest_server_version( - &self, - http: Arc, - ) -> BoxFuture<'static, Result> { - async move { - let release = http - .send( - surf::RequestBuilder::new( - Method::Get, - http::Url::parse( - "https://api.github.com/repos/clangd/clangd/releases/latest", - ) - .unwrap(), - ) - .middleware(surf::middleware::Redirect::default()) - .build(), - ) - .await - .map_err(|err| anyhow!("error fetching latest release: {}", err))? - .body_json::() - .await - .map_err(|err| anyhow!("error parsing latest release: {}", err))?; - let asset_name = format!("clangd-mac-{}.zip", release.name); - let asset = release - .assets - .iter() - .find(|asset| asset.name == asset_name) - .ok_or_else(|| anyhow!("no release found matching {:?}", asset_name))?; - Ok(LspBinaryVersion { - name: release.name, - url: Some(asset.browser_download_url.clone()), - }) - } - .boxed() - } - - fn fetch_server_binary( - &self, - version: LspBinaryVersion, - http: Arc, - container_dir: PathBuf, - ) -> BoxFuture<'static, Result> { - async move { - let zip_path = container_dir.join(format!("clangd_{}.zip", version.name)); - let version_dir = container_dir.join(format!("clangd_{}", version.name)); - let binary_path = version_dir.join("bin/clangd"); - - if fs::metadata(&binary_path).await.is_err() { - let response = http - .send( - surf::RequestBuilder::new(Method::Get, version.url.unwrap()) - .middleware(surf::middleware::Redirect::default()) - .build(), - ) - .await - .map_err(|err| anyhow!("error downloading release: {}", err))?; - let mut file = File::create(&zip_path).await?; - if !response.status().is_success() { - Err(anyhow!( - "download failed with status {}", - response.status().to_string() - ))?; - } - futures::io::copy(response, &mut file).await?; - - let unzip_status = smol::process::Command::new("unzip") - .current_dir(&container_dir) - .arg(&zip_path) - .output() - .await? - .status; - if !unzip_status.success() { - Err(anyhow!("failed to unzip clangd archive"))?; - } - - if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { - while let Some(entry) = entries.next().await { - if let Some(entry) = entry.log_err() { - let entry_path = entry.path(); - if entry_path.as_path() != version_dir { - fs::remove_dir_all(&entry_path).await.log_err(); - } - } - } - } - } - - Ok(binary_path) - } - .boxed() - } - - fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, Option> { - async move { - let mut last_clangd_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_clangd_dir = Some(entry.path()); - } - } - let clangd_dir = last_clangd_dir.ok_or_else(|| anyhow!("no cached binary"))?; - let clangd_bin = clangd_dir.join("bin/clangd"); - if clangd_bin.exists() { - Ok(clangd_bin) - } else { - Err(anyhow!( - "missing clangd binary in directory {:?}", - clangd_dir - )) - } - } - .log_err() - .boxed() - } - - fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} -} - -impl JsonLspAdapter { - const BIN_PATH: &'static str = - "node_modules/vscode-json-languageserver/bin/vscode-json-languageserver"; -} - -impl LspAdapter for JsonLspAdapter { - fn name(&self) -> &'static str { - "vscode-json-languageserver" - } - - fn server_args(&self) -> &[&str] { - &["--stdio"] - } - - fn fetch_latest_server_version( - &self, - _: Arc, - ) -> BoxFuture<'static, Result> { - async move { - #[derive(Deserialize)] - struct NpmInfo { - versions: Vec, - } - - let output = smol::process::Command::new("npm") - .args(["info", "vscode-json-languageserver", "--json"]) - .output() - .await?; - if !output.status.success() { - Err(anyhow!("failed to execute npm info"))?; - } - let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?; - - Ok(LspBinaryVersion { - name: info - .versions - .pop() - .ok_or_else(|| anyhow!("no versions found in npm info"))?, - url: Default::default(), - }) - } - .boxed() - } - - fn fetch_server_binary( - &self, - version: LspBinaryVersion, - _: Arc, - container_dir: PathBuf, - ) -> BoxFuture<'static, Result> { - async move { - let version_dir = container_dir.join(&version.name); - fs::create_dir_all(&version_dir) - .await - .context("failed to create version directory")?; - let binary_path = version_dir.join(Self::BIN_PATH); - - if fs::metadata(&binary_path).await.is_err() { - let output = smol::process::Command::new("npm") - .current_dir(&version_dir) - .arg("install") - .arg(format!("vscode-json-languageserver@{}", version.name)) - .output() - .await - .context("failed to run npm install")?; - if !output.status.success() { - Err(anyhow!("failed to install vscode-json-languageserver"))?; - } - - if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { - while let Some(entry) = entries.next().await { - if let Some(entry) = entry.log_err() { - let entry_path = entry.path(); - if entry_path.as_path() != version_dir { - fs::remove_dir_all(&entry_path).await.log_err(); - } - } - } - } - } - - Ok(binary_path) - } - .boxed() - } - - fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, 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 bin_path = last_version_dir.join(Self::BIN_PATH); - if bin_path.exists() { - Ok(bin_path) - } else { - Err(anyhow!( - "missing executable in directory {:?}", - last_version_dir - )) - } - } - .log_err() - .boxed() - } - - fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} - - fn initialization_options(&self) -> Option { - Some(json!({ - "provideFormatter": true - })) - } -} - -pub fn build_language_registry(login_shell_env_loaded: Task<()>) -> LanguageRegistry { - let languages = LanguageRegistry::new(login_shell_env_loaded); - languages.add(Arc::new(c())); - languages.add(Arc::new(json())); - languages.add(Arc::new(rust())); - languages.add(Arc::new(markdown())); - languages -} - -fn rust() -> Language { - let grammar = tree_sitter_rust::language(); - let config = toml::from_slice(&LanguageDir::get("rust/config.toml").unwrap().data).unwrap(); - Language::new(config, Some(grammar)) - .with_highlights_query(load_query("rust/highlights.scm").as_ref()) - .unwrap() - .with_brackets_query(load_query("rust/brackets.scm").as_ref()) - .unwrap() - .with_indents_query(load_query("rust/indents.scm").as_ref()) - .unwrap() - .with_outline_query(load_query("rust/outline.scm").as_ref()) - .unwrap() - .with_lsp_adapter(RustLspAdapter) -} - -fn c() -> Language { - let grammar = tree_sitter_c::language(); - let config = toml::from_slice(&LanguageDir::get("c/config.toml").unwrap().data).unwrap(); - Language::new(config, Some(grammar)) - .with_highlights_query(load_query("c/highlights.scm").as_ref()) - .unwrap() - .with_brackets_query(load_query("c/brackets.scm").as_ref()) - .unwrap() - .with_indents_query(load_query("c/indents.scm").as_ref()) - .unwrap() - .with_outline_query(load_query("c/outline.scm").as_ref()) - .unwrap() - .with_lsp_adapter(CLspAdapter) -} - -fn json() -> Language { - let grammar = tree_sitter_json::language(); - let config = toml::from_slice(&LanguageDir::get("json/config.toml").unwrap().data).unwrap(); - Language::new(config, Some(grammar)) - .with_highlights_query(load_query("json/highlights.scm").as_ref()) - .unwrap() - .with_brackets_query(load_query("json/brackets.scm").as_ref()) - .unwrap() - .with_indents_query(load_query("json/indents.scm").as_ref()) - .unwrap() - .with_outline_query(load_query("json/outline.scm").as_ref()) - .unwrap() - .with_lsp_adapter(JsonLspAdapter) -} - -fn markdown() -> Language { - let grammar = tree_sitter_markdown::language(); - let config = toml::from_slice(&LanguageDir::get("markdown/config.toml").unwrap().data).unwrap(); - Language::new(config, Some(grammar)) - .with_highlights_query(load_query("markdown/highlights.scm").as_ref()) - .unwrap() -} - -fn load_query(path: &str) -> Cow<'static, str> { - match LanguageDir::get(path).unwrap().data { - Cow::Borrowed(s) => Cow::Borrowed(str::from_utf8(s).unwrap()), - Cow::Owned(s) => Cow::Owned(String::from_utf8(s).unwrap()), - } -} - #[cfg(test)] mod tests { use super::*; + use crate::languages::{language, LspAdapter}; use gpui::color::Color; - use language::LspAdapter; use theme::SyntaxTheme; #[test] @@ -651,7 +303,11 @@ mod tests { #[test] fn test_rust_label_for_completion() { - let language = rust(); + let language = language( + "rust", + tree_sitter_rust::language(), + Some(Arc::new(RustLspAdapter)), + ); let grammar = language.grammar().unwrap(); let theme = SyntaxTheme::new(vec![ ("type".into(), Color::green().into()), @@ -726,7 +382,11 @@ mod tests { #[test] fn test_rust_label_for_symbol() { - let language = rust(); + let language = language( + "rust", + tree_sitter_rust::language(), + Some(Arc::new(RustLspAdapter)), + ); let grammar = language.grammar().unwrap(); let theme = SyntaxTheme::new(vec![ ("type".into(), Color::green().into()), diff --git a/crates/zed/languages/rust/brackets.scm b/crates/zed/src/languages/rust/brackets.scm similarity index 100% rename from crates/zed/languages/rust/brackets.scm rename to crates/zed/src/languages/rust/brackets.scm diff --git a/crates/zed/languages/rust/config.toml b/crates/zed/src/languages/rust/config.toml similarity index 79% rename from crates/zed/languages/rust/config.toml rename to crates/zed/src/languages/rust/config.toml index 97fe231e0f..e4d222cdde 100644 --- a/crates/zed/languages/rust/config.toml +++ b/crates/zed/src/languages/rust/config.toml @@ -10,7 +10,3 @@ brackets = [ { start = "\"", end = "\"", close = true, newline = false }, { start = "/*", end = " */", close = true, newline = false }, ] - -[language_server] -disk_based_diagnostic_sources = ["rustc"] -disk_based_diagnostics_progress_token = "rustAnalyzer/cargo check" diff --git a/crates/zed/languages/rust/highlights.scm b/crates/zed/src/languages/rust/highlights.scm similarity index 100% rename from crates/zed/languages/rust/highlights.scm rename to crates/zed/src/languages/rust/highlights.scm diff --git a/crates/zed/languages/rust/indents.scm b/crates/zed/src/languages/rust/indents.scm similarity index 100% rename from crates/zed/languages/rust/indents.scm rename to crates/zed/src/languages/rust/indents.scm diff --git a/crates/zed/languages/rust/outline.scm b/crates/zed/src/languages/rust/outline.scm similarity index 100% rename from crates/zed/languages/rust/outline.scm rename to crates/zed/src/languages/rust/outline.scm diff --git a/crates/zed/src/languages/tsx/brackets.scm b/crates/zed/src/languages/tsx/brackets.scm new file mode 120000 index 0000000000..e6835c943b --- /dev/null +++ b/crates/zed/src/languages/tsx/brackets.scm @@ -0,0 +1 @@ +../typescript/brackets.scm \ No newline at end of file diff --git a/crates/zed/src/languages/tsx/config.toml b/crates/zed/src/languages/tsx/config.toml new file mode 100644 index 0000000000..a6f4a6d2d0 --- /dev/null +++ b/crates/zed/src/languages/tsx/config.toml @@ -0,0 +1,12 @@ +name = "TSX" +path_suffixes = ["tsx", "js"] +line_comment = "// " +autoclose_before = ";:.,=}])>" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "<", end = ">", close = false, newline = true }, + { start = "\"", end = "\"", close = true, newline = false }, + { start = "/*", end = " */", close = true, newline = false }, +] diff --git a/crates/zed/src/languages/tsx/highlights-jsx.scm b/crates/zed/src/languages/tsx/highlights-jsx.scm new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/zed/src/languages/tsx/highlights.scm b/crates/zed/src/languages/tsx/highlights.scm new file mode 120000 index 0000000000..226302a5d1 --- /dev/null +++ b/crates/zed/src/languages/tsx/highlights.scm @@ -0,0 +1 @@ +../typescript/highlights.scm \ No newline at end of file diff --git a/crates/zed/src/languages/tsx/indents.scm b/crates/zed/src/languages/tsx/indents.scm new file mode 120000 index 0000000000..502c2a060a --- /dev/null +++ b/crates/zed/src/languages/tsx/indents.scm @@ -0,0 +1 @@ +../typescript/indents.scm \ No newline at end of file diff --git a/crates/zed/src/languages/tsx/outline.scm b/crates/zed/src/languages/tsx/outline.scm new file mode 120000 index 0000000000..a0df409fda --- /dev/null +++ b/crates/zed/src/languages/tsx/outline.scm @@ -0,0 +1 @@ +../typescript/outline.scm \ No newline at end of file diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs new file mode 100644 index 0000000000..aca8cdef52 --- /dev/null +++ b/crates/zed/src/languages/typescript.rs @@ -0,0 +1,146 @@ +use super::installation::{npm_install_packages, npm_package_latest_version}; +use anyhow::{anyhow, Context, Result}; +use client::http::HttpClient; +use futures::{future::BoxFuture, FutureExt, StreamExt}; +use language::{LanguageServerName, LspAdapter}; +use serde_json::json; +use smol::fs; +use std::{any::Any, path::PathBuf, sync::Arc}; +use util::{ResultExt, TryFutureExt}; + +pub struct TypeScriptLspAdapter; + +impl TypeScriptLspAdapter { + const BIN_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.js"; +} + +struct Versions { + typescript_version: String, + server_version: String, +} + +impl LspAdapter for TypeScriptLspAdapter { + fn name(&self) -> LanguageServerName { + LanguageServerName("typescript-language-server".into()) + } + + fn server_args(&self) -> &[&str] { + &["--stdio", "--tsserver-path", "node_modules/typescript/lib"] + } + + fn fetch_latest_server_version( + &self, + _: Arc, + ) -> BoxFuture<'static, Result>> { + async move { + Ok(Box::new(Versions { + typescript_version: npm_package_latest_version("typescript").await?, + server_version: npm_package_latest_version("typescript-language-server").await?, + }) as Box<_>) + } + .boxed() + } + + fn fetch_server_binary( + &self, + versions: Box, + _: Arc, + container_dir: PathBuf, + ) -> BoxFuture<'static, Result> { + let versions = versions.downcast::().unwrap(); + async move { + let version_dir = container_dir.join(&format!( + "typescript-{}:server-{}", + versions.typescript_version, versions.server_version + )); + fs::create_dir_all(&version_dir) + .await + .context("failed to create version directory")?; + let binary_path = version_dir.join(Self::BIN_PATH); + + if fs::metadata(&binary_path).await.is_err() { + npm_install_packages( + [ + ("typescript", versions.typescript_version.as_str()), + ( + "typescript-language-server", + &versions.server_version.as_str(), + ), + ], + &version_dir, + ) + .await?; + + if let Some(mut entries) = fs::read_dir(&container_dir).await.log_err() { + while let Some(entry) = entries.next().await { + if let Some(entry) = entry.log_err() { + let entry_path = entry.path(); + if entry_path.as_path() != version_dir { + fs::remove_dir_all(&entry_path).await.log_err(); + } + } + } + } + } + + Ok(binary_path) + } + .boxed() + } + + fn cached_server_binary(&self, container_dir: PathBuf) -> BoxFuture<'static, 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 bin_path = last_version_dir.join(Self::BIN_PATH); + if bin_path.exists() { + Ok(bin_path) + } else { + Err(anyhow!( + "missing executable in directory {:?}", + last_version_dir + )) + } + } + .log_err() + .boxed() + } + + fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} + + fn label_for_completion( + &self, + item: &lsp::CompletionItem, + language: &language::Language, + ) -> Option { + use lsp::CompletionItemKind as Kind; + let len = item.label.len(); + let grammar = language.grammar()?; + let highlight_id = match item.kind? { + Kind::CLASS | Kind::INTERFACE => grammar.highlight_id_for_name("type"), + Kind::CONSTRUCTOR => grammar.highlight_id_for_name("type"), + Kind::CONSTANT => grammar.highlight_id_for_name("constant"), + Kind::FUNCTION | Kind::METHOD => grammar.highlight_id_for_name("function"), + Kind::PROPERTY | Kind::FIELD => grammar.highlight_id_for_name("property"), + _ => None, + }?; + Some(language::CodeLabel { + text: item.label.clone(), + runs: vec![(0..len, highlight_id)], + filter_range: 0..len, + }) + } + + fn initialization_options(&self) -> Option { + Some(json!({ + "provideFormatter": true + })) + } +} diff --git a/crates/zed/src/languages/typescript/brackets.scm b/crates/zed/src/languages/typescript/brackets.scm new file mode 100644 index 0000000000..63395f81d8 --- /dev/null +++ b/crates/zed/src/languages/typescript/brackets.scm @@ -0,0 +1,5 @@ +("(" @open ")" @close) +("[" @open "]" @close) +("{" @open "}" @close) +("<" @open ">" @close) +("\"" @open "\"" @close) diff --git a/crates/zed/src/languages/typescript/config.toml b/crates/zed/src/languages/typescript/config.toml new file mode 100644 index 0000000000..5d491d2d32 --- /dev/null +++ b/crates/zed/src/languages/typescript/config.toml @@ -0,0 +1,12 @@ +name = "TypeScript" +path_suffixes = ["ts"] +line_comment = "// " +autoclose_before = ";:.,=}])>" +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "<", end = ">", close = false, newline = true }, + { start = "\"", end = "\"", close = true, newline = false }, + { start = "/*", end = " */", close = true, newline = false }, +] diff --git a/crates/zed/src/languages/typescript/highlights.scm b/crates/zed/src/languages/typescript/highlights.scm new file mode 100644 index 0000000000..cb4e82b33d --- /dev/null +++ b/crates/zed/src/languages/typescript/highlights.scm @@ -0,0 +1,219 @@ +; Variables + +(identifier) @variable + +; Properties + +(property_identifier) @property + +; Function and method calls + +(call_expression + function: (identifier) @function) + +(call_expression + function: (member_expression + property: (property_identifier) @function.method)) + +; Function and method definitions + +(function + name: (identifier) @function) +(function_declaration + name: (identifier) @function) +(method_definition + name: (property_identifier) @function.method) + +(pair + key: (property_identifier) @function.method + value: [(function) (arrow_function)]) + +(assignment_expression + left: (member_expression + property: (property_identifier) @function.method) + right: [(function) (arrow_function)]) + +(variable_declarator + name: (identifier) @function + value: [(function) (arrow_function)]) + +(assignment_expression + left: (identifier) @function + right: [(function) (arrow_function)]) + +; Special identifiers + +((identifier) @constructor + (#match? @constructor "^[A-Z]")) + +([ + (identifier) + (shorthand_property_identifier) + (shorthand_property_identifier_pattern) + ] @constant + (#match? @constant "^[A-Z_][A-Z\\d_]+$")) + +; Literals + +(this) @variable.builtin +(super) @variable.builtin + +[ + (true) + (false) + (null) + (undefined) +] @constant.builtin + +(comment) @comment + +[ + (string) + (template_string) +] @string + +(regex) @string.special +(number) @number + +; Tokens + +(template_substitution + "${" @punctuation.special + "}" @punctuation.special) @embedded + +[ + ";" + "?." + "." + "," +] @punctuation.delimiter + +[ + "-" + "--" + "-=" + "+" + "++" + "+=" + "*" + "*=" + "**" + "**=" + "/" + "/=" + "%" + "%=" + "<" + "<=" + "<<" + "<<=" + "=" + "==" + "===" + "!" + "!=" + "!==" + "=>" + ">" + ">=" + ">>" + ">>=" + ">>>" + ">>>=" + "~" + "^" + "&" + "|" + "^=" + "&=" + "|=" + "&&" + "||" + "??" + "&&=" + "||=" + "??=" +] @operator + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +[ + "as" + "async" + "await" + "break" + "case" + "catch" + "class" + "const" + "continue" + "debugger" + "default" + "delete" + "do" + "else" + "export" + "extends" + "finally" + "for" + "from" + "function" + "get" + "if" + "import" + "in" + "instanceof" + "let" + "new" + "of" + "return" + "set" + "static" + "switch" + "target" + "throw" + "try" + "typeof" + "var" + "void" + "while" + "with" + "yield" +] @keyword + +; Types + +(type_identifier) @type +(predefined_type) @type.builtin + +((identifier) @type + (#match? @type "^[A-Z]")) + +(type_arguments + "<" @punctuation.bracket + ">" @punctuation.bracket) + +; Keywords + +[ "abstract" + "declare" + "enum" + "export" + "implements" + "interface" + "keyof" + "namespace" + "private" + "protected" + "public" + "type" + "readonly" + "override" +] @keyword \ No newline at end of file diff --git a/crates/zed/src/languages/typescript/indents.scm b/crates/zed/src/languages/typescript/indents.scm new file mode 100644 index 0000000000..107e6ff8e0 --- /dev/null +++ b/crates/zed/src/languages/typescript/indents.scm @@ -0,0 +1,15 @@ +[ + (call_expression) + (assignment_expression) + (member_expression) + (lexical_declaration) + (variable_declaration) + (assignment_expression) + (if_statement) + (for_statement) +] @indent + +(_ "[" "]" @end) @indent +(_ "<" ">" @end) @indent +(_ "{" "}" @end) @indent +(_ "(" ")" @end) @indent diff --git a/crates/zed/src/languages/typescript/outline.scm b/crates/zed/src/languages/typescript/outline.scm new file mode 100644 index 0000000000..f8691fa41d --- /dev/null +++ b/crates/zed/src/languages/typescript/outline.scm @@ -0,0 +1,55 @@ +(internal_module + "namespace" @context + name: (_) @name) @item + +(enum_declaration + "enum" @context + name: (_) @name) @item + +(function_declaration + "async"? @context + "function" @context + name: (_) @name + parameters: (formal_parameters + "(" @context + ")" @context)) @item + +(interface_declaration + "interface" @context + name: (_) @name) @item + +(program + (lexical_declaration + ["let" "const"] @context + (variable_declarator + name: (_) @name) @item)) + +(class_declaration + "class" @context + name: (_) @name) @item + +(method_definition + [ + "get" + "set" + "async" + "*" + "readonly" + "static" + (override_modifier) + (accessibility_modifier) + ]* @context + name: (_) @name + parameters: (formal_parameters + "(" @context + ")" @context)) @item + +(public_field_definition + [ + "declare" + "readonly" + "abstract" + "static" + (accessibility_modifier) + ]* @context + name: (_) @name) @item diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 63721346c3..49efc9ade2 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -19,7 +19,7 @@ use workspace::{ AppState, OpenNew, OpenParams, OpenPaths, Settings, }; use zed::{ - self, assets::Assets, build_window_options, build_workspace, fs::RealFs, language, menus, + self, assets::Assets, build_window_options, build_workspace, fs::RealFs, languages, menus, }; fn main() { @@ -34,7 +34,7 @@ fn main() { let default_settings = Settings::new("Zed Mono", &app.font_cache(), theme) .unwrap() .with_overrides( - language::PLAIN_TEXT.name(), + languages::PLAIN_TEXT.name(), settings::LanguageOverride { soft_wrap: Some(settings::SoftWrap::PreferredLineLength), ..Default::default() @@ -60,7 +60,7 @@ fn main() { app.run(move |cx| { let http = http::client(); let client = client::Client::new(http.clone()); - let mut languages = language::build_language_registry(login_shell_env_loaded); + let mut languages = languages::build_language_registry(login_shell_env_loaded); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); let channel_list = cx.add_model(|cx| ChannelList::new(user_store.clone(), client.clone(), cx)); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 00f57ccccb..7b54b3df9a 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1,5 +1,5 @@ pub mod assets; -pub mod language; +pub mod languages; pub mod menus; #[cfg(any(test, feature = "test-support"))] pub mod test; @@ -574,7 +574,7 @@ mod tests { assert_eq!(editor.title(cx), "untitled"); assert!(Arc::ptr_eq( editor.language(cx).unwrap(), - &language::PLAIN_TEXT + &languages::PLAIN_TEXT )); editor.handle_input(&editor::Input("hi".into()), cx); assert!(editor.is_dirty(cx)); @@ -664,7 +664,7 @@ mod tests { editor.update(cx, |editor, cx| { assert!(Arc::ptr_eq( editor.language(cx).unwrap(), - &language::PLAIN_TEXT + &languages::PLAIN_TEXT )); editor.handle_input(&editor::Input("hi".into()), cx); assert!(editor.is_dirty(cx.as_ref()));