grammar.rs

  1use crate::{
  2    HighlightId, HighlightMap, LanguageConfig, LanguageConfigOverride, LanguageName,
  3    LanguageQueries, language_config::BracketPairConfig,
  4};
  5use anyhow::{Context as _, Result};
  6use collections::HashMap;
  7use gpui::SharedString;
  8use lsp::LanguageServerName;
  9use parking_lot::Mutex;
 10use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};
 11use tree_sitter::Query;
 12
 13pub static NEXT_GRAMMAR_ID: AtomicUsize = AtomicUsize::new(0);
 14
 15#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
 16pub struct GrammarId(pub usize);
 17
 18impl GrammarId {
 19    pub fn new() -> Self {
 20        Self(NEXT_GRAMMAR_ID.fetch_add(1, SeqCst))
 21    }
 22}
 23
 24impl Default for GrammarId {
 25    fn default() -> Self {
 26        Self::new()
 27    }
 28}
 29
 30pub struct Grammar {
 31    id: GrammarId,
 32    pub ts_language: tree_sitter::Language,
 33    pub error_query: Option<Query>,
 34    pub highlights_config: Option<HighlightsConfig>,
 35    pub brackets_config: Option<BracketsConfig>,
 36    pub redactions_config: Option<RedactionConfig>,
 37    pub runnable_config: Option<RunnableConfig>,
 38    pub indents_config: Option<IndentConfig>,
 39    pub outline_config: Option<OutlineConfig>,
 40    pub text_object_config: Option<TextObjectConfig>,
 41    pub injection_config: Option<InjectionConfig>,
 42    pub override_config: Option<OverrideConfig>,
 43    pub debug_variables_config: Option<DebugVariablesConfig>,
 44    pub imports_config: Option<ImportsConfig>,
 45    pub highlight_map: Mutex<HighlightMap>,
 46}
 47
 48pub struct HighlightsConfig {
 49    pub query: Query,
 50    pub identifier_capture_indices: Vec<u32>,
 51}
 52
 53pub struct IndentConfig {
 54    pub query: Query,
 55    pub indent_capture_ix: u32,
 56    pub start_capture_ix: Option<u32>,
 57    pub end_capture_ix: Option<u32>,
 58    pub outdent_capture_ix: Option<u32>,
 59    pub suffixed_start_captures: HashMap<u32, SharedString>,
 60}
 61
 62pub struct OutlineConfig {
 63    pub query: Query,
 64    pub item_capture_ix: u32,
 65    pub name_capture_ix: u32,
 66    pub context_capture_ix: Option<u32>,
 67    pub extra_context_capture_ix: Option<u32>,
 68    pub open_capture_ix: Option<u32>,
 69    pub close_capture_ix: Option<u32>,
 70    pub annotation_capture_ix: Option<u32>,
 71}
 72
 73#[derive(Debug, Clone, Copy, PartialEq)]
 74pub enum DebuggerTextObject {
 75    Variable,
 76    Scope,
 77}
 78
 79impl DebuggerTextObject {
 80    pub fn from_capture_name(name: &str) -> Option<DebuggerTextObject> {
 81        match name {
 82            "debug-variable" => Some(DebuggerTextObject::Variable),
 83            "debug-scope" => Some(DebuggerTextObject::Scope),
 84            _ => None,
 85        }
 86    }
 87}
 88
 89#[derive(Debug, Clone, Copy, PartialEq)]
 90pub enum TextObject {
 91    InsideFunction,
 92    AroundFunction,
 93    InsideClass,
 94    AroundClass,
 95    InsideComment,
 96    AroundComment,
 97}
 98
 99impl TextObject {
100    pub fn from_capture_name(name: &str) -> Option<TextObject> {
101        match name {
102            "function.inside" => Some(TextObject::InsideFunction),
103            "function.around" => Some(TextObject::AroundFunction),
104            "class.inside" => Some(TextObject::InsideClass),
105            "class.around" => Some(TextObject::AroundClass),
106            "comment.inside" => Some(TextObject::InsideComment),
107            "comment.around" => Some(TextObject::AroundComment),
108            _ => None,
109        }
110    }
111
112    pub fn around(&self) -> Option<Self> {
113        match self {
114            TextObject::InsideFunction => Some(TextObject::AroundFunction),
115            TextObject::InsideClass => Some(TextObject::AroundClass),
116            TextObject::InsideComment => Some(TextObject::AroundComment),
117            _ => None,
118        }
119    }
120}
121
122pub struct TextObjectConfig {
123    pub query: Query,
124    pub text_objects_by_capture_ix: Vec<(u32, TextObject)>,
125}
126
127pub struct InjectionConfig {
128    pub query: Query,
129    pub content_capture_ix: u32,
130    pub language_capture_ix: Option<u32>,
131    pub patterns: Vec<InjectionPatternConfig>,
132}
133
134pub struct RedactionConfig {
135    pub query: Query,
136    pub redaction_capture_ix: u32,
137}
138
139#[derive(Clone, Debug, PartialEq)]
140pub enum RunnableCapture {
141    Named(SharedString),
142    Run,
143}
144
145pub struct RunnableConfig {
146    pub query: Query,
147    /// A mapping from capture index to capture kind
148    pub extra_captures: Vec<RunnableCapture>,
149}
150
151pub struct OverrideConfig {
152    pub query: Query,
153    pub values: HashMap<u32, OverrideEntry>,
154}
155
156#[derive(Debug)]
157pub struct OverrideEntry {
158    pub name: String,
159    pub range_is_inclusive: bool,
160    pub value: LanguageConfigOverride,
161}
162
163#[derive(Default, Clone)]
164pub struct InjectionPatternConfig {
165    pub language: Option<Box<str>>,
166    pub combined: bool,
167}
168
169#[derive(Debug)]
170pub struct BracketsConfig {
171    pub query: Query,
172    pub open_capture_ix: u32,
173    pub close_capture_ix: u32,
174    pub patterns: Vec<BracketsPatternConfig>,
175}
176
177#[derive(Clone, Debug, Default)]
178pub struct BracketsPatternConfig {
179    pub newline_only: bool,
180    pub rainbow_exclude: bool,
181}
182
183pub struct DebugVariablesConfig {
184    pub query: Query,
185    pub objects_by_capture_ix: Vec<(u32, DebuggerTextObject)>,
186}
187
188pub struct ImportsConfig {
189    pub query: Query,
190    pub import_ix: u32,
191    pub name_ix: Option<u32>,
192    pub namespace_ix: Option<u32>,
193    pub source_ix: Option<u32>,
194    pub list_ix: Option<u32>,
195    pub wildcard_ix: Option<u32>,
196    pub alias_ix: Option<u32>,
197}
198
199enum Capture<'a> {
200    Required(&'static str, &'a mut u32),
201    Optional(&'static str, &'a mut Option<u32>),
202}
203
204fn populate_capture_indices(
205    query: &Query,
206    language_name: &LanguageName,
207    query_type: &str,
208    expected_prefixes: &[&str],
209    captures: &mut [Capture<'_>],
210) -> bool {
211    let mut found_required_indices = Vec::new();
212    'outer: for (ix, name) in query.capture_names().iter().enumerate() {
213        for (required_ix, capture) in captures.iter_mut().enumerate() {
214            match capture {
215                Capture::Required(capture_name, index) if capture_name == name => {
216                    **index = ix as u32;
217                    found_required_indices.push(required_ix);
218                    continue 'outer;
219                }
220                Capture::Optional(capture_name, index) if capture_name == name => {
221                    **index = Some(ix as u32);
222                    continue 'outer;
223                }
224                _ => {}
225            }
226        }
227        if !name.starts_with("_")
228            && !expected_prefixes
229                .iter()
230                .any(|&prefix| name.starts_with(prefix))
231        {
232            log::warn!(
233                "unrecognized capture name '{}' in {} {} TreeSitter query \
234                (suppress this warning by prefixing with '_')",
235                name,
236                language_name,
237                query_type
238            );
239        }
240    }
241    let mut missing_required_captures = Vec::new();
242    for (capture_ix, capture) in captures.iter().enumerate() {
243        if let Capture::Required(capture_name, _) = capture
244            && !found_required_indices.contains(&capture_ix)
245        {
246            missing_required_captures.push(*capture_name);
247        }
248    }
249    let success = missing_required_captures.is_empty();
250    if !success {
251        log::error!(
252            "missing required capture(s) in {} {} TreeSitter query: {}",
253            language_name,
254            query_type,
255            missing_required_captures.join(", ")
256        );
257    }
258    success
259}
260
261impl Grammar {
262    pub fn new(ts_language: tree_sitter::Language) -> Self {
263        Self {
264            id: GrammarId::new(),
265            highlights_config: None,
266            brackets_config: None,
267            outline_config: None,
268            text_object_config: None,
269            indents_config: None,
270            injection_config: None,
271            override_config: None,
272            redactions_config: None,
273            runnable_config: None,
274            error_query: Query::new(&ts_language, "(ERROR) @error").ok(),
275            debug_variables_config: None,
276            imports_config: None,
277            ts_language,
278            highlight_map: Default::default(),
279        }
280    }
281
282    pub fn id(&self) -> GrammarId {
283        self.id
284    }
285
286    pub fn highlight_map(&self) -> HighlightMap {
287        self.highlight_map.lock().clone()
288    }
289
290    pub fn highlight_id_for_name(&self, name: &str) -> Option<HighlightId> {
291        let capture_id = self
292            .highlights_config
293            .as_ref()?
294            .query
295            .capture_index_for_name(name)?;
296        Some(self.highlight_map.lock().get(capture_id))
297    }
298
299    pub fn debug_variables_config(&self) -> Option<&DebugVariablesConfig> {
300        self.debug_variables_config.as_ref()
301    }
302
303    pub fn imports_config(&self) -> Option<&ImportsConfig> {
304        self.imports_config.as_ref()
305    }
306
307    /// Load all queries from `LanguageQueries` into this grammar, mutating the
308    /// associated `LanguageConfig` (the override query clears
309    /// `brackets.disabled_scopes_by_bracket_ix`).
310    pub fn with_queries(
311        mut self,
312        queries: LanguageQueries,
313        config: &mut LanguageConfig,
314    ) -> Result<Self> {
315        let name = &config.name;
316        if let Some(query) = queries.highlights {
317            self = self
318                .with_highlights_query(query.as_ref())
319                .context("Error loading highlights query")?;
320        }
321        if let Some(query) = queries.brackets {
322            self = self
323                .with_brackets_query(query.as_ref(), name)
324                .context("Error loading brackets query")?;
325        }
326        if let Some(query) = queries.indents {
327            self = self
328                .with_indents_query(query.as_ref(), name)
329                .context("Error loading indents query")?;
330        }
331        if let Some(query) = queries.outline {
332            self = self
333                .with_outline_query(query.as_ref(), name)
334                .context("Error loading outline query")?;
335        }
336        if let Some(query) = queries.injections {
337            self = self
338                .with_injection_query(query.as_ref(), name)
339                .context("Error loading injection query")?;
340        }
341        if let Some(query) = queries.overrides {
342            self = self
343                .with_override_query(
344                    query.as_ref(),
345                    name,
346                    &config.overrides,
347                    &mut config.brackets,
348                    &config.scope_opt_in_language_servers,
349                )
350                .context("Error loading override query")?;
351        }
352        if let Some(query) = queries.redactions {
353            self = self
354                .with_redaction_query(query.as_ref(), name)
355                .context("Error loading redaction query")?;
356        }
357        if let Some(query) = queries.runnables {
358            self = self
359                .with_runnable_query(query.as_ref())
360                .context("Error loading runnables query")?;
361        }
362        if let Some(query) = queries.text_objects {
363            self = self
364                .with_text_object_query(query.as_ref(), name)
365                .context("Error loading textobject query")?;
366        }
367        if let Some(query) = queries.debugger {
368            self = self
369                .with_debug_variables_query(query.as_ref(), name)
370                .context("Error loading debug variables query")?;
371        }
372        if let Some(query) = queries.imports {
373            self = self
374                .with_imports_query(query.as_ref(), name)
375                .context("Error loading imports query")?;
376        }
377        Ok(self)
378    }
379
380    pub fn with_highlights_query(mut self, source: &str) -> Result<Self> {
381        let query = Query::new(&self.ts_language, source)?;
382
383        let mut identifier_capture_indices = Vec::new();
384        for name in [
385            "variable",
386            "constant",
387            "constructor",
388            "function",
389            "function.method",
390            "function.method.call",
391            "function.special",
392            "property",
393            "type",
394            "type.interface",
395        ] {
396            identifier_capture_indices.extend(query.capture_index_for_name(name));
397        }
398
399        self.highlights_config = Some(HighlightsConfig {
400            query,
401            identifier_capture_indices,
402        });
403
404        Ok(self)
405    }
406
407    pub fn with_runnable_query(mut self, source: &str) -> Result<Self> {
408        let query = Query::new(&self.ts_language, source)?;
409        let extra_captures: Vec<_> = query
410            .capture_names()
411            .iter()
412            .map(|&name| match name {
413                "run" => RunnableCapture::Run,
414                name => RunnableCapture::Named(name.to_string().into()),
415            })
416            .collect();
417
418        self.runnable_config = Some(RunnableConfig {
419            extra_captures,
420            query,
421        });
422
423        Ok(self)
424    }
425
426    pub fn with_outline_query(
427        mut self,
428        source: &str,
429        language_name: &LanguageName,
430    ) -> Result<Self> {
431        let query = Query::new(&self.ts_language, source)?;
432        let mut item_capture_ix = 0;
433        let mut name_capture_ix = 0;
434        let mut context_capture_ix = None;
435        let mut extra_context_capture_ix = None;
436        let mut open_capture_ix = None;
437        let mut close_capture_ix = None;
438        let mut annotation_capture_ix = None;
439        if populate_capture_indices(
440            &query,
441            language_name,
442            "outline",
443            &[],
444            &mut [
445                Capture::Required("item", &mut item_capture_ix),
446                Capture::Required("name", &mut name_capture_ix),
447                Capture::Optional("context", &mut context_capture_ix),
448                Capture::Optional("context.extra", &mut extra_context_capture_ix),
449                Capture::Optional("open", &mut open_capture_ix),
450                Capture::Optional("close", &mut close_capture_ix),
451                Capture::Optional("annotation", &mut annotation_capture_ix),
452            ],
453        ) {
454            self.outline_config = Some(OutlineConfig {
455                query,
456                item_capture_ix,
457                name_capture_ix,
458                context_capture_ix,
459                extra_context_capture_ix,
460                open_capture_ix,
461                close_capture_ix,
462                annotation_capture_ix,
463            });
464        }
465        Ok(self)
466    }
467
468    pub fn with_text_object_query(
469        mut self,
470        source: &str,
471        language_name: &LanguageName,
472    ) -> Result<Self> {
473        let query = Query::new(&self.ts_language, source)?;
474
475        let mut text_objects_by_capture_ix = Vec::new();
476        for (ix, name) in query.capture_names().iter().enumerate() {
477            if let Some(text_object) = TextObject::from_capture_name(name) {
478                text_objects_by_capture_ix.push((ix as u32, text_object));
479            } else {
480                log::warn!(
481                    "unrecognized capture name '{}' in {} textobjects TreeSitter query",
482                    name,
483                    language_name,
484                );
485            }
486        }
487
488        self.text_object_config = Some(TextObjectConfig {
489            query,
490            text_objects_by_capture_ix,
491        });
492        Ok(self)
493    }
494
495    pub fn with_debug_variables_query(
496        mut self,
497        source: &str,
498        language_name: &LanguageName,
499    ) -> Result<Self> {
500        let query = Query::new(&self.ts_language, source)?;
501
502        let mut objects_by_capture_ix = Vec::new();
503        for (ix, name) in query.capture_names().iter().enumerate() {
504            if let Some(text_object) = DebuggerTextObject::from_capture_name(name) {
505                objects_by_capture_ix.push((ix as u32, text_object));
506            } else {
507                log::warn!(
508                    "unrecognized capture name '{}' in {} debugger TreeSitter query",
509                    name,
510                    language_name,
511                );
512            }
513        }
514
515        self.debug_variables_config = Some(DebugVariablesConfig {
516            query,
517            objects_by_capture_ix,
518        });
519        Ok(self)
520    }
521
522    pub fn with_imports_query(
523        mut self,
524        source: &str,
525        language_name: &LanguageName,
526    ) -> Result<Self> {
527        let query = Query::new(&self.ts_language, source)?;
528
529        let mut import_ix = 0;
530        let mut name_ix = None;
531        let mut namespace_ix = None;
532        let mut source_ix = None;
533        let mut list_ix = None;
534        let mut wildcard_ix = None;
535        let mut alias_ix = None;
536        if populate_capture_indices(
537            &query,
538            language_name,
539            "imports",
540            &[],
541            &mut [
542                Capture::Required("import", &mut import_ix),
543                Capture::Optional("name", &mut name_ix),
544                Capture::Optional("namespace", &mut namespace_ix),
545                Capture::Optional("source", &mut source_ix),
546                Capture::Optional("list", &mut list_ix),
547                Capture::Optional("wildcard", &mut wildcard_ix),
548                Capture::Optional("alias", &mut alias_ix),
549            ],
550        ) {
551            self.imports_config = Some(ImportsConfig {
552                query,
553                import_ix,
554                name_ix,
555                namespace_ix,
556                source_ix,
557                list_ix,
558                wildcard_ix,
559                alias_ix,
560            });
561        }
562        Ok(self)
563    }
564
565    pub fn with_brackets_query(
566        mut self,
567        source: &str,
568        language_name: &LanguageName,
569    ) -> Result<Self> {
570        let query = Query::new(&self.ts_language, source)?;
571        let mut open_capture_ix = 0;
572        let mut close_capture_ix = 0;
573        if populate_capture_indices(
574            &query,
575            language_name,
576            "brackets",
577            &[],
578            &mut [
579                Capture::Required("open", &mut open_capture_ix),
580                Capture::Required("close", &mut close_capture_ix),
581            ],
582        ) {
583            let patterns = (0..query.pattern_count())
584                .map(|ix| {
585                    let mut config = BracketsPatternConfig::default();
586                    for setting in query.property_settings(ix) {
587                        let setting_key = setting.key.as_ref();
588                        if setting_key == "newline.only" {
589                            config.newline_only = true
590                        }
591                        if setting_key == "rainbow.exclude" {
592                            config.rainbow_exclude = true
593                        }
594                    }
595                    config
596                })
597                .collect();
598            self.brackets_config = Some(BracketsConfig {
599                query,
600                open_capture_ix,
601                close_capture_ix,
602                patterns,
603            });
604        }
605        Ok(self)
606    }
607
608    pub fn with_indents_query(
609        mut self,
610        source: &str,
611        language_name: &LanguageName,
612    ) -> Result<Self> {
613        let query = Query::new(&self.ts_language, source)?;
614        let mut indent_capture_ix = 0;
615        let mut start_capture_ix = None;
616        let mut end_capture_ix = None;
617        let mut outdent_capture_ix = None;
618        if populate_capture_indices(
619            &query,
620            language_name,
621            "indents",
622            &["start."],
623            &mut [
624                Capture::Required("indent", &mut indent_capture_ix),
625                Capture::Optional("start", &mut start_capture_ix),
626                Capture::Optional("end", &mut end_capture_ix),
627                Capture::Optional("outdent", &mut outdent_capture_ix),
628            ],
629        ) {
630            let mut suffixed_start_captures = HashMap::default();
631            for (ix, name) in query.capture_names().iter().enumerate() {
632                if let Some(suffix) = name.strip_prefix("start.") {
633                    suffixed_start_captures.insert(ix as u32, suffix.to_owned().into());
634                }
635            }
636
637            self.indents_config = Some(IndentConfig {
638                query,
639                indent_capture_ix,
640                start_capture_ix,
641                end_capture_ix,
642                outdent_capture_ix,
643                suffixed_start_captures,
644            });
645        }
646        Ok(self)
647    }
648
649    pub fn with_injection_query(
650        mut self,
651        source: &str,
652        language_name: &LanguageName,
653    ) -> Result<Self> {
654        let query = Query::new(&self.ts_language, source)?;
655        let mut language_capture_ix = None;
656        let mut injection_language_capture_ix = None;
657        let mut content_capture_ix = None;
658        let mut injection_content_capture_ix = None;
659        if populate_capture_indices(
660            &query,
661            language_name,
662            "injections",
663            &[],
664            &mut [
665                Capture::Optional("language", &mut language_capture_ix),
666                Capture::Optional("injection.language", &mut injection_language_capture_ix),
667                Capture::Optional("content", &mut content_capture_ix),
668                Capture::Optional("injection.content", &mut injection_content_capture_ix),
669            ],
670        ) {
671            language_capture_ix = match (language_capture_ix, injection_language_capture_ix) {
672                (None, Some(ix)) => Some(ix),
673                (Some(_), Some(_)) => {
674                    anyhow::bail!("both language and injection.language captures are present");
675                }
676                _ => language_capture_ix,
677            };
678            content_capture_ix = match (content_capture_ix, injection_content_capture_ix) {
679                (None, Some(ix)) => Some(ix),
680                (Some(_), Some(_)) => {
681                    anyhow::bail!("both content and injection.content captures are present")
682                }
683                _ => content_capture_ix,
684            };
685            let patterns = (0..query.pattern_count())
686                .map(|ix| {
687                    let mut config = InjectionPatternConfig::default();
688                    for setting in query.property_settings(ix) {
689                        match setting.key.as_ref() {
690                            "language" | "injection.language" => {
691                                config.language.clone_from(&setting.value);
692                            }
693                            "combined" | "injection.combined" => {
694                                config.combined = true;
695                            }
696                            _ => {}
697                        }
698                    }
699                    config
700                })
701                .collect();
702            if let Some(content_capture_ix) = content_capture_ix {
703                self.injection_config = Some(InjectionConfig {
704                    query,
705                    language_capture_ix,
706                    content_capture_ix,
707                    patterns,
708                });
709            } else {
710                log::error!(
711                    "missing required capture in injections {} TreeSitter query: \
712                    content or injection.content",
713                    language_name,
714                );
715            }
716        }
717        Ok(self)
718    }
719
720    pub fn with_override_query(
721        mut self,
722        source: &str,
723        language_name: &LanguageName,
724        overrides: &HashMap<String, LanguageConfigOverride>,
725        brackets: &mut BracketPairConfig,
726        scope_opt_in_language_servers: &[LanguageServerName],
727    ) -> Result<Self> {
728        let query = Query::new(&self.ts_language, source)?;
729
730        let mut override_configs_by_id = HashMap::default();
731        for (ix, mut name) in query.capture_names().iter().copied().enumerate() {
732            let mut range_is_inclusive = false;
733            if name.starts_with('_') {
734                continue;
735            }
736            if let Some(prefix) = name.strip_suffix(".inclusive") {
737                name = prefix;
738                range_is_inclusive = true;
739            }
740
741            let value = overrides.get(name).cloned().unwrap_or_default();
742            for server_name in &value.opt_into_language_servers {
743                if !scope_opt_in_language_servers.contains(server_name) {
744                    util::debug_panic!(
745                        "Server {server_name:?} has been opted-in by scope {name:?} but has not been marked as an opt-in server"
746                    );
747                }
748            }
749
750            override_configs_by_id.insert(
751                ix as u32,
752                OverrideEntry {
753                    name: name.to_string(),
754                    range_is_inclusive,
755                    value,
756                },
757            );
758        }
759
760        let referenced_override_names = overrides
761            .keys()
762            .chain(brackets.disabled_scopes_by_bracket_ix.iter().flatten());
763
764        for referenced_name in referenced_override_names {
765            if !override_configs_by_id
766                .values()
767                .any(|entry| entry.name == *referenced_name)
768            {
769                anyhow::bail!(
770                    "language {:?} has overrides in config not in query: {referenced_name:?}",
771                    language_name
772                );
773            }
774        }
775
776        for entry in override_configs_by_id.values_mut() {
777            entry.value.disabled_bracket_ixs = brackets
778                .disabled_scopes_by_bracket_ix
779                .iter()
780                .enumerate()
781                .filter_map(|(ix, disabled_scope_names)| {
782                    if disabled_scope_names.contains(&entry.name) {
783                        Some(ix as u16)
784                    } else {
785                        None
786                    }
787                })
788                .collect();
789        }
790
791        brackets.disabled_scopes_by_bracket_ix.clear();
792
793        self.override_config = Some(OverrideConfig {
794            query,
795            values: override_configs_by_id,
796        });
797        Ok(self)
798    }
799
800    pub fn with_redaction_query(
801        mut self,
802        source: &str,
803        language_name: &LanguageName,
804    ) -> Result<Self> {
805        let query = Query::new(&self.ts_language, source)?;
806        let mut redaction_capture_ix = 0;
807        if populate_capture_indices(
808            &query,
809            language_name,
810            "redactions",
811            &[],
812            &mut [Capture::Required("redact", &mut redaction_capture_ix)],
813        ) {
814            self.redactions_config = Some(RedactionConfig {
815                query,
816                redaction_capture_ix,
817            });
818        }
819        Ok(self)
820    }
821}