Add glob support for custom file type language (#12043)

Joshua Farayola created

Release Notes:

- Added glob support for file_types configuration
([#10765](https://github.com/zed-industries/zed/issues/10765)).

`file_types` can now be written like this:

```json
"file_types": {
  "Dockerfile": [
    "Dockerfile",
    "Dockerfile.*",
  ]
}
```

Change summary

crates/language/src/buffer_tests.rs      | 17 +++++++++++++++++
crates/language/src/language_registry.rs | 13 ++++++++-----
crates/language/src/language_settings.rs | 17 ++++++++++-------
docs/src/configuring-zed.md              |  7 ++++---
4 files changed, 39 insertions(+), 15 deletions(-)

Detailed changes

crates/language/src/buffer_tests.rs 🔗

@@ -184,6 +184,10 @@ async fn test_language_for_file_with_custom_file_types(cx: &mut TestAppContext)
             settings.file_types.extend([
                 ("TypeScript".into(), vec!["js".into()]),
                 ("C++".into(), vec!["c".into()]),
+                (
+                    "Dockerfile".into(),
+                    vec!["Dockerfile".into(), "Dockerfile.*".into()],
+                ),
             ]);
         })
     });
@@ -223,6 +227,14 @@ async fn test_language_for_file_with_custom_file_types(cx: &mut TestAppContext)
             },
             ..Default::default()
         },
+        LanguageConfig {
+            name: "Dockerfile".into(),
+            matcher: LanguageMatcher {
+                path_suffixes: vec!["Dockerfile".to_string()],
+                ..Default::default()
+            },
+            ..Default::default()
+        },
     ] {
         languages.add(Arc::new(Language::new(config, None)));
     }
@@ -237,6 +249,11 @@ async fn test_language_for_file_with_custom_file_types(cx: &mut TestAppContext)
         .await
         .unwrap();
     assert_eq!(language.name().as_ref(), "C++");
+    let language = cx
+        .read(|cx| languages.language_for_file(&file("Dockerfile.dev"), None, cx))
+        .await
+        .unwrap();
+    assert_eq!(language.name().as_ref(), "Dockerfile");
 }
 
 fn file(path: &str) -> Arc<dyn File> {

crates/language/src/language_registry.rs 🔗

@@ -14,6 +14,7 @@ use futures::{
     future::Shared,
     Future, FutureExt as _,
 };
+use globset::GlobSet;
 use gpui::{AppContext, BackgroundExecutor, Task};
 use lsp::LanguageServerId;
 use parking_lot::{Mutex, RwLock};
@@ -506,23 +507,25 @@ impl LanguageRegistry {
         self: &Arc<Self>,
         path: &Path,
         content: Option<&Rope>,
-        user_file_types: Option<&HashMap<Arc<str>, Vec<String>>>,
+        user_file_types: Option<&HashMap<Arc<str>, GlobSet>>,
     ) -> impl Future<Output = Result<Arc<Language>>> {
         let filename = path.file_name().and_then(|name| name.to_str());
         let extension = path.extension_or_hidden_file_name();
         let path_suffixes = [extension, filename];
-        let empty = Vec::new();
+        let empty = GlobSet::empty();
 
         let rx = self.get_or_load_language(move |language_name, config| {
             let path_matches_default_suffix = config
                 .path_suffixes
                 .iter()
                 .any(|suffix| path_suffixes.contains(&Some(suffix.as_str())));
-            let path_matches_custom_suffix = user_file_types
+            let custom_suffixes = user_file_types
                 .and_then(|types| types.get(language_name))
-                .unwrap_or(&empty)
+                .unwrap_or(&empty);
+            let path_matches_custom_suffix = path_suffixes
                 .iter()
-                .any(|suffix| path_suffixes.contains(&Some(suffix.as_str())));
+                .map(|suffix| suffix.unwrap_or(""))
+                .any(|suffix| custom_suffixes.is_match(suffix));
             let content_matches = content.zip(config.first_line_pattern.as_ref()).map_or(
                 false,
                 |(content, pattern)| {

crates/language/src/language_settings.rs 🔗

@@ -3,7 +3,7 @@
 use crate::{File, Language, LanguageServerName};
 use anyhow::Result;
 use collections::{HashMap, HashSet};
-use globset::GlobMatcher;
+use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
 use gpui::AppContext;
 use itertools::{Either, Itertools};
 use schemars::{
@@ -55,7 +55,7 @@ pub struct AllLanguageSettings {
     pub inline_completions: InlineCompletionSettings,
     defaults: LanguageSettings,
     languages: HashMap<Arc<str>, LanguageSettings>,
-    pub(crate) file_types: HashMap<Arc<str>, Vec<String>>,
+    pub(crate) file_types: HashMap<Arc<str>, GlobSet>,
 }
 
 /// The settings for a particular language.
@@ -573,7 +573,7 @@ impl settings::Settings for AllLanguageSettings {
             .and_then(|c| c.disabled_globs.as_ref())
             .ok_or_else(Self::missing_default)?;
 
-        let mut file_types: HashMap<Arc<str>, Vec<String>> = HashMap::default();
+        let mut file_types: HashMap<Arc<str>, GlobSet> = HashMap::default();
         for user_settings in sources.customizations() {
             if let Some(copilot) = user_settings.features.as_ref().and_then(|f| f.copilot) {
                 copilot_enabled = Some(copilot);
@@ -611,10 +611,13 @@ impl settings::Settings for AllLanguageSettings {
             }
 
             for (language, suffixes) in &user_settings.file_types {
-                file_types
-                    .entry(language.clone())
-                    .or_default()
-                    .extend_from_slice(suffixes);
+                let mut builder = GlobSetBuilder::new();
+
+                for suffix in suffixes {
+                    builder.add(Glob::new(suffix)?);
+                }
+
+                file_types.insert(language.clone(), builder.build()?);
             }
         }
 

docs/src/configuring-zed.md 🔗

@@ -651,18 +651,19 @@ The result is still `)))` and not `))))))`, which is what it would be by default
 ## File Types
 
 - Setting: `file_types`
-- Description: Configure how Zed selects a language for a file based on its filename or extension.
+- Description: Configure how Zed selects a language for a file based on its filename or extension. Supports glob entries.
 - Default: `{}`
 
 **Examples**
 
-To interpret all `.c` files as C++, and files called `MyLockFile` as TOML:
+To interpret all `.c` files as C++, files called `MyLockFile` as TOML and files starting with `Dockerfile` as Dockerfile:
 
 ```json
 {
   "file_types": {
     "C++": ["c"],
-    "TOML": ["MyLockFile"]
+    "TOML": ["MyLockFile"],
+    "Dockerfile": ["Dockerfile*"]
   }
 }
 ```