Add separate JSONC language (#12655)

Tim Havlicek created

Resolves https://github.com/zed-industries/extensions/issues/860 and
https://github.com/zed-industries/zed/issues/10921, also
https://github.com/biomejs/biome-zed/issues/11.

### Problem:
When opening .json files, zed allows comments by default in the JSON
language, which can cause some problems.
For example, language-servers also get "json" as the language, which may
show errors for those comments.

<img width="935" alt="image"
src="https://github.com/zed-industries/zed/assets/10381895/fed3d83d-abc0-44b5-9982-eb249bb04c3b">

### Solution:

This PR adds a JSONC language. 

<img width="816" alt="image"
src="https://github.com/zed-industries/zed/assets/10381895/8b40e671-d4f0-4e8d-80cb-82ee7c0ec490">

This allows for more specific configuration for language servers. 
Also any json file can be set explicitly to be JSONC using the
file_types setting:

```jsonc
{
  "file_types": {
    // set all .json files to be seen as JSONC
    "JSONC": ["*.json"]
  }
}
```


Release Notes:

- N/A

Change summary

assets/settings/default.json              |  4 +++-
crates/language/src/language_registry.rs  |  2 +-
crates/language/src/language_settings.rs  | 20 ++++++++++++++++++++
crates/languages/src/json.rs              |  7 ++++++-
crates/languages/src/jsonc/brackets.scm   |  3 +++
crates/languages/src/jsonc/config.toml    | 12 ++++++++++++
crates/languages/src/jsonc/embedding.scm  | 14 ++++++++++++++
crates/languages/src/jsonc/highlights.scm | 21 +++++++++++++++++++++
crates/languages/src/jsonc/indents.scm    |  2 ++
crates/languages/src/jsonc/outline.scm    |  2 ++
crates/languages/src/jsonc/overrides.scm  |  1 +
crates/languages/src/jsonc/redactions.scm |  4 ++++
crates/languages/src/lib.rs               |  9 +++++++++
13 files changed, 98 insertions(+), 3 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -688,7 +688,9 @@
   //   "TOML": ["Embargo.lock"]
   // }
   //
-  "file_types": {},
+  "file_types": {
+    "JSONC": ["**/.zed/**/*.json", "**/zed/**/*.json"]
+  },
   // The extensions that Zed should automatically install on startup.
   //
   // If you don't want any of these extensions, add this field to your settings

crates/language/src/language_registry.rs 🔗

@@ -511,7 +511,7 @@ impl LanguageRegistry {
     ) -> 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 path_suffixes = [extension, filename, path.to_str()];
         let empty = GlobSet::empty();
 
         let rx = self.get_or_load_language(move |language_name, config| {

crates/language/src/language_settings.rs 🔗

@@ -662,6 +662,17 @@ impl settings::Settings for AllLanguageSettings {
             .ok_or_else(Self::missing_default)?;
 
         let mut file_types: HashMap<Arc<str>, GlobSet> = HashMap::default();
+
+        for (language, suffixes) in &default_value.file_types {
+            let mut builder = GlobSetBuilder::new();
+
+            for suffix in suffixes {
+                builder.add(Glob::new(suffix)?);
+            }
+
+            file_types.insert(language.clone(), builder.build()?);
+        }
+
         for user_settings in sources.customizations() {
             if let Some(copilot) = user_settings.features.as_ref().and_then(|f| f.copilot) {
                 copilot_enabled = Some(copilot);
@@ -701,6 +712,15 @@ impl settings::Settings for AllLanguageSettings {
             for (language, suffixes) in &user_settings.file_types {
                 let mut builder = GlobSetBuilder::new();
 
+                let default_value = default_value.file_types.get(&language.clone());
+
+                // Merge the default value with the user's value.
+                if let Some(suffixes) = default_value {
+                    for suffix in suffixes {
+                        builder.add(Glob::new(suffix)?);
+                    }
+                }
+
                 for suffix in suffixes {
                     builder.add(Glob::new(suffix)?);
                 }

crates/languages/src/json.rs 🔗

@@ -213,7 +213,12 @@ impl LspAdapter for JsonLspAdapter {
     }
 
     fn language_ids(&self) -> HashMap<String, String> {
-        [("JSON".into(), "jsonc".into())].into_iter().collect()
+        [
+            ("JSON".into(), "json".into()),
+            ("JSONC".into(), "jsonc".into()),
+        ]
+        .into_iter()
+        .collect()
     }
 }
 

crates/languages/src/jsonc/config.toml 🔗

@@ -0,0 +1,12 @@
+name = "JSONC"
+grammar = "jsonc"
+path_suffixes = ["jsonc"]
+line_comments = ["// "]
+autoclose_before = ",]}"
+brackets = [
+    { start = "{", end = "}", close = true, newline = true },
+    { start = "[", end = "]", close = true, newline = true },
+    { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] },
+]
+tab_size = 2
+prettier_parser_name = "jsonc"

crates/languages/src/jsonc/embedding.scm 🔗

@@ -0,0 +1,14 @@
+; Only produce one embedding for the entire file.
+(document) @item
+
+; Collapse arrays, except for the first object.
+(array
+  "[" @keep
+  .
+  (object)? @keep
+  "]" @keep) @collapse
+
+; Collapse string values (but not keys).
+(pair value: (string
+  "\"" @keep
+  "\"" @keep) @collapse)

crates/languages/src/jsonc/highlights.scm 🔗

@@ -0,0 +1,21 @@
+(comment) @comment
+
+(string) @string
+
+(pair
+  key: (string) @property.json_key)
+
+(number) @number
+
+[
+  (true)
+  (false)
+  (null)
+] @constant
+
+[
+  "{"
+  "}"
+  "["
+  "]"
+] @punctuation.bracket

crates/languages/src/lib.rs 🔗

@@ -45,6 +45,7 @@ pub fn init(
         ("gowork", tree_sitter_gowork::language()),
         ("jsdoc", tree_sitter_jsdoc::language()),
         ("json", tree_sitter_json::language()),
+        ("jsonc", tree_sitter_json::language()),
         ("markdown", tree_sitter_markdown::language()),
         ("proto", tree_sitter_proto::language()),
         ("python", tree_sitter_python::language()),
@@ -126,6 +127,14 @@ pub fn init(
         ],
         json_task_context()
     );
+    language!(
+        "jsonc",
+        vec![Arc::new(json::JsonLspAdapter::new(
+            node_runtime.clone(),
+            languages.clone(),
+        ))],
+        json_task_context()
+    );
     language!("markdown");
     language!(
         "python",