Support .editorconfig (#19455)
Closes https://github.com/zed-industries/zed/issues/8534 Supersedes https://github.com/zed-industries/zed/pull/16349 Potential concerns: * we do not follow up to the `/` when looking for `.editorconfig`, only up to the worktree root. Seems fine for most of the cases, and the rest should be solved generically later, as the same issue exists for settings.json * `fn language` in `AllLanguageSettings` is very hot, called very frequently during rendering. We accumulate and parse all `.editorconfig` file contents beforehand, but have to go over globs and match these against the path given + merge the properties still. This does not seem to be very bad, but needs more testing and potentially some extra caching. Release Notes: - Added .editorconfig support --------- Co-authored-by: Ulysse Buonomo <buonomo.ulysse@gmail.com>
This commit is contained in:
co-authored by
Ulysse Buonomo
parent
d95a4f8671
commit
d3cb08bf35
@@ -1,9 +1,10 @@
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use collections::{btree_map, hash_map, BTreeMap, HashMap};
|
||||
use ec4rs::{ConfigParser, PropertiesSource, Section};
|
||||
use fs::Fs;
|
||||
use futures::{channel::mpsc, future::LocalBoxFuture, FutureExt, StreamExt};
|
||||
use gpui::{AppContext, AsyncAppContext, BorrowAppContext, Global, Task, UpdateGlobal};
|
||||
use paths::local_settings_file_relative_path;
|
||||
use paths::{local_settings_file_relative_path, EDITORCONFIG_NAME};
|
||||
use schemars::{gen::SchemaGenerator, schema::RootSchema, JsonSchema};
|
||||
use serde::{de::DeserializeOwned, Deserialize as _, Serialize};
|
||||
use smallvec::SmallVec;
|
||||
@@ -12,12 +13,14 @@ use std::{
|
||||
fmt::Debug,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
str,
|
||||
str::{self, FromStr},
|
||||
sync::{Arc, LazyLock},
|
||||
};
|
||||
use tree_sitter::Query;
|
||||
use util::{merge_non_null_json_value_into, RangeExt, ResultExt as _};
|
||||
|
||||
pub type EditorconfigProperties = ec4rs::Properties;
|
||||
|
||||
use crate::{SettingsJsonSchemaParams, WorktreeId};
|
||||
|
||||
/// A value that can be defined as a user setting.
|
||||
@@ -167,8 +170,8 @@ pub struct SettingsStore {
|
||||
raw_user_settings: serde_json::Value,
|
||||
raw_server_settings: Option<serde_json::Value>,
|
||||
raw_extension_settings: serde_json::Value,
|
||||
raw_local_settings:
|
||||
BTreeMap<(WorktreeId, Arc<Path>), HashMap<LocalSettingsKind, serde_json::Value>>,
|
||||
raw_local_settings: BTreeMap<(WorktreeId, Arc<Path>), serde_json::Value>,
|
||||
raw_editorconfig_settings: BTreeMap<(WorktreeId, Arc<Path>), (String, Option<Editorconfig>)>,
|
||||
tab_size_callback: Option<(
|
||||
TypeId,
|
||||
Box<dyn Fn(&dyn Any) -> Option<usize> + Send + Sync + 'static>,
|
||||
@@ -179,6 +182,26 @@ pub struct SettingsStore {
|
||||
>,
|
||||
}
|
||||
|
||||
#[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(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum LocalSettingsKind {
|
||||
Settings,
|
||||
@@ -226,6 +249,7 @@ impl SettingsStore {
|
||||
raw_server_settings: None,
|
||||
raw_extension_settings: serde_json::json!({}),
|
||||
raw_local_settings: Default::default(),
|
||||
raw_editorconfig_settings: BTreeMap::default(),
|
||||
tab_size_callback: Default::default(),
|
||||
setting_file_updates_tx,
|
||||
_setting_file_updates: cx.spawn(|cx| async move {
|
||||
@@ -567,33 +591,91 @@ impl SettingsStore {
|
||||
settings_content: Option<&str>,
|
||||
cx: &mut AppContext,
|
||||
) -> std::result::Result<(), InvalidSettingsError> {
|
||||
debug_assert!(
|
||||
kind != LocalSettingsKind::Tasks,
|
||||
"Attempted to submit tasks into the settings store"
|
||||
);
|
||||
|
||||
let raw_local_settings = self
|
||||
.raw_local_settings
|
||||
.entry((root_id, directory_path.clone()))
|
||||
.or_default();
|
||||
let changed = if settings_content.is_some_and(|content| !content.is_empty()) {
|
||||
let new_contents =
|
||||
parse_json_with_comments(settings_content.unwrap()).map_err(|e| {
|
||||
InvalidSettingsError::LocalSettings {
|
||||
let mut zed_settings_changed = false;
|
||||
match (
|
||||
kind,
|
||||
settings_content
|
||||
.map(|content| content.trim())
|
||||
.filter(|content| !content.is_empty()),
|
||||
) {
|
||||
(LocalSettingsKind::Tasks, _) => {
|
||||
return Err(InvalidSettingsError::Tasks {
|
||||
message: "Attempted to submit tasks into the settings store".to_string(),
|
||||
})
|
||||
}
|
||||
(LocalSettingsKind::Settings, None) => {
|
||||
zed_settings_changed = self
|
||||
.raw_local_settings
|
||||
.remove(&(root_id, directory_path.clone()))
|
||||
.is_some()
|
||||
}
|
||||
(LocalSettingsKind::Editorconfig, None) => {
|
||||
self.raw_editorconfig_settings
|
||||
.remove(&(root_id, directory_path.clone()));
|
||||
}
|
||||
(LocalSettingsKind::Settings, Some(settings_contents)) => {
|
||||
let new_settings = parse_json_with_comments::<serde_json::Value>(settings_contents)
|
||||
.map_err(|e| InvalidSettingsError::LocalSettings {
|
||||
path: directory_path.join(local_settings_file_relative_path()),
|
||||
message: e.to_string(),
|
||||
})?;
|
||||
match self
|
||||
.raw_local_settings
|
||||
.entry((root_id, directory_path.clone()))
|
||||
{
|
||||
btree_map::Entry::Vacant(v) => {
|
||||
v.insert(new_settings);
|
||||
zed_settings_changed = true;
|
||||
}
|
||||
})?;
|
||||
if Some(&new_contents) == raw_local_settings.get(&kind) {
|
||||
false
|
||||
} else {
|
||||
raw_local_settings.insert(kind, new_contents);
|
||||
true
|
||||
btree_map::Entry::Occupied(mut o) => {
|
||||
if o.get() != &new_settings {
|
||||
o.insert(new_settings);
|
||||
zed_settings_changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(LocalSettingsKind::Editorconfig, Some(editorconfig_contents)) => {
|
||||
match self
|
||||
.raw_editorconfig_settings
|
||||
.entry((root_id, directory_path.clone()))
|
||||
{
|
||||
btree_map::Entry::Vacant(v) => match editorconfig_contents.parse() {
|
||||
Ok(new_contents) => {
|
||||
v.insert((editorconfig_contents.to_owned(), Some(new_contents)));
|
||||
}
|
||||
Err(e) => {
|
||||
v.insert((editorconfig_contents.to_owned(), None));
|
||||
return Err(InvalidSettingsError::Editorconfig {
|
||||
message: e.to_string(),
|
||||
path: directory_path.join(EDITORCONFIG_NAME),
|
||||
});
|
||||
}
|
||||
},
|
||||
btree_map::Entry::Occupied(mut o) => {
|
||||
if o.get().0 != editorconfig_contents {
|
||||
match editorconfig_contents.parse() {
|
||||
Ok(new_contents) => {
|
||||
o.insert((
|
||||
editorconfig_contents.to_owned(),
|
||||
Some(new_contents),
|
||||
));
|
||||
}
|
||||
Err(e) => {
|
||||
o.insert((editorconfig_contents.to_owned(), None));
|
||||
return Err(InvalidSettingsError::Editorconfig {
|
||||
message: e.to_string(),
|
||||
path: directory_path.join(EDITORCONFIG_NAME),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
raw_local_settings.remove(&kind).is_some()
|
||||
};
|
||||
if changed {
|
||||
|
||||
if zed_settings_changed {
|
||||
self.recompute_values(Some((root_id, &directory_path)), cx)?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -605,13 +687,10 @@ impl SettingsStore {
|
||||
cx: &mut AppContext,
|
||||
) -> Result<()> {
|
||||
let settings: serde_json::Value = serde_json::to_value(content)?;
|
||||
if settings.is_object() {
|
||||
self.raw_extension_settings = settings;
|
||||
self.recompute_values(None, cx)?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("settings must be an object"))
|
||||
}
|
||||
anyhow::ensure!(settings.is_object(), "settings must be an object");
|
||||
self.raw_extension_settings = settings;
|
||||
self.recompute_values(None, cx)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add or remove a set of local settings via a JSON string.
|
||||
@@ -625,7 +704,7 @@ impl SettingsStore {
|
||||
pub fn local_settings(
|
||||
&self,
|
||||
root_id: WorktreeId,
|
||||
) -> impl '_ + Iterator<Item = (Arc<Path>, LocalSettingsKind, String)> {
|
||||
) -> impl '_ + Iterator<Item = (Arc<Path>, String)> {
|
||||
self.raw_local_settings
|
||||
.range(
|
||||
(root_id, Path::new("").into())
|
||||
@@ -634,11 +713,23 @@ impl SettingsStore {
|
||||
Path::new("").into(),
|
||||
),
|
||||
)
|
||||
.flat_map(|((_, path), content)| {
|
||||
content.iter().filter_map(|(&kind, raw_content)| {
|
||||
let parsed_content = serde_json::to_string(raw_content).log_err()?;
|
||||
Some((path.clone(), kind, parsed_content))
|
||||
})
|
||||
.map(|((_, path), content)| (path.clone(), serde_json::to_string(content).unwrap()))
|
||||
}
|
||||
|
||||
pub fn local_editorconfig_settings(
|
||||
&self,
|
||||
root_id: WorktreeId,
|
||||
) -> impl '_ + Iterator<Item = (Arc<Path>, String, Option<Editorconfig>)> {
|
||||
self.raw_editorconfig_settings
|
||||
.range(
|
||||
(root_id, Path::new("").into())
|
||||
..(
|
||||
WorktreeId::from_usize(root_id.to_usize() + 1),
|
||||
Path::new("").into(),
|
||||
),
|
||||
)
|
||||
.map(|((_, path), (content, parsed_content))| {
|
||||
(path.clone(), content.clone(), parsed_content.clone())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -753,7 +844,7 @@ impl SettingsStore {
|
||||
&mut self,
|
||||
changed_local_path: Option<(WorktreeId, &Path)>,
|
||||
cx: &mut AppContext,
|
||||
) -> Result<(), InvalidSettingsError> {
|
||||
) -> std::result::Result<(), InvalidSettingsError> {
|
||||
// Reload the global and local values for every setting.
|
||||
let mut project_settings_stack = Vec::<DeserializedSetting>::new();
|
||||
let mut paths_stack = Vec::<Option<(WorktreeId, &Path)>>::new();
|
||||
@@ -819,69 +910,90 @@ impl SettingsStore {
|
||||
paths_stack.clear();
|
||||
project_settings_stack.clear();
|
||||
for ((root_id, directory_path), local_settings) in &self.raw_local_settings {
|
||||
if let Some(local_settings) = local_settings.get(&LocalSettingsKind::Settings) {
|
||||
// Build a stack of all of the local values for that setting.
|
||||
while let Some(prev_entry) = paths_stack.last() {
|
||||
if let Some((prev_root_id, prev_path)) = prev_entry {
|
||||
if root_id != prev_root_id || !directory_path.starts_with(prev_path) {
|
||||
paths_stack.pop();
|
||||
project_settings_stack.pop();
|
||||
continue;
|
||||
}
|
||||
// Build a stack of all of the local values for that setting.
|
||||
while let Some(prev_entry) = paths_stack.last() {
|
||||
if let Some((prev_root_id, prev_path)) = prev_entry {
|
||||
if root_id != prev_root_id || !directory_path.starts_with(prev_path) {
|
||||
paths_stack.pop();
|
||||
project_settings_stack.pop();
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
match setting_value.deserialize_setting(local_settings) {
|
||||
Ok(local_settings) => {
|
||||
paths_stack.push(Some((*root_id, directory_path.as_ref())));
|
||||
project_settings_stack.push(local_settings);
|
||||
match setting_value.deserialize_setting(local_settings) {
|
||||
Ok(local_settings) => {
|
||||
paths_stack.push(Some((*root_id, directory_path.as_ref())));
|
||||
project_settings_stack.push(local_settings);
|
||||
|
||||
// If a local settings file changed, then avoid recomputing local
|
||||
// settings for any path outside of that directory.
|
||||
if changed_local_path.map_or(
|
||||
false,
|
||||
|(changed_root_id, changed_local_path)| {
|
||||
*root_id != changed_root_id
|
||||
|| !directory_path.starts_with(changed_local_path)
|
||||
// If a local settings file changed, then avoid recomputing local
|
||||
// settings for any path outside of that directory.
|
||||
if changed_local_path.map_or(
|
||||
false,
|
||||
|(changed_root_id, changed_local_path)| {
|
||||
*root_id != changed_root_id
|
||||
|| !directory_path.starts_with(changed_local_path)
|
||||
},
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(value) = setting_value
|
||||
.load_setting(
|
||||
SettingsSources {
|
||||
default: &default_settings,
|
||||
extensions: extension_settings.as_ref(),
|
||||
user: user_settings.as_ref(),
|
||||
release_channel: release_channel_settings.as_ref(),
|
||||
server: server_settings.as_ref(),
|
||||
project: &project_settings_stack.iter().collect::<Vec<_>>(),
|
||||
},
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(value) = setting_value
|
||||
.load_setting(
|
||||
SettingsSources {
|
||||
default: &default_settings,
|
||||
extensions: extension_settings.as_ref(),
|
||||
user: user_settings.as_ref(),
|
||||
release_channel: release_channel_settings.as_ref(),
|
||||
server: server_settings.as_ref(),
|
||||
project: &project_settings_stack.iter().collect::<Vec<_>>(),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.log_err()
|
||||
{
|
||||
setting_value.set_local_value(
|
||||
*root_id,
|
||||
directory_path.clone(),
|
||||
value,
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
return Err(InvalidSettingsError::LocalSettings {
|
||||
path: directory_path.join(local_settings_file_relative_path()),
|
||||
message: error.to_string(),
|
||||
});
|
||||
cx,
|
||||
)
|
||||
.log_err()
|
||||
{
|
||||
setting_value.set_local_value(*root_id, directory_path.clone(), value);
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
return Err(InvalidSettingsError::LocalSettings {
|
||||
path: directory_path.join(local_settings_file_relative_path()),
|
||||
message: error.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn editorconfg_properties(
|
||||
&self,
|
||||
for_worktree: WorktreeId,
|
||||
for_path: &Path,
|
||||
) -> Option<EditorconfigProperties> {
|
||||
let mut properties = EditorconfigProperties::new();
|
||||
|
||||
for (directory_with_config, _, parsed_editorconfig) in
|
||||
self.local_editorconfig_settings(for_worktree)
|
||||
{
|
||||
if !for_path.starts_with(&directory_with_config) {
|
||||
properties.use_fallbacks();
|
||||
return Some(properties);
|
||||
}
|
||||
let parsed_editorconfig = parsed_editorconfig?;
|
||||
if parsed_editorconfig.is_root {
|
||||
properties = EditorconfigProperties::new();
|
||||
}
|
||||
for section in parsed_editorconfig.sections {
|
||||
section.apply_to(&mut properties, for_path).log_err()?;
|
||||
}
|
||||
}
|
||||
|
||||
properties.use_fallbacks();
|
||||
Some(properties)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -890,6 +1002,8 @@ pub enum InvalidSettingsError {
|
||||
UserSettings { message: String },
|
||||
ServerSettings { message: String },
|
||||
DefaultSettings { message: String },
|
||||
Editorconfig { path: PathBuf, message: String },
|
||||
Tasks { message: String },
|
||||
}
|
||||
|
||||
impl std::fmt::Display for InvalidSettingsError {
|
||||
@@ -898,8 +1012,10 @@ impl std::fmt::Display for InvalidSettingsError {
|
||||
InvalidSettingsError::LocalSettings { message, .. }
|
||||
| InvalidSettingsError::UserSettings { message }
|
||||
| InvalidSettingsError::ServerSettings { message }
|
||||
| InvalidSettingsError::DefaultSettings { message } => {
|
||||
write!(f, "{}", message)
|
||||
| InvalidSettingsError::DefaultSettings { message }
|
||||
| InvalidSettingsError::Tasks { message }
|
||||
| InvalidSettingsError::Editorconfig { message, .. } => {
|
||||
write!(f, "{message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user