Detailed changes
@@ -55,10 +55,10 @@
"n": "vim::MoveToNextMatch",
"shift-n": "vim::MoveToPrevMatch",
"%": "vim::Matching",
- "] }": ["vim::UnmatchedForward", { "char": "}" } ],
- "[ {": ["vim::UnmatchedBackward", { "char": "{" } ],
- "] )": ["vim::UnmatchedForward", { "char": ")" } ],
- "[ (": ["vim::UnmatchedBackward", { "char": "(" } ],
+ "] }": ["vim::UnmatchedForward", { "char": "}" }],
+ "[ {": ["vim::UnmatchedBackward", { "char": "{" }],
+ "] )": ["vim::UnmatchedForward", { "char": ")" }],
+ "[ (": ["vim::UnmatchedBackward", { "char": "(" }],
"f": ["vim::PushOperator", { "FindForward": { "before": false } }],
"t": ["vim::PushOperator", { "FindForward": { "before": true } }],
"shift-f": ["vim::PushOperator", { "FindBackward": { "after": false } }],
@@ -209,6 +209,7 @@
"shift-s": "vim::SubstituteLine",
">": ["vim::PushOperator", "Indent"],
"<": ["vim::PushOperator", "Outdent"],
+ "=": ["vim::PushOperator", "AutoIndent"],
"g u": ["vim::PushOperator", "Lowercase"],
"g shift-u": ["vim::PushOperator", "Uppercase"],
"g ~": ["vim::PushOperator", "OppositeCase"],
@@ -275,6 +276,7 @@
"ctrl-[": ["vim::SwitchMode", "Normal"],
">": "vim::Indent",
"<": "vim::Outdent",
+ "=": "vim::AutoIndent",
"i": ["vim::PushOperator", { "Object": { "around": false } }],
"a": ["vim::PushOperator", { "Object": { "around": true } }],
"g c": "vim::ToggleComments",
@@ -303,6 +303,7 @@ gpui::actions!(
OpenPermalinkToLine,
OpenUrl,
Outdent,
+ AutoIndent,
PageDown,
PageUp,
Paste,
@@ -6362,6 +6362,25 @@ impl Editor {
});
}
+ pub fn autoindent(&mut self, _: &AutoIndent, cx: &mut ViewContext<Self>) {
+ if self.read_only(cx) {
+ return;
+ }
+ let selections = self
+ .selections
+ .all::<usize>(cx)
+ .into_iter()
+ .map(|s| s.range());
+
+ self.transact(cx, |this, cx| {
+ this.buffer.update(cx, |buffer, cx| {
+ buffer.autoindent_ranges(selections, cx);
+ });
+ let selections = this.selections.all::<usize>(cx);
+ this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
+ });
+ }
+
pub fn delete_line(&mut self, _: &DeleteLine, cx: &mut ViewContext<Self>) {
let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
let selections = self.selections.all::<Point>(cx);
@@ -34,6 +34,7 @@ use serde_json::{self, json};
use std::sync::atomic;
use std::sync::atomic::AtomicUsize;
use std::{cell::RefCell, future::Future, rc::Rc, time::Instant};
+use test::editor_lsp_test_context::rust_lang;
use unindent::Unindent;
use util::{
assert_set_eq,
@@ -5458,7 +5459,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) {
}
#[gpui::test]
-async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
+async fn test_autoindent(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let language = Arc::new(
@@ -5520,6 +5521,89 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
});
}
+#[gpui::test]
+async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) {
+ init_test(cx, |_| {});
+
+ {
+ let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
+ cx.set_state(indoc! {"
+ impl A {
+
+ fn b() {}
+
+ ยซfn c() {
+
+ }หยป
+ }
+ "});
+
+ cx.update_editor(|editor, cx| {
+ editor.autoindent(&Default::default(), cx);
+ });
+
+ cx.assert_editor_state(indoc! {"
+ impl A {
+
+ fn b() {}
+
+ ยซfn c() {
+
+ }หยป
+ }
+ "});
+ }
+
+ {
+ let mut cx = EditorTestContext::new_multibuffer(
+ cx,
+ [indoc! { "
+ impl A {
+ ยซ
+ // a
+ fn b(){}
+ ยป
+ ยซ
+ }
+ fn c(){}
+ ยป
+ "}],
+ );
+
+ let buffer = cx.update_editor(|editor, cx| {
+ let buffer = editor.buffer().update(cx, |buffer, _| {
+ buffer.all_buffers().iter().next().unwrap().clone()
+ });
+ buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
+ buffer
+ });
+
+ cx.run_until_parked();
+ cx.update_editor(|editor, cx| {
+ editor.select_all(&Default::default(), cx);
+ editor.autoindent(&Default::default(), cx)
+ });
+ cx.run_until_parked();
+
+ cx.update(|cx| {
+ pretty_assertions::assert_eq!(
+ buffer.read(cx).text(),
+ indoc! { "
+ impl A {
+
+ // a
+ fn b(){}
+
+
+ }
+ fn c(){}
+
+ " }
+ )
+ });
+ }
+}
+
#[gpui::test]
async fn test_autoclose_and_auto_surround_pairs(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
@@ -13912,20 +13996,6 @@ pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsC
update_test_language_settings(cx, f);
}
-pub(crate) fn rust_lang() -> Arc<Language> {
- Arc::new(Language::new(
- LanguageConfig {
- name: "Rust".into(),
- matcher: LanguageMatcher {
- path_suffixes: vec!["rs".to_string()],
- ..Default::default()
- },
- ..Default::default()
- },
- Some(tree_sitter_rust::LANGUAGE.into()),
- ))
-}
-
#[track_caller]
fn assert_hunk_revert(
not_reverted_text_with_selections: &str,
@@ -189,6 +189,7 @@ impl EditorElement {
register_action(view, cx, Editor::tab_prev);
register_action(view, cx, Editor::indent);
register_action(view, cx, Editor::outdent);
+ register_action(view, cx, Editor::autoindent);
register_action(view, cx, Editor::delete_line);
register_action(view, cx, Editor::join_lines);
register_action(view, cx, Editor::sort_lines_case_sensitive);
@@ -1258,6 +1258,7 @@ pub mod tests {
use crate::{
scroll::{scroll_amount::ScrollAmount, Autoscroll},
+ test::editor_lsp_test_context::rust_lang,
ExcerptRange,
};
use futures::StreamExt;
@@ -2274,7 +2275,7 @@ pub mod tests {
let project = Project::test(fs, ["/a".as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
- language_registry.add(crate::editor_tests::rust_lang());
+ language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp(
"Rust",
FakeLspAdapter {
@@ -2570,7 +2571,7 @@ pub mod tests {
let project = Project::test(fs, ["/a".as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
- let language = crate::editor_tests::rust_lang();
+ let language = rust_lang();
language_registry.add(language);
let mut fake_servers = language_registry.register_fake_lsp(
"Rust",
@@ -2922,7 +2923,7 @@ pub mod tests {
let project = Project::test(fs, ["/a".as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
- language_registry.add(crate::editor_tests::rust_lang());
+ language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp(
"Rust",
FakeLspAdapter {
@@ -3153,7 +3154,7 @@ pub mod tests {
let project = Project::test(fs, ["/a".as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
- language_registry.add(crate::editor_tests::rust_lang());
+ language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp(
"Rust",
FakeLspAdapter {
@@ -3396,7 +3397,7 @@ pub mod tests {
let project = Project::test(fs, ["/a".as_ref()], cx).await;
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
- language_registry.add(crate::editor_tests::rust_lang());
+ language_registry.add(rust_lang());
let mut fake_servers = language_registry.register_fake_lsp(
"Rust",
FakeLspAdapter {
@@ -31,6 +31,47 @@ pub struct EditorLspTestContext {
pub buffer_lsp_url: lsp::Url,
}
+pub(crate) fn rust_lang() -> Arc<Language> {
+ let language = Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ matcher: LanguageMatcher {
+ path_suffixes: vec!["rs".to_string()],
+ ..Default::default()
+ },
+ line_comments: vec!["// ".into(), "/// ".into(), "//! ".into()],
+ ..Default::default()
+ },
+ Some(tree_sitter_rust::LANGUAGE.into()),
+ )
+ .with_queries(LanguageQueries {
+ indents: Some(Cow::from(indoc! {r#"
+ [
+ ((where_clause) _ @end)
+ (field_expression)
+ (call_expression)
+ (assignment_expression)
+ (let_declaration)
+ (let_chain)
+ (await_expression)
+ ] @indent
+
+ (_ "[" "]" @end) @indent
+ (_ "<" ">" @end) @indent
+ (_ "{" "}" @end) @indent
+ (_ "(" ")" @end) @indent"#})),
+ brackets: Some(Cow::from(indoc! {r#"
+ ("(" @open ")" @close)
+ ("[" @open "]" @close)
+ ("{" @open "}" @close)
+ ("<" @open ">" @close)
+ ("\"" @open "\"" @close)
+ (closure_parameters "|" @open "|" @close)"#})),
+ ..Default::default()
+ })
+ .expect("Could not parse queries");
+ Arc::new(language)
+}
impl EditorLspTestContext {
pub async fn new(
language: Language,
@@ -119,46 +160,7 @@ impl EditorLspTestContext {
capabilities: lsp::ServerCapabilities,
cx: &mut gpui::TestAppContext,
) -> EditorLspTestContext {
- let language = Language::new(
- LanguageConfig {
- name: "Rust".into(),
- matcher: LanguageMatcher {
- path_suffixes: vec!["rs".to_string()],
- ..Default::default()
- },
- line_comments: vec!["// ".into(), "/// ".into(), "//! ".into()],
- ..Default::default()
- },
- Some(tree_sitter_rust::LANGUAGE.into()),
- )
- .with_queries(LanguageQueries {
- indents: Some(Cow::from(indoc! {r#"
- [
- ((where_clause) _ @end)
- (field_expression)
- (call_expression)
- (assignment_expression)
- (let_declaration)
- (let_chain)
- (await_expression)
- ] @indent
-
- (_ "[" "]" @end) @indent
- (_ "<" ">" @end) @indent
- (_ "{" "}" @end) @indent
- (_ "(" ")" @end) @indent"#})),
- brackets: Some(Cow::from(indoc! {r#"
- ("(" @open ")" @close)
- ("[" @open "]" @close)
- ("{" @open "}" @close)
- ("<" @open ">" @close)
- ("\"" @open "\"" @close)
- (closure_parameters "|" @open "|" @close)"#})),
- ..Default::default()
- })
- .expect("Could not parse queries");
-
- Self::new(language, capabilities, cx).await
+ Self::new(Arc::into_inner(rust_lang()).unwrap(), capabilities, cx).await
}
pub async fn new_typescript(
@@ -467,6 +467,7 @@ struct AutoindentRequest {
before_edit: BufferSnapshot,
entries: Vec<AutoindentRequestEntry>,
is_block_mode: bool,
+ ignore_empty_lines: bool,
}
#[derive(Debug, Clone)]
@@ -1381,7 +1382,7 @@ impl Buffer {
let autoindent_requests = self.autoindent_requests.clone();
Some(async move {
- let mut indent_sizes = BTreeMap::new();
+ let mut indent_sizes = BTreeMap::<u32, (IndentSize, bool)>::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.
@@ -1475,10 +1476,12 @@ impl Buffer {
let suggested_indent = indent_sizes
.get(&suggestion.basis_row)
.copied()
+ .map(|e| e.0)
.unwrap_or_else(|| {
snapshot.indent_size_for_line(suggestion.basis_row)
})
.with_delta(suggestion.delta, language_indent_size);
+
if old_suggestions.get(&new_row).map_or(
true,
|(old_indentation, was_within_error)| {
@@ -1486,7 +1489,10 @@ impl Buffer {
&& (!suggestion.within_error || *was_within_error)
},
) {
- indent_sizes.insert(new_row, suggested_indent);
+ indent_sizes.insert(
+ new_row,
+ (suggested_indent, request.ignore_empty_lines),
+ );
}
}
}
@@ -1494,10 +1500,12 @@ impl Buffer {
if let (true, Some(original_indent_column)) =
(request.is_block_mode, original_indent_column)
{
- let new_indent = indent_sizes
- .get(&row_range.start)
- .copied()
- .unwrap_or_else(|| snapshot.indent_size_for_line(row_range.start));
+ let new_indent =
+ if let Some((indent, _)) = indent_sizes.get(&row_range.start) {
+ *indent
+ } else {
+ snapshot.indent_size_for_line(row_range.start)
+ };
let delta = new_indent.len as i64 - original_indent_column as i64;
if delta != 0 {
for row in row_range.skip(1) {
@@ -1512,7 +1520,7 @@ impl Buffer {
Ordering::Equal => {}
}
}
- size
+ (size, request.ignore_empty_lines)
});
}
}
@@ -1523,6 +1531,15 @@ impl Buffer {
}
indent_sizes
+ .into_iter()
+ .filter_map(|(row, (indent, ignore_empty_lines))| {
+ if ignore_empty_lines && snapshot.line_len(row) == 0 {
+ None
+ } else {
+ Some((row, indent))
+ }
+ })
+ .collect()
})
}
@@ -2067,6 +2084,7 @@ impl Buffer {
before_edit,
entries,
is_block_mode: matches!(mode, AutoindentMode::Block { .. }),
+ ignore_empty_lines: false,
}));
}
@@ -2094,6 +2112,30 @@ impl Buffer {
cx.notify();
}
+ pub fn autoindent_ranges<I, T>(&mut self, ranges: I, cx: &mut ModelContext<Self>)
+ where
+ I: IntoIterator<Item = Range<T>>,
+ T: ToOffset + Copy,
+ {
+ let before_edit = self.snapshot();
+ let entries = ranges
+ .into_iter()
+ .map(|range| AutoindentRequestEntry {
+ range: before_edit.anchor_before(range.start)..before_edit.anchor_after(range.end),
+ first_line_is_new: true,
+ indent_size: before_edit.language_indent_size_at(range.start, cx),
+ original_indent_column: None,
+ })
+ .collect();
+ self.autoindent_requests.push(Arc::new(AutoindentRequest {
+ before_edit,
+ entries,
+ is_block_mode: false,
+ ignore_empty_lines: true,
+ }));
+ self.request_autoindent(cx);
+ }
+
// Inserts newlines at the given position to create an empty line, returning the start of the new line.
// You can also request the insertion of empty lines above and below the line starting at the returned point.
pub fn insert_empty_line(
@@ -325,6 +325,13 @@ struct ExcerptBytes<'a> {
reversed: bool,
}
+struct BufferEdit {
+ range: Range<usize>,
+ new_text: Arc<str>,
+ is_insertion: bool,
+ original_indent_column: u32,
+}
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ExpandExcerptDirection {
Up,
@@ -525,57 +532,146 @@ impl MultiBuffer {
pub fn edit<I, S, T>(
&self,
edits: I,
- mut autoindent_mode: Option<AutoindentMode>,
+ autoindent_mode: Option<AutoindentMode>,
cx: &mut ModelContext<Self>,
) where
I: IntoIterator<Item = (Range<S>, T)>,
S: ToOffset,
T: Into<Arc<str>>,
{
- if self.read_only() {
- return;
- }
- if self.buffers.borrow().is_empty() {
- return;
- }
-
let snapshot = self.read(cx);
- let edits = edits.into_iter().map(|(range, new_text)| {
- let mut range = range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot);
- if range.start > range.end {
- mem::swap(&mut range.start, &mut range.end);
+ let edits = edits
+ .into_iter()
+ .map(|(range, new_text)| {
+ let mut range = range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot);
+ if range.start > range.end {
+ mem::swap(&mut range.start, &mut range.end);
+ }
+ (range, new_text.into())
+ })
+ .collect::<Vec<_>>();
+
+ return edit_internal(self, snapshot, edits, autoindent_mode, cx);
+
+ // Non-generic part of edit, hoisted out to avoid blowing up LLVM IR.
+ fn edit_internal(
+ this: &MultiBuffer,
+ snapshot: Ref<MultiBufferSnapshot>,
+ edits: Vec<(Range<usize>, Arc<str>)>,
+ mut autoindent_mode: Option<AutoindentMode>,
+ cx: &mut ModelContext<MultiBuffer>,
+ ) {
+ if this.read_only() || this.buffers.borrow().is_empty() {
+ return;
+ }
+
+ if let Some(buffer) = this.as_singleton() {
+ buffer.update(cx, |buffer, cx| {
+ buffer.edit(edits, autoindent_mode, cx);
+ });
+ cx.emit(Event::ExcerptsEdited {
+ ids: this.excerpt_ids(),
+ });
+ return;
+ }
+
+ let original_indent_columns = match &mut autoindent_mode {
+ Some(AutoindentMode::Block {
+ original_indent_columns,
+ }) => mem::take(original_indent_columns),
+ _ => Default::default(),
+ };
+
+ let (buffer_edits, edited_excerpt_ids) =
+ this.convert_edits_to_buffer_edits(edits, &snapshot, &original_indent_columns);
+ drop(snapshot);
+
+ for (buffer_id, mut edits) in buffer_edits {
+ edits.sort_unstable_by_key(|edit| edit.range.start);
+ this.buffers.borrow()[&buffer_id]
+ .buffer
+ .update(cx, |buffer, cx| {
+ let mut edits = edits.into_iter().peekable();
+ let mut insertions = Vec::new();
+ let mut original_indent_columns = Vec::new();
+ let mut deletions = Vec::new();
+ let empty_str: Arc<str> = Arc::default();
+ while let Some(BufferEdit {
+ mut range,
+ new_text,
+ mut is_insertion,
+ original_indent_column,
+ }) = edits.next()
+ {
+ while let Some(BufferEdit {
+ range: next_range,
+ is_insertion: next_is_insertion,
+ ..
+ }) = edits.peek()
+ {
+ if range.end >= next_range.start {
+ range.end = cmp::max(next_range.end, range.end);
+ is_insertion |= *next_is_insertion;
+ edits.next();
+ } else {
+ break;
+ }
+ }
+
+ if is_insertion {
+ original_indent_columns.push(original_indent_column);
+ insertions.push((
+ buffer.anchor_before(range.start)
+ ..buffer.anchor_before(range.end),
+ new_text.clone(),
+ ));
+ } else if !range.is_empty() {
+ deletions.push((
+ buffer.anchor_before(range.start)
+ ..buffer.anchor_before(range.end),
+ empty_str.clone(),
+ ));
+ }
+ }
+
+ let deletion_autoindent_mode =
+ if let Some(AutoindentMode::Block { .. }) = autoindent_mode {
+ Some(AutoindentMode::Block {
+ original_indent_columns: Default::default(),
+ })
+ } else {
+ autoindent_mode.clone()
+ };
+ let insertion_autoindent_mode =
+ if let Some(AutoindentMode::Block { .. }) = autoindent_mode {
+ Some(AutoindentMode::Block {
+ original_indent_columns,
+ })
+ } else {
+ autoindent_mode.clone()
+ };
+
+ buffer.edit(deletions, deletion_autoindent_mode, cx);
+ buffer.edit(insertions, insertion_autoindent_mode, cx);
+ })
}
- (range, new_text)
- });
- if let Some(buffer) = self.as_singleton() {
- buffer.update(cx, |buffer, cx| {
- buffer.edit(edits, autoindent_mode, cx);
- });
cx.emit(Event::ExcerptsEdited {
- ids: self.excerpt_ids(),
+ ids: edited_excerpt_ids,
});
- return;
}
+ }
- let original_indent_columns = match &mut autoindent_mode {
- Some(AutoindentMode::Block {
- original_indent_columns,
- }) => mem::take(original_indent_columns),
- _ => Default::default(),
- };
-
- struct BufferEdit {
- range: Range<usize>,
- new_text: Arc<str>,
- is_insertion: bool,
- original_indent_column: u32,
- }
+ fn convert_edits_to_buffer_edits(
+ &self,
+ edits: Vec<(Range<usize>, Arc<str>)>,
+ snapshot: &MultiBufferSnapshot,
+ original_indent_columns: &[u32],
+ ) -> (HashMap<BufferId, Vec<BufferEdit>>, Vec<ExcerptId>) {
let mut buffer_edits: HashMap<BufferId, Vec<BufferEdit>> = Default::default();
let mut edited_excerpt_ids = Vec::new();
let mut cursor = snapshot.excerpts.cursor::<usize>(&());
- for (ix, (range, new_text)) in edits.enumerate() {
- let new_text: Arc<str> = new_text.into();
+ for (ix, (range, new_text)) in edits.into_iter().enumerate() {
let original_indent_column = original_indent_columns.get(ix).copied().unwrap_or(0);
cursor.seek(&range.start, Bias::Right, &());
if cursor.item().is_none() && range.start == *cursor.start() {
@@ -667,84 +763,71 @@ impl MultiBuffer {
}
}
}
+ (buffer_edits, edited_excerpt_ids)
+ }
- drop(cursor);
- drop(snapshot);
- // Non-generic part of edit, hoisted out to avoid blowing up LLVM IR.
- fn tail(
+ pub fn autoindent_ranges<I, S>(&self, ranges: I, cx: &mut ModelContext<Self>)
+ where
+ I: IntoIterator<Item = Range<S>>,
+ S: ToOffset,
+ {
+ let snapshot = self.read(cx);
+ let empty = Arc::<str>::from("");
+ let edits = ranges
+ .into_iter()
+ .map(|range| {
+ let mut range = range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot);
+ if range.start > range.end {
+ mem::swap(&mut range.start, &mut range.end);
+ }
+ (range, empty.clone())
+ })
+ .collect::<Vec<_>>();
+
+ return autoindent_ranges_internal(self, snapshot, edits, cx);
+
+ fn autoindent_ranges_internal(
this: &MultiBuffer,
- buffer_edits: HashMap<BufferId, Vec<BufferEdit>>,
- autoindent_mode: Option<AutoindentMode>,
- edited_excerpt_ids: Vec<ExcerptId>,
+ snapshot: Ref<MultiBufferSnapshot>,
+ edits: Vec<(Range<usize>, Arc<str>)>,
cx: &mut ModelContext<MultiBuffer>,
) {
+ if this.read_only() || this.buffers.borrow().is_empty() {
+ return;
+ }
+
+ if let Some(buffer) = this.as_singleton() {
+ buffer.update(cx, |buffer, cx| {
+ buffer.autoindent_ranges(edits.into_iter().map(|e| e.0), cx);
+ });
+ cx.emit(Event::ExcerptsEdited {
+ ids: this.excerpt_ids(),
+ });
+ return;
+ }
+
+ let (buffer_edits, edited_excerpt_ids) =
+ this.convert_edits_to_buffer_edits(edits, &snapshot, &[]);
+ drop(snapshot);
+
for (buffer_id, mut edits) in buffer_edits {
edits.sort_unstable_by_key(|edit| edit.range.start);
- this.buffers.borrow()[&buffer_id]
- .buffer
- .update(cx, |buffer, cx| {
- let mut edits = edits.into_iter().peekable();
- let mut insertions = Vec::new();
- let mut original_indent_columns = Vec::new();
- let mut deletions = Vec::new();
- let empty_str: Arc<str> = Arc::default();
- while let Some(BufferEdit {
- mut range,
- new_text,
- mut is_insertion,
- original_indent_column,
- }) = edits.next()
- {
- while let Some(BufferEdit {
- range: next_range,
- is_insertion: next_is_insertion,
- ..
- }) = edits.peek()
- {
- if range.end >= next_range.start {
- range.end = cmp::max(next_range.end, range.end);
- is_insertion |= *next_is_insertion;
- edits.next();
- } else {
- break;
- }
- }
- if is_insertion {
- original_indent_columns.push(original_indent_column);
- insertions.push((
- buffer.anchor_before(range.start)
- ..buffer.anchor_before(range.end),
- new_text.clone(),
- ));
- } else if !range.is_empty() {
- deletions.push((
- buffer.anchor_before(range.start)
- ..buffer.anchor_before(range.end),
- empty_str.clone(),
- ));
- }
+ let mut ranges: Vec<Range<usize>> = Vec::new();
+ for edit in edits {
+ if let Some(last_range) = ranges.last_mut() {
+ if edit.range.start <= last_range.end {
+ last_range.end = last_range.end.max(edit.range.end);
+ continue;
}
+ }
+ ranges.push(edit.range);
+ }
- let deletion_autoindent_mode =
- if let Some(AutoindentMode::Block { .. }) = autoindent_mode {
- Some(AutoindentMode::Block {
- original_indent_columns: Default::default(),
- })
- } else {
- autoindent_mode.clone()
- };
- let insertion_autoindent_mode =
- if let Some(AutoindentMode::Block { .. }) = autoindent_mode {
- Some(AutoindentMode::Block {
- original_indent_columns,
- })
- } else {
- autoindent_mode.clone()
- };
-
- buffer.edit(deletions, deletion_autoindent_mode, cx);
- buffer.edit(insertions, insertion_autoindent_mode, cx);
+ this.buffers.borrow()[&buffer_id]
+ .buffer
+ .update(cx, |buffer, cx| {
+ buffer.autoindent_ranges(ranges, cx);
})
}
@@ -752,7 +835,6 @@ impl MultiBuffer {
ids: edited_excerpt_ids,
});
}
- tail(self, buffer_edits, autoindent_mode, edited_excerpt_ids, cx);
}
// Inserts newlines at the given position to create an empty line, returning the start of the new line.
@@ -9,9 +9,10 @@ use ui::ViewContext;
pub(crate) enum IndentDirection {
In,
Out,
+ Auto,
}
-actions!(vim, [Indent, Outdent,]);
+actions!(vim, [Indent, Outdent, AutoIndent]);
pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
Vim::action(editor, cx, |vim, _: &Indent, cx| {
@@ -49,6 +50,24 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut ViewContext<Vim>) {
vim.switch_mode(Mode::Normal, true, cx)
}
});
+
+ Vim::action(editor, cx, |vim, _: &AutoIndent, cx| {
+ vim.record_current_action(cx);
+ let count = Vim::take_count(cx).unwrap_or(1);
+ vim.store_visual_marks(cx);
+ vim.update_editor(cx, |vim, editor, cx| {
+ editor.transact(cx, |editor, cx| {
+ let original_positions = vim.save_selection_starts(editor, cx);
+ for _ in 0..count {
+ editor.autoindent(&Default::default(), cx);
+ }
+ vim.restore_selection_cursors(editor, cx, original_positions);
+ });
+ });
+ if vim.mode.is_visual() {
+ vim.switch_mode(Mode::Normal, true, cx)
+ }
+ });
}
impl Vim {
@@ -71,10 +90,10 @@ impl Vim {
motion.expand_selection(map, selection, times, false, &text_layout_details);
});
});
- if dir == IndentDirection::In {
- editor.indent(&Default::default(), cx);
- } else {
- editor.outdent(&Default::default(), cx);
+ match dir {
+ IndentDirection::In => editor.indent(&Default::default(), cx),
+ IndentDirection::Out => editor.outdent(&Default::default(), cx),
+ IndentDirection::Auto => editor.autoindent(&Default::default(), cx),
}
editor.change_selections(None, cx, |s| {
s.move_with(|map, selection| {
@@ -104,10 +123,10 @@ impl Vim {
object.expand_selection(map, selection, around);
});
});
- if dir == IndentDirection::In {
- editor.indent(&Default::default(), cx);
- } else {
- editor.outdent(&Default::default(), cx);
+ match dir {
+ IndentDirection::In => editor.indent(&Default::default(), cx),
+ IndentDirection::Out => editor.outdent(&Default::default(), cx),
+ IndentDirection::Auto => editor.autoindent(&Default::default(), cx),
}
editor.change_selections(None, cx, |s| {
s.move_with(|map, selection| {
@@ -122,7 +141,11 @@ impl Vim {
#[cfg(test)]
mod test {
- use crate::test::NeovimBackedTestContext;
+ use crate::{
+ state::Mode,
+ test::{NeovimBackedTestContext, VimTestContext},
+ };
+ use indoc::indoc;
#[gpui::test]
async fn test_indent_gv(cx: &mut gpui::TestAppContext) {
@@ -135,4 +158,46 @@ mod test {
.await
.assert_eq("ยซ hello\n หยป world\n");
}
+
+ #[gpui::test]
+ async fn test_autoindent_op(cx: &mut gpui::TestAppContext) {
+ let mut cx = VimTestContext::new(cx, true).await;
+
+ cx.set_state(
+ indoc!(
+ "
+ fn a() {
+ b();
+ c();
+
+ d();
+ หe();
+ f();
+
+ g();
+ }
+ "
+ ),
+ Mode::Normal,
+ );
+
+ cx.simulate_keystrokes("= a p");
+ cx.assert_state(
+ indoc!(
+ "
+ fn a() {
+ b();
+ c();
+
+ d();
+ หe();
+ f();
+
+ g();
+ }
+ "
+ ),
+ Mode::Normal,
+ );
+ }
}
@@ -170,6 +170,9 @@ impl Vim {
Some(Operator::Indent) => self.indent_motion(motion, times, IndentDirection::In, cx),
Some(Operator::Rewrap) => self.rewrap_motion(motion, times, cx),
Some(Operator::Outdent) => self.indent_motion(motion, times, IndentDirection::Out, cx),
+ Some(Operator::AutoIndent) => {
+ self.indent_motion(motion, times, IndentDirection::Auto, cx)
+ }
Some(Operator::Lowercase) => {
self.change_case_motion(motion, times, CaseTarget::Lowercase, cx)
}
@@ -202,6 +205,9 @@ impl Vim {
Some(Operator::Outdent) => {
self.indent_object(object, around, IndentDirection::Out, cx)
}
+ Some(Operator::AutoIndent) => {
+ self.indent_object(object, around, IndentDirection::Auto, cx)
+ }
Some(Operator::Rewrap) => self.rewrap_object(object, around, cx),
Some(Operator::Lowercase) => {
self.change_case_object(object, around, CaseTarget::Lowercase, cx)
@@ -72,6 +72,7 @@ pub enum Operator {
Jump { line: bool },
Indent,
Outdent,
+ AutoIndent,
Rewrap,
Lowercase,
Uppercase,
@@ -465,6 +466,7 @@ impl Operator {
Operator::Jump { line: true } => "'",
Operator::Jump { line: false } => "`",
Operator::Indent => ">",
+ Operator::AutoIndent => "eq",
Operator::Rewrap => "gq",
Operator::Outdent => "<",
Operator::Uppercase => "gU",
@@ -510,6 +512,7 @@ impl Operator {
| Operator::Rewrap
| Operator::Indent
| Operator::Outdent
+ | Operator::AutoIndent
| Operator::Lowercase
| Operator::Uppercase
| Operator::Object { .. }
@@ -470,6 +470,7 @@ impl Vim {
| Operator::Replace
| Operator::Indent
| Operator::Outdent
+ | Operator::AutoIndent
| Operator::Lowercase
| Operator::Uppercase
| Operator::OppositeCase