Allow file paths ending in a language-specific-extension to be used as the language name for injections (#12368)

Nathan Sobo created

This allows us to detect the language from the extension if we use paths
in fenced code blocks.

Release Notes:

- You can now use file paths ending in a language-specific file
extension at the start of markdown code blocks.

Change summary

crates/language/src/syntax_map.rs                  | 34 +++++++++------
crates/language/src/syntax_map/syntax_map_tests.rs |  6 +-
2 files changed, 24 insertions(+), 16 deletions(-)

Detailed changes

crates/language/src/syntax_map.rs 🔗

@@ -1252,20 +1252,28 @@ fn get_injections(
             prev_match = Some((mat.pattern_index, content_range.clone()));
             let combined = config.patterns[mat.pattern_index].combined;
 
-            let mut language_name = None;
             let mut step_range = content_range.clone();
-            if let Some(name) = config.patterns[mat.pattern_index].language.as_ref() {
-                language_name = Some(Cow::Borrowed(name.as_ref()))
-            } else if let Some(language_node) = config
-                .language_capture_ix
-                .and_then(|ix| mat.nodes_for_capture_index(ix).next())
-            {
-                step_range.start = cmp::min(content_range.start, language_node.start_byte());
-                step_range.end = cmp::max(content_range.end, language_node.end_byte());
-                language_name = Some(Cow::Owned(
-                    text.text_for_range(language_node.byte_range()).collect(),
-                ))
-            };
+            let language_name =
+                if let Some(name) = config.patterns[mat.pattern_index].language.as_ref() {
+                    Some(Cow::Borrowed(name.as_ref()))
+                } else if let Some(language_node) = config
+                    .language_capture_ix
+                    .and_then(|ix| mat.nodes_for_capture_index(ix).next())
+                {
+                    step_range.start = cmp::min(content_range.start, language_node.start_byte());
+                    step_range.end = cmp::max(content_range.end, language_node.end_byte());
+                    let language_name: String =
+                        text.text_for_range(language_node.byte_range()).collect();
+
+                    // Enable paths ending in a language extension to represent a language name: e.g. "foo/bar/baz.rs"
+                    if let Some(last_dot_pos) = language_name.rfind('.') {
+                        Some(Cow::Owned(language_name[last_dot_pos + 1..].to_string()))
+                    } else {
+                        Some(Cow::Owned(language_name))
+                    }
+                } else {
+                    None
+                };
 
             if let Some(language_name) = language_name {
                 let language = language_registry

crates/language/src/syntax_map/syntax_map_tests.rs 🔗

@@ -214,9 +214,9 @@ fn test_dynamic_language_injection(cx: &mut AppContext) {
             ],
         );
 
-    // Replace Rust with Ruby in code block.
+    // Replace `rs` with a path to ending in `.rb` in code block.
     let macro_name_range = range_for_text(&buffer, "rs");
-    buffer.edit([(macro_name_range, "ruby")]);
+    buffer.edit([(macro_name_range, "foo/bar/baz.rb")]);
     syntax_map.interpolate(&buffer);
     syntax_map.reparse(markdown.clone(), &buffer);
     syntax_map.reparse(markdown_inline.clone(), &buffer);
@@ -232,7 +232,7 @@ fn test_dynamic_language_injection(cx: &mut AppContext) {
         );
 
     // Replace Ruby with a language that hasn't been loaded yet.
-    let macro_name_range = range_for_text(&buffer, "ruby");
+    let macro_name_range = range_for_text(&buffer, "foo/bar/baz.rb");
     buffer.edit([(macro_name_range, "html")]);
     syntax_map.interpolate(&buffer);
     syntax_map.reparse(markdown.clone(), &buffer);