diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 8f0f7d40ec269edbebd7e85abf88e478515550b9..ddacefec06e4d6f83f1e2d77dc5159f47c12da82 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -252,6 +252,8 @@ struct AutoindentRequestEntry { /// only be adjusted if the suggested indentation level has *changed* /// since the edit was made. first_line_is_new: bool, + /// The original indentation of the text that was inserted into this range. + original_indent: Option, } #[derive(Debug)] @@ -814,6 +816,8 @@ impl Buffer { Some(async move { let mut indent_sizes = BTreeMap::new(); for request in autoindent_requests { + // Resolve each edited range to its row in the current buffer and in the + // buffer before this batch of edits. let mut row_ranges = Vec::new(); let mut old_to_new_rows = BTreeMap::new(); for entry in &request.entries { @@ -824,11 +828,12 @@ impl Buffer { let old_row = position.to_point(&request.before_edit).row; old_to_new_rows.insert(old_row, new_row); } - if new_end_row > new_row { - row_ranges.push(new_row..new_end_row); - } + row_ranges.push((new_row..new_end_row, entry.original_indent)); } + // Build a map containing the suggested indentation for each of the edited lines + // with respect to the state of the buffer before these edits. This map is keyed + // by the rows for these lines in the current state of the buffer. let mut old_suggestions = BTreeMap::::default(); let old_edited_ranges = contiguous_ranges(old_to_new_rows.keys().copied(), max_rows_between_yields); @@ -856,17 +861,18 @@ impl Buffer { yield_now().await; } - // At this point, old_suggestions contains the suggested indentation for all edited lines - // with respect to the state of the buffer before the edit, but keyed by the row for these - // lines after the edits were applied. - + // In block mode, only compute indentation suggestions for the first line + // of each insertion. Otherwise, compute suggestions for every inserted line. let new_edited_row_ranges = contiguous_ranges( - row_ranges.iter().flat_map(|range| match request.mode { + row_ranges.iter().flat_map(|(range, _)| match request.mode { AutoindentMode::Block => range.start..range.start + 1, AutoindentMode::Independent => range.clone(), }), max_rows_between_yields, ); + + // Compute new suggestions for each line, but only include them in the result + // if they differ from the old suggestion for that line. for new_edited_row_range in new_edited_row_ranges { let suggestions = snapshot .suggest_autoindents(new_edited_row_range.clone()) @@ -894,34 +900,38 @@ impl Buffer { yield_now().await; } + // For each block of inserted text, adjust the indentation of the remaining + // lines of the block by the same amount as the first line was adjusted. if matches!(request.mode, AutoindentMode::Block) { - for row_range in row_ranges { - if row_range.len() > 1 { - if let Some(new_indent_size) = - indent_sizes.get(&row_range.start).copied() - { - let old_indent_size = - snapshot.indent_size_for_line(row_range.start); - if new_indent_size.kind == old_indent_size.kind { - let delta = - new_indent_size.len as i64 - old_indent_size.len as i64; - if delta != 0 { - for row in row_range.skip(1) { - indent_sizes.entry(row).or_insert_with(|| { - let mut size = snapshot.indent_size_for_line(row); - if size.kind == new_indent_size.kind { - if delta > 0 { - size.len += delta as u32; - } else if delta < 0 { - size.len = - size.len.saturating_sub(-delta as u32); - } - } - size - }); + for (row_range, original_indent) in + row_ranges + .into_iter() + .filter_map(|(range, original_indent)| { + if range.len() > 1 { + Some((range, original_indent?)) + } else { + None + } + }) + { + let new_indent = indent_sizes + .get(&row_range.start) + .copied() + .unwrap_or_else(|| snapshot.indent_size_for_line(row_range.start)); + let delta = new_indent.len as i64 - original_indent.len as i64; + if new_indent.kind == original_indent.kind && delta != 0 { + for row in row_range.skip(1) { + indent_sizes.entry(row).or_insert_with(|| { + let mut size = snapshot.indent_size_for_line(row); + if size.kind == new_indent.kind { + if delta > 0 { + size.len = size.len + delta as u32; + } else if delta < 0 { + size.len = size.len.saturating_sub(-delta as u32); } } - } + size + }); } } } @@ -1226,6 +1236,7 @@ impl Buffer { let mut range_of_insertion_to_indent = 0..new_text_len; let mut first_line_is_new = false; + let mut original_indent = None; // When inserting an entire line at the beginning of an existing line, // treat the insertion as new. @@ -1235,13 +1246,16 @@ impl Buffer { first_line_is_new = true; } - // Avoid auto-indenting lines before and after the insertion. + // When inserting text starting with a newline, avoid auto-indenting the + // previous line. if new_text[range_of_insertion_to_indent.clone()].starts_with('\n') { range_of_insertion_to_indent.start += 1; first_line_is_new = true; } + // Avoid auto-indenting before the insertion. if matches!(mode, AutoindentMode::Block) { + original_indent = Some(indent_size_for_text(new_text.chars())); if new_text[range_of_insertion_to_indent.clone()].ends_with('\n') { range_of_insertion_to_indent.end -= 1; } @@ -1249,6 +1263,7 @@ impl Buffer { AutoindentRequestEntry { first_line_is_new, + original_indent, range: self.anchor_before(new_start + range_of_insertion_to_indent.start) ..self.anchor_after(new_start + range_of_insertion_to_indent.end), } @@ -2144,8 +2159,12 @@ impl BufferSnapshot { } pub fn indent_size_for_line(text: &text::BufferSnapshot, row: u32) -> IndentSize { + indent_size_for_text(text.chars_at(Point::new(row, 0))) +} + +pub fn indent_size_for_text(text: impl Iterator) -> IndentSize { let mut result = IndentSize::spaces(0); - for c in text.chars_at(Point::new(row, 0)) { + for c in text { let kind = match c { ' ' => IndentKind::Space, '\t' => IndentKind::Tab, diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index acfaf5980d9df806cdcf3830364539383296275c..0657e256042fb4ebd304512fba380e86e5410b7d 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -920,20 +920,18 @@ fn test_autoindent_multi_line_insertion(cx: &mut MutableAppContext) { } #[gpui::test] -fn test_autoindent_preserves_relative_indentation_in_multi_line_insertion( - cx: &mut MutableAppContext, -) { +fn test_autoindent_block_mode(cx: &mut MutableAppContext) { cx.set_global(Settings::test(cx)); cx.add_model(|cx| { - let text = " + let text = r#" fn a() { b(); } - " + "# .unindent(); let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); - let pasted_text = r#" + let inserted_text = r#" " c d @@ -942,9 +940,33 @@ fn test_autoindent_preserves_relative_indentation_in_multi_line_insertion( "# .unindent(); - // insert at the beginning of a line + // Insert the block at column zero. The entire block is indented + // so that the first line matches the previous line's indentation. + buffer.edit( + [(Point::new(2, 0)..Point::new(2, 0), inserted_text.clone())], + Some(AutoindentMode::Block), + cx, + ); + assert_eq!( + buffer.text(), + r#" + fn a() { + b(); + " + c + d + e + " + } + "# + .unindent() + ); + + // Insert the block at a deeper indent level. The entire block is outdented. + buffer.undo(cx); + buffer.edit([(Point::new(2, 0)..Point::new(2, 0), " ")], None, cx); buffer.edit( - [(Point::new(2, 0)..Point::new(2, 0), pasted_text.clone())], + [(Point::new(2, 8)..Point::new(2, 8), inserted_text.clone())], Some(AutoindentMode::Block), cx, );