Add support for scrolling past the screen end
Also add REPL example
This commit is contained in:
Родитель
88bf000899
Коммит
862978b974
|
@ -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]
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
Загрузка…
Ссылка в новой задаче