editor: Add language setting for comment continuations (#2605)

Per @JosephTLyons request I've added a language setting for comment
continuations.

Release Notes:

- Added a language setting for comment continuations.
This commit is contained in:
Piotr Osiewicz 2023-06-13 18:59:46 +02:00 committed by GitHub
parent aedef7bc58
commit b272db9e21
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 98 additions and 72 deletions

View file

@ -108,6 +108,8 @@
// Whether or not to remove any trailing whitespace from lines of a buffer // Whether or not to remove any trailing whitespace from lines of a buffer
// before saving it. // before saving it.
"remove_trailing_whitespace_on_save": true, "remove_trailing_whitespace_on_save": true,
// Whether to start a new line with a comment when a previous line is a comment as well.
"extend_comment_on_newline": true,
// Whether or not to ensure there's a single newline at the end of a buffer // Whether or not to ensure there's a single newline at the end of a buffer
// when saving it. // when saving it.
"ensure_final_newline_on_save": true, "ensure_final_newline_on_save": true,

View file

@ -2169,8 +2169,8 @@ impl Editor {
self.transact(cx, |this, cx| { self.transact(cx, |this, cx| {
let (edits, selection_fixup_info): (Vec<_>, Vec<_>) = { let (edits, selection_fixup_info): (Vec<_>, Vec<_>) = {
let selections = this.selections.all::<usize>(cx); let selections = this.selections.all::<usize>(cx);
let multi_buffer = this.buffer.read(cx);
let buffer = this.buffer.read(cx).snapshot(cx); let buffer = multi_buffer.snapshot(cx);
selections selections
.iter() .iter()
.map(|selection| { .map(|selection| {
@ -2181,70 +2181,74 @@ impl Editor {
let end = selection.end; let end = selection.end;
let is_cursor = start == end; let is_cursor = start == end;
let language_scope = buffer.language_scope_at(start); let language_scope = buffer.language_scope_at(start);
let (comment_delimiter, insert_extra_newline) = let (comment_delimiter, insert_extra_newline) = if let Some(language) =
if let Some(language) = &language_scope { &language_scope
let leading_whitespace_len = buffer {
.reversed_chars_at(start) let leading_whitespace_len = buffer
.take_while(|c| c.is_whitespace() && *c != '\n') .reversed_chars_at(start)
.map(|c| c.len_utf8()) .take_while(|c| c.is_whitespace() && *c != '\n')
.sum::<usize>(); .map(|c| c.len_utf8())
.sum::<usize>();
let trailing_whitespace_len = buffer let trailing_whitespace_len = buffer
.chars_at(end) .chars_at(end)
.take_while(|c| c.is_whitespace() && *c != '\n') .take_while(|c| c.is_whitespace() && *c != '\n')
.map(|c| c.len_utf8()) .map(|c| c.len_utf8())
.sum::<usize>(); .sum::<usize>();
let insert_extra_newline = let insert_extra_newline =
language.brackets().any(|(pair, enabled)| { language.brackets().any(|(pair, enabled)| {
let pair_start = pair.start.trim_end(); let pair_start = pair.start.trim_end();
let pair_end = pair.end.trim_start(); let pair_end = pair.end.trim_start();
enabled enabled
&& pair.newline && pair.newline
&& buffer.contains_str_at( && buffer.contains_str_at(
end + trailing_whitespace_len, end + trailing_whitespace_len,
pair_end, pair_end,
) )
&& buffer.contains_str_at( && buffer.contains_str_at(
(start - leading_whitespace_len) (start - leading_whitespace_len)
.saturating_sub(pair_start.len()), .saturating_sub(pair_start.len()),
pair_start, pair_start,
) )
}); });
// Comment extension on newline is allowed only for cursor selections // Comment extension on newline is allowed only for cursor selections
let comment_delimiter = let comment_delimiter = language.line_comment_prefix().filter(|_| {
language.line_comment_prefix().filter(|_| is_cursor); let is_comment_extension_enabled =
let comment_delimiter = if let Some(delimiter) = comment_delimiter { multi_buffer.settings_at(0, cx).extend_comment_on_newline;
buffer is_cursor && is_comment_extension_enabled
.buffer_line_for_row(start_point.row) });
.is_some_and(|(snapshot, range)| { let comment_delimiter = if let Some(delimiter) = comment_delimiter {
let mut index_of_first_non_whitespace = 0; buffer
let line_starts_with_comment = snapshot .buffer_line_for_row(start_point.row)
.chars_for_range(range) .is_some_and(|(snapshot, range)| {
.skip_while(|c| { let mut index_of_first_non_whitespace = 0;
let should_skip = c.is_whitespace(); let line_starts_with_comment = snapshot
if should_skip { .chars_for_range(range)
index_of_first_non_whitespace += 1; .skip_while(|c| {
} let should_skip = c.is_whitespace();
should_skip if should_skip {
}) index_of_first_non_whitespace += 1;
.take(delimiter.len()) }
.eq(delimiter.chars()); should_skip
let cursor_is_placed_after_comment_marker = })
index_of_first_non_whitespace + delimiter.len() .take(delimiter.len())
<= start_point.column as usize; .eq(delimiter.chars());
line_starts_with_comment let cursor_is_placed_after_comment_marker =
&& cursor_is_placed_after_comment_marker index_of_first_non_whitespace + delimiter.len()
}) <= start_point.column as usize;
.then(|| delimiter.clone()) line_starts_with_comment
} else { && cursor_is_placed_after_comment_marker
None })
}; .then(|| delimiter.clone())
(comment_delimiter, insert_extra_newline)
} else { } else {
(None, false) None
}; };
(comment_delimiter, insert_extra_newline)
} else {
(None, false)
};
let capacity_for_delimiter = comment_delimiter let capacity_for_delimiter = comment_delimiter
.as_deref() .as_deref()

View file

@ -1732,26 +1732,40 @@ async fn test_newline_comments(cx: &mut gpui::TestAppContext) {
}, },
None, None,
)); ));
{
let mut cx = EditorTestContext::new(cx).await; let mut cx = EditorTestContext::new(cx).await;
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
cx.set_state(indoc! {" cx.set_state(indoc! {"
// Fooˇ // Fooˇ
"}); "});
cx.update_editor(|e, cx| e.newline(&Newline, cx)); cx.update_editor(|e, cx| e.newline(&Newline, cx));
cx.assert_editor_state(indoc! {" cx.assert_editor_state(indoc! {"
// Foo // Foo
//ˇ //ˇ
"}); "});
// Ensure that if cursor is before the comment start, we do not actually insert a comment prefix. // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
cx.set_state(indoc! {" cx.set_state(indoc! {"
ˇ// Foo ˇ// Foo
"});
cx.update_editor(|e, cx| e.newline(&Newline, cx));
cx.assert_editor_state(indoc! {"
ˇ// Foo
"});
}
// Ensure that comment continuations can be disabled.
update_test_settings(cx, |settings| {
settings.defaults.extend_comment_on_newline = Some(false);
});
let mut cx = EditorTestContext::new(cx).await;
cx.set_state(indoc! {"
// Fooˇ
"}); "});
cx.update_editor(|e, cx| e.newline(&Newline, cx)); cx.update_editor(|e, cx| e.newline(&Newline, cx));
cx.assert_editor_state(indoc! {" cx.assert_editor_state(indoc! {"
// Foo
ˇ// Foo ˇ
"}); "});
} }

View file

@ -51,6 +51,7 @@ pub struct LanguageSettings {
pub enable_language_server: bool, pub enable_language_server: bool,
pub show_copilot_suggestions: bool, pub show_copilot_suggestions: bool,
pub show_whitespaces: ShowWhitespaceSetting, pub show_whitespaces: ShowWhitespaceSetting,
pub extend_comment_on_newline: bool,
} }
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
@ -95,6 +96,8 @@ pub struct LanguageSettingsContent {
pub show_copilot_suggestions: Option<bool>, pub show_copilot_suggestions: Option<bool>,
#[serde(default)] #[serde(default)]
pub show_whitespaces: Option<ShowWhitespaceSetting>, pub show_whitespaces: Option<ShowWhitespaceSetting>,
#[serde(default)]
pub extend_comment_on_newline: Option<bool>,
} }
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
@ -340,7 +343,10 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent
src.show_copilot_suggestions, src.show_copilot_suggestions,
); );
merge(&mut settings.show_whitespaces, src.show_whitespaces); merge(&mut settings.show_whitespaces, src.show_whitespaces);
merge(
&mut settings.extend_comment_on_newline,
src.extend_comment_on_newline,
);
fn merge<T>(target: &mut T, value: Option<T>) { fn merge<T>(target: &mut T, value: Option<T>) {
if let Some(value) = value { if let Some(value) = value {
*target = value; *target = value;