Skip to content
5 changes: 5 additions & 0 deletions pyrefly/lib/state/epoch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions pyrefly/lib/state/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Arc<Solutions>> {
self.with_module_inner(handle, |x| x.steps.solutions.dupe())
}
Expand Down
102 changes: 102 additions & 0 deletions pyrefly/lib/test/tsp/tsp_interaction/get_snapshot.rs
Original file line number Diff line number Diff line change
@@ -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();
}
1 change: 1 addition & 0 deletions pyrefly/lib/test/tsp/tsp_interaction/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@

//! Tests for TSP (Type Server Protocol) request handlers

pub mod get_snapshot;
pub mod get_supported_protocol_version;
pub mod object_model;
9 changes: 9 additions & 0 deletions pyrefly/lib/test/tsp/tsp_interaction/object_model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
25 changes: 25 additions & 0 deletions pyrefly/lib/tsp/requests/get_snapshot.rs
Original file line number Diff line number Diff line change
@@ -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()
})
}
}
1 change: 1 addition & 0 deletions pyrefly/lib/tsp/requests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@

//! TSP request implementations

pub mod get_snapshot;
pub mod get_supported_protocol_version;
29 changes: 28 additions & 1 deletion pyrefly/lib/tsp/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

use std::collections::HashSet;
use std::sync::Arc;
use std::sync::Mutex;

use dupe::Dupe;
use lsp_server::Connection;
Expand All @@ -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<dyn TspInterface>,
/// Current snapshot version, updated on RecheckFinished events
pub(crate) current_snapshot: Arc<Mutex<i32>>,
}

impl TspServer {
pub fn new(lsp_server: Box<dyn TspInterface>) -> 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>(
Expand All @@ -43,6 +49,21 @@ impl TspServer {
subsequent_mutation: bool,
event: LspEvent,
) -> anyhow::Result<ProcessEvent> {
// 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}");
}
// 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)? {
Expand Down Expand Up @@ -94,6 +115,12 @@ 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)
Expand Down
Loading