From f3ba4159ad9c639c40fe6230564a8ca6d78b3643 Mon Sep 17 00:00:00 2001 From: Rich Chiodo false Date: Fri, 26 Sep 2025 15:17:35 -0700 Subject: [PATCH 01/11] Add support for snapshot request --- pyrefly/lib/state/epoch.rs | 5 + pyrefly/lib/state/state.rs | 5 + .../test/tsp/tsp_interaction/get_snapshot.rs | 102 ++++++++++++++++++ pyrefly/lib/test/tsp/tsp_interaction/mod.rs | 1 + .../test/tsp/tsp_interaction/object_model.rs | 9 ++ pyrefly/lib/tsp/requests/get_snapshot.rs | 25 +++++ pyrefly/lib/tsp/requests/mod.rs | 1 + pyrefly/lib/tsp/server.rs | 30 +++++- 8 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 pyrefly/lib/test/tsp/tsp_interaction/get_snapshot.rs create mode 100644 pyrefly/lib/tsp/requests/get_snapshot.rs diff --git a/pyrefly/lib/state/epoch.rs b/pyrefly/lib/state/epoch.rs index b6d212e81..92a9bb342 100644 --- a/pyrefly/lib/state/epoch.rs +++ b/pyrefly/lib/state/epoch.rs @@ -18,6 +18,11 @@ impl Epoch { pub fn next(&mut self) { self.0 += 1; } + + /// Get the epoch value as an integer + pub fn as_u32(self) -> u32 { + self.0 + } } /// Invariant: checked >= computed >= changed diff --git a/pyrefly/lib/state/state.rs b/pyrefly/lib/state/state.rs index db00fd640..710896089 100644 --- a/pyrefly/lib/state/state.rs +++ b/pyrefly/lib/state/state.rs @@ -295,6 +295,11 @@ impl<'a> Transaction<'a> { self.data.subscriber = subscriber; } + /// Get the current epoch of this transaction + pub fn current_epoch(&self) -> Epoch { + self.data.now + } + pub fn get_solutions(&self, handle: &Handle) -> Option> { self.with_module_inner(handle, |x| x.steps.solutions.dupe()) } diff --git a/pyrefly/lib/test/tsp/tsp_interaction/get_snapshot.rs b/pyrefly/lib/test/tsp/tsp_interaction/get_snapshot.rs new file mode 100644 index 000000000..ac8e4ab5e --- /dev/null +++ b/pyrefly/lib/test/tsp/tsp_interaction/get_snapshot.rs @@ -0,0 +1,102 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +//! Tests for TSP getSnapshot request + +use lsp_server::RequestId; +use lsp_server::Response; +use tempfile::TempDir; + +use crate::test::tsp::tsp_interaction::object_model::TspInteraction; + +#[test] +fn test_tsp_get_snapshot() { + // Test retrieval of TSP snapshot version + let temp_dir = TempDir::new().unwrap(); + let test_file_path = temp_dir.path().join("test.py"); + + let test_content = r#"# Simple test file +print("Hello, World!") +"#; + + std::fs::write(&test_file_path, test_content).unwrap(); + + let mut tsp = TspInteraction::new(); + tsp.set_root(temp_dir.path().to_path_buf()); + tsp.initialize(Default::default()); + + // Open the test file + tsp.server.did_open("test.py"); + + // Wait for any diagnostics/RecheckFinished events + tsp.client.expect_any_message(); + + // Get snapshot + tsp.server.get_snapshot(); + + // Expect snapshot response with integer + tsp.client.expect_response(Response { + id: RequestId::from(2), + result: Some(serde_json::json!(0)), // Should start at epoch 0 + error: None, + }); + + tsp.shutdown(); +} + +#[test] +fn test_tsp_snapshot_updates_on_file_change() { + // Test that snapshot increments when files change + let temp_dir = TempDir::new().unwrap(); + let test_file_path = temp_dir.path().join("changing_test.py"); + + let initial_content = r#"# Initial content +x = 1 +"#; + + std::fs::write(&test_file_path, initial_content).unwrap(); + + let mut tsp = TspInteraction::new(); + tsp.set_root(temp_dir.path().to_path_buf()); + tsp.initialize(Default::default()); + + // Open the test file + tsp.server.did_open("changing_test.py"); + + // Wait for any diagnostics/RecheckFinished events + tsp.client.expect_any_message(); + + // Get initial snapshot + tsp.server.get_snapshot(); + + // Expect first snapshot response + tsp.client.expect_response(Response { + id: RequestId::from(2), + result: Some(serde_json::json!(0)), // Should be 0 or higher + error: None, + }); + + // Modify the file to trigger a state change + let updated_content = r#"# Updated content +x = 2 +y = "hello" +"#; + + std::fs::write(&test_file_path, updated_content).unwrap(); + + // Get updated snapshot - snapshot tracking works automatically via RecheckFinished + tsp.server.get_snapshot(); + + // Expect second snapshot response (value may be same since file watching is complex) + tsp.client.expect_response(Response { + id: RequestId::from(3), + result: Some(serde_json::json!(0)), // May still be 0 without proper file watching + error: None, + }); + + tsp.shutdown(); +} \ No newline at end of file diff --git a/pyrefly/lib/test/tsp/tsp_interaction/mod.rs b/pyrefly/lib/test/tsp/tsp_interaction/mod.rs index fbeb421e2..83ed2d710 100644 --- a/pyrefly/lib/test/tsp/tsp_interaction/mod.rs +++ b/pyrefly/lib/test/tsp/tsp_interaction/mod.rs @@ -8,4 +8,5 @@ //! Tests for TSP (Type Server Protocol) request handlers pub mod get_supported_protocol_version; +pub mod get_snapshot; pub mod object_model; diff --git a/pyrefly/lib/test/tsp/tsp_interaction/object_model.rs b/pyrefly/lib/test/tsp/tsp_interaction/object_model.rs index 4d9e038be..f558c52e7 100644 --- a/pyrefly/lib/test/tsp/tsp_interaction/object_model.rs +++ b/pyrefly/lib/test/tsp/tsp_interaction/object_model.rs @@ -119,6 +119,15 @@ impl TestTspServer { })); } + pub fn get_snapshot(&mut self) { + let id = self.next_request_id(); + self.send_message(Message::Request(Request { + id, + method: "typeServer/getSnapshot".to_owned(), + params: serde_json::json!(null), + })); + } + pub fn did_open(&self, file: &'static str) { let path = self.get_root_or_panic().join(file); self.send_message(Message::Notification(Notification { diff --git a/pyrefly/lib/tsp/requests/get_snapshot.rs b/pyrefly/lib/tsp/requests/get_snapshot.rs new file mode 100644 index 000000000..e57f38b82 --- /dev/null +++ b/pyrefly/lib/tsp/requests/get_snapshot.rs @@ -0,0 +1,25 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +//! Implementation of the getSnapshot TSP request + +use crate::tsp::server::TspServer; + +impl TspServer { + /// Get the current snapshot version + /// + /// The snapshot represents the current epoch of the global state. + /// It changes whenever files are modified, configuration changes, + /// or any other event that would trigger a recomputation. + pub fn get_snapshot(&self) -> i32 { + *self.current_snapshot.lock().unwrap_or_else(|poisoned| { + // In case of poisoned mutex, recover and return the value + eprintln!("TSP: Warning - snapshot mutex was poisoned, recovering"); + poisoned.into_inner() + }) + } +} \ No newline at end of file diff --git a/pyrefly/lib/tsp/requests/mod.rs b/pyrefly/lib/tsp/requests/mod.rs index be221560b..46982fefa 100644 --- a/pyrefly/lib/tsp/requests/mod.rs +++ b/pyrefly/lib/tsp/requests/mod.rs @@ -8,3 +8,4 @@ //! TSP request implementations pub mod get_supported_protocol_version; +pub mod get_snapshot; diff --git a/pyrefly/lib/tsp/server.rs b/pyrefly/lib/tsp/server.rs index b74062d60..d30855bd3 100644 --- a/pyrefly/lib/tsp/server.rs +++ b/pyrefly/lib/tsp/server.rs @@ -7,6 +7,7 @@ use std::collections::HashSet; use std::sync::Arc; +use std::sync::Mutex; use dupe::Dupe; use lsp_server::Connection; @@ -29,11 +30,16 @@ use crate::lsp::transaction_manager::TransactionManager; /// TSP server that delegates to LSP server infrastructure while handling only TSP requests pub struct TspServer { pub inner: Box, + /// Current snapshot version, updated on RecheckFinished events + pub(crate) current_snapshot: Arc>, } impl TspServer { pub fn new(lsp_server: Box) -> Self { - Self { inner: lsp_server } + Self { + inner: lsp_server, + current_snapshot: Arc::new(Mutex::new(0)), // Initialize with epoch 0 + } } pub fn process_event<'a>( @@ -43,6 +49,20 @@ impl TspServer { subsequent_mutation: bool, event: LspEvent, ) -> anyhow::Result { + // Handle TSP-specific logic for RecheckFinished to track snapshot updates + if let LspEvent::RecheckFinished = event { + // Update our snapshot when global state changes + let transaction = ide_transaction_manager.non_committable_transaction(self.inner.state()); + let new_snapshot = transaction.current_epoch().as_u32() as i32; + if let Ok(mut current) = self.current_snapshot.lock() { + if *current != new_snapshot { + *current = new_snapshot; + eprintln!("TSP: Updated snapshot to {}", new_snapshot); + } + } + // Continue to let the inner server handle RecheckFinished as well + } + // For TSP requests, handle them specially if let LspEvent::LspRequest(ref request) = event { if self.handle_tsp_request(ide_transaction_manager, request)? { @@ -94,6 +114,14 @@ impl TspServer { ide_transaction_manager.save(transaction); Ok(true) } + TSPRequests::GetSnapshotRequest { .. } => { + // Get snapshot doesn't need a transaction since it just returns the cached value + self.inner.send_response(new_response( + request.id.clone(), + Ok(self.get_snapshot()), + )); + Ok(true) + } _ => { // Other TSP requests not yet implemented Ok(false) From 691c6551aeaca70ae4cb5026f7a96ceef1a6f863 Mon Sep 17 00:00:00 2001 From: Rich Chiodo false Date: Fri, 26 Sep 2025 15:22:08 -0700 Subject: [PATCH 02/11] Fix formatting --- .../lib/test/tsp/tsp_interaction/get_snapshot.rs | 14 +++++++------- pyrefly/lib/test/tsp/tsp_interaction/mod.rs | 2 +- pyrefly/lib/tsp/requests/get_snapshot.rs | 4 ++-- pyrefly/lib/tsp/requests/mod.rs | 2 +- pyrefly/lib/tsp/server.rs | 9 ++++----- 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/pyrefly/lib/test/tsp/tsp_interaction/get_snapshot.rs b/pyrefly/lib/test/tsp/tsp_interaction/get_snapshot.rs index ac8e4ab5e..6411da3d4 100644 --- a/pyrefly/lib/test/tsp/tsp_interaction/get_snapshot.rs +++ b/pyrefly/lib/test/tsp/tsp_interaction/get_snapshot.rs @@ -72,31 +72,31 @@ x = 1 // Get initial snapshot tsp.server.get_snapshot(); - + // Expect first snapshot response tsp.client.expect_response(Response { id: RequestId::from(2), result: Some(serde_json::json!(0)), // Should be 0 or higher error: None, }); - + // Modify the file to trigger a state change let updated_content = r#"# Updated content x = 2 y = "hello" "#; - + std::fs::write(&test_file_path, updated_content).unwrap(); - + // Get updated snapshot - snapshot tracking works automatically via RecheckFinished tsp.server.get_snapshot(); - + // Expect second snapshot response (value may be same since file watching is complex) tsp.client.expect_response(Response { - id: RequestId::from(3), + id: RequestId::from(3), result: Some(serde_json::json!(0)), // May still be 0 without proper file watching error: None, }); tsp.shutdown(); -} \ No newline at end of file +} diff --git a/pyrefly/lib/test/tsp/tsp_interaction/mod.rs b/pyrefly/lib/test/tsp/tsp_interaction/mod.rs index 83ed2d710..f168b22dd 100644 --- a/pyrefly/lib/test/tsp/tsp_interaction/mod.rs +++ b/pyrefly/lib/test/tsp/tsp_interaction/mod.rs @@ -7,6 +7,6 @@ //! Tests for TSP (Type Server Protocol) request handlers -pub mod get_supported_protocol_version; pub mod get_snapshot; +pub mod get_supported_protocol_version; pub mod object_model; diff --git a/pyrefly/lib/tsp/requests/get_snapshot.rs b/pyrefly/lib/tsp/requests/get_snapshot.rs index e57f38b82..b5098c377 100644 --- a/pyrefly/lib/tsp/requests/get_snapshot.rs +++ b/pyrefly/lib/tsp/requests/get_snapshot.rs @@ -11,7 +11,7 @@ use crate::tsp::server::TspServer; impl TspServer { /// Get the current snapshot version - /// + /// /// The snapshot represents the current epoch of the global state. /// It changes whenever files are modified, configuration changes, /// or any other event that would trigger a recomputation. @@ -22,4 +22,4 @@ impl TspServer { poisoned.into_inner() }) } -} \ No newline at end of file +} diff --git a/pyrefly/lib/tsp/requests/mod.rs b/pyrefly/lib/tsp/requests/mod.rs index 46982fefa..32f8e9395 100644 --- a/pyrefly/lib/tsp/requests/mod.rs +++ b/pyrefly/lib/tsp/requests/mod.rs @@ -7,5 +7,5 @@ //! TSP request implementations -pub mod get_supported_protocol_version; pub mod get_snapshot; +pub mod get_supported_protocol_version; diff --git a/pyrefly/lib/tsp/server.rs b/pyrefly/lib/tsp/server.rs index d30855bd3..5d19f8d55 100644 --- a/pyrefly/lib/tsp/server.rs +++ b/pyrefly/lib/tsp/server.rs @@ -52,7 +52,8 @@ impl TspServer { // Handle TSP-specific logic for RecheckFinished to track snapshot updates if let LspEvent::RecheckFinished = event { // Update our snapshot when global state changes - let transaction = ide_transaction_manager.non_committable_transaction(self.inner.state()); + let transaction = + ide_transaction_manager.non_committable_transaction(self.inner.state()); let new_snapshot = transaction.current_epoch().as_u32() as i32; if let Ok(mut current) = self.current_snapshot.lock() { if *current != new_snapshot { @@ -116,10 +117,8 @@ impl TspServer { } TSPRequests::GetSnapshotRequest { .. } => { // Get snapshot doesn't need a transaction since it just returns the cached value - self.inner.send_response(new_response( - request.id.clone(), - Ok(self.get_snapshot()), - )); + self.inner + .send_response(new_response(request.id.clone(), Ok(self.get_snapshot()))); Ok(true) } _ => { From 93facc5ca7b5357a34a2ce7e7d6c579abaa64e99 Mon Sep 17 00:00:00 2001 From: Rich Chiodo false Date: Fri, 26 Sep 2025 15:40:44 -0700 Subject: [PATCH 03/11] Fix clippy errors --- pyrefly/lib/tsp/server.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyrefly/lib/tsp/server.rs b/pyrefly/lib/tsp/server.rs index 5d19f8d55..570a0bc39 100644 --- a/pyrefly/lib/tsp/server.rs +++ b/pyrefly/lib/tsp/server.rs @@ -55,11 +55,11 @@ impl TspServer { let transaction = ide_transaction_manager.non_committable_transaction(self.inner.state()); let new_snapshot = transaction.current_epoch().as_u32() as i32; - if let Ok(mut current) = self.current_snapshot.lock() { - if *current != new_snapshot { - *current = new_snapshot; - eprintln!("TSP: Updated snapshot to {}", new_snapshot); - } + if let Ok(mut current) = self.current_snapshot.lock() + && *current != new_snapshot + { + *current = new_snapshot; + eprintln!("TSP: Updated snapshot to {new_snapshot}"); } // Continue to let the inner server handle RecheckFinished as well } From 834c5be86b78ff3b6e0384f7fd3fb4dbaa248080 Mon Sep 17 00:00:00 2001 From: Rich Chiodo false Date: Thu, 2 Oct 2025 09:55:18 -0700 Subject: [PATCH 04/11] Don't use epoch anymore and add test for doc change --- pyrefly/lib/state/epoch.rs | 5 -- pyrefly/lib/state/state.rs | 5 -- .../test/tsp/tsp_interaction/get_snapshot.rs | 57 ++++++++++++++++++- .../test/tsp/tsp_interaction/object_model.rs | 31 ++++++++++ pyrefly/lib/tsp/server.rs | 10 +--- 5 files changed, 90 insertions(+), 18 deletions(-) diff --git a/pyrefly/lib/state/epoch.rs b/pyrefly/lib/state/epoch.rs index 92a9bb342..b6d212e81 100644 --- a/pyrefly/lib/state/epoch.rs +++ b/pyrefly/lib/state/epoch.rs @@ -18,11 +18,6 @@ impl Epoch { pub fn next(&mut self) { self.0 += 1; } - - /// Get the epoch value as an integer - pub fn as_u32(self) -> u32 { - self.0 - } } /// Invariant: checked >= computed >= changed diff --git a/pyrefly/lib/state/state.rs b/pyrefly/lib/state/state.rs index 710896089..db00fd640 100644 --- a/pyrefly/lib/state/state.rs +++ b/pyrefly/lib/state/state.rs @@ -295,11 +295,6 @@ impl<'a> Transaction<'a> { self.data.subscriber = subscriber; } - /// Get the current epoch of this transaction - pub fn current_epoch(&self) -> Epoch { - self.data.now - } - pub fn get_solutions(&self, handle: &Handle) -> Option> { self.with_module_inner(handle, |x| x.steps.solutions.dupe()) } diff --git a/pyrefly/lib/test/tsp/tsp_interaction/get_snapshot.rs b/pyrefly/lib/test/tsp/tsp_interaction/get_snapshot.rs index 6411da3d4..27ce7a518 100644 --- a/pyrefly/lib/test/tsp/tsp_interaction/get_snapshot.rs +++ b/pyrefly/lib/test/tsp/tsp_interaction/get_snapshot.rs @@ -91,10 +91,65 @@ y = "hello" // Get updated snapshot - snapshot tracking works automatically via RecheckFinished tsp.server.get_snapshot(); + // Expect second snapshot response (value may be same or different) + tsp.client.expect_response(Response { + id: RequestId::from(3), + result: Some(serde_json::json!(3)), + error: None, + }); + + tsp.shutdown(); +} + +#[test] +fn test_tsp_snapshot_updates_on_did_change() { + // Test that didChange events cause snapshot to update + let temp_dir = TempDir::new().unwrap(); + let test_file_path = temp_dir.path().join("change_test.py"); + + let initial_content = r#"# Initial content +x = 1 +"#; + + std::fs::write(&test_file_path, initial_content).unwrap(); + + let mut tsp = TspInteraction::new(); + tsp.set_root(temp_dir.path().to_path_buf()); + tsp.initialize(Default::default()); + + // Open the test file + tsp.server.did_open("change_test.py"); + + // Wait for any diagnostics/RecheckFinished events from opening + tsp.client.expect_any_message(); + + // Get initial snapshot + tsp.server.get_snapshot(); + + // Expect second snapshot response (value may be same since file watching is complex) + tsp.client.expect_response(Response { + id: RequestId::from(3), + result: Some(serde_json::json!(1)), + error: None, + }); + + // Send a didChange notification with updated content + let changed_content = r#"# Changed content +x = 2 +y = 'updated' +"#; + tsp.server.did_change("change_test.py", changed_content, 2); + + // Wait for any RecheckFinished events triggered by the change + tsp.client.expect_any_message(); + + // Get updated snapshot - should potentially be incremented due to RecheckFinished + tsp.server.get_snapshot(); + // Expect second snapshot response (value may be same since file watching is complex) tsp.client.expect_response(Response { id: RequestId::from(3), - result: Some(serde_json::json!(0)), // May still be 0 without proper file watching + result: Some(serde_json::json!(2)), error: None, }); diff --git a/pyrefly/lib/test/tsp/tsp_interaction/object_model.rs b/pyrefly/lib/test/tsp/tsp_interaction/object_model.rs index f558c52e7..c2774a269 100644 --- a/pyrefly/lib/test/tsp/tsp_interaction/object_model.rs +++ b/pyrefly/lib/test/tsp/tsp_interaction/object_model.rs @@ -143,6 +143,22 @@ impl TestTspServer { })); } + pub fn did_change(&self, file: &'static str, content: &str, version: i32) { + let path = self.get_root_or_panic().join(file); + self.send_message(Message::Notification(Notification { + method: "textDocument/didChange".to_owned(), + params: serde_json::json!({ + "textDocument": { + "uri": Url::from_file_path(&path).unwrap().to_string(), + "version": version + }, + "contentChanges": [{ + "text": content + }] + }), + })); + } + pub fn get_initialize_params(&self, settings: &InitializeSettings) -> Value { let mut params: Value = serde_json::json!({ "rootPath": "/", @@ -260,6 +276,21 @@ impl TestTspClient { } } } + + pub fn receive_any_message(&self) -> Message { + match self.receiver.recv_timeout(self.timeout) { + Ok(msg) => { + eprintln!("client<---server {}", serde_json::to_string(&msg).unwrap()); + msg + } + Err(RecvTimeoutError::Timeout) => { + panic!("Timeout waiting for response"); + } + Err(RecvTimeoutError::Disconnected) => { + panic!("Channel disconnected"); + } + } + } } pub struct TspInteraction { diff --git a/pyrefly/lib/tsp/server.rs b/pyrefly/lib/tsp/server.rs index 570a0bc39..a5c4564c4 100644 --- a/pyrefly/lib/tsp/server.rs +++ b/pyrefly/lib/tsp/server.rs @@ -38,7 +38,7 @@ impl TspServer { pub fn new(lsp_server: Box) -> Self { Self { inner: lsp_server, - current_snapshot: Arc::new(Mutex::new(0)), // Initialize with epoch 0 + current_snapshot: Arc::new(Mutex::new(-1)), // -1 indicates invalid. } } @@ -52,14 +52,10 @@ impl TspServer { // Handle TSP-specific logic for RecheckFinished to track snapshot updates if let LspEvent::RecheckFinished = event { // Update our snapshot when global state changes - let transaction = - ide_transaction_manager.non_committable_transaction(self.inner.state()); - let new_snapshot = transaction.current_epoch().as_u32() as i32; if let Ok(mut current) = self.current_snapshot.lock() - && *current != new_snapshot { - *current = new_snapshot; - eprintln!("TSP: Updated snapshot to {new_snapshot}"); + *current = *current + 1; + eprintln!("TSP: Updated snapshot to {*current}"); } // Continue to let the inner server handle RecheckFinished as well } From 760d8711df7675ce40f0942c2ee2cef686f22ae5 Mon Sep 17 00:00:00 2001 From: Rich Chiodo false Date: Thu, 2 Oct 2025 10:58:59 -0700 Subject: [PATCH 05/11] Add didChangewatchedFiles and handle didChange event too --- pyrefly/lib/commands/tsp.rs | 4 +- .../test/tsp/tsp_interaction/get_snapshot.rs | 66 +++++++++++++++---- .../test/tsp/tsp_interaction/object_model.rs | 21 +++++- pyrefly/lib/tsp/server.rs | 39 +++++++---- 4 files changed, 100 insertions(+), 30 deletions(-) diff --git a/pyrefly/lib/commands/tsp.rs b/pyrefly/lib/commands/tsp.rs index 1ef0f9a74..5330bdd54 100644 --- a/pyrefly/lib/commands/tsp.rs +++ b/pyrefly/lib/commands/tsp.rs @@ -44,14 +44,14 @@ pub fn run_tsp(connection: Arc, args: TspArgs) -> anyhow::Result<()> let lsp_queue = LspQueue::new(); let lsp_server = Box::new(crate::lsp::server::Server::new( connection.dupe(), - lsp_queue, + lsp_queue.dupe(), initialization_params.clone(), args.indexing_mode, args.workspace_indexing_limit, )); // Reuse the existing lsp_loop but with TSP initialization - tsp_loop(lsp_server, connection, initialization_params)?; + tsp_loop(lsp_server, connection, initialization_params, lsp_queue)?; Ok(()) } diff --git a/pyrefly/lib/test/tsp/tsp_interaction/get_snapshot.rs b/pyrefly/lib/test/tsp/tsp_interaction/get_snapshot.rs index 27ce7a518..ddf2352b2 100644 --- a/pyrefly/lib/test/tsp/tsp_interaction/get_snapshot.rs +++ b/pyrefly/lib/test/tsp/tsp_interaction/get_snapshot.rs @@ -25,6 +25,17 @@ print("Hello, World!") std::fs::write(&test_file_path, test_content).unwrap(); + // Create a pyproject.toml to make this a recognized Python project + let pyproject_content = r#"[build-system] +requires = ["setuptools>=45", "setuptools-scm[toml]>=6.2"] +build-backend = "setuptools.build_meta" + +[project] +name = "test-project" +version = "1.0.0" +"#; + std::fs::write(temp_dir.path().join("pyproject.toml"), pyproject_content).unwrap(); + let mut tsp = TspInteraction::new(); tsp.set_root(temp_dir.path().to_path_buf()); tsp.initialize(Default::default()); @@ -38,10 +49,10 @@ print("Hello, World!") // Get snapshot tsp.server.get_snapshot(); - // Expect snapshot response with integer + // Expect snapshot response with integer (should increment after RecheckFinished from indexing) tsp.client.expect_response(Response { id: RequestId::from(2), - result: Some(serde_json::json!(0)), // Should start at epoch 0 + result: Some(serde_json::json!(2)), // Should be 2 after project + workspace RecheckFinished events error: None, }); @@ -50,7 +61,9 @@ print("Hello, World!") #[test] fn test_tsp_snapshot_updates_on_file_change() { - // Test that snapshot increments when files change + // Test that DidChangeWatchedFiles events are properly received and processed + // Note: In test environment, async recheck tasks don't execute, so snapshot doesn't increment + // but we verify that the mechanism is correctly triggered let temp_dir = TempDir::new().unwrap(); let test_file_path = temp_dir.path().join("changing_test.py"); @@ -60,6 +73,17 @@ x = 1 std::fs::write(&test_file_path, initial_content).unwrap(); + // Create a pyproject.toml to make this a recognized Python project + let pyproject_content = r#"[build-system] +requires = ["setuptools>=45", "setuptools-scm[toml]>=6.2"] +build-backend = "setuptools.build_meta" + +[project] +name = "test-project" +version = "1.0.0" +"#; + std::fs::write(temp_dir.path().join("pyproject.toml"), pyproject_content).unwrap(); + let mut tsp = TspInteraction::new(); tsp.set_root(temp_dir.path().to_path_buf()); tsp.initialize(Default::default()); @@ -76,7 +100,7 @@ x = 1 // Expect first snapshot response tsp.client.expect_response(Response { id: RequestId::from(2), - result: Some(serde_json::json!(0)), // Should be 0 or higher + result: Some(serde_json::json!(2)), // Should be 2 after project + workspace RecheckFinished events error: None, }); @@ -88,13 +112,18 @@ y = "hello" std::fs::write(&test_file_path, updated_content).unwrap(); - // Get updated snapshot - snapshot tracking works automatically via RecheckFinished + // Simulate the LSP DidChangeWatchedFiles notification for the file change + tsp.server.did_change_watched_files("changing_test.py", "changed"); + + // Get snapshot immediately after DidChangeWatchedFiles + // Note: In test environment, async recheck tasks don't execute, so snapshot remains at 2 + // In real environment, this would trigger invalidate() -> async task -> RecheckFinished -> snapshot increment tsp.server.get_snapshot(); - // Expect second snapshot response (value may be same or different) + // Expect snapshot to remain at 2 in test environment (async task doesn't execute) tsp.client.expect_response(Response { id: RequestId::from(3), - result: Some(serde_json::json!(3)), + result: Some(serde_json::json!(2)), // Remains 2 because async recheck doesn't execute in tests error: None, }); @@ -113,6 +142,17 @@ x = 1 std::fs::write(&test_file_path, initial_content).unwrap(); + // Create a pyproject.toml to make this a recognized Python project + let pyproject_content = r#"[build-system] +requires = ["setuptools>=45", "setuptools-scm[toml]>=6.2"] +build-backend = "setuptools.build_meta" + +[project] +name = "test-project" +version = "1.0.0" +"#; + std::fs::write(temp_dir.path().join("pyproject.toml"), pyproject_content).unwrap(); + let mut tsp = TspInteraction::new(); tsp.set_root(temp_dir.path().to_path_buf()); tsp.initialize(Default::default()); @@ -126,10 +166,10 @@ x = 1 // Get initial snapshot tsp.server.get_snapshot(); - // Expect second snapshot response (value may be same since file watching is complex) + // Expect first snapshot response (2 RecheckFinished: project + workspace indexing) tsp.client.expect_response(Response { - id: RequestId::from(3), - result: Some(serde_json::json!(1)), + id: RequestId::from(2), + result: Some(serde_json::json!(2)), // Should be 2 after project + workspace RecheckFinished events error: None, }); @@ -143,13 +183,13 @@ y = 'updated' // Wait for any RecheckFinished events triggered by the change tsp.client.expect_any_message(); - // Get updated snapshot - should potentially be incremented due to RecheckFinished + // Get updated snapshot tsp.server.get_snapshot(); - // Expect second snapshot response (value may be same since file watching is complex) + // Expect second snapshot response - should be incremented due to didChange tsp.client.expect_response(Response { id: RequestId::from(3), - result: Some(serde_json::json!(2)), + result: Some(serde_json::json!(3)), // Should be 3 after DidChangeTextDocument increment error: None, }); diff --git a/pyrefly/lib/test/tsp/tsp_interaction/object_model.rs b/pyrefly/lib/test/tsp/tsp_interaction/object_model.rs index c2774a269..35277e3f5 100644 --- a/pyrefly/lib/test/tsp/tsp_interaction/object_model.rs +++ b/pyrefly/lib/test/tsp/tsp_interaction/object_model.rs @@ -159,6 +159,25 @@ impl TestTspServer { })); } + pub fn did_change_watched_files(&self, file: &'static str, change_type: &str) { + let path = self.get_root_or_panic().join(file); + let file_change_type = match change_type { + "created" => 1, // FileChangeType::CREATED + "changed" => 2, // FileChangeType::CHANGED + "deleted" => 3, // FileChangeType::DELETED + _ => 2, // Default to changed + }; + self.send_message(Message::Notification(Notification { + method: "workspace/didChangeWatchedFiles".to_owned(), + params: serde_json::json!({ + "changes": [{ + "uri": Url::from_file_path(&path).unwrap().to_string(), + "type": file_change_type + }] + }), + })); + } + pub fn get_initialize_params(&self, settings: &InitializeSettings) -> Value { let mut params: Value = serde_json::json!({ "rootPath": "/", @@ -306,7 +325,7 @@ impl TspInteraction { let (language_server_sender, language_server_receiver) = bounded::(0); let args = TspArgs { - indexing_mode: IndexingMode::None, + indexing_mode: IndexingMode::LazyBlocking, workspace_indexing_limit: 0, }; let connection = Connection { diff --git a/pyrefly/lib/tsp/server.rs b/pyrefly/lib/tsp/server.rs index a5c4564c4..6ed965b47 100644 --- a/pyrefly/lib/tsp/server.rs +++ b/pyrefly/lib/tsp/server.rs @@ -38,7 +38,7 @@ impl TspServer { pub fn new(lsp_server: Box) -> Self { Self { inner: lsp_server, - current_snapshot: Arc::new(Mutex::new(-1)), // -1 indicates invalid. + current_snapshot: Arc::new(Mutex::new(0)), // Start at 0, increments on RecheckFinished } } @@ -49,16 +49,18 @@ impl TspServer { subsequent_mutation: bool, event: LspEvent, ) -> anyhow::Result { - // Handle TSP-specific logic for RecheckFinished to track snapshot updates - if let LspEvent::RecheckFinished = event { - // Update our snapshot when global state changes - if let Ok(mut current) = self.current_snapshot.lock() - { - *current = *current + 1; - eprintln!("TSP: Updated snapshot to {*current}"); - } - // Continue to let the inner server handle RecheckFinished as well - } + + // Remember if this event should increment the snapshot after processing + let should_increment_snapshot = match &event { + LspEvent::RecheckFinished => true, + // Increment on DidChange since it affects type checker state via synchronous validation + LspEvent::DidChangeTextDocument(_) => true, + // Don't increment on DidChangeWatchedFiles directly since it triggers RecheckFinished + // LspEvent::DidChangeWatchedFiles(_) => true, + // Don't increment on DidOpen since it triggers RecheckFinished events that will increment + // LspEvent::DidOpenTextDocument(_) => true, + _ => false, + }; // For TSP requests, handle them specially if let LspEvent::LspRequest(ref request) = event { @@ -75,12 +77,21 @@ impl TspServer { } // For all other events (notifications, responses, etc.), delegate to inner server - self.inner.process_event( + let result = self.inner.process_event( ide_transaction_manager, canceled_requests, subsequent_mutation, event, - ) + )?; + + // Increment snapshot after the inner server has processed the event + if should_increment_snapshot { + if let Ok(mut current) = self.current_snapshot.lock() { + *current += 1; + } + } + + Ok(result) } fn handle_tsp_request<'a>( @@ -129,10 +140,10 @@ pub fn tsp_loop( lsp_server: Box, connection: Arc, _initialization_params: InitializeParams, + lsp_queue: LspQueue, ) -> anyhow::Result<()> { eprintln!("Reading TSP messages"); let connection_for_dispatcher = connection.dupe(); - let lsp_queue = LspQueue::new(); let server = TspServer::new(lsp_server); From a7e77006d2ecc3b37a921d006382de6a328cc961 Mon Sep 17 00:00:00 2001 From: Rich Chiodo false Date: Thu, 2 Oct 2025 11:02:35 -0700 Subject: [PATCH 06/11] Eliminate double snapshot increment --- .../lib/test/tsp/tsp_interaction/get_snapshot.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pyrefly/lib/test/tsp/tsp_interaction/get_snapshot.rs b/pyrefly/lib/test/tsp/tsp_interaction/get_snapshot.rs index ddf2352b2..c965cac93 100644 --- a/pyrefly/lib/test/tsp/tsp_interaction/get_snapshot.rs +++ b/pyrefly/lib/test/tsp/tsp_interaction/get_snapshot.rs @@ -52,7 +52,7 @@ version = "1.0.0" // Expect snapshot response with integer (should increment after RecheckFinished from indexing) tsp.client.expect_response(Response { id: RequestId::from(2), - result: Some(serde_json::json!(2)), // Should be 2 after project + workspace RecheckFinished events + result: Some(serde_json::json!(1)), error: None, }); @@ -100,7 +100,7 @@ version = "1.0.0" // Expect first snapshot response tsp.client.expect_response(Response { id: RequestId::from(2), - result: Some(serde_json::json!(2)), // Should be 2 after project + workspace RecheckFinished events + result: Some(serde_json::json!(1)), error: None, }); @@ -120,10 +120,10 @@ y = "hello" // In real environment, this would trigger invalidate() -> async task -> RecheckFinished -> snapshot increment tsp.server.get_snapshot(); - // Expect snapshot to remain at 2 in test environment (async task doesn't execute) + // Expect snapshot to remain at 1 in test environment (async task doesn't execute) tsp.client.expect_response(Response { id: RequestId::from(3), - result: Some(serde_json::json!(2)), // Remains 2 because async recheck doesn't execute in tests + result: Some(serde_json::json!(1)), // Remains 1 because async recheck doesn't execute in tests error: None, }); @@ -166,10 +166,10 @@ version = "1.0.0" // Get initial snapshot tsp.server.get_snapshot(); - // Expect first snapshot response (2 RecheckFinished: project + workspace indexing) + // Expect first snapshot response tsp.client.expect_response(Response { id: RequestId::from(2), - result: Some(serde_json::json!(2)), // Should be 2 after project + workspace RecheckFinished events + result: Some(serde_json::json!(1)), error: None, }); @@ -189,7 +189,7 @@ y = 'updated' // Expect second snapshot response - should be incremented due to didChange tsp.client.expect_response(Response { id: RequestId::from(3), - result: Some(serde_json::json!(3)), // Should be 3 after DidChangeTextDocument increment + result: Some(serde_json::json!(2)), error: None, }); From ef74aae6ac3003ca64d3a239a137569cf2e33769 Mon Sep 17 00:00:00 2001 From: Rich Chiodo false Date: Thu, 2 Oct 2025 11:10:03 -0700 Subject: [PATCH 07/11] Support async events and write test to verify --- pyrefly/lib/lsp/server.rs | 7 +++++++ .../lib/test/tsp/tsp_interaction/get_snapshot.rs | 16 ++++++++-------- pyrefly/lib/tsp/server.rs | 6 ++++++ 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/pyrefly/lib/lsp/server.rs b/pyrefly/lib/lsp/server.rs index 32224fefc..481140d86 100644 --- a/pyrefly/lib/lsp/server.rs +++ b/pyrefly/lib/lsp/server.rs @@ -224,6 +224,9 @@ pub trait TspInterface { /// Get access to the state for creating transactions fn state(&self) -> &Arc; + /// Get access to the recheck queue for async task processing + fn recheck_queue(&self) -> &HeavyTaskQueue; + /// Process an LSP event and return the next step fn process_event<'a>( &'a self, @@ -2066,6 +2069,10 @@ impl TspInterface for Server { &self.state } + fn recheck_queue(&self) -> &HeavyTaskQueue { + &self.recheck_queue + } + fn process_event<'a>( &'a self, ide_transaction_manager: &mut TransactionManager<'a>, diff --git a/pyrefly/lib/test/tsp/tsp_interaction/get_snapshot.rs b/pyrefly/lib/test/tsp/tsp_interaction/get_snapshot.rs index c965cac93..e1b390305 100644 --- a/pyrefly/lib/test/tsp/tsp_interaction/get_snapshot.rs +++ b/pyrefly/lib/test/tsp/tsp_interaction/get_snapshot.rs @@ -61,9 +61,8 @@ version = "1.0.0" #[test] fn test_tsp_snapshot_updates_on_file_change() { - // Test that DidChangeWatchedFiles events are properly received and processed - // Note: In test environment, async recheck tasks don't execute, so snapshot doesn't increment - // but we verify that the mechanism is correctly triggered + // Test that DidChangeWatchedFiles events trigger async recheck and update snapshots + // With the recheck queue thread running, async tasks should execute and generate RecheckFinished events let temp_dir = TempDir::new().unwrap(); let test_file_path = temp_dir.path().join("changing_test.py"); @@ -115,15 +114,16 @@ y = "hello" // Simulate the LSP DidChangeWatchedFiles notification for the file change tsp.server.did_change_watched_files("changing_test.py", "changed"); - // Get snapshot immediately after DidChangeWatchedFiles - // Note: In test environment, async recheck tasks don't execute, so snapshot remains at 2 - // In real environment, this would trigger invalidate() -> async task -> RecheckFinished -> snapshot increment + // Wait for the async RecheckFinished event to be processed + tsp.client.expect_any_message(); + + // Get snapshot after async recheck completes tsp.server.get_snapshot(); - // Expect snapshot to remain at 1 in test environment (async task doesn't execute) + // Expect snapshot to be incremented to 2 after RecheckFinished from file change tsp.client.expect_response(Response { id: RequestId::from(3), - result: Some(serde_json::json!(1)), // Remains 1 because async recheck doesn't execute in tests + result: Some(serde_json::json!(2)), // Should be 2 after RecheckFinished from file change error: None, }); diff --git a/pyrefly/lib/tsp/server.rs b/pyrefly/lib/tsp/server.rs index 6ed965b47..a3b4658fa 100644 --- a/pyrefly/lib/tsp/server.rs +++ b/pyrefly/lib/tsp/server.rs @@ -147,6 +147,12 @@ pub fn tsp_loop( let server = TspServer::new(lsp_server); + // Start the recheck queue thread to process async tasks + let recheck_queue = server.inner.recheck_queue().dupe(); + std::thread::spawn(move || { + recheck_queue.run_until_stopped(); + }); + let lsp_queue2 = lsp_queue.dupe(); std::thread::spawn(move || { dispatch_lsp_events(&connection_for_dispatcher, lsp_queue2); From 9f4cfcf790ab85fde50ca7e49bd26abd8c51c93c Mon Sep 17 00:00:00 2001 From: Rich Chiodo false Date: Thu, 2 Oct 2025 11:13:30 -0700 Subject: [PATCH 08/11] Fix formatting --- pyrefly/lib/test/tsp/tsp_interaction/get_snapshot.rs | 9 +++++---- pyrefly/lib/test/tsp/tsp_interaction/object_model.rs | 4 ++-- pyrefly/lib/tsp/server.rs | 1 - 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pyrefly/lib/test/tsp/tsp_interaction/get_snapshot.rs b/pyrefly/lib/test/tsp/tsp_interaction/get_snapshot.rs index e1b390305..39adcee90 100644 --- a/pyrefly/lib/test/tsp/tsp_interaction/get_snapshot.rs +++ b/pyrefly/lib/test/tsp/tsp_interaction/get_snapshot.rs @@ -52,7 +52,7 @@ version = "1.0.0" // Expect snapshot response with integer (should increment after RecheckFinished from indexing) tsp.client.expect_response(Response { id: RequestId::from(2), - result: Some(serde_json::json!(1)), + result: Some(serde_json::json!(1)), error: None, }); @@ -99,7 +99,7 @@ version = "1.0.0" // Expect first snapshot response tsp.client.expect_response(Response { id: RequestId::from(2), - result: Some(serde_json::json!(1)), + result: Some(serde_json::json!(1)), error: None, }); @@ -112,7 +112,8 @@ y = "hello" std::fs::write(&test_file_path, updated_content).unwrap(); // Simulate the LSP DidChangeWatchedFiles notification for the file change - tsp.server.did_change_watched_files("changing_test.py", "changed"); + tsp.server + .did_change_watched_files("changing_test.py", "changed"); // Wait for the async RecheckFinished event to be processed tsp.client.expect_any_message(); @@ -185,7 +186,7 @@ y = 'updated' // Get updated snapshot tsp.server.get_snapshot(); - + // Expect second snapshot response - should be incremented due to didChange tsp.client.expect_response(Response { id: RequestId::from(3), diff --git a/pyrefly/lib/test/tsp/tsp_interaction/object_model.rs b/pyrefly/lib/test/tsp/tsp_interaction/object_model.rs index 35277e3f5..3a960b089 100644 --- a/pyrefly/lib/test/tsp/tsp_interaction/object_model.rs +++ b/pyrefly/lib/test/tsp/tsp_interaction/object_model.rs @@ -163,9 +163,9 @@ impl TestTspServer { let path = self.get_root_or_panic().join(file); let file_change_type = match change_type { "created" => 1, // FileChangeType::CREATED - "changed" => 2, // FileChangeType::CHANGED + "changed" => 2, // FileChangeType::CHANGED "deleted" => 3, // FileChangeType::DELETED - _ => 2, // Default to changed + _ => 2, // Default to changed }; self.send_message(Message::Notification(Notification { method: "workspace/didChangeWatchedFiles".to_owned(), diff --git a/pyrefly/lib/tsp/server.rs b/pyrefly/lib/tsp/server.rs index a3b4658fa..b974a2f1f 100644 --- a/pyrefly/lib/tsp/server.rs +++ b/pyrefly/lib/tsp/server.rs @@ -49,7 +49,6 @@ impl TspServer { subsequent_mutation: bool, event: LspEvent, ) -> anyhow::Result { - // Remember if this event should increment the snapshot after processing let should_increment_snapshot = match &event { LspEvent::RecheckFinished => true, From 73c5e7717cbab84655907707496fe84b8a3bf427 Mon Sep 17 00:00:00 2001 From: Rich Chiodo false Date: Thu, 2 Oct 2025 11:35:18 -0700 Subject: [PATCH 09/11] Fix clippy errors --- pyrefly/lib/test/lsp/lsp_interaction/configuration.rs | 2 -- pyrefly/lib/tsp/server.rs | 8 ++++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/pyrefly/lib/test/lsp/lsp_interaction/configuration.rs b/pyrefly/lib/test/lsp/lsp_interaction/configuration.rs index 8cec06cd9..050ba38d2 100644 --- a/pyrefly/lib/test/lsp/lsp_interaction/configuration.rs +++ b/pyrefly/lib/test/lsp/lsp_interaction/configuration.rs @@ -12,13 +12,11 @@ use std::os::unix::fs::PermissionsExt; use lsp_server::Message; use lsp_server::Notification; -use lsp_server::Request; use lsp_server::RequestId; use lsp_server::Response; use lsp_types::Url; use lsp_types::notification::DidChangeWorkspaceFolders; use lsp_types::notification::Notification as _; -use pyrefly_util::fs_anyhow::write; use crate::test::lsp::lsp_interaction::object_model::InitializeSettings; use crate::test::lsp::lsp_interaction::object_model::LspInteraction; diff --git a/pyrefly/lib/tsp/server.rs b/pyrefly/lib/tsp/server.rs index b974a2f1f..23ea8bc3e 100644 --- a/pyrefly/lib/tsp/server.rs +++ b/pyrefly/lib/tsp/server.rs @@ -84,10 +84,10 @@ impl TspServer { )?; // Increment snapshot after the inner server has processed the event - if should_increment_snapshot { - if let Ok(mut current) = self.current_snapshot.lock() { - *current += 1; - } + if should_increment_snapshot + && let Ok(mut current) = self.current_snapshot.lock() + { + *current += 1; } Ok(result) From 3c42cb7df9cf0316b4e94aeca9a28e8141452762 Mon Sep 17 00:00:00 2001 From: Rich Chiodo false Date: Thu, 2 Oct 2025 11:35:35 -0700 Subject: [PATCH 10/11] Fix formatting --- pyrefly/lib/tsp/server.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyrefly/lib/tsp/server.rs b/pyrefly/lib/tsp/server.rs index 23ea8bc3e..291bd15df 100644 --- a/pyrefly/lib/tsp/server.rs +++ b/pyrefly/lib/tsp/server.rs @@ -84,9 +84,7 @@ impl TspServer { )?; // Increment snapshot after the inner server has processed the event - if should_increment_snapshot - && let Ok(mut current) = self.current_snapshot.lock() - { + if should_increment_snapshot && let Ok(mut current) = self.current_snapshot.lock() { *current += 1; } From 173f0992faa8bcd7f4eb718f5a01c0d9f932d130 Mon Sep 17 00:00:00 2001 From: Rich Chiodo false Date: Thu, 2 Oct 2025 12:09:54 -0700 Subject: [PATCH 11/11] Put back configuration changes. --- pyrefly/lib/test/lsp/lsp_interaction/configuration.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyrefly/lib/test/lsp/lsp_interaction/configuration.rs b/pyrefly/lib/test/lsp/lsp_interaction/configuration.rs index 050ba38d2..8cec06cd9 100644 --- a/pyrefly/lib/test/lsp/lsp_interaction/configuration.rs +++ b/pyrefly/lib/test/lsp/lsp_interaction/configuration.rs @@ -12,11 +12,13 @@ use std::os::unix::fs::PermissionsExt; use lsp_server::Message; use lsp_server::Notification; +use lsp_server::Request; use lsp_server::RequestId; use lsp_server::Response; use lsp_types::Url; use lsp_types::notification::DidChangeWorkspaceFolders; use lsp_types::notification::Notification as _; +use pyrefly_util::fs_anyhow::write; use crate::test::lsp::lsp_interaction::object_model::InitializeSettings; use crate::test::lsp::lsp_interaction::object_model::LspInteraction;