This commit is contained in:
Patrik Svensson 2021-05-21 15:54:54 +02:00
Коммит c6aece9514
79 изменённых файлов: 3526 добавлений и 0 удалений

12
.config/dotnet-tools.json Normal file
Просмотреть файл

@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"cake.tool": {
"version": "1.1.0",
"commands": [
"dotnet-cake"
]
}
}
}

178
.editorconfig Normal file
Просмотреть файл

@ -0,0 +1,178 @@
root = true
[*]
charset = utf-8
end_of_line = CRLF
indent_style = space
indent_size = 4
insert_final_newline = false
trim_trailing_whitespace = true
[*.sln]
indent_style = tab
[*.{csproj,vbproj,vcxproj,vcxproj.filters}]
indent_size = 2
[*.{xml,config,props,targets,nuspec,ruleset}]
indent_size = 2
[*.{yml,yaml}]
indent_size = 2
[*.json]
indent_size = 2
[*.md]
trim_trailing_whitespace = false
[*.sh]
end_of_line = lf
[*.cs]
# Sort using and Import directives with System.* appearing first
dotnet_sort_system_directives_first = true
dotnet_separate_import_directive_groups = false
# Avoid "this." and "Me." if not necessary
dotnet_style_qualification_for_field = false:refactoring
dotnet_style_qualification_for_property = false:refactoring
dotnet_style_qualification_for_method = false:refactoring
dotnet_style_qualification_for_event = false:refactoring
# Use language keywords instead of framework type names for type references
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
dotnet_style_predefined_type_for_member_access = true:suggestion
# Suggest more modern language features when available
dotnet_style_object_initializer = true:suggestion
dotnet_style_collection_initializer = true:suggestion
dotnet_style_coalesce_expression = true:suggestion
dotnet_style_null_propagation = true:suggestion
dotnet_style_explicit_tuple_names = true:suggestion
# Non-private static fields are PascalCase
dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.symbols = non_private_static_fields
dotnet_naming_rule.non_private_static_fields_should_be_pascal_case.style = non_private_static_field_style
dotnet_naming_symbols.non_private_static_fields.applicable_kinds = field
dotnet_naming_symbols.non_private_static_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected
dotnet_naming_symbols.non_private_static_fields.required_modifiers = static
dotnet_naming_style.non_private_static_field_style.capitalization = pascal_case
# Non-private readonly fields are PascalCase
dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.symbols = non_private_readonly_fields
dotnet_naming_rule.non_private_readonly_fields_should_be_pascal_case.style = non_private_readonly_field_style
dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field
dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, protected, internal, protected_internal, private_protected
dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly
dotnet_naming_style.non_private_readonly_field_style.capitalization = pascal_case
# Constants are PascalCase
dotnet_naming_rule.constants_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.constants_should_be_pascal_case.symbols = constants
dotnet_naming_rule.constants_should_be_pascal_case.style = constant_style
dotnet_naming_symbols.constants.applicable_kinds = field, local
dotnet_naming_symbols.constants.required_modifiers = const
dotnet_naming_style.constant_style.capitalization = pascal_case
# Instance fields are camelCase and start with _
dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion
dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields
dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style
dotnet_naming_symbols.instance_fields.applicable_kinds = field
dotnet_naming_style.instance_field_style.capitalization = camel_case
dotnet_naming_style.instance_field_style.required_prefix = _
# Locals and parameters are camelCase
dotnet_naming_rule.locals_should_be_camel_case.severity = suggestion
dotnet_naming_rule.locals_should_be_camel_case.symbols = locals_and_parameters
dotnet_naming_rule.locals_should_be_camel_case.style = camel_case_style
dotnet_naming_symbols.locals_and_parameters.applicable_kinds = parameter, local
dotnet_naming_style.camel_case_style.capitalization = camel_case
# Local functions are PascalCase
dotnet_naming_rule.local_functions_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.local_functions_should_be_pascal_case.symbols = local_functions
dotnet_naming_rule.local_functions_should_be_pascal_case.style = local_function_style
dotnet_naming_symbols.local_functions.applicable_kinds = local_function
dotnet_naming_style.local_function_style.capitalization = pascal_case
# By default, name items with PascalCase
dotnet_naming_rule.members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.members_should_be_pascal_case.symbols = all_members
dotnet_naming_rule.members_should_be_pascal_case.style = pascal_case_style
dotnet_naming_symbols.all_members.applicable_kinds = *
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
# Newline settings
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_between_query_expression_clauses = true
# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true
csharp_indent_switch_labels = true
csharp_indent_labels = flush_left
# Prefer "var" everywhere
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
csharp_style_var_elsewhere = true:suggestion
# Prefer method-like constructs to have a block body
csharp_style_expression_bodied_methods = false:none
csharp_style_expression_bodied_constructors = false:none
csharp_style_expression_bodied_operators = false:none
# Prefer property-like constructs to have an expression-body
csharp_style_expression_bodied_properties = true:none
csharp_style_expression_bodied_indexers = true:none
csharp_style_expression_bodied_accessors = true:none
# Suggest more modern language features when available
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
csharp_style_inlined_variable_declaration = true:suggestion
csharp_style_throw_expression = true:suggestion
csharp_style_conditional_delegate_call = true:suggestion
# Space preferences
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_comma = true
csharp_space_after_dot = false
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_after_semicolon_in_for_statement = true
csharp_space_around_binary_operators = before_and_after
csharp_space_around_declaration_statements = do_not_ignore
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_before_comma = false
csharp_space_before_dot = false
csharp_space_before_open_square_brackets = false
csharp_space_before_semicolon_in_for_statement = false
csharp_space_between_empty_square_brackets = false
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_name_and_open_parenthesis = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
csharp_space_between_square_brackets = false
# Blocks are allowed
csharp_prefer_braces = true:silent
csharp_preserve_single_line_blocks = true
csharp_preserve_single_line_statements = true
# warning RS0037: PublicAPI.txt is missing '#nullable enable'
dotnet_diagnostic.RS0037.severity = none

1
.github/funding.yml поставляемый Normal file
Просмотреть файл

@ -0,0 +1 @@
github: patriksvensson

49
.github/workflows/ci.yaml поставляемый Normal file
Просмотреть файл

@ -0,0 +1,49 @@
name: Continuous Integration
on: pull_request
env:
# Set the DOTNET_SKIP_FIRST_TIME_EXPERIENCE environment variable to stop wasting time caching packages
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
# Disable sending usage data to Microsoft
DOTNET_CLI_TELEMETRY_OPTOUT: true
jobs:
###################################################
# BUILD
###################################################
build:
name: Build
if: "!contains(github.event.head_commit.message, 'skip-ci')"
strategy:
matrix:
kind: ['linux', 'windows', 'macOS']
include:
- kind: linux
os: ubuntu-latest
- kind: windows
os: windows-latest
- kind: macOS
os: macos-latest
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: 'Get Git tags'
run: git fetch --tags
shell: bash
- name: Setup dotnet
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.202
- name: Build
shell: bash
run: |
dotnet tool restore
dotnet cake

88
.github/workflows/publish.yaml поставляемый Normal file
Просмотреть файл

@ -0,0 +1,88 @@
name: Publish
on:
push:
tags:
- '*'
branches:
- main
paths:
- 'src/**'
env:
# Set the DOTNET_SKIP_FIRST_TIME_EXPERIENCE environment variable to stop wasting time caching packages
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
# Disable sending usage data to Microsoft
DOTNET_CLI_TELEMETRY_OPTOUT: true
jobs:
###################################################
# BUILD
###################################################
build:
name: Build
if: "!contains(github.event.head_commit.message, 'skip-ci')"
strategy:
matrix:
kind: ['linux', 'windows', 'macOS']
include:
- kind: linux
os: ubuntu-latest
- kind: windows
os: windows-latest
- kind: macOS
os: macos-latest
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: 'Get Git tags'
run: git fetch --tags
shell: bash
- name: Setup dotnet
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.202
- name: Build
shell: bash
run: |
dotnet tool restore
dotnet cake
###################################################
# PUBLISH
###################################################
publish:
name: Publish
needs: [build]
if: "!contains(github.event.head_commit.message, 'skip-ci')"
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: 'Get Git tags'
run: git fetch --tags
shell: bash
- name: Setup dotnet
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.202
- name: Publish
shell: bash
run: |
dotnet tool restore
dotnet cake --target="publish" \
--nuget-key="${{secrets.NUGET_API_KEY}}"

91
.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,91 @@
# Misc folders
[Bb]in/
[Oo]bj/
[Tt]emp/
[Pp]ackages/
/.artifacts/
/[Tt]ools/
.idea
.DS_Store
# Cakeup
cakeup-x86_64-latest.exe
# .NET Core CLI
/.dotnet/
/.packages/
dotnet-install.sh*
*.lock.json
# Visual Studio
.vs/
.vscode/
launchSettings.json
*.sln.ide/
# Rider
src/.idea/**/workspace.xml
src/.idea/**/tasks.xml
src/.idea/dictionaries
src/.idea/**/dataSources/
src/.idea/**/dataSources.ids
src/.idea/**/dataSources.xml
src/.idea/**/dataSources.local.xml
src/.idea/**/sqlDataSources.xml
src/.idea/**/dynamic.xml
src/.idea/**/uiDesigner.xml
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.sln.docstates
*.userprefs
*.GhostDoc.xml
*StyleCop.Cache
# Build results
[Dd]ebug/
[Rr]elease/
x64/
*_i.c
*_p.c
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.log
*.vspscc
*.vssscc
.builds
# Visual Studio profiler
*.psess
*.vsp
*.vspx
# ReSharper is a .NET coding add-in
_ReSharper*
# NCrunch
.*crunch*.local.xml
_NCrunch_*
# NuGet Packages Directory
packages
# Windows
Thumbs.db
*.received.*

46
CODE_OF_CONDUCT.md Normal file
Просмотреть файл

@ -0,0 +1,46 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@spectresystems.se. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

21
LICENSE.md Normal file
Просмотреть файл

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Patrik Svensson, Phil Scott
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

24
README.md Normal file
Просмотреть файл

@ -0,0 +1,24 @@
# RadLine
This is a preview of the RadLine library.
At this point, we will not be accepting pull requests for new functionality.
# Usage
See the `RadLine.Sandbox` project for usage examples.
# Building
```
> dotnet tool restore
> dotnet cake
```
# Known issues
* Terminal is not set in raw mode, so some key combinations might not
work in certain terminals.
* Any modifier with `ENTER` key does not register on macOS, due to a bug
in the System.Console.ReadKey implementation. We will be moving away
from using this before release.
* Lines do not update properly when moving past vertical screen buffer boundaries.

79
build.cake Normal file
Просмотреть файл

@ -0,0 +1,79 @@
var target = Argument("target", "Default");
var configuration = Argument("configuration", "Release");
////////////////////////////////////////////////////////////////
// Tasks
Task("Build")
.Does(context =>
{
DotNetCoreBuild("./src/RadLine.sln", new DotNetCoreBuildSettings {
Configuration = configuration,
NoIncremental = context.HasArgument("rebuild"),
MSBuildSettings = new DotNetCoreMSBuildSettings()
.TreatAllWarningsAs(MSBuildTreatAllWarningsAs.Error)
});
});
Task("Test")
.IsDependentOn("Build")
.Does(context =>
{
DotNetCoreTest("./src/RadLine.Tests/RadLine.Tests.csproj", new DotNetCoreTestSettings {
Configuration = configuration,
NoRestore = true,
NoBuild = true,
});
});
Task("Package")
.IsDependentOn("Test")
.Does(context =>
{
context.CleanDirectory("./.artifacts");
context.DotNetCorePack($"./src/RadLine.sln", new DotNetCorePackSettings {
Configuration = configuration,
NoRestore = true,
NoBuild = true,
OutputDirectory = "./.artifacts",
MSBuildSettings = new DotNetCoreMSBuildSettings()
.TreatAllWarningsAs(MSBuildTreatAllWarningsAs.Error)
});
});
Task("Publish-NuGet")
.WithCriteria(ctx => BuildSystem.IsRunningOnGitHubActions, "Not running on GitHub Actions")
.IsDependentOn("Package")
.Does(context =>
{
var apiKey = Argument<string>("nuget-key", null);
if(string.IsNullOrWhiteSpace(apiKey)) {
throw new CakeException("No NuGet API key was provided.");
}
// Publish to GitHub Packages
foreach(var file in context.GetFiles("./.artifacts/*.nupkg"))
{
context.Information("Publishing {0}...", file.GetFilename().FullPath);
DotNetCoreNuGetPush(file.FullPath, new DotNetCoreNuGetPushSettings
{
Source = "https://api.nuget.org/v3/index.json",
ApiKey = apiKey,
});
}
});
////////////////////////////////////////////////////////////////
// Targets
Task("Publish")
.IsDependentOn("Publish-NuGet");
Task("Default")
.IsDependentOn("Package");
////////////////////////////////////////////////////////////////
// Execution
RunTarget(target)

7
global.json Normal file
Просмотреть файл

@ -0,0 +1,7 @@
{
"projects": [ "src" ],
"sdk": {
"version": "5.0.202",
"rollForward": "latestPatch"
}
}

Двоичные данные
resources/gfx/large-logo.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 600 KiB

Двоичные данные
resources/gfx/medium-logo.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 30 KiB

Двоичные данные
resources/gfx/small-logo.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 9.5 KiB

113
src/.editorconfig Normal file
Просмотреть файл

@ -0,0 +1,113 @@
root = false
[*.cs]
# IDE0055: Fix formatting
dotnet_diagnostic.IDE0055.severity = warning
# SA1101: Prefix local calls with this
dotnet_diagnostic.SA1101.severity = none
# SA1633: File should have header
dotnet_diagnostic.SA1633.severity = none
# SA1201: Elements should appear in the correct order
dotnet_diagnostic.SA1201.severity = none
# SA1202: Public members should come before private members
dotnet_diagnostic.SA1202.severity = none
# SA1309: Field names should not begin with underscore
dotnet_diagnostic.SA1309.severity = none
# SA1404: Code analysis suppressions should have justification
dotnet_diagnostic.SA1404.severity = none
# SA1516: Elements should be separated by a blank line
dotnet_diagnostic.SA1516.severity = none
# CA1303: Do not pass literals as localized parameters
dotnet_diagnostic.CA1303.severity = none
# CSA1204: Static members should appear before non-static members
dotnet_diagnostic.SA1204.severity = none
# IDE0052: Remove unread private members
dotnet_diagnostic.IDE0052.severity = warning
# IDE0063: Use simple 'using' statement
csharp_prefer_simple_using_statement = false:suggestion
# IDE0018: Variable declaration can be inlined
dotnet_diagnostic.IDE0018.severity = warning
# SA1625: Element documenation should not be copied and pasted
dotnet_diagnostic.SA1625.severity = none
# IDE0005: Using directive is unnecessary
dotnet_diagnostic.IDE0005.severity = warning
# SA1117: Parameters should be on same line or separate lines
dotnet_diagnostic.SA1117.severity = none
# SA1404: Code analysis suppression should have justification
dotnet_diagnostic.SA1404.severity = none
# SA1101: Prefix local calls with this
dotnet_diagnostic.SA1101.severity = none
# SA1633: File should have header
dotnet_diagnostic.SA1633.severity = none
# SA1649: File name should match first type name
dotnet_diagnostic.SA1649.severity = none
# SA1402: File may only contain a single type
dotnet_diagnostic.SA1402.severity = none
# CA1814: Prefer jagged arrays over multidimensional
dotnet_diagnostic.CA1814.severity = none
# RCS1194: Implement exception constructors.
dotnet_diagnostic.RCS1194.severity = none
# CA1032: Implement standard exception constructors
dotnet_diagnostic.CA1032.severity = none
# CA1826: Do not use Enumerable methods on indexable collections. Instead use the collection directly
dotnet_diagnostic.CA1826.severity = none
# RCS1079: Throwing of new NotImplementedException.
dotnet_diagnostic.RCS1079.severity = none # TODO: Change this back
# RCS1057: Add empty line between declarations.
dotnet_diagnostic.RCS1057.severity = none
# RCS1057: Validate arguments correctly
dotnet_diagnostic.RCS1227.severity = none
# IDE0004: Remove Unnecessary Cast
dotnet_diagnostic.IDE0004.severity = warning
# CA1810: Initialize reference type static fields inline
dotnet_diagnostic.CA1810.severity = none
# IDE0044: Add readonly modifier
dotnet_diagnostic.IDE0044.severity = warning
########################################################################
# CS1591: Missing XML comment for publicly visible type or member
dotnet_diagnostic.CS1591.severity = silent
# SA1600: Elements should be documented
dotnet_diagnostic.SA1600.severity = none
# RCS1102: Make class static.
dotnet_diagnostic.RCS1102.severity = none
# SA1602: Enumeration items should be documented
dotnet_diagnostic.SA1602.severity = none
# RCS1079: Throwing of new NotImplementedException.
dotnet_diagnostic.RCS1079.severity = none

47
src/Directory.Build.props Normal file
Просмотреть файл

@ -0,0 +1,47 @@
<Project>
<PropertyGroup Label="Settings">
<Deterministic>true</Deterministic>
<LangVersion>9.0</LangVersion>
<DebugSymbols>true</DebugSymbols>
<DebugType>embedded</DebugType>
<MinVerSkip Condition="'$(Configuration)' == 'Debug'">true</MinVerSkip>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IsPackable>false</IsPackable>
</PropertyGroup>
<PropertyGroup Label="Deterministic Build" Condition="'$(GITHUB_ACTIONS)' == 'true'">
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>
<PropertyGroup Label="Package Information">
<Description>A library to read and display keyboard input. This is a preview version only and will be moved into Spectre.Console at some point.</Description>
<Copyright>Patrik Svensson, Phil Scott</Copyright>
<Authors>Patrik Svensson, Phil Scott</Authors>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/spectreconsole/radline</RepositoryUrl>
<PackageIcon>small-logo.png</PackageIcon>
<PackageRequireLicenseAcceptance>True</PackageRequireLicenseAcceptance>
<PackageProjectUrl>https://github.com/spectreconsole/radline</PackageProjectUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup>
<PropertyGroup Label="Source Link">
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="MinVer" PrivateAssets="All" Version="2.4.0" />
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" Version="1.0.0" />
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="5.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.312">
<PrivateAssets>All</PrivateAssets>
</PackageReference>
<PackageReference Include="Roslynator.Analyzers" Version="3.0.0">
<PrivateAssets>All</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>

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

@ -0,0 +1,8 @@
<Project>
<Target Name="Versioning" BeforeTargets="MinVer">
<PropertyGroup Label="Build">
<MinVerDefaultPreReleasePhase>preview</MinVerDefaultPreReleasePhase>
<MinVerVerbosity>normal</MinVerVerbosity>
</PropertyGroup>
</Target>
</Project>

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

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

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

@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Spectre.Console;
namespace RadLine.Sandbox
{
public static class Program
{
public static async Task Main()
{
if (!Debugger.IsAttached)
{
Debugger.Launch();
}
var editor = new LineEditor()
{
MultiLine = true,
Text = "HELLO ABC WORLD DEF GHIJKLMN 🥰 PATRIK WAS HERE",
Prompt = new LineNumberPrompt(),
Completion = new TestCompletion(),
Highlighter = new WordHighlighter()
.AddWord("git", new Style(foreground: Color.Yellow))
.AddWord("code", new Style(foreground: Color.Yellow))
.AddWord("vim", new Style(foreground: Color.Yellow))
.AddWord("init", new Style(foreground: Color.Blue))
.AddWord("push", new Style(foreground: Color.Red))
.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)),
};
// Add custom commands
editor.KeyBindings.Add<PrependSmiley>(ConsoleKey.I, ConsoleModifiers.Control);
// Read a line
var result = await editor.ReadLine(CancellationToken.None);
// Write the buffer
AnsiConsole.WriteLine();
AnsiConsole.Render(new Panel(result.EscapeMarkup())
.Header("[yellow]Commit details:[/]")
.RoundedBorder());
}
}
public sealed class PrependSmiley : LineEditorCommand
{
public override void Execute(LineEditorContext context)
{
context.Execute(new PreviousWordCommand());
context.Buffer.Insert(":-)");
}
}
public sealed class TestCompletion : ITextCompletion
{
public IEnumerable<string> GetCompletions(string context, string word, string suffix)
{
if (string.IsNullOrWhiteSpace(context))
{
return new[] { "git", "code", "vim" };
}
if (context.Equals("git ", StringComparison.Ordinal))
{
return new[] { "init", "initialize", "push", "commit", "rebase" };
}
return null;
}
}
}

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

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

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

@ -0,0 +1,140 @@
using System;
using System.Collections.Generic;
using RadLine.Tests.Utilities;
using Shouldly;
using Xunit;
namespace RadLine.Tests.Commands
{
public sealed class AutoCompleteCommandTests
{
private sealed class Fixture
{
private LineEditorContext? _context;
public LineBuffer Buffer { get; }
public Func<string, string, string, IEnumerable<string>?> AutoComplete { get; set; } = (_, _, _) => null;
public Fixture(string? text = null)
{
Buffer = new LineBuffer(text);
}
public void Execute(AutoComplete direction)
{
var provider = new SimpleServiceProvider();
provider.Register<ITextCompletion, DelegateTextCompletion>(new DelegateTextCompletion(AutoComplete));
_context ??= new LineEditorContext(Buffer, provider);
var command = new AutoCompleteCommand(direction);
command.Execute(_context);
}
}
public sealed class Next
{
[Fact]
public void Should_Insert_First_AutoComplete_Suggestion()
{
// Given
var fixture = new Fixture();
fixture.Buffer.Insert("git ");
fixture.Buffer.MoveEnd();
fixture.AutoComplete = (_, _, _) => new[] { "init" };
// When
fixture.Execute(AutoComplete.Next);
// Then
fixture.Buffer.Content.ShouldBe("git init");
}
[Fact]
public void Should_Replace_Previous_AutoComplete_Suggestion()
{
// Given
var fixture = new Fixture();
fixture.Buffer.Insert("git ");
fixture.Buffer.MoveEnd();
fixture.AutoComplete = (_, _, _) => new[] { "init", "push" };
// When
fixture.Execute(AutoComplete.Next);
fixture.Execute(AutoComplete.Next);
// Then
fixture.Buffer.Content.ShouldBe("git push");
}
[Fact]
public void Should_Replace_Previous_AutoComplete_Suggestion_If_Buffer_Has_Suffix()
{
// Given
var fixture = new Fixture();
fixture.Buffer.Insert("git -m 'Hello World'");
fixture.Buffer.Move(4);
fixture.AutoComplete = (_, _, _) => new[] { "init", "push" };
// When
fixture.Execute(AutoComplete.Next);
fixture.Execute(AutoComplete.Next);
// Then
fixture.Buffer.Content.ShouldBe("git push -m 'Hello World'");
}
}
public sealed class Previous
{
[Fact]
public void Should_Insert_Last_Autocomplete_Suggestion()
{
// Given
var fixture = new Fixture();
fixture.Buffer.Insert("git ");
fixture.Buffer.MoveEnd();
fixture.AutoComplete = (_, _, _) => new[] { "init", "push" };
// When
fixture.Execute(AutoComplete.Previous);
// Then
fixture.Buffer.Content.ShouldBe("git push");
}
[Fact]
public void Should_Replace_Previous_AutoComplete_Suggestion()
{
// Given
var fixture = new Fixture();
fixture.Buffer.Insert("git ");
fixture.Buffer.MoveEnd();
fixture.AutoComplete = (_, _, _) => new[] { "init", "push" };
// When
fixture.Execute(AutoComplete.Previous);
fixture.Execute(AutoComplete.Previous);
// Then
fixture.Buffer.Content.ShouldBe("git init");
}
[Fact]
public void Should_Replace_Previous_AutoComplete_Suggestion_If_Buffer_Has_Suffix()
{
// Given
var fixture = new Fixture();
fixture.Buffer.Insert("git --quiet");
fixture.Buffer.Move(4);
fixture.AutoComplete = (_, _, _) => new[] { "init", "push" };
// When
fixture.Execute(AutoComplete.Previous);
fixture.Execute(AutoComplete.Previous);
// Then
fixture.Buffer.Content.ShouldBe("git init --quiet");
}
}
}
}

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

@ -0,0 +1,42 @@
using Shouldly;
using Xunit;
namespace RadLine.Tests
{
public sealed class BackspaceCommandTests
{
[Fact]
public void Should_Remove_Previous_Character()
{
// Given
var buffer = new LineBuffer("Foo");
var context = new LineEditorContext(buffer);
var command = new BackspaceCommand();
// When
command.Execute(context);
// Then
buffer.Content.ShouldBe("Fo");
buffer.Position.ShouldBe(2);
}
[Fact]
public void Should_Do_Nothing_If_There_Is_Nothing_To_Remove()
{
// Given
var buffer = new LineBuffer("Foo");
buffer.Move(0);
var context = new LineEditorContext(buffer);
var command = new BackspaceCommand();
// When
command.Execute(context);
// Then
buffer.Content.ShouldBe("Foo");
buffer.Position.ShouldBe(0);
}
}
}

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

@ -0,0 +1,42 @@
using Shouldly;
using Xunit;
namespace RadLine.Tests
{
public sealed class DeleteCommandTests
{
[Fact]
public void Should_Delete_Next_Character()
{
// Given
var buffer = new LineBuffer("Foo");
buffer.Move(0);
var context = new LineEditorContext(buffer);
var command = new DeleteCommand();
// When
command.Execute(context);
// Then
buffer.Content.ShouldBe("oo");
buffer.Position.ShouldBe(0);
}
[Fact]
public void Should_Do_Nothing_If_There_Is_Nothing_To_Remove()
{
// Given
var buffer = new LineBuffer("Foo");
var context = new LineEditorContext(buffer);
var command = new DeleteCommand();
// When
command.Execute(context);
// Then
buffer.Content.ShouldBe("Foo");
buffer.Position.ShouldBe(3);
}
}
}

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

@ -0,0 +1,24 @@
using Shouldly;
using Xunit;
namespace RadLine.Tests
{
public sealed class InsertCommandTests
{
[Fact]
public void Should_Insert_Text_At_Position()
{
// Given
var buffer = new LineBuffer("Foo");
var context = new LineEditorContext(buffer);
var command = new InsertCommand('l');
// When
command.Execute(context);
// Then
buffer.Content.ShouldBe("Fool");
buffer.Position.ShouldBe(4);
}
}
}

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

@ -0,0 +1,109 @@
using System;
using Shouldly;
using Xunit;
namespace RadLine.Tests
{
public sealed class KeyBindingsTests
{
[Fact]
public void Should_Have_No_Bindings_When_Created()
{
// Given
var bindings = new KeyBindings();
// When
var result = bindings.Count;
// Then
result.ShouldBe(0);
}
public sealed class TheAddMethod
{
[Fact]
public void Should_Add_Command()
{
// Given
var bindings = new KeyBindings();
bindings.Add<MoveEndCommand>(ConsoleKey.Home);
// When
var result = bindings.Count;
// Then
result.ShouldBe(1);
}
}
public sealed class TheRemoveMethod
{
[Fact]
public void Should_Remove_Command()
{
// Given
var bindings = new KeyBindings();
bindings.Add<MoveEndCommand>(ConsoleKey.Home);
bindings.Remove(ConsoleKey.Home);
// When
var command = bindings.GetCommand(ConsoleKey.Home);
// Then
bindings.Count.ShouldBe(0);
command.ShouldBeNull();
}
}
public sealed class TheGetCommandMethod
{
[Fact]
public void Should_Get_Command_For_KeyBinding_Without_Modifier()
{
// Given
var bindings = new KeyBindings();
bindings.Add<MoveEndCommand>(ConsoleKey.End);
bindings.Add<MoveHomeCommand>(ConsoleKey.Home);
// When
var command = bindings.GetCommand(ConsoleKey.Home, null);
// Then
command.ShouldBeOfType<MoveHomeCommand>();
}
[Fact]
public void Should_Get_Command_For_KeyBinding_With_Modifier()
{
// Given
var bindings = new KeyBindings();
bindings.Add<MoveEndCommand>(ConsoleKey.Home);
bindings.Add<MoveHomeCommand>(ConsoleKey.Home, ConsoleModifiers.Shift);
// When
var command = bindings.GetCommand(ConsoleKey.Home, ConsoleModifiers.Shift);
// Then
command.ShouldBeOfType<MoveHomeCommand>();
}
[Fact]
public void Should_Not_Get_Command_For_KeyBinding_With_Modifier_When_No_Modifier_Was_Provided()
{
// Given
var bindings = new KeyBindings();
bindings.Add<MoveEndCommand>(ConsoleKey.End);
bindings.Add<MoveHomeCommand>(ConsoleKey.Home, ConsoleModifiers.Shift);
// When
var command = bindings.GetCommand(ConsoleKey.Home);
// Then
command.ShouldBeNull();
}
}
}
}

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

@ -0,0 +1,331 @@
using Shouldly;
using Xunit;
namespace RadLine.Tests
{
public sealed class LineBufferTests
{
public sealed class TheMoveMethod
{
[Fact]
public void Should_Move_To_Position()
{
// Given
var buffer = new LineBuffer("FOO");
// When
var result = buffer.Move(1);
// Then
result.ShouldBe(true);
buffer.Position.ShouldBe(1);
}
[Fact]
public void Should_Not_Move_If_Already_At_Position()
{
// Given
var buffer = new LineBuffer("FOO");
buffer.Move(3);
// When
var result = buffer.Move(3);
// Then
result.ShouldBe(false);
buffer.Position.ShouldBe(3);
}
}
public sealed class TheInsertCharacterMethod
{
[Fact]
public void Should_Insert_Character()
{
// Given
var buffer = new LineBuffer();
// When
buffer.Insert('A');
// Then
buffer.Content.ShouldBe("A");
}
[Fact]
public void Should_Not_Move_Caret_Position()
{
// Given
var buffer = new LineBuffer();
// When
buffer.Insert('A');
// Then
buffer.Position.ShouldBe(0);
}
}
public sealed class TheMoveEndMethod
{
[Fact]
public void Should_Move_To_End_Of_Buffer()
{
// Given
var buffer = new LineBuffer();
buffer.Insert("ABC");
// When
buffer.MoveEnd();
// Then
buffer.Position.ShouldBe(3);
}
}
public sealed class TheMoveRightMethod
{
[Fact]
public void Should_Move_Right_If_Caret_Is_Not_At_End_Of_Line()
{
// Given
var buffer = new LineBuffer();
buffer.Insert('A');
// When
buffer.MoveRight();
// Then
buffer.Position.ShouldBe(1);
}
[Fact]
public void Should_Move_Past_Grapheme_Cluster()
{
// Given
var buffer = new LineBuffer();
buffer.Insert("😄A");
// When
buffer.MoveRight();
// Then
buffer.Position.ShouldBe(2);
}
[Fact]
public void Should_Not_Move_Right_If_Position_Is_At_End_Of_Line()
{
// Given
var buffer = new LineBuffer();
// When
buffer.MoveRight();
// Then
buffer.Position.ShouldBe(0);
}
}
public sealed class TheMoveHomeMethod
{
[Fact]
public void Should_Move_To_Start_Of_Buffer()
{
// Given
var buffer = new LineBuffer();
buffer.Insert("A");
buffer.MoveEnd();
// When
buffer.MoveHome();
// Then
buffer.Position.ShouldBe(0);
}
}
public sealed class TheMoveToPreviousWordMethod
{
[Fact]
public void Should_Move_To_The_Previous_Word_If_Position_Is_At_End_Of_Line()
{
// Given
var buffer = new LineBuffer("Foo Bar Baz");
// When
buffer.MoveToPreviousWord();
// Then
buffer.Position.ShouldBe(8);
}
[Fact]
public void Should_Move_To_Left_Word_Boundary_If_Position_Is_Inside_Word()
{
// Given
var buffer = new LineBuffer("Foo Bar Baz");
buffer.MoveLeft();
// When
buffer.MoveToPreviousWord();
// Then
buffer.Position.ShouldBe(8);
}
[Fact]
public void Should_Move_To_Previous_If_Position_Is_At_Beginning_Of_Word()
{
// Given
var buffer = new LineBuffer("Foo Bar Baz");
buffer.Move(8);
// When
buffer.MoveToPreviousWord();
// Then
buffer.Position.ShouldBe(4);
}
}
public sealed class TheMoveToNextWordMethod
{
[Fact]
public void Should_Move_To_The_Beginning_Of_The_Next_Word()
{
// Given
var buffer = new LineBuffer("Foo Bar");
buffer.MoveHome();
// When
buffer.MoveToNextWord();
// Then
buffer.Position.ShouldBe(4);
}
[Fact]
public void Should_Move_To_The_End_Of_The_Last_Word_If_There_Is_No_Next_Word()
{
// Given
var buffer = new LineBuffer("Foo");
buffer.MoveHome();
// When
buffer.MoveToNextWord();
// Then
buffer.Position.ShouldBe(3);
}
[Fact]
public void Should_Move_To_Next_Word_If_Position_Is_Between_Two_Words()
{
// Given
var buffer = new LineBuffer("Foo Bar Baz");
buffer.Move(3);
// When
buffer.MoveToNextWord();
// Then
buffer.Position.ShouldBe(4);
}
}
public sealed class TheMoveLeftMethod
{
[Fact]
public void Should_Move_Left_If_Position_Is_Greater_Than_Zero()
{
// Given
var buffer = new LineBuffer();
buffer.Insert("ABC");
buffer.MoveEnd();
// When
buffer.MoveLeft();
// Then
buffer.Position.ShouldBe(2);
}
[Fact]
public void Should_Move_Past_Grapheme_Cluster()
{
// Given
var buffer = new LineBuffer();
buffer.Insert("😄");
buffer.MoveEnd();
// When
buffer.MoveLeft();
// Then
buffer.Position.ShouldBe(0);
}
[Fact]
public void Should_Not_Move_Left_If_Position_Is_At_Beginning_Of_Line()
{
// Given
var buffer = new LineBuffer();
// When
buffer.MoveLeft();
// Then
buffer.Position.ShouldBe(0);
}
}
public sealed class TheClearMethod
{
[Fact]
public void Should_Delete_Previous_Character()
{
// Given
var buffer = new LineBuffer();
buffer.Insert("AB");
// When
var result = buffer.Clear(1, 1);
// Then
result.ShouldBe(1);
buffer.Content.ShouldBe("A");
}
[Fact]
public void Should_Not_Delete_If_Index_Is_Past_Buffer_Length()
{
// Given
var buffer = new LineBuffer();
buffer.Insert("AB");
// When
var result = buffer.Clear(3, 1);
// Then
result.ShouldBe(0);
buffer.Content.ShouldBe("AB");
}
[Fact]
public void Should_Not_Delete_Past_Buffer()
{
// Given
var buffer = new LineBuffer();
buffer.Insert("AB");
// When
var result = buffer.Clear(1, 3);
// Then
result.ShouldBe(1);
buffer.Content.ShouldBe("A");
}
}
}
}

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

@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<AdditionalFiles Include="..\stylecop.json" Link="Properties/stylecop.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="Shouldly" Version="4.0.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="1.3.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\RadLine\RadLine.csproj" />
</ItemGroup>
</Project>

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

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace RadLine.Tests.Utilities
{
public sealed class DelegateTextCompletion : ITextCompletion
{
private readonly Func<string, string, string, IEnumerable<string>?> _callback;
public DelegateTextCompletion(Func<string, string, string, IEnumerable<string>?> callback)
{
_callback = callback ?? throw new ArgumentNullException(nameof(callback));
}
public IEnumerable<string>? GetCompletions(string context, string word, string suffix)
{
if (_callback == null)
{
return Enumerable.Empty<string>();
}
return _callback(context, word, suffix);
}
}
}

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

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace RadLine.Tests.Utilities
{
public sealed class SimpleServiceProvider : IServiceProvider
{
private readonly Dictionary<Type, object> _registrations;
public SimpleServiceProvider()
{
_registrations = new Dictionary<Type, object>();
}
public void Register<TService, TImplementation>(TImplementation implementation)
where TImplementation : notnull
{
_registrations[typeof(TService)] = implementation;
}
public object? GetService(Type serviceType)
{
_registrations.TryGetValue(serviceType, out var result);
return result;
}
}
}

62
src/RadLine.sln Normal file
Просмотреть файл

@ -0,0 +1,62 @@

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}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "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}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
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
{DFF3DD16-6AE7-4FDB-807F-F2E8A3166691}.Debug|x64.ActiveCfg = Debug|Any CPU
{DFF3DD16-6AE7-4FDB-807F-F2E8A3166691}.Debug|x64.Build.0 = Debug|Any CPU
{DFF3DD16-6AE7-4FDB-807F-F2E8A3166691}.Debug|x86.ActiveCfg = Debug|Any CPU
{DFF3DD16-6AE7-4FDB-807F-F2E8A3166691}.Debug|x86.Build.0 = Debug|Any CPU
{DFF3DD16-6AE7-4FDB-807F-F2E8A3166691}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DFF3DD16-6AE7-4FDB-807F-F2E8A3166691}.Release|Any CPU.Build.0 = Release|Any CPU
{DFF3DD16-6AE7-4FDB-807F-F2E8A3166691}.Release|x64.ActiveCfg = Release|Any CPU
{DFF3DD16-6AE7-4FDB-807F-F2E8A3166691}.Release|x64.Build.0 = Release|Any CPU
{DFF3DD16-6AE7-4FDB-807F-F2E8A3166691}.Release|x86.ActiveCfg = Release|Any CPU
{DFF3DD16-6AE7-4FDB-807F-F2E8A3166691}.Release|x86.Build.0 = Release|Any CPU
{5DFDEEA4-7B61-47FD-AEA0-14FB569EBE62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5DFDEEA4-7B61-47FD-AEA0-14FB569EBE62}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5DFDEEA4-7B61-47FD-AEA0-14FB569EBE62}.Debug|x64.ActiveCfg = Debug|Any CPU
{5DFDEEA4-7B61-47FD-AEA0-14FB569EBE62}.Debug|x64.Build.0 = Debug|Any CPU
{5DFDEEA4-7B61-47FD-AEA0-14FB569EBE62}.Debug|x86.ActiveCfg = Debug|Any CPU
{5DFDEEA4-7B61-47FD-AEA0-14FB569EBE62}.Debug|x86.Build.0 = Debug|Any CPU
{5DFDEEA4-7B61-47FD-AEA0-14FB569EBE62}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5DFDEEA4-7B61-47FD-AEA0-14FB569EBE62}.Release|Any CPU.Build.0 = Release|Any CPU
{5DFDEEA4-7B61-47FD-AEA0-14FB569EBE62}.Release|x64.ActiveCfg = Release|Any CPU
{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
EndGlobalSection
EndGlobal

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

@ -0,0 +1,6 @@
<SolutionConfiguration>
<Settings>
<AllowParallelTestExecution>True</AllowParallelTestExecution>
<SolutionConfigured>True</SolutionConfigured>
</Settings>
</SolutionConfiguration>

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

@ -0,0 +1,8 @@
namespace RadLine
{
public enum AutoComplete
{
Next,
Previous,
}
}

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

@ -0,0 +1,134 @@
using System;
namespace RadLine
{
public sealed class AutoCompleteCommand : LineEditorCommand
{
private const string Position = nameof(Position);
private const string Index = nameof(Index);
private readonly AutoComplete _kind;
public AutoCompleteCommand(AutoComplete kind)
{
_kind = kind;
}
public override void Execute(LineEditorContext context)
{
var completion = context.GetService<ITextCompletion>();
if (completion == null)
{
return;
}
var originalPosition = context.Buffer.Position;
// Get the start position of the word
var start = context.Buffer.Position;
if (context.Buffer.IsAtCharacter)
{
context.Buffer.MoveToBeginningOfWord();
start = context.Buffer.Position;
}
else if (context.Buffer.IsAtEndOfWord)
{
context.Buffer.MoveToPreviousWord();
start = context.Buffer.Position;
}
// Get the end position of the word.
var end = context.Buffer.Position;
if (context.Buffer.IsAtCharacter)
{
context.Buffer.MoveToEndOfWord();
end = context.Buffer.Position;
}
// Not the same start position as last time?
if (context.GetState(Position, () => 0) != start)
{
// Reset
var startIndex = _kind == AutoComplete.Next ? 0 : -1;
context.SetState(Index, startIndex);
}
// Get the prefix and word
var prefix = context.Buffer.Content.Substring(0, start);
var word = context.Buffer.Content.Substring(start, end - start);
var suffix = context.Buffer.Content.Substring(end, context.Buffer.Content.Length - end);
// Get the completions
if (!completion.TryGetCompletions(prefix, word, suffix, out var completions))
{
context.Buffer.Move(originalPosition);
return;
}
// Get the index to insert
var index = GetSuggestionIndex(context, word, completions);
if (index == -1)
{
context.Buffer.Move(originalPosition);
return;
}
// Remove the current word
if (start != end)
{
context.Buffer.Clear(start, end - start);
context.Buffer.Move(start);
}
// Insert the completion
context.Buffer.Insert(completions[index]);
// Move to the end of the word
context.Buffer.MoveToEndOfWord();
// Increase the completion index
context.SetState(Position, start);
context.SetState(Index, _kind == AutoComplete.Next ? ++index : --index);
}
private int GetSuggestionIndex(LineEditorContext context, string word, string[] completions)
{
if (completions is null)
{
throw new ArgumentNullException(nameof(completions));
}
if (!string.IsNullOrWhiteSpace(word))
{
// Try find an exact match
var index = 0;
foreach (var completion in completions)
{
if (completion.Equals(word, StringComparison.Ordinal))
{
var newIndex = _kind == AutoComplete.Next ? index + 1 : index - 1;
return newIndex.WrapAround(0, completions.Length - 1);
}
index++;
}
// Try find a partial match
index = 0;
foreach (var completion in completions)
{
if (completion.StartsWith(word, StringComparison.Ordinal))
{
return index;
}
index++;
}
return -1;
}
return context.GetState(Index, () => 0).WrapAround(0, completions.Length - 1);
}
}
}

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

@ -0,0 +1,14 @@
namespace RadLine
{
public sealed class BackspaceCommand : LineEditorCommand
{
public override void Execute(LineEditorContext context)
{
var removed = context.Buffer.Clear(context.Buffer.Position - 1, 1);
if (removed == 1)
{
context.Buffer.Move(context.Buffer.Position - 1);
}
}
}
}

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

@ -0,0 +1,10 @@
namespace RadLine
{
public sealed class MoveDownCommand : LineEditorCommand
{
public override void Execute(LineEditorContext context)
{
context.Submit(SubmitAction.MoveDown);
}
}
}

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

@ -0,0 +1,10 @@
namespace RadLine
{
public sealed class MoveEndCommand : LineEditorCommand
{
public override void Execute(LineEditorContext context)
{
context.Buffer.MoveEnd();
}
}
}

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

@ -0,0 +1,10 @@
namespace RadLine
{
public sealed class MoveFirstLineCommand : LineEditorCommand
{
public override void Execute(LineEditorContext context)
{
context.Submit(SubmitAction.MoveFirst);
}
}
}

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

@ -0,0 +1,10 @@
namespace RadLine
{
public sealed class MoveHomeCommand : LineEditorCommand
{
public override void Execute(LineEditorContext context)
{
context.Buffer.MoveHome();
}
}
}

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

@ -0,0 +1,10 @@
namespace RadLine
{
public sealed class MoveLastLineCommand : LineEditorCommand
{
public override void Execute(LineEditorContext context)
{
context.Submit(SubmitAction.MoveLast);
}
}
}

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

@ -0,0 +1,24 @@
using System;
namespace RadLine
{
public sealed class MoveLeftCommand : LineEditorCommand
{
private readonly int _count;
public MoveLeftCommand()
{
_count = 1;
}
public MoveLeftCommand(int count)
{
_count = Math.Max(0, count);
}
public override void Execute(LineEditorContext context)
{
context.Buffer.MoveLeft(_count);
}
}
}

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

@ -0,0 +1,24 @@
using System;
namespace RadLine
{
public sealed class MoveRightCommand : LineEditorCommand
{
private readonly int _count;
public MoveRightCommand()
{
_count = 1;
}
public MoveRightCommand(int count)
{
_count = Math.Max(0, count);
}
public override void Execute(LineEditorContext context)
{
context.Buffer.MoveRight(_count);
}
}
}

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

@ -0,0 +1,10 @@
namespace RadLine
{
public sealed class MoveUpCommand : LineEditorCommand
{
public override void Execute(LineEditorContext context)
{
context.Submit(SubmitAction.MoveUp);
}
}
}

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

@ -0,0 +1,10 @@
namespace RadLine
{
public sealed class NextWordCommand : LineEditorCommand
{
public override void Execute(LineEditorContext context)
{
context.Buffer.MoveToNextWord();
}
}
}

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

@ -0,0 +1,10 @@
namespace RadLine
{
public sealed class PreviousWordCommand : LineEditorCommand
{
public override void Execute(LineEditorContext context)
{
context.Buffer.MoveToPreviousWord();
}
}
}

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

@ -0,0 +1,11 @@
namespace RadLine
{
public sealed class DeleteCommand : LineEditorCommand
{
public override void Execute(LineEditorContext context)
{
var buffer = context.Buffer;
buffer.Clear(buffer.Position, 1);
}
}
}

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

@ -0,0 +1,36 @@
namespace RadLine
{
public sealed class InsertCommand : LineEditorCommand
{
private readonly char? _character;
private readonly string? _text;
public InsertCommand(char character)
{
_character = character;
_text = null;
}
public InsertCommand(string text)
{
_text = text ?? string.Empty;
_character = null;
}
public override void Execute(LineEditorContext context)
{
var buffer = context.Buffer;
if (_character != null)
{
buffer.Insert(_character.Value);
buffer.Move(buffer.Position + 1);
}
else if (_text != null)
{
buffer.Insert(_text);
buffer.Move(buffer.Position + _text.Length);
}
}
}
}

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

@ -0,0 +1,10 @@
namespace RadLine
{
public sealed class NewLineCommand : LineEditorCommand
{
public override void Execute(LineEditorContext context)
{
context.Submit(SubmitAction.NewLine);
}
}
}

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

@ -0,0 +1,10 @@
namespace RadLine
{
public sealed class SubmitCommand : LineEditorCommand
{
public override void Execute(LineEditorContext context)
{
context.Submit(SubmitAction.Submit);
}
}
}

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

@ -0,0 +1,9 @@
using Spectre.Console;
namespace RadLine
{
public interface IHighlighter
{
Style? Highlight(string token);
}
}

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

@ -0,0 +1,11 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace RadLine
{
public interface IInputSource
{
Task<ConsoleKeyInfo?> ReadKey(CancellationToken cancellationToken);
}
}

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

@ -0,0 +1,9 @@
using Spectre.Console;
namespace RadLine
{
public interface ILineEditorPrompt
{
(Markup Markup, int Margin) GetPrompt(int line);
}
}

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

@ -0,0 +1,30 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace RadLine
{
public interface ITextCompletion
{
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,43 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Spectre.Console;
namespace RadLine
{
internal sealed class DefaultInputSource : IInputSource
{
private readonly IAnsiConsole _console;
public DefaultInputSource(IAnsiConsole console)
{
_console = console ?? throw new ArgumentNullException(nameof(console));
}
public async Task<ConsoleKeyInfo?> ReadKey(CancellationToken cancellationToken)
{
if (!_console.Profile.Out.IsTerminal
|| !_console.Profile.Capabilities.Interactive)
{
throw new NotSupportedException("Only interactive terminals are supported as input source");
}
while (true)
{
if (cancellationToken.IsCancellationRequested)
{
return null;
}
if (Console.KeyAvailable)
{
break;
}
await Task.Delay(5, cancellationToken).ConfigureAwait(false);
}
return Console.ReadKey(true);
}
}
}

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

@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
namespace RadLine
{
internal sealed class DefaultServiceProvider : IServiceProvider
{
private readonly IServiceProvider? _provider;
private readonly Dictionary<Type, object> _registrations;
public DefaultServiceProvider(IServiceProvider? provider)
{
_provider = provider;
_registrations = new Dictionary<Type, object>();
}
public void RegisterOptional<TService, TImplementation>(TImplementation? implementation)
{
if (implementation != null)
{
_registrations[typeof(TService)] = implementation;
}
}
public object? GetService(Type serviceType)
{
if (_provider != null)
{
var result = _provider.GetService(serviceType);
if (result != null)
{
return result;
}
}
_registrations.TryGetValue(serviceType, out var registration);
return registration;
}
}
}

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

@ -0,0 +1,40 @@
using System;
using Spectre.Console;
namespace RadLine
{
internal static class AnsiConsoleExtensions
{
public static IDisposable HideCursor(this IAnsiConsole console)
{
if (console is null)
{
throw new ArgumentNullException(nameof(console));
}
return new CursorHider(console);
}
private sealed class CursorHider : IDisposable
{
private readonly IAnsiConsole _console;
public CursorHider(IAnsiConsole console)
{
_console = console ?? throw new ArgumentNullException(nameof(console));
_console.Cursor.Hide();
}
~CursorHider()
{
throw new InvalidOperationException("CursorHider: Dispose was never called");
}
public void Dispose()
{
GC.SuppressFinalize(this);
_console.Cursor.Show();
}
}
}
}

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

@ -0,0 +1,19 @@
using System;
namespace RadLine
{
internal static class IServiceProviderExtensions
{
public static T? GetService<T>(this IServiceProvider provider)
where T : class
{
if (provider is null)
{
throw new ArgumentNullException(nameof(provider));
}
var result = provider.GetService(typeof(T));
return result as T;
}
}
}

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

@ -0,0 +1,7 @@
namespace RadLine
{
internal interface IHighlighterAccessor
{
IHighlighter? Highlighter { get; }
}
}

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

@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace RadLine
{
internal static class IntExtensions
{
public static int Clamp(this int value, int min, int max)
{
if (value < min)
{
return min;
}
if (value > max)
{
return max;
}
return value;
}
public static int WrapAround(this int value, int min, int max)
{
if (value < min)
{
return max;
}
if (value > max)
{
return min;
}
return value;
}
}
}

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

@ -0,0 +1,16 @@
using System;
namespace RadLine
{
internal sealed class KeyBinding
{
public ConsoleKey Key { get; }
public ConsoleModifiers? Modifiers { get; }
public KeyBinding(ConsoleKey key, ConsoleModifiers? modifiers = null)
{
Key = key;
Modifiers = modifiers;
}
}
}

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

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
namespace RadLine
{
internal sealed class KeyBindingComparer : IEqualityComparer<KeyBinding>
{
public bool Equals(KeyBinding? x, KeyBinding? y)
{
if (x == null && y == null)
{
return true;
}
if (x == null || y == null)
{
return false;
}
return x.Key == y.Key && x.Modifiers == y.Modifiers;
}
public int GetHashCode(KeyBinding obj)
{
#if NET5_0
return HashCode.Combine(obj.Key, obj.Modifiers);
#else
unchecked
{
int hash = 17;
hash = (hash * 23) + (int)obj.Key;
hash = (hash * 23) + (obj?.Modifiers != null ? (int)obj.Modifiers : 0);
return hash;
}
#endif
}
}
}

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

@ -0,0 +1,36 @@
using System;
using Spectre.Console;
namespace RadLine
{
internal sealed class LineBufferRenderer
{
private readonly IAnsiConsole _console;
private readonly AnsiRenderingStrategy _ansiRendererer;
private readonly FallbackRenderingStrategy _fallbackRender;
public LineBufferRenderer(IAnsiConsole console, IHighlighterAccessor accessor)
{
if (accessor is null)
{
throw new ArgumentNullException(nameof(accessor));
}
_console = console ?? throw new ArgumentNullException(nameof(console));
_ansiRendererer = new AnsiRenderingStrategy(console, accessor);
_fallbackRender = new FallbackRenderingStrategy(console, accessor);
}
public void RenderLine(LineEditorState state, int? cursorPosition = null)
{
if (_console.Profile.Capabilities.Ansi)
{
_ansiRendererer.Render(state, cursorPosition);
}
else
{
_fallbackRender.Render(state, cursorPosition);
}
}
}
}

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

@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace RadLine
{
internal sealed class LineEditorState
{
private readonly List<LineBuffer> _lines;
private readonly ILineEditorPrompt _prompt;
private int _lineIndex;
public int LineIndex => _lineIndex;
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));
_lineIndex = 0;
}
public bool MoveUp()
{
if (_lineIndex > 0)
{
_lineIndex--;
return true;
}
return false;
}
public bool MoveDown()
{
if (_lineIndex < _lines.Count - 1)
{
_lineIndex++;
return true;
}
return false;
}
public void AddLine()
{
_lines.Add(new LineBuffer());
_lineIndex++;
}
}
}

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

@ -0,0 +1,71 @@
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;
}
}
}

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

@ -0,0 +1,51 @@
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");
}
}
}

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

@ -0,0 +1,46 @@
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();
}
}
}

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

@ -0,0 +1,34 @@
using System.Collections.Generic;
namespace RadLine
{
internal static class StringTokenizer
{
public static IEnumerable<string> Tokenize(string text)
{
var buffer = string.Empty;
foreach (var character in text)
{
if (char.IsLetterOrDigit(character))
{
buffer += character;
}
else
{
if (buffer.Length > 0)
{
yield return buffer;
buffer = string.Empty;
}
yield return new string(character, 1);
}
}
if (buffer.Length > 0)
{
yield return buffer;
}
}
}
}

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

@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace RadLine
{
public sealed class KeyBindings
{
private readonly Dictionary<KeyBinding, Func<LineEditorCommand>> _bindings;
public int Count => _bindings.Count;
public KeyBindings()
{
_bindings = new Dictionary<KeyBinding, Func<LineEditorCommand>>(new KeyBindingComparer());
}
internal void Add(KeyBinding binding, Func<LineEditorCommand> command)
{
if (binding is null)
{
throw new ArgumentNullException(nameof(binding));
}
_bindings[binding] = command;
}
internal void Remove(KeyBinding binding)
{
if (binding is null)
{
throw new ArgumentNullException(nameof(binding));
}
_bindings.Remove(binding);
}
public void Clear()
{
_bindings.Clear();
}
public LineEditorCommand? GetCommand(ConsoleKey key, ConsoleModifiers? modifiers = null)
{
var candidates = _bindings.Keys as IEnumerable<KeyBinding>;
if (modifiers != null && modifiers != 0)
{
candidates = _bindings.Keys.Where(b => b.Modifiers == modifiers);
}
var result = candidates.FirstOrDefault(x => x.Key == key);
if (result != null)
{
if (modifiers == null && result.Modifiers != null)
{
return null;
}
if (result != null && _bindings.TryGetValue(result, out var factory))
{
return factory();
}
}
return null;
}
}
}

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

@ -0,0 +1,50 @@
using System;
namespace RadLine
{
public static class KeyBindingsExtensions
{
public static void Add<TCommand>(this KeyBindings bindings, ConsoleKey key, ConsoleModifiers? modifiers = null)
where TCommand : LineEditorCommand, new()
{
if (bindings is null)
{
throw new ArgumentNullException(nameof(bindings));
}
bindings.Add(new KeyBinding(key, modifiers), () => new TCommand());
}
public static void Add<TCommand>(this KeyBindings bindings, ConsoleKey key, Func<TCommand> func)
where TCommand : LineEditorCommand
{
if (bindings is null)
{
throw new ArgumentNullException(nameof(bindings));
}
bindings.Add(new KeyBinding(key), () => func());
}
public static void Add<TCommand>(this KeyBindings bindings, ConsoleKey key, ConsoleModifiers modifiers, Func<TCommand> func)
where TCommand : LineEditorCommand
{
if (bindings is null)
{
throw new ArgumentNullException(nameof(bindings));
}
bindings.Add(new KeyBinding(key, modifiers), () => func());
}
public static void Remove(this KeyBindings bindings, ConsoleKey key, ConsoleModifiers? modifiers = null)
{
if (bindings is null)
{
throw new ArgumentNullException(nameof(bindings));
}
bindings.Remove(new KeyBinding(key, modifiers));
}
}
}

161
src/RadLine/LineBuffer.cs Normal file
Просмотреть файл

@ -0,0 +1,161 @@
using System;
using System.Globalization;
using System.Linq;
namespace RadLine
{
public sealed class LineBuffer
{
private string _buffer;
private int _position;
public int Position => _position;
public int Length => _buffer.Length;
public string Content => _buffer;
public bool AtBeginning => Position == 0;
public bool AtEnd => Position == Content.Length;
public bool IsAtCharacter
{
get
{
if (Length == 0)
{
return false;
}
if (AtEnd)
{
return false;
}
return !char.IsWhiteSpace(_buffer[_position]);
}
}
public bool IsAtBeginningOfWord
{
get
{
if (Length == 0)
{
return false;
}
if (_position == 0)
{
return !char.IsWhiteSpace(_buffer[0]);
}
return char.IsWhiteSpace(_buffer[_position - 1]);
}
}
public bool IsAtEndOfWord
{
get
{
if (Length == 0)
{
return false;
}
if (_position == 0)
{
return false;
}
return !char.IsWhiteSpace(_buffer[_position - 1]);
}
}
// TODO: Right now, this only returns the position in the line buffer.
// This is OK for western alphabets and most emojis which consist
// of a single surrogate pair, but everything else will be wrong.
public int CursorPosition => _position;
public LineBuffer(string? content = null)
{
_buffer = content ?? string.Empty;
_position = _buffer.Length;
}
public bool Move(int position)
{
if (position == _position)
{
return false;
}
var movingLeft = position < _position;
_position = MoveToPosition(position, movingLeft);
return true;
}
public void Insert(char character)
{
_buffer = _buffer.Insert(_position, character.ToString());
}
public void Insert(string text)
{
_buffer = _buffer.Insert(_position, text);
}
public int Clear(int index, int count)
{
if (index < 0)
{
return 0;
}
if (index > _buffer.Length - 1)
{
return 0;
}
var length = _buffer.Length;
_buffer = _buffer.Remove(Math.Max(0, index), Math.Min(count, _buffer.Length - index));
return Math.Max(length - _buffer.Length, 0);
}
private int MoveToPosition(int position, bool movingLeft)
{
if (position <= 0)
{
return 0;
}
else if (position >= _buffer.Length)
{
return _buffer.Length;
}
var indices = StringInfo.ParseCombiningCharacters(_buffer);
if (movingLeft)
{
foreach (var e in indices.Reverse())
{
if (e <= position)
{
return e;
}
}
}
else
{
foreach (var e in indices)
{
if (e >= position)
{
return e;
}
}
}
throw new InvalidOperationException("Could not find position in buffer");
}
}
}

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

@ -0,0 +1,143 @@
namespace RadLine
{
public static class LineBufferExtensions
{
public static bool MoveLeft(this LineBuffer buffer, int count = 1)
{
return buffer.Move(buffer.Position - count);
}
public static bool MoveRight(this LineBuffer buffer, int count = 1)
{
return buffer.Move(buffer.Position + count);
}
public static bool MoveHome(this LineBuffer buffer)
{
return buffer.Move(0);
}
public static bool MoveEnd(this LineBuffer buffer)
{
return buffer.Move(buffer.Length);
}
public static bool MoveToPreviousWord(this LineBuffer buffer)
{
var position = buffer.Position;
if (buffer.IsAtCharacter)
{
if (buffer.IsAtBeginningOfWord)
{
// At the beginning of a word.
// Move to the left side of the word.
buffer.MoveLeft();
// Move left until we encounter a new word
while (buffer.Position > 0 && !buffer.IsAtCharacter)
{
buffer.MoveLeft();
}
}
}
else
{
// Move until we encounter a word
while (buffer.Position > 0 && !buffer.IsAtCharacter)
{
buffer.MoveLeft();
}
}
// Move to the beginning of the word
buffer.MoveToBeginningOfWord();
// Return whether or not we moved
return position != buffer.Position;
}
public static bool MoveToNextWord(this LineBuffer buffer)
{
var position = buffer.Position;
if (buffer.IsAtCharacter)
{
// Move to the end of the word
buffer.MoveToEndOfWord();
// Move past any space
while (!buffer.AtEnd && !buffer.IsAtCharacter)
{
buffer.MoveRight();
}
}
else
{
// Move past any space
while (!buffer.AtEnd && !buffer.IsAtCharacter)
{
buffer.MoveRight();
}
}
// Return whether or not we moved
return position != buffer.Position;
}
public static bool MoveToEndOfWord(this LineBuffer buffer)
{
if (buffer.AtEnd)
{
return false;
}
// Not at a word? Do nothing
if (!buffer.IsAtCharacter)
{
return false;
}
var position = buffer.Position;
// Move until we find whitespace
while (!buffer.AtEnd && buffer.IsAtCharacter)
{
buffer.MoveRight();
}
// Return whether or not we moved
return position != buffer.Position;
}
public static bool MoveToBeginningOfWord(this LineBuffer buffer)
{
if (buffer.Position == 0)
{
return false;
}
// Not at a word? Do nothing
if (!buffer.IsAtCharacter)
{
return false;
}
var position = buffer.Position;
// Move until previous character is whitespace
while (buffer.Position > 0)
{
if (char.IsWhiteSpace(buffer.Content[buffer.Position - 1]))
{
break;
}
buffer.MoveLeft();
}
// Return whether or not we moved
return position != buffer.Position;
}
}
}

223
src/RadLine/LineEditor.cs Normal file
Просмотреть файл

@ -0,0 +1,223 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Spectre.Console;
namespace RadLine
{
public sealed class LineEditor : IHighlighterAccessor
{
private readonly IInputSource _source;
private readonly IServiceProvider? _provider;
private readonly IAnsiConsole _console;
private readonly LineBufferRenderer _presenter;
public KeyBindings KeyBindings { get; }
public bool MultiLine { get; init; } = false;
public string Text { get; init; } = string.Empty;
public ILineEditorPrompt Prompt { get; init; } = new LineEditorPrompt("[yellow]>[/]");
public ITextCompletion? Completion { get; init; }
public IHighlighter? Highlighter { get; init; }
public LineEditor(IAnsiConsole? terminal = null, IInputSource? source = null, IServiceProvider? provider = null)
{
_console = terminal ?? AnsiConsole.Console;
_source = source ?? new DefaultInputSource(_console);
_provider = provider;
_presenter = 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);
KeyBindings.Add<MoveEndCommand>(ConsoleKey.End);
KeyBindings.Add<MoveUpCommand>(ConsoleKey.UpArrow);
KeyBindings.Add<MoveDownCommand>(ConsoleKey.DownArrow);
KeyBindings.Add<MoveFirstLineCommand>(ConsoleKey.PageUp);
KeyBindings.Add<MoveLastLineCommand>(ConsoleKey.PageDown);
KeyBindings.Add<MoveLeftCommand>(ConsoleKey.LeftArrow);
KeyBindings.Add<MoveRightCommand>(ConsoleKey.RightArrow);
KeyBindings.Add<PreviousWordCommand>(ConsoleKey.LeftArrow, ConsoleModifiers.Control);
KeyBindings.Add<NextWordCommand>(ConsoleKey.RightArrow, ConsoleModifiers.Control);
KeyBindings.Add<SubmitCommand>(ConsoleKey.Enter);
KeyBindings.Add<NewLineCommand>(ConsoleKey.Enter, ConsoleModifiers.Shift);
}
public async Task<string?> ReadLine(CancellationToken cancellationToken)
{
var cancelled = false;
var state = new LineEditorState(Prompt, Text);
while (true)
{
var result = await ReadLine(state, cancellationToken).ConfigureAwait(false);
if (result.Result == SubmitAction.Cancel)
{
cancelled = true;
break;
}
else if (result.Result == SubmitAction.Submit)
{
break;
}
else if (result.Result == SubmitAction.NewLine && MultiLine && state.IsLastLine)
{
AddNewLine(state);
}
else if (result.Result == SubmitAction.MoveUp && MultiLine)
{
MoveUp(state);
}
else if (result.Result == SubmitAction.MoveDown && MultiLine)
{
MoveDown(state);
}
else if (result.Result == SubmitAction.MoveFirst && MultiLine)
{
MoveFirst(state);
}
else if (result.Result == SubmitAction.MoveLast && MultiLine)
{
MoveLast(state);
}
}
_presenter.RenderLine(state, cursorPosition: 0);
// Move the cursor to the last line
while (state.MoveDown())
{
_console.Cursor.MoveDown();
}
// 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();
// Return all the lines
return cancelled ? null : state.Text.TrimEnd('\r', '\n');
}
private async Task<(LineBuffer Buffer, SubmitAction Result)> ReadLine(
LineEditorState state,
CancellationToken cancellationToken)
{
var provider = new DefaultServiceProvider(_provider);
provider.RegisterOptional<ITextCompletion, ITextCompletion>(Completion);
var context = new LineEditorContext(state.Buffer, provider);
_presenter.RenderLine(state);
while (true)
{
if (cancellationToken.IsCancellationRequested)
{
return (state.Buffer, SubmitAction.Cancel);
}
// Get command
var command = default(LineEditorCommand);
var key = await _source.ReadKey(cancellationToken).ConfigureAwait(false);
if (key != null)
{
if (key.Value.KeyChar != 0 && !char.IsControl(key.Value.KeyChar))
{
command = new InsertCommand(key.Value.KeyChar);
}
else
{
command = KeyBindings.GetCommand(key.Value.Key, key.Value.Modifiers);
}
}
// Execute command
if (command != null)
{
context.Execute(command);
}
// Time to exit?
if (context.Result != null)
{
return (state.Buffer, context.Result.Value);
}
// Render the line
_presenter.RenderLine(state);
}
}
private void AddNewLine(LineEditorState state)
{
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();
}
}
private void MoveUp(LineEditorState state)
{
Move(state, state =>
{
if (state.MoveUp())
{
_console.Cursor.MoveUp();
}
});
}
private void MoveDown(LineEditorState state)
{
Move(state, state =>
{
if (state.MoveDown())
{
_console.Cursor.MoveDown();
}
});
}
private void MoveFirst(LineEditorState state)
{
Move(state, state =>
{
while (state.MoveUp())
{
_console.Cursor.MoveUp();
}
});
}
private void MoveLast(LineEditorState state)
{
Move(state, state =>
{
while (state.MoveDown())
{
_console.Cursor.MoveDown();
}
});
}
private void Move(LineEditorState state, Action<LineEditorState> action)
{
using (_console.HideCursor())
{
_presenter.RenderLine(state, cursorPosition: 0);
var position = state.Buffer.Position;
action(state);
state.Buffer.Move(position);
}
}
}
}

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

@ -0,0 +1,7 @@
namespace RadLine
{
public abstract class LineEditorCommand
{
public abstract void Execute(LineEditorContext context);
}
}

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

@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
namespace RadLine
{
public sealed class LineEditorContext : IServiceProvider
{
private readonly Dictionary<string, object?> _state;
private readonly IServiceProvider? _provider;
public LineBuffer Buffer { get; }
internal SubmitAction? Result { get; private set; }
public LineEditorContext(LineBuffer buffer, IServiceProvider? provider = null)
{
_state = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
_provider = provider;
Buffer = buffer ?? throw new ArgumentNullException(nameof(buffer));
}
public object? GetService(Type serviceType)
{
if (_provider != null)
{
return _provider.GetService(serviceType);
}
return null;
}
public void Execute(LineEditorCommand command)
{
if (Result != null)
{
// Don't execute any command
// if we're suppose to exit the
// current context.
return;
}
command.Execute(this);
}
public void SetState<T>(string key, T value)
{
_state[key] = value;
}
public T GetState<T>(string key, Func<T> defaultValue)
{
if (_state.TryGetValue(key, out var value))
{
if (value is T typedValue)
{
return typedValue;
}
}
return defaultValue();
}
public void Submit(SubmitAction action)
{
Result = action;
}
}
}

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

@ -0,0 +1,42 @@
using System;
using Spectre.Console;
namespace RadLine
{
public sealed class LineEditorPrompt : ILineEditorPrompt
{
private readonly Markup _prompt;
private readonly Markup? _more;
public LineEditorPrompt(string prompt, string? more = null)
{
if (prompt is null)
{
throw new ArgumentNullException(nameof(prompt));
}
_prompt = new Markup(prompt);
_more = more != null ? new Markup(more) : null;
if (_prompt.Lines > 1)
{
throw new ArgumentException("Prompt cannot contain line breaks", nameof(prompt));
}
if (_more?.Lines > 1)
{
throw new ArgumentException("Prompt cannot contain line breaks", nameof(more));
}
}
public (Markup Markup, int Margin) GetPrompt(int line)
{
if (line == 0)
{
return (_prompt, 1);
}
return (_more ?? _prompt, 1);
}
}
}

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

@ -0,0 +1,19 @@
using Spectre.Console;
namespace RadLine
{
public sealed class LineNumberPrompt : ILineEditorPrompt
{
private readonly Style _style;
public LineNumberPrompt(Style? style = null)
{
_style = style ?? new Style(foreground: Color.Yellow, background: Color.Blue);
}
public (Markup Markup, int Margin) GetPrompt(int line)
{
return (new Markup(line.ToString("D2"), _style), 1);
}
}
}

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

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net5.0;netstandard2.0</TargetFrameworks>
<Nullable>enable</Nullable>
<IsPackable>true</IsPackable>
</PropertyGroup>
<ItemGroup>
<AdditionalFiles Include="..\stylecop.json" Link="Properties/stylecop.json" />
<None Include="../../resources/gfx/small-logo.png" Pack="true" PackagePath="\" Link="Properties/small-logo.png" />
</ItemGroup>
<ItemGroup>
<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>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>

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

@ -0,0 +1,13 @@
namespace RadLine
{
public enum SubmitAction
{
Cancel,
Submit,
NewLine,
MoveDown,
MoveUp,
MoveFirst,
MoveLast,
}
}

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

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using Spectre.Console;
namespace RadLine
{
public sealed class WordHighlighter : IHighlighter
{
private readonly Dictionary<string, Style> _words;
public WordHighlighter(StringComparer? comparer = null)
{
_words = new Dictionary<string, Style>(comparer ?? StringComparer.OrdinalIgnoreCase);
}
public WordHighlighter AddWord(string word, Style style)
{
_words[word] = style;
return this;
}
Style? IHighlighter.Highlight(string token)
{
_words.TryGetValue(token, out var style);
return style;
}
}
}

26
src/stylecop.json Normal file
Просмотреть файл

@ -0,0 +1,26 @@
{
"$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
"settings": {
"documentationRules": {
"documentExposedElements": true,
"documentInternalElements": false,
"documentPrivateElements": false,
"documentPrivateFields": false
},
"layoutRules": {
"newlineAtEndOfFile": "allow",
"allowConsecutiveUsings": true
},
"orderingRules": {
"usingDirectivesPlacement": "outsideNamespace",
"systemUsingDirectivesFirst": true,
"elementOrder": [
"kind",
"accessibility",
"constant",
"static",
"readonly"
]
}
}
}