From: Mike McLaughlin Date: Sun, 17 Feb 2019 20:07:05 +0000 (-0800) Subject: Add Microsoft.Diagnostic.Repl containing console and command functions. X-Git-Tag: submit/tizen/20190813.035844~56^2~2 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=551fdccac122c8016766dfff96ef9760a23c925e;p=platform%2Fcore%2Fdotnet%2Fdiagnostics.git Add Microsoft.Diagnostic.Repl containing console and command functions. --- diff --git a/diagnostics.sln b/diagnostics.sln index 03b8f9473..a4f7cac45 100644 --- a/diagnostics.sln +++ b/diagnostics.sln @@ -39,6 +39,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SOS.InstallHelper", "src\SO EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-sos", "src\Tools\dotnet-sos\dotnet-sos.csproj", "{41351955-16D5-48D7-AF4C-AF25F5FB2E78}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SOS.Hosting", "src\SOS\SOS.Hosting\SOS.Hosting.csproj", "{ED27F39F-DF5C-4E22-87E0-EC5B5873B503}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Diagnostic.Repl", "src\Microsoft.Diagnostic.Repl\Microsoft.Diagnostic.Repl.csproj", "{90CF2633-58F0-44EE-943B-D70207455F20}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Checked|Any CPU = Checked|Any CPU @@ -593,6 +597,46 @@ Global {41351955-16D5-48D7-AF4C-AF25F5FB2E78}.RelWithDebInfo|x64.Build.0 = Release|Any CPU {41351955-16D5-48D7-AF4C-AF25F5FB2E78}.RelWithDebInfo|x86.ActiveCfg = Release|Any CPU {41351955-16D5-48D7-AF4C-AF25F5FB2E78}.RelWithDebInfo|x86.Build.0 = Release|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.Checked|Any CPU.Build.0 = Debug|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.Checked|ARM.ActiveCfg = Debug|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.Checked|ARM.Build.0 = Debug|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.Checked|ARM64.ActiveCfg = Debug|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.Checked|ARM64.Build.0 = Debug|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.Checked|x64.ActiveCfg = Debug|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.Checked|x64.Build.0 = Debug|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.Checked|x86.ActiveCfg = Debug|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.Checked|x86.Build.0 = Debug|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.Debug|Any CPU.Build.0 = Debug|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.Debug|ARM.ActiveCfg = Debug|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.Debug|ARM.Build.0 = Debug|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.Debug|ARM64.Build.0 = Debug|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.Debug|x64.ActiveCfg = Debug|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.Debug|x64.Build.0 = Debug|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.Debug|x86.ActiveCfg = Debug|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.Debug|x86.Build.0 = Debug|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.Release|Any CPU.ActiveCfg = Release|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.Release|Any CPU.Build.0 = Release|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.Release|ARM.ActiveCfg = Release|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.Release|ARM.Build.0 = Release|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.Release|ARM64.ActiveCfg = Release|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.Release|ARM64.Build.0 = Release|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.Release|x64.ActiveCfg = Release|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.Release|x64.Build.0 = Release|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.Release|x86.ActiveCfg = Release|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.Release|x86.Build.0 = Release|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.RelWithDebInfo|Any CPU.ActiveCfg = Release|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.RelWithDebInfo|Any CPU.Build.0 = Release|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.RelWithDebInfo|ARM.ActiveCfg = Release|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.RelWithDebInfo|ARM.Build.0 = Release|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.RelWithDebInfo|ARM64.ActiveCfg = Release|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.RelWithDebInfo|ARM64.Build.0 = Release|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.RelWithDebInfo|x64.ActiveCfg = Release|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.RelWithDebInfo|x64.Build.0 = Release|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.RelWithDebInfo|x86.ActiveCfg = Release|Any CPU + {90CF2633-58F0-44EE-943B-D70207455F20}.RelWithDebInfo|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -615,6 +659,7 @@ Global {20EBC3C4-917C-402D-B778-9A6E3742BF5A} = {41638A4C-0DAF-47ED-A774-ECBBAC0315D7} {1F012743-941B-4915-8C55-02097894CF3F} = {41638A4C-0DAF-47ED-A774-ECBBAC0315D7} {41351955-16D5-48D7-AF4C-AF25F5FB2E78} = {B62728C8-1267-4043-B46F-5537BBAEC692} + {90CF2633-58F0-44EE-943B-D70207455F20} = {19FAB78C-3351-4911-8F0C-8C6056401740} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {46465737-C938-44FC-BE1A-4CE139EBB5E0} diff --git a/eng/Versions.props b/eng/Versions.props index b533cb0d5..441e3ea30 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -29,7 +29,8 @@ https://dotnetfeed.blob.core.windows.net/dotnet-core/index.json; https://dotnet.myget.org/F/symstore/api/v3/index.json; https://dotnet.myget.org/F/dotnet-core/api/v3/index.json; - https://dotnet.myget.org/F/dotnet-buildtools/api/v3/index.json + https://dotnet.myget.org/F/dotnet-buildtools/api/v3/index.json; + https://dotnet.myget.org/F/system-commandline/api/v3/index.json diff --git a/src/Microsoft.Diagnostic.Repl/Command/Attributes.cs b/src/Microsoft.Diagnostic.Repl/Command/Attributes.cs new file mode 100644 index 000000000..8083b5e17 --- /dev/null +++ b/src/Microsoft.Diagnostic.Repl/Command/Attributes.cs @@ -0,0 +1,61 @@ +// -------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// -------------------------------------------------------------------- +using System; + +namespace Microsoft.Diagnostic.Repl +{ + /// + /// Base command option attribute. + /// + public class BaseAttribute : Attribute + { + /// + /// Name of the command + /// + public string Name; + + /// + /// Displayed in the help for the command + /// + public string Help; + } + + /// + /// Marks the class as a Command. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] + public class CommandAttribute : BaseAttribute + { + /// + /// Sets the value of the CommandBase.AliasExpansion when the command is executed. + /// + public string AliasExpansion; + } + + /// + /// Marks the property as a Option. + /// + [AttributeUsage(AttributeTargets.Property)] + public class OptionAttribute : BaseAttribute + { + } + + /// + /// Adds an alias to the Option. Help is ignored. + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] + public class OptionAliasAttribute : BaseAttribute + { + } + + /// + /// Marks the property the command Argument. + /// + [AttributeUsage(AttributeTargets.Property)] + public class ArgumentAttribute : BaseAttribute + { + } +} diff --git a/src/Microsoft.Diagnostic.Repl/Command/CommandBase.cs b/src/Microsoft.Diagnostic.Repl/Command/CommandBase.cs new file mode 100644 index 000000000..7c18f6963 --- /dev/null +++ b/src/Microsoft.Diagnostic.Repl/Command/CommandBase.cs @@ -0,0 +1,59 @@ +// -------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// -------------------------------------------------------------------- +using System; +using System.CommandLine; +using System.CommandLine.Invocation; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostic.Repl +{ + /// + /// The common command context + /// + public abstract class CommandBase + { + public const string EntryPointName = nameof(InvokeAsync); + + /// + /// Parser invocation context. Contains the ParseResult, CommandResult, etc. + /// + public InvocationContext InvocationContext { get; set; } + + /// + /// Console instance + /// + public IConsole Console { get { return InvocationContext.Console; } } + + /// + /// The AliasExpansion value from the CommandAttribute or null if none. + /// + public string AliasExpansion { get; set; } + + /// + /// Execute the command + /// + public abstract Task InvokeAsync(); + + /// + /// Display text + /// + /// text message + protected void WriteLine(string message) + { + Console.Out.WriteLine(message); + } + + /// + /// Display formatted text + /// + /// format string + /// arguments + protected void WriteLine(string format, params object[] args) + { + Console.Out.WriteLine(string.Format(format, args)); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Diagnostic.Repl/Command/CommandProcessor.cs b/src/Microsoft.Diagnostic.Repl/Command/CommandProcessor.cs new file mode 100644 index 000000000..957559932 --- /dev/null +++ b/src/Microsoft.Diagnostic.Repl/Command/CommandProcessor.cs @@ -0,0 +1,233 @@ +// -------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// -------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.CommandLine.Builder; +using System.CommandLine.Invocation; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostic.Repl +{ + public class CommandProcessor + { + private readonly Parser _parser; + private readonly Command _rootCommand; + + /// + /// Domain specific context passed to commands + /// + public object CommandContext { get; set; } + + /// + /// Create an instance of the command processor; + /// + /// The list of assemblies to look for commands + public CommandProcessor(IEnumerable assemblies) + { + var rootBuilder = new CommandLineBuilder(); + rootBuilder.UseHelp() + .UseParseDirective() + .UseSuggestDirective() + .UseParseErrorReporting() + .UseExceptionHandler(); + BuildCommands(rootBuilder, assemblies); + _rootCommand = rootBuilder.Command; + _parser = rootBuilder.Build(); + } + + /// + /// Parse the command line. + /// + /// command line txt + /// option console + /// exit code + public Task Parse(string commandLine, IConsole console = null) + { + ParseResult result = _parser.Parse(commandLine); + return _parser.InvokeAsync(result, console); + } + + /// + /// Display all the help or a specific command's help. + /// + /// command name or null + /// command instance or null if not found + public Command GetCommand(string name) + { + if (string.IsNullOrEmpty(name)) { + return _rootCommand; + } + else { + return _rootCommand.Children.OfType().FirstOrDefault((cmd) => name == cmd.Name || cmd.Aliases.Any((alias) => name == alias)); + } + } + + private void BuildCommands(CommandLineBuilder rootBuilder, IEnumerable assemblies) + { + foreach (Type type in assemblies.SelectMany((assembly) => assembly.GetExportedTypes())) + { + Command command = null; + + var commandAttributes = (CommandAttribute[])type.GetCustomAttributes(typeof(CommandAttribute), inherit: false); + foreach (CommandAttribute commandAttribute in commandAttributes) + { + // If there is a previous command and the current command doesn't have help or alias expansion, use "simple" aliasing + if (command != null && commandAttribute.Help == null && commandAttribute.AliasExpansion == null) { + command.AddAlias(commandAttribute.Name); + continue; + } + command = new Command(commandAttribute.Name, commandAttribute.Help); + var builder = new CommandLineBuilder(command); + builder.UseHelp(); + + var properties = new List<(PropertyInfo, Option)>(); + PropertyInfo argument = null; + + foreach (PropertyInfo property in type.GetProperties().Where(p => p.CanWrite)) + { + var argumentAttribute = (ArgumentAttribute)property.GetCustomAttributes(typeof(ArgumentAttribute), inherit: false).SingleOrDefault(); + if (argumentAttribute != null) + { + if (argument != null) { + throw new ArgumentException($"More than one ArgumentAttribute in command class: {type.Name}"); + } + command.Argument = new Argument { + Name = argumentAttribute.Name ?? property.Name.ToLowerInvariant(), + Description = argumentAttribute.Help, + ArgumentType = property.PropertyType, + Arity = new ArgumentArity(0, int.MaxValue) + }; + argument = property; + } + else + { + var optionAttribute = (OptionAttribute)property.GetCustomAttributes(typeof(OptionAttribute), inherit: false).SingleOrDefault(); + if (optionAttribute != null) + { + var option = new Option(optionAttribute.Name ?? BuildAlias(property.Name), optionAttribute.Help, new Argument { ArgumentType = property.PropertyType }); + command.AddOption(option); + properties.Add((property, option)); + + foreach (var optionAliasAttribute in (OptionAliasAttribute[])property.GetCustomAttributes(typeof(OptionAliasAttribute), inherit: false)) + { + option.AddAlias(optionAliasAttribute.Name); + } + } + else + { + // If not an option, add as just a settable properties + properties.Add((property, null)); + } + } + } + + command.Handler = new Handler(this, commandAttribute.AliasExpansion, argument, properties, type); + rootBuilder.AddCommand(command); + } + } + } + + private static string BuildAlias(string parameterName) + { + if (string.IsNullOrWhiteSpace(parameterName)) + { + throw new ArgumentException("Value cannot be null or whitespace.", nameof(parameterName)); + } + return parameterName.Length > 1 ? $"--{parameterName.ToKebabCase()}" : $"-{parameterName.ToLowerInvariant()}"; + } + + class Handler : ICommandHandler + { + private readonly CommandProcessor _commandProcessor; + private readonly string _aliasExpansion; + private readonly PropertyInfo _argument; + private readonly IEnumerable<(PropertyInfo Property, Option Option)> _properties; + + private readonly ConstructorInfo _constructor; + private readonly MethodInfo _methodInfo; + + public Handler(CommandProcessor commandProcessor, string aliasExpansion, PropertyInfo argument, IEnumerable<(PropertyInfo, Option)> properties, Type type) + { + _commandProcessor = commandProcessor; + _aliasExpansion = aliasExpansion; + _argument = argument; + _properties = properties; + + _constructor = type.GetConstructors().SingleOrDefault((info) => info.GetParameters().Length == 0) ?? + throw new ArgumentException($"No eligible constructor found in {type}"); + + _methodInfo = type.GetMethod(CommandBase.EntryPointName, new Type[] { typeof(IHelpBuilder) }) ?? type.GetMethod(CommandBase.EntryPointName) ?? + throw new ArgumentException($"{CommandBase.EntryPointName} method not found in {type}"); + } + + public Task InvokeAsync(InvocationContext context) + { + try + { + // Assumes zero parameter constructor + object instance = _constructor.Invoke(new object[0]); + SetProperties(context, instance); + + var methodBinder = new MethodBinder(_methodInfo, () => instance); + return methodBinder.InvokeAsync(context); + } + catch (TargetInvocationException ex) + { + throw ex.InnerException; + } + } + + private void SetProperties(InvocationContext context, object instance) + { + IEnumerable optionResults = context.ParseResult.CommandResult.Children.OfType(); + + foreach (var property in _properties) + { + object value = property.Property.GetValue(instance); + + if (property.Property.Name == nameof(CommandBase.AliasExpansion)) { + value = _aliasExpansion; + } + else + { + Type propertyType = property.Property.PropertyType; + if (propertyType == typeof(InvocationContext)) { + value = context; + } + else if (propertyType == typeof(IConsole)) { + value = context.Console; + } + else if (propertyType == typeof(CommandProcessor)) { + value = _commandProcessor; + } + else if (propertyType == _commandProcessor.CommandContext?.GetType()) { + value = _commandProcessor.CommandContext; + } + else if (property.Option != null) + { + OptionResult optionResult = optionResults.Where((result) => result.Option == property.Option).SingleOrDefault(); + if (optionResult != null) { + value = optionResult.GetValueOrDefault(); + } + } + } + + property.Property.SetValue(instance, value); + } + + if (_argument != null) + { + object value = context.ParseResult.CommandResult.GetValueOrDefault(); + _argument.SetValue(instance, value); + } + } + } + } +} diff --git a/src/Microsoft.Diagnostic.Repl/Console/CharToLineConverter.cs b/src/Microsoft.Diagnostic.Repl/Console/CharToLineConverter.cs new file mode 100644 index 000000000..ffb2b3c47 --- /dev/null +++ b/src/Microsoft.Diagnostic.Repl/Console/CharToLineConverter.cs @@ -0,0 +1,59 @@ +// -------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// -------------------------------------------------------------------- + +using System; +using System.Text; + +namespace Microsoft.Diagnostic.Repl +{ + public sealed class CharToLineConverter + { + readonly Action m_callback; + readonly StringBuilder m_text = new StringBuilder(); + + public CharToLineConverter(Action callback) + { + m_callback = callback; + } + + public void Input(byte[] buffer, int offset, int count) + { + for (int i = 0; i < count; i++) { + char c = (char)buffer[offset + i]; + if (c == '\r') { + continue; + } + if (c == '\n') { + Flush(); + } + else if (c == '\t' || (c >= (char)0x20 && c <= (char)127)) { + m_text.Append(c); + } + } + } + + public void Input(string text) + { + foreach (char c in text) { + if (c == '\r') { + continue; + } + if (c == '\n') { + Flush(); + } + else if (c == '\t' || (c >= (char)0x20 && c <= (char)127)) { + m_text.Append(c); + } + } + } + + public void Flush() + { + m_callback(m_text.ToString()); + m_text.Clear(); + } + } +} diff --git a/src/Microsoft.Diagnostic.Repl/Console/ConsoleProvider.cs b/src/Microsoft.Diagnostic.Repl/Console/ConsoleProvider.cs new file mode 100644 index 000000000..d4f9bbd2b --- /dev/null +++ b/src/Microsoft.Diagnostic.Repl/Console/ConsoleProvider.cs @@ -0,0 +1,487 @@ +// -------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// -------------------------------------------------------------------- +using System; +using System.Collections.Generic; +using System.CommandLine; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Diagnostic.Repl +{ + public sealed class ConsoleProvider : IConsole + { + private readonly List m_history; + + private readonly CharToLineConverter m_consoleConverter; + private readonly CharToLineConverter m_warningConverter; + private readonly CharToLineConverter m_errorConverter; + + private string m_prompt = "> "; + + private bool m_shutdown; + private CancellationTokenSource m_interruptExecutingCommand; + + private string m_clearLine; + private bool m_refreshingLine; + private StringBuilder m_activeLine; + + private int m_selectedHistory; + + private bool m_modified; + private int m_cursorPosition; + private int m_scrollPosition; + private bool m_insertMode; + + private int m_commandExecuting; + private string m_lastCommandLine; + + /// + /// Create an instance of the console provider + /// + /// error color (default red) + /// warning color (default yellow) + public ConsoleProvider(ConsoleColor errorColor = ConsoleColor.Red, ConsoleColor warningColor = ConsoleColor.Yellow) + { + m_history = new List(); + m_activeLine = new StringBuilder(); + + m_consoleConverter = new CharToLineConverter(text => { + NewOutput(text); + }); + + m_warningConverter = new CharToLineConverter(text => { + NewOutput(text, warningColor); + }); + + m_errorConverter = new CharToLineConverter(text => { + NewOutput(text, errorColor); + }); + + Out = new StandardStreamWriter((text) => WriteOutput(OutputType.Normal, text)); + Error = new StandardStreamWriter((text) => WriteOutput(OutputType.Error, text)); + + // Hook ctrl-C and ctrl-break + Console.CancelKeyPress += new ConsoleCancelEventHandler(OnCtrlBreakKeyPress); + } + + /// + /// Start input processing and command dispatching + /// + /// Called to dispatch a command on ENTER + public async Task Start(Func dispatchCommand) + { + m_lastCommandLine = null; + m_shutdown = false; + RefreshLine(); + + // Start keyboard processing + while (!m_shutdown) { + ConsoleKeyInfo keyInfo = Console.ReadKey(true); + await ProcessKeyInfo(keyInfo, dispatchCommand); + } + } + + /// + /// Stop input processing/dispatching + /// + public void Stop() + { + ClearLine(); + m_shutdown = true; + Console.CancelKeyPress -= new ConsoleCancelEventHandler(OnCtrlBreakKeyPress); + } + + /// + /// Change the command prompt + /// + /// new prompt + public void SetPrompt(string prompt) + { + m_prompt = prompt; + RefreshLine(); + } + + /// + /// Writes a message with a new line to console. + /// + public void WriteLine(OutputType type, string format, params object[] parameters) + { + WriteOutput(type, string.Format(format + Environment.NewLine, parameters)); + } + + /// + /// Write text on the console screen + /// + /// output type + /// text + /// ctrl-c interrupted the command + public void WriteOutput(OutputType type, string message) + { + switch (type) + { + case OutputType.Normal: + m_consoleConverter.Input(message); + break; + + case OutputType.Warning: + m_warningConverter.Input(message); + break; + + case OutputType.Error: + m_errorConverter.Input(message); + break; + } + } + + /// + /// Clear the console screen + /// + public void ClearScreen() + { + Console.Clear(); + PrintActiveLine(); + } + + /// + /// Write a line to the console. + /// + /// line of text + /// color of the text + private void NewOutput(string text, ConsoleColor? color = null) + { + ClearLine(); + + ConsoleColor? originalColor = null; + if (color.HasValue) { + originalColor = Console.ForegroundColor; + Console.ForegroundColor = color.Value; + } + Console.WriteLine(text); + if (originalColor.HasValue) { + Console.ForegroundColor = originalColor.Value; + } + + PrintActiveLine(); + } + + /// + /// This is the ctrl-c/ctrl-break handler + /// + private void OnCtrlBreakKeyPress(object sender, ConsoleCancelEventArgs e) + { + if (!m_shutdown) { + if (m_interruptExecutingCommand != null) { + m_interruptExecutingCommand.Cancel(); + } + e.Cancel = true; + } + } + + private void CommandStarting() + { + if (m_commandExecuting == 0) { + ClearLine(); + } + m_commandExecuting++; + } + + private void CommandFinished() + { + if (--m_commandExecuting == 0) { + RefreshLine(); + } + } + + private void ClearLine() + { + if (m_commandExecuting != 0) { + return; + } + + if (m_clearLine == null || m_clearLine.Length != Console.WindowWidth) { + m_clearLine = "\r" + new string(' ', Console.WindowWidth - 1); + } + + Console.Write(m_clearLine); + Console.CursorLeft = 0; + } + + private void PrintActiveLine() + { + if (m_shutdown) { + return; + } + + if (m_commandExecuting != 0) { + return; + } + + string prompt = m_prompt; + + int lineWidth = 80; + if (Console.WindowWidth > prompt.Length) { + lineWidth = Console.WindowWidth - prompt.Length - 1; + } + int scrollIncrement = lineWidth / 3; + + int activeLineLen = m_activeLine.Length; + + m_scrollPosition = Math.Min(Math.Max(m_scrollPosition, 0), activeLineLen); + m_cursorPosition = Math.Min(Math.Max(m_cursorPosition, 0), activeLineLen); + + while (m_cursorPosition < m_scrollPosition) { + m_scrollPosition = Math.Max(m_scrollPosition - scrollIncrement, 0); + } + + while (m_cursorPosition - m_scrollPosition > lineWidth - 5) { + m_scrollPosition += scrollIncrement; + } + + int lineRest = activeLineLen - m_scrollPosition; + int max = Math.Min(lineRest, lineWidth); + string text = m_activeLine.ToString(m_scrollPosition, max); + + Console.Write("{0}{1}", prompt, text); + Console.CursorLeft = prompt.Length + (m_cursorPosition - m_scrollPosition); + } + + private void RefreshLine() + { + // Check for recursions. + if (m_refreshingLine) { + return; + } + m_refreshingLine = true; + ClearLine(); + PrintActiveLine(); + m_refreshingLine = false; + } + + private async Task ProcessKeyInfo(ConsoleKeyInfo keyInfo, Func dispatchCommand) + { + int activeLineLen = m_activeLine.Length; + + switch (keyInfo.Key) { + case ConsoleKey.Backspace: // The BACKSPACE key. + if (m_cursorPosition > 0) { + EnsureNewEntry(); + m_activeLine.Remove(m_cursorPosition - 1, 1); + m_cursorPosition--; + RefreshLine(); + } + break; + + case ConsoleKey.Insert: // The INS (INSERT) key. + m_insertMode = !m_insertMode; + RefreshLine(); + break; + + case ConsoleKey.Delete: // The DEL (DELETE) key. + if (m_cursorPosition < activeLineLen) { + EnsureNewEntry(); + m_activeLine.Remove(m_cursorPosition, 1); + RefreshLine(); + } + break; + + case ConsoleKey.Enter: // The ENTER key. + string newCommand = m_activeLine.ToString(); + + if (m_modified) { + m_history.Add(m_activeLine); + } + m_selectedHistory = m_history.Count; + + await Dispatch(newCommand, dispatchCommand); + + SwitchToHistoryEntry(); + break; + + case ConsoleKey.Escape: // The ESC (ESCAPE) key. + EnsureNewEntry(); + m_activeLine.Clear(); + m_cursorPosition = 0; + RefreshLine(); + break; + + case ConsoleKey.End: // The END key. + m_cursorPosition = activeLineLen; + RefreshLine(); + break; + + case ConsoleKey.Home: // The HOME key. + m_cursorPosition = 0; + RefreshLine(); + break; + + case ConsoleKey.LeftArrow: // The LEFT ARROW key. + if (keyInfo.Modifiers == ConsoleModifiers.Control) { + while (m_cursorPosition > 0 && char.IsWhiteSpace(m_activeLine[m_cursorPosition - 1])) { + m_cursorPosition--; + } + + while (m_cursorPosition > 0 && !char.IsWhiteSpace(m_activeLine[m_cursorPosition - 1])) { + m_cursorPosition--; + } + } + else { + m_cursorPosition--; + } + + RefreshLine(); + break; + + case ConsoleKey.UpArrow: // The UP ARROW key. + if (m_selectedHistory > 0) { + m_selectedHistory--; + } + SwitchToHistoryEntry(); + break; + + case ConsoleKey.RightArrow: // The RIGHT ARROW key. + if (keyInfo.Modifiers == ConsoleModifiers.Control) { + while (m_cursorPosition < activeLineLen && !char.IsWhiteSpace(m_activeLine[m_cursorPosition])) { + m_cursorPosition++; + } + + while (m_cursorPosition < activeLineLen && char.IsWhiteSpace(m_activeLine[m_cursorPosition])) { + m_cursorPosition++; + } + } + else { + m_cursorPosition++; + } + + RefreshLine(); + break; + + case ConsoleKey.DownArrow: // The DOWN ARROW key. + if (m_selectedHistory < m_history.Count) { + m_selectedHistory++; + } + SwitchToHistoryEntry(); + + RefreshLine(); + break; + + default: + if (keyInfo.KeyChar != 0) { + if ((keyInfo.Modifiers & (ConsoleModifiers.Control | ConsoleModifiers.Alt)) == 0) { + AppendNewText(new string(keyInfo.KeyChar, 1)); + } + } + break; + } + } + + private async Task Dispatch(string newCommand, Func dispatchCommand) + { + CommandStarting(); + m_interruptExecutingCommand = new CancellationTokenSource(); + try + { + newCommand = newCommand.Trim(); + if (string.IsNullOrEmpty(newCommand) && m_lastCommandLine != null) { + newCommand = m_lastCommandLine; + } + try + { + WriteLine(OutputType.Normal, "{0}{1}", m_prompt, newCommand); + await dispatchCommand(newCommand, m_interruptExecutingCommand.Token); + m_lastCommandLine = newCommand; + } + catch (OperationCanceledException) + { + // ctrl-c interrupted the command + m_lastCommandLine = null; + } + catch (Exception ex) when (!(ex is NullReferenceException || ex is ArgumentNullException)) + { + WriteLine(OutputType.Error, "ERROR: {0}", ex.Message); + m_lastCommandLine = null; + } + } + finally + { + m_interruptExecutingCommand = null; + CommandFinished(); + } + } + + private void AppendNewText(string text) + { + EnsureNewEntry(); + + foreach (char c in text) { + // Filter unwanted characters. + switch (c) { + case '\t': + case '\r': + case '\n': + continue; + } + + if (m_insertMode && m_cursorPosition < m_activeLine.Length) { + m_activeLine[m_cursorPosition] = c; + } + else { + m_activeLine.Insert(m_cursorPosition, c); + } + m_modified = true; + m_cursorPosition++; + } + + RefreshLine(); + } + + private void SwitchToHistoryEntry() + { + if (m_selectedHistory < m_history.Count) { + m_activeLine = m_history[m_selectedHistory]; + } + else { + m_activeLine = new StringBuilder(); + } + + m_cursorPosition = m_activeLine.Length; + m_modified = false; + + RefreshLine(); + } + + private void EnsureNewEntry() + { + if (!m_modified) { + m_activeLine = new StringBuilder(m_activeLine.ToString()); + m_modified = true; + } + } + + #region IConsole + + public IStandardStreamWriter Out { get; } + + bool IStandardOut.IsOutputRedirected { get { return false; } } + + public IStandardStreamWriter Error { get; } + + bool IStandardError.IsErrorRedirected { get { return false; } } + + bool IStandardIn.IsInputRedirected { get { return false; } } + + class StandardStreamWriter : IStandardStreamWriter + { + readonly Action _write; + + public StandardStreamWriter(Action write) => _write = write; + + void IStandardStreamWriter.Write(string value) => _write(value); + } + + #endregion + } +} diff --git a/src/Microsoft.Diagnostic.Repl/Console/OutputType.cs b/src/Microsoft.Diagnostic.Repl/Console/OutputType.cs new file mode 100644 index 000000000..f1530aea8 --- /dev/null +++ b/src/Microsoft.Diagnostic.Repl/Console/OutputType.cs @@ -0,0 +1,18 @@ +// -------------------------------------------------------------------- +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// +// -------------------------------------------------------------------- + +namespace Microsoft.Diagnostic.Repl +{ + /// + /// The type of output. + /// + public enum OutputType + { + Normal = 1, + Error = 2, + Warning = 3, + } +} diff --git a/src/Microsoft.Diagnostic.Repl/Microsoft.Diagnostic.Repl.csproj b/src/Microsoft.Diagnostic.Repl/Microsoft.Diagnostic.Repl.csproj new file mode 100644 index 000000000..3fcbc8323 --- /dev/null +++ b/src/Microsoft.Diagnostic.Repl/Microsoft.Diagnostic.Repl.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.0 + true + ;1591;1701 + Diagnostic utility functions and helpers + $(Description) + embedded + true + + + + + + +