mirror of
https://github.com/PowerShell/PSReadLine
synced 2026-06-08 12:12:13 +00:00
Supporting line wise yanks, including paste and undo. (#811)
This commit is contained in:
committed by
Dongbo Wang
parent
7c825775eb
commit
f25a089290
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user