Add the ability for extensions to provide language settings (#10296)

This PR adds the ability for extensions to provide certain language
settings via the language `config.toml`.

These settings are then merged in with the rest of the settings when the
language is loaded from the extension.

The language settings that are available are:

- `tab_size`
- `hard_tabs`
- `soft_wrap`

Additionally, for bundled languages we moved these settings out of the
`settings/default.json` and into their respective `config.toml`s .

For languages currently provided by extensions, we are leaving the
values in the `settings/default.json` temporarily until all released
versions of Zed are able to load these settings from the extension.

---

Along the way we ended up refactoring the `Settings::load` method
slightly, introducing a new `SettingsSources` struct to better convey
where the settings are being loaded from.

This makes it easier to load settings from specific locations/sets of
locations in an explicit way.

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
This commit is contained in:
Marshall Bowers 2024-04-08 19:17:12 -04:00 committed by GitHub
parent 4a3032c5e5
commit 7c5bc3c26f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 349 additions and 338 deletions

View File

@ -556,18 +556,10 @@
"C": {
"format_on_save": "off"
},
"Plain Text": {
"soft_wrap": "preferred_line_length"
},
"Elixir": {
"tab_size": 2
},
"Gleam": {
"tab_size": 2
},
"Go": {
"tab_size": 4,
"hard_tabs": true,
"code_actions_on_format": {
"source.organizeImports": true
}
@ -575,34 +567,6 @@
"Make": {
"hard_tabs": true
},
"Markdown": {
"tab_size": 2,
"soft_wrap": "preferred_line_length"
},
"JavaScript": {
"tab_size": 2
},
"Terraform": {
"tab_size": 2
},
"TypeScript": {
"tab_size": 2
},
"TSX": {
"tab_size": 2
},
"YAML": {
"tab_size": 2
},
"JSON": {
"tab_size": 2
},
"OCaml": {
"tab_size": 2
},
"OCaml Interface": {
"tab_size": 2
},
"Prisma": {
"tab_size": 2
}

View File

@ -10,7 +10,7 @@ use serde::{
de::{self, Visitor},
Deserialize, Deserializer, Serialize, Serializer,
};
use settings::Settings;
use settings::{Settings, SettingsSources};
#[derive(Clone, Debug, Default, PartialEq)]
pub enum ZedDotDevModel {
@ -332,13 +332,12 @@ impl Settings for AssistantSettings {
type FileContent = AssistantSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
sources: SettingsSources<Self::FileContent>,
_: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
let mut settings = AssistantSettings::default();
for value in [default_value].iter().chain(user_values) {
for value in sources.defaults_and_customizations() {
let value = value.upgrade();
merge(&mut settings.enabled, value.enabled);
merge(&mut settings.button, value.button);

View File

@ -17,7 +17,7 @@ use serde::Deserialize;
use serde_derive::Serialize;
use smol::io::AsyncReadExt;
use settings::{Settings, SettingsStore};
use settings::{Settings, SettingsSources, SettingsStore};
use smol::{fs::File, process::Command};
use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
@ -91,13 +91,12 @@ impl Settings for AutoUpdateSetting {
type FileContent = AutoUpdateSettingOverride;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &mut AppContext,
) -> Result<Self> {
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
Ok(Self(
Self::json_merge(default_value, user_values)?
sources
.release_channel
.or(sources.user)
.unwrap_or(sources.default)
.0
.ok_or_else(Self::missing_default)?,
))

View File

@ -2,7 +2,7 @@ use anyhow::Result;
use gpui::AppContext;
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
use settings::Settings;
use settings::{Settings, SettingsSources};
#[derive(Deserialize, Debug)]
pub struct CallSettings {
@ -29,14 +29,7 @@ impl Settings for CallSettings {
type FileContent = CallSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_cx: &mut AppContext,
) -> Result<Self>
where
Self: Sized,
{
Self::load_via_json_merge(default_value, user_values)
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
sources.json_merge()
}
}

View File

@ -28,7 +28,7 @@ use release_channel::{AppVersion, ReleaseChannel};
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};
use settings::{Settings, SettingsSources, SettingsStore};
use std::fmt;
use std::{
any::TypeId,
@ -97,15 +97,11 @@ impl Settings for ClientSettings {
type FileContent = ClientSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &mut AppContext,
) -> Result<Self>
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self>
where
Self: Sized,
{
let mut result = Self::load_via_json_merge(default_value, user_values)?;
let mut result = sources.json_merge::<Self>()?;
if let Some(server_url) = &*ZED_SERVER_URL {
result.server_url = server_url.clone()
}
@ -427,21 +423,19 @@ impl settings::Settings for TelemetrySettings {
type FileContent = TelemetrySettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &mut AppContext,
) -> Result<Self> {
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
Ok(Self {
diagnostics: user_values.first().and_then(|v| v.diagnostics).unwrap_or(
default_value
diagnostics: sources.user.as_ref().and_then(|v| v.diagnostics).unwrap_or(
sources
.default
.diagnostics
.ok_or_else(Self::missing_default)?,
),
metrics: user_values
.first()
metrics: sources
.user
.as_ref()
.and_then(|v| v.metrics)
.unwrap_or(default_value.metrics.ok_or_else(Self::missing_default)?),
.unwrap_or(sources.default.metrics.ok_or_else(Self::missing_default)?),
})
}
}

View File

@ -2,7 +2,7 @@ use anyhow;
use gpui::Pixels;
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
use settings::Settings;
use settings::{Settings, SettingsSources};
use workspace::dock::DockPosition;
#[derive(Deserialize, Debug)]
@ -53,48 +53,52 @@ pub struct MessageEditorSettings {
impl Settings for CollaborationPanelSettings {
const KEY: Option<&'static str> = Some("collaboration_panel");
type FileContent = PanelSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
sources: SettingsSources<Self::FileContent>,
_: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
sources.json_merge()
}
}
impl Settings for ChatPanelSettings {
const KEY: Option<&'static str> = Some("chat_panel");
type FileContent = PanelSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
sources: SettingsSources<Self::FileContent>,
_: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
sources.json_merge()
}
}
impl Settings for NotificationPanelSettings {
const KEY: Option<&'static str> = Some("notification_panel");
type FileContent = PanelSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
sources: SettingsSources<Self::FileContent>,
_: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
sources.json_merge()
}
}
impl Settings for MessageEditorSettings {
const KEY: Option<&'static str> = Some("message_editor");
type FileContent = MessageEditorSettings;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
sources: SettingsSources<Self::FileContent>,
_: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
sources.json_merge()
}
}

View File

@ -1,5 +1,8 @@
use anyhow::Result;
use gpui::AppContext;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
#[derive(Deserialize, Debug)]
pub struct ProjectDiagnosticsSettings {
@ -15,18 +18,11 @@ pub struct ProjectDiagnosticsSettingsContent {
include_warnings: Option<bool>,
}
impl settings::Settings for ProjectDiagnosticsSettings {
impl Settings for ProjectDiagnosticsSettings {
const KEY: Option<&'static str> = Some("diagnostics");
type FileContent = ProjectDiagnosticsSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_cx: &mut gpui::AppContext,
) -> anyhow::Result<Self>
where
Self: Sized,
{
Self::load_via_json_merge(default_value, user_values)
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
sources.json_merge()
}
}

View File

@ -1,6 +1,7 @@
use gpui::AppContext;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
use settings::{Settings, SettingsSources};
#[derive(Deserialize, Clone)]
pub struct EditorSettings {
@ -224,10 +225,9 @@ impl Settings for EditorSettings {
type FileContent = EditorSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &mut gpui::AppContext,
sources: SettingsSources<Self::FileContent>,
_: &mut AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
sources.json_merge()
}
}

View File

@ -3,7 +3,7 @@ use collections::HashMap;
use gpui::AppContext;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
use settings::{Settings, SettingsSources};
use std::sync::Arc;
#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)]
@ -26,14 +26,7 @@ impl Settings for ExtensionSettings {
type FileContent = Self;
fn load(
_default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_cx: &mut AppContext,
) -> Result<Self>
where
Self: Sized,
{
Ok(user_values.get(0).copied().cloned().unwrap_or_default())
fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut AppContext) -> Result<Self> {
Ok(sources.user.cloned().unwrap_or_default())
}
}

View File

@ -5,7 +5,7 @@ use editor::Editor;
use gpui::{actions, AppContext, ViewContext, WindowContext};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
use settings::{Settings, SettingsSources};
use std::{
fs::OpenOptions,
path::{Path, PathBuf},
@ -50,12 +50,8 @@ impl settings::Settings for JournalSettings {
type FileContent = Self;
fn load(
defaults: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &mut AppContext,
) -> Result<Self> {
Self::load_via_json_merge(defaults, user_values)
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
sources.json_merge()
}
}

View File

@ -38,6 +38,7 @@ use schemars::{
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use serde_json::Value;
use smol::future::FutureExt as _;
use std::num::NonZeroU32;
use std::{
any::Any,
cell::RefCell,
@ -73,6 +74,8 @@ pub use syntax_map::{OwnedSyntaxLayer, SyntaxLayer};
pub use text::LineEnding;
pub use tree_sitter::{Parser, Tree};
use crate::language_settings::SoftWrap;
/// Initializes the `language` crate.
///
/// This should be called before making use of items from the create.
@ -99,6 +102,7 @@ lazy_static! {
pub static ref PLAIN_TEXT: Arc<Language> = Arc::new(Language::new(
LanguageConfig {
name: "Plain Text".into(),
soft_wrap: Some(SoftWrap::PreferredLineLength),
..Default::default()
},
None,
@ -576,6 +580,17 @@ pub struct LanguageConfig {
/// The names of any Prettier plugins that should be used for this language.
#[serde(default)]
pub prettier_plugins: Vec<Arc<str>>,
/// Whether to indent lines using tab characters, as opposed to multiple
/// spaces.
#[serde(default)]
pub hard_tabs: Option<bool>,
/// How many columns a tab should occupy.
#[serde(default)]
pub tab_size: Option<NonZeroU32>,
/// How to soft-wrap long lines of text.
#[serde(default)]
pub soft_wrap: Option<SoftWrap>,
}
#[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)]
@ -660,6 +675,9 @@ impl Default for LanguageConfig {
prettier_parser_name: None,
prettier_plugins: Default::default(),
collapsed_placeholder: Default::default(),
hard_tabs: Default::default(),
tab_size: Default::default(),
soft_wrap: Default::default(),
}
}
}

View File

@ -1,3 +1,4 @@
use crate::language_settings::{AllLanguageSettingsContent, LanguageSettingsContent};
use crate::{
language_settings::all_language_settings, task_context::ContextProvider, CachedLspAdapter,
File, Language, LanguageConfig, LanguageId, LanguageMatcher, LanguageServerName, LspAdapter,
@ -38,6 +39,7 @@ pub struct LanguageRegistry {
struct LanguageRegistryState {
next_language_server_id: usize,
languages: Vec<Arc<Language>>,
language_settings: AllLanguageSettingsContent,
available_languages: Vec<AvailableLanguage>,
grammars: HashMap<Arc<str>, AvailableGrammar>,
lsp_adapters: HashMap<Arc<str>, Vec<Arc<CachedLspAdapter>>>,
@ -145,6 +147,7 @@ impl LanguageRegistry {
languages: Vec::new(),
available_languages: Vec::new(),
grammars: Default::default(),
language_settings: Default::default(),
loading_languages: Default::default(),
lsp_adapters: Default::default(),
subscription: watch::channel(),
@ -338,6 +341,10 @@ impl LanguageRegistry {
*state.subscription.0.borrow_mut() = ();
}
pub fn language_settings(&self) -> AllLanguageSettingsContent {
self.state.read().language_settings.clone()
}
pub fn language_names(&self) -> Vec<String> {
let state = self.state.read();
let mut result = state
@ -854,6 +861,16 @@ impl LanguageRegistryState {
if let Some(theme) = self.theme.as_ref() {
language.set_theme(theme.syntax());
}
self.language_settings.languages.insert(
language.name(),
LanguageSettingsContent {
tab_size: language.config.tab_size,
hard_tabs: language.config.hard_tabs,
soft_wrap: language.config.soft_wrap,
..Default::default()
}
.clone(),
);
self.languages.push(language);
self.version += 1;
*self.subscription.0.borrow_mut() = ();

View File

@ -10,7 +10,7 @@ use schemars::{
JsonSchema,
};
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsLocation};
use settings::{Settings, SettingsLocation, SettingsSources};
use std::{num::NonZeroU32, path::Path, sync::Arc};
impl<'a> Into<SettingsLocation<'a>> for &'a dyn File {
@ -119,7 +119,7 @@ pub struct CopilotSettings {
}
/// The settings for all languages.
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct AllLanguageSettingsContent {
/// The settings for enabling/disabling features.
#[serde(default)]
@ -140,7 +140,7 @@ pub struct AllLanguageSettingsContent {
}
/// The settings for a particular language.
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
pub struct LanguageSettingsContent {
/// How many columns a tab should occupy.
///
@ -249,7 +249,7 @@ pub struct LanguageSettingsContent {
}
/// The contents of the GitHub Copilot settings.
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
pub struct CopilotSettingsContent {
/// A list of globs representing files that Copilot should be disabled for.
#[serde(default)]
@ -257,7 +257,7 @@ pub struct CopilotSettingsContent {
}
/// The settings for enabling/disabling features.
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub struct FeaturesContent {
/// Whether the GitHub Copilot feature is enabled.
@ -473,11 +473,9 @@ impl settings::Settings for AllLanguageSettings {
type FileContent = AllLanguageSettingsContent;
fn load(
default_value: &Self::FileContent,
user_settings: &[&Self::FileContent],
_: &mut AppContext,
) -> Result<Self> {
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
let default_value = sources.default;
// A default is provided for all settings.
let mut defaults: LanguageSettings =
serde_json::from_value(serde_json::to_value(&default_value.defaults)?)?;
@ -500,7 +498,8 @@ impl settings::Settings for AllLanguageSettings {
.and_then(|c| c.disabled_globs.as_ref())
.ok_or_else(Self::missing_default)?;
for user_settings in user_settings {
let mut file_types: HashMap<Arc<str>, Vec<String>> = HashMap::default();
for user_settings in sources.customizations() {
if let Some(copilot) = user_settings.features.as_ref().and_then(|f| f.copilot) {
copilot_enabled = copilot;
}
@ -528,11 +527,8 @@ impl settings::Settings for AllLanguageSettings {
user_language_settings,
);
}
}
let mut file_types: HashMap<Arc<str>, Vec<String>> = HashMap::default();
for user_file_types in user_settings.iter().map(|s| &s.file_types) {
for (language, suffixes) in user_file_types {
for (language, suffixes) in &user_settings.file_types {
file_types
.entry(language.clone())
.or_default()

View File

@ -2,12 +2,13 @@ use anyhow::{anyhow, bail, Context, Result};
use async_trait::async_trait;
use collections::HashMap;
use futures::StreamExt;
use gpui::AppContext;
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
use lsp::{CodeActionKind, LanguageServerBinary};
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
use serde_json::json;
use settings::Settings;
use settings::{Settings, SettingsSources};
use smol::{fs, fs::File};
use std::{any::Any, env::consts, ffi::OsString, path::PathBuf, sync::Arc};
use util::{
@ -31,15 +32,8 @@ impl Settings for DenoSettings {
type FileContent = DenoSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &mut gpui::AppContext,
) -> Result<Self>
where
Self: Sized,
{
Self::load_via_json_merge(default_value, user_values)
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
sources.json_merge()
}
}

View File

@ -1,14 +1,14 @@
use anyhow::{anyhow, bail, Context, Result};
use async_trait::async_trait;
use futures::StreamExt;
use gpui::{AsyncAppContext, Task};
use gpui::{AppContext, AsyncAppContext, Task};
pub use language::*;
use lsp::{CompletionItemKind, LanguageServerBinary, SymbolKind};
use project::project_settings::ProjectSettings;
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
use serde_json::Value;
use settings::Settings;
use settings::{Settings, SettingsSources};
use smol::fs::{self, File};
use std::{
any::Any,
@ -56,15 +56,8 @@ impl Settings for ElixirSettings {
type FileContent = ElixirSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &mut gpui::AppContext,
) -> Result<Self>
where
Self: Sized,
{
Self::load_via_json_merge(default_value, user_values)
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
sources.json_merge()
}
}

View File

@ -10,6 +10,7 @@ brackets = [
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string", "comment"] },
{ start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] },
]
tab_size = 2
scope_opt_in_language_servers = ["tailwindcss-language-server"]
[overrides.string]

View File

@ -11,3 +11,5 @@ brackets = [
{ start = "'", end = "'", close = true, newline = false, not_in = ["comment", "string"] },
{ start = "/*", end = " */", close = true, newline = false, not_in = ["comment", "string"] },
]
tab_size = 4
hard_tabs = true

View File

@ -15,6 +15,7 @@ brackets = [
{ start = "/*", end = " */", close = true, newline = false, not_in = ["comment", "string"] },
]
word_characters = ["$", "#"]
tab_size = 2
scope_opt_in_language_servers = ["tailwindcss-language-server"]
prettier_parser_name = "babel"

View File

@ -9,3 +9,4 @@ brackets = [
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
]
prettier_parser_name = "json"
tab_size = 2

View File

@ -1,11 +1,12 @@
use anyhow::Context;
use gpui::AppContext;
use gpui::{AppContext, BorrowAppContext};
pub use language::*;
use node_runtime::NodeRuntime;
use rust_embed::RustEmbed;
use settings::Settings;
use settings::{Settings, SettingsStore};
use smol::stream::StreamExt;
use std::{str, sync::Arc};
use util::asset_str;
use util::{asset_str, ResultExt};
use crate::{elixir::elixir_task_context, rust::RustContextProvider};
@ -327,6 +328,27 @@ pub fn init(
"Svelte".into(),
Arc::new(tailwind::TailwindLspAdapter::new(node_runtime.clone())),
);
let mut subscription = languages.subscribe();
let mut prev_language_settings = languages.language_settings();
cx.spawn(|cx| async move {
while subscription.next().await.is_some() {
let language_settings = languages.language_settings();
if language_settings != prev_language_settings {
cx.update(|cx| {
cx.update_global(|settings: &mut SettingsStore, cx| {
settings
.set_extension_settings(language_settings.clone(), cx)
.log_err();
});
})?;
prev_language_settings = language_settings;
}
}
anyhow::Ok(())
})
.detach();
}
#[cfg(any(test, feature = "test-support"))]

View File

@ -12,3 +12,6 @@ brackets = [
{ start = "`", end = "`", close = false, newline = false },
]
prettier_parser_name = "markdown"
tab_size = 2
soft_wrap = "preferred_line_length"

View File

@ -10,3 +10,4 @@ brackets = [
{ start = "[", end = "]", close = true, newline = true },
{ start = "(", end = ")", close = true, newline = true }
]
tab_size = 2

View File

@ -11,3 +11,4 @@ brackets = [
{ start = "(", end = ")", close = true, newline = true },
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] }
]
tab_size = 2

View File

@ -12,3 +12,4 @@ brackets = [
{ start = "'", end = "'", close = true, newline = false, not_in = ["comment", "string"] },
{ start = "/*", end = " */", close = true, newline = false, not_in = ["comment", "string"] },
]
tab_size = 2

View File

@ -16,6 +16,7 @@ brackets = [
word_characters = ["#", "$"]
scope_opt_in_language_servers = ["tailwindcss-language-server"]
prettier_parser_name = "typescript"
tab_size = 2
[overrides.element]
line_comments = { remove = true }

View File

@ -15,3 +15,4 @@ brackets = [
]
word_characters = ["#", "$"]
prettier_parser_name = "typescript"
tab_size = 2

View File

@ -11,3 +11,4 @@ brackets = [
increase_indent_pattern = ":\\s*[|>]?\\s*$"
prettier_parser_name = "yaml"
tab_size = 2

View File

@ -2,7 +2,7 @@ use collections::HashMap;
use gpui::AppContext;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
use settings::{Settings, SettingsSources};
use std::sync::Arc;
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
@ -61,10 +61,9 @@ impl Settings for ProjectSettings {
type FileContent = Self;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
sources: SettingsSources<Self::FileContent>,
_: &mut AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
sources.json_merge()
}
}

View File

@ -2,7 +2,7 @@ use anyhow;
use gpui::Pixels;
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};
use settings::Settings;
use settings::{Settings, SettingsSources};
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
@ -62,10 +62,9 @@ impl Settings for ProjectPanelSettings {
type FileContent = ProjectPanelSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
sources: SettingsSources<Self::FileContent>,
_: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
sources.json_merge()
}
}

View File

@ -8,7 +8,9 @@ use util::asset_str;
pub use keymap_file::KeymapFile;
pub use settings_file::*;
pub use settings_store::{Settings, SettingsJsonSchemaParams, SettingsLocation, SettingsStore};
pub use settings_store::{
Settings, SettingsJsonSchemaParams, SettingsLocation, SettingsSources, SettingsStore,
};
#[derive(RustEmbed)]
#[folder = "../../assets"]

View File

@ -29,14 +29,7 @@ pub trait Settings: 'static + Send + Sync {
/// The logic for combining together values from one or more JSON files into the
/// final value for this setting.
///
/// The user values are ordered from least specific (the global settings file)
/// to most specific (the innermost local settings file).
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
cx: &mut AppContext,
) -> Result<Self>
fn load(sources: SettingsSources<Self::FileContent>, cx: &mut AppContext) -> Result<Self>
where
Self: Sized;
@ -48,31 +41,6 @@ pub trait Settings: 'static + Send + Sync {
generator.root_schema_for::<Self::FileContent>()
}
fn json_merge(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
) -> Result<Self::FileContent> {
let mut merged = serde_json::Value::Null;
for value in [default_value].iter().chain(user_values) {
merge_non_null_json_value_into(serde_json::to_value(value).unwrap(), &mut merged);
}
Ok(serde_json::from_value(merged)?)
}
fn load_via_json_merge(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
) -> Result<Self>
where
Self: DeserializeOwned,
{
let mut merged = serde_json::Value::Null;
for value in [default_value].iter().chain(user_values) {
merge_non_null_json_value_into(serde_json::to_value(value).unwrap(), &mut merged);
}
Ok(serde_json::from_value(merged)?)
}
fn missing_default() -> anyhow::Error {
anyhow::anyhow!("missing default")
}
@ -119,6 +87,48 @@ pub trait Settings: 'static + Send + Sync {
}
}
#[derive(Clone, Copy, Debug)]
pub struct SettingsSources<'a, T> {
/// The default Zed settings.
pub default: &'a T,
/// Settings provided by extensions.
pub extensions: Option<&'a T>,
/// The user settings.
pub user: Option<&'a T>,
/// The user settings for the current release channel.
pub release_channel: Option<&'a T>,
/// The project settings, ordered from least specific to most specific.
pub project: &'a [&'a T],
}
impl<'a, T: Serialize> SettingsSources<'a, T> {
/// Returns an iterator over the default settings as well as all settings customizations.
pub fn defaults_and_customizations(&self) -> impl Iterator<Item = &T> {
[self.default].into_iter().chain(self.customizations())
}
/// Returns an iterator over all of the settings customizations.
pub fn customizations(&self) -> impl Iterator<Item = &T> {
self.extensions
.into_iter()
.chain(self.user)
.chain(self.release_channel)
.chain(self.project.iter().copied())
}
/// Returns the settings after performing a JSON merge of the customizations into the
/// default settings.
///
/// More-specific customizations win out over the less-specific ones.
pub fn json_merge<O: DeserializeOwned>(&self) -> Result<O> {
let mut merged = serde_json::Value::Null;
for value in self.defaults_and_customizations() {
merge_non_null_json_value_into(serde_json::to_value(value).unwrap(), &mut merged);
}
Ok(serde_json::from_value(merged)?)
}
}
#[derive(Clone, Copy)]
pub struct SettingsLocation<'a> {
pub worktree_id: usize,
@ -136,6 +146,7 @@ pub struct SettingsStore {
setting_values: HashMap<TypeId, Box<dyn AnySettingValue>>,
raw_default_settings: serde_json::Value,
raw_user_settings: serde_json::Value,
raw_extension_settings: serde_json::Value,
raw_local_settings: BTreeMap<(usize, Arc<Path>), serde_json::Value>,
tab_size_callback: Option<(
TypeId,
@ -151,6 +162,7 @@ impl Default for SettingsStore {
setting_values: Default::default(),
raw_default_settings: serde_json::json!({}),
raw_user_settings: serde_json::json!({}),
raw_extension_settings: serde_json::json!({}),
raw_local_settings: Default::default(),
tab_size_callback: Default::default(),
}
@ -169,8 +181,7 @@ trait AnySettingValue: 'static + Send + Sync {
fn deserialize_setting(&self, json: &serde_json::Value) -> Result<DeserializedSetting>;
fn load_setting(
&self,
default_value: &DeserializedSetting,
custom: &[DeserializedSetting],
sources: SettingsSources<DeserializedSetting>,
cx: &mut AppContext,
) -> Result<Box<dyn Any>>;
fn value_for_path(&self, path: Option<SettingsLocation>) -> &dyn Any;
@ -204,29 +215,35 @@ impl SettingsStore {
.deserialize_setting(&self.raw_default_settings)
.log_err()
{
let mut user_values_stack = Vec::new();
if let Some(user_settings) = setting_value
let user_value = setting_value
.deserialize_setting(&self.raw_user_settings)
.log_err()
{
user_values_stack = vec![user_settings];
}
.log_err();
let mut release_channel_value = None;
if let Some(release_settings) = &self
.raw_user_settings
.get(release_channel::RELEASE_CHANNEL.dev_name())
{
if let Some(release_settings) = setting_value
release_channel_value = setting_value
.deserialize_setting(release_settings)
.log_err()
{
user_values_stack.push(release_settings);
}
.log_err();
}
let extension_value = setting_value
.deserialize_setting(&self.raw_extension_settings)
.log_err();
if let Some(setting) = setting_value
.load_setting(&default_settings, &user_values_stack, cx)
.load_setting(
SettingsSources {
default: &default_settings,
release_channel: release_channel_value.as_ref(),
extensions: extension_value.as_ref(),
user: user_value.as_ref(),
project: &[],
},
cx,
)
.context("A default setting must be added to the `default.json` file")
.log_err()
{
@ -425,6 +442,21 @@ impl SettingsStore {
Ok(())
}
pub fn set_extension_settings<T: Serialize>(
&mut self,
content: T,
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"))
}
}
/// Add or remove a set of local settings via a JSON string.
pub fn clear_local_settings(&mut self, root_id: usize, cx: &mut AppContext) -> Result<()> {
self.raw_local_settings.retain(|k, _| k.0 != root_id);
@ -551,22 +583,20 @@ impl SettingsStore {
cx: &mut AppContext,
) -> Result<()> {
// Reload the global and local values for every setting.
let mut user_settings_stack = Vec::<DeserializedSetting>::new();
let mut project_settings_stack = Vec::<DeserializedSetting>::new();
let mut paths_stack = Vec::<Option<(usize, &Path)>>::new();
for setting_value in self.setting_values.values_mut() {
let default_settings = setting_value.deserialize_setting(&self.raw_default_settings)?;
user_settings_stack.clear();
paths_stack.clear();
let extension_settings = setting_value
.deserialize_setting(&self.raw_extension_settings)
.log_err();
if let Some(user_settings) = setting_value
let user_settings = setting_value
.deserialize_setting(&self.raw_user_settings)
.log_err()
{
user_settings_stack.push(user_settings);
paths_stack.push(None);
}
.log_err();
let mut release_channel_settings = None;
if let Some(release_settings) = &self
.raw_user_settings
.get(release_channel::RELEASE_CHANNEL.dev_name())
@ -575,15 +605,25 @@ impl SettingsStore {
.deserialize_setting(release_settings)
.log_err()
{
user_settings_stack.push(release_settings);
paths_stack.push(None);
release_channel_settings = Some(release_settings);
}
}
// If the global settings file changed, reload the global value for the field.
project_settings_stack.clear();
paths_stack.clear();
if changed_local_path.is_none() {
if let Some(value) = setting_value
.load_setting(&default_settings, &user_settings_stack, cx)
.load_setting(
SettingsSources {
default: &default_settings,
extensions: extension_settings.as_ref(),
user: user_settings.as_ref(),
release_channel: release_channel_settings.as_ref(),
project: &[],
},
cx,
)
.log_err()
{
setting_value.set_global_value(value);
@ -597,7 +637,7 @@ impl SettingsStore {
if let Some((prev_root_id, prev_path)) = prev_entry {
if root_id != prev_root_id || !path.starts_with(prev_path) {
paths_stack.pop();
user_settings_stack.pop();
project_settings_stack.pop();
continue;
}
}
@ -608,7 +648,7 @@ impl SettingsStore {
setting_value.deserialize_setting(local_settings).log_err()
{
paths_stack.push(Some((*root_id, path.as_ref())));
user_settings_stack.push(local_settings);
project_settings_stack.push(local_settings);
// If a local settings file changed, then avoid recomputing local
// settings for any path outside of that directory.
@ -619,7 +659,16 @@ impl SettingsStore {
}
if let Some(value) = setting_value
.load_setting(&default_settings, &user_settings_stack, cx)
.load_setting(
SettingsSources {
default: &default_settings,
extensions: extension_settings.as_ref(),
user: user_settings.as_ref(),
release_channel: release_channel_settings.as_ref(),
project: &project_settings_stack.iter().collect::<Vec<_>>(),
},
cx,
)
.log_err()
{
setting_value.set_local_value(*root_id, path.clone(), value);
@ -660,16 +709,30 @@ impl<T: Settings> AnySettingValue for SettingValue<T> {
fn load_setting(
&self,
default_value: &DeserializedSetting,
user_values: &[DeserializedSetting],
values: SettingsSources<DeserializedSetting>,
cx: &mut AppContext,
) -> Result<Box<dyn Any>> {
let default_value = default_value.0.downcast_ref::<T::FileContent>().unwrap();
let values: SmallVec<[&T::FileContent; 6]> = user_values
.iter()
.map(|value| value.0.downcast_ref().unwrap())
.collect();
Ok(Box::new(T::load(default_value, &values, cx)?))
Ok(Box::new(T::load(
SettingsSources {
default: values.default.0.downcast_ref::<T::FileContent>().unwrap(),
extensions: values
.extensions
.map(|value| value.0.downcast_ref::<T::FileContent>().unwrap()),
user: values
.user
.map(|value| value.0.downcast_ref::<T::FileContent>().unwrap()),
release_channel: values
.release_channel
.map(|value| value.0.downcast_ref::<T::FileContent>().unwrap()),
project: values
.project
.iter()
.map(|value| value.0.downcast_ref().unwrap())
.collect::<SmallVec<[_; 3]>>()
.as_slice(),
},
cx,
)?))
}
fn deserialize_setting(&self, mut json: &serde_json::Value) -> Result<DeserializedSetting> {
@ -1277,12 +1340,8 @@ mod tests {
const KEY: Option<&'static str> = Some("user");
type FileContent = UserSettingsJson;
fn load(
default_value: &UserSettingsJson,
user_values: &[&UserSettingsJson],
_: &mut AppContext,
) -> Result<Self> {
Self::load_via_json_merge(default_value, user_values)
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
sources.json_merge()
}
}
@ -1293,12 +1352,8 @@ mod tests {
const KEY: Option<&'static str> = Some("turbo");
type FileContent = Option<bool>;
fn load(
default_value: &Option<bool>,
user_values: &[&Option<bool>],
_: &mut AppContext,
) -> Result<Self> {
Self::load_via_json_merge(default_value, user_values)
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
sources.json_merge()
}
}
@ -1321,12 +1376,8 @@ mod tests {
type FileContent = MultiKeySettingsJson;
fn load(
default_value: &MultiKeySettingsJson,
user_values: &[&MultiKeySettingsJson],
_: &mut AppContext,
) -> Result<Self> {
Self::load_via_json_merge(default_value, user_values)
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
sources.json_merge()
}
}
@ -1354,12 +1405,8 @@ mod tests {
type FileContent = JournalSettingsJson;
fn load(
default_value: &JournalSettingsJson,
user_values: &[&JournalSettingsJson],
_: &mut AppContext,
) -> Result<Self> {
Self::load_via_json_merge(default_value, user_values)
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
sources.json_merge()
}
}
@ -1380,8 +1427,8 @@ mod tests {
type FileContent = Self;
fn load(default_value: &Self, user_values: &[&Self], _: &mut AppContext) -> Result<Self> {
Self::load_via_json_merge(default_value, user_values)
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
sources.json_merge()
}
}
}

View File

@ -1,5 +1,6 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources};
#[derive(Serialize, Deserialize, PartialEq, Default)]
pub(crate) struct TaskSettings {
@ -13,22 +14,15 @@ pub(crate) struct TaskSettingsContent {
show_status_indicator: Option<bool>,
}
impl settings::Settings for TaskSettings {
impl Settings for TaskSettings {
const KEY: Option<&'static str> = Some("task");
type FileContent = TaskSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
sources: SettingsSources<Self::FileContent>,
_: &mut gpui::AppContext,
) -> gpui::Result<Self>
where
Self: Sized,
{
let this = Self::json_merge(default_value, user_values)?;
Ok(Self {
show_status_indicator: this.show_status_indicator.unwrap_or(true),
})
) -> gpui::Result<Self> {
sources.json_merge()
}
}

View File

@ -7,7 +7,7 @@ use schemars::{
};
use serde_derive::{Deserialize, Serialize};
use serde_json::Value;
use settings::SettingsJsonSchemaParams;
use settings::{SettingsJsonSchemaParams, SettingsSources};
use std::path::PathBuf;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
@ -171,12 +171,12 @@ impl settings::Settings for TerminalSettings {
type FileContent = TerminalSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
sources: SettingsSources<Self::FileContent>,
_: &mut AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
sources.json_merge()
}
fn json_schema(
generator: &mut SchemaGenerator,
params: &SettingsJsonSchemaParams,

View File

@ -14,7 +14,7 @@ use schemars::{
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use settings::{Settings, SettingsJsonSchemaParams};
use settings::{Settings, SettingsJsonSchemaParams, SettingsSources};
use std::sync::Arc;
use util::ResultExt as _;
@ -316,14 +316,11 @@ impl settings::Settings for ThemeSettings {
type FileContent = ThemeSettingsContent;
fn load(
defaults: &Self::FileContent,
user_values: &[&Self::FileContent],
cx: &mut AppContext,
) -> Result<Self> {
fn load(sources: SettingsSources<Self::FileContent>, cx: &mut AppContext) -> Result<Self> {
let themes = ThemeRegistry::default_global(cx);
let system_appearance = SystemAppearance::default_global(cx);
let defaults = sources.default;
let mut this = Self {
ui_font_size: defaults.ui_font_size.unwrap().into(),
ui_font: Font {
@ -348,15 +345,15 @@ impl settings::Settings for ThemeSettings {
theme_overrides: None,
};
for value in user_values.iter().copied().cloned() {
if let Some(value) = value.buffer_font_family {
for value in sources.user.into_iter().chain(sources.release_channel) {
if let Some(value) = value.buffer_font_family.clone() {
this.buffer_font.family = value.into();
}
if let Some(value) = value.buffer_font_features {
this.buffer_font.features = value;
}
if let Some(value) = value.ui_font_family {
if let Some(value) = value.ui_font_family.clone() {
this.ui_font.family = value.into();
}
if let Some(value) = value.ui_font_features {
@ -373,7 +370,7 @@ impl settings::Settings for ThemeSettings {
}
}
this.theme_overrides = value.theme_overrides;
this.theme_overrides = value.theme_overrides.clone();
this.apply_theme_overrides();
merge(&mut this.ui_font_size, value.ui_font_size.map(Into::into));

View File

@ -35,7 +35,7 @@ use replace::multi_replace;
use schemars::JsonSchema;
use serde::Deserialize;
use serde_derive::Serialize;
use settings::{update_settings_file, Settings, SettingsStore};
use settings::{update_settings_file, Settings, SettingsSources, SettingsStore};
use state::{EditorState, Mode, Operator, RecordedSelection, WorkspaceState};
use std::{ops::Range, sync::Arc};
use surrounds::{add_surrounds, change_surrounds, delete_surrounds};
@ -779,13 +779,9 @@ impl Settings for VimModeSetting {
type FileContent = Option<bool>;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &mut AppContext,
) -> Result<Self> {
Ok(Self(user_values.iter().rev().find_map(|v| **v).unwrap_or(
default_value.ok_or_else(Self::missing_default)?,
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
Ok(Self(sources.user.copied().flatten().unwrap_or(
sources.default.ok_or_else(Self::missing_default)?,
)))
}
}
@ -824,11 +820,7 @@ impl Settings for VimSettings {
type FileContent = VimSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &mut AppContext,
) -> Result<Self> {
Self::load_via_json_merge(default_value, user_values)
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
sources.json_merge()
}
}

View File

@ -2,7 +2,7 @@ use std::fmt::{Display, Formatter};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
use settings::{Settings, SettingsSources};
/// Base key bindings scheme. Base keymaps can be overridden with user keymaps.
///
@ -70,16 +70,12 @@ impl Settings for BaseKeymap {
type FileContent = Option<Self>;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
sources: SettingsSources<Self::FileContent>,
_: &mut gpui::AppContext,
) -> anyhow::Result<Self>
where
Self: Sized,
{
Ok(user_values
.first()
.and_then(|v| **v)
.unwrap_or(default_value.unwrap()))
) -> anyhow::Result<Self> {
if let Some(Some(user_value)) = sources.user.copied() {
return Ok(user_value);
}
sources.default.ok_or_else(Self::missing_default)
}
}

View File

@ -20,7 +20,7 @@ use gpui::{
use project::{Project, ProjectEntryId, ProjectPath};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
use settings::{Settings, SettingsSources};
use smallvec::SmallVec;
use std::{
any::{Any, TypeId},
@ -76,12 +76,8 @@ impl Settings for ItemSettings {
type FileContent = ItemSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &mut AppContext,
) -> Result<Self> {
Self::load_via_json_merge(default_value, user_values)
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
sources.json_merge()
}
}

View File

@ -1,6 +1,8 @@
use anyhow::Result;
use gpui::AppContext;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
use settings::{Settings, SettingsSources};
#[derive(Deserialize)]
pub struct WorkspaceSettings {
@ -63,12 +65,8 @@ impl Settings for WorkspaceSettings {
type FileContent = WorkspaceSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
sources.json_merge()
}
}
@ -77,11 +75,7 @@ impl Settings for TabBarSettings {
type FileContent = TabBarSettingsContent;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
_: &mut gpui::AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
sources.json_merge()
}
}

View File

@ -1,7 +1,7 @@
use gpui::AppContext;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::Settings;
use settings::{Settings, SettingsSources};
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
pub struct WorktreeSettings {
@ -31,10 +31,9 @@ impl Settings for WorktreeSettings {
type FileContent = Self;
fn load(
default_value: &Self::FileContent,
user_values: &[&Self::FileContent],
sources: SettingsSources<Self::FileContent>,
_: &mut AppContext,
) -> anyhow::Result<Self> {
Self::load_via_json_merge(default_value, user_values)
sources.json_merge()
}
}

View File

@ -607,6 +607,7 @@ pub fn handle_keymap_file_changes(
cx.observe_global::<SettingsStore>(move |cx| {
let new_base_keymap = *BaseKeymap::get_global(cx);
let new_vim_enabled = VimModeSetting::get_global(cx).0;
if new_base_keymap != old_base_keymap || new_vim_enabled != old_vim_enabled {
old_base_keymap = new_base_keymap;
old_vim_enabled = new_vim_enabled;
@ -3062,6 +3063,7 @@ mod tests {
let mut app_state = AppState::test(cx);
let state = Arc::get_mut(&mut app_state).unwrap();
env_logger::try_init().ok();
state.build_window_options = build_window_options;
theme::init(theme::LoadThemes::JustBase, cx);

View File

@ -9,3 +9,4 @@ brackets = [
{ start = "(", end = ")", close = true, newline = true },
{ start = "\"", end = "\"", close = true, newline = false, not_in = ["string", "comment"] },
]
tab_size = 2

View File

@ -7,3 +7,4 @@ brackets = [
{ start = "[", end = "]", close = true, newline = true },
{ start = "(", end = ")", close = true, newline = true }
]
tab_size = 2