Detailed changes
@@ -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::<String>();
+ 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);
@@ -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, |_| {});
@@ -755,6 +755,12 @@ pub struct LanguageConfig {
/// A list of preferred debuggers for this language.
#[serde(default)]
pub debuggers: IndexSet<SharedString>,
+ /// A character to add as a prefix when a new line is added to a documentation block.
+ #[serde(default)]
+ pub documentation_comment_prefix: Option<Arc<str>>,
+ /// Returns string documentation block of this language should start with.
+ #[serde(default)]
+ pub documentation_block: Option<Vec<Arc<str>>>,
}
#[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<str>> {
+ self.language.config.documentation_comment_prefix.as_ref()
+ }
+
+ /// Returns prefix and suffix for documentation block of this language.
+ pub fn documentation_block(&self) -> &[Arc<str>] {
+ 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<Item = (&BracketPair, bool)> {
@@ -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"
@@ -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"
@@ -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 = ["."]