Skip to content

Commit e87c19a

Browse files
committed
feat: Add injection.parent-layer property
1 parent b282375 commit e87c19a

File tree

5 files changed

+240
-16
lines changed

5 files changed

+240
-16
lines changed

bindings/src/query.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,30 @@ pub enum UserPredicate<'a> {
1717
key: &'a str,
1818
val: Option<&'a str>,
1919
},
20+
/// A custom `#any-of? <value> [...<values>]` predicate where
21+
/// `<value>` is any string and `[...<values>]` is a list of values for
22+
/// which the predicate succeeds if `<value>` is in the list.
23+
///
24+
/// # Example
25+
///
26+
/// Field values in the following example:
27+
/// - `negated`: `false`
28+
/// - `value`: `"injection.parent-layer"`
29+
/// - `values`: `["gleam", "zig"]`
30+
///
31+
/// ```scheme
32+
/// (#any-of? injection.parent-layer "gleam" "zig")
33+
/// ```
34+
IsAnyOf {
35+
/// - If `false`, will be `any-of?`. Will match *if* `values` includes `value`
36+
/// - If `true`, will be `not-any-of?`. Will match *unless* `values` includes `value`
37+
negated: bool,
38+
/// What we are trying to find. E.g. in `#any-of? hello-world` this will be
39+
/// `"hello-world"`. We will try to find this value in `values`
40+
value: &'a str,
41+
/// List of valid (or invalid, if `negated`) values for `value`
42+
values: Vec<&'a str>,
43+
},
2044
SetProperty {
2145
key: &'a str,
2246
val: Option<&'a str>,
@@ -27,6 +51,26 @@ pub enum UserPredicate<'a> {
2751
impl Display for UserPredicate<'_> {
2852
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2953
match *self {
54+
UserPredicate::IsAnyOf {
55+
negated,
56+
value,
57+
ref values,
58+
} => {
59+
let values_len = values.len();
60+
write!(
61+
f,
62+
"(#{not}any-of? {value} {values})",
63+
not = if negated { "not-" } else { "" },
64+
values = values
65+
.iter()
66+
.enumerate()
67+
.fold(String::new(), |s, (i, value)| {
68+
let comma = if i + 1 == values_len { "" } else { ", " };
69+
70+
format!("{s}\"{value}\"{comma}")
71+
}),
72+
)
73+
}
3074
UserPredicate::IsPropertySet { negate, key, val } => {
3175
let predicate = if negate { "is-not?" } else { "is?" };
3276
let spacer = if val.is_some() { " " } else { "" };

bindings/src/query/predicate.rs

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -217,17 +217,39 @@ impl Query {
217217

218218
"any-of?" | "not-any-of?" => {
219219
predicate.check_min_arg_count(1)?;
220-
let capture = predicate.capture_arg(0)?;
221220
let negated = predicate.name() == "not-any-of?";
222-
let values: Result<_, InvalidPredicateError> = (1..predicate.num_args())
223-
.map(|i| predicate.query_str_arg(i))
224-
.collect();
225-
self.text_predicates.push(TextPredicate {
226-
capture,
227-
kind: TextPredicateKind::AnyString(values?),
228-
negated,
229-
match_all: false,
230-
});
221+
let args = 1..predicate.num_args();
222+
223+
match predicate.capture_arg(0) {
224+
Ok(capture) => {
225+
let args = args.map(|i| predicate.query_str_arg(i));
226+
let values: Result<_, InvalidPredicateError> = args.collect();
227+
228+
self.text_predicates.push(TextPredicate {
229+
capture,
230+
kind: TextPredicateKind::AnyString(values?),
231+
negated,
232+
match_all: false,
233+
});
234+
}
235+
Err(missing_capture_err) => {
236+
let Ok(value) = predicate.str_arg(0) else {
237+
return Err(missing_capture_err);
238+
};
239+
let values = args
240+
.map(|i| predicate.str_arg(i))
241+
.collect::<Result<Vec<_>, _>>()?;
242+
243+
custom_predicate(
244+
pattern,
245+
UserPredicate::IsAnyOf {
246+
negated,
247+
value,
248+
values,
249+
},
250+
)?
251+
}
252+
}
231253
}
232254

233255
// is and is-not are better handled as custom predicates since interpreting is context dependent
@@ -369,6 +391,7 @@ impl InvalidPredicateError {
369391
UserPredicate::SetProperty { key, .. } => Self::UnknownProperty {
370392
property: key.into(),
371393
},
394+
UserPredicate::IsAnyOf { value, .. } => Self::UnknownPredicate { name: value.into() },
372395
UserPredicate::Other(predicate) => Self::UnknownPredicate {
373396
name: predicate.name().into(),
374397
},

fixtures/highlighter/rust_doc_comment.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,29 @@
55
// │ ││╰─ comment markup.bold punctuation.bracket
66
// │ │╰─ comment
77
// │ ╰─ comment comment
8+
// ╰─ comment
9+
///
10+
// ┡┛╿╰─ comment
11+
// │ ╰─ comment comment
12+
// ╰─ comment
13+
/// ```
14+
// ┡┛╿┡━━┛╰─ comment markup.raw.block
15+
// │ │╰─ comment markup.raw.block punctuation.bracket
16+
// │ ╰─ comment comment
17+
// ╰─ comment
18+
/// fn foo()
19+
// ┡┛╿╿┡┛╿┡━┛┡┛╰─ comment markup.raw.block
20+
// │ │││ ││ ╰─ comment markup.raw.block punctuation.bracket
21+
// │ │││ │╰─ comment markup.raw.block function
22+
// │ │││ ╰─ comment markup.raw.block
23+
// │ ││╰─ comment markup.raw.block keyword.function
24+
// │ │╰─ comment markup.raw.block
25+
// │ ╰─ comment comment
26+
// ╰─ comment
27+
/// ```
28+
// ┡┛╿┡━━┛╰─ comment markup.raw.block
29+
// │ │╰─ comment markup.raw.block punctuation.bracket
30+
// │ ╰─ comment comment
831
// ╰─ comment
932
/// **foo
1033
// ┡┛╿╿┡┛┗━┹─ comment markup.bold

highlighter/src/injections_query.rs

Lines changed: 131 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ pub struct InjectionsQuery {
8989
injection_language_capture: Option<Capture>,
9090
injection_filename_capture: Option<Capture>,
9191
injection_shebang_capture: Option<Capture>,
92+
/// 1. The list of matches to compare the parent layer's language
93+
/// 1. Whether it is negated: `#any-of` or `#not-any-of?`
94+
injection_parent_layer_langs_predicate: Option<(Vec<String>, bool)>,
9295
// Note that the injections query is concatenated with the locals query.
9396
pub(crate) local_query: Query,
9497
// TODO: Use a Vec<bool> instead?
@@ -108,6 +111,8 @@ impl InjectionsQuery {
108111
query_source.push_str(injection_query_text);
109112
query_source.push_str(local_query_text);
110113

114+
let mut injection_parent_layer_langs_predicate = None;
115+
111116
let mut injection_properties: HashMap<Pattern, InjectionProperties> = HashMap::new();
112117
let mut not_scope_inherits = HashSet::new();
113118
let injection_query = Query::new(grammar, injection_query_text, |pattern, predicate| {
@@ -122,6 +127,16 @@ impl InjectionsQuery {
122127
.or_default()
123128
.include_children = IncludedChildren::Unnamed
124129
}
130+
// Allow filtering for specific languages in
131+
// `#set! injection.languae injection.parent-layer`
132+
UserPredicate::IsAnyOf {
133+
negated,
134+
value: INJECTION_PARENT_LAYER,
135+
values,
136+
} => {
137+
injection_parent_layer_langs_predicate =
138+
Some((values.into_iter().map(ToOwned::to_owned).collect(), negated));
139+
}
125140
UserPredicate::SetProperty {
126141
key: "injection.include-children",
127142
val: None,
@@ -167,6 +182,7 @@ impl InjectionsQuery {
167182
local_query.disable_capture("local.reference");
168183

169184
Ok(InjectionsQuery {
185+
injection_parent_layer_langs_predicate,
170186
injection_properties,
171187
injection_content_capture: injection_query.get_capture("injection.content"),
172188
injection_language_capture: injection_query.get_capture("injection.language"),
@@ -195,6 +211,7 @@ impl InjectionsQuery {
195211

196212
fn process_match<'a, 'tree>(
197213
&self,
214+
injection_parent_language: Language,
198215
query_match: &QueryMatch<'a, 'tree>,
199216
node_idx: MatchedNodeIdx,
200217
source: RopeSlice<'a>,
@@ -242,11 +259,41 @@ impl InjectionsQuery {
242259
last_content_node = i as u32;
243260
}
244261
}
245-
let marker = marker.or(properties
246-
.and_then(|p| p.language.as_deref())
247-
.map(InjectionLanguageMarker::Name))?;
248262

249-
let language = loader.language_for_marker(marker)?;
263+
let language = marker
264+
.and_then(|m| loader.language_for_marker(m))
265+
.or_else(|| {
266+
properties
267+
.and_then(|p| p.language.as_deref())
268+
.and_then(|name| {
269+
let matches_predicate = || {
270+
self.injection_parent_layer_langs_predicate
271+
.as_ref()
272+
.is_none_or(|(predicate, is_negated)| {
273+
predicate.iter().any(|capture| {
274+
let Some(marker) = loader.language_for_marker(
275+
InjectionLanguageMarker::Name(capture),
276+
) else {
277+
return false;
278+
};
279+
280+
if *is_negated {
281+
marker != injection_parent_language
282+
} else {
283+
marker == injection_parent_language
284+
}
285+
})
286+
})
287+
};
288+
289+
if name == INJECTION_PARENT_LAYER && matches_predicate() {
290+
Some(injection_parent_language)
291+
} else {
292+
loader.language_for_marker(InjectionLanguageMarker::Name(name))
293+
}
294+
})
295+
})?;
296+
250297
let scope = if properties.is_some_and(|p| p.combined) {
251298
Some(InjectionScope::Pattern {
252299
pattern: query_match.pattern(),
@@ -286,6 +333,7 @@ impl InjectionsQuery {
286333
/// This case should be handled by the calling function
287334
fn execute<'a>(
288335
&'a self,
336+
injection_parent_language: Language,
289337
node: &Node<'a>,
290338
source: RopeSlice<'a>,
291339
loader: &'a impl LanguageLoader,
@@ -298,7 +346,14 @@ impl InjectionsQuery {
298346
if query_match.matched_node(node_idx).capture != injection_content_capture {
299347
continue;
300348
}
301-
let Some(mat) = self.process_match(&query_match, node_idx, source, loader) else {
349+
350+
let Some(mat) = self.process_match(
351+
injection_parent_language,
352+
&query_match,
353+
node_idx,
354+
source,
355+
loader,
356+
) else {
302357
query_match.remove();
303358
continue;
304359
};
@@ -384,7 +439,18 @@ impl Syntax {
384439
let mut injections: Vec<Injection> = Vec::with_capacity(layer_data.injections.len());
385440
let mut old_injections = take(&mut layer_data.injections).into_iter().peekable();
386441

387-
let injection_query = injections_query.execute(&parse_tree.root_node(), source, loader);
442+
// The language to inject if `(#set! injection.language injection.parent-layer)` is set
443+
let injection_parent_language = layer_data.parent.map_or_else(
444+
|| self.layer(self.root).language,
445+
|layer| self.layer(layer).language,
446+
);
447+
448+
let injection_query = injections_query.execute(
449+
injection_parent_language,
450+
&parse_tree.root_node(),
451+
source,
452+
loader,
453+
);
388454

389455
let mut combined_injections: HashMap<InjectionScope, Layer> = HashMap::with_capacity(32);
390456
for mat in injection_query {
@@ -713,3 +779,62 @@ fn ranges_intersect(a: &Range, b: &Range) -> bool {
713779
// Adapted from <https://github.com/helix-editor/helix/blob/8df58b2e1779dcf0046fb51ae1893c1eebf01e7c/helix-core/src/selection.rs#L156-L163>
714780
a.start == b.start || (a.end > b.start && b.end > a.start)
715781
}
782+
783+
/// When the language is injected, this value will be set to the
784+
/// language of the parent layer.
785+
///
786+
/// This is useful e.g. when injecting markdown into documentation
787+
/// comments for a language such as Rust, and we want the default
788+
/// code block without any info string to be the same as the parent layer.
789+
///
790+
/// In the next two examples, the language injected into the inner
791+
/// code block in the documentation comments will be the same as the parent
792+
/// layer
793+
///
794+
/// ````gleam
795+
/// /// This code block will have the "gleam" language when
796+
/// /// no info string is supplied:
797+
/// ///
798+
/// /// ```
799+
/// /// let foo: Int = example()
800+
/// /// ```
801+
/// fn example() -> Int { todo }
802+
/// ````
803+
///
804+
/// ````rust
805+
/// /// This code block will have the "rust" language when
806+
/// /// no info string is supplied:
807+
/// ///
808+
/// /// ```
809+
/// /// let foo: i32 = example();
810+
/// /// ```
811+
/// fn example() -> i32 { todo!() }
812+
/// ````
813+
///
814+
/// In the above example, we have two layers:
815+
///
816+
/// ```text
817+
/// <-- rust -->
818+
/// <-- markdown -->
819+
/// ```
820+
///
821+
/// In the `markdown` layer, by default there will be no injection for a
822+
/// code block with no `(info_string)` node.
823+
///
824+
/// By using `injection.parent-layer`, when markdown is injected into a
825+
/// language the code block's default value will be the parent layer.
826+
///
827+
/// # Example
828+
///
829+
/// The following injection will have the effect described above for the
830+
/// specified languages `gleam` and `rust`. All other languages are treated
831+
/// normally.
832+
///
833+
/// ```scheme
834+
/// (fenced_code_block
835+
/// (code_fence_content) @injection.content
836+
/// (#set! injection.include-unnamed-children)
837+
/// (#set! injection.language injection.parent-layer)
838+
/// (#any-of? injection.parent-layer "gleam" "rust"))
839+
/// ```
840+
const INJECTION_PARENT_LAYER: &str = "injection.parent-layer";

test-grammars/markdown/injections.scm

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@
44
(code_fence_content) @injection.shebang @injection.content
55
(#set! injection.include-unnamed-children))
66

7+
(fenced_code_block
8+
(fenced_code_block_delimiter)
9+
(block_continuation)
10+
(code_fence_content) @injection.content
11+
(fenced_code_block_delimiter)
12+
(#set! injection.language injection.parent-layer)
13+
(#set! injection.include-unnamed-children)
14+
(#any-of? injection.parent-layer "rust"))
15+
716
(fenced_code_block
817
(info_string
918
(language) @injection.language)

0 commit comments

Comments
 (0)