1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
use agnix_core::normalize_line_endings;
use super::*;
impl Backend {
pub(crate) async fn handle_did_open(&self, params: DidOpenTextDocumentParams) {
let version = params.text_document.version;
let uri = params.text_document.uri;
// Normalize CRLF so the cached content matches the LF-relative byte offsets
// produced by validate_content and used by code actions for fix ranges.
// Match on the Cow to reuse the original String for LF-only documents.
let raw = params.text_document.text;
let text = match normalize_line_endings(&raw) {
std::borrow::Cow::Borrowed(_) => raw,
std::borrow::Cow::Owned(normalized) => normalized,
};
// Acquire both locks atomically to update content and version together.
// Readers that need both values must capture them in a single operation
// (see validate_from_content_and_publish).
{
let mut docs = self.documents.write().await;
let mut versions = self.document_versions.write().await;
docs.insert(uri.clone(), Arc::new(text));
versions.insert(uri.clone(), version);
// Both guards dropped here in reverse acquisition order (versions then docs)
}
self.validate_from_content_and_publish(uri, None).await;
}
pub(crate) async fn handle_did_change(&self, params: DidChangeTextDocumentParams) {
let version = params.text_document.version;
let uri = params.text_document.uri;
if let Some(change) = params.content_changes.into_iter().next() {
// Normalize CRLF so the cached content matches the LF-relative byte offsets
// produced by validate_content and used by code actions for fix ranges.
// Match on the Cow to reuse the original String for LF-only documents.
let raw = change.text;
let text = match normalize_line_endings(&raw) {
std::borrow::Cow::Borrowed(_) => raw,
std::borrow::Cow::Owned(normalized) => normalized,
};
// Acquire both locks atomically to update content and version together.
// Readers that need both values must capture them in a single operation
// (see validate_from_content_and_publish).
{
let mut docs = self.documents.write().await;
let mut versions = self.document_versions.write().await;
docs.insert(uri.clone(), Arc::new(text));
versions.insert(uri.clone(), version);
// Both guards dropped here in reverse acquisition order (versions then docs)
}
self.validate_from_content_and_publish(uri, None).await;
} else {
// Even when content_changes is empty, the version from
// VersionedTextDocumentIdentifier is authoritative per LSP spec.
self.document_versions.write().await.insert(uri, version);
}
}
pub(crate) async fn handle_did_save(&self, params: DidSaveTextDocumentParams) {
let uri = params.text_document.uri;
self.validate_from_content_and_publish(uri.clone(), None)
.await;
// Re-run project-level validation when a relevant file is saved
if let Ok(path) = uri.to_file_path() {
if Self::is_project_level_trigger(&path) {
self.spawn_project_validation();
}
}
}
pub(crate) async fn handle_did_close(&self, params: DidCloseTextDocumentParams) {
let uri = params.text_document.uri;
{
let mut docs = self.documents.write().await;
docs.remove(&uri);
}
self.document_versions.write().await.remove(&uri);
// Clearing diagnostics for a closed document - version is intentionally None
// since the document is no longer tracked.
self.client.publish_diagnostics(uri, vec![], None).await;
}
}