Supporting line wise yanks, including paste and undo. (#811)

This commit is contained in:
Maxime Labelle
2019-11-08 14:42:13 -08:00
committed by Dongbo Wang
parent 7c825775eb
commit f25a089290
13 changed files with 630 additions and 62 deletions
+5 -10
View File
@@ -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
+2 -2
View File
@@ -121,8 +121,8 @@ namespace Microsoft.PowerShell
{
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();
+72
View File
@@ -0,0 +1,72 @@
using System;
using System.Text;
namespace Microsoft.PowerShell
{
internal static class MultiLineBufferHelper
{
/// <summary>
/// Represents a range of text (subset) of the buffer.
/// </summary>
public class Range
{
public int Offset { get; set; }
public int Count { get; set; }
}
/// <summary>
/// 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
/// </summary>
/// <param name="buffer" />
/// <param name="lineOffset">
/// The 0-based number of the logical line for the current cursor position.
/// This argument comes from a call to the <see cref="PSConsoleReadLine.GetLogicalLineNumber()" />
/// method and is thus guaranteed to represent a valid line number.
/// </param>
/// <param name="lineCount">
/// 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.
/// </param>
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,
};
}
}
}
-3
View File
@@ -32,7 +32,4 @@
<None Include="..\docs\Set-PSReadLineKeyHandler.md" Link="docs\Set-PSReadLineKeyHandler.md" />
<None Include="..\docs\Set-PSReadLineOption.md" Link="docs\Set-PSReadLineOption.md" />
</ItemGroup>
<ItemGroup Condition=" '$(IsWindows)' != 'true' ">
<Compile Remove="AssemblyInfo.cs" />
</ItemGroup>
</Project>
+10 -1
View File
@@ -37,7 +37,10 @@ namespace Microsoft.PowerShell
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 @@ namespace Microsoft.PowerShell
}
}
static PSConsoleReadLine()
{
_singleton = new PSConsoleReadLine();
_clipboard = new ViRegister(_singleton);
}
private PSConsoleReadLine()
{
_mockableMethods = this;
+15 -15
View File
@@ -221,9 +221,9 @@ namespace Microsoft.PowerShell
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 @@ namespace Microsoft.PowerShell
{
_singleton.SaveToClipboard(_singleton._current, endPoint - _singleton._current);
_singleton.SaveEditItem(EditItemDelete.Create(
_singleton._clipboard,
_clipboard,
_singleton._current,
instigator,
arg
@@ -277,7 +277,7 @@ namespace Microsoft.PowerShell
_singleton.SaveToClipboard(endPoint, deleteLength);
_singleton.SaveEditItem(EditItemDelete.Create(
_singleton._clipboard,
_clipboard,
endPoint,
instigator,
arg
@@ -302,7 +302,7 @@ namespace Microsoft.PowerShell
_singleton.SaveToClipboard(_singleton._current, length);
_singleton.SaveEditItem(EditItemDelete.Create(
_singleton._clipboard,
_clipboard,
_singleton._current,
ViDeleteGlob,
arg
@@ -334,7 +334,7 @@ namespace Microsoft.PowerShell
}
_singleton.SaveToClipboard(_singleton._current, 1 + endPoint - _singleton._current);
_singleton.SaveEditItem(EditItemDelete.Create(
_singleton._clipboard,
_clipboard,
_singleton._current,
DeleteEndOfWord,
arg
@@ -361,7 +361,7 @@ namespace Microsoft.PowerShell
_singleton.SaveToClipboard(_singleton._current, 1 + endPoint - _singleton._current);
_singleton.SaveEditItem(EditItemDelete.Create(
_singleton._clipboard,
_clipboard,
_singleton._current,
ViDeleteEndOfGlob,
arg
@@ -706,7 +706,7 @@ namespace Microsoft.PowerShell
}
_singleton.SaveToClipboard(i, _singleton._current - i);
_singleton.SaveEditItem(EditItemDelete.Create(_singleton._clipboard, i, DeleteLineToFirstChar));
_singleton.SaveEditItem(EditItemDelete.Create(_clipboard, i, DeleteLineToFirstChar));
_singleton._buffer.Remove(i, _singleton._current - i);
_singleton._current = i;
@@ -723,8 +723,8 @@ namespace Microsoft.PowerShell
/// </summary>
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 @@ namespace Microsoft.PowerShell
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 @@ namespace Microsoft.PowerShell
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 @@ namespace Microsoft.PowerShell
int length = last - first + 1;
_singleton.SaveToClipboard(first, length);
_singleton.SaveEditItem(EditItemDelete.Create(_singleton._clipboard, first, action));
_singleton.SaveEditItem(EditItemDelete.Create(_clipboard, first, action));
_singleton._current = first;
_singleton._buffer.Remove(first, length);
_singleton.Render();
+35 -7
View File
@@ -148,16 +148,18 @@ namespace Microsoft.PowerShell
{
// 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 @@ namespace Microsoft.PowerShell
}
}
[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
{
+252
View File
@@ -0,0 +1,252 @@
using System;
using System.Text;
namespace Microsoft.PowerShell
{
public partial class PSConsoleReadLine
{
/// <summary>
/// Represents a named register.
/// </summary>
internal sealed class ViRegister
{
private readonly PSConsoleReadLine _singleton;
private string _text;
/// <summary>
/// Initialize a new instance of the <see cref="ViRegister" /> class.
/// </summary>
/// <param name="singleton">The <see cref="PSConsoleReadLine" /> object.
/// Used to hook into the undo / redo subsystem as part of
/// pasting the contents of the register into a buffer.
/// </param>
public ViRegister(PSConsoleReadLine singleton)
{
_singleton = singleton;
}
/// <summary>
/// Returns whether this register is empty.
/// </summary>
public bool IsEmpty
=> String.IsNullOrEmpty(_text);
/// <summary>
/// Returns whether this register contains
/// linewise yanked text.
/// </summary>
public bool HasLinewiseText { get; private set; }
/// <summary>
/// Gets the raw text contained in the register
/// </summary>
public string RawText
=> _text;
/// <summary>
/// Records the entire buffer in the register.
/// </summary>
/// <param name="buffer"></param>
public void Record(StringBuilder buffer)
{
Record(buffer, 0, buffer.Length);
}
/// <summary>
/// Records a piece of text in the register.
/// </summary>
/// <param name="buffer"></param>
/// <param name="offset"></param>
/// <param name="count"></param>
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);
}
/// <summary>
/// Records a block of lines in the register.
/// </summary>
/// <param name="text"></param>
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);
}
}
/// <summary>
/// Called to record the paste operation in the undo subsystem.
/// </summary>
/// <param name="text">
/// The text being pasted.
/// </param>
/// <param name="position">
/// The position in the buffer at
/// which the pasted text will be inserted.
/// </param>
/// <param name="anchor">
/// 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.
/// </param>
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
}
}
}
+26 -14
View File
@@ -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;
/// <summary>
/// Paste the clipboard after the cursor, moving the cursor to the end of the pasted text.
/// </summary>
public static void PasteAfter(ConsoleKeyInfo? key = null, object arg = null)
{
if (string.IsNullOrEmpty(_singleton._clipboard))
if (_clipboard.IsEmpty)
{
Ding();
return;
@@ -29,7 +33,7 @@ namespace Microsoft.PowerShell
/// </summary>
public static void PasteBefore(ConsoleKeyInfo? key = null, object arg = null)
{
if (string.IsNullOrEmpty(_singleton._clipboard))
if (_clipboard.IsEmpty)
{
Ding();
return;
@@ -39,25 +43,31 @@ namespace Microsoft.PowerShell
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);
}
/// <summary>
/// Saves a number of logical lines in the unnamed register
/// starting at the specified line number and specified count.
/// </summary>
/// <param name="lineIndex">The logical number of the current line, starting at 0.</param>
/// <param name="lineCount">The number of lines to record to the unnamed register</param>
private void SaveLinesToClipboard(int lineIndex, int lineCount)
{
var range = MultiLineBufferHelper.GetRange(_buffer, lineIndex, lineCount);
_clipboard.LinewiseRecord(_buffer.ToString(range.Offset, range.Count));
}
/// <summary>
@@ -65,7 +75,9 @@ namespace Microsoft.PowerShell
/// </summary>
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);
}
/// <summary>
+39
View File
@@ -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);
}
}
}
-6
View File
@@ -36,12 +36,6 @@
<PackageReference Include="XunitXml.TestLogger" Version="2.1.26" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\PSReadLine\CharMap.cs">
<Link>CharMap.cs</Link>
</Compile>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PSReadLine\PSReadLine.csproj">
<Project>{615788cb-1b9a-4b34-97b3-4608686e59ca}</Project>
+136
View File
@@ -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);
}
}
}
+38 -4
View File
@@ -1,4 +1,4 @@
using Microsoft.PowerShell;
using Microsoft.PowerShell;
using Xunit;
namespace Test
@@ -209,12 +209,14 @@ namespace Test
{
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 @@ namespace Test
"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'
));
}
}
}