diff --git a/sway-lsp/Cargo.toml b/sway-lsp/Cargo.toml index 04481002867..fa049a0da79 100644 --- a/sway-lsp/Cargo.toml +++ b/sway-lsp/Cargo.toml @@ -36,6 +36,7 @@ syn = { version = "1.0.73", features = ["full"] } tempfile = "3" thiserror = "1.0.30" tokio = { version = "1.3", features = [ + "fs", "io-std", "io-util", "macros", diff --git a/sway-lsp/benches/lsp_benchmarks/compile.rs b/sway-lsp/benches/lsp_benchmarks/compile.rs index 6e2a07f8b4c..8983879302b 100644 --- a/sway-lsp/benches/lsp_benchmarks/compile.rs +++ b/sway-lsp/benches/lsp_benchmarks/compile.rs @@ -2,14 +2,18 @@ use criterion::{black_box, criterion_group, Criterion}; use lsp_types::Url; use sway_core::Engines; use sway_lsp::core::session::{self, Session}; +use tokio::runtime::Runtime; const NUM_DID_CHANGE_ITERATIONS: usize = 4; fn benchmarks(c: &mut Criterion) { // Load the test project let uri = Url::from_file_path(super::benchmark_dir().join("src/main.sw")).unwrap(); - let session = Session::new(); - session.handle_open_file(&uri); + let session = Runtime::new().unwrap().block_on(async { + let session = Session::new(); + session.handle_open_file(&uri).await; + session + }); c.bench_function("compile", |b| { b.iter(|| { diff --git a/sway-lsp/benches/lsp_benchmarks/mod.rs b/sway-lsp/benches/lsp_benchmarks/mod.rs index 5bb10d7ced7..f21fb3e8ae2 100644 --- a/sway-lsp/benches/lsp_benchmarks/mod.rs +++ b/sway-lsp/benches/lsp_benchmarks/mod.rs @@ -6,11 +6,11 @@ use lsp_types::Url; use std::{path::PathBuf, sync::Arc}; use sway_lsp::core::session::{self, Session}; -pub fn compile_test_project() -> (Url, Arc) { +pub async fn compile_test_project() -> (Url, Arc) { let session = Session::new(); // Load the test project let uri = Url::from_file_path(benchmark_dir().join("src/main.sw")).unwrap(); - session.handle_open_file(&uri); + session.handle_open_file(&uri).await; // Compile the project and write the parse result to the session let parse_result = session::parse_project(&uri, &session.engines.read()).unwrap(); session.write_parse_result(parse_result); diff --git a/sway-lsp/benches/lsp_benchmarks/requests.rs b/sway-lsp/benches/lsp_benchmarks/requests.rs index fe87df01026..3c395ccb6d3 100644 --- a/sway-lsp/benches/lsp_benchmarks/requests.rs +++ b/sway-lsp/benches/lsp_benchmarks/requests.rs @@ -4,9 +4,12 @@ use lsp_types::{ TextDocumentIdentifier, }; use sway_lsp::{capabilities, lsp_ext::OnEnterParams, utils::keyword_docs::KeywordDocs}; +use tokio::runtime::Runtime; fn benchmarks(c: &mut Criterion) { - let (uri, session) = black_box(super::compile_test_project()); + let (uri, session) = Runtime::new() + .unwrap() + .block_on(async { black_box(super::compile_test_project().await) }); let config = sway_lsp::config::Config::default(); let keyword_docs = KeywordDocs::new(); let position = Position::new(1717, 24); diff --git a/sway-lsp/benches/lsp_benchmarks/token_map.rs b/sway-lsp/benches/lsp_benchmarks/token_map.rs index 5ae1ff64b3d..a19dcea8611 100644 --- a/sway-lsp/benches/lsp_benchmarks/token_map.rs +++ b/sway-lsp/benches/lsp_benchmarks/token_map.rs @@ -1,8 +1,11 @@ use criterion::{black_box, criterion_group, Criterion}; use lsp_types::Position; +use tokio::runtime::Runtime; fn benchmarks(c: &mut Criterion) { - let (uri, session) = black_box(super::compile_test_project()); + let (uri, session) = Runtime::new() + .unwrap() + .block_on(async { black_box(super::compile_test_project().await) }); let engines = session.engines.read(); let position = Position::new(1716, 24); diff --git a/sway-lsp/src/capabilities/on_enter.rs b/sway-lsp/src/capabilities/on_enter.rs index d2c14af7bd2..4c83810623f 100644 --- a/sway-lsp/src/capabilities/on_enter.rs +++ b/sway-lsp/src/capabilities/on_enter.rs @@ -119,12 +119,13 @@ mod tests { } } - #[test] - fn get_comment_workspace_edit_double_slash_indented() { + #[tokio::test] + async fn get_comment_workspace_edit_double_slash_indented() { let path = get_absolute_path("sway-lsp/tests/fixtures/diagnostics/dead_code/src/main.sw"); let uri = Url::from_file_path(path.clone()).unwrap(); - let text_document = - TextDocument::build_from_path(path.as_str()).expect("failed to build document"); + let text_document = TextDocument::build_from_path(path.as_str()) + .await + .expect("failed to build document"); let params = OnEnterParams { text_document: TextDocumentIdentifier { uri }, content_changes: vec![TextDocumentContentChangeEvent { @@ -156,12 +157,13 @@ mod tests { assert_text_edit(&edits[0].edits[0], "// ".to_string(), 48, 4); } - #[test] - fn get_comment_workspace_edit_triple_slash_paste() { + #[tokio::test] + async fn get_comment_workspace_edit_triple_slash_paste() { let path = get_absolute_path("sway-lsp/tests/fixtures/diagnostics/dead_code/src/main.sw"); let uri = Url::from_file_path(path.clone()).unwrap(); - let text_document = - TextDocument::build_from_path(path.as_str()).expect("failed to build document"); + let text_document = TextDocument::build_from_path(path.as_str()) + .await + .expect("failed to build document"); let params = OnEnterParams { text_document: TextDocumentIdentifier { uri }, content_changes: vec![TextDocumentContentChangeEvent { diff --git a/sway-lsp/src/core/document.rs b/sway-lsp/src/core/document.rs index 8d600974625..4c0294df304 100644 --- a/sway-lsp/src/core/document.rs +++ b/sway-lsp/src/core/document.rs @@ -5,6 +5,7 @@ use crate::{ }; use lsp_types::{Position, Range, TextDocumentContentChangeEvent, Url}; use ropey::Rope; +use tokio::fs::File; #[derive(Debug, Clone)] pub struct TextDocument { @@ -17,8 +18,9 @@ pub struct TextDocument { } impl TextDocument { - pub fn build_from_path(path: &str) -> Result { - std::fs::read_to_string(path) + pub async fn build_from_path(path: &str) -> Result { + tokio::fs::read_to_string(path) + .await .map(|content| Self { language_id: "sway".into(), version: 1, @@ -109,33 +111,39 @@ impl TextDocument { /// Marks the specified file as "dirty" by creating a corresponding flag file. /// /// This function ensures the necessary directory structure exists before creating the flag file. -pub fn mark_file_as_dirty(uri: &Url) -> Result<(), LanguageServerError> { +pub async fn mark_file_as_dirty(uri: &Url) -> Result<(), LanguageServerError> { let path = document::get_path_from_url(uri)?; let dirty_file_path = forc_util::is_dirty_path(&path); if let Some(dir) = dirty_file_path.parent() { // Ensure the directory exists - std::fs::create_dir_all(dir).map_err(|_| DirectoryError::LspLocksDirFailed)?; + tokio::fs::create_dir_all(dir) + .await + .map_err(|_| DirectoryError::LspLocksDirFailed)?; } // Create an empty "dirty" file - std::fs::File::create(&dirty_file_path).map_err(|err| DocumentError::UnableToCreateFile { - path: uri.path().to_string(), - err: err.to_string(), - })?; + File::create(&dirty_file_path) + .await + .map_err(|err| DocumentError::UnableToCreateFile { + path: uri.path().to_string(), + err: err.to_string(), + })?; Ok(()) } /// Removes the corresponding flag file for the specifed Url. /// /// If the flag file does not exist, this function will do nothing. -pub fn remove_dirty_flag(uri: &Url) -> Result<(), LanguageServerError> { +pub async fn remove_dirty_flag(uri: &Url) -> Result<(), LanguageServerError> { let path = document::get_path_from_url(uri)?; let dirty_file_path = forc_util::is_dirty_path(&path); if dirty_file_path.exists() { // Remove the "dirty" file - std::fs::remove_file(dirty_file_path).map_err(|err| DocumentError::UnableToRemoveFile { - path: uri.path().to_string(), - err: err.to_string(), - })?; + tokio::fs::remove_file(dirty_file_path) + .await + .map_err(|err| DocumentError::UnableToRemoveFile { + path: uri.path().to_string(), + err: err.to_string(), + })?; } Ok(()) } @@ -152,17 +160,19 @@ mod tests { use super::*; use sway_lsp_test_utils::get_absolute_path; - #[test] - fn build_from_path_returns_text_document() { + #[tokio::test] + async fn build_from_path_returns_text_document() { let path = get_absolute_path("sway-lsp/tests/fixtures/cats.txt"); - let result = TextDocument::build_from_path(&path); + let result = TextDocument::build_from_path(&path).await; assert!(result.is_ok(), "result = {result:?}"); } - #[test] - fn build_from_path_returns_document_not_found_error() { + #[tokio::test] + async fn build_from_path_returns_document_not_found_error() { let path = get_absolute_path("not/a/real/file/path"); - let result = TextDocument::build_from_path(&path).expect_err("expected DocumentNotFound"); + let result = TextDocument::build_from_path(&path) + .await + .expect_err("expected DocumentNotFound"); assert_eq!(result, DocumentError::DocumentNotFound { path }); } } diff --git a/sway-lsp/src/core/session.rs b/sway-lsp/src/core/session.rs index 3c19652c69c..4abd6290b60 100644 --- a/sway-lsp/src/core/session.rs +++ b/sway-lsp/src/core/session.rs @@ -25,14 +25,7 @@ use lsp_types::{ use parking_lot::RwLock; use pkg::{manifest::ManifestFile, BuildPlan}; use rayon::iter::{ParallelBridge, ParallelIterator}; -use std::{ - fs::File, - io::Write, - ops::Deref, - path::PathBuf, - sync::{atomic::Ordering, Arc}, - vec, -}; +use std::{ops::Deref, path::PathBuf, sync::Arc}; use sway_core::{ decl_engine::DeclEngine, language::{ @@ -46,7 +39,7 @@ use sway_core::{ use sway_error::{error::CompileError, handler::Handler, warning::CompileWarning}; use sway_types::{SourceEngine, SourceId, Spanned}; use sway_utils::{helpers::get_sway_files, PerformanceData}; -use tokio::sync::Semaphore; +use tokio::{fs::File, io::AsyncWriteExt, sync::Semaphore}; pub type Documents = DashMap; pub type ProjectDirectory = PathBuf; @@ -111,26 +104,23 @@ impl Session { } } - pub fn init(&self, uri: &Url) -> Result { + pub async fn init(&self, uri: &Url) -> Result { let manifest_dir = PathBuf::from(uri.path()); // Create a new temp dir that clones the current workspace // and store manifest and temp paths self.sync.create_temp_dir_from_workspace(&manifest_dir)?; self.sync.clone_manifest_dir_to_temp()?; // iterate over the project dir, parse all sway files - let _ = self.store_sway_files(); + let _ = self.store_sway_files().await; self.sync.watch_and_sync_manifest(); self.sync.manifest_dir().map_err(Into::into) } pub fn shutdown(&self) { - // Set the should_end flag to true - self.sync.should_end.store(true, Ordering::Relaxed); - - // Wait for the thread to finish - let mut join_handle_option = self.sync.notify_join_handle.write(); - if let Some(join_handle) = std::mem::take(&mut *join_handle_option) { - let _ = join_handle.join(); + // shutdown the thread watching the manifest file + let handle = self.sync.notify_join_handle.read(); + if let Some(join_handle) = &*handle { + join_handle.abort(); } // Delete the temporary directory. @@ -276,16 +266,16 @@ impl Session { .map(|page_text_edit| vec![page_text_edit]) } - pub fn handle_open_file(&self, uri: &Url) { + pub async fn handle_open_file(&self, uri: &Url) { if !self.documents.contains_key(uri.path()) { - if let Ok(text_document) = TextDocument::build_from_path(uri.path()) { + if let Ok(text_document) = TextDocument::build_from_path(uri.path()).await { let _ = self.store_document(text_document); } } } - /// Writes the changes to the file and updates the document. - pub fn write_changes_to_file( + /// Asynchronously writes the changes to the file and updates the document. + pub async fn write_changes_to_file( &self, uri: &Url, changes: Vec, @@ -295,15 +285,22 @@ impl Session { path: uri.path().to_string(), } })?; + let mut file = - File::create(uri.path()).map_err(|err| DocumentError::UnableToCreateFile { + File::create(uri.path()) + .await + .map_err(|err| DocumentError::UnableToCreateFile { + path: uri.path().to_string(), + err: err.to_string(), + })?; + + file.write_all(src.as_bytes()) + .await + .map_err(|err| DocumentError::UnableToWriteFile { path: uri.path().to_string(), err: err.to_string(), })?; - writeln!(&mut file, "{src}").map_err(|err| DocumentError::UnableToWriteFile { - path: uri.path().to_string(), - err: err.to_string(), - })?; + Ok(()) } @@ -399,11 +396,11 @@ impl Session { } /// Populate [Documents] with sway files found in the workspace. - fn store_sway_files(&self) -> Result<(), LanguageServerError> { + async fn store_sway_files(&self) -> Result<(), LanguageServerError> { let temp_dir = self.sync.temp_dir()?; // Store the documents. for path in get_sway_files(temp_dir).iter().filter_map(|fp| fp.to_str()) { - self.store_document(TextDocument::build_from_path(path)?)?; + self.store_document(TextDocument::build_from_path(path).await?)?; } Ok(()) } @@ -594,22 +591,22 @@ mod tests { use super::*; use sway_lsp_test_utils::{get_absolute_path, get_url}; - #[test] - fn store_document_returns_empty_tuple() { + #[tokio::test] + async fn store_document_returns_empty_tuple() { let session = Session::new(); let path = get_absolute_path("sway-lsp/tests/fixtures/cats.txt"); - let document = TextDocument::build_from_path(&path).unwrap(); + let document = TextDocument::build_from_path(&path).await.unwrap(); let result = Session::store_document(&session, document); assert!(result.is_ok()); } - #[test] - fn store_document_returns_document_already_stored_error() { + #[tokio::test] + async fn store_document_returns_document_already_stored_error() { let session = Session::new(); let path = get_absolute_path("sway-lsp/tests/fixtures/cats.txt"); - let document = TextDocument::build_from_path(&path).unwrap(); + let document = TextDocument::build_from_path(&path).await.unwrap(); Session::store_document(&session, document).expect("expected successfully stored"); - let document = TextDocument::build_from_path(&path).unwrap(); + let document = TextDocument::build_from_path(&path).await.unwrap(); let result = Session::store_document(&session, document) .expect_err("expected DocumentAlreadyStored"); assert_eq!(result, DocumentError::DocumentAlreadyStored { path }); diff --git a/sway-lsp/src/core/sync.rs b/sway-lsp/src/core/sync.rs index 989470872f1..4129c8f9a2e 100644 --- a/sway-lsp/src/core/sync.rs +++ b/sway-lsp/src/core/sync.rs @@ -13,12 +13,12 @@ use std::{ fs::{self, File}, io::{Read, Write}, path::{Path, PathBuf}, - sync::{atomic::AtomicBool, mpsc, Arc}, - thread::JoinHandle, + sync::Arc, time::Duration, }; use sway_types::{SourceEngine, Span}; use tempfile::Builder; +use tokio::task::JoinHandle; #[derive(Debug, Eq, PartialEq, Hash)] pub enum Directory { @@ -30,8 +30,6 @@ pub enum Directory { pub struct SyncWorkspace { pub directories: DashMap, pub notify_join_handle: RwLock>>, - // if we should shutdown the thread watching the manifest file - pub should_end: Arc, } impl SyncWorkspace { @@ -41,7 +39,6 @@ impl SyncWorkspace { Self { directories: DashMap::new(), notify_join_handle: RwLock::new(None), - should_end: Arc::new(AtomicBool::new(false)), } } @@ -195,13 +192,13 @@ impl SyncWorkspace { if let Some(temp_manifest_path) = self.temp_manifest_path() { edit_manifest_dependency_paths(&manifest, &temp_manifest_path); - let (tx, rx) = mpsc::channel(); - let handle = std::thread::spawn(move || { + let handle = tokio::spawn(async move { + let (tx, mut rx) = tokio::sync::mpsc::channel(10); // Setup debouncer. No specific tickrate, max debounce time 2 seconds let mut debouncer = new_debouncer(Duration::from_secs(1), None, move |event| { if let Ok(e) = event { - let _ = tx.send(e); + let _ = tx.blocking_send(e); } }) .unwrap(); @@ -211,7 +208,7 @@ impl SyncWorkspace { .watch(manifest_dir.as_ref().path(), RecursiveMode::NonRecursive) .unwrap(); - while let Ok(_events) = rx.recv() { + while let Some(_events) = rx.recv().await { // Rescan the Forc.toml and convert // relative paths to absolute. Save into our temp directory. edit_manifest_dependency_paths(&manifest, &temp_manifest_path); diff --git a/sway-lsp/src/handlers/notification.rs b/sway-lsp/src/handlers/notification.rs index f39d21ce51c..a45f9c661a8 100644 --- a/sway-lsp/src/handlers/notification.rs +++ b/sway-lsp/src/handlers/notification.rs @@ -13,8 +13,9 @@ pub async fn handle_did_open_text_document( ) -> Result<(), LanguageServerError> { let (uri, session) = state .sessions - .uri_and_session_from_workspace(¶ms.text_document.uri)?; - session.handle_open_file(&uri); + .uri_and_session_from_workspace(¶ms.text_document.uri) + .await?; + session.handle_open_file(&uri).await; // If the token map is empty, then we need to parse the project. // Otherwise, don't recompile the project when a new file in the project is opened // as the workspace is already compiled. @@ -30,11 +31,14 @@ pub async fn handle_did_change_text_document( state: &ServerState, params: DidChangeTextDocumentParams, ) -> Result<(), LanguageServerError> { - document::mark_file_as_dirty(¶ms.text_document.uri)?; + document::mark_file_as_dirty(¶ms.text_document.uri).await?; let (uri, session) = state .sessions - .uri_and_session_from_workspace(¶ms.text_document.uri)?; - session.write_changes_to_file(&uri, params.content_changes)?; + .uri_and_session_from_workspace(¶ms.text_document.uri) + .await?; + session + .write_changes_to_file(&uri, params.content_changes) + .await?; state .parse_project( uri, @@ -50,10 +54,11 @@ pub(crate) async fn handle_did_save_text_document( state: &ServerState, params: DidSaveTextDocumentParams, ) -> Result<(), LanguageServerError> { - document::remove_dirty_flag(¶ms.text_document.uri)?; + document::remove_dirty_flag(¶ms.text_document.uri).await?; let (uri, session) = state .sessions - .uri_and_session_from_workspace(¶ms.text_document.uri)?; + .uri_and_session_from_workspace(¶ms.text_document.uri) + .await?; session.sync.resync()?; state .parse_project(uri, params.text_document.uri, None, session.clone()) @@ -61,14 +66,17 @@ pub(crate) async fn handle_did_save_text_document( Ok(()) } -pub(crate) fn handle_did_change_watched_files( +pub(crate) async fn handle_did_change_watched_files( state: &ServerState, params: DidChangeWatchedFilesParams, ) -> Result<(), LanguageServerError> { for event in params.changes { - let (uri, session) = state.sessions.uri_and_session_from_workspace(&event.uri)?; + let (uri, session) = state + .sessions + .uri_and_session_from_workspace(&event.uri) + .await?; if let FileChangeType::DELETED = event.typ { - document::remove_dirty_flag(&event.uri)?; + document::remove_dirty_flag(&event.uri).await?; let _ = session.remove_document(&uri); } } diff --git a/sway-lsp/src/handlers/request.rs b/sway-lsp/src/handlers/request.rs index fef0432b7e6..b3e08c70a98 100644 --- a/sway-lsp/src/handlers/request.rs +++ b/sway-lsp/src/handlers/request.rs @@ -48,13 +48,14 @@ pub fn handle_initialize( }) } -pub fn handle_document_symbol( +pub async fn handle_document_symbol( state: &ServerState, params: lsp_types::DocumentSymbolParams, ) -> Result> { match state .sessions .uri_and_session_from_workspace(¶ms.text_document.uri) + .await { Ok((uri, session)) => { let _ = session.wait_for_parsing(); @@ -69,13 +70,14 @@ pub fn handle_document_symbol( } } -pub fn handle_goto_definition( +pub async fn handle_goto_definition( state: &ServerState, params: lsp_types::GotoDefinitionParams, ) -> Result> { match state .sessions .uri_and_session_from_workspace(¶ms.text_document_position_params.text_document.uri) + .await { Ok((uri, session)) => { let position = params.text_document_position_params.position; @@ -88,7 +90,7 @@ pub fn handle_goto_definition( } } -pub fn handle_completion( +pub async fn handle_completion( state: &ServerState, params: lsp_types::CompletionParams, ) -> Result> { @@ -101,6 +103,7 @@ pub fn handle_completion( match state .sessions .uri_and_session_from_workspace(¶ms.text_document_position.text_document.uri) + .await { Ok((uri, session)) => Ok(session .completion_items(&uri, position, trigger_char) @@ -112,13 +115,14 @@ pub fn handle_completion( } } -pub fn handle_hover( +pub async fn handle_hover( state: &ServerState, params: lsp_types::HoverParams, ) -> Result> { match state .sessions .uri_and_session_from_workspace(¶ms.text_document_position_params.text_document.uri) + .await { Ok((uri, session)) => { let position = params.text_document_position_params.position; @@ -136,13 +140,14 @@ pub fn handle_hover( } } -pub fn handle_prepare_rename( +pub async fn handle_prepare_rename( state: &ServerState, params: lsp_types::TextDocumentPositionParams, ) -> Result> { match state .sessions .uri_and_session_from_workspace(¶ms.text_document.uri) + .await { Ok((uri, session)) => { match capabilities::rename::prepare_rename(session, uri, params.position) { @@ -160,10 +165,14 @@ pub fn handle_prepare_rename( } } -pub fn handle_rename(state: &ServerState, params: RenameParams) -> Result> { +pub async fn handle_rename( + state: &ServerState, + params: RenameParams, +) -> Result> { match state .sessions .uri_and_session_from_workspace(¶ms.text_document_position.text_document.uri) + .await { Ok((uri, session)) => { let new_name = params.new_name; @@ -183,13 +192,14 @@ pub fn handle_rename(state: &ServerState, params: RenameParams) -> Result