Add support for scrolling past the screen end

Also add REPL example
This commit is contained in:
Patrik Svensson 2021-05-22 11:16:04 +02:00
Родитель 88bf000899
Коммит 862978b974
24 изменённых файлов: 504 добавлений и 276 удалений

Просмотреть файл

@ -7,6 +7,12 @@
"commands": [
"dotnet-cake"
]
},
"dotnet-example": {
"version": "1.3.1",
"commands": [
"dotnet-example"
]
}
}
}

Просмотреть файл

Просмотреть файл

@ -3,6 +3,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
@ -10,7 +11,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\RadLine\RadLine.csproj" />
<ProjectReference Include="..\..\src\RadLine\RadLine.csproj" />
</ItemGroup>
</Project>

Просмотреть файл

@ -1,26 +1,26 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Spectre.Console;
namespace RadLine.Sandbox
namespace RadLine.Examples
{
public static class Program
{
public static async Task Main()
{
if (!Debugger.IsAttached)
if (!LineEditor.IsSupported(AnsiConsole.Console))
{
Debugger.Launch();
AnsiConsole.MarkupLine("The terminal does not support ANSI codes, or it isn't a terminal.");
}
// Create editor
var editor = new LineEditor()
{
MultiLine = true,
Text = "HELLO ABC WORLD DEF GHIJKLMN 🥰 PATRIK WAS HERE",
Prompt = new LineNumberPrompt(),
Text = "Hello, and welcome to RadLine!\nPress SHIFT+ENTER to insert a new line\nPress ENTER to submit",
Prompt = new LineNumberPrompt(new Style(foreground: Color.Yellow)),
Completion = new TestCompletion(),
Highlighter = new WordHighlighter()
.AddWord("git", new Style(foreground: Color.Yellow))
@ -31,31 +31,29 @@ namespace RadLine.Sandbox
.AddWord("commit", new Style(foreground: Color.Blue))
.AddWord("rebase", new Style(foreground: Color.Red))
.AddWord("Hello", new Style(foreground: Color.Blue))
.AddWord("Goodbye", new Style(foreground: Color.Green))
.AddWord("World", new Style(foreground: Color.Yellow))
.AddWord("Syntax", new Style(decoration: Decoration.Strikethrough))
.AddWord("Highlighting", new Style(decoration: Decoration.SlowBlink)),
.AddWord("SHIFT", new Style(foreground: Color.Grey))
.AddWord("ENTER", new Style(foreground: Color.Grey))
.AddWord("RadLine", new Style(foreground: Color.Yellow, decoration: Decoration.SlowBlink)),
};
// Add custom commands
editor.KeyBindings.Add<PrependSmiley>(ConsoleKey.I, ConsoleModifiers.Control);
editor.KeyBindings.Add<InsertSmiley>(ConsoleKey.I, ConsoleModifiers.Control);
// Read a line
// Read a line (or many)
var result = await editor.ReadLine(CancellationToken.None);
// Write the buffer
AnsiConsole.WriteLine();
AnsiConsole.Render(new Panel(result.EscapeMarkup())
.Header("[yellow]Commit details:[/]")
.Header("[yellow]Text:[/]")
.RoundedBorder());
}
}
public sealed class PrependSmiley : LineEditorCommand
public sealed class InsertSmiley : LineEditorCommand
{
public override void Execute(LineEditorContext context)
{
context.Execute(new PreviousWordCommand());
context.Buffer.Insert(":-)");
}
}
@ -71,7 +69,7 @@ namespace RadLine.Sandbox
if (context.Equals("git ", StringComparison.Ordinal))
{
return new[] { "init", "initialize", "push", "commit", "rebase" };
return new[] { "init", "branch", "push", "commit", "rebase" };
}
return null;

Просмотреть файл

@ -0,0 +1,3 @@
root = false
[*.cs]

57
examples/Repl/Program.cs Normal file
Просмотреть файл

@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Spectre.Console;
namespace RadLine.Examples
{
public static class Program
{
public static async Task Main()
{
if (!LineEditor.IsSupported(AnsiConsole.Console))
{
AnsiConsole.MarkupLine("The terminal does not support ANSI codes, or it isn't a terminal.");
}
AnsiConsole.Render(new FigletText("BASIC REPL"));
// Create editor
var editor = new LineEditor()
{
MultiLine = true,
Text = "PRINT \"Hello\"",
Prompt = new LineNumberPrompt(new Style(foreground: Color.Yellow)),
Highlighter = new WordHighlighter()
.AddWord("$", new Style(foreground: Color.Yellow))
.AddWord("INPUT", new Style(foreground: Color.Blue))
.AddWord("PRINT", new Style(foreground: Color.Blue)),
};
// Add custom commands
editor.KeyBindings.Clear();
editor.KeyBindings.Add<BackspaceCommand>(ConsoleKey.Backspace);
editor.KeyBindings.Add<DeleteCommand>(ConsoleKey.Delete);
editor.KeyBindings.Add<MoveHomeCommand>(ConsoleKey.Home);
editor.KeyBindings.Add<MoveEndCommand>(ConsoleKey.End);
editor.KeyBindings.Add<MoveFirstLineCommand>(ConsoleKey.PageUp);
editor.KeyBindings.Add<MoveLastLineCommand>(ConsoleKey.PageDown);
editor.KeyBindings.Add<MoveLeftCommand>(ConsoleKey.LeftArrow);
editor.KeyBindings.Add<MoveRightCommand>(ConsoleKey.RightArrow);
editor.KeyBindings.Add<PreviousWordCommand>(ConsoleKey.LeftArrow, ConsoleModifiers.Control);
editor.KeyBindings.Add<NextWordCommand>(ConsoleKey.RightArrow, ConsoleModifiers.Control);
editor.KeyBindings.Add<SubmitCommand>(ConsoleKey.Enter);
editor.KeyBindings.Add<NewLineCommand>(ConsoleKey.Enter, ConsoleModifiers.Shift);
// Read a line (or many)
var result = await editor.ReadLine(CancellationToken.None);
// Write the buffer
AnsiConsole.WriteLine();
AnsiConsole.Render(new Panel(result.EscapeMarkup())
.Header("[yellow]Program:[/]")
.RoundedBorder());
}
}
}

17
examples/Repl/Repl.csproj Normal file
Просмотреть файл

@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<AdditionalFiles Include="..\stylecop.json" Link="Properties/stylecop.json" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\RadLine\RadLine.csproj" />
</ItemGroup>
</Project>

Просмотреть файл

@ -1,8 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace RadLine.Tests.Utilities
{

Просмотреть файл

@ -3,11 +3,15 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.6.30114.105
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RadLine", "RadLine\RadLine.csproj", "{DFF3DD16-6AE7-4FDB-807F-F2E8A3166691}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RadLine", "RadLine\RadLine.csproj", "{DFF3DD16-6AE7-4FDB-807F-F2E8A3166691}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RadLine.Tests", "RadLine.Tests\RadLine.Tests.csproj", "{5DFDEEA4-7B61-47FD-AEA0-14FB569EBE62}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RadLine.Tests", "RadLine.Tests\RadLine.Tests.csproj", "{5DFDEEA4-7B61-47FD-AEA0-14FB569EBE62}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RadLine.Sandbox", "RadLine.Sandbox\RadLine.Sandbox.csproj", "{0DEC2184-5587-49C0-80FF-A963E6F494A9}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{4BC15770-BCF4-443D-B3BB-2DD0936D9B15}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Demo", "..\examples\Demo\Demo.csproj", "{D4041FF2-FA9D-499C-8523-E5461983A503}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Repl", "..\examples\Repl\Repl.csproj", "{B9E9618E-CB8F-4441-8D36-D96D01F73311}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -18,9 +22,6 @@ Global
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{DFF3DD16-6AE7-4FDB-807F-F2E8A3166691}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DFF3DD16-6AE7-4FDB-807F-F2E8A3166691}.Debug|Any CPU.Build.0 = Debug|Any CPU
@ -46,17 +47,39 @@ Global
{5DFDEEA4-7B61-47FD-AEA0-14FB569EBE62}.Release|x64.Build.0 = Release|Any CPU
{5DFDEEA4-7B61-47FD-AEA0-14FB569EBE62}.Release|x86.ActiveCfg = Release|Any CPU
{5DFDEEA4-7B61-47FD-AEA0-14FB569EBE62}.Release|x86.Build.0 = Release|Any CPU
{0DEC2184-5587-49C0-80FF-A963E6F494A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0DEC2184-5587-49C0-80FF-A963E6F494A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0DEC2184-5587-49C0-80FF-A963E6F494A9}.Debug|x64.ActiveCfg = Debug|Any CPU
{0DEC2184-5587-49C0-80FF-A963E6F494A9}.Debug|x64.Build.0 = Debug|Any CPU
{0DEC2184-5587-49C0-80FF-A963E6F494A9}.Debug|x86.ActiveCfg = Debug|Any CPU
{0DEC2184-5587-49C0-80FF-A963E6F494A9}.Debug|x86.Build.0 = Debug|Any CPU
{0DEC2184-5587-49C0-80FF-A963E6F494A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0DEC2184-5587-49C0-80FF-A963E6F494A9}.Release|Any CPU.Build.0 = Release|Any CPU
{0DEC2184-5587-49C0-80FF-A963E6F494A9}.Release|x64.ActiveCfg = Release|Any CPU
{0DEC2184-5587-49C0-80FF-A963E6F494A9}.Release|x64.Build.0 = Release|Any CPU
{0DEC2184-5587-49C0-80FF-A963E6F494A9}.Release|x86.ActiveCfg = Release|Any CPU
{0DEC2184-5587-49C0-80FF-A963E6F494A9}.Release|x86.Build.0 = Release|Any CPU
{D4041FF2-FA9D-499C-8523-E5461983A503}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D4041FF2-FA9D-499C-8523-E5461983A503}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D4041FF2-FA9D-499C-8523-E5461983A503}.Debug|x64.ActiveCfg = Debug|Any CPU
{D4041FF2-FA9D-499C-8523-E5461983A503}.Debug|x64.Build.0 = Debug|Any CPU
{D4041FF2-FA9D-499C-8523-E5461983A503}.Debug|x86.ActiveCfg = Debug|Any CPU
{D4041FF2-FA9D-499C-8523-E5461983A503}.Debug|x86.Build.0 = Debug|Any CPU
{D4041FF2-FA9D-499C-8523-E5461983A503}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D4041FF2-FA9D-499C-8523-E5461983A503}.Release|Any CPU.Build.0 = Release|Any CPU
{D4041FF2-FA9D-499C-8523-E5461983A503}.Release|x64.ActiveCfg = Release|Any CPU
{D4041FF2-FA9D-499C-8523-E5461983A503}.Release|x64.Build.0 = Release|Any CPU
{D4041FF2-FA9D-499C-8523-E5461983A503}.Release|x86.ActiveCfg = Release|Any CPU
{D4041FF2-FA9D-499C-8523-E5461983A503}.Release|x86.Build.0 = Release|Any CPU
{B9E9618E-CB8F-4441-8D36-D96D01F73311}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B9E9618E-CB8F-4441-8D36-D96D01F73311}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B9E9618E-CB8F-4441-8D36-D96D01F73311}.Debug|x64.ActiveCfg = Debug|Any CPU
{B9E9618E-CB8F-4441-8D36-D96D01F73311}.Debug|x64.Build.0 = Debug|Any CPU
{B9E9618E-CB8F-4441-8D36-D96D01F73311}.Debug|x86.ActiveCfg = Debug|Any CPU
{B9E9618E-CB8F-4441-8D36-D96D01F73311}.Debug|x86.Build.0 = Debug|Any CPU
{B9E9618E-CB8F-4441-8D36-D96D01F73311}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B9E9618E-CB8F-4441-8D36-D96D01F73311}.Release|Any CPU.Build.0 = Release|Any CPU
{B9E9618E-CB8F-4441-8D36-D96D01F73311}.Release|x64.ActiveCfg = Release|Any CPU
{B9E9618E-CB8F-4441-8D36-D96D01F73311}.Release|x64.Build.0 = Release|Any CPU
{B9E9618E-CB8F-4441-8D36-D96D01F73311}.Release|x86.ActiveCfg = Release|Any CPU
{B9E9618E-CB8F-4441-8D36-D96D01F73311}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{D4041FF2-FA9D-499C-8523-E5461983A503} = {4BC15770-BCF4-443D-B3BB-2DD0936D9B15}
{B9E9618E-CB8F-4441-8D36-D96D01F73311} = {4BC15770-BCF4-443D-B3BB-2DD0936D9B15}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {704BD8C0-DF3C-41CA-84B7-5A2634CB9129}
EndGlobalSection
EndGlobal

Просмотреть файл

@ -4,6 +4,6 @@ namespace RadLine
{
public interface ILineEditorPrompt
{
(Markup Markup, int Margin) GetPrompt(int line);
(Markup Markup, int Margin) GetPrompt(ILineEditorState state, int line);
}
}

Просмотреть файл

@ -0,0 +1,10 @@
namespace RadLine
{
public interface ILineEditorState
{
public int LineIndex { get; }
public int LineCount { get; }
public bool IsFirstLine { get; }
public bool IsLastLine { get; }
}
}

Просмотреть файл

@ -1,6 +1,4 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace RadLine
{
@ -8,23 +6,4 @@ namespace RadLine
{
public IEnumerable<string>? GetCompletions(string prefix, string word, string suffix);
}
public static class TextCompletionExtensions
{
public static bool TryGetCompletions(
this ITextCompletion completion,
string prefix, string word, string suffix,
[NotNullWhen(true)] out string[]? result)
{
var completions = completion.GetCompletions(prefix, word, suffix);
if (completions == null || !completions.Any())
{
result = null;
return false;
}
result = completions.ToArray();
return true;
}
}
}

Просмотреть файл

@ -0,0 +1,24 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace RadLine
{
public static class ITextCompletionExtensions
{
public static bool TryGetCompletions(
this ITextCompletion completion,
string prefix, string word, string suffix,
[NotNullWhen(true)] out string[]? result)
{
var completions = completion.GetCompletions(prefix, word, suffix);
if (completions == null || !completions.Any())
{
result = null;
return false;
}
result = completions.ToArray();
return true;
}
}
}

Просмотреть файл

@ -1,9 +1,3 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace RadLine
{
internal static class IntExtensions

Просмотреть файл

@ -0,0 +1,12 @@
namespace RadLine
{
internal static class StringExtensions
{
public static string NormalizeNewLines(this string? text)
{
text = text?.Replace("\r\n", "\n");
text ??= string.Empty;
return text;
}
}
}

Просмотреть файл

@ -1,13 +1,17 @@
using System;
using System.Linq;
using System.Text;
using Spectre.Console;
using Spectre.Console.Advanced;
using Spectre.Console.Rendering;
namespace RadLine
{
internal sealed class LineBufferRenderer
{
private readonly IAnsiConsole _console;
private readonly AnsiRenderingStrategy _ansiRendererer;
private readonly FallbackRenderingStrategy _fallbackRender;
private readonly IHighlighterAccessor _accessor;
private bool _initialized;
public LineBufferRenderer(IAnsiConsole console, IHighlighterAccessor accessor)
{
@ -17,20 +21,213 @@ namespace RadLine
}
_console = console ?? throw new ArgumentNullException(nameof(console));
_ansiRendererer = new AnsiRenderingStrategy(console, accessor);
_fallbackRender = new FallbackRenderingStrategy(console, accessor);
_accessor = accessor;
}
public void Refresh(LineEditorState state)
{
if (!_console.Profile.Capabilities.Ansi)
{
throw new NotSupportedException("Terminal does not support ANSI");
}
var builder = new StringBuilder();
// First render?
if (!_initialized)
{
Initialize(state, builder);
return;
}
// Set cursor to home position
builder.Append("\u001b[1;1H");
var rowIndex = 0;
var height = _console.Profile.Height;
var lineCount = state.LineCount - 1;
var middleOfList = height / 2;
var offset = (height % 2 == 0) ? 1 : 0;
var pointer = state.LineIndex;
// Calculate the visible part
var scrollable = lineCount >= height;
if (scrollable)
{
var skip = Math.Max(0, state.LineIndex - middleOfList);
if (lineCount - state.LineIndex < middleOfList)
{
// Pointer should be below the end of the list
var diff = middleOfList - (lineCount - state.LineIndex);
skip -= diff - offset;
pointer = middleOfList + diff - offset;
}
else
{
// Take skip into account
pointer -= skip;
}
rowIndex = skip;
}
// Render all lines
for (var i = 0; i < Math.Min(state.LineCount, height); i++)
{
// Set cursor to beginning of line
builder.Append("\u001b[1G");
// Render the line
var (prompt, margin) = state.Prompt.GetPrompt(state, rowIndex);
AppendLine(builder, state.GetBufferAt(rowIndex), prompt, margin, 0);
// Move cursor down
builder.Append("\u001b[1E");
rowIndex++;
}
// Position the cursor at the right line
builder.Append("\u001b[").Append(pointer + 1).Append(";1H");
// Flush
_console.WriteAnsi(builder.ToString());
// Refresh the current line
RenderLine(state);
}
private void Initialize(LineEditorState state, StringBuilder builder)
{
_initialized = true;
// Everything fit inside the terminal?
if (state.LineCount < _console.Profile.Height)
{
_console.Cursor.Hide();
for (var i = 0; i < state.LineCount; i++)
{
var (prompt, margin) = state.Prompt.GetPrompt(state, i);
AppendLine(builder, state.GetBufferAt(i), prompt, margin, 0);
if (i != state.LineCount - 1)
{
builder.Append(Environment.NewLine);
}
}
_console.WriteAnsi(builder.ToString());
_console.Cursor.Show();
// Move to the last line
state.Move(state.LineCount);
RenderLine(state);
}
}
public void RenderLine(LineEditorState state, int? cursorPosition = null)
{
if (_console.Profile.Capabilities.Ansi)
if (!_console.Profile.Capabilities.Ansi)
{
_ansiRendererer.Render(state, cursorPosition);
throw new NotSupportedException("Terminal does not support ANSI");
}
else
var builder = new StringBuilder();
// Prepare
builder.Append("\u001b[?7l"); // Autowrap off
builder.Append("\u001b[2K"); // Clear the current line
builder.Append("\u001b[1G"); // Set cursor to beginning of line
// Append the whole line
var (prompt, margin) = state.Prompt.GetPrompt(state, state.LineIndex);
var position = AppendLine(builder, state.Buffer, prompt, margin, cursorPosition ?? state.Buffer.CursorPosition);
// Move the cursor to the right position
var cursorPos = position + prompt.Length + margin + 1;
builder.Append("\u001b[").Append(cursorPos).Append('G');
// Flush
_console.WriteAnsi(builder.ToString());
// Turn on auto wrap
builder.Append("\u001b[?7h");
}
private int? AppendLine(StringBuilder builder, LineBuffer buffer, Markup prompt, int margin, int cursorPosition)
{
// Render the prompt
builder.Append(_console.ToAnsi(prompt));
builder.Append(new string(' ', margin));
// Build the buffer
var width = _console.Profile.Width - prompt.Length - margin - 1;
var (content, position) = BuildLine(buffer, width, cursorPosition);
var output = _console.ToAnsi(Highlight(content));
if (output.Length < width)
{
_fallbackRender.Render(state, cursorPosition);
output = output.PadRight(width);
}
// Output the buffer
builder.Append(output);
// Return the position
return position;
}
private IRenderable Highlight(string text)
{
var highlighter = _accessor.Highlighter;
if (highlighter == null)
{
return new Text(text);
}
var paragraph = new Paragraph();
foreach (var token in StringTokenizer.Tokenize(text))
{
var style = string.IsNullOrWhiteSpace(token) ? null : highlighter.Highlight(token);
paragraph.Append(token, style);
}
return paragraph;
}
private static (string Content, int? Cursor) BuildLine(LineBuffer buffer, int width, int position)
{
var middleOfList = width / 2;
var skip = 0;
var take = buffer.Content.Length;
var pointer = position;
var scrollable = buffer.Content.Length > width;
if (scrollable)
{
skip = Math.Max(0, position - middleOfList);
take = Math.Min(width, buffer.Content.Length - skip);
if (buffer.Content.Length - position < middleOfList)
{
// Pointer should be below the end of the list
var diff = middleOfList - (buffer.Content.Length - position);
skip -= diff;
take += diff;
pointer = middleOfList + diff;
}
else
{
// Take skip into account
pointer -= skip;
}
}
return (
string.Concat(buffer.Content.Skip(skip).Take(take)),
pointer);
}
}
}

Просмотреть файл

@ -4,25 +4,49 @@ using System.Linq;
namespace RadLine
{
internal sealed class LineEditorState
internal sealed class LineEditorState : ILineEditorState
{
private readonly List<LineBuffer> _lines;
private readonly ILineEditorPrompt _prompt;
private int _lineIndex;
public ILineEditorPrompt Prompt { get; }
public int LineIndex => _lineIndex;
public int LineCount => _lines.Count;
public bool IsFirstLine => _lineIndex == 0;
public bool IsLastLine => _lineIndex == _lines.Count - 1;
public ILineEditorPrompt Prompt => _prompt;
public LineBuffer Buffer => _lines[_lineIndex];
public string Text => string.Join(Environment.NewLine, _lines.Select(x => x.Content));
public LineEditorState(ILineEditorPrompt prompt, string text)
{
_lines = new List<LineBuffer>(new[] { new LineBuffer(text) });
_prompt = prompt ?? throw new ArgumentNullException(nameof(prompt));
_lines = new List<LineBuffer>();
_lineIndex = 0;
Prompt = prompt ?? throw new ArgumentNullException(nameof(prompt));
// Add all lines
foreach (var line in text.NormalizeNewLines().Split(new[] { '\n' }))
{
_lines.Add(new LineBuffer(line));
}
// No lines?
if (_lines.Count == 0)
{
_lines.Add(new LineBuffer());
}
}
public LineBuffer GetBufferAt(int line)
{
return _lines[line];
}
public void Move(int line)
{
_lineIndex = Math.Max(0, Math.Min(line, LineCount - 1));
}
public bool MoveUp()

Просмотреть файл

@ -1,71 +0,0 @@
using System;
using System.Linq;
using Spectre.Console;
using Spectre.Console.Rendering;
namespace RadLine
{
internal abstract class LineRenderingStrategy
{
private readonly IHighlighterAccessor _accessor;
protected LineRenderingStrategy(IHighlighterAccessor accessor)
{
_accessor = accessor ?? throw new ArgumentNullException(nameof(accessor));
}
public abstract void Render(LineEditorState state, int? cursorPosition);
protected (string Content, int? Cursor) BuildLine(LineBuffer buffer, int width, int position)
{
var middleOfList = width / 2;
var skip = 0;
var take = buffer.Content.Length;
var pointer = position;
var scrollable = buffer.Content.Length > width;
if (scrollable)
{
skip = Math.Max(0, position - middleOfList);
take = Math.Min(width, buffer.Content.Length - skip);
if (buffer.Content.Length - position < middleOfList)
{
// Pointer should be below the end of the list
var diff = middleOfList - (buffer.Content.Length - position);
skip -= diff;
take += diff;
pointer = middleOfList + diff;
}
else
{
// Take skip into account
pointer -= skip;
}
}
return (
string.Concat(buffer.Content.Skip(skip).Take(take)),
pointer);
}
protected IRenderable Highlight(string text)
{
var highlighter = _accessor.Highlighter;
if (highlighter == null)
{
return new Text(text);
}
var paragraph = new Paragraph();
foreach (var token in StringTokenizer.Tokenize(text))
{
var style = string.IsNullOrWhiteSpace(token) ? null : highlighter.Highlight(token);
paragraph.Append(token, style);
}
return paragraph;
}
}
}

Просмотреть файл

@ -1,51 +0,0 @@
using System;
using System.Text;
using Spectre.Console;
using Spectre.Console.Advanced;
namespace RadLine
{
internal sealed class AnsiRenderingStrategy : LineRenderingStrategy
{
private readonly IAnsiConsole _console;
public AnsiRenderingStrategy(IAnsiConsole console, IHighlighterAccessor accessor)
: base(accessor)
{
_console = console ?? throw new ArgumentNullException(nameof(console));
}
public override void Render(LineEditorState state, int? cursorPosition)
{
var builder = new StringBuilder();
var (prompt, margin) = state.Prompt.GetPrompt(state.LineIndex);
// Prepare
builder.Append("\u001b[?7l"); // Autowrap off
builder.Append("\u001b[2K"); // Clear the current line
builder.Append("\u001b[1G"); // Set cursor to beginning of line
// Render the prompt
builder.Append(_console.ToAnsi(prompt));
builder.Append(new string(' ', margin));
// Build the buffer
var width = _console.Profile.Width - prompt.Length - margin - 1;
var (content, position) = BuildLine(state.Buffer, width, cursorPosition ?? state.Buffer.CursorPosition);
// Output the buffer
builder.Append(_console.ToAnsi(Highlight(content)));
// Move the cursor to the right position
var cursorPos = position + prompt.Length + margin + 1;
builder.Append("\u001b[").Append(cursorPos).Append('G');
// Flush
_console.WriteAnsi(builder.ToString());
// Turn on auto wrap
builder.Append("\u001b[?7h");
}
}
}

Просмотреть файл

@ -1,46 +0,0 @@
using System;
using Spectre.Console;
namespace RadLine
{
internal sealed class FallbackRenderingStrategy : LineRenderingStrategy
{
private readonly IAnsiConsole _console;
public FallbackRenderingStrategy(IAnsiConsole console, IHighlighterAccessor accessor)
: base(accessor)
{
_console = console ?? throw new ArgumentNullException(nameof(console));
}
public override void Render(LineEditorState state, int? cursorPosition)
{
var (prompt, margin) = state.Prompt.GetPrompt(state.LineIndex);
// Hide the cursor
_console.Cursor.Hide();
// Clear the current line
Console.CursorLeft = 0;
Console.Write(new string(' ', _console.Profile.Width));
Console.CursorLeft = 0;
// Render the prompt
_console.Write(prompt);
_console.Write(new string(' ', margin));
// Build the buffer
var width = _console.Profile.Width - prompt.Length - margin - 1;
var (content, position) = BuildLine(state.Buffer, width, cursorPosition ?? state.Buffer.CursorPosition);
// Write the buffer
_console.Write(Highlight(content));
// Move the cursor to the right position
Console.CursorLeft = (position ?? 0) + prompt.Length + margin;
// Show the cursor
_console.Cursor.Show();
}
}
}

Просмотреть файл

@ -10,10 +10,9 @@ namespace RadLine
private readonly IInputSource _source;
private readonly IServiceProvider? _provider;
private readonly IAnsiConsole _console;
private readonly LineBufferRenderer _presenter;
private readonly LineBufferRenderer _renderer;
public KeyBindings KeyBindings { get; }
public bool MultiLine { get; init; } = false;
public string Text { get; init; } = string.Empty;
@ -26,11 +25,12 @@ namespace RadLine
_console = terminal ?? AnsiConsole.Console;
_source = source ?? new DefaultInputSource(_console);
_provider = provider;
_presenter = new LineBufferRenderer(_console, this);
_renderer = new LineBufferRenderer(_console, this);
KeyBindings = new KeyBindings();
KeyBindings.Add(ConsoleKey.Tab, () => new AutoCompleteCommand(AutoComplete.Next));
KeyBindings.Add(ConsoleKey.Tab, ConsoleModifiers.Control, () => new AutoCompleteCommand(AutoComplete.Previous));
KeyBindings.Add<BackspaceCommand>(ConsoleKey.Backspace);
KeyBindings.Add<DeleteCommand>(ConsoleKey.Delete);
KeyBindings.Add<MoveHomeCommand>(ConsoleKey.Home);
@ -47,11 +47,26 @@ namespace RadLine
KeyBindings.Add<NewLineCommand>(ConsoleKey.Enter, ConsoleModifiers.Shift);
}
public static bool IsSupported(IAnsiConsole console)
{
if (console is null)
{
throw new ArgumentNullException(nameof(console));
}
return
console.Profile.Out.IsTerminal &&
console.Profile.Capabilities.Ansi &&
console.Profile.Capabilities.Interactive;
}
public async Task<string?> ReadLine(CancellationToken cancellationToken)
{
var cancelled = false;
var state = new LineEditorState(Prompt, Text);
_renderer.Refresh(state);
while (true)
{
var result = await ReadLine(state, cancellationToken).ConfigureAwait(false);
@ -87,7 +102,7 @@ namespace RadLine
}
}
_presenter.RenderLine(state, cursorPosition: 0);
_renderer.RenderLine(state, cursorPosition: 0);
// Move the cursor to the last line
while (state.MoveDown())
@ -111,8 +126,6 @@ namespace RadLine
provider.RegisterOptional<ITextCompletion, ITextCompletion>(Completion);
var context = new LineEditorContext(state.Buffer, provider);
_presenter.RenderLine(state);
while (true)
{
if (cancellationToken.IsCancellationRequested)
@ -148,7 +161,7 @@ namespace RadLine
}
// Render the line
_presenter.RenderLine(state);
_renderer.RenderLine(state);
}
}
@ -156,20 +169,33 @@ namespace RadLine
{
using (_console.HideCursor())
{
_presenter.RenderLine(state, cursorPosition: 0);
state.AddLine();
// Moving the cursor won't work here if we're at
// the bottom of the screen, so let's insert a new line.
_console.WriteLine();
if (state.LineCount > _console.Profile.Height)
{
_console.Cursor.MoveDown();
}
else
{
_console.WriteLine();
}
if (state.LineCount > _console.Profile.Height)
{
_renderer.Refresh(state);
}
else
{
_renderer.RenderLine(state, cursorPosition: 0);
}
}
}
private void MoveUp(LineEditorState state)
{
Move(state, state =>
Move(state, (state, moveCursor) =>
{
if (state.MoveUp())
if (state.MoveUp() && moveCursor)
{
_console.Cursor.MoveUp();
}
@ -178,9 +204,9 @@ namespace RadLine
private void MoveDown(LineEditorState state)
{
Move(state, state =>
Move(state, (state, moveCursor) =>
{
if (state.MoveDown())
if (state.MoveDown() && moveCursor)
{
_console.Cursor.MoveDown();
}
@ -189,34 +215,62 @@ namespace RadLine
private void MoveFirst(LineEditorState state)
{
Move(state, state =>
Move(state, (state, moveCursor) =>
{
while (state.MoveUp())
{
_console.Cursor.MoveUp();
if (moveCursor)
{
_console.Cursor.MoveUp();
}
}
});
}
private void MoveLast(LineEditorState state)
{
Move(state, state =>
Move(state, (state, moveCursor) =>
{
while (state.MoveDown())
{
_console.Cursor.MoveDown();
if (moveCursor)
{
_console.Cursor.MoveDown();
}
}
});
}
private void Move(LineEditorState state, Action<LineEditorState> action)
private void Move(LineEditorState state, Action<LineEditorState, bool> action)
{
using (_console.HideCursor())
{
_presenter.RenderLine(state, cursorPosition: 0);
var position = state.Buffer.Position;
action(state);
state.Buffer.Move(position);
if (state.LineCount > _console.Profile.Height)
{
// Get the current position
var position = state.Buffer.Position;
// Refresh everything
action(state, true);
_renderer.Refresh(state);
// Re-render the current line at the correct position
state.Buffer.Move(position);
_renderer.RenderLine(state);
}
else
{
// Get the current position
var position = state.Buffer.Position;
// Reset the line
_renderer.RenderLine(state, cursorPosition: 0);
action(state, true);
// Render the current line at the correct position
state.Buffer.Move(position);
_renderer.RenderLine(state);
}
}
}
}

Просмотреть файл

@ -29,7 +29,7 @@ namespace RadLine
}
}
public (Markup Markup, int Margin) GetPrompt(int line)
public (Markup Markup, int Margin) GetPrompt(ILineEditorState state, int line)
{
if (line == 0)
{

Просмотреть файл

@ -11,9 +11,9 @@ namespace RadLine
_style = style ?? new Style(foreground: Color.Yellow, background: Color.Blue);
}
public (Markup Markup, int Margin) GetPrompt(int line)
public (Markup Markup, int Margin) GetPrompt(ILineEditorState state, int line)
{
return (new Markup(line.ToString("D2"), _style), 1);
return (new Markup((line + 1).ToString("D2"), _style), 1);
}
}
}

Просмотреть файл

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net5.0;netstandard2.0</TargetFrameworks>
@ -13,7 +13,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Spectre.Console" Version="0.39.1-preview.0.26" />
<PackageReference Include="Spectre.Console" Version="0.39.1-preview.0.13" />
<PackageReference Include="IsExternalInit" Version="1.0.0" PrivateAssets="all" />
<PackageReference Include="Nullable" Version="1.3.0">
<PrivateAssets>all</PrivateAssets>