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
parent d95a4f8671
commit d3cb08bf35
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 869 additions and 263 deletions

View file

@ -37,6 +37,7 @@ use smallvec::SmallVec;
use smol::future::yield_now;
use std::{
any::Any,
borrow::Cow,
cell::Cell,
cmp::{self, Ordering, Reverse},
collections::BTreeMap,
@ -2490,7 +2491,11 @@ impl BufferSnapshot {
/// Returns [`IndentSize`] for a given position that respects user settings
/// and language preferences.
pub fn language_indent_size_at<T: ToOffset>(&self, position: T, cx: &AppContext) -> IndentSize {
let settings = language_settings(self.language_at(position), self.file(), cx);
let settings = language_settings(
self.language_at(position).map(|l| l.name()),
self.file(),
cx,
);
if settings.hard_tabs {
IndentSize::tab()
} else {
@ -2823,11 +2828,15 @@ impl BufferSnapshot {
/// Returns the settings for the language at the given location.
pub fn settings_at<'a, D: ToOffset>(
&self,
&'a self,
position: D,
cx: &'a AppContext,
) -> &'a LanguageSettings {
language_settings(self.language_at(position), self.file.as_ref(), cx)
) -> Cow<'a, LanguageSettings> {
language_settings(
self.language_at(position).map(|l| l.name()),
self.file.as_ref(),
cx,
)
}
pub fn char_classifier_at<T: ToOffset>(&self, point: T) -> CharClassifier {
@ -3529,7 +3538,8 @@ impl BufferSnapshot {
ignore_disabled_for_language: bool,
cx: &AppContext,
) -> Vec<IndentGuide> {
let language_settings = language_settings(self.language(), self.file.as_ref(), cx);
let language_settings =
language_settings(self.language().map(|l| l.name()), self.file.as_ref(), cx);
let settings = language_settings.indent_guides;
if !ignore_disabled_for_language && !settings.enabled {
return Vec::new();

View file

@ -4,6 +4,10 @@ use crate::{File, Language, LanguageName, LanguageServerName};
use anyhow::Result;
use collections::{HashMap, HashSet};
use core::slice;
use ec4rs::{
property::{FinalNewline, IndentSize, IndentStyle, MaxLineLen, TabWidth, TrimTrailingWs},
Properties as EditorconfigProperties,
};
use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
use gpui::AppContext;
use itertools::{Either, Itertools};
@ -16,8 +20,10 @@ use serde::{
Deserialize, Deserializer, Serialize,
};
use serde_json::Value;
use settings::{add_references_to_properties, Settings, SettingsLocation, SettingsSources};
use std::{num::NonZeroU32, path::Path, sync::Arc};
use settings::{
add_references_to_properties, Settings, SettingsLocation, SettingsSources, SettingsStore,
};
use std::{borrow::Cow, num::NonZeroU32, path::Path, sync::Arc};
use util::serde::default_true;
/// Initializes the language settings.
@ -27,17 +33,20 @@ pub fn init(cx: &mut AppContext) {
/// Returns the settings for the specified language from the provided file.
pub fn language_settings<'a>(
language: Option<&Arc<Language>>,
file: Option<&Arc<dyn File>>,
language: Option<LanguageName>,
file: Option<&'a Arc<dyn File>>,
cx: &'a AppContext,
) -> &'a LanguageSettings {
let language_name = language.map(|l| l.name());
all_language_settings(file, cx).language(language_name.as_ref())
) -> Cow<'a, LanguageSettings> {
let location = file.map(|f| SettingsLocation {
worktree_id: f.worktree_id(cx),
path: f.path().as_ref(),
});
AllLanguageSettings::get(location, cx).language(location, language.as_ref(), cx)
}
/// Returns the settings for all languages from the provided file.
pub fn all_language_settings<'a>(
file: Option<&Arc<dyn File>>,
file: Option<&'a Arc<dyn File>>,
cx: &'a AppContext,
) -> &'a AllLanguageSettings {
let location = file.map(|f| SettingsLocation {
@ -810,13 +819,27 @@ impl InlayHintSettings {
impl AllLanguageSettings {
/// Returns the [`LanguageSettings`] for the language with the specified name.
pub fn language<'a>(&'a self, language_name: Option<&LanguageName>) -> &'a LanguageSettings {
if let Some(name) = language_name {
if let Some(overrides) = self.languages.get(name) {
return overrides;
}
pub fn language<'a>(
&'a self,
location: Option<SettingsLocation<'a>>,
language_name: Option<&LanguageName>,
cx: &'a AppContext,
) -> Cow<'a, LanguageSettings> {
let settings = language_name
.and_then(|name| self.languages.get(name))
.unwrap_or(&self.defaults);
let editorconfig_properties = location.and_then(|location| {
cx.global::<SettingsStore>()
.editorconfg_properties(location.worktree_id, location.path)
});
if let Some(editorconfig_properties) = editorconfig_properties {
let mut settings = settings.clone();
merge_with_editorconfig(&mut settings, &editorconfig_properties);
Cow::Owned(settings)
} else {
Cow::Borrowed(settings)
}
&self.defaults
}
/// Returns whether inline completions are enabled for the given path.
@ -833,6 +856,7 @@ impl AllLanguageSettings {
&self,
language: Option<&Arc<Language>>,
path: Option<&Path>,
cx: &AppContext,
) -> bool {
if let Some(path) = path {
if !self.inline_completions_enabled_for_path(path) {
@ -840,11 +864,64 @@ impl AllLanguageSettings {
}
}
self.language(language.map(|l| l.name()).as_ref())
self.language(None, language.map(|l| l.name()).as_ref(), cx)
.show_inline_completions
}
}
fn merge_with_editorconfig(settings: &mut LanguageSettings, cfg: &EditorconfigProperties) {
let max_line_length = cfg.get::<MaxLineLen>().ok().and_then(|v| match v {
MaxLineLen::Value(u) => Some(u as u32),
MaxLineLen::Off => None,
});
let tab_size = cfg.get::<IndentSize>().ok().and_then(|v| match v {
IndentSize::Value(u) => NonZeroU32::new(u as u32),
IndentSize::UseTabWidth => cfg.get::<TabWidth>().ok().and_then(|w| match w {
TabWidth::Value(u) => NonZeroU32::new(u as u32),
}),
});
let hard_tabs = cfg
.get::<IndentStyle>()
.map(|v| v.eq(&IndentStyle::Tabs))
.ok();
let ensure_final_newline_on_save = cfg
.get::<FinalNewline>()
.map(|v| match v {
FinalNewline::Value(b) => b,
})
.ok();
let remove_trailing_whitespace_on_save = cfg
.get::<TrimTrailingWs>()
.map(|v| match v {
TrimTrailingWs::Value(b) => b,
})
.ok();
let preferred_line_length = max_line_length;
let soft_wrap = if max_line_length.is_some() {
Some(SoftWrap::PreferredLineLength)
} else {
None
};
fn merge<T>(target: &mut T, value: Option<T>) {
if let Some(value) = value {
*target = value;
}
}
merge(&mut settings.tab_size, tab_size);
merge(&mut settings.hard_tabs, hard_tabs);
merge(
&mut settings.remove_trailing_whitespace_on_save,
remove_trailing_whitespace_on_save,
);
merge(
&mut settings.ensure_final_newline_on_save,
ensure_final_newline_on_save,
);
merge(&mut settings.preferred_line_length, preferred_line_length);
merge(&mut settings.soft_wrap, soft_wrap);
}
/// The kind of an inlay hint.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum InlayHintKind {