changeset 6784:9900b670afa2

syntax: Move config types to a separate module
author Michael Davis <mcarsondavis@gmail.com>
date Thu, 20 Feb 2025 17:08:47 -0500
parents acb76767dab3
children f1e0f64160d7
files helix-core/src/comment.rs helix-core/src/config.rs helix-core/src/indent.rs helix-core/src/movement.rs helix-core/src/syntax.rs helix-core/src/syntax/config.rs helix-core/src/textobject.rs helix-core/tests/indent.rs helix-dap/src/client.rs helix-lsp/src/client.rs helix-lsp/src/lib.rs helix-term/src/commands.rs helix-term/src/commands/dap.rs helix-term/src/commands/lsp.rs helix-term/src/handlers/completion.rs helix-term/src/handlers/completion/request.rs helix-term/src/handlers/document_colors.rs helix-term/src/handlers/signature_help.rs helix-term/src/ui/mod.rs helix-term/tests/integration.rs helix-view/src/document.rs helix-view/src/editor.rs helix-view/src/gutter.rs xtask/src/helpers.rs
diffstat 24 files changed, 657 insertions(+), 639 deletions(-) [+]
line wrap: on
line diff
--- a/helix-core/src/comment.rs	Tue May 13 15:21:51 2025 -0700
+++ b/helix-core/src/comment.rs	Thu Feb 20 17:08:47 2025 -0500
@@ -4,7 +4,8 @@
 use smallvec::SmallVec;
 
 use crate::{
-    syntax::BlockCommentToken, Change, Range, Rope, RopeSlice, Selection, Tendril, Transaction,
+    syntax::config::BlockCommentToken, Change, Range, Rope, RopeSlice, Selection, Tendril,
+    Transaction,
 };
 use helix_stdx::rope::RopeSliceExt;
 use std::borrow::Cow;
--- a/helix-core/src/config.rs	Tue May 13 15:21:51 2025 -0700
+++ b/helix-core/src/config.rs	Thu Feb 20 17:08:47 2025 -0500
@@ -1,4 +1,4 @@
-use crate::syntax::{Configuration, Loader, LoaderError};
+use crate::syntax::{config::Configuration, Loader, LoaderError};
 
 /// Language configuration based on built-in languages.toml.
 pub fn default_lang_config() -> Configuration {
--- a/helix-core/src/indent.rs	Tue May 13 15:21:51 2025 -0700
+++ b/helix-core/src/indent.rs	Thu Feb 20 17:08:47 2025 -0500
@@ -6,7 +6,10 @@
 use crate::{
     chars::{char_is_line_ending, char_is_whitespace},
     graphemes::{grapheme_width, tab_width_at},
-    syntax::{IndentationHeuristic, LanguageConfiguration, RopeProvider, Syntax},
+    syntax::{
+        config::{IndentationHeuristic, LanguageConfiguration},
+        RopeProvider, Syntax,
+    },
     tree_sitter::Node,
     Position, Rope, RopeSlice, Tendril,
 };
--- a/helix-core/src/movement.rs	Tue May 13 15:21:51 2025 -0700
+++ b/helix-core/src/movement.rs	Thu Feb 20 17:08:47 2025 -0500
@@ -13,7 +13,7 @@
     },
     line_ending::rope_is_line_ending,
     position::char_idx_at_visual_block_offset,
-    syntax::LanguageConfiguration,
+    syntax::config::LanguageConfiguration,
     text_annotations::TextAnnotations,
     textobject::TextObject,
     visual_offset_from_block, Range, RopeSlice, Selection, Syntax,
--- a/helix-core/src/syntax.rs	Tue May 13 15:21:51 2025 -0700
+++ b/helix-core/src/syntax.rs	Thu Feb 20 17:08:47 2025 -0500
@@ -1,9 +1,8 @@
+pub mod config;
 mod tree_cursor;
 
 use crate::{
-    auto_pairs::AutoPairs,
     chars::char_is_line_ending,
-    diagnostic::Severity,
     regex::Regex,
     transaction::{ChangeSet, Operation},
     RopeSlice, Tendril,
@@ -12,7 +11,7 @@
 use ahash::RandomState;
 use arc_swap::{ArcSwap, Guard};
 use bitflags::bitflags;
-use globset::GlobSet;
+use config::{Configuration, FileType, LanguageConfiguration, LanguageServerConfiguration};
 use hashbrown::raw::RawTable;
 use helix_stdx::rope::{self, RopeSliceExt};
 use slotmap::{DefaultKey as LayerId, HopSlotMap};
@@ -20,595 +19,20 @@
 use std::{
     borrow::Cow,
     cell::RefCell,
-    collections::{HashMap, HashSet, VecDeque},
-    fmt::{self, Display, Write},
+    collections::{HashMap, VecDeque},
+    fmt::{self, Write},
     hash::{Hash, Hasher},
     mem::replace,
-    path::{Path, PathBuf},
-    str::FromStr,
+    path::Path,
     sync::Arc,
 };
 
-use once_cell::sync::{Lazy, OnceCell};
-use serde::{ser::SerializeSeq, Deserialize, Serialize};
+use once_cell::sync::Lazy;
 
 use helix_loader::grammar::{get_language, load_runtime_file};
 
 pub use tree_cursor::TreeCursor;
 
-fn deserialize_regex<'de, D>(deserializer: D) -> Result<Option<rope::Regex>, D::Error>
-where
-    D: serde::Deserializer<'de>,
-{
-    Option::<String>::deserialize(deserializer)?
-        .map(|buf| rope::Regex::new(&buf).map_err(serde::de::Error::custom))
-        .transpose()
-}
-
-fn deserialize_lsp_config<'de, D>(deserializer: D) -> Result<Option<serde_json::Value>, D::Error>
-where
-    D: serde::Deserializer<'de>,
-{
-    Option::<toml::Value>::deserialize(deserializer)?
-        .map(|toml| toml.try_into().map_err(serde::de::Error::custom))
-        .transpose()
-}
-
-fn deserialize_tab_width<'de, D>(deserializer: D) -> Result<usize, D::Error>
-where
-    D: serde::Deserializer<'de>,
-{
-    usize::deserialize(deserializer).and_then(|n| {
-        if n > 0 && n <= 16 {
-            Ok(n)
-        } else {
-            Err(serde::de::Error::custom(
-                "tab width must be a value from 1 to 16 inclusive",
-            ))
-        }
-    })
-}
-
-pub fn deserialize_auto_pairs<'de, D>(deserializer: D) -> Result<Option<AutoPairs>, D::Error>
-where
-    D: serde::Deserializer<'de>,
-{
-    Ok(Option::<AutoPairConfig>::deserialize(deserializer)?.and_then(AutoPairConfig::into))
-}
-
-fn default_timeout() -> u64 {
-    20
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-#[serde(rename_all = "kebab-case")]
-pub struct Configuration {
-    pub language: Vec<LanguageConfiguration>,
-    #[serde(default)]
-    pub language_server: HashMap<String, LanguageServerConfiguration>,
-}
-
-// largely based on tree-sitter/cli/src/loader.rs
-#[derive(Debug, Serialize, Deserialize)]
-#[serde(rename_all = "kebab-case", deny_unknown_fields)]
-pub struct LanguageConfiguration {
-    #[serde(rename = "name")]
-    pub language_id: String, // c-sharp, rust, tsx
-    #[serde(rename = "language-id")]
-    // see the table under https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentItem
-    pub language_server_language_id: Option<String>, // csharp, rust, typescriptreact, for the language-server
-    pub scope: String,             // source.rust
-    pub file_types: Vec<FileType>, // filename extension or ends_with? <Gemfile, rb, etc>
-    #[serde(default)]
-    pub shebangs: Vec<String>, // interpreter(s) associated with language
-    #[serde(default)]
-    pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
-    #[serde(
-        default,
-        skip_serializing,
-        deserialize_with = "from_comment_tokens",
-        alias = "comment-token"
-    )]
-    pub comment_tokens: Option<Vec<String>>,
-    #[serde(
-        default,
-        skip_serializing,
-        deserialize_with = "from_block_comment_tokens"
-    )]
-    pub block_comment_tokens: Option<Vec<BlockCommentToken>>,
-    pub text_width: Option<usize>,
-    pub soft_wrap: Option<SoftWrap>,
-
-    #[serde(default)]
-    pub auto_format: bool,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub formatter: Option<FormatterConfiguration>,
-
-    /// If set, overrides `editor.path-completion`.
-    pub path_completion: Option<bool>,
-
-    #[serde(default)]
-    pub diagnostic_severity: Severity,
-
-    pub grammar: Option<String>, // tree-sitter grammar name, defaults to language_id
-
-    // content_regex
-    #[serde(default, skip_serializing, deserialize_with = "deserialize_regex")]
-    pub injection_regex: Option<rope::Regex>,
-    // first_line_regex
-    //
-    #[serde(skip)]
-    pub(crate) highlight_config: OnceCell<Option<Arc<HighlightConfiguration>>>,
-    // tags_config OnceCell<> https://github.com/tree-sitter/tree-sitter/pull/583
-    #[serde(
-        default,
-        skip_serializing_if = "Vec::is_empty",
-        serialize_with = "serialize_lang_features",
-        deserialize_with = "deserialize_lang_features"
-    )]
-    pub language_servers: Vec<LanguageServerFeatures>,
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub indent: Option<IndentationConfiguration>,
-
-    #[serde(skip)]
-    pub(crate) indent_query: OnceCell<Option<Query>>,
-    #[serde(skip)]
-    pub(crate) textobject_query: OnceCell<Option<TextObjectQuery>>,
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub debugger: Option<DebugAdapterConfig>,
-
-    /// Automatic insertion of pairs to parentheses, brackets,
-    /// etc. Defaults to true. Optionally, this can be a list of 2-tuples
-    /// to specify a list of characters to pair. This overrides the
-    /// global setting.
-    #[serde(default, skip_serializing, deserialize_with = "deserialize_auto_pairs")]
-    pub auto_pairs: Option<AutoPairs>,
-
-    pub rulers: Option<Vec<u16>>, // if set, override editor's rulers
-
-    /// Hardcoded LSP root directories relative to the workspace root, like `examples` or `tools/fuzz`.
-    /// Falling back to the current working directory if none are configured.
-    pub workspace_lsp_roots: Option<Vec<PathBuf>>,
-    #[serde(default)]
-    pub persistent_diagnostic_sources: Vec<String>,
-}
-
-#[derive(Debug, PartialEq, Eq, Hash)]
-pub enum FileType {
-    /// The extension of the file, either the `Path::extension` or the full
-    /// filename if the file does not have an extension.
-    Extension(String),
-    /// A Unix-style path glob. This is compared to the file's absolute path, so
-    /// it can be used to detect files based on their directories. If the glob
-    /// is not an absolute path and does not already start with a glob pattern,
-    /// a glob pattern will be prepended to it.
-    Glob(globset::Glob),
-}
-
-impl Serialize for FileType {
-    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-    where
-        S: serde::Serializer,
-    {
-        use serde::ser::SerializeMap;
-
-        match self {
-            FileType::Extension(extension) => serializer.serialize_str(extension),
-            FileType::Glob(glob) => {
-                let mut map = serializer.serialize_map(Some(1))?;
-                map.serialize_entry("glob", glob.glob())?;
-                map.end()
-            }
-        }
-    }
-}
-
-impl<'de> Deserialize<'de> for FileType {
-    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-    where
-        D: serde::de::Deserializer<'de>,
-    {
-        struct FileTypeVisitor;
-
-        impl<'de> serde::de::Visitor<'de> for FileTypeVisitor {
-            type Value = FileType;
-
-            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
-                formatter.write_str("string or table")
-            }
-
-            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
-            where
-                E: serde::de::Error,
-            {
-                Ok(FileType::Extension(value.to_string()))
-            }
-
-            fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
-            where
-                M: serde::de::MapAccess<'de>,
-            {
-                match map.next_entry::<String, String>()? {
-                    Some((key, mut glob)) if key == "glob" => {
-                        // If the glob isn't an absolute path or already starts
-                        // with a glob pattern, add a leading glob so we
-                        // properly match relative paths.
-                        if !glob.starts_with('/') && !glob.starts_with("*/") {
-                            glob.insert_str(0, "*/");
-                        }
-
-                        globset::Glob::new(glob.as_str())
-                            .map(FileType::Glob)
-                            .map_err(|err| {
-                                serde::de::Error::custom(format!("invalid `glob` pattern: {}", err))
-                            })
-                    }
-                    Some((key, _value)) => Err(serde::de::Error::custom(format!(
-                        "unknown key in `file-types` list: {}",
-                        key
-                    ))),
-                    None => Err(serde::de::Error::custom(
-                        "expected a `suffix` key in the `file-types` entry",
-                    )),
-                }
-            }
-        }
-
-        deserializer.deserialize_any(FileTypeVisitor)
-    }
-}
-
-fn from_comment_tokens<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
-where
-    D: serde::Deserializer<'de>,
-{
-    #[derive(Deserialize)]
-    #[serde(untagged)]
-    enum CommentTokens {
-        Multiple(Vec<String>),
-        Single(String),
-    }
-    Ok(
-        Option::<CommentTokens>::deserialize(deserializer)?.map(|tokens| match tokens {
-            CommentTokens::Single(val) => vec![val],
-            CommentTokens::Multiple(vals) => vals,
-        }),
-    )
-}
-
-#[derive(Clone, Debug, Serialize, Deserialize)]
-pub struct BlockCommentToken {
-    pub start: String,
-    pub end: String,
-}
-
-impl Default for BlockCommentToken {
-    fn default() -> Self {
-        BlockCommentToken {
-            start: "/*".to_string(),
-            end: "*/".to_string(),
-        }
-    }
-}
-
-fn from_block_comment_tokens<'de, D>(
-    deserializer: D,
-) -> Result<Option<Vec<BlockCommentToken>>, D::Error>
-where
-    D: serde::Deserializer<'de>,
-{
-    #[derive(Deserialize)]
-    #[serde(untagged)]
-    enum BlockCommentTokens {
-        Multiple(Vec<BlockCommentToken>),
-        Single(BlockCommentToken),
-    }
-    Ok(
-        Option::<BlockCommentTokens>::deserialize(deserializer)?.map(|tokens| match tokens {
-            BlockCommentTokens::Single(val) => vec![val],
-            BlockCommentTokens::Multiple(vals) => vals,
-        }),
-    )
-}
-
-#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
-#[serde(rename_all = "kebab-case")]
-pub enum LanguageServerFeature {
-    Format,
-    GotoDeclaration,
-    GotoDefinition,
-    GotoTypeDefinition,
-    GotoReference,
-    GotoImplementation,
-    // Goto, use bitflags, combining previous Goto members?
-    SignatureHelp,
-    Hover,
-    DocumentHighlight,
-    Completion,
-    CodeAction,
-    WorkspaceCommand,
-    DocumentSymbols,
-    WorkspaceSymbols,
-    // Symbols, use bitflags, see above?
-    Diagnostics,
-    RenameSymbol,
-    InlayHints,
-    DocumentColors,
-}
-
-impl Display for LanguageServerFeature {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        use LanguageServerFeature::*;
-        let feature = match self {
-            Format => "format",
-            GotoDeclaration => "goto-declaration",
-            GotoDefinition => "goto-definition",
-            GotoTypeDefinition => "goto-type-definition",
-            GotoReference => "goto-reference",
-            GotoImplementation => "goto-implementation",
-            SignatureHelp => "signature-help",
-            Hover => "hover",
-            DocumentHighlight => "document-highlight",
-            Completion => "completion",
-            CodeAction => "code-action",
-            WorkspaceCommand => "workspace-command",
-            DocumentSymbols => "document-symbols",
-            WorkspaceSymbols => "workspace-symbols",
-            Diagnostics => "diagnostics",
-            RenameSymbol => "rename-symbol",
-            InlayHints => "inlay-hints",
-            DocumentColors => "document-colors",
-        };
-        write!(f, "{feature}",)
-    }
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-#[serde(untagged, rename_all = "kebab-case", deny_unknown_fields)]
-enum LanguageServerFeatureConfiguration {
-    #[serde(rename_all = "kebab-case")]
-    Features {
-        #[serde(default, skip_serializing_if = "HashSet::is_empty")]
-        only_features: HashSet<LanguageServerFeature>,
-        #[serde(default, skip_serializing_if = "HashSet::is_empty")]
-        except_features: HashSet<LanguageServerFeature>,
-        name: String,
-    },
-    Simple(String),
-}
-
-#[derive(Debug, Default)]
-pub struct LanguageServerFeatures {
-    pub name: String,
-    pub only: HashSet<LanguageServerFeature>,
-    pub excluded: HashSet<LanguageServerFeature>,
-}
-
-impl LanguageServerFeatures {
-    pub fn has_feature(&self, feature: LanguageServerFeature) -> bool {
-        (self.only.is_empty() || self.only.contains(&feature)) && !self.excluded.contains(&feature)
-    }
-}
-
-fn deserialize_lang_features<'de, D>(
-    deserializer: D,
-) -> Result<Vec<LanguageServerFeatures>, D::Error>
-where
-    D: serde::Deserializer<'de>,
-{
-    let raw: Vec<LanguageServerFeatureConfiguration> = Deserialize::deserialize(deserializer)?;
-    let res = raw
-        .into_iter()
-        .map(|config| match config {
-            LanguageServerFeatureConfiguration::Simple(name) => LanguageServerFeatures {
-                name,
-                ..Default::default()
-            },
-            LanguageServerFeatureConfiguration::Features {
-                only_features,
-                except_features,
-                name,
-            } => LanguageServerFeatures {
-                name,
-                only: only_features,
-                excluded: except_features,
-            },
-        })
-        .collect();
-    Ok(res)
-}
-fn serialize_lang_features<S>(
-    map: &Vec<LanguageServerFeatures>,
-    serializer: S,
-) -> Result<S::Ok, S::Error>
-where
-    S: serde::Serializer,
-{
-    let mut serializer = serializer.serialize_seq(Some(map.len()))?;
-    for features in map {
-        let features = if features.only.is_empty() && features.excluded.is_empty() {
-            LanguageServerFeatureConfiguration::Simple(features.name.to_owned())
-        } else {
-            LanguageServerFeatureConfiguration::Features {
-                only_features: features.only.clone(),
-                except_features: features.excluded.clone(),
-                name: features.name.to_owned(),
-            }
-        };
-        serializer.serialize_element(&features)?;
-    }
-    serializer.end()
-}
-
-fn deserialize_required_root_patterns<'de, D>(deserializer: D) -> Result<Option<GlobSet>, D::Error>
-where
-    D: serde::Deserializer<'de>,
-{
-    let patterns = Vec::<String>::deserialize(deserializer)?;
-    if patterns.is_empty() {
-        return Ok(None);
-    }
-    let mut builder = globset::GlobSetBuilder::new();
-    for pattern in patterns {
-        let glob = globset::Glob::new(&pattern).map_err(serde::de::Error::custom)?;
-        builder.add(glob);
-    }
-    builder.build().map(Some).map_err(serde::de::Error::custom)
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-#[serde(rename_all = "kebab-case")]
-pub struct LanguageServerConfiguration {
-    pub command: String,
-    #[serde(default)]
-    #[serde(skip_serializing_if = "Vec::is_empty")]
-    pub args: Vec<String>,
-    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
-    pub environment: HashMap<String, String>,
-    #[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")]
-    pub config: Option<serde_json::Value>,
-    #[serde(default = "default_timeout")]
-    pub timeout: u64,
-    #[serde(
-        default,
-        skip_serializing,
-        deserialize_with = "deserialize_required_root_patterns"
-    )]
-    pub required_root_patterns: Option<GlobSet>,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize)]
-#[serde(rename_all = "kebab-case")]
-pub struct FormatterConfiguration {
-    pub command: String,
-    #[serde(default)]
-    #[serde(skip_serializing_if = "Vec::is_empty")]
-    pub args: Vec<String>,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
-#[serde(rename_all = "kebab-case")]
-pub struct AdvancedCompletion {
-    pub name: Option<String>,
-    pub completion: Option<String>,
-    pub default: Option<String>,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
-#[serde(rename_all = "kebab-case", untagged)]
-pub enum DebugConfigCompletion {
-    Named(String),
-    Advanced(AdvancedCompletion),
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
-#[serde(untagged)]
-pub enum DebugArgumentValue {
-    String(String),
-    Array(Vec<String>),
-    Boolean(bool),
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
-#[serde(rename_all = "kebab-case")]
-pub struct DebugTemplate {
-    pub name: String,
-    pub request: String,
-    #[serde(default)]
-    pub completion: Vec<DebugConfigCompletion>,
-    pub args: HashMap<String, DebugArgumentValue>,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
-#[serde(rename_all = "kebab-case")]
-pub struct DebugAdapterConfig {
-    pub name: String,
-    pub transport: String,
-    #[serde(default)]
-    pub command: String,
-    #[serde(default)]
-    pub args: Vec<String>,
-    pub port_arg: Option<String>,
-    pub templates: Vec<DebugTemplate>,
-    #[serde(default)]
-    pub quirks: DebuggerQuirks,
-}
-
-// Different workarounds for adapters' differences
-#[derive(Debug, Default, PartialEq, Eq, Clone, Serialize, Deserialize)]
-pub struct DebuggerQuirks {
-    #[serde(default)]
-    pub absolute_paths: bool,
-}
-
-#[derive(Debug, Serialize, Deserialize)]
-#[serde(rename_all = "kebab-case")]
-pub struct IndentationConfiguration {
-    #[serde(deserialize_with = "deserialize_tab_width")]
-    pub tab_width: usize,
-    pub unit: String,
-}
-
-/// How the indentation for a newly inserted line should be determined.
-/// If the selected heuristic is not available (e.g. because the current
-/// language has no tree-sitter indent queries), a simpler one will be used.
-#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "kebab-case")]
-pub enum IndentationHeuristic {
-    /// Just copy the indentation of the line that the cursor is currently on.
-    Simple,
-    /// Use tree-sitter indent queries to compute the expected absolute indentation level of the new line.
-    TreeSitter,
-    /// Use tree-sitter indent queries to compute the expected difference in indentation between the new line
-    /// and the line before. Add this to the actual indentation level of the line before.
-    #[default]
-    Hybrid,
-}
-
-/// Configuration for auto pairs
-#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(rename_all = "kebab-case", deny_unknown_fields, untagged)]
-pub enum AutoPairConfig {
-    /// Enables or disables auto pairing. False means disabled. True means to use the default pairs.
-    Enable(bool),
-
-    /// The mappings of pairs.
-    Pairs(HashMap<char, char>),
-}
-
-impl Default for AutoPairConfig {
-    fn default() -> Self {
-        AutoPairConfig::Enable(true)
-    }
-}
-
-impl From<&AutoPairConfig> for Option<AutoPairs> {
-    fn from(auto_pair_config: &AutoPairConfig) -> Self {
-        match auto_pair_config {
-            AutoPairConfig::Enable(false) => None,
-            AutoPairConfig::Enable(true) => Some(AutoPairs::default()),
-            AutoPairConfig::Pairs(pairs) => Some(AutoPairs::new(pairs.iter())),
-        }
-    }
-}
-
-impl From<AutoPairConfig> for Option<AutoPairs> {
-    fn from(auto_pairs_config: AutoPairConfig) -> Self {
-        (&auto_pairs_config).into()
-    }
-}
-
-impl FromStr for AutoPairConfig {
-    type Err = std::str::ParseBoolError;
-
-    // only do bool parsing for runtime setting
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        let enable: bool = s.parse()?;
-        Ok(AutoPairConfig::Enable(enable))
-    }
-}
-
 #[derive(Debug)]
 pub struct TextObjectQuery {
     pub query: Query,
@@ -743,7 +167,7 @@
         .to_string()
 }
 
-impl LanguageConfiguration {
+impl config::LanguageConfiguration {
     fn initialize_highlight(&self, scopes: &[String]) -> Option<Arc<HighlightConfiguration>> {
         let highlights_query = read_query(&self.language_id, "highlights.scm");
         // always highlight syntax errors
@@ -831,35 +255,6 @@
             .ok()
     }
 }
-#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
-#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
-pub struct SoftWrap {
-    /// Soft wrap lines that exceed viewport width. Default to off
-    // NOTE: Option on purpose because the struct is shared between language config and global config.
-    // By default the option is None so that the language config falls back to the global config unless explicitly set.
-    pub enable: Option<bool>,
-    /// Maximum space left free at the end of the line.
-    /// This space is used to wrap text at word boundaries. If that is not possible within this limit
-    /// the word is simply split at the end of the line.
-    ///
-    /// This is automatically hard-limited to a quarter of the viewport to ensure correct display on small views.
-    ///
-    /// Default to 20
-    pub max_wrap: Option<u16>,
-    /// Maximum number of indentation that can be carried over from the previous line when softwrapping.
-    /// If a line is indented further then this limit it is rendered at the start of the viewport instead.
-    ///
-    /// This is automatically hard-limited to a quarter of the viewport to ensure correct display on small views.
-    ///
-    /// Default to 40
-    pub max_indent_retain: Option<u16>,
-    /// Indicator placed at the beginning of softwrapped lines
-    ///
-    /// Defaults to ↪
-    pub wrap_indicator: Option<String>,
-    /// Softwrap at `text_width` instead of viewport width if it is shorter
-    pub wrap_at_text_width: Option<bool>,
-}
 
 #[derive(Debug)]
 struct FileTypeGlob {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/helix-core/src/syntax/config.rs	Thu Feb 20 17:08:47 2025 -0500
@@ -0,0 +1,616 @@
+use crate::{auto_pairs::AutoPairs, diagnostic::Severity};
+
+use globset::GlobSet;
+use helix_stdx::rope;
+use once_cell::sync::OnceCell;
+use serde::{ser::SerializeSeq as _, Deserialize, Serialize};
+
+use std::{
+    collections::{HashMap, HashSet},
+    fmt::{self, Display},
+    path::PathBuf,
+    str::FromStr,
+    sync::Arc,
+};
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub struct Configuration {
+    pub language: Vec<LanguageConfiguration>,
+    #[serde(default)]
+    pub language_server: HashMap<String, LanguageServerConfiguration>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case", deny_unknown_fields)]
+pub struct LanguageConfiguration {
+    #[serde(rename = "name")]
+    pub language_id: String, // c-sharp, rust, tsx
+    #[serde(rename = "language-id")]
+    // see the table under https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentItem
+    pub language_server_language_id: Option<String>, // csharp, rust, typescriptreact, for the language-server
+    pub scope: String,             // source.rust
+    pub file_types: Vec<FileType>, // filename extension or ends_with? <Gemfile, rb, etc>
+    #[serde(default)]
+    pub shebangs: Vec<String>, // interpreter(s) associated with language
+    #[serde(default)]
+    pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
+    #[serde(
+        default,
+        skip_serializing,
+        deserialize_with = "from_comment_tokens",
+        alias = "comment-token"
+    )]
+    pub comment_tokens: Option<Vec<String>>,
+    #[serde(
+        default,
+        skip_serializing,
+        deserialize_with = "from_block_comment_tokens"
+    )]
+    pub block_comment_tokens: Option<Vec<BlockCommentToken>>,
+    pub text_width: Option<usize>,
+    pub soft_wrap: Option<SoftWrap>,
+
+    #[serde(default)]
+    pub auto_format: bool,
+
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub formatter: Option<FormatterConfiguration>,
+
+    /// If set, overrides `editor.path-completion`.
+    pub path_completion: Option<bool>,
+
+    #[serde(default)]
+    pub diagnostic_severity: Severity,
+
+    pub grammar: Option<String>, // tree-sitter grammar name, defaults to language_id
+
+    // content_regex
+    #[serde(default, skip_serializing, deserialize_with = "deserialize_regex")]
+    pub injection_regex: Option<rope::Regex>,
+    // first_line_regex
+    //
+    #[serde(skip)]
+    pub(crate) highlight_config: OnceCell<Option<Arc<super::HighlightConfiguration>>>,
+    // tags_config OnceCell<> https://github.com/tree-sitter/tree-sitter/pull/583
+    #[serde(
+        default,
+        skip_serializing_if = "Vec::is_empty",
+        serialize_with = "serialize_lang_features",
+        deserialize_with = "deserialize_lang_features"
+    )]
+    pub language_servers: Vec<LanguageServerFeatures>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub indent: Option<IndentationConfiguration>,
+
+    #[serde(skip)]
+    pub(crate) indent_query: OnceCell<Option<tree_sitter::Query>>,
+    #[serde(skip)]
+    pub(crate) textobject_query: OnceCell<Option<super::TextObjectQuery>>,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub debugger: Option<DebugAdapterConfig>,
+
+    /// Automatic insertion of pairs to parentheses, brackets,
+    /// etc. Defaults to true. Optionally, this can be a list of 2-tuples
+    /// to specify a list of characters to pair. This overrides the
+    /// global setting.
+    #[serde(default, skip_serializing, deserialize_with = "deserialize_auto_pairs")]
+    pub auto_pairs: Option<AutoPairs>,
+
+    pub rulers: Option<Vec<u16>>, // if set, override editor's rulers
+
+    /// Hardcoded LSP root directories relative to the workspace root, like `examples` or `tools/fuzz`.
+    /// Falling back to the current working directory if none are configured.
+    pub workspace_lsp_roots: Option<Vec<PathBuf>>,
+    #[serde(default)]
+    pub persistent_diagnostic_sources: Vec<String>,
+}
+
+#[derive(Debug, PartialEq, Eq, Hash)]
+pub enum FileType {
+    /// The extension of the file, either the `Path::extension` or the full
+    /// filename if the file does not have an extension.
+    Extension(String),
+    /// A Unix-style path glob. This is compared to the file's absolute path, so
+    /// it can be used to detect files based on their directories. If the glob
+    /// is not an absolute path and does not already start with a glob pattern,
+    /// a glob pattern will be prepended to it.
+    Glob(globset::Glob),
+}
+
+impl Serialize for FileType {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        use serde::ser::SerializeMap;
+
+        match self {
+            FileType::Extension(extension) => serializer.serialize_str(extension),
+            FileType::Glob(glob) => {
+                let mut map = serializer.serialize_map(Some(1))?;
+                map.serialize_entry("glob", glob.glob())?;
+                map.end()
+            }
+        }
+    }
+}
+
+impl<'de> Deserialize<'de> for FileType {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::de::Deserializer<'de>,
+    {
+        struct FileTypeVisitor;
+
+        impl<'de> serde::de::Visitor<'de> for FileTypeVisitor {
+            type Value = FileType;
+
+            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
+                formatter.write_str("string or table")
+            }
+
+            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
+            where
+                E: serde::de::Error,
+            {
+                Ok(FileType::Extension(value.to_string()))
+            }
+
+            fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
+            where
+                M: serde::de::MapAccess<'de>,
+            {
+                match map.next_entry::<String, String>()? {
+                    Some((key, mut glob)) if key == "glob" => {
+                        // If the glob isn't an absolute path or already starts
+                        // with a glob pattern, add a leading glob so we
+                        // properly match relative paths.
+                        if !glob.starts_with('/') && !glob.starts_with("*/") {
+                            glob.insert_str(0, "*/");
+                        }
+
+                        globset::Glob::new(glob.as_str())
+                            .map(FileType::Glob)
+                            .map_err(|err| {
+                                serde::de::Error::custom(format!("invalid `glob` pattern: {}", err))
+                            })
+                    }
+                    Some((key, _value)) => Err(serde::de::Error::custom(format!(
+                        "unknown key in `file-types` list: {}",
+                        key
+                    ))),
+                    None => Err(serde::de::Error::custom(
+                        "expected a `suffix` key in the `file-types` entry",
+                    )),
+                }
+            }
+        }
+
+        deserializer.deserialize_any(FileTypeVisitor)
+    }
+}
+
+fn from_comment_tokens<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
+where
+    D: serde::Deserializer<'de>,
+{
+    #[derive(Deserialize)]
+    #[serde(untagged)]
+    enum CommentTokens {
+        Multiple(Vec<String>),
+        Single(String),
+    }
+    Ok(
+        Option::<CommentTokens>::deserialize(deserializer)?.map(|tokens| match tokens {
+            CommentTokens::Single(val) => vec![val],
+            CommentTokens::Multiple(vals) => vals,
+        }),
+    )
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct BlockCommentToken {
+    pub start: String,
+    pub end: String,
+}
+
+impl Default for BlockCommentToken {
+    fn default() -> Self {
+        BlockCommentToken {
+            start: "/*".to_string(),
+            end: "*/".to_string(),
+        }
+    }
+}
+
+fn from_block_comment_tokens<'de, D>(
+    deserializer: D,
+) -> Result<Option<Vec<BlockCommentToken>>, D::Error>
+where
+    D: serde::Deserializer<'de>,
+{
+    #[derive(Deserialize)]
+    #[serde(untagged)]
+    enum BlockCommentTokens {
+        Multiple(Vec<BlockCommentToken>),
+        Single(BlockCommentToken),
+    }
+    Ok(
+        Option::<BlockCommentTokens>::deserialize(deserializer)?.map(|tokens| match tokens {
+            BlockCommentTokens::Single(val) => vec![val],
+            BlockCommentTokens::Multiple(vals) => vals,
+        }),
+    )
+}
+
+#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
+#[serde(rename_all = "kebab-case")]
+pub enum LanguageServerFeature {
+    Format,
+    GotoDeclaration,
+    GotoDefinition,
+    GotoTypeDefinition,
+    GotoReference,
+    GotoImplementation,
+    // Goto, use bitflags, combining previous Goto members?
+    SignatureHelp,
+    Hover,
+    DocumentHighlight,
+    Completion,
+    CodeAction,
+    WorkspaceCommand,
+    DocumentSymbols,
+    WorkspaceSymbols,
+    // Symbols, use bitflags, see above?
+    Diagnostics,
+    RenameSymbol,
+    InlayHints,
+    DocumentColors,
+}
+
+impl Display for LanguageServerFeature {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        use LanguageServerFeature::*;
+        let feature = match self {
+            Format => "format",
+            GotoDeclaration => "goto-declaration",
+            GotoDefinition => "goto-definition",
+            GotoTypeDefinition => "goto-type-definition",
+            GotoReference => "goto-reference",
+            GotoImplementation => "goto-implementation",
+            SignatureHelp => "signature-help",
+            Hover => "hover",
+            DocumentHighlight => "document-highlight",
+            Completion => "completion",
+            CodeAction => "code-action",
+            WorkspaceCommand => "workspace-command",
+            DocumentSymbols => "document-symbols",
+            WorkspaceSymbols => "workspace-symbols",
+            Diagnostics => "diagnostics",
+            RenameSymbol => "rename-symbol",
+            InlayHints => "inlay-hints",
+            DocumentColors => "document-colors",
+        };
+        write!(f, "{feature}",)
+    }
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(untagged, rename_all = "kebab-case", deny_unknown_fields)]
+enum LanguageServerFeatureConfiguration {
+    #[serde(rename_all = "kebab-case")]
+    Features {
+        #[serde(default, skip_serializing_if = "HashSet::is_empty")]
+        only_features: HashSet<LanguageServerFeature>,
+        #[serde(default, skip_serializing_if = "HashSet::is_empty")]
+        except_features: HashSet<LanguageServerFeature>,
+        name: String,
+    },
+    Simple(String),
+}
+
+#[derive(Debug, Default)]
+pub struct LanguageServerFeatures {
+    pub name: String,
+    pub only: HashSet<LanguageServerFeature>,
+    pub excluded: HashSet<LanguageServerFeature>,
+}
+
+impl LanguageServerFeatures {
+    pub fn has_feature(&self, feature: LanguageServerFeature) -> bool {
+        (self.only.is_empty() || self.only.contains(&feature)) && !self.excluded.contains(&feature)
+    }
+}
+
+fn deserialize_lang_features<'de, D>(
+    deserializer: D,
+) -> Result<Vec<LanguageServerFeatures>, D::Error>
+where
+    D: serde::Deserializer<'de>,
+{
+    let raw: Vec<LanguageServerFeatureConfiguration> = Deserialize::deserialize(deserializer)?;
+    let res = raw
+        .into_iter()
+        .map(|config| match config {
+            LanguageServerFeatureConfiguration::Simple(name) => LanguageServerFeatures {
+                name,
+                ..Default::default()
+            },
+            LanguageServerFeatureConfiguration::Features {
+                only_features,
+                except_features,
+                name,
+            } => LanguageServerFeatures {
+                name,
+                only: only_features,
+                excluded: except_features,
+            },
+        })
+        .collect();
+    Ok(res)
+}
+fn serialize_lang_features<S>(
+    map: &Vec<LanguageServerFeatures>,
+    serializer: S,
+) -> Result<S::Ok, S::Error>
+where
+    S: serde::Serializer,
+{
+    let mut serializer = serializer.serialize_seq(Some(map.len()))?;
+    for features in map {
+        let features = if features.only.is_empty() && features.excluded.is_empty() {
+            LanguageServerFeatureConfiguration::Simple(features.name.to_owned())
+        } else {
+            LanguageServerFeatureConfiguration::Features {
+                only_features: features.only.clone(),
+                except_features: features.excluded.clone(),
+                name: features.name.to_owned(),
+            }
+        };
+        serializer.serialize_element(&features)?;
+    }
+    serializer.end()
+}
+
+fn deserialize_required_root_patterns<'de, D>(deserializer: D) -> Result<Option<GlobSet>, D::Error>
+where
+    D: serde::Deserializer<'de>,
+{
+    let patterns = Vec::<String>::deserialize(deserializer)?;
+    if patterns.is_empty() {
+        return Ok(None);
+    }
+    let mut builder = globset::GlobSetBuilder::new();
+    for pattern in patterns {
+        let glob = globset::Glob::new(&pattern).map_err(serde::de::Error::custom)?;
+        builder.add(glob);
+    }
+    builder.build().map(Some).map_err(serde::de::Error::custom)
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub struct LanguageServerConfiguration {
+    pub command: String,
+    #[serde(default)]
+    #[serde(skip_serializing_if = "Vec::is_empty")]
+    pub args: Vec<String>,
+    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
+    pub environment: HashMap<String, String>,
+    #[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")]
+    pub config: Option<serde_json::Value>,
+    #[serde(default = "default_timeout")]
+    pub timeout: u64,
+    #[serde(
+        default,
+        skip_serializing,
+        deserialize_with = "deserialize_required_root_patterns"
+    )]
+    pub required_root_patterns: Option<GlobSet>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub struct FormatterConfiguration {
+    pub command: String,
+    #[serde(default)]
+    #[serde(skip_serializing_if = "Vec::is_empty")]
+    pub args: Vec<String>,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
+#[serde(rename_all = "kebab-case")]
+pub struct AdvancedCompletion {
+    pub name: Option<String>,
+    pub completion: Option<String>,
+    pub default: Option<String>,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
+#[serde(rename_all = "kebab-case", untagged)]
+pub enum DebugConfigCompletion {
+    Named(String),
+    Advanced(AdvancedCompletion),
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
+#[serde(untagged)]
+pub enum DebugArgumentValue {
+    String(String),
+    Array(Vec<String>),
+    Boolean(bool),
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
+#[serde(rename_all = "kebab-case")]
+pub struct DebugTemplate {
+    pub name: String,
+    pub request: String,
+    #[serde(default)]
+    pub completion: Vec<DebugConfigCompletion>,
+    pub args: HashMap<String, DebugArgumentValue>,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
+#[serde(rename_all = "kebab-case")]
+pub struct DebugAdapterConfig {
+    pub name: String,
+    pub transport: String,
+    #[serde(default)]
+    pub command: String,
+    #[serde(default)]
+    pub args: Vec<String>,
+    pub port_arg: Option<String>,
+    pub templates: Vec<DebugTemplate>,
+    #[serde(default)]
+    pub quirks: DebuggerQuirks,
+}
+
+// Different workarounds for adapters' differences
+#[derive(Debug, Default, PartialEq, Eq, Clone, Serialize, Deserialize)]
+pub struct DebuggerQuirks {
+    #[serde(default)]
+    pub absolute_paths: bool,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub struct IndentationConfiguration {
+    #[serde(deserialize_with = "deserialize_tab_width")]
+    pub tab_width: usize,
+    pub unit: String,
+}
+
+/// How the indentation for a newly inserted line should be determined.
+/// If the selected heuristic is not available (e.g. because the current
+/// language has no tree-sitter indent queries), a simpler one will be used.
+#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub enum IndentationHeuristic {
+    /// Just copy the indentation of the line that the cursor is currently on.
+    Simple,
+    /// Use tree-sitter indent queries to compute the expected absolute indentation level of the new line.
+    TreeSitter,
+    /// Use tree-sitter indent queries to compute the expected difference in indentation between the new line
+    /// and the line before. Add this to the actual indentation level of the line before.
+    #[default]
+    Hybrid,
+}
+
+/// Configuration for auto pairs
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case", deny_unknown_fields, untagged)]
+pub enum AutoPairConfig {
+    /// Enables or disables auto pairing. False means disabled. True means to use the default pairs.
+    Enable(bool),
+
+    /// The mappings of pairs.
+    Pairs(HashMap<char, char>),
+}
+
+impl Default for AutoPairConfig {
+    fn default() -> Self {
+        AutoPairConfig::Enable(true)
+    }
+}
+
+impl From<&AutoPairConfig> for Option<AutoPairs> {
+    fn from(auto_pair_config: &AutoPairConfig) -> Self {
+        match auto_pair_config {
+            AutoPairConfig::Enable(false) => None,
+            AutoPairConfig::Enable(true) => Some(AutoPairs::default()),
+            AutoPairConfig::Pairs(pairs) => Some(AutoPairs::new(pairs.iter())),
+        }
+    }
+}
+
+impl From<AutoPairConfig> for Option<AutoPairs> {
+    fn from(auto_pairs_config: AutoPairConfig) -> Self {
+        (&auto_pairs_config).into()
+    }
+}
+
+impl FromStr for AutoPairConfig {
+    type Err = std::str::ParseBoolError;
+
+    // only do bool parsing for runtime setting
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        let enable: bool = s.parse()?;
+        Ok(AutoPairConfig::Enable(enable))
+    }
+}
+
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
+#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
+pub struct SoftWrap {
+    /// Soft wrap lines that exceed viewport width. Default to off
+    // NOTE: Option on purpose because the struct is shared between language config and global config.
+    // By default the option is None so that the language config falls back to the global config unless explicitly set.
+    pub enable: Option<bool>,
+    /// Maximum space left free at the end of the line.
+    /// This space is used to wrap text at word boundaries. If that is not possible within this limit
+    /// the word is simply split at the end of the line.
+    ///
+    /// This is automatically hard-limited to a quarter of the viewport to ensure correct display on small views.
+    ///
+    /// Default to 20
+    pub max_wrap: Option<u16>,
+    /// Maximum number of indentation that can be carried over from the previous line when softwrapping.
+    /// If a line is indented further then this limit it is rendered at the start of the viewport instead.
+    ///
+    /// This is automatically hard-limited to a quarter of the viewport to ensure correct display on small views.
+    ///
+    /// Default to 40
+    pub max_indent_retain: Option<u16>,
+    /// Indicator placed at the beginning of softwrapped lines
+    ///
+    /// Defaults to ↪
+    pub wrap_indicator: Option<String>,
+    /// Softwrap at `text_width` instead of viewport width if it is shorter
+    pub wrap_at_text_width: Option<bool>,
+}
+
+fn deserialize_regex<'de, D>(deserializer: D) -> Result<Option<rope::Regex>, D::Error>
+where
+    D: serde::Deserializer<'de>,
+{
+    Option::<String>::deserialize(deserializer)?
+        .map(|buf| rope::Regex::new(&buf).map_err(serde::de::Error::custom))
+        .transpose()
+}
+
+fn deserialize_lsp_config<'de, D>(deserializer: D) -> Result<Option<serde_json::Value>, D::Error>
+where
+    D: serde::Deserializer<'de>,
+{
+    Option::<toml::Value>::deserialize(deserializer)?
+        .map(|toml| toml.try_into().map_err(serde::de::Error::custom))
+        .transpose()
+}
+
+fn deserialize_tab_width<'de, D>(deserializer: D) -> Result<usize, D::Error>
+where
+    D: serde::Deserializer<'de>,
+{
+    usize::deserialize(deserializer).and_then(|n| {
+        if n > 0 && n <= 16 {
+            Ok(n)
+        } else {
+            Err(serde::de::Error::custom(
+                "tab width must be a value from 1 to 16 inclusive",
+            ))
+        }
+    })
+}
+
+pub fn deserialize_auto_pairs<'de, D>(deserializer: D) -> Result<Option<AutoPairs>, D::Error>
+where
+    D: serde::Deserializer<'de>,
+{
+    Ok(Option::<AutoPairConfig>::deserialize(deserializer)?.and_then(AutoPairConfig::into))
+}
+
+fn default_timeout() -> u64 {
+    20
+}
--- a/helix-core/src/textobject.rs	Tue May 13 15:21:51 2025 -0700
+++ b/helix-core/src/textobject.rs	Thu Feb 20 17:08:47 2025 -0500
@@ -7,7 +7,7 @@
 use crate::graphemes::{next_grapheme_boundary, prev_grapheme_boundary};
 use crate::line_ending::rope_is_line_ending;
 use crate::movement::Direction;
-use crate::syntax::LanguageConfiguration;
+use crate::syntax::config::LanguageConfiguration;
 use crate::Range;
 use crate::{surround, Syntax};
 
--- a/helix-core/tests/indent.rs	Tue May 13 15:21:51 2025 -0700
+++ b/helix-core/tests/indent.rs	Thu Feb 20 17:08:47 2025 -0500
@@ -1,7 +1,7 @@
 use arc_swap::ArcSwap;
 use helix_core::{
     indent::{indent_level_for_line, treesitter_indent_for_pos, IndentStyle},
-    syntax::{Configuration, Loader},
+    syntax::{config::Configuration, Loader},
     Syntax,
 };
 use helix_stdx::rope::RopeSliceExt;
--- a/helix-dap/src/client.rs	Tue May 13 15:21:51 2025 -0700
+++ b/helix-dap/src/client.rs	Thu Feb 20 17:08:47 2025 -0500
@@ -4,7 +4,7 @@
     types::*,
     Error, Result,
 };
-use helix_core::syntax::DebuggerQuirks;
+use helix_core::syntax::config::DebuggerQuirks;
 
 use serde_json::Value;
 
--- a/helix-lsp/src/client.rs	Tue May 13 15:21:51 2025 -0700
+++ b/helix-lsp/src/client.rs	Thu Feb 20 17:08:47 2025 -0500
@@ -10,7 +10,7 @@
     DidChangeWorkspaceFoldersParams, OneOf, PositionEncodingKind, SignatureHelp, Url,
     WorkspaceFolder, WorkspaceFoldersChangeEvent,
 };
-use helix_core::{find_workspace, syntax::LanguageServerFeature, ChangeSet, Rope};
+use helix_core::{find_workspace, syntax::config::LanguageServerFeature, ChangeSet, Rope};
 use helix_loader::VERSION_AND_GIT_HASH;
 use helix_stdx::path;
 use parking_lot::Mutex;
--- a/helix-lsp/src/lib.rs	Tue May 13 15:21:51 2025 -0700
+++ b/helix-lsp/src/lib.rs	Thu Feb 20 17:08:47 2025 -0500
@@ -12,7 +12,7 @@
 pub use lsp::{Position, Url};
 
 use futures_util::stream::select_all::SelectAll;
-use helix_core::syntax::{
+use helix_core::syntax::config::{
     LanguageConfiguration, LanguageServerConfiguration, LanguageServerFeatures,
 };
 use helix_stdx::path;
--- a/helix-term/src/commands.rs	Tue May 13 15:21:51 2025 -0700
+++ b/helix-term/src/commands.rs	Thu Feb 20 17:08:47 2025 -0500
@@ -34,7 +34,7 @@
     regex::{self, Regex},
     search::{self, CharMatcher},
     selection, surround,
-    syntax::{BlockCommentToken, LanguageServerFeature},
+    syntax::config::{BlockCommentToken, LanguageServerFeature},
     text_annotations::{Overlay, TextAnnotations},
     textobject,
     unicode::width::UnicodeWidthChar,
--- a/helix-term/src/commands/dap.rs	Tue May 13 15:21:51 2025 -0700
+++ b/helix-term/src/commands/dap.rs	Thu Feb 20 17:08:47 2025 -0500
@@ -5,7 +5,7 @@
     ui::{self, overlay::overlaid, Picker, Popup, Prompt, PromptEvent, Text},
 };
 use dap::{StackFrame, Thread, ThreadStates};
-use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate};
+use helix_core::syntax::config::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate};
 use helix_dap::{self as dap, Client};
 use helix_lsp::block_on;
 use helix_view::editor::Breakpoint;
--- a/helix-term/src/commands/lsp.rs	Tue May 13 15:21:51 2025 -0700
+++ b/helix-term/src/commands/lsp.rs	Thu Feb 20 17:08:47 2025 -0500
@@ -14,7 +14,7 @@
 use super::{align_view, push_jump, Align, Context, Editor};
 
 use helix_core::{
-    diagnostic::DiagnosticProvider, syntax::LanguageServerFeature,
+    diagnostic::DiagnosticProvider, syntax::config::LanguageServerFeature,
     text_annotations::InlineAnnotation, Selection, Uri,
 };
 use helix_stdx::path;
--- a/helix-term/src/handlers/completion.rs	Tue May 13 15:21:51 2025 -0700
+++ b/helix-term/src/handlers/completion.rs	Thu Feb 20 17:08:47 2025 -0500
@@ -2,7 +2,7 @@
 
 use helix_core::chars::char_is_word;
 use helix_core::completion::CompletionProvider;
-use helix_core::syntax::LanguageServerFeature;
+use helix_core::syntax::config::LanguageServerFeature;
 use helix_event::{register_hook, TaskHandle};
 use helix_lsp::lsp;
 use helix_stdx::rope::RopeSliceExt;
--- a/helix-term/src/handlers/completion/request.rs	Tue May 13 15:21:51 2025 -0700
+++ b/helix-term/src/handlers/completion/request.rs	Thu Feb 20 17:08:47 2025 -0500
@@ -5,7 +5,7 @@
 use arc_swap::ArcSwap;
 use futures_util::Future;
 use helix_core::completion::CompletionProvider;
-use helix_core::syntax::LanguageServerFeature;
+use helix_core::syntax::config::LanguageServerFeature;
 use helix_event::{cancelable_future, TaskController, TaskHandle};
 use helix_lsp::lsp;
 use helix_lsp::lsp::{CompletionContext, CompletionTriggerKind};
--- a/helix-term/src/handlers/document_colors.rs	Tue May 13 15:21:51 2025 -0700
+++ b/helix-term/src/handlers/document_colors.rs	Thu Feb 20 17:08:47 2025 -0500
@@ -1,7 +1,7 @@
 use std::{collections::HashSet, time::Duration};
 
 use futures_util::{stream::FuturesOrdered, StreamExt};
-use helix_core::{syntax::LanguageServerFeature, text_annotations::InlineAnnotation};
+use helix_core::{syntax::config::LanguageServerFeature, text_annotations::InlineAnnotation};
 use helix_event::{cancelable_future, register_hook};
 use helix_lsp::lsp;
 use helix_view::{
--- a/helix-term/src/handlers/signature_help.rs	Tue May 13 15:21:51 2025 -0700
+++ b/helix-term/src/handlers/signature_help.rs	Thu Feb 20 17:08:47 2025 -0500
@@ -1,7 +1,7 @@
 use std::sync::Arc;
 use std::time::Duration;
 
-use helix_core::syntax::LanguageServerFeature;
+use helix_core::syntax::config::LanguageServerFeature;
 use helix_event::{cancelable_future, register_hook, send_blocking, TaskController, TaskHandle};
 use helix_lsp::lsp::{self, SignatureInformation};
 use helix_stdx::rope::RopeSliceExt;
--- a/helix-term/src/ui/mod.rs	Tue May 13 15:21:51 2025 -0700
+++ b/helix-term/src/ui/mod.rs	Thu Feb 20 17:08:47 2025 -0500
@@ -373,7 +373,7 @@
     use crate::ui::prompt::Completion;
     use helix_core::command_line::{self, Tokenizer};
     use helix_core::fuzzy::fuzzy_match;
-    use helix_core::syntax::LanguageServerFeature;
+    use helix_core::syntax::config::LanguageServerFeature;
     use helix_view::document::SCRATCH_BUFFER_NAME;
     use helix_view::theme;
     use helix_view::{editor::Config, Editor};
--- a/helix-term/tests/integration.rs	Tue May 13 15:21:51 2025 -0700
+++ b/helix-term/tests/integration.rs	Thu Feb 20 17:08:47 2025 -0500
@@ -2,7 +2,7 @@
 mod test {
     mod helpers;
 
-    use helix_core::{syntax::AutoPairConfig, Selection};
+    use helix_core::{syntax::config::AutoPairConfig, Selection};
     use helix_term::config::Config;
 
     use indoc::indoc;
--- a/helix-view/src/document.rs	Tue May 13 15:21:51 2025 -0700
+++ b/helix-view/src/document.rs	Thu Feb 20 17:08:47 2025 -0500
@@ -10,7 +10,7 @@
 use helix_core::doc_formatter::TextFormat;
 use helix_core::encoding::Encoding;
 use helix_core::snippets::{ActiveSnippet, SnippetRenderCtx};
-use helix_core::syntax::{Highlight, LanguageServerFeature};
+use helix_core::syntax::{config::LanguageServerFeature, Highlight};
 use helix_core::text_annotations::{InlineAnnotation, Overlay};
 use helix_event::TaskController;
 use helix_lsp::util::lsp_pos_to_pos;
@@ -39,7 +39,7 @@
     history::{History, State, UndoKind},
     indent::{auto_detect_indent_style, IndentStyle},
     line_ending::auto_detect_line_ending,
-    syntax::{self, LanguageConfiguration},
+    syntax::{self, config::LanguageConfiguration},
     ChangeSet, Diagnostic, LineEnding, Range, Rope, RopeBuilder, Selection, Syntax, Transaction,
 };
 
@@ -1134,7 +1134,7 @@
     pub fn detect_language_config(
         &self,
         config_loader: &syntax::Loader,
-    ) -> Option<Arc<helix_core::syntax::LanguageConfiguration>> {
+    ) -> Option<Arc<syntax::config::LanguageConfiguration>> {
         config_loader
             .language_config_for_file_name(self.path.as_ref()?)
             .or_else(|| config_loader.language_config_for_shebang(self.text().slice(..)))
@@ -1276,8 +1276,8 @@
     /// if it exists.
     pub fn set_language(
         &mut self,
-        language_config: Option<Arc<helix_core::syntax::LanguageConfiguration>>,
-        loader: Option<Arc<ArcSwap<helix_core::syntax::Loader>>>,
+        language_config: Option<Arc<syntax::config::LanguageConfiguration>>,
+        loader: Option<Arc<ArcSwap<syntax::Loader>>>,
     ) {
         if let (Some(language_config), Some(loader)) = (language_config, loader) {
             if let Some(highlight_config) =
@@ -1294,7 +1294,7 @@
     }
 
     /// Set the programming language for the file if you know the language but don't have the
-    /// [`syntax::LanguageConfiguration`] for it.
+    /// [`syntax::config::LanguageConfiguration`] for it.
     pub fn set_language_by_language_id(
         &mut self,
         language_id: &str,
--- a/helix-view/src/editor.rs	Tue May 13 15:21:51 2025 -0700
+++ b/helix-view/src/editor.rs	Thu Feb 20 17:08:47 2025 -0500
@@ -46,7 +46,10 @@
 use helix_core::{
     auto_pairs::AutoPairs,
     diagnostic::DiagnosticProvider,
-    syntax::{self, AutoPairConfig, IndentationHeuristic, LanguageServerFeature, SoftWrap},
+    syntax::{
+        self,
+        config::{AutoPairConfig, IndentationHeuristic, LanguageServerFeature, SoftWrap},
+    },
     Change, LineEnding, Position, Range, Selection, Uri, NATIVE_LINE_ENDING,
 };
 use helix_dap as dap;
--- a/helix-view/src/gutter.rs	Tue May 13 15:21:51 2025 -0700
+++ b/helix-view/src/gutter.rs	Thu Feb 20 17:08:47 2025 -0500
@@ -1,6 +1,6 @@
 use std::fmt::Write;
 
-use helix_core::syntax::LanguageServerFeature;
+use helix_core::syntax::config::LanguageServerFeature;
 
 use crate::{
     editor::GutterType,
--- a/xtask/src/helpers.rs	Tue May 13 15:21:51 2025 -0700
+++ b/xtask/src/helpers.rs	Thu Feb 20 17:08:47 2025 -0500
@@ -1,7 +1,7 @@
 use std::path::{Path, PathBuf};
 
 use crate::path;
-use helix_core::syntax::Configuration as LangConfig;
+use helix_core::syntax::config::Configuration as LangConfig;
 use helix_term::health::TsFeature;
 
 /// Get the list of languages that support a particular tree-sitter