Initial commit of the HTTP REPL

This commit is contained in:
Mike Lorbetske 2018-07-29 12:38:02 -07:00
parent 4baed363e5
commit 89ab0cfde8
No known key found for this signature in database
GPG Key ID: 9EA3D5A33FBABE3B
106 changed files with 7485 additions and 0 deletions

View File

@ -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}

View File

@ -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"
}
}
}
}
},

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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";
}
}
}

View File

@ -0,0 +1,9 @@
namespace Microsoft.HttpRepl.Commands
{
public class DeleteCommand : BaseHttpCommand
{
protected override string Verb => "delete";
protected override bool RequiresBody => true;
}
}

View File

@ -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;
}
}
}

View File

@ -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";
}
}
}

View File

@ -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;
}
}
}

View File

@ -0,0 +1,9 @@
namespace Microsoft.HttpRepl.Commands
{
public class GetCommand : BaseHttpCommand
{
protected override string Verb => "get";
protected override bool RequiresBody => false;
}
}

View File

@ -0,0 +1,9 @@
namespace Microsoft.HttpRepl.Commands
{
public class HeadCommand : BaseHttpCommand
{
protected override string Verb => "head";
protected override bool RequiresBody => false;
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -0,0 +1,9 @@
namespace Microsoft.HttpRepl.Commands
{
public class OptionsCommand : BaseHttpCommand
{
protected override string Verb => "options";
protected override bool RequiresBody => false;
}
}

View File

@ -0,0 +1,9 @@
namespace Microsoft.HttpRepl.Commands
{
public class PatchCommand : BaseHttpCommand
{
protected override string Verb => "patch";
protected override bool RequiresBody => true;
}
}

View File

@ -0,0 +1,9 @@
namespace Microsoft.HttpRepl.Commands
{
public class PostCommand : BaseHttpCommand
{
protected override string Verb => "post";
protected override bool RequiresBody => true;
}
}

View File

@ -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;
}
}
}

View File

@ -0,0 +1,9 @@
namespace Microsoft.HttpRepl.Commands
{
public class PutCommand : BaseHttpCommand
{
protected override string Verb => "put";
protected override bool RequiresBody => true;
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -0,0 +1,9 @@
namespace Microsoft.HttpRepl.Diagnostics
{
public class ConfigItem
{
public string Key { get; set; }
public string Value { get; set; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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();
}
}
}

View File

@ -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; }
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View File

@ -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; }
}
}

View File

@ -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;
//}
}
}

View File

@ -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; }
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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; }
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -0,0 +1,8 @@
{
"profiles": {
"Microsoft.HttpRepl": {
"commandName": "Project",
"commandLineArgs": "http://localhost"
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -0,0 +1,10 @@
namespace Microsoft.Repl.Commanding
{
public enum CommandInputLocation
{
CommandName,
Argument,
OptionName,
OptionValue
}
}

View File

@ -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;
}
}
}

View File

@ -0,0 +1,11 @@
namespace Microsoft.Repl.Commanding
{
public enum CommandInputProcessingIssueKind
{
CommandMismatch,
ArgumentCountOutOfRange,
UnknownOption,
OptionUseCountOutOfRange,
MissingRequiredOptionInput,
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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; }
}
}

View File

@ -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;
}
}
}
}

View File

@ -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; }
}
}

View File

@ -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);
}
}

View File

@ -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; }
}
}

View File

@ -0,0 +1,15 @@
using System;
namespace Microsoft.Repl.Commanding
{
public interface ICommandHistory
{
string GetPreviousCommand();
string GetNextCommand();
void AddCommand(string command);
IDisposable SuspendHistory();
}
}

View File

@ -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;
}
}
}

View File

@ -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
}
}

View File

@ -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;
}
}
}
}

View File

@ -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;
}
}
}
}
}

View File

@ -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; }
}
}

View File

@ -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);
}
}

View File

@ -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; }
}
}

View File

@ -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})";
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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();
}
}
}

View File

@ -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; }
}
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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; }
}
}

View File

@ -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;
}
}
}

View File

@ -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