diff --git a/PSReadLine/BasicEditing.cs b/PSReadLine/BasicEditing.cs
index 21a26a508..7ab74e78e 100644
--- a/PSReadLine/BasicEditing.cs
+++ b/PSReadLine/BasicEditing.cs
@@ -121,7 +121,7 @@ public static void BackwardDeleteLine(ConsoleKeyInfo? key = null, object arg = n
{
if (_singleton._current > 0)
{
- _singleton._clipboard = _singleton._buffer.ToString(0, _singleton._current);
+ _singleton._clipboard.Record(_singleton._buffer, 0, _singleton._current);
_singleton.SaveEditItem(EditItemDelete.Create(_singleton._clipboard, 0));
_singleton._buffer.Remove(0, _singleton._current);
_singleton._current = 0;
diff --git a/PSReadLine/Position.cs b/PSReadLine/Position.cs
index 4beb03259..3a8585445 100644
--- a/PSReadLine/Position.cs
+++ b/PSReadLine/Position.cs
@@ -39,8 +39,7 @@ private static int GetBeginningOfLinePos(int current)
/// Returns the position of the end of the logical line
/// as specified by the "current" position.
///
- ///
- ///
+ ///
private static int GetEndOfLogicalLinePos(int current)
{
var newCurrent = current;
diff --git a/PSReadLine/ReadLine.vi.cs b/PSReadLine/ReadLine.vi.cs
index 78c2ec641..05e0dbf30 100644
--- a/PSReadLine/ReadLine.vi.cs
+++ b/PSReadLine/ReadLine.vi.cs
@@ -221,7 +221,7 @@ public static void DeleteToEnd(ConsoleKeyInfo? key = null, object arg = null)
return;
}
- _singleton._clipboard = _singleton._buffer.ToString(_singleton._current, _singleton._buffer.Length - _singleton._current);
+ _singleton._clipboard.Record(_singleton._buffer, _singleton._current, _singleton._buffer.Length - _singleton._current);
_singleton.SaveEditItem(EditItemDelete.Create(
_singleton._clipboard,
_singleton._current,
@@ -723,7 +723,7 @@ public static void DeleteLineToFirstChar(ConsoleKeyInfo? key = null, object arg
///
public static void DeleteLine(ConsoleKeyInfo? key = null, object arg = null)
{
- _singleton._clipboard = _singleton._buffer.ToString();
+ _singleton._clipboard.Record(_singleton._buffer);
_singleton.SaveEditItem(EditItemDelete.Create(_singleton._clipboard, 0));
_singleton._current = 0;
_singleton._buffer.Remove(0, _singleton._buffer.Length);
@@ -746,7 +746,7 @@ public static void BackwardDeleteWord(ConsoleKeyInfo? key = null, object arg = n
Ding();
return;
}
- _singleton._clipboard = _singleton._buffer.ToString(deletePoint, _singleton._current - deletePoint);
+ _singleton._clipboard.Record(_singleton._buffer, deletePoint, _singleton._current - deletePoint);
_singleton.SaveEditItem(EditItemDelete.Create(
_singleton._clipboard,
deletePoint,
@@ -779,7 +779,7 @@ public static void ViBackwardDeleteGlob(ConsoleKeyInfo? key = null, object arg =
Ding();
return;
}
- _singleton._clipboard = _singleton._buffer.ToString(deletePoint, _singleton._current - deletePoint);
+ _singleton._clipboard.Record(_singleton._buffer, deletePoint, _singleton._current - deletePoint);
_singleton.SaveEditItem(EditItemDelete.Create(
_singleton._clipboard,
deletePoint,
diff --git a/PSReadLine/StringBuilderLinewiseExtensions.cs b/PSReadLine/StringBuilderLinewiseExtensions.cs
new file mode 100644
index 000000000..2e141e1a8
--- /dev/null
+++ b/PSReadLine/StringBuilderLinewiseExtensions.cs
@@ -0,0 +1,61 @@
+using System;
+using System.Text;
+
+namespace Microsoft.PowerShell
+{
+ public class Range
+ {
+ public int Offset { get; set; }
+ public int Count { get; set; }
+ }
+
+ public static class StringBuilderLinewiseExtensions
+ {
+ ///
+ /// Determines the offset and the length of the fragment
+ /// in the specified buffer that corresponds to a
+ /// given number of lines starting from the specified line index
+ ///
+ ///
+ ///
+ ///
+ public static Range GetRange(this StringBuilder buffer, int lineIndex, int lineCount)
+ {
+ var length = buffer.Length;
+
+ var startPosition = 0;
+ var startPositionIdentified = false;
+
+ var endPosition = length - 1;
+ var endPositionIdentified = false;
+
+ var currentLine = 0;
+
+ for (var position = 0; position < length; position++)
+ {
+ if (currentLine == lineIndex && !startPositionIdentified)
+ {
+ startPosition = position;
+ startPositionIdentified = true;
+ }
+
+ if (buffer[position] == '\n')
+ {
+ currentLine++;
+ }
+
+ if (currentLine == lineIndex + lineCount && !endPositionIdentified)
+ {
+ endPosition = position;
+ endPositionIdentified = true;
+ }
+ }
+
+ return new Range
+ {
+ Offset = startPosition,
+ Count = endPosition - startPosition + 1,
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/PSReadLine/UndoRedo.cs b/PSReadLine/UndoRedo.cs
index d1d62e890..ed00003a2 100644
--- a/PSReadLine/UndoRedo.cs
+++ b/PSReadLine/UndoRedo.cs
@@ -148,16 +148,18 @@ class EditItemInsertString : EditItem
{
// The string inserted tells us the length to delete on undo.
// The contents of the string are only needed for redo.
- private string _insertedString;
- private int _insertStartPosition;
+ private readonly string _insertedString;
+ private readonly int _insertStartPosition;
+
+ protected EditItemInsertString(string str, int position)
+ {
+ _insertedString = str;
+ _insertStartPosition = position;
+ }
public static EditItem Create(string str, int position)
{
- return new EditItemInsertString
- {
- _insertedString = str,
- _insertStartPosition = position
- };
+ return new EditItemInsertString(str, position);
}
public override void Undo()
@@ -175,6 +177,32 @@ public override void Redo()
}
}
+ [DebuggerDisplay("Insert '{_insertedString}' ({_insertStartPosition}, Anchor: {_insertAnchor})")]
+ class EditItemInsertLines : EditItemInsertString
+ {
+ // in linewise pastes, the _insertAnchor represents the position
+ // of the cursor at the time paste was invoked. This is recorded
+ // so as to be restored when undoing the paste.
+ private readonly int _insertAnchor;
+
+ private EditItemInsertLines(string str, int position, int anchor)
+ :base(str, position)
+ {
+ _insertAnchor = anchor;
+ }
+
+ public static EditItem Create(string str, int position, int anchor)
+ {
+ return new EditItemInsertLines(str, position, anchor);
+ }
+
+ public override void Undo()
+ {
+ base.Undo();
+ _singleton._current = _insertAnchor;
+ }
+ }
+
[DebuggerDisplay("Delete '{_deletedString}' ({_deleteStartPosition})")]
class EditItemDelete : EditItem
{
diff --git a/PSReadLine/ViRegister.cs b/PSReadLine/ViRegister.cs
new file mode 100644
index 000000000..2a26e5964
--- /dev/null
+++ b/PSReadLine/ViRegister.cs
@@ -0,0 +1,232 @@
+using System;
+using System.Text;
+
+namespace Microsoft.PowerShell
+{
+ public partial class PSConsoleReadLine
+ {
+ ///
+ /// Used to report when the buffer is about to change
+ /// as part of a paste operation.
+ ///
+ public sealed class PasteEventArgs : EventArgs
+ {
+ ///
+ /// The text being pasted.
+ ///
+ public string Text { get; set; }
+ ///
+ /// The position in the buffer at
+ /// which the pasted text will be inserted.
+ ///
+ public int Position { get; set; }
+ ///
+ /// The recorded position in the buffer
+ /// from which the paste operation originates.
+ /// This is usually the same as Position, but
+ /// not always. For instance, when pasting a
+ /// linewise selection before the current line,
+ /// the Anchor is the cursor position, whereas
+ /// the Position is the beginning of the previous line.
+ ///
+ public int Anchor { get; set; }
+ }
+
+ ///
+ /// Represents a named register.
+ ///
+ public sealed class ViRegister
+ {
+ private string _text;
+ private bool _linewise;
+
+ ///
+ /// Raised when the text from this register is about
+ /// to be pasted to the buffer specified as part of
+ /// a call to Paste[After|Before].
+ ///
+ public event EventHandler OnInserting;
+
+ ///
+ /// Returns whether this register is empty.
+ ///
+ public bool IsEmpty
+ => String.IsNullOrEmpty(_text);
+
+ ///
+ /// Returns whether this register contains
+ /// linewise yanked text.
+ ///
+ public bool Linewise
+ => _linewise;
+
+ public string RawText
+ => _text;
+
+ ///
+ /// Records the entire buffer in the register.
+ ///
+ ///
+ public void Record(StringBuilder buffer)
+ {
+ Record(buffer, 0, buffer.Length);
+ }
+
+ ///
+ /// Records a piece of text in the register.
+ ///
+ ///
+ ///
+ ///
+ public void Record(StringBuilder buffer, int offset, int count)
+ {
+ System.Diagnostics.Debug.Assert(offset >= 0 && offset < buffer.Length);
+ System.Diagnostics.Debug.Assert(offset + count <= buffer.Length);
+
+ _linewise = false;
+ _text = buffer.ToString(offset, count);
+ }
+
+ ///
+ /// Records a block of lines in the register.
+ ///
+ ///
+ public void LinewizeRecord(string text)
+ {
+ _linewise = true;
+ _text = text;
+ }
+
+ // for compatibility reasons, as an interim solution
+ public static implicit operator string(ViRegister register)
+ {
+ return register._text;
+ }
+
+ public int PasteAfter(StringBuilder buffer, int position)
+ {
+ if (IsEmpty)
+ {
+ return position;
+ }
+
+ if (_linewise)
+ {
+ var text = _text;
+
+ // paste text after the next line
+
+ var pastePosition = -1;
+ var newCursorPosition = position;
+
+ for (var index = position; index < buffer.Length; index++)
+ {
+ if (buffer[index] == '\n')
+ {
+ pastePosition = index + 1;
+ newCursorPosition = pastePosition;
+ break;
+ }
+ }
+
+ if (pastePosition == -1)
+ {
+ if (text[0] != '\n')
+ {
+ text = '\n' + text;
+ }
+
+ pastePosition = buffer.Length;
+ newCursorPosition = pastePosition + 1;
+ }
+
+ InsertAt(buffer, text, pastePosition, position);
+
+ return newCursorPosition;
+ }
+
+ else
+ {
+ if (position < buffer.Length)
+ {
+ position += 1;
+ }
+
+ InsertAt(buffer, _text, position, position);
+ position += _text.Length - 1;
+
+ return position;
+ }
+ }
+
+ public int PasteBefore(StringBuilder buffer, int position)
+ {
+ if (_linewise)
+ {
+ var text = _text;
+
+ if (text[text.Length - 1] != '\n')
+ {
+ text += '\n';
+ }
+
+ // paste text before the current line
+
+ var previousLinePos = -1;
+
+ for (var index = position; index > 0; index--)
+ {
+ if (buffer[index] == '\n')
+ {
+ previousLinePos = index + 1;
+ break;
+ }
+ }
+
+ if (previousLinePos == -1)
+ {
+ previousLinePos = 0;
+ }
+
+ InsertBefore(buffer, text, previousLinePos, position);
+
+ return previousLinePos;
+ }
+ else
+ {
+ InsertAt(buffer, _text, position, position);
+ return position + _text.Length - 1;
+ }
+ }
+
+ private void InsertBefore(StringBuilder buffer, string text, int pastePosition, int position)
+ {
+ OnInserting?.Invoke(this, new PasteEventArgs { Text = text, Position = pastePosition, Anchor = position, });
+ buffer.Insert(pastePosition, text);
+ }
+
+ private void InsertAt(StringBuilder buffer, string text, int pastePosition, int position)
+ {
+ OnInserting?.Invoke(this, new PasteEventArgs { Text = text, Position = pastePosition, Anchor = position, });
+
+ // Use Append if possible because Insert at end makes StringBuilder quite slow.
+ if (pastePosition == buffer.Length)
+ {
+ buffer.Append(text);
+ }
+ else
+ {
+ buffer.Insert(pastePosition, text);
+ }
+ }
+
+#if DEBUG
+ public override string ToString()
+ {
+ var text = _text.Replace("\n", "\\n");
+ return (_linewise ? "line: " : "") + "\"" + text + "\"";
+ }
+#endif
+ }
+ }
+}
\ No newline at end of file
diff --git a/PSReadLine/YankPaste.vi.cs b/PSReadLine/YankPaste.vi.cs
index 0d3a86cf2..272fb6b8b 100644
--- a/PSReadLine/YankPaste.vi.cs
+++ b/PSReadLine/YankPaste.vi.cs
@@ -3,24 +3,42 @@
--********************************************************************/
using System;
+using System.Text;
namespace Microsoft.PowerShell
{
public partial class PSConsoleReadLine
{
- private string _clipboard = string.Empty;
+ private readonly ViRegister _clipboard = InitializeClipboard();
+
+ internal static ViRegister InitializeClipboard()
+ {
+ // the register's PasteBefore and PasteAfter
+ // methods adjust the cursor position in the buffer
+ // before inserted its content.
+
+ // use this hook to support undo
+
+ var register = new ViRegister();
+ register.OnInserting += (sender, e) =>
+ {
+ var editItem = EditItemInsertLines.Create(
+ e.Text,
+ e.Position,
+ e.Anchor
+ );
+
+ _singleton.SaveEditItem(editItem);
+ };
+
+ return register;
+ }
///
/// Paste the clipboard after the cursor, moving the cursor to the end of the pasted text.
///
public static void PasteAfter(ConsoleKeyInfo? key = null, object arg = null)
{
- if (string.IsNullOrEmpty(_singleton._clipboard))
- {
- Ding();
- return;
- }
-
_singleton.PasteAfterImpl();
}
@@ -29,43 +47,63 @@ public static void PasteAfter(ConsoleKeyInfo? key = null, object arg = null)
///
public static void PasteBefore(ConsoleKeyInfo? key = null, object arg = null)
{
- if (string.IsNullOrEmpty(_singleton._clipboard))
- {
- Ding();
- return;
- }
_singleton.PasteBeforeImpl();
}
private void PasteAfterImpl()
{
- if (_current < _buffer.Length)
+ if (_clipboard.IsEmpty)
{
- _current++;
+ Ding();
+ return;
}
- Insert(_clipboard);
- _current--;
+
+ _current = _clipboard.PasteAfter(_buffer, _current);
Render();
}
private void PasteBeforeImpl()
- {
- Insert(_clipboard);
- _current--;
+ {
+ if (_clipboard.IsEmpty)
+ {
+ Ding();
+ return;
+ }
+
+ if (_clipboard.Linewise)
+ _current = _clipboard.PasteBefore(_buffer, _current);
+ else
+ {
+ _current = _clipboard.PasteBefore(_buffer, _current);
+ }
Render();
}
private void SaveToClipboard(int startIndex, int length)
{
- _clipboard = _buffer.ToString(startIndex, length);
+ _clipboard.Record(_buffer, startIndex, length);
+ }
+
+ ///
+ /// Saves a number of logical lines in the unnamed register
+ /// starting at the specified line number and specified count.
+ ///
+ /// The logical number of the current line, starting at 0.
+ /// The number of lines to record to the unnamed register
+ private void SaveLinesToClipboard(int lineIndex, int lineCount)
+ {
+ var range = _buffer.GetRange(lineIndex, lineCount);
+ _clipboard.LinewizeRecord(_buffer.ToString(range.Offset, range.Count));
}
///
/// Yank the entire buffer.
///
public static void ViYankLine(ConsoleKeyInfo? key = null, object arg = null)
- {
- _singleton.SaveToClipboard(0, _singleton._buffer.Length);
+ {
+ TryGetArgAsInt(arg, out var lineCount, 1);
+ var lineIndex = _singleton.GetLogicalLineNumber() - 1;
+ _singleton.SaveLinesToClipboard(lineIndex, lineCount);
}
///
diff --git a/test/StringBuilderLinewiseExtensionsTests.cs b/test/StringBuilderLinewiseExtensionsTests.cs
new file mode 100644
index 000000000..9423ff1c8
--- /dev/null
+++ b/test/StringBuilderLinewiseExtensionsTests.cs
@@ -0,0 +1,54 @@
+using System.Text;
+using Microsoft.PowerShell;
+using Xunit;
+
+namespace Test
+{
+ public sealed class StringBuilderLinewiseExtensionsTests
+ {
+ [Fact]
+ public void StringBuilderLinewiseExtensions_LinewiseYank_Fragment()
+ {
+ var buffer = new StringBuilder("line1\nline2");
+
+ // system under test
+
+ var range = buffer.GetRange(1, 42);
+
+ // assert
+
+ Assert.Equal(6, range.Offset);
+ Assert.Equal(5, range.Count);
+ }
+
+ [Fact]
+ public void StringBuilderLinewiseExtensions_LinewiseYank_Line()
+ {
+ var buffer = new StringBuilder("line1\nline2");
+
+ // system under test
+
+ var range = buffer.GetRange(1, 42);
+
+ // assert
+
+ Assert.Equal(6, range.Offset);
+ Assert.Equal(5, range.Count);
+ }
+
+ [Fact]
+ public void StringBuilderLinewiseExtensions_LinewiseYank_Lines()
+ {
+ var buffer = new StringBuilder("line1\nline2\nline3\nline4");
+
+ // system under test
+
+ var range = buffer.GetRange(1, 2);
+
+ // assert
+
+ Assert.Equal(6, range.Offset);
+ Assert.Equal(12, range.Count);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/ViRegisterTests.cs b/test/ViRegisterTests.cs
new file mode 100644
index 000000000..36d13ff68
--- /dev/null
+++ b/test/ViRegisterTests.cs
@@ -0,0 +1,140 @@
+using System.Text;
+using Microsoft.PowerShell;
+using Xunit;
+
+namespace Test
+{
+ public sealed class ViRegisterTests
+ {
+ #region Linewize paste
+
+ [Fact]
+ public void ViRegister_Fragment_LinewizePasteBefore()
+ {
+ const string yanked = "line1";
+
+ var register = new PSConsoleReadLine.ViRegister();
+ register.LinewizeRecord(yanked);
+
+ // system under test
+
+ var buffer = new StringBuilder("line2");
+ const int position = 2;
+
+ var newPosition = register.PasteBefore(buffer, position);
+
+ // assert expectations
+
+ Assert.Equal("line1\nline2", buffer.ToString());
+ Assert.Equal(0, newPosition);
+ }
+
+ [Fact]
+ public void ViRegister_Lines_LinewizePasteBefore()
+ {
+ const string yanked = "line1\n";
+
+ var register = new PSConsoleReadLine.ViRegister();
+ register.LinewizeRecord(yanked);
+
+ // system under test
+
+ var buffer = new StringBuilder("line2");
+ const int position = 2;
+
+ var newPosition = register.PasteBefore(buffer, position);
+
+ // assert expectations
+
+ Assert.Equal("line1\nline2", buffer.ToString());
+ Assert.Equal(0, newPosition);
+ }
+
+
+ [Fact]
+ public void ViRegister_Fragment_LinewizePasteAfter_Fragment()
+ {
+ const string yanked = "line2";
+
+ var register = new PSConsoleReadLine.ViRegister();
+ register.LinewizeRecord(yanked);
+
+ // system under test
+
+ var buffer = new StringBuilder("line1");
+ const int position = 2;
+
+ var newPosition = register.PasteAfter(buffer, position);
+
+ // assert expectations
+
+ Assert.Equal("line1\nline2", buffer.ToString());
+ Assert.Equal(6, newPosition);
+ }
+
+ [Fact]
+ public void ViRegister_Fragment_LinewizePasteAfter_Lines()
+ {
+ const string yanked = "line2";
+
+ var register = new PSConsoleReadLine.ViRegister();
+ register.LinewizeRecord(yanked);
+
+ // system under test
+
+ var buffer = new StringBuilder("line1\n");
+ const int position = 2;
+
+ var newPosition = register.PasteAfter(buffer, position);
+
+ // assert expectations
+
+ Assert.Equal("line1\nline2", buffer.ToString());
+ Assert.Equal(6, newPosition);
+ }
+
+ [Fact]
+ public void ViRegister_Lines_LinewizePasteAfter_Fragment()
+ {
+ const string yanked = "line2\nline3\n";
+
+ var register = new PSConsoleReadLine.ViRegister();
+ register.LinewizeRecord(yanked);
+
+ // system under test
+
+ var buffer = new StringBuilder("line1");
+ const int position = 2;
+
+ var newPosition = register.PasteAfter(buffer, position);
+
+ // assert expectations
+
+ Assert.Equal("line1\nline2\nline3\n", buffer.ToString());
+ Assert.Equal(6, newPosition);
+ }
+
+ [Fact]
+ public void ViRegister_Lines_LinewizePasteAfter_Lines()
+ {
+ const string yanked = "line2\nline3\n";
+
+ var register = new PSConsoleReadLine.ViRegister();
+ register.LinewizeRecord(yanked);
+
+ // system under test
+
+ var buffer = new StringBuilder("line1\n");
+ const int position = 2;
+
+ var newPosition = register.PasteAfter(buffer, position);
+
+ // assert expectations
+
+ Assert.Equal("line1\nline2\nline3\n", buffer.ToString());
+ Assert.Equal(6, newPosition);
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/test/YankPasteTest.VI.cs b/test/YankPasteTest.VI.cs
index 0bf02433b..e048afc2f 100644
--- a/test/YankPasteTest.VI.cs
+++ b/test/YankPasteTest.VI.cs
@@ -209,12 +209,14 @@ public void ViPasteAfterYankLine()
{
TestSetup(KeyMode.Vi);
+ var continuationPrefixLength = PSConsoleReadLineOptions.DefaultContinuationPrompt.Length;
+
Test("012 456", Keys(
"012 456", _.Escape,
- "byyP", CheckThat(() => AssertLineIs("012 012 456456")), CheckThat(() => AssertCursorLeftIs(10)),
+ "byyP", CheckThat(() => AssertLineIs("012 456\n012 456")), CheckThat(() => AssertCursorLeftIs(0)),
"u", CheckThat(() => AssertLineIs("012 456")), CheckThat(() => AssertCursorLeftIs(4)),
- "p", CheckThat(() => AssertLineIs("012 4012 45656")), CheckThat(() => AssertCursorLeftIs(11)),
- "u", CheckThat(() => AssertLineIs("012 456")), CheckThat(() => AssertCursorLeftIs(5))
+ "p", CheckThat(() => AssertLineIs("012 456\n012 456")), CheckThat(() => AssertCursorLeftIs(continuationPrefixLength + 0)),
+ "u", CheckThat(() => AssertLineIs("012 456")), CheckThat(() => AssertCursorLeftIs(4))
));
}
@@ -457,5 +459,20 @@ public void ViPasteAfterYankEndOfGlob()
"u"
));
}
+
+ [Fact]
+ public void ViYankAndPasteLogicalLines()
+ {
+ TestSetup(KeyMode.Vi);
+
+ Test("\"\nline1\nline2\nline1\nline2\n\"", Keys(
+ _.DQuote, _.Enter,
+ "line1", _.Enter,
+ "line2", _.Enter,
+ _.DQuote, _.Escape,
+ _.K, _.K,
+ "2yy", 'P'
+ ));
+ }
}
}