diff --git a/Directory.Build.props b/Directory.Build.props index 8ca8a8757b..9921cb358e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,4 +1,4 @@ - + @@ -16,6 +16,7 @@ true MicrosoftNuGet Microsoft + Microsoft3rdPartyAppComponentDual true true diff --git a/build/dependencies.props b/build/dependencies.props index ee0b0b8ccf..bffcc722e2 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -17,7 +17,7 @@ 2.0.3 4.5.1 4.5.0 - 11.0.2 + 11.0.2 9.0.1 2.3.1 2.4.0 diff --git a/src/Microsoft.HttpRepl/Commands/BaseHttpCommand.cs b/src/Microsoft.HttpRepl/Commands/BaseHttpCommand.cs index 374544790b..ee9b4bcf0e 100644 --- a/src/Microsoft.HttpRepl/Commands/BaseHttpCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/BaseHttpCommand.cs @@ -31,6 +31,8 @@ namespace Microsoft.HttpRepl.Commands private const string ResponseFileOption = nameof(ResponseFileOption); private const string BodyFileOption = nameof(BodyFileOption); private const string NoBodyOption = nameof(NoBodyOption); + private const string NoFormattingOption = nameof(NoFormattingOption); + private const string NoStreamingOption = nameof(NoStreamingOption); private const string BodyContentOption = nameof(BodyContentOption); private static readonly char[] HeaderSeparatorChars = new[] { '=', ':' }; @@ -54,7 +56,9 @@ namespace Microsoft.HttpRepl.Commands .WithOption(new CommandOptionSpecification(HeaderOption, requiresValue: true, forms: new[] {"--header", "-h"})) .WithOption(new CommandOptionSpecification(ResponseFileOption, requiresValue: true, maximumOccurrences: 1, forms: new[] { "--response", })) .WithOption(new CommandOptionSpecification(ResponseHeadersFileOption, requiresValue: true, maximumOccurrences: 1, forms: new[] { "--response:headers", })) - .WithOption(new CommandOptionSpecification(ResponseBodyFileOption, requiresValue: true, maximumOccurrences: 1, forms: new[] { "--response:body", })); + .WithOption(new CommandOptionSpecification(ResponseBodyFileOption, requiresValue: true, maximumOccurrences: 1, forms: new[] { "--response:body", })) + .WithOption(new CommandOptionSpecification(NoFormattingOption, maximumOccurrences: 1, forms: new[] { "--no-formatting", "-F" })) + .WithOption(new CommandOptionSpecification(NoStreamingOption, maximumOccurrences: 1, forms: new[] { "--no-streaming", "-S" })); if (RequiresBody) { @@ -70,9 +74,9 @@ namespace Microsoft.HttpRepl.Commands protected override async Task ExecuteAsync(IShellState shellState, HttpState programState, DefaultCommandInput commandInput, ICoreParseResult parseResult, CancellationToken cancellationToken) { - if (programState.BaseAddress == null) + if (programState.BaseAddress == null && (commandInput.Arguments.Count == 0 || !Uri.TryCreate(commandInput.Arguments[0].Text, UriKind.Absolute, out Uri _))) { - shellState.ConsoleManager.Error.WriteLine("'set base {url}' must be called before issuing requests".Bold().Red()); + shellState.ConsoleManager.Error.WriteLine("'set base {url}' must be called before issuing requests to a relative path".Bold().Red()); return; } @@ -203,10 +207,10 @@ namespace Microsoft.HttpRepl.Commands string bodyTarget = commandInput.Options[ResponseBodyFileOption].FirstOrDefault()?.Text ?? commandInput.Options[ResponseFileOption].FirstOrDefault()?.Text; HttpResponseMessage response = await programState.Client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); - await HandleResponseAsync(programState, shellState.ConsoleManager, response, programState.EchoRequest, headersTarget, bodyTarget, cancellationToken).ConfigureAwait(false); + await HandleResponseAsync(programState, commandInput, shellState.ConsoleManager, response, programState.EchoRequest, headersTarget, bodyTarget, cancellationToken).ConfigureAwait(false); } - private static async Task HandleResponseAsync(HttpState programState, IConsoleManager consoleManager, HttpResponseMessage response, bool echoRequest, string headersTargetFile, string bodyTargetFile, CancellationToken cancellationToken) + private static async Task HandleResponseAsync(HttpState programState, DefaultCommandInput commandInput, IConsoleManager consoleManager, HttpResponseMessage response, bool echoRequest, string headersTargetFile, string bodyTargetFile, CancellationToken cancellationToken) { RequestConfig requestConfig = new RequestConfig(programState); ResponseConfig responseConfig = new ResponseConfig(programState); @@ -244,7 +248,7 @@ namespace Microsoft.HttpRepl.Commands { using (StreamWriter writer = new StreamWriter(new MemoryStream())) { - await FormatBodyAsync(programState, consoleManager, response.RequestMessage.Content, writer, cancellationToken).ConfigureAwait(false); + await FormatBodyAsync(commandInput, programState, consoleManager, response.RequestMessage.Content, writer, cancellationToken).ConfigureAwait(false); } } @@ -311,7 +315,7 @@ namespace Microsoft.HttpRepl.Commands if (response.Content != null) { - await FormatBodyAsync(programState, consoleManager, response.Content, bodyFileWriter, cancellationToken).ConfigureAwait(false); + await FormatBodyAsync(commandInput, programState, consoleManager, response.Content, bodyFileWriter, cancellationToken).ConfigureAwait(false); } bodyFileWriter.Flush(); @@ -321,7 +325,7 @@ namespace Microsoft.HttpRepl.Commands consoleManager.WriteLine(); } - private static async Task FormatBodyAsync(HttpState programState, IConsoleManager consoleManager, HttpContent content, StreamWriter bodyFileWriter, CancellationToken cancellationToken) + private static async Task FormatBodyAsync(DefaultCommandInput commandInput, HttpState programState, IConsoleManager consoleManager, HttpContent content, StreamWriter bodyFileWriter, CancellationToken cancellationToken) { string contentType = null; if (content.Headers.TryGetValues("Content-Type", out IEnumerable contentTypeValues)) @@ -331,33 +335,36 @@ namespace Microsoft.HttpRepl.Commands contentType = contentType?.ToUpperInvariant() ?? "text/plain"; - if (contentType.EndsWith("/JSON", StringComparison.OrdinalIgnoreCase) - || contentType.EndsWith("-JSON", StringComparison.OrdinalIgnoreCase) - || contentType.EndsWith("+JSON", StringComparison.OrdinalIgnoreCase) - || contentType.EndsWith("/JAVASCRIPT", StringComparison.OrdinalIgnoreCase) - || contentType.EndsWith("-JAVASCRIPT", StringComparison.OrdinalIgnoreCase) - || contentType.EndsWith("+JAVASCRIPT", StringComparison.OrdinalIgnoreCase)) + if (commandInput.Options[NoFormattingOption].Count == 0) { - if (await FormatJsonAsync(programState, consoleManager, content, bodyFileWriter)) + if (contentType.EndsWith("/JSON", StringComparison.OrdinalIgnoreCase) + || contentType.EndsWith("-JSON", StringComparison.OrdinalIgnoreCase) + || contentType.EndsWith("+JSON", StringComparison.OrdinalIgnoreCase) + || contentType.EndsWith("/JAVASCRIPT", StringComparison.OrdinalIgnoreCase) + || contentType.EndsWith("-JAVASCRIPT", StringComparison.OrdinalIgnoreCase) + || contentType.EndsWith("+JAVASCRIPT", StringComparison.OrdinalIgnoreCase)) { - return; + if (await FormatJsonAsync(programState, consoleManager, content, bodyFileWriter)) + { + return; + } } - } - else if (contentType.EndsWith("/HTML", StringComparison.OrdinalIgnoreCase) - || contentType.EndsWith("-HTML", StringComparison.OrdinalIgnoreCase) - || contentType.EndsWith("+HTML", StringComparison.OrdinalIgnoreCase) - || contentType.EndsWith("/XML", StringComparison.OrdinalIgnoreCase) - || contentType.EndsWith("-XML", StringComparison.OrdinalIgnoreCase) - || contentType.EndsWith("+XML", StringComparison.OrdinalIgnoreCase)) - { - if (await FormatXmlAsync(consoleManager, content, bodyFileWriter)) + else if (contentType.EndsWith("/HTML", StringComparison.OrdinalIgnoreCase) + || contentType.EndsWith("-HTML", StringComparison.OrdinalIgnoreCase) + || contentType.EndsWith("+HTML", StringComparison.OrdinalIgnoreCase) + || contentType.EndsWith("/XML", StringComparison.OrdinalIgnoreCase) + || contentType.EndsWith("-XML", StringComparison.OrdinalIgnoreCase) + || contentType.EndsWith("+XML", StringComparison.OrdinalIgnoreCase)) { - return; + if (await FormatXmlAsync(consoleManager, content, bodyFileWriter)) + { + return; + } } } - //If we don't have content length, assume streaming - if (!content.Headers.TryGetValues("Content-Length", out IEnumerable _)) + //Unless the user has explicitly specified to not stream the response, if we don't have content length, assume streaming + if (commandInput.Options[NoStreamingOption].Count == 0 && !content.Headers.TryGetValues("Content-Length", out IEnumerable _)) { Memory buffer = new Memory(new char[2048]); Stream s = await content.ReadAsStreamAsync().ConfigureAwait(false); diff --git a/src/Microsoft.HttpRepl/Commands/HelpCommand.cs b/src/Microsoft.HttpRepl/Commands/HelpCommand.cs index f205fc1243..646dd4c544 100644 --- a/src/Microsoft.HttpRepl/Commands/HelpCommand.cs +++ b/src/Microsoft.HttpRepl/Commands/HelpCommand.cs @@ -30,15 +30,7 @@ namespace Microsoft.HttpRepl.Commands { if (parseResult.Sections.Count == 1) { - foreach (ICommand command in dispatcher.Commands) - { - string help = command.GetHelpSummary(shellState, programState); - - if (!string.IsNullOrEmpty(help)) - { - shellState.ConsoleManager.WriteLine(help); - } - } + CoreGetHelp(shellState, dispatcher, programState); } else { @@ -171,5 +163,18 @@ namespace Microsoft.HttpRepl.Commands return null; } + + public void CoreGetHelp(IShellState shellState, ICommandDispatcher dispatcher, HttpState programState) + { + foreach (ICommand command in dispatcher.Commands) + { + string help = command.GetHelpSummary(shellState, programState); + + if (!string.IsNullOrEmpty(help)) + { + shellState.ConsoleManager.WriteLine(help); + } + } + } } } diff --git a/src/Microsoft.HttpRepl/Microsoft.HttpRepl.csproj b/src/Microsoft.HttpRepl/Microsoft.HttpRepl.csproj index cf53e4b62e..2dce88c68b 100644 --- a/src/Microsoft.HttpRepl/Microsoft.HttpRepl.csproj +++ b/src/Microsoft.HttpRepl/Microsoft.HttpRepl.csproj @@ -23,8 +23,8 @@ - - + + diff --git a/src/Microsoft.HttpRepl/OpenApi/PointerUtil.cs b/src/Microsoft.HttpRepl/OpenApi/PointerUtil.cs index db36c082bf..c8566095b7 100644 --- a/src/Microsoft.HttpRepl/OpenApi/PointerUtil.cs +++ b/src/Microsoft.HttpRepl/OpenApi/PointerUtil.cs @@ -145,77 +145,5 @@ namespace Microsoft.HttpRepl.OpenApi return toResolve; } - - //public static async Task ResolvePointerAsync(this JToken root, HttpClient client, string pointer) - //{ - // if (!pointer.StartsWith("#/", StringComparison.Ordinal)) - // { - // HttpResponseMessage response = await client.GetAsync(pointer).ConfigureAwait(false); - - // if (!response.IsSuccessStatusCode) - // { - // //TODO: Failed to resolve pointer message - // return new JValue((object)null); - // } - - // string responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - - // try - // { - // root = JToken.Parse(responseString); - // int hashIndex = pointer.IndexOf("#"); - - // if (hashIndex < 0) - // { - // return root; - // } - - // pointer = pointer.Substring(hashIndex); - // } - // catch (Exception ex) - // { - // //TODO: Failed to deserialize pointer message - // return new JValue((object)null); - // } - // } - - // string[] pointerParts = pointer.Split('/'); - - // for (int i = 1; !(root is null) && i < pointerParts.Length; ++i) - // { - // if (root is JArray arr) - // { - // if (!int.TryParse(pointerParts[i], System.Globalization.NumberStyles.Integer, System.Globalization.CultureInfo.InvariantCulture, out int result) || result < 0 || result >= arr.Count) - // { - // //TODO: Failed to resolve pointer part message (non-integer index to array or index out of range) - // return null; - // } - - // root = arr[result]; - // } - // else if (root is JObject obj) - // { - // root = obj[pointerParts[i]]; - - // if (root is null) - // { - // //TODO: Failed to resolve pointer part message (no such path in object) - // } - - // JToken nestedRef = root["$ref"]; - // if (nestedRef is JValue value && value.Type == JTokenType.String) - // { - // root = await ResolvePointerAsync(root, ) - // } - // } - // else - // { - // //TODO: Failed to resolve pointer part message (pathing into literal) - // return null; - // } - // } - - // return root; - //} } } diff --git a/src/Microsoft.HttpRepl/Preferences/WellKnownPreference.cs b/src/Microsoft.HttpRepl/Preferences/WellKnownPreference.cs index 1cd17a04ce..424c0bb9a4 100644 --- a/src/Microsoft.HttpRepl/Preferences/WellKnownPreference.cs +++ b/src/Microsoft.HttpRepl/Preferences/WellKnownPreference.cs @@ -38,7 +38,6 @@ namespace Microsoft.HttpRepl.Preferences } } - #region JSON public static string JsonArrayBraceColor { get; } = "colors.json.arrayBrace"; public static string JsonObjectBraceColor { get; } = "colors.json.objectBrace"; @@ -66,7 +65,6 @@ namespace Microsoft.HttpRepl.Preferences public static string JsonSyntaxColor { get; } = "colors.json.syntax"; public static string JsonBraceColor { get; } = "colors.json.brace"; - #endregion JSON public static string RequestColor { get; } = "colors.request"; diff --git a/src/Microsoft.HttpRepl/Program.cs b/src/Microsoft.HttpRepl/Program.cs index a0e1c9addf..6509e7a38c 100644 --- a/src/Microsoft.HttpRepl/Program.cs +++ b/src/Microsoft.HttpRepl/Program.cs @@ -1,10 +1,12 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +using System; using System.Threading; using System.Threading.Tasks; using Microsoft.Repl; using Microsoft.Repl.Commanding; +using Microsoft.Repl.ConsoleHandling; using Microsoft.Repl.Parsing; using Microsoft.HttpRepl.Commands; @@ -14,6 +16,12 @@ namespace Microsoft.HttpRepl { static async Task Main(string[] args) { + if(Console.IsOutputRedirected) + { + Reporter.Error.WriteLine("Cannot start the REPL when output is being redirected".Bold().Red()); + return; + } + var state = new HttpState(); var dispatcher = DefaultCommandDispatcher.Create(state.GetPrompt, state); @@ -44,6 +52,22 @@ namespace Microsoft.HttpRepl shell.ShellState.ConsoleManager.AddBreakHandler(() => source.Cancel()); if (args.Length > 0) { + if (string.Equals(args[0], "--help", StringComparison.OrdinalIgnoreCase)) + { + shell.ShellState.ConsoleManager.WriteLine("Usage: dotnet httprepl [] [options]"); + shell.ShellState.ConsoleManager.WriteLine(); + shell.ShellState.ConsoleManager.WriteLine("Arguments:"); + shell.ShellState.ConsoleManager.WriteLine(" - The initial base address for the REPL."); + shell.ShellState.ConsoleManager.WriteLine(); + shell.ShellState.ConsoleManager.WriteLine("Options:"); + shell.ShellState.ConsoleManager.WriteLine(" --help - Show help information."); + + shell.ShellState.ConsoleManager.WriteLine(); + shell.ShellState.ConsoleManager.WriteLine("REPL Commands:"); + new HelpCommand().CoreGetHelp(shell.ShellState, (ICommandDispatcher)shell.ShellState.CommandDispatcher, state); + return; + } + shell.ShellState.CommandDispatcher.OnReady(shell.ShellState); shell.ShellState.InputManager.SetInput(shell.ShellState, $"set base \"{args[0]}\""); await shell.ShellState.CommandDispatcher.ExecuteCommandAsync(shell.ShellState, CancellationToken.None).ConfigureAwait(false); diff --git a/src/Microsoft.Repl/Commanding/DefaultCommandDispatcher.cs b/src/Microsoft.Repl/Commanding/DefaultCommandDispatcher.cs index 2b85c2bc57..22be314827 100644 --- a/src/Microsoft.Repl/Commanding/DefaultCommandDispatcher.cs +++ b/src/Microsoft.Repl/Commanding/DefaultCommandDispatcher.cs @@ -156,6 +156,7 @@ namespace Microsoft.Repl.Commanding } shellState.ConsoleManager.Error.WriteLine("No matching command found".Red().Bold()); + shellState.ConsoleManager.Error.WriteLine("Execute 'help' to se available commands".Red().Bold()); } } diff --git a/src/Microsoft.Repl/Input/IInputManager.cs b/src/Microsoft.Repl/Input/IInputManager.cs index b67f3e3936..e8e98b2d54 100644 --- a/src/Microsoft.Repl/Input/IInputManager.cs +++ b/src/Microsoft.Repl/Input/IInputManager.cs @@ -13,6 +13,8 @@ namespace Microsoft.Repl.Input IInputManager RegisterKeyHandler(ConsoleKey key, AsyncKeyPressHandler handler); + IInputManager RegisterKeyHandler(ConsoleKey key, ConsoleModifiers modifiers, AsyncKeyPressHandler handler); + void ResetInput(); Task StartAsync(IShellState state, CancellationToken cancellationToken); diff --git a/src/Microsoft.Repl/Input/InputManager.cs b/src/Microsoft.Repl/Input/InputManager.cs index e1d6055683..cd1157c6ff 100644 --- a/src/Microsoft.Repl/Input/InputManager.cs +++ b/src/Microsoft.Repl/Input/InputManager.cs @@ -12,7 +12,7 @@ namespace Microsoft.Repl.Input { public class InputManager : IInputManager { - private readonly Dictionary _handlers = new Dictionary(); + private readonly Dictionary> _handlers = new Dictionary>(); private readonly List _inputBuffer = new List(); public bool IsOverwriteMode { get; set; } @@ -29,13 +29,37 @@ namespace Microsoft.Repl.Input public IInputManager RegisterKeyHandler(ConsoleKey key, AsyncKeyPressHandler handler) { + if (!_handlers.TryGetValue(key, out Dictionary handlers)) + { + _handlers[key] = handlers = new Dictionary(); + } + if (handler == null) { - _handlers.Remove(key); + handlers.Remove(default(ConsoleModifiers)); } else { - _handlers[key] = handler; + handlers[default(ConsoleModifiers)] = handler; + } + + return this; + } + + public IInputManager RegisterKeyHandler(ConsoleKey key, ConsoleModifiers modifiers, AsyncKeyPressHandler handler) + { + if (!_handlers.TryGetValue(key, out Dictionary handlers)) + { + _handlers[key] = handlers = new Dictionary(); + } + + if (handler == null) + { + handlers.Remove(modifiers); + } + else + { + handlers[modifiers] = handler; } return this; @@ -169,7 +193,7 @@ namespace Microsoft.Repl.Input { ConsoleKeyInfo keyPress = state.ConsoleManager.ReadKey(cancellationToken); - if (_handlers.TryGetValue(keyPress.Key, out AsyncKeyPressHandler handler)) + if (_handlers.TryGetValue(keyPress.Key, out Dictionary handlerLookup) && handlerLookup.TryGetValue(keyPress.Modifiers, out AsyncKeyPressHandler handler)) { using (CancellationTokenSource source = new CancellationTokenSource()) using (state.ConsoleManager.AddBreakHandler(() => source.Cancel())) @@ -189,6 +213,7 @@ namespace Microsoft.Repl.Input FlushInput(state, ref presses); } + //TODO: Verify on a mac whether these are still needed if (keyPress.Key == ConsoleKey.A) { state.ConsoleManager.MoveCaret(-state.ConsoleManager.CaretPosition); @@ -198,6 +223,7 @@ namespace Microsoft.Repl.Input state.ConsoleManager.MoveCaret(_inputBuffer.Count - state.ConsoleManager.CaretPosition); } } + //TODO: Register these like regular commands else if (!string.IsNullOrEmpty(_ttyState) && keyPress.Modifiers == ConsoleModifiers.Alt) { if (presses != null) diff --git a/src/Microsoft.Repl/Input/KeyHandlers.cs b/src/Microsoft.Repl/Input/KeyHandlers.cs index 192c55319c..750db8d2f6 100644 --- a/src/Microsoft.Repl/Input/KeyHandlers.cs +++ b/src/Microsoft.Repl/Input/KeyHandlers.cs @@ -14,9 +14,13 @@ namespace Microsoft.Repl.Input { //Navigation in line inputManager.RegisterKeyHandler(ConsoleKey.LeftArrow, LeftArrow); + inputManager.RegisterKeyHandler(ConsoleKey.LeftArrow, ConsoleModifiers.Control, LeftArrow); inputManager.RegisterKeyHandler(ConsoleKey.RightArrow, RightArrow); + inputManager.RegisterKeyHandler(ConsoleKey.RightArrow, ConsoleModifiers.Control, RightArrow); inputManager.RegisterKeyHandler(ConsoleKey.Home, Home); + inputManager.RegisterKeyHandler(ConsoleKey.A, ConsoleModifiers.Control, Home); inputManager.RegisterKeyHandler(ConsoleKey.End, End); + inputManager.RegisterKeyHandler(ConsoleKey.E, ConsoleModifiers.Control, End); //Command history inputManager.RegisterKeyHandler(ConsoleKey.UpArrow, UpArrow); @@ -24,9 +28,11 @@ namespace Microsoft.Repl.Input //Completion inputManager.RegisterKeyHandler(ConsoleKey.Tab, Tab); + inputManager.RegisterKeyHandler(ConsoleKey.Tab, ConsoleModifiers.Shift, Tab); //Input manipulation inputManager.RegisterKeyHandler(ConsoleKey.Escape, Escape); + inputManager.RegisterKeyHandler(ConsoleKey.U, ConsoleModifiers.Control, Escape); inputManager.RegisterKeyHandler(ConsoleKey.Delete, Delete); inputManager.RegisterKeyHandler(ConsoleKey.Backspace, Backspace); diff --git a/src/Microsoft.Repl/Microsoft.Repl.csproj b/src/Microsoft.Repl/Microsoft.Repl.csproj index 63af949be4..cb7311eeeb 100644 --- a/src/Microsoft.Repl/Microsoft.Repl.csproj +++ b/src/Microsoft.Repl/Microsoft.Repl.csproj @@ -4,6 +4,7 @@ netcoreapp2.2 A framework for creating REPLs in .NET Core. dotnet;repl + false diff --git a/test/Microsoft.HttpRepl.Tests/Microsoft.HttpRepl.Tests.csproj b/test/Microsoft.HttpRepl.Tests/Microsoft.HttpRepl.Tests.csproj index 7a79d0121e..a50dd1df30 100644 --- a/test/Microsoft.HttpRepl.Tests/Microsoft.HttpRepl.Tests.csproj +++ b/test/Microsoft.HttpRepl.Tests/Microsoft.HttpRepl.Tests.csproj @@ -10,7 +10,7 @@ - +