language: Accept multiple values in line_comment language knob. (#6713)

Piotr Osiewicz created

This opens up a possibility of supporting multiple comment continuation
flavours in editor, e.g. doc comments for Rust (which we seize as well
in this commit). Only the first `line_comment` value is used for
Editor::ToggleComments

Fixes: https://github.com/zed-industries/zed/issues/6692

Release Notes:
- Added support for doc-comment continuations in Rust language.

Change summary

crates/editor/src/editor.rs                     | 62 +++++++++++-------
crates/editor/src/editor_tests.rs               |  8 +-
crates/language/src/buffer_tests.rs             | 34 ++++++---
crates/language/src/language.rs                 | 14 ++-
crates/zed/src/languages/bash/config.toml       |  2 
crates/zed/src/languages/c/config.toml          |  2 
crates/zed/src/languages/cpp/config.toml        |  2 
crates/zed/src/languages/elixir/config.toml     |  2 
crates/zed/src/languages/elm/config.toml        |  2 
crates/zed/src/languages/glsl/config.toml       |  2 
crates/zed/src/languages/go/config.toml         |  2 
crates/zed/src/languages/javascript/config.toml |  4 
crates/zed/src/languages/json/config.toml       |  2 
crates/zed/src/languages/lua/config.toml        |  2 
crates/zed/src/languages/nix/config.toml        |  2 
crates/zed/src/languages/nu/config.toml         |  2 
crates/zed/src/languages/php/config.toml        |  2 
crates/zed/src/languages/python/config.toml     |  2 
crates/zed/src/languages/racket/config.toml     |  2 
crates/zed/src/languages/ruby/config.toml       |  2 
crates/zed/src/languages/rust/config.toml       |  2 
crates/zed/src/languages/scheme/config.toml     |  2 
crates/zed/src/languages/toml/config.toml       |  2 
crates/zed/src/languages/tsx/config.toml        |  4 
crates/zed/src/languages/typescript/config.toml |  2 
crates/zed/src/languages/uiua/config.toml       |  2 
crates/zed/src/languages/yaml/config.toml       |  2 
27 files changed, 95 insertions(+), 73 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -2502,34 +2502,43 @@ impl Editor {
                                         )
                                 });
                             // Comment extension on newline is allowed only for cursor selections
-                            let comment_delimiter = language.line_comment_prefix().filter(|_| {
+                            let comment_delimiter = language.line_comment_prefixes().filter(|_| {
                                 let is_comment_extension_enabled =
                                     multi_buffer.settings_at(0, cx).extend_comment_on_newline;
                                 is_cursor && is_comment_extension_enabled
                             });
-                            let comment_delimiter = if let Some(delimiter) = comment_delimiter {
-                                buffer
-                                    .buffer_line_for_row(start_point.row)
-                                    .is_some_and(|(snapshot, range)| {
-                                        let mut index_of_first_non_whitespace = 0;
-                                        let line_starts_with_comment = snapshot
-                                            .chars_for_range(range)
-                                            .skip_while(|c| {
-                                                let should_skip = c.is_whitespace();
-                                                if should_skip {
-                                                    index_of_first_non_whitespace += 1;
-                                                }
-                                                should_skip
-                                            })
-                                            .take(delimiter.len())
-                                            .eq(delimiter.chars());
-                                        let cursor_is_placed_after_comment_marker =
-                                            index_of_first_non_whitespace + delimiter.len()
-                                                <= start_point.column as usize;
-                                        line_starts_with_comment
-                                            && cursor_is_placed_after_comment_marker
+                            let get_comment_delimiter = |delimiters: &[Arc<str>]| {
+                                let max_len_of_delimiter =
+                                    delimiters.iter().map(|delimiter| delimiter.len()).max()?;
+                                let (snapshot, range) =
+                                    buffer.buffer_line_for_row(start_point.row)?;
+
+                                let mut index_of_first_non_whitespace = 0;
+                                let comment_candidate = snapshot
+                                    .chars_for_range(range)
+                                    .skip_while(|c| {
+                                        let should_skip = c.is_whitespace();
+                                        if should_skip {
+                                            index_of_first_non_whitespace += 1;
+                                        }
+                                        should_skip
                                     })
-                                    .then(|| delimiter.clone())
+                                    .take(max_len_of_delimiter)
+                                    .collect::<String>();
+                                let comment_prefix = delimiters.iter().find(|comment_prefix| {
+                                    comment_candidate.starts_with(comment_prefix.as_ref())
+                                })?;
+                                let cursor_is_placed_after_comment_marker =
+                                    index_of_first_non_whitespace + comment_prefix.len()
+                                        <= start_point.column as usize;
+                                if cursor_is_placed_after_comment_marker {
+                                    Some(comment_prefix.clone())
+                                } else {
+                                    None
+                                }
+                            };
+                            let comment_delimiter = if let Some(delimiters) = comment_delimiter {
+                                get_comment_delimiter(delimiters)
                             } else {
                                 None
                             };
@@ -6561,7 +6570,10 @@ impl Editor {
                 }
 
                 // If the language has line comments, toggle those.
-                if let Some(full_comment_prefix) = language.line_comment_prefix() {
+                if let Some(full_comment_prefix) = language
+                    .line_comment_prefixes()
+                    .and_then(|prefixes| prefixes.first())
+                {
                     // Split the comment prefix's trailing whitespace into a separate string,
                     // as that portion won't be used for detecting if a line is a comment.
                     let comment_prefix = full_comment_prefix.trim_end_matches(' ');
@@ -6569,7 +6581,7 @@ impl Editor {
                     let mut all_selection_lines_are_comments = true;
 
                     for row in start_row..=end_row {
-                        if snapshot.is_line_blank(row) && start_row < end_row {
+                        if start_row < end_row && snapshot.is_line_blank(row) {
                             continue;
                         }
 

crates/editor/src/editor_tests.rs 🔗

@@ -1942,7 +1942,7 @@ async fn test_newline_comments(cx: &mut gpui::TestAppContext) {
 
     let language = Arc::new(Language::new(
         LanguageConfig {
-            line_comment: Some("//".into()),
+            line_comments: vec!["//".into()],
             ..LanguageConfig::default()
         },
         None,
@@ -5736,7 +5736,7 @@ async fn test_toggle_comment(cx: &mut gpui::TestAppContext) {
     let mut cx = EditorTestContext::new(cx).await;
     let language = Arc::new(Language::new(
         LanguageConfig {
-            line_comment: Some("// ".into()),
+            line_comments: vec!["// ".into()],
             ..Default::default()
         },
         Some(tree_sitter_rust::language()),
@@ -5838,7 +5838,7 @@ async fn test_advance_downward_on_toggle_comment(cx: &mut gpui::TestAppContext)
 
     let language = Arc::new(Language::new(
         LanguageConfig {
-            line_comment: Some("// ".into()),
+            line_comments: vec!["// ".into()],
             ..Default::default()
         },
         Some(tree_sitter_rust::language()),
@@ -5993,7 +5993,7 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) {
     let javascript_language = Arc::new(Language::new(
         LanguageConfig {
             name: "JavaScript".into(),
-            line_comment: Some("// ".into()),
+            line_comments: vec!["// ".into()],
             ..Default::default()
         },
         Some(tree_sitter_typescript::language_tsx()),

crates/language/src/buffer_tests.rs 🔗

@@ -1657,7 +1657,7 @@ fn test_language_scope_at_with_javascript(cx: &mut AppContext) {
         let language = Language::new(
             LanguageConfig {
                 name: "JavaScript".into(),
-                line_comment: Some("// ".into()),
+                line_comments: vec!["// ".into()],
                 brackets: BracketPairConfig {
                     pairs: vec![
                         BracketPair {
@@ -1681,7 +1681,7 @@ fn test_language_scope_at_with_javascript(cx: &mut AppContext) {
                 overrides: [(
                     "element".into(),
                     LanguageConfigOverride {
-                        line_comment: Override::Remove { remove: true },
+                        line_comments: Override::Remove { remove: true },
                         block_comment: Override::Set(("{/*".into(), "*/}".into())),
                         ..Default::default()
                     },
@@ -1718,7 +1718,7 @@ fn test_language_scope_at_with_javascript(cx: &mut AppContext) {
         let snapshot = buffer.snapshot();
 
         let config = snapshot.language_scope_at(0).unwrap();
-        assert_eq!(config.line_comment_prefix().unwrap().as_ref(), "// ");
+        assert_eq!(config.line_comment_prefixes().unwrap(), &[Arc::from("// ")]);
         // Both bracket pairs are enabled
         assert_eq!(
             config.brackets().map(|e| e.1).collect::<Vec<_>>(),
@@ -1728,7 +1728,10 @@ fn test_language_scope_at_with_javascript(cx: &mut AppContext) {
         let string_config = snapshot
             .language_scope_at(text.find("b\"").unwrap())
             .unwrap();
-        assert_eq!(string_config.line_comment_prefix().unwrap().as_ref(), "// ");
+        assert_eq!(
+            string_config.line_comment_prefixes().unwrap(),
+            &[Arc::from("// ")]
+        );
         // Second bracket pair is disabled
         assert_eq!(
             string_config.brackets().map(|e| e.1).collect::<Vec<_>>(),
@@ -1739,7 +1742,7 @@ fn test_language_scope_at_with_javascript(cx: &mut AppContext) {
         let element_config = snapshot
             .language_scope_at(text.find("<F>").unwrap())
             .unwrap();
-        assert_eq!(element_config.line_comment_prefix(), None);
+        assert_eq!(element_config.line_comment_prefixes(), None);
         assert_eq!(
             element_config.block_comment_delimiters(),
             Some((&"{/*".into(), &"*/}".into()))
@@ -1753,7 +1756,10 @@ fn test_language_scope_at_with_javascript(cx: &mut AppContext) {
         let tag_config = snapshot
             .language_scope_at(text.find(" d=").unwrap() + 1)
             .unwrap();
-        assert_eq!(tag_config.line_comment_prefix().unwrap().as_ref(), "// ");
+        assert_eq!(
+            tag_config.line_comment_prefixes().unwrap(),
+            &[Arc::from("// ")]
+        );
         assert_eq!(
             tag_config.brackets().map(|e| e.1).collect::<Vec<_>>(),
             &[true, true]
@@ -1765,10 +1771,9 @@ fn test_language_scope_at_with_javascript(cx: &mut AppContext) {
             .unwrap();
         assert_eq!(
             expression_in_element_config
-                .line_comment_prefix()
-                .unwrap()
-                .as_ref(),
-            "// "
+                .line_comment_prefixes()
+                .unwrap(),
+            &[Arc::from("// ")]
         );
         assert_eq!(
             expression_in_element_config
@@ -1884,14 +1889,17 @@ fn test_language_scope_at_with_combined_injections(cx: &mut AppContext) {
 
         let snapshot = buffer.snapshot();
         let html_config = snapshot.language_scope_at(Point::new(2, 4)).unwrap();
-        assert_eq!(html_config.line_comment_prefix(), None);
+        assert_eq!(html_config.line_comment_prefixes(), Some(&vec![]));
         assert_eq!(
             html_config.block_comment_delimiters(),
             Some((&"<!--".into(), &"-->".into()))
         );
 
         let ruby_config = snapshot.language_scope_at(Point::new(3, 12)).unwrap();
-        assert_eq!(ruby_config.line_comment_prefix().unwrap().as_ref(), "# ");
+        assert_eq!(
+            ruby_config.line_comment_prefixes().unwrap(),
+            &[Arc::from("# ")]
+        );
         assert_eq!(ruby_config.block_comment_delimiters(), None);
 
         buffer
@@ -2293,7 +2301,7 @@ fn ruby_lang() -> Language {
         LanguageConfig {
             name: "Ruby".into(),
             path_suffixes: vec!["rb".to_string()],
-            line_comment: Some("# ".into()),
+            line_comments: vec!["# ".into()],
             ..Default::default()
         },
         Some(tree_sitter_ruby::language()),

crates/language/src/language.rs 🔗

@@ -416,8 +416,10 @@ pub struct LanguageConfig {
     #[serde(default)]
     pub collapsed_placeholder: String,
     /// A line comment string that is inserted in e.g. `toggle comments` action.
+    /// A language can have multiple flavours of line comments. All of the provided line comments are
+    /// used for comment continuations on the next line, but only the first one is used for Editor::ToggleComments.
     #[serde(default)]
-    pub line_comment: Option<Arc<str>>,
+    pub line_comments: Vec<Arc<str>>,
     /// Starting and closing characters of a block comment.
     #[serde(default)]
     pub block_comment: Option<(Arc<str>, Arc<str>)>,
@@ -460,7 +462,7 @@ pub struct LanguageScope {
 #[derive(Clone, Deserialize, Default, Debug)]
 pub struct LanguageConfigOverride {
     #[serde(default)]
-    pub line_comment: Override<Arc<str>>,
+    pub line_comments: Override<Vec<Arc<str>>>,
     #[serde(default)]
     pub block_comment: Override<(Arc<str>, Arc<str>)>,
     #[serde(skip_deserializing)]
@@ -506,7 +508,7 @@ impl Default for LanguageConfig {
             increase_indent_pattern: Default::default(),
             decrease_indent_pattern: Default::default(),
             autoclose_before: Default::default(),
-            line_comment: Default::default(),
+            line_comments: Default::default(),
             block_comment: Default::default(),
             scope_opt_in_language_servers: Default::default(),
             overrides: Default::default(),
@@ -1710,10 +1712,10 @@ impl LanguageScope {
 
     /// Returns line prefix that is inserted in e.g. line continuations or
     /// in `toggle comments` action.
-    pub fn line_comment_prefix(&self) -> Option<&Arc<str>> {
+    pub fn line_comment_prefixes(&self) -> Option<&Vec<Arc<str>>> {
         Override::as_option(
-            self.config_override().map(|o| &o.line_comment),
-            self.language.config.line_comment.as_ref(),
+            self.config_override().map(|o| &o.line_comments),
+            Some(&self.language.config.line_comments),
         )
     }
 

crates/zed/src/languages/bash/config.toml 🔗

@@ -1,6 +1,6 @@
 name = "Shell Script"
 path_suffixes = ["sh", "bash", "bashrc", "bash_profile", "bash_aliases", "bash_logout", "profile", "zsh", "zshrc", "zshenv", "zsh_profile", "zsh_aliases", "zsh_histfile", "zlogin", "zprofile"]
-line_comment = "# "
+line_comments = ["# "]
 first_line_pattern = "^#!.*\\b(?:ba|z)?sh\\b"
 brackets = [
     { start = "[", end = "]", close = true, newline = false },

crates/zed/src/languages/c/config.toml 🔗

@@ -1,6 +1,6 @@
 name = "C"
 path_suffixes = ["c"]
-line_comment = "// "
+line_comments = ["// "]
 autoclose_before = ";:.,=}])>"
 brackets = [
     { start = "{", end = "}", close = true, newline = true },

crates/zed/src/languages/cpp/config.toml 🔗

@@ -1,6 +1,6 @@
 name = "C++"
 path_suffixes = ["cc", "cpp", "h", "hpp", "cxx", "hxx", "inl"]
-line_comment = "// "
+line_comments = ["// "]
 autoclose_before = ";:.,=}])>"
 brackets = [
     { start = "{", end = "}", close = true, newline = true },

crates/zed/src/languages/elixir/config.toml 🔗

@@ -1,6 +1,6 @@
 name = "Elixir"
 path_suffixes = ["ex", "exs"]
-line_comment = "# "
+line_comments = ["# "]
 autoclose_before = ";:.,=}])>"
 brackets = [
     { start = "{", end = "}", close = true, newline = true },

crates/zed/src/languages/elm/config.toml 🔗

@@ -1,6 +1,6 @@
 name = "Elm"
 path_suffixes = ["elm"]
-line_comment = "-- "
+line_comments = ["-- "]
 block_comment = ["{- ", " -}"]
 brackets = [
     { start = "{", end = "}", close = true, newline = true },

crates/zed/src/languages/glsl/config.toml 🔗

@@ -1,6 +1,6 @@
 name = "GLSL"
 path_suffixes = ["vert", "frag", "tesc", "tese", "geom", "comp"]
-line_comment = "// "
+line_comments = ["// "]
 block_comment = ["/* ", " */"]
 brackets = [
     { start = "{", end = "}", close = true, newline = true },

crates/zed/src/languages/go/config.toml 🔗

@@ -1,6 +1,6 @@
 name = "Go"
 path_suffixes = ["go"]
-line_comment = "// "
+line_comments = ["// "]
 autoclose_before = ";:.,=}])>"
 brackets = [
     { start = "{", end = "}", close = true, newline = true },

crates/zed/src/languages/javascript/config.toml 🔗

@@ -1,7 +1,7 @@
 name = "JavaScript"
 path_suffixes = ["js", "jsx", "mjs", "cjs"]
 first_line_pattern = '^#!.*\bnode\b'
-line_comment = "// "
+line_comments = ["// "]
 autoclose_before = ";:.,=}])>"
 brackets = [
     { start = "{", end = "}", close = true, newline = true },
@@ -18,7 +18,7 @@ scope_opt_in_language_servers = ["tailwindcss-language-server"]
 prettier_parser_name = "babel"
 
 [overrides.element]
-line_comment = { remove = true }
+line_comments = { remove = true }
 block_comment = ["{/* ", " */}"]
 
 [overrides.string]

crates/zed/src/languages/json/config.toml 🔗

@@ -1,6 +1,6 @@
 name = "JSON"
 path_suffixes = ["json"]
-line_comment = "// "
+line_comments = ["// "]
 autoclose_before = ",]}"
 brackets = [
     { start = "{", end = "}", close = true, newline = true },

crates/zed/src/languages/lua/config.toml 🔗

@@ -1,6 +1,6 @@
 name = "Lua"
 path_suffixes = ["lua"]
-line_comment = "-- "
+line_comments = ["-- "]
 autoclose_before = ",]}"
 brackets = [
     { start = "{", end = "}", close = true, newline = true },

crates/zed/src/languages/nix/config.toml 🔗

@@ -1,6 +1,6 @@
 name = "Nix"
 path_suffixes = ["nix"]
-line_comment = "# "
+line_comments = ["# "]
 block_comment = ["/* ", " */"]
 autoclose_before = ";:.,=}])>` \n\t\""
 brackets = [

crates/zed/src/languages/nu/config.toml 🔗

@@ -1,6 +1,6 @@
 name = "Nu"
 path_suffixes = ["nu"]
-line_comment = "# "
+line_comments = ["# "]
 autoclose_before = ";:.,=}])>` \n\t\""
 brackets = [
     { start = "{", end = "}", close = true, newline = true },

crates/zed/src/languages/php/config.toml 🔗

@@ -1,7 +1,7 @@
 name = "PHP"
 path_suffixes = ["php"]
 first_line_pattern = '^#!.*php'
-line_comment = "// "
+line_comments = ["// "]
 autoclose_before = ";:.,=}])>"
 brackets = [
     { start = "{", end = "}", close = true, newline = true },

crates/zed/src/languages/python/config.toml 🔗

@@ -1,7 +1,7 @@
 name = "Python"
 path_suffixes = ["py", "pyi", "mpy"]
 first_line_pattern = '^#!.*\bpython[0-9.]*\b'
-line_comment = "# "
+line_comments = ["# "]
 autoclose_before = ";:.,=}])>"
 brackets = [
     { start = "{", end = "}", close = true, newline = true },

crates/zed/src/languages/racket/config.toml 🔗

@@ -1,6 +1,6 @@
 name = "Racket"
 path_suffixes = ["rkt"]
-line_comment = "; "
+line_comments = ["; "]
 autoclose_before = "])"
 brackets = [
     { start = "[", end = "]", close = true, newline = false },

crates/zed/src/languages/ruby/config.toml 🔗

@@ -1,7 +1,7 @@
 name = "Ruby"
 path_suffixes = ["rb", "Gemfile"]
 first_line_pattern = '^#!.*\bruby\b'
-line_comment = "# "
+line_comments = ["# "]
 autoclose_before = ";:.,=}])>"
 brackets = [
   { start = "{", end = "}", close = true, newline = true },

crates/zed/src/languages/rust/config.toml 🔗

@@ -1,6 +1,6 @@
 name = "Rust"
 path_suffixes = ["rs"]
-line_comment = "// "
+line_comments = ["// ", "/// ", "//! "]
 autoclose_before = ";:.,=}])>"
 brackets = [
     { start = "{", end = "}", close = true, newline = true },

crates/zed/src/languages/scheme/config.toml 🔗

@@ -1,6 +1,6 @@
 name = "Scheme"
 path_suffixes = ["scm", "ss"]
-line_comment = "; "
+line_comments = ["; "]
 autoclose_before = "])"
 brackets = [
     { start = "[", end = "]", close = true, newline = false },

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

@@ -1,6 +1,6 @@
 name = "TOML"
 path_suffixes = ["Cargo.lock", "toml"]
-line_comment = "# "
+line_comments = ["# "]
 autoclose_before = ",]}"
 brackets = [
     { start = "{", end = "}", close = true, newline = true },

crates/zed/src/languages/tsx/config.toml 🔗

@@ -1,6 +1,6 @@
 name = "TSX"
 path_suffixes = ["tsx"]
-line_comment = "// "
+line_comments = ["// "]
 autoclose_before = ";:.,=}])>"
 brackets = [
     { start = "{", end = "}", close = true, newline = true },
@@ -17,7 +17,7 @@ scope_opt_in_language_servers = ["tailwindcss-language-server"]
 prettier_parser_name = "typescript"
 
 [overrides.element]
-line_comment = { remove = true }
+line_comments = { remove = true }
 block_comment = ["{/* ", " */}"]
 
 [overrides.string]

crates/zed/src/languages/typescript/config.toml 🔗

@@ -1,6 +1,6 @@
 name = "TypeScript"
 path_suffixes = ["ts", "cts", "d.cts", "d.mts", "mts"]
-line_comment = "// "
+line_comments = ["// "]
 autoclose_before = ";:.,=}])>"
 brackets = [
     { start = "{", end = "}", close = true, newline = true },

crates/zed/src/languages/uiua/config.toml 🔗

@@ -1,6 +1,6 @@
 name = "Uiua"
 path_suffixes = ["ua"]
-line_comment = "# "
+line_comments = ["# "]
 autoclose_before = ")]}\""
 brackets = [
     { start = "{", end = "}", close = true, newline = false},

crates/zed/src/languages/yaml/config.toml 🔗

@@ -1,6 +1,6 @@
 name = "YAML"
 path_suffixes = ["yml", "yaml"]
-line_comment = "# "
+line_comments = ["# "]
 autoclose_before = ",]}"
 brackets = [
     { start = "{", end = "}", close = true, newline = true },