Compare commits

...

2 Commits

Author SHA1 Message Date
Richard Feldman
4c7d1b36df Fix for comment indentation 2025-07-03 15:46:04 -04:00
Richard Feldman
c54f3f6443 Reproduce indenting error for comments 2025-07-02 15:05:44 -04:00
2 changed files with 191 additions and 46 deletions

View File

@@ -9743,6 +9743,14 @@ impl Editor {
let mut edits = Vec::new();
let mut prev_edited_row = 0;
let mut row_delta = 0;
// Check if we have multiple cursors all at the beginning of their lines (column 0)
// This is specifically for fixing the issue where indenting multiple comment lines
// results in inconsistent indentation
let empty_selections: Vec<_> = selections.iter().filter(|s| s.is_empty()).collect();
let multiple_cursors_at_line_start =
empty_selections.len() > 1 && empty_selections.iter().all(|s| s.head().column == 0);
for selection in &mut selections {
if selection.start.row != prev_edited_row {
row_delta = 0;
@@ -9761,67 +9769,87 @@ impl Editor {
if let Some(suggested_indent) =
suggested_indents.get(&MultiBufferRow(cursor.row)).copied()
{
// Don't do anything if already at suggested indent
// and there is any other cursor which is not
if has_some_cursor_in_whitespace
&& cursor.column == current_indent.len
&& current_indent.len == suggested_indent.len
{
continue;
}
// Adjust line and move cursor to suggested indent
// if cursor is not at suggested indent
if cursor.column < suggested_indent.len
&& cursor.column <= current_indent.len
&& current_indent.len <= suggested_indent.len
{
selection.start = Point::new(cursor.row, suggested_indent.len);
selection.end = selection.start;
if row_delta == 0 {
edits.extend(Buffer::edit_for_indent_size_adjustment(
cursor.row,
current_indent,
suggested_indent,
));
row_delta = suggested_indent.len - current_indent.len;
// Skip suggested indent logic when we have multiple cursors at the beginning
// of lines to ensure consistent indentation across multiple lines
if !(multiple_cursors_at_line_start && cursor.column == 0) {
// Don't do anything if already at suggested indent
// and there is any other cursor which is not
if has_some_cursor_in_whitespace
&& cursor.column == current_indent.len
&& current_indent.len == suggested_indent.len
{
continue;
}
continue;
}
// If current indent is more than suggested indent
// only move cursor to current indent and skip indent
if cursor.column < current_indent.len && current_indent.len > suggested_indent.len {
selection.start = Point::new(cursor.row, current_indent.len);
selection.end = selection.start;
continue;
// Adjust line and move cursor to suggested indent
// if cursor is not at suggested indent
if cursor.column < suggested_indent.len
&& cursor.column <= current_indent.len
&& current_indent.len <= suggested_indent.len
{
selection.start = Point::new(cursor.row, suggested_indent.len);
selection.end = selection.start;
if row_delta == 0 {
edits.extend(Buffer::edit_for_indent_size_adjustment(
cursor.row,
current_indent,
suggested_indent,
));
row_delta = suggested_indent.len - current_indent.len;
}
continue;
}
// If current indent is more than suggested indent
// only move cursor to current indent and skip indent
if cursor.column < current_indent.len
&& current_indent.len > suggested_indent.len
{
selection.start = Point::new(cursor.row, current_indent.len);
selection.end = selection.start;
continue;
}
}
}
// Otherwise, insert a hard or soft tab.
let settings = buffer.language_settings_at(cursor, cx);
let tab_size = if settings.hard_tabs {
let indent_size = if settings.hard_tabs {
IndentSize::tab()
} else {
let tab_size = settings.tab_size.get();
let indent_remainder = snapshot
.text_for_range(Point::new(cursor.row, 0)..cursor)
.flat_map(str::chars)
.fold(row_delta % tab_size, |counter: u32, c| {
if c == '\t' {
0
} else {
(counter + 1) % tab_size
}
});
// When we have multiple cursors at the beginning of their lines,
// don't use row_delta in the calculation to ensure consistent indentation
let indent_remainder = if multiple_cursors_at_line_start && cursor.column == 0 {
0
} else {
snapshot
.text_for_range(Point::new(cursor.row, 0)..cursor)
.flat_map(str::chars)
.fold(row_delta % tab_size, |counter: u32, c| {
if c == '\t' {
0
} else {
(counter + 1) % tab_size
}
})
};
let chars_to_next_tab_stop = tab_size - indent_remainder;
IndentSize::spaces(chars_to_next_tab_stop)
};
selection.start = Point::new(cursor.row, cursor.column + row_delta + tab_size.len);
// When we have multiple cursors at line start, don't apply row_delta
let new_column = if multiple_cursors_at_line_start && cursor.column == 0 {
cursor.column + indent_size.len
} else {
cursor.column + row_delta + indent_size.len
};
selection.start = Point::new(cursor.row, new_column);
selection.end = selection.start;
edits.push((cursor..cursor, tab_size.chars().collect::<String>()));
row_delta += tab_size.len;
edits.push((cursor..cursor, indent_size.chars().collect::<String>()));
row_delta += indent_size.len;
}
self.transact(window, cx, |this, window, cx| {

View File

@@ -3468,6 +3468,123 @@ async fn test_indent_outdent(cx: &mut TestAppContext) {
"});
}
#[gpui::test]
async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let yaml_language = Arc::new(Language::new(
LanguageConfig {
name: "YAML".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["yml".to_string(), "yaml".to_string()],
..Default::default()
},
line_comments: vec!["# ".into()],
..Default::default()
},
None,
));
cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
cx.set_state(indoc! {"
ˇ# ingress:
ˇ# api:
ˇ# enabled: false
ˇ# pathType: Prefix
ˇ# console:
ˇ# enabled: false
ˇ# pathType: Prefix
"});
// Press tab to indent all lines
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
cx.assert_editor_state(indoc! {"
ˇ# ingress:
ˇ# api:
ˇ# enabled: false
ˇ# pathType: Prefix
ˇ# console:
ˇ# enabled: false
ˇ# pathType: Prefix
"});
}
#[gpui::test]
async fn test_indent_yaml_comments_simple(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let yaml_language = Arc::new(Language::new(
LanguageConfig {
name: "YAML".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["yml".to_string(), "yaml".to_string()],
..Default::default()
},
line_comments: vec!["# ".into()],
..Default::default()
},
None,
));
cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
cx.set_state(indoc! {"
ˇ# foo
ˇ# bar
"});
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
cx.assert_editor_state(indoc! {"
ˇ# foo
ˇ# bar
"});
}
#[gpui::test]
async fn test_indent_yaml_comments_full(cx: &mut TestAppContext) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
let yaml_language = Arc::new(Language::new(
LanguageConfig {
name: "YAML".into(),
matcher: LanguageMatcher {
path_suffixes: vec!["yml".to_string(), "yaml".to_string()],
..Default::default()
},
line_comments: vec!["# ".into()],
..Default::default()
},
None,
));
cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
cx.set_state(indoc! {"
ˇ# ingress:
ˇ# api:
ˇ# enabled: false
ˇ# pathType: Prefix
ˇ# console:
ˇ# enabled: false
ˇ# pathType: Prefix
"});
cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
cx.assert_editor_state(indoc! {"
ˇ# ingress:
ˇ# api:
ˇ# enabled: false
ˇ# pathType: Prefix
ˇ# console:
ˇ# enabled: false
ˇ# pathType: Prefix
"});
}
#[gpui::test]
async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
init_test(cx, |settings| {