Autocomplete language names in file_types setting (#58595)

## Context

Closes #58571

When editing `settings.json`, typing a new key under `languages`
triggers language-name autocomplete. The same did not work for
`file_types`, even though both keys map language names to configuration.

The root cause: the JSON schema generator uses `replace_subschema` to
inject installed language names as the allowed properties of a named
type. The `languages` field backed by `LanguageToSettingsMap` (a named
newtype) got this treatment; `file_types` was backed by a raw
`HashMap<Arc<str>, ExtendingVec<String>>` with no named type to target.

The fix introduces a `FileTypeMap` newtype parallel to
`LanguageToSettingsMap` and wires it into the same schema-injection
path.

Video of manual test below :

[Screencast from 2026-06-05
00-04-16.webm](https://github.com/user-attachments/assets/5d8afb60-d566-4c99-ad2a-66ebbb47ed2e)

## How to Review

- `crates/settings_content/src/language.rs` : Adds
`FileTypeMap(HashMap<Arc<str>, ExtendingVec<String>>)` with the full set
of derives (`Debug`, `Clone`, `Default`, `PartialEq`, `Serialize`,
`Deserialize`, `JsonSchema`, `MergeFrom`) and an `IntoIterator for
&FileTypeMap` impl so existing call sites
(`all_languages.file_types.iter().flatten()`) continue to work without
changes. Changes the `file_types` field in `AllLanguageSettingsContent`
from the raw HashMap to `Option<FileTypeMap>`.

- `crates/settings/src/settings_store.rs` : In
`configure_schema_generator`, adds a `replace_subschema::<FileTypeMap>`
call mirroring the existing `LanguageToSettingsMap` one: injects
installed language names as the allowed properties, with
`ExtendingVec<String>` (array of glob patterns) as the value schema.
Adds `test_file_types_schema_generation` to verify the injected
properties.

- `crates/settings/src/vscode_import.rs` : Updates `file_types()` to
return `Option<FileTypeMap>` and wraps the constructed map accordingly.

- `crates/language/src/buffer_tests.rs` : Updates the existing custom
`file_types` test setup to extend the inner map of the new `FileTypeMap`
wrapper explicitly, keeping the language crate test build passing after
the type change.

## Self-Review Checklist

- [x] I've reviewed my own diff for quality, security, and reliability
- [ ] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the UI/UX checklist
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

**Release Notes :**

- Fixed language name autocomplete not working in the `file_types`
setting

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
This commit is contained in:
saberoueslatiandKirill Bulatov authored and GitHub committed 2026-07-21 11:01:05 +00:00
1 parent 23c2eb2d0c
commit 1806aee21f
4 files changed
+68 -7

No files matched your search

+1 -1
View File
@@ -277,7 +277,7 @@ async fn test_first_line_pattern(cx: &mut TestAppContext) {
async fn test_language_for_file_with_custom_file_types(cx: &mut TestAppContext) {
cx.update(|cx| {
init_settings(cx, |settings| {
settings.file_types.get_or_insert_default().extend([
settings.file_types.get_or_insert_default().0.extend([
("TypeScript".into(), vec!["js".into()].into()),
(
"JavaScript".into(),
+50 -3
View File
@@ -32,12 +32,12 @@ use util::{
use crate::editorconfig_store::EditorconfigStore;
use crate::{
ActiveSettingsProfileName, FontFamilyName, IconThemeName, LanguageSettingsContent,
ActiveSettingsProfileName, FileTypeMap, FontFamilyName, IconThemeName, LanguageSettingsContent,
LanguageToSettingsMap, LspSettings, LspSettingsMap, SemanticTokenRules, ThemeName,
UserSettingsContentExt, VsCodeSettings, WorktreeId,
settings_content::{
ExtensionsSettingsContent, ProfileBase, ProjectSettingsContent, RootUserSettings,
SettingsContent, UserSettingsContent, merge_from::MergeFrom,
ExtendingVec, ExtensionsSettingsContent, ProfileBase, ProjectSettingsContent,
RootUserSettings, SettingsContent, UserSettingsContent, merge_from::MergeFrom,
},
};
@@ -1201,6 +1201,17 @@ impl SettingsStore {
"properties": params.language_names.iter().map(|name| (name.clone(), language_settings_content_ref.clone())).collect::<serde_json::Map<_, _>>()
})
});
let file_type_patterns_ref =
generator.subschema_for::<ExtendingVec<String>>().to_value();
replace_subschema::<FileTypeMap>(generator, || {
json_schema!({
"type": "object",
"errorMessage": "No language with this name is installed.",
"properties": params.language_names.iter().map(|name| (name.clone(), file_type_patterns_ref.clone())).collect::<serde_json::Map<_, _>>(),
"additionalProperties": file_type_patterns_ref.clone()
})
});
}
generator.subschema_for::<LspSettings>();
@@ -3166,6 +3177,42 @@ mod tests {
);
}
#[gpui::test]
fn test_file_types_schema_generation(cx: &mut App) {
SettingsStore::test(cx);
let schema = SettingsStore::json_schema(&SettingsJsonSchemaParams {
language_names: &["Rust".to_string(), "TypeScript".to_string()],
font_names: &["Zed Mono".to_string()],
theme_names: &["One Dark".into()],
icon_theme_names: &["Zed Icons".into()],
lsp_adapter_names: &[],
action_names: &[],
action_documentation: &HashMap::default(),
deprecations: &HashMap::default(),
deprecation_messages: &HashMap::default(),
});
let file_type_map = schema
.pointer("/$defs/FileTypeMap")
.expect("schema should have a FileTypeMap definition");
let properties = file_type_map
.pointer("/properties")
.expect("FileTypeMap should have properties")
.as_object()
.expect("FileTypeMap properties should be an object");
let mut language_names = properties.keys().collect::<Vec<_>>();
language_names.sort();
assert_eq!(language_names, ["Rust", "TypeScript"]);
let patterns_schema = file_type_map
.pointer("/additionalProperties")
.expect("FileTypeMap should validate values of unknown language names");
assert_eq!(properties.get("Rust"), Some(patterns_schema));
assert_eq!(properties.get("TypeScript"), Some(patterns_schema));
}
#[gpui::test]
fn test_project_json_schema_differs_from_user_schema(cx: &mut App) {
SettingsStore::test(cx);
+2 -2
View File
@@ -642,7 +642,7 @@ impl VsCodeSettings {
}
}
fn file_types(&self) -> Option<HashMap<Arc<str>, ExtendingVec<String>>> {
fn file_types(&self) -> Option<FileTypeMap> {
// vscodes file association map is inverted from ours, so we flip the mapping before merging
let mut associations: HashMap<Arc<str>, ExtendingVec<String>> = HashMap::default();
let map = self.read_value("files.associations")?.as_object()?;
@@ -650,7 +650,7 @@ impl VsCodeSettings {
let Some(v) = v.as_str() else { continue };
associations.entry(v.into()).or_default().0.push(k.clone());
}
skip_default(associations)
skip_default(FileTypeMap(associations))
}
fn edit_predictions_settings_content(&self) -> Option<EditPredictionSettingsContent> {
+15 -1
View File
@@ -44,7 +44,7 @@ pub struct AllLanguageSettingsContent {
pub languages: LanguageToSettingsMap,
/// Settings for associating file extensions and filenames
/// with languages.
pub file_types: Option<HashMap<Arc<str>, ExtendingVec<String>>>,
pub file_types: Option<FileTypeMap>,
}
impl merge_from::MergeFrom for AllLanguageSettingsContent {
@@ -1144,6 +1144,20 @@ pub struct LanguageTaskSettingsContent {
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
pub struct LanguageToSettingsMap(pub HashMap<String, LanguageSettingsContent>);
/// Map from language name to file patterns.
#[with_fallible_options]
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)]
pub struct FileTypeMap(pub HashMap<Arc<str>, ExtendingVec<String>>);
impl<'a> IntoIterator for &'a FileTypeMap {
type Item = (&'a Arc<str>, &'a ExtendingVec<String>);
type IntoIter = std::collections::hash_map::Iter<'a, Arc<str>, ExtendingVec<String>>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
/// Determines how indent guides are colored.
#[derive(
Default,