Skip to content

Commit c4a1c56

Browse files
authored
Avoid sending all diagnostics on every file change (#310)
This avoids sending all diagnostics on every small change by only sending diagnostics that the server has not seen before. The intent is to improve performance by reducing the communication overhead
1 parent e5a0b99 commit c4a1c56

File tree

6 files changed

+306
-133
lines changed

6 files changed

+306
-133
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vhdl_lang/src/project.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -229,9 +229,7 @@ impl Project {
229229
self.root.add_design_file(library_name.clone(), design_file);
230230
}
231231

232-
for diagnostic in source_file.parser_diagnostics.iter().cloned() {
233-
diagnostics.push(diagnostic);
234-
}
232+
diagnostics.extend(source_file.parser_diagnostics.iter().cloned());
235233
}
236234

237235
for library_name in self.empty_libraries.iter() {

vhdl_ls/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ fuzzy-matcher = "0.3.7"
2929
[dev-dependencies]
3030
tempfile = "3"
3131
pretty_assertions = "1"
32+
regex = "1.10.4"
3233

3334
[features]
3435
default = []

vhdl_ls/src/rpc_channel.rs

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ impl SharedRpcChannel {
4848
pub mod test_support {
4949

5050
use pretty_assertions::assert_eq;
51+
use regex::Regex;
5152
use serde_json::Value;
5253
use std::cell::RefCell;
5354
use std::collections::VecDeque;
@@ -60,7 +61,14 @@ pub mod test_support {
6061
notification: serde_json::Value,
6162
},
6263
/// Check that the string representation of the notification contains a string
63-
NotificationContainsString { method: String, contains: String },
64+
NotificationContainsString {
65+
method: String,
66+
contains: String,
67+
},
68+
NotificationContainsRegex {
69+
method: String,
70+
contains: Regex,
71+
},
6472
Request {
6573
method: String,
6674
params: serde_json::Value,
@@ -92,7 +100,7 @@ pub mod test_support {
92100
});
93101
}
94102

95-
fn expect_notification_contains(
103+
pub fn expect_notification_contains(
96104
&self,
97105
method: impl Into<String>,
98106
contains: impl Into<String>,
@@ -105,6 +113,19 @@ pub mod test_support {
105113
});
106114
}
107115

116+
pub fn expect_notification_contains_regex(
117+
&self,
118+
method: impl Into<String>,
119+
contains: Regex,
120+
) {
121+
self.expected
122+
.borrow_mut()
123+
.push_back(RpcExpected::NotificationContainsRegex {
124+
method: method.into(),
125+
contains,
126+
});
127+
}
128+
108129
pub fn expect_request(
109130
&self,
110131
method: impl Into<String>,
@@ -159,6 +180,21 @@ pub mod test_support {
159180
}
160181
}
161182

183+
fn contains_regex(value: &serde_json::Value, regex: &Regex) -> bool {
184+
match value {
185+
serde_json::Value::Array(values) => {
186+
values.iter().any(|value| contains_regex(value, regex))
187+
}
188+
serde_json::Value::Object(map) => {
189+
map.values().any(|value| contains_regex(value, regex))
190+
}
191+
serde_json::Value::String(got_string) => regex.is_match(got_string),
192+
serde_json::Value::Null => false,
193+
serde_json::Value::Bool(..) => false,
194+
serde_json::Value::Number(..) => false,
195+
}
196+
}
197+
162198
impl super::RpcChannel for RpcMock {
163199
fn send_notification(&self, method: String, notification: Value) {
164200
let expected = self
@@ -190,6 +226,19 @@ pub mod test_support {
190226
panic!("{notification:?} does not contain sub-string {contains:?}");
191227
}
192228
}
229+
RpcExpected::NotificationContainsRegex {
230+
method: exp_method,
231+
contains,
232+
} => {
233+
assert_eq!(
234+
method, exp_method,
235+
"{:?} contains {:?}",
236+
notification, contains
237+
);
238+
if !contains_regex(&notification, &contains) {
239+
panic!("{notification:?} does not match regex {contains:?}");
240+
}
241+
}
193242
_ => panic!("Expected {expected:?}, got notification {method} {notification:?}"),
194243
}
195244
}

vhdl_ls/src/vhdl_server.rs

Lines changed: 17 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// Copyright (c) 2018, Olof Kraigher olof.kraigher@gmail.com
66

77
mod completion;
8+
mod diagnostics;
89
mod lifecycle;
910
mod rename;
1011
mod text_document;
@@ -13,7 +14,6 @@ mod workspace;
1314
use lsp_types::*;
1415

1516
use fnv::FnvHashMap;
16-
use std::collections::hash_map::Entry;
1717
use vhdl_lang::ast::ObjectClass;
1818

1919
use crate::rpc_channel::SharedRpcChannel;
@@ -22,8 +22,8 @@ use std::io;
2222
use std::io::ErrorKind;
2323
use std::path::{Path, PathBuf};
2424
use vhdl_lang::{
25-
AnyEntKind, Concurrent, Config, Diagnostic, EntHierarchy, EntRef, Message, MessageHandler,
26-
Object, Overloaded, Project, Severity, SeverityMap, SrcPos, Token, Type, VHDLStandard,
25+
AnyEntKind, Concurrent, Config, EntHierarchy, EntRef, Message, MessageHandler, Object,
26+
Overloaded, Project, SeverityMap, SrcPos, Token, Type, VHDLStandard,
2727
};
2828

2929
/// Defines how the language server handles files
@@ -61,7 +61,7 @@ pub struct VHDLServer {
6161
// To have well defined unit tests that are not affected by environment
6262
use_external_config: bool,
6363
project: Project,
64-
files_with_notifications: FnvHashMap<Url, ()>,
64+
diagnostic_cache: FnvHashMap<Url, Vec<vhdl_lang::Diagnostic>>,
6565
init_params: Option<InitializeParams>,
6666
config_file: Option<PathBuf>,
6767
severity_map: SeverityMap,
@@ -75,7 +75,7 @@ impl VHDLServer {
7575
settings,
7676
use_external_config: true,
7777
project: Project::new(VHDLStandard::default()),
78-
files_with_notifications: FnvHashMap::default(),
78+
diagnostic_cache: FnvHashMap::default(),
7979
init_params: None,
8080
config_file: None,
8181
severity_map: SeverityMap::default(),
@@ -90,7 +90,7 @@ impl VHDLServer {
9090
settings: Default::default(),
9191
use_external_config,
9292
project: Project::new(VHDLStandard::default()),
93-
files_with_notifications: FnvHashMap::default(),
93+
diagnostic_cache: Default::default(),
9494
init_params: None,
9595
config_file: None,
9696
severity_map: SeverityMap::default(),
@@ -230,56 +230,6 @@ impl VHDLServer {
230230
try_fun().unwrap_or(false)
231231
}
232232

233-
fn publish_diagnostics(&mut self) {
234-
let diagnostics = self.project.analyse();
235-
236-
if self.settings.no_lint {
237-
return;
238-
}
239-
240-
let supports_related_information = self.client_supports_related_information();
241-
let diagnostics = {
242-
if supports_related_information {
243-
diagnostics
244-
} else {
245-
flatten_related(diagnostics)
246-
}
247-
};
248-
249-
let mut files_with_notifications = std::mem::take(&mut self.files_with_notifications);
250-
for (file_uri, diagnostics) in diagnostics_by_uri(diagnostics).into_iter() {
251-
let lsp_diagnostics = diagnostics
252-
.into_iter()
253-
.filter_map(|diag| to_lsp_diagnostic(diag, &self.severity_map))
254-
.collect();
255-
256-
let publish_diagnostics = PublishDiagnosticsParams {
257-
uri: file_uri.clone(),
258-
diagnostics: lsp_diagnostics,
259-
version: None,
260-
};
261-
262-
self.rpc
263-
.send_notification("textDocument/publishDiagnostics", publish_diagnostics);
264-
265-
self.files_with_notifications.insert(file_uri.clone(), ());
266-
}
267-
268-
for (file_uri, _) in files_with_notifications.drain() {
269-
// File has no longer any diagnostics, publish empty notification to clear them
270-
if !self.files_with_notifications.contains_key(&file_uri) {
271-
let publish_diagnostics = PublishDiagnosticsParams {
272-
uri: file_uri.clone(),
273-
diagnostics: vec![],
274-
version: None,
275-
};
276-
277-
self.rpc
278-
.send_notification("textDocument/publishDiagnostics", publish_diagnostics);
279-
}
280-
}
281-
}
282-
283233
pub fn document_symbol(&self, params: &DocumentSymbolParams) -> Option<DocumentSymbolResponse> {
284234
let source = self
285235
.project
@@ -449,32 +399,6 @@ fn from_lsp_range(range: lsp_types::Range) -> vhdl_lang::Range {
449399
}
450400
}
451401

452-
fn diagnostics_by_uri(diagnostics: Vec<Diagnostic>) -> FnvHashMap<Url, Vec<Diagnostic>> {
453-
let mut map: FnvHashMap<Url, Vec<Diagnostic>> = FnvHashMap::default();
454-
455-
for diagnostic in diagnostics {
456-
let uri = file_name_to_uri(diagnostic.pos.source.file_name());
457-
match map.entry(uri) {
458-
Entry::Occupied(mut entry) => entry.get_mut().push(diagnostic),
459-
Entry::Vacant(entry) => {
460-
let vec = vec![diagnostic];
461-
entry.insert(vec);
462-
}
463-
}
464-
}
465-
466-
map
467-
}
468-
469-
fn flatten_related(diagnostics: Vec<Diagnostic>) -> Vec<Diagnostic> {
470-
let mut flat_diagnostics = Vec::new();
471-
for mut diagnostic in diagnostics {
472-
flat_diagnostics.extend(diagnostic.drain_related());
473-
flat_diagnostics.push(diagnostic);
474-
}
475-
flat_diagnostics
476-
}
477-
478402
fn file_name_to_uri(file_name: &Path) -> Url {
479403
// @TODO return error to client
480404
Url::from_file_path(file_name).unwrap()
@@ -485,45 +409,6 @@ fn uri_to_file_name(uri: &Url) -> PathBuf {
485409
uri.to_file_path().unwrap()
486410
}
487411

488-
fn to_lsp_diagnostic(
489-
diagnostic: Diagnostic,
490-
severity_map: &SeverityMap,
491-
) -> Option<lsp_types::Diagnostic> {
492-
let severity = match severity_map[diagnostic.code]? {
493-
Severity::Error => DiagnosticSeverity::ERROR,
494-
Severity::Warning => DiagnosticSeverity::WARNING,
495-
Severity::Info => DiagnosticSeverity::INFORMATION,
496-
Severity::Hint => DiagnosticSeverity::HINT,
497-
};
498-
499-
let related_information = if !diagnostic.related.is_empty() {
500-
let mut related_information = Vec::new();
501-
for (pos, msg) in diagnostic.related {
502-
let uri = file_name_to_uri(pos.source.file_name());
503-
related_information.push(DiagnosticRelatedInformation {
504-
location: Location {
505-
uri: uri.to_owned(),
506-
range: to_lsp_range(pos.range()),
507-
},
508-
message: msg,
509-
})
510-
}
511-
Some(related_information)
512-
} else {
513-
None
514-
};
515-
516-
Some(lsp_types::Diagnostic {
517-
range: to_lsp_range(diagnostic.pos.range()),
518-
severity: Some(severity),
519-
code: Some(NumberOrString::String(format!("{}", diagnostic.code))),
520-
source: Some("vhdl ls".to_owned()),
521-
message: diagnostic.message,
522-
related_information,
523-
..Default::default()
524-
})
525-
}
526-
527412
fn overloaded_kind(overloaded: &Overloaded) -> SymbolKind {
528413
match overloaded {
529414
Overloaded::SubprogramDecl(_) => SymbolKind::FUNCTION,
@@ -615,7 +500,7 @@ mod tests {
615500
use super::*;
616501
use crate::rpc_channel::test_support::*;
617502

618-
fn initialize_server(server: &mut VHDLServer, root_uri: Url) {
503+
pub(crate) fn initialize_server(server: &mut VHDLServer, root_uri: Url) {
619504
let capabilities = ClientCapabilities::default();
620505

621506
#[allow(deprecated)]
@@ -636,13 +521,13 @@ mod tests {
636521
server.initialized_notification();
637522
}
638523

639-
fn temp_root_uri() -> (tempfile::TempDir, Url) {
524+
pub(crate) fn temp_root_uri() -> (tempfile::TempDir, Url) {
640525
let tempdir = tempfile::tempdir().unwrap();
641526
let root_uri = Url::from_file_path(tempdir.path().canonicalize().unwrap()).unwrap();
642527
(tempdir, root_uri)
643528
}
644529

645-
fn expect_loaded_config_messages(mock: &RpcMock, config_uri: &Url) {
530+
pub(crate) fn expect_loaded_config_messages(mock: &RpcMock, config_uri: &Url) {
646531
let file_name = config_uri
647532
.to_file_path()
648533
.unwrap()
@@ -666,7 +551,7 @@ mod tests {
666551
}
667552

668553
/// Create RpcMock and VHDLServer
669-
fn setup_server() -> (Rc<RpcMock>, VHDLServer) {
554+
pub(crate) fn setup_server() -> (Rc<RpcMock>, VHDLServer) {
670555
let mock = Rc::new(RpcMock::new());
671556
let server = VHDLServer::new_external_config(SharedRpcChannel::new(mock.clone()), false);
672557
(mock, server)
@@ -787,13 +672,17 @@ end entity ent;
787672
server.text_document_did_change_notification(&did_change);
788673
}
789674

790-
fn write_file(root_uri: &Url, file_name: impl AsRef<str>, contents: impl AsRef<str>) -> Url {
675+
pub(crate) fn write_file(
676+
root_uri: &Url,
677+
file_name: impl AsRef<str>,
678+
contents: impl AsRef<str>,
679+
) -> Url {
791680
let path = root_uri.to_file_path().unwrap().join(file_name.as_ref());
792681
std::fs::write(&path, contents.as_ref()).unwrap();
793682
Url::from_file_path(path).unwrap()
794683
}
795684

796-
fn write_config(root_uri: &Url, contents: impl AsRef<str>) -> Url {
685+
pub(crate) fn write_config(root_uri: &Url, contents: impl AsRef<str>) -> Url {
797686
write_file(root_uri, "vhdl_ls.toml", contents)
798687
}
799688

@@ -1086,8 +975,8 @@ lib.files = [
1086975
mock.expect_notification("textDocument/publishDiagnostics", publish_diagnostics1);
1087976
mock.expect_message_contains("Configuration file has changed, reloading project...");
1088977
expect_loaded_config_messages(&mock, &config_uri);
1089-
mock.expect_notification("textDocument/publishDiagnostics", publish_diagnostics2a);
1090978
mock.expect_notification("textDocument/publishDiagnostics", publish_diagnostics2b);
979+
mock.expect_notification("textDocument/publishDiagnostics", publish_diagnostics2a);
1091980

1092981
initialize_server(&mut server, root_uri.clone());
1093982

0 commit comments

Comments
 (0)