editor: Add prefix on newline in documentation block (e.g. JSDoc) (#30768)
Closes #8973 - [x] Tests https://github.com/user-attachments/assets/7fc6608f-1c11-4c70-a69b-34bfa8f789a2 Release Notes: - Added auto-insertion of asterisk (*) prefix when creating new lines within JSDoc comment blocks.
This commit is contained in:
parent
4b7b5db58c
commit
c2feffac9d
6 changed files with 225 additions and 3 deletions
|
@ -3930,12 +3930,12 @@ impl Editor {
|
||||||
let (comment_delimiter, insert_extra_newline) = if let Some(language) =
|
let (comment_delimiter, insert_extra_newline) = if let Some(language) =
|
||||||
&language_scope
|
&language_scope
|
||||||
{
|
{
|
||||||
let insert_extra_newline =
|
let mut insert_extra_newline =
|
||||||
insert_extra_newline_brackets(&buffer, start..end, language)
|
insert_extra_newline_brackets(&buffer, start..end, language)
|
||||||
|| insert_extra_newline_tree_sitter(&buffer, start..end);
|
|| insert_extra_newline_tree_sitter(&buffer, start..end);
|
||||||
|
|
||||||
// Comment extension on newline is allowed only for cursor selections
|
// Comment extension on newline is allowed only for cursor selections
|
||||||
let comment_delimiter = maybe!({
|
let mut comment_delimiter = maybe!({
|
||||||
if !selection_is_empty {
|
if !selection_is_empty {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
@ -3974,6 +3974,93 @@ impl Editor {
|
||||||
None
|
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)
|
(comment_delimiter, insert_extra_newline)
|
||||||
} else {
|
} else {
|
||||||
(None, false)
|
(None, false)
|
||||||
|
@ -3987,11 +4074,14 @@ impl Editor {
|
||||||
String::with_capacity(1 + capacity_for_delimiter + indent.len as usize);
|
String::with_capacity(1 + capacity_for_delimiter + indent.len as usize);
|
||||||
new_text.push('\n');
|
new_text.push('\n');
|
||||||
new_text.extend(indent.chars());
|
new_text.extend(indent.chars());
|
||||||
|
|
||||||
if let Some(delimiter) = &comment_delimiter {
|
if let Some(delimiter) = &comment_delimiter {
|
||||||
new_text.push_str(delimiter);
|
new_text.push_str(delimiter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if insert_extra_newline {
|
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);
|
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]
|
#[gpui::test]
|
||||||
fn test_insert_with_old_selections(cx: &mut TestAppContext) {
|
fn test_insert_with_old_selections(cx: &mut TestAppContext) {
|
||||||
init_test(cx, |_| {});
|
init_test(cx, |_| {});
|
||||||
|
|
|
@ -755,6 +755,12 @@ pub struct LanguageConfig {
|
||||||
/// A list of preferred debuggers for this language.
|
/// A list of preferred debuggers for this language.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub debuggers: IndexSet<SharedString>,
|
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)]
|
#[derive(Clone, Debug, Serialize, Deserialize, Default, JsonSchema)]
|
||||||
|
@ -883,6 +889,8 @@ impl Default for LanguageConfig {
|
||||||
completion_query_characters: Default::default(),
|
completion_query_characters: Default::default(),
|
||||||
debuggers: Default::default(),
|
debuggers: Default::default(),
|
||||||
significant_indentation: Default::default(),
|
significant_indentation: Default::default(),
|
||||||
|
documentation_comment_prefix: None,
|
||||||
|
documentation_block: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1802,6 +1810,23 @@ impl LanguageScope {
|
||||||
.unwrap_or(false)
|
.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
|
/// 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.
|
/// 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)> {
|
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"]
|
scope_opt_in_language_servers = ["tailwindcss-language-server", "emmet-language-server"]
|
||||||
prettier_parser_name = "babel"
|
prettier_parser_name = "babel"
|
||||||
debuggers = ["JavaScript"]
|
debuggers = ["JavaScript"]
|
||||||
|
documentation_comment_prefix = "*"
|
||||||
|
documentation_block = ["/**", "*/"]
|
||||||
|
|
||||||
[jsx_tag_auto_close]
|
[jsx_tag_auto_close]
|
||||||
open_tag_node_name = "jsx_opening_element"
|
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"
|
prettier_parser_name = "typescript"
|
||||||
tab_size = 2
|
tab_size = 2
|
||||||
debuggers = ["JavaScript"]
|
debuggers = ["JavaScript"]
|
||||||
|
documentation_comment_prefix = "*"
|
||||||
|
documentation_block = ["/**", "*/"]
|
||||||
|
|
||||||
[jsx_tag_auto_close]
|
[jsx_tag_auto_close]
|
||||||
open_tag_node_name = "jsx_opening_element"
|
open_tag_node_name = "jsx_opening_element"
|
||||||
|
|
|
@ -18,6 +18,8 @@ word_characters = ["#", "$"]
|
||||||
prettier_parser_name = "typescript"
|
prettier_parser_name = "typescript"
|
||||||
tab_size = 2
|
tab_size = 2
|
||||||
debuggers = ["JavaScript"]
|
debuggers = ["JavaScript"]
|
||||||
|
documentation_comment_prefix = "*"
|
||||||
|
documentation_block = ["/**", "*/"]
|
||||||
|
|
||||||
[overrides.string]
|
[overrides.string]
|
||||||
completion_query_characters = ["."]
|
completion_query_characters = ["."]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue