Repository: kkawakam/rustyline
Pull request #452 (2020-10-13 12:48:25)

Implement indentation commands
They are currently only attached to vim commands `>` and `<` (as Tab is used for completion).

gwenn: `python-prompt-toolkit` and `jline3` seem to provide this feature.
A conditional binding is needed to make `Tab` key behave correctly.

Patch:
diff --git a/src/config.rs b/src/config.rs
index ff9881f..5db7a8a 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -28,6 +28,8 @@ pub struct Config {
     output_stream: OutputStreamType,
     /// Horizontal space taken by a tab.
     tab_stop: usize,
+    /// Indentation size for indent/dedent commands
+    indent_size: usize,
     /// Check if cursor position is at leftmost before displaying prompt
     check_cursor_position: bool,
 }
@@ -154,6 +156,16 @@ impl Config {
     pub fn check_cursor_position(&self) -> bool {
         self.check_cursor_position
     }
+    /// Indentation size used by indentation commands
+    ///
+    /// By default, 2.
+    pub fn indent_size(&self) -> usize {
+        self.indent_size
+    }
+
+    pub(crate) fn set_indent_size(&mut self, indent_size: usize) {
+        self.indent_size = indent_size;
+    }
 }
 
 impl Default for Config {
@@ -171,6 +183,7 @@ impl Default for Config {
             color_mode: ColorMode::Enabled,
             output_stream: OutputStreamType::Stdout,
             tab_stop: 8,
+            indent_size: 2,
             check_cursor_position: false,
         }
     }
@@ -375,6 +388,14 @@ impl Builder {
         self
     }
 
+    /// Indentation size
+    ///
+    /// By default, `2`
+    pub fn indent_size(mut self, indent_size: usize) -> Self {
+        self.set_indent_size(indent_size);
+        self
+    }
+
     /// Builds a `Config` with the settings specified so far.
     pub fn build(self) -> Config {
         self.p
@@ -476,4 +497,10 @@ pub trait Configurer {
     fn set_check_cursor_position(&mut self, yes: bool) {
         self.config_mut().check_cursor_position = yes;
     }
+    /// Indentation size for indent/dedent commands
+    ///
+    /// By default, `2`
+    fn set_indent_size(&mut self, size: usize) {
+        self.config_mut().set_indent_size(size);
+    }
 }
diff --git a/src/edit.rs b/src/edit.rs
index f4bdfab..18fe287 100644
--- a/src/edit.rs
+++ b/src/edit.rs
@@ -653,6 +653,18 @@ impl<'out, 'prompt, H: Helper> State<'out, 'prompt, H> {
         }
         self.refresh_line()
     }
+
+    /// Change the indentation of the lines covered by movement
+    pub fn edit_indent(&mut self, mvt: &Movement,
+        amount: usize, dedent: bool)
+        -> Result<()>
+    {
+        if self.line.indent(mvt, amount, dedent) {
+            self.refresh_line()
+        } else {
+            Ok(())
+        }
+    }
 }
 
 #[cfg(test)]
diff --git a/src/keymap.rs b/src/keymap.rs
index 1fb1978..6f807c2 100644
--- a/src/keymap.rs
+++ b/src/keymap.rs
@@ -35,6 +35,8 @@ pub enum Cmd {
     CompleteBackward,
     /// complete-hint
     CompleteHint,
+    /// Dedent current line
+    Dedent(Movement),
     /// downcase-word
     DowncaseWord,
     /// vi-eof-maybe
@@ -47,6 +49,8 @@ pub enum Cmd {
     HistorySearchBackward,
     /// history-search-forward
     HistorySearchForward,
+    /// Indent current line
+    Indent(Movement),
     /// Insert text
     Insert(RepeatCount, String),
     /// Interrupt signal (Ctrl-C)
@@ -138,7 +142,10 @@ impl Cmd {
     }
 
     fn is_repeatable_change(&self) -> bool {
-        matches!(*self, Cmd::Insert(..)
+        matches!(*self,
+          Cmd::Dedent(..)
+        | Cmd::Indent(..)
+        | Cmd::Insert(..)
         | Cmd::Kill(_)
         | Cmd::ReplaceChar(..)
         | Cmd::Replace(..)
@@ -159,6 +166,8 @@ impl Cmd {
     // Replay this command with a possible different `RepeatCount`.
     fn redo(&self, new: Option<RepeatCount>, wrt: &dyn Refresher) -> Self {
         match *self {
+            Cmd::Dedent(ref mvt) => Cmd::Dedent(mvt.redo(new)),
+            Cmd::Indent(ref mvt) => Cmd::Indent(mvt.redo(new)),
             Cmd::Insert(previous, ref text) => {
                 Cmd::Insert(repeat_count(previous, new), text.clone())
             }
@@ -767,6 +776,14 @@ impl InputState {
                 self.input_mode = InputMode::Insert; // TODO Validate
                 Cmd::ForwardSearchHistory
             }
+            E(K::Char('<'), M::NONE) => match self.vi_cmd_motion(rdr, wrt, key, n)? {
+                Some(mvt) => Cmd::Dedent(mvt),
+                None => Cmd::Unknown,
+            },
+            E(K::Char('>'), M::NONE) => match self.vi_cmd_motion(rdr, wrt, key, n)? {
+                Some(mvt) => Cmd::Indent(mvt),
+                None => Cmd::Unknown,
+            },
             E::ESC => Cmd::Noop,
             _ => self.common(rdr, key, n, true)?,
         };
diff --git a/src/lib.rs b/src/lib.rs
index a4e98f9..c7304fa 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -684,6 +684,12 @@ fn readline_edit<H: Helper>(
                     s.refresh_line()?;
                 }
             }
+            Cmd::Dedent(mvt) => {
+                s.edit_indent(&mvt, editor.config.indent_size(), true)?;
+            }
+            Cmd::Indent(mvt) => {
+                s.edit_indent(&mvt, editor.config.indent_size(), false)?;
+            }
             Cmd::Interrupt => {
                 // Move to end, in case cursor was in the middle of the
                 // line, so that next thing application prints goes after
diff --git a/src/line_buffer.rs b/src/line_buffer.rs
index b2f35ba..a020b4d 100644
--- a/src/line_buffer.rs
+++ b/src/line_buffer.rs
@@ -1,6 +1,7 @@
 //! Line buffer with current cursor position
 use crate::keymap::{At, CharSearch, Movement, RepeatCount, Word};
 use std::cell::RefCell;
+use std::cmp::min;
 use std::fmt;
 use std::iter;
 use std::ops::{Deref, Index, Range};
@@ -11,6 +12,7 @@ use unicode_segmentation::UnicodeSegmentation;
 
 /// Default maximum buffer size for the line read
 pub(crate) const MAX_LINE: usize = 4096;
+pub(crate) const INDENT: &str = "                                ";
 
 /// Word's case change
 #[derive(Clone, Copy)]
@@ -1097,6 +1099,78 @@ impl LineBuffer {
         }
         killed
     }
+
+    /// Indent range specified by `mvt`.
+    pub fn indent(&mut self, mvt: &Movement, amount: usize, dedent: bool)
+        -> bool
+    {
+        let pair = match *mvt {
+            // All inline operators are the same: indent current line
+            | Movement::WholeLine
+            | Movement::BeginningOfLine
+            | Movement::ViFirstPrint
+            | Movement::EndOfLine
+            | Movement::BackwardChar(..)
+            | Movement::ForwardChar(..)
+            | Movement::ViCharSearch(..)
+            => {
+                Some((self.pos, self.pos))
+            }
+            Movement::EndOfBuffer => {
+                Some((self.pos, self.buf.len()))
+            }
+            Movement::WholeBuffer => {
+                Some((0, self.buf.len()))
+            }
+            Movement::BeginningOfBuffer => {
+                Some((0, self.pos))
+            }
+            Movement::BackwardWord(n, word_def) => {
+                self.prev_word_pos(self.pos, word_def, n)
+                    .map(|pos| (pos, self.pos))
+            }
+            Movement::ForwardWord(n, at, word_def) => {
+                self.next_word_pos(self.pos, at, word_def, n)
+                    .map(|pos| (self.pos, pos))
+            }
+            Movement::LineUp(n) => self.n_lines_up(n),
+            Movement::LineDown(n) => self.n_lines_down(n),
+        };
+        let (start, end) = pair.unwrap_or((self.pos, self.pos));
+        let start = self.buf[..start].rfind('\n')
+            .map(|pos| pos + 1).unwrap_or(0);
+        let end = self.buf[end..].rfind('\n')
+            .map(|pos| end + pos).unwrap_or(self.buf.len());
+        let mut index = start;
+        if dedent {
+            for line in self.buf[start..end].to_string().split('\n') {
+                let max = line.len() - line.trim_start().len();
+                let deleting = min(max, amount);
+                self.drain(index..index+deleting, Default::default());
+                if self.pos >= index {
+                    if self.pos.saturating_sub(index) < deleting {
+                        // don't wrap into the previous line
+                        self.pos = index;
+                    } else {
+                        self.pos -= deleting;
+                    }
+                }
+                index += line.len() + 1 - deleting;
+            }
+        } else {
+            for line in self.buf[start..end].to_string().split('\n') {
+                for off in (0..amount).step_by(INDENT.len()) {
+                    self.insert_str(index,
+                        &INDENT[..min(amount - off, INDENT.len())]);
+                }
+                if self.pos >= index {
+                    self.pos += amount;
+                }
+                index += amount + line.len() + 1;
+            }
+        }
+        return true;
+    }
 }
 
 impl Deref for LineBuffer {


Test patch:
diff --git a/src/test/vi_cmd.rs b/src/test/vi_cmd.rs
index ec5234d..eac3221 100644
--- a/src/test/vi_cmd.rs
+++ b/src/test/vi_cmd.rs
@@ -504,3 +504,80 @@ fn uppercase_t() {
         ("Hel", "lo, world!"),
     );
 }
+
+#[test]
+fn indent() {
+    assert_cursor(
+        EditMode::Vi,
+        ("Hello, world!", ""),
+        &[E::ESC, E::from('>'), E::from('>'), E::ENTER],
+        ("  Hello, world", "!"),  // Esc moves to the left
+    );
+    assert_cursor(
+        EditMode::Vi,
+        ("line1\nline2", ""),
+        &[E::ESC, E::from('>'), E::from('>'), E::ENTER],
+        ("line1\n  line", "2"),  // Esc moves to the left
+    );
+    assert_cursor(
+        EditMode::Vi,
+        ("line1\nline2", ""),
+        &[E::ESC, E::from('>'), E::from('k'), E::ENTER],
+        ("  line1\n  line", "2"),  // Esc moves to the left
+    );
+    assert_cursor(
+        EditMode::Vi,
+        ("  li", "ne1\n  line2",),
+        &[E::ESC, E::from('>'), E::from('j'), E::ENTER],
+        ("    l", "ine1\n    line2"),  // Esc moves to the left
+    );
+    assert_cursor(
+        EditMode::Vi,
+        ("  ", "line1\n  line2",),
+        &[E::ESC, E::from('>'), E::from('j'), E::ENTER],
+        ("   ", " line1\n    line2"),  // Esc moves to the left
+    );
+    assert_cursor(
+        EditMode::Vi,
+        ("  ", "line1\n  line2",),
+        &[E::ESC, E::from('>'), E::from('j'), E::ENTER],
+        ("   ", " line1\n    line2"),  // Esc moves to the left
+    );
+}
+
+#[test]
+fn dedent() {
+    assert_cursor(
+        EditMode::Vi,
+        ("  line1\n  line2", ""),
+        &[E::ESC, E::from('<'), E::from('<'), E::ENTER],
+        ("  line1\nline", "2"),
+    );
+
+    assert_cursor(
+        EditMode::Vi,
+        ("  line1\n  line2", ""),
+        &[E::ESC, E::from('<'), E::from('k'), E::ENTER],
+        ("line1\nline", "2"),
+    );
+
+    assert_cursor(
+        EditMode::Vi,
+        ("  li", "ne1\n  line2", ),
+        &[E::ESC, E::from('<'), E::from('j'), E::ENTER],
+        ("l", "ine1\nline2"),
+    );
+
+    assert_cursor(
+        EditMode::Vi,
+        ("  ", "line1\n  line2"),
+        &[E::ESC, E::from('<'), E::from('j'), E::ENTER],
+        ("", "line1\nline2"),
+    );
+    assert_cursor(
+        EditMode::Vi,
+        ("line", "1\n  line2"),
+        &[E::ESC, E::from('<'), E::from('j'), E::ENTER],
+        ("lin", "e1\nline2"),
+    );
+}