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:
Kirill Bulatov
2024-10-21 13:05:30 +03:00
committed by GitHub
co-authored by Ulysse Buonomo
parent d95a4f8671
commit d3cb08bf35
30 changed files with 869 additions and 263 deletions
+209 -93
View File
@@ -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}")
}
}
}