Initial commit of the HTTP REPL
This commit is contained in:
parent
4baed363e5
commit
89ab0cfde8
|
|
@ -56,6 +56,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.VisualStudio.Secr
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.DeveloperCertificates.XPlat", "src\Microsoft.AspNetCore.DeveloperCertificates.XPlat\Microsoft.AspNetCore.DeveloperCertificates.XPlat.csproj", "{96E71881-1465-44F5-B4B7-DF9B370FFD02}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.HttpRepl", "src\Microsoft.HttpRepl\Microsoft.HttpRepl.csproj", "{4725BEAD-34F0-43C1-BF46-7AB16B4DE81D}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Repl", "src\Microsoft.Repl\Microsoft.Repl.csproj", "{EE9A6128-3DE2-4206-A5A4-3ED935084590}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Repl.Tests", "test\Microsoft.Repl.Tests\Microsoft.Repl.Tests.csproj", "{59C2B354-3B5E-40EB-A7BC-74583A5707CA}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.HttpRepl.Tests", "test\Microsoft.HttpRepl.Tests\Microsoft.HttpRepl.Tests.csproj", "{BE7CC4CD-CD76-4211-B593-CAC84407162A}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
|
@ -140,6 +148,38 @@ Global
|
|||
{96E71881-1465-44F5-B4B7-DF9B370FFD02}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{96E71881-1465-44F5-B4B7-DF9B370FFD02}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{96E71881-1465-44F5-B4B7-DF9B370FFD02}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
|
||||
{4725BEAD-34F0-43C1-BF46-7AB16B4DE81D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4725BEAD-34F0-43C1-BF46-7AB16B4DE81D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4725BEAD-34F0-43C1-BF46-7AB16B4DE81D}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4725BEAD-34F0-43C1-BF46-7AB16B4DE81D}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4725BEAD-34F0-43C1-BF46-7AB16B4DE81D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4725BEAD-34F0-43C1-BF46-7AB16B4DE81D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4725BEAD-34F0-43C1-BF46-7AB16B4DE81D}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4725BEAD-34F0-43C1-BF46-7AB16B4DE81D}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
|
||||
{EE9A6128-3DE2-4206-A5A4-3ED935084590}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EE9A6128-3DE2-4206-A5A4-3ED935084590}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EE9A6128-3DE2-4206-A5A4-3ED935084590}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{EE9A6128-3DE2-4206-A5A4-3ED935084590}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
|
||||
{EE9A6128-3DE2-4206-A5A4-3ED935084590}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EE9A6128-3DE2-4206-A5A4-3ED935084590}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{EE9A6128-3DE2-4206-A5A4-3ED935084590}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{EE9A6128-3DE2-4206-A5A4-3ED935084590}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
|
||||
{59C2B354-3B5E-40EB-A7BC-74583A5707CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{59C2B354-3B5E-40EB-A7BC-74583A5707CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{59C2B354-3B5E-40EB-A7BC-74583A5707CA}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{59C2B354-3B5E-40EB-A7BC-74583A5707CA}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
|
||||
{59C2B354-3B5E-40EB-A7BC-74583A5707CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{59C2B354-3B5E-40EB-A7BC-74583A5707CA}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{59C2B354-3B5E-40EB-A7BC-74583A5707CA}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{59C2B354-3B5E-40EB-A7BC-74583A5707CA}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
|
||||
{BE7CC4CD-CD76-4211-B593-CAC84407162A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BE7CC4CD-CD76-4211-B593-CAC84407162A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BE7CC4CD-CD76-4211-B593-CAC84407162A}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BE7CC4CD-CD76-4211-B593-CAC84407162A}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BE7CC4CD-CD76-4211-B593-CAC84407162A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BE7CC4CD-CD76-4211-B593-CAC84407162A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{BE7CC4CD-CD76-4211-B593-CAC84407162A}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BE7CC4CD-CD76-4211-B593-CAC84407162A}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
|
@ -155,6 +195,10 @@ Global
|
|||
{5E117F2E-7152-447F-BF47-59F759EEF3A7} = {62826851-7D74-4F1E-B7D1-12553B789CD8}
|
||||
{965F8820-F809-4081-9090-1AEC903F291B} = {62826851-7D74-4F1E-B7D1-12553B789CD8}
|
||||
{96E71881-1465-44F5-B4B7-DF9B370FFD02} = {66517987-2A5A-4330-B130-207039378FD4}
|
||||
{4725BEAD-34F0-43C1-BF46-7AB16B4DE81D} = {66517987-2A5A-4330-B130-207039378FD4}
|
||||
{EE9A6128-3DE2-4206-A5A4-3ED935084590} = {66517987-2A5A-4330-B130-207039378FD4}
|
||||
{59C2B354-3B5E-40EB-A7BC-74583A5707CA} = {F5B382BC-258F-46E1-AC3D-10E5CCD55134}
|
||||
{BE7CC4CD-CD76-4211-B593-CAC84407162A} = {F5B382BC-258F-46E1-AC3D-10E5CCD55134}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {57C07F14-2EAC-44FF-A277-B9221B4B2BF7}
|
||||
|
|
|
|||
|
|
@ -24,12 +24,33 @@
|
|||
"DotnetTool"
|
||||
]
|
||||
},
|
||||
"dotnet-httprepl": {
|
||||
"packageTypes": [
|
||||
"DotnetTool"
|
||||
],
|
||||
"Exclusions": {
|
||||
"ASSEMBLY_DESCRIPTION": {
|
||||
"tools/netcoreapp2.2/any/System.Net.Http.Formatting.dll": "Referenced assembly, not built as part of this process"
|
||||
},
|
||||
"VERSION_INFORMATIONALVERSION": {
|
||||
"tools/netcoreapp2.2/any/Newtonsoft.Json.dll": "Referenced assembly, not built as part of this process",
|
||||
"tools/netcoreapp2.2/any/Newtonsoft.Json.Bson.dll": "Referenced assembly, not built as part of this process"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.AspNetCore.DeveloperCertificates.XPlat": {
|
||||
"Exclusions": {
|
||||
"DOC_MISSING": {
|
||||
"lib/netcoreapp2.2/Microsoft.AspNetCore.DeveloperCertificates.XPlat.dll": "Docs not required to shipoob package"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Microsoft.Repl": {
|
||||
"Exclusions": {
|
||||
"DOC_MISSING": {
|
||||
"lib/netcoreapp2.2/Microsoft.Repl.dll": "Docs not required to shipoob package"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -13,9 +13,11 @@
|
|||
<MicrosoftNETCoreApp21PackageVersion>2.1.2</MicrosoftNETCoreApp21PackageVersion>
|
||||
<MicrosoftNETCoreApp22PackageVersion>2.2.0-preview1-26618-02</MicrosoftNETCoreApp22PackageVersion>
|
||||
<MicrosoftNETTestSdkPackageVersion>15.6.1</MicrosoftNETTestSdkPackageVersion>
|
||||
<MicrosoftWebApiClientPackageVersion>5.2.6</MicrosoftWebApiClientPackageVersion>
|
||||
<NETStandardLibrary20PackageVersion>2.0.3</NETStandardLibrary20PackageVersion>
|
||||
<SystemDataSqlClientPackageVersion>4.5.1</SystemDataSqlClientPackageVersion>
|
||||
<SystemSecurityCryptographyCngPackageVersion>4.5.0</SystemSecurityCryptographyCngPackageVersion>
|
||||
<CommandLine_NewtonsoftJsonPackageVersion>10.0.1</CommandLine_NewtonsoftJsonPackageVersion>
|
||||
<VisualStudio_NewtonsoftJsonPackageVersion>9.0.1</VisualStudio_NewtonsoftJsonPackageVersion>
|
||||
<XunitPackageVersion>2.3.1</XunitPackageVersion>
|
||||
<XunitRunnerVisualStudioPackageVersion>2.4.0</XunitRunnerVisualStudioPackageVersion>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.HttpRepl
|
||||
{
|
||||
public class AggregateDirectoryStructure : IDirectoryStructure
|
||||
{
|
||||
private readonly IDirectoryStructure _first;
|
||||
private readonly IDirectoryStructure _second;
|
||||
|
||||
public AggregateDirectoryStructure(IDirectoryStructure first, IDirectoryStructure second)
|
||||
{
|
||||
_first = first;
|
||||
_second = second;
|
||||
}
|
||||
|
||||
public IEnumerable<string> DirectoryNames
|
||||
{
|
||||
get
|
||||
{
|
||||
HashSet<string> values = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
values.UnionWith(_first.DirectoryNames);
|
||||
values.UnionWith(_second.DirectoryNames);
|
||||
return values.OrderBy(x => x, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
public IDirectoryStructure Parent => _first.Parent ?? _second.Parent;
|
||||
|
||||
public IDirectoryStructure GetChildDirectory(string name)
|
||||
{
|
||||
return new AggregateDirectoryStructure(_first.GetChildDirectory(name), _second.GetChildDirectory(name));
|
||||
}
|
||||
|
||||
public IRequestInfo RequestInfo => _first.RequestInfo ?? _second.RequestInfo;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,574 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.HttpRepl.Formatting;
|
||||
using Microsoft.HttpRepl.Preferences;
|
||||
using Microsoft.HttpRepl.Suggestions;
|
||||
using Microsoft.Repl;
|
||||
using Microsoft.Repl.Commanding;
|
||||
using Microsoft.Repl.ConsoleHandling;
|
||||
using Microsoft.Repl.Parsing;
|
||||
using Microsoft.Repl.Suggestions;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Microsoft.HttpRepl.Commands
|
||||
{
|
||||
public abstract class BaseHttpCommand : CommandWithStructuredInputBase<HttpState, ICoreParseResult>
|
||||
{
|
||||
private const string HeaderOption = nameof(HeaderOption);
|
||||
private const string ResponseHeadersFileOption = nameof(ResponseHeadersFileOption);
|
||||
private const string ResponseBodyFileOption = nameof(ResponseBodyFileOption);
|
||||
private const string ResponseFileOption = nameof(ResponseFileOption);
|
||||
private const string BodyFileOption = nameof(BodyFileOption);
|
||||
private const string NoBodyOption = nameof(NoBodyOption);
|
||||
private const string BodyContentOption = nameof(BodyContentOption);
|
||||
private static readonly char[] HeaderSeparatorChars = new[] { '=', ':' };
|
||||
|
||||
private CommandInputSpecification _inputSpec;
|
||||
|
||||
protected abstract string Verb { get; }
|
||||
|
||||
protected abstract bool RequiresBody { get; }
|
||||
|
||||
protected override CommandInputSpecification InputSpec
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_inputSpec != null)
|
||||
{
|
||||
return _inputSpec;
|
||||
}
|
||||
|
||||
CommandInputSpecificationBuilder builder = CommandInputSpecification.Create(Verb)
|
||||
.MaximumArgCount(1)
|
||||
.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", }));
|
||||
|
||||
if (RequiresBody)
|
||||
{
|
||||
builder = builder.WithOption(new CommandOptionSpecification(NoBodyOption, maximumOccurrences: 1, forms: "--no-body"))
|
||||
.WithOption(new CommandOptionSpecification(BodyFileOption, requiresValue: true, maximumOccurrences: 1, forms: new[] {"--file", "-f"}))
|
||||
.WithOption(new CommandOptionSpecification(BodyContentOption, requiresValue: true, maximumOccurrences: 1, forms: new[] {"--content", "-c"}));
|
||||
}
|
||||
|
||||
_inputSpec = builder.Finish();
|
||||
return _inputSpec;
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(IShellState shellState, HttpState programState, DefaultCommandInput<ICoreParseResult> commandInput, ICoreParseResult parseResult, CancellationToken cancellationToken)
|
||||
{
|
||||
if (programState.BaseAddress == null)
|
||||
{
|
||||
shellState.ConsoleManager.Error.WriteLine("'set base {url}' must be called before issuing requests".Bold().Red());
|
||||
return;
|
||||
}
|
||||
|
||||
Dictionary<string, string> thisRequestHeaders = new Dictionary<string, string>();
|
||||
|
||||
foreach (InputElement header in commandInput.Options[HeaderOption])
|
||||
{
|
||||
int equalsIndex = header.Text.IndexOfAny(HeaderSeparatorChars);
|
||||
|
||||
if (equalsIndex < 0)
|
||||
{
|
||||
shellState.ConsoleManager.Error.WriteLine("Headers must be formatted as {header}={value} or {header}:{value}".Bold().Red());
|
||||
return;
|
||||
}
|
||||
|
||||
thisRequestHeaders[header.Text.Substring(0, equalsIndex)] = header.Text.Substring(equalsIndex + 1);
|
||||
}
|
||||
|
||||
Uri effectivePath = programState.GetEffectivePath(commandInput.Arguments.Count > 0 ? commandInput.Arguments[0].Text : string.Empty);
|
||||
HttpRequestMessage request = new HttpRequestMessage(new HttpMethod(Verb.ToUpperInvariant()), effectivePath);
|
||||
bool noBody = false;
|
||||
|
||||
if (RequiresBody)
|
||||
{
|
||||
string filePath = null;
|
||||
string bodyContent = null;
|
||||
bool deleteFile = false;
|
||||
noBody = commandInput.Options[NoBodyOption].Count > 0;
|
||||
|
||||
if (!noBody)
|
||||
{
|
||||
if (commandInput.Options[BodyFileOption].Count > 0)
|
||||
{
|
||||
filePath = commandInput.Options[BodyFileOption][0].Text;
|
||||
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
shellState.ConsoleManager.Error.WriteLine($"Content file {filePath} does not exist".Bold().Red());
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (commandInput.Options[BodyContentOption].Count > 0)
|
||||
{
|
||||
bodyContent = commandInput.Options[BodyContentOption][0].Text;
|
||||
}
|
||||
else
|
||||
{
|
||||
string defaultEditorCommand = programState.GetStringPreference(WellKnownPreference.DefaultEditorCommand);
|
||||
if (defaultEditorCommand == null)
|
||||
{
|
||||
shellState.ConsoleManager.Error.WriteLine($"The default editor must be configured using the command `pref set {WellKnownPreference.DefaultEditorCommand} \"{{commandline}}\"`".Bold().Red());
|
||||
return;
|
||||
}
|
||||
|
||||
deleteFile = true;
|
||||
filePath = Path.GetTempFileName();
|
||||
|
||||
if (!thisRequestHeaders.TryGetValue("content-type", out string contentType) && programState.Headers.TryGetValue("content-type", out IEnumerable<string> contentTypes))
|
||||
{
|
||||
contentType = contentTypes.FirstOrDefault();
|
||||
}
|
||||
|
||||
if (contentType == null)
|
||||
{
|
||||
contentType = "application/json";
|
||||
}
|
||||
|
||||
string exampleBody = programState.GetExampleBody(commandInput.Arguments.Count > 0 ? commandInput.Arguments[0].Text : string.Empty, contentType, Verb);
|
||||
|
||||
if (!string.IsNullOrEmpty(exampleBody))
|
||||
{
|
||||
File.WriteAllText(filePath, exampleBody);
|
||||
}
|
||||
|
||||
string defaultEditorArguments = programState.GetStringPreference(WellKnownPreference.DefaultEditorArguments) ?? "";
|
||||
string original = defaultEditorArguments;
|
||||
string pathString = $"\"{filePath}\"";
|
||||
|
||||
defaultEditorArguments = defaultEditorArguments.Replace("{filename}", pathString);
|
||||
|
||||
if (string.Equals(defaultEditorArguments, original, StringComparison.Ordinal))
|
||||
{
|
||||
defaultEditorArguments = (defaultEditorArguments + " " + pathString).Trim();
|
||||
}
|
||||
|
||||
ProcessStartInfo info = new ProcessStartInfo(defaultEditorCommand, defaultEditorArguments);
|
||||
|
||||
Process.Start(info)?.WaitForExit();
|
||||
}
|
||||
}
|
||||
|
||||
byte[] data = noBody
|
||||
? new byte[0]
|
||||
: string.IsNullOrEmpty(bodyContent)
|
||||
? File.ReadAllBytes(filePath)
|
||||
: Encoding.UTF8.GetBytes(bodyContent);
|
||||
|
||||
HttpContent content = new ByteArrayContent(data);
|
||||
request.Content = content;
|
||||
|
||||
if (deleteFile)
|
||||
{
|
||||
File.Delete(filePath);
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<string, IEnumerable<string>> header in programState.Headers)
|
||||
{
|
||||
content.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<string, string> header in thisRequestHeaders)
|
||||
{
|
||||
content.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<string, IEnumerable<string>> header in programState.Headers)
|
||||
{
|
||||
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<string, string> header in thisRequestHeaders)
|
||||
{
|
||||
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
|
||||
}
|
||||
|
||||
string headersTarget = commandInput.Options[ResponseHeadersFileOption].FirstOrDefault()?.Text ?? commandInput.Options[ResponseFileOption].FirstOrDefault()?.Text;
|
||||
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);
|
||||
}
|
||||
|
||||
private static async Task HandleResponseAsync(HttpState programState, IConsoleManager consoleManager, HttpResponseMessage response, bool echoRequest, string headersTargetFile, string bodyTargetFile, CancellationToken cancellationToken)
|
||||
{
|
||||
RequestConfig requestConfig = new RequestConfig(programState);
|
||||
ResponseConfig responseConfig = new ResponseConfig(programState);
|
||||
string protocolInfo;
|
||||
|
||||
if (echoRequest)
|
||||
{
|
||||
string hostString = response.RequestMessage.RequestUri.Scheme + "://" + response.RequestMessage.RequestUri.Host + (!response.RequestMessage.RequestUri.IsDefaultPort ? ":" + response.RequestMessage.RequestUri.Port : "");
|
||||
consoleManager.WriteLine($"Request to {hostString}...".SetColor(requestConfig.AddressColor));
|
||||
consoleManager.WriteLine();
|
||||
|
||||
string method = response.RequestMessage.Method.ToString().ToUpperInvariant().SetColor(requestConfig.MethodColor);
|
||||
string pathAndQuery = response.RequestMessage.RequestUri.PathAndQuery.SetColor(requestConfig.AddressColor);
|
||||
protocolInfo = $"{"HTTP".SetColor(requestConfig.ProtocolNameColor)}{"/".SetColor(requestConfig.ProtocolSeparatorColor)}{response.RequestMessage.Version.ToString().SetColor(requestConfig.ProtocolVersionColor)}";
|
||||
|
||||
consoleManager.WriteLine($"{method} {pathAndQuery} {protocolInfo}");
|
||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>> requestHeaders = response.RequestMessage.Headers;
|
||||
|
||||
if (response.RequestMessage.Content != null)
|
||||
{
|
||||
requestHeaders = requestHeaders.Union(response.RequestMessage.Content.Headers);
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<string, IEnumerable<string>> header in requestHeaders.OrderBy(x => x.Key))
|
||||
{
|
||||
string headerKey = header.Key.SetColor(requestConfig.HeaderKeyColor);
|
||||
string headerSep = ":".SetColor(requestConfig.HeaderSeparatorColor);
|
||||
string headerValue = string.Join(";".SetColor(requestConfig.HeaderValueSeparatorColor), header.Value.Select(x => x.Trim().SetColor(requestConfig.HeaderValueColor)));
|
||||
consoleManager.WriteLine($"{headerKey}{headerSep} {headerValue}");
|
||||
}
|
||||
|
||||
consoleManager.WriteLine();
|
||||
|
||||
if (response.RequestMessage.Content != null)
|
||||
{
|
||||
using (StreamWriter writer = new StreamWriter(new MemoryStream()))
|
||||
{
|
||||
await FormatBodyAsync(programState, consoleManager, response.RequestMessage.Content, writer, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
consoleManager.WriteLine();
|
||||
consoleManager.WriteLine($"Response from {hostString}...".SetColor(requestConfig.AddressColor));
|
||||
consoleManager.WriteLine();
|
||||
}
|
||||
|
||||
protocolInfo = $"{"HTTP".SetColor(responseConfig.ProtocolNameColor)}{"/".SetColor(responseConfig.ProtocolSeparatorColor)}{response.Version.ToString().SetColor(responseConfig.ProtocolVersionColor)}";
|
||||
string status = ((int)response.StatusCode).ToString().SetColor(responseConfig.StatusCodeColor) + " " + response.ReasonPhrase.SetColor(responseConfig.StatusReasonPhraseColor);
|
||||
|
||||
consoleManager.WriteLine($"{protocolInfo} {status}");
|
||||
|
||||
IEnumerable<KeyValuePair<string, IEnumerable<string>>> responseHeaders = response.Headers;
|
||||
|
||||
if (response.Content != null)
|
||||
{
|
||||
responseHeaders = responseHeaders.Union(response.Content.Headers);
|
||||
}
|
||||
|
||||
StreamWriter headerFileWriter;
|
||||
|
||||
if (headersTargetFile != null)
|
||||
{
|
||||
headerFileWriter = new StreamWriter(File.Create(headersTargetFile));
|
||||
}
|
||||
else
|
||||
{
|
||||
headerFileWriter = new StreamWriter(new MemoryStream());
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<string, IEnumerable<string>> header in responseHeaders.OrderBy(x => x.Key))
|
||||
{
|
||||
string headerKey = header.Key.SetColor(responseConfig.HeaderKeyColor);
|
||||
string headerSep = ":".SetColor(responseConfig.HeaderSeparatorColor);
|
||||
string headerValue = string.Join(";".SetColor(responseConfig.HeaderValueSeparatorColor), header.Value.Select(x => x.Trim().SetColor(responseConfig.HeaderValueColor)));
|
||||
consoleManager.WriteLine($"{headerKey}{headerSep} {headerValue}");
|
||||
headerFileWriter.WriteLine($"{header.Key}: {string.Join(";", header.Value.Select(x => x.Trim()))}");
|
||||
}
|
||||
|
||||
StreamWriter bodyFileWriter;
|
||||
if (!string.Equals(headersTargetFile, bodyTargetFile, StringComparison.Ordinal))
|
||||
{
|
||||
headerFileWriter.Flush();
|
||||
headerFileWriter.Close();
|
||||
headerFileWriter.Dispose();
|
||||
|
||||
if (bodyTargetFile != null)
|
||||
{
|
||||
bodyFileWriter = new StreamWriter(File.Create(bodyTargetFile));
|
||||
}
|
||||
else
|
||||
{
|
||||
bodyFileWriter = new StreamWriter(new MemoryStream());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
headerFileWriter.WriteLine();
|
||||
bodyFileWriter = headerFileWriter;
|
||||
}
|
||||
|
||||
consoleManager.WriteLine();
|
||||
|
||||
if (response.Content != null)
|
||||
{
|
||||
await FormatBodyAsync(programState, consoleManager, response.Content, bodyFileWriter, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
bodyFileWriter.Flush();
|
||||
bodyFileWriter.Close();
|
||||
bodyFileWriter.Dispose();
|
||||
|
||||
consoleManager.WriteLine();
|
||||
}
|
||||
|
||||
private static async Task FormatBodyAsync(HttpState programState, IConsoleManager consoleManager, HttpContent content, StreamWriter bodyFileWriter, CancellationToken cancellationToken)
|
||||
{
|
||||
string contentType = null;
|
||||
if (content.Headers.TryGetValues("Content-Type", out IEnumerable<string> contentTypeValues))
|
||||
{
|
||||
contentType = contentTypeValues.FirstOrDefault()?.Split(';').FirstOrDefault();
|
||||
}
|
||||
|
||||
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 (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))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//If we don't have content length, assume streaming
|
||||
if (!content.Headers.TryGetValues("Content-Length", out IEnumerable<string> _))
|
||||
{
|
||||
Memory<char> buffer = new Memory<char>(new char[2048]);
|
||||
Stream s = await content.ReadAsStreamAsync().ConfigureAwait(false);
|
||||
StreamReader reader = new StreamReader(s);
|
||||
consoleManager.WriteLine("Streaming the response, press any key to stop...".Bold().Yellow());
|
||||
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
ValueTask<int> readTask = reader.ReadAsync(buffer, cancellationToken);
|
||||
if (await WaitForCompletionAsync(readTask, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (readTask.Result == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
string str = new string(buffer.Span.Slice(0, readTask.Result));
|
||||
consoleManager.Write(str);
|
||||
bodyFileWriter.Write(str);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
string responseContent = await content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
bodyFileWriter.WriteLine(responseContent);
|
||||
consoleManager.WriteLine(responseContent);
|
||||
}
|
||||
|
||||
private static async Task<bool> WaitForCompletionAsync(ValueTask<int> readTask, CancellationToken cancellationToken)
|
||||
{
|
||||
while (!readTask.IsCompleted && !cancellationToken.IsCancellationRequested && !Console.KeyAvailable)
|
||||
{
|
||||
await Task.Delay(1, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (Console.KeyAvailable)
|
||||
{
|
||||
Console.ReadKey(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
return readTask.IsCompleted;
|
||||
}
|
||||
|
||||
private static async Task<bool> FormatXmlAsync(IWritable consoleManager, HttpContent content, StreamWriter bodyFileWriter)
|
||||
{
|
||||
string responseContent = await content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
XDocument body = XDocument.Parse(responseContent);
|
||||
consoleManager.WriteLine(body.ToString());
|
||||
bodyFileWriter.WriteLine(body.ToString());
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static async Task<bool> FormatJsonAsync(HttpState programState, IWritable outputSink, HttpContent content, StreamWriter bodyFileWriter)
|
||||
{
|
||||
string responseContent = await content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
JsonConfig config = new JsonConfig(programState);
|
||||
string formatted = JsonVisitor.FormatAndColorize(config, responseContent);
|
||||
outputSink.WriteLine(formatted);
|
||||
bodyFileWriter.WriteLine(JToken.Parse(responseContent).ToString());
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override string GetHelpDetails(IShellState shellState, HttpState programState, DefaultCommandInput<ICoreParseResult> commandInput, ICoreParseResult parseResult)
|
||||
{
|
||||
return $"Issues a {Verb.ToUpperInvariant()} request";
|
||||
}
|
||||
|
||||
public override string GetHelpSummary(IShellState shellState, HttpState programState)
|
||||
{
|
||||
return $"{Verb.ToLowerInvariant()} - Issues a {Verb.ToUpperInvariant()} request";
|
||||
}
|
||||
|
||||
protected override IEnumerable<string> GetArgumentSuggestionsForText(IShellState shellState, HttpState programState, ICoreParseResult parseResult, DefaultCommandInput<ICoreParseResult> commandInput, string normalCompletionString)
|
||||
{
|
||||
List<string> results = new List<string>();
|
||||
|
||||
if (programState.Structure != null && programState.BaseAddress != null)
|
||||
{
|
||||
//If it's an absolute URI, nothing to suggest
|
||||
if (Uri.TryCreate(parseResult.Sections[1], UriKind.Absolute, out Uri _))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string path = normalCompletionString.Replace('\\', '/');
|
||||
int searchFrom = normalCompletionString.Length - 1;
|
||||
int lastSlash = path.LastIndexOf('/', searchFrom);
|
||||
string prefix;
|
||||
|
||||
if (lastSlash < 0)
|
||||
{
|
||||
path = string.Empty;
|
||||
prefix = normalCompletionString;
|
||||
}
|
||||
else
|
||||
{
|
||||
path = path.Substring(0, lastSlash + 1);
|
||||
prefix = normalCompletionString.Substring(lastSlash + 1);
|
||||
}
|
||||
|
||||
IDirectoryStructure s = programState.Structure.TraverseTo(programState.PathSections.Reverse()).TraverseTo(path);
|
||||
|
||||
foreach (string child in s.DirectoryNames)
|
||||
{
|
||||
if (child.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
results.Add(path + child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
protected override IEnumerable<string> GetOptionValueCompletions(IShellState shellState, HttpState programState, string optionId, DefaultCommandInput<ICoreParseResult> commandInput, ICoreParseResult parseResult, string normalizedCompletionText)
|
||||
{
|
||||
if (string.Equals(optionId, BodyFileOption, StringComparison.Ordinal) || string.Equals(optionId, ResponseFileOption, StringComparison.OrdinalIgnoreCase) || string.Equals(optionId, ResponseBodyFileOption, StringComparison.OrdinalIgnoreCase) || string.Equals(optionId, ResponseHeadersFileOption, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return FileSystemCompletion.GetCompletions(normalizedCompletionText);
|
||||
}
|
||||
|
||||
if (string.Equals(optionId, HeaderOption, StringComparison.Ordinal))
|
||||
{
|
||||
HashSet<string> alreadySpecifiedHeaders = new HashSet<string>(StringComparer.Ordinal);
|
||||
IReadOnlyList<InputElement> options = commandInput.Options[HeaderOption];
|
||||
for (int i = 0; i < options.Count; ++i)
|
||||
{
|
||||
if (options[i] == commandInput.SelectedElement)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string elementText = options[i].Text;
|
||||
string existingHeaderName = elementText.Split(HeaderSeparatorChars)[0];
|
||||
alreadySpecifiedHeaders.Add(existingHeaderName);
|
||||
}
|
||||
|
||||
//Check to see if the selected element is in a header name or value
|
||||
int equalsIndex = normalizedCompletionText.IndexOfAny(HeaderSeparatorChars);
|
||||
string path = commandInput.Arguments.Count > 0 ? commandInput.Arguments[0].Text : string.Empty;
|
||||
|
||||
if (equalsIndex < 0)
|
||||
{
|
||||
IEnumerable<string> headerNameOptions = HeaderCompletion.GetCompletions(alreadySpecifiedHeaders, normalizedCompletionText);
|
||||
|
||||
if (headerNameOptions == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
List<string> allSuggestions = new List<string>();
|
||||
foreach (string suggestion in headerNameOptions.Select(x => x))
|
||||
{
|
||||
allSuggestions.Add(suggestion + ":");
|
||||
|
||||
IEnumerable<string> suggestions = HeaderCompletion.GetValueCompletions(Verb, path, suggestion, string.Empty, programState);
|
||||
|
||||
if (suggestions != null)
|
||||
{
|
||||
foreach (string valueSuggestion in suggestions)
|
||||
{
|
||||
allSuggestions.Add(suggestion + ":" + valueSuggestion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allSuggestions;
|
||||
}
|
||||
else
|
||||
{
|
||||
//Didn't exit from the header name check, so must be a value
|
||||
string headerName = normalizedCompletionText.Substring(0, equalsIndex);
|
||||
IEnumerable<string> suggestions = HeaderCompletion.GetValueCompletions(Verb, path, headerName, normalizedCompletionText.Substring(equalsIndex + 1), programState);
|
||||
|
||||
if (suggestions == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return suggestions.Select(x => normalizedCompletionText.Substring(0, equalsIndex + 1) + x);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.HttpRepl.Suggestions;
|
||||
using Microsoft.Repl;
|
||||
using Microsoft.Repl.Commanding;
|
||||
using Microsoft.Repl.Parsing;
|
||||
|
||||
namespace Microsoft.HttpRepl.Commands
|
||||
{
|
||||
public class ChangeDirectoryCommand : CommandWithStructuredInputBase<HttpState, ICoreParseResult>
|
||||
{
|
||||
protected override Task ExecuteAsync(IShellState shellState, HttpState programState, DefaultCommandInput<ICoreParseResult> commandInput, ICoreParseResult parseResult, CancellationToken cancellationToken)
|
||||
{
|
||||
if (commandInput.Arguments.Count == 0 || string.IsNullOrEmpty(commandInput.Arguments[0]?.Text))
|
||||
{
|
||||
shellState.ConsoleManager.WriteLine($"/{string.Join("/", programState.PathSections.Reverse())}");
|
||||
}
|
||||
else
|
||||
{
|
||||
string[] parts = commandInput.Arguments[0].Text.Replace('\\', '/').Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
if (commandInput.Arguments[0].Text.StartsWith("/", StringComparison.Ordinal))
|
||||
{
|
||||
programState.PathSections.Clear();
|
||||
}
|
||||
|
||||
foreach (string part in parts)
|
||||
{
|
||||
switch (part)
|
||||
{
|
||||
case ".":
|
||||
break;
|
||||
case "..":
|
||||
if (programState.PathSections.Count > 0)
|
||||
{
|
||||
programState.PathSections.Pop();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
programState.PathSections.Push(part);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
shellState.ConsoleManager.WriteLine($"/{string.Join("/", programState.PathSections.Reverse())}");
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override CommandInputSpecification InputSpec { get; } = CommandInputSpecification.Create("cd")
|
||||
.MaximumArgCount(1)
|
||||
.Finish();
|
||||
|
||||
protected override string GetHelpDetails(IShellState shellState, HttpState programState, DefaultCommandInput<ICoreParseResult> commandInput, ICoreParseResult parseResult)
|
||||
{
|
||||
if (commandInput.Arguments.Count == 1 && !string.IsNullOrEmpty(commandInput.Arguments[0]?.Text))
|
||||
{
|
||||
return "Prints the current directory if no argument is specified, otherwise changes to the specified directory";
|
||||
}
|
||||
|
||||
return "Changes to the directory " + commandInput.Arguments[0].Text;
|
||||
}
|
||||
|
||||
public override string GetHelpSummary(IShellState shellState, HttpState programState)
|
||||
{
|
||||
return "cd [directory name] - Prints the current directory if no argument is specified, otherwise changes to the specified directory";
|
||||
}
|
||||
|
||||
protected override IEnumerable<string> GetArgumentSuggestionsForText(IShellState shellState, HttpState programState, ICoreParseResult parseResult, DefaultCommandInput<ICoreParseResult> commandInput, string normalCompletionString)
|
||||
{
|
||||
return ServerPathCompletion.GetCompletions(programState, normalCompletionString);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Repl;
|
||||
using Microsoft.Repl.Commanding;
|
||||
using Microsoft.Repl.Parsing;
|
||||
|
||||
namespace Microsoft.HttpRepl.Commands
|
||||
{
|
||||
public class ClearCommand : ICommand<object, ICoreParseResult>
|
||||
{
|
||||
private static readonly string Name = "clear";
|
||||
private static readonly string AlternateName = "cls";
|
||||
|
||||
public bool? CanHandle(IShellState shellState, object programState, ICoreParseResult parseResult)
|
||||
{
|
||||
return parseResult.Sections.Count == 1 && (string.Equals(parseResult.Sections[0], Name, StringComparison.OrdinalIgnoreCase) || string.Equals(parseResult.Sections[0], AlternateName, StringComparison.OrdinalIgnoreCase))
|
||||
? (bool?) true
|
||||
: null;
|
||||
}
|
||||
|
||||
public Task ExecuteAsync(IShellState shellState, object programState, ICoreParseResult parseResult, CancellationToken cancellationToken)
|
||||
{
|
||||
shellState.ConsoleManager.Clear();
|
||||
shellState.CommandDispatcher.OnReady(shellState);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public string GetHelpDetails(IShellState shellState, object programState, ICoreParseResult parseResult)
|
||||
{
|
||||
if (parseResult.Sections.Count == 1 && (string.Equals(parseResult.Sections[0], Name, StringComparison.OrdinalIgnoreCase) || string.Equals(parseResult.Sections[0], AlternateName, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return "Clears the shell";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string GetHelpSummary(IShellState shellState, object programState)
|
||||
{
|
||||
return "clear - Clears the shell";
|
||||
}
|
||||
|
||||
public IEnumerable<string> Suggest(IShellState shellState, object programState, ICoreParseResult parseResult)
|
||||
{
|
||||
if (parseResult.SelectedSection == 0 &&
|
||||
(string.IsNullOrEmpty(parseResult.Sections[parseResult.SelectedSection]) || Name.StartsWith(parseResult.Sections[0].Substring(0, parseResult.CaretPositionWithinSelectedSection), StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return new[] { Name };
|
||||
}
|
||||
|
||||
if (parseResult.SelectedSection == 0 &&
|
||||
(string.IsNullOrEmpty(parseResult.Sections[parseResult.SelectedSection]) || AlternateName.StartsWith(parseResult.Sections[0].Substring(0, parseResult.CaretPositionWithinSelectedSection), StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return new[] { Name };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.HttpRepl.Diagnostics;
|
||||
using Microsoft.Repl;
|
||||
using Microsoft.Repl.Commanding;
|
||||
using Microsoft.Repl.ConsoleHandling;
|
||||
using Microsoft.Repl.Parsing;
|
||||
|
||||
namespace Microsoft.HttpRepl.Commands
|
||||
{
|
||||
public class ConfigCommand : CommandWithStructuredInputBase<HttpState, ICoreParseResult>
|
||||
{
|
||||
protected override async Task ExecuteAsync(IShellState shellState, HttpState programState, DefaultCommandInput<ICoreParseResult> commandInput, ICoreParseResult parseResult, CancellationToken cancellationToken)
|
||||
{
|
||||
if (programState.BaseAddress == null)
|
||||
{
|
||||
shellState.ConsoleManager.Error.WriteLine("Must be connected to a server to query configuration".Bold().Red());
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(programState.DiagnosticsState.DiagnosticsEndpoint))
|
||||
{
|
||||
shellState.ConsoleManager.Error.WriteLine("Diagnostics endpoint must be set to query configuration (see set diag)".Bold().Red());
|
||||
return;
|
||||
}
|
||||
|
||||
string configUrl = programState.DiagnosticsState.DiagnosticItems.FirstOrDefault(x => x.DisplayName == "Configuration")?.Url;
|
||||
|
||||
if (configUrl == null)
|
||||
{
|
||||
shellState.ConsoleManager.Error.WriteLine("Diagnostics endpoint does not expose configuration information".Bold().Red());
|
||||
return;
|
||||
}
|
||||
|
||||
HttpResponseMessage response = await programState.Client.GetAsync(new Uri(programState.BaseAddress, configUrl), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
shellState.ConsoleManager.Error.WriteLine("Unable to get configuration information from diagnostics endpoint".Bold().Red());
|
||||
return;
|
||||
}
|
||||
|
||||
List<ConfigItem> configItems = await response.Content.ReadAsAsync<List<ConfigItem>>(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (ConfigItem item in configItems)
|
||||
{
|
||||
shellState.ConsoleManager.WriteLine($"{item.Key.Cyan()}: {item.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
protected override CommandInputSpecification InputSpec { get; } = CommandInputSpecification.Create("config").Finish();
|
||||
|
||||
protected override string GetHelpDetails(IShellState shellState, HttpState programState, DefaultCommandInput<ICoreParseResult> commandInput, ICoreParseResult parseResult)
|
||||
{
|
||||
return "config - Gets configuration information for the site if connected to a diagnostics endpoint";
|
||||
}
|
||||
|
||||
public override string GetHelpSummary(IShellState shellState, HttpState programState)
|
||||
{
|
||||
return "config - Gets configuration information for the site if connected to a diagnostics endpoint";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
namespace Microsoft.HttpRepl.Commands
|
||||
{
|
||||
public class DeleteCommand : BaseHttpCommand
|
||||
{
|
||||
protected override string Verb => "delete";
|
||||
|
||||
protected override bool RequiresBody => true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Repl;
|
||||
using Microsoft.Repl.Commanding;
|
||||
using Microsoft.Repl.ConsoleHandling;
|
||||
using Microsoft.Repl.Parsing;
|
||||
|
||||
namespace Microsoft.HttpRepl.Commands
|
||||
{
|
||||
public class EchoCommand : CommandWithStructuredInputBase<HttpState, ICoreParseResult>
|
||||
{
|
||||
private readonly HashSet<string> _allowedModes = new HashSet<string>(StringComparer.OrdinalIgnoreCase) {"on", "off"};
|
||||
|
||||
protected override bool CanHandle(IShellState shellState, HttpState programState, DefaultCommandInput<ICoreParseResult> commandInput)
|
||||
{
|
||||
if (commandInput.Arguments.Count == 0 || !_allowedModes.Contains(commandInput.Arguments[0]?.Text))
|
||||
{
|
||||
shellState.ConsoleManager.Error.WriteLine("Allowed echo modes are 'on' and 'off'".Bold().Red());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override Task ExecuteAsync(IShellState shellState, HttpState programState, DefaultCommandInput<ICoreParseResult> commandInput, ICoreParseResult parseResult, CancellationToken cancellationToken)
|
||||
{
|
||||
bool turnOn = string.Equals(commandInput.Arguments[0].Text, "on", StringComparison.OrdinalIgnoreCase);
|
||||
programState.EchoRequest = turnOn;
|
||||
|
||||
shellState.ConsoleManager.WriteLine("Request echoing is " + (turnOn ? "on" : "off"));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override CommandInputSpecification InputSpec { get; } = CommandInputSpecification.Create("echo").ExactArgCount(1).Finish();
|
||||
|
||||
protected override string GetHelpDetails(IShellState shellState, HttpState programState, DefaultCommandInput<ICoreParseResult> commandInput, ICoreParseResult parseResult)
|
||||
{
|
||||
return "Turns request echoing on or off";
|
||||
}
|
||||
|
||||
public override string GetHelpSummary(IShellState shellState, HttpState programState)
|
||||
{
|
||||
return "echo [on/off] - Turns request echoing on or off";
|
||||
}
|
||||
|
||||
protected override IEnumerable<string> GetArgumentSuggestionsForText(IShellState shellState, HttpState programState, ICoreParseResult parseResult, DefaultCommandInput<ICoreParseResult> commandInput, string normalCompletionString)
|
||||
{
|
||||
List<string> result = _allowedModes.Where(x => x.StartsWith(normalCompletionString, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
return result.Count > 0 ? result : null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Repl;
|
||||
using Microsoft.Repl.Commanding;
|
||||
using Microsoft.Repl.Parsing;
|
||||
|
||||
namespace Microsoft.HttpRepl.Commands
|
||||
{
|
||||
public class ExitCommand : CommandWithStructuredInputBase<object, ICoreParseResult>
|
||||
{
|
||||
protected override Task ExecuteAsync(IShellState shellState, object programState, DefaultCommandInput<ICoreParseResult> commandInput, ICoreParseResult parseResult, CancellationToken cancellationToken)
|
||||
{
|
||||
shellState.IsExiting = true;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override CommandInputSpecification InputSpec { get; } = CommandInputSpecification.Create("exit").ExactArgCount(0).Finish();
|
||||
|
||||
protected override string GetHelpDetails(IShellState shellState, object programState, DefaultCommandInput<ICoreParseResult> commandInput, ICoreParseResult parseResult)
|
||||
{
|
||||
return "Exits the shell";
|
||||
}
|
||||
|
||||
public override string GetHelpSummary(IShellState shellState, object programState)
|
||||
{
|
||||
return "exit - Exits the shell";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
namespace Microsoft.HttpRepl.Commands
|
||||
{
|
||||
public class Formatter
|
||||
{
|
||||
//private readonly List<int> _prefix = new List<int>();
|
||||
private int _prefix;
|
||||
private int _maxDepth;
|
||||
|
||||
public void RegisterEntry(int prefixLength, int depth)
|
||||
{
|
||||
//while (_prefix.Count < depth + 1)
|
||||
//{
|
||||
// _prefix.Add(0);
|
||||
//}
|
||||
|
||||
if (depth > _maxDepth)
|
||||
{
|
||||
_maxDepth = depth;
|
||||
}
|
||||
|
||||
if (prefixLength > _prefix)
|
||||
{
|
||||
_prefix = prefixLength;
|
||||
}
|
||||
}
|
||||
|
||||
public string Format(string prefix, string entry, int level)
|
||||
{
|
||||
string indent = "".PadRight(level * 4);
|
||||
return (indent + prefix).PadRight(_prefix + 3 + _maxDepth * 4) + entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
namespace Microsoft.HttpRepl.Commands
|
||||
{
|
||||
public class GetCommand : BaseHttpCommand
|
||||
{
|
||||
protected override string Verb => "get";
|
||||
|
||||
protected override bool RequiresBody => false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
namespace Microsoft.HttpRepl.Commands
|
||||
{
|
||||
public class HeadCommand : BaseHttpCommand
|
||||
{
|
||||
protected override string Verb => "head";
|
||||
|
||||
protected override bool RequiresBody => false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.HttpRepl.Suggestions;
|
||||
using Microsoft.Repl;
|
||||
using Microsoft.Repl.Commanding;
|
||||
using Microsoft.Repl.Parsing;
|
||||
|
||||
namespace Microsoft.HttpRepl.Commands
|
||||
{
|
||||
public class HelpCommand : ICommand<HttpState, ICoreParseResult>
|
||||
{
|
||||
private static readonly string Name = "help";
|
||||
|
||||
public bool? CanHandle(IShellState shellState, HttpState programState, ICoreParseResult parseResult)
|
||||
{
|
||||
return parseResult.Sections.Count > 0 && string.Equals(parseResult.Sections[0], Name)
|
||||
? (bool?)true
|
||||
: null;
|
||||
}
|
||||
|
||||
public Task ExecuteAsync(IShellState shellState, HttpState programState, ICoreParseResult parseResult, CancellationToken cancellationToken)
|
||||
{
|
||||
if (shellState.CommandDispatcher is ICommandDispatcher<HttpState, ICoreParseResult> dispatcher)
|
||||
{
|
||||
if (parseResult.Sections.Count == 1)
|
||||
{
|
||||
foreach (ICommand<HttpState, ICoreParseResult> command in dispatcher.Commands)
|
||||
{
|
||||
string help = command.GetHelpSummary(shellState, programState);
|
||||
|
||||
if (!string.IsNullOrEmpty(help))
|
||||
{
|
||||
shellState.ConsoleManager.WriteLine(help);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
bool anyHelp = false;
|
||||
|
||||
if (parseResult.Slice(1) is ICoreParseResult continuationParseResult)
|
||||
{
|
||||
foreach (ICommand<HttpState, ICoreParseResult> command in dispatcher.Commands)
|
||||
{
|
||||
string help = command.GetHelpDetails(shellState, programState, continuationParseResult);
|
||||
|
||||
if (!string.IsNullOrEmpty(help))
|
||||
{
|
||||
anyHelp = true;
|
||||
shellState.ConsoleManager.WriteLine(help);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!anyHelp)
|
||||
{
|
||||
//Maybe the input is an URL
|
||||
if (parseResult.Sections.Count == 2)
|
||||
{
|
||||
IDirectoryStructure structure = programState.Structure.TraverseTo(parseResult.Sections[1]);
|
||||
if (structure.DirectoryNames.Any())
|
||||
{
|
||||
shellState.ConsoleManager.WriteLine("Child directories:");
|
||||
|
||||
foreach (string name in structure.DirectoryNames)
|
||||
{
|
||||
shellState.ConsoleManager.WriteLine(" " + name + "/");
|
||||
}
|
||||
|
||||
anyHelp = true;
|
||||
}
|
||||
|
||||
if (structure.RequestInfo != null)
|
||||
{
|
||||
if (structure.RequestInfo.Methods.Count > 0)
|
||||
{
|
||||
if (anyHelp)
|
||||
{
|
||||
shellState.ConsoleManager.WriteLine();
|
||||
}
|
||||
|
||||
anyHelp = true;
|
||||
shellState.ConsoleManager.WriteLine("Available methods:");
|
||||
|
||||
foreach (string method in structure.RequestInfo.Methods)
|
||||
{
|
||||
shellState.ConsoleManager.WriteLine(" " + method.ToUpperInvariant());
|
||||
IReadOnlyList<string> accepts = structure.RequestInfo.ContentTypesByMethod[method];
|
||||
string acceptsString = string.Join(", ", accepts.Where(x => !string.IsNullOrEmpty(x)));
|
||||
if (!string.IsNullOrEmpty(acceptsString))
|
||||
{
|
||||
shellState.ConsoleManager.WriteLine(" Accepts: " + acceptsString);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!anyHelp)
|
||||
{
|
||||
shellState.ConsoleManager.WriteLine("Unable to locate any help information for the specified command");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public string GetHelpDetails(IShellState shellState, HttpState programState, ICoreParseResult parseResult)
|
||||
{
|
||||
if (parseResult.Sections.Count > 0 && string.Equals(parseResult.Sections[0], Name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (parseResult.Sections.Count > 1)
|
||||
{
|
||||
return "Gets help about " + parseResult.Slice(1).CommandText;
|
||||
}
|
||||
else
|
||||
{
|
||||
return "Gets help";
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string GetHelpSummary(IShellState shellState, HttpState programState)
|
||||
{
|
||||
return "help - Gets help";
|
||||
}
|
||||
|
||||
public IEnumerable<string> Suggest(IShellState shellState, HttpState programState, ICoreParseResult parseResult)
|
||||
{
|
||||
if (parseResult.SelectedSection == 0 &&
|
||||
(string.IsNullOrEmpty(parseResult.Sections[parseResult.SelectedSection]) || Name.StartsWith(parseResult.Sections[0].Substring(0, parseResult.CaretPositionWithinSelectedSection), StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return new[] { Name };
|
||||
}
|
||||
else if (parseResult.Sections.Count > 1 && string.Equals(parseResult.Sections[0], Name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (shellState.CommandDispatcher is ICommandDispatcher<HttpState, ICoreParseResult> dispatcher
|
||||
&& parseResult.Slice(1) is ICoreParseResult continuationParseResult)
|
||||
{
|
||||
HashSet<string> suggestions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (ICommand<HttpState, ICoreParseResult> command in dispatcher.Commands)
|
||||
{
|
||||
IEnumerable<string> commandSuggestions = command.Suggest(shellState, programState, continuationParseResult);
|
||||
|
||||
if (commandSuggestions != null)
|
||||
{
|
||||
suggestions.UnionWith(commandSuggestions);
|
||||
}
|
||||
}
|
||||
|
||||
if (continuationParseResult.SelectedSection == 0)
|
||||
{
|
||||
string normalizedCompletionText = continuationParseResult.Sections[0].Substring(0, continuationParseResult.CaretPositionWithinSelectedSection);
|
||||
suggestions.UnionWith(ServerPathCompletion.GetCompletions(programState, normalizedCompletionText));
|
||||
}
|
||||
|
||||
return suggestions.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Repl;
|
||||
using Microsoft.Repl.Commanding;
|
||||
using Microsoft.Repl.Parsing;
|
||||
|
||||
namespace Microsoft.HttpRepl.Commands
|
||||
{
|
||||
public class ListCommand : CommandWithStructuredInputBase<HttpState, ICoreParseResult>
|
||||
{
|
||||
private const string RecursiveOption = nameof(RecursiveOption);
|
||||
|
||||
protected override Task ExecuteAsync(IShellState shellState, HttpState programState, DefaultCommandInput<ICoreParseResult> commandInput, ICoreParseResult parseResult, CancellationToken cancellationToken)
|
||||
{
|
||||
if (programState.Structure == null || programState.BaseAddress == null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
string path = commandInput.Arguments.Count > 0 ? commandInput.Arguments[0].Text : string.Empty;
|
||||
|
||||
//If it's an absolute URI, nothing to suggest
|
||||
if (Uri.TryCreate(path, UriKind.Absolute, out Uri _))
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
IDirectoryStructure s = programState.Structure.TraverseTo(programState.PathSections.Reverse()).TraverseTo(path);
|
||||
|
||||
List<TreeNode> roots = new List<TreeNode>();
|
||||
Formatter formatter = new Formatter();
|
||||
|
||||
roots.Add(new TreeNode(formatter, ".", string.Empty));
|
||||
|
||||
if (s.Parent != null)
|
||||
{
|
||||
roots.Add(new TreeNode(formatter, "..", string.Empty));
|
||||
}
|
||||
|
||||
int recursionDepth = 1;
|
||||
|
||||
if (commandInput.Options[RecursiveOption].Count > 0)
|
||||
{
|
||||
if (string.IsNullOrEmpty(commandInput.Options[RecursiveOption][0]?.Text))
|
||||
{
|
||||
recursionDepth = int.MaxValue;
|
||||
}
|
||||
else if (int.TryParse(commandInput.Options[RecursiveOption][0].Text, NumberStyles.Integer, CultureInfo.InvariantCulture, out int rd) && rd > 1)
|
||||
{
|
||||
recursionDepth = rd;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string child in s.DirectoryNames)
|
||||
{
|
||||
IDirectoryStructure dir = s.GetChildDirectory(child);
|
||||
|
||||
string methods = dir.RequestInfo != null && dir.RequestInfo.Methods.Count > 0
|
||||
? "[" + string.Join("|", dir.RequestInfo.Methods) + "]"
|
||||
: "[]";
|
||||
|
||||
TreeNode dirNode = new TreeNode(formatter, child, methods);
|
||||
roots.Add(dirNode);
|
||||
Recurse(dirNode, dir, recursionDepth - 1);
|
||||
}
|
||||
|
||||
foreach (TreeNode node in roots)
|
||||
{
|
||||
shellState.ConsoleManager.WriteLine(node.ToString());
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static void Recurse(TreeNode parentNode, IDirectoryStructure parent, int remainingDepth)
|
||||
{
|
||||
if (remainingDepth <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (string child in parent.DirectoryNames)
|
||||
{
|
||||
IDirectoryStructure dir = parent.GetChildDirectory(child);
|
||||
|
||||
string methods = dir.RequestInfo != null && dir.RequestInfo.Methods.Count > 0
|
||||
? "[" + string.Join("|", dir.RequestInfo.Methods) + "]"
|
||||
: "[]";
|
||||
|
||||
TreeNode node = parentNode.AddChild(child, methods);
|
||||
Recurse(node, dir, remainingDepth - 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected override CommandInputSpecification InputSpec { get; } = CommandInputSpecification.Create("ls")
|
||||
.MaximumArgCount(1)
|
||||
.WithOption(new CommandOptionSpecification(RecursiveOption, maximumOccurrences: 1, acceptsValue: true, forms: new[] {"-r", "--recursive"}))
|
||||
.Finish();
|
||||
|
||||
protected override string GetHelpDetails(IShellState shellState, HttpState programState, DefaultCommandInput<ICoreParseResult> commandInput, ICoreParseResult parseResult)
|
||||
{
|
||||
return "Lists the contents of a directory";
|
||||
}
|
||||
|
||||
public override string GetHelpSummary(IShellState shellState, HttpState programState)
|
||||
{
|
||||
return "ls - Performs a directory listing";
|
||||
}
|
||||
|
||||
protected override IEnumerable<string> GetArgumentSuggestionsForText(IShellState shellState, HttpState programState, ICoreParseResult parseResult, DefaultCommandInput<ICoreParseResult> commandInput, string normalCompletionString)
|
||||
{
|
||||
if (programState.Structure == null || programState.BaseAddress == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
//If it's an absolute URI, nothing to suggest
|
||||
if (Uri.TryCreate(normalCompletionString, UriKind.Absolute, out Uri _))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string path = normalCompletionString.Replace('\\', '/');
|
||||
int searchFrom = normalCompletionString.Length - 1;
|
||||
int lastSlash = path.LastIndexOf('/', searchFrom);
|
||||
string prefix;
|
||||
|
||||
if (lastSlash < 0)
|
||||
{
|
||||
path = string.Empty;
|
||||
prefix = normalCompletionString;
|
||||
}
|
||||
else
|
||||
{
|
||||
path = path.Substring(0, lastSlash + 1);
|
||||
prefix = normalCompletionString.Substring(lastSlash + 1);
|
||||
}
|
||||
|
||||
IDirectoryStructure s = programState.Structure.TraverseTo(programState.PathSections.Reverse()).TraverseTo(path);
|
||||
|
||||
List<string> results = new List<string>();
|
||||
|
||||
foreach (string child in s.DirectoryNames)
|
||||
{
|
||||
if (child.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
results.Add(path + child);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
namespace Microsoft.HttpRepl.Commands
|
||||
{
|
||||
public class OptionsCommand : BaseHttpCommand
|
||||
{
|
||||
protected override string Verb => "options";
|
||||
|
||||
protected override bool RequiresBody => false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
namespace Microsoft.HttpRepl.Commands
|
||||
{
|
||||
public class PatchCommand : BaseHttpCommand
|
||||
{
|
||||
protected override string Verb => "patch";
|
||||
|
||||
protected override bool RequiresBody => true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
namespace Microsoft.HttpRepl.Commands
|
||||
{
|
||||
public class PostCommand : BaseHttpCommand
|
||||
{
|
||||
protected override string Verb => "post";
|
||||
|
||||
protected override bool RequiresBody => true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,172 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.HttpRepl.Preferences;
|
||||
using Microsoft.Repl;
|
||||
using Microsoft.Repl.Commanding;
|
||||
using Microsoft.Repl.ConsoleHandling;
|
||||
using Microsoft.Repl.Parsing;
|
||||
|
||||
namespace Microsoft.HttpRepl.Commands
|
||||
{
|
||||
public class PrefCommand : CommandWithStructuredInputBase<HttpState, ICoreParseResult>
|
||||
{
|
||||
private readonly HashSet<string> _allowedSubcommands = new HashSet<string>(StringComparer.OrdinalIgnoreCase) {"get", "set"};
|
||||
|
||||
public override string GetHelpSummary(IShellState shellState, HttpState programState)
|
||||
{
|
||||
return "pref [get/set] {setting} [{value}] - Allows viewing or changing preferences";
|
||||
}
|
||||
|
||||
protected override bool CanHandle(IShellState shellState, HttpState programState, DefaultCommandInput<ICoreParseResult> commandInput)
|
||||
{
|
||||
if (commandInput.Arguments.Count == 0 || !_allowedSubcommands.Contains(commandInput.Arguments[0]?.Text))
|
||||
{
|
||||
shellState.ConsoleManager.Error.WriteLine("Whether get or set settings must be specified");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.Equals("get", commandInput.Arguments[0].Text) && (commandInput.Arguments.Count < 2 || string.IsNullOrEmpty(commandInput.Arguments[1]?.Text)))
|
||||
{
|
||||
shellState.ConsoleManager.Error.WriteLine("The preference to set must be specified");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override string GetHelpDetails(IShellState shellState, HttpState programState, DefaultCommandInput<ICoreParseResult> commandInput, ICoreParseResult parseResult)
|
||||
{
|
||||
if (commandInput.Arguments.Count == 0 || !_allowedSubcommands.Contains(commandInput.Arguments[0]?.Text))
|
||||
{
|
||||
return "pref [get/set] {setting} [{value}] - Get or sets a preference to a particular value";
|
||||
}
|
||||
|
||||
if (string.Equals(commandInput.Arguments[0].Text, "get", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "pref get [{setting}] - Gets the value of the specified preference or lists all preferences if no preference is specified";
|
||||
}
|
||||
else
|
||||
{
|
||||
return "pref set {setting} [{value}] - Sets (or clears if value is not specified) the value of the specified preference";
|
||||
}
|
||||
}
|
||||
|
||||
protected override Task ExecuteAsync(IShellState shellState, HttpState programState, DefaultCommandInput<ICoreParseResult> commandInput, ICoreParseResult parseResult, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.Equals(commandInput.Arguments[0].Text, "get", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return GetSetting(shellState, programState, commandInput);
|
||||
}
|
||||
|
||||
return SetSetting(shellState, programState, commandInput);
|
||||
}
|
||||
|
||||
private static Task SetSetting(IShellState shellState, HttpState programState, DefaultCommandInput<ICoreParseResult> commandInput)
|
||||
{
|
||||
string prefName = commandInput.Arguments[1].Text;
|
||||
string prefValue = commandInput.Arguments.Count > 2 ? commandInput.Arguments[2]?.Text : null;
|
||||
|
||||
if (string.IsNullOrEmpty(prefValue))
|
||||
{
|
||||
if (!programState.DefaultPreferences.TryGetValue(prefName, out string defaultValue))
|
||||
{
|
||||
programState.Preferences.Remove(prefName);
|
||||
}
|
||||
else
|
||||
{
|
||||
programState.Preferences[prefName] = defaultValue;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
programState.Preferences[prefName] = prefValue;
|
||||
}
|
||||
|
||||
if (!programState.SavePreferences())
|
||||
{
|
||||
shellState.ConsoleManager.Error.WriteLine("Error saving preferences".Bold().Red());
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static Task GetSetting(IShellState shellState, HttpState programState, DefaultCommandInput<ICoreParseResult> commandInput)
|
||||
{
|
||||
string preferenceName = commandInput.Arguments.Count > 1 ? commandInput.Arguments[1]?.Text : null;
|
||||
|
||||
//If there's a particular setting to get the value of
|
||||
if (!string.IsNullOrEmpty(preferenceName))
|
||||
{
|
||||
if (programState.Preferences.TryGetValue(preferenceName, out string value))
|
||||
{
|
||||
shellState.ConsoleManager.WriteLine("Configured value: " + value);
|
||||
}
|
||||
else
|
||||
{
|
||||
shellState.ConsoleManager.Error.WriteLine((commandInput.Arguments[1].Text + " does not have a configured value").Bold().Red());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (KeyValuePair<string, string> entry in programState.Preferences.OrderBy(x => x.Key))
|
||||
{
|
||||
shellState.ConsoleManager.WriteLine($"{entry.Key}={entry.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override CommandInputSpecification InputSpec { get; } = CommandInputSpecification.Create("pref")
|
||||
.MinimumArgCount(1)
|
||||
.MaximumArgCount(3)
|
||||
.Finish();
|
||||
|
||||
|
||||
protected override IEnumerable<string> GetArgumentSuggestionsForText(IShellState shellState, HttpState programState, ICoreParseResult parseResult, DefaultCommandInput<ICoreParseResult> commandInput, string normalCompletionString)
|
||||
{
|
||||
if (parseResult.SelectedSection == 1)
|
||||
{
|
||||
return _allowedSubcommands.Where(x => x.StartsWith(normalCompletionString, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
if (parseResult.SelectedSection == 2)
|
||||
{
|
||||
string prefix = parseResult.Sections.Count > 2 ? normalCompletionString : string.Empty;
|
||||
List<string> matchingProperties = new List<string>();
|
||||
|
||||
foreach (string val in WellKnownPreference.Catalog.Names)
|
||||
{
|
||||
if (val.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
matchingProperties.Add(val);
|
||||
}
|
||||
}
|
||||
|
||||
return matchingProperties;
|
||||
}
|
||||
|
||||
if (parseResult.SelectedSection == 3
|
||||
&& parseResult.Sections[2].StartsWith("colors.", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string prefix = parseResult.Sections.Count > 3 ? normalCompletionString : string.Empty;
|
||||
List<string> matchingProperties = new List<string>();
|
||||
|
||||
foreach (string val in Enum.GetNames(typeof(AllowedColors)))
|
||||
{
|
||||
if (val.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
matchingProperties.Add(val);
|
||||
}
|
||||
}
|
||||
|
||||
return matchingProperties;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
namespace Microsoft.HttpRepl.Commands
|
||||
{
|
||||
public class PutCommand : BaseHttpCommand
|
||||
{
|
||||
protected override string Verb => "put";
|
||||
|
||||
protected override bool RequiresBody => true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Repl;
|
||||
using Microsoft.Repl.Commanding;
|
||||
using Microsoft.Repl.Parsing;
|
||||
using Microsoft.Repl.Scripting;
|
||||
using Microsoft.Repl.Suggestions;
|
||||
|
||||
namespace Microsoft.HttpRepl.Commands
|
||||
{
|
||||
public class RunCommand : ICommand<HttpState, ICoreParseResult>
|
||||
{
|
||||
private static readonly string Name = "run";
|
||||
|
||||
public bool? CanHandle(IShellState shellState, HttpState programState, ICoreParseResult parseResult)
|
||||
{
|
||||
return parseResult.Sections.Count > 1 && parseResult.Sections.Count < 4 && string.Equals(Name, parseResult.Sections[0], StringComparison.OrdinalIgnoreCase)
|
||||
? (bool?)true
|
||||
: null;
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(IShellState shellState, HttpState programState, ICoreParseResult parseResult, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!File.Exists(parseResult.Sections[1]))
|
||||
{
|
||||
shellState.ConsoleManager.Error.WriteLine($"Could not file script file {parseResult.Sections[1]}");
|
||||
return;
|
||||
}
|
||||
|
||||
bool suppressScriptLinesInHistory = true;
|
||||
if (parseResult.Sections.Count == 3)
|
||||
{
|
||||
suppressScriptLinesInHistory = !string.Equals(parseResult.Sections[2], "+history");
|
||||
}
|
||||
|
||||
string[] lines = File.ReadAllLines(parseResult.Sections[1]);
|
||||
IScriptExecutor scriptExecutor = new ScriptExecutor<HttpState, ICoreParseResult>(suppressScriptLinesInHistory);
|
||||
await scriptExecutor.ExecuteScriptAsync(shellState, lines, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public string GetHelpDetails(IShellState shellState, HttpState programState, ICoreParseResult parseResult)
|
||||
{
|
||||
if (parseResult.Sections.Count > 0 && string.Equals(parseResult.Sections[0], Name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (parseResult.Sections.Count == 1)
|
||||
{
|
||||
return "Runs the specified script";
|
||||
}
|
||||
|
||||
return "Runs the script " + parseResult.Sections[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string GetHelpSummary(IShellState shellState, HttpState programState)
|
||||
{
|
||||
return "run {path to script} - Runs a script";
|
||||
}
|
||||
|
||||
public IEnumerable<string> Suggest(IShellState shellState, HttpState programState, ICoreParseResult parseResult)
|
||||
{
|
||||
if (parseResult.SelectedSection == 0 &&
|
||||
(string.IsNullOrEmpty(parseResult.Sections[parseResult.SelectedSection]) || Name.StartsWith(parseResult.Sections[0].Substring(0, parseResult.CaretPositionWithinSelectedSection), StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return new[] { Name };
|
||||
}
|
||||
|
||||
if (parseResult.SelectedSection == 1 && string.Equals(parseResult.Sections[0], Name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return FileSystemCompletion.GetCompletions(parseResult.Sections[1].Substring(0, parseResult.CaretPositionWithinSelectedSection));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Repl;
|
||||
using Microsoft.Repl.Commanding;
|
||||
using Microsoft.Repl.ConsoleHandling;
|
||||
using Microsoft.Repl.Parsing;
|
||||
|
||||
namespace Microsoft.HttpRepl.Commands
|
||||
{
|
||||
public class SetBaseCommand : ICommand<HttpState, ICoreParseResult>
|
||||
{
|
||||
private const string Name = "set";
|
||||
private const string SubCommand = "base";
|
||||
|
||||
public string Description => "Sets the base address to direct requests to.";
|
||||
|
||||
public bool? CanHandle(IShellState shellState, HttpState programState, ICoreParseResult parseResult)
|
||||
{
|
||||
return parseResult.Sections.Count > 1 && string.Equals(parseResult.Sections[0], Name, StringComparison.OrdinalIgnoreCase) && string.Equals(parseResult.Sections[1], SubCommand, StringComparison.OrdinalIgnoreCase)
|
||||
? (bool?)true
|
||||
: null;
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(IShellState shellState, HttpState state, ICoreParseResult parseResult, CancellationToken cancellationToken)
|
||||
{
|
||||
if (parseResult.Sections.Count == 2)
|
||||
{
|
||||
state.BaseAddress = null;
|
||||
}
|
||||
else if (parseResult.Sections.Count != 3 || string.IsNullOrEmpty(parseResult.Sections[2]) || !Uri.TryCreate(EnsureTrailingSlash(parseResult.Sections[2]), UriKind.Absolute, out Uri serverUri))
|
||||
{
|
||||
shellState.ConsoleManager.Error.WriteLine("Must specify a server".Bold().Red());
|
||||
}
|
||||
else
|
||||
{
|
||||
state.BaseAddress = serverUri;
|
||||
}
|
||||
|
||||
if (state.BaseAddress == null || !Uri.TryCreate(state.BaseAddress, "/swagger/v1/swagger.json", out Uri result))
|
||||
{
|
||||
state.SwaggerStructure = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
await SetSwaggerCommand.CreateDirectoryStructureForSwaggerEndpointAsync(shellState, state, result, cancellationToken).ConfigureAwait(false);
|
||||
if (state.SwaggerStructure != null)
|
||||
{
|
||||
shellState.ConsoleManager.WriteLine("Using swagger metadata from " + result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string EnsureTrailingSlash(string v)
|
||||
{
|
||||
if (!v.EndsWith("/", StringComparison.Ordinal))
|
||||
{
|
||||
v += "/";
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
public string GetHelpDetails(IShellState shellState, HttpState programState, ICoreParseResult parseResult)
|
||||
{
|
||||
if (parseResult.Sections.Count > 1 && string.Equals(parseResult.Sections[0], Name, StringComparison.OrdinalIgnoreCase) && string.Equals(parseResult.Sections[1], SubCommand, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Description;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string GetHelpSummary(IShellState shellState, HttpState programState)
|
||||
{
|
||||
return Description;
|
||||
}
|
||||
|
||||
public IEnumerable<string> Suggest(IShellState shellState, HttpState programState, ICoreParseResult parseResult)
|
||||
{
|
||||
if (parseResult.Sections.Count == 0)
|
||||
{
|
||||
return new[] { Name };
|
||||
}
|
||||
|
||||
if (parseResult.Sections.Count > 0 && parseResult.SelectedSection == 0 && Name.StartsWith(parseResult.Sections[0].Substring(0, parseResult.CaretPositionWithinSelectedSection), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new[] { Name };
|
||||
}
|
||||
|
||||
if (string.Equals(Name, parseResult.Sections[0], StringComparison.OrdinalIgnoreCase) && parseResult.SelectedSection == 1 && (parseResult.Sections.Count < 2 || SubCommand.StartsWith(parseResult.Sections[1].Substring(0, parseResult.CaretPositionWithinSelectedSection), StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return new[] { SubCommand };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.HttpRepl.Diagnostics;
|
||||
using Microsoft.Repl;
|
||||
using Microsoft.Repl.Commanding;
|
||||
using Microsoft.Repl.ConsoleHandling;
|
||||
using Microsoft.Repl.Parsing;
|
||||
|
||||
namespace Microsoft.HttpRepl.Commands
|
||||
{
|
||||
public class SetDiagCommand : ICommand<HttpState, ICoreParseResult>
|
||||
{
|
||||
private static readonly string Name = "set";
|
||||
private static readonly string SubCommand = "diag";
|
||||
|
||||
public string Description => "Sets the diagnostics path to direct requests to.";
|
||||
|
||||
public bool? CanHandle(IShellState shellState, HttpState programState, ICoreParseResult parseResult)
|
||||
{
|
||||
return parseResult.Sections.Count > 1 && string.Equals(parseResult.Sections[0], Name, StringComparison.OrdinalIgnoreCase) && string.Equals(parseResult.Sections[1], SubCommand, StringComparison.OrdinalIgnoreCase)
|
||||
? (bool?)true
|
||||
: null;
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(IShellState shellState, HttpState programState, ICoreParseResult parseResult, CancellationToken cancellationToken)
|
||||
{
|
||||
if (parseResult.Sections.Count == 2)
|
||||
{
|
||||
programState.DiagnosticsState.DiagnosticsEndpoint = null;
|
||||
programState.DiagnosticsState.DiagnosticItems = null;
|
||||
programState.DiagnosticsState.DiagEndpointsStructure = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (parseResult.Sections.Count != 3 || string.IsNullOrEmpty(parseResult.Sections[2]) || !Uri.TryCreate(parseResult.Sections[2], UriKind.Relative, out Uri _))
|
||||
{
|
||||
shellState.ConsoleManager.Error.WriteLine("Must specify a relative path".Bold().Red());
|
||||
}
|
||||
else
|
||||
{
|
||||
programState.DiagnosticsState.DiagnosticsEndpoint = parseResult.Sections[2];
|
||||
HttpResponseMessage response = await programState.Client.GetAsync(new Uri(programState.BaseAddress, programState.DiagnosticsState.DiagnosticsEndpoint), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
shellState.ConsoleManager.Error.WriteLine("Unable to access diagnostics endpoint".Bold().Red());
|
||||
programState.DiagnosticsState.DiagnosticsEndpoint = null;
|
||||
programState.DiagnosticsState.DiagnosticItems = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
programState.DiagnosticsState.DiagnosticItems = (await response.Content.ReadAsAsync<Dictionary<string, DiagItem>>(cancellationToken).ConfigureAwait(false))?.Select(x => x.Value).ToList();
|
||||
|
||||
DiagItem endpointsItem = programState.DiagnosticsState.DiagnosticItems?.FirstOrDefault(x => string.Equals(x.DisplayName, "Endpoints", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (endpointsItem != null)
|
||||
{
|
||||
HttpResponseMessage endpointsResponse = await programState.Client.GetAsync(new Uri(programState.BaseAddress, endpointsItem.Url), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!endpointsResponse.IsSuccessStatusCode)
|
||||
{
|
||||
shellState.ConsoleManager.Error.WriteLine("Unable to get endpoints information from diagnostics endpoint".Bold().Red());
|
||||
return;
|
||||
}
|
||||
|
||||
List<DiagEndpoint> endpoints = await endpointsResponse.Content.ReadAsAsync<List<DiagEndpoint>>(cancellationToken).ConfigureAwait(false);
|
||||
DirectoryStructure structure = new DirectoryStructure(null);
|
||||
|
||||
foreach (DiagEndpoint endpoint in endpoints)
|
||||
{
|
||||
if (endpoint.Url.StartsWith(endpointsItem.Url, StringComparison.OrdinalIgnoreCase)
|
||||
|| endpoint.Url.StartsWith("/graphql", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
FillDirectoryInfo(structure, endpoint.Url);
|
||||
}
|
||||
|
||||
programState.DiagnosticsState.DiagEndpointsStructure = structure;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void FillDirectoryInfo(DirectoryStructure parent, string endpoint)
|
||||
{
|
||||
string[] parts = endpoint.Split('/');
|
||||
|
||||
foreach (string part in parts)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(part))
|
||||
{
|
||||
parent = parent.DeclareDirectory(part);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string GetHelpSummary(IShellState shellState, HttpState programState)
|
||||
{
|
||||
return Description;
|
||||
}
|
||||
|
||||
public string GetHelpDetails(IShellState shellState, HttpState programState, ICoreParseResult parseResult)
|
||||
{
|
||||
if (parseResult.Sections.Count > 1 && string.Equals(parseResult.Sections[0], Name, StringComparison.OrdinalIgnoreCase) && string.Equals(parseResult.Sections[1], SubCommand, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Description;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public IEnumerable<string> Suggest(IShellState shellState, HttpState programState, ICoreParseResult parseResult)
|
||||
{
|
||||
if (parseResult.Sections.Count == 0)
|
||||
{
|
||||
return new[] { Name };
|
||||
}
|
||||
|
||||
if (parseResult.Sections.Count > 0 && parseResult.SelectedSection == 0 && Name.StartsWith(parseResult.Sections[0].Substring(0, parseResult.CaretPositionWithinSelectedSection), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new[] { Name };
|
||||
}
|
||||
|
||||
if (string.Equals(Name, parseResult.Sections[0], StringComparison.OrdinalIgnoreCase) && parseResult.SelectedSection == 1 && (parseResult.Sections.Count < 2 || SubCommand.StartsWith(parseResult.Sections[1].Substring(0, parseResult.CaretPositionWithinSelectedSection), StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return new[] { SubCommand };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.HttpRepl.Suggestions;
|
||||
using Microsoft.Repl;
|
||||
using Microsoft.Repl.Commanding;
|
||||
using Microsoft.Repl.Parsing;
|
||||
|
||||
namespace Microsoft.HttpRepl.Commands
|
||||
{
|
||||
public class SetHeaderCommand : ICommand<HttpState, ICoreParseResult>
|
||||
{
|
||||
private static readonly string Name = "set";
|
||||
private static readonly string SubCommand = "header";
|
||||
|
||||
public string Description => "set header {name} [{{value}}] - Sets or clears a header";
|
||||
|
||||
public bool? CanHandle(IShellState shellState, HttpState programState, ICoreParseResult parseResult)
|
||||
{
|
||||
return parseResult.Sections.Count > 2 && string.Equals(parseResult.Sections[0], Name, StringComparison.OrdinalIgnoreCase) && string.Equals(parseResult.Sections[1], SubCommand, StringComparison.OrdinalIgnoreCase)
|
||||
? (bool?)true
|
||||
: null;
|
||||
}
|
||||
|
||||
public Task ExecuteAsync(IShellState shellState, HttpState programState, ICoreParseResult parseResult, CancellationToken cancellationToken)
|
||||
{
|
||||
if (parseResult.Sections.Count == 3)
|
||||
{
|
||||
programState.Headers.Remove(parseResult.Sections[2]);
|
||||
}
|
||||
else
|
||||
{
|
||||
programState.Headers[parseResult.Sections[2]] = parseResult.Sections.Skip(3).ToList();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public string GetHelpDetails(IShellState shellState, HttpState programState, ICoreParseResult parseResult)
|
||||
{
|
||||
if (parseResult.Sections.Count > 1 && string.Equals(parseResult.Sections[0], Name, StringComparison.OrdinalIgnoreCase) && string.Equals(parseResult.Sections[1], SubCommand, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Description;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string GetHelpSummary(IShellState shellState, HttpState programState)
|
||||
{
|
||||
return Description;
|
||||
}
|
||||
|
||||
public IEnumerable<string> Suggest(IShellState shellState, HttpState programState, ICoreParseResult parseResult)
|
||||
{
|
||||
if (parseResult.Sections.Count == 0)
|
||||
{
|
||||
return new[] { Name };
|
||||
}
|
||||
|
||||
if (parseResult.Sections.Count > 0 && parseResult.SelectedSection == 0 && Name.StartsWith(parseResult.Sections[0].Substring(0, parseResult.CaretPositionWithinSelectedSection), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new[] { Name };
|
||||
}
|
||||
|
||||
if (string.Equals(Name, parseResult.Sections[0], StringComparison.OrdinalIgnoreCase) && parseResult.SelectedSection == 1 && (parseResult.Sections.Count < 2 || SubCommand.StartsWith(parseResult.Sections[1].Substring(0, parseResult.CaretPositionWithinSelectedSection), StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return new[] { SubCommand };
|
||||
}
|
||||
|
||||
if (parseResult.Sections.Count > 2
|
||||
&& string.Equals(Name, parseResult.Sections[0], StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(SubCommand, parseResult.Sections[1], StringComparison.OrdinalIgnoreCase) && parseResult.SelectedSection == 2)
|
||||
{
|
||||
string prefix = parseResult.Sections[2].Substring(0, parseResult.CaretPositionWithinSelectedSection);
|
||||
return HeaderCompletion.GetCompletions(null, prefix);
|
||||
}
|
||||
|
||||
if (parseResult.Sections.Count > 3
|
||||
&& string.Equals(Name, parseResult.Sections[0], StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(SubCommand, parseResult.Sections[1], StringComparison.OrdinalIgnoreCase) && parseResult.SelectedSection == 3)
|
||||
{
|
||||
string prefix = parseResult.Sections[3].Substring(0, parseResult.CaretPositionWithinSelectedSection);
|
||||
return HeaderCompletion.GetValueCompletions(null, string.Empty, parseResult.Sections[2], prefix, programState);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,272 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.HttpRepl.OpenApi;
|
||||
using Microsoft.Repl;
|
||||
using Microsoft.Repl.Commanding;
|
||||
using Microsoft.Repl.ConsoleHandling;
|
||||
using Microsoft.Repl.Parsing;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Microsoft.HttpRepl.Commands
|
||||
{
|
||||
public class SetSwaggerCommand : ICommand<HttpState, ICoreParseResult>
|
||||
{
|
||||
private static readonly string Name = "set";
|
||||
private static readonly string SubCommand = "swagger";
|
||||
|
||||
public string Description => "Sets the swagger document to use for information about the current server";
|
||||
|
||||
private static void FillDirectoryInfo(DirectoryStructure parent, EndpointMetadata entry)
|
||||
{
|
||||
string[] parts = entry.Path.Split('/');
|
||||
|
||||
foreach (string part in parts)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(part))
|
||||
{
|
||||
parent = parent.DeclareDirectory(part);
|
||||
}
|
||||
}
|
||||
|
||||
RequestInfo dirRequestInfo = new RequestInfo();
|
||||
|
||||
foreach (KeyValuePair<string, IReadOnlyDictionary<string, IReadOnlyList<Parameter>>> requestInfo in entry.AvailableRequests)
|
||||
{
|
||||
string method = requestInfo.Key;
|
||||
|
||||
foreach (KeyValuePair<string, IReadOnlyList<Parameter>> parameterSetsByContentType in requestInfo.Value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(parameterSetsByContentType.Key))
|
||||
{
|
||||
dirRequestInfo.SetFallbackRequestBody(method, GetBodyString(null, parameterSetsByContentType.Value));
|
||||
}
|
||||
|
||||
dirRequestInfo.SetRequestBody(method, parameterSetsByContentType.Key, GetBodyString(parameterSetsByContentType.Key, parameterSetsByContentType.Value));
|
||||
}
|
||||
|
||||
dirRequestInfo.AddMethod(method);
|
||||
}
|
||||
|
||||
if (dirRequestInfo.Methods.Count > 0)
|
||||
{
|
||||
parent.RequestInfo = dirRequestInfo;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetBodyString(string contentType, IEnumerable<Parameter> operation)
|
||||
{
|
||||
Parameter body = operation.FirstOrDefault(x => string.Equals(x.Location, "body", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (body != null)
|
||||
{
|
||||
JToken result = GenerateData(body.Schema);
|
||||
return result?.ToString() ?? "{\n}";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static JToken GenerateData(Schema schema)
|
||||
{
|
||||
if (schema == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (schema.Example != null)
|
||||
{
|
||||
return JToken.FromObject(schema.Example);
|
||||
}
|
||||
|
||||
if (schema.Default != null)
|
||||
{
|
||||
return JToken.FromObject(schema.Default);
|
||||
}
|
||||
|
||||
if (schema.Type is null)
|
||||
{
|
||||
if (schema.Properties != null || schema.AdditionalProperties != null || schema.MinProperties.HasValue || schema.MaxProperties.HasValue)
|
||||
{
|
||||
schema.Type = "OBJECT";
|
||||
}
|
||||
else if (schema.Items != null || schema.MinItems.HasValue || schema.MaxItems.HasValue)
|
||||
{
|
||||
schema.Type = "ARRAY";
|
||||
}
|
||||
else if (schema.Minimum.HasValue || schema.Maximum.HasValue || schema.MultipleOf.HasValue)
|
||||
{
|
||||
schema.Type = "INTEGER";
|
||||
}
|
||||
}
|
||||
|
||||
switch (schema.Type?.ToUpperInvariant())
|
||||
{
|
||||
case null:
|
||||
case "STRING":
|
||||
return "";
|
||||
case "NUMBER":
|
||||
if (schema.Minimum.HasValue)
|
||||
{
|
||||
if (schema.Maximum.HasValue)
|
||||
{
|
||||
return (schema.Maximum.Value + schema.Minimum.Value) / 2;
|
||||
}
|
||||
|
||||
if (schema.ExclusiveMinimum)
|
||||
{
|
||||
return schema.Minimum.Value + 1;
|
||||
}
|
||||
|
||||
return schema.Minimum.Value;
|
||||
}
|
||||
return 1.1;
|
||||
case "INTEGER":
|
||||
if (schema.Minimum.HasValue)
|
||||
{
|
||||
if (schema.Maximum.HasValue)
|
||||
{
|
||||
return (int)((schema.Maximum.Value + schema.Minimum.Value) / 2);
|
||||
}
|
||||
|
||||
if (schema.ExclusiveMinimum)
|
||||
{
|
||||
return schema.Minimum.Value + 1;
|
||||
}
|
||||
|
||||
return schema.Minimum.Value;
|
||||
}
|
||||
return 0;
|
||||
case "BOOLEAN":
|
||||
return true;
|
||||
case "ARRAY":
|
||||
JArray container = new JArray();
|
||||
JToken item = GenerateData(schema.Items) ?? "";
|
||||
|
||||
int count = schema.MinItems.GetValueOrDefault();
|
||||
count = Math.Max(1, count);
|
||||
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
container.Add(item.DeepClone());
|
||||
}
|
||||
|
||||
return container;
|
||||
case "OBJECT":
|
||||
JObject obj = new JObject();
|
||||
foreach (KeyValuePair<string, Schema> property in schema.Properties)
|
||||
{
|
||||
JToken data = GenerateData(property.Value) ?? "";
|
||||
obj[property.Key] = data;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static async Task<IEnumerable<EndpointMetadata>> GetSwaggerDocAsync(HttpClient client, Uri uri)
|
||||
{
|
||||
var resp = await client.GetAsync(uri).ConfigureAwait(false);
|
||||
resp.EnsureSuccessStatusCode();
|
||||
string responseString = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
JsonSerializer serializer = new JsonSerializer{ PreserveReferencesHandling = PreserveReferencesHandling.All };
|
||||
JObject responseObject = (JObject)serializer.Deserialize(new StringReader(responseString), typeof(JObject));
|
||||
EndpointMetadataReader reader = new EndpointMetadataReader();
|
||||
responseObject = await PointerUtil.ResolvePointersAsync(uri, responseObject, client).ConfigureAwait(false) as JObject;
|
||||
|
||||
if (responseObject is null)
|
||||
{
|
||||
return new EndpointMetadata[0];
|
||||
}
|
||||
|
||||
return reader.Read(responseObject);
|
||||
}
|
||||
|
||||
public string GetHelpSummary(IShellState shellState, HttpState programState)
|
||||
{
|
||||
return Description;
|
||||
}
|
||||
|
||||
public string GetHelpDetails(IShellState shellState, HttpState programState, ICoreParseResult parseResult)
|
||||
{
|
||||
if (parseResult.Sections.Count > 1 && string.Equals(parseResult.Sections[0], Name, StringComparison.OrdinalIgnoreCase) && string.Equals(parseResult.Sections[1], SubCommand, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Description;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public IEnumerable<string> Suggest(IShellState shellState, HttpState programState, ICoreParseResult parseResult)
|
||||
{
|
||||
if (parseResult.Sections.Count == 0)
|
||||
{
|
||||
return new[] { Name };
|
||||
}
|
||||
|
||||
if (parseResult.Sections.Count > 0 && parseResult.SelectedSection == 0 && Name.StartsWith(parseResult.Sections[0].Substring(0, parseResult.CaretPositionWithinSelectedSection), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new[] { Name };
|
||||
}
|
||||
|
||||
if (string.Equals(Name, parseResult.Sections[0], StringComparison.OrdinalIgnoreCase) && parseResult.SelectedSection == 1 && (parseResult.Sections.Count < 2 || SubCommand.StartsWith(parseResult.Sections[1].Substring(0, parseResult.CaretPositionWithinSelectedSection), StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return new[] { SubCommand };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public bool? CanHandle(IShellState shellState, HttpState programState, ICoreParseResult parseResult)
|
||||
{
|
||||
return parseResult.Sections.Count > 1 && string.Equals(parseResult.Sections[0], Name, StringComparison.OrdinalIgnoreCase) && string.Equals(parseResult.Sections[1], SubCommand, StringComparison.OrdinalIgnoreCase)
|
||||
? (bool?)true
|
||||
: null;
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(IShellState shellState, HttpState programState, ICoreParseResult parseResult, CancellationToken cancellationToken)
|
||||
{
|
||||
if (parseResult.Sections.Count == 2)
|
||||
{
|
||||
programState.SwaggerStructure = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (parseResult.Sections.Count != 3 || string.IsNullOrEmpty(parseResult.Sections[2]) || !Uri.TryCreate(parseResult.Sections[2], UriKind.Absolute, out Uri serverUri))
|
||||
{
|
||||
shellState.ConsoleManager.Error.WriteLine("Must specify a swagger document".Bold().Red());
|
||||
}
|
||||
else
|
||||
{
|
||||
await CreateDirectoryStructureForSwaggerEndpointAsync(shellState, programState, serverUri, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
internal static async Task CreateDirectoryStructureForSwaggerEndpointAsync(IShellState shellState, HttpState programState, Uri serverUri, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
IEnumerable<EndpointMetadata> doc = await GetSwaggerDocAsync(programState.Client, serverUri).ConfigureAwait(false);
|
||||
|
||||
DirectoryStructure d = new DirectoryStructure(null);
|
||||
|
||||
foreach (EndpointMetadata entry in doc)
|
||||
{
|
||||
FillDirectoryInfo(d, entry);
|
||||
}
|
||||
|
||||
programState.SwaggerStructure = !cancellationToken.IsCancellationRequested ? d : null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
programState.SwaggerStructure = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.HttpRepl.Commands
|
||||
{
|
||||
public class TreeNode
|
||||
{
|
||||
private readonly int _depth;
|
||||
private readonly Formatter _formatter;
|
||||
private readonly string _prefix;
|
||||
private readonly string _entry;
|
||||
private readonly List<TreeNode> _children = new List<TreeNode>();
|
||||
|
||||
public TreeNode(Formatter formatter, string prefix, string entry)
|
||||
: this(formatter, prefix, entry, 0)
|
||||
{
|
||||
}
|
||||
|
||||
private TreeNode(Formatter formatter, string prefix, string entry, int depth)
|
||||
{
|
||||
_formatter = formatter;
|
||||
formatter.RegisterEntry(prefix.Length, depth);
|
||||
_prefix = prefix;
|
||||
_entry = entry;
|
||||
_depth = depth;
|
||||
}
|
||||
|
||||
public TreeNode AddChild(string prefix, string entry)
|
||||
{
|
||||
TreeNode child = new TreeNode(_formatter, prefix, entry, _depth + 1);
|
||||
_children.Add(child);
|
||||
return child;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
string self = _formatter.Format(_prefix, _entry, _depth);
|
||||
|
||||
if (_children.Count == 0)
|
||||
{
|
||||
return self;
|
||||
}
|
||||
|
||||
return self + Environment.NewLine + string.Join(Environment.NewLine, _children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Repl;
|
||||
using Microsoft.Repl.Commanding;
|
||||
using Microsoft.Repl.ConsoleHandling;
|
||||
using Microsoft.Repl.Parsing;
|
||||
|
||||
namespace Microsoft.HttpRepl.Commands
|
||||
{
|
||||
public class UICommand : ICommand<HttpState, ICoreParseResult>
|
||||
{
|
||||
private static readonly string Name = "ui";
|
||||
|
||||
public bool? CanHandle(IShellState shellState, HttpState programState, ICoreParseResult parseResult)
|
||||
{
|
||||
return parseResult.Sections.Count == 1 && string.Equals(parseResult.Sections[0], Name)
|
||||
? (bool?)true
|
||||
: null;
|
||||
}
|
||||
|
||||
public Task ExecuteAsync(IShellState shellState, HttpState programState, ICoreParseResult parseResult, CancellationToken cancellationToken)
|
||||
{
|
||||
if (programState.BaseAddress == null)
|
||||
{
|
||||
shellState.ConsoleManager.Error.WriteLine("Must be connected to a server to launch Swagger UI".Bold().Red());
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Uri uri = new Uri(programState.BaseAddress, "swagger");
|
||||
string agent = "cmd";
|
||||
string agentParam = $"/c {uri.AbsoluteUri}";
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
agent = "open";
|
||||
agentParam = uri.AbsoluteUri;
|
||||
}
|
||||
else if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
agent = "xdg-open";
|
||||
agentParam = uri.AbsoluteUri;
|
||||
}
|
||||
|
||||
Process.Start(new ProcessStartInfo(agent, agentParam) { CreateNoWindow = true });
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public string GetHelpDetails(IShellState shellState, HttpState programState, ICoreParseResult parseResult)
|
||||
{
|
||||
if (parseResult.Sections.Count == 1 && string.Equals(parseResult.Sections[0], Name))
|
||||
{
|
||||
return "ui - Launches the Swagger UI page (if available) in the default browser";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public string GetHelpSummary(IShellState shellState, HttpState programState)
|
||||
{
|
||||
return "ui - Launches the Swagger UI page (if available) in the default browser";
|
||||
}
|
||||
|
||||
public IEnumerable<string> Suggest(IShellState shellState, HttpState programState, ICoreParseResult parseResult)
|
||||
{
|
||||
if (parseResult.SelectedSection == 0 &&
|
||||
(string.IsNullOrEmpty(parseResult.Sections[parseResult.SelectedSection]) || Name.StartsWith(parseResult.Sections[0].Substring(0, parseResult.CaretPositionWithinSelectedSection), StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return new[] { Name };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
namespace Microsoft.HttpRepl.Diagnostics
|
||||
{
|
||||
public class ConfigItem
|
||||
{
|
||||
public string Key { get; set; }
|
||||
|
||||
public string Value { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
namespace Microsoft.HttpRepl.Diagnostics
|
||||
{
|
||||
public class DiagEndpoint
|
||||
{
|
||||
public string DisplayName { get; set; }
|
||||
public string Url { get; set; }
|
||||
public DiagEndpointMetadata[] Metadata { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
namespace Microsoft.HttpRepl.Diagnostics
|
||||
{
|
||||
public class DiagEndpointMetadata
|
||||
{
|
||||
public object Item { get; set; }
|
||||
public string Type { get; set; }
|
||||
public string[] Interfaces { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
namespace Microsoft.HttpRepl.Diagnostics
|
||||
{
|
||||
public class DiagItem
|
||||
{
|
||||
public string DisplayName { get; set; }
|
||||
|
||||
public string Description { get; set; }
|
||||
|
||||
public string Url { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.HttpRepl.Diagnostics
|
||||
{
|
||||
public class DiagnosticsState
|
||||
{
|
||||
public string DiagnosticsEndpoint { get; set; }
|
||||
|
||||
public IReadOnlyList<DiagItem> DiagnosticItems { get; internal set; }
|
||||
|
||||
public IDirectoryStructure DiagEndpointsStructure { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.HttpRepl
|
||||
{
|
||||
public class DirectoryStructure : IDirectoryStructure
|
||||
{
|
||||
private readonly Dictionary<string, DirectoryStructure> _childDirectories = new Dictionary<string, DirectoryStructure>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public DirectoryStructure(IDirectoryStructure parent)
|
||||
{
|
||||
Parent = parent;
|
||||
}
|
||||
|
||||
public IEnumerable<string> DirectoryNames => _childDirectories.Keys;
|
||||
|
||||
public IDirectoryStructure Parent { get; }
|
||||
|
||||
public DirectoryStructure DeclareDirectory(string name)
|
||||
{
|
||||
if (_childDirectories.TryGetValue(name, out DirectoryStructure existing))
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
return _childDirectories[name] = new DirectoryStructure(this);
|
||||
}
|
||||
|
||||
public IDirectoryStructure GetChildDirectory(string name)
|
||||
{
|
||||
if (_childDirectories.TryGetValue(name, out DirectoryStructure result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
IDirectoryStructure parameterizedTarget = _childDirectories.FirstOrDefault(x => x.Key.StartsWith('{') && x.Key.EndsWith('}')).Value;
|
||||
|
||||
if (!(parameterizedTarget is null))
|
||||
{
|
||||
return parameterizedTarget;
|
||||
}
|
||||
|
||||
return new DirectoryStructure(this);
|
||||
}
|
||||
|
||||
public IRequestInfo RequestInfo { get; set; }
|
||||
}
|
||||
|
||||
public class RequestInfo : IRequestInfo
|
||||
{
|
||||
private readonly HashSet<string> _methods = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, Dictionary<string, string>> _requestBodiesByMethodByContentType = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, string> _fallbackBodyStringsByMethod = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, IReadOnlyList<string>> _contentTypesByMethod = new Dictionary<string, IReadOnlyList<string>>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public IReadOnlyList<string> Methods => _methods.ToList();
|
||||
|
||||
public IReadOnlyDictionary<string, IReadOnlyList<string>> ContentTypesByMethod => _contentTypesByMethod;
|
||||
|
||||
public string GetRequestBodyForContentType(string contentType, string method)
|
||||
{
|
||||
if (_requestBodiesByMethodByContentType.TryGetValue(method, out Dictionary<string, string> bodiesByContentType)
|
||||
&& bodiesByContentType.TryGetValue(contentType, out string body))
|
||||
{
|
||||
return body;
|
||||
}
|
||||
|
||||
if (_fallbackBodyStringsByMethod.TryGetValue(method, out body))
|
||||
{
|
||||
return body;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void SetRequestBody(string method, string contentType, string body)
|
||||
{
|
||||
if (!_requestBodiesByMethodByContentType.TryGetValue(method, out Dictionary<string, string> bodiesByContentType))
|
||||
{
|
||||
_requestBodiesByMethodByContentType[method] = bodiesByContentType = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
if (!_contentTypesByMethod.TryGetValue(method, out IReadOnlyList<string> contentTypesRaw))
|
||||
{
|
||||
_contentTypesByMethod[method] = contentTypesRaw = new List<string>();
|
||||
}
|
||||
|
||||
List<string> contentTypes = (List<string>)contentTypesRaw;
|
||||
contentTypes.Add(contentType);
|
||||
|
||||
bodiesByContentType[contentType] = body;
|
||||
}
|
||||
|
||||
public void AddMethod(string method)
|
||||
{
|
||||
_methods.Add(method);
|
||||
}
|
||||
|
||||
public void SetFallbackRequestBody(string method, string fallbackBodyString)
|
||||
{
|
||||
_fallbackBodyStringsByMethod[method] = fallbackBodyString;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.HttpRepl
|
||||
{
|
||||
public static class DirectoryStructureExtensions
|
||||
{
|
||||
public static IEnumerable<string> GetDirectoryListingAtPath(this IDirectoryStructure structure, string path)
|
||||
{
|
||||
return structure.TraverseTo(path).DirectoryNames;
|
||||
}
|
||||
|
||||
public static IDirectoryStructure TraverseTo(this IDirectoryStructure structure, string path)
|
||||
{
|
||||
string[] parts = path.Replace('\\', '/').Split('/');
|
||||
return structure.TraverseTo(parts);
|
||||
}
|
||||
|
||||
public static IDirectoryStructure TraverseTo(this IDirectoryStructure structure, IEnumerable<string> pathParts)
|
||||
{
|
||||
IDirectoryStructure s = structure;
|
||||
IReadOnlyList<string> parts = pathParts.ToList();
|
||||
|
||||
if (parts.Count == 0)
|
||||
{
|
||||
return s;
|
||||
}
|
||||
|
||||
if (parts[0] == string.Empty && parts.Count > 1)
|
||||
{
|
||||
while (s.Parent != null)
|
||||
{
|
||||
s = s.Parent;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string part in parts)
|
||||
{
|
||||
if (part == ".")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (part == "..")
|
||||
{
|
||||
s = s.Parent ?? s;
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(part))
|
||||
{
|
||||
s = s.GetChildDirectory(part);
|
||||
}
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
using System.IO;
|
||||
using System.Text;
|
||||
using Microsoft.HttpRepl.Preferences;
|
||||
using Microsoft.Repl.ConsoleHandling;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Microsoft.HttpRepl.Formatting
|
||||
{
|
||||
public static class JsonVisitor
|
||||
{
|
||||
public static string FormatAndColorize(IJsonConfig config, string jsonData)
|
||||
{
|
||||
if (jsonData == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
JsonTextReader reader = new JsonTextReader(new StringReader(jsonData));
|
||||
bool isValuePosition = false;
|
||||
bool isTerminalValue = false;
|
||||
bool isFirstToken = true;
|
||||
|
||||
while (reader.Read())
|
||||
{
|
||||
if (!isValuePosition)
|
||||
{
|
||||
//If we're about to write an end object/array, we shouldn't have a comma
|
||||
if (reader.TokenType != JsonToken.EndArray && reader.TokenType != JsonToken.EndObject
|
||||
&& isTerminalValue)
|
||||
{
|
||||
result.Append(",".SetColor(config.CommaColor));
|
||||
}
|
||||
|
||||
if (!isFirstToken)
|
||||
{
|
||||
result.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
isFirstToken = false;
|
||||
|
||||
if (!isValuePosition)
|
||||
{
|
||||
result.Append("".PadLeft(reader.Depth * config.IndentSize));
|
||||
}
|
||||
|
||||
isTerminalValue = false;
|
||||
isValuePosition = false;
|
||||
JsonToken type = reader.TokenType;
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case JsonToken.StartObject:
|
||||
result.Append("{".SetColor(config.ObjectBraceColor));
|
||||
break;
|
||||
case JsonToken.EndObject:
|
||||
result.Append("}".SetColor(config.ObjectBraceColor));
|
||||
isTerminalValue = true;
|
||||
break;
|
||||
case JsonToken.StartArray:
|
||||
result.Append("[".SetColor(config.ArrayBraceColor));
|
||||
break;
|
||||
case JsonToken.EndArray:
|
||||
result.Append("]".SetColor(config.ArrayBraceColor));
|
||||
isTerminalValue = true;
|
||||
break;
|
||||
case JsonToken.PropertyName:
|
||||
result.Append((reader.QuoteChar.ToString() + reader.Value + reader.QuoteChar).SetColor(config.NameColor) + ": ".SetColor(config.NameSeparatorColor));
|
||||
isValuePosition = true;
|
||||
break;
|
||||
case JsonToken.Boolean:
|
||||
result.Append(reader.Value.ToString().ToLowerInvariant().SetColor(config.BoolColor));
|
||||
isTerminalValue = true;
|
||||
break;
|
||||
case JsonToken.Integer:
|
||||
case JsonToken.Float:
|
||||
result.Append(reader.Value.ToString().ToLowerInvariant().SetColor(config.NumericColor));
|
||||
isTerminalValue = true;
|
||||
break;
|
||||
case JsonToken.Null:
|
||||
result.Append("null".SetColor(config.NullColor));
|
||||
isTerminalValue = true;
|
||||
break;
|
||||
case JsonToken.Comment:
|
||||
result.Append(("//" + reader.Value).SetColor(config.NumericColor));
|
||||
break;
|
||||
case JsonToken.String:
|
||||
result.Append((reader.QuoteChar.ToString() + reader.Value + reader.QuoteChar.ToString()).SetColor(config.StringColor));
|
||||
isTerminalValue = true;
|
||||
break;
|
||||
case JsonToken.Raw:
|
||||
case JsonToken.Date:
|
||||
case JsonToken.Bytes:
|
||||
case JsonToken.Undefined:
|
||||
case JsonToken.None:
|
||||
result.Append(reader.Value.ToString().SetColor(config.DefaultColor));
|
||||
isTerminalValue = true;
|
||||
break;
|
||||
case JsonToken.EndConstructor:
|
||||
case JsonToken.StartConstructor:
|
||||
default:
|
||||
result.Append(reader.Value.ToString().SetColor(config.DefaultColor));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,370 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.HttpRepl.Diagnostics;
|
||||
using Microsoft.HttpRepl.Preferences;
|
||||
|
||||
namespace Microsoft.HttpRepl
|
||||
{
|
||||
public class HttpState
|
||||
{
|
||||
private string _userProfileDir;
|
||||
private string _prefsFilePath;
|
||||
|
||||
public HttpClient Client { get; }
|
||||
|
||||
public Stack<string> PathSections { get; }
|
||||
|
||||
public IDirectoryStructure SwaggerStructure { get; set; }
|
||||
|
||||
public IDirectoryStructure Structure => DiagnosticsState.DiagEndpointsStructure == null
|
||||
? SwaggerStructure
|
||||
: SwaggerStructure == null
|
||||
? DiagnosticsState.DiagEndpointsStructure
|
||||
: new AggregateDirectoryStructure(SwaggerStructure, DiagnosticsState.DiagEndpointsStructure);
|
||||
|
||||
public Uri BaseAddress { get; set; }
|
||||
|
||||
public bool EchoRequest { get; set; }
|
||||
|
||||
public Dictionary<string, string> Preferences { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, string> DefaultPreferences { get; }
|
||||
|
||||
public string UserProfileDir
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_userProfileDir == null)
|
||||
{
|
||||
bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||
|
||||
string profileDir = Environment.GetEnvironmentVariable(isWindows
|
||||
? "USERPROFILE"
|
||||
: "HOME");
|
||||
|
||||
_userProfileDir = profileDir;
|
||||
}
|
||||
|
||||
return _userProfileDir;
|
||||
}
|
||||
}
|
||||
|
||||
public string PrefsFilePath => _prefsFilePath ?? (_prefsFilePath = Path.Combine(UserProfileDir, ".httpreplprefs"));
|
||||
|
||||
public Dictionary<string, IEnumerable<string>> Headers { get; }
|
||||
|
||||
public DiagnosticsState DiagnosticsState { get; }
|
||||
|
||||
public HttpState()
|
||||
{
|
||||
Client = new HttpClient();
|
||||
PathSections = new Stack<string>();
|
||||
Preferences = new Dictionary<string, string>();
|
||||
DefaultPreferences = CreateDefaultPreferencs();
|
||||
Headers = new Dictionary<string, IEnumerable<string>>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "User-Agent", new[] { "HTTP-REPL" } }
|
||||
};
|
||||
Preferences = new Dictionary<string, string>(DefaultPreferences);
|
||||
LoadPreferences();
|
||||
DiagnosticsState = new DiagnosticsState();
|
||||
}
|
||||
|
||||
public string GetPrompt()
|
||||
{
|
||||
return $"{GetEffectivePath(new string[0], false, out int _)?.ToString() ?? "(Disconnected)"}~ ";
|
||||
}
|
||||
|
||||
private void LoadPreferences()
|
||||
{
|
||||
if (File.Exists(PrefsFilePath))
|
||||
{
|
||||
string[] prefsFile = File.ReadAllLines(PrefsFilePath);
|
||||
|
||||
foreach (string line in prefsFile)
|
||||
{
|
||||
int equalsIndex = line.IndexOf('=');
|
||||
|
||||
if (equalsIndex < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Preferences[line.Substring(0, equalsIndex)] = line.Substring(equalsIndex + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IReadOnlyDictionary<string, string> CreateDefaultPreferencs()
|
||||
{
|
||||
return new Dictionary<string, string>
|
||||
{
|
||||
{ WellKnownPreference.ProtocolColor, "BoldGreen" },
|
||||
{ WellKnownPreference.StatusColor, "BoldYellow" },
|
||||
|
||||
{ WellKnownPreference.JsonArrayBraceColor, "BoldCyan" },
|
||||
{ WellKnownPreference.JsonCommaColor, "BoldYellow" },
|
||||
{ WellKnownPreference.JsonNameColor, "BoldMagenta" },
|
||||
{ WellKnownPreference.JsonNameSeparatorColor, "BoldWhite" },
|
||||
{ WellKnownPreference.JsonObjectBraceColor, "Cyan" },
|
||||
{ WellKnownPreference.JsonColor, "Green" }
|
||||
};
|
||||
}
|
||||
|
||||
public bool SavePreferences()
|
||||
{
|
||||
List<string> lines = new List<string>();
|
||||
foreach (KeyValuePair<string, string> entry in Preferences.OrderBy(x => x.Key))
|
||||
{
|
||||
//If the value didn't exist in the defaults or the value's different, include it in the user's preferences file
|
||||
if (!DefaultPreferences.TryGetValue(entry.Key, out string value) || !string.Equals(value, entry.Value, StringComparison.Ordinal))
|
||||
{
|
||||
lines.Add($"{entry.Key}={entry.Value}");
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllLines(PrefsFilePath, lines);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public string GetExampleBody(string path, string contentType, string method)
|
||||
{
|
||||
Uri effectivePath = GetEffectivePath(path);
|
||||
string rootRelativePath = effectivePath.LocalPath.Substring(BaseAddress.LocalPath.Length).TrimStart('/');
|
||||
IDirectoryStructure structure = SwaggerStructure?.TraverseTo(rootRelativePath);
|
||||
return structure?.RequestInfo?.GetRequestBodyForContentType(contentType, method);
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetApplicableContentTypes(string method, string path)
|
||||
{
|
||||
Uri effectivePath = GetEffectivePath(path);
|
||||
string rootRelativePath = effectivePath.LocalPath.Substring(BaseAddress.LocalPath.Length).TrimStart('/');
|
||||
IDirectoryStructure structure = SwaggerStructure?.TraverseTo(rootRelativePath);
|
||||
IReadOnlyDictionary<string, IReadOnlyList<string>> contentTypesByMethod = structure?.RequestInfo?.ContentTypesByMethod;
|
||||
|
||||
if (contentTypesByMethod != null)
|
||||
{
|
||||
if (method is null)
|
||||
{
|
||||
return contentTypesByMethod.Values.SelectMany(x => x).Distinct(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
if (contentTypesByMethod.TryGetValue(method, out IReadOnlyList<string> contentTypes))
|
||||
{
|
||||
return contentTypes;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Uri GetEffectivePath(string commandSpecifiedPath)
|
||||
{
|
||||
if (Uri.TryCreate(commandSpecifiedPath, UriKind.Absolute, out Uri absoluteUri))
|
||||
{
|
||||
return absoluteUri;
|
||||
}
|
||||
|
||||
UriBuilder builder = new UriBuilder(BaseAddress);
|
||||
string path = string.Join('/', PathSections.Reverse());
|
||||
string[] parts = path.Split('?');
|
||||
string query = null;
|
||||
string query2 = null;
|
||||
|
||||
if (parts.Length > 1)
|
||||
{
|
||||
path = parts[0];
|
||||
query = string.Join('?', parts.Skip(1));
|
||||
}
|
||||
|
||||
builder.Path += path;
|
||||
|
||||
if (commandSpecifiedPath.Length > 0)
|
||||
{
|
||||
if (commandSpecifiedPath[0] != '/')
|
||||
{
|
||||
string argPath = commandSpecifiedPath;
|
||||
if (builder.Path.Length > 0 && builder.Path[builder.Path.Length - 1] != '/')
|
||||
{
|
||||
argPath = "/" + argPath;
|
||||
}
|
||||
|
||||
int queryIndex = argPath.IndexOf('?');
|
||||
path = argPath;
|
||||
|
||||
if (queryIndex > -1)
|
||||
{
|
||||
query2 = argPath.Substring(queryIndex + 1);
|
||||
path = argPath.Substring(0, queryIndex);
|
||||
}
|
||||
|
||||
builder.Path += path;
|
||||
}
|
||||
else
|
||||
{
|
||||
int queryIndex = commandSpecifiedPath.IndexOf('?');
|
||||
path = commandSpecifiedPath;
|
||||
|
||||
if (queryIndex > -1)
|
||||
{
|
||||
query2 = commandSpecifiedPath.Substring(queryIndex + 1);
|
||||
path = commandSpecifiedPath.Substring(0, queryIndex);
|
||||
}
|
||||
|
||||
builder.Path = path;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
int queryIndex = commandSpecifiedPath.IndexOf('?');
|
||||
path = commandSpecifiedPath;
|
||||
|
||||
if (queryIndex > -1)
|
||||
{
|
||||
query2 = commandSpecifiedPath.Substring(queryIndex + 1);
|
||||
path = commandSpecifiedPath.Substring(0, queryIndex);
|
||||
}
|
||||
|
||||
builder.Path += path;
|
||||
}
|
||||
|
||||
if (query != null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(builder.Query))
|
||||
{
|
||||
query = "&" + query;
|
||||
}
|
||||
|
||||
builder.Query += query;
|
||||
}
|
||||
|
||||
if (query2 != null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(builder.Query))
|
||||
{
|
||||
query2 = "&" + query2;
|
||||
}
|
||||
|
||||
builder.Query += query2;
|
||||
}
|
||||
|
||||
return builder.Uri;
|
||||
}
|
||||
|
||||
public Uri GetEffectivePath(IReadOnlyList<string> sections, bool requiresBody, out int filePathIndex)
|
||||
{
|
||||
filePathIndex = 1;
|
||||
|
||||
if (BaseAddress == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
UriBuilder builder = new UriBuilder(BaseAddress);
|
||||
string path = string.Join('/', PathSections.Reverse());
|
||||
string[] parts = path.Split('?');
|
||||
string query = null;
|
||||
string query2 = null;
|
||||
|
||||
if (parts.Length > 1)
|
||||
{
|
||||
path = parts[0];
|
||||
query = string.Join('?', parts.Skip(1));
|
||||
}
|
||||
|
||||
builder.Path += path;
|
||||
|
||||
if (sections.Count > 1)
|
||||
{
|
||||
if (!requiresBody || !File.Exists(sections[1]))
|
||||
{
|
||||
if (sections[1].Length > 0)
|
||||
{
|
||||
if (sections[1][0] != '/')
|
||||
{
|
||||
string argPath = sections[1];
|
||||
if (builder.Path.Length > 0 && builder.Path[builder.Path.Length - 1] != '/')
|
||||
{
|
||||
argPath = "/" + argPath;
|
||||
}
|
||||
|
||||
int queryIndex = argPath.IndexOf('?');
|
||||
path = argPath;
|
||||
|
||||
if (queryIndex > -1)
|
||||
{
|
||||
query2 = argPath.Substring(queryIndex + 1);
|
||||
path = argPath.Substring(0, queryIndex);
|
||||
}
|
||||
|
||||
builder.Path += path;
|
||||
}
|
||||
else
|
||||
{
|
||||
int queryIndex = sections[1].IndexOf('?');
|
||||
path = sections[1];
|
||||
|
||||
if (queryIndex > -1)
|
||||
{
|
||||
query2 = sections[1].Substring(queryIndex + 1);
|
||||
path = sections[1].Substring(0, queryIndex);
|
||||
}
|
||||
|
||||
builder.Path = path;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
int queryIndex = sections[1].IndexOf('?');
|
||||
path = sections[1];
|
||||
|
||||
if (queryIndex > -1)
|
||||
{
|
||||
query2 = sections[1].Substring(queryIndex + 1);
|
||||
path = sections[1].Substring(0, queryIndex);
|
||||
}
|
||||
|
||||
builder.Path += path;
|
||||
}
|
||||
|
||||
filePathIndex = 2;
|
||||
}
|
||||
}
|
||||
|
||||
if (query != null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(builder.Query))
|
||||
{
|
||||
query = "&" + query;
|
||||
}
|
||||
|
||||
builder.Query += query;
|
||||
}
|
||||
|
||||
if (query2 != null)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(builder.Query))
|
||||
{
|
||||
query2 = "&" + query2;
|
||||
}
|
||||
|
||||
builder.Query += query2;
|
||||
}
|
||||
|
||||
return builder.Uri;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.HttpRepl
|
||||
{
|
||||
public interface IDirectoryStructure
|
||||
{
|
||||
IEnumerable<string> DirectoryNames { get; }
|
||||
|
||||
IDirectoryStructure Parent { get; }
|
||||
|
||||
IDirectoryStructure GetChildDirectory(string name);
|
||||
|
||||
IRequestInfo RequestInfo { get; }
|
||||
}
|
||||
|
||||
public interface IRequestInfo
|
||||
{
|
||||
IReadOnlyDictionary<string, IReadOnlyList<string>> ContentTypesByMethod { get; }
|
||||
|
||||
IReadOnlyList<string> Methods { get; }
|
||||
|
||||
string GetRequestBodyForContentType(string contentType, string method);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk;Microsoft.DotNet.GlobalTools.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.2</TargetFramework>
|
||||
<PackAsTool>true</PackAsTool>
|
||||
<AssemblyName>dotnet-httprepl</AssemblyName>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Description>Command line tool to for making HTTP calls and viewing their results.</Description>
|
||||
<PackageTags>dotnet;http;httprepl</PackageTags>
|
||||
|
||||
<!-- This is a requirement for Microsoft tool packages only. -->
|
||||
<GenerateToolShims>true</GenerateToolShims>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="$(MicrosoftWebApiClientPackageVersion)" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Microsoft.Repl\Microsoft.Repl.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<SignedPackageFile Include="tools/$(TargetFramework)/any/Microsoft.Repl.dll" Certificate="$(AssemblySigningCertName)" />
|
||||
<SignedPackageFile Include="tools/$(TargetFramework)/any/Newtonsoft.Json.dll" Certificate="$(AssemblySigningCertName)" />
|
||||
<SignedPackageFile Include="tools/$(TargetFramework)/any/Newtonsoft.Json.Bson.dll" Certificate="$(AssemblySigningCertName)" />
|
||||
<SignedPackageFile Include="tools/$(TargetFramework)/any/System.Net.Http.Formatting.dll" Certificate="$(AssemblySigningCertName)" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
namespace Microsoft.HttpRepl.OpenApi
|
||||
{
|
||||
public class Either<TOption1, TOption2>
|
||||
{
|
||||
public Either(TOption1 option1)
|
||||
{
|
||||
Option1 = option1;
|
||||
IsOption1 = true;
|
||||
}
|
||||
|
||||
public Either(TOption2 option2)
|
||||
{
|
||||
Option2 = option2;
|
||||
IsOption1 = false;
|
||||
}
|
||||
|
||||
public bool IsOption1 { get; }
|
||||
|
||||
public TOption1 Option1 { get; }
|
||||
|
||||
public TOption2 Option2 { get; }
|
||||
|
||||
public static implicit operator Either<TOption1, TOption2>(TOption1 value)
|
||||
{
|
||||
return new Either<TOption1, TOption2>(value);
|
||||
}
|
||||
|
||||
public static implicit operator Either<TOption1, TOption2>(TOption2 value)
|
||||
{
|
||||
return new Either<TOption1, TOption2>(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
using System;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Microsoft.HttpRepl.OpenApi
|
||||
{
|
||||
public class EitherConverter<TOption1, TOption2> : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
{
|
||||
return typeof(TOption1).IsAssignableFrom(objectType) || typeof(TOption2).IsAssignableFrom(objectType) || typeof(EitherConverter<TOption1, TOption2>) == objectType;
|
||||
}
|
||||
|
||||
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
|
||||
{
|
||||
try
|
||||
{
|
||||
TOption1 option1 = serializer.Deserialize<TOption1>(reader);
|
||||
return new Either<TOption1, TOption2>(option1);
|
||||
}
|
||||
catch
|
||||
{
|
||||
TOption2 option2 = serializer.Deserialize<TOption2>(reader);
|
||||
return new Either<TOption1, TOption2>(option2);
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.HttpRepl.OpenApi
|
||||
{
|
||||
public class EndpointMetadata
|
||||
{
|
||||
public EndpointMetadata(string path, IReadOnlyDictionary<string, IReadOnlyDictionary<string, IReadOnlyList<Parameter>>> requestsByMethodAndContentType)
|
||||
{
|
||||
Path = path;
|
||||
AvailableRequests = requestsByMethodAndContentType ?? new Dictionary<string, IReadOnlyDictionary<string, IReadOnlyList<Parameter>>>();
|
||||
}
|
||||
|
||||
public string Path { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, IReadOnlyDictionary<string, IReadOnlyList<Parameter>>> AvailableRequests { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Microsoft.HttpRepl.OpenApi
|
||||
{
|
||||
public class EndpointMetadataReader
|
||||
{
|
||||
private readonly List<IEndpointMetadataReader> _readers = new List<IEndpointMetadataReader>
|
||||
{
|
||||
new OpenApiV3EndpointMetadataReader(),
|
||||
new SwaggerV2EndpointMetadataReader(),
|
||||
new SwaggerV1EndpointMetadataReader()
|
||||
};
|
||||
|
||||
public void RegisterReader(IEndpointMetadataReader reader)
|
||||
{
|
||||
_readers.Add(reader);
|
||||
}
|
||||
|
||||
public IEnumerable<EndpointMetadata> Read(JObject document)
|
||||
{
|
||||
foreach (IEndpointMetadataReader reader in _readers)
|
||||
{
|
||||
if (reader.CanHandle(document))
|
||||
{
|
||||
IEnumerable<EndpointMetadata> result = reader.ReadMetadata(document);
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Microsoft.HttpRepl.OpenApi
|
||||
{
|
||||
public interface IEndpointMetadataReader
|
||||
{
|
||||
bool CanHandle(JObject document);
|
||||
|
||||
IEnumerable<EndpointMetadata> ReadMetadata(JObject document);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Microsoft.HttpRepl.OpenApi
|
||||
{
|
||||
public class OpenApiV3EndpointMetadataReader : IEndpointMetadataReader
|
||||
{
|
||||
public bool CanHandle(JObject document)
|
||||
{
|
||||
return (document["openapi"]?.ToString() ?? "").StartsWith("3.", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public IEnumerable<EndpointMetadata> ReadMetadata(JObject document)
|
||||
{
|
||||
List<EndpointMetadata> metadata = new List<EndpointMetadata>();
|
||||
|
||||
if (document["paths"] is JObject paths)
|
||||
{
|
||||
foreach (JProperty path in paths.Properties())
|
||||
{
|
||||
if (!(path.Value is JObject pathBody))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Dictionary<string, IReadOnlyDictionary<string, IReadOnlyList<Parameter>>> requestMethods = new Dictionary<string, IReadOnlyDictionary<string, IReadOnlyList<Parameter>>>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (JProperty method in pathBody.Properties())
|
||||
{
|
||||
List<Parameter> parameters = new List<Parameter>();
|
||||
|
||||
if (method.Value is JObject methodBody)
|
||||
{
|
||||
if (methodBody["parameters"] is JArray parametersArray)
|
||||
{
|
||||
foreach (JObject parameterObj in parametersArray.OfType<JObject>())
|
||||
{
|
||||
Parameter p = parameterObj.ToObject<Parameter>();
|
||||
p.Location = parameterObj["in"]?.ToString();
|
||||
|
||||
if (!(parameterObj["schema"] is JObject schemaObject))
|
||||
{
|
||||
schemaObject = null;
|
||||
}
|
||||
|
||||
p.Schema = schemaObject?.ToObject<Schema>() ?? parameterObj.ToObject<Schema>();
|
||||
parameters.Add(p);
|
||||
}
|
||||
}
|
||||
|
||||
if (methodBody["requestBody"] is JObject bodyObject)
|
||||
{
|
||||
if (!(bodyObject["content"] is JObject contentTypeLookup))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (JProperty contentTypeEntry in contentTypeLookup.Properties())
|
||||
{
|
||||
List<Parameter> parametersByContentType = new List<Parameter>(parameters);
|
||||
Parameter p = bodyObject.ToObject<Parameter>();
|
||||
p.Location = "body";
|
||||
p.IsRequired = bodyObject["required"]?.ToObject<bool>() ?? false;
|
||||
|
||||
if (!(bodyObject["schema"] is JObject schemaObject))
|
||||
{
|
||||
schemaObject = null;
|
||||
}
|
||||
|
||||
p.Schema = schemaObject?.ToObject<Schema>() ?? bodyObject.ToObject<Schema>();
|
||||
parametersByContentType.Add(p);
|
||||
|
||||
Dictionary<string, IReadOnlyList<Parameter>> bucketByMethod;
|
||||
if (!requestMethods.TryGetValue(method.Name, out IReadOnlyDictionary<string, IReadOnlyList<Parameter>> bucketByMethodRaw))
|
||||
{
|
||||
requestMethods[method.Name] = bucketByMethodRaw = new Dictionary<string, IReadOnlyList<Parameter>>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "", parametersByContentType }
|
||||
};
|
||||
}
|
||||
|
||||
bucketByMethod = (Dictionary<string, IReadOnlyList<Parameter>>)bucketByMethodRaw;
|
||||
bucketByMethod[contentTypeEntry.Name] = parametersByContentType;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
metadata.Add(new EndpointMetadata(path.Name, requestMethods));
|
||||
}
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
namespace Microsoft.HttpRepl.OpenApi
|
||||
{
|
||||
public class Parameter
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
||||
public string Location { get; set; }
|
||||
|
||||
public bool IsRequired { get; set; }
|
||||
|
||||
public Schema Schema { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,218 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Microsoft.HttpRepl.OpenApi
|
||||
{
|
||||
public static class PointerUtil
|
||||
{
|
||||
public static Task<JToken> ResolvePointersAsync(Uri loadLocation, JToken root, HttpClient client)
|
||||
{
|
||||
return ResolvePointersAsync(loadLocation, root, root, client);
|
||||
}
|
||||
|
||||
private static async Task<JToken> ResolvePointersAsync(Uri loadLocation, JToken root, JToken toResolve, HttpClient client)
|
||||
{
|
||||
if (toResolve is JArray arr)
|
||||
{
|
||||
for (int i = 0; i < arr.Count; ++i)
|
||||
{
|
||||
arr[i] = await ResolvePointersAsync(loadLocation, root, arr[i], client).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else if (toResolve is JObject obj)
|
||||
{
|
||||
if (obj["$ref"] is JValue refVal && refVal.Type == JTokenType.String)
|
||||
{
|
||||
if (!Uri.TryCreate((string)refVal.Value, UriKind.RelativeOrAbsolute, out Uri loadTarget))
|
||||
{
|
||||
//TODO: Error resolving pointer (pointer must be a valid URI)
|
||||
return new JValue((object)null);
|
||||
}
|
||||
|
||||
if (!loadTarget.IsAbsoluteUri)
|
||||
{
|
||||
if (!Uri.TryCreate(loadLocation, loadTarget, out loadTarget))
|
||||
{
|
||||
//TODO: Error resolving pointer (could not combine with base path)
|
||||
return new JValue((object)null);
|
||||
}
|
||||
}
|
||||
|
||||
//Check to see if we're changing source documents, if we are, get it
|
||||
if (!string.Equals(loadLocation.Host, loadTarget.Host, StringComparison.OrdinalIgnoreCase) || !string.Equals(loadLocation.AbsolutePath, loadTarget.AbsolutePath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
HttpResponseMessage responseMessage = await client.GetAsync(loadTarget).ConfigureAwait(false);
|
||||
|
||||
if (!responseMessage.IsSuccessStatusCode)
|
||||
{
|
||||
//TODO: Error resolving pointer (could not get referenced document)
|
||||
return new JValue((object)null);
|
||||
}
|
||||
|
||||
string responseString = await responseMessage.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
JToken newRoot;
|
||||
|
||||
try
|
||||
{
|
||||
newRoot = JToken.Parse(responseString);
|
||||
}
|
||||
catch
|
||||
{
|
||||
//TODO: Error resolving pointer (referenced document is not valid JSON)
|
||||
return new JValue((object)null);
|
||||
}
|
||||
|
||||
return await ResolvePointersAsync(loadTarget, newRoot, newRoot, client).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
//We're in the right document, grab the bookmark (fragment) of the URI and get the element at that path
|
||||
string fragment = loadTarget.Fragment;
|
||||
|
||||
if (fragment.StartsWith('#'))
|
||||
{
|
||||
fragment = fragment.Substring(1);
|
||||
}
|
||||
|
||||
string[] parts = fragment.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
JToken cursor = root;
|
||||
|
||||
for (int i = 0; i < parts.Length; ++i)
|
||||
{
|
||||
if (cursor is JArray ca)
|
||||
{
|
||||
if (!int.TryParse(parts[i], NumberStyles.Integer, CultureInfo.InvariantCulture, out int index))
|
||||
{
|
||||
//TODO: Error resolving pointer, array index is non-integral
|
||||
return new JValue((object)null);
|
||||
}
|
||||
|
||||
if (index < 0 || index >= ca.Count)
|
||||
{
|
||||
//TODO: Error resolving pointer, array index is out of bounds
|
||||
return new JValue((object)null);
|
||||
}
|
||||
|
||||
JToken val = ca[index];
|
||||
if (val is JObject vo && vo.TryGetValue("$ref", out JToken vor) && vor is JValue vorv && vorv.Type == JTokenType.String)
|
||||
{
|
||||
cursor = await ResolvePointersAsync(loadLocation, root, val, client).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
cursor = val;
|
||||
}
|
||||
}
|
||||
else if (cursor is JObject co)
|
||||
{
|
||||
if (!co.TryGetValue(parts[i], out JToken val))
|
||||
{
|
||||
//TODO: Error resolving pointer, no such property on object
|
||||
return new JValue((object)null);
|
||||
}
|
||||
|
||||
if (val is JObject vo && vo.TryGetValue("$ref", out JToken vor) && vor is JValue vorv && vorv.Type == JTokenType.String)
|
||||
{
|
||||
cursor = await ResolvePointersAsync(loadLocation, root, val, client).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
cursor = val;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//TODO: Error resolving pointer, cannot index into literal
|
||||
return new JValue((object)null);
|
||||
}
|
||||
}
|
||||
|
||||
cursor = await ResolvePointersAsync(loadLocation, root, cursor, client);
|
||||
return cursor.DeepClone();
|
||||
}
|
||||
|
||||
foreach (JProperty property in obj.Properties().ToList())
|
||||
{
|
||||
obj[property.Name] = await ResolvePointersAsync(loadLocation, root, property.Value, client).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return toResolve;
|
||||
}
|
||||
|
||||
//public static async Task<JToken> 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;
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Microsoft.HttpRepl.OpenApi
|
||||
{
|
||||
public class Schema
|
||||
{
|
||||
public void PrepareForUsage(JToken document)
|
||||
{
|
||||
AdditionalProperties?.Option1?.PrepareForUsage(document);
|
||||
|
||||
if (AllOf != null)
|
||||
{
|
||||
for (int i = 0; i < AllOf.Length; ++i)
|
||||
{
|
||||
AllOf[i].PrepareForUsage(document);
|
||||
}
|
||||
}
|
||||
|
||||
if (AnyOf != null)
|
||||
{
|
||||
for (int i = 0; i < AnyOf.Length; ++i)
|
||||
{
|
||||
AnyOf[i].PrepareForUsage(document);
|
||||
}
|
||||
}
|
||||
|
||||
if (OneOf != null)
|
||||
{
|
||||
for (int i = 0; i < OneOf.Length; ++i)
|
||||
{
|
||||
OneOf[i].PrepareForUsage(document);
|
||||
}
|
||||
}
|
||||
|
||||
if (Properties != null)
|
||||
{
|
||||
IReadOnlyList<string> keys = Properties.Keys.ToList();
|
||||
for (int i = 0; i < keys.Count; ++i)
|
||||
{
|
||||
Properties[keys[i]]?.PrepareForUsage(document);
|
||||
}
|
||||
}
|
||||
|
||||
Items?.PrepareForUsage(document);
|
||||
Not?.PrepareForUsage(document);
|
||||
|
||||
if (Required?.Option1 != null)
|
||||
{
|
||||
if (Properties != null)
|
||||
{
|
||||
foreach (string propertyName in Required.Option1)
|
||||
{
|
||||
if (Properties.TryGetValue(propertyName, out Schema value))
|
||||
{
|
||||
value.Required = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Required = false;
|
||||
}
|
||||
}
|
||||
|
||||
[JsonConverter(typeof(EitherConverter<Schema, bool>))]
|
||||
public Either<Schema, bool> AdditionalProperties { get; set; }
|
||||
|
||||
public Schema[] AllOf { get; set; }
|
||||
|
||||
public Schema[] AnyOf { get; set; }
|
||||
|
||||
public object Default { get; set; }
|
||||
|
||||
public string Description { get; set; }
|
||||
|
||||
public object[] Enum { get; set; }
|
||||
|
||||
public object Example { get; set; }
|
||||
|
||||
public bool ExclusiveMaximum { get; set; }
|
||||
|
||||
public bool ExclusiveMinimum { get; set; }
|
||||
|
||||
public string Format { get; set; }
|
||||
|
||||
public Schema Items { get; set; }
|
||||
|
||||
public double? Maximum { get; set; }
|
||||
|
||||
public double? Minimum { get; set; }
|
||||
|
||||
public int? MaxItems { get; set; }
|
||||
|
||||
public int? MinItems { get; set; }
|
||||
|
||||
public int? MaxLength { get; set; }
|
||||
|
||||
public int? MinLength { get; set; }
|
||||
|
||||
public int? MaxProperties { get; set; }
|
||||
|
||||
public int? MinProperties { get; set; }
|
||||
|
||||
public double? MultipleOf { get; set; }
|
||||
|
||||
public Schema Not { get; set; }
|
||||
|
||||
public Schema[] OneOf { get; set; }
|
||||
|
||||
public string Pattern { get; set; }
|
||||
|
||||
public Dictionary<string, Schema> Properties { get; set; }
|
||||
|
||||
[JsonConverter(typeof(EitherConverter<string[], bool>))]
|
||||
public Either<string[], bool> Required { get; set; }
|
||||
|
||||
public string Title { get; set; }
|
||||
|
||||
public string Type { get; set; }
|
||||
|
||||
public bool UniqueItems { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Microsoft.HttpRepl.OpenApi
|
||||
{
|
||||
public class SwaggerV1EndpointMetadataReader : IEndpointMetadataReader
|
||||
{
|
||||
public bool CanHandle(JObject document)
|
||||
{
|
||||
return (document["swaggerVersion"]?.ToString() ?? "").StartsWith("1.", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public IEnumerable<EndpointMetadata> ReadMetadata(JObject document)
|
||||
{
|
||||
List<EndpointMetadata> metadata = new List<EndpointMetadata>();
|
||||
|
||||
if (!(document["consumes"] is JArray globalConsumes))
|
||||
{
|
||||
globalConsumes = new JArray();
|
||||
}
|
||||
|
||||
if (document["apis"] is JObject obj)
|
||||
{
|
||||
foreach (JProperty property in obj.Properties())
|
||||
{
|
||||
string path = obj["path"]?.ToString();
|
||||
|
||||
if (path is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Dictionary<string, IReadOnlyDictionary<string, IReadOnlyList<Parameter>>> requestMethods = new Dictionary<string, IReadOnlyDictionary<string, IReadOnlyList<Parameter>>>(StringComparer.Ordinal);
|
||||
|
||||
if (obj["operations"] is JArray operations)
|
||||
{
|
||||
foreach (JObject operationObject in operations.OfType<JObject>())
|
||||
{
|
||||
string method = operationObject["method"]?.ToString();
|
||||
List<Parameter> parameters = new List<Parameter>();
|
||||
|
||||
if (operationObject["parameters"] is JArray parametersArray)
|
||||
{
|
||||
foreach (JObject parameterObj in parametersArray.OfType<JObject>())
|
||||
{
|
||||
Parameter p = parameterObj.ToObject<Parameter>();
|
||||
p.Location = parameterObj["paramType"]?.ToString();
|
||||
p.IsRequired = parameterObj["required"]?.ToObject<bool>() ?? false;
|
||||
|
||||
string type = parameterObj["type"]?.ToString();
|
||||
|
||||
if (type is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (type.ToUpperInvariant())
|
||||
{
|
||||
case "INTEGER":
|
||||
case "NUMBER":
|
||||
case "STRING":
|
||||
case "BOOLEAN":
|
||||
p.Schema = new Schema { Type = type };
|
||||
break;
|
||||
case "FILE":
|
||||
break;
|
||||
default:
|
||||
if (document["models"]?[type] is JObject schemaObject)
|
||||
{
|
||||
//TODO: Handle subtypes (https://github.com/OAI/OpenAPI-Specification/blob/master/versions/1.2.md#527-model-object)
|
||||
p.Schema = schemaObject.ToObject<Schema>();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
parameters.Add(p);
|
||||
}
|
||||
}
|
||||
|
||||
if (!(operationObject["consumes"] is JArray consumes))
|
||||
{
|
||||
consumes = globalConsumes;
|
||||
}
|
||||
|
||||
Dictionary<string, IReadOnlyList<Parameter>> parametersByContentType = new Dictionary<string, IReadOnlyList<Parameter>>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "", parameters }
|
||||
};
|
||||
|
||||
foreach (JValue value in consumes.OfType<JValue>().Where(x => x.Type == JTokenType.String))
|
||||
{
|
||||
parametersByContentType[value.ToString()] = parameters;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
metadata.Add(new EndpointMetadata(path, requestMethods));
|
||||
}
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace Microsoft.HttpRepl.OpenApi
|
||||
{
|
||||
public class SwaggerV2EndpointMetadataReader : IEndpointMetadataReader
|
||||
{
|
||||
public bool CanHandle(JObject document)
|
||||
{
|
||||
return (document["swagger"]?.ToString() ?? "").StartsWith("2.", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public IEnumerable<EndpointMetadata> ReadMetadata(JObject document)
|
||||
{
|
||||
List<EndpointMetadata> metadata = new List<EndpointMetadata>();
|
||||
|
||||
if (!(document["consumes"] is JArray globalConsumes))
|
||||
{
|
||||
globalConsumes = new JArray();
|
||||
}
|
||||
|
||||
if (document["paths"] is JObject obj)
|
||||
{
|
||||
foreach (JProperty property in obj.Properties())
|
||||
{
|
||||
if (!(property.Value is JObject requestMethodInfos))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Dictionary<string, IReadOnlyDictionary<string, IReadOnlyList<Parameter>>> requestMethods = new Dictionary<string, IReadOnlyDictionary<string, IReadOnlyList<Parameter>>>(StringComparer.Ordinal);
|
||||
|
||||
foreach (JProperty methodInfo in requestMethodInfos.Properties())
|
||||
{
|
||||
List<Parameter> parameters = new List<Parameter>();
|
||||
|
||||
if (methodInfo.Value is JObject methodInfoDescription)
|
||||
{
|
||||
if (methodInfoDescription["parameters"] is JArray parametersArray)
|
||||
{
|
||||
foreach (JObject parameterObj in parametersArray.OfType<JObject>())
|
||||
{
|
||||
//TODO: Resolve refs here
|
||||
|
||||
Parameter p = parameterObj.ToObject<Parameter>();
|
||||
p.Location = parameterObj["in"]?.ToString();
|
||||
p.IsRequired = parameterObj["required"]?.ToObject<bool>() ?? false;
|
||||
|
||||
if (!(parameterObj["schema"] is JObject schemaObject))
|
||||
{
|
||||
schemaObject = null;
|
||||
}
|
||||
|
||||
p.Schema = schemaObject?.ToObject<Schema>() ?? parameterObj.ToObject<Schema>();
|
||||
parameters.Add(p);
|
||||
}
|
||||
}
|
||||
|
||||
if (!(methodInfoDescription["consumes"] is JArray consumes))
|
||||
{
|
||||
consumes = globalConsumes;
|
||||
}
|
||||
|
||||
Dictionary<string, IReadOnlyList<Parameter>> parametersByContentType = new Dictionary<string, IReadOnlyList<Parameter>>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "", parameters }
|
||||
};
|
||||
|
||||
foreach (JValue value in consumes.OfType<JValue>().Where(x => x.Type == JTokenType.String))
|
||||
{
|
||||
parametersByContentType[value.ToString()] = parameters;
|
||||
}
|
||||
|
||||
requestMethods[methodInfo.Name] = parametersByContentType;
|
||||
}
|
||||
}
|
||||
|
||||
metadata.Add(new EndpointMetadata(property.Name, requestMethods));
|
||||
}
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
using Microsoft.Repl.ConsoleHandling;
|
||||
|
||||
namespace Microsoft.HttpRepl.Preferences
|
||||
{
|
||||
public interface IJsonConfig
|
||||
{
|
||||
int IndentSize { get; }
|
||||
|
||||
AllowedColors DefaultColor { get; }
|
||||
|
||||
AllowedColors ArrayBraceColor { get; }
|
||||
|
||||
AllowedColors ObjectBraceColor { get; }
|
||||
|
||||
AllowedColors CommaColor { get; }
|
||||
|
||||
AllowedColors NameColor { get; }
|
||||
|
||||
AllowedColors NameSeparatorColor { get; }
|
||||
|
||||
AllowedColors BoolColor { get; }
|
||||
|
||||
AllowedColors NumericColor { get; }
|
||||
|
||||
AllowedColors StringColor { get; }
|
||||
|
||||
AllowedColors NullColor { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
using Microsoft.Repl.ConsoleHandling;
|
||||
|
||||
namespace Microsoft.HttpRepl.Preferences
|
||||
{
|
||||
public class JsonConfig : IJsonConfig
|
||||
{
|
||||
private readonly HttpState _state;
|
||||
|
||||
public int IndentSize => _state.GetIntPreference(WellKnownPreference.JsonIndentSize, 2);
|
||||
|
||||
public AllowedColors DefaultColor => _state.GetColorPreference(WellKnownPreference.JsonColor);
|
||||
|
||||
private AllowedColors DefaultBraceColor => _state.GetColorPreference(WellKnownPreference.JsonBraceColor, DefaultSyntaxColor);
|
||||
|
||||
private AllowedColors DefaultSyntaxColor => _state.GetColorPreference(WellKnownPreference.JsonSyntaxColor, DefaultColor);
|
||||
|
||||
private AllowedColors DefaultLiteralColor => _state.GetColorPreference(WellKnownPreference.JsonLiteralColor, DefaultColor);
|
||||
|
||||
public AllowedColors ArrayBraceColor => _state.GetColorPreference(WellKnownPreference.JsonArrayBraceColor, DefaultBraceColor);
|
||||
|
||||
public AllowedColors ObjectBraceColor => _state.GetColorPreference(WellKnownPreference.JsonObjectBraceColor, DefaultBraceColor);
|
||||
|
||||
public AllowedColors CommaColor => _state.GetColorPreference(WellKnownPreference.JsonCommaColor, DefaultSyntaxColor);
|
||||
|
||||
public AllowedColors NameColor => _state.GetColorPreference(WellKnownPreference.JsonNameColor, StringColor);
|
||||
|
||||
public AllowedColors NameSeparatorColor => _state.GetColorPreference(WellKnownPreference.JsonNameSeparatorColor, DefaultSyntaxColor);
|
||||
|
||||
public AllowedColors BoolColor => _state.GetColorPreference(WellKnownPreference.JsonBoolColor, DefaultLiteralColor);
|
||||
|
||||
public AllowedColors NumericColor => _state.GetColorPreference(WellKnownPreference.JsonNumericColor, DefaultLiteralColor);
|
||||
|
||||
public AllowedColors StringColor => _state.GetColorPreference(WellKnownPreference.JsonStringColor, DefaultLiteralColor);
|
||||
|
||||
public AllowedColors NullColor => _state.GetColorPreference(WellKnownPreference.JsonNullColor, DefaultLiteralColor);
|
||||
|
||||
public JsonConfig(HttpState state)
|
||||
{
|
||||
_state = state;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
using Microsoft.Repl.ConsoleHandling;
|
||||
|
||||
namespace Microsoft.HttpRepl.Preferences
|
||||
{
|
||||
public class RequestConfig : RequestOrResponseConfig
|
||||
{
|
||||
public RequestConfig(HttpState state)
|
||||
: base(state)
|
||||
{
|
||||
}
|
||||
|
||||
public override AllowedColors BodyColor => State.GetColorPreference(WellKnownPreference.RequestBodyColor, base.BodyColor);
|
||||
|
||||
public override AllowedColors SchemeColor => State.GetColorPreference(WellKnownPreference.RequestSchemeColor, base.SchemeColor);
|
||||
|
||||
public override AllowedColors HeaderKeyColor => State.GetColorPreference(WellKnownPreference.RequestHeaderKeyColor, base.HeaderKeyColor);
|
||||
|
||||
public override AllowedColors HeaderSeparatorColor => State.GetColorPreference(WellKnownPreference.RequestHeaderSeparatorColor, base.HeaderSeparatorColor);
|
||||
|
||||
public override AllowedColors HeaderValueSeparatorColor => State.GetColorPreference(WellKnownPreference.RequestHeaderValueSeparatorColor, base.HeaderValueSeparatorColor);
|
||||
|
||||
public override AllowedColors HeaderValueColor => State.GetColorPreference(WellKnownPreference.RequestHeaderValueColor, base.HeaderValueColor);
|
||||
|
||||
public override AllowedColors HeaderColor => State.GetColorPreference(WellKnownPreference.RequestHeaderColor, base.HeaderColor);
|
||||
|
||||
public override AllowedColors GeneralColor => State.GetColorPreference(WellKnownPreference.RequestColor, base.GeneralColor);
|
||||
|
||||
public override AllowedColors ProtocolColor => State.GetColorPreference(WellKnownPreference.RequestProtocolColor, base.ProtocolColor);
|
||||
|
||||
public override AllowedColors ProtocolNameColor => State.GetColorPreference(WellKnownPreference.RequestProtocolNameColor, base.ProtocolNameColor);
|
||||
|
||||
public override AllowedColors ProtocolVersionColor => State.GetColorPreference(WellKnownPreference.RequestProtocolVersionColor, base.ProtocolVersionColor);
|
||||
|
||||
public override AllowedColors ProtocolSeparatorColor => State.GetColorPreference(WellKnownPreference.RequestProtocolSeparatorColor, base.ProtocolSeparatorColor);
|
||||
|
||||
public override AllowedColors StatusColor => State.GetColorPreference(WellKnownPreference.RequestStatusColor, base.StatusColor);
|
||||
|
||||
public override AllowedColors StatusCodeColor => State.GetColorPreference(WellKnownPreference.RequestStatusCodeColor, base.StatusCodeColor);
|
||||
|
||||
public override AllowedColors StatusReasonPhraseColor => State.GetColorPreference(WellKnownPreference.RequestStatusReaseonPhraseColor, base.StatusReasonPhraseColor);
|
||||
|
||||
public AllowedColors MethodColor => State.GetColorPreference(WellKnownPreference.RequestMethodColor, GeneralColor);
|
||||
|
||||
public AllowedColors AddressColor => State.GetColorPreference(WellKnownPreference.RequestAddressColor, GeneralColor);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
using Microsoft.Repl.ConsoleHandling;
|
||||
|
||||
namespace Microsoft.HttpRepl.Preferences
|
||||
{
|
||||
public abstract class RequestOrResponseConfig
|
||||
{
|
||||
protected HttpState State { get; }
|
||||
|
||||
protected RequestOrResponseConfig(HttpState state)
|
||||
{
|
||||
State = state;
|
||||
}
|
||||
|
||||
public virtual AllowedColors BodyColor => State.GetColorPreference(WellKnownPreference.BodyColor, GeneralColor);
|
||||
|
||||
public virtual AllowedColors SchemeColor => State.GetColorPreference(WellKnownPreference.SchemeColor, GeneralColor);
|
||||
|
||||
public virtual AllowedColors HeaderKeyColor => State.GetColorPreference(WellKnownPreference.HeaderKeyColor, HeaderColor);
|
||||
|
||||
public virtual AllowedColors HeaderSeparatorColor => State.GetColorPreference(WellKnownPreference.HeaderSeparatorColor, HeaderColor);
|
||||
|
||||
public virtual AllowedColors HeaderValueSeparatorColor => State.GetColorPreference(WellKnownPreference.HeaderValueSeparatorColor, HeaderSeparatorColor);
|
||||
|
||||
public virtual AllowedColors HeaderValueColor => State.GetColorPreference(WellKnownPreference.HeaderValueColor, HeaderColor);
|
||||
|
||||
public virtual AllowedColors HeaderColor => State.GetColorPreference(WellKnownPreference.HeaderColor, GeneralColor);
|
||||
|
||||
public virtual AllowedColors GeneralColor => State.GetColorPreference(WellKnownPreference.RequestOrResponseColor);
|
||||
|
||||
public virtual AllowedColors ProtocolColor => State.GetColorPreference(WellKnownPreference.ProtocolColor, GeneralColor);
|
||||
|
||||
public virtual AllowedColors ProtocolNameColor => State.GetColorPreference(WellKnownPreference.ProtocolNameColor, ProtocolColor);
|
||||
|
||||
public virtual AllowedColors ProtocolVersionColor => State.GetColorPreference(WellKnownPreference.ProtocolVersionColor, ProtocolColor);
|
||||
|
||||
public virtual AllowedColors ProtocolSeparatorColor => State.GetColorPreference(WellKnownPreference.ProtocolSeparatorColor, ProtocolColor);
|
||||
|
||||
public virtual AllowedColors StatusColor => State.GetColorPreference(WellKnownPreference.StatusColor, GeneralColor);
|
||||
|
||||
public virtual AllowedColors StatusCodeColor => State.GetColorPreference(WellKnownPreference.StatusCodeColor, StatusColor);
|
||||
|
||||
public virtual AllowedColors StatusReasonPhraseColor => State.GetColorPreference(WellKnownPreference.StatusReaseonPhraseColor, StatusColor);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
using Microsoft.Repl.ConsoleHandling;
|
||||
|
||||
namespace Microsoft.HttpRepl.Preferences
|
||||
{
|
||||
public class ResponseConfig : RequestOrResponseConfig
|
||||
{
|
||||
public ResponseConfig(HttpState state)
|
||||
: base(state)
|
||||
{
|
||||
}
|
||||
|
||||
public override AllowedColors BodyColor => State.GetColorPreference(WellKnownPreference.ResponseBodyColor, base.BodyColor);
|
||||
|
||||
public override AllowedColors SchemeColor => State.GetColorPreference(WellKnownPreference.ResponseSchemeColor, base.SchemeColor);
|
||||
|
||||
public override AllowedColors HeaderKeyColor => State.GetColorPreference(WellKnownPreference.ResponseHeaderKeyColor, base.HeaderKeyColor);
|
||||
|
||||
public override AllowedColors HeaderSeparatorColor => State.GetColorPreference(WellKnownPreference.ResponseHeaderSeparatorColor, base.HeaderSeparatorColor);
|
||||
|
||||
public override AllowedColors HeaderValueSeparatorColor => State.GetColorPreference(WellKnownPreference.ResponseHeaderValueSeparatorColor, base.HeaderValueSeparatorColor);
|
||||
|
||||
public override AllowedColors HeaderValueColor => State.GetColorPreference(WellKnownPreference.ResponseHeaderValueColor, base.HeaderValueColor);
|
||||
|
||||
public override AllowedColors HeaderColor => State.GetColorPreference(WellKnownPreference.ResponseHeaderColor, base.HeaderColor);
|
||||
|
||||
public override AllowedColors GeneralColor => State.GetColorPreference(WellKnownPreference.ResponseColor, base.GeneralColor);
|
||||
|
||||
public override AllowedColors ProtocolColor => State.GetColorPreference(WellKnownPreference.ResponseProtocolColor, base.ProtocolColor);
|
||||
|
||||
public override AllowedColors ProtocolNameColor => State.GetColorPreference(WellKnownPreference.ResponseProtocolNameColor, base.ProtocolNameColor);
|
||||
|
||||
public override AllowedColors ProtocolVersionColor => State.GetColorPreference(WellKnownPreference.ResponseProtocolVersionColor, base.ProtocolVersionColor);
|
||||
|
||||
public override AllowedColors ProtocolSeparatorColor => State.GetColorPreference(WellKnownPreference.ResponseProtocolSeparatorColor, base.ProtocolSeparatorColor);
|
||||
|
||||
public override AllowedColors StatusColor => State.GetColorPreference(WellKnownPreference.ResponseStatusColor, base.StatusColor);
|
||||
|
||||
public override AllowedColors StatusCodeColor => State.GetColorPreference(WellKnownPreference.ResponseStatusCodeColor, base.StatusCodeColor);
|
||||
|
||||
public override AllowedColors StatusReasonPhraseColor => State.GetColorPreference(WellKnownPreference.ResponseStatusReaseonPhraseColor, base.StatusReasonPhraseColor);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Microsoft.Repl.ConsoleHandling;
|
||||
|
||||
namespace Microsoft.HttpRepl.Preferences
|
||||
{
|
||||
public static class WellKnownPreference
|
||||
{
|
||||
public static class Catalog
|
||||
{
|
||||
private static IReadOnlyList<string> _names;
|
||||
|
||||
public static IReadOnlyList<string> Names
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_names != null)
|
||||
{
|
||||
return _names;
|
||||
}
|
||||
|
||||
List<string> matchingProperties = new List<string>();
|
||||
|
||||
foreach (PropertyInfo property in typeof(WellKnownPreference).GetProperties(BindingFlags.Public | BindingFlags.Static))
|
||||
{
|
||||
if (property.PropertyType == typeof(string) && property.GetMethod != null && property.GetValue(null) is string val)
|
||||
{
|
||||
matchingProperties.Add(val);
|
||||
}
|
||||
}
|
||||
|
||||
return _names = matchingProperties;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region JSON
|
||||
public static string JsonArrayBraceColor { get; } = "colors.json.arrayBrace";
|
||||
|
||||
public static string JsonObjectBraceColor { get; } = "colors.json.objectBrace";
|
||||
|
||||
public static string JsonNameColor { get; } = "colors.json.name";
|
||||
|
||||
public static string JsonNameSeparatorColor { get; } = "colors.json.nameSeparator";
|
||||
|
||||
public static string JsonIndentSize { get; } = "formatting.json.indentSize";
|
||||
|
||||
public static string JsonCommaColor { get; } = "colors.json.comma";
|
||||
|
||||
public static string JsonLiteralColor { get; } = "colors.json.literal";
|
||||
|
||||
public static string JsonNullColor { get; } = "colors.json.null";
|
||||
|
||||
public static string JsonBoolColor { get; } = "colors.json.bool";
|
||||
|
||||
public static string JsonNumericColor { get; } = "colors.json.numeric";
|
||||
|
||||
public static string JsonStringColor { get; } = "colors.json.string";
|
||||
|
||||
public static string JsonColor { get; } = "colors.json";
|
||||
|
||||
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";
|
||||
|
||||
public static string RequestBodyColor { get; } = "colors.request.body";
|
||||
|
||||
public static string RequestSchemeColor { get; } = "colors.request.scheme";
|
||||
|
||||
public static string RequestHeaderKeyColor { get; } = "colors.request.header.key";
|
||||
|
||||
public static string RequestHeaderSeparatorColor { get; } = "colors.request.header.separator";
|
||||
|
||||
public static string RequestHeaderValueSeparatorColor { get; } = "colors.request.header.valueSeparator";
|
||||
|
||||
public static string RequestHeaderValueColor { get; } = "colors.request.header.value";
|
||||
|
||||
public static string RequestHeaderColor { get; } = "colors.request.header";
|
||||
|
||||
public static string RequestProtocolColor { get; } = "colors.request.protocol";
|
||||
|
||||
public static string RequestProtocolNameColor { get; } = "colors.request.protocol.name";
|
||||
|
||||
public static string RequestProtocolSeparatorColor { get; } = "colors.request.protocol.separator";
|
||||
|
||||
public static string RequestProtocolVersionColor { get; } = "colors.request.protocol.version";
|
||||
|
||||
public static string RequestStatusColor { get; } = "colors.request.status";
|
||||
|
||||
public static string RequestStatusCodeColor { get; } = "colors.request.status.code";
|
||||
|
||||
public static string RequestStatusReaseonPhraseColor { get; } = "colors.request.status.reasonPhrase";
|
||||
|
||||
public static string RequestMethodColor { get; } = "colors.request.method";
|
||||
|
||||
public static string RequestAddressColor { get; } = "colors.request.address";
|
||||
|
||||
|
||||
public static string ResponseColor { get; } = "colors.response";
|
||||
|
||||
public static string ResponseBodyColor { get; } = "colors.response.body";
|
||||
|
||||
public static string ResponseSchemeColor { get; } = "colors.response.scheme";
|
||||
|
||||
public static string ResponseHeaderKeyColor { get; } = "colors.response.header.key";
|
||||
|
||||
public static string ResponseHeaderSeparatorColor { get; } = "colors.response.header.separator";
|
||||
|
||||
public static string ResponseHeaderValueSeparatorColor { get; } = "colors.response.header.valueSeparator";
|
||||
|
||||
public static string ResponseHeaderValueColor { get; } = "colors.response.header.value";
|
||||
|
||||
public static string ResponseHeaderColor { get; } = "colors.response.header";
|
||||
|
||||
public static string ResponseProtocolColor { get; } = "colors.response.protocol";
|
||||
|
||||
public static string ResponseProtocolNameColor { get; } = "colors.response.protocol.name";
|
||||
|
||||
public static string ResponseProtocolSeparatorColor { get; } = "colors.response.protocol.separator";
|
||||
|
||||
public static string ResponseProtocolVersionColor { get; } = "colors.response.protocol.version";
|
||||
|
||||
public static string ResponseStatusColor { get; } = "colors.response.status";
|
||||
|
||||
public static string ResponseStatusCodeColor { get; } = "colors.response.status.code";
|
||||
|
||||
public static string ResponseStatusReaseonPhraseColor { get; } = "colors.response.status.reasonPhrase";
|
||||
|
||||
|
||||
public static string RequestOrResponseColor { get; } = "colors.requestOrResponse";
|
||||
|
||||
public static string BodyColor { get; } = "colors.body";
|
||||
|
||||
public static string SchemeColor { get; } = "colors.scheme";
|
||||
|
||||
public static string HeaderKeyColor { get; } = "colors.header.key";
|
||||
|
||||
public static string HeaderSeparatorColor { get; } = "colors.header.separator";
|
||||
|
||||
public static string HeaderValueSeparatorColor { get; } = "colors.header.valueSeparator";
|
||||
|
||||
public static string HeaderValueColor { get; } = "colors.header.value";
|
||||
|
||||
public static string HeaderColor { get; } = "colors.header";
|
||||
|
||||
public static string ProtocolColor { get; } = "colors.protocol";
|
||||
|
||||
public static string ProtocolNameColor { get; } = "colors.protocol.name";
|
||||
|
||||
public static string ProtocolSeparatorColor { get; } = "colors.protocol.separator";
|
||||
|
||||
public static string ProtocolVersionColor { get; } = "colors.protocol.version";
|
||||
|
||||
public static string StatusColor { get; } = "colors.status";
|
||||
|
||||
public static string StatusCodeColor { get; } = "colors.status.code";
|
||||
|
||||
public static string StatusReaseonPhraseColor { get; } = "colors.status.reasonPhrase";
|
||||
|
||||
|
||||
public static string DefaultEditorCommand { get; } = "editor.command.default";
|
||||
|
||||
public static string DefaultEditorArguments { get; } = "editor.command.default.arguments";
|
||||
|
||||
|
||||
public static AllowedColors GetColorPreference(this HttpState programState, string preference, AllowedColors defaultvalue = AllowedColors.None)
|
||||
{
|
||||
if (!programState.Preferences.TryGetValue(preference, out string preferenceValueString) || !Enum.TryParse(preferenceValueString, true, out AllowedColors result))
|
||||
{
|
||||
result = defaultvalue;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static int GetIntPreference(this HttpState programState, string preference, int defaultValue = 0)
|
||||
{
|
||||
if (!programState.Preferences.TryGetValue(preference, out string preferenceValueString) || !int.TryParse(preferenceValueString, out int result))
|
||||
{
|
||||
result = defaultValue;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static string GetStringPreference(this HttpState programState, string preference, string defaultValue = null)
|
||||
{
|
||||
if (!programState.Preferences.TryGetValue(preference, out string result))
|
||||
{
|
||||
result = defaultValue;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Repl;
|
||||
using Microsoft.Repl.Commanding;
|
||||
using Microsoft.Repl.Parsing;
|
||||
using Microsoft.HttpRepl.Commands;
|
||||
|
||||
namespace Microsoft.HttpRepl
|
||||
{
|
||||
class Program
|
||||
{
|
||||
static async Task Main(string[] args)
|
||||
{
|
||||
var state = new HttpState();
|
||||
var dispatcher = DefaultCommandDispatcher.Create(state.GetPrompt, state);
|
||||
|
||||
dispatcher.AddCommand(new ChangeDirectoryCommand());
|
||||
dispatcher.AddCommand(new ClearCommand());
|
||||
//dispatcher.AddCommand(new ConfigCommand());
|
||||
dispatcher.AddCommand(new DeleteCommand());
|
||||
dispatcher.AddCommand(new EchoCommand());
|
||||
dispatcher.AddCommand(new ExitCommand());
|
||||
dispatcher.AddCommand(new HeadCommand());
|
||||
dispatcher.AddCommand(new HelpCommand());
|
||||
dispatcher.AddCommand(new GetCommand());
|
||||
dispatcher.AddCommand(new ListCommand());
|
||||
dispatcher.AddCommand(new OptionsCommand());
|
||||
dispatcher.AddCommand(new PatchCommand());
|
||||
dispatcher.AddCommand(new PrefCommand());
|
||||
dispatcher.AddCommand(new PostCommand());
|
||||
dispatcher.AddCommand(new PutCommand());
|
||||
dispatcher.AddCommand(new RunCommand());
|
||||
dispatcher.AddCommand(new SetBaseCommand());
|
||||
dispatcher.AddCommand(new SetDiagCommand());
|
||||
dispatcher.AddCommand(new SetHeaderCommand());
|
||||
dispatcher.AddCommand(new SetSwaggerCommand());
|
||||
dispatcher.AddCommand(new UICommand());
|
||||
|
||||
CancellationTokenSource source = new CancellationTokenSource();
|
||||
var shell = new Shell(dispatcher);
|
||||
shell.ShellState.ConsoleManager.AddBreakHandler(() => source.Cancel());
|
||||
if (args.Length > 0)
|
||||
{
|
||||
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);
|
||||
}
|
||||
Task result = shell.RunAsync(source.Token);
|
||||
await result.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"profiles": {
|
||||
"Microsoft.HttpRepl": {
|
||||
"commandName": "Project",
|
||||
"commandLineArgs": "http://localhost"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.HttpRepl.Suggestions
|
||||
{
|
||||
public class HeaderCompletion
|
||||
{
|
||||
private static readonly IEnumerable<string> CommonHeaders = new[]
|
||||
{
|
||||
"A-IM",
|
||||
"Accept",
|
||||
"Accept-Charset",
|
||||
"Accept-Encoding",
|
||||
"Accept-Language",
|
||||
"Accept-Datetime",
|
||||
"Access-Control-Request-Method",
|
||||
"Access-Control-Request-Headers",
|
||||
"Authorization",
|
||||
"Cache-Control",
|
||||
"Connection",
|
||||
"Content-Length",
|
||||
"Content-MD5",
|
||||
"Content-Type",
|
||||
"Cookie",
|
||||
"Date",
|
||||
"Expect",
|
||||
"Forwarded",
|
||||
"From",
|
||||
"Host",
|
||||
"If-Match",
|
||||
"If-Modified-Since",
|
||||
"If-None-Match",
|
||||
"If-Range",
|
||||
"If-Unmodified-Since",
|
||||
"Max-Forwards",
|
||||
"Origin",
|
||||
"Pragma",
|
||||
"Proxy-Authentication",
|
||||
"Range",
|
||||
"Referer",
|
||||
"TE",
|
||||
"User-Agent",
|
||||
"Upgrade",
|
||||
"Via",
|
||||
"Warning",
|
||||
//Non-standard
|
||||
"Upgrade-Insecure-Requests",
|
||||
"X-Requested-With",
|
||||
"DNT",
|
||||
"X-Forwarded-For",
|
||||
"X-Forwarded-Host",
|
||||
"X-Forwarded-Proto",
|
||||
"Front-End-Https",
|
||||
"X-Http-Method-Override",
|
||||
"X-ATT-DeviceId",
|
||||
"X-Wap-Profile",
|
||||
"Proxy-Connection",
|
||||
"X-UIDH",
|
||||
"X-Csrf-Token",
|
||||
"X-Request-ID",
|
||||
"X-Correlation-ID"
|
||||
};
|
||||
|
||||
private static readonly IReadOnlyList<string> DefaultContentTypesList = null;
|
||||
|
||||
public static IEnumerable<string> GetCompletions(IReadOnlyCollection<string> existingHeaders, string prefix)
|
||||
{
|
||||
List<string> result = CommonHeaders.Where(x => x.StartsWith(prefix, StringComparison.OrdinalIgnoreCase) && !(existingHeaders?.Contains(x) ?? false)).ToList();
|
||||
|
||||
if (result.Count > 0)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static IEnumerable<string> GetValueCompletions(string method, string path, string header, string prefix, HttpState programState)
|
||||
{
|
||||
switch (header.ToUpperInvariant())
|
||||
{
|
||||
case "CONTENT-TYPE":
|
||||
IEnumerable<string> results = programState.GetApplicableContentTypes(method, path) ?? DefaultContentTypesList;
|
||||
|
||||
if (results is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return results.Where(x => !string.IsNullOrEmpty(x) && x.StartsWith(prefix, StringComparison.OrdinalIgnoreCase));
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.HttpRepl.Suggestions
|
||||
{
|
||||
public static class ServerPathCompletion
|
||||
{
|
||||
public static IEnumerable<string> GetCompletions(HttpState programState, string normalCompletionString)
|
||||
{
|
||||
|
||||
//If it's an absolute URI, nothing to suggest
|
||||
if (Uri.TryCreate(normalCompletionString, UriKind.Absolute, out Uri _))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string path = normalCompletionString.Replace('\\', '/');
|
||||
int searchFrom = normalCompletionString.Length - 1;
|
||||
int lastSlash = path.LastIndexOf('/', searchFrom);
|
||||
string prefix;
|
||||
|
||||
if (lastSlash < 0)
|
||||
{
|
||||
path = string.Empty;
|
||||
prefix = normalCompletionString;
|
||||
}
|
||||
else
|
||||
{
|
||||
path = path.Substring(0, lastSlash + 1);
|
||||
prefix = normalCompletionString.Substring(lastSlash + 1);
|
||||
}
|
||||
|
||||
IDirectoryStructure s = programState.Structure.TraverseTo(programState.PathSections.Reverse()).TraverseTo(path);
|
||||
|
||||
if (s?.DirectoryNames == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
List<string> results = new List<string>();
|
||||
|
||||
foreach (string child in s.DirectoryNames)
|
||||
{
|
||||
if (child.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
results.Add(path + child);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Repl.Commanding
|
||||
{
|
||||
public class CommandHistory : ICommandHistory
|
||||
{
|
||||
private readonly int _maxEntries;
|
||||
private readonly List<string> _commandLines = new List<string>();
|
||||
private int _currentCommand = -1;
|
||||
private int _suspensionDepth;
|
||||
|
||||
public CommandHistory(int maxEntries = 50)
|
||||
{
|
||||
_maxEntries = maxEntries;
|
||||
}
|
||||
|
||||
public void AddCommand(string command)
|
||||
{
|
||||
if (_suspensionDepth > 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_commandLines.Add(command);
|
||||
if (_commandLines.Count > _maxEntries)
|
||||
{
|
||||
_commandLines.RemoveAt(0);
|
||||
}
|
||||
_currentCommand = -1;
|
||||
}
|
||||
|
||||
public string GetNextCommand()
|
||||
{
|
||||
if (_commandLines.Count == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (_currentCommand == -1 || _currentCommand >= _commandLines.Count - 1)
|
||||
{
|
||||
_currentCommand = -1;
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return _commandLines[++_currentCommand];
|
||||
}
|
||||
|
||||
public string GetPreviousCommand()
|
||||
{
|
||||
if (_commandLines.Count == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (_currentCommand == -1)
|
||||
{
|
||||
_currentCommand = _commandLines.Count;
|
||||
}
|
||||
|
||||
if (_currentCommand > 0)
|
||||
{
|
||||
return _commandLines[--_currentCommand];
|
||||
}
|
||||
|
||||
return _commandLines[0];
|
||||
}
|
||||
|
||||
public IDisposable SuspendHistory()
|
||||
{
|
||||
++_suspensionDepth;
|
||||
return new Disposable(() => --_suspensionDepth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
namespace Microsoft.Repl.Commanding
|
||||
{
|
||||
public enum CommandInputLocation
|
||||
{
|
||||
CommandName,
|
||||
Argument,
|
||||
OptionName,
|
||||
OptionValue
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
namespace Microsoft.Repl.Commanding
|
||||
{
|
||||
public class CommandInputProcessingIssue
|
||||
{
|
||||
public CommandInputProcessingIssueKind Kind { get; }
|
||||
|
||||
public string Text { get; }
|
||||
|
||||
public CommandInputProcessingIssue(CommandInputProcessingIssueKind kind, string text)
|
||||
{
|
||||
Kind = kind;
|
||||
Text = text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
namespace Microsoft.Repl.Commanding
|
||||
{
|
||||
public enum CommandInputProcessingIssueKind
|
||||
{
|
||||
CommandMismatch,
|
||||
ArgumentCountOutOfRange,
|
||||
UnknownOption,
|
||||
OptionUseCountOutOfRange,
|
||||
MissingRequiredOptionInput,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Repl.Commanding
|
||||
{
|
||||
public class CommandInputSpecification
|
||||
{
|
||||
public IReadOnlyList<string> CommandName { get; }
|
||||
|
||||
public char OptionPreamble { get; }
|
||||
|
||||
public int MinimumArguments { get; }
|
||||
|
||||
public int MaximumArguments { get; }
|
||||
|
||||
public IReadOnlyList<CommandOptionSpecification> Options { get; }
|
||||
|
||||
public CommandInputSpecification(IReadOnlyList<string> name, char optionPreamble, IReadOnlyList<CommandOptionSpecification> options, int minimumArgs, int maximumArgs)
|
||||
{
|
||||
CommandName = name;
|
||||
OptionPreamble = optionPreamble;
|
||||
MinimumArguments = minimumArgs;
|
||||
MaximumArguments = maximumArgs;
|
||||
|
||||
if (MinimumArguments < 0)
|
||||
{
|
||||
MinimumArguments = 0;
|
||||
}
|
||||
|
||||
if (MaximumArguments < MinimumArguments)
|
||||
{
|
||||
MaximumArguments = MinimumArguments;
|
||||
}
|
||||
|
||||
Options = options;
|
||||
}
|
||||
|
||||
public static CommandInputSpecificationBuilder Create(string baseName, params string[] additionalNameParts)
|
||||
{
|
||||
List<string> nameParts = new List<string> {baseName};
|
||||
nameParts.AddRange(additionalNameParts);
|
||||
return new CommandInputSpecificationBuilder(nameParts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Repl.Commanding
|
||||
{
|
||||
public class CommandInputSpecificationBuilder
|
||||
{
|
||||
private readonly IReadOnlyList<string> _name;
|
||||
private char _optionPreamble;
|
||||
private int _minimumArgs;
|
||||
private int _maximumArgs;
|
||||
private readonly List<CommandOptionSpecification> _options = new List<CommandOptionSpecification>();
|
||||
|
||||
public CommandInputSpecificationBuilder(IReadOnlyList<string> name)
|
||||
{
|
||||
_name = name;
|
||||
_optionPreamble = '-';
|
||||
}
|
||||
|
||||
public CommandInputSpecificationBuilder WithOptionPreamble(char optionChar)
|
||||
{
|
||||
_optionPreamble = optionChar;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandInputSpecificationBuilder ExactArgCount(int count)
|
||||
{
|
||||
_minimumArgs = count;
|
||||
_maximumArgs = count;
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandInputSpecificationBuilder MinimumArgCount(int count)
|
||||
{
|
||||
_minimumArgs = count;
|
||||
if (_maximumArgs < count)
|
||||
{
|
||||
_maximumArgs = count;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandInputSpecificationBuilder MaximumArgCount(int count)
|
||||
{
|
||||
_maximumArgs = count;
|
||||
|
||||
if (_minimumArgs > count)
|
||||
{
|
||||
_minimumArgs = count;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandInputSpecificationBuilder WithOption(CommandOptionSpecification option)
|
||||
{
|
||||
_options.Add(option);
|
||||
return this;
|
||||
}
|
||||
|
||||
public CommandInputSpecification Finish()
|
||||
{
|
||||
return new CommandInputSpecification(_name, _optionPreamble, _options, _minimumArgs, _maximumArgs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Repl.Commanding
|
||||
{
|
||||
public class CommandOptionSpecification
|
||||
{
|
||||
public string Id { get; }
|
||||
|
||||
public IReadOnlyList<string> Forms { get; }
|
||||
|
||||
public int MaximumOccurrences { get; }
|
||||
|
||||
public int MinimumOccurrences { get; }
|
||||
|
||||
public bool AcceptsValue { get; }
|
||||
|
||||
public bool RequiresValue { get; }
|
||||
|
||||
public CommandOptionSpecification(string id, bool acceptsValue = false, bool requiresValue = false, int minimumOccurrences = 0, int maximumOccurrences = int.MaxValue, params string[] forms)
|
||||
{
|
||||
Id = id;
|
||||
Forms = forms;
|
||||
MinimumOccurrences = minimumOccurrences;
|
||||
MaximumOccurrences = maximumOccurrences > minimumOccurrences ? maximumOccurrences : minimumOccurrences;
|
||||
RequiresValue = requiresValue;
|
||||
AcceptsValue = RequiresValue || acceptsValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Repl.Parsing;
|
||||
|
||||
namespace Microsoft.Repl.Commanding
|
||||
{
|
||||
public abstract class CommandWithStructuredInputBase<TProgramState, TParseResult> : ICommand<TProgramState, TParseResult>
|
||||
where TParseResult : ICoreParseResult
|
||||
{
|
||||
public abstract string GetHelpSummary(IShellState shellState, TProgramState programState);
|
||||
|
||||
public string GetHelpDetails(IShellState shellState, TProgramState programState, TParseResult parseResult)
|
||||
{
|
||||
if (!DefaultCommandInput<TParseResult>.TryProcess(InputSpec, parseResult, out DefaultCommandInput<TParseResult> commandInput, out IReadOnlyList<CommandInputProcessingIssue> processingIssues)
|
||||
&& processingIssues.Any(x => x.Kind == CommandInputProcessingIssueKind.CommandMismatch))
|
||||
{
|
||||
//If this is the right command, just not the right syntax, report the usage errors
|
||||
return null;
|
||||
}
|
||||
|
||||
return GetHelpDetails(shellState, programState, commandInput, parseResult);
|
||||
}
|
||||
|
||||
protected abstract string GetHelpDetails(IShellState shellState, TProgramState programState, DefaultCommandInput<TParseResult> commandInput, TParseResult parseResult);
|
||||
|
||||
public IEnumerable<string> Suggest(IShellState shellState, TProgramState programState, TParseResult parseResult)
|
||||
{
|
||||
DefaultCommandInput<TParseResult>.TryProcess(InputSpec, parseResult, out DefaultCommandInput<TParseResult> commandInput, out IReadOnlyList<CommandInputProcessingIssue> _);
|
||||
|
||||
string normalCompletionString = parseResult.SelectedSection == parseResult.Sections.Count
|
||||
? string.Empty
|
||||
: parseResult.Sections[parseResult.SelectedSection].Substring(0, parseResult.CaretPositionWithinSelectedSection);
|
||||
|
||||
//If we're completing in a name position, offer completion for the command name
|
||||
if (parseResult.SelectedSection < InputSpec.CommandName.Count)
|
||||
{
|
||||
for (int i = 0; i < parseResult.SelectedSection; ++i)
|
||||
{
|
||||
if (!string.Equals(InputSpec.CommandName[i], parseResult.Sections[i], StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (InputSpec.CommandName[parseResult.SelectedSection].StartsWith(normalCompletionString, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return new[] {InputSpec.CommandName[parseResult.SelectedSection]};
|
||||
}
|
||||
}
|
||||
|
||||
if (commandInput is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (normalCompletionString.StartsWith(InputSpec.OptionPreamble))
|
||||
{
|
||||
return GetOptionCompletions(commandInput, normalCompletionString);
|
||||
}
|
||||
|
||||
IEnumerable<string> completions = Enumerable.Empty<string>();
|
||||
CommandInputLocation? inputLocation = commandInput.SelectedElement?.Location;
|
||||
|
||||
if (inputLocation != CommandInputLocation.OptionValue && commandInput.Arguments.Count < InputSpec.MaximumArguments)
|
||||
{
|
||||
IEnumerable<string> results = GetArgumentSuggestionsForText(shellState, programState, parseResult, commandInput, normalCompletionString);
|
||||
|
||||
if (results != null)
|
||||
{
|
||||
completions = results;
|
||||
}
|
||||
}
|
||||
|
||||
switch (inputLocation)
|
||||
{
|
||||
case CommandInputLocation.OptionName:
|
||||
{
|
||||
IEnumerable<string> results = GetOptionCompletions(commandInput, normalCompletionString);
|
||||
|
||||
if (results != null)
|
||||
{
|
||||
completions = completions.Union(results);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case CommandInputLocation.OptionValue:
|
||||
{
|
||||
IEnumerable<string> results = GetOptionValueCompletions(shellState, programState, commandInput.SelectedElement.Owner.NormalizedText, commandInput, parseResult, normalCompletionString);
|
||||
|
||||
if (results != null)
|
||||
{
|
||||
completions = completions.Union(results);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case CommandInputLocation.Argument:
|
||||
{
|
||||
IEnumerable<string> argumentResults = GetArgumentSuggestionsForText(shellState, programState, parseResult, commandInput, normalCompletionString);
|
||||
|
||||
if (argumentResults != null)
|
||||
{
|
||||
completions = completions.Union(argumentResults);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(normalCompletionString))
|
||||
{
|
||||
IEnumerable<string> results = GetOptionCompletions(commandInput, normalCompletionString);
|
||||
|
||||
if (results != null)
|
||||
{
|
||||
completions = completions.Union(results);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return completions;
|
||||
}
|
||||
|
||||
protected virtual IEnumerable<string> GetOptionValueCompletions(IShellState shellState, TProgramState programState, string optionId, DefaultCommandInput<TParseResult> commandInput, TParseResult parseResult, string normalizedCompletionText)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
protected virtual IEnumerable<string> GetArgumentSuggestionsForText(IShellState shellState, TProgramState programState, TParseResult parseResult, DefaultCommandInput<TParseResult> commandInput, string normalCompletionString)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetOptionCompletions(DefaultCommandInput<TParseResult> commandInput, string normalCompletionString)
|
||||
{
|
||||
return InputSpec.Options.Where(x => commandInput.Options[x.Id].Count < x.MaximumOccurrences)
|
||||
.SelectMany(x => x.Forms)
|
||||
.Where(x => x.StartsWith(normalCompletionString, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
public bool? CanHandle(IShellState shellState, TProgramState programState, TParseResult parseResult)
|
||||
{
|
||||
if (!DefaultCommandInput<TParseResult>.TryProcess(InputSpec, parseResult, out DefaultCommandInput<TParseResult> commandInput, out IReadOnlyList<CommandInputProcessingIssue> processingIssues))
|
||||
{
|
||||
//If this is the right command, just not the right syntax, report the usage errors
|
||||
if (processingIssues.All(x => x.Kind != CommandInputProcessingIssueKind.CommandMismatch))
|
||||
{
|
||||
foreach (CommandInputProcessingIssue issue in processingIssues)
|
||||
{
|
||||
shellState.ConsoleManager.Error.WriteLine(GetStringForIssue(issue));
|
||||
}
|
||||
|
||||
string help = GetHelpDetails(shellState, programState, parseResult);
|
||||
shellState.ConsoleManager.WriteLine(help);
|
||||
return false;
|
||||
}
|
||||
|
||||
//If there was a mismatch in the command name, this isn't our input to handle
|
||||
return null;
|
||||
}
|
||||
|
||||
return CanHandle(shellState, programState, commandInput);
|
||||
}
|
||||
|
||||
protected virtual bool CanHandle(IShellState shellState, TProgramState programState, DefaultCommandInput<TParseResult> commandInput)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected virtual string GetStringForIssue(CommandInputProcessingIssue issue)
|
||||
{
|
||||
//TODO: Make this nicer
|
||||
return issue.Kind + " -- " + issue.Text;
|
||||
}
|
||||
|
||||
public Task ExecuteAsync(IShellState shellState, TProgramState programState, TParseResult parseResult, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!DefaultCommandInput<TParseResult>.TryProcess(InputSpec, parseResult, out DefaultCommandInput<TParseResult> commandInput, out IReadOnlyList<CommandInputProcessingIssue> _))
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return ExecuteAsync(shellState, programState, commandInput, parseResult, cancellationToken);
|
||||
}
|
||||
|
||||
protected abstract Task ExecuteAsync(IShellState shellState, TProgramState programState, DefaultCommandInput<TParseResult> commandInput, TParseResult parseResult, CancellationToken cancellationToken);
|
||||
|
||||
protected abstract CommandInputSpecification InputSpec { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Repl.ConsoleHandling;
|
||||
using Microsoft.Repl.Parsing;
|
||||
|
||||
namespace Microsoft.Repl.Commanding
|
||||
{
|
||||
public static class DefaultCommandDispatcher
|
||||
{
|
||||
public static DefaultCommandDispatcher<TProgramState> Create<TProgramState>(Func<string> getPrompt, TProgramState programState)
|
||||
{
|
||||
return new DefaultCommandDispatcher<TProgramState>(getPrompt, programState);
|
||||
}
|
||||
|
||||
public static DefaultCommandDispatcher<TProgramState> Create<TProgramState>(Action<IShellState> onReady, TProgramState programState)
|
||||
{
|
||||
return new DefaultCommandDispatcher<TProgramState>(onReady, programState);
|
||||
}
|
||||
|
||||
public static DefaultCommandDispatcher<TProgramState, TParseResult> Create<TProgramState, TParseResult>(Func<string> getPrompt, TProgramState programState, IParser<TParseResult> parser)
|
||||
where TParseResult : ICoreParseResult
|
||||
{
|
||||
return new DefaultCommandDispatcher<TProgramState, TParseResult>(getPrompt, programState, parser);
|
||||
}
|
||||
|
||||
public static DefaultCommandDispatcher<TProgramState, TParseResult> Create<TProgramState, TParseResult>(Action<IShellState> onReady, TProgramState programState, IParser<TParseResult> parser)
|
||||
where TParseResult : ICoreParseResult
|
||||
{
|
||||
return new DefaultCommandDispatcher<TProgramState, TParseResult>(onReady, programState, parser);
|
||||
}
|
||||
}
|
||||
|
||||
public class DefaultCommandDispatcher<TProgramState> : DefaultCommandDispatcher<TProgramState, ICoreParseResult>
|
||||
{
|
||||
public DefaultCommandDispatcher(Func<string> getPrompt, TProgramState programState)
|
||||
: base(getPrompt, programState, new CoreParser())
|
||||
{
|
||||
}
|
||||
|
||||
public DefaultCommandDispatcher(Action<IShellState> onReady, TProgramState programState)
|
||||
: base(onReady, programState, new CoreParser())
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class DefaultCommandDispatcher<TProgramState, TParseResult> : ICommandDispatcher<TProgramState, TParseResult>
|
||||
where TParseResult : ICoreParseResult
|
||||
{
|
||||
private readonly Action<IShellState> _onReady;
|
||||
private readonly TProgramState _programState;
|
||||
private readonly IParser<TParseResult> _parser;
|
||||
private readonly HashSet<ICommand<TProgramState, TParseResult>> _commands = new HashSet<ICommand<TProgramState, TParseResult>>();
|
||||
private bool _isReady;
|
||||
|
||||
public DefaultCommandDispatcher(Func<string> getPrompt, TProgramState programState, IParser<TParseResult> parser)
|
||||
: this(s => s.ConsoleManager.Write(getPrompt()), programState, parser)
|
||||
{
|
||||
}
|
||||
|
||||
public DefaultCommandDispatcher(Action<IShellState> onReady, TProgramState programState, IParser<TParseResult> parser)
|
||||
{
|
||||
_onReady = onReady;
|
||||
_programState = programState;
|
||||
_parser = parser;
|
||||
}
|
||||
|
||||
public void AddCommand(ICommand<TProgramState, TParseResult> command)
|
||||
{
|
||||
_commands.Add(command);
|
||||
}
|
||||
|
||||
public IEnumerable<ICommand<TProgramState, TParseResult>> Commands => _commands;
|
||||
|
||||
public IParser Parser => _parser;
|
||||
|
||||
public IReadOnlyList<string> CollectSuggesetions(IShellState shellState)
|
||||
{
|
||||
string line = shellState.InputManager.GetCurrentBuffer();
|
||||
TParseResult parseResult = _parser.Parse(line, shellState.ConsoleManager.CaretPosition);
|
||||
HashSet<string> suggestions = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (ICommand<TProgramState, TParseResult> command in _commands)
|
||||
{
|
||||
IEnumerable<string> commandSuggestions = command.Suggest(shellState, _programState, parseResult);
|
||||
|
||||
if (commandSuggestions != null)
|
||||
{
|
||||
suggestions.UnionWith(commandSuggestions);
|
||||
}
|
||||
}
|
||||
|
||||
return suggestions.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
}
|
||||
|
||||
public async Task ExecuteCommandAsync(IShellState shellState, CancellationToken cancellationToken)
|
||||
{
|
||||
_isReady = false;
|
||||
shellState.ConsoleManager.WriteLine();
|
||||
string commandText = shellState.InputManager.GetCurrentBuffer();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(commandText))
|
||||
{
|
||||
shellState.CommandHistory.AddCommand(shellState.InputManager.GetCurrentBuffer());
|
||||
|
||||
try
|
||||
{
|
||||
await ExecuteCommandInternalAsync(shellState, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
shellState.ConsoleManager.Error.WriteLine(ex.ToString().Bold().Red());
|
||||
}
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
shellState.ConsoleManager.Error.WriteLine("Execution was cancelled".Bold().Red());
|
||||
}
|
||||
}
|
||||
|
||||
if (!_isReady)
|
||||
{
|
||||
shellState.ConsoleManager.WriteLine();
|
||||
OnReady(shellState);
|
||||
}
|
||||
|
||||
shellState.InputManager.ResetInput();
|
||||
}
|
||||
|
||||
private async Task ExecuteCommandInternalAsync(IShellState shellState, CancellationToken cancellationToken)
|
||||
{
|
||||
string line = shellState.InputManager.GetCurrentBuffer();
|
||||
TParseResult parseResult = _parser.Parse(line, shellState.ConsoleManager.CaretPosition);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(parseResult.CommandText))
|
||||
{
|
||||
foreach (ICommand<TProgramState, TParseResult> command in _commands)
|
||||
{
|
||||
bool? result = command.CanHandle(shellState, _programState, parseResult);
|
||||
|
||||
if (result.HasValue)
|
||||
{
|
||||
if (result.Value)
|
||||
{
|
||||
await command.ExecuteAsync(shellState, _programState, parseResult, cancellationToken);
|
||||
}
|
||||
|
||||
//If the handler returned non-null, the input would be directed to it, but it's not valid input
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
shellState.ConsoleManager.Error.WriteLine("No matching command found".Red().Bold());
|
||||
}
|
||||
}
|
||||
|
||||
public void OnReady(IShellState shellState)
|
||||
{
|
||||
if (!_isReady)
|
||||
{
|
||||
_onReady(shellState);
|
||||
shellState.ConsoleManager.ResetCommandStart();
|
||||
_isReady = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.Repl.Parsing;
|
||||
|
||||
namespace Microsoft.Repl.Commanding
|
||||
{
|
||||
public class DefaultCommandInput<TParseResult>
|
||||
where TParseResult : ICoreParseResult
|
||||
{
|
||||
public DefaultCommandInput(IReadOnlyList<InputElement> commandName, IReadOnlyList<InputElement> arguments, IReadOnlyDictionary<string, IReadOnlyList<InputElement>> options, InputElement selectedElement)
|
||||
{
|
||||
CommandName = commandName;
|
||||
Arguments = arguments;
|
||||
Options = options;
|
||||
SelectedElement = selectedElement;
|
||||
}
|
||||
|
||||
public static bool TryProcess(CommandInputSpecification spec, TParseResult parseResult, out DefaultCommandInput<TParseResult> result, out IReadOnlyList<CommandInputProcessingIssue> processingIssues)
|
||||
{
|
||||
List<CommandInputProcessingIssue> issues = new List<CommandInputProcessingIssue>();
|
||||
List<InputElement> commandNameElements = new List<InputElement>();
|
||||
|
||||
if (spec.CommandName.Count > parseResult.Sections.Count)
|
||||
{
|
||||
issues.Add(new CommandInputProcessingIssue(CommandInputProcessingIssueKind.CommandMismatch, spec.CommandName[parseResult.Sections.Count]));
|
||||
}
|
||||
|
||||
for (int i = 0; i < spec.CommandName.Count && i < parseResult.Sections.Count; ++i)
|
||||
{
|
||||
if (!string.Equals(spec.CommandName[i], parseResult.Sections[i], StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
issues.Add(new CommandInputProcessingIssue(CommandInputProcessingIssueKind.CommandMismatch, parseResult.Sections[i]));
|
||||
}
|
||||
|
||||
commandNameElements.Add(new InputElement(CommandInputLocation.CommandName, parseResult.Sections[i], spec.CommandName[i], i));
|
||||
}
|
||||
|
||||
//If we have a command name mismatch, no point in continuing
|
||||
if (issues.Count > 0)
|
||||
{
|
||||
result = null;
|
||||
processingIssues = issues;
|
||||
return false;
|
||||
}
|
||||
|
||||
List<InputElement> arguments = new List<InputElement>();
|
||||
Dictionary<InputElement, InputElement> options = new Dictionary<InputElement, InputElement>();
|
||||
InputElement currentOption = null;
|
||||
CommandOptionSpecification currentOptionSpec = null;
|
||||
InputElement selectedElement = null;
|
||||
|
||||
for (int i = spec.CommandName.Count; i < parseResult.Sections.Count; ++i)
|
||||
{
|
||||
//If we're not looking at an option name
|
||||
if (!parseResult.Sections[i].StartsWith(spec.OptionPreamble) || parseResult.IsQuotedSection(i))
|
||||
{
|
||||
if (currentOption is null)
|
||||
{
|
||||
InputElement currentElement = new InputElement(CommandInputLocation.Argument, parseResult.Sections[i], parseResult.Sections[i], i);
|
||||
|
||||
if (i == parseResult.SelectedSection)
|
||||
{
|
||||
selectedElement = currentElement;
|
||||
}
|
||||
|
||||
arguments.Add(currentElement);
|
||||
}
|
||||
else
|
||||
{
|
||||
//If the option isn't a defined one or it is and indicates that it accepts a value, add the section as an option value,
|
||||
// otherwise add it as an argument
|
||||
if (currentOptionSpec?.AcceptsValue ?? true)
|
||||
{
|
||||
InputElement currentElement = new InputElement(currentOption, CommandInputLocation.OptionValue, parseResult.Sections[i], parseResult.Sections[i], i);
|
||||
|
||||
if (i == parseResult.SelectedSection)
|
||||
{
|
||||
selectedElement = currentElement;
|
||||
}
|
||||
|
||||
options[currentOption] = currentElement;
|
||||
currentOption = null;
|
||||
currentOptionSpec = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
InputElement currentElement = new InputElement(CommandInputLocation.Argument, parseResult.Sections[i], parseResult.Sections[i], i);
|
||||
|
||||
if (i == parseResult.SelectedSection)
|
||||
{
|
||||
selectedElement = currentElement;
|
||||
}
|
||||
|
||||
arguments.Add(currentElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
//If we are looking at an option name
|
||||
else
|
||||
{
|
||||
//Otherwise, check to see whether the previous option had a required argument before committing it
|
||||
if (!(currentOption is null))
|
||||
{
|
||||
options[currentOption] = null;
|
||||
|
||||
if (currentOptionSpec?.RequiresValue ?? false)
|
||||
{
|
||||
issues.Add(new CommandInputProcessingIssue(CommandInputProcessingIssueKind.MissingRequiredOptionInput, currentOption.Text));
|
||||
}
|
||||
}
|
||||
|
||||
CommandOptionSpecification optionSpec = spec.Options.FirstOrDefault(x => x.Forms.Any(y => string.Equals(y, parseResult.Sections[i], StringComparison.Ordinal)));
|
||||
|
||||
if (optionSpec is null)
|
||||
{
|
||||
issues.Add(new CommandInputProcessingIssue(CommandInputProcessingIssueKind.UnknownOption, parseResult.Sections[i]));
|
||||
}
|
||||
|
||||
currentOption = new InputElement(CommandInputLocation.OptionName, parseResult.Sections[i], optionSpec?.Id, i);
|
||||
|
||||
if (i == parseResult.SelectedSection)
|
||||
{
|
||||
selectedElement = currentOption;
|
||||
}
|
||||
|
||||
currentOptionSpec = optionSpec;
|
||||
}
|
||||
}
|
||||
|
||||
//Clear any option in progress
|
||||
if (!(currentOption is null))
|
||||
{
|
||||
options[currentOption] = null;
|
||||
|
||||
if (currentOptionSpec?.RequiresValue ?? false)
|
||||
{
|
||||
issues.Add(new CommandInputProcessingIssue(CommandInputProcessingIssueKind.MissingRequiredOptionInput, currentOption.Text));
|
||||
}
|
||||
}
|
||||
|
||||
//Check to make sure our argument count is in range, if not add an issue
|
||||
if (arguments.Count > spec.MaximumArguments || arguments.Count < spec.MinimumArguments)
|
||||
{
|
||||
issues.Add(new CommandInputProcessingIssue(CommandInputProcessingIssueKind.ArgumentCountOutOfRange, arguments.Count.ToString()));
|
||||
}
|
||||
|
||||
//Build up the dictionary of options by normal form, then validate counts for every option in the spec
|
||||
Dictionary<string, IReadOnlyList<InputElement>> optionsByNormalForm = new Dictionary<string, IReadOnlyList<InputElement>>(StringComparer.Ordinal);
|
||||
|
||||
foreach (KeyValuePair<InputElement, InputElement> entry in options)
|
||||
{
|
||||
if (entry.Key.NormalizedText is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!optionsByNormalForm.TryGetValue(entry.Key.NormalizedText, out IReadOnlyList<InputElement> rawBucket))
|
||||
{
|
||||
optionsByNormalForm[entry.Key.NormalizedText] = rawBucket = new List<InputElement>();
|
||||
}
|
||||
|
||||
List<InputElement> bucket = (List<InputElement>) rawBucket;
|
||||
bucket.Add(entry.Value);
|
||||
}
|
||||
|
||||
foreach (CommandOptionSpecification optionSpec in spec.Options)
|
||||
{
|
||||
if (!optionsByNormalForm.TryGetValue(optionSpec.Id, out IReadOnlyList<InputElement> values))
|
||||
{
|
||||
optionsByNormalForm[optionSpec.Id] = values = new List<InputElement>();
|
||||
}
|
||||
|
||||
if (values.Count < optionSpec.MinimumOccurrences || values.Count > optionSpec.MaximumOccurrences)
|
||||
{
|
||||
issues.Add(new CommandInputProcessingIssue(CommandInputProcessingIssueKind.OptionUseCountOutOfRange, values.Count.ToString()));
|
||||
}
|
||||
}
|
||||
|
||||
result = new DefaultCommandInput<TParseResult>(commandNameElements, arguments, optionsByNormalForm, selectedElement);
|
||||
processingIssues = issues;
|
||||
return issues.Count == 0;
|
||||
}
|
||||
|
||||
public InputElement SelectedElement { get; }
|
||||
|
||||
public IReadOnlyList<InputElement> CommandName { get; }
|
||||
|
||||
public IReadOnlyList<InputElement> Arguments { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, IReadOnlyList<InputElement>> Options { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Repl.Parsing;
|
||||
|
||||
namespace Microsoft.Repl.Commanding
|
||||
{
|
||||
public interface ICommand<in TProgramState, in TParseResult>
|
||||
where TParseResult : ICoreParseResult
|
||||
{
|
||||
string GetHelpSummary(IShellState shellState, TProgramState programState);
|
||||
|
||||
string GetHelpDetails(IShellState shellState, TProgramState programState, TParseResult parseResult);
|
||||
|
||||
IEnumerable<string> Suggest(IShellState shellState, TProgramState programState, TParseResult parseResult);
|
||||
|
||||
bool? CanHandle(IShellState shellState, TProgramState programState, TParseResult parseResult);
|
||||
|
||||
Task ExecuteAsync(IShellState shellState, TProgramState programState, TParseResult parseResult, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Repl.Parsing;
|
||||
|
||||
namespace Microsoft.Repl.Commanding
|
||||
{
|
||||
public interface ICommandDispatcher
|
||||
{
|
||||
IParser Parser { get; }
|
||||
|
||||
IReadOnlyList<string> CollectSuggesetions(IShellState shellState);
|
||||
|
||||
void OnReady(IShellState shellState);
|
||||
|
||||
Task ExecuteCommandAsync(IShellState shellState, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
public interface ICommandDispatcher<in TProgramState, in TParseResult> : ICommandDispatcher
|
||||
where TParseResult : ICoreParseResult
|
||||
{
|
||||
IEnumerable<ICommand<TProgramState, TParseResult>> Commands { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
using System;
|
||||
|
||||
namespace Microsoft.Repl.Commanding
|
||||
{
|
||||
public interface ICommandHistory
|
||||
{
|
||||
string GetPreviousCommand();
|
||||
|
||||
string GetNextCommand();
|
||||
|
||||
void AddCommand(string command);
|
||||
|
||||
IDisposable SuspendHistory();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
namespace Microsoft.Repl.Commanding
|
||||
{
|
||||
public class InputElement
|
||||
{
|
||||
public CommandInputLocation Location { get; }
|
||||
|
||||
public string Text { get; }
|
||||
|
||||
public string NormalizedText { get; }
|
||||
|
||||
public InputElement Owner { get; }
|
||||
|
||||
public int ParseResultSectionIndex { get; }
|
||||
|
||||
public InputElement(CommandInputLocation location, string text, string normalizedText, int sectionIndex)
|
||||
: this(null, location, text, normalizedText, sectionIndex)
|
||||
{
|
||||
}
|
||||
|
||||
public InputElement(InputElement owner, CommandInputLocation location, string text, string normalizedText, int sectionIndex)
|
||||
{
|
||||
Owner = owner;
|
||||
Location = location;
|
||||
Text = text;
|
||||
NormalizedText = normalizedText;
|
||||
ParseResultSectionIndex = sectionIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
using System;
|
||||
|
||||
namespace Microsoft.Repl.ConsoleHandling
|
||||
{
|
||||
[Flags]
|
||||
public enum AllowedColors
|
||||
{
|
||||
Black = 0x00,
|
||||
BoldBlack = Bold | Black,
|
||||
Red = 0x01,
|
||||
BoldRed = Bold | Red,
|
||||
Green = 0x02,
|
||||
BoldGreen = Bold | Green,
|
||||
Yellow = 0x03,
|
||||
BoldYellow = Bold | Yellow,
|
||||
Blue = 0x04,
|
||||
BoldBlue = Bold | Blue,
|
||||
Magenta = 0x05,
|
||||
BoldMagenta = Bold | Magenta,
|
||||
Cyan = 0x06,
|
||||
BoldCyan = Bold | Cyan,
|
||||
White = 0x07,
|
||||
BoldWhite = White | Bold,
|
||||
Bold = 0x100,
|
||||
None = 0x99
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
namespace Microsoft.Repl.ConsoleHandling
|
||||
{
|
||||
public static class AnsiColorExtensions
|
||||
{
|
||||
public static string Black(this string text)
|
||||
{
|
||||
return "\x1B[30m" + text + "\x1B[39m";
|
||||
}
|
||||
|
||||
public static string Red(this string text)
|
||||
{
|
||||
return "\x1B[31m" + text + "\x1B[39m";
|
||||
}
|
||||
public static string Green(this string text)
|
||||
{
|
||||
return "\x1B[32m" + text + "\x1B[39m";
|
||||
}
|
||||
|
||||
public static string Yellow(this string text)
|
||||
{
|
||||
return "\x1B[33m" + text + "\x1B[39m";
|
||||
}
|
||||
|
||||
public static string Blue(this string text)
|
||||
{
|
||||
return "\x1B[34m" + text + "\x1B[39m";
|
||||
}
|
||||
|
||||
public static string Magenta(this string text)
|
||||
{
|
||||
return "\x1B[35m" + text + "\x1B[39m";
|
||||
}
|
||||
|
||||
public static string Cyan(this string text)
|
||||
{
|
||||
return "\x1B[36m" + text + "\x1B[39m";
|
||||
}
|
||||
|
||||
public static string White(this string text)
|
||||
{
|
||||
return "\x1B[37m" + text + "\x1B[39m";
|
||||
}
|
||||
|
||||
public static string Bold(this string text)
|
||||
{
|
||||
return "\x1B[1m" + text + "\x1B[22m";
|
||||
}
|
||||
|
||||
public static string SetColor(this string text, AllowedColors color)
|
||||
{
|
||||
if (color.HasFlag(AllowedColors.Bold))
|
||||
{
|
||||
text = text.Bold();
|
||||
color = color & ~AllowedColors.Bold;
|
||||
}
|
||||
|
||||
switch (color)
|
||||
{
|
||||
case AllowedColors.Black:
|
||||
return text.Black();
|
||||
case AllowedColors.Red:
|
||||
return text.Red();
|
||||
case AllowedColors.Green:
|
||||
return text.Green();
|
||||
case AllowedColors.Yellow:
|
||||
return text.Yellow();
|
||||
case AllowedColors.Blue:
|
||||
return text.Blue();
|
||||
case AllowedColors.Magenta:
|
||||
return text.Magenta();
|
||||
case AllowedColors.Cyan:
|
||||
return text.Cyan();
|
||||
case AllowedColors.White:
|
||||
return text.White();
|
||||
default:
|
||||
return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,151 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace Microsoft.Repl.ConsoleHandling
|
||||
{
|
||||
public class AnsiConsole
|
||||
{
|
||||
private AnsiConsole(TextWriter writer)
|
||||
{
|
||||
Writer = writer;
|
||||
|
||||
OriginalForegroundColor = Console.ForegroundColor;
|
||||
}
|
||||
|
||||
private int _boldRecursion;
|
||||
|
||||
public static AnsiConsole GetOutput()
|
||||
{
|
||||
return new AnsiConsole(Console.Out);
|
||||
}
|
||||
|
||||
public static AnsiConsole GetError()
|
||||
{
|
||||
return new AnsiConsole(Console.Error);
|
||||
}
|
||||
|
||||
public TextWriter Writer { get; }
|
||||
|
||||
public ConsoleColor OriginalForegroundColor { get; }
|
||||
|
||||
private void SetColor(ConsoleColor color)
|
||||
{
|
||||
const int light = 0x08;
|
||||
int c = (int)color;
|
||||
|
||||
Console.ForegroundColor =
|
||||
c < 0 ? color : // unknown, just use it
|
||||
_boldRecursion > 0 ? (ConsoleColor)(c | light) : // ensure color is light
|
||||
(ConsoleColor)(c & ~light); // ensure color is dark
|
||||
}
|
||||
|
||||
private void SetBold(bool bold)
|
||||
{
|
||||
_boldRecursion += bold ? 1 : -1;
|
||||
if (_boldRecursion > 1 || (_boldRecursion == 1 && !bold))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// switches on _boldRecursion to handle boldness
|
||||
SetColor(Console.ForegroundColor);
|
||||
}
|
||||
|
||||
public void WriteLine(string message)
|
||||
{
|
||||
Write(message);
|
||||
Writer.WriteLine();
|
||||
}
|
||||
|
||||
public void Write(char message)
|
||||
{
|
||||
Writer.Write(message);
|
||||
}
|
||||
|
||||
public void Write(string message)
|
||||
{
|
||||
if (message is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var escapeScan = 0;
|
||||
for (; ; )
|
||||
{
|
||||
var escapeIndex = message.IndexOf("\x1b[", escapeScan, StringComparison.Ordinal);
|
||||
if (escapeIndex == -1)
|
||||
{
|
||||
var text = message.Substring(escapeScan);
|
||||
Writer.Write(text);
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
var startIndex = escapeIndex + 2;
|
||||
var endIndex = startIndex;
|
||||
while (endIndex != message.Length &&
|
||||
message[endIndex] >= 0x20 &&
|
||||
message[endIndex] <= 0x3f)
|
||||
{
|
||||
endIndex += 1;
|
||||
}
|
||||
|
||||
var text = message.Substring(escapeScan, escapeIndex - escapeScan);
|
||||
Writer.Write(text);
|
||||
if (endIndex == message.Length)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
switch (message[endIndex])
|
||||
{
|
||||
case 'm':
|
||||
if (int.TryParse(message.Substring(startIndex, endIndex - startIndex), out int value))
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case 1:
|
||||
SetBold(true);
|
||||
break;
|
||||
case 22:
|
||||
SetBold(false);
|
||||
break;
|
||||
case 30:
|
||||
SetColor(ConsoleColor.Black);
|
||||
break;
|
||||
case 31:
|
||||
SetColor(ConsoleColor.Red);
|
||||
break;
|
||||
case 32:
|
||||
SetColor(ConsoleColor.Green);
|
||||
break;
|
||||
case 33:
|
||||
SetColor(ConsoleColor.Yellow);
|
||||
break;
|
||||
case 34:
|
||||
SetColor(ConsoleColor.Blue);
|
||||
break;
|
||||
case 35:
|
||||
SetColor(ConsoleColor.Magenta);
|
||||
break;
|
||||
case 36:
|
||||
SetColor(ConsoleColor.Cyan);
|
||||
break;
|
||||
case 37:
|
||||
SetColor(ConsoleColor.Gray);
|
||||
break;
|
||||
case 39:
|
||||
Console.ForegroundColor = OriginalForegroundColor;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
escapeScan = endIndex + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace Microsoft.Repl.ConsoleHandling
|
||||
{
|
||||
public class ConsoleManager : IConsoleManager
|
||||
{
|
||||
private readonly List<Action> _breakHandlers = new List<Action>();
|
||||
|
||||
public Point Caret => new Point(Console.CursorLeft, Console.CursorTop);
|
||||
|
||||
public Point CommandStart => new Point(Caret.X - CaretPosition % Console.BufferWidth, Caret.Y - CaretPosition / Console.BufferWidth);
|
||||
|
||||
public int CaretPosition { get; private set; }
|
||||
|
||||
public bool IsKeyAvailable => Console.KeyAvailable;
|
||||
|
||||
public bool IsCaretVisible
|
||||
{
|
||||
get => Reporter.Output.IsCaretVisible;
|
||||
set => Reporter.Output.IsCaretVisible = value;
|
||||
}
|
||||
|
||||
public ConsoleManager()
|
||||
{
|
||||
Error = new Writable(CaretUpdateScope, Reporter.Error);
|
||||
Console.CancelKeyPress += OnCancelKeyPress;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
using (CaretUpdateScope())
|
||||
{
|
||||
Console.Clear();
|
||||
ResetCommandStart();
|
||||
}
|
||||
}
|
||||
|
||||
public void MoveCaret(int positions)
|
||||
{
|
||||
using (CaretUpdateScope())
|
||||
{
|
||||
if (positions == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
while (positions < 0 && CaretPosition > 0)
|
||||
{
|
||||
if (-positions > Console.BufferWidth)
|
||||
{
|
||||
if (Console.CursorTop == 0)
|
||||
{
|
||||
Console.CursorLeft = 0;
|
||||
positions = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
positions += Console.BufferWidth;
|
||||
--Console.CursorTop;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int remaining = Console.CursorLeft + positions;
|
||||
|
||||
if (remaining >= 0)
|
||||
{
|
||||
Console.CursorLeft = remaining;
|
||||
}
|
||||
else if (Console.CursorTop == 0)
|
||||
{
|
||||
Console.CursorLeft = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
--Console.CursorTop;
|
||||
Console.CursorLeft = Console.BufferWidth + remaining;
|
||||
}
|
||||
|
||||
positions = 0;
|
||||
}
|
||||
}
|
||||
|
||||
while (positions > 0)
|
||||
{
|
||||
if (positions > Console.BufferWidth)
|
||||
{
|
||||
positions -= Console.BufferWidth;
|
||||
++Console.CursorTop;
|
||||
}
|
||||
else
|
||||
{
|
||||
int spaceLeftOnLine = Console.BufferWidth - Console.CursorLeft - 1;
|
||||
if (positions > spaceLeftOnLine)
|
||||
{
|
||||
++Console.CursorTop;
|
||||
Console.CursorLeft = positions - spaceLeftOnLine - 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.CursorLeft += positions;
|
||||
}
|
||||
|
||||
positions = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ConsoleKeyInfo ReadKey(CancellationToken cancellationToken)
|
||||
{
|
||||
while (!Console.KeyAvailable && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
Thread.Sleep(2);
|
||||
}
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return default(ConsoleKeyInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Console.ReadKey(true);
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetCommandStart()
|
||||
{
|
||||
CaretPosition = 0;
|
||||
}
|
||||
|
||||
public void Write(char c)
|
||||
{
|
||||
using (CaretUpdateScope())
|
||||
{
|
||||
Reporter.Output.Write(c);
|
||||
}
|
||||
}
|
||||
|
||||
public void Write(string s)
|
||||
{
|
||||
using (CaretUpdateScope())
|
||||
{
|
||||
Reporter.Output.Write(s);
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteLine()
|
||||
{
|
||||
using (CaretUpdateScope())
|
||||
{
|
||||
Reporter.Output.WriteLine();
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteLine(string s)
|
||||
{
|
||||
if (s is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (CaretUpdateScope())
|
||||
{
|
||||
Reporter.Output.WriteLine(s);
|
||||
}
|
||||
}
|
||||
|
||||
public IDisposable AddBreakHandler(Action handler)
|
||||
{
|
||||
Disposable result = new Disposable(() => ReleaseBreakHandler(handler));
|
||||
_breakHandlers.Add(handler);
|
||||
return result;
|
||||
}
|
||||
|
||||
private IDisposable CaretUpdateScope()
|
||||
{
|
||||
Point currentCaret = Caret;
|
||||
return new Disposable(() =>
|
||||
{
|
||||
int y = Caret.Y - currentCaret.Y;
|
||||
int x = Caret.X - currentCaret.X;
|
||||
CaretPosition += y * Console.BufferWidth + x;
|
||||
});
|
||||
}
|
||||
|
||||
private void OnCancelKeyPress(object sender, ConsoleCancelEventArgs e)
|
||||
{
|
||||
e.Cancel = true;
|
||||
Action handler = _breakHandlers.LastOrDefault();
|
||||
handler?.Invoke();
|
||||
}
|
||||
|
||||
private void ReleaseBreakHandler(Action handler)
|
||||
{
|
||||
_breakHandlers.Remove(handler);
|
||||
}
|
||||
|
||||
public IWritable Error { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace Microsoft.Repl.ConsoleHandling
|
||||
{
|
||||
public interface IConsoleManager : IWritable
|
||||
{
|
||||
Point Caret { get; }
|
||||
|
||||
Point CommandStart { get; }
|
||||
|
||||
int CaretPosition { get; }
|
||||
|
||||
IWritable Error { get; }
|
||||
|
||||
bool IsKeyAvailable { get; }
|
||||
|
||||
void Clear();
|
||||
|
||||
void MoveCaret(int positions);
|
||||
|
||||
ConsoleKeyInfo ReadKey(CancellationToken cancellationToken);
|
||||
|
||||
void ResetCommandStart();
|
||||
|
||||
IDisposable AddBreakHandler(Action onBreak);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
namespace Microsoft.Repl.ConsoleHandling
|
||||
{
|
||||
public interface IWritable
|
||||
{
|
||||
void Write(char c);
|
||||
|
||||
void Write(string s);
|
||||
|
||||
void WriteLine();
|
||||
|
||||
void WriteLine(string s);
|
||||
|
||||
bool IsCaretVisible { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
namespace Microsoft.Repl.ConsoleHandling
|
||||
{
|
||||
public struct Point
|
||||
{
|
||||
public readonly int X;
|
||||
|
||||
public readonly int Y;
|
||||
|
||||
public Point(int x, int y)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
}
|
||||
|
||||
public static bool operator >(Point left, Point right)
|
||||
{
|
||||
return left.Y > right.Y || (left.Y == right.Y && right.X > left.X);
|
||||
}
|
||||
|
||||
public static bool operator <(Point left, Point right)
|
||||
{
|
||||
return left.Y < right.Y || (left.Y == right.Y && right.X < left.X);
|
||||
}
|
||||
|
||||
public static bool operator ==(Point left, Point right)
|
||||
{
|
||||
return left.X == right.X && left.Y == right.Y;
|
||||
}
|
||||
|
||||
public static bool operator !=(Point left, Point right)
|
||||
{
|
||||
return left.X != right.X || left.Y != right.Y;
|
||||
}
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
return obj is Point other && other.X == X && other.Y == Y;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return X ^ Y;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"(X={X}, Y={Y})";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
using System;
|
||||
|
||||
namespace Microsoft.Repl.ConsoleHandling
|
||||
{
|
||||
public class Reporter : IWritable
|
||||
{
|
||||
private static readonly Reporter NullReporter = new Reporter(null);
|
||||
private static readonly object Sync = new object();
|
||||
|
||||
private readonly AnsiConsole _console;
|
||||
|
||||
static Reporter()
|
||||
{
|
||||
Reset();
|
||||
}
|
||||
|
||||
private Reporter(AnsiConsole console)
|
||||
{
|
||||
_console = console;
|
||||
}
|
||||
|
||||
public static Reporter Output { get; private set; }
|
||||
public static Reporter Error { get; private set; }
|
||||
public static Reporter Verbose { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Resets the Reporters to write to the current Console Out/Error.
|
||||
/// </summary>
|
||||
public static void Reset()
|
||||
{
|
||||
lock (Sync)
|
||||
{
|
||||
Output = new Reporter(AnsiConsole.GetOutput());
|
||||
Error = new Reporter(AnsiConsole.GetError());
|
||||
Verbose = IsVerbose ?
|
||||
new Reporter(AnsiConsole.GetOutput()) :
|
||||
NullReporter;
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteLine(string message)
|
||||
{
|
||||
if (message is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (Sync)
|
||||
{
|
||||
if (ShouldPassAnsiCodesThrough)
|
||||
{
|
||||
_console?.Writer?.WriteLine(message);
|
||||
}
|
||||
else
|
||||
{
|
||||
_console?.WriteLine(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteLine()
|
||||
{
|
||||
lock (Sync)
|
||||
{
|
||||
_console?.Writer?.WriteLine();
|
||||
}
|
||||
}
|
||||
|
||||
public void Write(char message)
|
||||
{
|
||||
lock (Sync)
|
||||
{
|
||||
if (ShouldPassAnsiCodesThrough)
|
||||
{
|
||||
_console?.Writer?.Write(message);
|
||||
}
|
||||
else
|
||||
{
|
||||
_console?.Write(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Write(string message)
|
||||
{
|
||||
lock (Sync)
|
||||
{
|
||||
if (ShouldPassAnsiCodesThrough)
|
||||
{
|
||||
_console?.Writer?.Write(message);
|
||||
}
|
||||
else
|
||||
{
|
||||
_console?.Write(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsVerbose => bool.TryParse(Environment.GetEnvironmentVariable("DOTNET_CLI_CONTEXT_VERBOSE") ?? "false", out bool value) && value;
|
||||
|
||||
private bool ShouldPassAnsiCodesThrough => bool.TryParse(Environment.GetEnvironmentVariable("DOTNET_CLI_CONTEXT_ANSI_PASS_THRU") ?? "false", out bool value) && value;
|
||||
|
||||
private bool _isCaretVisible = true;
|
||||
|
||||
public bool IsCaretVisible
|
||||
{
|
||||
get => _isCaretVisible;
|
||||
set
|
||||
{
|
||||
Console.CursorVisible = value;
|
||||
_isCaretVisible = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
using System;
|
||||
|
||||
namespace Microsoft.Repl.ConsoleHandling
|
||||
{
|
||||
internal class Writable : IWritable
|
||||
{
|
||||
private readonly Func<IDisposable> _caretUpdater;
|
||||
private readonly Reporter _reporter;
|
||||
|
||||
public Writable(Func<IDisposable> caretUpdater, Reporter reporter)
|
||||
{
|
||||
_caretUpdater = caretUpdater;
|
||||
_reporter = reporter;
|
||||
}
|
||||
|
||||
public bool IsCaretVisible
|
||||
{
|
||||
get => _reporter.IsCaretVisible;
|
||||
set => _reporter.IsCaretVisible = value;
|
||||
}
|
||||
|
||||
public void Write(char c)
|
||||
{
|
||||
using (_caretUpdater())
|
||||
{
|
||||
_reporter.Write(c);
|
||||
}
|
||||
}
|
||||
|
||||
public void Write(string s)
|
||||
{
|
||||
using (_caretUpdater())
|
||||
{
|
||||
_reporter.Write(s);
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteLine()
|
||||
{
|
||||
using (_caretUpdater())
|
||||
{
|
||||
_reporter.WriteLine();
|
||||
}
|
||||
}
|
||||
|
||||
public void WriteLine(string s)
|
||||
{
|
||||
using (_caretUpdater())
|
||||
{
|
||||
_reporter.WriteLine(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
using System;
|
||||
|
||||
namespace Microsoft.Repl
|
||||
{
|
||||
public class Disposable : IDisposable
|
||||
{
|
||||
private Action _onDispose;
|
||||
|
||||
public Disposable(Action onDispose)
|
||||
{
|
||||
_onDispose = onDispose;
|
||||
}
|
||||
public virtual void Dispose()
|
||||
{
|
||||
_onDispose?.Invoke();
|
||||
_onDispose = null;
|
||||
}
|
||||
}
|
||||
|
||||
public class Disposable<T> : Disposable
|
||||
where T : class
|
||||
{
|
||||
public Disposable(T value, Action onDispose)
|
||||
: base (onDispose)
|
||||
{
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public T Value { get; private set; }
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
if (Value is IDisposable d)
|
||||
{
|
||||
d.Dispose();
|
||||
Value = null;
|
||||
}
|
||||
|
||||
base.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
using Microsoft.Repl.Commanding;
|
||||
using Microsoft.Repl.ConsoleHandling;
|
||||
using Microsoft.Repl.Input;
|
||||
using Microsoft.Repl.Suggestions;
|
||||
|
||||
namespace Microsoft.Repl
|
||||
{
|
||||
public interface IShellState
|
||||
{
|
||||
IInputManager InputManager { get; }
|
||||
|
||||
ICommandHistory CommandHistory { get; }
|
||||
|
||||
IConsoleManager ConsoleManager { get; }
|
||||
|
||||
ICommandDispatcher CommandDispatcher { get; }
|
||||
|
||||
ISuggestionManager SuggestionManager { get; }
|
||||
|
||||
bool IsExiting { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Repl.Input
|
||||
{
|
||||
public delegate Task AsyncKeyPressHandler(ConsoleKeyInfo keyInfo, IShellState state, CancellationToken cancellationToken);
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Repl.Input
|
||||
{
|
||||
public interface IInputManager
|
||||
{
|
||||
bool IsOverwriteMode { get; set; }
|
||||
|
||||
IInputManager RegisterKeyHandler(ConsoleKey key, AsyncKeyPressHandler handler);
|
||||
|
||||
void ResetInput();
|
||||
|
||||
Task StartAsync(IShellState state, CancellationToken cancellationToken);
|
||||
|
||||
void SetInput(IShellState state, string input);
|
||||
|
||||
string GetCurrentBuffer();
|
||||
|
||||
void RemovePreviousCharacter(IShellState state);
|
||||
|
||||
void RemoveCurrentCharacter(IShellState state);
|
||||
|
||||
void Clear(IShellState state);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,344 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Repl.Input
|
||||
{
|
||||
public class InputManager : IInputManager
|
||||
{
|
||||
private readonly Dictionary<ConsoleKey, AsyncKeyPressHandler> _handlers = new Dictionary<ConsoleKey, AsyncKeyPressHandler>();
|
||||
private readonly List<char> _inputBuffer = new List<char>();
|
||||
|
||||
public bool IsOverwriteMode { get; set; }
|
||||
|
||||
public void Clear(IShellState state)
|
||||
{
|
||||
SetInput(state, string.Empty);
|
||||
}
|
||||
|
||||
public string GetCurrentBuffer()
|
||||
{
|
||||
return _inputBuffer.Stringify();
|
||||
}
|
||||
|
||||
public IInputManager RegisterKeyHandler(ConsoleKey key, AsyncKeyPressHandler handler)
|
||||
{
|
||||
if (handler == null)
|
||||
{
|
||||
_handlers.Remove(key);
|
||||
}
|
||||
else
|
||||
{
|
||||
_handlers[key] = handler;
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public void RemoveCurrentCharacter(IShellState state)
|
||||
{
|
||||
int caret = state.ConsoleManager.CaretPosition;
|
||||
|
||||
if (caret == _inputBuffer.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<char> update = _inputBuffer.ToList();
|
||||
update.RemoveAt(caret);
|
||||
state.ConsoleManager.IsCaretVisible = false;
|
||||
SetInput(state, update);
|
||||
state.ConsoleManager.MoveCaret(caret - state.ConsoleManager.CaretPosition);
|
||||
state.ConsoleManager.IsCaretVisible = true;
|
||||
}
|
||||
|
||||
public void RemovePreviousCharacter(IShellState state)
|
||||
{
|
||||
int caret = state.ConsoleManager.CaretPosition;
|
||||
if (caret == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<char> update = _inputBuffer.ToList();
|
||||
update.RemoveAt(caret - 1);
|
||||
state.ConsoleManager.IsCaretVisible = false;
|
||||
SetInput(state, update, false);
|
||||
state.ConsoleManager.MoveCaret(caret - state.ConsoleManager.CaretPosition - 1);
|
||||
state.ConsoleManager.IsCaretVisible = true;
|
||||
}
|
||||
|
||||
public void SetInput(IShellState state, string input)
|
||||
{
|
||||
SetInput(state, input.ToCharArray());
|
||||
}
|
||||
|
||||
public void ResetInput()
|
||||
{
|
||||
_inputBuffer.Clear();
|
||||
}
|
||||
|
||||
private string _ttyState;
|
||||
|
||||
private void StashEchoState()
|
||||
{
|
||||
_ttyState = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.OSX)
|
||||
? GetTtyState()
|
||||
: null;
|
||||
|
||||
if (!string.IsNullOrEmpty(_ttyState))
|
||||
{
|
||||
//"gfmt1:cflag=4300:iflag=6b02:lflag=200005c7:oflag=3:discard=f:dsusp=19:eof=4:eol=ff:eol2=ff:erase=7f:intr=3:kill=15:lnext=16:min=1:quit=1c:reprint=12:start=11:status=14:stop=13:susp=1a:time=0:werase=17:ispeed=38400:ospeed=38400\n"
|
||||
ProcessStartInfo psi = new ProcessStartInfo("stty", "gfmt1:erase=08:werase=08 -echo");
|
||||
Process p = Process.Start(psi);
|
||||
p?.WaitForExit();
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetTtyState()
|
||||
{
|
||||
ProcessStartInfo psi = new ProcessStartInfo("stty", "-g")
|
||||
{
|
||||
RedirectStandardOutput = true
|
||||
};
|
||||
Process p = Process.Start(psi);
|
||||
p?.WaitForExit();
|
||||
string result = p?.StandardOutput.ReadToEnd();
|
||||
return result;
|
||||
}
|
||||
|
||||
private void RestoreTtyState()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(_ttyState))
|
||||
{
|
||||
ProcessStartInfo psi = new ProcessStartInfo("stty", _ttyState);
|
||||
Process p = Process.Start(psi);
|
||||
p?.WaitForExit();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetInput(IShellState state, IReadOnlyList<char> input, bool moveCaret = true)
|
||||
{
|
||||
bool oldCaretVisibility = state.ConsoleManager.IsCaretVisible;
|
||||
state.ConsoleManager.IsCaretVisible = false;
|
||||
int lastCommonPosition = 0;
|
||||
|
||||
for (; lastCommonPosition < input.Count && lastCommonPosition < _inputBuffer.Count && _inputBuffer[lastCommonPosition] == input[lastCommonPosition]; ++lastCommonPosition)
|
||||
{
|
||||
}
|
||||
|
||||
state.ConsoleManager.MoveCaret(-state.ConsoleManager.CaretPosition + lastCommonPosition);
|
||||
string str = new string(input.Skip(lastCommonPosition).ToArray());
|
||||
int trailing = _inputBuffer.Count - input.Count;
|
||||
|
||||
if (trailing > 0)
|
||||
{
|
||||
str = str.PadRight(trailing + str.Length);
|
||||
}
|
||||
|
||||
state.ConsoleManager.Write(str);
|
||||
|
||||
if (trailing > 0 && moveCaret)
|
||||
{
|
||||
state.ConsoleManager.MoveCaret(-trailing);
|
||||
}
|
||||
|
||||
_inputBuffer.Clear();
|
||||
_inputBuffer.AddRange(input);
|
||||
|
||||
if (oldCaretVisibility)
|
||||
{
|
||||
state.ConsoleManager.IsCaretVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StartAsync(IShellState state, CancellationToken cancellationToken)
|
||||
{
|
||||
StashEchoState();
|
||||
|
||||
try
|
||||
{
|
||||
List<ConsoleKeyInfo> presses = null;
|
||||
while (!state.IsExiting && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
ConsoleKeyInfo keyPress = state.ConsoleManager.ReadKey(cancellationToken);
|
||||
|
||||
if (_handlers.TryGetValue(keyPress.Key, out AsyncKeyPressHandler handler))
|
||||
{
|
||||
using (CancellationTokenSource source = new CancellationTokenSource())
|
||||
using (state.ConsoleManager.AddBreakHandler(() => source.Cancel()))
|
||||
{
|
||||
if (presses != null)
|
||||
{
|
||||
FlushInput(state, ref presses);
|
||||
}
|
||||
|
||||
await handler(keyPress, state, source.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(_ttyState) && keyPress.Modifiers == ConsoleModifiers.Control)
|
||||
{
|
||||
if (presses != null)
|
||||
{
|
||||
FlushInput(state, ref presses);
|
||||
}
|
||||
|
||||
if (keyPress.Key == ConsoleKey.A)
|
||||
{
|
||||
state.ConsoleManager.MoveCaret(-state.ConsoleManager.CaretPosition);
|
||||
}
|
||||
else if (keyPress.Key == ConsoleKey.E)
|
||||
{
|
||||
state.ConsoleManager.MoveCaret(_inputBuffer.Count - state.ConsoleManager.CaretPosition);
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(_ttyState) && keyPress.Modifiers == ConsoleModifiers.Alt)
|
||||
{
|
||||
if (presses != null)
|
||||
{
|
||||
FlushInput(state, ref presses);
|
||||
}
|
||||
|
||||
//Move back a word
|
||||
if (keyPress.Key == ConsoleKey.B)
|
||||
{
|
||||
int i = state.ConsoleManager.CaretPosition - 1;
|
||||
|
||||
if (i < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bool letterMode = char.IsLetterOrDigit(_inputBuffer[i]);
|
||||
|
||||
for (; i > 0 && (char.IsLetterOrDigit(_inputBuffer[i]) == letterMode); --i)
|
||||
{
|
||||
}
|
||||
|
||||
if (letterMode && i > 0)
|
||||
{
|
||||
++i;
|
||||
}
|
||||
|
||||
if (i > -1)
|
||||
{
|
||||
state.ConsoleManager.MoveCaret(i - state.ConsoleManager.CaretPosition);
|
||||
}
|
||||
}
|
||||
//Move forward a word
|
||||
else if (keyPress.Key == ConsoleKey.F)
|
||||
{
|
||||
int i = state.ConsoleManager.CaretPosition + 1;
|
||||
|
||||
if (i >= _inputBuffer.Count)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bool letterMode = char.IsLetterOrDigit(_inputBuffer[i]);
|
||||
|
||||
for (; i < _inputBuffer.Count && (char.IsLetterOrDigit(_inputBuffer[i]) == letterMode); ++i)
|
||||
{
|
||||
}
|
||||
|
||||
if (letterMode && i < _inputBuffer.Count - 1 && i > 0)
|
||||
{
|
||||
--i;
|
||||
}
|
||||
|
||||
state.ConsoleManager.MoveCaret(i - state.ConsoleManager.CaretPosition);
|
||||
}
|
||||
}
|
||||
else if (!keyPress.Modifiers.HasFlag(ConsoleModifiers.Alt) && !keyPress.Modifiers.HasFlag(ConsoleModifiers.Control))
|
||||
{
|
||||
if (state.ConsoleManager.IsKeyAvailable)
|
||||
{
|
||||
if (presses == null)
|
||||
{
|
||||
presses = new List<ConsoleKeyInfo>();
|
||||
}
|
||||
|
||||
presses.Add(keyPress);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (presses != null)
|
||||
{
|
||||
presses.Add(keyPress);
|
||||
FlushInput(state, ref presses);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (state.ConsoleManager.CaretPosition == _inputBuffer.Count)
|
||||
{
|
||||
_inputBuffer.Add(keyPress.KeyChar);
|
||||
state.ConsoleManager.Write(keyPress.KeyChar);
|
||||
}
|
||||
else if (IsOverwriteMode)
|
||||
{
|
||||
_inputBuffer[state.ConsoleManager.CaretPosition] = keyPress.KeyChar;
|
||||
state.ConsoleManager.Write(keyPress.KeyChar);
|
||||
}
|
||||
else
|
||||
{
|
||||
state.ConsoleManager.IsCaretVisible = false;
|
||||
_inputBuffer.Insert(state.ConsoleManager.CaretPosition, keyPress.KeyChar);
|
||||
int currentCaretPosition = state.ConsoleManager.CaretPosition;
|
||||
string s = new string(_inputBuffer.ToArray(), state.ConsoleManager.CaretPosition, _inputBuffer.Count - state.ConsoleManager.CaretPosition);
|
||||
state.ConsoleManager.Write(s);
|
||||
state.ConsoleManager.MoveCaret(currentCaretPosition - state.ConsoleManager.CaretPosition + 1);
|
||||
state.ConsoleManager.IsCaretVisible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
RestoreTtyState();
|
||||
}
|
||||
}
|
||||
|
||||
private void FlushInput(IShellState state, ref List<ConsoleKeyInfo> presses)
|
||||
{
|
||||
string str = new string(presses.Select(x => x.KeyChar).ToArray());
|
||||
|
||||
if (state.ConsoleManager.CaretPosition == _inputBuffer.Count)
|
||||
{
|
||||
_inputBuffer.AddRange(str);
|
||||
state.ConsoleManager.Write(str);
|
||||
}
|
||||
else if (IsOverwriteMode)
|
||||
{
|
||||
for (int i = 0; i < str.Length; ++i)
|
||||
{
|
||||
if (state.ConsoleManager.CaretPosition + i < _inputBuffer.Count)
|
||||
{
|
||||
_inputBuffer[state.ConsoleManager.CaretPosition + i] = str[i];
|
||||
}
|
||||
else
|
||||
{
|
||||
_inputBuffer.AddRange(str.Skip(i));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
state.ConsoleManager.Write(str);
|
||||
}
|
||||
else
|
||||
{
|
||||
state.ConsoleManager.IsCaretVisible = false;
|
||||
_inputBuffer.InsertRange(state.ConsoleManager.CaretPosition, str);
|
||||
int currentCaretPosition = state.ConsoleManager.CaretPosition;
|
||||
string s = new string(_inputBuffer.ToArray(), state.ConsoleManager.CaretPosition, _inputBuffer.Count - state.ConsoleManager.CaretPosition);
|
||||
state.ConsoleManager.Write(s);
|
||||
state.ConsoleManager.MoveCaret(currentCaretPosition - state.ConsoleManager.CaretPosition + str.Length);
|
||||
state.ConsoleManager.IsCaretVisible = true;
|
||||
}
|
||||
|
||||
presses = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,239 @@
|
|||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Repl.Parsing;
|
||||
|
||||
namespace Microsoft.Repl.Input
|
||||
{
|
||||
public static class KeyHandlers
|
||||
{
|
||||
public static void RegisterDefaultKeyHandlers(IInputManager inputManager)
|
||||
{
|
||||
//Navigation in line
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.LeftArrow, LeftArrow);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.RightArrow, RightArrow);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.Home, Home);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.End, End);
|
||||
|
||||
//Command history
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.UpArrow, UpArrow);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.DownArrow, DownArrow);
|
||||
|
||||
//Completion
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.Tab, Tab);
|
||||
|
||||
//Input manipulation
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.Escape, Escape);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.Delete, Delete);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.Backspace, Backspace);
|
||||
|
||||
//Insert/Overwrite mode
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.Insert, Insert);
|
||||
|
||||
//Execute command
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.Enter, Enter);
|
||||
|
||||
//Map non-printable keys that aren't handled by default
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.F1, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.F2, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.F3, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.F4, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.F5, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.F6, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.F7, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.F8, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.F9, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.F10, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.F11, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.F12, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.F13, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.F14, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.F15, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.F16, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.F17, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.F18, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.F19, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.F20, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.F21, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.F22, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.F23, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.F24, Unhandled);
|
||||
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.Applications, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.Attention, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.BrowserBack, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.BrowserFavorites, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.BrowserForward, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.BrowserHome, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.BrowserRefresh, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.BrowserSearch, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.BrowserStop, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.Clear, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.CrSel, Unhandled);
|
||||
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.EraseEndOfFile, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.Execute, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.ExSel, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.Help, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.LaunchApp1, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.LaunchApp2, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.LaunchMail, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.LaunchMediaSelect, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.LeftWindows, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.MediaNext, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.MediaPlay, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.MediaPrevious, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.MediaStop, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.NoName, Unhandled);
|
||||
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.Pa1, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.Packet, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.PageDown, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.PageUp, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.Pause, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.Play, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.Print, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.PrintScreen, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.Process, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.RightWindows, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.Select, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.Separator, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.Sleep, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.VolumeDown, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.VolumeMute, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.VolumeUp, Unhandled);
|
||||
inputManager.RegisterKeyHandler(ConsoleKey.Zoom, Unhandled);
|
||||
}
|
||||
|
||||
private static Task End(ConsoleKeyInfo keyInfo, IShellState state, CancellationToken cancellationToken)
|
||||
{
|
||||
state.ConsoleManager.MoveCaret(state.InputManager.GetCurrentBuffer().Length - state.ConsoleManager.CaretPosition);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public static Task Home(ConsoleKeyInfo keyInfo, IShellState state, CancellationToken cancellationToken)
|
||||
{
|
||||
state.ConsoleManager.MoveCaret(-state.ConsoleManager.CaretPosition);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public static Task LeftArrow(ConsoleKeyInfo keyInfo, IShellState state, CancellationToken cancellationToken)
|
||||
{
|
||||
if (state.ConsoleManager.CaretPosition > 0)
|
||||
{
|
||||
if (!keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control))
|
||||
{
|
||||
state.ConsoleManager.MoveCaret(-1);
|
||||
}
|
||||
else
|
||||
{
|
||||
string line = state.InputManager.GetCurrentBuffer();
|
||||
ICoreParseResult parseResult = state.CommandDispatcher.Parser.Parse(line, state.ConsoleManager.CaretPosition);
|
||||
int targetSection = parseResult.SelectedSection - (parseResult.CaretPositionWithinSelectedSection > 0 ? 0 : 1);
|
||||
|
||||
if (targetSection < 0)
|
||||
{
|
||||
targetSection = 0;
|
||||
}
|
||||
|
||||
int desiredPosition = parseResult.SectionStartLookup[targetSection];
|
||||
state.ConsoleManager.MoveCaret(desiredPosition - state.ConsoleManager.CaretPosition);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public static Task RightArrow(ConsoleKeyInfo keyInfo, IShellState state, CancellationToken cancellationToken)
|
||||
{
|
||||
string line = state.InputManager.GetCurrentBuffer();
|
||||
|
||||
if (state.ConsoleManager.CaretPosition < line.Length)
|
||||
{
|
||||
if (!keyInfo.Modifiers.HasFlag(ConsoleModifiers.Control))
|
||||
{
|
||||
state.ConsoleManager.MoveCaret(1);
|
||||
}
|
||||
else
|
||||
{
|
||||
ICoreParseResult parseResult = state.CommandDispatcher.Parser.Parse(line, state.ConsoleManager.CaretPosition);
|
||||
int targetSection = parseResult.SelectedSection + 1;
|
||||
|
||||
if (targetSection >= parseResult.Sections.Count)
|
||||
{
|
||||
state.ConsoleManager.MoveCaret(line.Length - state.ConsoleManager.CaretPosition);
|
||||
}
|
||||
else
|
||||
{
|
||||
int desiredPosition = parseResult.SectionStartLookup[targetSection];
|
||||
state.ConsoleManager.MoveCaret(desiredPosition - state.ConsoleManager.CaretPosition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public static Task UpArrow(ConsoleKeyInfo keyInfo, IShellState state, CancellationToken cancellationToken)
|
||||
{
|
||||
string line = state.CommandHistory.GetPreviousCommand();
|
||||
state.InputManager.SetInput(state, line);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public static Task DownArrow(ConsoleKeyInfo keyInfo, IShellState state, CancellationToken cancellationToken)
|
||||
{
|
||||
string line = state.CommandHistory.GetNextCommand();
|
||||
state.InputManager.SetInput(state, line);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public static Task Enter(ConsoleKeyInfo keyInfo, IShellState state, CancellationToken cancellationToken)
|
||||
{
|
||||
return state.CommandDispatcher.ExecuteCommandAsync(state, cancellationToken);
|
||||
}
|
||||
|
||||
public static Task Backspace(ConsoleKeyInfo keyInfo, IShellState state, CancellationToken cancellationToken)
|
||||
{
|
||||
state.InputManager.RemovePreviousCharacter(state);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public static Task Unhandled(ConsoleKeyInfo keyInfo, IShellState state, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public static Task Escape(ConsoleKeyInfo keyInfo, IShellState state, CancellationToken cancellationToken)
|
||||
{
|
||||
state.InputManager.SetInput(state, string.Empty);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public static Task Tab(ConsoleKeyInfo keyInfo, IShellState state, CancellationToken cancellationToken)
|
||||
{
|
||||
if (keyInfo.Modifiers.HasFlag(ConsoleModifiers.Shift))
|
||||
{
|
||||
state.SuggestionManager.PreviousSuggestion(state);
|
||||
}
|
||||
else
|
||||
{
|
||||
state.SuggestionManager.NextSuggestion(state);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public static Task Delete(ConsoleKeyInfo keyInfo, IShellState state, CancellationToken cancellationToken)
|
||||
{
|
||||
state.InputManager.RemoveCurrentCharacter(state);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public static Task Insert(ConsoleKeyInfo keyInfo, IShellState state, CancellationToken cancellationToken)
|
||||
{
|
||||
state.InputManager.IsOverwriteMode = !state.InputManager.IsOverwriteMode;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.2</TargetFramework>
|
||||
<Description>A framework for creating REPLs in .NET Core.</Description>
|
||||
<PackageTags>dotnet;repl</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.Repl.Parsing
|
||||
{
|
||||
public class CoreParseResult : ICoreParseResult
|
||||
{
|
||||
public CoreParseResult(int caretPositionWithinCommandText, int caretPositionWithinSelectedSection, string commandText, IReadOnlyList<string> sections, int selectedSection, IReadOnlyDictionary<int, int> sectionStartLookup, HashSet<int> quotedSections)
|
||||
{
|
||||
CaretPositionWithinCommandText = caretPositionWithinCommandText;
|
||||
CaretPositionWithinSelectedSection = caretPositionWithinSelectedSection;
|
||||
CommandText = commandText;
|
||||
Sections = sections;
|
||||
SelectedSection = selectedSection;
|
||||
SectionStartLookup = sectionStartLookup;
|
||||
_quotedSections = quotedSections;
|
||||
}
|
||||
|
||||
public int CaretPositionWithinCommandText { get; }
|
||||
|
||||
public int CaretPositionWithinSelectedSection { get; }
|
||||
|
||||
public string CommandText { get; }
|
||||
|
||||
public IReadOnlyList<string> Sections { get; }
|
||||
|
||||
public int SelectedSection { get; }
|
||||
|
||||
public IReadOnlyDictionary<int, int> SectionStartLookup { get; }
|
||||
|
||||
private readonly HashSet<int> _quotedSections;
|
||||
|
||||
public bool IsQuotedSection(int index)
|
||||
{
|
||||
return _quotedSections.Contains(index);
|
||||
}
|
||||
|
||||
public virtual ICoreParseResult Slice(int numberOfLeadingSectionsToRemove)
|
||||
{
|
||||
if (numberOfLeadingSectionsToRemove == 0)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
if (numberOfLeadingSectionsToRemove >= Sections.Count)
|
||||
{
|
||||
return new CoreParseResult(0, 0, string.Empty, new[] { string.Empty }, 0, new Dictionary<int, int> { { 0, 0 } }, new HashSet<int>());
|
||||
}
|
||||
|
||||
string commandText = CommandText.Substring(SectionStartLookup[numberOfLeadingSectionsToRemove]);
|
||||
int caretPositionWithinCommandText = CaretPositionWithinCommandText - SectionStartLookup[numberOfLeadingSectionsToRemove];
|
||||
|
||||
if (caretPositionWithinCommandText < 0)
|
||||
{
|
||||
caretPositionWithinCommandText = 0;
|
||||
}
|
||||
|
||||
Dictionary<int, int> sectionStartLookup = new Dictionary<int, int>();
|
||||
List<string> sections = new List<string>();
|
||||
for (int i = 0; i < Sections.Count - numberOfLeadingSectionsToRemove; ++i)
|
||||
{
|
||||
sectionStartLookup[i] = SectionStartLookup[numberOfLeadingSectionsToRemove + i] - SectionStartLookup[numberOfLeadingSectionsToRemove];
|
||||
sections.Add(Sections[numberOfLeadingSectionsToRemove + i]);
|
||||
}
|
||||
|
||||
int selectedSection = SelectedSection - numberOfLeadingSectionsToRemove;
|
||||
|
||||
if (selectedSection < 0)
|
||||
{
|
||||
selectedSection = 0;
|
||||
}
|
||||
|
||||
HashSet<int> quotedSections = new HashSet<int>(_quotedSections.Where(x => x > 0).Select(x => x - 1));
|
||||
return new CoreParseResult(caretPositionWithinCommandText, CaretPositionWithinSelectedSection, commandText, sections, selectedSection, sectionStartLookup, quotedSections);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.Repl.Parsing
|
||||
{
|
||||
public class CoreParser : IParser<ICoreParseResult>
|
||||
{
|
||||
public ICoreParseResult Parse(string commandText, int caretPosition)
|
||||
{
|
||||
List<string> sections = commandText.Split(' ').ToList();
|
||||
Dictionary<int, int> sectionStartLookup = new Dictionary<int, int>();
|
||||
HashSet<int> quotedSections = new HashSet<int>();
|
||||
int runningIndex = 0;
|
||||
int selectedSection = -1;
|
||||
int caretPositionWithinSelectedSection = 0;
|
||||
bool isInQuotedSection = false;
|
||||
|
||||
for (int i = 0; i < sections.Count; ++i)
|
||||
{
|
||||
int thisSectionLength = sections[i].Length;
|
||||
bool isLastSection = i == sections.Count - 1;
|
||||
|
||||
//If currently in a quoted section, combine with the previous section, check to see if this section closes the quotes
|
||||
if (isInQuotedSection)
|
||||
{
|
||||
//Combine with the previous section
|
||||
sections[i - 1] += " " + sections[i];
|
||||
sections.RemoveAt(i--);
|
||||
|
||||
//Check for the closing quote
|
||||
int sectionLength = sections[i].Length;
|
||||
if (sections[i][sectionLength - 1] == '"')
|
||||
{
|
||||
if (sectionLength > 1 && sections[i][sectionLength - 2] != '\\')
|
||||
{
|
||||
isInQuotedSection = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
//Not in a quoted section, check to see if we're starting one
|
||||
else
|
||||
{
|
||||
sectionStartLookup[i] = runningIndex;
|
||||
|
||||
if (sections[i].Length > 0)
|
||||
{
|
||||
if (sections[i][0] == '"')
|
||||
{
|
||||
isInQuotedSection = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Update the running index, adding one for all but the last element to account for the spaces between the sections
|
||||
runningIndex += thisSectionLength + (isLastSection ? 0 : 1);
|
||||
|
||||
//If the selected section hasn't been determined yet, and the end of the text is past the caret, set the selected
|
||||
// section to the current section and set the initial value for the caret position within the selected section.
|
||||
// Note that the caret position within the selected section, unlike the other positions, accounts for escape
|
||||
// sequences and must be fixed up when escape sequences are removed
|
||||
if (selectedSection == -1 && runningIndex > caretPosition)
|
||||
{
|
||||
selectedSection = i;
|
||||
caretPositionWithinSelectedSection = caretPosition - sectionStartLookup[i];
|
||||
}
|
||||
}
|
||||
|
||||
//Unescape the sections
|
||||
// Note that this isn't combined with the above loop to avoid additional complexity in the quoted section case
|
||||
for (int i = 0; i < sections.Count; ++i)
|
||||
{
|
||||
string s = sections[i];
|
||||
|
||||
//Trim quotes if needed
|
||||
if (s.Length > 1)
|
||||
{
|
||||
if (s[0] == s[s.Length - 1] && s[0] == '"')
|
||||
{
|
||||
s = s.Substring(1, s.Length - 2);
|
||||
quotedSections.Add(i);
|
||||
|
||||
//Fix up the caret position in the text
|
||||
if (selectedSection == i)
|
||||
{
|
||||
//If the caret was on the closing quote, back up to the last character of the section
|
||||
if (caretPositionWithinSelectedSection == s.Length - 1)
|
||||
{
|
||||
caretPositionWithinSelectedSection -= 2;
|
||||
}
|
||||
//If the caret was after the opening quote, back up one
|
||||
else if (caretPositionWithinSelectedSection > 0)
|
||||
{
|
||||
--caretPositionWithinSelectedSection;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int j = 0; j < s.Length - 1; ++j)
|
||||
{
|
||||
if (s[j] == '\\')
|
||||
{
|
||||
if (s[j + 1] == '\\' || s[j + 1] == '"')
|
||||
{
|
||||
s = s.Substring(0, j) + s.Substring(j + 1);
|
||||
|
||||
//If we're changing the selected section, and we're removing a character
|
||||
// from before the caret position, back the caret position up to account for it
|
||||
if (selectedSection == i && j < caretPositionWithinSelectedSection)
|
||||
{
|
||||
--caretPositionWithinSelectedSection;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sections[i] = s;
|
||||
}
|
||||
|
||||
if (selectedSection == -1)
|
||||
{
|
||||
selectedSection = sections.Count - 1;
|
||||
caretPositionWithinSelectedSection = sections[selectedSection].Length;
|
||||
}
|
||||
|
||||
return new CoreParseResult(caretPosition, caretPositionWithinSelectedSection, commandText, sections, selectedSection, sectionStartLookup, quotedSections);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
using System.Collections.Generic;
|
||||
|
||||
namespace Microsoft.Repl.Parsing
|
||||
{
|
||||
public interface ICoreParseResult
|
||||
{
|
||||
int CaretPositionWithinCommandText { get; }
|
||||
|
||||
int CaretPositionWithinSelectedSection { get; }
|
||||
|
||||
string CommandText { get; }
|
||||
|
||||
IReadOnlyList<string> Sections { get; }
|
||||
|
||||
bool IsQuotedSection(int index);
|
||||
|
||||
int SelectedSection { get; }
|
||||
|
||||
IReadOnlyDictionary<int, int> SectionStartLookup { get; }
|
||||
|
||||
ICoreParseResult Slice(int numberOfLeadingSectionsToRemove);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
namespace Microsoft.Repl.Parsing
|
||||
{
|
||||
public interface IParser
|
||||
{
|
||||
ICoreParseResult Parse(string commandText, int caretPosition);
|
||||
}
|
||||
|
||||
public interface IParser<out TParseResult> : IParser
|
||||
{
|
||||
new TParseResult Parse(string commandText, int caretPosition);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.Repl.Scripting
|
||||
{
|
||||
public interface IScriptExecutor
|
||||
{
|
||||
Task ExecuteScriptAsync(IShellState shellState, IEnumerable<string> commandTexts, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Repl.Commanding;
|
||||
using Microsoft.Repl.Parsing;
|
||||
|
||||
namespace Microsoft.Repl.Scripting
|
||||
{
|
||||
public class ScriptExecutor<TProgramState, TParseResult> : IScriptExecutor
|
||||
where TParseResult : ICoreParseResult
|
||||
{
|
||||
private readonly bool _hideScriptLinesFromHistory;
|
||||
|
||||
public ScriptExecutor(bool hideScriptLinesFromHistory = true)
|
||||
{
|
||||
_hideScriptLinesFromHistory = hideScriptLinesFromHistory;
|
||||
}
|
||||
|
||||
public async Task ExecuteScriptAsync(IShellState shellState, IEnumerable<string> commandTexts, CancellationToken cancellationToken)
|
||||
{
|
||||
if (shellState.CommandDispatcher is ICommandDispatcher<TProgramState, TParseResult> dispatcher)
|
||||
{
|
||||
IDisposable suppressor = _hideScriptLinesFromHistory ? shellState.CommandHistory.SuspendHistory() : null;
|
||||
|
||||
using (suppressor)
|
||||
{
|
||||
foreach (string commandText in commandTexts)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(commandText))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
dispatcher.OnReady(shellState);
|
||||
shellState.ConsoleManager.ResetCommandStart();
|
||||
shellState.InputManager.SetInput(shellState, commandText);
|
||||
await dispatcher.ExecuteCommandAsync(shellState, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Repl.Commanding;
|
||||
using Microsoft.Repl.Input;
|
||||
using Microsoft.Repl.Suggestions;
|
||||
|
||||
namespace Microsoft.Repl
|
||||
{
|
||||
public class Shell
|
||||
{
|
||||
public Shell(IShellState shellState)
|
||||
{
|
||||
KeyHandlers.RegisterDefaultKeyHandlers(shellState.InputManager);
|
||||
ShellState = shellState;
|
||||
}
|
||||
|
||||
public Shell(ICommandDispatcher dispatcher, ISuggestionManager suggestionManager = null)
|
||||
: this(new ShellState(dispatcher, suggestionManager))
|
||||
{
|
||||
}
|
||||
|
||||
public IShellState ShellState { get; }
|
||||
|
||||
public Task RunAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
ShellState.CommandDispatcher.OnReady(ShellState);
|
||||
return ShellState.InputManager.StartAsync(ShellState, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
using Microsoft.Repl.Commanding;
|
||||
using Microsoft.Repl.ConsoleHandling;
|
||||
using Microsoft.Repl.Input;
|
||||
using Microsoft.Repl.Suggestions;
|
||||
|
||||
namespace Microsoft.Repl
|
||||
{
|
||||
public class ShellState : IShellState
|
||||
{
|
||||
public ShellState(ICommandDispatcher commandDispatcher, ISuggestionManager suggestionManager = null, IInputManager inputManager = null, ICommandHistory commandHistory = null, IConsoleManager consoleManager = null)
|
||||
{
|
||||
InputManager = inputManager ?? new InputManager();
|
||||
CommandHistory = commandHistory ?? new CommandHistory();
|
||||
ConsoleManager = consoleManager ?? new ConsoleManager();
|
||||
CommandDispatcher = commandDispatcher;
|
||||
SuggestionManager = suggestionManager ?? new SuggestionManager();
|
||||
}
|
||||
|
||||
public IInputManager InputManager { get; }
|
||||
|
||||
public ICommandHistory CommandHistory { get; }
|
||||
|
||||
public IConsoleManager ConsoleManager { get; }
|
||||
|
||||
public ICommandDispatcher CommandDispatcher { get; }
|
||||
|
||||
public bool IsExiting { get; set; }
|
||||
|
||||
public ISuggestionManager SuggestionManager { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
namespace Microsoft.Repl.Suggestions
|
||||
{
|
||||
public static class FileSystemCompletion
|
||||
{
|
||||
public static IEnumerable<string> GetCompletions(string prefix)
|
||||
{
|
||||
if (prefix.StartsWith('\"'))
|
||||
{
|
||||
prefix = prefix.Substring(1);
|
||||
|
||||
int lastQuote = prefix.LastIndexOf('\"');
|
||||
|
||||
if (lastQuote > -1)
|
||||
{
|
||||
prefix = prefix.Remove(lastQuote, 1);
|
||||
}
|
||||
|
||||
while (prefix.EndsWith($"{Path.DirectorySeparatorChar}{Path.DirectorySeparatorChar}"))
|
||||
{
|
||||
prefix = prefix.Substring(0, prefix.Length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
int lastPathIndex = prefix.LastIndexOfAny(new[] { '\\', '/' });
|
||||
if (lastPathIndex < 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string dir = prefix.Substring(0, lastPathIndex + 1);
|
||||
|
||||
if (dir.IndexOfAny(Path.GetInvalidPathChars()) > -1)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string partPrefix = prefix.Substring(lastPathIndex + 1);
|
||||
if (Directory.Exists(dir))
|
||||
{
|
||||
return Directory.EnumerateDirectories(dir).Where(x => Path.GetFileName(x).StartsWith(partPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
.Union(Directory.EnumerateFiles(dir).Where(x => Path.GetFileName(x).StartsWith(partPrefix, StringComparison.OrdinalIgnoreCase))).Select(x => x.IndexOf(' ') > -1 ? $"\"{x}\"" : x);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
namespace Microsoft.Repl.Suggestions
|
||||
{
|
||||
public interface ISuggestionManager
|
||||
{
|
||||
void NextSuggestion(IShellState shellState);
|
||||
|
||||
void PreviousSuggestion(IShellState shellState);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue