Replaces O(N) iteration over all internal configs with O(D × log N) direct ancestor lookups, where D is path depth and N is total config count. Release Notes: - N/A
394 lines
15 KiB
Rust
394 lines
15 KiB
Rust
use anyhow::{Context as _, Result};
|
|
use collections::{BTreeMap, BTreeSet, HashSet};
|
|
use ec4rs::{ConfigParser, PropertiesSource, Section};
|
|
use fs::Fs;
|
|
use futures::StreamExt;
|
|
use gpui::{Context, EventEmitter, Task};
|
|
use paths::EDITORCONFIG_NAME;
|
|
use smallvec::SmallVec;
|
|
use std::{path::Path, str::FromStr, sync::Arc};
|
|
use util::{ResultExt as _, rel_path::RelPath};
|
|
|
|
use crate::{InvalidSettingsError, LocalSettingsPath, WorktreeId, watch_config_file};
|
|
|
|
pub type EditorconfigProperties = ec4rs::Properties;
|
|
|
|
#[derive(Clone)]
|
|
pub struct Editorconfig {
|
|
pub is_root: bool,
|
|
pub sections: SmallVec<[Section; 5]>,
|
|
}
|
|
|
|
impl FromStr for Editorconfig {
|
|
type Err = anyhow::Error;
|
|
|
|
fn from_str(contents: &str) -> Result<Self, Self::Err> {
|
|
let parser = ConfigParser::new_buffered(contents.as_bytes())
|
|
.context("creating editorconfig parser")?;
|
|
let is_root = parser.is_root;
|
|
let sections = parser
|
|
.collect::<Result<SmallVec<_>, _>>()
|
|
.context("parsing editorconfig sections")?;
|
|
Ok(Self { is_root, sections })
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub enum EditorconfigEvent {
|
|
ExternalConfigChanged {
|
|
path: LocalSettingsPath,
|
|
content: Option<String>,
|
|
affected_worktree_ids: Vec<WorktreeId>,
|
|
},
|
|
}
|
|
|
|
impl EventEmitter<EditorconfigEvent> for EditorconfigStore {}
|
|
|
|
#[derive(Default)]
|
|
pub struct EditorconfigStore {
|
|
external_configs: BTreeMap<Arc<Path>, (String, Option<Editorconfig>)>,
|
|
worktree_state: BTreeMap<WorktreeId, EditorconfigWorktreeState>,
|
|
local_external_config_watchers: BTreeMap<Arc<Path>, Task<()>>,
|
|
local_external_config_discovery_tasks: BTreeMap<WorktreeId, Task<()>>,
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct EditorconfigWorktreeState {
|
|
internal_configs: BTreeMap<Arc<RelPath>, (String, Option<Editorconfig>)>,
|
|
external_config_paths: BTreeSet<Arc<Path>>,
|
|
}
|
|
|
|
impl EditorconfigStore {
|
|
pub(crate) fn set_configs(
|
|
&mut self,
|
|
worktree_id: WorktreeId,
|
|
path: LocalSettingsPath,
|
|
content: Option<&str>,
|
|
) -> std::result::Result<(), InvalidSettingsError> {
|
|
match (&path, content) {
|
|
(LocalSettingsPath::InWorktree(rel_path), None) => {
|
|
if let Some(state) = self.worktree_state.get_mut(&worktree_id) {
|
|
state.internal_configs.remove(rel_path);
|
|
}
|
|
}
|
|
(LocalSettingsPath::OutsideWorktree(abs_path), None) => {
|
|
if let Some(state) = self.worktree_state.get_mut(&worktree_id) {
|
|
state.external_config_paths.remove(abs_path);
|
|
}
|
|
let still_in_use = self
|
|
.worktree_state
|
|
.values()
|
|
.any(|state| state.external_config_paths.contains(abs_path));
|
|
if !still_in_use {
|
|
self.external_configs.remove(abs_path);
|
|
self.local_external_config_watchers.remove(abs_path);
|
|
}
|
|
}
|
|
(LocalSettingsPath::InWorktree(rel_path), Some(content)) => {
|
|
let state = self.worktree_state.entry(worktree_id).or_default();
|
|
let should_update = state
|
|
.internal_configs
|
|
.get(rel_path)
|
|
.map_or(true, |entry| entry.0 != content);
|
|
if should_update {
|
|
let parsed = match content.parse::<Editorconfig>() {
|
|
Ok(parsed) => Some(parsed),
|
|
Err(e) => {
|
|
state
|
|
.internal_configs
|
|
.insert(rel_path.clone(), (content.to_owned(), None));
|
|
return Err(InvalidSettingsError::Editorconfig {
|
|
message: e.to_string(),
|
|
path: LocalSettingsPath::InWorktree(
|
|
rel_path.join(RelPath::unix(EDITORCONFIG_NAME).unwrap()),
|
|
),
|
|
});
|
|
}
|
|
};
|
|
state
|
|
.internal_configs
|
|
.insert(rel_path.clone(), (content.to_owned(), parsed));
|
|
}
|
|
}
|
|
(LocalSettingsPath::OutsideWorktree(abs_path), Some(content)) => {
|
|
let state = self.worktree_state.entry(worktree_id).or_default();
|
|
state.external_config_paths.insert(abs_path.clone());
|
|
let should_update = self
|
|
.external_configs
|
|
.get(abs_path)
|
|
.map_or(true, |entry| entry.0 != content);
|
|
if should_update {
|
|
let parsed = match content.parse::<Editorconfig>() {
|
|
Ok(parsed) => Some(parsed),
|
|
Err(e) => {
|
|
self.external_configs
|
|
.insert(abs_path.clone(), (content.to_owned(), None));
|
|
return Err(InvalidSettingsError::Editorconfig {
|
|
message: e.to_string(),
|
|
path: LocalSettingsPath::OutsideWorktree(
|
|
abs_path.join(EDITORCONFIG_NAME).into(),
|
|
),
|
|
});
|
|
}
|
|
};
|
|
self.external_configs
|
|
.insert(abs_path.clone(), (content.to_owned(), parsed));
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn remove_for_worktree(&mut self, root_id: WorktreeId) {
|
|
self.local_external_config_discovery_tasks.remove(&root_id);
|
|
let Some(removed) = self.worktree_state.remove(&root_id) else {
|
|
return;
|
|
};
|
|
let paths_in_use: HashSet<_> = self
|
|
.worktree_state
|
|
.values()
|
|
.flat_map(|w| w.external_config_paths.iter())
|
|
.collect();
|
|
for path in removed.external_config_paths.iter() {
|
|
if !paths_in_use.contains(path) {
|
|
self.external_configs.remove(path);
|
|
self.local_external_config_watchers.remove(path);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn internal_configs(
|
|
&self,
|
|
root_id: WorktreeId,
|
|
) -> impl '_ + Iterator<Item = (&RelPath, &str, Option<&Editorconfig>)> {
|
|
self.worktree_state
|
|
.get(&root_id)
|
|
.into_iter()
|
|
.flat_map(|state| {
|
|
state
|
|
.internal_configs
|
|
.iter()
|
|
.map(|(path, data)| (path.as_ref(), data.0.as_str(), data.1.as_ref()))
|
|
})
|
|
}
|
|
|
|
fn external_configs(
|
|
&self,
|
|
worktree_id: WorktreeId,
|
|
) -> impl '_ + Iterator<Item = (&Path, &str, Option<&Editorconfig>)> {
|
|
self.worktree_state
|
|
.get(&worktree_id)
|
|
.into_iter()
|
|
.flat_map(|state| {
|
|
state.external_config_paths.iter().filter_map(|path| {
|
|
self.external_configs
|
|
.get(path)
|
|
.map(|entry| (path.as_ref(), entry.0.as_str(), entry.1.as_ref()))
|
|
})
|
|
})
|
|
}
|
|
|
|
pub fn local_editorconfig_settings(
|
|
&self,
|
|
worktree_id: WorktreeId,
|
|
) -> impl '_ + Iterator<Item = (LocalSettingsPath, &str, Option<&Editorconfig>)> {
|
|
let external = self
|
|
.external_configs(worktree_id)
|
|
.map(|(path, content, parsed)| {
|
|
(
|
|
LocalSettingsPath::OutsideWorktree(path.into()),
|
|
content,
|
|
parsed,
|
|
)
|
|
});
|
|
let internal = self
|
|
.internal_configs(worktree_id)
|
|
.map(|(path, content, parsed)| {
|
|
(LocalSettingsPath::InWorktree(path.into()), content, parsed)
|
|
});
|
|
external.chain(internal)
|
|
}
|
|
|
|
pub fn discover_local_external_configs_chain(
|
|
&mut self,
|
|
worktree_id: WorktreeId,
|
|
worktree_path: Arc<Path>,
|
|
fs: Arc<dyn Fs>,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
// We should only have one discovery task per worktree.
|
|
if self
|
|
.local_external_config_discovery_tasks
|
|
.contains_key(&worktree_id)
|
|
{
|
|
return;
|
|
}
|
|
|
|
let task = cx.spawn({
|
|
let fs = fs.clone();
|
|
async move |this, cx| {
|
|
let discovered_paths = {
|
|
let mut paths = Vec::new();
|
|
let mut current = worktree_path.parent().map(|p| p.to_path_buf());
|
|
while let Some(dir) = current {
|
|
let dir_path: Arc<Path> = Arc::from(dir.as_path());
|
|
let path = dir.join(EDITORCONFIG_NAME);
|
|
if fs.load(&path).await.is_ok() {
|
|
paths.push(dir_path);
|
|
}
|
|
current = dir.parent().map(|p| p.to_path_buf());
|
|
}
|
|
paths
|
|
};
|
|
|
|
this.update(cx, |this, cx| {
|
|
for dir_path in discovered_paths {
|
|
// We insert it here so that watchers can send events to appropriate worktrees.
|
|
// external_config_paths gets populated again in set_configs.
|
|
this.worktree_state
|
|
.entry(worktree_id)
|
|
.or_default()
|
|
.external_config_paths
|
|
.insert(dir_path.clone());
|
|
match this.local_external_config_watchers.entry(dir_path.clone()) {
|
|
std::collections::btree_map::Entry::Occupied(_) => {
|
|
if let Some(existing_config) = this.external_configs.get(&dir_path)
|
|
{
|
|
cx.emit(EditorconfigEvent::ExternalConfigChanged {
|
|
path: LocalSettingsPath::OutsideWorktree(dir_path),
|
|
content: Some(existing_config.0.clone()),
|
|
affected_worktree_ids: vec![worktree_id],
|
|
});
|
|
} else {
|
|
log::error!("Watcher exists for {dir_path:?} but no config found in external_configs");
|
|
}
|
|
}
|
|
std::collections::btree_map::Entry::Vacant(entry) => {
|
|
let watcher =
|
|
Self::watch_local_external_config(fs.clone(), dir_path, cx);
|
|
entry.insert(watcher);
|
|
}
|
|
}
|
|
}
|
|
})
|
|
.ok();
|
|
}
|
|
});
|
|
|
|
self.local_external_config_discovery_tasks
|
|
.insert(worktree_id, task);
|
|
}
|
|
|
|
fn watch_local_external_config(
|
|
fs: Arc<dyn Fs>,
|
|
dir_path: Arc<Path>,
|
|
cx: &mut Context<Self>,
|
|
) -> Task<()> {
|
|
let config_path = dir_path.join(EDITORCONFIG_NAME);
|
|
let (mut config_rx, watcher_task) =
|
|
watch_config_file(cx.background_executor(), fs, config_path);
|
|
|
|
cx.spawn(async move |this, cx| {
|
|
let _watcher_task = watcher_task;
|
|
while let Some(content) = config_rx.next().await {
|
|
let content = Some(content).filter(|c| !c.is_empty());
|
|
let dir_path = dir_path.clone();
|
|
this.update(cx, |this, cx| {
|
|
let affected_worktree_ids: Vec<WorktreeId> = this
|
|
.worktree_state
|
|
.iter()
|
|
.filter_map(|(id, state)| {
|
|
state
|
|
.external_config_paths
|
|
.contains(&dir_path)
|
|
.then_some(*id)
|
|
})
|
|
.collect();
|
|
|
|
cx.emit(EditorconfigEvent::ExternalConfigChanged {
|
|
path: LocalSettingsPath::OutsideWorktree(dir_path),
|
|
content,
|
|
affected_worktree_ids,
|
|
});
|
|
})
|
|
.ok();
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn properties(
|
|
&self,
|
|
for_worktree: WorktreeId,
|
|
for_path: &RelPath,
|
|
) -> Option<EditorconfigProperties> {
|
|
let mut properties = EditorconfigProperties::new();
|
|
let state = self.worktree_state.get(&for_worktree);
|
|
let internal_root_config_is_root = state
|
|
.and_then(|state| state.internal_configs.get(RelPath::empty()))
|
|
.and_then(|data| data.1.as_ref())
|
|
.is_some_and(|ec| ec.is_root);
|
|
|
|
let std_path = for_path.as_std_path();
|
|
|
|
if !internal_root_config_is_root {
|
|
for (_, _, parsed_editorconfig) in self.external_configs(for_worktree) {
|
|
if let Some(parsed_editorconfig) = parsed_editorconfig {
|
|
if parsed_editorconfig.is_root {
|
|
properties = EditorconfigProperties::new();
|
|
}
|
|
for section in &parsed_editorconfig.sections {
|
|
section.apply_to(&mut properties, std_path).log_err()?;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(state) = state {
|
|
let mut internal_configs: SmallVec<[&Editorconfig; 8]> = SmallVec::new();
|
|
|
|
for ancestor in for_path.ancestors() {
|
|
if let Some((_, parsed)) = state.internal_configs.get(ancestor) {
|
|
let config = parsed.as_ref()?;
|
|
internal_configs.push(config);
|
|
if config.is_root {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
for config in internal_configs.into_iter().rev() {
|
|
if config.is_root {
|
|
properties = EditorconfigProperties::new();
|
|
}
|
|
for section in &config.sections {
|
|
section.apply_to(&mut properties, std_path).log_err()?;
|
|
}
|
|
}
|
|
}
|
|
|
|
properties.use_fallbacks();
|
|
Some(properties)
|
|
}
|
|
}
|
|
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
impl EditorconfigStore {
|
|
pub fn test_state(&self) -> (Vec<WorktreeId>, Vec<Arc<Path>>, Vec<Arc<Path>>) {
|
|
let worktree_ids: Vec<_> = self.worktree_state.keys().copied().collect();
|
|
let external_paths: Vec<_> = self.external_configs.keys().cloned().collect();
|
|
let watcher_paths: Vec<_> = self
|
|
.local_external_config_watchers
|
|
.keys()
|
|
.cloned()
|
|
.collect();
|
|
(worktree_ids, external_paths, watcher_paths)
|
|
}
|
|
|
|
pub fn external_config_paths_for_worktree(&self, worktree_id: WorktreeId) -> Vec<Arc<Path>> {
|
|
self.worktree_state
|
|
.get(&worktree_id)
|
|
.map(|state| state.external_config_paths.iter().cloned().collect())
|
|
.unwrap_or_default()
|
|
}
|
|
}
|