Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/aft/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5442,7 +5442,7 @@ impl AppContext {
pub fn lsp_notify_file_changed(&self, file_path: &Path, content: &str) {
let config = self.config();
if let Some(mut lsp) = self.lsp_manager.try_lock() {
if let Err(e) = lsp.notify_file_changed(file_path, content, &config) {
if let Err(e) = lsp.notify_file_changed_if_running(file_path, content, &config) {
crate::slog_warn!("sync error for {}: {}", file_path.display(), e);
}
}
Expand Down
60 changes: 60 additions & 0 deletions crates/aft/src/lsp/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,23 @@ impl LspManager {
.successful
}

fn running_server_keys_for_file(&self, file_path: &Path, config: &Config) -> Vec<ServerKey> {
let mut keys = Vec::new();
for def in servers_for_file(file_path, config) {
let Some(root) = def.workspace_root_for_file(file_path) else {
continue;
};
let key = ServerKey {
kind: def.kind.clone(),
root,
};
if self.clients.contains_key(&key) {
keys.push(key);
}
}
keys
}

/// Detailed version of [`ensure_server_for_file`] that records every
/// matching server's outcome (`Ok` / `NoRootMarker` / `BinaryNotInstalled`
/// / `SpawnFailed`).
Expand Down Expand Up @@ -628,6 +645,32 @@ impl LspManager {
) -> Result<Vec<(ServerKey, i32)>, LspError> {
let canonical_path = canonicalize_for_lsp(file_path)?;
let server_keys = self.ensure_server_for_file(&canonical_path, config);
self.notify_file_changed_for_server_keys(canonical_path, content, server_keys)
}

/// Notify only LSP servers that are already running for this file.
///
/// Post-write notifications are best-effort and must not make a mutation
/// wait for cold server startup. Explicit LSP requests use
/// [`Self::notify_file_changed_versioned`] and retain lazy startup.
pub fn notify_file_changed_if_running(
&mut self,
file_path: &Path,
content: &str,
config: &Config,
) -> Result<(), LspError> {
let canonical_path = canonicalize_for_lsp(file_path)?;
let server_keys = self.running_server_keys_for_file(&canonical_path, config);
self.notify_file_changed_for_server_keys(canonical_path, content, server_keys)
.map(|_| ())
}

fn notify_file_changed_for_server_keys(
&mut self,
canonical_path: PathBuf,
content: &str,
server_keys: Vec<ServerKey>,
) -> Result<Vec<(ServerKey, i32)>, LspError> {
if server_keys.is_empty() {
return Ok(Vec::new());
}
Expand Down Expand Up @@ -2153,7 +2196,10 @@ mod failure_hint_tests {

#[cfg(test)]
mod diagnostic_capacity_tests {
use std::fs;

use super::LspManager;
use crate::config::Config;

// The lsp.diagnostic_cache_size config knob must actually take effect:
// set_diagnostic_capacity (called at AppContext construction with the config
Expand All @@ -2178,6 +2224,20 @@ mod diagnostic_capacity_tests {
assert_eq!(manager.clear_failed_spawns(), 1);
assert_eq!(manager.clear_failed_spawns(), 0);
}

#[test]
fn post_write_notification_does_not_start_a_cold_server() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("main.ts");
fs::write(dir.path().join("package.json"), "{}").unwrap();
fs::write(&file, "export const value = 1;\n").unwrap();

let mut manager = LspManager::new();
manager
.notify_file_changed_if_running(&file, "export const value = 1;\n", &Config::default())
.unwrap();
assert!(manager.clients.is_empty());
}
}

#[cfg(test)]
Expand Down
61 changes: 58 additions & 3 deletions crates/aft/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,8 +367,11 @@ fn drain_runtime_events_and_write_pending(
registry: &RuntimeRegistry,
pending: &mut PendingResponses,
) -> io::Result<usize> {
let ctx = registry.current();
let mut written = write_ready_pending(ctx, pending)?;
drain_runtime_events(registry);
write_ready_pending(registry.current(), pending)
written += write_ready_pending(ctx, pending)?;
Ok(written)
}

fn write_ready_pending(ctx: &AppContext, pending: &mut PendingResponses) -> io::Result<usize> {
Expand All @@ -381,8 +384,11 @@ fn drain_runtime_events_and_write_pending_to_writer(
pending: &mut PendingResponses,
writer: &mut impl Write,
) -> io::Result<usize> {
let ctx = registry.current();
let mut written = write_ready_pending_to_writer(ctx, pending, writer)?;
drain_runtime_events(registry);
write_ready_pending_to_writer(registry.current(), pending, writer)
written += write_ready_pending_to_writer(ctx, pending, writer)?;
Ok(written)
}

#[cfg(test)]
Expand Down Expand Up @@ -1278,11 +1284,60 @@ mod pending_response_tests {
0
);
assert_eq!(drain_frames.load(Ordering::SeqCst), 1);
assert_eq!(poll_calls.get(), 1);
assert_eq!(poll_calls.get(), 2);
assert!(writer.is_empty());
assert!(!pending.is_empty());
}

#[test]
fn tick_flushes_ready_pending_before_runtime_maintenance() {
let root = TempDir::new().unwrap();
let registry = make_runtime_registry(root.path());
let events = Arc::new(Mutex::new(Vec::new()));
let events_for_drain = Arc::clone(&events);
registry
.current()
.set_progress_sender(Some(Arc::new(Box::new(move |_| {
events_for_drain.lock().unwrap().push("drain");
}))));
let generation = registry.current().advance_configure_generation();
registry
.current()
.configure_warnings_sender()
.send((
generation,
ConfigureWarningsFrame {
frame_type: "configure_warnings",
session_id: Some("session-order".to_string()),
project_root: root.path().display().to_string(),
warnings: Vec::new(),
},
))
.unwrap();

let events_for_pending = Arc::clone(&events);
let mut pending = PendingResponses::default();
pending.register(PendingResponse {
request_id: "pending-order".to_string(),
session_id: "session-order".to_string(),
attach_command: "bash".to_string(),
poll: Box::new(move |_| {
events_for_pending.lock().unwrap().push("pending");
Some(Response::success("pending-order", serde_json::json!({})))
}),
});
let mut writer = Vec::new();

assert_eq!(
drain_runtime_events_and_write_pending_to_writer(&registry, &mut pending, &mut writer)
.unwrap(),
1
);
assert_eq!(*events.lock().unwrap(), ["pending", "drain"]);
assert_eq!(line_values(&writer).len(), 1);
assert!(pending.is_empty());
}

#[test]
fn long_pending_response_is_polled_each_tick_without_writing_until_ready() {
let root = TempDir::new().unwrap();
Expand Down