diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
index 6cf141e..0345315 100644
--- a/.config/dotnet-tools.json
+++ b/.config/dotnet-tools.json
@@ -7,6 +7,12 @@
"commands": [
"dotnet-cake"
]
+ },
+ "dotnet-example": {
+ "version": "1.3.1",
+ "commands": [
+ "dotnet-example"
+ ]
}
}
}
\ No newline at end of file
diff --git a/src/RadLine.Sandbox/.editorconfig b/examples/Demo/.editorconfig
similarity index 100%
rename from src/RadLine.Sandbox/.editorconfig
rename to examples/Demo/.editorconfig
diff --git a/src/RadLine.Sandbox/RadLine.Sandbox.csproj b/examples/Demo/Demo.csproj
similarity index 74%
rename from src/RadLine.Sandbox/RadLine.Sandbox.csproj
rename to examples/Demo/Demo.csproj
index 8a6567e..e6d257b 100644
--- a/src/RadLine.Sandbox/RadLine.Sandbox.csproj
+++ b/examples/Demo/Demo.csproj
@@ -3,6 +3,7 @@
Exe
net5.0
+ false
@@ -10,7 +11,7 @@
-
+
diff --git a/src/RadLine.Sandbox/Program.cs b/examples/Demo/Program.cs
similarity index 65%
rename from src/RadLine.Sandbox/Program.cs
rename to examples/Demo/Program.cs
index ac2a4e0..fd61a6d 100644
--- a/src/RadLine.Sandbox/Program.cs
+++ b/examples/Demo/Program.cs
@@ -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(ConsoleKey.I, ConsoleModifiers.Control);
+ editor.KeyBindings.Add(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;
diff --git a/examples/Repl/.editorconfig b/examples/Repl/.editorconfig
new file mode 100644
index 0000000..2adf83c
--- /dev/null
+++ b/examples/Repl/.editorconfig
@@ -0,0 +1,3 @@
+root = false
+
+[*.cs]
diff --git a/examples/Repl/Program.cs b/examples/Repl/Program.cs
new file mode 100644
index 0000000..c173828
--- /dev/null
+++ b/examples/Repl/Program.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(ConsoleKey.Backspace);
+ editor.KeyBindings.Add(ConsoleKey.Delete);
+ editor.KeyBindings.Add(ConsoleKey.Home);
+ editor.KeyBindings.Add(ConsoleKey.End);
+ editor.KeyBindings.Add(ConsoleKey.PageUp);
+ editor.KeyBindings.Add(ConsoleKey.PageDown);
+ editor.KeyBindings.Add(ConsoleKey.LeftArrow);
+ editor.KeyBindings.Add(ConsoleKey.RightArrow);
+ editor.KeyBindings.Add(ConsoleKey.LeftArrow, ConsoleModifiers.Control);
+ editor.KeyBindings.Add(ConsoleKey.RightArrow, ConsoleModifiers.Control);
+ editor.KeyBindings.Add(ConsoleKey.Enter);
+ editor.KeyBindings.Add(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());
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/Repl/Repl.csproj b/examples/Repl/Repl.csproj
new file mode 100644
index 0000000..e6d257b
--- /dev/null
+++ b/examples/Repl/Repl.csproj
@@ -0,0 +1,17 @@
+
+
+
+ Exe
+ net5.0
+ false
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/RadLine.Tests/Utilities/SimpleServiceProvider.cs b/src/RadLine.Tests/Utilities/SimpleServiceProvider.cs
index 6a4d0c7..4c2f4b5 100644
--- a/src/RadLine.Tests/Utilities/SimpleServiceProvider.cs
+++ b/src/RadLine.Tests/Utilities/SimpleServiceProvider.cs
@@ -1,8 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
namespace RadLine.Tests.Utilities
{
diff --git a/src/RadLine.sln b/src/RadLine.sln
index 9b51460..1b1c5b3 100644
--- a/src/RadLine.sln
+++ b/src/RadLine.sln
@@ -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
diff --git a/src/RadLine/ILineEditorPrompt.cs b/src/RadLine/ILineEditorPrompt.cs
index 0a36f59..70146e0 100644
--- a/src/RadLine/ILineEditorPrompt.cs
+++ b/src/RadLine/ILineEditorPrompt.cs
@@ -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);
}
}
diff --git a/src/RadLine/ILineEditorState.cs b/src/RadLine/ILineEditorState.cs
new file mode 100644
index 0000000..2ef3f20
--- /dev/null
+++ b/src/RadLine/ILineEditorState.cs
@@ -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; }
+ }
+}
diff --git a/src/RadLine/ITextCompletion.cs b/src/RadLine/ITextCompletion.cs
index 3edd25c..a0722d2 100644
--- a/src/RadLine/ITextCompletion.cs
+++ b/src/RadLine/ITextCompletion.cs
@@ -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? 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;
- }
- }
}
diff --git a/src/RadLine/ITextCompletionExtensions.cs b/src/RadLine/ITextCompletionExtensions.cs
new file mode 100644
index 0000000..3b5cbe9
--- /dev/null
+++ b/src/RadLine/ITextCompletionExtensions.cs
@@ -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;
+ }
+ }
+}
diff --git a/src/RadLine/Internal/Extensions/IntExtensions.cs b/src/RadLine/Internal/Extensions/IntExtensions.cs
index 195ca74..0d80ef3 100644
--- a/src/RadLine/Internal/Extensions/IntExtensions.cs
+++ b/src/RadLine/Internal/Extensions/IntExtensions.cs
@@ -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
diff --git a/src/RadLine/Internal/Extensions/StringExtensions.cs b/src/RadLine/Internal/Extensions/StringExtensions.cs
new file mode 100644
index 0000000..ca553c8
--- /dev/null
+++ b/src/RadLine/Internal/Extensions/StringExtensions.cs
@@ -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;
+ }
+ }
+}
diff --git a/src/RadLine/Internal/LineBufferRenderer.cs b/src/RadLine/Internal/LineBufferRenderer.cs
index 528a9ea..a9626fb 100644
--- a/src/RadLine/Internal/LineBufferRenderer.cs
+++ b/src/RadLine/Internal/LineBufferRenderer.cs
@@ -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);
}
}
}
diff --git a/src/RadLine/Internal/LineEditorState.cs b/src/RadLine/Internal/LineEditorState.cs
index 35d00f3..d0775ae 100644
--- a/src/RadLine/Internal/LineEditorState.cs
+++ b/src/RadLine/Internal/LineEditorState.cs
@@ -4,25 +4,49 @@ using System.Linq;
namespace RadLine
{
- internal sealed class LineEditorState
+ internal sealed class LineEditorState : ILineEditorState
{
private readonly List _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(new[] { new LineBuffer(text) });
- _prompt = prompt ?? throw new ArgumentNullException(nameof(prompt));
+ _lines = new List();
_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()
diff --git a/src/RadLine/Internal/LineRendererStrategy.cs b/src/RadLine/Internal/LineRendererStrategy.cs
deleted file mode 100644
index 5862fd7..0000000
--- a/src/RadLine/Internal/LineRendererStrategy.cs
+++ /dev/null
@@ -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;
- }
- }
-}
diff --git a/src/RadLine/Internal/Rendering/AnsiRenderingStrategy.cs b/src/RadLine/Internal/Rendering/AnsiRenderingStrategy.cs
deleted file mode 100644
index cd3a3ef..0000000
--- a/src/RadLine/Internal/Rendering/AnsiRenderingStrategy.cs
+++ /dev/null
@@ -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");
- }
- }
-}
diff --git a/src/RadLine/Internal/Rendering/FallbackRenderingStrategy.cs b/src/RadLine/Internal/Rendering/FallbackRenderingStrategy.cs
deleted file mode 100644
index 2587bff..0000000
--- a/src/RadLine/Internal/Rendering/FallbackRenderingStrategy.cs
+++ /dev/null
@@ -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();
- }
- }
-}
diff --git a/src/RadLine/LineEditor.cs b/src/RadLine/LineEditor.cs
index d5b0c35..68b0459 100644
--- a/src/RadLine/LineEditor.cs
+++ b/src/RadLine/LineEditor.cs
@@ -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(ConsoleKey.Backspace);
KeyBindings.Add(ConsoleKey.Delete);
KeyBindings.Add(ConsoleKey.Home);
@@ -47,11 +47,26 @@ namespace RadLine
KeyBindings.Add(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 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(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 action)
+ private void Move(LineEditorState state, Action 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);
+ }
}
}
}
diff --git a/src/RadLine/Prompts/LineEditorPrompt.cs b/src/RadLine/Prompts/LineEditorPrompt.cs
index 013a069..566be50 100644
--- a/src/RadLine/Prompts/LineEditorPrompt.cs
+++ b/src/RadLine/Prompts/LineEditorPrompt.cs
@@ -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)
{
diff --git a/src/RadLine/Prompts/LineNumberPrompt.cs b/src/RadLine/Prompts/LineNumberPrompt.cs
index 783bc89..4deb98e 100644
--- a/src/RadLine/Prompts/LineNumberPrompt.cs
+++ b/src/RadLine/Prompts/LineNumberPrompt.cs
@@ -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);
}
}
}
diff --git a/src/RadLine/RadLine.csproj b/src/RadLine/RadLine.csproj
index 82fdba3..defa5f4 100644
--- a/src/RadLine/RadLine.csproj
+++ b/src/RadLine/RadLine.csproj
@@ -1,4 +1,4 @@
-
+
net5.0;netstandard2.0
@@ -13,7 +13,7 @@
-
+
all