diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 7808254009..ec182c1167 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3930,12 +3930,12 @@ impl Editor { let (comment_delimiter, insert_extra_newline) = if let Some(language) = &language_scope { - let insert_extra_newline = + let mut insert_extra_newline = insert_extra_newline_brackets(&buffer, start..end, language) || insert_extra_newline_tree_sitter(&buffer, start..end); // Comment extension on newline is allowed only for cursor selections - let comment_delimiter = maybe!({ + let mut comment_delimiter = maybe!({ if !selection_is_empty { return None; } @@ -3974,6 +3974,93 @@ impl Editor { None } }); + + if comment_delimiter.is_none() { + comment_delimiter = maybe!({ + if !selection_is_empty { + return None; + } + + if !multi_buffer.language_settings(cx).extend_comment_on_newline + { + return None; + } + + let doc_block = language.documentation_block(); + let doc_block_prefix = doc_block.first()?; + let doc_block_suffix = doc_block.last()?; + + let doc_comment_prefix = + language.documentation_comment_prefix()?; + + let (snapshot, range) = buffer + .buffer_line_for_row(MultiBufferRow(start_point.row))?; + + let cursor_is_after_prefix = { + let doc_block_prefix_len = doc_block_prefix.len(); + let max_len_of_delimiter = std::cmp::max( + doc_comment_prefix.len(), + doc_block_prefix_len, + ); + let index_of_first_non_whitespace = snapshot + .chars_for_range(range.clone()) + .take_while(|c| c.is_whitespace()) + .count(); + let doc_line_candidate = snapshot + .chars_for_range(range.clone()) + .skip(index_of_first_non_whitespace) + .take(max_len_of_delimiter) + .collect::(); + if doc_line_candidate.starts_with(doc_block_prefix.as_ref()) + { + index_of_first_non_whitespace + doc_block_prefix_len + <= start_point.column as usize + } else if doc_line_candidate + .starts_with(doc_comment_prefix.as_ref()) + { + index_of_first_non_whitespace + doc_comment_prefix.len() + <= start_point.column as usize + } else { + false + } + }; + + let cursor_is_before_suffix_if_exits = { + let whitespace_char_from_last = snapshot + .reversed_chars_for_range(range.clone()) + .take_while(|c| c.is_whitespace()) + .count(); + let mut line_rev_iter = snapshot + .reversed_chars_for_range(range) + .skip(whitespace_char_from_last); + let suffix_exists = doc_block_suffix + .chars() + .rev() + .all(|char| line_rev_iter.next() == Some(char)); + if suffix_exists { + let max_point = + snapshot.line_len(start_point.row) as usize; + let cursor_is_before_suffix = whitespace_char_from_last + + doc_block_suffix.len() + + start_point.column as usize + <= max_point; + if cursor_is_before_suffix { + insert_extra_newline = true; + } + cursor_is_before_suffix + } else { + true + } + }; + + if cursor_is_after_prefix && cursor_is_before_suffix_if_exits { + Some(doc_comment_prefix.clone()) + } else { + None + } + }); + } + (comment_delimiter, insert_extra_newline) } else { (None, false) @@ -3987,11 +4074,14 @@ impl Editor { String::with_capacity(1 + capacity_for_delimiter + indent.len as usize); new_text.push('\n'); new_text.extend(indent.chars()); + if let Some(delimiter) = &comment_delimiter { new_text.push_str(delimiter); } + if insert_extra_newline { - new_text = new_text.repeat(2); + new_text.push('\n'); + new_text.extend(indent.chars()); } let anchor = buffer.anchor_after(end); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 2502579b21..61381c974c 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -2797,6 +2797,107 @@ async fn test_newline_comments(cx: &mut TestAppContext) { "}); } +#[gpui::test] +async fn test_newline_documentation_comments(cx: &mut TestAppContext) { + init_test(cx, |settings| { + settings.defaults.tab_size = NonZeroU32::new(4) + }); + + let language = Arc::new(Language::new( + LanguageConfig { + documentation_block: Some(vec!["/**".into(), "*/".into()]), + documentation_comment_prefix: Some("*".into()), + ..LanguageConfig::default() + }, + None, + )); + { + let mut cx = EditorTestContext::new(cx).await; + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + cx.set_state(indoc! {" + /**ˇ + "}); + + cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx)); + cx.assert_editor_state(indoc! {" + /** + *ˇ + "}); + // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix. + cx.set_state(indoc! {" + ˇ/** + "}); + cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx)); + cx.assert_editor_state(indoc! {" + + ˇ/** + "}); + // Ensure that if cursor is between it doesn't add comment prefix. + cx.set_state(indoc! {" + /*ˇ* + "}); + cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx)); + cx.assert_editor_state(indoc! {" + /* + ˇ* + "}); + // Ensure that if suffix exists on same line after cursor it adds new line. + cx.set_state(indoc! {" + /**ˇ*/ + "}); + cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx)); + cx.assert_editor_state(indoc! {" + /** + *ˇ + */ + "}); + // Ensure that it detects suffix after existing prefix. + cx.set_state(indoc! {" + /**ˇ/ + "}); + cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx)); + cx.assert_editor_state(indoc! {" + /** + ˇ/ + "}); + // Ensure that if suffix exists on same line before cursor it does not add comment prefix. + cx.set_state(indoc! {" + /** */ˇ + "}); + cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx)); + cx.assert_editor_state(indoc! {" + /** */ + ˇ + "}); + // Ensure that if suffix exists on same line before cursor it does not add comment prefix. + cx.set_state(indoc! {" + /** + * + */ˇ + "}); + cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx)); + cx.assert_editor_state(indoc! {" + /** + * + */ + ˇ + "}); + } + // Ensure that comment continuations can be disabled. + update_test_language_settings(cx, |settings| { + settings.defaults.extend_comment_on_newline = Some(false); + }); + let mut cx = EditorTestContext::new(cx).await; + cx.set_state(indoc! {" + /**ˇ + "}); + cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx)); + cx.assert_editor_state(indoc! {" + /** + ˇ + "}); +} + #[gpui::test] fn test_insert_with_old_selections(cx: &mut TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 1c6c686709..dd89f7e78a 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -755,6 +755,12 @@ pub struct LanguageConfig { /// A list of preferred debuggers for this language. #[serde(default)] pub debuggers: IndexSet, + /// A character to add as a prefix when a new line is added to a documentation block. + #[serde(default)] + pub documentation_comment_prefix: Option>, + /// Returns string documentation block of this language should start with. + #[serde(default)] + pub documentation_block: Option>>, } #[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)] @@ -883,6 +889,8 @@ impl Default for LanguageConfig { completion_query_characters: Default::default(), debuggers: Default::default(), significant_indentation: Default::default(), + documentation_comment_prefix: None, + documentation_block: None, } } } @@ -1802,6 +1810,23 @@ impl LanguageScope { .unwrap_or(false) } + /// A character to add as a prefix when a new line is added to a documentation block. + /// + /// Used for documentation styles that require a leading character on each line, + /// such as the asterisk in JSDoc, Javadoc, etc. + pub fn documentation_comment_prefix(&self) -> Option<&Arc> { + self.language.config.documentation_comment_prefix.as_ref() + } + + /// Returns prefix and suffix for documentation block of this language. + pub fn documentation_block(&self) -> &[Arc] { + self.language + .config + .documentation_block + .as_ref() + .map_or([].as_slice(), |e| e.as_slice()) + } + /// Returns a list of bracket pairs for a given language with an additional /// piece of information about whether the particular bracket pair is currently active for a given language. pub fn brackets(&self) -> impl Iterator { diff --git a/crates/languages/src/javascript/config.toml b/crates/languages/src/javascript/config.toml index 112357f6c0..1559a2b295 100644 --- a/crates/languages/src/javascript/config.toml +++ b/crates/languages/src/javascript/config.toml @@ -20,6 +20,8 @@ tab_size = 2 scope_opt_in_language_servers = ["tailwindcss-language-server", "emmet-language-server"] prettier_parser_name = "babel" debuggers = ["JavaScript"] +documentation_comment_prefix = "*" +documentation_block = ["/**", "*/"] [jsx_tag_auto_close] open_tag_node_name = "jsx_opening_element" diff --git a/crates/languages/src/tsx/config.toml b/crates/languages/src/tsx/config.toml index 2c9fccc5b2..2a2a3000b1 100644 --- a/crates/languages/src/tsx/config.toml +++ b/crates/languages/src/tsx/config.toml @@ -18,6 +18,8 @@ scope_opt_in_language_servers = ["tailwindcss-language-server", "emmet-language- prettier_parser_name = "typescript" tab_size = 2 debuggers = ["JavaScript"] +documentation_comment_prefix = "*" +documentation_block = ["/**", "*/"] [jsx_tag_auto_close] open_tag_node_name = "jsx_opening_element" diff --git a/crates/languages/src/typescript/config.toml b/crates/languages/src/typescript/config.toml index 8aff96104c..5e76789a58 100644 --- a/crates/languages/src/typescript/config.toml +++ b/crates/languages/src/typescript/config.toml @@ -18,6 +18,8 @@ word_characters = ["#", "$"] prettier_parser_name = "typescript" tab_size = 2 debuggers = ["JavaScript"] +documentation_comment_prefix = "*" +documentation_block = ["/**", "*/"] [overrides.string] completion_query_characters = ["."]