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:
Leon Friedrich
2025-02-16 21:55:05 +11:00
committed by GitHub
parent faef44daaa
commit b323c8bd1e
47 changed files with 963 additions and 275 deletions
+12
View File
@@ -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
+6 -7
View File
@@ -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.
+5 -2
View File
@@ -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;
+41 -12
View File
@@ -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 '}')");
}
}
+17 -26
View File
@@ -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);
}
+30 -13
View File
@@ -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;
}
}
+17
View File
@@ -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>")]
+32 -1
View File
@@ -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})>";
}
}
+24
View File
@@ -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]";
}
}
+2 -3
View File
@@ -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");