Skip to content

Commit 6b4cd5b

Browse files
committed
Better multi-line editing
These changes provide a more seemless editing experience when the command input has multiple lines. The history recall functions now avoid recalling history if the input has multiple lines _and_ we aren't in the middle of recalling history. If you do want to edit a recalled multi-line line (ugh) then you can first use something like LeftArrow then UpArrow will work like you would want. BeginningOfLine/EndOfLine have been changed as well. If you're already at the start/end of a line, you'll go to the start/end of the input, otherwise you'll go to the start/end of the line. Two new functions have been added for moving up/down lines in case somebody wants to bind those independently of history, but I don't see those being used much. Fixes #47
1 parent 64ff1b8 commit 6b4cd5b

File tree

8 files changed

+316
-10
lines changed

8 files changed

+316
-10
lines changed

PSReadLine/History.cs

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,12 @@ private void SaveCurrentLine()
294294

295295
private void HistoryRecall(int direction)
296296
{
297+
if (_recallHistoryCommandCount == 0 && LineIsMultiLine())
298+
{
299+
MoveToLine(direction);
300+
return;
301+
}
302+
297303
int newHistoryIndex;
298304
if (Options.HistoryNoDuplicates)
299305
{
@@ -340,23 +346,39 @@ private void HistoryRecall(int direction)
340346
/// </summary>
341347
public static void PreviousHistory(ConsoleKeyInfo? key = null, object arg = null)
342348
{
349+
int numericArg;
350+
TryGetArgAsInt(arg, out numericArg, -1);
351+
if (numericArg > 0)
352+
{
353+
numericArg = -numericArg;
354+
}
355+
343356
_singleton.SaveCurrentLine();
344-
_singleton.HistoryRecall(-1);
357+
_singleton.HistoryRecall(numericArg);
345358
}
346359

347360
/// <summary>
348361
/// Replace the current input with the 'next' item from PSReadline history.
349362
/// </summary>
350363
public static void NextHistory(ConsoleKeyInfo? key = null, object arg = null)
351364
{
365+
int numericArg;
366+
TryGetArgAsInt(arg, out numericArg, +1);
367+
352368
_singleton.SaveCurrentLine();
353-
_singleton.HistoryRecall(+1);
369+
_singleton.HistoryRecall(numericArg);
354370
}
355371

356372
private void HistorySearch(int direction)
357373
{
358374
if (_searchHistoryCommandCount == 0)
359375
{
376+
if (LineIsMultiLine())
377+
{
378+
MoveToLine(direction);
379+
return;
380+
}
381+
360382
_searchHistoryPrefix = _buffer.ToString(0, _current);
361383
_emphasisStart = 0;
362384
_emphasisLength = _current;
@@ -416,8 +438,15 @@ public static void EndOfHistory(ConsoleKeyInfo? key = null, object arg = null)
416438
/// </summary>
417439
public static void HistorySearchBackward(ConsoleKeyInfo? key = null, object arg = null)
418440
{
441+
int numericArg;
442+
TryGetArgAsInt(arg, out numericArg, -1);
443+
if (numericArg > 0)
444+
{
445+
numericArg = -numericArg;
446+
}
447+
419448
_singleton.SaveCurrentLine();
420-
_singleton.HistorySearch(-1);
449+
_singleton.HistorySearch(numericArg);
421450
}
422451

423452
/// <summary>
@@ -426,8 +455,11 @@ public static void HistorySearchBackward(ConsoleKeyInfo? key = null, object arg
426455
/// </summary>
427456
public static void HistorySearchForward(ConsoleKeyInfo? key = null, object arg = null)
428457
{
458+
int numericArg;
459+
TryGetArgAsInt(arg, out numericArg, +1);
460+
429461
_singleton.SaveCurrentLine();
430-
_singleton.HistorySearch(+1);
462+
_singleton.HistorySearch(numericArg);
431463
}
432464

433465
private void UpdateHistoryDuringInteractiveSearch(string toMatch, int direction, ref int searchFromPoint)

PSReadLine/Movement.cs

Lines changed: 106 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,61 @@ namespace PSConsoleUtilities
66
{
77
public partial class PSConsoleReadLine
88
{
9+
private int _moveToLineCommandCount;
10+
private int _moveToLineDesiredColumn;
11+
912
/// <summary>
10-
/// Move the cursor to the end of the input.
13+
/// If the input has multiple lines, move to the end of the current line,
14+
/// or if already at the end of the line, move to the end of the input.
15+
/// If the input has a single line, move to the end of the input.
1116
/// </summary>
1217
public static void EndOfLine(ConsoleKeyInfo? key = null, object arg = null)
1318
{
14-
_singleton._current = _singleton._buffer.Length;
19+
if (_singleton.LineIsMultiLine())
20+
{
21+
int i = _singleton._current;
22+
for (; i < _singleton._buffer.Length; i++)
23+
{
24+
if (_singleton._buffer[i] == '\n')
25+
{
26+
break;
27+
}
28+
}
29+
30+
_singleton._current = (i == _singleton._current) ? _singleton._buffer.Length : i;
31+
}
32+
else
33+
{
34+
_singleton._current = _singleton._buffer.Length;
35+
}
1536
_singleton.PlaceCursor();
1637
}
1738

1839
/// <summary>
19-
/// Move the cursor to the end of the input.
40+
/// If the input has multiple lines, move to the start of the current line,
41+
/// or if already at the start of the line, move to the start of the input.
42+
/// If the input has a single line, move to the start of the input.
2043
/// </summary>
2144
public static void BeginningOfLine(ConsoleKeyInfo? key = null, object arg = null)
2245
{
23-
_singleton._current = 0;
46+
if (_singleton.LineIsMultiLine())
47+
{
48+
int i = Math.Max(0, _singleton._current - 1);
49+
for (; i > 1; i--)
50+
{
51+
if (_singleton._buffer[i] == '\n')
52+
{
53+
i += 1;
54+
break;
55+
}
56+
}
57+
58+
_singleton._current = (i == _singleton._current) ? 0 : i;
59+
}
60+
else
61+
{
62+
_singleton._current = 0;
63+
}
2464
_singleton.PlaceCursor();
2565
}
2666

@@ -50,6 +90,68 @@ public static void BackwardChar(ConsoleKeyInfo? key = null, object arg = null)
5090
}
5191
}
5292

93+
private void MoveToLine(int numericArg)
94+
{
95+
const int endOfLine = int.MaxValue;
96+
97+
_moveToLineCommandCount += 1;
98+
var coords = ConvertOffsetToCoordinates(_current);
99+
if (_moveToLineCommandCount == 1)
100+
{
101+
_moveToLineDesiredColumn =
102+
(_current == _buffer.Length || _buffer[_current] == '\n')
103+
? endOfLine
104+
: coords.X;
105+
}
106+
107+
var topLine = _initialY + Options.ExtraPromptLineCount;
108+
109+
var newY = coords.Y + numericArg;
110+
coords.Y = (short)Math.Max(newY, topLine);
111+
if (_moveToLineDesiredColumn != endOfLine)
112+
{
113+
coords.X = (short)_moveToLineDesiredColumn;
114+
}
115+
116+
var newCurrent = ConvertLineAndColumnToOffset(coords);
117+
if (newCurrent != -1)
118+
{
119+
_current = newCurrent;
120+
if (_moveToLineDesiredColumn == endOfLine)
121+
{
122+
while (_current < _buffer.Length && _buffer[_current] != '\n')
123+
{
124+
_current += 1;
125+
}
126+
}
127+
PlaceCursor();
128+
}
129+
}
130+
131+
/// <summary>
132+
/// Move the cursor to the previous line.
133+
/// </summary>
134+
public static void PreviousLine(ConsoleKeyInfo? key = null, object arg = null)
135+
{
136+
int numericArg;
137+
if (TryGetArgAsInt(arg, out numericArg, 1))
138+
{
139+
_singleton.MoveToLine(-numericArg);
140+
}
141+
}
142+
143+
/// <summary>
144+
/// Move the cursor to the next line.
145+
/// </summary>
146+
public static void NextLine(ConsoleKeyInfo? key = null, object arg = null)
147+
{
148+
int numericArg;
149+
if (TryGetArgAsInt(arg, out numericArg, 1))
150+
{
151+
_singleton.MoveToLine(numericArg);
152+
}
153+
}
154+
53155
/// <summary>
54156
/// Move the cursor forward to the start of the next word.
55157
/// Word boundaries are defined by a configurable set of characters.

PSReadLine/PSReadLineResources.Designer.cs

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

PSReadLine/PSReadLineResources.resx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,4 +399,10 @@ Exception:
399399
{2}
400400
-----------------------------------------------------------------------</value>
401401
</data>
402+
<data name="NextLineDescription" xml:space="preserve">
403+
<value>Move the cursor to the next line if the input has multiple lines.</value>
404+
</data>
405+
<data name="PreviousLineDescription" xml:space="preserve">
406+
<value>Move the cursor to the previous line if the input has multiple lines.</value>
407+
</data>
402408
</root>

PSReadLine/ReadLine.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@ private string InputLoop()
280280
var recallHistoryCommandCount = _recallHistoryCommandCount;
281281
var yankLastArgCommandCount = _yankLastArgCommandCount;
282282
var visualSelectionCommandCount = _visualSelectionCommandCount;
283+
var movingAtEndOfLineCount = _moveToLineCommandCount;
283284

284285
var key = ReadKey();
285286
ProcessOneKey(key, _dispatchTable, ignoreIfNoAction: false, arg: null);
@@ -340,6 +341,10 @@ private string InputLoop()
340341
_visualSelectionCommandCount = 0;
341342
Render(); // Clears the visual selection
342343
}
344+
if (movingAtEndOfLineCount == _moveToLineCommandCount)
345+
{
346+
_moveToLineCommandCount = 0;
347+
}
343348
}
344349
}
345350

PSReadLine/Render.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,61 @@ private COORD ConvertOffsetToCoordinates(int offset)
501501
return new COORD {X = (short)x, Y = (short)y};
502502
}
503503

504+
private int ConvertLineAndColumnToOffset(COORD coord)
505+
{
506+
int offset;
507+
int x = _initialX;
508+
int y = _initialY + Options.ExtraPromptLineCount;
509+
510+
int bufferWidth = Console.BufferWidth;
511+
var continuationPromptLength = Options.ContinuationPrompt.Length;
512+
for (offset = 0; offset < _buffer.Length; offset++)
513+
{
514+
// If we are on the correct line, return when we find
515+
// the correct column
516+
if (coord.Y == y && coord.X <= x)
517+
{
518+
return offset;
519+
}
520+
char c = _buffer[offset];
521+
if (c == '\n')
522+
{
523+
// If we are about to move off of the correct line,
524+
// the line was shorter than the column we wanted so return.
525+
if (coord.Y == y)
526+
{
527+
return offset;
528+
}
529+
y += 1;
530+
x = continuationPromptLength;
531+
}
532+
else
533+
{
534+
x += char.IsControl(c) ? 2 : 1;
535+
// Wrap? No prompt when wrapping
536+
if (x >= bufferWidth)
537+
{
538+
x -= bufferWidth;
539+
y += 1;
540+
}
541+
}
542+
}
543+
544+
// Return -1 if y is out of range, otherwise the last line was shorter
545+
// than we wanted, but still in range so just return the last offset.B
546+
return (coord.Y == y) ? offset : -1;
547+
}
548+
549+
private bool LineIsMultiLine()
550+
{
551+
for (int i = 0; i < _buffer.Length; i++)
552+
{
553+
if (_buffer[i] == '\n')
554+
return true;
555+
}
556+
return false;
557+
}
558+
504559
private int GetStatusLineCount()
505560
{
506561
if (_statusLinePrompt == null)

PSReadLine/en-US/about_PSReadline.help.txt

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,23 @@ LONG DESCRIPTION
2828

2929
EndOfLine (Cmd: <End> Emacs: <End> or <Ctrl+E>)
3030

31-
Move the cursor to the end of the input.
31+
If the input has multiple lines, move to the end of the current line,
32+
or if already at the end of the line, move to the end of the input.
33+
If the input has a single line, move to the end of the input.
3234

3335
BeginningOfLine (Cmd: <Home> Emacs: <Home> or <Ctrl+A>)
3436

35-
Move the cursor to the start of the input.
37+
If the input has multiple lines, move to the start of the current line,
38+
or if already at the start of the line, move to the start of the input.
39+
If the input has a single line, move to the start of the input.
40+
41+
NextLine (Cmd: unbound Emacs: unbound)
42+
43+
Move the cursor to the next line if the input has multiple lines.
44+
45+
PreviousLine (Cmd: unbound Emacs: unbound)
46+
47+
Move the cursor to the previous line if the input has multiple lines.
3648

3749
ForwardChar (Cmd: <RightArrow> Emacs: <RightArrow> or <Ctrl+F>)
3850

0 commit comments

Comments
 (0)