Skip to content

Commit 8a95e79

Browse files
Add support for pasting text in multiline editors
1 parent 8a9a412 commit 8a95e79

File tree

7 files changed

+191
-37
lines changed

7 files changed

+191
-37
lines changed

src/RadLine.Tests/Utilities/TestInputSource.cs

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
using System;
22
using System.Collections.Generic;
3-
using System.Threading;
4-
using System.Threading.Tasks;
53

64
namespace RadLine.Tests
75
{
86
public sealed class TestInputSource : IInputSource
97
{
108
private readonly Queue<ConsoleKeyInfo> _input;
119

10+
public bool ByPassProcessing => true;
11+
1212
public TestInputSource()
1313
{
1414
_input = new Queue<ConsoleKeyInfo>();
@@ -64,25 +64,19 @@ public TestInputSource Push(ConsoleKey input, ConsoleModifiers modifiers)
6464
return this;
6565
}
6666

67-
public ConsoleKeyInfo? ReadKey(bool intercept)
67+
public bool IsKeyAvailable()
6868
{
69-
if (_input.Count == 0)
70-
{
71-
throw new InvalidOperationException("No input available.");
72-
}
73-
74-
return _input.Dequeue();
69+
return _input.Count > 0;
7570
}
7671

77-
Task<ConsoleKeyInfo?> IInputSource.ReadKey(CancellationToken cancellationToken)
72+
ConsoleKeyInfo IInputSource.ReadKey()
7873
{
7974
if (_input.Count == 0)
8075
{
8176
throw new InvalidOperationException("No keys available");
8277
}
8378

84-
var key = _input.Dequeue();
85-
return Task.FromResult<ConsoleKeyInfo?>(key);
79+
return _input.Dequeue();
8680
}
8781
}
8882
}

src/RadLine/IInputSource.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
using System;
2-
using System.Threading;
3-
using System.Threading.Tasks;
42

53
namespace RadLine
64
{
75
public interface IInputSource
86
{
9-
Task<ConsoleKeyInfo?> ReadKey(CancellationToken cancellationToken);
7+
bool ByPassProcessing { get; }
8+
9+
bool IsKeyAvailable();
10+
ConsoleKeyInfo ReadKey();
1011
}
1112
}

src/RadLine/Internal/DefaultInputSource.cs

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
using System;
2-
using System.Threading;
3-
using System.Threading.Tasks;
42
using Spectre.Console;
53

64
namespace RadLine
@@ -9,12 +7,19 @@ internal sealed class DefaultInputSource : IInputSource
97
{
108
private readonly IAnsiConsole _console;
119

10+
public bool ByPassProcessing => false;
11+
1212
public DefaultInputSource(IAnsiConsole console)
1313
{
1414
_console = console ?? throw new ArgumentNullException(nameof(console));
1515
}
1616

17-
public async Task<ConsoleKeyInfo?> ReadKey(CancellationToken cancellationToken)
17+
public bool IsKeyAvailable()
18+
{
19+
return Console.KeyAvailable;
20+
}
21+
22+
public ConsoleKeyInfo ReadKey()
1823
{
1924
if (!_console.Profile.Out.IsTerminal
2025
|| !_console.Profile.Capabilities.Interactive)
@@ -23,21 +28,6 @@ public DefaultInputSource(IAnsiConsole console)
2328
}
2429

2530
// TODO: Put terminal in raw mode
26-
while (true)
27-
{
28-
if (cancellationToken.IsCancellationRequested)
29-
{
30-
return null;
31-
}
32-
33-
if (Console.KeyAvailable)
34-
{
35-
break;
36-
}
37-
38-
await Task.Delay(5, cancellationToken).ConfigureAwait(false);
39-
}
40-
4131
return Console.ReadKey(true);
4232
}
4333
}

src/RadLine/Internal/InputBuffer.cs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
6+
namespace RadLine
7+
{
8+
internal sealed class InputBuffer
9+
{
10+
private readonly IInputSource _source;
11+
private readonly Queue<ConsoleKeyInfo> _queue;
12+
private KeyBinding? _newLineBinding;
13+
private KeyBinding? _submitBinding;
14+
15+
public InputBuffer(IInputSource source)
16+
{
17+
_source = source ?? throw new ArgumentNullException(nameof(source));
18+
_queue = new Queue<ConsoleKeyInfo>();
19+
}
20+
21+
public void Initialize(KeyBindings bindings)
22+
{
23+
bindings.TryFindKeyBindings<NewLineCommand>(out _newLineBinding);
24+
bindings.TryFindKeyBindings<SubmitCommand>(out _submitBinding);
25+
}
26+
27+
public async Task<ConsoleKeyInfo?> ReadKey(bool multiline, CancellationToken cancellationToken)
28+
{
29+
if (_queue.Count > 0)
30+
{
31+
return _queue.Dequeue();
32+
}
33+
34+
// Wait for the user to enter a key
35+
var key = await ReadKeyFromSource(wait: true, cancellationToken);
36+
if (key == null)
37+
{
38+
return null;
39+
}
40+
else
41+
{
42+
_queue.Enqueue(key.Value);
43+
}
44+
45+
if (_source.IsKeyAvailable())
46+
{
47+
// Read all remaining keys from the buffer
48+
await ReadRemainingKeys(multiline, cancellationToken);
49+
}
50+
51+
// Got something?
52+
if (_queue.Count > 0)
53+
{
54+
return _queue.Dequeue();
55+
}
56+
57+
return null;
58+
}
59+
60+
private async Task ReadRemainingKeys(bool multiline, CancellationToken cancellationToken)
61+
{
62+
var keys = new Queue<ConsoleKeyInfo>();
63+
64+
while (true)
65+
{
66+
var key = await ReadKeyFromSource(wait: false, cancellationToken);
67+
if (key == null)
68+
{
69+
break;
70+
}
71+
72+
keys.Enqueue(key.Value);
73+
}
74+
75+
if (keys.Count > 0)
76+
{
77+
// Process the input when we're somewhat sure that
78+
// the input has been automated in some fashion,
79+
// and the editor support multiline. The input source
80+
// can bypass this kind of behavior, so we need to check
81+
// it as well to see if we should do any processing.
82+
var shouldProcess = multiline && keys.Count >= 5 && !_source.ByPassProcessing;
83+
84+
while (keys.Count > 0)
85+
{
86+
var key = keys.Dequeue();
87+
88+
if (shouldProcess && _submitBinding != null && _newLineBinding != null)
89+
{
90+
// Is the key trying to submit?
91+
if (_submitBinding.Equals(key))
92+
{
93+
// Insert a new line instead
94+
key = _newLineBinding.AsConsoleKeyInfo();
95+
}
96+
}
97+
98+
_queue.Enqueue(key);
99+
}
100+
}
101+
}
102+
103+
private async Task<ConsoleKeyInfo?> ReadKeyFromSource(bool wait, CancellationToken cancellationToken)
104+
{
105+
if (wait)
106+
{
107+
while (true)
108+
{
109+
if (cancellationToken.IsCancellationRequested)
110+
{
111+
return null;
112+
}
113+
114+
if (_source.IsKeyAvailable())
115+
{
116+
break;
117+
}
118+
119+
await Task.Delay(5, cancellationToken).ConfigureAwait(false);
120+
}
121+
}
122+
123+
if (_source.IsKeyAvailable())
124+
{
125+
return _source.ReadKey();
126+
}
127+
128+
return null;
129+
}
130+
}
131+
}

src/RadLine/Internal/KeyBinding.cs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
namespace RadLine
44
{
5-
internal sealed class KeyBinding
5+
internal sealed class KeyBinding : IEquatable<ConsoleKeyInfo>
66
{
77
public ConsoleKey Key { get; }
88
public ConsoleModifiers? Modifiers { get; }
@@ -12,5 +12,29 @@ public KeyBinding(ConsoleKey key, ConsoleModifiers? modifiers = null)
1212
Key = key;
1313
Modifiers = modifiers;
1414
}
15+
16+
public ConsoleKeyInfo AsConsoleKeyInfo()
17+
{
18+
return new ConsoleKeyInfo(
19+
(char)0, Key,
20+
HasModifier(ConsoleModifiers.Shift),
21+
HasModifier(ConsoleModifiers.Alt),
22+
HasModifier(ConsoleModifiers.Control));
23+
}
24+
25+
private bool HasModifier(ConsoleModifiers modifier)
26+
{
27+
if (Modifiers != null)
28+
{
29+
return Modifiers.Value.HasFlag(modifier);
30+
}
31+
32+
return false;
33+
}
34+
35+
public bool Equals(ConsoleKeyInfo other)
36+
{
37+
return other.Modifiers == (Modifiers ?? 0) && other.Key == Key;
38+
}
1539
}
1640
}

src/RadLine/KeyBindings.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,39 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Diagnostics.CodeAnalysis;
34
using System.Linq;
45

56
namespace RadLine
67
{
78
public sealed class KeyBindings
89
{
910
private readonly Dictionary<KeyBinding, Func<LineEditorCommand>> _bindings;
11+
private readonly Dictionary<Type, KeyBinding> _bindingLookup;
1012

1113
public int Count => _bindings.Count;
1214

1315
public KeyBindings()
1416
{
1517
_bindings = new Dictionary<KeyBinding, Func<LineEditorCommand>>(new KeyBindingComparer());
18+
_bindingLookup = new Dictionary<Type, KeyBinding>();
1619
}
1720

18-
internal void Add(KeyBinding binding, Func<LineEditorCommand> command)
21+
internal void Add<TCommand>(KeyBinding binding, Func<TCommand> command)
22+
where TCommand : LineEditorCommand
1923
{
2024
if (binding is null)
2125
{
2226
throw new ArgumentNullException(nameof(binding));
2327
}
2428

25-
_bindings[binding] = command;
29+
_bindings[binding] = () => command();
30+
_bindingLookup[typeof(TCommand)] = binding;
31+
}
32+
33+
internal bool TryFindKeyBindings<TCommand>([NotNullWhen(true)] out KeyBinding? binding)
34+
where TCommand : LineEditorCommand
35+
{
36+
return _bindingLookup.TryGetValue(typeof(TCommand), out binding);
2637
}
2738

2839
internal void Remove(KeyBinding binding)

src/RadLine/LineEditor.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public sealed class LineEditor : IHighlighterAccessor
1515
private readonly IAnsiConsole _console;
1616
private readonly LineEditorRenderer _renderer;
1717
private readonly LineEditorHistory _history;
18+
private readonly InputBuffer _input;
1819

1920
public KeyBindings KeyBindings { get; }
2021
public bool MultiLine { get; init; } = false;
@@ -32,6 +33,7 @@ public LineEditor(IAnsiConsole? terminal = null, IInputSource? source = null, IS
3233
_provider = provider;
3334
_renderer = new LineEditorRenderer(_console, this);
3435
_history = new LineEditorHistory();
36+
_input = new InputBuffer(_source);
3537

3638
KeyBindings = new KeyBindings();
3739
KeyBindings.AddDefault();
@@ -56,6 +58,7 @@ public static bool IsSupported(IAnsiConsole console)
5658
var state = new LineEditorState(Prompt, Text);
5759

5860
_history.Reset();
61+
_input.Initialize(KeyBindings);
5962
_renderer.Refresh(state);
6063

6164
while (true)
@@ -155,7 +158,7 @@ public static bool IsSupported(IAnsiConsole console)
155158

156159
// Get command
157160
var command = default(LineEditorCommand);
158-
var key = await _source.ReadKey(cancellationToken).ConfigureAwait(false);
161+
var key = await _input.ReadKey(MultiLine, cancellationToken).ConfigureAwait(false);
159162
if (key != null)
160163
{
161164
if (key.Value.KeyChar != 0 && !char.IsControl(key.Value.KeyChar))

0 commit comments

Comments
 (0)