Add a setting for custom associations between languages and files (#9290)

Closes #5178

Release Notes:

- Added a `file_types` setting that can be used to associate languages
with file names and file extensions. For example, to interpret all `.c`
files as C++, and files called `MyLockFile` as TOML, add the following
to `settings.json`:

    ```json
    {
      "file_types": {
        "C++": ["c"],
        "TOML": ["MyLockFile"]
      }
    }
    ```

As with most zed settings, this can be configured on a per-directory
basis by including a local `.zed/settings.json` file in that directory.

---------

Co-authored-by: Marshall <marshall@zed.dev>
This commit is contained in:
Max Brunsfeld 2024-03-13 10:23:30 -07:00 committed by GitHub
parent 77de5689a3
commit 724c19a223
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 640 additions and 415 deletions

View file

@ -69,8 +69,10 @@ fn test_line_endings(cx: &mut gpui::AppContext) {
}
#[gpui::test]
fn test_select_language() {
let registry = Arc::new(LanguageRegistry::test());
fn test_select_language(cx: &mut AppContext) {
init_settings(cx, |_| {});
let registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
registry.add(Arc::new(Language::new(
LanguageConfig {
name: "Rust".into(),
@ -97,14 +99,14 @@ fn test_select_language() {
// matching file extension
assert_eq!(
registry
.language_for_file("zed/lib.rs".as_ref(), None)
.language_for_file(&file("src/lib.rs"), None, cx)
.now_or_never()
.and_then(|l| Some(l.ok()?.name())),
Some("Rust".into())
);
assert_eq!(
registry
.language_for_file("zed/lib.mk".as_ref(), None)
.language_for_file(&file("src/lib.mk"), None, cx)
.now_or_never()
.and_then(|l| Some(l.ok()?.name())),
Some("Make".into())
@ -113,7 +115,7 @@ fn test_select_language() {
// matching filename
assert_eq!(
registry
.language_for_file("zed/Makefile".as_ref(), None)
.language_for_file(&file("src/Makefile"), None, cx)
.now_or_never()
.and_then(|l| Some(l.ok()?.name())),
Some("Make".into())
@ -122,27 +124,132 @@ fn test_select_language() {
// matching suffix that is not the full file extension or filename
assert_eq!(
registry
.language_for_file("zed/cars".as_ref(), None)
.language_for_file(&file("zed/cars"), None, cx)
.now_or_never()
.and_then(|l| Some(l.ok()?.name())),
None
);
assert_eq!(
registry
.language_for_file("zed/a.cars".as_ref(), None)
.language_for_file(&file("zed/a.cars"), None, cx)
.now_or_never()
.and_then(|l| Some(l.ok()?.name())),
None
);
assert_eq!(
registry
.language_for_file("zed/sumk".as_ref(), None)
.language_for_file(&file("zed/sumk"), None, cx)
.now_or_never()
.and_then(|l| Some(l.ok()?.name())),
None
);
}
#[gpui::test(iterations = 10)]
async fn test_first_line_pattern(cx: &mut TestAppContext) {
cx.update(|cx| init_settings(cx, |_| {}));
let languages = LanguageRegistry::test(cx.executor());
let languages = Arc::new(languages);
languages.register_test_language(LanguageConfig {
name: "JavaScript".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["js".into()],
first_line_pattern: Some(Regex::new(r"\bnode\b").unwrap()),
},
..Default::default()
});
cx.read(|cx| languages.language_for_file(&file("the/script"), None, cx))
.await
.unwrap_err();
cx.read(|cx| languages.language_for_file(&file("the/script"), Some(&"nothing".into()), cx))
.await
.unwrap_err();
assert_eq!(
cx.read(|cx| languages.language_for_file(
&file("the/script"),
Some(&"#!/bin/env node".into()),
cx
))
.await
.unwrap()
.name()
.as_ref(),
"JavaScript"
);
}
#[gpui::test]
async fn test_language_for_file_with_custom_file_types(cx: &mut TestAppContext) {
cx.update(|cx| {
init_settings(cx, |settings| {
settings.file_types.extend([
("TypeScript".into(), vec!["js".into()]),
("C++".into(), vec!["c".into()]),
]);
})
});
let languages = Arc::new(LanguageRegistry::test(cx.executor()));
for config in [
LanguageConfig {
name: "JavaScript".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["js".to_string()],
..Default::default()
},
..Default::default()
},
LanguageConfig {
name: "TypeScript".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["js".to_string()],
..Default::default()
},
..Default::default()
},
LanguageConfig {
name: "C++".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["cpp".to_string()],
..Default::default()
},
..Default::default()
},
LanguageConfig {
name: "C".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["c".to_string()],
..Default::default()
},
..Default::default()
},
] {
languages.add(Arc::new(Language::new(config, None)));
}
let language = cx
.read(|cx| languages.language_for_file(&file("foo.js"), None, cx))
.await
.unwrap();
assert_eq!(language.name().as_ref(), "TypeScript");
let language = cx
.read(|cx| languages.language_for_file(&file("foo.c"), None, cx))
.await
.unwrap();
assert_eq!(language.name().as_ref(), "C++");
}
fn file(path: &str) -> Arc<dyn File> {
Arc::new(TestFile {
path: Path::new(path).into(),
root_name: "zed".into(),
})
}
#[gpui::test]
fn test_edit_events(cx: &mut gpui::AppContext) {
let mut now = Instant::now();
@ -1575,7 +1682,7 @@ fn test_autoindent_with_injected_languages(cx: &mut AppContext) {
let javascript_language = Arc::new(javascript_lang());
let language_registry = Arc::new(LanguageRegistry::test());
let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
language_registry.add(html_language.clone());
language_registry.add(javascript_language.clone());
@ -1895,7 +2002,7 @@ fn test_language_scope_at_with_combined_injections(cx: &mut AppContext) {
"#
.unindent();
let language_registry = Arc::new(LanguageRegistry::test());
let language_registry = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
language_registry.add(Arc::new(ruby_lang()));
language_registry.add(Arc::new(html_lang()));
language_registry.add(Arc::new(erb_lang()));