use std::collections::VecDeque;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use lsp_types::{
DidChangeTextDocumentParams, DidChangeWatchedFilesParams, DidCloseTextDocumentParams,
DidOpenTextDocumentParams, DidSaveTextDocumentParams,
};
use tokio::sync::{Mutex, Notify};
use crate::handlers;
use super::snapshot::{ServerContextInner, ServerContextSnapshot};
const IDLE_WAIT: Duration = Duration::from_secs(1);
#[derive(Debug)]
pub enum UpdateEvent {
Opened(DidOpenTextDocumentParams),
Changed(DidChangeTextDocumentParams),
Saved(DidSaveTextDocumentParams),
Closed(DidCloseTextDocumentParams),
WatchedFilesChanged(DidChangeWatchedFilesParams),
}
impl UpdateEvent {
fn absorbs(&self, other: &UpdateEvent) -> bool {
match (self, other) {
(UpdateEvent::Changed(earlier), UpdateEvent::Changed(later)) => {
earlier.text_document.uri == later.text_document.uri
}
_ => false,
}
}
}
struct QueuedUpdate {
seq: u64,
event: UpdateEvent,
}
pub struct UpdateInbox {
pending: Mutex<VecDeque<QueuedUpdate>>,
applying: Mutex<()>,
queued: AtomicU64,
work: Notify,
}
impl Default for UpdateInbox {
fn default() -> Self {
Self::new()
}
}
impl UpdateInbox {
pub fn new() -> Self {
UpdateInbox {
pending: Mutex::new(VecDeque::new()),
applying: Mutex::new(()),
queued: AtomicU64::new(0),
work: Notify::new(),
}
}
pub async fn push(&self, event: UpdateEvent) {
let seq = self.queued.fetch_add(1, Ordering::SeqCst) + 1;
let mut pending = self.pending.lock().await;
match pending.back_mut() {
Some(last) if last.event.absorbs(&event) => last.event = event,
_ => pending.push_back(QueuedUpdate { seq, event }),
}
drop(pending);
self.work.notify_one();
}
pub fn queued(&self) -> u64 {
self.queued.load(Ordering::SeqCst)
}
pub async fn wait_for_work(&self, timeout: Duration) -> bool {
tokio::time::timeout(timeout, self.work.notified())
.await
.is_ok()
}
pub async fn apply_upto<F, Fut>(&self, upto: u64, mut apply: F) -> usize
where
F: FnMut(UpdateEvent) -> Fut,
Fut: std::future::Future<Output = ()>,
{
let started = std::time::Instant::now();
let _applying = self.applying.lock().await;
let waited = started.elapsed().as_millis();
if waited > 50 {
log::debug!("apply_upto: waited {waited} ms for the applier (upto {upto})");
}
let mut applied = 0;
loop {
let next = {
let mut pending = self.pending.lock().await;
match pending.front() {
Some(queued) if queued.seq <= upto => pending.pop_front(),
_ => None,
}
};
let Some(queued) = next else {
break;
};
log::debug!(
"apply_upto: applying event {} ({:?})",
queued.seq,
queued.event
);
let started = std::time::Instant::now();
apply(queued.event).await;
let took = started.elapsed().as_millis();
if took > 50 {
log::debug!("apply_upto: event {} took {took} ms", queued.seq);
}
applied += 1;
}
applied
}
}
pub async fn catch_up_upto(snapshot: &ServerContextSnapshot, upto: u64) -> usize {
let started = std::time::Instant::now();
let applied = snapshot
.inbox()
.apply_upto(upto, |event| async move { process(snapshot, event).await })
.await;
let took = started.elapsed().as_millis();
if took > 50 {
log::debug!("catch_up_upto: {applied} event(s) applied in {took} ms (upto {upto})");
}
applied
}
async fn process(snapshot: &ServerContextSnapshot, event: UpdateEvent) {
match event {
UpdateEvent::Opened(params) => {
handlers::process_did_open_text_document(snapshot.clone(), params).await;
}
UpdateEvent::Changed(params) => {
handlers::process_did_change_text_document(snapshot.clone(), params).await;
}
UpdateEvent::Saved(params) => {
handlers::process_did_save_text_document(snapshot.clone(), params).await;
}
UpdateEvent::Closed(params) => {
handlers::process_did_close_document(snapshot.clone(), params).await;
}
UpdateEvent::WatchedFilesChanged(params) => {
handlers::process_did_change_watched_files(snapshot.clone(), params).await;
}
}
}
pub fn spawn_update_queue(inner: Arc<ServerContextInner>) {
tokio::spawn(async move {
let snapshot = ServerContextSnapshot::new(inner);
loop {
if catch_up_upto(&snapshot, u64::MAX).await == 0 {
snapshot.inbox().wait_for_work(IDLE_WAIT).await;
}
}
});
}
#[cfg(test)]
mod tests {
use super::{UpdateEvent, UpdateInbox};
use lsp_types::{
DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams,
TextDocumentIdentifier,
};
fn opened(uri: &str) -> UpdateEvent {
UpdateEvent::Opened(DidOpenTextDocumentParams {
text_document: lsp_types::TextDocumentItem {
uri: uri.parse().expect("a uri"),
language_id: "cpp".to_string(),
version: 1,
text: String::new(),
},
})
}
fn closed(uri: &str) -> UpdateEvent {
UpdateEvent::Closed(DidCloseTextDocumentParams {
text_document: TextDocumentIdentifier {
uri: uri.parse().expect("a uri"),
},
})
}
fn changed(uri: &str, version: i32, text: &str) -> UpdateEvent {
UpdateEvent::Changed(DidChangeTextDocumentParams {
text_document: lsp_types::VersionedTextDocumentIdentifier {
uri: uri.parse().expect("a uri"),
version,
},
content_changes: vec![lsp_types::TextDocumentContentChangeEvent {
range: None,
range_length: None,
text: text.to_string(),
}],
})
}
fn text_of(event: &UpdateEvent) -> Option<&str> {
match event {
UpdateEvent::Changed(params) => Some(¶ms.content_changes.last()?.text),
_ => None,
}
}
#[tokio::test]
async fn a_run_of_changes_to_one_document_is_one_piece_of_work() {
let inbox = UpdateInbox::new();
for version in 1..=24 {
inbox
.push(changed(
"file:///a.cpp",
version,
&format!("text {version}"),
))
.await;
}
let mut applied = Vec::new();
let count = inbox
.apply_upto(u64::MAX, |event| {
applied.push(text_of(&event).unwrap_or_default().to_string());
async {}
})
.await;
assert_eq!(count, 1, "one document's run of changes is applied once");
assert_eq!(applied, ["text 24"], "and the text applied is the last one");
assert_eq!(inbox.queued(), 24);
}
#[tokio::test]
async fn changes_that_are_not_the_same_document_in_a_row_are_left_alone() {
let inbox = UpdateInbox::new();
inbox.push(changed("file:///a.cpp", 1, "a1")).await;
inbox.push(changed("file:///b.cpp", 1, "b1")).await;
inbox.push(changed("file:///a.cpp", 2, "a2")).await;
inbox.push(closed("file:///c.cpp")).await;
inbox.push(opened("file:///c.cpp")).await;
let mut applied = Vec::new();
let count = inbox
.apply_upto(u64::MAX, |event| {
applied.push(text_of(&event).map(str::to_string));
async {}
})
.await;
assert_eq!(count, 5, "five messages, five applications: {applied:?}");
assert_eq!(
applied
.iter()
.filter_map(|text| text.as_deref())
.collect::<Vec<_>>(),
["a1", "b1", "a2"],
"in the order the client sent them"
);
}
#[tokio::test]
async fn applying_stops_at_the_bound_it_was_given() {
let inbox = UpdateInbox::new();
inbox.push(opened("file:///a.cpp")).await;
let dispatched_at = inbox.queued();
inbox.push(closed("file:///a.cpp")).await;
let mut applied = Vec::new();
let count = inbox
.apply_upto(dispatched_at, |event| {
applied.push(matches!(event, UpdateEvent::Opened(_)));
async {}
})
.await;
assert_eq!(count, 1);
assert_eq!(
applied,
[true],
"the `Closed` is not this request's business"
);
let count = inbox
.apply_upto(u64::MAX, |event| {
applied.push(matches!(event, UpdateEvent::Opened(_)));
async {}
})
.await;
assert_eq!(count, 1);
assert_eq!(applied, [true, false], "in the order the client sent them");
}
#[tokio::test]
async fn only_one_processor_is_inside_the_queue() {
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
let inbox = Arc::new(UpdateInbox::new());
inbox.push(opened("file:///a.cpp")).await;
inbox.push(closed("file:///a.cpp")).await;
let inside = Arc::new(AtomicUsize::new(0));
let most = Arc::new(AtomicUsize::new(0));
let mut running = Vec::new();
for _ in 0..2 {
let inbox = inbox.clone();
let inside = inside.clone();
let most = most.clone();
running.push(tokio::spawn(async move {
inbox
.apply_upto(u64::MAX, |_| {
let inside = inside.clone();
let most = most.clone();
async move {
let now = inside.fetch_add(1, Ordering::SeqCst) + 1;
most.fetch_max(now, Ordering::SeqCst);
tokio::task::yield_now().await;
inside.fetch_sub(1, Ordering::SeqCst);
}
})
.await
}));
}
let mut applied = 0;
for task in running {
applied += task.await.expect("the task ran");
}
assert_eq!(
applied, 2,
"each event is applied exactly once, by one of the two"
);
assert_eq!(most.load(Ordering::SeqCst), 1, "and never by both at once");
}
}