Skip to content

Commit b01f5d3

Browse files
committed
Reload design when workspace configuration changes
If supported by the client, the server registers for changes to the configuration file using client/registerCapability. When the configuration file changes, the client send a notification workspace/didChangeWatchedFiles to the server. The configuration is then loaded again and project configuration is updated: The design state is reset, new files are added and parsed. As required for incremental document updates, source files that are already loaded are parsed from in-memory sources.
1 parent 9dc8040 commit b01f5d3

File tree

4 files changed

+604
-168
lines changed

4 files changed

+604
-168
lines changed

vhdl_lang/src/project.rs

Lines changed: 128 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,68 @@ impl Project {
3131
}
3232
}
3333

34+
/// Create instance from given configuration.
35+
/// Files referred by configuration are parsed into corresponding libraries.
3436
pub fn from_config(config: &Config, messages: &mut dyn MessageHandler) -> Project {
3537
let mut project = Project::new();
36-
let mut files_to_parse: FnvHashMap<PathBuf, FnvHashSet<Symbol>> = FnvHashMap::default();
38+
39+
let files = project.load_files_from_config(config, messages);
40+
project.parse_and_add_files(files, messages);
41+
42+
project
43+
}
44+
45+
/// Replace active project configuration.
46+
/// The design state is reset, new files are added and parsed. Existing source files will be
47+
/// kept and parsed from in-memory source (required for incremental document updates).
48+
pub fn update_config(&mut self, config: &Config, messages: &mut dyn MessageHandler) {
49+
self.parser = VHDLParser::default();
50+
self.root = DesignRoot::new(self.parser.symbols.clone());
51+
52+
// Reset library associations for known files,
53+
// all project files are added to the corresponding libraries later on.
54+
self.files
55+
.values_mut()
56+
.for_each(|source_file| source_file.library_names.clear());
57+
58+
// Files might already be part of self.files, these have to be parsed
59+
// from in-memory source. New files can be parsed as usual.
60+
let (known_files, new_files) = self
61+
.load_files_from_config(config, messages)
62+
.into_iter()
63+
.partition(|(file_name, _library_names)| self.files.contains_key(file_name));
64+
65+
for (file_name, library_names) in known_files {
66+
if let Some(source_file) = self.files.get_mut(&file_name) {
67+
source_file.parser_diagnostics.clear();
68+
source_file.library_names = library_names;
69+
source_file.design_file = self
70+
.parser
71+
.parse_design_source(&source_file.source, &mut source_file.parser_diagnostics);
72+
}
73+
}
74+
75+
self.parse_and_add_files(new_files, messages);
76+
}
77+
78+
fn load_files_from_config(
79+
&mut self,
80+
config: &Config,
81+
messages: &mut dyn MessageHandler,
82+
) -> FnvHashMap<PathBuf, FnvHashSet<Symbol>> {
83+
let mut files: FnvHashMap<PathBuf, FnvHashSet<Symbol>> = FnvHashMap::default();
84+
self.empty_libraries.clear();
3785

3886
for library in config.iter_libraries() {
3987
let library_name =
4088
Latin1String::from_utf8(library.name()).expect("Library name not latin-1 encoded");
41-
let library_name = project.parser.symbol(&library_name);
89+
let library_name = self.parser.symbol(&library_name);
4290

4391
let mut empty_library = true;
4492
for file_name in library.file_names(messages) {
4593
empty_library = false;
4694

47-
match files_to_parse.entry(file_name.clone()) {
95+
match files.entry(file_name.clone()) {
4896
Entry::Occupied(mut entry) => {
4997
entry.get_mut().insert(library_name.clone());
5098
}
@@ -57,16 +105,23 @@ impl Project {
57105
}
58106

59107
if empty_library {
60-
project.empty_libraries.insert(library_name);
108+
self.empty_libraries.insert(library_name);
61109
}
62110
}
111+
files
112+
}
63113

114+
fn parse_and_add_files(
115+
&mut self,
116+
files_to_parse: FnvHashMap<PathBuf, FnvHashSet<Symbol>>,
117+
messages: &mut dyn MessageHandler,
118+
) {
64119
use rayon::prelude::*;
65120

66121
let parsed: Vec<_> = files_to_parse
67122
.into_par_iter()
68123
.map_init(
69-
|| &project.parser,
124+
|| &self.parser,
70125
|parser, (file_name, library_names)| {
71126
let mut diagnostics = Vec::new();
72127
let result = parser.parse_design_file(&file_name, &mut diagnostics);
@@ -84,7 +139,7 @@ impl Project {
84139
}
85140
};
86141

87-
project.files.insert(
142+
self.files.insert(
88143
source.file_name().to_owned(),
89144
SourceFile {
90145
source,
@@ -94,8 +149,6 @@ impl Project {
94149
},
95150
);
96151
}
97-
98-
project
99152
}
100153

101154
pub fn get_source(&self, file_name: &Path) -> Option<Source> {
@@ -400,4 +453,71 @@ end package;
400453
);
401454
check_no_diagnostics(&project.analyse());
402455
}
456+
457+
/// Test that the configuration can be updated
458+
#[test]
459+
fn test_config_update() {
460+
let tempdir = tempfile::tempdir().unwrap();
461+
let root = dunce::canonicalize(tempdir.path()).unwrap();
462+
463+
let path1 = root.join("file1.vhd");
464+
let path2 = root.join("file2.vhd");
465+
std::fs::write(
466+
&path1,
467+
"
468+
library unkown;
469+
use unkown.pkg.all;
470+
471+
package pkg is
472+
end package;
473+
",
474+
)
475+
.unwrap();
476+
let source1 = Source::from_latin1_file(&path1).unwrap();
477+
478+
std::fs::write(
479+
&path2,
480+
"
481+
library unkown;
482+
use unkown.pkg.all;
483+
484+
package pkg is
485+
end package;
486+
",
487+
)
488+
.unwrap();
489+
let source2 = Source::from_latin1_file(&path2).unwrap();
490+
491+
let config_str1 = "
492+
[libraries]
493+
lib.files = ['file1.vhd']
494+
";
495+
let config1 = Config::from_str(config_str1, &root).unwrap();
496+
497+
let config_str2 = "
498+
[libraries]
499+
lib.files = ['file2.vhd']
500+
";
501+
let config2 = Config::from_str(config_str2, &root).unwrap();
502+
503+
let mut messages = Vec::new();
504+
let mut project = Project::from_config(&config1, &mut messages);
505+
assert_eq!(messages, vec![]);
506+
507+
// Invalid library should only be reported in source1
508+
let diagnostics = project.analyse();
509+
assert_eq!(diagnostics.len(), 2);
510+
assert_eq!(diagnostics[0].pos.source, source1); // No such library
511+
assert_eq!(diagnostics[1].pos.source, source1); // No declaration
512+
513+
// Change configuration file
514+
project.update_config(&config2, &mut messages);
515+
assert_eq!(messages, vec![]);
516+
517+
// Invalid library should only be reported in source2
518+
let diagnostics = project.analyse();
519+
assert_eq!(diagnostics.len(), 2);
520+
assert_eq!(diagnostics[0].pos.source, source2); // No such library
521+
assert_eq!(diagnostics[1].pos.source, source2); // No declaration
522+
}
403523
}

vhdl_ls/src/rpc_channel.rs

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,16 @@ use lsp_types::*;
1010
use vhdl_lang::{Message, MessageHandler};
1111

1212
pub trait RpcChannel {
13+
/// Send notification to the client.
1314
fn send_notification(
1415
&self,
1516
method: impl Into<String>,
1617
notification: impl serde::ser::Serialize,
1718
);
1819

20+
/// Send request to the client.
21+
fn send_request(&self, method: impl Into<String>, params: impl serde::ser::Serialize);
22+
1923
fn window_show_message(&self, typ: MessageType, message: impl Into<String>) {
2024
self.send_notification(
2125
"window/showMessage",
@@ -85,6 +89,10 @@ pub mod test_support {
8589
},
8690
/// Check that the string representation of the notification contains a string
8791
NotificationContainsString { method: String, contains: String },
92+
Request {
93+
method: String,
94+
params: serde_json::Value,
95+
},
8896
}
8997

9098
#[derive(Clone)]
@@ -125,6 +133,17 @@ pub mod test_support {
125133
});
126134
}
127135

136+
pub fn expect_request(
137+
&self,
138+
method: impl Into<String>,
139+
params: impl serde::ser::Serialize,
140+
) {
141+
self.expected.borrow_mut().push_back(RpcExpected::Request {
142+
method: method.into(),
143+
params: serde_json::to_value(params).unwrap(),
144+
});
145+
}
146+
128147
pub fn expect_error_contains(&self, contains: impl Into<String>) {
129148
let contains = contains.into();
130149
self.expect_notification_contains("window/showMessage", contains.clone());
@@ -178,7 +197,7 @@ pub mod test_support {
178197
.pop_front()
179198
.ok_or_else(|| {
180199
panic!(
181-
"No expected value, got method={} {:?}",
200+
"No expected value, got notification method={} {:?}",
182201
method, notification
183202
)
184203
})
@@ -207,6 +226,39 @@ pub mod test_support {
207226
);
208227
}
209228
}
229+
_ => panic!(
230+
"Expected {:?}, got notification {} {:?}",
231+
expected, method, notification
232+
),
233+
}
234+
}
235+
236+
fn send_request(&self, method: impl Into<String>, params: impl serde::ser::Serialize) {
237+
let method = method.into();
238+
let params = serde_json::to_value(params).unwrap();
239+
let expected = self
240+
.expected
241+
.borrow_mut()
242+
.pop_front()
243+
.ok_or_else(|| {
244+
panic!(
245+
"No expected value, got request method={} {:?}",
246+
method, params
247+
)
248+
})
249+
.unwrap();
250+
251+
match expected {
252+
RpcExpected::Request {
253+
method: exp_method,
254+
params: exp_params,
255+
} => {
256+
assert_eq!((method, params), (exp_method, exp_params));
257+
}
258+
_ => panic!(
259+
"Expected {:?}, got request {} {:?}",
260+
expected, method, params
261+
),
210262
}
211263
}
212264
}

0 commit comments

Comments
 (0)