diff --git a/crates/integration_tests/tests/test_lsp.rs b/crates/integration_tests/tests/test_lsp.rs index 2285308a..da9a09df 100644 --- a/crates/integration_tests/tests/test_lsp.rs +++ b/crates/integration_tests/tests/test_lsp.rs @@ -1,19 +1,22 @@ use lsp_server::{Connection, Message, Notification, Request, RequestId, Response}; use lsp_types::request::{Initialize, Shutdown}; use lsp_types::{ - ClientCapabilities, DidChangeConfigurationParams, InitializeParams, InitializedParams, + ClientCapabilities, DidChangeConfigurationParams, DidChangeWatchedFilesParams, + FileChangeType, FileEvent, InitializeParams, InitializedParams, Url, }; use analysis::config::Config; use dialects::DialectName; -use integration_tests::config; +use integration_tests::{config, get_script_path}; -use lsp_types::notification::{DidChangeConfiguration, Initialized}; +use lsp_types::notification::{DidChangeConfiguration, DidChangeWatchedFiles, Initialized}; use move_language_server::global_state::{initialize_new_global_state, GlobalState}; -use move_language_server::main_loop::{main_loop, notification_new, request_new}; +use move_language_server::main_loop::{ + main_loop, notification_new, request_new, FileSystemEvent, +}; use move_language_server::server::run_server; const SHUTDOWN_REQ_ID: u64 = 10; @@ -58,20 +61,28 @@ fn response(req_id: usize, contents: serde_json::Value) -> Message { trait MessageType { fn into_request(self) -> Request; fn into_response(self) -> Response; + fn into_notification(self) -> Notification; } impl MessageType for Message { fn into_request(self) -> Request { match self { Message::Request(req) => req, - _ => panic!(), + _ => panic!("not a request"), } } fn into_response(self) -> Response { match self { Message::Response(resp) => resp, - _ => panic!(), + _ => panic!("not a response"), + } + } + + fn into_notification(self) -> Notification { + match self { + Message::Notification(notification) => notification, + _ => panic!("not a notification"), } } } @@ -115,13 +126,18 @@ fn test_server_initialization() { assert_eq!(init_finished_resp.id, RequestId::from(1)); assert_eq!( init_finished_resp.result.unwrap()["capabilities"]["textDocumentSync"], - 1 + serde_json::json!({"change": 1, "openClose": true}) ); - let shutdown_req = client_conn.receiver.try_recv().unwrap(); + let registration_req = client_conn.receiver.try_recv().unwrap().into_request(); + assert_eq!(registration_req.method, "client/registerCapability"); assert_eq!( - shutdown_req.into_response().id, - RequestId::from(SHUTDOWN_REQ_ID) + registration_req.params["registrations"][0]["method"], + "workspace/didChangeWatchedFiles" ); + + let shutdown_resp = client_conn.receiver.try_recv().unwrap().into_response(); + assert_eq!(shutdown_resp.id, RequestId::from(SHUTDOWN_REQ_ID)); + client_conn.receiver.try_recv().unwrap_err(); } @@ -157,3 +173,31 @@ fn test_server_config_change() { ); assert_eq!(global_state.config().dialect_name, DialectName::DFinance); } + +#[test] +fn test_removed_file_not_present_in_the_diagnostics() { + let (client_conn, server_conn) = Connection::memory(); + + let script_text = r"script { + use 0x0::Unknown; + fun main() {} + }"; + let script_file = (get_script_path(), script_text.to_string()); + + let mut global_state = global_state(config!()); + global_state.update_from_events(vec![FileSystemEvent::AddFile(script_file)]); + + let delete_event = FileEvent::new( + Url::from_file_path(get_script_path()).unwrap(), + FileChangeType::Deleted, + ); + let files_changed_notification = + notification::(DidChangeWatchedFilesParams { + changes: vec![delete_event], + }); + send_messages(&client_conn, vec![files_changed_notification]); + + main_loop(&mut global_state, &server_conn).unwrap(); + + assert!(global_state.analysis().db().available_files.is_empty()); +} diff --git a/crates/move-language-server/src/global_state.rs b/crates/move-language-server/src/global_state.rs index 6e187d0a..f24fea33 100644 --- a/crates/move-language-server/src/global_state.rs +++ b/crates/move-language-server/src/global_state.rs @@ -64,17 +64,17 @@ impl GlobalState { } pub fn initialize_new_global_state(config: Config) -> GlobalState { - let mut fs_events = vec![]; + let mut initial_fs_events = vec![]; match &config.stdlib_folder { Some(folder) => { for file in io::load_move_files(vec![folder.clone()]).unwrap() { - fs_events.push(FileSystemEvent::AddFile(file)); + initial_fs_events.push(FileSystemEvent::AddFile(file)); } } None => {} } for file in io::load_move_files(config.modules_folders.clone()).unwrap() { - fs_events.push(FileSystemEvent::AddFile(file)); + initial_fs_events.push(FileSystemEvent::AddFile(file)); } - GlobalState::new(config, fs_events) + GlobalState::new(config, initial_fs_events) } diff --git a/crates/move-language-server/src/main_loop.rs b/crates/move-language-server/src/main_loop.rs index 9d2d9767..ac62cf1e 100644 --- a/crates/move-language-server/src/main_loop.rs +++ b/crates/move-language-server/src/main_loop.rs @@ -6,8 +6,8 @@ use anyhow::Result; use crossbeam_channel::{unbounded, Sender}; use lsp_server::{Connection, Message, Notification, Request, RequestId, Response}; use lsp_types::notification::{ - DidChangeConfiguration, DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument, - PublishDiagnostics, ShowMessage, + DidChangeConfiguration, DidChangeTextDocument, DidChangeWatchedFiles, DidCloseTextDocument, + DidOpenTextDocument, PublishDiagnostics, ShowMessage, }; use lsp_types::request::WorkspaceConfiguration; use lsp_types::{ @@ -370,6 +370,23 @@ fn on_notification( } Err(not) => not, }; + let not = match notification_cast::(not) { + Ok(params) => { + for file_event in params.changes { + let uri = file_event.uri; + let fpath = uri + .to_file_path() + .map_err(|_| anyhow::anyhow!("invalid uri: {}", uri))?; + let fpath = leaked_fpath(fpath); + loop_state.opened_files.remove(fpath); + fs_events_sender + .send(FileSystemEvent::RemoveFile(fpath)) + .unwrap(); + } + return Ok(()); + } + Err(not) => not, + }; if not.method.starts_with("$/") { return Ok(()); } diff --git a/crates/move-language-server/src/server.rs b/crates/move-language-server/src/server.rs index dd6238ab..7f23b310 100644 --- a/crates/move-language-server/src/server.rs +++ b/crates/move-language-server/src/server.rs @@ -1,18 +1,29 @@ use std::path::PathBuf; use anyhow::Result; -use lsp_server::{Connection, ProtocolError}; -use lsp_types::{ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind}; +use lsp_server::{Connection, ProtocolError, RequestId}; +use lsp_types::{ + DidChangeWatchedFilesRegistrationOptions, FileSystemWatcher, RegistrationParams, + ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind, + TextDocumentSyncOptions, WatchKind, +}; use serde::de::DeserializeOwned; use analysis::config::Config; use crate::global_state::initialize_new_global_state; use crate::main_loop; +use crate::main_loop::request_new; fn move_language_server_capabilities() -> ServerCapabilities { ServerCapabilities { - text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::Full)), + text_document_sync: Some(TextDocumentSyncCapability::Options( + TextDocumentSyncOptions { + open_close: Some(true), + change: Some(TextDocumentSyncKind::Full), + ..TextDocumentSyncOptions::default() + }, + )), ..ServerCapabilities::default() } } @@ -45,13 +56,36 @@ pub fn parse_initialize_params(init_params: serde_json::Value) -> Result<(PathBu Ok((root, config)) } +fn register_for_file_changes(connection: &Connection) { + let move_files_watcher = FileSystemWatcher { + glob_pattern: "**/*.move".to_string(), + kind: Some(WatchKind::Delete), + }; + let registration_options = DidChangeWatchedFilesRegistrationOptions { + watchers: vec![move_files_watcher], + }; + let registration = lsp_types::Registration { + id: "workspace/didChangeWatchedFiles".to_string(), + method: "workspace/didChangeWatchedFiles".to_string(), + register_options: Some(serde_json::to_value(registration_options).unwrap()), + }; + let registration_req = request_new::( + RequestId::from(1), + RegistrationParams { + registrations: vec![registration], + }, + ); + connection.sender.send(registration_req.into()).unwrap(); +} + pub fn run_server(connection: &Connection) -> Result<()> { let init_params = initialize_server(connection)?; let (_, config) = parse_initialize_params(init_params)?; log::info!("Initialization is finished"); + register_for_file_changes(connection); + let mut global_state = initialize_new_global_state(config); - dbg!(&global_state.config()); main_loop::main_loop(&mut global_state, connection) }