diff --git a/PSReadLine/AssemblyInfo.cs b/PSReadLine/AssemblyInfo.cs
index a725bc41..d67ca619 100644
--- a/PSReadLine/AssemblyInfo.cs
+++ b/PSReadLine/AssemblyInfo.cs
@@ -2,21 +2,16 @@
Copyright (c) Microsoft Corporation. All rights reserved.
--********************************************************************/
-using System.Reflection;
-using System.Runtime.InteropServices;
using System.Diagnostics.CodeAnalysis;
+using System.Runtime.InteropServices;
+using System.Runtime.CompilerServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
-[assembly: AssemblyTitle("PSReadLine")]
-[assembly: AssemblyDescription("Great command line editing in PowerShell")]
-[assembly: AssemblyConfiguration("")]
-[assembly: AssemblyCompany("")]
-[assembly: AssemblyProduct("PSReadLine")]
-[assembly: AssemblyCopyright("Copyright")]
-[assembly: AssemblyTrademark("")]
-[assembly: AssemblyCulture("")]
+
+// Make it a friend assembly to 'PSReadLine.Tests' for testing.
+[assembly:InternalsVisibleTo("PSReadLine.Tests")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
diff --git a/PSReadLine/BasicEditing.cs b/PSReadLine/BasicEditing.cs
index eaa83a0b..a207c038 100644
--- a/PSReadLine/BasicEditing.cs
+++ b/PSReadLine/BasicEditing.cs
@@ -121,8 +121,8 @@ public static void BackwardDeleteLine(ConsoleKeyInfo? key = null, object arg = n
{
if (_singleton._current > 0)
{
- _singleton._clipboard = _singleton._buffer.ToString(0, _singleton._current);
- _singleton.SaveEditItem(EditItemDelete.Create(_singleton._clipboard, 0));
+ _clipboard.Record(_singleton._buffer, 0, _singleton._current);
+ _singleton.SaveEditItem(EditItemDelete.Create(_clipboard, 0));
_singleton._buffer.Remove(0, _singleton._current);
_singleton._current = 0;
_singleton.Render();
diff --git a/PSReadLine/MultiLineBufferHelper.cs b/PSReadLine/MultiLineBufferHelper.cs
new file mode 100644
index 00000000..a2decc3c
--- /dev/null
+++ b/PSReadLine/MultiLineBufferHelper.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Text;
+
+namespace Microsoft.PowerShell
+{
+ internal static class MultiLineBufferHelper
+ {
+ ///
+ /// Represents a range of text (subset) of the buffer.
+ ///
+ public class Range
+ {
+ public int Offset { get; set; }
+ public int Count { get; set; }
+ }
+
+ ///
+ /// 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
+ ///
+ ///
+ ///
+ /// The 0-based number of the logical line for the current cursor position.
+ /// This argument comes from a call to the
+ /// method and is thus guaranteed to represent a valid line number.
+ ///
+ ///
+ /// The number of lines to be taken into account.
+ /// If more lines are taken into account than there are lines available,
+ /// this method still returns a valid range corresponding to the available
+ /// lines from the buffer.
+ ///
+ public static Range GetRange(StringBuilder buffer, int lineOffset, int lineCount)
+ {
+ var length = buffer.Length;
+
+ var startPosition = 0;
+ var startPositionIdentified = false;
+
+ var endPosition = length - 1;
+
+ var currentLine = 0;
+
+ for (var position = 0; position < length; position++)
+ {
+ if (currentLine == lineOffset && !startPositionIdentified)
+ {
+ startPosition = position;
+ startPositionIdentified = true;
+ }
+
+ if (buffer[position] == '\n')
+ {
+ currentLine++;
+ }
+
+ if (currentLine == lineOffset + lineCount)
+ {
+ endPosition = position;
+ break;
+ }
+ }
+
+ return new Range
+ {
+ Offset = startPosition,
+ Count = endPosition - startPosition + 1,
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/PSReadLine/PSReadLine.csproj b/PSReadLine/PSReadLine.csproj
index 48f63c45..9dbf2dd2 100644
--- a/PSReadLine/PSReadLine.csproj
+++ b/PSReadLine/PSReadLine.csproj
@@ -32,7 +32,4 @@
-
-
-
diff --git a/PSReadLine/ReadLine.cs b/PSReadLine/ReadLine.cs
index 1cdc9441..3dfc468d 100644
--- a/PSReadLine/ReadLine.cs
+++ b/PSReadLine/ReadLine.cs
@@ -37,7 +37,10 @@ public partial class PSConsoleReadLine : IPSConsoleReadLineMockableMethods
private const int EventProcessingRequested = 3;
- private static readonly PSConsoleReadLine _singleton = new PSConsoleReadLine();
+ // *must* be initialized in the static ctor
+ // because the static member _clipboard depends upon it
+ // for its own initialization
+ private static readonly PSConsoleReadLine _singleton;
private static readonly CancellationToken _defaultCancellationToken = new CancellationTokenSource().Token;
@@ -607,6 +610,12 @@ void ProcessOneKey(PSKeyInfo key, Dictionary dispatchTabl
}
}
+ static PSConsoleReadLine()
+ {
+ _singleton = new PSConsoleReadLine();
+ _clipboard = new ViRegister(_singleton);
+ }
+
private PSConsoleReadLine()
{
_mockableMethods = this;
diff --git a/PSReadLine/ReadLine.vi.cs b/PSReadLine/ReadLine.vi.cs
index f402184d..4b6977be 100644
--- a/PSReadLine/ReadLine.vi.cs
+++ b/PSReadLine/ReadLine.vi.cs
@@ -221,9 +221,9 @@ public static void DeleteToEnd(ConsoleKeyInfo? key = null, object arg = null)
return;
}
- _singleton._clipboard = _singleton._buffer.ToString(_singleton._current, _singleton._buffer.Length - _singleton._current);
+ _clipboard.Record(_singleton._buffer, _singleton._current, _singleton._buffer.Length - _singleton._current);
_singleton.SaveEditItem(EditItemDelete.Create(
- _singleton._clipboard,
+ _clipboard,
_singleton._current,
DeleteToEnd,
arg
@@ -258,7 +258,7 @@ private static void DeleteToEndPoint(object arg, int endPoint, Action
public static void DeleteLine(ConsoleKeyInfo? key = null, object arg = null)
{
- _singleton._clipboard = _singleton._buffer.ToString();
- _singleton.SaveEditItem(EditItemDelete.Create(_singleton._clipboard, 0));
+ _clipboard.Record(_singleton._buffer);
+ _singleton.SaveEditItem(EditItemDelete.Create(_clipboard, 0));
_singleton._current = 0;
_singleton._buffer.Remove(0, _singleton._buffer.Length);
_singleton.Render();
@@ -746,9 +746,9 @@ public static void BackwardDeleteWord(ConsoleKeyInfo? key = null, object arg = n
Ding();
return;
}
- _singleton._clipboard = _singleton._buffer.ToString(deletePoint, _singleton._current - deletePoint);
+ _clipboard.Record(_singleton._buffer, deletePoint, _singleton._current - deletePoint);
_singleton.SaveEditItem(EditItemDelete.Create(
- _singleton._clipboard,
+ _clipboard,
deletePoint,
BackwardDeleteWord,
arg
@@ -779,9 +779,9 @@ public static void ViBackwardDeleteGlob(ConsoleKeyInfo? key = null, object arg =
Ding();
return;
}
- _singleton._clipboard = _singleton._buffer.ToString(deletePoint, _singleton._current - deletePoint);
+ _clipboard.Record(_singleton._buffer, deletePoint, _singleton._current - deletePoint);
_singleton.SaveEditItem(EditItemDelete.Create(
- _singleton._clipboard,
+ _clipboard,
deletePoint,
BackwardDeleteWord,
arg
@@ -823,7 +823,7 @@ private static void DeleteRange(int first, int last, Action
+ /// Represents a named register.
+ ///
+ internal sealed class ViRegister
+ {
+ private readonly PSConsoleReadLine _singleton;
+ private string _text;
+
+ ///
+ /// Initialize a new instance of the class.
+ ///
+ /// The object.
+ /// Used to hook into the undo / redo subsystem as part of
+ /// pasting the contents of the register into a buffer.
+ ///
+ public ViRegister(PSConsoleReadLine singleton)
+ {
+ _singleton = singleton;
+ }
+
+ ///
+ /// Returns whether this register is empty.
+ ///
+ public bool IsEmpty
+ => String.IsNullOrEmpty(_text);
+
+ ///
+ /// Returns whether this register contains
+ /// linewise yanked text.
+ ///
+ public bool HasLinewiseText { get; private set; }
+
+ ///
+ /// Gets the raw text contained in the register
+ ///
+ 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);
+
+ HasLinewiseText = false;
+ _text = buffer.ToString(offset, count);
+ }
+
+ ///
+ /// Records a block of lines in the register.
+ ///
+ ///
+ public void LinewiseRecord(string text)
+ {
+ HasLinewiseText = 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 (HasLinewiseText)
+ {
+ 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 (HasLinewiseText)
+ {
+ // currently, in Vi Edit Mode, the cursor may be positioned
+ // exactly one character past the end of the buffer.
+
+ // we adjust the current position to prevent a crash
+
+ position = Math.Max(0, Math.Min(position, buffer.Length - 1));
+
+ 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)
+ {
+ RecordPaste(text, pastePosition, position);
+ buffer.Insert(pastePosition, text);
+ }
+
+ private void InsertAt(StringBuilder buffer, string text, int pastePosition, int position)
+ {
+ RecordPaste(text, pastePosition, 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);
+ }
+ }
+
+ ///
+ /// Called to record the paste operation in the undo subsystem.
+ ///
+ ///
+ /// The text being pasted.
+ ///
+ ///
+ /// The position in the buffer at
+ /// which the pasted text will be inserted.
+ ///
+ ///
+ /// 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.
+ ///
+ private void RecordPaste(string text, int position, int anchor)
+ {
+ if (_singleton != null)
+ {
+ var editItem = EditItemInsertLines.Create(
+ text,
+ position,
+ anchor
+ );
+
+ _singleton.SaveEditItem(editItem);
+ }
+ }
+
+#if DEBUG
+ public override string ToString()
+ {
+ var text = _text.Replace("\n", "\\n");
+ return (HasLinewiseText ? "line: " : "") + "\"" + text + "\"";
+ }
+#endif
+ }
+ }
+}
\ No newline at end of file
diff --git a/PSReadLine/YankPaste.vi.cs b/PSReadLine/YankPaste.vi.cs
index 0d3a86cf..5fe7d553 100644
--- a/PSReadLine/YankPaste.vi.cs
+++ b/PSReadLine/YankPaste.vi.cs
@@ -1,21 +1,25 @@
-/********************************************************************++
+/********************************************************************++
Copyright (c) Microsoft Corporation. All rights reserved.
--********************************************************************/
using System;
+using System.Text;
namespace Microsoft.PowerShell
{
public partial class PSConsoleReadLine
{
- private string _clipboard = string.Empty;
+ // *must* be initialized in the static ctor
+ // because it depends on static member _singleton
+ // being initialized first.
+ private static readonly ViRegister _clipboard;
///
/// 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))
+ if (_clipboard.IsEmpty)
{
Ding();
return;
@@ -29,7 +33,7 @@ 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))
+ if (_clipboard.IsEmpty)
{
Ding();
return;
@@ -39,25 +43,31 @@ public static void PasteBefore(ConsoleKeyInfo? key = null, object arg = null)
private void PasteAfterImpl()
{
- if (_current < _buffer.Length)
- {
- _current++;
- }
- Insert(_clipboard);
- _current--;
+ _current = _clipboard.PasteAfter(_buffer, _current);
Render();
}
private void PasteBeforeImpl()
{
- Insert(_clipboard);
- _current--;
+ _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 = MultiLineBufferHelper.GetRange(_buffer, lineIndex, lineCount);
+ _clipboard.LinewiseRecord(_buffer.ToString(range.Offset, range.Count));
}
///
@@ -65,7 +75,9 @@ private void SaveToClipboard(int startIndex, int length)
///
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/MultiLineBufferHelperTests.cs b/test/MultiLineBufferHelperTests.cs
new file mode 100644
index 00000000..3f6264fb
--- /dev/null
+++ b/test/MultiLineBufferHelperTests.cs
@@ -0,0 +1,39 @@
+using System.Text;
+using Microsoft.PowerShell;
+using Xunit;
+
+namespace Test
+{
+ public sealed class MultiLineBufferHelperTests
+ {
+ [Fact]
+ public void MultiLineBufferHelper_LinewiseYank_Lines()
+ {
+ var buffer = new StringBuilder("line1\nline2\nline3\nline4");
+
+ // system under test
+
+ var range = MultiLineBufferHelper.GetRange(buffer, 1, 2);
+
+ // assert
+
+ Assert.Equal(6, range.Offset);
+ Assert.Equal(12, range.Count);
+ }
+
+ [Fact]
+ public void MultilineBufferHelper_LinewiseYank_MoreLinesThanAvailable()
+ {
+ var buffer = new StringBuilder("line1\nline2");
+
+ // system under test
+
+ var range = MultiLineBufferHelper.GetRange(buffer, 1, 42);
+
+ // assert
+
+ Assert.Equal(6, range.Offset);
+ Assert.Equal(5, range.Count);
+ }
+ }
+}
diff --git a/test/PSReadLine.Tests.csproj b/test/PSReadLine.Tests.csproj
index 27548736..f5cc905d 100644
--- a/test/PSReadLine.Tests.csproj
+++ b/test/PSReadLine.Tests.csproj
@@ -36,12 +36,6 @@
-
-
- CharMap.cs
-
-
-
{615788cb-1b9a-4b34-97b3-4608686e59ca}
diff --git a/test/ViRegisterTests.cs b/test/ViRegisterTests.cs
new file mode 100644
index 00000000..f8adfd24
--- /dev/null
+++ b/test/ViRegisterTests.cs
@@ -0,0 +1,136 @@
+using System.Text;
+using Microsoft.PowerShell;
+using Xunit;
+
+namespace Test
+{
+ public sealed class ViRegisterTests
+ {
+ [Fact]
+ public void ViRegister_Fragment_LinewisePasteBefore()
+ {
+ const string yanked = "line1";
+
+ var register = new PSConsoleReadLine.ViRegister(null);
+ register.LinewiseRecord(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_LinewisePasteBefore()
+ {
+ const string yanked = "line1\n";
+
+ var register = new PSConsoleReadLine.ViRegister(null);
+ register.LinewiseRecord(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_LinewisePasteAfter_Fragment()
+ {
+ const string yanked = "line2";
+
+ var register = new PSConsoleReadLine.ViRegister(null);
+ register.LinewiseRecord(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_LinewisePasteAfter_Lines()
+ {
+ const string yanked = "line2";
+
+ var register = new PSConsoleReadLine.ViRegister(null);
+ register.LinewiseRecord(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_LinewisePasteAfter_Fragment()
+ {
+ const string yanked = "line2\nline3\n";
+
+ var register = new PSConsoleReadLine.ViRegister(null);
+ register.LinewiseRecord(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_LinewisePasteAfter_Lines()
+ {
+ const string yanked = "line2\nline3\n";
+
+ var register = new PSConsoleReadLine.ViRegister(null);
+ register.LinewiseRecord(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);
+ }
+ }
+}
diff --git a/test/YankPasteTest.VI.cs b/test/YankPasteTest.VI.cs
index 0bf02433..4b236ba8 100644
--- a/test/YankPasteTest.VI.cs
+++ b/test/YankPasteTest.VI.cs
@@ -1,4 +1,4 @@
-using Microsoft.PowerShell;
+using Microsoft.PowerShell;
using Xunit;
namespace Test
@@ -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,37 @@ public void ViPasteAfterYankEndOfGlob()
"u"
));
}
+
+ [SkippableFact]
+ 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'
+ ));
+ }
+
+ [SkippableFact]
+ public void ViYankAndPasteLogicalLines_LastLine()
+ {
+ TestSetup(KeyMode.Vi);
+
+ Test("\"\nHello\nWorld!\nWorld!\n\"", Keys(
+ _.DQuote, _.Enter,
+ "Hello", _.Enter,
+ "World!", _.Enter,
+ _.DQuote, _.Escape,
+ _.k,
+ "yy",
+ _.j, // move to last line
+ 'P'
+ ));
+ }
}
}