Skip to content

Commit d7b9372

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

File tree

5 files changed

+242
-16
lines changed

5 files changed

+242
-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: 133 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ pub struct InjectionsQuery {
8989
injection_language_capture: Option<Capture>,
9090
injection_filename_capture: Option<Capture>,
9191
injection_shebang_capture: Option<Capture>,
92+
injection_parent_layer_langs_predicate: Option<Vec<String>>,
93+
injection_parent_layer_langs_predicate_negated: bool,
9294
// Note that the injections query is concatenated with the locals query.
9395
pub(crate) local_query: Query,
9496
// TODO: Use a Vec<bool> instead?
@@ -108,6 +110,9 @@ impl InjectionsQuery {
108110
query_source.push_str(injection_query_text);
109111
query_source.push_str(local_query_text);
110112

113+
let mut injection_parent_layer_langs_predicate = None;
114+
let mut injection_parent_layer_langs_predicate_negated = false;
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,17 @@ 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_negated = negated;
138+
injection_parent_layer_langs_predicate =
139+
Some(values.into_iter().map(ToOwned::to_owned).collect());
140+
}
125141
UserPredicate::SetProperty {
126142
key: "injection.include-children",
127143
val: None,
@@ -167,6 +183,8 @@ impl InjectionsQuery {
167183
local_query.disable_capture("local.reference");
168184

169185
Ok(InjectionsQuery {
186+
injection_parent_layer_langs_predicate,
187+
injection_parent_layer_langs_predicate_negated,
170188
injection_properties,
171189
injection_content_capture: injection_query.get_capture("injection.content"),
172190
injection_language_capture: injection_query.get_capture("injection.language"),
@@ -195,6 +213,7 @@ impl InjectionsQuery {
195213

196214
fn process_match<'a, 'tree>(
197215
&self,
216+
injection_parent_language: Language,
198217
query_match: &QueryMatch<'a, 'tree>,
199218
node_idx: MatchedNodeIdx,
200219
source: RopeSlice<'a>,
@@ -242,11 +261,41 @@ impl InjectionsQuery {
242261
last_content_node = i as u32;
243262
}
244263
}
245-
let marker = marker.or(properties
246-
.and_then(|p| p.language.as_deref())
247-
.map(InjectionLanguageMarker::Name))?;
248264

249-
let language = loader.language_for_marker(marker)?;
265+
let language = marker
266+
.and_then(|m| loader.language_for_marker(m))
267+
.or_else(|| {
268+
properties
269+
.and_then(|p| p.language.as_deref())
270+
.and_then(|name| {
271+
let matches_predicate = || {
272+
self.injection_parent_layer_langs_predicate
273+
.as_ref()
274+
.is_none_or(|predicate| {
275+
predicate.iter().any(|capture| {
276+
let Some(marker) = loader.language_for_marker(
277+
InjectionLanguageMarker::Name(capture),
278+
) else {
279+
return false;
280+
};
281+
282+
if self.injection_parent_layer_langs_predicate_negated {
283+
marker != injection_parent_language
284+
} else {
285+
marker == injection_parent_language
286+
}
287+
})
288+
})
289+
};
290+
291+
if name == INJECTION_PARENT_LAYER && matches_predicate() {
292+
Some(injection_parent_language)
293+
} else {
294+
loader.language_for_marker(InjectionLanguageMarker::Name(name))
295+
}
296+
})
297+
})?;
298+
250299
let scope = if properties.is_some_and(|p| p.combined) {
251300
Some(InjectionScope::Pattern {
252301
pattern: query_match.pattern(),
@@ -286,6 +335,7 @@ impl InjectionsQuery {
286335
/// This case should be handled by the calling function
287336
fn execute<'a>(
288337
&'a self,
338+
injection_parent_language: Language,
289339
node: &Node<'a>,
290340
source: RopeSlice<'a>,
291341
loader: &'a impl LanguageLoader,
@@ -298,7 +348,14 @@ impl InjectionsQuery {
298348
if query_match.matched_node(node_idx).capture != injection_content_capture {
299349
continue;
300350
}
301-
let Some(mat) = self.process_match(&query_match, node_idx, source, loader) else {
351+
352+
let Some(mat) = self.process_match(
353+
injection_parent_language,
354+
&query_match,
355+
node_idx,
356+
source,
357+
loader,
358+
) else {
302359
query_match.remove();
303360
continue;
304361
};
@@ -384,7 +441,18 @@ impl Syntax {
384441
let mut injections: Vec<Injection> = Vec::with_capacity(layer_data.injections.len());
385442
let mut old_injections = take(&mut layer_data.injections).into_iter().peekable();
386443

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

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