Skip to content

Commit 7c5f33c

Browse files
committed
ConfigTree
1 parent 10c1db6 commit 7c5f33c

File tree

4 files changed

+261
-0
lines changed

4 files changed

+261
-0
lines changed

Cargo.lock

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

crates/rust-analyzer/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ toolchain.workspace = true
7575
vfs-notify.workspace = true
7676
vfs.workspace = true
7777
la-arena.workspace = true
78+
indextree = "4.6.0"
79+
slotmap = "1.0.7"
7880

7981
[target.'cfg(windows)'.dependencies]
8082
winapi = "0.3.9"

crates/rust-analyzer/src/config.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ use crate::{
4242
};
4343

4444
mod patch_old_style;
45+
mod tree;
4546

4647
// Conventions for configuration keys to preserve maximal extendability without breakage:
4748
// - Toggles (be it binary true/false or with more options in-between) should almost always suffix as `_enable`
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
use indextree::NodeId;
2+
use parking_lot::{RwLock, RwLockUpgradableReadGuard};
3+
use rustc_hash::FxHashMap;
4+
use slotmap::SlotMap;
5+
use std::sync::Arc;
6+
use vfs::{FileId, Vfs};
7+
8+
use super::{ConfigInput, LocalConfigData, RootLocalConfigData};
9+
10+
pub struct ConcurrentConfigTree {
11+
// One rwlock on the whole thing is probably fine.
12+
// If you have 40,000 crates and you need to edit your config 200x/second, let us know.
13+
rwlock: RwLock<ConfigTree>,
14+
}
15+
16+
pub enum ConfigTreeError {
17+
Removed,
18+
NonExistent,
19+
Utf8(vfs::VfsPath, std::str::Utf8Error),
20+
TomlParse(vfs::VfsPath, toml::de::Error),
21+
TomlDeserialize { path: vfs::VfsPath, field: String, error: toml::de::Error },
22+
}
23+
24+
/// Some rust-analyzer.toml files have changed, and/or the LSP client sent a new configuration.
25+
pub struct ConfigChanges {
26+
ra_toml_changes: Vec<vfs::ChangedFile>,
27+
client_change: Option<Arc<ConfigInput>>,
28+
}
29+
30+
impl ConcurrentConfigTree {
31+
pub fn apply_changes(&self, changes: ConfigChanges, vfs: &Vfs) -> Vec<ConfigTreeError> {
32+
let mut errors = Vec::new();
33+
self.rwlock.write().apply_changes(changes, vfs, &mut errors);
34+
errors
35+
}
36+
pub fn read_config(&self, file_id: FileId) -> Result<Arc<LocalConfigData>, ConfigTreeError> {
37+
let reader = self.rwlock.upgradable_read();
38+
if let Some(computed) = reader.read_only(file_id)? {
39+
return Ok(computed);
40+
} else {
41+
let mut writer = RwLockUpgradableReadGuard::upgrade(reader);
42+
return writer.compute(file_id);
43+
}
44+
}
45+
}
46+
47+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
48+
enum ConfigSource {
49+
ClientConfig,
50+
RaToml(FileId),
51+
}
52+
53+
slotmap::new_key_type! {
54+
struct ComputedIdx;
55+
}
56+
57+
struct ConfigNode {
58+
src: ConfigSource,
59+
input: Arc<ConfigInput>,
60+
computed: ComputedIdx,
61+
}
62+
63+
struct ConfigTree {
64+
tree: indextree::Arena<ConfigNode>,
65+
client_config: NodeId,
66+
ra_file_id_map: FxHashMap<FileId, NodeId>,
67+
computed: SlotMap<ComputedIdx, Option<Arc<LocalConfigData>>>,
68+
}
69+
70+
fn parse_toml(
71+
file_id: FileId,
72+
vfs: &Vfs,
73+
scratch: &mut Vec<(String, toml::de::Error)>,
74+
errors: &mut Vec<ConfigTreeError>,
75+
) -> Option<Arc<ConfigInput>> {
76+
let content = vfs.file_contents(file_id);
77+
let path = vfs.file_path(file_id);
78+
let content_str = match std::str::from_utf8(content) {
79+
Err(e) => {
80+
tracing::error!("non-UTF8 TOML content for {path}: {e}");
81+
errors.push(ConfigTreeError::Utf8(path, e));
82+
return None;
83+
}
84+
Ok(str) => str,
85+
};
86+
let table = match toml::from_str(content_str) {
87+
Ok(table) => table,
88+
Err(e) => {
89+
errors.push(ConfigTreeError::TomlParse(path, e));
90+
return None;
91+
}
92+
};
93+
let input = Arc::new(ConfigInput::from_toml(table, scratch));
94+
scratch.drain(..).for_each(|(field, error)| {
95+
errors.push(ConfigTreeError::TomlDeserialize { path: path.clone(), field, error });
96+
});
97+
Some(input)
98+
}
99+
100+
impl ConfigTree {
101+
fn new() -> Self {
102+
let mut tree = indextree::Arena::new();
103+
let mut computed = SlotMap::default();
104+
let client_config = tree.new_node(ConfigNode {
105+
src: ConfigSource::ClientConfig,
106+
input: Arc::new(ConfigInput::default()),
107+
computed: computed.insert(Option::<Arc<LocalConfigData>>::None),
108+
});
109+
Self { client_config, ra_file_id_map: FxHashMap::default(), tree, computed }
110+
}
111+
112+
fn read_only(&self, file_id: FileId) -> Result<Option<Arc<LocalConfigData>>, ConfigTreeError> {
113+
let node_id = *self.ra_file_id_map.get(&file_id).ok_or(ConfigTreeError::NonExistent)?;
114+
// indextree does not check this during get(), probably for perf reasons?
115+
// get() is apparently only a bounds check
116+
if node_id.is_removed(&self.tree) {
117+
return Err(ConfigTreeError::Removed);
118+
}
119+
let node = self.tree.get(node_id).ok_or(ConfigTreeError::NonExistent)?.get();
120+
Ok(self.computed[node.computed].clone())
121+
}
122+
123+
fn compute(&mut self, file_id: FileId) -> Result<Arc<LocalConfigData>, ConfigTreeError> {
124+
let node_id = *self.ra_file_id_map.get(&file_id).ok_or(ConfigTreeError::NonExistent)?;
125+
self.compute_inner(node_id)
126+
}
127+
fn compute_inner(&mut self, node_id: NodeId) -> Result<Arc<LocalConfigData>, ConfigTreeError> {
128+
if node_id.is_removed(&self.tree) {
129+
return Err(ConfigTreeError::Removed);
130+
}
131+
let node = self.tree.get(node_id).ok_or(ConfigTreeError::NonExistent)?.get();
132+
let idx = node.computed;
133+
let slot = &mut self.computed[idx];
134+
if let Some(slot) = slot {
135+
Ok(slot.clone())
136+
} else {
137+
let self_computed = if let Some(parent) =
138+
self.tree.get(node_id).ok_or(ConfigTreeError::NonExistent)?.parent()
139+
{
140+
let self_input = node.input.clone();
141+
let parent_computed = self.compute_inner(parent)?;
142+
Arc::new(parent_computed.clone_with_overrides(self_input.local.clone()))
143+
} else {
144+
// We have hit a root node
145+
let self_input = node.input.clone();
146+
let root_local = RootLocalConfigData::from_root_input(self_input.local.clone());
147+
Arc::new(root_local.0)
148+
};
149+
// Get a new &mut slot because self.compute(parent) also gets mut access
150+
let slot = &mut self.computed[idx];
151+
slot.replace(self_computed.clone());
152+
Ok(self_computed)
153+
}
154+
}
155+
156+
fn insert_toml(&mut self, file_id: FileId, input: Arc<ConfigInput>) -> NodeId {
157+
let computed = self.computed.insert(None);
158+
let node =
159+
self.tree.new_node(ConfigNode { src: ConfigSource::RaToml(file_id), input, computed });
160+
self.ra_file_id_map.insert(file_id, node);
161+
node
162+
}
163+
164+
fn update_toml(
165+
&mut self,
166+
file_id: FileId,
167+
input: Arc<ConfigInput>,
168+
) -> Result<(), ConfigTreeError> {
169+
let Some(node_id) = self.ra_file_id_map.get(&file_id).cloned() else {
170+
return Err(ConfigTreeError::NonExistent);
171+
};
172+
if node_id.is_removed(&self.tree) {
173+
return Err(ConfigTreeError::Removed);
174+
}
175+
let node = self.tree.get_mut(node_id).ok_or(ConfigTreeError::NonExistent)?;
176+
node.get_mut().input = input;
177+
178+
self.invalidate_subtree(node_id);
179+
Ok(())
180+
}
181+
182+
fn invalidate_subtree(&mut self, node_id: NodeId) {
183+
//
184+
// This is why we need the computed values outside the indextree: we iterate immutably
185+
// over the tree while holding a &mut self.computed.
186+
node_id.descendants(&self.tree).for_each(|x| {
187+
let Some(desc) = self.tree.get(x) else {
188+
return;
189+
};
190+
self.computed.get_mut(desc.get().computed).take();
191+
});
192+
}
193+
194+
fn remove_toml(&mut self, file_id: FileId) -> Option<()> {
195+
let node_id = self.ra_file_id_map.remove(&file_id)?;
196+
if node_id.is_removed(&self.tree) {
197+
return None;
198+
}
199+
let node = self.tree.get(node_id)?;
200+
let idx = node.get().computed;
201+
let _ = self.computed.remove(idx);
202+
self.invalidate_subtree(node_id);
203+
Some(())
204+
}
205+
206+
fn apply_changes(
207+
&mut self,
208+
changes: ConfigChanges,
209+
vfs: &Vfs,
210+
errors: &mut Vec<ConfigTreeError>,
211+
) {
212+
let mut scratch_errors = Vec::new();
213+
let ConfigChanges { client_change, ra_toml_changes } = changes;
214+
if let Some(change) = client_change {
215+
let node =
216+
self.tree.get_mut(self.client_config).expect("client_config node should exist");
217+
node.get_mut().input = change;
218+
self.invalidate_subtree(self.client_config);
219+
}
220+
for change in ra_toml_changes {
221+
// turn and face the strain
222+
match change.change_kind {
223+
vfs::ChangeKind::Create => {
224+
let input = parse_toml(change.file_id, vfs, &mut scratch_errors, errors)
225+
.unwrap_or_default();
226+
let _new_node = self.insert_toml(change.file_id, input);
227+
}
228+
vfs::ChangeKind::Modify => {
229+
let input = parse_toml(change.file_id, vfs, &mut scratch_errors, errors)
230+
.unwrap_or_default();
231+
if let Err(e) = self.update_toml(change.file_id, input) {
232+
errors.push(e);
233+
}
234+
}
235+
vfs::ChangeKind::Delete => {
236+
self.remove_toml(change.file_id);
237+
}
238+
}
239+
}
240+
}
241+
}

0 commit comments

Comments
 (0)