mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-09-06 17:58:05 +02:00
Add support for optional and params T[] toolshed command arguments. (#5573)
* Include argument name in completion suggestions * Support optional args * It (not so shrimply) works * Add tests * Add TestGenericPipeInference * Fix tests * Release notes * Overzealous YAMLLinter * Improve help signatures, fix map command * Improve NoImplementationError * Better type argument help signatures * better pipe syntax * fix NRE * Add test * a * Fix silent toolshed failure * Fix GetConcreteMethodInternal * Improve vars command * EntProtoId IAsType * More GetConcreteMethodInternal fixes * I hate this so much * update tp command description The command arguments call the the "other" entity the "target" * Support localized argument hints/signatures
This commit is contained in:
@@ -51,20 +51,32 @@ END TEMPLATE-->
|
||||
* Any global & session overrides (`PvsOverrideSystem.AddGlobalOverride()` & `PvsOverrideSystem.AddSessionOverride()`) now respect visibility masks.
|
||||
* Entities added via the `ExpandPvsEvent` respect visibility masks.
|
||||
* The mask used for any global/session overrides can be modified via `ExpandPvsEvent.Mask`.
|
||||
* Toolshed Changes:
|
||||
* The signature of Toolshed type parsers have changed. Instead of taking in an optional command argument name string, they now take in a `CommandArgument` struct.
|
||||
* Toolshed commands can no longer contain a '|', as this symbol is now used for explicitly piping the output of one command to another. command pipes. The existing `|` and '|~' commands have been renamed to `bitor` and `bitnotor`.
|
||||
* Semicolon terminated command blocks in toolshed commands no longer return anything. I.e., `i { i 2 ; }` is no longer a valid command, as the block has no return value.
|
||||
|
||||
### New features
|
||||
|
||||
* The current map format/version has increased from 6 to 7 and now contains more information to try support serialization of maps with null-space entities and full game saves.
|
||||
* `IEntitySystemManager` now provides access to the system `IDependencyCollection`.
|
||||
* Toolshed commands now support optional and `params T[]` arguments. optional / variable length commands can be terminated using ';' or '|'.
|
||||
|
||||
### Bugfixes
|
||||
|
||||
* Fixed entity deserialization for components with a data fields that have a AlwaysPushInheritance Attribute
|
||||
* Audio entities attached to invisible / masked entities should no longer be able to temporarily make those entities visible to all players.
|
||||
* The map-like Toolshed commands now work when a collection is piped in.
|
||||
* Fixed a bug in toolshed that could cause it to preferentially use the incorrect command implementation.
|
||||
* E.g., passing a concrete enumerable type would previously use the command implementation that takes in an unconstrained generic parameter `T` instead of a dedicated `IEnumeerable<T>` implementation.
|
||||
|
||||
### Other
|
||||
|
||||
* `MapChangedEvent` has been marked as obsolete, and should be replaced with `MapCreatedEvent` and `MapRemovedEvent.
|
||||
* The default auto-completion hint for Toolshed commands have been changed and somewhat standardized. Most parsers should now generate a hint of the form:
|
||||
* `<name (Type)>` for mandatory arguments
|
||||
* `[name (Type)]` for optional arguments
|
||||
* `[name (Type)]...` for variable length arguments (i.e., for `params T[]`)
|
||||
|
||||
### Internal
|
||||
|
||||
|
||||
@@ -42,8 +42,7 @@ command-description-as =
|
||||
command-description-count =
|
||||
Counts the amount of entries in it's input, returning an integer.
|
||||
command-description-map =
|
||||
Maps the input over the given block, with the provided expected return type.
|
||||
This command may be modified to not need an explicit return type in the future.
|
||||
Maps the input over the given block.
|
||||
command-description-select =
|
||||
Selects N objects or N% of objects from the input.
|
||||
One can additionally invert this command with not to make it select everything except N objects instead.
|
||||
@@ -149,7 +148,7 @@ command-description-max =
|
||||
Returns the maximum of two values.
|
||||
command-description-BitAndCommand =
|
||||
Performs bitwise AND.
|
||||
command-description-BitOrCommand =
|
||||
command-description-bitor =
|
||||
Performs bitwise OR.
|
||||
command-description-BitXorCommand =
|
||||
Performs bitwise XOR.
|
||||
@@ -203,11 +202,11 @@ command-description-mappos =
|
||||
command-description-pos =
|
||||
Returns an entity's coordinates.
|
||||
command-description-tp-coords =
|
||||
Teleports the target to the given coordinates.
|
||||
Teleports the given entities to the target coordinates.
|
||||
command-description-tp-to =
|
||||
Teleports the target to the given other entity.
|
||||
Teleports the given entities to the target entity.
|
||||
command-description-tp-into =
|
||||
Teleports the target "into" the given other entity, attaching it at (0 0) relative to it.
|
||||
Teleports the given entities "into" the target entity, attaching it at (0 0) relative to it.
|
||||
command-description-comp-get =
|
||||
Gets the given component from the given entity.
|
||||
command-description-comp-add =
|
||||
@@ -277,7 +276,7 @@ command-description-ModVecCommand =
|
||||
Performs the modulus operation over the input with the given constant right-hand value.
|
||||
command-description-BitAndNotCommand =
|
||||
Performs bitwise AND-NOT over the input.
|
||||
command-description-BitOrNotCommand =
|
||||
command-description-bitornot =
|
||||
Performs bitwise OR-NOT over the input.
|
||||
command-description-BitXnorCommand =
|
||||
Performs bitwise XNOR over the input.
|
||||
|
||||
@@ -17,7 +17,8 @@ namespace Robust.Shared.Prototypes;
|
||||
/// </remarks>
|
||||
/// <remarks><seealso cref="ProtoId{T}"/> for a wrapper of other prototype kinds.</remarks>
|
||||
[Serializable, NetSerializable]
|
||||
public readonly record struct EntProtoId(string Id) : IEquatable<string>, IComparable<EntProtoId>, IAsType<string>
|
||||
public readonly record struct EntProtoId(string Id) : IEquatable<string>, IComparable<EntProtoId>, IAsType<string>,
|
||||
IAsType<ProtoId<EntityPrototype>>
|
||||
{
|
||||
public static implicit operator string(EntProtoId protoId)
|
||||
{
|
||||
@@ -49,7 +50,9 @@ public readonly record struct EntProtoId(string Id) : IEquatable<string>, ICompa
|
||||
return string.Compare(Id, other.Id, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public string AsType() => Id;
|
||||
string IAsType<string>.AsType() => Id;
|
||||
|
||||
ProtoId<EntityPrototype> IAsType<ProtoId<EntityPrototype>>.AsType() => new(Id);
|
||||
|
||||
public override string ToString() => Id ?? string.Empty;
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ namespace Robust.Shared.Toolshed.Commands.Generic;
|
||||
public sealed class CountCommand : ToolshedCommand
|
||||
{
|
||||
[CommandImplementation, TakesPipedTypeAsGeneric]
|
||||
public int Count<T>([PipedArgument] IEnumerable<T> enumerable)
|
||||
public int Count<T>([PipedArgument] IEnumerable<T> input)
|
||||
{
|
||||
return enumerable.Count();
|
||||
return input.Count();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,8 +174,14 @@ public sealed class EmplaceCommand : ToolshedCommand
|
||||
{
|
||||
public static bool TryParse(ParserContext ctx, [NotNullWhen(true)] out CommandRun? result)
|
||||
{
|
||||
if (ctx.Bundle.PipedType == null)
|
||||
{
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the piped type is IEnumerable<T> we want to extract the type T.
|
||||
var pipeInferredType = ctx.Bundle.PipedType!;
|
||||
var pipeInferredType = ctx.Bundle.PipedType;
|
||||
if (pipeInferredType.IsGenericType(typeof(IEnumerable<>)))
|
||||
pipeInferredType = pipeInferredType.GetGenericArguments()[0];
|
||||
|
||||
@@ -219,7 +225,7 @@ public sealed class EmplaceCommand : ToolshedCommand
|
||||
return true;
|
||||
}
|
||||
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName)
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg)
|
||||
{
|
||||
TryParse(ctx, out _);
|
||||
return ctx.Completions;
|
||||
@@ -231,6 +237,7 @@ public sealed class EmplaceCommand : ToolshedCommand
|
||||
/// </summary>
|
||||
private sealed class EmplaceBlockOutputParser : CustomTypeParser<Type>
|
||||
{
|
||||
public override bool ShowTypeArgSignature => false;
|
||||
public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Type? result)
|
||||
{
|
||||
result = null;
|
||||
@@ -246,7 +253,7 @@ public sealed class EmplaceCommand : ToolshedCommand
|
||||
return true;
|
||||
}
|
||||
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName)
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg)
|
||||
{
|
||||
EmplaceBlockParser.TryParse(ctx, out _);
|
||||
return ctx.Completions;
|
||||
|
||||
@@ -66,7 +66,7 @@ public sealed class ReduceCommand : ToolshedCommand
|
||||
return true;
|
||||
}
|
||||
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName)
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg)
|
||||
{
|
||||
if (ctx.Bundle.PipedType is not {IsGenericType: true})
|
||||
return null;
|
||||
|
||||
@@ -8,6 +8,6 @@ public sealed class VarsCommand : ToolshedCommand
|
||||
[CommandImplementation]
|
||||
public void Vars(IInvocationContext ctx)
|
||||
{
|
||||
ctx.WriteLine(Toolshed.PrettyPrintType(ctx.GetVars().Select(x => $"{x} = {ctx.ReadVar(x)}"), out var more));
|
||||
ctx.WriteLine(Toolshed.PrettyPrintType(ctx.GetVars().Select(x => $"{x} = {Toolshed.PrettyPrintType(ctx.ReadVar(x), out _)}"), out _));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,7 +295,7 @@ public sealed class BitAndNotCommand : ToolshedCommand
|
||||
});
|
||||
}
|
||||
|
||||
[ToolshedCommand(Name = "|")]
|
||||
[ToolshedCommand]
|
||||
public sealed class BitOrCommand : ToolshedCommand
|
||||
{
|
||||
[CommandImplementation, TakesPipedTypeAsGeneric]
|
||||
@@ -314,7 +314,7 @@ public sealed class BitOrCommand : ToolshedCommand
|
||||
});
|
||||
}
|
||||
|
||||
[ToolshedCommand(Name = "|~")]
|
||||
[ToolshedCommand]
|
||||
public sealed class BitOrNotCommand : ToolshedCommand
|
||||
{
|
||||
[CommandImplementation, TakesPipedTypeAsGeneric]
|
||||
|
||||
@@ -16,32 +16,40 @@ public sealed class ExplainCommand : ToolshedCommand
|
||||
var builder = new StringBuilder();
|
||||
foreach (var (cmd, _) in expr.Commands)
|
||||
{
|
||||
builder.AppendLine();
|
||||
var name = cmd.Implementor.FullName;
|
||||
builder.AppendLine($"{name} - {cmd.Implementor.Description()}");
|
||||
|
||||
var piped = cmd.PipedType?.PrettyName() ?? "[none]";
|
||||
builder.AppendLine($"Pipe input: {piped}");
|
||||
builder.AppendLine($"Pipe output: {cmd.ReturnType.PrettyName()}");
|
||||
|
||||
builder.Append($"Signature:\n ");
|
||||
|
||||
if (cmd.PipedType != null)
|
||||
{
|
||||
var pipeArg = cmd.Method.Base.PipeArg;
|
||||
DebugTools.AssertNotNull(pipeArg);
|
||||
builder.Append($"<{pipeArg?.Name} ({ToolshedCommandImplementor.GetFriendlyName(cmd.PipedType)})> -> ");
|
||||
|
||||
var locKey = $"command-arg-sig-{cmd.Implementor.LocName}-{pipeArg?.Name}";
|
||||
if (Loc.TryGetString(locKey, out var msg))
|
||||
{
|
||||
builder.Append(msg);
|
||||
builder.Append(" → ");
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append($"<{pipeArg?.Name}> → "); // No type information, as that is already given above.
|
||||
}
|
||||
}
|
||||
|
||||
if (cmd.Bundle.Inverted)
|
||||
builder.Append("not ");
|
||||
|
||||
builder.Append($"{name}");
|
||||
foreach (var (argName, argType, _) in cmd.Method.Args)
|
||||
{
|
||||
builder.Append($" <{argName} ({ToolshedCommandImplementor.GetFriendlyName(argType)})>");
|
||||
}
|
||||
|
||||
builder.AppendLine();
|
||||
var piped = cmd.PipedType?.PrettyName() ?? "[none]";
|
||||
var returned = cmd.ReturnType?.PrettyName() ?? "[none]";
|
||||
builder.AppendLine($"{piped} -> {returned}");
|
||||
cmd.Implementor.AddMethodSignature(builder, cmd.Method.Args, cmd.Bundle.TypeArguments);
|
||||
builder.AppendLine();
|
||||
}
|
||||
|
||||
ctx.WriteLine(builder.ToString());
|
||||
ctx.WriteLine(builder.ToString().TrimEnd());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,8 +191,20 @@ internal static class ReflectionExtensions
|
||||
}
|
||||
|
||||
// IEnumerable<EntityUid> ^ IEnumerable<T> -> EntityUid
|
||||
// List<T> ^ IEnumerable<T> -> EntityUid
|
||||
// T[] ^ IEnumerable<T> -> EntityUid
|
||||
public static Type Intersect(this Type left, Type right)
|
||||
{
|
||||
// TODO TOOLSHED implement this properly.
|
||||
// AAAAHhhhhh
|
||||
// this is all sphagetti and needs fixing.
|
||||
// I'm just bodging a fix for now that makes it treat arrays as equivalent to a list.
|
||||
if (left.IsArray)
|
||||
return Intersect(typeof(List<>).MakeGenericType(left.GetElementType()!), right);
|
||||
|
||||
if (right.IsArray)
|
||||
return Intersect(left, typeof(List<>).MakeGenericType(right.GetElementType()!));
|
||||
|
||||
if (!left.IsGenericType)
|
||||
return left;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Toolshed.Errors;
|
||||
using Robust.Shared.Toolshed.TypeParsers.Math;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.Toolshed.Syntax;
|
||||
@@ -79,13 +80,13 @@ public sealed class CommandRun
|
||||
{
|
||||
expr = null;
|
||||
var cmds = new List<(ParsedCommand, Vector2i)>();
|
||||
var start = ctx.Index;
|
||||
ctx.ConsumeWhitespace();
|
||||
DebugTools.AssertNull(ctx.Error);
|
||||
DebugTools.AssertNull(ctx.Completions);
|
||||
if (pipedType == typeof(void))
|
||||
throw new ArgumentException($"Piped type cannot be void");
|
||||
|
||||
var start = ctx.Index;
|
||||
if (ctx.PeekBlockTerminator())
|
||||
{
|
||||
// Trying to parse an empty block as a command run? I.e. " { } "
|
||||
@@ -94,6 +95,7 @@ public sealed class CommandRun
|
||||
return false;
|
||||
}
|
||||
|
||||
bool commandExpected;
|
||||
while (true)
|
||||
{
|
||||
if (!ParsedCommand.TryParse(ctx, pipedType, out var cmd))
|
||||
@@ -107,27 +109,55 @@ public sealed class CommandRun
|
||||
cmds.Add((cmd, (start, ctx.Index)));
|
||||
ctx.ConsumeWhitespace();
|
||||
|
||||
if (ctx.EatCommandTerminators())
|
||||
{
|
||||
ctx.ConsumeWhitespace();
|
||||
pipedType = null;
|
||||
}
|
||||
ctx.EatCommandTerminators(ref pipedType, out commandExpected);
|
||||
|
||||
// If the command run encounters a block terminator we exit out.
|
||||
// The parser that pushed the block terminator is what should actually eat & pop it, so that it can
|
||||
// return appropriate errors if the block was not terminated.
|
||||
if (ctx.PeekBlockTerminator())
|
||||
break;
|
||||
{
|
||||
if (!commandExpected)
|
||||
break;
|
||||
|
||||
// Lets enforce that poeple don't end command blocks with a dangling explicit pipe.
|
||||
// I.e., force people to use `{ i 2 }` instead of `{ i 2 | }`.
|
||||
if (!ctx.GenerateCompletions)
|
||||
{
|
||||
ctx.Error = new UnexpectedCloseBrace();
|
||||
ctx.Error.Contextualize(ctx.Input, (ctx.Index, ctx.Index + 1));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ctx.OutOfInput)
|
||||
break;
|
||||
{
|
||||
// If the last command was terminated by an explicit pipe symbol we require that there be a follow-up
|
||||
// command
|
||||
if (!commandExpected)
|
||||
break;
|
||||
|
||||
if (ctx.GenerateCompletions)
|
||||
{
|
||||
// TODO TOOLSHED COMPLETIONS improve this
|
||||
// Currently completions are only shown if a command ends in a space. I.e. "| " instead of "|".
|
||||
// Ideally the completions should still be shown, and it should know to auto-insert a leading space.
|
||||
// AFAIK this requires updating the client-side code like FilterCompletions(), as well as somehow
|
||||
// communicating this to the client, maybe by adding a new bit to the CompletionOptionFlags, or
|
||||
// adding some similar flag field to the whole whole completion collection?
|
||||
ParsedCommand.TryParse(ctx, pipedType, out _);
|
||||
}
|
||||
else
|
||||
ctx.Error = new OutOfInputError();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
start = ctx.Index;
|
||||
|
||||
if (pipedType != typeof(void))
|
||||
continue;
|
||||
|
||||
// The previously parsed command does not generate any output that can be piped/chained into another
|
||||
// The previously parsed command does not generate any output that can be piped/chained into another
|
||||
// command. This can happen if someone tries to provide more arguments than a command accepts.
|
||||
// e.g., " i 5 5". In this case, the parsing fails and should make it clear that no more input was expected.
|
||||
// Multiple unrelated commands on a single line are still supported via the ';' terminator.
|
||||
@@ -147,8 +177,7 @@ public sealed class CommandRun
|
||||
return false;
|
||||
}
|
||||
|
||||
// Return the last type, even if the command ended with a ';'
|
||||
var returnType = cmds[^1].Item1.ReturnType;
|
||||
var returnType = pipedType != null ? cmds[^1].Item1.ReturnType : typeof(void);
|
||||
if (targetOutput != null && !returnType.IsAssignableTo(targetOutput))
|
||||
{
|
||||
ctx.Error = new WrongCommandReturn(targetOutput, returnType);
|
||||
@@ -333,6 +362,6 @@ public sealed class EndOfCommandError : ConError
|
||||
{
|
||||
public override FormattedMessage DescribeInner()
|
||||
{
|
||||
return FormattedMessage.FromUnformatted("Expected an end of command (;)");
|
||||
return FormattedMessage.FromUnformatted("Expected a command or block terminator (';' or '}')");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
@@ -155,11 +156,12 @@ public sealed class ParsedCommand
|
||||
|
||||
private static bool TryParseCommandName(ParserContext ctx, [NotNullWhen(true)] out string? name)
|
||||
{
|
||||
var cmdNameStart = ctx.Index;
|
||||
ctx.Bundle.NameStart = ctx.Index;
|
||||
name = ctx.GetWord(ParserContext.IsCommandToken);
|
||||
if (name != null)
|
||||
{
|
||||
ctx.Bundle.Command = name;
|
||||
ctx.Bundle.NameEnd = ctx.Index;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -182,7 +184,7 @@ public sealed class ParsedCommand
|
||||
return false;
|
||||
|
||||
ctx.Error = new NotValidCommandError();
|
||||
ctx.Error.Contextualize(ctx.Input, (cmdNameStart, ctx.Index+1));
|
||||
ctx.Error.Contextualize(ctx.Input, (ctx.Bundle.NameStart, ctx.Index+1));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -232,6 +234,7 @@ public sealed class ParsedCommand
|
||||
return false;
|
||||
}
|
||||
|
||||
ctx.Bundle.NameEnd = ctx.Index;
|
||||
ctx.Bundle.SubCommand = subcmd;
|
||||
return true;
|
||||
}
|
||||
@@ -287,36 +290,24 @@ public sealed class NoImplementationError(ParserContext ctx) : ConError
|
||||
|
||||
public override FormattedMessage DescribeInner()
|
||||
{
|
||||
var msg = FormattedMessage.FromUnformatted($"Could not find an implementation for {Cmd} given the input type {PipedType?.PrettyName() ?? "void"}.");
|
||||
msg.PushNewline();
|
||||
var msg = FormattedMessage.FromUnformatted($"Could not find an implementation of the '{Cmd}' command given the input type '{PipedType?.PrettyName() ?? "void"}'.\n");
|
||||
|
||||
var typeArgs = "";
|
||||
|
||||
if (Types != null && Types.Length != 0)
|
||||
{
|
||||
typeArgs = "<" + string.Join(",", Types.Select(ReflectionExtensions.PrettyName)) + ">";
|
||||
}
|
||||
|
||||
msg.AddText($"Signature: {Cmd}{(SubCommand is not null ? $":{SubCommand}" : "")}{typeArgs} {PipedType?.PrettyName() ?? "void"} -> ???");
|
||||
|
||||
var piped = PipedType ?? typeof(void);
|
||||
var cmdImpl = Env.GetCommand(Cmd);
|
||||
var accepted = cmdImpl.AcceptedTypes(SubCommand);
|
||||
|
||||
foreach (var (command, subCommand) in Env.CommandsTakingType(piped))
|
||||
{
|
||||
if (!command.TryGetReturnType(subCommand, piped, null, out var retType) || !accepted.Any(x => retType.IsAssignableTo(x)))
|
||||
continue;
|
||||
// If one of the signatures just takes T Or IEnumerable<T> we just don't print anything, as it doesn't provide any useful information.
|
||||
// TODO TOOLSHED list accepted generic types
|
||||
var isGeneric = accepted.Any(x => x.IsGenericParameter);
|
||||
if (isGeneric)
|
||||
return msg;
|
||||
|
||||
if (!cmdImpl.TryGetReturnType(SubCommand, retType, Types, out var myRetType))
|
||||
continue;
|
||||
|
||||
msg.PushNewline();
|
||||
msg.AddText($"The command {command.Name}{(subCommand is not null ? $":{subCommand}" : "")} can convert from {piped.PrettyName()} to {retType.PrettyName()}.");
|
||||
msg.PushNewline();
|
||||
msg.AddText($"With this fix, the new signature will be: {Cmd}{(SubCommand is not null ? $":{SubCommand}" : "")}{typeArgs} {retType?.PrettyName() ?? "void"} -> {myRetType?.PrettyName() ?? "void"}.");
|
||||
}
|
||||
var isGenericEnumerable = accepted.Any(x=> x.IsGenericType
|
||||
&& x.GetGenericTypeDefinition() == typeof(IEnumerable<>)
|
||||
&& x.GetGenericArguments()[0].IsGenericParameter);
|
||||
if (isGenericEnumerable)
|
||||
return msg;
|
||||
|
||||
msg.AddText($"Accepted types: '{string.Join("','", accepted.Select(x => x.PrettyName()))}'.\n");
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ public sealed partial class ParserContext
|
||||
&& c != new Rune('\'')
|
||||
&& c != new Rune(':')
|
||||
&& c != new Rune(';')
|
||||
&& c != new Rune('|')
|
||||
&& c != new Rune('$')
|
||||
&& !Rune.IsControl(c);
|
||||
}
|
||||
|
||||
@@ -412,6 +412,9 @@ public sealed partial class ParserContext
|
||||
if (c == new Rune(';'))
|
||||
return true;
|
||||
|
||||
if (c == new Rune('|'))
|
||||
return true;
|
||||
|
||||
if (NoMultilineExprs && c == new Rune('\n'))
|
||||
return true;
|
||||
|
||||
@@ -422,36 +425,50 @@ public sealed partial class ParserContext
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to consume a single command terminator, which is either a ';' or a newline (if <see cref="NoMultilineExprs"/> is
|
||||
/// enabled).
|
||||
/// Attempts to consume a single command terminator
|
||||
/// </summary>
|
||||
public bool EatCommandTerminator()
|
||||
/// <param name="pipedType"></param>
|
||||
public bool EatCommandTerminator(ref Type? pipedType, out bool commandExpected)
|
||||
{
|
||||
commandExpected = false;
|
||||
|
||||
// Command terminator drops piped values.
|
||||
if (EatMatch(new Rune(';')))
|
||||
{
|
||||
pipedType = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Explicit pipe operator keeps piped value, but is only valid if there is a piped value.
|
||||
if (pipedType != null && pipedType != typeof(void) && EatMatch(new Rune('|')))
|
||||
{
|
||||
commandExpected = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// If multi-line commands are not enabled, we treat a newline like a ';'
|
||||
// I.e., it terminates the command currently being parsed in
|
||||
return NoMultilineExprs && EatMatch(new Rune('\n'));
|
||||
if (NoMultilineExprs && EatMatch(new Rune('\n')))
|
||||
{
|
||||
pipedType = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to repeatedly consume command terminators, and return true if any were consumed.
|
||||
/// </summary>
|
||||
public bool EatCommandTerminators()
|
||||
public void EatCommandTerminators(ref Type? pipedType, out bool commandExpected)
|
||||
{
|
||||
if (!EatCommandTerminator())
|
||||
return false;
|
||||
if (!EatCommandTerminator(ref pipedType, out commandExpected))
|
||||
return;
|
||||
|
||||
// Maybe one day we want to allow ';;' to have special meaning?
|
||||
// But for now, just eat em all.
|
||||
ConsumeWhitespace();
|
||||
while (EatCommandTerminator())
|
||||
while (!commandExpected && EatCommandTerminator(ref pipedType, out commandExpected))
|
||||
{
|
||||
ConsumeWhitespace();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Toolshed.Errors;
|
||||
@@ -32,6 +33,22 @@ public abstract class ValueRef<T>
|
||||
$"Input: {obj}")
|
||||
};
|
||||
}
|
||||
|
||||
internal static T[] EvaluateParamsCollection(object? obj, IInvocationContext ctx)
|
||||
{
|
||||
if (obj is not List<object?> parsedValues)
|
||||
throw new Exception("Failed to parse command parameter. This likely is a toolshed bug and should be reported.");
|
||||
|
||||
var i = 0;
|
||||
var arr = new T[parsedValues.Count];
|
||||
foreach (var parsed in parsedValues)
|
||||
{
|
||||
arr[i++] = EvaluateParameter(parsed, ctx)!;
|
||||
}
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
[Obsolete("Use EntProtoId / ProtoId<T>")]
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace Robust.Shared.Toolshed;
|
||||
using System;
|
||||
|
||||
namespace Robust.Shared.Toolshed;
|
||||
|
||||
public abstract partial class ToolshedCommand
|
||||
{
|
||||
@@ -33,4 +35,33 @@ public abstract partial class ToolshedCommand
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method for generating auto-completion hints while parsing command arguments.
|
||||
/// </summary>
|
||||
public static string GetArgHint(CommandArgument? arg, Type t)
|
||||
{
|
||||
if (arg == null)
|
||||
return t.PrettyName();
|
||||
|
||||
return GetArgHint(arg.Value.Name, arg.Value.IsOptional, arg.Value.IsParamsCollection, t);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper method for generating auto-completion hints while parsing command arguments.
|
||||
/// </summary>
|
||||
public static string GetArgHint(string name, bool optional, bool isParams, Type t)
|
||||
{
|
||||
var type = t.PrettyName();
|
||||
|
||||
// optional arguments wrapped in square braces, inspired by the syntax of man pages
|
||||
if (optional)
|
||||
return $"[{name} ({type})]";
|
||||
|
||||
// ellipses for params / variable length arguments
|
||||
if (isParams)
|
||||
return $"[{name} ({type})]...";
|
||||
|
||||
return $"<{name} ({type})>";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +129,7 @@ public abstract partial class ToolshedCommand
|
||||
if (param.Name == null || !argNames.Add(param.Name))
|
||||
throw new InvalidCommandImplementation($"Command arguments must have a unique name");
|
||||
hasAnyAttribute = true;
|
||||
ValidateArg(param);
|
||||
}
|
||||
|
||||
if (param.HasCustomAttribute<PipedArgumentAttribute>())
|
||||
@@ -180,6 +181,7 @@ public abstract partial class ToolshedCommand
|
||||
// Implicit [CommandArgument]
|
||||
if (param.Name == null || !argNames.Add(param.Name))
|
||||
throw new InvalidCommandImplementation($"Command arguments must have a unique name");
|
||||
ValidateArg(param);
|
||||
}
|
||||
|
||||
var takesPipedGeneric = impl.HasCustomAttribute<TakesPipedTypeAsGenericAttribute>();
|
||||
@@ -230,6 +232,18 @@ public abstract partial class ToolshedCommand
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateArg(ParameterInfo arg)
|
||||
{
|
||||
var isParams = arg.HasCustomAttribute<ParamArrayAttribute>();
|
||||
if (!isParams)
|
||||
return;
|
||||
|
||||
// I'm honestly not even sure if dotnet 9 collections use the same attribute, a quick search hasn't come
|
||||
// up with anything.
|
||||
if (!arg.ParameterType.IsArray)
|
||||
throw new InvalidCommandImplementation(".net 9 params collections are not yet supported");
|
||||
}
|
||||
|
||||
internal HashSet<Type> AcceptedTypes(string? subCommand)
|
||||
{
|
||||
if (_acceptedTypes.TryGetValue(subCommand ?? "", out var set))
|
||||
@@ -289,6 +303,16 @@ public struct CommandArgumentBundle
|
||||
/// The type of input that will be piped into this command.
|
||||
/// </summary>
|
||||
public required Type? PipedType;
|
||||
|
||||
/// <summary>
|
||||
/// The index where the command's name starts. Used for contextualising errors.
|
||||
/// </summary>
|
||||
public int NameStart;
|
||||
|
||||
/// <summary>
|
||||
/// The index where the (sub)command's name ends. Used for contextualising errors.
|
||||
/// </summary>
|
||||
public int NameEnd;
|
||||
}
|
||||
|
||||
internal readonly record struct CommandDiscriminator(Type? PipedType, Type[]? TypeArguments)
|
||||
|
||||
@@ -55,7 +55,7 @@ internal sealed class ToolshedCommandImplementor
|
||||
.GetMethods(ToolshedCommand.MethodFlags)
|
||||
.Where(x => x.GetCustomAttribute<CommandImplementationAttribute>() is { } attr &&
|
||||
attr.SubCommand == SubCommand)
|
||||
.Select(x => new CommandMethod(x))
|
||||
.Select(x => new CommandMethod(x, this))
|
||||
.ToArray();
|
||||
|
||||
LocName = Owner.Name.All(char.IsAsciiLetterOrDigit)
|
||||
@@ -80,8 +80,11 @@ internal sealed class ToolshedCommandImplementor
|
||||
|
||||
if (!TryGetConcreteMethod(ctx.Bundle.PipedType, ctx.Bundle.TypeArguments, out method))
|
||||
{
|
||||
if (!ctx.GenerateCompletions)
|
||||
ctx.Error = new NoImplementationError(ctx);
|
||||
if (ctx.GenerateCompletions)
|
||||
return false;
|
||||
|
||||
ctx.Error = new NoImplementationError(ctx);
|
||||
ctx.Error.Contextualize(ctx.Input, (ctx.Bundle.NameStart, ctx.Bundle.NameEnd));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -107,11 +110,20 @@ internal sealed class ToolshedCommandImplementor
|
||||
DebugTools.AssertNull(ctx.Error);
|
||||
DebugTools.AssertNull(ctx.Completions);
|
||||
|
||||
ref var args = ref ctx.Bundle.Arguments;
|
||||
foreach (var arg in method.Args)
|
||||
{
|
||||
if (!TryParseArgument(ctx, arg, ref args))
|
||||
object? parsed;
|
||||
if (arg.IsParamsCollection)
|
||||
{
|
||||
DebugTools.Assert(arg == method.Args[^1]);
|
||||
if (!ParseParamsCollection(ctx, arg, out parsed))
|
||||
return false;
|
||||
}
|
||||
else if (!TryParseArgument(ctx, arg, out parsed))
|
||||
return false;
|
||||
|
||||
ctx.Bundle.Arguments ??= new();
|
||||
ctx.Bundle.Arguments[arg.Name] = parsed;
|
||||
}
|
||||
|
||||
DebugTools.AssertNull(ctx.Error);
|
||||
@@ -119,22 +131,68 @@ internal sealed class ToolshedCommandImplementor
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryParseArgument(ParserContext ctx, CommandArgument arg, ref Dictionary<string, object?>? args)
|
||||
private bool ParseParamsCollection(ParserContext ctx, CommandArgument arg, out object? collection)
|
||||
{
|
||||
var list = new List<object?>();
|
||||
collection = list;
|
||||
|
||||
while (true)
|
||||
{
|
||||
ctx.ConsumeWhitespace();
|
||||
if (ctx.PeekCommandOrBlockTerminated())
|
||||
break;
|
||||
|
||||
if (ctx is {OutOfInput: true, GenerateCompletions: false})
|
||||
break;
|
||||
|
||||
if (!TryParseArgument(ctx, arg, out var parsed))
|
||||
return false;
|
||||
|
||||
list.Add(parsed);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryParseArgument(ParserContext ctx, CommandArgument arg, out object? parsed)
|
||||
{
|
||||
DebugTools.AssertNull(ctx.Error);
|
||||
DebugTools.AssertNull(ctx.Completions);
|
||||
var start = ctx.Index;
|
||||
var save = ctx.Save();
|
||||
ctx.ConsumeWhitespace();
|
||||
DebugTools.AssertNotNull(arg.Parser);
|
||||
|
||||
parsed = null;
|
||||
if (ctx.PeekCommandOrBlockTerminated() || ctx is {OutOfInput: true, GenerateCompletions: false})
|
||||
{
|
||||
ctx.Error = new ExpectedArgumentError(arg.Type);
|
||||
ctx.Error.Contextualize(ctx.Input, (start, ctx.Index+1));
|
||||
return false;
|
||||
}
|
||||
if (!arg.IsOptional)
|
||||
{
|
||||
ctx.Error = new ExpectedArgumentError(arg.Type);
|
||||
ctx.Error.Contextualize(ctx.Input, (start, ctx.Index + 1));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!arg.Parser.TryParse(ctx, out var parsed))
|
||||
parsed = arg.DefaultValue;
|
||||
}
|
||||
else if (arg.Parser!.TryParse(ctx, out parsed))
|
||||
{
|
||||
// All arguments should have been parsed as a ValueRef<T> or Block, unless this is using some custom type parser
|
||||
#if DEBUG
|
||||
var t = parsed.GetType();
|
||||
if (arg.Parser.GetType().IsCustomParser())
|
||||
{
|
||||
DebugTools.Assert(t.IsAssignableTo(arg.Type)
|
||||
|| t.IsAssignableTo(typeof(Block))
|
||||
|| t.IsValueRef());
|
||||
}
|
||||
else if (arg.Type.IsAssignableTo(typeof(Block)))
|
||||
DebugTools.Assert(t.IsAssignableTo(typeof(Block)));
|
||||
else
|
||||
DebugTools.Assert(t.IsValueRef());
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
if (ctx.GenerateCompletions)
|
||||
{
|
||||
@@ -149,8 +207,8 @@ internal sealed class ToolshedCommandImplementor
|
||||
|
||||
ctx.Restore(save);
|
||||
ctx.Error = null;
|
||||
ctx.Completions ??= arg.Parser.TryAutocomplete(ctx, arg.Name);
|
||||
TrySetArgHint(ctx, arg.Name);
|
||||
ctx.Completions ??= arg.Parser.TryAutocomplete(ctx, arg);
|
||||
TrySetArgHint(ctx, arg);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -163,24 +221,6 @@ internal sealed class ToolshedCommandImplementor
|
||||
return false;
|
||||
}
|
||||
|
||||
// All arguments should have been parsed as a ValueRef<T> or Block, unless this is using some custom type parser
|
||||
#if DEBUG
|
||||
var t = parsed.GetType();
|
||||
if (arg.Parser.GetType().IsCustomParser())
|
||||
{
|
||||
DebugTools.Assert(t.IsAssignableTo(arg.Type)
|
||||
|| t.IsAssignableTo(typeof(Block))
|
||||
|| t.IsValueRef());
|
||||
}
|
||||
else if (arg.Type.IsAssignableTo(typeof(Block)))
|
||||
DebugTools.Assert(t.IsAssignableTo(typeof(Block)));
|
||||
else
|
||||
DebugTools.Assert(t.IsValueRef());
|
||||
#endif
|
||||
|
||||
args ??= new();
|
||||
args[arg.Name] = parsed;
|
||||
|
||||
if (!ctx.GenerateCompletions || !ctx.OutOfInput)
|
||||
return true;
|
||||
|
||||
@@ -191,8 +231,8 @@ internal sealed class ToolshedCommandImplementor
|
||||
|
||||
ctx.Restore(save);
|
||||
ctx.Error = null;
|
||||
ctx.Completions ??= arg.Parser.TryAutocomplete(ctx, arg.Name);
|
||||
TrySetArgHint(ctx, arg.Name);
|
||||
ctx.Completions ??= arg.Parser!.TryAutocomplete(ctx, arg);
|
||||
TrySetArgHint(ctx, arg);
|
||||
|
||||
// TODO TOOLSHED invalid-fail
|
||||
// This can technically "fail" to parse a valid command, however this only happens when generating
|
||||
@@ -201,13 +241,12 @@ internal sealed class ToolshedCommandImplementor
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private void TrySetArgHint(ParserContext ctx, string argName)
|
||||
private void TrySetArgHint(ParserContext ctx, CommandArgument arg)
|
||||
{
|
||||
if (ctx.Completions == null)
|
||||
return;
|
||||
|
||||
if (_loc.TryGetString($"command-arg-hint-{LocName}-{argName}", out var hint))
|
||||
if (_loc.TryGetString($"command-arg-hint-{LocName}-{arg.Name}", out var hint))
|
||||
ctx.Completions.Hint = hint;
|
||||
}
|
||||
|
||||
@@ -318,18 +357,45 @@ internal sealed class ToolshedCommandImplementor
|
||||
|
||||
var args = info.GetParameters()
|
||||
.Where(x => x.IsCommandArgument())
|
||||
.Select(x => new CommandArgument(x.Name!, x.ParameterType, GetArgumentParser(x)))
|
||||
.Select(GetCommandArgument)
|
||||
.ToArray();
|
||||
|
||||
_methodCache[idx] = method = new(info, args, cmd);
|
||||
return true;
|
||||
}
|
||||
|
||||
private ITypeParser GetArgumentParser(ParameterInfo param)
|
||||
internal CommandArgument GetCommandArgument(ParameterInfo arg)
|
||||
{
|
||||
var argType = arg.ParameterType;
|
||||
|
||||
// Is this a "params T[] argument"?
|
||||
var isParamsCollection = arg.HasCustomAttribute<ParamArrayAttribute>();
|
||||
if (isParamsCollection)
|
||||
{
|
||||
if (!argType.IsArray)
|
||||
throw new NotSupportedException(".net 9 params collections are not yet supported");
|
||||
|
||||
// We parse each element directly, as opposed to trying to parse an array.
|
||||
argType = argType.GetElementType()!;
|
||||
}
|
||||
|
||||
return new CommandArgument(
|
||||
arg.Name!,
|
||||
argType,
|
||||
GetArgumentParser(arg, argType),
|
||||
arg.IsOptional,
|
||||
arg.DefaultValue,
|
||||
isParamsCollection);
|
||||
}
|
||||
|
||||
private ITypeParser? GetArgumentParser(ParameterInfo param, Type type)
|
||||
{
|
||||
if (type.ContainsGenericParameters)
|
||||
return null;
|
||||
|
||||
var attrib = param.GetCustomAttribute<CommandArgumentAttribute>();
|
||||
var parser = attrib?.CustomParser is not {} custom
|
||||
? _toolshed.GetArgumentParser(param.ParameterType)
|
||||
? _toolshed.GetArgumentParser(type)
|
||||
: _toolshed.GetArgumentParser(_toolshed.GetCustomParser(custom));
|
||||
|
||||
if (parser == null)
|
||||
@@ -346,25 +412,16 @@ internal sealed class ToolshedCommandImplementor
|
||||
return pipedType is null;
|
||||
|
||||
if (pipedType == null)
|
||||
return false; // We want exact match to be preferred!
|
||||
return false;
|
||||
|
||||
return x.Generic || _toolshed.IsTransformableTo(pipedType, param.ParameterType);
|
||||
|
||||
// Finally, prefer specialized (type exact) implementations.
|
||||
})
|
||||
.OrderByDescending(x =>
|
||||
{
|
||||
if (x.PipeArg is not { } param)
|
||||
return 0;
|
||||
|
||||
if (pipedType!.IsAssignableTo(param.ParameterType))
|
||||
return 1000; // We want exact match to be preferred!
|
||||
if (param.ParameterType.GetMostGenericPossible() == pipedType.GetMostGenericPossible())
|
||||
return 500; // If not, try to prefer the same base type.
|
||||
|
||||
// Finally, prefer specialized (type exact) implementations.
|
||||
return param.ParameterType.IsGenericTypeParameter ? 0 : 100;
|
||||
|
||||
return GetMethodRating(pipedType, param.ParameterType);
|
||||
})
|
||||
.Select(x =>
|
||||
{
|
||||
@@ -387,6 +444,53 @@ internal sealed class ToolshedCommandImplementor
|
||||
.FirstOrDefault(x => x != null);
|
||||
}
|
||||
|
||||
private int GetMethodRating(Type? pipedType, Type paramType)
|
||||
{
|
||||
// This method is used to try rate possible command methods to determine how good of a match
|
||||
// they for a given piped input based on the type of the method's piped argument. I.e., if we are
|
||||
// piping an List<EntProtoId> into a command, in order of most to least preferred we want a method that
|
||||
// takes in a:
|
||||
// - List<EntProtoId>
|
||||
// - List<string>
|
||||
// - Any concrete type (IEnumerable<EntProtoId>, IEnumerable<string>, string, EntProtoId, etc)
|
||||
// - List<T>
|
||||
// - constrained generic parameters (e.g., T where T : IEnumerable)
|
||||
// - unconstrained generic parameters
|
||||
// - Any type constructed out of generic types (e.g., List<T>, IEnumerable<T>)
|
||||
//
|
||||
// Finally, subsequent Select() calls in GetConcreteMethodInternal() will effectively discard any methods that
|
||||
// can't actually be used. E.g., List<int> is preferred over List<T> here, but obviously couldn't be used.
|
||||
//
|
||||
// TBH this whole method is pretty janky, but it works well enough.
|
||||
|
||||
// We want exact match to be preferred.
|
||||
if (pipedType!.IsAssignableTo(paramType))
|
||||
return 1000;
|
||||
|
||||
// We prefer non-generic methods
|
||||
if (!paramType.ContainsGenericParameters)
|
||||
{
|
||||
// Next, we also prefer methods that have the same base type.
|
||||
// E.g., given a List<EntProtoId> we should preferentially match to methods that take in an List<string>
|
||||
if (paramType.GetMostGenericPossible() == pipedType.GetMostGenericPossible())
|
||||
return 500;
|
||||
|
||||
return 400;
|
||||
}
|
||||
|
||||
// Again. we prefer methods that have the same base type.
|
||||
// E.g., given an List<EntProtoId> we should preferentially match to methods that take in an List<T>
|
||||
if (paramType.GetMostGenericPossible() == pipedType.GetMostGenericPossible())
|
||||
return 300;
|
||||
|
||||
// Next we prefer methods that just directly take in some generic type
|
||||
// i.e., we prefer matching the method that takes T over IEnumerable<T>
|
||||
if (paramType.IsGenericParameter)
|
||||
return Math.Min(100 + paramType.GetGenericParameterConstraints().Length, 299);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// When a method has the <see cref="TakesPipedTypeAsGenericAttribute"/>, this method is used to actually
|
||||
/// determine the generic type argument given the type of the piped in value.
|
||||
@@ -477,12 +581,24 @@ internal sealed class ToolshedCommandImplementor
|
||||
// args.Context
|
||||
var ctx = Expression.Property(args, nameof(CommandInvocationArguments.Context));
|
||||
|
||||
// ValueRef<T>.TryEvaluate
|
||||
var evalMethod = typeof(ValueRef<>)
|
||||
.MakeGenericType(param.ParameterType)
|
||||
.GetMethod(nameof(ValueRef<int>.EvaluateParameter), BindingFlags.Static | BindingFlags.NonPublic)!;
|
||||
MethodInfo? evalMethod;
|
||||
|
||||
var isParamsCollection = param.HasCustomAttribute<ParamArrayAttribute>();
|
||||
if (isParamsCollection)
|
||||
{
|
||||
// ValueRef<T>.EvaluateParamsCollection
|
||||
evalMethod = typeof(ValueRef<>)
|
||||
.MakeGenericType(param.ParameterType.GetElementType()!)
|
||||
.GetMethod(nameof(ValueRef<int>.EvaluateParamsCollection), BindingFlags.Static | BindingFlags.NonPublic)!;
|
||||
}
|
||||
else
|
||||
{
|
||||
// ValueRef<T>.EvaluateParameter
|
||||
evalMethod = typeof(ValueRef<>)
|
||||
.MakeGenericType(param.ParameterType)
|
||||
.GetMethod(nameof(ValueRef<int>.EvaluateParameter), BindingFlags.Static | BindingFlags.NonPublic)!;
|
||||
}
|
||||
|
||||
// ValueRef<T>.TryEvaluate(args.Arguments[param.Name], args.Context)
|
||||
return Expression.Call(evalMethod, argValue, ctx);
|
||||
}
|
||||
|
||||
@@ -509,26 +625,82 @@ internal sealed class ToolshedCommandImplementor
|
||||
{
|
||||
builder.Append(Environment.NewLine + " ");
|
||||
|
||||
if (method.PipeArg != null)
|
||||
builder.Append($"<{method.PipeArg.Name} ({GetFriendlyName(method.PipeArg.ParameterType)})> -> ");
|
||||
// TODO TOOLSHED
|
||||
// FormattedMessage support for help strings
|
||||
// make the argument type hint colour coded, for easier parsing of help strings.
|
||||
// I.e., in "<input (IEnumerable<Int>)> make the "(IEnumerable<Int>)" part gray?
|
||||
if (method.PipeArg is {} pipeArg)
|
||||
{
|
||||
var locKey = $"command-arg-sig-{LocName}-{pipeArg.Name}";
|
||||
if (!_loc.TryGetString(locKey, out var pipeSig))
|
||||
pipeSig = ToolshedCommand.GetArgHint(pipeArg.Name!, false, false, pipeArg.ParameterType);
|
||||
|
||||
builder.Append($"{pipeSig} → ");
|
||||
}
|
||||
|
||||
if (method.Invertible)
|
||||
builder.Append("[not] ");
|
||||
|
||||
builder.Append(FullName);
|
||||
|
||||
foreach (var (argName, argType) in method.Arguments)
|
||||
{
|
||||
builder.Append($" <{argName} ({GetFriendlyName(argType)})>");
|
||||
}
|
||||
AddMethodSignature(builder, method.Arguments);
|
||||
|
||||
if (method.Info.ReturnType != typeof(void))
|
||||
builder.Append($" -> {GetFriendlyName(method.Info.ReturnType)}");
|
||||
builder.Append($" → {method.Info.ReturnType.PrettyName()}");
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Construct the methods signature for help and explain commands.
|
||||
/// </summary>
|
||||
internal void AddMethodSignature(StringBuilder builder, CommandArgument[] args, Type[]? typeArgs = null)
|
||||
{
|
||||
builder.Append(FullName);
|
||||
|
||||
var tParsers = Owner.TypeParameterParsers;
|
||||
var numParsers = 0;
|
||||
foreach (var parserType in tParsers)
|
||||
{
|
||||
if (parserType == typeof(TypeTypeParser))
|
||||
continue;
|
||||
|
||||
var parser = _toolshed.GetCustomParser(parserType);
|
||||
if (parser.ShowTypeArgSignature)
|
||||
numParsers++;
|
||||
}
|
||||
|
||||
// Add "<T1, T2>" for methods that take in type arguments.
|
||||
if (numParsers > 0)
|
||||
{
|
||||
builder.Append('<');
|
||||
for (var i = 0; i < numParsers; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
builder.Append(", ");
|
||||
|
||||
if (typeArgs != null)
|
||||
{
|
||||
builder.Append(typeArgs[i].PrettyName());
|
||||
continue;
|
||||
}
|
||||
|
||||
builder.Append('T');
|
||||
if (numParsers > 1)
|
||||
builder.Append(i + 1);
|
||||
}
|
||||
builder.Append('>');
|
||||
}
|
||||
|
||||
foreach (var arg in args)
|
||||
{
|
||||
builder.Append(' ');
|
||||
if (_loc.TryGetString($"command-arg-sig-{LocName}-{arg.Name}", out var msg))
|
||||
builder.Append(msg);
|
||||
else
|
||||
builder.Append(ToolshedCommand.GetArgHint(arg, arg.Type));
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ToolshedCommand.DescriptionLocKey"/>
|
||||
public string DescriptionLocKey() => $"command-description-{LocName}";
|
||||
|
||||
@@ -537,33 +709,11 @@ internal sealed class ToolshedCommandImplementor
|
||||
{
|
||||
return _loc.GetString(DescriptionLocKey());
|
||||
}
|
||||
|
||||
public static string GetFriendlyName(Type type)
|
||||
{
|
||||
var friendlyName = type.Name;
|
||||
if (!type.IsGenericType)
|
||||
return friendlyName;
|
||||
|
||||
var iBacktick = friendlyName.IndexOf('`');
|
||||
if (iBacktick > 0)
|
||||
friendlyName = friendlyName.Remove(iBacktick);
|
||||
|
||||
friendlyName += "<";
|
||||
var typeParameters = type.GetGenericArguments();
|
||||
for (var i = 0; i < typeParameters.Length; ++i)
|
||||
{
|
||||
var typeParamName = GetFriendlyName(typeParameters[i]);
|
||||
friendlyName += (i == 0 ? typeParamName : "," + typeParamName);
|
||||
}
|
||||
friendlyName += ">";
|
||||
|
||||
return friendlyName;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Struct for caching information about a command's methods. Helps reduce LINQ & reflection calls when attempting
|
||||
/// Class for caching information about a command's methods. Helps reduce LINQ & reflection calls when attempting
|
||||
/// to find matching methods.
|
||||
/// </summary>
|
||||
internal sealed class CommandMethod
|
||||
@@ -587,9 +737,9 @@ internal sealed class CommandMethod
|
||||
/// </summary>
|
||||
public readonly bool PipeGeneric;
|
||||
|
||||
public readonly (string, Type)[] Arguments;
|
||||
public readonly CommandArgument[] Arguments;
|
||||
|
||||
public CommandMethod(MethodInfo info)
|
||||
public CommandMethod(MethodInfo info, ToolshedCommandImplementor impl)
|
||||
{
|
||||
Info = info;
|
||||
PipeArg = info.ConsoleGetPipedArgument();
|
||||
@@ -597,7 +747,7 @@ internal sealed class CommandMethod
|
||||
|
||||
Arguments = info.GetParameters()
|
||||
.Where(x => x.IsCommandArgument())
|
||||
.Select(x => (x.Name ?? string.Empty, x.ParameterType))
|
||||
.Select(impl.GetCommandArgument)
|
||||
.ToArray();
|
||||
|
||||
if (!info.IsGenericMethodDefinition)
|
||||
@@ -609,7 +759,14 @@ internal sealed class CommandMethod
|
||||
}
|
||||
|
||||
internal readonly record struct ConcreteCommandMethod(MethodInfo Info, CommandArgument[] Args, CommandMethod Base);
|
||||
internal readonly record struct CommandArgument(string Name, Type Type, ITypeParser Parser);
|
||||
|
||||
public readonly record struct CommandArgument(
|
||||
string Name,
|
||||
Type Type,
|
||||
ITypeParser? Parser,
|
||||
bool IsOptional,
|
||||
object? DefaultValue,
|
||||
bool IsParamsCollection);
|
||||
|
||||
public sealed class ArgumentParseError(Type type, Type parser) : ConError
|
||||
{
|
||||
|
||||
@@ -242,12 +242,12 @@ public sealed partial class ToolshedManager
|
||||
/// <summary>
|
||||
/// iunno man it does autocomplete what more do u want
|
||||
/// </summary>
|
||||
public CompletionResult? TryAutocomplete(ParserContext ctx, Type t, string? argName)
|
||||
public CompletionResult? TryAutocomplete(ParserContext ctx, Type t, CommandArgument? arg)
|
||||
{
|
||||
DebugTools.AssertNull(ctx.Error);
|
||||
DebugTools.AssertNull(ctx.Completions);
|
||||
DebugTools.AssertEqual(ctx.GenerateCompletions, true);
|
||||
return GetParserForType(t)?.TryAutocomplete(ctx, argName);
|
||||
return GetParserForType(t)?.TryAutocomplete(ctx, arg);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -54,6 +54,20 @@ public sealed partial class ToolshedManager
|
||||
return t.PrettyName();
|
||||
}
|
||||
|
||||
if (value.GetType().IsAssignableTo(typeof(IDictionary)))
|
||||
{
|
||||
var dict = ((IDictionary) value).GetEnumerator();
|
||||
|
||||
var kvList = new List<string>();
|
||||
|
||||
while (dict.MoveNext())
|
||||
{
|
||||
kvList.Add($"({PrettyPrintType(dict.Key, out _)}, {PrettyPrintType(dict.Value, out _)}");
|
||||
}
|
||||
|
||||
return $"Dictionary {{\n{string.Join(",\n", kvList)}\n}}";
|
||||
}
|
||||
|
||||
if (value is IEnumerable @enum)
|
||||
{
|
||||
var list = @enum.Cast<object?>().ToList();
|
||||
@@ -67,20 +81,6 @@ public sealed partial class ToolshedManager
|
||||
return res;
|
||||
}
|
||||
|
||||
if (value.GetType().IsAssignableTo(typeof(IDictionary)))
|
||||
{
|
||||
var dict = ((IDictionary) value).GetEnumerator();
|
||||
|
||||
var kvList = new List<string>();
|
||||
|
||||
do
|
||||
{
|
||||
kvList.Add($"({PrettyPrintType(dict.Key, out _)}, {PrettyPrintType(dict.Value, out _)}");
|
||||
} while (dict.MoveNext());
|
||||
|
||||
return $"Dictionary {{\n{string.Join(",\n", kvList)}\n}}";
|
||||
}
|
||||
|
||||
return value.ToString() ?? "[unrepresentable]";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,9 +91,8 @@ public sealed partial class ToolshedManager
|
||||
{
|
||||
if (!_contexts.TryGetValue(session.UserId, out var ctx))
|
||||
{
|
||||
// Can't get a shell here.
|
||||
result = null;
|
||||
return false;
|
||||
var shell = new ConsoleShell(_conHost, session, false);
|
||||
_contexts[session.UserId] = ctx = new(shell);
|
||||
}
|
||||
|
||||
ctx.ClearErrors();
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Toolshed.Errors;
|
||||
using Robust.Shared.Toolshed.Syntax;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.Toolshed.TypeParsers;
|
||||
|
||||
@@ -12,6 +14,7 @@ namespace Robust.Shared.Toolshed.TypeParsers;
|
||||
/// </summary>
|
||||
public sealed class BlockOutputParser : CustomTypeParser<Type>
|
||||
{
|
||||
public override bool ShowTypeArgSignature => false;
|
||||
public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Type? result)
|
||||
{
|
||||
result = null;
|
||||
@@ -31,7 +34,7 @@ public sealed class BlockOutputParser : CustomTypeParser<Type>
|
||||
return true;
|
||||
}
|
||||
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName)
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg)
|
||||
{
|
||||
Block.TryParseBlock(ctx, null, null, out _);
|
||||
return ctx.Completions;
|
||||
@@ -52,13 +55,32 @@ public sealed class BlockOutputParser : CustomTypeParser<Type>
|
||||
/// </remarks>>
|
||||
public sealed class MapBlockOutputParser : CustomTypeParser<Type>
|
||||
{
|
||||
public override bool ShowTypeArgSignature => false;
|
||||
public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Type? result)
|
||||
{
|
||||
result = null;
|
||||
|
||||
var pipeType = ctx.Bundle.PipedType;
|
||||
if (pipeType != null && pipeType.IsGenericType(typeof(IEnumerable<>)))
|
||||
pipeType = pipeType.GetGenericArguments()[0];
|
||||
if (pipeType != null)
|
||||
{
|
||||
// TODO TOOLSHED
|
||||
// The concrete implementation should already know that the piped in type must be assignable tosome kind of IEnumerable<T>
|
||||
// So why can't we just already have modified the PipedType to match?
|
||||
|
||||
if (pipeType.IsGenericType(typeof(IEnumerable<>))) // most common case
|
||||
pipeType = pipeType.GetGenericArguments()[0];
|
||||
else if (pipeType.IsGenericType(typeof(List<>))) // common for toolshed variables
|
||||
pipeType = pipeType.GetGenericArguments()[0];
|
||||
else if (pipeType.IsArray)
|
||||
pipeType = pipeType.GetElementType()!;
|
||||
else
|
||||
{
|
||||
// Slow fallback
|
||||
// TODO TOOLSHED Cache this?
|
||||
var @interface = pipeType.GetInterfaces().FirstOrDefault(x => x.IsGenericType(typeof(IEnumerable<>)));
|
||||
pipeType = @interface?.GetGenericArguments()[0] ?? pipeType;
|
||||
}
|
||||
}
|
||||
|
||||
var save = ctx.Save();
|
||||
var start = ctx.Index;
|
||||
@@ -77,7 +99,7 @@ public sealed class MapBlockOutputParser : CustomTypeParser<Type>
|
||||
return true;
|
||||
}
|
||||
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName)
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg)
|
||||
{
|
||||
var pipeType = ctx.Bundle.PipedType;
|
||||
if (pipeType != null && pipeType.IsGenericType(typeof(IEnumerable<>)))
|
||||
|
||||
@@ -11,7 +11,7 @@ internal sealed class BlockTypeParser : TypeParser<Block>
|
||||
return Block.TryParse(ctx, out result);
|
||||
}
|
||||
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName)
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg)
|
||||
{
|
||||
Block.TryParse(ctx, out _);
|
||||
return ctx.Completions;
|
||||
@@ -25,7 +25,7 @@ internal sealed class BlockTypeParser<T> : TypeParser<Block<T>>
|
||||
return Block<T>.TryParse(ctx, out result);
|
||||
}
|
||||
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName)
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg)
|
||||
{
|
||||
Block<T>.TryParse(ctx, out _);
|
||||
return ctx.Completions;
|
||||
@@ -39,7 +39,7 @@ internal sealed class BlockTypeParser<TIn, TOut> : TypeParser<Block<TIn, TOut>>
|
||||
return Block<TIn, TOut>.TryParse(ctx, out result);
|
||||
}
|
||||
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName)
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg)
|
||||
{
|
||||
Block<TIn, TOut>.TryParse(ctx, out _);
|
||||
return ctx.Completions;
|
||||
|
||||
@@ -43,9 +43,9 @@ public sealed class BoolTypeParser : TypeParser<bool>
|
||||
return false;
|
||||
}
|
||||
|
||||
public override CompletionResult TryAutocomplete(ParserContext parserContext, string? argName)
|
||||
public override CompletionResult TryAutocomplete(ParserContext parserContext, CommandArgument? arg)
|
||||
{
|
||||
return CompletionResult.FromOptions(new[] {"true", "false"});
|
||||
return CompletionResult.FromHintOptions(new[] {"true", "false"}, GetArgHint(arg));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ internal sealed class CommandRunTypeParser : TypeParser<CommandRun>
|
||||
return CommandRun.TryParse(ctx, null, null, out result);
|
||||
}
|
||||
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName)
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg)
|
||||
{
|
||||
CommandRun.TryParse(ctx, null, null, out _);
|
||||
return ctx.Completions;
|
||||
@@ -25,7 +25,7 @@ internal sealed class ExpressionTypeParser<T> : TypeParser<CommandRun<T>>
|
||||
return CommandRun<T>.TryParse(ctx, null, out result);
|
||||
}
|
||||
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName)
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg)
|
||||
{
|
||||
CommandRun<T>.TryParse(ctx, null, out _);
|
||||
return ctx.Completions;
|
||||
@@ -39,7 +39,7 @@ internal sealed class ExpressionTypeParser<TIn, TOut> : TypeParser<CommandRun<TI
|
||||
return CommandRun<TIn, TOut>.TryParse(ctx, out result);
|
||||
}
|
||||
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName)
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg)
|
||||
{
|
||||
CommandRun<TIn, TOut>.TryParse(ctx, out _);
|
||||
return ctx.Completions;
|
||||
|
||||
@@ -72,7 +72,7 @@ public sealed class CommandSpecTypeParser : TypeParser<CommandSpec>
|
||||
return true;
|
||||
}
|
||||
|
||||
public override CompletionResult TryAutocomplete(ParserContext parserContext, string? argName)
|
||||
public override CompletionResult? TryAutocomplete(ParserContext parserContext, CommandArgument? arg)
|
||||
{
|
||||
var cmds = parserContext.Environment.AllCommands();
|
||||
return CompletionResult.FromHintOptions(cmds.Select(x => x.AsCompletion()), "<command name>");
|
||||
|
||||
@@ -40,10 +40,11 @@ public sealed class ComponentTypeParser : CustomTypeParser<Type>
|
||||
return true;
|
||||
}
|
||||
|
||||
public override CompletionResult? TryAutocomplete(ParserContext parserContext,
|
||||
string? argName)
|
||||
public override CompletionResult TryAutocomplete(
|
||||
ParserContext parserContext,
|
||||
CommandArgument? arg)
|
||||
{
|
||||
return CompletionResult.FromOptions(_factory.AllRegisteredTypes.Select(_factory.GetComponentName));
|
||||
return CompletionResult.FromHintOptions(_factory.AllRegisteredTypes.Select(_factory.GetComponentName), GetArgHint(arg));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,8 +53,8 @@ internal sealed class EntityTypeParser : TypeParser<EntityUid>
|
||||
return TryParseEntity(_entMan, parser, out result);
|
||||
}
|
||||
|
||||
public override CompletionResult TryAutocomplete(ParserContext ctx, string? argName)
|
||||
=> CompletionResult.FromHint(argName == null ? "<NetEntity>" : $"<{argName}> (NetEntity)");
|
||||
public override CompletionResult TryAutocomplete(ParserContext ctx, CommandArgument? arg)
|
||||
=> CompletionResult.FromHint(ToolshedCommand.GetArgHint(arg, typeof(NetEntity)));
|
||||
}
|
||||
|
||||
internal sealed class NetEntityTypeParser : TypeParser<NetEntity>
|
||||
@@ -100,8 +100,8 @@ internal sealed class NetEntityTypeParser : TypeParser<NetEntity>
|
||||
return false;
|
||||
}
|
||||
|
||||
public override CompletionResult TryAutocomplete(ParserContext ctx, string? argName)
|
||||
=> CompletionResult.FromHint(argName == null ? "<NetEntity>" : $"<{argName}> (NetEntity)");
|
||||
public override CompletionResult TryAutocomplete(ParserContext ctx, CommandArgument? arg)
|
||||
=> CompletionResult.FromHint(ToolshedCommand.GetArgHint(arg, typeof(NetEntity)));
|
||||
}
|
||||
|
||||
internal sealed class EntityTypeParser<T> : TypeParser<Entity<T>>
|
||||
@@ -122,7 +122,7 @@ internal sealed class EntityTypeParser<T> : TypeParser<Entity<T>>
|
||||
return true;
|
||||
}
|
||||
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName)
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg)
|
||||
{
|
||||
// Avoid commands with loose permissions accidentally leaking information about entities.
|
||||
// I.e., if some command had an Entity<MindComponent> argument, we don't want auto-completions for
|
||||
@@ -130,7 +130,7 @@ internal sealed class EntityTypeParser<T> : TypeParser<Entity<T>>
|
||||
if (!ctx.CheckInvokable<EntitiesCommand>())
|
||||
return null;
|
||||
|
||||
var hint = argName == null ? "<NetEntity>" : $"<{argName}> (NetEntity)";
|
||||
var hint = ToolshedCommand.GetArgHint(arg, typeof(NetEntity));
|
||||
|
||||
// Avoid dumping too many entities
|
||||
if (_entMan.Count<T>() > 128)
|
||||
@@ -169,12 +169,12 @@ internal sealed class EntityTypeParser<T1, T2> : TypeParser<Entity<T1, T2>>
|
||||
return true;
|
||||
}
|
||||
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName)
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg)
|
||||
{
|
||||
if (!ctx.CheckInvokable<EntitiesCommand>())
|
||||
return null;
|
||||
|
||||
var hint = argName == null ? "<NetEntity>" : $"<{argName}> (NetEntity)";
|
||||
var hint = ToolshedCommand.GetArgHint(arg, typeof(NetEntity));
|
||||
if (_entMan.Count<T1>() > 128)
|
||||
return CompletionResult.FromHint(hint);
|
||||
|
||||
@@ -215,12 +215,12 @@ internal sealed class EntityTypeParser<T1, T2, T3> : TypeParser<Entity<T1, T2, T
|
||||
return true;
|
||||
}
|
||||
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName)
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg)
|
||||
{
|
||||
if (!ctx.CheckInvokable<EntitiesCommand>())
|
||||
return null;
|
||||
|
||||
var hint = argName == null ? "<NetEntity>" : $"<{argName}> (NetEntity)";
|
||||
var hint = ToolshedCommand.GetArgHint(arg, typeof(NetEntity));
|
||||
if (_entMan.Count<T1>() > 128)
|
||||
return CompletionResult.FromHint(hint);
|
||||
|
||||
|
||||
@@ -40,9 +40,9 @@ public sealed class EnumTypeParser<T> : TypeParser<T>
|
||||
return true;
|
||||
}
|
||||
|
||||
public override CompletionResult? TryAutocomplete(ParserContext parserContext, string? argName)
|
||||
public override CompletionResult? TryAutocomplete(ParserContext parserContext, CommandArgument? arg)
|
||||
{
|
||||
return CompletionResult.FromOptions(Enum.GetNames<T>());
|
||||
return CompletionResult.FromHintOptions(Enum.GetNames<T>(), GetArgHint(arg));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,9 +12,9 @@ public sealed class InstanceIdTypeParser : TypeParser<InstanceId>
|
||||
return true;
|
||||
}
|
||||
|
||||
public override CompletionResult? TryAutocomplete(ParserContext parserContext, string? argName)
|
||||
public override CompletionResult? TryAutocomplete(ParserContext parserContext, CommandArgument? arg)
|
||||
{
|
||||
return null;
|
||||
return CompletionResult.FromHint(GetArgHint(arg));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,9 +51,9 @@ public sealed class AngleTypeParser : TypeParser<Angle>
|
||||
}
|
||||
}
|
||||
|
||||
public override CompletionResult TryAutocomplete(ParserContext parserContext, string? argName)
|
||||
public override CompletionResult? TryAutocomplete(ParserContext parserContext, CommandArgument? arg)
|
||||
{
|
||||
return CompletionResult.FromHint("angle (append deg for degrees, otherwise radians)");
|
||||
return CompletionResult.FromHint($"{GetArgHint(arg)}\nAppend \"deg\" for degrees");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,10 +36,10 @@ public sealed class ColorTypeParser : TypeParser<Color>
|
||||
|
||||
}
|
||||
|
||||
public override CompletionResult? TryAutocomplete(ParserContext parserContext, string? argName)
|
||||
public override CompletionResult TryAutocomplete(ParserContext parserContext, CommandArgument? arg)
|
||||
{
|
||||
return CompletionResult.FromHintOptions(Color.GetAllDefaultColors().Select(x => x.Key),
|
||||
"RGB color or color name.");
|
||||
$"{GetArgHint(arg)}\nHex code or color name.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,10 +30,11 @@ internal sealed class NumberBaseTypeParser<T> : TypeParser<T>
|
||||
return false;
|
||||
}
|
||||
|
||||
public override CompletionResult TryAutocomplete(ParserContext parserContext,
|
||||
string? argName)
|
||||
public override CompletionResult? TryAutocomplete(
|
||||
ParserContext parserContext,
|
||||
CommandArgument? arg)
|
||||
{
|
||||
return CompletionResult.FromHint(typeof(T).PrettyName());
|
||||
return CompletionResult.FromHint(GetArgHint(arg));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ public abstract class SpanLikeTypeParser<T, TElem> : TypeParser<T>
|
||||
return true;
|
||||
}
|
||||
|
||||
public override CompletionResult TryAutocomplete(ParserContext parserContext, string? argName)
|
||||
public override CompletionResult? TryAutocomplete(ParserContext parserContext, CommandArgument? arg)
|
||||
{
|
||||
return CompletionResult.FromHint(typeof(T).PrettyName());
|
||||
}
|
||||
|
||||
@@ -49,13 +49,13 @@ public sealed class ProtoIdTypeParser<T> : TypeParser<ProtoId<T>>
|
||||
return true;
|
||||
}
|
||||
|
||||
public override CompletionResult TryAutocomplete(ParserContext ctx, string? argName)
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg)
|
||||
{
|
||||
if (_completions != null)
|
||||
return _completions;
|
||||
|
||||
_proto.TryGetKindFrom<T>(out var kind);
|
||||
var hint = $"<{kind} prototype>";
|
||||
var hint = ToolshedCommand.GetArgHint(arg, typeof(ProtoId<T>));
|
||||
|
||||
_completions = _proto.Count<T>() < 256
|
||||
? CompletionResult.FromHintOptions( CompletionHelper.PrototypeIDs<T>(proto: _proto), hint)
|
||||
@@ -76,12 +76,12 @@ public sealed class EntProtoIdTypeParser : TypeParser<EntProtoId>
|
||||
return true;
|
||||
}
|
||||
|
||||
public override CompletionResult TryAutocomplete(ParserContext parserContext, string? argName)
|
||||
public override CompletionResult? TryAutocomplete(ParserContext parserContext, CommandArgument? arg)
|
||||
{
|
||||
// TODO TOOLSHED Improve ProtoId completions
|
||||
// Completion options should be able to communicate to a client that it can populate the options by itself.
|
||||
// I.e., instead of dumping all entity prototypes on the client, tell it how to generate them locally.
|
||||
return CompletionResult.FromHint($"<Entity prototype>");
|
||||
return CompletionResult.FromHint(GetArgHint(arg));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,9 +106,9 @@ public sealed class PrototypeInstanceTypeParser<T> : TypeParser<T>
|
||||
return false;
|
||||
}
|
||||
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName)
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg)
|
||||
{
|
||||
return Toolshed.TryAutocomplete(ctx, typeof(ProtoId<T>), argName);
|
||||
return Toolshed.TryAutocomplete(ctx, typeof(ProtoId<T>), arg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ internal sealed class PrototypeTypeParser<T> : TypeParser<Prototype<T>>
|
||||
return true;
|
||||
}
|
||||
|
||||
public override CompletionResult TryAutocomplete(ParserContext ctx, string? argName)
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg)
|
||||
{
|
||||
IEnumerable<CompletionOption> options;
|
||||
|
||||
|
||||
@@ -43,10 +43,11 @@ internal sealed class QuantityParser : TypeParser<Quantity>
|
||||
return true;
|
||||
}
|
||||
|
||||
public override CompletionResult? TryAutocomplete(ParserContext parserContext,
|
||||
string? argName)
|
||||
public override CompletionResult? TryAutocomplete(
|
||||
ParserContext parserContext,
|
||||
CommandArgument? arg)
|
||||
{
|
||||
return CompletionResult.FromHint($"{argName ?? "quantity"}");
|
||||
return CompletionResult.FromHint(GetArgHint(arg));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,9 +16,9 @@ internal sealed class ResPathTypeParser : TypeParser<ResPath>
|
||||
return true;
|
||||
}
|
||||
|
||||
public override CompletionResult TryAutocomplete(ParserContext parserContext, string? argName)
|
||||
public override CompletionResult? TryAutocomplete(ParserContext parserContext, CommandArgument? arg)
|
||||
{
|
||||
// TODO TOOLSHED ResPath Completion
|
||||
return CompletionResult.FromHint($"\"<{argName ?? nameof(ResPath)}>\"");
|
||||
return CompletionResult.FromHint(GetArgHint(arg));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,10 +41,10 @@ internal sealed class SessionTypeParser : TypeParser<ICommonSession>
|
||||
return false;
|
||||
}
|
||||
|
||||
public override CompletionResult? TryAutocomplete(ParserContext parserContext, string? argName)
|
||||
public override CompletionResult TryAutocomplete(ParserContext parserContext, CommandArgument? arg)
|
||||
{
|
||||
var opts = CompletionHelper.SessionNames(true, _player);
|
||||
return CompletionResult.FromHintOptions(opts, "<player session>");
|
||||
return CompletionResult.FromHintOptions(opts, GetArgHint(arg));
|
||||
}
|
||||
|
||||
public record InvalidUsername(ILocalizationManager Loc, string Username) : IConError
|
||||
|
||||
@@ -70,9 +70,9 @@ internal sealed class StringTypeParser : TypeParser<string>
|
||||
return false;
|
||||
}
|
||||
|
||||
public override CompletionResult TryAutocomplete(ParserContext parserContext, string? argName)
|
||||
public override CompletionResult? TryAutocomplete(ParserContext parserContext, CommandArgument? arg)
|
||||
{
|
||||
var hint = argName != null ? $"<{argName}> (string)" : "<string>";
|
||||
var hint = GetArgHint(arg);
|
||||
parserContext.ConsumeWhitespace();
|
||||
return parserContext.PeekRune() == new Rune('"')
|
||||
? CompletionResult.FromHint(hint)
|
||||
|
||||
@@ -34,7 +34,7 @@ public abstract class BaseTupleTypeParser<TParses> : TypeParser<TParses>
|
||||
return true;
|
||||
}
|
||||
|
||||
public override CompletionResult? TryAutocomplete(ParserContext parserContext, string? argName)
|
||||
public override CompletionResult? TryAutocomplete(ParserContext parserContext, CommandArgument? arg)
|
||||
{
|
||||
foreach (var field in Fields)
|
||||
{
|
||||
@@ -44,7 +44,7 @@ public abstract class BaseTupleTypeParser<TParses> : TypeParser<TParses>
|
||||
continue;
|
||||
|
||||
parserContext.Restore(checkpoint);
|
||||
return Toolshed.TryAutocomplete(parserContext, field, argName);
|
||||
return Toolshed.TryAutocomplete(parserContext, field, null);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -13,11 +13,11 @@ namespace Robust.Shared.Toolshed.TypeParsers;
|
||||
/// Base interface used by both custom and default type parsers.
|
||||
/// </summary>
|
||||
[UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)]
|
||||
internal interface ITypeParser
|
||||
public interface ITypeParser
|
||||
{
|
||||
public Type Parses { get; }
|
||||
bool TryParse(ParserContext ctx, [NotNullWhen(true)] out object? result);
|
||||
CompletionResult? TryAutocomplete(ParserContext ctx, string? argName);
|
||||
CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg);
|
||||
|
||||
/// <summary>
|
||||
/// If true, then before attempting to use this parser directly, toolshed will instead first try to parse this as a
|
||||
@@ -26,11 +26,18 @@ internal interface ITypeParser
|
||||
/// or variables
|
||||
/// </summary>
|
||||
public bool EnableValueRef { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the type argument should appear in the method's signature. This mainly exists for type-argument
|
||||
/// parsers that infer a type argument based on a regular arguments, like <see cref="VarTypeParser"/>.
|
||||
/// </summary>
|
||||
public virtual bool ShowTypeArgSignature => true;
|
||||
}
|
||||
|
||||
public abstract class BaseParser<T> : ITypeParser, IPostInjectInit where T : notnull
|
||||
{
|
||||
public virtual bool EnableValueRef => true;
|
||||
public virtual bool ShowTypeArgSignature => true;
|
||||
|
||||
// TODO TOOLSHED Localization
|
||||
// Ensure that all of the type parser auto-completions actually use localized strings
|
||||
@@ -46,7 +53,13 @@ public abstract class BaseParser<T> : ITypeParser, IPostInjectInit where T : not
|
||||
}
|
||||
|
||||
public abstract bool TryParse(ParserContext ctx, [NotNullWhen(true)] out T? result);
|
||||
public abstract CompletionResult? TryAutocomplete(ParserContext ctx, string? argName);
|
||||
|
||||
public abstract CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg);
|
||||
|
||||
protected string GetArgHint(CommandArgument? arg)
|
||||
{
|
||||
return ToolshedCommand.GetArgHint(arg, typeof(T));
|
||||
}
|
||||
|
||||
public Type Parses => typeof(T);
|
||||
|
||||
|
||||
@@ -165,10 +165,13 @@ public sealed class TypeTypeParser : TypeParser<Type>
|
||||
return ty;
|
||||
}
|
||||
|
||||
public override CompletionResult? TryAutocomplete(ParserContext parserContext,
|
||||
string? argName)
|
||||
public override CompletionResult? TryAutocomplete(
|
||||
ParserContext parserContext,
|
||||
CommandArgument? arg)
|
||||
{
|
||||
// TODO TOOLSHED Generic Type Suggestions.
|
||||
if (_optionsCache != null)
|
||||
_optionsCache.Hint = GetArgHint(arg);
|
||||
return _optionsCache;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,9 +21,9 @@ internal sealed class ValueRefTypeParser<T, TAuto> : TypeParser<ValueRef<T, TAut
|
||||
return res;
|
||||
}
|
||||
|
||||
public override CompletionResult? TryAutocomplete(ParserContext parserContext, string? argName)
|
||||
public override CompletionResult? TryAutocomplete(ParserContext parserContext, CommandArgument? arg)
|
||||
{
|
||||
return Toolshed.TryAutocomplete(parserContext, typeof(ValueRef<T, T>), argName);
|
||||
return Toolshed.TryAutocomplete(parserContext, typeof(ValueRef<T, T>), arg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,13 +84,13 @@ internal sealed class ValueRefTypeParser<T> : TypeParser<ValueRef<T>>
|
||||
public static CompletionResult? TryAutocomplete(
|
||||
ToolshedManager shed,
|
||||
ParserContext ctx,
|
||||
string? argName,
|
||||
CommandArgument? arg,
|
||||
ITypeParser? parser)
|
||||
{
|
||||
ctx.ConsumeWhitespace();
|
||||
var rune = ctx.PeekRune();
|
||||
if (rune == new Rune('$'))
|
||||
return shed.TryAutocomplete(ctx, typeof(VarRef<T>), argName);
|
||||
return shed.TryAutocomplete(ctx, typeof(VarRef<T>), arg);
|
||||
|
||||
if (rune == new Rune('{'))
|
||||
{
|
||||
@@ -103,13 +103,13 @@ internal sealed class ValueRefTypeParser<T> : TypeParser<ValueRef<T>>
|
||||
if (parser == null)
|
||||
return CompletionResult.FromHint($"<variable or block of type {typeof(T).PrettyName()}>");
|
||||
|
||||
var res = parser.TryAutocomplete(ctx, null);
|
||||
var res = parser.TryAutocomplete(ctx, arg);
|
||||
return res ?? CompletionResult.FromHint($"<variable, block, or value of type {typeof(T).PrettyName()}>");
|
||||
}
|
||||
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName)
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg)
|
||||
{
|
||||
return TryAutocomplete(Toolshed, ctx, argName, null);
|
||||
return TryAutocomplete(Toolshed, ctx, arg, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,10 +117,10 @@ internal sealed class CustomValueRefTypeParser<T, TParser> : CustomTypeParser<Va
|
||||
where TParser : CustomTypeParser<T>, new()
|
||||
where T : notnull
|
||||
{
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName)
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg)
|
||||
{
|
||||
var parser = Toolshed.GetCustomParser<TParser, T>();
|
||||
return ValueRefTypeParser<T>.TryAutocomplete(Toolshed, ctx, argName, parser);
|
||||
return ValueRefTypeParser<T>.TryAutocomplete(Toolshed, ctx, arg, parser);
|
||||
}
|
||||
|
||||
public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out ValueRef<T>? result)
|
||||
|
||||
@@ -23,6 +23,7 @@ namespace Robust.Shared.Toolshed.TypeParsers;
|
||||
/// </remarks>
|
||||
public sealed class VarTypeParser : CustomTypeParser<Type>
|
||||
{
|
||||
public override bool ShowTypeArgSignature => false;
|
||||
public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Type? result)
|
||||
{
|
||||
result = null;
|
||||
@@ -53,7 +54,7 @@ public sealed class VarTypeParser : CustomTypeParser<Type>
|
||||
return false;
|
||||
}
|
||||
|
||||
public override CompletionResult TryAutocomplete(ParserContext ctx, string? argName)
|
||||
public override CompletionResult TryAutocomplete(ParserContext ctx, CommandArgument? arg)
|
||||
{
|
||||
return ctx.VariableParser.GenerateCompletions();
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ internal sealed class VarRefTypeParser<T> : TypeParser<VarRef<T>>
|
||||
return true;
|
||||
}
|
||||
|
||||
public override CompletionResult TryAutocomplete(ParserContext parserContext, string? argName)
|
||||
public override CompletionResult TryAutocomplete(ParserContext parserContext, CommandArgument? arg)
|
||||
{
|
||||
return parserContext.VariableParser.GenerateCompletions<T>();
|
||||
}
|
||||
@@ -67,7 +67,7 @@ public sealed class WriteableVarRefParser<T> : TypeParser<WriteableVarRef<T>>
|
||||
return false;
|
||||
}
|
||||
|
||||
public override CompletionResult TryAutocomplete(ParserContext parserContext, string? argName)
|
||||
public override CompletionResult? TryAutocomplete(ParserContext parserContext, CommandArgument? arg)
|
||||
{
|
||||
return parserContext.VariableParser.GenerateCompletions<T>(false);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Toolshed;
|
||||
using Robust.Shared.Toolshed.Syntax;
|
||||
using Robust.Shared.Toolshed.TypeParsers;
|
||||
@@ -68,13 +71,37 @@ public sealed class TestCustomVarRefParserCommand : ToolshedCommand
|
||||
return true;
|
||||
}
|
||||
|
||||
public override CompletionResult TryAutocomplete(ParserContext ctx, string? argName)
|
||||
public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg)
|
||||
{
|
||||
return new CompletionResult([new("A")], "B");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[ToolshedCommand]
|
||||
public sealed class TestOptionalArgsCommand : ToolshedCommand
|
||||
{
|
||||
[CommandImplementation]
|
||||
public int[] Impl(int x, int y = 0, int z = 1)
|
||||
=> [x, y, z];
|
||||
}
|
||||
|
||||
[ToolshedCommand]
|
||||
public sealed class TestParamsCollectionCommand : ToolshedCommand
|
||||
{
|
||||
[CommandImplementation]
|
||||
public int[] Impl(int x, int y = 0, params int[] others)
|
||||
=> [x, y, ..others];
|
||||
}
|
||||
|
||||
[ToolshedCommand]
|
||||
public sealed class TestParamsOnlyCommand : ToolshedCommand
|
||||
{
|
||||
[CommandImplementation]
|
||||
public int[] Impl(params int[] others)
|
||||
=> others;
|
||||
}
|
||||
|
||||
[ToolshedCommand]
|
||||
public sealed class TestCustomParserCommand : ToolshedCommand
|
||||
{
|
||||
@@ -88,3 +115,103 @@ public sealed class TestCustomParserCommand : ToolshedCommand
|
||||
public override bool EnableValueRef => false;
|
||||
}
|
||||
}
|
||||
|
||||
[ToolshedCommand]
|
||||
public sealed class TestEnumerableInferCommand : ToolshedCommand
|
||||
{
|
||||
[CommandImplementation, TakesPipedTypeAsGeneric]
|
||||
public Type Impl<T>([PipedArgument] IEnumerable<T> x, T y) => typeof(T);
|
||||
}
|
||||
|
||||
[ToolshedCommand]
|
||||
public sealed class TestListInferCommand : ToolshedCommand
|
||||
{
|
||||
[CommandImplementation, TakesPipedTypeAsGeneric]
|
||||
public Type Impl<T>([PipedArgument] List<T> x, T y) => typeof(T);
|
||||
}
|
||||
|
||||
[ToolshedCommand]
|
||||
public sealed class TestArrayInferCommand : ToolshedCommand
|
||||
{
|
||||
[CommandImplementation, TakesPipedTypeAsGeneric]
|
||||
public Type Impl<T>([PipedArgument] T[] x, T y) => typeof(T);
|
||||
}
|
||||
|
||||
[ToolshedCommand]
|
||||
public sealed class TestNestedEnumerableInferCommand : ToolshedCommand
|
||||
{
|
||||
[CommandImplementation, TakesPipedTypeAsGeneric]
|
||||
public Type Impl<T>([PipedArgument] IEnumerable<ProtoId<T>> x)
|
||||
where T : class, IPrototype
|
||||
{
|
||||
return typeof(T);
|
||||
}
|
||||
}
|
||||
|
||||
[ToolshedCommand]
|
||||
public sealed class TestNestedListInferCommand : ToolshedCommand
|
||||
{
|
||||
[CommandImplementation, TakesPipedTypeAsGeneric]
|
||||
public Type Impl<T>([PipedArgument] List<ProtoId<T>> x)
|
||||
where T : class, IPrototype
|
||||
{
|
||||
return typeof(T);
|
||||
}
|
||||
}
|
||||
|
||||
[ToolshedCommand]
|
||||
public sealed class TestNestedArrayInferCommand : ToolshedCommand
|
||||
{
|
||||
[CommandImplementation, TakesPipedTypeAsGeneric]
|
||||
public Type Impl<T>([PipedArgument] ProtoId<T>[] x)
|
||||
where T : class, IPrototype
|
||||
{
|
||||
return typeof(T);
|
||||
}
|
||||
}
|
||||
|
||||
[ToolshedCommand]
|
||||
public sealed class TestArrayCommand : ToolshedCommand
|
||||
{
|
||||
[CommandImplementation]
|
||||
public int[] Impl() => Array.Empty<int>();
|
||||
}
|
||||
|
||||
[ToolshedCommand]
|
||||
public sealed class TestListCommand : ToolshedCommand
|
||||
{
|
||||
[CommandImplementation]
|
||||
public List<int> Impl() => new();
|
||||
}
|
||||
|
||||
[ToolshedCommand]
|
||||
public sealed class TestEnumerableCommand : ToolshedCommand
|
||||
{
|
||||
private static int[] _arr = {1, 3, 3};
|
||||
|
||||
[CommandImplementation]
|
||||
public IEnumerable<int> Impl() => _arr.Select(x => 2 * x);
|
||||
}
|
||||
|
||||
[ToolshedCommand]
|
||||
public sealed class TestNestedArrayCommand : ToolshedCommand
|
||||
{
|
||||
[CommandImplementation]
|
||||
public ProtoId<EntityPrototype>[] Impl() => Array.Empty<ProtoId<EntityPrototype>>();
|
||||
}
|
||||
|
||||
[ToolshedCommand]
|
||||
public sealed class TestNestedListCommand : ToolshedCommand
|
||||
{
|
||||
[CommandImplementation]
|
||||
public List<ProtoId<EntityPrototype>> Impl() => new();
|
||||
}
|
||||
|
||||
[ToolshedCommand]
|
||||
public sealed class TestNestedEnumerableCommand : ToolshedCommand
|
||||
{
|
||||
private static ProtoId<EntityPrototype>[] _arr = Array.Empty<ProtoId<EntityPrototype>>();
|
||||
|
||||
[CommandImplementation]
|
||||
public IEnumerable<ProtoId<EntityPrototype>> Impl() => _arr.OrderByDescending(x => x.Id);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Toolshed;
|
||||
using Robust.Shared.Toolshed.Commands.Generic;
|
||||
using Robust.Shared.Toolshed.Syntax;
|
||||
@@ -118,8 +120,8 @@ public sealed class ToolshedTests : ToolshedTest
|
||||
// Terminators don't actually discard the final output type if it is the end of the command.;
|
||||
AssertResult("testint;", 1);
|
||||
AssertResult("testint; testint;", 1);
|
||||
AssertResult("i 2 + { i 2; }", 4);
|
||||
AssertResult("i 2 + { i 2; ; } ;; ;", 4);
|
||||
ParseError<WrongCommandReturn>("i 2 + { i 2; }");
|
||||
ParseError<WrongCommandReturn>("i 2 + { i 2; ; } ;; ;");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -186,6 +188,216 @@ public sealed class ToolshedTests : ToolshedTest
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestTerminators()
|
||||
{
|
||||
await Server.WaitAssertion(() =>
|
||||
{
|
||||
// Baseline check that these commands work:
|
||||
AssertResult("i 1", 1);
|
||||
AssertResult("i 1 + 1", 2);
|
||||
AssertResult("i { i 1 }", 1);
|
||||
|
||||
// Trailing terminators have no clear effect.
|
||||
AssertResult("i 1;", 1);
|
||||
AssertResult("i { i 1 };", 1);
|
||||
|
||||
// Simple explicit piping works
|
||||
AssertResult("i 1 | + 1", 2);
|
||||
|
||||
// Explicit pipes imply a command is expected. Ending a command or a block after a pipe should error.
|
||||
ParseError<OutOfInputError>("i 1 |");
|
||||
ParseError<UnexpectedCloseBrace>("i { i 1 | }");
|
||||
|
||||
// A terminator inside a block or command run doesn't pipe anything;
|
||||
ParseError<NoImplementationError>("i 1 ; + 1");
|
||||
ParseError<WrongCommandReturn>("i { i 1 ; }");
|
||||
|
||||
// Check double terminators
|
||||
// A starting terminators/pipes will try to be parsed as a command.
|
||||
ParseError<NotValidCommandError>("|");
|
||||
ParseError<NotValidCommandError>(";");
|
||||
ParseError<NotValidCommandError>(";;");
|
||||
ParseError<NotValidCommandError>("||");
|
||||
ParseError<NotValidCommandError>("|;");
|
||||
ParseError<NotValidCommandError>(";|");
|
||||
AssertResult("i 1 ;;", 1);
|
||||
|
||||
// Consecutive pipes will try to parse the second one as the command, which will not succeed.
|
||||
ParseError<NotValidCommandError>("i 1 ||");
|
||||
ParseError<NotValidCommandError>("i 1 |;");
|
||||
ParseError<NotValidCommandError>("i 1 ;|");
|
||||
AssertResult("i 1 ;; i 1", 1);
|
||||
ParseError<NotValidCommandError>("i 1 || i 1");
|
||||
ParseError<NotValidCommandError>("i 1 |; i 1");
|
||||
ParseError<NotValidCommandError>("i 1 ;| i 1");
|
||||
ParseError<NoImplementationError>("i 1 ;; + 1");
|
||||
ParseError<NotValidCommandError>("i 1 || + 1");
|
||||
ParseError<NotValidCommandError>("i 1 |; + 1");
|
||||
ParseError<NotValidCommandError>("i 1 ;| + 1");
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestOptionalArgs()
|
||||
{
|
||||
await Server.WaitAssertion(() =>
|
||||
{
|
||||
// Check that straightforward optional args work.
|
||||
ParseError<ExpectedArgumentError>("testoptionalargs ");
|
||||
AssertResult("testoptionalargs 1", new[] {1, 0, 1});
|
||||
AssertResult("testoptionalargs 1 2", new[] {1, 2, 1});
|
||||
AssertResult("testoptionalargs 1 2 3", new[] {1, 2, 3});
|
||||
AssertResult("testoptionalargs 1 2 3 append 4", new[] {1, 2, 3, 4});
|
||||
ParseError<UnknownCommandError>("testoptionalargs 1 2 3 4");
|
||||
ParseError<InvalidNumber<int>>("testoptionalargs 1 append 4");
|
||||
ParseError<InvalidNumber<int>>("testoptionalargs 1 2 append 4");
|
||||
|
||||
// Check that semicolon terminators interrupt optional args
|
||||
ParseError<ExpectedArgumentError>("testoptionalargs ;");
|
||||
AssertResult("testoptionalargs 1;", new[] {1, 0, 1});
|
||||
AssertResult("testoptionalargs 1 2;", new[] {1, 2, 1});
|
||||
AssertResult("testoptionalargs 1 2 3;", new[] {1, 2, 3});
|
||||
ParseError<UnknownCommandError>("testoptionalargs 1 2 3; 4");
|
||||
AssertResult("testoptionalargs 1 2; i 3", 3);
|
||||
AssertResult("testoptionalargs 1 2 3; i 4", 4);
|
||||
|
||||
// Check that explicit pipes interrupt optional args
|
||||
ParseError<ExpectedArgumentError>("testoptionalargs |");
|
||||
ParseError<OutOfInputError>("testoptionalargs 1 |");
|
||||
AssertResult("testoptionalargs 1 | append 4", new[] {1, 0, 1, 4});
|
||||
AssertResult("testoptionalargs 1 2 | append 4", new[] {1, 2, 1, 4});
|
||||
AssertResult("testoptionalargs 1 2 3 | append 4", new[] {1, 2, 3, 4});
|
||||
|
||||
// Check that variables and blocks can be used to specify optional args;
|
||||
AssertResult("i -1 => $i", -1);
|
||||
AssertResult("testoptionalargs 1 $i", new[] {1, -1, 1});
|
||||
AssertResult("testoptionalargs 1 $i 2", new[] {1, -1, 2});
|
||||
AssertResult("testoptionalargs 1 { i -1 }", new[] {1, -1, 1});
|
||||
AssertResult("testoptionalargs 1 { i -1 } 2", new[] {1, -1, 2});
|
||||
|
||||
// Repeat the above groups of tests, but within a command block.
|
||||
// I.e., wrap the commands in "i 1 join { <old command> }" to prepend "1" to the results.
|
||||
|
||||
// This first block also effectively checks that closing braces can interrupt optional args
|
||||
ParseError<ExpectedArgumentError>("i 1 join { testoptionalargs } ");
|
||||
AssertResult("i 1 join { testoptionalargs 1 } ", new[] {1, 1, 0, 1});
|
||||
AssertResult("i 1 join { testoptionalargs 1 2 }", new[] {1, 1, 2, 1});
|
||||
AssertResult("i 1 join { testoptionalargs 1 2 3 }", new[] {1, 1, 2, 3});
|
||||
AssertResult("i 1 join { testoptionalargs 1 2 3 append 4 }", new[] {1, 1, 2, 3, 4});
|
||||
ParseError<UnknownCommandError>("testoptionalargs 1 2 3 4 }");
|
||||
ParseError<InvalidNumber<int>>("testoptionalargs 1 2 i 3 }");
|
||||
ParseError<NoImplementationError>("testoptionalargs 1 2 3 i 4 }");
|
||||
|
||||
ParseError<ExpectedArgumentError>("i 1 join { testoptionalargs | }");
|
||||
ParseError<UnexpectedCloseBrace>("i 1 join { testoptionalargs 1 | }");
|
||||
AssertResult("i 1 join { testoptionalargs 1 | append 4 }", new[] {1, 1, 0, 1, 4});
|
||||
AssertResult("i 1 join { testoptionalargs 1 2 | append 4 }", new[] {1, 1, 2, 1, 4});
|
||||
AssertResult("i 1 join { testoptionalargs 1 2 3 | append 4 }", new[] {1, 1, 2, 3, 4});
|
||||
|
||||
AssertResult("i 1 join { testoptionalargs 1 $i }", new[] {1, 1, -1, 1});
|
||||
AssertResult("i 1 join { testoptionalargs 1 $i 2 }", new[] {1, 1, -1, 2});
|
||||
AssertResult("i 1 join { testoptionalargs 1 { i -1 } }", new[] {1, 1, -1, 1});
|
||||
AssertResult("i 1 join { testoptionalargs 1 { i -1 } 2 }", new[] {1, 1, -1, 2});
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestParamsCollections()
|
||||
{
|
||||
await Server.WaitAssertion(() =>
|
||||
{
|
||||
// Check that straightforward optional args work.
|
||||
ParseError<ExpectedArgumentError>("testparamscollection");
|
||||
AssertResult("testparamsonly", new int[] {});
|
||||
AssertResult("testparamscollection 1", new[] {1, 0});
|
||||
AssertResult("testparamscollection 1 2", new[] {1, 2});
|
||||
AssertResult("testparamscollection 1 2 3", new[] {1, 2, 3});
|
||||
AssertResult("testparamscollection 1 2 3 4", new[] {1, 2, 3, 4});
|
||||
ParseError<InvalidNumber<int>>("testparamscollection 1 2 append 4");
|
||||
ParseError<InvalidNumber<int>>("testparamscollection 1 2 3 append 4");
|
||||
ParseError<InvalidNumber<int>>("testparamscollection 1 2 3 4 append 4");
|
||||
|
||||
// Check that semicolon terminators interrupt optional args
|
||||
ParseError<ExpectedArgumentError>("testparamscollection ;");
|
||||
AssertResult("testparamsonly;", new int[] { });
|
||||
AssertResult("testparamscollection 1;", new[] {1, 0});
|
||||
AssertResult("testparamscollection 1 2;", new[] {1, 2});
|
||||
AssertResult("testparamscollection 1 2 3;", new[] {1, 2, 3});
|
||||
AssertResult("testparamscollection 1 2 3 4;", new[] {1, 2, 3, 4});
|
||||
AssertResult("testparamscollection 1 2; i 4", 4);
|
||||
AssertResult("testparamscollection 1 2 3; i 4", 4);
|
||||
AssertResult("testparamscollection 1 2 3 4; i 4", 4);
|
||||
|
||||
// Check that explicit pipes interrupt optional args
|
||||
ParseError<ExpectedArgumentError>("testparamscollection |");
|
||||
ParseError<OutOfInputError>("testparamsonly |");
|
||||
ParseError<OutOfInputError>("testparamscollection 1 |");
|
||||
ParseError<OutOfInputError>("testparamscollection 1 2 |");
|
||||
ParseError<OutOfInputError>("testparamscollection 1 2 3 |");
|
||||
ParseError<OutOfInputError>("testparamscollection 1 2 3 4 |");
|
||||
AssertResult("testparamsonly | append 1", new[] {1});
|
||||
AssertResult("testparamscollection 1 | append 1", new[] {1, 0, 1});
|
||||
AssertResult("testparamscollection 1 2 | append 1", new[] {1, 2, 1});
|
||||
AssertResult("testparamscollection 1 2 3 | append 1", new[] {1, 2, 3, 1});
|
||||
AssertResult("testparamscollection 1 2 3 4 | append 1", new[] {1, 2, 3, 4, 1});
|
||||
|
||||
// Check that variables and blocks can be used to specify args inside params arrays;
|
||||
AssertResult("i -1 => $i", -1);
|
||||
AssertResult("testparamscollection 1 2 3 $i 5", new[] {1, 2, 3, -1, 5});
|
||||
AssertResult("testparamscollection 1 2 3 { i -1 } 5", new[] {1, 2, 3, -1, 5});
|
||||
|
||||
// Check that closing braces interrupt optional args
|
||||
AssertResult("i 1 join { testparamsonly }", new[] {1});
|
||||
AssertResult("i 1 join { testparamscollection 1 }", new[] {1, 1, 0});
|
||||
AssertResult("i 1 join { testparamscollection 1 2 }", new[] {1, 1, 2});
|
||||
AssertResult("i 1 join { testparamscollection 1 2 3 }", new[] {1, 1, 2, 3});
|
||||
AssertResult("i 1 join { testparamscollection 1 2 3 4 }", new[] {1, 1, 2, 3, 4});
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check that the type of generic parameters can be correctly inferred from the piped-in value. I.e., when check
|
||||
/// that if we pipe a <see cref="List{T}"/> into a command that takes an <see cref="IEnumerable{T}"/>, the value of
|
||||
/// the generic parameter can be properly inferred.
|
||||
/// </summary>
|
||||
[Test]
|
||||
[TestOf(typeof(TakesPipedTypeAsGenericAttribute))]
|
||||
public async Task TestGenericPipeInference()
|
||||
{
|
||||
await Server.WaitAssertion(() =>
|
||||
{
|
||||
// Pipe T[] -> T[]
|
||||
AssertResult("testarray testarrayinfer 1", typeof(int));
|
||||
|
||||
// Pipe List<T> -> List<T>
|
||||
AssertResult("testlist testlistinfer 1", typeof(int));
|
||||
|
||||
// Pipe T[] -> IEnumerable<T>
|
||||
AssertResult("testarray testenumerableinfer 1", typeof(int));
|
||||
|
||||
// Pipe List<T> -> IEnumerable<T>
|
||||
AssertResult("testlist testenumerableinfer 1", typeof(int));
|
||||
|
||||
// Pipe IEnumerable<T> -> IEnumerable<T>
|
||||
AssertResult("testenumerable testenumerableinfer 1", typeof(int));
|
||||
|
||||
// Repeat but with nested types. i.e. extracting T when piping ProtoId<T> -> IEnumerable<ProtoId<T>>
|
||||
AssertResult("testnestedarray testnestedarrayinfer", typeof(EntityPrototype));
|
||||
AssertResult("testnestedlist testnestedlistinfer", typeof(EntityPrototype));
|
||||
AssertResult("testnestedarray testnestedenumerableinfer", typeof(EntityPrototype));
|
||||
AssertResult("testnestedlist testnestedenumerableinfer", typeof(EntityPrototype));
|
||||
AssertResult("testnestedenumerable testnestedenumerableinfer", typeof(EntityPrototype));
|
||||
|
||||
// The map command used to work when the piped type was passed as an IEnumerable<T> directly, but would fail
|
||||
// when given a List<T> or something else that implemented the interface.
|
||||
// In particular, this would become relevant when using command variables (which store enumerables as a List<T>).
|
||||
AssertResult("i 1 to 4 map { * 2 }", new[] {2, 4, 6, 8});
|
||||
InvokeCommand("i 1 to 4 => $x", out _);
|
||||
AssertResult("var $x map { * 2 }", new[] {2, 4, 6, 8});
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestCompletions()
|
||||
{
|
||||
@@ -199,14 +411,14 @@ public sealed class ToolshedTests : ToolshedTest
|
||||
AssertCompletionEmpty($"testvoid ");
|
||||
|
||||
// Without a whitespace, they will still suggest the hint for the command that is currently being typed.
|
||||
AssertCompletionHint("i 1", "Int32");
|
||||
AssertCompletionHint("i 1", "<value (Int32)>");
|
||||
AssertCompletionSingle($"i 1 => $x", "$x");
|
||||
AssertCompletionContains($"testvoid", "testvoid");
|
||||
|
||||
// If an error occurs while parsing something, but tha error is not at the end of the command, we should
|
||||
// not generate completions. I.e., we don't want to mislead people into thinking a command is valid and is
|
||||
// expecting additional arguments.
|
||||
AssertCompletionHint("i a", "Int32");
|
||||
AssertCompletionHint("i a", "<value (Int32)>");
|
||||
AssertCompletionEmpty("i a ");
|
||||
AssertCompletionEmpty("i a 1");
|
||||
AssertCompletionSingle("i $", "$x");
|
||||
@@ -239,13 +451,13 @@ public sealed class ToolshedTests : ToolshedTest
|
||||
|
||||
// Check completions when typing out: testintstrarg 1 "a"
|
||||
AssertCompletionContains("testintstrarg", "testintstrarg");
|
||||
AssertCompletionHint("testintstrarg ", "Int32");
|
||||
AssertCompletionHint("testintstrarg 1", "Int32");
|
||||
AssertCompletionHint("testintstrarg ", "<i (Int32)>");
|
||||
AssertCompletionHint("testintstrarg 1", "<i (Int32)>");
|
||||
AssertCompletionSingle("testintstrarg 1 ", "\"");
|
||||
AssertCompletionHint("testintstrarg 1 \"", "<string>");
|
||||
AssertCompletionHint("testintstrarg 1 \"a\"", "<string>");
|
||||
AssertCompletionHint("testintstrarg 1 \"", "<str (String)>");
|
||||
AssertCompletionHint("testintstrarg 1 \"a\"", "<str (String)>");
|
||||
AssertCompletionEmpty("testintstrarg 1 \"a\" ");
|
||||
AssertCompletionHint("testintstrarg 1 \"a\" + ", "Int32");
|
||||
AssertCompletionHint("testintstrarg 1 \"a\" + ", "<y (Int32)>");
|
||||
|
||||
AssertCompletionContains("i 5 iota reduce { ma", "max");
|
||||
AssertCompletionContains("i 5 iota reduce { max $", "$x", "$value");
|
||||
|
||||
Reference in New Issue
Block a user