From 01dfd067154b9c78622d7409712b2ed5730e26be Mon Sep 17 00:00:00 2001 From: Julian Date: Fri, 11 Oct 2024 17:42:11 +0200 Subject: [PATCH 01/33] refactor: tokio main, isolate crossbeam channels --- crates/pg_lsp/Cargo.toml | 1 + crates/pg_lsp/src/main.rs | 7 +- crates/pg_lsp/src/server.rs | 184 +++++++++++++++++++++++------------- 3 files changed, 122 insertions(+), 70 deletions(-) diff --git a/crates/pg_lsp/Cargo.toml b/crates/pg_lsp/Cargo.toml index 122e3ccd..84e97785 100644 --- a/crates/pg_lsp/Cargo.toml +++ b/crates/pg_lsp/Cargo.toml @@ -32,6 +32,7 @@ pg_base_db.workspace = true pg_schema_cache.workspace = true pg_workspace.workspace = true pg_diagnostics.workspace = true +tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread", "sync"] } [dev-dependencies] diff --git a/crates/pg_lsp/src/main.rs b/crates/pg_lsp/src/main.rs index eb5eddb6..803e0f39 100644 --- a/crates/pg_lsp/src/main.rs +++ b/crates/pg_lsp/src/main.rs @@ -1,9 +1,12 @@ use lsp_server::Connection; use pg_lsp::server::Server; -fn main() -> anyhow::Result<()> { +#[tokio::main] +async fn main() -> anyhow::Result<()> { let (connection, threads) = Connection::stdio(); - Server::init(connection)?; + let server = Server::init(connection)?; + + server.run().await?; threads.join()?; Ok(()) diff --git a/crates/pg_lsp/src/server.rs b/crates/pg_lsp/src/server.rs index 927d7f16..351112a0 100644 --- a/crates/pg_lsp/src/server.rs +++ b/crates/pg_lsp/src/server.rs @@ -3,7 +3,6 @@ mod dispatch; pub mod options; use async_std::task::{self}; -use crossbeam_channel::{unbounded, Receiver, Sender}; use lsp_server::{Connection, ErrorCode, Message, RequestId}; use lsp_types::{ notification::{ @@ -33,6 +32,8 @@ use std::{collections::HashSet, sync::Arc, time::Duration}; use text_size::TextSize; use threadpool::ThreadPool; +use tokio::sync::{mpsc, oneshot}; + use crate::{ client::{client_flags::ClientFlags, LspClient}, utils::{file_path, from_proto, line_index_ext::LineIndexExt, normalize_uri, to_proto}, @@ -68,11 +69,39 @@ impl DbConnection { } } +/// `lsp-servers` `Connection` type uses a crossbeam channel, which is not compatible with tokio's async runtime. +/// For now, we move it into a separate task and use tokio's channels to communicate. +fn get_client_receiver( + connection: Connection, +) -> (mpsc::UnboundedReceiver, oneshot::Receiver<()>) { + let (message_tx, message_rx) = mpsc::unbounded_channel(); + let (close_tx, close_rx) = oneshot::channel(); + + tokio::task::spawn(async move { + // TODO: improve Result handling + loop { + let msg = connection.receiver.recv().unwrap(); + + match msg { + Message::Request(r) if connection.handle_shutdown(&r).unwrap() => { + close_tx.send(()).unwrap(); + return; + } + + _ => message_tx.send(msg).unwrap(), + }; + } + }); + + (message_rx, close_rx) +} + pub struct Server { - connection: Arc, + client_rx: mpsc::UnboundedReceiver, + close_rx: oneshot::Receiver<()>, client: LspClient, - internal_tx: Sender, - internal_rx: Receiver, + internal_tx: mpsc::UnboundedSender, + internal_rx: mpsc::UnboundedReceiver, pool: Arc, client_flags: Arc, ide: Arc, @@ -81,10 +110,10 @@ pub struct Server { } impl Server { - pub fn init(connection: Connection) -> anyhow::Result<()> { + pub fn init(connection: Connection) -> anyhow::Result { let client = LspClient::new(connection.sender.clone()); - let (internal_tx, internal_rx) = unbounded(); + let (internal_tx, internal_rx) = mpsc::unbounded_channel(); let (id, params) = connection.initialize_start()?; let params: InitializeParams = serde_json::from_value(params)?; @@ -110,8 +139,11 @@ impl Server { let cloned_pool = pool.clone(); let cloned_client = client.clone(); + let (client_rx, close_rx) = get_client_receiver(connection); + let server = Self { - connection: Arc::new(connection), + close_rx, + client_rx, internal_rx, internal_tx, client, @@ -158,8 +190,7 @@ impl Server { pool, }; - server.run()?; - Ok(()) + Ok(server) } fn compute_now(&self) { @@ -763,67 +794,84 @@ impl Server { Ok(()) } - fn process_messages(&mut self) -> anyhow::Result<()> { + async fn process_messages(&mut self) -> anyhow::Result<()> { loop { - crossbeam_channel::select! { - recv(&self.connection.receiver) -> msg => { - match msg? { - Message::Request(request) => { - if self.connection.handle_shutdown(&request)? { - return Ok(()); - } - - if let Some(response) = dispatch::RequestDispatcher::new(request) - .on::(|id, params| self.inlay_hint(id, params))? - .on::(|id, params| self.hover(id, params))? - .on::(|id, params| self.execute_command(id, params))? - .on::(|id, params| { - self.completion(id, params) - })? - .on::(|id, params| { - self.code_actions(id, params) - })? - .default() - { - self.client.send_response(response)?; - } - } - Message::Notification(notification) => { - dispatch::NotificationDispatcher::new(notification) - .on::(|params| { - self.did_change_configuration(params) - })? - .on::(|params| self.did_close(params))? - .on::(|params| self.did_open(params))? - .on::(|params| self.did_change(params))? - .on::(|params| self.did_save(params))? - .on::(|params| self.did_close(params))? - .default(); - } - Message::Response(response) => { - self.client.recv_response(response)?; - } - }; + tokio::select! { + _ = &mut self.close_rx => { + return Ok(()) }, - recv(&self.internal_rx) -> msg => { - match msg? { - InternalMessage::SetSchemaCache(c) => { - self.ide.set_schema_cache(c); - self.compute_now(); - } - InternalMessage::RefreshSchemaCache => { - self.refresh_schema_cache(); - } - InternalMessage::PublishDiagnostics(uri) => { - self.publish_diagnostics(uri)?; - } - InternalMessage::SetOptions(options) => { - self.update_options(options); - } - }; + + msg = self.internal_rx.recv() => { + match msg { + // TODO: handle internal sender close? Is that valid state? + None => return Ok(()), + Some(m) => self.handle_internal_message(m) + } + }, + + msg = self.client_rx.recv() => { + match msg { + // the client sender is closed, we can return + None => return Ok(()), + Some(m) => self.handle_message(m) + } + }, + }?; + } + } + + fn handle_message(&mut self, msg: Message) -> anyhow::Result<()> { + match msg { + Message::Request(request) => { + if let Some(response) = dispatch::RequestDispatcher::new(request) + .on::(|id, params| self.inlay_hint(id, params))? + .on::(|id, params| self.hover(id, params))? + .on::(|id, params| self.execute_command(id, params))? + .on::(|id, params| self.completion(id, params))? + .on::(|id, params| self.code_actions(id, params))? + .default() + { + self.client.send_response(response)?; } - }; + } + Message::Notification(notification) => { + dispatch::NotificationDispatcher::new(notification) + .on::(|params| { + self.did_change_configuration(params) + })? + .on::(|params| self.did_close(params))? + .on::(|params| self.did_open(params))? + .on::(|params| self.did_change(params))? + .on::(|params| self.did_save(params))? + .on::(|params| self.did_close(params))? + .default(); + } + Message::Response(response) => { + self.client.recv_response(response)?; + } } + + Ok(()) + } + + fn handle_internal_message(&mut self, msg: InternalMessage) -> anyhow::Result<()> { + match msg { + InternalMessage::SetSchemaCache(c) => { + self.ide.set_schema_cache(c); + self.compute_now(); + } + InternalMessage::RefreshSchemaCache => { + self.refresh_schema_cache(); + } + InternalMessage::PublishDiagnostics(uri) => { + self.publish_diagnostics(uri)?; + } + InternalMessage::SetOptions(options) => { + self.update_options(options); + } + } + + Ok(()) } fn pull_options(&mut self) { @@ -881,10 +929,10 @@ impl Server { } } - pub fn run(mut self) -> anyhow::Result<()> { + pub async fn run(mut self) -> anyhow::Result<()> { self.register_configuration(); self.pull_options(); - self.process_messages()?; + self.process_messages().await?; self.pool.join(); Ok(()) } From 29fa6299cce3083896a97c32ea43f579e99711e9 Mon Sep 17 00:00:00 2001 From: Julian Date: Sun, 13 Oct 2024 10:40:53 +0200 Subject: [PATCH 02/33] so far --- Cargo.lock | 74 +++++++++++++++++++++++++++++++++++-- crates/pg_lsp/src/server.rs | 22 +++++------ 2 files changed, 81 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0044279e..8fbbee9e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + [[package]] name = "adler" version = "1.0.2" @@ -216,6 +225,21 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "base64" version = "0.21.7" @@ -311,11 +335,11 @@ checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "cc" -version = "1.0.83" +version = "1.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "58e804ac3194a48bb129643eb1d62fcc20d18c6b8c181704489353d13120bcd1" dependencies = [ - "libc", + "shlex", ] [[package]] @@ -816,6 +840,12 @@ dependencies = [ "wasi", ] +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + [[package]] name = "glob" version = "0.3.1" @@ -1250,6 +1280,15 @@ dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.19.0" @@ -1447,6 +1486,7 @@ dependencies = [ "sqlx", "text-size", "threadpool", + "tokio", ] [[package]] @@ -1879,6 +1919,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + [[package]] name = "rustc-hash" version = "1.1.0" @@ -2452,6 +2498,28 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tokio" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +dependencies = [ + "backtrace", + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.71", +] + [[package]] name = "tracing" version = "0.1.40" diff --git a/crates/pg_lsp/src/server.rs b/crates/pg_lsp/src/server.rs index 351112a0..078ed925 100644 --- a/crates/pg_lsp/src/server.rs +++ b/crates/pg_lsp/src/server.rs @@ -758,7 +758,7 @@ impl Server { }); } - fn refresh_schema_cache(&self) { + async fn refresh_schema_cache(&self) { if self.db_conn.is_none() { return; } @@ -767,17 +767,15 @@ impl Server { let conn = self.db_conn.as_ref().unwrap().pool.clone(); let client = self.client.clone(); - async_std::task::spawn(async move { - client - .send_notification::(ShowMessageParams { - typ: lsp_types::MessageType::INFO, - message: "Refreshing schema cache...".to_string(), - }) - .unwrap(); - let schema_cache = SchemaCache::load(&conn).await; - tx.send(InternalMessage::SetSchemaCache(schema_cache)) - .unwrap(); - }); + client + .send_notification::(ShowMessageParams { + typ: lsp_types::MessageType::INFO, + message: "Refreshing schema cache...".to_string(), + }) + .unwrap(); + let schema_cache = SchemaCache::load(&conn).await; + tx.send(InternalMessage::SetSchemaCache(schema_cache)) + .unwrap(); } fn did_change_configuration( From 5742e63a92167fdb358f65f25e4bdf2e0e3b349f Mon Sep 17 00:00:00 2001 From: Julian Date: Sun, 13 Oct 2024 13:32:20 +0200 Subject: [PATCH 03/33] fully remove async_std::task --- crates/pg_lsp/src/server.rs | 40 ++++++++++++++----------------- crates/pg_schema_cache/src/lib.rs | 2 +- xtask/src/install.rs | 5 +--- 3 files changed, 20 insertions(+), 27 deletions(-) diff --git a/crates/pg_lsp/src/server.rs b/crates/pg_lsp/src/server.rs index 078ed925..488732e5 100644 --- a/crates/pg_lsp/src/server.rs +++ b/crates/pg_lsp/src/server.rs @@ -2,7 +2,6 @@ mod debouncer; mod dispatch; pub mod options; -use async_std::task::{self}; use lsp_server::{Connection, ErrorCode, Message, RequestId}; use lsp_types::{ notification::{ @@ -240,7 +239,7 @@ impl Server { }); } - fn start_listening(&self) { + async fn start_listening(&self) { if self.db_conn.is_none() { return; } @@ -248,27 +247,25 @@ impl Server { let pool = self.db_conn.as_ref().unwrap().pool.clone(); let tx = self.internal_tx.clone(); - task::spawn(async move { - let mut listener = PgListener::connect_with(&pool).await.unwrap(); - listener - .listen_all(["postgres_lsp", "pgrst"]) - .await - .unwrap(); + let mut listener = PgListener::connect_with(&pool).await.unwrap(); + listener + .listen_all(["postgres_lsp", "pgrst"]) + .await + .unwrap(); - loop { - match listener.recv().await { - Ok(notification) => { - if notification.payload().to_string() == "reload schema" { - tx.send(InternalMessage::RefreshSchemaCache).unwrap(); - } - } - Err(e) => { - eprintln!("Listener error: {}", e); - break; + loop { + match listener.recv().await { + Ok(notification) => { + if notification.payload().to_string() == "reload schema" { + tx.send(InternalMessage::RefreshSchemaCache).unwrap(); } } + Err(e) => { + eprintln!("Listener error: {}", e); + break; + } } - }); + } } async fn update_db_connection(&mut self, connection_string: Option) { @@ -298,9 +295,8 @@ impl Server { }) .unwrap(); - self.refresh_schema_cache(); - - self.start_listening(); + self.refresh_schema_cache().await; + self.start_listening().await; } fn update_options(&mut self, options: Options) { diff --git a/crates/pg_schema_cache/src/lib.rs b/crates/pg_schema_cache/src/lib.rs index aed612c6..82454fc3 100644 --- a/crates/pg_schema_cache/src/lib.rs +++ b/crates/pg_schema_cache/src/lib.rs @@ -4,11 +4,11 @@ #![feature(future_join)] mod functions; -mod versions; mod schema_cache; mod schemas; mod tables; mod types; +mod versions; use sqlx::postgres::PgPool; diff --git a/xtask/src/install.rs b/xtask/src/install.rs index 85c03e13..c149bd5a 100644 --- a/xtask/src/install.rs +++ b/xtask/src/install.rs @@ -137,10 +137,7 @@ fn install_client(sh: &Shell, client_opt: ClientOpt) -> anyhow::Result<()> { } fn install_server(sh: &Shell) -> anyhow::Result<()> { - let cmd = cmd!( - sh, - "cargo install --path crates/pg_lsp --locked --force" - ); + let cmd = cmd!(sh, "cargo install --path crates/pg_lsp --locked --force"); cmd.run()?; Ok(()) } From f64661e5c299f27a052af423ab9242ddc7d2dea6 Mon Sep 17 00:00:00 2001 From: Julian Date: Sun, 13 Oct 2024 14:06:19 +0200 Subject: [PATCH 04/33] move initializer to ClientFlags --- crates/pg_lsp/src/client/client_flags.rs | 34 +++++++++++++++++++++--- crates/pg_lsp/src/main.rs | 2 +- crates/pg_lsp/src/server.rs | 10 +++---- crates/pg_lsp/src/utils/from_proto.rs | 22 --------------- 4 files changed, 36 insertions(+), 32 deletions(-) diff --git a/crates/pg_lsp/src/client/client_flags.rs b/crates/pg_lsp/src/client/client_flags.rs index 8fca812d..6209f443 100644 --- a/crates/pg_lsp/src/client/client_flags.rs +++ b/crates/pg_lsp/src/client/client_flags.rs @@ -1,10 +1,36 @@ +use lsp_types::InitializeParams; + /// Contains information about the client's capabilities. /// This is used to determine which features the server can use. #[derive(Debug, Clone)] pub struct ClientFlags { - /// If `true`, the server can pull the configuration from the client. - pub configuration_pull: bool, + /// If `true`, the server can pull configuration from the client. + pub has_configuration: bool, + + /// If `true`, the client notifies the server when its configuration changes. + pub will_push_configuration: bool, +} + +impl ClientFlags { + pub(crate) fn from_initialize_request_params(params: &InitializeParams) -> Self { + let has_configuration = params + .capabilities + .workspace + .as_ref() + .and_then(|w| w.configuration) + .unwrap_or(false); + + let will_push_configuration = params + .capabilities + .workspace + .as_ref() + .and_then(|w| w.did_change_configuration) + .and_then(|c| c.dynamic_registration) + .unwrap_or(false); - /// If `true`, the client notifies the server when the configuration changes. - pub configuration_push: bool, + Self { + has_configuration, + will_push_configuration, + } + } } diff --git a/crates/pg_lsp/src/main.rs b/crates/pg_lsp/src/main.rs index 803e0f39..9c678fac 100644 --- a/crates/pg_lsp/src/main.rs +++ b/crates/pg_lsp/src/main.rs @@ -4,8 +4,8 @@ use pg_lsp::server::Server; #[tokio::main] async fn main() -> anyhow::Result<()> { let (connection, threads) = Connection::stdio(); - let server = Server::init(connection)?; + let server = Server::init(connection)?; server.run().await?; threads.join()?; diff --git a/crates/pg_lsp/src/server.rs b/crates/pg_lsp/src/server.rs index 488732e5..1a65dcc0 100644 --- a/crates/pg_lsp/src/server.rs +++ b/crates/pg_lsp/src/server.rs @@ -127,7 +127,7 @@ impl Server { connection.initialize_finish(id, serde_json::to_value(result)?)?; - let client_flags = Arc::new(from_proto::client_flags(params.capabilities)); + let client_flags = Arc::new(ClientFlags::from_initialize_request_params(¶ms)); let pool = Arc::new(threadpool::Builder::new().build()); @@ -200,7 +200,7 @@ impl Server { self.compute_debouncer.clear(); - self.pool.execute(move || { + tokio::spawn(async move { client .send_notification::(ShowMessageParams { typ: lsp_types::MessageType::INFO, @@ -778,7 +778,7 @@ impl Server { &mut self, params: DidChangeConfigurationParams, ) -> anyhow::Result<()> { - if self.client_flags.configuration_pull { + if self.client_flags.has_configuration { self.pull_options(); } else { let options = self.client.parse_options(params.settings)?; @@ -869,7 +869,7 @@ impl Server { } fn pull_options(&mut self) { - if !self.client_flags.configuration_pull { + if !self.client_flags.has_configuration { return; } @@ -899,7 +899,7 @@ impl Server { } fn register_configuration(&mut self) { - if self.client_flags.configuration_push { + if self.client_flags.will_push_configuration { let registration = Registration { id: "pull-config".to_string(), method: DidChangeConfiguration::METHOD.to_string(), diff --git a/crates/pg_lsp/src/utils/from_proto.rs b/crates/pg_lsp/src/utils/from_proto.rs index 47708be7..eaae06ce 100644 --- a/crates/pg_lsp/src/utils/from_proto.rs +++ b/crates/pg_lsp/src/utils/from_proto.rs @@ -1,5 +1,3 @@ -use crate::client::client_flags::ClientFlags; - use super::line_index_ext::LineIndexExt; use pg_base_db::{Change, Document}; @@ -17,23 +15,3 @@ pub fn content_changes( }) .collect() } - -pub fn client_flags(capabilities: lsp_types::ClientCapabilities) -> ClientFlags { - let configuration_pull = capabilities - .workspace - .as_ref() - .and_then(|cap| cap.configuration) - .unwrap_or(false); - - let configuration_push = capabilities - .workspace - .as_ref() - .and_then(|cap| cap.did_change_configuration) - .and_then(|cap| cap.dynamic_registration) - .unwrap_or(false); - - ClientFlags { - configuration_pull, - configuration_push, - } -} From 12c86fd8c39a5087218fafc2c8564a82aa892afe Mon Sep 17 00:00:00 2001 From: Julian Date: Sun, 13 Oct 2024 15:46:28 +0200 Subject: [PATCH 05/33] so far so good --- Cargo.lock | 14 ++++ crates/pg_lsp/Cargo.toml | 1 + crates/pg_lsp/src/server.rs | 146 ++++++++++++++++++++---------------- 3 files changed, 97 insertions(+), 64 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8fbbee9e..fd12e85a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1487,6 +1487,7 @@ dependencies = [ "text-size", "threadpool", "tokio", + "tokio-util", ] [[package]] @@ -2520,6 +2521,19 @@ dependencies = [ "syn 2.0.71", ] +[[package]] +name = "tokio-util" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "tracing" version = "0.1.40" diff --git a/crates/pg_lsp/Cargo.toml b/crates/pg_lsp/Cargo.toml index 84e97785..9d23bce9 100644 --- a/crates/pg_lsp/Cargo.toml +++ b/crates/pg_lsp/Cargo.toml @@ -33,6 +33,7 @@ pg_schema_cache.workspace = true pg_workspace.workspace = true pg_diagnostics.workspace = true tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread", "sync"] } +tokio-util = "0.7.12" [dev-dependencies] diff --git a/crates/pg_lsp/src/server.rs b/crates/pg_lsp/src/server.rs index 1a65dcc0..2e038e0e 100644 --- a/crates/pg_lsp/src/server.rs +++ b/crates/pg_lsp/src/server.rs @@ -27,11 +27,11 @@ use pg_hover::HoverParams; use pg_schema_cache::SchemaCache; use pg_workspace::Workspace; use serde::{de::DeserializeOwned, Serialize}; -use std::{collections::HashSet, sync::Arc, time::Duration}; +use std::{collections::HashSet, future::Future, sync::Arc, time::Duration}; use text_size::TextSize; -use threadpool::ThreadPool; -use tokio::sync::{mpsc, oneshot}; +use tokio::sync::mpsc; +use tokio_util::sync::CancellationToken; use crate::{ client::{client_flags::ClientFlags, LspClient}, @@ -72,9 +72,9 @@ impl DbConnection { /// For now, we move it into a separate task and use tokio's channels to communicate. fn get_client_receiver( connection: Connection, -) -> (mpsc::UnboundedReceiver, oneshot::Receiver<()>) { + cancel_token: Arc, +) -> mpsc::UnboundedReceiver { let (message_tx, message_rx) = mpsc::unbounded_channel(); - let (close_tx, close_rx) = oneshot::channel(); tokio::task::spawn(async move { // TODO: improve Result handling @@ -83,7 +83,7 @@ fn get_client_receiver( match msg { Message::Request(r) if connection.handle_shutdown(&r).unwrap() => { - close_tx.send(()).unwrap(); + cancel_token.cancel(); return; } @@ -92,16 +92,15 @@ fn get_client_receiver( } }); - (message_rx, close_rx) + message_rx } pub struct Server { client_rx: mpsc::UnboundedReceiver, - close_rx: oneshot::Receiver<()>, + cancel_token: Arc, client: LspClient, internal_tx: mpsc::UnboundedSender, internal_rx: mpsc::UnboundedReceiver, - pool: Arc, client_flags: Arc, ide: Arc, db_conn: Option, @@ -138,10 +137,12 @@ impl Server { let cloned_pool = pool.clone(); let cloned_client = client.clone(); - let (client_rx, close_rx) = get_client_receiver(connection); + let cancel_token = Arc::new(CancellationToken::new()); + + let client_rx = get_client_receiver(connection, cancel_token.clone()); let server = Self { - close_rx, + cancel_token, client_rx, internal_rx, internal_tx, @@ -186,7 +187,6 @@ impl Server { }); }, ), - pool, }; Ok(server) @@ -200,7 +200,7 @@ impl Server { self.compute_debouncer.clear(); - tokio::spawn(async move { + self.spawn_with_cancel(async move { client .send_notification::(ShowMessageParams { typ: lsp_types::MessageType::INFO, @@ -714,15 +714,17 @@ impl Server { Q: FnOnce() -> anyhow::Result + Send + 'static, { let client = self.client.clone(); - self.pool.execute(move || match query() { - Ok(result) => { - let response = lsp_server::Response::new_ok(id, result); - client.send_response(response).unwrap(); - } - Err(why) => { - client - .send_error(id, ErrorCode::InternalError, why.to_string()) - .unwrap(); + self.spawn_with_cancel(async move { + match query() { + Ok(result) => { + let response = lsp_server::Response::new_ok(id, result); + client.send_response(response).unwrap(); + } + Err(why) => { + client + .send_error(id, ErrorCode::InternalError, why.to_string()) + .unwrap(); + } } }); } @@ -748,9 +750,11 @@ impl Server { let client = self.client.clone(); let ide = Arc::clone(&self.ide); - self.pool.execute(move || { + self.spawn_with_cancel(async move { let response = lsp_server::Response::new_ok(id, query(&ide)); - client.send_response(response).unwrap(); + client + .send_response(response) + .expect("Failed to send query to client"); }); } @@ -791,22 +795,21 @@ impl Server { async fn process_messages(&mut self) -> anyhow::Result<()> { loop { tokio::select! { - _ = &mut self.close_rx => { + _ = self.cancel_token.cancelled() => { + // Close the loop, proceed to shutdown. return Ok(()) }, msg = self.internal_rx.recv() => { match msg { - // TODO: handle internal sender close? Is that valid state? - None => return Ok(()), - Some(m) => self.handle_internal_message(m) + None => panic!("The LSP's internal sender closed. This should never happen."), + Some(m) => self.handle_internal_message(m).await } }, msg = self.client_rx.recv() => { match msg { - // the client sender is closed, we can return - None => return Ok(()), + None => panic!("The LSP's client closed, but not via an 'exit' method. This should never happen."), Some(m) => self.handle_message(m) } }, @@ -848,14 +851,14 @@ impl Server { Ok(()) } - fn handle_internal_message(&mut self, msg: InternalMessage) -> anyhow::Result<()> { + async fn handle_internal_message(&mut self, msg: InternalMessage) -> anyhow::Result<()> { match msg { InternalMessage::SetSchemaCache(c) => { self.ide.set_schema_cache(c); self.compute_now(); } InternalMessage::RefreshSchemaCache => { - self.refresh_schema_cache(); + self.refresh_schema_cache().await; } InternalMessage::PublishDiagnostics(uri) => { self.publish_diagnostics(uri)?; @@ -869,10 +872,6 @@ impl Server { } fn pull_options(&mut self) { - if !self.client_flags.has_configuration { - return; - } - let params = ConfigurationParams { items: vec![ConfigurationItem { section: Some("postgres_lsp".to_string()), @@ -881,53 +880,72 @@ impl Server { }; let client = self.client.clone(); - let sender = self.internal_tx.clone(); - self.pool.execute(move || { + let internal_tx = self.internal_tx.clone(); + self.spawn_with_cancel(async move { match client.send_request::(params) { Ok(mut json) => { let options = client .parse_options(json.pop().expect("invalid configuration request")) .unwrap(); - sender.send(InternalMessage::SetOptions(options)).unwrap(); + if let Err(why) = internal_tx.send(InternalMessage::SetOptions(options)) { + println!("Failed to set internal options: {}", why); + } } - Err(_why) => { - // log::error!("Retrieving configuration failed: {}", why); + Err(why) => { + println!("Retrieving configuration failed: {}", why); } }; }); } fn register_configuration(&mut self) { - if self.client_flags.will_push_configuration { - let registration = Registration { - id: "pull-config".to_string(), - method: DidChangeConfiguration::METHOD.to_string(), - register_options: None, - }; + let registration = Registration { + id: "pull-config".to_string(), + method: DidChangeConfiguration::METHOD.to_string(), + register_options: None, + }; - let params = RegistrationParams { - registrations: vec![registration], - }; + let params = RegistrationParams { + registrations: vec![registration], + }; - let client = self.client.clone(); - self.pool.execute(move || { - if let Err(_why) = client.send_request::(params) { - // log::error!( - // "Failed to register \"{}\" notification: {}", - // DidChangeConfiguration::METHOD, - // why - // ); - } - }); - } + let client = self.client.clone(); + self.spawn_with_cancel(async move { + if let Err(why) = client.send_request::(params) { + println!( + "Failed to register \"{}\" notification: {}", + DidChangeConfiguration::METHOD, + why + ); + } + }); + } + + fn spawn_with_cancel(&self, f: F) -> tokio::task::JoinHandle<()> + where + F: Future + Send + 'static, + { + let cancel_token = self.cancel_token.clone(); + tokio::spawn(async move { + tokio::select! { + _ = cancel_token.cancelled() => {}, + _ = f => {} + }; + }) } pub async fn run(mut self) -> anyhow::Result<()> { - self.register_configuration(); - self.pull_options(); + if self.client_flags.will_push_configuration { + self.register_configuration(); + } + + if self.client_flags.has_configuration { + self.pull_options(); + } + self.process_messages().await?; - self.pool.join(); + Ok(()) } } From c22d977f53d62d6e038027b10a8cbc8c747dc9cb Mon Sep 17 00:00:00 2001 From: Julian Date: Fri, 18 Oct 2024 10:16:21 +0200 Subject: [PATCH 06/33] so far so good --- crates/pg_lsp/src/db_connection.rs | 61 ++++++++++++ crates/pg_lsp/src/lib.rs | 1 + crates/pg_lsp/src/server.rs | 152 +++++++++++------------------ 3 files changed, 117 insertions(+), 97 deletions(-) create mode 100644 crates/pg_lsp/src/db_connection.rs diff --git a/crates/pg_lsp/src/db_connection.rs b/crates/pg_lsp/src/db_connection.rs new file mode 100644 index 00000000..c74f8642 --- /dev/null +++ b/crates/pg_lsp/src/db_connection.rs @@ -0,0 +1,61 @@ +use pg_schema_cache::SchemaCache; +use sqlx::{postgres::PgListener, PgPool}; + +#[derive(Debug)] +pub(crate) struct DbConnection { + pub pool: PgPool, + connection_string: String, +} + +impl DbConnection { + pub(crate) async fn new(connection_string: String) -> Result { + let pool = PgPool::connect(&connection_string).await?; + Ok(Self { + pool, + connection_string: connection_string, + }) + } + + pub(crate) async fn refresh_db_connection( + self, + connection_string: Option, + ) -> anyhow::Result { + if connection_string.is_none() + || connection_string.as_ref() == Some(&self.connection_string) + { + return Ok(self); + } + + self.pool.close().await; + + let conn = DbConnection::new(connection_string.unwrap()).await?; + + Ok(conn) + } + + pub(crate) async fn start_listening(&self, on_schema_update: F) -> anyhow::Result<()> + where + F: Fn() -> () + Send + 'static, + { + let mut listener = PgListener::connect_with(&self.pool).await?; + listener.listen_all(["postgres_lsp", "pgrst"]).await?; + + loop { + match listener.recv().await { + Ok(notification) => { + if notification.payload().to_string() == "reload schema" { + on_schema_update(); + } + } + Err(e) => { + eprintln!("Listener error: {}", e); + return Err(e.into()); + } + } + } + } + + pub(crate) async fn get_schema_cache(&self) -> SchemaCache { + SchemaCache::load(&self.pool).await + } +} diff --git a/crates/pg_lsp/src/lib.rs b/crates/pg_lsp/src/lib.rs index ac95c913..97474d52 100644 --- a/crates/pg_lsp/src/lib.rs +++ b/crates/pg_lsp/src/lib.rs @@ -1,3 +1,4 @@ mod client; +mod db_connection; pub mod server; mod utils; diff --git a/crates/pg_lsp/src/server.rs b/crates/pg_lsp/src/server.rs index 2e038e0e..fc1fda71 100644 --- a/crates/pg_lsp/src/server.rs +++ b/crates/pg_lsp/src/server.rs @@ -35,6 +35,7 @@ use tokio_util::sync::CancellationToken; use crate::{ client::{client_flags::ClientFlags, LspClient}, + db_connection::DbConnection, utils::{file_path, from_proto, line_index_ext::LineIndexExt, normalize_uri, to_proto}, }; @@ -52,22 +53,6 @@ enum InternalMessage { SetSchemaCache(SchemaCache), } -#[derive(Debug)] -struct DbConnection { - pub pool: PgPool, - connection_string: String, -} - -impl DbConnection { - pub async fn new(connection_string: &str) -> Result { - let pool = PgPool::connect(connection_string).await?; - Ok(Self { - pool, - connection_string: connection_string.to_owned(), - }) - } -} - /// `lsp-servers` `Connection` type uses a crossbeam channel, which is not compatible with tokio's async runtime. /// For now, we move it into a separate task and use tokio's channels to communicate. fn get_client_receiver( @@ -110,21 +95,9 @@ pub struct Server { impl Server { pub fn init(connection: Connection) -> anyhow::Result { let client = LspClient::new(connection.sender.clone()); + let cancel_token = Arc::new(CancellationToken::new()); - let (internal_tx, internal_rx) = mpsc::unbounded_channel(); - - let (id, params) = connection.initialize_start()?; - let params: InitializeParams = serde_json::from_value(params)?; - - let result = InitializeResult { - capabilities: Self::capabilities(), - server_info: Some(ServerInfo { - name: "Postgres LSP".to_owned(), - version: Some(env!("CARGO_PKG_VERSION").to_owned()), - }), - }; - - connection.initialize_finish(id, serde_json::to_value(result)?)?; + let (params, client_rx) = Self::establish_client_connection(connection, &cancel_token)?; let client_flags = Arc::new(ClientFlags::from_initialize_request_params(¶ms)); @@ -132,15 +105,13 @@ impl Server { let ide = Arc::new(Workspace::new()); + let (internal_tx, internal_rx) = mpsc::unbounded_channel(); + let cloned_tx = internal_tx.clone(); let cloned_ide = ide.clone(); let cloned_pool = pool.clone(); let cloned_client = client.clone(); - let cancel_token = Arc::new(CancellationToken::new()); - - let client_rx = get_client_receiver(connection, cancel_token.clone()); - let server = Self { cancel_token, client_rx, @@ -239,68 +210,31 @@ impl Server { }); } - async fn start_listening(&self) { - if self.db_conn.is_none() { - return; - } - - let pool = self.db_conn.as_ref().unwrap().pool.clone(); - let tx = self.internal_tx.clone(); - - let mut listener = PgListener::connect_with(&pool).await.unwrap(); - listener - .listen_all(["postgres_lsp", "pgrst"]) - .await - .unwrap(); - - loop { - match listener.recv().await { - Ok(notification) => { - if notification.payload().to_string() == "reload schema" { - tx.send(InternalMessage::RefreshSchemaCache).unwrap(); - } - } - Err(e) => { - eprintln!("Listener error: {}", e); - break; - } - } - } - } - - async fn update_db_connection(&mut self, connection_string: Option) { - if connection_string == self.db_conn.as_ref().map(|c| c.connection_string.clone()) { - return; - } - if let Some(conn) = self.db_conn.take() { - conn.pool.close().await; - } - - if connection_string.is_none() { - return; - } - - let new_conn = DbConnection::new(connection_string.unwrap().as_str()).await; - - if new_conn.is_err() { - return; + async fn update_options(&mut self, options: Options) -> anyhow::Result<()> { + if options.db_connection_string.is_none() { + return Ok(()); } - self.db_conn = Some(new_conn.unwrap()); - - self.client - .send_notification::(ShowMessageParams { - typ: lsp_types::MessageType::INFO, - message: "Connection to database established".to_string(), - }) - .unwrap(); + let new_conn = if self.db_conn.is_none() { + DbConnection::new(options.db_connection_string.clone().unwrap()).await? + } else { + let current_conn = self.db_conn.take().unwrap(); + current_conn + .refresh_db_connection(options.db_connection_string.clone()) + .await? + }; - self.refresh_schema_cache().await; - self.start_listening().await; - } + let internal_tx = self.internal_tx.clone(); + self.spawn_with_cancel(async move { + new_conn.start_listening(move || { + internal_tx + .send(InternalMessage::RefreshSchemaCache) + .unwrap(); + // TODO: handle result + }).await.unwrap() + }); - fn update_options(&mut self, options: Options) { - async_std::task::block_on(self.update_db_connection(options.db_connection_string)); + Ok(()) } fn capabilities() -> ServerCapabilities { @@ -922,16 +856,40 @@ impl Server { }); } - fn spawn_with_cancel(&self, f: F) -> tokio::task::JoinHandle<()> + fn establish_client_connection( + connection: Connection, + cancel_token: &Arc, + ) -> anyhow::Result<(InitializeParams, mpsc::UnboundedReceiver)> { + let (id, params) = connection.initialize_start()?; + + let params: InitializeParams = serde_json::from_value(params)?; + + let result = InitializeResult { + capabilities: Self::capabilities(), + server_info: Some(ServerInfo { + name: "Postgres LSP".to_owned(), + version: Some(env!("CARGO_PKG_VERSION").to_owned()), + }), + }; + + connection.initialize_finish(id, serde_json::to_value(result)?)?; + + let client_rx = get_client_receiver(connection, cancel_token.clone()); + + Ok((params, client_rx)) + } + + fn spawn_with_cancel(&self, f: F) -> tokio::task::JoinHandle> where - F: Future + Send + 'static, + F: Future + Send + 'static, + O: Send + 'static, { let cancel_token = self.cancel_token.clone(); tokio::spawn(async move { tokio::select! { - _ = cancel_token.cancelled() => {}, - _ = f => {} - }; + _ = cancel_token.cancelled() => None, + output = f => Some(output) + } }) } From da769cbb5aef8315a774aa77d6cec115a3d73acc Mon Sep 17 00:00:00 2001 From: Julian Date: Fri, 18 Oct 2024 12:10:52 +0200 Subject: [PATCH 07/33] udpate database via messages --- crates/pg_lsp/src/client.rs | 8 +++ crates/pg_lsp/src/db_connection.rs | 61 +++++++++-------- crates/pg_lsp/src/server.rs | 106 ++++++++++++++++------------- 3 files changed, 99 insertions(+), 76 deletions(-) diff --git a/crates/pg_lsp/src/client.rs b/crates/pg_lsp/src/client.rs index 85ff5a61..85176942 100644 --- a/crates/pg_lsp/src/client.rs +++ b/crates/pg_lsp/src/client.rs @@ -50,6 +50,14 @@ impl LspClient { Ok(()) } + /// This will ignore any errors that occur while sending the notification. + pub fn send_info_notification(&self, message: &str) { + let _ = self.send_notification::(ShowMessageParams { + message: message.into(), + typ: MessageType::INFO, + }); + } + pub fn send_request(&self, params: R::Params) -> Result where R: lsp_types::request::Request, diff --git a/crates/pg_lsp/src/db_connection.rs b/crates/pg_lsp/src/db_connection.rs index c74f8642..51ba633d 100644 --- a/crates/pg_lsp/src/db_connection.rs +++ b/crates/pg_lsp/src/db_connection.rs @@ -1,10 +1,12 @@ use pg_schema_cache::SchemaCache; use sqlx::{postgres::PgListener, PgPool}; +use tokio::task::JoinHandle; #[derive(Debug)] pub(crate) struct DbConnection { pub pool: PgPool, connection_string: String, + schema_update_handle: Option>, } impl DbConnection { @@ -13,49 +15,52 @@ impl DbConnection { Ok(Self { pool, connection_string: connection_string, + schema_update_handle: None, }) } - pub(crate) async fn refresh_db_connection( - self, - connection_string: Option, - ) -> anyhow::Result { - if connection_string.is_none() - || connection_string.as_ref() == Some(&self.connection_string) - { - return Ok(self); - } + pub(crate) fn connected_to(&self, connection_string: &str) -> bool { + connection_string == self.connection_string + } + pub(crate) async fn close(self) { + if self.schema_update_handle.is_some() { + self.schema_update_handle.unwrap().abort(); + } self.pool.close().await; - - let conn = DbConnection::new(connection_string.unwrap()).await?; - - Ok(conn) } - pub(crate) async fn start_listening(&self, on_schema_update: F) -> anyhow::Result<()> + pub(crate) async fn listen_for_schema_updates( + &mut self, + on_schema_update: F, + ) -> anyhow::Result<()> where - F: Fn() -> () + Send + 'static, + F: Fn(SchemaCache) -> () + Send + 'static, { let mut listener = PgListener::connect_with(&self.pool).await?; listener.listen_all(["postgres_lsp", "pgrst"]).await?; - loop { - match listener.recv().await { - Ok(notification) => { - if notification.payload().to_string() == "reload schema" { - on_schema_update(); + let pool = self.pool.clone(); + + let handle: JoinHandle<()> = tokio::spawn(async move { + loop { + match listener.recv().await { + Ok(not) => { + if not.payload().to_string() == "reload schema" { + let schema_cache = SchemaCache::load(&pool).await; + on_schema_update(schema_cache); + }; + } + Err(why) => { + eprintln!("Error receiving notification: {:?}", why); + break; } - } - Err(e) => { - eprintln!("Listener error: {}", e); - return Err(e.into()); } } - } - } + }); + + self.schema_update_handle = Some(handle); - pub(crate) async fn get_schema_cache(&self) -> SchemaCache { - SchemaCache::load(&self.pool).await + Ok(()) } } diff --git a/crates/pg_lsp/src/server.rs b/crates/pg_lsp/src/server.rs index fc1fda71..6d6af373 100644 --- a/crates/pg_lsp/src/server.rs +++ b/crates/pg_lsp/src/server.rs @@ -40,17 +40,14 @@ use crate::{ }; use self::{debouncer::EventDebouncer, options::Options}; -use sqlx::{ - postgres::{PgListener, PgPool}, - Executor, -}; +use sqlx::{postgres::PgPool, Executor}; #[derive(Debug)] enum InternalMessage { PublishDiagnostics(lsp_types::Url), SetOptions(Options), - RefreshSchemaCache, SetSchemaCache(SchemaCache), + SetDatabaseConnection(DbConnection), } /// `lsp-servers` `Connection` type uses a crossbeam channel, which is not compatible with tokio's async runtime. @@ -210,29 +207,54 @@ impl Server { }); } - async fn update_options(&mut self, options: Options) -> anyhow::Result<()> { - if options.db_connection_string.is_none() { + fn update_db_connection(&self, options: Options) -> anyhow::Result<()> { + if options.db_connection_string.is_none() + || self + .db_conn + .as_ref() + .is_some_and(|c| c.connected_to(options.db_connection_string.as_ref().unwrap())) + { return Ok(()); } - let new_conn = if self.db_conn.is_none() { - DbConnection::new(options.db_connection_string.clone().unwrap()).await? - } else { - let current_conn = self.db_conn.take().unwrap(); - current_conn - .refresh_db_connection(options.db_connection_string.clone()) - .await? - }; + let connection_string = options.db_connection_string.unwrap(); let internal_tx = self.internal_tx.clone(); + let client = self.client.clone(); self.spawn_with_cancel(async move { - new_conn.start_listening(move || { + match DbConnection::new(connection_string.into()).await { + Ok(conn) => { + internal_tx + .send(InternalMessage::SetDatabaseConnection(conn)) + .unwrap(); + } + Err(why) => { + client.send_info_notification(&format!("Unable to update database connection: {}", why)); + + } + } + }); + + Ok(()) + } + + async fn listen_for_schema_updates(&mut self) -> anyhow::Result<()> { + if self.db_conn.is_none() { + eprintln!("Error trying to listen for schema updates: No database connection"); + return Ok(()); + } + + let internal_tx = self.internal_tx.clone(); + self.db_conn + .as_mut() + .unwrap() + .listen_for_schema_updates(move |schema_cache| { internal_tx - .send(InternalMessage::RefreshSchemaCache) + .send(InternalMessage::SetSchemaCache(schema_cache)) .unwrap(); - // TODO: handle result - }).await.unwrap() - }); + // TODO: handle result + }) + .await?; Ok(()) } @@ -692,26 +714,6 @@ impl Server { }); } - async fn refresh_schema_cache(&self) { - if self.db_conn.is_none() { - return; - } - - let tx = self.internal_tx.clone(); - let conn = self.db_conn.as_ref().unwrap().pool.clone(); - let client = self.client.clone(); - - client - .send_notification::(ShowMessageParams { - typ: lsp_types::MessageType::INFO, - message: "Refreshing schema cache...".to_string(), - }) - .unwrap(); - let schema_cache = SchemaCache::load(&conn).await; - tx.send(InternalMessage::SetSchemaCache(schema_cache)) - .unwrap(); - } - fn did_change_configuration( &mut self, params: DidChangeConfigurationParams, @@ -720,7 +722,7 @@ impl Server { self.pull_options(); } else { let options = self.client.parse_options(params.settings)?; - self.update_options(options); + self.update_db_connection(options); } Ok(()) @@ -744,14 +746,14 @@ impl Server { msg = self.client_rx.recv() => { match msg { None => panic!("The LSP's client closed, but not via an 'exit' method. This should never happen."), - Some(m) => self.handle_message(m) + Some(m) => self.handle_message(m).await } }, }?; } } - fn handle_message(&mut self, msg: Message) -> anyhow::Result<()> { + async fn handle_message(&mut self, msg: Message) -> anyhow::Result<()> { match msg { Message::Request(request) => { if let Some(response) = dispatch::RequestDispatcher::new(request) @@ -768,7 +770,8 @@ impl Server { Message::Notification(notification) => { dispatch::NotificationDispatcher::new(notification) .on::(|params| { - self.did_change_configuration(params) + self.did_change_configuration(params); + Ok(()) })? .on::(|params| self.did_close(params))? .on::(|params| self.did_open(params))? @@ -788,17 +791,24 @@ impl Server { async fn handle_internal_message(&mut self, msg: InternalMessage) -> anyhow::Result<()> { match msg { InternalMessage::SetSchemaCache(c) => { + self.client + .send_info_notification("Refreshing Schema Cache..."); self.ide.set_schema_cache(c); + self.client.send_info_notification("Updated Schema Cache."); self.compute_now(); } - InternalMessage::RefreshSchemaCache => { - self.refresh_schema_cache().await; - } InternalMessage::PublishDiagnostics(uri) => { self.publish_diagnostics(uri)?; } InternalMessage::SetOptions(options) => { - self.update_options(options); + self.update_db_connection(options); + } + InternalMessage::SetDatabaseConnection(conn) => { + let current = self.db_conn.replace(conn); + if current.is_some() { + current.unwrap().close().await + } + self.listen_for_schema_updates(); } } From 6f672dba2d69ac38fd010df746c0303e82c40ed1 Mon Sep 17 00:00:00 2001 From: Julian Date: Fri, 18 Oct 2024 12:12:55 +0200 Subject: [PATCH 08/33] ok ok --- crates/pg_lsp/src/server.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/pg_lsp/src/server.rs b/crates/pg_lsp/src/server.rs index 6d6af373..535ddbbd 100644 --- a/crates/pg_lsp/src/server.rs +++ b/crates/pg_lsp/src/server.rs @@ -801,14 +801,14 @@ impl Server { self.publish_diagnostics(uri)?; } InternalMessage::SetOptions(options) => { - self.update_db_connection(options); + self.update_db_connection(options)?; } InternalMessage::SetDatabaseConnection(conn) => { let current = self.db_conn.replace(conn); if current.is_some() { current.unwrap().close().await } - self.listen_for_schema_updates(); + self.listen_for_schema_updates().await?; } } From f47f04474bbac2dd486f6268c62ae0598801bef7 Mon Sep 17 00:00:00 2001 From: Julian Date: Fri, 18 Oct 2024 12:15:45 +0200 Subject: [PATCH 09/33] clean up linting --- crates/pg_lsp/src/server.rs | 5 ++--- crates/pg_lsp/src/server/debouncer/thread.rs | 14 -------------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/crates/pg_lsp/src/server.rs b/crates/pg_lsp/src/server.rs index 535ddbbd..2c3550e5 100644 --- a/crates/pg_lsp/src/server.rs +++ b/crates/pg_lsp/src/server.rs @@ -722,7 +722,7 @@ impl Server { self.pull_options(); } else { let options = self.client.parse_options(params.settings)?; - self.update_db_connection(options); + self.update_db_connection(options)?; } Ok(()) @@ -770,8 +770,7 @@ impl Server { Message::Notification(notification) => { dispatch::NotificationDispatcher::new(notification) .on::(|params| { - self.did_change_configuration(params); - Ok(()) + self.did_change_configuration(params) })? .on::(|params| self.did_close(params))? .on::(|params| self.did_open(params))? diff --git a/crates/pg_lsp/src/server/debouncer/thread.rs b/crates/pg_lsp/src/server/debouncer/thread.rs index 1aa85939..9329b7cd 100644 --- a/crates/pg_lsp/src/server/debouncer/thread.rs +++ b/crates/pg_lsp/src/server/debouncer/thread.rs @@ -8,7 +8,6 @@ use super::buffer::{EventBuffer, Get, State}; struct DebouncerThread { mutex: Arc>, thread: JoinHandle<()>, - stopped: Arc, } impl DebouncerThread { @@ -36,14 +35,8 @@ impl DebouncerThread { Self { mutex, thread, - stopped, } } - - fn stop(self) -> JoinHandle<()> { - self.stopped.store(true, Ordering::Relaxed); - self.thread - } } /// Threaded debouncer wrapping [EventBuffer]. Accepts a common delay and a @@ -68,13 +61,6 @@ impl EventDebouncer { pub fn clear(&self) { self.0.mutex.lock().unwrap().clear(); } - - /// Signals the debouncer thread to quit and returns a - /// [std::thread::JoinHandle] which can be `.join()`ed in the consumer - /// thread. The common idiom is: `debouncer.stop().join().unwrap();` - pub fn stop(self) -> JoinHandle<()> { - self.0.stop() - } } #[cfg(test)] From 17f884f9e214703ec8adcd518cd0c6440d427559 Mon Sep 17 00:00:00 2001 From: Julian Date: Fri, 18 Oct 2024 12:18:22 +0200 Subject: [PATCH 10/33] undo odd changes --- crates/pg_schema_cache/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/pg_schema_cache/src/lib.rs b/crates/pg_schema_cache/src/lib.rs index 82454fc3..aed612c6 100644 --- a/crates/pg_schema_cache/src/lib.rs +++ b/crates/pg_schema_cache/src/lib.rs @@ -4,11 +4,11 @@ #![feature(future_join)] mod functions; +mod versions; mod schema_cache; mod schemas; mod tables; mod types; -mod versions; use sqlx::postgres::PgPool; From 69e19a06623b300e335321e51907298d313759ad Mon Sep 17 00:00:00 2001 From: Julian Date: Fri, 18 Oct 2024 12:24:14 +0200 Subject: [PATCH 11/33] tidy --- crates/pg_lsp/src/server.rs | 28 ++++++++++---------- crates/pg_lsp/src/server/debouncer/thread.rs | 5 +--- crates/pg_schema_cache/src/lib.rs | 2 +- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/crates/pg_lsp/src/server.rs b/crates/pg_lsp/src/server.rs index 2c3550e5..af9b7ad4 100644 --- a/crates/pg_lsp/src/server.rs +++ b/crates/pg_lsp/src/server.rs @@ -94,11 +94,8 @@ impl Server { let client = LspClient::new(connection.sender.clone()); let cancel_token = Arc::new(CancellationToken::new()); - let (params, client_rx) = Self::establish_client_connection(connection, &cancel_token)?; + let (client_flags, client_rx) = Self::establish_client_connection(connection, &cancel_token)?; - let client_flags = Arc::new(ClientFlags::from_initialize_request_params(¶ms)); - - let pool = Arc::new(threadpool::Builder::new().build()); let ide = Arc::new(Workspace::new()); @@ -106,7 +103,7 @@ impl Server { let cloned_tx = internal_tx.clone(); let cloned_ide = ide.clone(); - let cloned_pool = pool.clone(); + let pool = Arc::new(threadpool::Builder::new().build()); let cloned_client = client.clone(); let server = Self { @@ -115,7 +112,7 @@ impl Server { internal_rx, internal_tx, client, - client_flags, + client_flags: Arc::new(client_flags), db_conn: None, ide, compute_debouncer: EventDebouncer::new( @@ -124,7 +121,7 @@ impl Server { let inner_cloned_ide = cloned_ide.clone(); let inner_cloned_tx = cloned_tx.clone(); let inner_cloned_client = cloned_client.clone(); - cloned_pool.execute(move || { + pool.execute(move || { inner_cloned_client .send_notification::(ShowMessageParams { typ: lsp_types::MessageType::INFO, @@ -229,8 +226,10 @@ impl Server { .unwrap(); } Err(why) => { - client.send_info_notification(&format!("Unable to update database connection: {}", why)); - + client.send_info_notification(&format!( + "Unable to update database connection: {}", + why + )); } } }); @@ -868,7 +867,7 @@ impl Server { fn establish_client_connection( connection: Connection, cancel_token: &Arc, - ) -> anyhow::Result<(InitializeParams, mpsc::UnboundedReceiver)> { + ) -> anyhow::Result<(ClientFlags, mpsc::UnboundedReceiver)> { let (id, params) = connection.initialize_start()?; let params: InitializeParams = serde_json::from_value(params)?; @@ -885,9 +884,12 @@ impl Server { let client_rx = get_client_receiver(connection, cancel_token.clone()); - Ok((params, client_rx)) + let client_flags = ClientFlags::from_initialize_request_params(¶ms); + + Ok((client_flags, client_rx)) } + /// Spawns an asynchronous task that can be cancelled with the `Server`'s `cancel_token`. fn spawn_with_cancel(&self, f: F) -> tokio::task::JoinHandle> where F: Future + Send + 'static, @@ -911,8 +913,6 @@ impl Server { self.pull_options(); } - self.process_messages().await?; - - Ok(()) + self.process_messages().await } } diff --git a/crates/pg_lsp/src/server/debouncer/thread.rs b/crates/pg_lsp/src/server/debouncer/thread.rs index 9329b7cd..a7486f21 100644 --- a/crates/pg_lsp/src/server/debouncer/thread.rs +++ b/crates/pg_lsp/src/server/debouncer/thread.rs @@ -32,10 +32,7 @@ impl DebouncerThread { } } }); - Self { - mutex, - thread, - } + Self { mutex, thread } } } diff --git a/crates/pg_schema_cache/src/lib.rs b/crates/pg_schema_cache/src/lib.rs index aed612c6..82454fc3 100644 --- a/crates/pg_schema_cache/src/lib.rs +++ b/crates/pg_schema_cache/src/lib.rs @@ -4,11 +4,11 @@ #![feature(future_join)] mod functions; -mod versions; mod schema_cache; mod schemas; mod tables; mod types; +mod versions; use sqlx::postgres::PgPool; From f011c25f459ccea2138c50fa592573a5dd38e93a Mon Sep 17 00:00:00 2001 From: Julian Date: Fri, 18 Oct 2024 12:24:49 +0200 Subject: [PATCH 12/33] what --- crates/pg_schema_cache/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/pg_schema_cache/src/lib.rs b/crates/pg_schema_cache/src/lib.rs index 82454fc3..aed612c6 100644 --- a/crates/pg_schema_cache/src/lib.rs +++ b/crates/pg_schema_cache/src/lib.rs @@ -4,11 +4,11 @@ #![feature(future_join)] mod functions; +mod versions; mod schema_cache; mod schemas; mod tables; mod types; -mod versions; use sqlx::postgres::PgPool; From 577d0f94253fa15aca26978aa6028733b7ddf292 Mon Sep 17 00:00:00 2001 From: Julian Date: Fri, 18 Oct 2024 12:27:47 +0200 Subject: [PATCH 13/33] improve error handling --- crates/pg_lsp/src/server.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/pg_lsp/src/server.rs b/crates/pg_lsp/src/server.rs index af9b7ad4..637debbf 100644 --- a/crates/pg_lsp/src/server.rs +++ b/crates/pg_lsp/src/server.rs @@ -59,9 +59,15 @@ fn get_client_receiver( let (message_tx, message_rx) = mpsc::unbounded_channel(); tokio::task::spawn(async move { - // TODO: improve Result handling loop { - let msg = connection.receiver.recv().unwrap(); + let msg = match connection.receiver.recv() { + Ok(msg) => msg, + Err(e) => { + eprint!("Connection was closed by LSP client: {}", e); + cancel_token.cancel(); + return; + } + }; match msg { Message::Request(r) if connection.handle_shutdown(&r).unwrap() => { @@ -69,6 +75,7 @@ fn get_client_receiver( return; } + // any non-shutdown request is forwarded to the server _ => message_tx.send(msg).unwrap(), }; } From 5abcc137884745883f6636d810825852c2e1535b Mon Sep 17 00:00:00 2001 From: Julian Date: Fri, 18 Oct 2024 12:30:37 +0200 Subject: [PATCH 14/33] improve error handling #2 --- crates/pg_lsp/src/server.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/pg_lsp/src/server.rs b/crates/pg_lsp/src/server.rs index 637debbf..65d2a354 100644 --- a/crates/pg_lsp/src/server.rs +++ b/crates/pg_lsp/src/server.rs @@ -216,6 +216,7 @@ impl Server { || self .db_conn .as_ref() + // if the connection is already connected to the same database, do nothing .is_some_and(|c| c.connected_to(options.db_connection_string.as_ref().unwrap())) { return Ok(()); @@ -257,8 +258,7 @@ impl Server { .listen_for_schema_updates(move |schema_cache| { internal_tx .send(InternalMessage::SetSchemaCache(schema_cache)) - .unwrap(); - // TODO: handle result + .expect("LSP Server: Failed to send internal message."); }) .await?; From 0c1713346427e0f1baa0095c7a60bb17b67bdab7 Mon Sep 17 00:00:00 2001 From: Julian Date: Mon, 21 Oct 2024 18:43:01 +0200 Subject: [PATCH 15/33] intermediary --- Cargo.lock | 151 ++++++++++++++++++++++++++++++++++++++- crates/pg_lsp/Cargo.toml | 1 + 2 files changed, 151 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index fd12e85a..dbcdede9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -194,6 +194,17 @@ version = "4.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbb36e985947064623dbd357f727af08ffd077f93d696782f3c56365fa2e2799" +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.71", +] + [[package]] name = "atoi" version = "2.0.0" @@ -219,6 +230,17 @@ dependencies = [ "rand", ] +[[package]] +name = "auto_impl" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c87f3f15e7794432337fc718554eaa4dc8f04c9677a950ffe366f20a162ae42" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.71", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -710,6 +732,20 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.30" @@ -782,6 +818,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.71", +] + [[package]] name = "futures-sink" version = "0.3.30" @@ -800,8 +847,10 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ + "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -937,6 +986,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + [[package]] name = "idna" version = "0.5.0" @@ -1146,6 +1201,19 @@ dependencies = [ "serde_json", ] +[[package]] +name = "lsp-types" +version = "0.94.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66bfd44a06ae10647fe3f8214762e9369fd4248df1350924b4ef9e770a85ea1" +dependencies = [ + "bitflags 1.3.2", + "serde", + "serde_json", + "serde_repr", + "url", +] + [[package]] name = "lsp-types" version = "0.95.0" @@ -1472,7 +1540,7 @@ dependencies = [ "dashmap", "line_index", "lsp-server", - "lsp-types", + "lsp-types 0.95.0", "pg_base_db", "pg_commands", "pg_completions", @@ -1488,6 +1556,7 @@ dependencies = [ "threadpool", "tokio", "tokio-util", + "tower-lsp", ] [[package]] @@ -1607,6 +1676,26 @@ dependencies = [ "tree_sitter_sql", ] +[[package]] +name = "pin-project" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf123a161dde1e524adf36f90bc5d8d3462824a9c43553ad07a8183161189ec" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4502d8515ca9f32f1fb543d987f63d95a14934883db45bdb48060b6b69257f8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.71", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -2534,6 +2623,66 @@ dependencies = [ "tokio", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-lsp" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ba052b54a6627628d9b3c34c176e7eda8359b7da9acd497b9f20998d118508" +dependencies = [ + "async-trait", + "auto_impl", + "bytes", + "dashmap", + "futures", + "httparse", + "lsp-types 0.94.1", + "memchr", + "serde", + "serde_json", + "tokio", + "tokio-util", + "tower", + "tower-lsp-macros", + "tracing", +] + +[[package]] +name = "tower-lsp-macros" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84fd902d4e0b9a4b27f2f440108dc034e1758628a9b702f8ec61ad66355422fa" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.71", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.40" diff --git a/crates/pg_lsp/Cargo.toml b/crates/pg_lsp/Cargo.toml index 9d23bce9..0f0cf703 100644 --- a/crates/pg_lsp/Cargo.toml +++ b/crates/pg_lsp/Cargo.toml @@ -34,6 +34,7 @@ pg_workspace.workspace = true pg_diagnostics.workspace = true tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread", "sync"] } tokio-util = "0.7.12" +tower-lsp = "0.20.0" [dev-dependencies] From 0af11c41e9518c473bae39854959b08ef89bac93 Mon Sep 17 00:00:00 2001 From: Julian Date: Tue, 22 Oct 2024 17:00:15 +0200 Subject: [PATCH 16/33] so far --- crates/pg_lsp/src/b_server.rs | 159 +++++++++++++++++++++++ crates/pg_lsp/src/client.rs | 95 +++----------- crates/pg_lsp/src/client/client_flags.rs | 2 +- crates/pg_lsp/src/lib.rs | 2 + crates/pg_lsp/src/server.rs | 17 +-- crates/pg_lsp/src/server/options.rs | 2 +- crates/pg_schema_cache/src/lib.rs | 2 +- 7 files changed, 191 insertions(+), 88 deletions(-) create mode 100644 crates/pg_lsp/src/b_server.rs diff --git a/crates/pg_lsp/src/b_server.rs b/crates/pg_lsp/src/b_server.rs new file mode 100644 index 00000000..c5c51941 --- /dev/null +++ b/crates/pg_lsp/src/b_server.rs @@ -0,0 +1,159 @@ +use std::sync::Arc; + +use notification::ShowMessage; +use pg_commands::CommandType; +use pg_workspace::Workspace; +use tokio::sync::{Mutex, RwLock}; +use tower_lsp::jsonrpc::Result; +use tower_lsp::lsp_types::*; +use tower_lsp::{Client, LanguageServer}; + +use crate::client::client_flags::ClientFlags; +use crate::db_connection::DbConnection; +use crate::server::options::ClientConfigurationOptions; + +struct Server { + client: Client, + db: Mutex>, + ide: Arc>, + client_capabilities: RwLock>, +} + +impl Server { + pub async fn new(client: Client) -> Self { + let ide = Arc::new(RwLock::new(Workspace::new())); + Self { + client, + db: Mutex::new(None), + ide, + client_capabilities: RwLock::new(None), + } + } + + /// When the client sends a didChangeConfiguration notification, we need to parse the received JSON. + fn parse_options_from_client( + &self, + mut value: serde_json::Value, + ) -> Result { + let options = match value.get_mut("pglsp") { + Some(section) => section.take(), + None => value, + }; + + let options = match serde_json::from_value::(options) { + Ok(new_options) => Some(new_options), + Err(why) => { + let message = format!( + "The texlab configuration is invalid; using the default settings instead.\nDetails: {why}" + ); + let typ = MessageType::WARNING; + self.client + .send_notification::(ShowMessageParams { message, typ }); + None + } + }; + + Ok(options.unwrap_or_default()) + } + + async fn update_db_connection( + &self, + options: ClientConfigurationOptions, + ) -> anyhow::Result<()> { + if options.db_connection_string.is_none() + || self + .db + .lock() + .await + .as_ref() + // if the connection is already connected to the same database, do nothing + .is_some_and(|c| c.connected_to(options.db_connection_string.as_ref().unwrap())) + { + return Ok(()); + } + + let connection_string = options.db_connection_string.unwrap(); + + let mut db = DbConnection::new(connection_string).await?; + + let ide = self.ide.clone(); + db.listen_for_schema_updates(move |schema| { + let _guard = ide.blocking_write().set_schema_cache(schema); + }); + + let mut current_db = self.db.lock().await; + let old_db = current_db.replace(db); + + if old_db.is_some() { + let old_db = old_db.unwrap(); + old_db.close().await; + } + + Ok(()) + } +} + +#[tower_lsp::async_trait] +impl LanguageServer for Server { + async fn initialize(&self, params: InitializeParams) -> Result { + let flags = ClientFlags::from_initialize_request_params(¶ms); + self.client_capabilities.blocking_write().replace(flags); + + Ok(InitializeResult { + server_info: None, + capabilities: ServerCapabilities { + text_document_sync: Some(TextDocumentSyncCapability::Options( + TextDocumentSyncOptions { + open_close: Some(true), + change: Some(TextDocumentSyncKind::INCREMENTAL), + will_save: None, + will_save_wait_until: None, + save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions { + include_text: Some(false), + })), + }, + )), + hover_provider: Some(HoverProviderCapability::Simple(true)), + execute_command_provider: Some(ExecuteCommandOptions { + commands: CommandType::ALL + .iter() + .map(|c| c.id().to_string()) + .collect(), + ..Default::default() + }), + inlay_hint_provider: Some(OneOf::Left(true)), + code_action_provider: Some(CodeActionProviderCapability::Simple(true)), + completion_provider: Some(CompletionOptions::default()), + ..ServerCapabilities::default() + }, + }) + } + + async fn initialized(&self, _params: InitializedParams) { + self.client + .log_message(MessageType::INFO, "Postgres LSP Connected!") + .await; + } + + async fn shutdown(&self) -> Result<()> { + self.client + .log_message(MessageType::INFO, "Postgres LSP terminated.") + .await; + Ok(()) + } + + + async fn did_change_configuration(&self, params: DidChangeConfigurationParams) { + match self.parse_options_from_client(params.settings) { + Ok(opts) => { + self.update_db_connection(opts).await; + } + Err(e) => { + self.client + .log_message(MessageType::ERROR, format!("Error parsing configuration: {}", e)) + .await; + } + }; + + } +} diff --git a/crates/pg_lsp/src/client.rs b/crates/pg_lsp/src/client.rs index 85176942..bf626ab8 100644 --- a/crates/pg_lsp/src/client.rs +++ b/crates/pg_lsp/src/client.rs @@ -1,52 +1,28 @@ pub mod client_flags; -use std::{ - collections::HashMap, - sync::{ - atomic::{AtomicI32, Ordering}, - Arc, Mutex, - }, -}; +use anyhow::Result; +use serde::Serialize; +use tower_lsp::lsp_types::{notification::ShowMessage, MessageType, ShowMessageParams}; +use tower_lsp::Client; -use anyhow::{bail, Result}; -use crossbeam_channel::Sender; -use lsp_server::{ErrorCode, Message, Request, RequestId, Response}; -use lsp_types::{notification::ShowMessage, MessageType, ShowMessageParams}; -use serde::{de::DeserializeOwned, Serialize}; - -use crate::server::options::Options; - -#[derive(Debug)] -struct RawClient { - sender: Sender, - next_id: AtomicI32, - pending: Mutex>>, -} +use crate::server::options::ClientConfigurationOptions; #[derive(Debug, Clone)] pub struct LspClient { - raw: Arc, + client: Client, } impl LspClient { - pub fn new(sender: Sender) -> Self { - let raw = Arc::new(RawClient { - sender, - next_id: AtomicI32::new(1), - pending: Default::default(), - }); - - Self { raw } + pub fn new(client: Client) -> Self { + Self { client } } pub fn send_notification(&self, params: N::Params) -> Result<()> where - N: lsp_types::notification::Notification, + N: tower_lsp::lsp_types::notification::Notification, N::Params: Serialize, { - self.raw - .sender - .send(lsp_server::Notification::new(N::METHOD.to_string(), params).into())?; + self.client.send_notification::(params); Ok(()) } @@ -58,54 +34,19 @@ impl LspClient { }); } - pub fn send_request(&self, params: R::Params) -> Result + pub async fn send_request(&self, params: R::Params) -> Result where - R: lsp_types::request::Request, - R::Params: Serialize, - R::Result: DeserializeOwned, + R: tower_lsp::lsp_types::request::Request, { - let id = RequestId::from(self.raw.next_id.fetch_add(1, Ordering::SeqCst)); + let response = self.client.send_request::(params).await?; - let (tx, rx) = crossbeam_channel::bounded(1); - self.raw.pending.lock().unwrap().insert(id.clone(), tx); - - self.raw - .sender - .send(Request::new(id, R::METHOD.to_string(), params).into())?; - - let response = rx.recv()?; - let result = match response.error { - Some(error) => bail!(error.message), - None => response.result.unwrap_or_default(), - }; - - Ok(serde_json::from_value(result)?) - } - - pub fn send_response(&self, response: lsp_server::Response) -> Result<()> { - self.raw.sender.send(response.into())?; - Ok(()) - } - - pub fn send_error(&self, id: RequestId, code: ErrorCode, message: String) -> Result<()> { - self.send_response(lsp_server::Response::new_err(id, code as i32, message))?; - Ok(()) - } - - pub fn recv_response(&self, response: lsp_server::Response) -> Result<()> { - let tx = self - .raw - .pending - .lock() - .unwrap() - .remove(&response.id) - .expect("response with known request id received"); - - tx.send(response)?; - Ok(()) + Ok(response) } - pub fn parse_options(&self, mut value: serde_json::Value) -> Result { + pub fn parse_options( + &self, + mut value: serde_json::Value, + ) -> Result { // if there are multiple servers, we need to extract the options for pglsp first let options = match value.get_mut("pglsp") { Some(section) => section.take(), diff --git a/crates/pg_lsp/src/client/client_flags.rs b/crates/pg_lsp/src/client/client_flags.rs index 6209f443..c4a722ee 100644 --- a/crates/pg_lsp/src/client/client_flags.rs +++ b/crates/pg_lsp/src/client/client_flags.rs @@ -1,4 +1,4 @@ -use lsp_types::InitializeParams; +use tower_lsp::lsp_types::InitializeParams; /// Contains information about the client's capabilities. /// This is used to determine which features the server can use. diff --git a/crates/pg_lsp/src/lib.rs b/crates/pg_lsp/src/lib.rs index 97474d52..303a6ac4 100644 --- a/crates/pg_lsp/src/lib.rs +++ b/crates/pg_lsp/src/lib.rs @@ -2,3 +2,5 @@ mod client; mod db_connection; pub mod server; mod utils; + +mod b_server; diff --git a/crates/pg_lsp/src/server.rs b/crates/pg_lsp/src/server.rs index 65d2a354..65dc687c 100644 --- a/crates/pg_lsp/src/server.rs +++ b/crates/pg_lsp/src/server.rs @@ -29,6 +29,7 @@ use pg_workspace::Workspace; use serde::{de::DeserializeOwned, Serialize}; use std::{collections::HashSet, future::Future, sync::Arc, time::Duration}; use text_size::TextSize; +use tower_lsp::Client; use tokio::sync::mpsc; use tokio_util::sync::CancellationToken; @@ -39,13 +40,13 @@ use crate::{ utils::{file_path, from_proto, line_index_ext::LineIndexExt, normalize_uri, to_proto}, }; -use self::{debouncer::EventDebouncer, options::Options}; +use self::{debouncer::EventDebouncer, options::ClientConfigurationOptions}; use sqlx::{postgres::PgPool, Executor}; #[derive(Debug)] enum InternalMessage { PublishDiagnostics(lsp_types::Url), - SetOptions(Options), + SetOptions(ClientConfigurationOptions), SetSchemaCache(SchemaCache), SetDatabaseConnection(DbConnection), } @@ -61,7 +62,7 @@ fn get_client_receiver( tokio::task::spawn(async move { loop { let msg = match connection.receiver.recv() { - Ok(msg) => msg, + Ok(msg) => msg, Err(e) => { eprint!("Connection was closed by LSP client: {}", e); cancel_token.cancel(); @@ -97,12 +98,12 @@ pub struct Server { } impl Server { - pub fn init(connection: Connection) -> anyhow::Result { - let client = LspClient::new(connection.sender.clone()); + pub fn init(client: Client) -> anyhow::Result { + let client = LspClient::new(client); let cancel_token = Arc::new(CancellationToken::new()); - let (client_flags, client_rx) = Self::establish_client_connection(connection, &cancel_token)?; - + let (client_flags, client_rx) = + Self::establish_client_connection(connection, &cancel_token)?; let ide = Arc::new(Workspace::new()); @@ -211,7 +212,7 @@ impl Server { }); } - fn update_db_connection(&self, options: Options) -> anyhow::Result<()> { + fn update_db_connection(&self, options: ClientConfigurationOptions) -> anyhow::Result<()> { if options.db_connection_string.is_none() || self .db_conn diff --git a/crates/pg_lsp/src/server/options.rs b/crates/pg_lsp/src/server/options.rs index a9d89c7e..d3b7f8f8 100644 --- a/crates/pg_lsp/src/server/options.rs +++ b/crates/pg_lsp/src/server/options.rs @@ -3,6 +3,6 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Default, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] #[serde(default)] -pub struct Options { +pub struct ClientConfigurationOptions { pub db_connection_string: Option, } diff --git a/crates/pg_schema_cache/src/lib.rs b/crates/pg_schema_cache/src/lib.rs index aed612c6..82454fc3 100644 --- a/crates/pg_schema_cache/src/lib.rs +++ b/crates/pg_schema_cache/src/lib.rs @@ -4,11 +4,11 @@ #![feature(future_join)] mod functions; -mod versions; mod schema_cache; mod schemas; mod tables; mod types; +mod versions; use sqlx::postgres::PgPool; From e4d0311087afb38dcf95bb5e744ef5f32b0bee00 Mon Sep 17 00:00:00 2001 From: Julian Date: Wed, 23 Oct 2024 09:31:35 +0200 Subject: [PATCH 17/33] todo: adjust result types to use tower_lsp::jsonrpc --- crates/pg_lsp/src/b_server.rs | 138 +++++++++++++++++++---- crates/pg_lsp/src/client/client_flags.rs | 12 +- crates/pg_lsp/src/db_connection.rs | 14 ++- crates/pg_lsp/src/server.rs | 6 +- 4 files changed, 141 insertions(+), 29 deletions(-) diff --git a/crates/pg_lsp/src/b_server.rs b/crates/pg_lsp/src/b_server.rs index c5c51941..2217a113 100644 --- a/crates/pg_lsp/src/b_server.rs +++ b/crates/pg_lsp/src/b_server.rs @@ -3,8 +3,8 @@ use std::sync::Arc; use notification::ShowMessage; use pg_commands::CommandType; use pg_workspace::Workspace; -use tokio::sync::{Mutex, RwLock}; -use tower_lsp::jsonrpc::Result; +use tokio::sync::RwLock; +use tower_lsp::jsonrpc::Error; use tower_lsp::lsp_types::*; use tower_lsp::{Client, LanguageServer}; @@ -14,7 +14,7 @@ use crate::server::options::ClientConfigurationOptions; struct Server { client: Client, - db: Mutex>, + db: RwLock>, ide: Arc>, client_capabilities: RwLock>, } @@ -24,7 +24,7 @@ impl Server { let ide = Arc::new(RwLock::new(Workspace::new())); Self { client, - db: Mutex::new(None), + db: RwLock::new(None), ide, client_capabilities: RwLock::new(None), } @@ -34,13 +34,13 @@ impl Server { fn parse_options_from_client( &self, mut value: serde_json::Value, - ) -> Result { + ) -> Option { let options = match value.get_mut("pglsp") { Some(section) => section.take(), None => value, }; - let options = match serde_json::from_value::(options) { + match serde_json::from_value::(options) { Ok(new_options) => Some(new_options), Err(why) => { let message = format!( @@ -51,11 +51,13 @@ impl Server { .send_notification::(ShowMessageParams { message, typ }); None } - }; - - Ok(options.unwrap_or_default()) + } } + /// `update_db_connection` will update `Self`'s database connection. + /// If the passed-in connection string is the same that we're already connected to, it's a noop. + /// Otherwise, it'll first open a new connection, replace `Self`'s connection, and then close + /// the old one. async fn update_db_connection( &self, options: ClientConfigurationOptions, @@ -63,7 +65,7 @@ impl Server { if options.db_connection_string.is_none() || self .db - .lock() + .read() .await .as_ref() // if the connection is already connected to the same database, do nothing @@ -81,7 +83,7 @@ impl Server { let _guard = ide.blocking_write().set_schema_cache(schema); }); - let mut current_db = self.db.lock().await; + let mut current_db = self.db.blocking_write(); let old_db = current_db.replace(db); if old_db.is_some() { @@ -91,11 +93,48 @@ impl Server { Ok(()) } + + async fn request_opts_from_client(&self) -> Option { + let params = ConfigurationParams { + items: vec![ConfigurationItem { + section: Some("pglsp".to_string()), + scope_uri: None, + }], + }; + + match self + .client + .send_request::(params) + .await + { + Ok(json) => { + // The client reponse fits the requested `ConfigurationParams.items`, + // so the first value is what we're looking for. + let relevant = json + .into_iter() + .next() + .expect("workspace/configuration request did not yield expected response."); + + let opts = self.parse_options_from_client(relevant); + + opts + } + Err(why) => { + let message = format!( + "Unable to pull client options via workspace/configuration request: {}", + why + ); + println!("{}", message); + self.client.log_message(MessageType::ERROR, message); + None + } + } + } } #[tower_lsp::async_trait] impl LanguageServer for Server { - async fn initialize(&self, params: InitializeParams) -> Result { + async fn initialize(&self, params: InitializeParams) -> tower_lsp::jsonrpc::Result { let flags = ClientFlags::from_initialize_request_params(¶ms); self.client_capabilities.blocking_write().replace(flags); @@ -135,25 +174,86 @@ impl LanguageServer for Server { .await; } - async fn shutdown(&self) -> Result<()> { + async fn shutdown(&self) -> anyhow::Result<()> { self.client .log_message(MessageType::INFO, "Postgres LSP terminated.") .await; Ok(()) } - async fn did_change_configuration(&self, params: DidChangeConfigurationParams) { - match self.parse_options_from_client(params.settings) { - Ok(opts) => { - self.update_db_connection(opts).await; + let capabilities = self.client_capabilities.read().await; + + if capabilities.as_ref().unwrap().supports_pull_opts { + let opts = self.request_opts_from_client().await; + if opts.is_some() { + self.update_db_connection(opts.unwrap()).await; + return; + } + } + + let opts = self.parse_options_from_client(params.settings); + + if opts.is_some() { + self.update_db_connection(opts.unwrap()).await; + } + } + + async fn execute_command( + &self, + params: ExecuteCommandParams, + ) -> tower_lsp::jsonrpc::Result> { + match CommandType::from_id(params.command.replace("pglsp.", "").as_str()) { + Some(CommandType::ExecuteStatement) => { + if params.arguments.is_empty() { + return tower_lsp::jsonrpc::Result::Err(Error::new("No arguments provided!")); + } + + let stmt = params + .arguments + .into_iter() + .next() + .map(|v| serde_json::from_value(v)) + .unwrap()?; + + let conn = self.db.read().await; + match conn + .as_ref() + .expect("No connection to the database.") + .run_stmt(stmt) + .await + { + Ok(pg_result) => { + self.client + .send_notification::(ShowMessageParams { + typ: MessageType::INFO, + message: format!( + "Success! Affected rows: {}", + pg_result.rows_affected() + ), + }) + .await; + } + Err(why) => { + self.client + .send_notification::(ShowMessageParams { + typ: MessageType::ERROR, + message: format!("Error! Statement exectuion failed: {}", why), + }) + .await; + } + }; } - Err(e) => { + None => { self.client - .log_message(MessageType::ERROR, format!("Error parsing configuration: {}", e)) + .show_message( + MessageType::ERROR, + format!("Unknown command: {}", params.command), + ) .await; } }; + Ok(None) } } diff --git a/crates/pg_lsp/src/client/client_flags.rs b/crates/pg_lsp/src/client/client_flags.rs index c4a722ee..689a3cb0 100644 --- a/crates/pg_lsp/src/client/client_flags.rs +++ b/crates/pg_lsp/src/client/client_flags.rs @@ -5,22 +5,22 @@ use tower_lsp::lsp_types::InitializeParams; #[derive(Debug, Clone)] pub struct ClientFlags { /// If `true`, the server can pull configuration from the client. - pub has_configuration: bool, + pub supports_pull_opts: bool, /// If `true`, the client notifies the server when its configuration changes. - pub will_push_configuration: bool, + pub supports_dynamic_registration: bool, } impl ClientFlags { pub(crate) fn from_initialize_request_params(params: &InitializeParams) -> Self { - let has_configuration = params + let supports_pull_opts = params .capabilities .workspace .as_ref() .and_then(|w| w.configuration) .unwrap_or(false); - let will_push_configuration = params + let supports_dynamic_registration = params .capabilities .workspace .as_ref() @@ -29,8 +29,8 @@ impl ClientFlags { .unwrap_or(false); Self { - has_configuration, - will_push_configuration, + supports_pull_opts, + supports_dynamic_registration, } } } diff --git a/crates/pg_lsp/src/db_connection.rs b/crates/pg_lsp/src/db_connection.rs index 51ba633d..ef660bc7 100644 --- a/crates/pg_lsp/src/db_connection.rs +++ b/crates/pg_lsp/src/db_connection.rs @@ -1,5 +1,9 @@ +use pg_commands::ExecuteStatementCommand; use pg_schema_cache::SchemaCache; -use sqlx::{postgres::PgListener, PgPool}; +use sqlx::{ + postgres::{PgListener, PgQueryResult}, + PgPool, +}; use tokio::task::JoinHandle; #[derive(Debug)] @@ -19,6 +23,14 @@ impl DbConnection { }) } + /// TODO: this should simply take a `Command` type, and the individual + /// enums should have their deps included (i.e. `ExecuteStatement(String)`) + pub async fn run_stmt(&self, stmt: String) -> anyhow::Result { + let command = ExecuteStatementCommand::new(stmt); + let pool = self.pool.clone(); + command.run(Some(pool)).await + } + pub(crate) fn connected_to(&self, connection_string: &str) -> bool { connection_string == self.connection_string } diff --git a/crates/pg_lsp/src/server.rs b/crates/pg_lsp/src/server.rs index 65dc687c..b3026da2 100644 --- a/crates/pg_lsp/src/server.rs +++ b/crates/pg_lsp/src/server.rs @@ -725,7 +725,7 @@ impl Server { &mut self, params: DidChangeConfigurationParams, ) -> anyhow::Result<()> { - if self.client_flags.has_configuration { + if self.client_flags.supports_pull_opts { self.pull_options(); } else { let options = self.client.parse_options(params.settings)?; @@ -913,11 +913,11 @@ impl Server { } pub async fn run(mut self) -> anyhow::Result<()> { - if self.client_flags.will_push_configuration { + if self.client_flags.supports_dynamic_registration { self.register_configuration(); } - if self.client_flags.has_configuration { + if self.client_flags.supports_pull_opts { self.pull_options(); } From abb8893e3e1ffb6cb0fbcf8926c87cb8c85bc5ba Mon Sep 17 00:00:00 2001 From: Julian Date: Fri, 25 Oct 2024 09:20:35 +0200 Subject: [PATCH 18/33] fixed it --- crates/pg_lsp/src/b_server.rs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/crates/pg_lsp/src/b_server.rs b/crates/pg_lsp/src/b_server.rs index 2217a113..a1c36777 100644 --- a/crates/pg_lsp/src/b_server.rs +++ b/crates/pg_lsp/src/b_server.rs @@ -4,7 +4,7 @@ use notification::ShowMessage; use pg_commands::CommandType; use pg_workspace::Workspace; use tokio::sync::RwLock; -use tower_lsp::jsonrpc::Error; +use tower_lsp::jsonrpc; use tower_lsp::lsp_types::*; use tower_lsp::{Client, LanguageServer}; @@ -134,7 +134,7 @@ impl Server { #[tower_lsp::async_trait] impl LanguageServer for Server { - async fn initialize(&self, params: InitializeParams) -> tower_lsp::jsonrpc::Result { + async fn initialize(&self, params: InitializeParams) -> jsonrpc::Result { let flags = ClientFlags::from_initialize_request_params(¶ms); self.client_capabilities.blocking_write().replace(flags); @@ -174,7 +174,7 @@ impl LanguageServer for Server { .await; } - async fn shutdown(&self) -> anyhow::Result<()> { + async fn shutdown(&self) -> jsonrpc::Result<()> { self.client .log_message(MessageType::INFO, "Postgres LSP terminated.") .await; @@ -202,19 +202,16 @@ impl LanguageServer for Server { async fn execute_command( &self, params: ExecuteCommandParams, - ) -> tower_lsp::jsonrpc::Result> { + ) -> jsonrpc::Result> { match CommandType::from_id(params.command.replace("pglsp.", "").as_str()) { Some(CommandType::ExecuteStatement) => { if params.arguments.is_empty() { - return tower_lsp::jsonrpc::Result::Err(Error::new("No arguments provided!")); + return jsonrpc::Result::Err(jsonrpc::Error::invalid_request()); } - let stmt = params - .arguments - .into_iter() - .next() - .map(|v| serde_json::from_value(v)) - .unwrap()?; + let params = params.arguments.into_iter().next().unwrap(); + let stmt = serde_json::from_value(params) + .map_err(|_| jsonrpc::Error::invalid_request())?; let conn = self.db.read().await; match conn From 5e4e89e9bb2fd128192218bff5bffb9e51b65147 Mon Sep 17 00:00:00 2001 From: Julian Date: Fri, 25 Oct 2024 10:05:49 +0200 Subject: [PATCH 19/33] get_diagnostics --- crates/pg_lsp/src/b_server.rs | 109 ++++++++++--------------- crates/pg_lsp/src/lib.rs | 1 + crates/pg_lsp/src/workspace_handler.rs | 90 ++++++++++++++++++++ 3 files changed, 135 insertions(+), 65 deletions(-) create mode 100644 crates/pg_lsp/src/workspace_handler.rs diff --git a/crates/pg_lsp/src/b_server.rs b/crates/pg_lsp/src/b_server.rs index a1c36777..2f1fa87c 100644 --- a/crates/pg_lsp/src/b_server.rs +++ b/crates/pg_lsp/src/b_server.rs @@ -1,31 +1,27 @@ -use std::sync::Arc; - use notification::ShowMessage; use pg_commands::CommandType; -use pg_workspace::Workspace; use tokio::sync::RwLock; use tower_lsp::jsonrpc; use tower_lsp::lsp_types::*; use tower_lsp::{Client, LanguageServer}; use crate::client::client_flags::ClientFlags; -use crate::db_connection::DbConnection; use crate::server::options::ClientConfigurationOptions; +use crate::utils::file_path; +use crate::utils::normalize_uri; +use crate::workspace_handler::WorkspaceHandler; struct Server { client: Client, - db: RwLock>, - ide: Arc>, + workspace_handler: WorkspaceHandler, client_capabilities: RwLock>, } impl Server { pub async fn new(client: Client) -> Self { - let ide = Arc::new(RwLock::new(Workspace::new())); Self { client, - db: RwLock::new(None), - ide, + workspace_handler: WorkspaceHandler::new(), client_capabilities: RwLock::new(None), } } @@ -54,46 +50,6 @@ impl Server { } } - /// `update_db_connection` will update `Self`'s database connection. - /// If the passed-in connection string is the same that we're already connected to, it's a noop. - /// Otherwise, it'll first open a new connection, replace `Self`'s connection, and then close - /// the old one. - async fn update_db_connection( - &self, - options: ClientConfigurationOptions, - ) -> anyhow::Result<()> { - if options.db_connection_string.is_none() - || self - .db - .read() - .await - .as_ref() - // if the connection is already connected to the same database, do nothing - .is_some_and(|c| c.connected_to(options.db_connection_string.as_ref().unwrap())) - { - return Ok(()); - } - - let connection_string = options.db_connection_string.unwrap(); - - let mut db = DbConnection::new(connection_string).await?; - - let ide = self.ide.clone(); - db.listen_for_schema_updates(move |schema| { - let _guard = ide.blocking_write().set_schema_cache(schema); - }); - - let mut current_db = self.db.blocking_write(); - let old_db = current_db.replace(db); - - if old_db.is_some() { - let old_db = old_db.unwrap(); - old_db.close().await; - } - - Ok(()) - } - async fn request_opts_from_client(&self) -> Option { let params = ConfigurationParams { items: vec![ConfigurationItem { @@ -130,6 +86,30 @@ impl Server { } } } + + async fn publish_diagnostics(&self, mut uri: Url) -> anyhow::Result<()> { + normalize_uri(&mut uri); + + let diagnostics = self + .workspace_handler + .get_diagnostics(file_path(&uri)) + .await; + + self.client + .send_notification::(ShowMessageParams { + typ: MessageType::INFO, + message: format!("diagnostics {}", diagnostics.len()), + }) + .await; + + let params = PublishDiagnosticsParams { + uri, + diagnostics, + version: None, + }; + + Ok(()) + } } #[tower_lsp::async_trait] @@ -186,16 +166,24 @@ impl LanguageServer for Server { if capabilities.as_ref().unwrap().supports_pull_opts { let opts = self.request_opts_from_client().await; - if opts.is_some() { - self.update_db_connection(opts.unwrap()).await; + if opts + .as_ref() + .is_some_and(|o| o.db_connection_string.is_some()) + { + let conn_str = opts.unwrap().db_connection_string.unwrap(); + self.workspace_handler.change_db(conn_str).await; return; } } let opts = self.parse_options_from_client(params.settings); - if opts.is_some() { - self.update_db_connection(opts.unwrap()).await; + if opts + .as_ref() + .is_some_and(|o| o.db_connection_string.is_some()) + { + let conn_str = opts.unwrap().db_connection_string.unwrap(); + self.workspace_handler.change_db(conn_str).await; } } @@ -213,21 +201,12 @@ impl LanguageServer for Server { let stmt = serde_json::from_value(params) .map_err(|_| jsonrpc::Error::invalid_request())?; - let conn = self.db.read().await; - match conn - .as_ref() - .expect("No connection to the database.") - .run_stmt(stmt) - .await - { - Ok(pg_result) => { + match self.workspace_handler.run_stmt(stmt).await { + Ok(rows_affected) => { self.client .send_notification::(ShowMessageParams { typ: MessageType::INFO, - message: format!( - "Success! Affected rows: {}", - pg_result.rows_affected() - ), + message: format!("Success! Affected rows: {}", rows_affected), }) .await; } diff --git a/crates/pg_lsp/src/lib.rs b/crates/pg_lsp/src/lib.rs index 303a6ac4..ef79325f 100644 --- a/crates/pg_lsp/src/lib.rs +++ b/crates/pg_lsp/src/lib.rs @@ -4,3 +4,4 @@ pub mod server; mod utils; mod b_server; +mod workspace_handler; diff --git a/crates/pg_lsp/src/workspace_handler.rs b/crates/pg_lsp/src/workspace_handler.rs new file mode 100644 index 00000000..3929c44c --- /dev/null +++ b/crates/pg_lsp/src/workspace_handler.rs @@ -0,0 +1,90 @@ +use std::sync::Arc; + +use lsp_types::Range; +use pg_base_db::PgLspPath; +use pg_diagnostics::Diagnostic; +use pg_workspace::Workspace; +use text_size::TextRange; +use tokio::sync::RwLock; + +use crate::{db_connection::DbConnection, utils::line_index_ext::LineIndexExt}; + +pub struct WorkspaceHandler { + db: RwLock>, + ide: Arc>, +} + +impl WorkspaceHandler { + pub fn new() -> Self { + let ide = Arc::new(RwLock::new(Workspace::new())); + Self { + db: RwLock::new(None), + ide, + } + } + + /// `update_db_connection` will update `Self`'s database connection. + /// If the passed-in connection string is the same that we're already connected to, it's a noop. + /// Otherwise, it'll first open a new connection, replace `Self`'s connection, and then close + /// the old one. + pub async fn change_db(&self, connection_string: String) -> anyhow::Result<()> { + if self + .db + .read() + .await + .as_ref() + // if the connection is already connected to the same database, do nothing + .is_some_and(|c| c.connected_to(&connection_string)) + { + return Ok(()); + } + + let mut db = DbConnection::new(connection_string).await?; + + let ide = self.ide.clone(); + db.listen_for_schema_updates(move |schema| { + let _guard = ide.blocking_write().set_schema_cache(schema); + }); + + let mut current_db = self.db.blocking_write(); + let old_db = current_db.replace(db); + + if old_db.is_some() { + let old_db = old_db.unwrap(); + old_db.close().await; + } + + Ok(()) + } + + pub async fn run_stmt(&self, stmt: String) -> anyhow::Result { + let db = self.db.read().await; + db.as_ref() + .expect("No Db Connection") + .run_stmt(stmt) + .await + .map(|pg_query_result| pg_query_result.rows_affected()) + } + + pub async fn get_diagnostics(&self, path: PgLspPath) -> Vec<(Diagnostic, Range)> { + let ide = self.ide.read().await; + + // make sure there are documents at the provided path before + // trying to collect diagnostics. + let doc = ide.documents.get(&path); + if doc.is_none() { + return vec![]; + } + + self.ide + .read() + .await + .diagnostics(&path) + .into_iter() + .map(|d| { + let range = doc.as_ref().unwrap().line_index.line_col_lsp_range(d.range).unwrap(); + (d, range) + }) + .collect() + } +} From 12a079e643ecf69d8729703cbd7f27f1bec51e47 Mon Sep 17 00:00:00 2001 From: Julian Date: Fri, 25 Oct 2024 10:12:56 +0200 Subject: [PATCH 20/33] diagnostics --- crates/pg_lsp/src/b_server.rs | 14 ++++++++++---- crates/pg_lsp/src/utils/line_index_ext.rs | 2 +- crates/pg_lsp/src/utils/to_proto.rs | 15 ++++----------- crates/pg_lsp/src/workspace_handler.rs | 10 +++++++--- 4 files changed, 22 insertions(+), 19 deletions(-) diff --git a/crates/pg_lsp/src/b_server.rs b/crates/pg_lsp/src/b_server.rs index 2f1fa87c..ee0254cf 100644 --- a/crates/pg_lsp/src/b_server.rs +++ b/crates/pg_lsp/src/b_server.rs @@ -9,6 +9,7 @@ use crate::client::client_flags::ClientFlags; use crate::server::options::ClientConfigurationOptions; use crate::utils::file_path; use crate::utils::normalize_uri; +use crate::utils::to_proto; use crate::workspace_handler::WorkspaceHandler; struct Server { @@ -87,7 +88,7 @@ impl Server { } } - async fn publish_diagnostics(&self, mut uri: Url) -> anyhow::Result<()> { + async fn publish_diagnostics(&self, mut uri: Url) { normalize_uri(&mut uri); let diagnostics = self @@ -95,12 +96,16 @@ impl Server { .get_diagnostics(file_path(&uri)) .await; + let diagnostics: Vec = diagnostics + .into_iter() + .map(|(d, r)| to_proto::diagnostic(d, r)) + .collect(); + self.client .send_notification::(ShowMessageParams { typ: MessageType::INFO, message: format!("diagnostics {}", diagnostics.len()), - }) - .await; + }); let params = PublishDiagnosticsParams { uri, @@ -108,7 +113,8 @@ impl Server { version: None, }; - Ok(()) + self.client + .send_notification::(params); } } diff --git a/crates/pg_lsp/src/utils/line_index_ext.rs b/crates/pg_lsp/src/utils/line_index_ext.rs index 6928326c..cd92e03a 100644 --- a/crates/pg_lsp/src/utils/line_index_ext.rs +++ b/crates/pg_lsp/src/utils/line_index_ext.rs @@ -1,6 +1,6 @@ use line_index::{LineCol, LineColUtf16, LineIndex}; -use lsp_types::{Position, Range}; use text_size::{TextRange, TextSize}; +use tower_lsp::lsp_types::{Position, Range}; pub trait LineIndexExt { fn offset_lsp(&self, line_col: Position) -> Option; diff --git a/crates/pg_lsp/src/utils/to_proto.rs b/crates/pg_lsp/src/utils/to_proto.rs index f06f80f8..6f076ad7 100644 --- a/crates/pg_lsp/src/utils/to_proto.rs +++ b/crates/pg_lsp/src/utils/to_proto.rs @@ -1,9 +1,7 @@ -use pg_base_db::Document; use pg_diagnostics::Diagnostic; +use tower_lsp::lsp_types; -use super::line_index_ext::LineIndexExt; - -pub fn diagnostic(document: &Document, diagnostic: &Diagnostic) -> lsp_types::Diagnostic { +pub fn diagnostic(diagnostic: Diagnostic, range: lsp_types::Range) -> lsp_types::Diagnostic { let severity = match diagnostic.severity { pg_diagnostics::Severity::Error => lsp_types::DiagnosticSeverity::ERROR, pg_diagnostics::Severity::Warning => lsp_types::DiagnosticSeverity::WARNING, @@ -12,14 +10,9 @@ pub fn diagnostic(document: &Document, diagnostic: &Diagnostic) -> lsp_types::Di pg_diagnostics::Severity::Fatal => lsp_types::DiagnosticSeverity::ERROR, }; - let range = document - .line_index - .line_col_lsp_range(diagnostic.range) - .unwrap(); - lsp_types::Diagnostic { severity: Some(severity), - source: Some(diagnostic.source.clone()), - ..lsp_types::Diagnostic::new_simple(range, diagnostic.message.clone()) + source: Some(diagnostic.source), + ..lsp_types::Diagnostic::new_simple(range, diagnostic.message) } } diff --git a/crates/pg_lsp/src/workspace_handler.rs b/crates/pg_lsp/src/workspace_handler.rs index 3929c44c..ca99ecb1 100644 --- a/crates/pg_lsp/src/workspace_handler.rs +++ b/crates/pg_lsp/src/workspace_handler.rs @@ -1,11 +1,10 @@ use std::sync::Arc; -use lsp_types::Range; use pg_base_db::PgLspPath; use pg_diagnostics::Diagnostic; use pg_workspace::Workspace; -use text_size::TextRange; use tokio::sync::RwLock; +use tower_lsp::lsp_types::Range; use crate::{db_connection::DbConnection, utils::line_index_ext::LineIndexExt}; @@ -82,7 +81,12 @@ impl WorkspaceHandler { .diagnostics(&path) .into_iter() .map(|d| { - let range = doc.as_ref().unwrap().line_index.line_col_lsp_range(d.range).unwrap(); + let range = doc + .as_ref() + .unwrap() + .line_index + .line_col_lsp_range(d.range) + .unwrap(); (d, range) }) .collect() From 557ad5b7aed307c497a251bc96dc064899026b25 Mon Sep 17 00:00:00 2001 From: Julian Date: Sun, 27 Oct 2024 15:11:34 +0100 Subject: [PATCH 21/33] three to go --- crates/pg_lsp/src/b_server.rs | 75 +++++++++-- crates/pg_lsp/src/db_connection.rs | 6 +- crates/pg_lsp/src/lib.rs | 2 +- crates/pg_lsp/src/server.rs | 4 +- crates/pg_lsp/src/session.rs | 180 +++++++++++++++++++++++++ crates/pg_lsp/src/workspace_handler.rs | 94 ------------- 6 files changed, 254 insertions(+), 107 deletions(-) create mode 100644 crates/pg_lsp/src/session.rs delete mode 100644 crates/pg_lsp/src/workspace_handler.rs diff --git a/crates/pg_lsp/src/b_server.rs b/crates/pg_lsp/src/b_server.rs index ee0254cf..0030c68e 100644 --- a/crates/pg_lsp/src/b_server.rs +++ b/crates/pg_lsp/src/b_server.rs @@ -7,14 +7,14 @@ use tower_lsp::{Client, LanguageServer}; use crate::client::client_flags::ClientFlags; use crate::server::options::ClientConfigurationOptions; +use crate::session::Session; use crate::utils::file_path; use crate::utils::normalize_uri; use crate::utils::to_proto; -use crate::workspace_handler::WorkspaceHandler; struct Server { client: Client, - workspace_handler: WorkspaceHandler, + session: Session, client_capabilities: RwLock>, } @@ -22,7 +22,7 @@ impl Server { pub async fn new(client: Client) -> Self { Self { client, - workspace_handler: WorkspaceHandler::new(), + session: Session::new(), client_capabilities: RwLock::new(None), } } @@ -91,10 +91,8 @@ impl Server { async fn publish_diagnostics(&self, mut uri: Url) { normalize_uri(&mut uri); - let diagnostics = self - .workspace_handler - .get_diagnostics(file_path(&uri)) - .await; + let url = file_path(&uri); + let diagnostics = self.session.get_diagnostics(url).await; let diagnostics: Vec = diagnostics .into_iter() @@ -177,11 +175,13 @@ impl LanguageServer for Server { .is_some_and(|o| o.db_connection_string.is_some()) { let conn_str = opts.unwrap().db_connection_string.unwrap(); - self.workspace_handler.change_db(conn_str).await; + self.session.change_db(conn_str).await; return; } } + // if we couldn't pull settings from the client, + // we'll try parsing the passed in params. let opts = self.parse_options_from_client(params.settings); if opts @@ -189,10 +189,65 @@ impl LanguageServer for Server { .is_some_and(|o| o.db_connection_string.is_some()) { let conn_str = opts.unwrap().db_connection_string.unwrap(); - self.workspace_handler.change_db(conn_str).await; + self.session.change_db(conn_str).await; + } + } + + async fn did_open(&self, params: DidOpenTextDocumentParams) { + let mut uri = params.text_document.uri; + normalize_uri(&mut uri); + + let changed_urls = self + .session + .apply_doc_changes( + file_path(url), + params.text_document.version, + params.text_document.text, + ) + .await; + + for url in changed_urls { + self.publish_diagnostics(url).await; + } + } + + async fn did_save(&self, params: DidSaveTextDocumentParams) { + let mut uri = params.text_document.uri; + normalize_uri(&mut uri); + + self.publish_diagnostics(uri).await; + + // TODO: "Compute Now" + let changed_urls = self.session.recompute_and_get_changed_files(); + for url in changed_urls { + self.publish_diagnostics(url).await; } } + async fn did_change(&self, params: DidChangeTextDocumentParams) { + todo!() + } + + async fn did_close(&self, params: DidSaveTextDocumentParams) { + let mut uri = params.text_document.uri; + normalize_uri(&mut uri); + let path = file_path(&uri); + + self.session.on_file_closed(path); + } + + async fn code_action(&self, params: CodeActionParams) -> Result> { + let mut uri = params.text_document.uri; + normalize_uri(&mut uri); + + let path = file_path(&uri); + let range = params.range; + + let actions = self.session.get_available_code_actions(path, range); + + Ok(actions) + } + async fn execute_command( &self, params: ExecuteCommandParams, @@ -207,7 +262,7 @@ impl LanguageServer for Server { let stmt = serde_json::from_value(params) .map_err(|_| jsonrpc::Error::invalid_request())?; - match self.workspace_handler.run_stmt(stmt).await { + match self.session.run_stmt(stmt).await { Ok(rows_affected) => { self.client .send_notification::(ShowMessageParams { diff --git a/crates/pg_lsp/src/db_connection.rs b/crates/pg_lsp/src/db_connection.rs index ef660bc7..82f1221c 100644 --- a/crates/pg_lsp/src/db_connection.rs +++ b/crates/pg_lsp/src/db_connection.rs @@ -8,7 +8,7 @@ use tokio::task::JoinHandle; #[derive(Debug)] pub(crate) struct DbConnection { - pub pool: PgPool, + pool: PgPool, connection_string: String, schema_update_handle: Option>, } @@ -75,4 +75,8 @@ impl DbConnection { Ok(()) } + + pub(crate) fn get_pool(&self) -> PgPool { + self.pool.clone() + } } diff --git a/crates/pg_lsp/src/lib.rs b/crates/pg_lsp/src/lib.rs index ef79325f..76c6c8f1 100644 --- a/crates/pg_lsp/src/lib.rs +++ b/crates/pg_lsp/src/lib.rs @@ -4,4 +4,4 @@ pub mod server; mod utils; mod b_server; -mod workspace_handler; +mod session; diff --git a/crates/pg_lsp/src/server.rs b/crates/pg_lsp/src/server.rs index b3026da2..16d4628e 100644 --- a/crates/pg_lsp/src/server.rs +++ b/crates/pg_lsp/src/server.rs @@ -197,7 +197,9 @@ impl Server { }) .unwrap(); } + let changed = cloned_ide.compute(conn); + let urls = HashSet::<&str>::from_iter( changed.iter().map(|f| f.document_url.to_str().unwrap()), ); @@ -375,7 +377,7 @@ impl Server { DocumentChange::new(params.text_document.version, changes), ); - let conn = self.db_conn.as_ref().map(|p| p.pool.clone()); + let conn = self.db_conn.as_ref().map(|p| p.get_pool()); self.compute_debouncer.put(conn); Ok(()) diff --git a/crates/pg_lsp/src/session.rs b/crates/pg_lsp/src/session.rs new file mode 100644 index 00000000..f55888cb --- /dev/null +++ b/crates/pg_lsp/src/session.rs @@ -0,0 +1,180 @@ +use std::{collections::HashSet, sync::Arc}; + +use pg_base_db::{Change, DocumentChange, PgLspPath}; +use pg_diagnostics::Diagnostic; +use pg_workspace::Workspace; +use tokio::sync::RwLock; +use tower_lsp::lsp_types::{CodeAction, Range}; + +use crate::{db_connection::DbConnection, utils::line_index_ext::LineIndexExt}; + +pub struct Session { + db: RwLock>, + ide: Arc>, +} + +impl Session { + pub fn new() -> Self { + let ide = Arc::new(RwLock::new(Workspace::new())); + Self { + db: RwLock::new(None), + ide, + } + } + + /// `update_db_connection` will update `Self`'s database connection. + /// If the passed-in connection string is the same that we're already connected to, it's a noop. + /// Otherwise, it'll first open a new connection, replace `Self`'s connection, and then close + /// the old one. + pub async fn change_db(&self, connection_string: String) -> anyhow::Result<()> { + if self + .db + .read() + .await + .as_ref() + // if the connection is already connected to the same database, do nothing + .is_some_and(|c| c.connected_to(&connection_string)) + { + return Ok(()); + } + + let mut db = DbConnection::new(connection_string).await?; + + let ide = self.ide.clone(); + db.listen_for_schema_updates(move |schema| { + let _guard = ide.blocking_write().set_schema_cache(schema); + }); + + let mut current_db = self.db.blocking_write(); + let old_db = current_db.replace(db); + + if old_db.is_some() { + let old_db = old_db.unwrap(); + old_db.close().await; + } + + Ok(()) + } + + pub async fn run_stmt(&self, stmt: String) -> anyhow::Result { + let db = self.db.read().await; + db.as_ref() + .expect("No Db Connection") + .run_stmt(stmt) + .await + .map(|pg_query_result| pg_query_result.rows_affected()) + } + + pub async fn on_file_closed(&self, path: PgLspPath) { + let ide = self.ide.read().await; + ide.remove_document(path); + } + + pub async fn get_diagnostics(&self, path: PgLspPath) -> Vec<(Diagnostic, Range)> { + let ide = self.ide.read().await; + + // make sure there are documents at the provided path before + // trying to collect diagnostics. + let doc = ide.documents.get(&path); + if doc.is_none() { + return vec![]; + } + + ide.diagnostics(&path) + .into_iter() + .map(|d| { + let range = doc + .as_ref() + .unwrap() + .line_index + .line_col_lsp_range(d.range) + .unwrap(); + (d, range) + }) + .collect() + } + + pub async fn apply_doc_changes( + &self, + path: PgLspPath, + version: i32, + text: String, + ) -> HashSet { + { + let ide = self.ide.read().await; + + let doc = ide.documents.get(&path); + if doc.is_none() { + return HashSet::new(); + } + + ide.apply_change( + path, + DocumentChange::new(version, vec![Change { range: None, text }]), + ); + } + + self.recompute_and_get_changed_files() + } + + pub async fn recompute_and_get_changed_files(&self) -> HashSet { + let ide = self.ide.read().await; + + let db = self.db.read().await; + let pool = db.as_ref().map(|d| d.get_pool()); + + let changed_files = ide.compute(pool); + + changed_files + .into_iter() + .map(|f| f.document_url.to_string_lossy().to_string()) + .collect() + } + + pub async fn get_available_code_actions( + &self, + path: PgLspPath, + range: Range, + ) -> Option> { + let ide = self.ide.read().await; + let db = self.db.read().await; + + let doc = ide.documents.get(&path); + if doc.is_none() || db.is_none() { + return None; + } + + let doc = doc.unwrap(); + + let range = doc.line_index.offset_lsp_range(range).unwrap(); + + // for now, we only provide `ExcecuteStatementCommand`s. + let actions = doc + .statements_at_range(&range) + .into_iter() + .map(|stmt| { + let cmd = ExecuteStatementCommand::command_type(); + let title = format!( + "Execute '{}'", + ExecuteStatementCommand::trim_statement(stmt.text.clone(), 50) + ); + CodeAction { + title: title.clone(), + kind: None, + edit: None, + command: Some(Command { + title, + command: format!("pglsp.{}", cmd.id()), + arguments: Some(vec![serde_json::to_value(stmt.text.clone()).unwrap()]), + }), + diagnostics: None, + is_preferred: None, + disabled: None, + data: None, + } + }) + .collect(); + + Some(actions) + } +} diff --git a/crates/pg_lsp/src/workspace_handler.rs b/crates/pg_lsp/src/workspace_handler.rs deleted file mode 100644 index ca99ecb1..00000000 --- a/crates/pg_lsp/src/workspace_handler.rs +++ /dev/null @@ -1,94 +0,0 @@ -use std::sync::Arc; - -use pg_base_db::PgLspPath; -use pg_diagnostics::Diagnostic; -use pg_workspace::Workspace; -use tokio::sync::RwLock; -use tower_lsp::lsp_types::Range; - -use crate::{db_connection::DbConnection, utils::line_index_ext::LineIndexExt}; - -pub struct WorkspaceHandler { - db: RwLock>, - ide: Arc>, -} - -impl WorkspaceHandler { - pub fn new() -> Self { - let ide = Arc::new(RwLock::new(Workspace::new())); - Self { - db: RwLock::new(None), - ide, - } - } - - /// `update_db_connection` will update `Self`'s database connection. - /// If the passed-in connection string is the same that we're already connected to, it's a noop. - /// Otherwise, it'll first open a new connection, replace `Self`'s connection, and then close - /// the old one. - pub async fn change_db(&self, connection_string: String) -> anyhow::Result<()> { - if self - .db - .read() - .await - .as_ref() - // if the connection is already connected to the same database, do nothing - .is_some_and(|c| c.connected_to(&connection_string)) - { - return Ok(()); - } - - let mut db = DbConnection::new(connection_string).await?; - - let ide = self.ide.clone(); - db.listen_for_schema_updates(move |schema| { - let _guard = ide.blocking_write().set_schema_cache(schema); - }); - - let mut current_db = self.db.blocking_write(); - let old_db = current_db.replace(db); - - if old_db.is_some() { - let old_db = old_db.unwrap(); - old_db.close().await; - } - - Ok(()) - } - - pub async fn run_stmt(&self, stmt: String) -> anyhow::Result { - let db = self.db.read().await; - db.as_ref() - .expect("No Db Connection") - .run_stmt(stmt) - .await - .map(|pg_query_result| pg_query_result.rows_affected()) - } - - pub async fn get_diagnostics(&self, path: PgLspPath) -> Vec<(Diagnostic, Range)> { - let ide = self.ide.read().await; - - // make sure there are documents at the provided path before - // trying to collect diagnostics. - let doc = ide.documents.get(&path); - if doc.is_none() { - return vec![]; - } - - self.ide - .read() - .await - .diagnostics(&path) - .into_iter() - .map(|d| { - let range = doc - .as_ref() - .unwrap() - .line_index - .line_col_lsp_range(d.range) - .unwrap(); - (d, range) - }) - .collect() - } -} From 94077e8df066181d4cb5e7ca6063df0a97c85283 Mon Sep 17 00:00:00 2001 From: Julian Date: Sun, 27 Oct 2024 15:25:22 +0100 Subject: [PATCH 22/33] two to go --- crates/pg_lsp/src/b_server.rs | 12 +++++ crates/pg_lsp/src/db_connection.rs | 8 --- crates/pg_lsp/src/session.rs | 78 ++++++++++++++++++++++++++---- 3 files changed, 80 insertions(+), 18 deletions(-) diff --git a/crates/pg_lsp/src/b_server.rs b/crates/pg_lsp/src/b_server.rs index 0030c68e..ef6a5da0 100644 --- a/crates/pg_lsp/src/b_server.rs +++ b/crates/pg_lsp/src/b_server.rs @@ -248,6 +248,18 @@ impl LanguageServer for Server { Ok(actions) } + async fn inlay_hint(&self, params: InlayHintParams) -> Result>> { + let mut uri = params.text_document.uri; + normalize_uri(&mut uri); + + let path = file_path(&uri); + let range = params.range; + + let hints = self.session.get_inlay_hints(path, range).await; + + Ok(hints) + } + async fn execute_command( &self, params: ExecuteCommandParams, diff --git a/crates/pg_lsp/src/db_connection.rs b/crates/pg_lsp/src/db_connection.rs index 82f1221c..1389768b 100644 --- a/crates/pg_lsp/src/db_connection.rs +++ b/crates/pg_lsp/src/db_connection.rs @@ -23,14 +23,6 @@ impl DbConnection { }) } - /// TODO: this should simply take a `Command` type, and the individual - /// enums should have their deps included (i.e. `ExecuteStatement(String)`) - pub async fn run_stmt(&self, stmt: String) -> anyhow::Result { - let command = ExecuteStatementCommand::new(stmt); - let pool = self.pool.clone(); - command.run(Some(pool)).await - } - pub(crate) fn connected_to(&self, connection_string: &str) -> bool { connection_string == self.connection_string } diff --git a/crates/pg_lsp/src/session.rs b/crates/pg_lsp/src/session.rs index f55888cb..fff00879 100644 --- a/crates/pg_lsp/src/session.rs +++ b/crates/pg_lsp/src/session.rs @@ -1,10 +1,11 @@ use std::{collections::HashSet, sync::Arc}; use pg_base_db::{Change, DocumentChange, PgLspPath}; +use pg_commands::ExecuteStatementCommand; use pg_diagnostics::Diagnostic; use pg_workspace::Workspace; use tokio::sync::RwLock; -use tower_lsp::lsp_types::{CodeAction, Range}; +use tower_lsp::lsp_types::{CodeAction, InlayHint, Range}; use crate::{db_connection::DbConnection, utils::line_index_ext::LineIndexExt}; @@ -56,13 +57,14 @@ impl Session { Ok(()) } + /// Runs the passed-in statement against the underlying database. pub async fn run_stmt(&self, stmt: String) -> anyhow::Result { let db = self.db.read().await; - db.as_ref() - .expect("No Db Connection") - .run_stmt(stmt) - .await - .map(|pg_query_result| pg_query_result.rows_affected()) + let pool = db.map(|d| d.get_pool()); + + let cmd = ExecuteStatementCommand::new(stmt); + + cmd.run(pool).await } pub async fn on_file_closed(&self, path: PgLspPath) { @@ -137,15 +139,17 @@ impl Session { range: Range, ) -> Option> { let ide = self.ide.read().await; - let db = self.db.read().await; - let doc = ide.documents.get(&path); - if doc.is_none() || db.is_none() { + if doc.is_none() { return None; } - let doc = doc.unwrap(); + let db = self.db.read().await; + if db.is_none() { + return None; + } + let doc = doc.unwrap(); let range = doc.line_index.offset_lsp_range(range).unwrap(); // for now, we only provide `ExcecuteStatementCommand`s. @@ -177,4 +181,58 @@ impl Session { Some(actions) } + + pub async fn get_inlay_hints(&self, path: PgLspPath, range: Range) -> Option> { + let ide = self.ide.read().await; + let doc = ide.documents.get(&path); + if doc.is_none() { + return None; + } + + let doc = doc.unwrap(); + let range = doc.line_index.offset_lsp_range(range).unwrap(); + + let schema_cache = ide.schema_cache.read().expect("Unable to get Schema Cache"); + + let hints = doc + .statements_at_range(&range) + .into_iter() + .flat_map(|stmt| { + ::pg_inlay_hints::inlay_hints(::pg_inlay_hints::InlayHintsParams { + ast: ide.pg_query.ast(&stmt).as_ref().map(|x| x.as_ref()), + enriched_ast: ide + .pg_query + .enriched_ast(&stmt) + .as_ref() + .map(|x| x.as_ref()), + tree: ide.tree_sitter.tree(&stmt).as_ref().map(|x| x.as_ref()), + cst: ide.pg_query.cst(&stmt).as_ref().map(|x| x.as_ref()), + schema_cache: &schema_cache, + }) + }) + .map(|hint| InlayHint { + position: doc.line_index.line_col_lsp(hint.offset).unwrap(), + label: match hint.content { + pg_inlay_hints::InlayHintContent::FunctionArg(arg) => { + InlayHintLabel::String(match arg.name { + Some(name) => format!("{} ({})", name, arg.type_name), + None => arg.type_name.clone(), + }) + } + }, + kind: match hint.content { + pg_inlay_hints::InlayHintContent::FunctionArg(_) => { + Some(InlayHintKind::PARAMETER) + } + }, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }) + .collect(); + + Some(hints) + } } From 1bc33d09ca4782ca0686409080aed38169780fd8 Mon Sep 17 00:00:00 2001 From: Julian Date: Sun, 27 Oct 2024 15:25:37 +0100 Subject: [PATCH 23/33] two to go --- crates/pg_lsp/src/b_server.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/pg_lsp/src/b_server.rs b/crates/pg_lsp/src/b_server.rs index ef6a5da0..641b33a7 100644 --- a/crates/pg_lsp/src/b_server.rs +++ b/crates/pg_lsp/src/b_server.rs @@ -251,7 +251,7 @@ impl LanguageServer for Server { async fn inlay_hint(&self, params: InlayHintParams) -> Result>> { let mut uri = params.text_document.uri; normalize_uri(&mut uri); - + let path = file_path(&uri); let range = params.range; From ee96db11e7148620ee709366335b51b78227fff6 Mon Sep 17 00:00:00 2001 From: Julian Date: Sun, 27 Oct 2024 16:19:21 +0100 Subject: [PATCH 24/33] hover --- crates/pg_lsp/src/b_server.rs | 32 ++++++++- crates/pg_lsp/src/session.rs | 130 ++++++++++++++++++++++++++-------- 2 files changed, 130 insertions(+), 32 deletions(-) diff --git a/crates/pg_lsp/src/b_server.rs b/crates/pg_lsp/src/b_server.rs index 641b33a7..4a7ab08e 100644 --- a/crates/pg_lsp/src/b_server.rs +++ b/crates/pg_lsp/src/b_server.rs @@ -243,7 +243,10 @@ impl LanguageServer for Server { let path = file_path(&uri); let range = params.range; - let actions = self.session.get_available_code_actions(path, range); + let actions = self + .session + .get_available_code_actions_or_commands(path, range) + .await; Ok(actions) } @@ -260,6 +263,33 @@ impl LanguageServer for Server { Ok(hints) } + async fn completion(&self, params: CompletionParams) -> Result> { + let mut uri = params.text_document_position.text_document.uri; + normalize_uri(&mut uri); + + let path = file_path(&uri); + let position = params.text_document_position.position; + + let completions = self.session.get_available_completions(path, position).await; + + Ok(completions.map(|c| CompletionResponse::List(c))) + } + + async fn hover(&self, params: HoverParams) -> Result> { + let mut uri = params.text_document_position_params.text_document.uri; + normalize_uri(&mut uri); + + let path = file_path(&uri); + let position = params.text_document_position_params.position; + + let hover_diagnostics = self + .session + .get_available_hover_diagnostics(path, position) + .await; + + Ok(hover_diagnostics) + } + async fn execute_command( &self, params: ExecuteCommandParams, diff --git a/crates/pg_lsp/src/session.rs b/crates/pg_lsp/src/session.rs index fff00879..ccc9c981 100644 --- a/crates/pg_lsp/src/session.rs +++ b/crates/pg_lsp/src/session.rs @@ -3,9 +3,13 @@ use std::{collections::HashSet, sync::Arc}; use pg_base_db::{Change, DocumentChange, PgLspPath}; use pg_commands::ExecuteStatementCommand; use pg_diagnostics::Diagnostic; +use pg_hover::HoverParams; use pg_workspace::Workspace; use tokio::sync::RwLock; -use tower_lsp::lsp_types::{CodeAction, InlayHint, Range}; +use tower_lsp::lsp_types::{ + CodeAction, CodeActionOrCommand, CompletionItem, CompletionItemKind, CompletionList, Hover, + HoverContents, InlayHint, MarkedString, Position, Range, +}; use crate::{db_connection::DbConnection, utils::line_index_ext::LineIndexExt}; @@ -133,21 +137,15 @@ impl Session { .collect() } - pub async fn get_available_code_actions( + pub async fn get_available_code_actions_or_commands( &self, path: PgLspPath, range: Range, - ) -> Option> { + ) -> Option> { let ide = self.ide.read().await; - let doc = ide.documents.get(&path); - if doc.is_none() { - return None; - } + let doc = ide.documents.get(&path)?; - let db = self.db.read().await; - if db.is_none() { - return None; - } + let db = self.db.read().await?; let doc = doc.unwrap(); let range = doc.line_index.offset_lsp_range(range).unwrap(); @@ -162,20 +160,11 @@ impl Session { "Execute '{}'", ExecuteStatementCommand::trim_statement(stmt.text.clone(), 50) ); - CodeAction { - title: title.clone(), - kind: None, - edit: None, - command: Some(Command { - title, - command: format!("pglsp.{}", cmd.id()), - arguments: Some(vec![serde_json::to_value(stmt.text.clone()).unwrap()]), - }), - diagnostics: None, - is_preferred: None, - disabled: None, - data: None, - } + CodeActionOrCommand::Command(Command { + title, + command: format!("pglsp.{}", cmd.id()), + arguments: Some(vec![serde_json::to_value(stmt.text.clone()).unwrap()]), + }) }) .collect(); @@ -184,13 +173,9 @@ impl Session { pub async fn get_inlay_hints(&self, path: PgLspPath, range: Range) -> Option> { let ide = self.ide.read().await; - let doc = ide.documents.get(&path); - if doc.is_none() { - return None; - } + let doc = ide.documents.get(&path)?; - let doc = doc.unwrap(); - let range = doc.line_index.offset_lsp_range(range).unwrap(); + let range = doc.line_index.offset_lsp_range(range)?; let schema_cache = ide.schema_cache.read().expect("Unable to get Schema Cache"); @@ -235,4 +220,87 @@ impl Session { Some(hints) } + + pub async fn get_available_completions( + &self, + path: PgLspPath, + position: Position, + ) -> Option { + let ide = self.ide.read().await; + + let doc = ide.documents.get(&path)?; + let offset = doc.line_index.offset_lsp(position)?; + let (range, stmt) = doc.statement_at_offset_with_range(&offset)?; + + let schema_cache = ide.schema_cache.read().expect("No Schema Cache"); + + let completion_items = pg_completions::complete(&CompletionParams { + position: pos - range.start() - TextSize::from(1), + text: stmt.text.as_str(), + tree: ide.tree_sitter.tree(&stmt).as_ref().map(|x| x.as_ref()), + schema: &schema, + }) + .items + .into_iter() + .map(|i| CompletionItem { + // TODO: add more data + label: i.data.label().to_string(), + label_details: None, + kind: Some(CompletionItemKind::CLASS), + detail: None, + documentation: None, + deprecated: None, + preselect: None, + sort_text: None, + filter_text: None, + insert_text: None, + insert_text_format: None, + insert_text_mode: None, + text_edit: None, + additional_text_edits: None, + commit_characters: None, + data: None, + tags: None, + command: None, + }) + .collect(); + + Some(CompletionList { + is_incomplete: false, + items: completion_items, + }) + } + + pub async fn get_available_hover_diagnostics( + &self, + path: PgLspPath, + position: Position, + ) -> Option { + let ide = self.ide.read().await; + let doc = ide.documents.get(&path)?; + + let offset = doc.line_index.offset_lsp(position)?; + + let (range, stmt) = doc.statement_at_offset_with_range(&offset)?; + let range_start = range.start(); + let hover_range = doc.line_index.line_col_lsp_range(range); + + let schema_cache = ide.schema_cache.read().expect("No Schema Cache"); + + ::pg_hover::hover(HoverParams { + position: offset - range_start, + source: stmt.text.as_str(), + enriched_ast: ide + .pg_query + .enriched_ast(&stmt) + .as_ref() + .map(|x| x.as_ref()), + tree: ide.tree_sitter.tree(&stmt).as_ref().map(|x| x.as_ref()), + schema_cache: schema_cache.clone(), + }) + .map(|hover| Hover { + contents: HoverContents::Scalar(MarkedString::String(hover.content)), + range: hover_range, + }) + } } From 97fc042c877a1d567c180c61d26e229b383edf99 Mon Sep 17 00:00:00 2001 From: Julian Date: Mon, 4 Nov 2024 11:38:31 +0100 Subject: [PATCH 25/33] async trouble --- Cargo.toml | 2 - crates/pg_base_db/src/path.rs | 1 + crates/pg_lsp/Cargo.toml | 2 +- crates/pg_lsp/src/b_server.rs | 338 ----- crates/pg_lsp/src/client.rs | 70 -- .../pg_lsp/src/client/client_config_opts.rs | 7 + crates/pg_lsp/src/client/mod.rs | 2 + crates/pg_lsp/src/db_connection.rs | 6 +- crates/pg_lsp/src/debouncer.rs | 56 + crates/pg_lsp/src/lib.rs | 5 +- crates/pg_lsp/src/main.rs | 9 +- crates/pg_lsp/src/server.rs | 1101 +++++------------ crates/pg_lsp/src/session.rs | 62 +- crates/pg_lsp/src/utils.rs | 2 +- crates/pg_lsp/src/utils/from_proto.rs | 1 + 15 files changed, 400 insertions(+), 1264 deletions(-) delete mode 100644 crates/pg_lsp/src/b_server.rs delete mode 100644 crates/pg_lsp/src/client.rs create mode 100644 crates/pg_lsp/src/client/client_config_opts.rs create mode 100644 crates/pg_lsp/src/client/mod.rs create mode 100644 crates/pg_lsp/src/debouncer.rs diff --git a/Cargo.toml b/Cargo.toml index 5f37a1a5..c9cbbb0e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,6 @@ pg_hover = { path = "./crates/pg_hover", version = "0.0.0" } pg_inlay_hints = { path = "./crates/pg_inlay_hints", version = "0.0.0" } pg_lint = { path = "./crates/pg_lint", version = "0.0.0" } pg_workspace = { path = "./crates/pg_workspace", version = "0.0.0" } -pg_lsp = { path = "./crates/lsp", version = "0.0.0" } # parser = { path = "./crates/parser", version = "0.0.0" } # sql_parser = { path = "./crates/sql_parser", version = "0.0.0" } @@ -43,4 +42,3 @@ pg_lsp = { path = "./crates/lsp", version = "0.0.0" } [profile.dev.package] insta.opt-level = 3 -similar.opt-level = 3 diff --git a/crates/pg_base_db/src/path.rs b/crates/pg_base_db/src/path.rs index 994aaa80..fdd8f7a4 100644 --- a/crates/pg_base_db/src/path.rs +++ b/crates/pg_base_db/src/path.rs @@ -20,3 +20,4 @@ impl PgLspPath { } } } + diff --git a/crates/pg_lsp/Cargo.toml b/crates/pg_lsp/Cargo.toml index 0f0cf703..d6283d89 100644 --- a/crates/pg_lsp/Cargo.toml +++ b/crates/pg_lsp/Cargo.toml @@ -32,7 +32,7 @@ pg_base_db.workspace = true pg_schema_cache.workspace = true pg_workspace.workspace = true pg_diagnostics.workspace = true -tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread", "sync"] } +tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread", "sync", "time"] } tokio-util = "0.7.12" tower-lsp = "0.20.0" diff --git a/crates/pg_lsp/src/b_server.rs b/crates/pg_lsp/src/b_server.rs deleted file mode 100644 index 4a7ab08e..00000000 --- a/crates/pg_lsp/src/b_server.rs +++ /dev/null @@ -1,338 +0,0 @@ -use notification::ShowMessage; -use pg_commands::CommandType; -use tokio::sync::RwLock; -use tower_lsp::jsonrpc; -use tower_lsp::lsp_types::*; -use tower_lsp::{Client, LanguageServer}; - -use crate::client::client_flags::ClientFlags; -use crate::server::options::ClientConfigurationOptions; -use crate::session::Session; -use crate::utils::file_path; -use crate::utils::normalize_uri; -use crate::utils::to_proto; - -struct Server { - client: Client, - session: Session, - client_capabilities: RwLock>, -} - -impl Server { - pub async fn new(client: Client) -> Self { - Self { - client, - session: Session::new(), - client_capabilities: RwLock::new(None), - } - } - - /// When the client sends a didChangeConfiguration notification, we need to parse the received JSON. - fn parse_options_from_client( - &self, - mut value: serde_json::Value, - ) -> Option { - let options = match value.get_mut("pglsp") { - Some(section) => section.take(), - None => value, - }; - - match serde_json::from_value::(options) { - Ok(new_options) => Some(new_options), - Err(why) => { - let message = format!( - "The texlab configuration is invalid; using the default settings instead.\nDetails: {why}" - ); - let typ = MessageType::WARNING; - self.client - .send_notification::(ShowMessageParams { message, typ }); - None - } - } - } - - async fn request_opts_from_client(&self) -> Option { - let params = ConfigurationParams { - items: vec![ConfigurationItem { - section: Some("pglsp".to_string()), - scope_uri: None, - }], - }; - - match self - .client - .send_request::(params) - .await - { - Ok(json) => { - // The client reponse fits the requested `ConfigurationParams.items`, - // so the first value is what we're looking for. - let relevant = json - .into_iter() - .next() - .expect("workspace/configuration request did not yield expected response."); - - let opts = self.parse_options_from_client(relevant); - - opts - } - Err(why) => { - let message = format!( - "Unable to pull client options via workspace/configuration request: {}", - why - ); - println!("{}", message); - self.client.log_message(MessageType::ERROR, message); - None - } - } - } - - async fn publish_diagnostics(&self, mut uri: Url) { - normalize_uri(&mut uri); - - let url = file_path(&uri); - let diagnostics = self.session.get_diagnostics(url).await; - - let diagnostics: Vec = diagnostics - .into_iter() - .map(|(d, r)| to_proto::diagnostic(d, r)) - .collect(); - - self.client - .send_notification::(ShowMessageParams { - typ: MessageType::INFO, - message: format!("diagnostics {}", diagnostics.len()), - }); - - let params = PublishDiagnosticsParams { - uri, - diagnostics, - version: None, - }; - - self.client - .send_notification::(params); - } -} - -#[tower_lsp::async_trait] -impl LanguageServer for Server { - async fn initialize(&self, params: InitializeParams) -> jsonrpc::Result { - let flags = ClientFlags::from_initialize_request_params(¶ms); - self.client_capabilities.blocking_write().replace(flags); - - Ok(InitializeResult { - server_info: None, - capabilities: ServerCapabilities { - text_document_sync: Some(TextDocumentSyncCapability::Options( - TextDocumentSyncOptions { - open_close: Some(true), - change: Some(TextDocumentSyncKind::INCREMENTAL), - will_save: None, - will_save_wait_until: None, - save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions { - include_text: Some(false), - })), - }, - )), - hover_provider: Some(HoverProviderCapability::Simple(true)), - execute_command_provider: Some(ExecuteCommandOptions { - commands: CommandType::ALL - .iter() - .map(|c| c.id().to_string()) - .collect(), - ..Default::default() - }), - inlay_hint_provider: Some(OneOf::Left(true)), - code_action_provider: Some(CodeActionProviderCapability::Simple(true)), - completion_provider: Some(CompletionOptions::default()), - ..ServerCapabilities::default() - }, - }) - } - - async fn initialized(&self, _params: InitializedParams) { - self.client - .log_message(MessageType::INFO, "Postgres LSP Connected!") - .await; - } - - async fn shutdown(&self) -> jsonrpc::Result<()> { - self.client - .log_message(MessageType::INFO, "Postgres LSP terminated.") - .await; - Ok(()) - } - - async fn did_change_configuration(&self, params: DidChangeConfigurationParams) { - let capabilities = self.client_capabilities.read().await; - - if capabilities.as_ref().unwrap().supports_pull_opts { - let opts = self.request_opts_from_client().await; - if opts - .as_ref() - .is_some_and(|o| o.db_connection_string.is_some()) - { - let conn_str = opts.unwrap().db_connection_string.unwrap(); - self.session.change_db(conn_str).await; - return; - } - } - - // if we couldn't pull settings from the client, - // we'll try parsing the passed in params. - let opts = self.parse_options_from_client(params.settings); - - if opts - .as_ref() - .is_some_and(|o| o.db_connection_string.is_some()) - { - let conn_str = opts.unwrap().db_connection_string.unwrap(); - self.session.change_db(conn_str).await; - } - } - - async fn did_open(&self, params: DidOpenTextDocumentParams) { - let mut uri = params.text_document.uri; - normalize_uri(&mut uri); - - let changed_urls = self - .session - .apply_doc_changes( - file_path(url), - params.text_document.version, - params.text_document.text, - ) - .await; - - for url in changed_urls { - self.publish_diagnostics(url).await; - } - } - - async fn did_save(&self, params: DidSaveTextDocumentParams) { - let mut uri = params.text_document.uri; - normalize_uri(&mut uri); - - self.publish_diagnostics(uri).await; - - // TODO: "Compute Now" - let changed_urls = self.session.recompute_and_get_changed_files(); - for url in changed_urls { - self.publish_diagnostics(url).await; - } - } - - async fn did_change(&self, params: DidChangeTextDocumentParams) { - todo!() - } - - async fn did_close(&self, params: DidSaveTextDocumentParams) { - let mut uri = params.text_document.uri; - normalize_uri(&mut uri); - let path = file_path(&uri); - - self.session.on_file_closed(path); - } - - async fn code_action(&self, params: CodeActionParams) -> Result> { - let mut uri = params.text_document.uri; - normalize_uri(&mut uri); - - let path = file_path(&uri); - let range = params.range; - - let actions = self - .session - .get_available_code_actions_or_commands(path, range) - .await; - - Ok(actions) - } - - async fn inlay_hint(&self, params: InlayHintParams) -> Result>> { - let mut uri = params.text_document.uri; - normalize_uri(&mut uri); - - let path = file_path(&uri); - let range = params.range; - - let hints = self.session.get_inlay_hints(path, range).await; - - Ok(hints) - } - - async fn completion(&self, params: CompletionParams) -> Result> { - let mut uri = params.text_document_position.text_document.uri; - normalize_uri(&mut uri); - - let path = file_path(&uri); - let position = params.text_document_position.position; - - let completions = self.session.get_available_completions(path, position).await; - - Ok(completions.map(|c| CompletionResponse::List(c))) - } - - async fn hover(&self, params: HoverParams) -> Result> { - let mut uri = params.text_document_position_params.text_document.uri; - normalize_uri(&mut uri); - - let path = file_path(&uri); - let position = params.text_document_position_params.position; - - let hover_diagnostics = self - .session - .get_available_hover_diagnostics(path, position) - .await; - - Ok(hover_diagnostics) - } - - async fn execute_command( - &self, - params: ExecuteCommandParams, - ) -> jsonrpc::Result> { - match CommandType::from_id(params.command.replace("pglsp.", "").as_str()) { - Some(CommandType::ExecuteStatement) => { - if params.arguments.is_empty() { - return jsonrpc::Result::Err(jsonrpc::Error::invalid_request()); - } - - let params = params.arguments.into_iter().next().unwrap(); - let stmt = serde_json::from_value(params) - .map_err(|_| jsonrpc::Error::invalid_request())?; - - match self.session.run_stmt(stmt).await { - Ok(rows_affected) => { - self.client - .send_notification::(ShowMessageParams { - typ: MessageType::INFO, - message: format!("Success! Affected rows: {}", rows_affected), - }) - .await; - } - Err(why) => { - self.client - .send_notification::(ShowMessageParams { - typ: MessageType::ERROR, - message: format!("Error! Statement exectuion failed: {}", why), - }) - .await; - } - }; - } - None => { - self.client - .show_message( - MessageType::ERROR, - format!("Unknown command: {}", params.command), - ) - .await; - } - }; - - Ok(None) - } -} diff --git a/crates/pg_lsp/src/client.rs b/crates/pg_lsp/src/client.rs deleted file mode 100644 index bf626ab8..00000000 --- a/crates/pg_lsp/src/client.rs +++ /dev/null @@ -1,70 +0,0 @@ -pub mod client_flags; - -use anyhow::Result; -use serde::Serialize; -use tower_lsp::lsp_types::{notification::ShowMessage, MessageType, ShowMessageParams}; -use tower_lsp::Client; - -use crate::server::options::ClientConfigurationOptions; - -#[derive(Debug, Clone)] -pub struct LspClient { - client: Client, -} - -impl LspClient { - pub fn new(client: Client) -> Self { - Self { client } - } - - pub fn send_notification(&self, params: N::Params) -> Result<()> - where - N: tower_lsp::lsp_types::notification::Notification, - N::Params: Serialize, - { - self.client.send_notification::(params); - Ok(()) - } - - /// This will ignore any errors that occur while sending the notification. - pub fn send_info_notification(&self, message: &str) { - let _ = self.send_notification::(ShowMessageParams { - message: message.into(), - typ: MessageType::INFO, - }); - } - - pub async fn send_request(&self, params: R::Params) -> Result - where - R: tower_lsp::lsp_types::request::Request, - { - let response = self.client.send_request::(params).await?; - - Ok(response) - } - - pub fn parse_options( - &self, - mut value: serde_json::Value, - ) -> Result { - // if there are multiple servers, we need to extract the options for pglsp first - let options = match value.get_mut("pglsp") { - Some(section) => section.take(), - None => value, - }; - - let options = match serde_json::from_value(options) { - Ok(new_options) => new_options, - Err(why) => { - let message = format!( - "The texlab configuration is invalid; using the default settings instead.\nDetails: {why}" - ); - let typ = MessageType::WARNING; - self.send_notification::(ShowMessageParams { message, typ })?; - None - } - }; - - Ok(options.unwrap_or_default()) - } -} diff --git a/crates/pg_lsp/src/client/client_config_opts.rs b/crates/pg_lsp/src/client/client_config_opts.rs new file mode 100644 index 00000000..f931a146 --- /dev/null +++ b/crates/pg_lsp/src/client/client_config_opts.rs @@ -0,0 +1,7 @@ +use serde::Deserialize; + +// TODO: Check that the Opts are correct (existed in server.rs) +#[derive(Deserialize)] +pub struct ClientConfigurationOptions { + pub db_connection_string: Option +} \ No newline at end of file diff --git a/crates/pg_lsp/src/client/mod.rs b/crates/pg_lsp/src/client/mod.rs new file mode 100644 index 00000000..655b3d8a --- /dev/null +++ b/crates/pg_lsp/src/client/mod.rs @@ -0,0 +1,2 @@ +pub mod client_flags; +pub mod client_config_opts; \ No newline at end of file diff --git a/crates/pg_lsp/src/db_connection.rs b/crates/pg_lsp/src/db_connection.rs index 1389768b..0f652785 100644 --- a/crates/pg_lsp/src/db_connection.rs +++ b/crates/pg_lsp/src/db_connection.rs @@ -1,9 +1,5 @@ -use pg_commands::ExecuteStatementCommand; use pg_schema_cache::SchemaCache; -use sqlx::{ - postgres::{PgListener, PgQueryResult}, - PgPool, -}; +use sqlx::{postgres::PgListener, PgPool}; use tokio::task::JoinHandle; #[derive(Debug)] diff --git a/crates/pg_lsp/src/debouncer.rs b/crates/pg_lsp/src/debouncer.rs new file mode 100644 index 00000000..ddb80658 --- /dev/null +++ b/crates/pg_lsp/src/debouncer.rs @@ -0,0 +1,56 @@ +pub(crate) struct SimpleTokioDebouncer { + handle: tokio::task::JoinHandle<()>, + tx: tokio::sync::mpsc::Sender, +} + +impl SimpleTokioDebouncer { + pub fn new(timeout: std::time::Duration, mut callback: F) -> Self + where + F: FnMut(Args) + Send + 'static, + Args: Send + 'static, + { + let (tx, mut rx) = tokio::sync::mpsc::channel(100); + + let handle = tokio::spawn(async move { + let mut maybe_args: Option = None; + let mut instant = tokio::time::Instant::now() + timeout; + + loop { + tokio::select! { + // If the timeout is reached, execute and reset the last received action + _ = tokio::time::sleep_until(instant) => { + match maybe_args { + Some(args) => { + callback(args); + maybe_args = None; + } + None => continue, + } + } + + // If a new action is received, update the action and reset the timeout + cb = rx.recv() => { + match cb { + Some(cb) => { + maybe_args = Some(cb); + instant = tokio::time::Instant::now() + timeout; + } + None => break, // channel closed + } + } + } + } + }); + + Self { handle, tx } + } + + pub async fn debounce(&self, args: Args) + { + self.tx.send(args).await.unwrap(); + } + + pub async fn shutdown(self) { + let _ = self.handle.await.unwrap(); + } +} diff --git a/crates/pg_lsp/src/lib.rs b/crates/pg_lsp/src/lib.rs index 76c6c8f1..bd7a7b9a 100644 --- a/crates/pg_lsp/src/lib.rs +++ b/crates/pg_lsp/src/lib.rs @@ -1,7 +1,6 @@ mod client; mod db_connection; -pub mod server; +mod debouncer; mod utils; - -mod b_server; mod session; +pub mod server; diff --git a/crates/pg_lsp/src/main.rs b/crates/pg_lsp/src/main.rs index 9c678fac..fbcb42c3 100644 --- a/crates/pg_lsp/src/main.rs +++ b/crates/pg_lsp/src/main.rs @@ -1,13 +1,12 @@ -use lsp_server::Connection; use pg_lsp::server::Server; +use tower_lsp::LspService; #[tokio::main] async fn main() -> anyhow::Result<()> { - let (connection, threads) = Connection::stdio(); + let (server, client_socket) = LspService::build(|client| { + Server::new(client) + }).finish(); - let server = Server::init(connection)?; - server.run().await?; - threads.join()?; Ok(()) } diff --git a/crates/pg_lsp/src/server.rs b/crates/pg_lsp/src/server.rs index 16d4628e..1e8a3b4a 100644 --- a/crates/pg_lsp/src/server.rs +++ b/crates/pg_lsp/src/server.rs @@ -1,327 +1,143 @@ -mod debouncer; -mod dispatch; -pub mod options; - -use lsp_server::{Connection, ErrorCode, Message, RequestId}; -use lsp_types::{ - notification::{ - DidChangeConfiguration, DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument, - DidSaveTextDocument, Notification as _, PublishDiagnostics, ShowMessage, - }, - request::{ - CodeActionRequest, Completion, ExecuteCommand, HoverRequest, InlayHintRequest, - RegisterCapability, WorkspaceConfiguration, - }, - CompletionList, CompletionOptions, ConfigurationItem, ConfigurationParams, - DidChangeConfigurationParams, DidChangeTextDocumentParams, DidCloseTextDocumentParams, - DidOpenTextDocumentParams, DidSaveTextDocumentParams, ExecuteCommandOptions, - ExecuteCommandParams, HoverProviderCapability, InitializeParams, InitializeResult, - PublishDiagnosticsParams, Registration, RegistrationParams, SaveOptions, ServerCapabilities, - ServerInfo, ShowMessageParams, TextDocumentSyncCapability, TextDocumentSyncKind, - TextDocumentSyncOptions, TextDocumentSyncSaveOptions, -}; -use pg_base_db::{Change, DocumentChange}; -use pg_commands::{Command, CommandType, ExecuteStatementCommand}; -use pg_completions::CompletionParams; -use pg_hover::HoverParams; -use pg_schema_cache::SchemaCache; -use pg_workspace::Workspace; -use serde::{de::DeserializeOwned, Serialize}; -use std::{collections::HashSet, future::Future, sync::Arc, time::Duration}; -use text_size::TextSize; -use tower_lsp::Client; - -use tokio::sync::mpsc; -use tokio_util::sync::CancellationToken; - -use crate::{ - client::{client_flags::ClientFlags, LspClient}, - db_connection::DbConnection, - utils::{file_path, from_proto, line_index_ext::LineIndexExt, normalize_uri, to_proto}, -}; - -use self::{debouncer::EventDebouncer, options::ClientConfigurationOptions}; -use sqlx::{postgres::PgPool, Executor}; - -#[derive(Debug)] -enum InternalMessage { - PublishDiagnostics(lsp_types::Url), - SetOptions(ClientConfigurationOptions), - SetSchemaCache(SchemaCache), - SetDatabaseConnection(DbConnection), -} - -/// `lsp-servers` `Connection` type uses a crossbeam channel, which is not compatible with tokio's async runtime. -/// For now, we move it into a separate task and use tokio's channels to communicate. -fn get_client_receiver( - connection: Connection, - cancel_token: Arc, -) -> mpsc::UnboundedReceiver { - let (message_tx, message_rx) = mpsc::unbounded_channel(); - - tokio::task::spawn(async move { - loop { - let msg = match connection.receiver.recv() { - Ok(msg) => msg, - Err(e) => { - eprint!("Connection was closed by LSP client: {}", e); - cancel_token.cancel(); - return; - } - }; - - match msg { - Message::Request(r) if connection.handle_shutdown(&r).unwrap() => { - cancel_token.cancel(); - return; - } - - // any non-shutdown request is forwarded to the server - _ => message_tx.send(msg).unwrap(), - }; - } - }); - - message_rx -} +use notification::ShowMessage; +use pg_commands::CommandType; +use tokio::sync::RwLock; +use tower_lsp::jsonrpc; +use tower_lsp::lsp_types::*; +use tower_lsp::{Client, LanguageServer}; + +use crate::client::client_config_opts::ClientConfigurationOptions; +use crate::client::client_flags::ClientFlags; +use crate::debouncer::SimpleTokioDebouncer; +use crate::session::Session; +use crate::utils::file_path; +use crate::utils::normalize_uri; +use crate::utils::to_proto; pub struct Server { - client_rx: mpsc::UnboundedReceiver, - cancel_token: Arc, - client: LspClient, - internal_tx: mpsc::UnboundedSender, - internal_rx: mpsc::UnboundedReceiver, - client_flags: Arc, - ide: Arc, - db_conn: Option, - compute_debouncer: EventDebouncer>, + client: Client, + session: Session, + client_capabilities: RwLock>, + debouncer: SimpleTokioDebouncer, } impl Server { - pub fn init(client: Client) -> anyhow::Result { - let client = LspClient::new(client); - let cancel_token = Arc::new(CancellationToken::new()); - - let (client_flags, client_rx) = - Self::establish_client_connection(connection, &cancel_token)?; - - let ide = Arc::new(Workspace::new()); + pub fn new(client: Client) -> Self { + let session = Session::new(); - let (internal_tx, internal_rx) = mpsc::unbounded_channel(); - - let cloned_tx = internal_tx.clone(); - let cloned_ide = ide.clone(); - let pool = Arc::new(threadpool::Builder::new().build()); + let cloned_session = session.clone(); let cloned_client = client.clone(); - let server = Self { - cancel_token, - client_rx, - internal_rx, - internal_tx, - client, - client_flags: Arc::new(client_flags), - db_conn: None, - ide, - compute_debouncer: EventDebouncer::new( - Duration::from_millis(500), - move |conn: Option| { - let inner_cloned_ide = cloned_ide.clone(); - let inner_cloned_tx = cloned_tx.clone(); - let inner_cloned_client = cloned_client.clone(); - pool.execute(move || { - inner_cloned_client - .send_notification::(ShowMessageParams { - typ: lsp_types::MessageType::INFO, - message: format!("Computing debounced {}", conn.is_some()), - }) - .unwrap(); - let r = - async_std::task::block_on(conn.as_ref().unwrap().execute("SELECT 1")); - inner_cloned_client - .send_notification::(ShowMessageParams { - typ: lsp_types::MessageType::INFO, - message: format!("res {:?}", r.unwrap()), - }) - .unwrap(); - - let changed = inner_cloned_ide.compute(conn); - - let urls = HashSet::<&str>::from_iter( - changed.iter().map(|f| f.document_url.to_str().unwrap()), - ); - for url in urls.iter() { - inner_cloned_tx - .send(InternalMessage::PublishDiagnostics( - lsp_types::Url::from_file_path(url).unwrap(), - )) - .unwrap(); - } - }); - }, - ), - }; + let debouncer = + SimpleTokioDebouncer::new(std::time::Duration::from_millis(500), move |mut uri| { + normalize_uri(&mut uri); + let url = file_path(&uri); - Ok(server) - } + let diagnostics = cloned_session.get_diagnostics_sync(url); - fn compute_now(&self) { - let conn = self.db_conn.as_ref().map(|p| p.pool.clone()); - let cloned_ide = self.ide.clone(); - let cloned_tx = self.internal_tx.clone(); - let client = self.client.clone(); - - self.compute_debouncer.clear(); - - self.spawn_with_cancel(async move { - client - .send_notification::(ShowMessageParams { - typ: lsp_types::MessageType::INFO, - message: format!("Computing now {}", conn.is_some()), - }) - .unwrap(); - - if conn.is_some() { - client - .send_notification::(ShowMessageParams { - typ: lsp_types::MessageType::INFO, - message: format!("pool closed {}", conn.as_ref().unwrap().is_closed()), - }) - .unwrap(); - - let r = async_std::task::block_on(conn.as_ref().unwrap().execute("SELECT 1")); - client - .send_notification::(ShowMessageParams { - typ: lsp_types::MessageType::INFO, - message: format!("res {:?}", r.unwrap()), - }) - .unwrap(); - } + let diagnostics: Vec = diagnostics + .into_iter() + .map(|(d, r)| to_proto::diagnostic(d, r)) + .collect(); - let changed = cloned_ide.compute(conn); + cloned_client.send_notification::(ShowMessageParams { + typ: MessageType::INFO, + message: format!("diagnostics {}", diagnostics.len()), + }); - let urls = HashSet::<&str>::from_iter( - changed.iter().map(|f| f.document_url.to_str().unwrap()), - ); + let params = PublishDiagnosticsParams { + uri, + diagnostics, + version: None, + }; - for url in urls { - cloned_tx - .send(InternalMessage::PublishDiagnostics( - lsp_types::Url::from_file_path(url).unwrap(), - )) - .unwrap(); - } - }); - } + cloned_client.send_notification::(params); + }); - fn update_db_connection(&self, options: ClientConfigurationOptions) -> anyhow::Result<()> { - if options.db_connection_string.is_none() - || self - .db_conn - .as_ref() - // if the connection is already connected to the same database, do nothing - .is_some_and(|c| c.connected_to(options.db_connection_string.as_ref().unwrap())) - { - return Ok(()); + Self { + client, + session: Session::new(), + client_capabilities: RwLock::new(None), + debouncer, } + } - let connection_string = options.db_connection_string.unwrap(); + /// When the client sends a didChangeConfiguration notification, we need to parse the received JSON. + fn parse_options_from_client( + &self, + mut value: serde_json::Value, + ) -> Option { + let options = match value.get_mut("pglsp") { + Some(section) => section.take(), + None => value, + }; - let internal_tx = self.internal_tx.clone(); - let client = self.client.clone(); - self.spawn_with_cancel(async move { - match DbConnection::new(connection_string.into()).await { - Ok(conn) => { - internal_tx - .send(InternalMessage::SetDatabaseConnection(conn)) - .unwrap(); - } - Err(why) => { - client.send_info_notification(&format!( - "Unable to update database connection: {}", - why - )); - } + match serde_json::from_value::(options) { + Ok(new_options) => Some(new_options), + Err(why) => { + let message = format!( + "The texlab configuration is invalid; using the default settings instead.\nDetails: {why}" + ); + let typ = MessageType::WARNING; + self.client + .send_notification::(ShowMessageParams { message, typ }); + None } - }); - - Ok(()) + } } - async fn listen_for_schema_updates(&mut self) -> anyhow::Result<()> { - if self.db_conn.is_none() { - eprintln!("Error trying to listen for schema updates: No database connection"); - return Ok(()); - } + async fn request_opts_from_client(&self) -> Option { + let params = ConfigurationParams { + items: vec![ConfigurationItem { + section: Some("pglsp".to_string()), + scope_uri: None, + }], + }; - let internal_tx = self.internal_tx.clone(); - self.db_conn - .as_mut() - .unwrap() - .listen_for_schema_updates(move |schema_cache| { - internal_tx - .send(InternalMessage::SetSchemaCache(schema_cache)) - .expect("LSP Server: Failed to send internal message."); - }) - .await?; + match self + .client + .send_request::(params) + .await + { + Ok(json) => { + // The client reponse fits the requested `ConfigurationParams.items`, + // so the first value is what we're looking for. + let relevant = json + .into_iter() + .next() + .expect("workspace/configuration request did not yield expected response."); - Ok(()) - } + let opts = self.parse_options_from_client(relevant); - fn capabilities() -> ServerCapabilities { - ServerCapabilities { - text_document_sync: Some(TextDocumentSyncCapability::Options( - TextDocumentSyncOptions { - open_close: Some(true), - change: Some(TextDocumentSyncKind::INCREMENTAL), - will_save: None, - will_save_wait_until: None, - save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions { - include_text: Some(false), - })), - }, - )), - hover_provider: Some(HoverProviderCapability::Simple(true)), - execute_command_provider: Some(ExecuteCommandOptions { - commands: CommandType::ALL - .iter() - .map(|c| c.id().to_string()) - .collect(), - ..Default::default() - }), - inlay_hint_provider: Some(lsp_types::OneOf::Left(true)), - code_action_provider: Some(lsp_types::CodeActionProviderCapability::Simple(true)), - completion_provider: Some(CompletionOptions::default()), - ..ServerCapabilities::default() + opts + } + Err(why) => { + let message = format!( + "Unable to pull client options via workspace/configuration request: {}", + why + ); + println!("{}", message); + self.client.log_message(MessageType::ERROR, message); + None + } } } - // TODO allow option url and publish diagnostics for all files - fn publish_diagnostics(&self, uri: lsp_types::Url) -> anyhow::Result<()> { - let mut url = uri.clone(); - normalize_uri(&mut url); - - let path = file_path(&url); - - let doc = self.ide.documents.get(&path); + async fn publish_diagnostics(&self, mut uri: Url) { + normalize_uri(&mut uri); - if doc.is_none() { - return Ok(()); - } + let url = file_path(&uri); + let diagnostics = self.session.get_diagnostics(url).await; - let diagnostics: Vec = self - .ide - .diagnostics(&path) - .iter() - .map(|d| to_proto::diagnostic(&doc.as_ref().unwrap(), d)) + let diagnostics: Vec = diagnostics + .into_iter() + .map(|(d, r)| to_proto::diagnostic(d, r)) .collect(); self.client .send_notification::(ShowMessageParams { - typ: lsp_types::MessageType::INFO, + typ: MessageType::INFO, message: format!("diagnostics {}", diagnostics.len()), - }) - .unwrap(); + }); let params = PublishDiagnosticsParams { uri, @@ -330,599 +146,242 @@ impl Server { }; self.client - .send_notification::(params)?; + .send_notification::(params); + } - Ok(()) + async fn publish_diagnostics_debounced(&self, uri: Url) { + self.debouncer.debounce(uri); } +} - fn did_open(&self, params: DidOpenTextDocumentParams) -> anyhow::Result<()> { - let mut uri = params.text_document.uri; +#[tower_lsp::async_trait] +impl LanguageServer for Server { + async fn initialize(&self, params: InitializeParams) -> jsonrpc::Result { + let flags = ClientFlags::from_initialize_request_params(¶ms); + self.client_capabilities.blocking_write().replace(flags); + + Ok(InitializeResult { + server_info: None, + capabilities: ServerCapabilities { + text_document_sync: Some(TextDocumentSyncCapability::Options( + TextDocumentSyncOptions { + open_close: Some(true), + change: Some(TextDocumentSyncKind::INCREMENTAL), + will_save: None, + will_save_wait_until: None, + save: Some(TextDocumentSyncSaveOptions::SaveOptions(SaveOptions { + include_text: Some(false), + })), + }, + )), + hover_provider: Some(HoverProviderCapability::Simple(true)), + execute_command_provider: Some(ExecuteCommandOptions { + commands: CommandType::ALL + .iter() + .map(|c| c.id().to_string()) + .collect(), + ..Default::default() + }), + inlay_hint_provider: Some(OneOf::Left(true)), + code_action_provider: Some(CodeActionProviderCapability::Simple(true)), + completion_provider: Some(CompletionOptions::default()), + ..ServerCapabilities::default() + }, + }) + } - normalize_uri(&mut uri); + async fn initialized(&self, _params: InitializedParams) { + self.client + .log_message(MessageType::INFO, "Postgres LSP Connected!") + .await; + } - let path = file_path(&uri); + async fn shutdown(&self) -> jsonrpc::Result<()> { + self.client + .log_message(MessageType::INFO, "Postgres LSP terminated.") + .await; + Ok(()) + } - self.ide.apply_change( - path, - DocumentChange::new( - params.text_document.version, - vec![Change { - range: None, - text: params.text_document.text, - }], - ), - ); + async fn did_change_configuration(&self, params: DidChangeConfigurationParams) { + let capabilities = self.client_capabilities.read().await; - self.compute_now(); + if capabilities.as_ref().unwrap().supports_pull_opts { + let opts = self.request_opts_from_client().await; + if opts + .as_ref() + .is_some_and(|o| o.db_connection_string.is_some()) + { + let conn_str = opts.unwrap().db_connection_string.unwrap(); + self.session.change_db(conn_str).await; + return; + } + } - Ok(()) + // if we couldn't pull settings from the client, + // we'll try parsing the passed in params. + let opts = self.parse_options_from_client(params.settings); + + if opts + .as_ref() + .is_some_and(|o| o.db_connection_string.is_some()) + { + let conn_str = opts.unwrap().db_connection_string.unwrap(); + self.session.change_db(conn_str).await; + } } - fn did_change(&self, params: DidChangeTextDocumentParams) -> anyhow::Result<()> { + async fn did_open(&self, params: DidOpenTextDocumentParams) { let mut uri = params.text_document.uri; normalize_uri(&mut uri); - let path = file_path(&uri); - - let document = self.ide.documents.get(&path); + let changed_urls = self + .session + .apply_doc_changes( + file_path(&uri), + params.text_document.version, + params.text_document.text, + ) + .await; - if document.is_none() { - return Ok(()); + for url in changed_urls { + let url = Url::from_file_path(url.as_path()).expect("Expected absolute File Path"); + self.publish_diagnostics(url).await; } + } - let changes = from_proto::content_changes(&document.unwrap(), params.content_changes); - - self.ide.apply_change( - path, - DocumentChange::new(params.text_document.version, changes), - ); + async fn did_save(&self, params: DidSaveTextDocumentParams) { + let mut uri = params.text_document.uri; + normalize_uri(&mut uri); - let conn = self.db_conn.as_ref().map(|p| p.get_pool()); - self.compute_debouncer.put(conn); + self.publish_diagnostics(uri).await; - Ok(()) + // TODO: "Compute Now" + let changed_urls = self.session.recompute_and_get_changed_files().await; + for url in changed_urls { + let url = Url::from_file_path(url.as_path()).expect("Expected absolute File Path"); + self.publish_diagnostics(url).await; + } } - fn did_save(&self, params: DidSaveTextDocumentParams) -> anyhow::Result<()> { + async fn did_change(&self, params: DidChangeTextDocumentParams) { let mut uri = params.text_document.uri; normalize_uri(&mut uri); - self.publish_diagnostics(uri)?; - - self.compute_now(); - - Ok(()) + self.debouncer.debounce(uri).await } - fn did_close(&self, params: DidCloseTextDocumentParams) -> anyhow::Result<()> { - // this just means that the document is no longer open in the client - // if we would listen to fs events, we would use this to overwrite the files owner to be - // "server" instead of "client". for now we will ignore this notification since we dont - // need to watch files that are not open. - + async fn did_close(&self, params: DidCloseTextDocumentParams) { let mut uri = params.text_document.uri; normalize_uri(&mut uri); let path = file_path(&uri); - self.ide.remove_document(path); - - Ok(()) + self.session.on_file_closed(path).await } - fn code_actions( + async fn code_action( &self, - id: RequestId, - params: lsp_types::CodeActionParams, - ) -> anyhow::Result<()> { - let db_conn = self.db_conn.as_ref().map(|p| p.pool.clone()); - self.run_query(id, move |ide| { - let mut actions = Vec::::new(); - - if db_conn.is_none() { - return actions; - } + params: CodeActionParams, + ) -> jsonrpc::Result> { + let mut uri = params.text_document.uri; + normalize_uri(&mut uri); - let mut uri = params.text_document.uri; - normalize_uri(&mut uri); - let path = file_path(&uri); + let path = file_path(&uri); + let range = params.range; - let doc = ide.documents.get(&path); + let actions = self + .session + .get_available_code_actions_or_commands(path, range) + .await; - if doc.is_none() { - return actions; - } - - let doc = doc.unwrap(); + Ok(actions) + } - let range = doc.line_index.offset_lsp_range(params.range).unwrap(); + async fn inlay_hint(&self, params: InlayHintParams) -> jsonrpc::Result>> { + let mut uri = params.text_document.uri; + normalize_uri(&mut uri); - actions.extend(doc.statements_at_range(&range).iter().map(|stmt| { - let cmd = ExecuteStatementCommand::command_type(); - let title = format!( - "Execute '{}'", - ExecuteStatementCommand::trim_statement(stmt.text.clone(), 50) - ); - lsp_types::CodeAction { - title: title.clone(), - kind: None, - edit: None, - command: Some(lsp_types::Command { - title, - command: format!("pglsp.{}", cmd.id()), - arguments: Some(vec![serde_json::to_value(stmt.text.clone()).unwrap()]), - }), - diagnostics: None, - is_preferred: None, - disabled: None, - data: None, - } - })); + let path = file_path(&uri); + let range = params.range; - actions - }); + let hints = self.session.get_inlay_hints(path, range).await; - Ok(()) + Ok(hints) } - fn inlay_hint( + async fn completion( &self, - id: RequestId, - mut params: lsp_types::InlayHintParams, - ) -> anyhow::Result<()> { - normalize_uri(&mut params.text_document.uri); - - let c = self.client.clone(); - - self.run_query(id, move |ide| { - let path = file_path(¶ms.text_document.uri); - - let doc = ide.documents.get(&path); + params: CompletionParams, + ) -> jsonrpc::Result> { + let mut uri = params.text_document_position.text_document.uri; + normalize_uri(&mut uri); - if doc.is_none() { - return Vec::new(); - } + let path = file_path(&uri); + let position = params.text_document_position.position; - let doc = doc.unwrap(); - - let range = doc.line_index.offset_lsp_range(params.range).unwrap(); - - let schema_cache = ide.schema_cache.read().unwrap(); - - c.send_notification::(ShowMessageParams { - typ: lsp_types::MessageType::INFO, - message: "querying inlay hints".to_string(), - }) - .unwrap(); - - doc.statements_at_range(&range) - .iter() - .flat_map(|stmt| { - ::pg_inlay_hints::inlay_hints(::pg_inlay_hints::InlayHintsParams { - ast: ide.pg_query.ast(&stmt).as_ref().map(|x| x.as_ref()), - enriched_ast: ide - .pg_query - .enriched_ast(&stmt) - .as_ref() - .map(|x| x.as_ref()), - tree: ide.tree_sitter.tree(&stmt).as_ref().map(|x| x.as_ref()), - cst: ide.pg_query.cst(&stmt).as_ref().map(|x| x.as_ref()), - schema_cache: &schema_cache, - }) - }) - .map(|hint| lsp_types::InlayHint { - position: doc.line_index.line_col_lsp(hint.offset).unwrap(), - label: match hint.content { - pg_inlay_hints::InlayHintContent::FunctionArg(arg) => { - lsp_types::InlayHintLabel::String(match arg.name { - Some(name) => format!("{} ({})", name, arg.type_name), - None => arg.type_name.clone(), - }) - } - }, - kind: match hint.content { - pg_inlay_hints::InlayHintContent::FunctionArg(_) => { - Some(lsp_types::InlayHintKind::PARAMETER) - } - }, - text_edits: None, - tooltip: None, - padding_left: None, - padding_right: None, - data: None, - }) - .collect() - }); + let completions = self.session.get_available_completions(path, position).await; - Ok(()) + Ok(completions.map(|c| CompletionResponse::List(c))) } - fn completion( - &self, - id: RequestId, - mut params: lsp_types::CompletionParams, - ) -> anyhow::Result<()> { - normalize_uri(&mut params.text_document_position.text_document.uri); - - self.run_query(id, move |ide| { - let path = file_path(¶ms.text_document_position.text_document.uri); - - let doc = ide.documents.get(&path)?; - - let pos = doc - .line_index - .offset_lsp(params.text_document_position.position) - .unwrap(); - - let (range, stmt) = doc.statement_at_offset_with_range(&pos)?; - - let schema = ide.schema_cache.read().unwrap(); - - Some(CompletionList { - is_incomplete: false, - items: pg_completions::complete(&CompletionParams { - position: pos - range.start() - TextSize::from(1), - text: stmt.text.as_str(), - tree: ide.tree_sitter.tree(&stmt).as_ref().map(|x| x.as_ref()), - schema: &schema, - }) - .items - .iter() - .map(|i| lsp_types::CompletionItem { - // TODO: add more data - label: i.data.label().to_string(), - label_details: None, - kind: Some(lsp_types::CompletionItemKind::CLASS), - detail: None, - documentation: None, - deprecated: None, - preselect: None, - sort_text: None, - filter_text: None, - insert_text: None, - insert_text_format: None, - insert_text_mode: None, - text_edit: None, - additional_text_edits: None, - commit_characters: None, - data: None, - tags: None, - command: None, - }) - .collect(), - }) - }); + async fn hover(&self, params: HoverParams) -> jsonrpc::Result> { + let mut uri = params.text_document_position_params.text_document.uri; + normalize_uri(&mut uri); - Ok(()) - } + let path = file_path(&uri); + let position = params.text_document_position_params.position; - fn hover(&self, id: RequestId, mut params: lsp_types::HoverParams) -> anyhow::Result<()> { - normalize_uri(&mut params.text_document_position_params.text_document.uri); - - self.run_query(id, move |ide| { - let path = file_path(¶ms.text_document_position_params.text_document.uri); - let doc = ide.documents.get(&path)?; - - let pos = doc - .line_index - .offset_lsp(params.text_document_position_params.position) - .unwrap(); - - let (range, stmt) = doc.statement_at_offset_with_range(&pos)?; - - ::pg_hover::hover(HoverParams { - position: pos - range.start(), - source: stmt.text.as_str(), - enriched_ast: ide - .pg_query - .enriched_ast(&stmt) - .as_ref() - .map(|x| x.as_ref()), - tree: ide.tree_sitter.tree(&stmt).as_ref().map(|x| x.as_ref()), - schema_cache: ide.schema_cache.read().unwrap().clone(), - }) - .map(|hover| lsp_types::Hover { - contents: lsp_types::HoverContents::Scalar(lsp_types::MarkedString::String( - hover.content, - )), - range: Some(doc.line_index.line_col_lsp_range(range).unwrap()), - }) - }); + let hover_diagnostics = self + .session + .get_available_hover_diagnostics(path, position) + .await; - Ok(()) + Ok(hover_diagnostics) } - fn execute_command(&self, id: RequestId, params: ExecuteCommandParams) -> anyhow::Result<()> { + async fn execute_command( + &self, + params: ExecuteCommandParams, + ) -> jsonrpc::Result> { match CommandType::from_id(params.command.replace("pglsp.", "").as_str()) { Some(CommandType::ExecuteStatement) => { - let stmt = self.parse_command_params::(params.arguments)?; - - let command = ExecuteStatementCommand::new(stmt); - - let conn = self.db_conn.as_ref().map(|p| p.pool.clone()); - - let client = self.client.clone(); - - self.run_fallible(id, move || { - // todo return the rows and do something with them - // maybe store them and add the table to the hover output? - let res = async_std::task::block_on(command.run(conn))?; - - // todo if its a ddl statement, recompute schema cache - - client - .send_notification::(ShowMessageParams { - typ: lsp_types::MessageType::INFO, - message: format!("Success! Affected rows: {}", res.rows_affected()), - }) - .unwrap(); - - Ok(()) - }); - } - None => { - self.client - .send_error( - id, - ErrorCode::InvalidParams, - format!("Unknown command: {}", params.command), - ) - .unwrap(); - } - }; - - Ok(()) - } - - fn run_fallible(&self, id: RequestId, query: Q) - where - R: Serialize, - Q: FnOnce() -> anyhow::Result + Send + 'static, - { - let client = self.client.clone(); - self.spawn_with_cancel(async move { - match query() { - Ok(result) => { - let response = lsp_server::Response::new_ok(id, result); - client.send_response(response).unwrap(); + if params.arguments.is_empty() { + return jsonrpc::Result::Err(jsonrpc::Error::invalid_request()); } - Err(why) => { - client - .send_error(id, ErrorCode::InternalError, why.to_string()) - .unwrap(); - } - } - }); - } - fn parse_command_params( - &self, - params: Vec, - ) -> anyhow::Result { - if params.is_empty() { - anyhow::bail!("No argument provided!"); - } - - let value = params.into_iter().next().unwrap(); - let value = serde_json::from_value(value)?; - Ok(value) - } + let params = params.arguments.into_iter().next().unwrap(); + let stmt = serde_json::from_value(params) + .map_err(|_| jsonrpc::Error::invalid_request())?; - fn run_query(&self, id: RequestId, query: Q) - where - R: Serialize, - Q: FnOnce(&Workspace) -> R + Send + 'static, - { - let client = self.client.clone(); - let ide = Arc::clone(&self.ide); - - self.spawn_with_cancel(async move { - let response = lsp_server::Response::new_ok(id, query(&ide)); - client - .send_response(response) - .expect("Failed to send query to client"); - }); - } - - fn did_change_configuration( - &mut self, - params: DidChangeConfigurationParams, - ) -> anyhow::Result<()> { - if self.client_flags.supports_pull_opts { - self.pull_options(); - } else { - let options = self.client.parse_options(params.settings)?; - self.update_db_connection(options)?; - } - - Ok(()) - } - - async fn process_messages(&mut self) -> anyhow::Result<()> { - loop { - tokio::select! { - _ = self.cancel_token.cancelled() => { - // Close the loop, proceed to shutdown. - return Ok(()) - }, - - msg = self.internal_rx.recv() => { - match msg { - None => panic!("The LSP's internal sender closed. This should never happen."), - Some(m) => self.handle_internal_message(m).await + match self.session.run_stmt(stmt).await { + Ok(rows_affected) => { + self.client + .send_notification::(ShowMessageParams { + typ: MessageType::INFO, + message: format!("Success! Affected rows: {}", rows_affected), + }) + .await; } - }, - - msg = self.client_rx.recv() => { - match msg { - None => panic!("The LSP's client closed, but not via an 'exit' method. This should never happen."), - Some(m) => self.handle_message(m).await + Err(why) => { + self.client + .send_notification::(ShowMessageParams { + typ: MessageType::ERROR, + message: format!("Error! Statement exectuion failed: {}", why), + }) + .await; } - }, - }?; - } - } - - async fn handle_message(&mut self, msg: Message) -> anyhow::Result<()> { - match msg { - Message::Request(request) => { - if let Some(response) = dispatch::RequestDispatcher::new(request) - .on::(|id, params| self.inlay_hint(id, params))? - .on::(|id, params| self.hover(id, params))? - .on::(|id, params| self.execute_command(id, params))? - .on::(|id, params| self.completion(id, params))? - .on::(|id, params| self.code_actions(id, params))? - .default() - { - self.client.send_response(response)?; - } + }; } - Message::Notification(notification) => { - dispatch::NotificationDispatcher::new(notification) - .on::(|params| { - self.did_change_configuration(params) - })? - .on::(|params| self.did_close(params))? - .on::(|params| self.did_open(params))? - .on::(|params| self.did_change(params))? - .on::(|params| self.did_save(params))? - .on::(|params| self.did_close(params))? - .default(); - } - Message::Response(response) => { - self.client.recv_response(response)?; - } - } - - Ok(()) - } - - async fn handle_internal_message(&mut self, msg: InternalMessage) -> anyhow::Result<()> { - match msg { - InternalMessage::SetSchemaCache(c) => { + None => { self.client - .send_info_notification("Refreshing Schema Cache..."); - self.ide.set_schema_cache(c); - self.client.send_info_notification("Updated Schema Cache."); - self.compute_now(); - } - InternalMessage::PublishDiagnostics(uri) => { - self.publish_diagnostics(uri)?; - } - InternalMessage::SetOptions(options) => { - self.update_db_connection(options)?; - } - InternalMessage::SetDatabaseConnection(conn) => { - let current = self.db_conn.replace(conn); - if current.is_some() { - current.unwrap().close().await - } - self.listen_for_schema_updates().await?; - } - } - - Ok(()) - } - - fn pull_options(&mut self) { - let params = ConfigurationParams { - items: vec![ConfigurationItem { - section: Some("postgres_lsp".to_string()), - scope_uri: None, - }], - }; - - let client = self.client.clone(); - let internal_tx = self.internal_tx.clone(); - self.spawn_with_cancel(async move { - match client.send_request::(params) { - Ok(mut json) => { - let options = client - .parse_options(json.pop().expect("invalid configuration request")) - .unwrap(); - - if let Err(why) = internal_tx.send(InternalMessage::SetOptions(options)) { - println!("Failed to set internal options: {}", why); - } - } - Err(why) => { - println!("Retrieving configuration failed: {}", why); - } - }; - }); - } - - fn register_configuration(&mut self) { - let registration = Registration { - id: "pull-config".to_string(), - method: DidChangeConfiguration::METHOD.to_string(), - register_options: None, - }; - - let params = RegistrationParams { - registrations: vec![registration], - }; - - let client = self.client.clone(); - self.spawn_with_cancel(async move { - if let Err(why) = client.send_request::(params) { - println!( - "Failed to register \"{}\" notification: {}", - DidChangeConfiguration::METHOD, - why - ); + .show_message( + MessageType::ERROR, + format!("Unknown command: {}", params.command), + ) + .await; } - }); - } - - fn establish_client_connection( - connection: Connection, - cancel_token: &Arc, - ) -> anyhow::Result<(ClientFlags, mpsc::UnboundedReceiver)> { - let (id, params) = connection.initialize_start()?; - - let params: InitializeParams = serde_json::from_value(params)?; - - let result = InitializeResult { - capabilities: Self::capabilities(), - server_info: Some(ServerInfo { - name: "Postgres LSP".to_owned(), - version: Some(env!("CARGO_PKG_VERSION").to_owned()), - }), }; - connection.initialize_finish(id, serde_json::to_value(result)?)?; - - let client_rx = get_client_receiver(connection, cancel_token.clone()); - - let client_flags = ClientFlags::from_initialize_request_params(¶ms); - - Ok((client_flags, client_rx)) - } - - /// Spawns an asynchronous task that can be cancelled with the `Server`'s `cancel_token`. - fn spawn_with_cancel(&self, f: F) -> tokio::task::JoinHandle> - where - F: Future + Send + 'static, - O: Send + 'static, - { - let cancel_token = self.cancel_token.clone(); - tokio::spawn(async move { - tokio::select! { - _ = cancel_token.cancelled() => None, - output = f => Some(output) - } - }) - } - - pub async fn run(mut self) -> anyhow::Result<()> { - if self.client_flags.supports_dynamic_registration { - self.register_configuration(); - } - - if self.client_flags.supports_pull_opts { - self.pull_options(); - } - - self.process_messages().await + Ok(None) } } diff --git a/crates/pg_lsp/src/session.rs b/crates/pg_lsp/src/session.rs index ccc9c981..889305c3 100644 --- a/crates/pg_lsp/src/session.rs +++ b/crates/pg_lsp/src/session.rs @@ -1,20 +1,22 @@ -use std::{collections::HashSet, sync::Arc}; +use std::{collections::HashSet,sync::Arc}; use pg_base_db::{Change, DocumentChange, PgLspPath}; -use pg_commands::ExecuteStatementCommand; +use pg_commands::{Command, ExecuteStatementCommand}; +use pg_completions::CompletionParams; use pg_diagnostics::Diagnostic; use pg_hover::HoverParams; use pg_workspace::Workspace; +use text_size::TextSize; use tokio::sync::RwLock; use tower_lsp::lsp_types::{ - CodeAction, CodeActionOrCommand, CompletionItem, CompletionItemKind, CompletionList, Hover, - HoverContents, InlayHint, MarkedString, Position, Range, + CodeActionOrCommand, CompletionItem, CompletionItemKind, CompletionList, Hover, HoverContents, InlayHint, InlayHintKind, InlayHintLabel, MarkedString, Position, Range }; use crate::{db_connection::DbConnection, utils::line_index_ext::LineIndexExt}; +#[derive(Clone)] pub struct Session { - db: RwLock>, + db: Arc>>, ide: Arc>, } @@ -22,7 +24,7 @@ impl Session { pub fn new() -> Self { let ide = Arc::new(RwLock::new(Workspace::new())); Self { - db: RwLock::new(None), + db: Arc::new(RwLock::new(None)), ide, } } @@ -64,11 +66,14 @@ impl Session { /// Runs the passed-in statement against the underlying database. pub async fn run_stmt(&self, stmt: String) -> anyhow::Result { let db = self.db.read().await; - let pool = db.map(|d| d.get_pool()); + let pool = db.as_ref().map(|d| d.get_pool()); let cmd = ExecuteStatementCommand::new(stmt); - cmd.run(pool).await + match cmd.run(pool).await { + Err(e) => Err(e), + Ok(res) => Ok(res.rows_affected()), + } } pub async fn on_file_closed(&self, path: PgLspPath) { @@ -76,6 +81,30 @@ impl Session { ide.remove_document(path); } + pub fn get_diagnostics_sync(&self, path: PgLspPath) -> Vec<(Diagnostic, Range)> { + let ide = self.ide.blocking_read(); + + // make sure there are documents at the provided path before + // trying to collect diagnostics. + let doc = ide.documents.get(&path); + if doc.is_none() { + return vec![]; + } + + ide.diagnostics(&path) + .into_iter() + .map(|d| { + let range = doc + .as_ref() + .unwrap() + .line_index + .line_col_lsp_range(d.range) + .unwrap(); + (d, range) + }) + .collect() + } + pub async fn get_diagnostics(&self, path: PgLspPath) -> Vec<(Diagnostic, Range)> { let ide = self.ide.read().await; @@ -105,7 +134,7 @@ impl Session { path: PgLspPath, version: i32, text: String, - ) -> HashSet { + ) -> HashSet { { let ide = self.ide.read().await; @@ -120,10 +149,10 @@ impl Session { ); } - self.recompute_and_get_changed_files() + self.recompute_and_get_changed_files().await } - pub async fn recompute_and_get_changed_files(&self) -> HashSet { + pub async fn recompute_and_get_changed_files(&self) -> HashSet { let ide = self.ide.read().await; let db = self.db.read().await; @@ -133,7 +162,7 @@ impl Session { changed_files .into_iter() - .map(|f| f.document_url.to_string_lossy().to_string()) + .map(|p| p.document_url) .collect() } @@ -145,9 +174,6 @@ impl Session { let ide = self.ide.read().await; let doc = ide.documents.get(&path)?; - let db = self.db.read().await?; - - let doc = doc.unwrap(); let range = doc.line_index.offset_lsp_range(range).unwrap(); // for now, we only provide `ExcecuteStatementCommand`s. @@ -160,7 +186,7 @@ impl Session { "Execute '{}'", ExecuteStatementCommand::trim_statement(stmt.text.clone(), 50) ); - CodeActionOrCommand::Command(Command { + CodeActionOrCommand::Command(tower_lsp::lsp_types::Command { title, command: format!("pglsp.{}", cmd.id()), arguments: Some(vec![serde_json::to_value(stmt.text.clone()).unwrap()]), @@ -235,10 +261,10 @@ impl Session { let schema_cache = ide.schema_cache.read().expect("No Schema Cache"); let completion_items = pg_completions::complete(&CompletionParams { - position: pos - range.start() - TextSize::from(1), + position: offset - range.start() - TextSize::from(1), text: stmt.text.as_str(), tree: ide.tree_sitter.tree(&stmt).as_ref().map(|x| x.as_ref()), - schema: &schema, + schema: &schema_cache, }) .items .into_iter() diff --git a/crates/pg_lsp/src/utils.rs b/crates/pg_lsp/src/utils.rs index b176bda2..9aa80a2f 100644 --- a/crates/pg_lsp/src/utils.rs +++ b/crates/pg_lsp/src/utils.rs @@ -4,8 +4,8 @@ pub mod to_proto; use std::path::PathBuf; -use lsp_types; use pg_base_db::PgLspPath; +use tower_lsp::lsp_types; /// Convert a `lsp_types::Url` to a `PgLspPath`. pub(crate) fn file_path(url: &lsp_types::Url) -> PgLspPath { diff --git a/crates/pg_lsp/src/utils/from_proto.rs b/crates/pg_lsp/src/utils/from_proto.rs index eaae06ce..d4bc3c8d 100644 --- a/crates/pg_lsp/src/utils/from_proto.rs +++ b/crates/pg_lsp/src/utils/from_proto.rs @@ -1,5 +1,6 @@ use super::line_index_ext::LineIndexExt; use pg_base_db::{Change, Document}; +use tower_lsp::lsp_types; pub fn content_changes( document: &Document, From cb72d8472eb78b51e4d7e6878dfe6ec66dad10d2 Mon Sep 17 00:00:00 2001 From: Julian Date: Mon, 4 Nov 2024 11:57:54 +0100 Subject: [PATCH 26/33] handle results --- crates/pg_base_db/src/path.rs | 1 - .../pg_lsp/src/client/client_config_opts.rs | 4 +- crates/pg_lsp/src/client/mod.rs | 2 +- crates/pg_lsp/src/debouncer.rs | 27 ++-- crates/pg_lsp/src/lib.rs | 4 +- crates/pg_lsp/src/main.rs | 5 +- crates/pg_lsp/src/server.rs | 118 +++++++++++------- crates/pg_lsp/src/session.rs | 17 +-- 8 files changed, 100 insertions(+), 78 deletions(-) diff --git a/crates/pg_base_db/src/path.rs b/crates/pg_base_db/src/path.rs index fdd8f7a4..994aaa80 100644 --- a/crates/pg_base_db/src/path.rs +++ b/crates/pg_base_db/src/path.rs @@ -20,4 +20,3 @@ impl PgLspPath { } } } - diff --git a/crates/pg_lsp/src/client/client_config_opts.rs b/crates/pg_lsp/src/client/client_config_opts.rs index f931a146..3d29e986 100644 --- a/crates/pg_lsp/src/client/client_config_opts.rs +++ b/crates/pg_lsp/src/client/client_config_opts.rs @@ -3,5 +3,5 @@ use serde::Deserialize; // TODO: Check that the Opts are correct (existed in server.rs) #[derive(Deserialize)] pub struct ClientConfigurationOptions { - pub db_connection_string: Option -} \ No newline at end of file + pub db_connection_string: Option, +} diff --git a/crates/pg_lsp/src/client/mod.rs b/crates/pg_lsp/src/client/mod.rs index 655b3d8a..63472a43 100644 --- a/crates/pg_lsp/src/client/mod.rs +++ b/crates/pg_lsp/src/client/mod.rs @@ -1,2 +1,2 @@ +pub mod client_config_opts; pub mod client_flags; -pub mod client_config_opts; \ No newline at end of file diff --git a/crates/pg_lsp/src/debouncer.rs b/crates/pg_lsp/src/debouncer.rs index ddb80658..7b460abb 100644 --- a/crates/pg_lsp/src/debouncer.rs +++ b/crates/pg_lsp/src/debouncer.rs @@ -1,18 +1,18 @@ -pub(crate) struct SimpleTokioDebouncer { +use std::{future::Future, pin::Pin}; + +type AsyncBlock = Pin + 'static + Send>>; + +pub(crate) struct SimpleTokioDebouncer { handle: tokio::task::JoinHandle<()>, - tx: tokio::sync::mpsc::Sender, + tx: tokio::sync::mpsc::Sender, } -impl SimpleTokioDebouncer { - pub fn new(timeout: std::time::Duration, mut callback: F) -> Self - where - F: FnMut(Args) + Send + 'static, - Args: Send + 'static, - { +impl SimpleTokioDebouncer { + pub fn new(timeout: std::time::Duration) -> Self { let (tx, mut rx) = tokio::sync::mpsc::channel(100); let handle = tokio::spawn(async move { - let mut maybe_args: Option = None; + let mut maybe_args: Option = None; let mut instant = tokio::time::Instant::now() + timeout; loop { @@ -20,8 +20,8 @@ impl SimpleTokioDebouncer { // If the timeout is reached, execute and reset the last received action _ = tokio::time::sleep_until(instant) => { match maybe_args { - Some(args) => { - callback(args); + Some(block) => { + block.await; maybe_args = None; } None => continue, @@ -45,9 +45,8 @@ impl SimpleTokioDebouncer { Self { handle, tx } } - pub async fn debounce(&self, args: Args) - { - self.tx.send(args).await.unwrap(); + pub async fn debounce(&self, block: AsyncBlock) { + self.tx.send(block).await.unwrap(); } pub async fn shutdown(self) { diff --git a/crates/pg_lsp/src/lib.rs b/crates/pg_lsp/src/lib.rs index bd7a7b9a..579d584a 100644 --- a/crates/pg_lsp/src/lib.rs +++ b/crates/pg_lsp/src/lib.rs @@ -1,6 +1,6 @@ mod client; mod db_connection; mod debouncer; -mod utils; -mod session; pub mod server; +mod session; +mod utils; diff --git a/crates/pg_lsp/src/main.rs b/crates/pg_lsp/src/main.rs index fbcb42c3..00c70c2b 100644 --- a/crates/pg_lsp/src/main.rs +++ b/crates/pg_lsp/src/main.rs @@ -3,10 +3,7 @@ use tower_lsp::LspService; #[tokio::main] async fn main() -> anyhow::Result<()> { - let (server, client_socket) = LspService::build(|client| { - Server::new(client) - }).finish(); - + let (server, client_socket) = LspService::build(|client| Server::new(client)).finish(); Ok(()) } diff --git a/crates/pg_lsp/src/server.rs b/crates/pg_lsp/src/server.rs index 1e8a3b4a..4927b46a 100644 --- a/crates/pg_lsp/src/server.rs +++ b/crates/pg_lsp/src/server.rs @@ -17,52 +17,21 @@ pub struct Server { client: Client, session: Session, client_capabilities: RwLock>, - debouncer: SimpleTokioDebouncer, + debouncer: SimpleTokioDebouncer, } impl Server { pub fn new(client: Client) -> Self { - let session = Session::new(); - - let cloned_session = session.clone(); - let cloned_client = client.clone(); - - let debouncer = - SimpleTokioDebouncer::new(std::time::Duration::from_millis(500), move |mut uri| { - normalize_uri(&mut uri); - let url = file_path(&uri); - - let diagnostics = cloned_session.get_diagnostics_sync(url); - - let diagnostics: Vec = diagnostics - .into_iter() - .map(|(d, r)| to_proto::diagnostic(d, r)) - .collect(); - - cloned_client.send_notification::(ShowMessageParams { - typ: MessageType::INFO, - message: format!("diagnostics {}", diagnostics.len()), - }); - - let params = PublishDiagnosticsParams { - uri, - diagnostics, - version: None, - }; - - cloned_client.send_notification::(params); - }); - Self { client, session: Session::new(), client_capabilities: RwLock::new(None), - debouncer, + debouncer: SimpleTokioDebouncer::new(std::time::Duration::from_millis(500)), } } /// When the client sends a didChangeConfiguration notification, we need to parse the received JSON. - fn parse_options_from_client( + async fn parse_options_from_client( &self, mut value: serde_json::Value, ) -> Option { @@ -79,7 +48,8 @@ impl Server { ); let typ = MessageType::WARNING; self.client - .send_notification::(ShowMessageParams { message, typ }); + .send_notification::(ShowMessageParams { message, typ }) + .await; None } } @@ -106,9 +76,7 @@ impl Server { .next() .expect("workspace/configuration request did not yield expected response."); - let opts = self.parse_options_from_client(relevant); - - opts + self.parse_options_from_client(relevant).await } Err(why) => { let message = format!( @@ -116,7 +84,7 @@ impl Server { why ); println!("{}", message); - self.client.log_message(MessageType::ERROR, message); + self.client.log_message(MessageType::ERROR, message).await; None } } @@ -137,7 +105,8 @@ impl Server { .send_notification::(ShowMessageParams { typ: MessageType::INFO, message: format!("diagnostics {}", diagnostics.len()), - }); + }) + .await; let params = PublishDiagnosticsParams { uri, @@ -146,11 +115,44 @@ impl Server { }; self.client - .send_notification::(params); + .send_notification::(params) + .await; } - async fn publish_diagnostics_debounced(&self, uri: Url) { - self.debouncer.debounce(uri); + async fn publish_diagnostics_debounced(&self, mut uri: Url) { + let session = self.session.clone(); + let client = self.client.clone(); + + self.debouncer + .debounce(Box::pin(async move { + normalize_uri(&mut uri); + let url = file_path(&uri); + + let diagnostics = session.get_diagnostics_sync(url); + + let diagnostics: Vec = diagnostics + .into_iter() + .map(|(d, r)| to_proto::diagnostic(d, r)) + .collect(); + + client + .send_notification::(ShowMessageParams { + typ: MessageType::INFO, + message: format!("diagnostics {}", diagnostics.len()), + }) + .await; + + let params = PublishDiagnosticsParams { + uri, + diagnostics, + version: None, + }; + + client + .send_notification::(params) + .await; + })) + .await; } } @@ -197,6 +199,8 @@ impl LanguageServer for Server { } async fn shutdown(&self) -> jsonrpc::Result<()> { + // TODO: Shutdown stuff. + self.client .log_message(MessageType::INFO, "Postgres LSP terminated.") .await; @@ -213,21 +217,41 @@ impl LanguageServer for Server { .is_some_and(|o| o.db_connection_string.is_some()) { let conn_str = opts.unwrap().db_connection_string.unwrap(); - self.session.change_db(conn_str).await; + match self.session.change_db(conn_str).await { + Ok(_) => {} + Err(err) => { + self.client + .show_message( + MessageType::ERROR, + format!("Pulled Client Options but failed to set them: {}", err), + ) + .await + } + } return; } } // if we couldn't pull settings from the client, // we'll try parsing the passed in params. - let opts = self.parse_options_from_client(params.settings); + let opts = self.parse_options_from_client(params.settings).await; if opts .as_ref() .is_some_and(|o| o.db_connection_string.is_some()) { let conn_str = opts.unwrap().db_connection_string.unwrap(); - self.session.change_db(conn_str).await; + match self.session.change_db(conn_str).await { + Ok(_) => {} + Err(err) => { + self.client + .show_message( + MessageType::ERROR, + format!("Used Client Options from params but failed to set them: {}", err), + ) + .await + } + } } } @@ -268,7 +292,7 @@ impl LanguageServer for Server { let mut uri = params.text_document.uri; normalize_uri(&mut uri); - self.debouncer.debounce(uri).await + self.publish_diagnostics_debounced(uri).await; } async fn did_close(&self, params: DidCloseTextDocumentParams) { diff --git a/crates/pg_lsp/src/session.rs b/crates/pg_lsp/src/session.rs index 889305c3..19ed93a6 100644 --- a/crates/pg_lsp/src/session.rs +++ b/crates/pg_lsp/src/session.rs @@ -1,4 +1,4 @@ -use std::{collections::HashSet,sync::Arc}; +use std::{collections::HashSet, sync::Arc}; use pg_base_db::{Change, DocumentChange, PgLspPath}; use pg_commands::{Command, ExecuteStatementCommand}; @@ -9,7 +9,8 @@ use pg_workspace::Workspace; use text_size::TextSize; use tokio::sync::RwLock; use tower_lsp::lsp_types::{ - CodeActionOrCommand, CompletionItem, CompletionItemKind, CompletionList, Hover, HoverContents, InlayHint, InlayHintKind, InlayHintLabel, MarkedString, Position, Range + CodeActionOrCommand, CompletionItem, CompletionItemKind, CompletionList, Hover, HoverContents, + InlayHint, InlayHintKind, InlayHintLabel, MarkedString, Position, Range, }; use crate::{db_connection::DbConnection, utils::line_index_ext::LineIndexExt}; @@ -29,6 +30,10 @@ impl Session { } } + async fn shutdown(&mut self) { + // TODO + } + /// `update_db_connection` will update `Self`'s database connection. /// If the passed-in connection string is the same that we're already connected to, it's a noop. /// Otherwise, it'll first open a new connection, replace `Self`'s connection, and then close @@ -50,7 +55,8 @@ impl Session { let ide = self.ide.clone(); db.listen_for_schema_updates(move |schema| { let _guard = ide.blocking_write().set_schema_cache(schema); - }); + }) + .await?; let mut current_db = self.db.blocking_write(); let old_db = current_db.replace(db); @@ -160,10 +166,7 @@ impl Session { let changed_files = ide.compute(pool); - changed_files - .into_iter() - .map(|p| p.document_url) - .collect() + changed_files.into_iter().map(|p| p.document_url).collect() } pub async fn get_available_code_actions_or_commands( From 0a57982ee33ac210849982d6fadc7bda2db1d57a Mon Sep 17 00:00:00 2001 From: Julian Date: Sun, 10 Nov 2024 14:52:38 +0100 Subject: [PATCH 27/33] intermediary --- crates/pg_lsp/Cargo.toml | 2 +- crates/pg_lsp/src/client/client_flags.rs | 16 +--- crates/pg_lsp/src/db_connection.rs | 86 +++++++++--------- crates/pg_lsp/src/debouncer.rs | 36 +++++++- crates/pg_lsp/src/main.rs | 13 +-- crates/pg_lsp/src/server.rs | 29 +++--- crates/pg_lsp/src/server/debouncer/buffer.rs | 92 -------------------- crates/pg_lsp/src/server/debouncer/mod.rs | 7 -- crates/pg_lsp/src/server/debouncer/thread.rs | 79 ----------------- crates/pg_lsp/src/server/dispatch.rs | 86 ------------------ crates/pg_lsp/src/server/options.rs | 8 -- crates/pg_lsp/src/session.rs | 24 ++--- crates/pg_lsp/src/utils.rs | 1 - crates/pg_lsp/src/utils/from_proto.rs | 18 ---- 14 files changed, 120 insertions(+), 377 deletions(-) delete mode 100644 crates/pg_lsp/src/server/debouncer/buffer.rs delete mode 100644 crates/pg_lsp/src/server/debouncer/mod.rs delete mode 100644 crates/pg_lsp/src/server/debouncer/thread.rs delete mode 100644 crates/pg_lsp/src/server/dispatch.rs delete mode 100644 crates/pg_lsp/src/server/options.rs delete mode 100644 crates/pg_lsp/src/utils/from_proto.rs diff --git a/crates/pg_lsp/Cargo.toml b/crates/pg_lsp/Cargo.toml index d6283d89..771f4e94 100644 --- a/crates/pg_lsp/Cargo.toml +++ b/crates/pg_lsp/Cargo.toml @@ -32,7 +32,7 @@ pg_base_db.workspace = true pg_schema_cache.workspace = true pg_workspace.workspace = true pg_diagnostics.workspace = true -tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread", "sync", "time"] } +tokio = { version = "1.40.0", features = ["io-std", "macros", "rt-multi-thread", "sync", "time"] } tokio-util = "0.7.12" tower-lsp = "0.20.0" diff --git a/crates/pg_lsp/src/client/client_flags.rs b/crates/pg_lsp/src/client/client_flags.rs index 689a3cb0..f8ef2445 100644 --- a/crates/pg_lsp/src/client/client_flags.rs +++ b/crates/pg_lsp/src/client/client_flags.rs @@ -6,9 +6,6 @@ use tower_lsp::lsp_types::InitializeParams; pub struct ClientFlags { /// If `true`, the server can pull configuration from the client. pub supports_pull_opts: bool, - - /// If `true`, the client notifies the server when its configuration changes. - pub supports_dynamic_registration: bool, } impl ClientFlags { @@ -20,17 +17,6 @@ impl ClientFlags { .and_then(|w| w.configuration) .unwrap_or(false); - let supports_dynamic_registration = params - .capabilities - .workspace - .as_ref() - .and_then(|w| w.did_change_configuration) - .and_then(|c| c.dynamic_registration) - .unwrap_or(false); - - Self { - supports_pull_opts, - supports_dynamic_registration, - } + Self { supports_pull_opts } } } diff --git a/crates/pg_lsp/src/db_connection.rs b/crates/pg_lsp/src/db_connection.rs index 0f652785..ab94e026 100644 --- a/crates/pg_lsp/src/db_connection.rs +++ b/crates/pg_lsp/src/db_connection.rs @@ -2,66 +2,74 @@ use pg_schema_cache::SchemaCache; use sqlx::{postgres::PgListener, PgPool}; use tokio::task::JoinHandle; -#[derive(Debug)] pub(crate) struct DbConnection { pool: PgPool, connection_string: String, - schema_update_handle: Option>, + schema_update_handle: JoinHandle<()>, + close_tx: tokio::sync::oneshot::Sender<()>, } impl DbConnection { - pub(crate) async fn new(connection_string: String) -> Result { - let pool = PgPool::connect(&connection_string).await?; - Ok(Self { - pool, - connection_string: connection_string, - schema_update_handle: None, - }) - } - - pub(crate) fn connected_to(&self, connection_string: &str) -> bool { - connection_string == self.connection_string - } - - pub(crate) async fn close(self) { - if self.schema_update_handle.is_some() { - self.schema_update_handle.unwrap().abort(); - } - self.pool.close().await; - } - - pub(crate) async fn listen_for_schema_updates( - &mut self, + pub(crate) async fn new( + connection_string: String, on_schema_update: F, - ) -> anyhow::Result<()> + ) -> Result where F: Fn(SchemaCache) -> () + Send + 'static, { - let mut listener = PgListener::connect_with(&self.pool).await?; + let pool = PgPool::connect(&connection_string).await?; + + let mut listener = PgListener::connect_with(&pool).await?; listener.listen_all(["postgres_lsp", "pgrst"]).await?; - let pool = self.pool.clone(); + let (close_tx, close_rx) = tokio::sync::oneshot::channel::<()>(); + + let cloned_pool = pool.clone(); + + let schema_update_handle: JoinHandle<()> = tokio::spawn(async move { + let mut moved_rx = close_rx; - let handle: JoinHandle<()> = tokio::spawn(async move { loop { - match listener.recv().await { - Ok(not) => { - if not.payload().to_string() == "reload schema" { - let schema_cache = SchemaCache::load(&pool).await; - on_schema_update(schema_cache); - }; + tokio::select! { + res = listener.recv() => { + match res { + Ok(not) => { + if not.payload().to_string() == "reload schema" { + let schema_cache = SchemaCache::load(&cloned_pool).await; + on_schema_update(schema_cache); + }; + } + Err(why) => { + eprintln!("Error receiving notification: {:?}", why); + break; + } + } } - Err(why) => { - eprintln!("Error receiving notification: {:?}", why); - break; + + _ = &mut moved_rx => { + return; } } } }); - self.schema_update_handle = Some(handle); + Ok(Self { + pool, + connection_string: connection_string, + schema_update_handle, + close_tx, + }) + } + + pub(crate) fn connected_to(&self, connection_string: &str) -> bool { + connection_string == self.connection_string + } + + pub(crate) async fn close(self) { + let _ = self.close_tx.send(()); + let _ = self.schema_update_handle.await; - Ok(()) + self.pool.close().await; } pub(crate) fn get_pool(&self) -> PgPool { diff --git a/crates/pg_lsp/src/debouncer.rs b/crates/pg_lsp/src/debouncer.rs index 7b460abb..23d0f254 100644 --- a/crates/pg_lsp/src/debouncer.rs +++ b/crates/pg_lsp/src/debouncer.rs @@ -1,21 +1,33 @@ -use std::{future::Future, pin::Pin}; +use std::{ + future::Future, + pin::Pin, + sync::{atomic::AtomicBool, Arc}, +}; type AsyncBlock = Pin + 'static + Send>>; pub(crate) struct SimpleTokioDebouncer { handle: tokio::task::JoinHandle<()>, tx: tokio::sync::mpsc::Sender, + shutdown_flag: Arc, } impl SimpleTokioDebouncer { pub fn new(timeout: std::time::Duration) -> Self { let (tx, mut rx) = tokio::sync::mpsc::channel(100); + let shutdown_flag = Arc::new(AtomicBool::new(false)); + let shutdown_flag_clone = shutdown_flag.clone(); + let handle = tokio::spawn(async move { let mut maybe_args: Option = None; let mut instant = tokio::time::Instant::now() + timeout; loop { + if shutdown_flag_clone.load(std::sync::atomic::Ordering::Relaxed) { + break; + } + tokio::select! { // If the timeout is reached, execute and reset the last received action _ = tokio::time::sleep_until(instant) => { @@ -42,14 +54,30 @@ impl SimpleTokioDebouncer { } }); - Self { handle, tx } + Self { + handle, + tx, + shutdown_flag, + } } pub async fn debounce(&self, block: AsyncBlock) { + if self + .shutdown_flag + .load(std::sync::atomic::Ordering::Relaxed) + { + println!( + "Trying to debounce tasks, but the Debouncer is in the process of shutting down." + ); + return; + } + self.tx.send(block).await.unwrap(); } - pub async fn shutdown(self) { - let _ = self.handle.await.unwrap(); + pub async fn shutdown(&self) { + self.shutdown_flag + .store(true, std::sync::atomic::Ordering::Relaxed); + let _ = self.handle.abort(); // we don't care about any errors during shutdown } } diff --git a/crates/pg_lsp/src/main.rs b/crates/pg_lsp/src/main.rs index 00c70c2b..68eccf90 100644 --- a/crates/pg_lsp/src/main.rs +++ b/crates/pg_lsp/src/main.rs @@ -1,9 +1,12 @@ -use pg_lsp::server::Server; -use tower_lsp::LspService; +use pg_lsp::server::LspServer; +use tower_lsp::{LspService, Server}; #[tokio::main] -async fn main() -> anyhow::Result<()> { - let (server, client_socket) = LspService::build(|client| Server::new(client)).finish(); +async fn main() { + let stdin = tokio::io::stdin(); + let stdout = tokio::io::stdout(); - Ok(()) + let (service, socket) = LspService::new(|client| LspServer::new(client)); + + Server::new(stdin, stdout, socket).serve(service).await; } diff --git a/crates/pg_lsp/src/server.rs b/crates/pg_lsp/src/server.rs index 4927b46a..a50307b9 100644 --- a/crates/pg_lsp/src/server.rs +++ b/crates/pg_lsp/src/server.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use notification::ShowMessage; use pg_commands::CommandType; use tokio::sync::RwLock; @@ -13,18 +15,18 @@ use crate::utils::file_path; use crate::utils::normalize_uri; use crate::utils::to_proto; -pub struct Server { - client: Client, - session: Session, +pub struct LspServer { + client: Arc, + session: Arc, client_capabilities: RwLock>, debouncer: SimpleTokioDebouncer, } -impl Server { +impl LspServer { pub fn new(client: Client) -> Self { Self { - client, - session: Session::new(), + client: Arc::new(client), + session: Arc::new(Session::new()), client_capabilities: RwLock::new(None), debouncer: SimpleTokioDebouncer::new(std::time::Duration::from_millis(500)), } @@ -120,8 +122,8 @@ impl Server { } async fn publish_diagnostics_debounced(&self, mut uri: Url) { - let session = self.session.clone(); - let client = self.client.clone(); + let client = Arc::clone(&self.client); + let session = Arc::clone(&self.session); self.debouncer .debounce(Box::pin(async move { @@ -157,7 +159,7 @@ impl Server { } #[tower_lsp::async_trait] -impl LanguageServer for Server { +impl LanguageServer for LspServer { async fn initialize(&self, params: InitializeParams) -> jsonrpc::Result { let flags = ClientFlags::from_initialize_request_params(¶ms); self.client_capabilities.blocking_write().replace(flags); @@ -199,11 +201,13 @@ impl LanguageServer for Server { } async fn shutdown(&self) -> jsonrpc::Result<()> { - // TODO: Shutdown stuff. + self.session.shutdown().await; + self.debouncer.shutdown().await; self.client .log_message(MessageType::INFO, "Postgres LSP terminated.") .await; + Ok(()) } @@ -247,7 +251,10 @@ impl LanguageServer for Server { self.client .show_message( MessageType::ERROR, - format!("Used Client Options from params but failed to set them: {}", err), + format!( + "Used Client Options from params but failed to set them: {}", + err + ), ) .await } diff --git a/crates/pg_lsp/src/server/debouncer/buffer.rs b/crates/pg_lsp/src/server/debouncer/buffer.rs deleted file mode 100644 index 6fa0574e..00000000 --- a/crates/pg_lsp/src/server/debouncer/buffer.rs +++ /dev/null @@ -1,92 +0,0 @@ -use std::collections::VecDeque; -use std::time::{Duration, Instant}; - -#[derive(Debug)] -struct Event { - item: T, - release_at: Instant, -} - -/// Current state of the debouncing buffer returned from [Get::get()]: -/// -/// - `Ready(T)` when the event is ready to be delivered after the timeout -/// (moves data out of the buffer) -/// - `Wait(Duration)` indicates how much time is left until `Ready` -/// - `Empty` means the buffer is empty -#[derive(Debug, PartialEq, Eq)] -pub enum State { - Ready(T), - Wait(Duration), - Empty, -} - -/// Common interface for getting events out of debouncing buffers. -pub trait Get: Sized { - type Data; - - /// Attemtps to get the next element out of a buffer. If an element is - /// [State::Ready] it's removed from the buffer. - fn get(&mut self) -> State; -} - -/// Debouncing buffer with a common delay for all events. Accepts events via -/// [EventBuffer::put()] which tracks the time of events and de-duplicates them -/// against the current buffer content. Subsequent call to [EventBuffer::get -/// ()] which returns the [State] of the buffer. -pub struct EventBuffer { - delay: Duration, - events: VecDeque>, -} - -impl EventBuffer { - pub fn new(delay: Duration) -> EventBuffer { - EventBuffer { - delay, - events: VecDeque::new(), - } - } - - pub fn put(&mut self, item: T) { - let time = Instant::now(); - self.events.retain(|e| e.release_at <= time); - self.events.push_back(Event { - item, - release_at: time + self.delay, - }); - } - - pub fn clear(&mut self) { - self.events.clear(); - } -} - -impl Get for EventBuffer { - type Data = T; - - fn get(&mut self) -> State { - let time = Instant::now(); - match self.events.get(0) { - None => State::Empty, - Some(e) if e.release_at > time => State::Wait(e.release_at - time), - Some(_) => State::Ready(self.events.pop_front().unwrap().item), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::thread::sleep; - use std::time::Duration; - - #[test] - fn wait() { - let mut debouncer = EventBuffer::new(Duration::from_millis(20)); - debouncer.put(1); - assert!(matches!(debouncer.get(), State::Wait(_))); - sleep(Duration::from_millis(10)); - assert!(matches!(debouncer.get(), State::Wait(_))); - sleep(Duration::from_millis(10)); - assert!(matches!(debouncer.get(), State::Ready(_))); - } -} diff --git a/crates/pg_lsp/src/server/debouncer/mod.rs b/crates/pg_lsp/src/server/debouncer/mod.rs deleted file mode 100644 index c0c42899..00000000 --- a/crates/pg_lsp/src/server/debouncer/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! Data structures and helpers for *debouncing* a stream of events: removing -//! duplicate events occurring closely in time. - -pub mod buffer; -pub mod thread; - -pub use thread::EventDebouncer; diff --git a/crates/pg_lsp/src/server/debouncer/thread.rs b/crates/pg_lsp/src/server/debouncer/thread.rs deleted file mode 100644 index a7486f21..00000000 --- a/crates/pg_lsp/src/server/debouncer/thread.rs +++ /dev/null @@ -1,79 +0,0 @@ -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; -use std::thread::{self, JoinHandle}; -use std::time::Duration; - -use super::buffer::{EventBuffer, Get, State}; - -struct DebouncerThread { - mutex: Arc>, - thread: JoinHandle<()>, -} - -impl DebouncerThread { - fn new(buffer: B, mut f: F) -> Self - where - B: Get + Send + 'static, - F: FnMut(B::Data) + Send + 'static, - { - let mutex = Arc::new(Mutex::new(buffer)); - let stopped = Arc::new(AtomicBool::new(false)); - let thread = thread::spawn({ - let mutex = mutex.clone(); - let stopped = stopped.clone(); - move || { - while !stopped.load(Ordering::Relaxed) { - let state = mutex.lock().unwrap().get(); - match state { - State::Empty => thread::park(), - State::Wait(duration) => thread::sleep(duration), - State::Ready(data) => f(data), - } - } - } - }); - Self { mutex, thread } - } -} - -/// Threaded debouncer wrapping [EventBuffer]. Accepts a common delay and a -/// callback function which is going to be called by a background thread with -/// debounced events. -pub struct EventDebouncer(DebouncerThread>); - -impl EventDebouncer { - pub fn new(delay: Duration, f: F) -> Self - where - F: FnMut(T) + Send + 'static, - T: Send + 'static, - { - Self(DebouncerThread::new(EventBuffer::new(delay), f)) - } - - pub fn put(&self, data: T) { - self.0.mutex.lock().unwrap().put(data); - self.0.thread.thread().unpark(); - } - - pub fn clear(&self) { - self.0.mutex.lock().unwrap().clear(); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::sync::mpsc::channel; - - #[test] - fn event_debouncer() { - let (tx, rx) = channel(); - let debouncer = EventDebouncer::new(Duration::from_millis(10), move |s| { - tx.send(s).unwrap(); - }); - debouncer.put(String::from("Test1")); - debouncer.put(String::from("Test2")); - thread::sleep(Duration::from_millis(20)); - assert!(rx.try_iter().eq([String::from("Test2")])); - } -} diff --git a/crates/pg_lsp/src/server/dispatch.rs b/crates/pg_lsp/src/server/dispatch.rs deleted file mode 100644 index d8c3dc7f..00000000 --- a/crates/pg_lsp/src/server/dispatch.rs +++ /dev/null @@ -1,86 +0,0 @@ -use anyhow::Result; -use lsp_server::{ErrorCode, Notification, Request, RequestId, Response}; -use serde::de::DeserializeOwned; - -pub struct NotificationDispatcher { - not: Option, -} - -impl NotificationDispatcher { - pub fn new(not: Notification) -> Self { - Self { not: Some(not) } - } - - pub fn on(mut self, handler: F) -> Result - where - N: lsp_types::notification::Notification, - N::Params: DeserializeOwned, - F: FnOnce(N::Params) -> Result<()>, - { - if let Some(not) = self.not { - match not.extract::(N::METHOD) { - Ok(params) => { - handler(params)?; - self.not = None; - } - Err(lsp_server::ExtractError::MethodMismatch(not)) => { - self.not = Some(not); - } - Err(lsp_server::ExtractError::JsonError { .. }) => { - self.not = None; - } - }; - } - Ok(self) - } - - pub fn default(self) { - if let Some(_not) = &self.not { - // log::warn!("Unknown notification: {}", not.method); - } - } -} - -pub struct RequestDispatcher { - req: Option, -} - -impl RequestDispatcher { - pub fn new(req: Request) -> Self { - Self { req: Some(req) } - } - - pub fn on(mut self, handler: F) -> Result - where - R: lsp_types::request::Request, - R::Params: DeserializeOwned, - F: FnOnce(RequestId, R::Params) -> Result<()>, - { - if let Some(req) = self.req { - match req.extract::(R::METHOD) { - Ok((id, params)) => { - handler(id, params)?; - self.req = None; - } - Err(lsp_server::ExtractError::MethodMismatch(req)) => { - self.req = Some(req); - } - Err(lsp_server::ExtractError::JsonError { .. }) => { - self.req = None; - } - } - } - Ok(self) - } - - pub fn default(self) -> Option { - self.req.map(|req| { - // log::warn!("Unknown request: {}", req.method); - Response::new_err( - req.id, - ErrorCode::MethodNotFound as i32, - "method not found".to_string(), - ) - }) - } -} diff --git a/crates/pg_lsp/src/server/options.rs b/crates/pg_lsp/src/server/options.rs deleted file mode 100644 index d3b7f8f8..00000000 --- a/crates/pg_lsp/src/server/options.rs +++ /dev/null @@ -1,8 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(default)] -pub struct ClientConfigurationOptions { - pub db_connection_string: Option, -} diff --git a/crates/pg_lsp/src/session.rs b/crates/pg_lsp/src/session.rs index 19ed93a6..359d0786 100644 --- a/crates/pg_lsp/src/session.rs +++ b/crates/pg_lsp/src/session.rs @@ -15,23 +15,26 @@ use tower_lsp::lsp_types::{ use crate::{db_connection::DbConnection, utils::line_index_ext::LineIndexExt}; -#[derive(Clone)] pub struct Session { - db: Arc>>, + db: RwLock>, ide: Arc>, } impl Session { pub fn new() -> Self { - let ide = Arc::new(RwLock::new(Workspace::new())); Self { - db: Arc::new(RwLock::new(None)), - ide, + db: RwLock::new(None), + ide: Arc::new(RwLock::new(Workspace::new())), } } - async fn shutdown(&mut self) { - // TODO + pub async fn shutdown(&self) { + let mut db = self.db.blocking_write(); + let db = db.take(); + + if db.is_some() { + db.unwrap().close().await; + } } /// `update_db_connection` will update `Self`'s database connection. @@ -50,16 +53,15 @@ impl Session { return Ok(()); } - let mut db = DbConnection::new(connection_string).await?; - let ide = self.ide.clone(); - db.listen_for_schema_updates(move |schema| { + let new_db = DbConnection::new(connection_string, move |schema| { let _guard = ide.blocking_write().set_schema_cache(schema); }) .await?; let mut current_db = self.db.blocking_write(); - let old_db = current_db.replace(db); + let old_db = current_db.replace(new_db); + drop(current_db); if old_db.is_some() { let old_db = old_db.unwrap(); diff --git a/crates/pg_lsp/src/utils.rs b/crates/pg_lsp/src/utils.rs index 9aa80a2f..39ce6549 100644 --- a/crates/pg_lsp/src/utils.rs +++ b/crates/pg_lsp/src/utils.rs @@ -1,4 +1,3 @@ -pub mod from_proto; pub mod line_index_ext; pub mod to_proto; diff --git a/crates/pg_lsp/src/utils/from_proto.rs b/crates/pg_lsp/src/utils/from_proto.rs deleted file mode 100644 index d4bc3c8d..00000000 --- a/crates/pg_lsp/src/utils/from_proto.rs +++ /dev/null @@ -1,18 +0,0 @@ -use super::line_index_ext::LineIndexExt; -use pg_base_db::{Change, Document}; -use tower_lsp::lsp_types; - -pub fn content_changes( - document: &Document, - changes: Vec, -) -> Vec { - changes - .iter() - .map(|change| Change { - range: change - .range - .map(|range| document.line_index.offset_lsp_range(range).unwrap()), - text: change.text.clone(), - }) - .collect() -} From b4c3e1fc0cd2e77b966e818bfd16c70074729c77 Mon Sep 17 00:00:00 2001 From: Julian Date: Sat, 23 Nov 2024 17:58:12 +0100 Subject: [PATCH 28/33] we got logging --- .gitignore | 2 + Cargo.lock | 69 ++++++++++++++++++ crates/pg_lsp/Cargo.toml | 2 + crates/pg_lsp/src/db_connection.rs | 16 ++--- crates/pg_lsp/src/debouncer.rs | 4 +- crates/pg_lsp/src/main.rs | 18 ++++- crates/pg_lsp/src/server.rs | 109 +++++++++++++++++++++++++---- crates/pg_lsp/src/session.rs | 34 +-------- editors/code/src/main.ts | 27 ++++--- editors/code/tsconfig.json | 2 +- pglsp.log | 3 + test.sql | 4 +- 12 files changed, 219 insertions(+), 71 deletions(-) create mode 100644 pglsp.log diff --git a/.gitignore b/.gitignore index 2da1549c..0ff7be51 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ target/ # File system .DS_Store desktop.ini + +*.log \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index dbcdede9..69df7660 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1285,6 +1285,16 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -1363,6 +1373,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking" version = "2.2.0" @@ -1557,6 +1573,8 @@ dependencies = [ "tokio", "tokio-util", "tower-lsp", + "tracing", + "tracing-subscriber", ] [[package]] @@ -2173,6 +2191,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shlex" version = "1.3.0" @@ -2545,6 +2572,16 @@ dependencies = [ "syn 2.0.71", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "threadpool" version = "1.8.1" @@ -2713,6 +2750,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", ] [[package]] @@ -2805,6 +2868,12 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "value-bag" version = "1.7.0" diff --git a/crates/pg_lsp/Cargo.toml b/crates/pg_lsp/Cargo.toml index 771f4e94..15f1538c 100644 --- a/crates/pg_lsp/Cargo.toml +++ b/crates/pg_lsp/Cargo.toml @@ -35,6 +35,8 @@ pg_diagnostics.workspace = true tokio = { version = "1.40.0", features = ["io-std", "macros", "rt-multi-thread", "sync", "time"] } tokio-util = "0.7.12" tower-lsp = "0.20.0" +tracing = "0.1.40" +tracing-subscriber = "0.3.18" [dev-dependencies] diff --git a/crates/pg_lsp/src/db_connection.rs b/crates/pg_lsp/src/db_connection.rs index ab94e026..37c9e8e1 100644 --- a/crates/pg_lsp/src/db_connection.rs +++ b/crates/pg_lsp/src/db_connection.rs @@ -1,6 +1,9 @@ +use std::sync::Arc; + use pg_schema_cache::SchemaCache; +use pg_workspace::Workspace; use sqlx::{postgres::PgListener, PgPool}; -use tokio::task::JoinHandle; +use tokio::{sync::RwLock, task::JoinHandle}; pub(crate) struct DbConnection { pool: PgPool, @@ -10,13 +13,10 @@ pub(crate) struct DbConnection { } impl DbConnection { - pub(crate) async fn new( + pub(crate) async fn new( connection_string: String, - on_schema_update: F, - ) -> Result - where - F: Fn(SchemaCache) -> () + Send + 'static, - { + ide: Arc>, + ) -> Result { let pool = PgPool::connect(&connection_string).await?; let mut listener = PgListener::connect_with(&pool).await?; @@ -36,7 +36,7 @@ impl DbConnection { Ok(not) => { if not.payload().to_string() == "reload schema" { let schema_cache = SchemaCache::load(&cloned_pool).await; - on_schema_update(schema_cache); + ide.write().await.set_schema_cache(schema_cache); }; } Err(why) => { diff --git a/crates/pg_lsp/src/debouncer.rs b/crates/pg_lsp/src/debouncer.rs index 23d0f254..380246da 100644 --- a/crates/pg_lsp/src/debouncer.rs +++ b/crates/pg_lsp/src/debouncer.rs @@ -61,12 +61,13 @@ impl SimpleTokioDebouncer { } } + #[tracing::instrument(name = "Adding task to debouncer", skip(self, block))] pub async fn debounce(&self, block: AsyncBlock) { if self .shutdown_flag .load(std::sync::atomic::Ordering::Relaxed) { - println!( + tracing::error!( "Trying to debounce tasks, but the Debouncer is in the process of shutting down." ); return; @@ -75,6 +76,7 @@ impl SimpleTokioDebouncer { self.tx.send(block).await.unwrap(); } + #[tracing::instrument(name = "Shutting down debouncer", skip(self))] pub async fn shutdown(&self) { self.shutdown_flag .store(true, std::sync::atomic::Ordering::Relaxed); diff --git a/crates/pg_lsp/src/main.rs b/crates/pg_lsp/src/main.rs index 68eccf90..e71a9897 100644 --- a/crates/pg_lsp/src/main.rs +++ b/crates/pg_lsp/src/main.rs @@ -1,12 +1,28 @@ +use std::{fs::File, path::PathBuf, str::FromStr, sync::Mutex}; + use pg_lsp::server::LspServer; use tower_lsp::{LspService, Server}; #[tokio::main] -async fn main() { +async fn main() -> anyhow::Result<()> { + let path = PathBuf::from_str("pglsp.log").expect("Opened the log file."); + let file = File::create(path).expect("Could not open the file."); + + let subscriber = tracing_subscriber::FmtSubscriber::builder() + .with_ansi(false) + .with_writer(Mutex::new(file)) + .finish(); + + tracing::subscriber::set_global_default(subscriber)?; + let stdin = tokio::io::stdin(); let stdout = tokio::io::stdout(); + tracing::info!("Starting server."); + let (service, socket) = LspService::new(|client| LspServer::new(client)); Server::new(stdin, stdout, socket).serve(service).await; + + Ok(()) } diff --git a/crates/pg_lsp/src/server.rs b/crates/pg_lsp/src/server.rs index a50307b9..fca60653 100644 --- a/crates/pg_lsp/src/server.rs +++ b/crates/pg_lsp/src/server.rs @@ -24,16 +24,25 @@ pub struct LspServer { impl LspServer { pub fn new(client: Client) -> Self { - Self { + tracing::info!("Setting up server."); + let s = Self { client: Arc::new(client), session: Arc::new(Session::new()), client_capabilities: RwLock::new(None), debouncer: SimpleTokioDebouncer::new(std::time::Duration::from_millis(500)), - } + }; + tracing::info!("Server setup complete."); + + s } /// When the client sends a didChangeConfiguration notification, we need to parse the received JSON. - async fn parse_options_from_client( + #[tracing::instrument( + name = "Parsing config from client", + skip(self), + fields(options = %value) + )] + async fn parse_config_from_client( &self, mut value: serde_json::Value, ) -> Option { @@ -57,7 +66,8 @@ impl LspServer { } } - async fn request_opts_from_client(&self) -> Option { + #[tracing::instrument(name = "Requesting Configuration from Client", skip(self))] + async fn request_config_from_client(&self) -> Option { let params = ConfigurationParams { items: vec![ConfigurationItem { section: Some("pglsp".to_string()), @@ -65,6 +75,7 @@ impl LspServer { }], }; + tracing::info!("sending workspace/configuration request"); match self .client .send_request::(params) @@ -78,20 +89,24 @@ impl LspServer { .next() .expect("workspace/configuration request did not yield expected response."); - self.parse_options_from_client(relevant).await + self.parse_config_from_client(relevant).await } Err(why) => { let message = format!( "Unable to pull client options via workspace/configuration request: {}", why ); - println!("{}", message); self.client.log_message(MessageType::ERROR, message).await; None } } } + #[tracing::instrument( + name="Publishing diagnostics", + skip(self), + fields(%uri) + )] async fn publish_diagnostics(&self, mut uri: Url) { normalize_uri(&mut uri); @@ -121,6 +136,11 @@ impl LspServer { .await; } + #[tracing::instrument( + name="Publishing diagnostics via Debouncer", + skip(self), + fields(%uri) + )] async fn publish_diagnostics_debounced(&self, mut uri: Url) { let client = Arc::clone(&self.client); let session = Arc::clone(&self.session); @@ -130,7 +150,7 @@ impl LspServer { normalize_uri(&mut uri); let url = file_path(&uri); - let diagnostics = session.get_diagnostics_sync(url); + let diagnostics = session.get_diagnostics(url).await; let diagnostics: Vec = diagnostics .into_iter() @@ -160,9 +180,13 @@ impl LspServer { #[tower_lsp::async_trait] impl LanguageServer for LspServer { + #[tracing::instrument(name = "initialize", skip(self, params))] async fn initialize(&self, params: InitializeParams) -> jsonrpc::Result { + self.client + .show_message(MessageType::INFO, "Initialize Request received") + .await; let flags = ClientFlags::from_initialize_request_params(¶ms); - self.client_capabilities.blocking_write().replace(flags); + self.client_capabilities.write().await.replace(flags); Ok(InitializeResult { server_info: None, @@ -194,15 +218,17 @@ impl LanguageServer for LspServer { }) } + #[tracing::instrument(name = "initialized", skip(self, _params))] async fn initialized(&self, _params: InitializedParams) { self.client .log_message(MessageType::INFO, "Postgres LSP Connected!") .await; } + #[tracing::instrument(name = "shutdown", skip(self))] async fn shutdown(&self) -> jsonrpc::Result<()> { - self.session.shutdown().await; - self.debouncer.shutdown().await; + // self.session.shutdown().await; + // self.debouncer.shutdown().await; self.client .log_message(MessageType::INFO, "Postgres LSP terminated.") @@ -211,11 +237,12 @@ impl LanguageServer for LspServer { Ok(()) } + #[tracing::instrument(name = "workspace/didChangeConfiguration", skip(self, params))] async fn did_change_configuration(&self, params: DidChangeConfigurationParams) { let capabilities = self.client_capabilities.read().await; if capabilities.as_ref().unwrap().supports_pull_opts { - let opts = self.request_opts_from_client().await; + let opts = self.request_config_from_client().await; if opts .as_ref() .is_some_and(|o| o.db_connection_string.is_some()) @@ -238,7 +265,7 @@ impl LanguageServer for LspServer { // if we couldn't pull settings from the client, // we'll try parsing the passed in params. - let opts = self.parse_options_from_client(params.settings).await; + let opts = self.parse_config_from_client(params.settings).await; if opts .as_ref() @@ -262,8 +289,16 @@ impl LanguageServer for LspServer { } } + #[tracing::instrument( + name= "textDocument/didOpen", + skip(self), + fields( + uri = %params.text_document.uri + ) + )] async fn did_open(&self, params: DidOpenTextDocumentParams) { let mut uri = params.text_document.uri; + normalize_uri(&mut uri); let changed_urls = self @@ -281,6 +316,13 @@ impl LanguageServer for LspServer { } } + #[tracing::instrument( + name= "textDocument/didSave", + skip(self), + fields( + uri = %params.text_document.uri + ) + )] async fn did_save(&self, params: DidSaveTextDocumentParams) { let mut uri = params.text_document.uri; normalize_uri(&mut uri); @@ -295,6 +337,13 @@ impl LanguageServer for LspServer { } } + #[tracing::instrument( + name= "textDocument/didChange", + skip(self), + fields( + uri = %params.text_document.uri + ) + )] async fn did_change(&self, params: DidChangeTextDocumentParams) { let mut uri = params.text_document.uri; normalize_uri(&mut uri); @@ -302,6 +351,13 @@ impl LanguageServer for LspServer { self.publish_diagnostics_debounced(uri).await; } + #[tracing::instrument( + name= "textDocument/didClose", + skip(self), + fields( + uri = %params.text_document.uri + ) + )] async fn did_close(&self, params: DidCloseTextDocumentParams) { let mut uri = params.text_document.uri; normalize_uri(&mut uri); @@ -310,6 +366,13 @@ impl LanguageServer for LspServer { self.session.on_file_closed(path).await } + #[tracing::instrument( + name= "textDocument/codeAction", + skip(self), + fields( + uri = %params.text_document.uri + ) + )] async fn code_action( &self, params: CodeActionParams, @@ -328,6 +391,13 @@ impl LanguageServer for LspServer { Ok(actions) } + #[tracing::instrument( + name= "inlayHint/resolve", + skip(self), + fields( + uri = %params.text_document.uri + ) + )] async fn inlay_hint(&self, params: InlayHintParams) -> jsonrpc::Result>> { let mut uri = params.text_document.uri; normalize_uri(&mut uri); @@ -340,6 +410,13 @@ impl LanguageServer for LspServer { Ok(hints) } + #[tracing::instrument( + name= "textDocument/completion", + skip(self), + fields( + uri = %params.text_document_position.text_document.uri + ) + )] async fn completion( &self, params: CompletionParams, @@ -355,6 +432,13 @@ impl LanguageServer for LspServer { Ok(completions.map(|c| CompletionResponse::List(c))) } + #[tracing::instrument( + name= "textDocument/hover", + skip(self), + fields( + uri = %params.text_document_position_params.text_document.uri + ) + )] async fn hover(&self, params: HoverParams) -> jsonrpc::Result> { let mut uri = params.text_document_position_params.text_document.uri; normalize_uri(&mut uri); @@ -370,6 +454,7 @@ impl LanguageServer for LspServer { Ok(hover_diagnostics) } + #[tracing::instrument(name = "workspace/executeCommand", skip(self, params))] async fn execute_command( &self, params: ExecuteCommandParams, diff --git a/crates/pg_lsp/src/session.rs b/crates/pg_lsp/src/session.rs index 359d0786..082151ce 100644 --- a/crates/pg_lsp/src/session.rs +++ b/crates/pg_lsp/src/session.rs @@ -29,7 +29,7 @@ impl Session { } pub async fn shutdown(&self) { - let mut db = self.db.blocking_write(); + let mut db = self.db.write().await; let db = db.take(); if db.is_some() { @@ -53,13 +53,9 @@ impl Session { return Ok(()); } - let ide = self.ide.clone(); - let new_db = DbConnection::new(connection_string, move |schema| { - let _guard = ide.blocking_write().set_schema_cache(schema); - }) - .await?; + let new_db = DbConnection::new(connection_string, Arc::clone(&self.ide)).await?; - let mut current_db = self.db.blocking_write(); + let mut current_db = self.db.write().await; let old_db = current_db.replace(new_db); drop(current_db); @@ -89,30 +85,6 @@ impl Session { ide.remove_document(path); } - pub fn get_diagnostics_sync(&self, path: PgLspPath) -> Vec<(Diagnostic, Range)> { - let ide = self.ide.blocking_read(); - - // make sure there are documents at the provided path before - // trying to collect diagnostics. - let doc = ide.documents.get(&path); - if doc.is_none() { - return vec![]; - } - - ide.diagnostics(&path) - .into_iter() - .map(|d| { - let range = doc - .as_ref() - .unwrap() - .line_index - .line_col_lsp_range(d.range) - .unwrap(); - (d, range) - }) - .collect() - } - pub async fn get_diagnostics(&self, path: PgLspPath) -> Vec<(Diagnostic, Range)> { let ide = self.ide.read().await; diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts index 1c0e599b..3950df5e 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts @@ -1,20 +1,23 @@ -import { ExtensionContext } from 'vscode'; +import type { ExtensionContext } from 'vscode'; import { - Executable, + type Executable, LanguageClient, - LanguageClientOptions, - ServerOptions + type LanguageClientOptions, + type ServerOptions } from 'vscode-languageclient/node'; let client: LanguageClient; -export function activate(context: ExtensionContext) { +export function activate(_context: ExtensionContext) { // If the extension is launched in debug mode then the debug server options are used // Otherwise the run options are used const run: Executable = { command: 'pglsp' }; + + // const outputChannel = window.createOutputChannel('postgres_lsp'); + const serverOptions: ServerOptions = { run, debug: run @@ -23,24 +26,18 @@ export function activate(context: ExtensionContext) { // Options to control the language client const clientOptions: LanguageClientOptions = { // Register the server for plain text documents - documentSelector: [ - { scheme: 'file', language: 'sql' } - ] + documentSelector: [{ scheme: 'file', language: 'sql' }] }; // Create the language client and start the client. - client = new LanguageClient( - 'postgres_lsp', - 'Postgres LSP', - serverOptions, - clientOptions - ); + client = new LanguageClient('postgres_lsp', 'Postgres LSP', serverOptions, clientOptions); // Start the client. This will also launch the server - client.start(); + void client.start(); } export function deactivate(): Thenable | undefined { + console.log('Deactivating client...'); if (!client) { return undefined; } diff --git a/editors/code/tsconfig.json b/editors/code/tsconfig.json index ee353c28..125021ee 100644 --- a/editors/code/tsconfig.json +++ b/editors/code/tsconfig.json @@ -2,7 +2,7 @@ "extends": "@tsconfig/strictest/tsconfig.json", "compilerOptions": { "esModuleInterop": false, - "module": "commonjs", + "module": "Node16", "moduleResolution": "node16", "target": "es2021", "outDir": "out", diff --git a/pglsp.log b/pglsp.log new file mode 100644 index 00000000..1c147e6a --- /dev/null +++ b/pglsp.log @@ -0,0 +1,3 @@ +2024-11-23T16:57:38.897370Z INFO pglsp: Starting server. +2024-11-23T16:57:38.897489Z INFO pg_lsp::server: Setting up server. +2024-11-23T16:57:38.897666Z INFO pg_lsp::server: Server setup complete. diff --git a/test.sql b/test.sql index e8405740..33d5f65c 100644 --- a/test.sql +++ b/test.sql @@ -1,7 +1,7 @@ select id, name, test1231234123, unknown from co; -select 14433313331333 +select 1123123; alter table test drop column id; -select lower('test'); +select lower(); From 3fee1b09a4b68531bedf0c3322b952e2d07d5f31 Mon Sep 17 00:00:00 2001 From: Julian Date: Sat, 23 Nov 2024 21:15:34 +0100 Subject: [PATCH 29/33] it works --- crates/pg_lsp/src/main.rs | 7 +++++-- crates/pg_lsp/src/server.rs | 29 +++++++++++++++++------------ crates/pg_lsp/src/session.rs | 6 +----- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/crates/pg_lsp/src/main.rs b/crates/pg_lsp/src/main.rs index e71a9897..ea366fb6 100644 --- a/crates/pg_lsp/src/main.rs +++ b/crates/pg_lsp/src/main.rs @@ -1,7 +1,8 @@ -use std::{fs::File, path::PathBuf, str::FromStr, sync::Mutex}; +use std::{fs::File, path::PathBuf, str::FromStr}; use pg_lsp::server::LspServer; use tower_lsp::{LspService, Server}; +use tracing_subscriber::fmt::format::FmtSpan; #[tokio::main] async fn main() -> anyhow::Result<()> { @@ -9,8 +10,10 @@ async fn main() -> anyhow::Result<()> { let file = File::create(path).expect("Could not open the file."); let subscriber = tracing_subscriber::FmtSubscriber::builder() + .with_span_events(FmtSpan::ENTER) + .with_span_events(FmtSpan::CLOSE) .with_ansi(false) - .with_writer(Mutex::new(file)) + .with_writer(file) .finish(); tracing::subscriber::set_global_default(subscriber)?; diff --git a/crates/pg_lsp/src/server.rs b/crates/pg_lsp/src/server.rs index fca60653..1685a2b7 100644 --- a/crates/pg_lsp/src/server.rs +++ b/crates/pg_lsp/src/server.rs @@ -39,7 +39,7 @@ impl LspServer { /// When the client sends a didChangeConfiguration notification, we need to parse the received JSON. #[tracing::instrument( name = "Parsing config from client", - skip(self), + skip(self, value), fields(options = %value) )] async fn parse_config_from_client( @@ -104,7 +104,7 @@ impl LspServer { #[tracing::instrument( name="Publishing diagnostics", - skip(self), + skip(self, uri), fields(%uri) )] async fn publish_diagnostics(&self, mut uri: Url) { @@ -138,7 +138,7 @@ impl LspServer { #[tracing::instrument( name="Publishing diagnostics via Debouncer", - skip(self), + skip(self, uri), fields(%uri) )] async fn publish_diagnostics_debounced(&self, mut uri: Url) { @@ -290,8 +290,8 @@ impl LanguageServer for LspServer { } #[tracing::instrument( - name= "textDocument/didOpen", - skip(self), + name = "textDocument/didOpen", + skip(self, params), fields( uri = %params.text_document.uri ) @@ -318,7 +318,7 @@ impl LanguageServer for LspServer { #[tracing::instrument( name= "textDocument/didSave", - skip(self), + skip(self, params), fields( uri = %params.text_document.uri ) @@ -333,13 +333,16 @@ impl LanguageServer for LspServer { let changed_urls = self.session.recompute_and_get_changed_files().await; for url in changed_urls { let url = Url::from_file_path(url.as_path()).expect("Expected absolute File Path"); + + tracing::info!("publishing diagnostics: {}", url); + self.publish_diagnostics(url).await; } } #[tracing::instrument( name= "textDocument/didChange", - skip(self), + skip(self, params), fields( uri = %params.text_document.uri ) @@ -348,12 +351,14 @@ impl LanguageServer for LspServer { let mut uri = params.text_document.uri; normalize_uri(&mut uri); + tracing::info!("{}", uri); + self.publish_diagnostics_debounced(uri).await; } #[tracing::instrument( name= "textDocument/didClose", - skip(self), + skip(self, params), fields( uri = %params.text_document.uri ) @@ -368,7 +373,7 @@ impl LanguageServer for LspServer { #[tracing::instrument( name= "textDocument/codeAction", - skip(self), + skip(self, params), fields( uri = %params.text_document.uri ) @@ -393,7 +398,7 @@ impl LanguageServer for LspServer { #[tracing::instrument( name= "inlayHint/resolve", - skip(self), + skip(self, params), fields( uri = %params.text_document.uri ) @@ -412,7 +417,7 @@ impl LanguageServer for LspServer { #[tracing::instrument( name= "textDocument/completion", - skip(self), + skip(self, params), fields( uri = %params.text_document_position.text_document.uri ) @@ -434,7 +439,7 @@ impl LanguageServer for LspServer { #[tracing::instrument( name= "textDocument/hover", - skip(self), + skip(self, params), fields( uri = %params.text_document_position_params.text_document.uri ) diff --git a/crates/pg_lsp/src/session.rs b/crates/pg_lsp/src/session.rs index 082151ce..ca526670 100644 --- a/crates/pg_lsp/src/session.rs +++ b/crates/pg_lsp/src/session.rs @@ -92,6 +92,7 @@ impl Session { // trying to collect diagnostics. let doc = ide.documents.get(&path); if doc.is_none() { + tracing::info!("Doc not found, path: {:?}", &path); return vec![]; } @@ -118,11 +119,6 @@ impl Session { { let ide = self.ide.read().await; - let doc = ide.documents.get(&path); - if doc.is_none() { - return HashSet::new(); - } - ide.apply_change( path, DocumentChange::new(version, vec![Change { range: None, text }]), From 066023b80aa3efe29c6202c9f1b94de6c199b9b8 Mon Sep 17 00:00:00 2001 From: Julian Date: Sat, 23 Nov 2024 21:16:48 +0100 Subject: [PATCH 30/33] remove pglsp from git --- pglsp.log | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 pglsp.log diff --git a/pglsp.log b/pglsp.log deleted file mode 100644 index 1c147e6a..00000000 --- a/pglsp.log +++ /dev/null @@ -1,3 +0,0 @@ -2024-11-23T16:57:38.897370Z INFO pglsp: Starting server. -2024-11-23T16:57:38.897489Z INFO pg_lsp::server: Setting up server. -2024-11-23T16:57:38.897666Z INFO pg_lsp::server: Server setup complete. From e5ef2a7166a478e433293d4523d4b73fbe702fda Mon Sep 17 00:00:00 2001 From: Julian Date: Sat, 23 Nov 2024 21:21:59 +0100 Subject: [PATCH 31/33] with shutdown? --- crates/pg_lsp/src/debouncer.rs | 1 + crates/pg_lsp/src/server.rs | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/pg_lsp/src/debouncer.rs b/crates/pg_lsp/src/debouncer.rs index 380246da..61f17bf3 100644 --- a/crates/pg_lsp/src/debouncer.rs +++ b/crates/pg_lsp/src/debouncer.rs @@ -80,6 +80,7 @@ impl SimpleTokioDebouncer { pub async fn shutdown(&self) { self.shutdown_flag .store(true, std::sync::atomic::Ordering::Relaxed); + let _ = self.handle.abort(); // we don't care about any errors during shutdown } } diff --git a/crates/pg_lsp/src/server.rs b/crates/pg_lsp/src/server.rs index 1685a2b7..c37f4138 100644 --- a/crates/pg_lsp/src/server.rs +++ b/crates/pg_lsp/src/server.rs @@ -227,13 +227,18 @@ impl LanguageServer for LspServer { #[tracing::instrument(name = "shutdown", skip(self))] async fn shutdown(&self) -> jsonrpc::Result<()> { - // self.session.shutdown().await; - // self.debouncer.shutdown().await; + tracing::info!("Shutting down session..."); + self.session.shutdown().await; + + tracing::info!("Shutting down debouncer..."); + self.debouncer.shutdown().await; self.client .log_message(MessageType::INFO, "Postgres LSP terminated.") .await; + tracing::info!("Shutdown successful."); + Ok(()) } From f25a15d7ec837cc7869c78ab3319204adcd901f8 Mon Sep 17 00:00:00 2001 From: Julian Date: Sat, 23 Nov 2024 21:31:12 +0100 Subject: [PATCH 32/33] shutdown logs --- crates/pg_lsp/src/db_connection.rs | 1 + crates/pg_lsp/src/server.rs | 10 ++++------ crates/pg_lsp/src/session.rs | 1 + 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/pg_lsp/src/db_connection.rs b/crates/pg_lsp/src/db_connection.rs index 37c9e8e1..493eeb64 100644 --- a/crates/pg_lsp/src/db_connection.rs +++ b/crates/pg_lsp/src/db_connection.rs @@ -65,6 +65,7 @@ impl DbConnection { connection_string == self.connection_string } + #[tracing::instrument(name = "Closing DB Pool", skip(self))] pub(crate) async fn close(self) { let _ = self.close_tx.send(()); let _ = self.schema_update_handle.await; diff --git a/crates/pg_lsp/src/server.rs b/crates/pg_lsp/src/server.rs index c37f4138..a3c9e8fb 100644 --- a/crates/pg_lsp/src/server.rs +++ b/crates/pg_lsp/src/server.rs @@ -227,18 +227,16 @@ impl LanguageServer for LspServer { #[tracing::instrument(name = "shutdown", skip(self))] async fn shutdown(&self) -> jsonrpc::Result<()> { - tracing::info!("Shutting down session..."); self.session.shutdown().await; - - tracing::info!("Shutting down debouncer..."); self.debouncer.shutdown().await; self.client - .log_message(MessageType::INFO, "Postgres LSP terminated.") + .send_notification::(ShowMessageParams { + message: "Shutdown successful.".into(), + typ: MessageType::INFO, + }) .await; - tracing::info!("Shutdown successful."); - Ok(()) } diff --git a/crates/pg_lsp/src/session.rs b/crates/pg_lsp/src/session.rs index ca526670..a84d1b10 100644 --- a/crates/pg_lsp/src/session.rs +++ b/crates/pg_lsp/src/session.rs @@ -28,6 +28,7 @@ impl Session { } } + #[tracing::instrument(name = "Shutting down Session", skip(self))] pub async fn shutdown(&self) { let mut db = self.db.write().await; let db = db.take(); From bfec60a306303765cc05e155a59bc8eeff0fd250 Mon Sep 17 00:00:00 2001 From: Julian Date: Sat, 23 Nov 2024 21:44:09 +0100 Subject: [PATCH 33/33] change import --- Cargo.lock | 36 ++++++++++++++++++------------------ crates/pg_lsp/src/session.rs | 3 ++- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4d323979..c1b9bd66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -560,16 +560,6 @@ dependencies = [ ] [[package]] -<<<<<<< HEAD -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.89", -======= name = "directories" version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -588,7 +578,17 @@ dependencies = [ "option-ext", "redox_users", "windows-sys 0.48.0", ->>>>>>> upstream/main +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.89", ] [[package]] @@ -1246,7 +1246,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.6.0", "libc", ] @@ -1517,18 +1517,18 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking" version = "2.2.1" diff --git a/crates/pg_lsp/src/session.rs b/crates/pg_lsp/src/session.rs index a84d1b10..9d7ef641 100644 --- a/crates/pg_lsp/src/session.rs +++ b/crates/pg_lsp/src/session.rs @@ -1,9 +1,10 @@ use std::{collections::HashSet, sync::Arc}; -use pg_base_db::{Change, DocumentChange, PgLspPath}; +use pg_base_db::{Change, DocumentChange}; use pg_commands::{Command, ExecuteStatementCommand}; use pg_completions::CompletionParams; use pg_diagnostics::Diagnostic; +use pg_fs::PgLspPath; use pg_hover::HoverParams; use pg_workspace::Workspace; use text_size::TextSize;