1use crate::LanguageName;
2use collections::{HashMap, HashSet, IndexSet};
3use gpui::SharedString;
4use lsp::LanguageServerName;
5use regex::Regex;
6use schemars::{JsonSchema, SchemaGenerator, json_schema};
7use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
8use std::{num::NonZeroU32, path::Path, sync::Arc};
9use util::serde::default_true;
10
11/// Controls the soft-wrapping behavior in the editor.
12#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
13#[serde(rename_all = "snake_case")]
14pub enum SoftWrap {
15 /// Prefer a single line generally, unless an overly long line is encountered.
16 None,
17 /// Deprecated: use None instead. Left to avoid breaking existing users' configs.
18 /// Prefer a single line generally, unless an overly long line is encountered.
19 PreferLine,
20 /// Soft wrap lines that exceed the editor width.
21 EditorWidth,
22 /// Soft wrap lines at the preferred line length.
23 PreferredLineLength,
24 /// Soft wrap line at the preferred line length or the editor width (whichever is smaller).
25 Bounded,
26}
27
28/// Top-level configuration for a language, typically loaded from a `config.toml`
29/// shipped alongside the grammar.
30#[derive(Clone, Debug, Deserialize, JsonSchema)]
31pub struct LanguageConfig {
32 /// Human-readable name of the language.
33 pub name: LanguageName,
34 /// The name of this language for a Markdown code fence block
35 pub code_fence_block_name: Option<Arc<str>>,
36 /// Alternative language names that Jupyter kernels may report for this language.
37 /// Used when a kernel's `language` field differs from Zed's language name.
38 /// For example, the Nu extension would set this to `["nushell"]`.
39 #[serde(default)]
40 pub kernel_language_names: Vec<Arc<str>>,
41 // The name of the grammar in a WASM bundle (experimental).
42 pub grammar: Option<Arc<str>>,
43 /// The criteria for matching this language to a given file.
44 #[serde(flatten)]
45 pub matcher: LanguageMatcher,
46 /// List of bracket types in a language.
47 #[serde(default)]
48 pub brackets: BracketPairConfig,
49 /// If set to true, auto indentation uses last non empty line to determine
50 /// the indentation level for a new line.
51 #[serde(default = "auto_indent_using_last_non_empty_line_default")]
52 pub auto_indent_using_last_non_empty_line: bool,
53 // Whether indentation of pasted content should be adjusted based on the context.
54 #[serde(default)]
55 pub auto_indent_on_paste: Option<bool>,
56 /// A regex that is used to determine whether the indentation level should be
57 /// increased in the following line.
58 #[serde(default, deserialize_with = "deserialize_regex")]
59 #[schemars(schema_with = "regex_json_schema")]
60 pub increase_indent_pattern: Option<Regex>,
61 /// A regex that is used to determine whether the indentation level should be
62 /// decreased in the following line.
63 #[serde(default, deserialize_with = "deserialize_regex")]
64 #[schemars(schema_with = "regex_json_schema")]
65 pub decrease_indent_pattern: Option<Regex>,
66 /// A list of rules for decreasing indentation. Each rule pairs a regex with a set of valid
67 /// "block-starting" tokens. When a line matches a pattern, its indentation is aligned with
68 /// the most recent line that began with a corresponding token. This enables context-aware
69 /// outdenting, like aligning an `else` with its `if`.
70 #[serde(default)]
71 pub decrease_indent_patterns: Vec<DecreaseIndentConfig>,
72 /// A list of characters that trigger the automatic insertion of a closing
73 /// bracket when they immediately precede the point where an opening
74 /// bracket is inserted.
75 #[serde(default)]
76 pub autoclose_before: String,
77 /// A placeholder used internally by Semantic Index.
78 #[serde(default)]
79 pub collapsed_placeholder: String,
80 /// A line comment string that is inserted in e.g. `toggle comments` action.
81 /// A language can have multiple flavours of line comments. All of the provided line comments are
82 /// used for comment continuations on the next line, but only the first one is used for Editor::ToggleComments.
83 #[serde(default)]
84 pub line_comments: Vec<Arc<str>>,
85 /// Delimiters and configuration for recognizing and formatting block comments.
86 #[serde(default)]
87 pub block_comment: Option<BlockCommentConfig>,
88 /// Delimiters and configuration for recognizing and formatting documentation comments.
89 #[serde(default, alias = "documentation")]
90 pub documentation_comment: Option<BlockCommentConfig>,
91 /// List markers that are inserted unchanged on newline (e.g., `- `, `* `, `+ `).
92 #[serde(default)]
93 pub unordered_list: Vec<Arc<str>>,
94 /// Configuration for ordered lists with auto-incrementing numbers on newline (e.g., `1. ` becomes `2. `).
95 #[serde(default)]
96 pub ordered_list: Vec<OrderedListConfig>,
97 /// Configuration for task lists where multiple markers map to a single continuation prefix (e.g., `- [x] ` continues as `- [ ] `).
98 #[serde(default)]
99 pub task_list: Option<TaskListConfig>,
100 /// A list of additional regex patterns that should be treated as prefixes
101 /// for creating boundaries during rewrapping, ensuring content from one
102 /// prefixed section doesn't merge with another (e.g., markdown list items).
103 /// By default, Zed treats as paragraph and comment prefixes as boundaries.
104 #[serde(default, deserialize_with = "deserialize_regex_vec")]
105 #[schemars(schema_with = "regex_vec_json_schema")]
106 pub rewrap_prefixes: Vec<Regex>,
107 /// A list of language servers that are allowed to run on subranges of a given language.
108 #[serde(default)]
109 pub scope_opt_in_language_servers: Vec<LanguageServerName>,
110 #[serde(default)]
111 pub overrides: HashMap<String, LanguageConfigOverride>,
112 /// A list of characters that Zed should treat as word characters for the
113 /// purpose of features that operate on word boundaries, like 'move to next word end'
114 /// or a whole-word search in buffer search.
115 #[serde(default)]
116 pub word_characters: HashSet<char>,
117 /// Whether to indent lines using tab characters, as opposed to multiple
118 /// spaces.
119 #[serde(default)]
120 pub hard_tabs: Option<bool>,
121 /// How many columns a tab should occupy.
122 #[serde(default)]
123 #[schemars(range(min = 1, max = 128))]
124 pub tab_size: Option<NonZeroU32>,
125 /// How to soft-wrap long lines of text.
126 #[serde(default)]
127 pub soft_wrap: Option<SoftWrap>,
128 /// When set, selections can be wrapped using prefix/suffix pairs on both sides.
129 #[serde(default)]
130 pub wrap_characters: Option<WrapCharactersConfig>,
131 /// The name of a Prettier parser that will be used for this language when no file path is available.
132 /// If there's a parser name in the language settings, that will be used instead.
133 #[serde(default)]
134 pub prettier_parser_name: Option<String>,
135 /// If true, this language is only for syntax highlighting via an injection into other
136 /// languages, but should not appear to the user as a distinct language.
137 #[serde(default)]
138 pub hidden: bool,
139 /// If configured, this language contains JSX style tags, and should support auto-closing of those tags.
140 #[serde(default)]
141 pub jsx_tag_auto_close: Option<JsxTagAutoCloseConfig>,
142 /// A list of characters that Zed should treat as word characters for completion queries.
143 #[serde(default)]
144 pub completion_query_characters: HashSet<char>,
145 /// A list of characters that Zed should treat as word characters for linked edit operations.
146 #[serde(default)]
147 pub linked_edit_characters: HashSet<char>,
148 /// A list of preferred debuggers for this language.
149 #[serde(default)]
150 pub debuggers: IndexSet<SharedString>,
151}
152
153impl LanguageConfig {
154 pub const FILE_NAME: &str = "config.toml";
155
156 pub fn load(config_path: impl AsRef<Path>) -> anyhow::Result<Self> {
157 let config = std::fs::read_to_string(config_path.as_ref())?;
158 toml::from_str(&config).map_err(Into::into)
159 }
160}
161
162impl Default for LanguageConfig {
163 fn default() -> Self {
164 Self {
165 name: LanguageName::new_static(""),
166 code_fence_block_name: None,
167 kernel_language_names: Default::default(),
168 grammar: None,
169 matcher: LanguageMatcher::default(),
170 brackets: Default::default(),
171 auto_indent_using_last_non_empty_line: auto_indent_using_last_non_empty_line_default(),
172 auto_indent_on_paste: None,
173 increase_indent_pattern: Default::default(),
174 decrease_indent_pattern: Default::default(),
175 decrease_indent_patterns: Default::default(),
176 autoclose_before: Default::default(),
177 line_comments: Default::default(),
178 block_comment: Default::default(),
179 documentation_comment: Default::default(),
180 unordered_list: Default::default(),
181 ordered_list: Default::default(),
182 task_list: Default::default(),
183 rewrap_prefixes: Default::default(),
184 scope_opt_in_language_servers: Default::default(),
185 overrides: Default::default(),
186 word_characters: Default::default(),
187 collapsed_placeholder: Default::default(),
188 hard_tabs: None,
189 tab_size: None,
190 soft_wrap: None,
191 wrap_characters: None,
192 prettier_parser_name: None,
193 hidden: false,
194 jsx_tag_auto_close: None,
195 completion_query_characters: Default::default(),
196 linked_edit_characters: Default::default(),
197 debuggers: Default::default(),
198 }
199 }
200}
201
202#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
203pub struct DecreaseIndentConfig {
204 #[serde(default, deserialize_with = "deserialize_regex")]
205 #[schemars(schema_with = "regex_json_schema")]
206 pub pattern: Option<Regex>,
207 #[serde(default)]
208 pub valid_after: Vec<String>,
209}
210
211/// Configuration for continuing ordered lists with auto-incrementing numbers.
212#[derive(Clone, Debug, Deserialize, JsonSchema)]
213pub struct OrderedListConfig {
214 /// A regex pattern with a capture group for the number portion (e.g., `(\\d+)\\. `).
215 pub pattern: String,
216 /// A format string where `{1}` is replaced with the incremented number (e.g., `{1}. `).
217 pub format: String,
218}
219
220/// Configuration for continuing task lists on newline.
221#[derive(Clone, Debug, Deserialize, JsonSchema)]
222pub struct TaskListConfig {
223 /// The list markers to match (e.g., `- [ ] `, `- [x] `).
224 pub prefixes: Vec<Arc<str>>,
225 /// The marker to insert when continuing the list on a new line (e.g., `- [ ] `).
226 pub continuation: Arc<str>,
227}
228
229#[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)]
230pub struct LanguageMatcher {
231 /// Given a list of `LanguageConfig`'s, the language of a file can be determined based on the path extension matching any of the `path_suffixes`.
232 #[serde(default)]
233 pub path_suffixes: Vec<String>,
234 /// A regex pattern that determines whether the language should be assigned to a file or not.
235 #[serde(
236 default,
237 serialize_with = "serialize_regex",
238 deserialize_with = "deserialize_regex"
239 )]
240 #[schemars(schema_with = "regex_json_schema")]
241 pub first_line_pattern: Option<Regex>,
242 /// Alternative names for this language used in vim/emacs modelines.
243 /// These are matched case-insensitively against the `mode` (emacs) or
244 /// `filetype`/`ft` (vim) specified in the modeline.
245 #[serde(default)]
246 pub modeline_aliases: Vec<String>,
247}
248
249impl Ord for LanguageMatcher {
250 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
251 self.path_suffixes
252 .cmp(&other.path_suffixes)
253 .then_with(|| {
254 self.first_line_pattern
255 .as_ref()
256 .map(Regex::as_str)
257 .cmp(&other.first_line_pattern.as_ref().map(Regex::as_str))
258 })
259 .then_with(|| self.modeline_aliases.cmp(&other.modeline_aliases))
260 }
261}
262
263impl PartialOrd for LanguageMatcher {
264 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
265 Some(self.cmp(other))
266 }
267}
268
269impl Eq for LanguageMatcher {}
270
271impl PartialEq for LanguageMatcher {
272 fn eq(&self, other: &Self) -> bool {
273 self.path_suffixes == other.path_suffixes
274 && self.first_line_pattern.as_ref().map(Regex::as_str)
275 == other.first_line_pattern.as_ref().map(Regex::as_str)
276 && self.modeline_aliases == other.modeline_aliases
277 }
278}
279
280/// The configuration for JSX tag auto-closing.
281#[derive(Clone, Deserialize, JsonSchema, Debug)]
282pub struct JsxTagAutoCloseConfig {
283 /// The name of the node for a opening tag
284 pub open_tag_node_name: String,
285 /// The name of the node for an closing tag
286 pub close_tag_node_name: String,
287 /// The name of the node for a complete element with children for open and close tags
288 pub jsx_element_node_name: String,
289 /// The name of the node found within both opening and closing
290 /// tags that describes the tag name
291 pub tag_name_node_name: String,
292 /// Alternate Node names for tag names.
293 /// Specifically needed as TSX represents the name in `<Foo.Bar>`
294 /// as `member_expression` rather than `identifier` as usual
295 #[serde(default)]
296 pub tag_name_node_name_alternates: Vec<String>,
297 /// Some grammars are smart enough to detect a closing tag
298 /// that is not valid i.e. doesn't match it's corresponding
299 /// opening tag or does not have a corresponding opening tag
300 /// This should be set to the name of the node for invalid
301 /// closing tags if the grammar contains such a node, otherwise
302 /// detecting already closed tags will not work properly
303 #[serde(default)]
304 pub erroneous_close_tag_node_name: Option<String>,
305 /// See above for erroneous_close_tag_node_name for details
306 /// This should be set if the node used for the tag name
307 /// within erroneous closing tags is different from the
308 /// normal tag name node name
309 #[serde(default)]
310 pub erroneous_close_tag_name_node_name: Option<String>,
311}
312
313/// The configuration for block comments for this language.
314#[derive(Clone, Debug, JsonSchema, PartialEq)]
315pub struct BlockCommentConfig {
316 /// A start tag of block comment.
317 pub start: Arc<str>,
318 /// A end tag of block comment.
319 pub end: Arc<str>,
320 /// A character to add as a prefix when a new line is added to a block comment.
321 pub prefix: Arc<str>,
322 /// A indent to add for prefix and end line upon new line.
323 #[schemars(range(min = 1, max = 128))]
324 pub tab_size: u32,
325}
326
327impl<'de> Deserialize<'de> for BlockCommentConfig {
328 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
329 where
330 D: Deserializer<'de>,
331 {
332 #[derive(Deserialize)]
333 #[serde(untagged)]
334 enum BlockCommentConfigHelper {
335 New {
336 start: Arc<str>,
337 end: Arc<str>,
338 prefix: Arc<str>,
339 tab_size: u32,
340 },
341 Old([Arc<str>; 2]),
342 }
343
344 match BlockCommentConfigHelper::deserialize(deserializer)? {
345 BlockCommentConfigHelper::New {
346 start,
347 end,
348 prefix,
349 tab_size,
350 } => Ok(BlockCommentConfig {
351 start,
352 end,
353 prefix,
354 tab_size,
355 }),
356 BlockCommentConfigHelper::Old([start, end]) => Ok(BlockCommentConfig {
357 start,
358 end,
359 prefix: "".into(),
360 tab_size: 0,
361 }),
362 }
363 }
364}
365
366#[derive(Clone, Deserialize, Default, Debug, JsonSchema)]
367pub struct LanguageConfigOverride {
368 #[serde(default)]
369 pub line_comments: Override<Vec<Arc<str>>>,
370 #[serde(default)]
371 pub block_comment: Override<BlockCommentConfig>,
372 #[serde(skip)]
373 pub disabled_bracket_ixs: Vec<u16>,
374 #[serde(default)]
375 pub word_characters: Override<HashSet<char>>,
376 #[serde(default)]
377 pub completion_query_characters: Override<HashSet<char>>,
378 #[serde(default)]
379 pub linked_edit_characters: Override<HashSet<char>>,
380 #[serde(default)]
381 pub opt_into_language_servers: Vec<LanguageServerName>,
382 #[serde(default)]
383 pub prefer_label_for_snippet: Option<bool>,
384}
385
386#[derive(Clone, Deserialize, Debug, Serialize, JsonSchema)]
387#[serde(untagged)]
388pub enum Override<T> {
389 Remove { remove: bool },
390 Set(T),
391}
392
393impl<T> Default for Override<T> {
394 fn default() -> Self {
395 Override::Remove { remove: false }
396 }
397}
398
399impl<T> Override<T> {
400 pub fn as_option<'a>(this: Option<&'a Self>, original: Option<&'a T>) -> Option<&'a T> {
401 match this {
402 Some(Self::Set(value)) => Some(value),
403 Some(Self::Remove { remove: true }) => None,
404 Some(Self::Remove { remove: false }) | None => original,
405 }
406 }
407}
408
409/// Configuration of handling bracket pairs for a given language.
410///
411/// This struct includes settings for defining which pairs of characters are considered brackets and
412/// also specifies any language-specific scopes where these pairs should be ignored for bracket matching purposes.
413#[derive(Clone, Debug, Default, JsonSchema)]
414#[schemars(with = "Vec::<BracketPairContent>")]
415pub struct BracketPairConfig {
416 /// A list of character pairs that should be treated as brackets in the context of a given language.
417 pub pairs: Vec<BracketPair>,
418 /// A list of tree-sitter scopes for which a given bracket should not be active.
419 /// N-th entry in `[Self::disabled_scopes_by_bracket_ix]` contains a list of disabled scopes for an n-th entry in `[Self::pairs]`
420 pub disabled_scopes_by_bracket_ix: Vec<Vec<String>>,
421}
422
423impl BracketPairConfig {
424 pub fn is_closing_brace(&self, c: char) -> bool {
425 self.pairs.iter().any(|pair| pair.end.starts_with(c))
426 }
427}
428
429#[derive(Deserialize, JsonSchema)]
430pub struct BracketPairContent {
431 #[serde(flatten)]
432 pub bracket_pair: BracketPair,
433 #[serde(default)]
434 pub not_in: Vec<String>,
435}
436
437impl<'de> Deserialize<'de> for BracketPairConfig {
438 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
439 where
440 D: Deserializer<'de>,
441 {
442 let result = Vec::<BracketPairContent>::deserialize(deserializer)?;
443 let (brackets, disabled_scopes_by_bracket_ix) = result
444 .into_iter()
445 .map(|entry| (entry.bracket_pair, entry.not_in))
446 .unzip();
447
448 Ok(BracketPairConfig {
449 pairs: brackets,
450 disabled_scopes_by_bracket_ix,
451 })
452 }
453}
454
455/// Describes a single bracket pair and how an editor should react to e.g. inserting
456/// an opening bracket or to a newline character insertion in between `start` and `end` characters.
457#[derive(Clone, Debug, Default, Deserialize, PartialEq, JsonSchema)]
458pub struct BracketPair {
459 /// Starting substring for a bracket.
460 pub start: String,
461 /// Ending substring for a bracket.
462 pub end: String,
463 /// True if `end` should be automatically inserted right after `start` characters.
464 pub close: bool,
465 /// True if selected text should be surrounded by `start` and `end` characters.
466 #[serde(default = "default_true")]
467 pub surround: bool,
468 /// True if an extra newline should be inserted while the cursor is in the middle
469 /// of that bracket pair.
470 pub newline: bool,
471}
472
473#[derive(Clone, Debug, Deserialize, JsonSchema)]
474pub struct WrapCharactersConfig {
475 /// Opening token split into a prefix and suffix. The first caret goes
476 /// after the prefix (i.e., between prefix and suffix).
477 pub start_prefix: String,
478 pub start_suffix: String,
479 /// Closing token split into a prefix and suffix. The second caret goes
480 /// after the prefix (i.e., between prefix and suffix).
481 pub end_prefix: String,
482 pub end_suffix: String,
483}
484
485pub fn auto_indent_using_last_non_empty_line_default() -> bool {
486 true
487}
488
489pub fn deserialize_regex<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Regex>, D::Error> {
490 let source = Option::<String>::deserialize(d)?;
491 if let Some(source) = source {
492 Ok(Some(regex::Regex::new(&source).map_err(de::Error::custom)?))
493 } else {
494 Ok(None)
495 }
496}
497
498pub fn regex_json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
499 json_schema!({
500 "type": "string"
501 })
502}
503
504pub fn serialize_regex<S>(regex: &Option<Regex>, serializer: S) -> Result<S::Ok, S::Error>
505where
506 S: Serializer,
507{
508 match regex {
509 Some(regex) => serializer.serialize_str(regex.as_str()),
510 None => serializer.serialize_none(),
511 }
512}
513
514pub fn deserialize_regex_vec<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<Regex>, D::Error> {
515 let sources = Vec::<String>::deserialize(d)?;
516 sources
517 .into_iter()
518 .map(|source| regex::Regex::new(&source))
519 .collect::<Result<_, _>>()
520 .map_err(de::Error::custom)
521}
522
523pub fn regex_vec_json_schema(_: &mut SchemaGenerator) -> schemars::Schema {
524 json_schema!({
525 "type": "array",
526 "items": { "type": "string" }
527 })
528}