From 54d9bef2584773208f5c41a211628b348ee4d130 Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Mon, 23 Dec 2024 21:35:18 +1300 Subject: [PATCH 01/23] Include argument name in completion suggestions --- .../Commands/Generic/EmplaceCommand.cs | 4 +-- .../Commands/Generic/ReduceCommand.cs | 2 +- .../Toolshed/Commands/Misc/ExplainCommand.cs | 3 +- .../Toolshed/ToolshedCommandImplementor.cs | 22 +++++++++------ .../Toolshed/ToolshedManager.Parsing.cs | 4 +-- .../Toolshed/TypeParsers/BlockType.cs | 4 +-- .../Toolshed/TypeParsers/BlockTypeParser.cs | 6 ++-- .../Toolshed/TypeParsers/BoolTypeParser.cs | 4 +-- .../TypeParsers/CommandRunTypeParser.cs | 6 ++-- .../TypeParsers/CommandSpecTypeParser.cs | 2 +- .../TypeParsers/ComponentTypeParser.cs | 7 +++-- .../Toolshed/TypeParsers/EntityTypeParser.cs | 20 ++++++------- .../Toolshed/TypeParsers/EnumTypeParser.cs | 4 +-- .../TypeParsers/InstanceIdTypeParser.cs | 4 +-- .../TypeParsers/Math/AngleTypeParser.cs | 4 +-- .../TypeParsers/Math/ColorTypeParser.cs | 4 +-- .../TypeParsers/Math/NumberBaseTypeParser.cs | 7 +++-- .../TypeParsers/Math/SpanLikeTypeParser.cs | 2 +- .../TypeParsers/PrototypeTypeParser.cs | 14 +++++----- .../TypeParsers/QuantityTypeParser.cs | 7 +++-- .../Toolshed/TypeParsers/ResPathTypeParser.cs | 4 +-- .../Toolshed/TypeParsers/SessionTypeParser.cs | 4 +-- .../Toolshed/TypeParsers/StringTypeParser.cs | 4 +-- .../TypeParsers/Tuples/BaseTupleTypeParser.cs | 4 +-- .../Toolshed/TypeParsers/TypeParser.cs | 28 +++++++++++++++++-- .../Toolshed/TypeParsers/TypeTypeParser.cs | 7 +++-- .../TypeParsers/ValueRefTypeParser.cs | 16 +++++------ .../Toolshed/TypeParsers/VarRefType.cs | 2 +- .../Toolshed/TypeParsers/VarRefTypeParser.cs | 4 +-- .../Shared/Toolshed/TestCommands.cs | 2 +- 30 files changed, 120 insertions(+), 85 deletions(-) diff --git a/Robust.Shared/Toolshed/Commands/Generic/EmplaceCommand.cs b/Robust.Shared/Toolshed/Commands/Generic/EmplaceCommand.cs index 8cf76e377c0..dd0e2698ec3 100644 --- a/Robust.Shared/Toolshed/Commands/Generic/EmplaceCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Generic/EmplaceCommand.cs @@ -219,7 +219,7 @@ public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Block? return true; } - public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName) + public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg) { TryParse(ctx, out _); return ctx.Completions; @@ -246,7 +246,7 @@ public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Type? r 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; diff --git a/Robust.Shared/Toolshed/Commands/Generic/ReduceCommand.cs b/Robust.Shared/Toolshed/Commands/Generic/ReduceCommand.cs index ec93194e9c9..73fb00060f8 100644 --- a/Robust.Shared/Toolshed/Commands/Generic/ReduceCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Generic/ReduceCommand.cs @@ -66,7 +66,7 @@ public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Block? 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; diff --git a/Robust.Shared/Toolshed/Commands/Misc/ExplainCommand.cs b/Robust.Shared/Toolshed/Commands/Misc/ExplainCommand.cs index 741f96d8ebc..b97c51831e5 100644 --- a/Robust.Shared/Toolshed/Commands/Misc/ExplainCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Misc/ExplainCommand.cs @@ -30,8 +30,9 @@ CommandRun expr builder.Append("not "); builder.Append($"{name}"); - foreach (var (argName, argType, _) in cmd.Method.Args) + foreach (var (argName, argType, _, optional, _, @params) in cmd.Method.Args) { + aaaa builder.Append($" <{argName} ({ToolshedCommandImplementor.GetFriendlyName(argType)})>"); } diff --git a/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs b/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs index 274f5059df7..167c66f2624 100644 --- a/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs +++ b/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs @@ -149,8 +149,8 @@ private bool TryParseArgument(ParserContext ctx, CommandArgument arg, ref Dictio 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; } @@ -191,8 +191,8 @@ private bool TryParseArgument(ParserContext ctx, CommandArgument arg, ref Dictio 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 +201,12 @@ private bool TryParseArgument(ParserContext ctx, CommandArgument arg, ref Dictio 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; } @@ -609,7 +608,14 @@ public CommandMethod(MethodInfo info) } 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 Optional, + object? Default, + bool Params); public sealed class ArgumentParseError(Type type, Type parser) : ConError { diff --git a/Robust.Shared/Toolshed/ToolshedManager.Parsing.cs b/Robust.Shared/Toolshed/ToolshedManager.Parsing.cs index d6a43f1fa63..f002955a904 100644 --- a/Robust.Shared/Toolshed/ToolshedManager.Parsing.cs +++ b/Robust.Shared/Toolshed/ToolshedManager.Parsing.cs @@ -242,12 +242,12 @@ public bool TryParse(ParserContext parserContext, [NotNullWhen(true)] out T? /// /// iunno man it does autocomplete what more do u want /// - 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); } /// diff --git a/Robust.Shared/Toolshed/TypeParsers/BlockType.cs b/Robust.Shared/Toolshed/TypeParsers/BlockType.cs index cf4a4d2386c..b645adb83cd 100644 --- a/Robust.Shared/Toolshed/TypeParsers/BlockType.cs +++ b/Robust.Shared/Toolshed/TypeParsers/BlockType.cs @@ -31,7 +31,7 @@ public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Type? r 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; @@ -77,7 +77,7 @@ public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Type? r 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<>))) diff --git a/Robust.Shared/Toolshed/TypeParsers/BlockTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/BlockTypeParser.cs index 0945cb5ff98..ba9f76c5b8e 100644 --- a/Robust.Shared/Toolshed/TypeParsers/BlockTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/BlockTypeParser.cs @@ -11,7 +11,7 @@ public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out 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 @@ public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out 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; @@ -39,7 +39,7 @@ public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out 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; diff --git a/Robust.Shared/Toolshed/TypeParsers/BoolTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/BoolTypeParser.cs index 3621b042552..ddfecaa65d8 100644 --- a/Robust.Shared/Toolshed/TypeParsers/BoolTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/BoolTypeParser.cs @@ -43,9 +43,9 @@ public override bool TryParse(ParserContext ctx, out bool result) 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)); } } diff --git a/Robust.Shared/Toolshed/TypeParsers/CommandRunTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/CommandRunTypeParser.cs index 69593f2546d..d353b20da67 100644 --- a/Robust.Shared/Toolshed/TypeParsers/CommandRunTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/CommandRunTypeParser.cs @@ -11,7 +11,7 @@ public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Command 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 @@ public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Command return CommandRun.TryParse(ctx, null, out result); } - public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName) + public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg) { CommandRun.TryParse(ctx, null, out _); return ctx.Completions; @@ -39,7 +39,7 @@ public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Command return CommandRun.TryParse(ctx, out result); } - public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName) + public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg) { CommandRun.TryParse(ctx, out _); return ctx.Completions; diff --git a/Robust.Shared/Toolshed/TypeParsers/CommandSpecTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/CommandSpecTypeParser.cs index f994b59daec..d364a7bd141 100644 --- a/Robust.Shared/Toolshed/TypeParsers/CommandSpecTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/CommandSpecTypeParser.cs @@ -72,7 +72,7 @@ public override bool TryParse(ParserContext ctx, out CommandSpec result) 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()), ""); diff --git a/Robust.Shared/Toolshed/TypeParsers/ComponentTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/ComponentTypeParser.cs index 12ff01a9014..4c83fd39420 100644 --- a/Robust.Shared/Toolshed/TypeParsers/ComponentTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/ComponentTypeParser.cs @@ -40,10 +40,11 @@ public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Type? r 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)); } } diff --git a/Robust.Shared/Toolshed/TypeParsers/EntityTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/EntityTypeParser.cs index bd5872cb98b..96cd1d9248f 100644 --- a/Robust.Shared/Toolshed/TypeParsers/EntityTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/EntityTypeParser.cs @@ -53,8 +53,8 @@ public override bool TryParse(ParserContext parser, out EntityUid result) return TryParseEntity(_entMan, parser, out result); } - public override CompletionResult TryAutocomplete(ParserContext ctx, string? argName) - => CompletionResult.FromHint(argName == null ? "" : $"<{argName}> (NetEntity)"); + public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg) + => CompletionResult.FromHint(GetArgHint(arg, typeof(NetEntity))); } internal sealed class NetEntityTypeParser : TypeParser @@ -100,8 +100,8 @@ public override bool TryParse(ParserContext ctx, out NetEntity result) return false; } - public override CompletionResult TryAutocomplete(ParserContext ctx, string? argName) - => CompletionResult.FromHint(argName == null ? "" : $"<{argName}> (NetEntity)"); + public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg) + => CompletionResult.FromHint(GetArgHint(arg, typeof(NetEntity))); } internal sealed class EntityTypeParser : TypeParser> @@ -122,7 +122,7 @@ public override bool TryParse(ParserContext parser, out Entity result) 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 argument, we don't want auto-completions for @@ -130,7 +130,7 @@ public override bool TryParse(ParserContext parser, out Entity result) if (!ctx.CheckInvokable()) return null; - var hint = argName == null ? "" : $"<{argName}> (NetEntity)"; + var hint = GetArgHint(arg, typeof(NetEntity)); // Avoid dumping too many entities if (_entMan.Count() > 128) @@ -169,12 +169,12 @@ public override bool TryParse(ParserContext parser, out Entity result) return true; } - public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName) + public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg) { if (!ctx.CheckInvokable()) return null; - var hint = argName == null ? "" : $"<{argName}> (NetEntity)"; + var hint = GetArgHint(arg, typeof(NetEntity)); if (_entMan.Count() > 128) return CompletionResult.FromHint(hint); @@ -215,12 +215,12 @@ public override bool TryParse(ParserContext parser, out Entity resul return true; } - public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName) + public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg) { if (!ctx.CheckInvokable()) return null; - var hint = argName == null ? "" : $"<{argName}> (NetEntity)"; + var hint = GetArgHint(arg, typeof(NetEntity)); if (_entMan.Count() > 128) return CompletionResult.FromHint(hint); diff --git a/Robust.Shared/Toolshed/TypeParsers/EnumTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/EnumTypeParser.cs index ceaa52f52fb..8af4027280b 100644 --- a/Robust.Shared/Toolshed/TypeParsers/EnumTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/EnumTypeParser.cs @@ -40,9 +40,9 @@ public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out T resul return true; } - public override CompletionResult? TryAutocomplete(ParserContext parserContext, string? argName) + public override CompletionResult? TryAutocomplete(ParserContext parserContext, CommandArgument? arg) { - return CompletionResult.FromOptions(Enum.GetNames()); + return CompletionResult.FromHintOptions(Enum.GetNames(), GetArgHint(arg)); } } diff --git a/Robust.Shared/Toolshed/TypeParsers/InstanceIdTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/InstanceIdTypeParser.cs index 5bff38a5cf7..aa57160924d 100644 --- a/Robust.Shared/Toolshed/TypeParsers/InstanceIdTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/InstanceIdTypeParser.cs @@ -12,9 +12,9 @@ public override bool TryParse(ParserContext parserContext, out InstanceId result 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)); } } diff --git a/Robust.Shared/Toolshed/TypeParsers/Math/AngleTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/Math/AngleTypeParser.cs index 3aa46faf127..1beb23a3a64 100644 --- a/Robust.Shared/Toolshed/TypeParsers/Math/AngleTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/Math/AngleTypeParser.cs @@ -51,9 +51,9 @@ public override bool TryParse(ParserContext ctx, out Angle result) } } - 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"); } } diff --git a/Robust.Shared/Toolshed/TypeParsers/Math/ColorTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/Math/ColorTypeParser.cs index 284fa393d62..553517dce32 100644 --- a/Robust.Shared/Toolshed/TypeParsers/Math/ColorTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/Math/ColorTypeParser.cs @@ -36,10 +36,10 @@ public override bool TryParse(ParserContext ctx, out Color result) } - 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."); } } diff --git a/Robust.Shared/Toolshed/TypeParsers/Math/NumberBaseTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/Math/NumberBaseTypeParser.cs index 4f1fadc4ddf..4cd2fb70cc5 100644 --- a/Robust.Shared/Toolshed/TypeParsers/Math/NumberBaseTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/Math/NumberBaseTypeParser.cs @@ -30,10 +30,11 @@ public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out T? resu 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)); } } diff --git a/Robust.Shared/Toolshed/TypeParsers/Math/SpanLikeTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/Math/SpanLikeTypeParser.cs index d35b9c0d9f7..9af205da6df 100644 --- a/Robust.Shared/Toolshed/TypeParsers/Math/SpanLikeTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/Math/SpanLikeTypeParser.cs @@ -79,7 +79,7 @@ public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out T? resu return true; } - public override CompletionResult TryAutocomplete(ParserContext parserContext, string? argName) + public override CompletionResult? TryAutocomplete(ParserContext parserContext, CommandArgument? arg) { return CompletionResult.FromHint(typeof(T).PrettyName()); } diff --git a/Robust.Shared/Toolshed/TypeParsers/PrototypeTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/PrototypeTypeParser.cs index 64f6088c263..c8bd895a44c 100644 --- a/Robust.Shared/Toolshed/TypeParsers/PrototypeTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/PrototypeTypeParser.cs @@ -49,13 +49,13 @@ public override bool TryParse(ParserContext ctx, out ProtoId result) 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(out var kind); - var hint = $"<{kind} prototype>"; + var hint = GetArgHint(arg, typeof(ProtoId)); _completions = _proto.Count() < 256 ? CompletionResult.FromHintOptions( CompletionHelper.PrototypeIDs(proto: _proto), hint) @@ -76,12 +76,12 @@ public override bool TryParse(ParserContext ctx, out EntProtoId result) 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($""); + return CompletionResult.FromHint(GetArgHint(arg)); } } @@ -106,9 +106,9 @@ public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out T? resu return false; } - public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName) + public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg) { - return Toolshed.TryAutocomplete(ctx, typeof(ProtoId), argName); + return Toolshed.TryAutocomplete(ctx, typeof(ProtoId), arg); } } @@ -137,7 +137,7 @@ public override bool TryParse(ParserContext ctx, out Prototype result) return true; } - public override CompletionResult TryAutocomplete(ParserContext ctx, string? argName) + public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg) { IEnumerable options; diff --git a/Robust.Shared/Toolshed/TypeParsers/QuantityTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/QuantityTypeParser.cs index 73875894a12..4e3eb3a0006 100644 --- a/Robust.Shared/Toolshed/TypeParsers/QuantityTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/QuantityTypeParser.cs @@ -43,10 +43,11 @@ public override bool TryParse(ParserContext ctx, out Quantity result) 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)); } } diff --git a/Robust.Shared/Toolshed/TypeParsers/ResPathTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/ResPathTypeParser.cs index 0144e4bdc5a..e336cd06859 100644 --- a/Robust.Shared/Toolshed/TypeParsers/ResPathTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/ResPathTypeParser.cs @@ -16,9 +16,9 @@ public override bool TryParse(ParserContext parserContext, out ResPath result) 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)); } } diff --git a/Robust.Shared/Toolshed/TypeParsers/SessionTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/SessionTypeParser.cs index 39c6ded7e54..06bafb09fd6 100644 --- a/Robust.Shared/Toolshed/TypeParsers/SessionTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/SessionTypeParser.cs @@ -41,10 +41,10 @@ public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out ICommon 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, ""); + return CompletionResult.FromHintOptions(opts, GetArgHint(arg)); } public record InvalidUsername(ILocalizationManager Loc, string Username) : IConError diff --git a/Robust.Shared/Toolshed/TypeParsers/StringTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/StringTypeParser.cs index dedfa213296..aba65edf15f 100644 --- a/Robust.Shared/Toolshed/TypeParsers/StringTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/StringTypeParser.cs @@ -70,9 +70,9 @@ public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out 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)" : ""; + var hint = GetArgHint(arg); parserContext.ConsumeWhitespace(); return parserContext.PeekRune() == new Rune('"') ? CompletionResult.FromHint(hint) diff --git a/Robust.Shared/Toolshed/TypeParsers/Tuples/BaseTupleTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/Tuples/BaseTupleTypeParser.cs index 3d52f0b1819..9bc61169782 100644 --- a/Robust.Shared/Toolshed/TypeParsers/Tuples/BaseTupleTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/Tuples/BaseTupleTypeParser.cs @@ -34,7 +34,7 @@ public override bool TryParse(ParserContext parserContext, [NotNullWhen(true)] o 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 override bool TryParse(ParserContext parserContext, [NotNullWhen(true)] o continue; parserContext.Restore(checkpoint); - return Toolshed.TryAutocomplete(parserContext, field, argName); + return Toolshed.TryAutocomplete(parserContext, field, null); } return null; diff --git a/Robust.Shared/Toolshed/TypeParsers/TypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/TypeParser.cs index 79e6da76d5e..b250b33c701 100644 --- a/Robust.Shared/Toolshed/TypeParsers/TypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/TypeParser.cs @@ -13,11 +13,11 @@ namespace Robust.Shared.Toolshed.TypeParsers; /// Base interface used by both custom and default type parsers. /// [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); /// /// If true, then before attempting to use this parser directly, toolshed will instead first try to parse this as a @@ -46,7 +46,29 @@ public virtual void PostInject() } 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); + + /// + /// Helper method for generating auto-completion hints while parsing command arguments. + /// + protected string GetArgHint(CommandArgument? arg, Type? t = null) + { + var type = (t ?? typeof(T)).PrettyName(); + + if (arg == null) + return type; + + // optional arguments wrapped in square braces, inspired by the syntax of man pages + if (arg.Value.Optional) + return $"[{arg.Value.Name} ({type})]"; + + // ellipses for params / variable length arguments + if (arg.Value.Params) + return $"[{arg.Value.Name} ({type})]..."; + + return $"<{arg.Value.Name} ({type})>"; + } public Type Parses => typeof(T); diff --git a/Robust.Shared/Toolshed/TypeParsers/TypeTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/TypeTypeParser.cs index 18284d83f4b..738f6e87283 100644 --- a/Robust.Shared/Toolshed/TypeParsers/TypeTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/TypeTypeParser.cs @@ -165,10 +165,13 @@ public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Type? r 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; } } diff --git a/Robust.Shared/Toolshed/TypeParsers/ValueRefTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/ValueRefTypeParser.cs index 276a7af83ef..a05cecc1e11 100644 --- a/Robust.Shared/Toolshed/TypeParsers/ValueRefTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/ValueRefTypeParser.cs @@ -21,9 +21,9 @@ public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out ValueRe return res; } - public override CompletionResult? TryAutocomplete(ParserContext parserContext, string? argName) + public override CompletionResult? TryAutocomplete(ParserContext parserContext, CommandArgument? arg) { - return Toolshed.TryAutocomplete(parserContext, typeof(ValueRef), argName); + return Toolshed.TryAutocomplete(parserContext, typeof(ValueRef), arg); } } @@ -84,13 +84,13 @@ public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out ValueRe 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), argName); + return shed.TryAutocomplete(ctx, typeof(VarRef), arg); if (rune == new Rune('{')) { @@ -107,9 +107,9 @@ public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out ValueRe return res ?? CompletionResult.FromHint($""); } - 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 : CustomTypeParser, new() where T : notnull { - public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName) + public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg) { var parser = Toolshed.GetCustomParser(); - return ValueRefTypeParser.TryAutocomplete(Toolshed, ctx, argName, parser); + return ValueRefTypeParser.TryAutocomplete(Toolshed, ctx, arg, parser); } public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out ValueRef? result) diff --git a/Robust.Shared/Toolshed/TypeParsers/VarRefType.cs b/Robust.Shared/Toolshed/TypeParsers/VarRefType.cs index 9c99d64b1dc..14dc59ab286 100644 --- a/Robust.Shared/Toolshed/TypeParsers/VarRefType.cs +++ b/Robust.Shared/Toolshed/TypeParsers/VarRefType.cs @@ -53,7 +53,7 @@ public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Type? r return false; } - public override CompletionResult TryAutocomplete(ParserContext ctx, string? argName) + public override CompletionResult TryAutocomplete(ParserContext ctx, CommandArgument? arg) { return ctx.VariableParser.GenerateCompletions(); } diff --git a/Robust.Shared/Toolshed/TypeParsers/VarRefTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/VarRefTypeParser.cs index c6251468b2b..4aaf22828a0 100644 --- a/Robust.Shared/Toolshed/TypeParsers/VarRefTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/VarRefTypeParser.cs @@ -39,7 +39,7 @@ public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out VarRef< return true; } - public override CompletionResult TryAutocomplete(ParserContext parserContext, string? argName) + public override CompletionResult TryAutocomplete(ParserContext parserContext, CommandArgument? arg) { return parserContext.VariableParser.GenerateCompletions(); } @@ -67,7 +67,7 @@ public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Writeab return false; } - public override CompletionResult TryAutocomplete(ParserContext parserContext, string? argName) + public override CompletionResult? TryAutocomplete(ParserContext parserContext, CommandArgument? arg) { return parserContext.VariableParser.GenerateCompletions(false); } diff --git a/Robust.UnitTesting/Shared/Toolshed/TestCommands.cs b/Robust.UnitTesting/Shared/Toolshed/TestCommands.cs index 8f45915c74f..ddcaa46b4eb 100644 --- a/Robust.UnitTesting/Shared/Toolshed/TestCommands.cs +++ b/Robust.UnitTesting/Shared/Toolshed/TestCommands.cs @@ -68,7 +68,7 @@ public override bool TryParse(ParserContext ctx, out int result) return true; } - public override CompletionResult TryAutocomplete(ParserContext ctx, string? argName) + public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg) { return new CompletionResult([new("A")], "B"); } From 9ff34e0e2287b3e7c198d9f67be8034e502eedb4 Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Mon, 23 Dec 2024 22:14:19 +1300 Subject: [PATCH 02/23] Support optional args --- .../Commands/Math/ArithmeticCommands.cs | 4 +- .../Toolshed/Commands/Misc/ExplainCommand.cs | 8 +- Robust.Shared/Toolshed/Syntax/Expression.cs | 55 ++++++-- .../Toolshed/Syntax/ParserContext.Config.cs | 1 + .../Toolshed/Syntax/ParserContext.cs | 35 +++-- .../Toolshed/ToolshedCommand.Help.cs | 25 +++- Robust.Shared/Toolshed/ToolshedCommand.cs | 14 ++ .../Toolshed/ToolshedCommandImplementor.cs | 132 ++++++++++-------- .../Toolshed/TypeParsers/EntityTypeParser.cs | 14 +- .../TypeParsers/PrototypeTypeParser.cs | 2 +- .../Toolshed/TypeParsers/TypeParser.cs | 20 +-- .../TypeParsers/ValueRefTypeParser.cs | 2 +- 12 files changed, 201 insertions(+), 111 deletions(-) diff --git a/Robust.Shared/Toolshed/Commands/Math/ArithmeticCommands.cs b/Robust.Shared/Toolshed/Commands/Math/ArithmeticCommands.cs index e55ca5a4a6e..f1efdde9af7 100644 --- a/Robust.Shared/Toolshed/Commands/Math/ArithmeticCommands.cs +++ b/Robust.Shared/Toolshed/Commands/Math/ArithmeticCommands.cs @@ -295,7 +295,7 @@ public IEnumerable Operation([PipedArgument] IEnumerable x, IEnumerable }); } -[ToolshedCommand(Name = "|")] +[ToolshedCommand] public sealed class BitOrCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] @@ -314,7 +314,7 @@ public IEnumerable Operation([PipedArgument] IEnumerable x, IEnumerable }); } -[ToolshedCommand(Name = "|~")] +[ToolshedCommand] public sealed class BitOrNotCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] diff --git a/Robust.Shared/Toolshed/Commands/Misc/ExplainCommand.cs b/Robust.Shared/Toolshed/Commands/Misc/ExplainCommand.cs index b97c51831e5..5c6a4785ca5 100644 --- a/Robust.Shared/Toolshed/Commands/Misc/ExplainCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Misc/ExplainCommand.cs @@ -23,17 +23,17 @@ CommandRun expr { var pipeArg = cmd.Method.Base.PipeArg; DebugTools.AssertNotNull(pipeArg); - builder.Append($"<{pipeArg?.Name} ({ToolshedCommandImplementor.GetFriendlyName(cmd.PipedType)})> -> "); + builder.Append($"<{pipeArg?.Name} ({cmd.PipedType.PrettyName()})> -> "); } if (cmd.Bundle.Inverted) builder.Append("not "); builder.Append($"{name}"); - foreach (var (argName, argType, _, optional, _, @params) in cmd.Method.Args) + foreach (var arg in cmd.Method.Args) { - aaaa - builder.Append($" <{argName} ({ToolshedCommandImplementor.GetFriendlyName(argType)})>"); + builder.Append(' '); + builder.Append(GetArgHint(arg, arg.Type)); } builder.AppendLine(); diff --git a/Robust.Shared/Toolshed/Syntax/Expression.cs b/Robust.Shared/Toolshed/Syntax/Expression.cs index b43a23bab55..888f63b0e49 100644 --- a/Robust.Shared/Toolshed/Syntax/Expression.cs +++ b/Robust.Shared/Toolshed/Syntax/Expression.cs @@ -4,6 +4,7 @@ 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 static bool TryParse( { 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 static bool TryParse( return false; } + bool expectCommand; while (true) { if (!ParsedCommand.TryParse(ctx, pipedType, out var cmd)) @@ -107,27 +109,61 @@ public static bool TryParse( cmds.Add((cmd, (start, ctx.Index))); ctx.ConsumeWhitespace(); - if (ctx.EatCommandTerminators()) + expectCommand = false; + if (ctx.EatCommandTerminators(ref pipedType)) { - ctx.ConsumeWhitespace(); - pipedType = null; + // If a command was terminated by an explicit pipe symbol (i.e., '|'), then it implies the user intends + // to write another command after this one. + expectCommand = pipedType != null; } // 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 (!expectCommand) + 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 (!expectCommand) + 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 +183,7 @@ public static bool TryParse( 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 +368,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 '}')"); } } diff --git a/Robust.Shared/Toolshed/Syntax/ParserContext.Config.cs b/Robust.Shared/Toolshed/Syntax/ParserContext.Config.cs index 2d08368109c..110f8a25f77 100644 --- a/Robust.Shared/Toolshed/Syntax/ParserContext.Config.cs +++ b/Robust.Shared/Toolshed/Syntax/ParserContext.Config.cs @@ -26,6 +26,7 @@ public static bool IsCommandToken(Rune c) && c != new Rune('\'') && c != new Rune(':') && c != new Rune(';') + && c != new Rune('|') && c != new Rune('$') && !Rune.IsControl(c); } diff --git a/Robust.Shared/Toolshed/Syntax/ParserContext.cs b/Robust.Shared/Toolshed/Syntax/ParserContext.cs index 9f20c56e3d0..e1356f5f14e 100644 --- a/Robust.Shared/Toolshed/Syntax/ParserContext.cs +++ b/Robust.Shared/Toolshed/Syntax/ParserContext.cs @@ -412,6 +412,9 @@ public bool PeekCommandOrBlockTerminated() if (c == new Rune(';')) return true; + if (c == new Rune('|')) + return true; + if (NoMultilineExprs && c == new Rune('\n')) return true; @@ -422,31 +425,47 @@ public bool PeekCommandOrBlockTerminated() } /// - /// Attempts to consume a single command terminator, which is either a ';' or a newline (if is - /// enabled). + /// Attempts to consume a single command terminator /// - public bool EatCommandTerminator() + /// + public bool EatCommandTerminator(ref Type? pipedType) { + // 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('|'))) + { 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; } /// /// Attempts to repeatedly consume command terminators, and return true if any were consumed. /// - public bool EatCommandTerminators() + public bool EatCommandTerminators(ref Type? pipedType) { - if (!EatCommandTerminator()) + if (!EatCommandTerminator(ref pipedType)) return false; // Maybe one day we want to allow ';;' to have special meaning? // But for now, just eat em all. ConsumeWhitespace(); - while (EatCommandTerminator()) + while (EatCommandTerminator(ref pipedType)) { ConsumeWhitespace(); } diff --git a/Robust.Shared/Toolshed/ToolshedCommand.Help.cs b/Robust.Shared/Toolshed/ToolshedCommand.Help.cs index f827d15a1ae..b2cd7931c90 100644 --- a/Robust.Shared/Toolshed/ToolshedCommand.Help.cs +++ b/Robust.Shared/Toolshed/ToolshedCommand.Help.cs @@ -1,4 +1,6 @@ -namespace Robust.Shared.Toolshed; +using System; + +namespace Robust.Shared.Toolshed; public abstract partial class ToolshedCommand { @@ -33,4 +35,25 @@ public override string ToString() { return Name; } + + /// + /// Helper method for generating auto-completion hints while parsing command arguments. + /// + public static string GetArgHint(CommandArgument? arg, Type t) + { + var type = t.PrettyName(); + + if (arg == null) + return type; + + // optional arguments wrapped in square braces, inspired by the syntax of man pages + if (arg.Value.IsOptional) + return $"[{arg.Value.Name} ({type})]"; + + // ellipses for params / variable length arguments + if (arg.Value.IsParamsCollection) + return $"[{arg.Value.Name} ({type})]..."; + + return $"<{arg.Value.Name} ({type})>"; + } } diff --git a/Robust.Shared/Toolshed/ToolshedCommand.cs b/Robust.Shared/Toolshed/ToolshedCommand.cs index 2304ec2acf9..c770cf80393 100644 --- a/Robust.Shared/Toolshed/ToolshedCommand.cs +++ b/Robust.Shared/Toolshed/ToolshedCommand.cs @@ -129,6 +129,7 @@ internal void Init() 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()) @@ -180,6 +181,7 @@ internal void Init() // 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(); @@ -230,6 +232,18 @@ internal void Init() } } + private void ValidateArg(ParameterInfo arg) + { + var isParams = arg.HasCustomAttribute(); + 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 AcceptedTypes(string? subCommand) { if (_acceptedTypes.TryGetValue(subCommand ?? "", out var set)) diff --git a/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs b/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs index 167c66f2624..5669a561746 100644 --- a/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs +++ b/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs @@ -55,7 +55,7 @@ public ToolshedCommandImplementor(string? subCommand, ToolshedCommand owner, Too .GetMethods(ToolshedCommand.MethodFlags) .Where(x => x.GetCustomAttribute() is { } attr && attr.SubCommand == SubCommand) - .Select(x => new CommandMethod(x)) + .Select(x => new CommandMethod(x, this)) .ToArray(); LocName = Owner.Name.All(char.IsAsciiLetterOrDigit) @@ -126,15 +126,38 @@ private bool TryParseArgument(ParserContext ctx, CommandArgument arg, ref Dictio var start = ctx.Index; var save = ctx.Save(); ctx.ConsumeWhitespace(); + DebugTools.AssertNotNull(arg.Parser); + object? parsed; 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 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) { @@ -163,21 +186,6 @@ private bool TryParseArgument(ParserContext ctx, CommandArgument arg, ref Dictio return false; } - // All arguments should have been parsed as a ValueRef 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; @@ -191,7 +199,7 @@ private bool TryParseArgument(ParserContext ctx, CommandArgument arg, ref Dictio ctx.Restore(save); ctx.Error = null; - ctx.Completions ??= arg.Parser.TryAutocomplete(ctx, arg); + ctx.Completions ??= arg.Parser!.TryAutocomplete(ctx, arg); TrySetArgHint(ctx, arg); // TODO TOOLSHED invalid-fail @@ -317,18 +325,45 @@ internal bool TryGetConcreteMethod( 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 isParams = arg.HasCustomAttribute(); + if (isParams) + { + 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, + isParams); + } + + private ITypeParser? GetArgumentParser(ParameterInfo param, Type type) { + if (type.ContainsGenericParameters) + return null; + var attrib = param.GetCustomAttribute(); var parser = attrib?.CustomParser is not {} custom - ? _toolshed.GetArgumentParser(param.ParameterType) + ? _toolshed.GetArgumentParser(type) : _toolshed.GetArgumentParser(_toolshed.GetCustomParser(custom)); if (parser == null) @@ -509,20 +544,21 @@ public string GetHelp() builder.Append(Environment.NewLine + " "); if (method.PipeArg != null) - builder.Append($"<{method.PipeArg.Name} ({GetFriendlyName(method.PipeArg.ParameterType)})> -> "); + builder.Append($"<{method.PipeArg.Name} ({method.PipeArg.ParameterType.PrettyName()})> -> "); if (method.Invertible) builder.Append("[not] "); builder.Append(FullName); - foreach (var (argName, argType) in method.Arguments) + foreach (var arg in method.Arguments) { - builder.Append($" <{argName} ({GetFriendlyName(argType)})>"); + builder.Append(' '); + builder.Append(ToolshedCommand.GetArgHint(arg, arg.Type)); } if (method.Info.ReturnType != typeof(void)) - builder.Append($" -> {GetFriendlyName(method.Info.ReturnType)}"); + builder.Append($" -> {method.Info.ReturnType.PrettyName()}"); } return builder.ToString(); @@ -536,33 +572,11 @@ public string Description() { 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; - } } /// -/// 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. /// internal sealed class CommandMethod @@ -586,9 +600,9 @@ internal sealed class CommandMethod /// 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(); @@ -596,7 +610,7 @@ public CommandMethod(MethodInfo info) Arguments = info.GetParameters() .Where(x => x.IsCommandArgument()) - .Select(x => (x.Name ?? string.Empty, x.ParameterType)) + .Select(impl.GetCommandArgument) .ToArray(); if (!info.IsGenericMethodDefinition) @@ -612,10 +626,10 @@ public CommandMethod(MethodInfo info) public readonly record struct CommandArgument( string Name, Type Type, - ITypeParser Parser, - bool Optional, - object? Default, - bool Params); + ITypeParser? Parser, + bool IsOptional, + object? DefaultValue, + bool IsParamsCollection); public sealed class ArgumentParseError(Type type, Type parser) : ConError { diff --git a/Robust.Shared/Toolshed/TypeParsers/EntityTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/EntityTypeParser.cs index 96cd1d9248f..e4bc868f27b 100644 --- a/Robust.Shared/Toolshed/TypeParsers/EntityTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/EntityTypeParser.cs @@ -53,8 +53,8 @@ public override bool TryParse(ParserContext parser, out EntityUid result) return TryParseEntity(_entMan, parser, out result); } - public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg) - => CompletionResult.FromHint(GetArgHint(arg, typeof(NetEntity))); + public override CompletionResult TryAutocomplete(ParserContext ctx, CommandArgument? arg) + => CompletionResult.FromHint(ToolshedCommand.GetArgHint(arg, typeof(NetEntity))); } internal sealed class NetEntityTypeParser : TypeParser @@ -100,8 +100,8 @@ public override bool TryParse(ParserContext ctx, out NetEntity result) return false; } - public override CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg) - => CompletionResult.FromHint(GetArgHint(arg, typeof(NetEntity))); + public override CompletionResult TryAutocomplete(ParserContext ctx, CommandArgument? arg) + => CompletionResult.FromHint(ToolshedCommand.GetArgHint(arg, typeof(NetEntity))); } internal sealed class EntityTypeParser : TypeParser> @@ -130,7 +130,7 @@ public override bool TryParse(ParserContext parser, out Entity result) if (!ctx.CheckInvokable()) return null; - var hint = GetArgHint(arg, typeof(NetEntity)); + var hint = ToolshedCommand.GetArgHint(arg, typeof(NetEntity)); // Avoid dumping too many entities if (_entMan.Count() > 128) @@ -174,7 +174,7 @@ public override bool TryParse(ParserContext parser, out Entity result) if (!ctx.CheckInvokable()) return null; - var hint = GetArgHint(arg, typeof(NetEntity)); + var hint = ToolshedCommand.GetArgHint(arg, typeof(NetEntity)); if (_entMan.Count() > 128) return CompletionResult.FromHint(hint); @@ -220,7 +220,7 @@ public override bool TryParse(ParserContext parser, out Entity resul if (!ctx.CheckInvokable()) return null; - var hint = GetArgHint(arg, typeof(NetEntity)); + var hint = ToolshedCommand.GetArgHint(arg, typeof(NetEntity)); if (_entMan.Count() > 128) return CompletionResult.FromHint(hint); diff --git a/Robust.Shared/Toolshed/TypeParsers/PrototypeTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/PrototypeTypeParser.cs index c8bd895a44c..fc66007b2be 100644 --- a/Robust.Shared/Toolshed/TypeParsers/PrototypeTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/PrototypeTypeParser.cs @@ -55,7 +55,7 @@ public override bool TryParse(ParserContext ctx, out ProtoId result) return _completions; _proto.TryGetKindFrom(out var kind); - var hint = GetArgHint(arg, typeof(ProtoId)); + var hint = ToolshedCommand.GetArgHint(arg, typeof(ProtoId)); _completions = _proto.Count() < 256 ? CompletionResult.FromHintOptions( CompletionHelper.PrototypeIDs(proto: _proto), hint) diff --git a/Robust.Shared/Toolshed/TypeParsers/TypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/TypeParser.cs index b250b33c701..d4933df08e4 100644 --- a/Robust.Shared/Toolshed/TypeParsers/TypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/TypeParser.cs @@ -49,25 +49,9 @@ public virtual void PostInject() public abstract CompletionResult? TryAutocomplete(ParserContext ctx, CommandArgument? arg); - /// - /// Helper method for generating auto-completion hints while parsing command arguments. - /// - protected string GetArgHint(CommandArgument? arg, Type? t = null) + protected string GetArgHint(CommandArgument? arg) { - var type = (t ?? typeof(T)).PrettyName(); - - if (arg == null) - return type; - - // optional arguments wrapped in square braces, inspired by the syntax of man pages - if (arg.Value.Optional) - return $"[{arg.Value.Name} ({type})]"; - - // ellipses for params / variable length arguments - if (arg.Value.Params) - return $"[{arg.Value.Name} ({type})]..."; - - return $"<{arg.Value.Name} ({type})>"; + return ToolshedCommand.GetArgHint(arg, typeof(T)); } public Type Parses => typeof(T); diff --git a/Robust.Shared/Toolshed/TypeParsers/ValueRefTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/ValueRefTypeParser.cs index a05cecc1e11..3bc883191f5 100644 --- a/Robust.Shared/Toolshed/TypeParsers/ValueRefTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/ValueRefTypeParser.cs @@ -103,7 +103,7 @@ public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out ValueRe if (parser == null) return CompletionResult.FromHint($""); - var res = parser.TryAutocomplete(ctx, null); + var res = parser.TryAutocomplete(ctx, arg); return res ?? CompletionResult.FromHint($""); } From 4a5082a7a0b8b048b919e9fc358fef8df0a8944d Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Tue, 24 Dec 2024 00:33:30 +1300 Subject: [PATCH 03/23] It (not so shrimply) works --- Robust.Shared/Toolshed/Syntax/ValueRef.cs | 17 +++++ .../Toolshed/ToolshedCommandImplementor.cs | 71 +++++++++++++++---- 2 files changed, 73 insertions(+), 15 deletions(-) diff --git a/Robust.Shared/Toolshed/Syntax/ValueRef.cs b/Robust.Shared/Toolshed/Syntax/ValueRef.cs index 580aa983b77..3b01b73ea28 100644 --- a/Robust.Shared/Toolshed/Syntax/ValueRef.cs +++ b/Robust.Shared/Toolshed/Syntax/ValueRef.cs @@ -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 $"Input: {obj}") }; } + + internal static T[] EvaluateParamsCollection(object? obj, IInvocationContext ctx) + { + if (obj is not List 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")] diff --git a/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs b/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs index 5669a561746..336bef4fe60 100644 --- a/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs +++ b/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs @@ -107,11 +107,20 @@ public bool TryParseArguments(ParserContext ctx, ConcreteCommandMethod method) 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,7 +128,30 @@ public bool TryParseArguments(ParserContext ctx, ConcreteCommandMethod method) return true; } - private bool TryParseArgument(ParserContext ctx, CommandArgument arg, ref Dictionary? args) + private bool ParseParamsCollection(ParserContext ctx, CommandArgument arg, out object? collection) + { + var list = new List(); + 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); @@ -128,7 +160,7 @@ private bool TryParseArgument(ParserContext ctx, CommandArgument arg, ref Dictio ctx.ConsumeWhitespace(); DebugTools.AssertNotNull(arg.Parser); - object? parsed; + parsed = null; if (ctx.PeekCommandOrBlockTerminated() || ctx is {OutOfInput: true, GenerateCompletions: false}) { if (!arg.IsOptional) @@ -186,9 +218,6 @@ private bool TryParseArgument(ParserContext ctx, CommandArgument arg, ref Dictio return false; } - args ??= new(); - args[arg.Name] = parsed; - if (!ctx.GenerateCompletions || !ctx.OutOfInput) return true; @@ -337,8 +366,8 @@ internal CommandArgument GetCommandArgument(ParameterInfo arg) var argType = arg.ParameterType; // Is this a "params T[] argument"? - var isParams = arg.HasCustomAttribute(); - if (isParams) + var isParamsCollection = arg.HasCustomAttribute(); + if (isParamsCollection) { if (!argType.IsArray) throw new NotSupportedException(".net 9 params collections are not yet supported"); @@ -353,7 +382,7 @@ internal CommandArgument GetCommandArgument(ParameterInfo arg) GetArgumentParser(arg, argType), arg.IsOptional, arg.DefaultValue, - isParams); + isParamsCollection); } private ITypeParser? GetArgumentParser(ParameterInfo param, Type type) @@ -511,12 +540,24 @@ private Expression GetArgExpr(ParameterInfo param, ParameterExpression args) // args.Context var ctx = Expression.Property(args, nameof(CommandInvocationArguments.Context)); - // ValueRef.TryEvaluate - var evalMethod = typeof(ValueRef<>) - .MakeGenericType(param.ParameterType) - .GetMethod(nameof(ValueRef.EvaluateParameter), BindingFlags.Static | BindingFlags.NonPublic)!; + MethodInfo? evalMethod; + + var isParamsCollection = param.HasCustomAttribute(); + if (isParamsCollection) + { + // ValueRef.EvaluateParamsCollection + evalMethod = typeof(ValueRef<>) + .MakeGenericType(param.ParameterType.GetElementType()!) + .GetMethod(nameof(ValueRef.EvaluateParamsCollection), BindingFlags.Static | BindingFlags.NonPublic)!; + } + else + { + // ValueRef.EvaluateParameter + evalMethod = typeof(ValueRef<>) + .MakeGenericType(param.ParameterType) + .GetMethod(nameof(ValueRef.EvaluateParameter), BindingFlags.Static | BindingFlags.NonPublic)!; + } - // ValueRef.TryEvaluate(args.Arguments[param.Name], args.Context) return Expression.Call(evalMethod, argValue, ctx); } From 4e9d66ca7b3e9d0c9d6fab8646a9b35a93772978 Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Tue, 24 Dec 2024 01:37:03 +1300 Subject: [PATCH 04/23] Add tests --- .../Toolshed/ReflectionExtensions.cs | 12 ++ Robust.Shared/Toolshed/Syntax/Expression.cs | 14 +- .../Toolshed/Syntax/ParserContext.cs | 18 +- .../Shared/Toolshed/TestCommands.cs | 24 +++ .../Shared/Toolshed/ToolshedTests.cs | 168 ++++++++++++++++++ 5 files changed, 216 insertions(+), 20 deletions(-) diff --git a/Robust.Shared/Toolshed/ReflectionExtensions.cs b/Robust.Shared/Toolshed/ReflectionExtensions.cs index bdf87e84b8f..a85f33792bf 100644 --- a/Robust.Shared/Toolshed/ReflectionExtensions.cs +++ b/Robust.Shared/Toolshed/ReflectionExtensions.cs @@ -191,8 +191,20 @@ public static Expression CreateEmptyExpr(this Type t) } // IEnumerable ^ IEnumerable -> EntityUid + // List ^ IEnumerable -> EntityUid + // T[] ^ IEnumerable -> 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; diff --git a/Robust.Shared/Toolshed/Syntax/Expression.cs b/Robust.Shared/Toolshed/Syntax/Expression.cs index 888f63b0e49..d17d1c83b71 100644 --- a/Robust.Shared/Toolshed/Syntax/Expression.cs +++ b/Robust.Shared/Toolshed/Syntax/Expression.cs @@ -95,7 +95,7 @@ public static bool TryParse( return false; } - bool expectCommand; + bool commandExpected; while (true) { if (!ParsedCommand.TryParse(ctx, pipedType, out var cmd)) @@ -109,20 +109,14 @@ public static bool TryParse( cmds.Add((cmd, (start, ctx.Index))); ctx.ConsumeWhitespace(); - expectCommand = false; - if (ctx.EatCommandTerminators(ref pipedType)) - { - // If a command was terminated by an explicit pipe symbol (i.e., '|'), then it implies the user intends - // to write another command after this one. - expectCommand = 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()) { - if (!expectCommand) + if (!commandExpected) break; // Lets enforce that poeple don't end command blocks with a dangling explicit pipe. @@ -139,7 +133,7 @@ public static bool TryParse( { // If the last command was terminated by an explicit pipe symbol we require that there be a follow-up // command - if (!expectCommand) + if (!commandExpected) break; if (ctx.GenerateCompletions) diff --git a/Robust.Shared/Toolshed/Syntax/ParserContext.cs b/Robust.Shared/Toolshed/Syntax/ParserContext.cs index e1356f5f14e..d7ff9d38cd8 100644 --- a/Robust.Shared/Toolshed/Syntax/ParserContext.cs +++ b/Robust.Shared/Toolshed/Syntax/ParserContext.cs @@ -428,8 +428,10 @@ public bool PeekCommandOrBlockTerminated() /// Attempts to consume a single command terminator /// /// - public bool EatCommandTerminator(ref Type? pipedType) + public bool EatCommandTerminator(ref Type? pipedType, out bool commandExpected) { + commandExpected = false; + // Command terminator drops piped values. if (EatMatch(new Rune(';'))) { @@ -440,6 +442,7 @@ public bool EatCommandTerminator(ref Type? pipedType) // 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; } @@ -448,7 +451,6 @@ public bool EatCommandTerminator(ref Type? pipedType) { pipedType = null; return true; - } return false; @@ -457,20 +459,16 @@ public bool EatCommandTerminator(ref Type? pipedType) /// /// Attempts to repeatedly consume command terminators, and return true if any were consumed. /// - public bool EatCommandTerminators(ref Type? pipedType) + public void EatCommandTerminators(ref Type? pipedType, out bool commandExpected) { - if (!EatCommandTerminator(ref pipedType)) - 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(ref pipedType)) + while (!commandExpected && EatCommandTerminator(ref pipedType, out commandExpected)) { ConsumeWhitespace(); } - - return true; } } diff --git a/Robust.UnitTesting/Shared/Toolshed/TestCommands.cs b/Robust.UnitTesting/Shared/Toolshed/TestCommands.cs index ddcaa46b4eb..c593328f053 100644 --- a/Robust.UnitTesting/Shared/Toolshed/TestCommands.cs +++ b/Robust.UnitTesting/Shared/Toolshed/TestCommands.cs @@ -75,6 +75,30 @@ public override bool TryParse(ParserContext ctx, out int result) } } +[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 { diff --git a/Robust.UnitTesting/Shared/Toolshed/ToolshedTests.cs b/Robust.UnitTesting/Shared/Toolshed/ToolshedTests.cs index 23746e02280..6013faf6209 100644 --- a/Robust.UnitTesting/Shared/Toolshed/ToolshedTests.cs +++ b/Robust.UnitTesting/Shared/Toolshed/ToolshedTests.cs @@ -186,6 +186,174 @@ await Server.WaitAssertion(() => }); } + [Test] + public async Task TestOTerminators() + { + 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("i 1 |"); + ParseError("i { i 1 | }"); + + // A terminator inside a block or command run doesn't pipe anything; + ParseError("i 1 ; + 1"); + ParseError("i { i 1 ; }"); + + // Check double terminators + // A starting terminators/pipes will try to be parsed as a command. + ParseError("|"); + ParseError(";"); + ParseError(";;"); + ParseError("||"); + ParseError("|;"); + ParseError(";|"); + AssertResult("i 1 ;;", 1); + + // Consecutive pipes will try to parse the second one as the command, which will not succeed. + ParseError("i 1 ||"); + ParseError("i 1 |;"); + ParseError("i 1 ;|"); + AssertResult("i 1 ;; i 1", 1); + ParseError("i 1 || i 1"); + ParseError("i 1 |; i 1"); + ParseError("i 1 ;| i 1"); + ParseError("i 1 ;; + 1"); + ParseError("i 1 || + 1"); + ParseError("i 1 |; + 1"); + ParseError("i 1 ;| + 1"); + }); + } + + [Test] + public async Task TestOptionalArgs() + { + await Server.WaitAssertion(() => + { + // Check that straightforward optional args work. + ParseError("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("testoptionalargs 1 2 3 4"); + ParseError>("testoptionalargs 1 append 4"); + ParseError>("testoptionalargs 1 2 append 4"); + + // Check that semicolon terminators interrupt optional args + ParseError("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("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("testoptionalargs |"); + ParseError("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 { }" to prepend "1" to the results. + + // This first block also effectively checks that closing braces can interrupt optional args + ParseError("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("testoptionalargs 1 2 3 4 }"); + ParseError>("testoptionalargs 1 2 i 3 }"); + ParseError("testoptionalargs 1 2 3 i 4 }"); + + ParseError("i 1 join { testoptionalargs | }"); + ParseError("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("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>("testparamscollection 1 2 append 4"); + ParseError>("testparamscollection 1 2 3 append 4"); + ParseError>("testparamscollection 1 2 3 4 append 4"); + + // Check that semicolon terminators interrupt optional args + ParseError("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("testparamscollection |"); + ParseError("testparamsonly |"); + ParseError("testparamscollection 1 |"); + ParseError("testparamscollection 1 2 |"); + ParseError("testparamscollection 1 2 3 |"); + ParseError("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}); + }); + } + [Test] public async Task TestCompletions() { From d454efb987ce49623a96f9e2bfa0d07a96ccc313 Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Tue, 24 Dec 2024 03:11:44 +1300 Subject: [PATCH 05/23] Add TestGenericPipeInference --- .../Shared/Toolshed/TestCommands.cs | 104 ++++++++++++++++++ .../Shared/Toolshed/ToolshedTests.cs | 38 +++++++ 2 files changed, 142 insertions(+) diff --git a/Robust.UnitTesting/Shared/Toolshed/TestCommands.cs b/Robust.UnitTesting/Shared/Toolshed/TestCommands.cs index c593328f053..8792669adc9 100644 --- a/Robust.UnitTesting/Shared/Toolshed/TestCommands.cs +++ b/Robust.UnitTesting/Shared/Toolshed/TestCommands.cs @@ -1,5 +1,9 @@ using System; +using System.Collections.Generic; +using System.Linq; using Robust.Shared.Console; +using Robust.Shared.GameObjects; +using Robust.Shared.Prototypes; using Robust.Shared.Toolshed; using Robust.Shared.Toolshed.Syntax; using Robust.Shared.Toolshed.TypeParsers; @@ -112,3 +116,103 @@ public sealed class Parser : TestCustomVarRefParserCommand.Parser public override bool EnableValueRef => false; } } + +[ToolshedCommand] +public sealed class TestEnumerableInferCommand : ToolshedCommand +{ + [CommandImplementation, TakesPipedTypeAsGeneric] + public Type Impl([PipedArgument] IEnumerable x, T y) => typeof(T); +} + +[ToolshedCommand] +public sealed class TestListInferCommand : ToolshedCommand +{ + [CommandImplementation, TakesPipedTypeAsGeneric] + public Type Impl([PipedArgument] List x, T y) => typeof(T); +} + +[ToolshedCommand] +public sealed class TestArrayInferCommand : ToolshedCommand +{ + [CommandImplementation, TakesPipedTypeAsGeneric] + public Type Impl([PipedArgument] T[] x, T y) => typeof(T); +} + +[ToolshedCommand] +public sealed class TestNestedEnumerableInferCommand : ToolshedCommand +{ + [CommandImplementation, TakesPipedTypeAsGeneric] + public Type Impl([PipedArgument] IEnumerable> x) + where T : class, IPrototype + { + return typeof(T); + } +} + +[ToolshedCommand] +public sealed class TestNestedListInferCommand : ToolshedCommand +{ + [CommandImplementation, TakesPipedTypeAsGeneric] + public Type Impl([PipedArgument] List> x) + where T : class, IPrototype + { + return typeof(T); + } +} + +[ToolshedCommand] +public sealed class TestNestedArrayInferCommand : ToolshedCommand +{ + [CommandImplementation, TakesPipedTypeAsGeneric] + public Type Impl([PipedArgument] ProtoId[] x) + where T : class, IPrototype + { + return typeof(T); + } +} + +[ToolshedCommand] +public sealed class TestArrayCommand : ToolshedCommand +{ + [CommandImplementation] + public int[] Impl() => Array.Empty(); +} + +[ToolshedCommand] +public sealed class TestListCommand : ToolshedCommand +{ + [CommandImplementation] + public List Impl() => new(); +} + +[ToolshedCommand] +public sealed class TestEnumerableCommand : ToolshedCommand +{ + private static int[] _arr = {1, 3, 3}; + + [CommandImplementation] + public IEnumerable Impl() => _arr.Select(x => 2 * x); +} + +[ToolshedCommand] +public sealed class TestNestedArrayCommand : ToolshedCommand +{ + [CommandImplementation] + public ProtoId[] Impl() => Array.Empty>(); +} + +[ToolshedCommand] +public sealed class TestNestedListCommand : ToolshedCommand +{ + [CommandImplementation] + public List> Impl() => new(); +} + +[ToolshedCommand] +public sealed class TestNestedEnumerableCommand : ToolshedCommand +{ + private static ProtoId[] _arr = {new("a"), new("b"), new("c")}; + + [CommandImplementation] + public IEnumerable> Impl() => _arr.OrderByDescending(x => x.Id); +} diff --git a/Robust.UnitTesting/Shared/Toolshed/ToolshedTests.cs b/Robust.UnitTesting/Shared/Toolshed/ToolshedTests.cs index 6013faf6209..0058153ee2e 100644 --- a/Robust.UnitTesting/Shared/Toolshed/ToolshedTests.cs +++ b/Robust.UnitTesting/Shared/Toolshed/ToolshedTests.cs @@ -1,8 +1,11 @@ using System; +using System.Collections.Generic; +using System.Linq; 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; @@ -354,6 +357,41 @@ await Server.WaitAssertion(() => }); } + /// + /// 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 into a command that takes an , the value of + /// the generic parameter can be properly inferred. + /// + [Test] + [TestOf(typeof(TakesPipedTypeAsGenericAttribute))] + public async Task TestGenericPipeInference() + { + await Server.WaitAssertion(() => + { + // Pipe T[] -> T[] + AssertResult("testarray testarrayinfer 1", typeof(int)); + + // Pipe List -> List + AssertResult("testlist testlistinfer 1", typeof(int)); + + // Pipe T[] -> IEnumerable + AssertResult("testarray testenumerableinfer 1", typeof(int)); + + // Pipe List -> IEnumerable + AssertResult("testlist testenumerableinfer 1", typeof(int)); + + // Pipe IEnumerable -> IEnumerable + AssertResult("testenumerable testenumerableinfer 1", typeof(int)); + + // Repeat but with nested types. i.e. extracting T when piping ProtoId -> IEnumerable> + 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)); + }); + } + [Test] public async Task TestCompletions() { From e1fcf627f8aa9c79806b2acfb4ac0bf03833b0ed Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Tue, 24 Dec 2024 03:34:23 +1300 Subject: [PATCH 06/23] Fix tests --- Resources/Locale/en-US/toolshed-commands.ftl | 4 ++-- .../Shared/Toolshed/ToolshedTests.cs | 20 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Resources/Locale/en-US/toolshed-commands.ftl b/Resources/Locale/en-US/toolshed-commands.ftl index 08330dcffbb..72a306dd038 100644 --- a/Resources/Locale/en-US/toolshed-commands.ftl +++ b/Resources/Locale/en-US/toolshed-commands.ftl @@ -149,7 +149,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. @@ -277,7 +277,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. diff --git a/Robust.UnitTesting/Shared/Toolshed/ToolshedTests.cs b/Robust.UnitTesting/Shared/Toolshed/ToolshedTests.cs index 0058153ee2e..3c6fd4841b5 100644 --- a/Robust.UnitTesting/Shared/Toolshed/ToolshedTests.cs +++ b/Robust.UnitTesting/Shared/Toolshed/ToolshedTests.cs @@ -121,8 +121,8 @@ await Server.WaitAssertion(() => // 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("i 2 + { i 2; }"); + ParseError("i 2 + { i 2; ; } ;; ;"); }); } @@ -190,7 +190,7 @@ await Server.WaitAssertion(() => } [Test] - public async Task TestOTerminators() + public async Task TestTerminators() { await Server.WaitAssertion(() => { @@ -405,14 +405,14 @@ await Server.WaitAssertion(() => 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", ""); 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", ""); AssertCompletionEmpty("i a "); AssertCompletionEmpty("i a 1"); AssertCompletionSingle("i $", "$x"); @@ -445,13 +445,13 @@ await Server.WaitAssertion(() => // Check completions when typing out: testintstrarg 1 "a" AssertCompletionContains("testintstrarg", "testintstrarg"); - AssertCompletionHint("testintstrarg ", "Int32"); - AssertCompletionHint("testintstrarg 1", "Int32"); + AssertCompletionHint("testintstrarg ", ""); + AssertCompletionHint("testintstrarg 1", ""); AssertCompletionSingle("testintstrarg 1 ", "\""); - AssertCompletionHint("testintstrarg 1 \"", ""); - AssertCompletionHint("testintstrarg 1 \"a\"", ""); + AssertCompletionHint("testintstrarg 1 \"", ""); + AssertCompletionHint("testintstrarg 1 \"a\"", ""); AssertCompletionEmpty("testintstrarg 1 \"a\" "); - AssertCompletionHint("testintstrarg 1 \"a\" + ", "Int32"); + AssertCompletionHint("testintstrarg 1 \"a\" + ", ""); AssertCompletionContains("i 5 iota reduce { ma", "max"); AssertCompletionContains("i 5 iota reduce { max $", "$x", "$value"); From b4b085aa01ddfb675db5bb9371335f437e8ab694 Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Tue, 24 Dec 2024 03:34:53 +1300 Subject: [PATCH 07/23] Release notes --- RELEASE-NOTES.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index a0daaf128e8..2c07f2a4398 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -35,11 +35,13 @@ END TEMPLATE--> ### Breaking changes -*None yet* +* 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 -*None yet* +* Toolshed commands now support optional and `params T[]` arguments. optional / variable length commands can be terminated using ';' or '|'. ### Bugfixes @@ -47,7 +49,10 @@ END TEMPLATE--> ### Other -*None yet* +* The default auto-completion hint for Toolshed commands have been changed and somewhat standardized. Most type parsers should now have a hint of the form: + * `` for mandatory arguments + * `[name (Type)]` for optional arguments + * `[name (Type)]...` for variable length arguments (i.e., for `params T[]`) ### Internal From 2159337364be65e4753cb543adbdfbb58c15ebd8 Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Tue, 24 Dec 2024 04:00:13 +1300 Subject: [PATCH 08/23] Overzealous YAMLLinter --- Robust.UnitTesting/Shared/Toolshed/TestCommands.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Robust.UnitTesting/Shared/Toolshed/TestCommands.cs b/Robust.UnitTesting/Shared/Toolshed/TestCommands.cs index 8792669adc9..3e032352fff 100644 --- a/Robust.UnitTesting/Shared/Toolshed/TestCommands.cs +++ b/Robust.UnitTesting/Shared/Toolshed/TestCommands.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using Robust.Shared.Console; -using Robust.Shared.GameObjects; using Robust.Shared.Prototypes; using Robust.Shared.Toolshed; using Robust.Shared.Toolshed.Syntax; @@ -211,7 +210,7 @@ public sealed class TestNestedListCommand : ToolshedCommand [ToolshedCommand] public sealed class TestNestedEnumerableCommand : ToolshedCommand { - private static ProtoId[] _arr = {new("a"), new("b"), new("c")}; + private static ProtoId[] _arr = Array.Empty>(); [CommandImplementation] public IEnumerable> Impl() => _arr.OrderByDescending(x => x.Id); From 0199be987c1888d406a78e0ee32e74509edae15a Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Thu, 2 Jan 2025 12:39:36 +1300 Subject: [PATCH 09/23] Improve help signatures, fix map command --- RELEASE-NOTES.md | 2 +- Resources/Locale/en-US/toolshed-commands.ftl | 3 +- .../Toolshed/Commands/Generic/CountCommand.cs | 4 +- .../Commands/Generic/EmplaceCommand.cs | 1 + .../Toolshed/Commands/Misc/ExplainCommand.cs | 16 +++---- .../Toolshed/ToolshedCommandImplementor.cs | 43 ++++++++++++++++--- .../Toolshed/TypeParsers/BlockType.cs | 26 ++++++++++- .../Toolshed/TypeParsers/TypeParser.cs | 7 +++ .../Toolshed/TypeParsers/VarRefType.cs | 1 + 9 files changed, 80 insertions(+), 23 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 2c07f2a4398..fc2ff0e121f 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -45,7 +45,7 @@ END TEMPLATE--> ### Bugfixes -*None yet* +* The map-like Toolshed commands now work when a collection is piped in. ### Other diff --git a/Resources/Locale/en-US/toolshed-commands.ftl b/Resources/Locale/en-US/toolshed-commands.ftl index 72a306dd038..1acf8e57c89 100644 --- a/Resources/Locale/en-US/toolshed-commands.ftl +++ b/Resources/Locale/en-US/toolshed-commands.ftl @@ -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. diff --git a/Robust.Shared/Toolshed/Commands/Generic/CountCommand.cs b/Robust.Shared/Toolshed/Commands/Generic/CountCommand.cs index ed5bcf75ed4..0b0b99e5dea 100644 --- a/Robust.Shared/Toolshed/Commands/Generic/CountCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Generic/CountCommand.cs @@ -7,8 +7,8 @@ namespace Robust.Shared.Toolshed.Commands.Generic; public sealed class CountCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public int Count([PipedArgument] IEnumerable enumerable) + public int Count([PipedArgument] IEnumerable input) { - return enumerable.Count(); + return input.Count(); } } diff --git a/Robust.Shared/Toolshed/Commands/Generic/EmplaceCommand.cs b/Robust.Shared/Toolshed/Commands/Generic/EmplaceCommand.cs index dd0e2698ec3..800ed11fca5 100644 --- a/Robust.Shared/Toolshed/Commands/Generic/EmplaceCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Generic/EmplaceCommand.cs @@ -231,6 +231,7 @@ public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Block? /// private sealed class EmplaceBlockOutputParser : CustomTypeParser { + public override bool ShowTypeArgSignature => false; public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Type? result) { result = null; diff --git a/Robust.Shared/Toolshed/Commands/Misc/ExplainCommand.cs b/Robust.Shared/Toolshed/Commands/Misc/ExplainCommand.cs index 5c6a4785ca5..7442b692316 100644 --- a/Robust.Shared/Toolshed/Commands/Misc/ExplainCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Misc/ExplainCommand.cs @@ -1,5 +1,8 @@ -using System.Text; +using System; +using System.Text; +using Microsoft.Extensions.Primitives; using Robust.Shared.Toolshed.Syntax; +using Robust.Shared.Toolshed.TypeParsers; using Robust.Shared.Utility; namespace Robust.Shared.Toolshed.Commands.Misc; @@ -16,6 +19,7 @@ CommandRun expr var builder = new StringBuilder(); foreach (var (cmd, _) in expr.Commands) { + builder.AppendLine(); var name = cmd.Implementor.FullName; builder.AppendLine($"{name} - {cmd.Implementor.Description()}"); @@ -29,20 +33,14 @@ CommandRun expr if (cmd.Bundle.Inverted) builder.Append("not "); - builder.Append($"{name}"); - foreach (var arg in cmd.Method.Args) - { - builder.Append(' '); - builder.Append(GetArgHint(arg, arg.Type)); - } + cmd.Implementor.AddMethodSignature(builder, cmd.Method.Args); builder.AppendLine(); var piped = cmd.PipedType?.PrettyName() ?? "[none]"; var returned = cmd.ReturnType?.PrettyName() ?? "[none]"; builder.AppendLine($"{piped} -> {returned}"); - builder.AppendLine(); } - ctx.WriteLine(builder.ToString()); + ctx.WriteLine(builder.ToString().TrimEnd()); } } diff --git a/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs b/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs index 336bef4fe60..8ce32d8b211 100644 --- a/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs +++ b/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs @@ -5,6 +5,7 @@ using System.Linq.Expressions; using System.Reflection; using System.Text; +using Microsoft.Extensions.Primitives; using Robust.Shared.Exceptions; using Robust.Shared.Localization; using Robust.Shared.Toolshed.Errors; @@ -590,13 +591,7 @@ public string GetHelp() if (method.Invertible) builder.Append("[not] "); - builder.Append(FullName); - - foreach (var arg in method.Arguments) - { - builder.Append(' '); - builder.Append(ToolshedCommand.GetArgHint(arg, arg.Type)); - } + AddMethodSignature(builder, method.Arguments); if (method.Info.ReturnType != typeof(void)) builder.Append($" -> {method.Info.ReturnType.PrettyName()}"); @@ -605,6 +600,40 @@ public string GetHelp() return builder.ToString(); } + /// + /// Construct the methods signature for help and explain commands. + /// + internal void AddMethodSignature(StringBuilder builder, CommandArgument[] args) + { + 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++; + } + + for (var i = 0; i < numParsers; i++) + { + builder.Append(" 1) + builder.Append(i); + builder.Append('>'); + } + + foreach (var arg in args) + { + builder.Append(' '); + builder.Append(ToolshedCommand.GetArgHint(arg, arg.Type)); + } + } + /// public string DescriptionLocKey() => $"command-description-{LocName}"; diff --git a/Robust.Shared/Toolshed/TypeParsers/BlockType.cs b/Robust.Shared/Toolshed/TypeParsers/BlockType.cs index b645adb83cd..8bf6f79a673 100644 --- a/Robust.Shared/Toolshed/TypeParsers/BlockType.cs +++ b/Robust.Shared/Toolshed/TypeParsers/BlockType.cs @@ -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; /// public sealed class BlockOutputParser : CustomTypeParser { + public override bool ShowTypeArgSignature => false; public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Type? result) { result = null; @@ -52,13 +55,32 @@ public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Type? r /// > public sealed class MapBlockOutputParser : CustomTypeParser { + 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 + // 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; diff --git a/Robust.Shared/Toolshed/TypeParsers/TypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/TypeParser.cs index d4933df08e4..158c7546c4e 100644 --- a/Robust.Shared/Toolshed/TypeParsers/TypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/TypeParser.cs @@ -26,11 +26,18 @@ public interface ITypeParser /// or variables /// public bool EnableValueRef { get; } + + /// + /// 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 . + /// + public virtual bool ShowTypeArgSignature => true; } public abstract class BaseParser : 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 diff --git a/Robust.Shared/Toolshed/TypeParsers/VarRefType.cs b/Robust.Shared/Toolshed/TypeParsers/VarRefType.cs index 14dc59ab286..657754d6ca9 100644 --- a/Robust.Shared/Toolshed/TypeParsers/VarRefType.cs +++ b/Robust.Shared/Toolshed/TypeParsers/VarRefType.cs @@ -23,6 +23,7 @@ namespace Robust.Shared.Toolshed.TypeParsers; /// public sealed class VarTypeParser : CustomTypeParser { + public override bool ShowTypeArgSignature => false; public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Type? result) { result = null; From 89dba31b51965929b6737dc96fc087c5df1255d5 Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Thu, 2 Jan 2025 13:30:44 +1300 Subject: [PATCH 10/23] Improve NoImplementationError --- .../Toolshed/Syntax/ParsedCommand.cs | 43 ++++++++----------- Robust.Shared/Toolshed/ToolshedCommand.cs | 10 +++++ .../Toolshed/ToolshedCommandImplementor.cs | 7 ++- 3 files changed, 32 insertions(+), 28 deletions(-) diff --git a/Robust.Shared/Toolshed/Syntax/ParsedCommand.cs b/Robust.Shared/Toolshed/Syntax/ParsedCommand.cs index 5e77652220c..4bc4251e8b7 100644 --- a/Robust.Shared/Toolshed/Syntax/ParsedCommand.cs +++ b/Robust.Shared/Toolshed/Syntax/ParsedCommand.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -155,11 +156,12 @@ private static bool TryParseCommand( 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 @@ private static bool TryParseCommandName(ParserContext ctx, [NotNullWhen(true)] o 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 @@ private static bool TryParseImplementor(ParserContext ctx, ToolshedCommand cmd, 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 typeArgs = ""; - - if (Types != null && Types.Length != 0) - { - typeArgs = "<" + string.Join(",", Types.Select(ReflectionExtensions.PrettyName)) + ">"; - } + var msg = FormattedMessage.FromUnformatted($"Could not find an implementation of the '{Cmd}' command given the input type '{PipedType?.PrettyName() ?? "void"}'.\n"); - 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 (!cmdImpl.TryGetReturnType(SubCommand, retType, Types, out var myRetType)) - continue; + // If one of the signatures just takes T Or IEnumerable 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; - 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; } } diff --git a/Robust.Shared/Toolshed/ToolshedCommand.cs b/Robust.Shared/Toolshed/ToolshedCommand.cs index c770cf80393..b31d30aee3d 100644 --- a/Robust.Shared/Toolshed/ToolshedCommand.cs +++ b/Robust.Shared/Toolshed/ToolshedCommand.cs @@ -303,6 +303,16 @@ public struct CommandArgumentBundle /// The type of input that will be piped into this command. /// public required Type? PipedType; + + /// + /// The index where the command's name starts. Used for contextualising errors. + /// + public int NameStart; + + /// + /// The index where the (sub)command's name ends. Used for contextualising errors. + /// + public int NameEnd; } internal readonly record struct CommandDiscriminator(Type? PipedType, Type[]? TypeArguments) diff --git a/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs b/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs index 8ce32d8b211..cfbf664e9f0 100644 --- a/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs +++ b/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs @@ -81,8 +81,11 @@ public bool TryParse(ParserContext ctx, out Invocable? invocable, [NotNullWhen(t 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; } From d9c8972d894d5d42bd9cf99e732712ce2e6dc443 Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Thu, 2 Jan 2025 13:58:29 +1300 Subject: [PATCH 11/23] Better type argument help signatures --- .../Toolshed/Commands/Misc/ExplainCommand.cs | 2 +- .../Toolshed/ToolshedCommandImplementor.cs | 24 +++++++++++++++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/Robust.Shared/Toolshed/Commands/Misc/ExplainCommand.cs b/Robust.Shared/Toolshed/Commands/Misc/ExplainCommand.cs index 7442b692316..c80840ebe09 100644 --- a/Robust.Shared/Toolshed/Commands/Misc/ExplainCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Misc/ExplainCommand.cs @@ -33,7 +33,7 @@ CommandRun expr if (cmd.Bundle.Inverted) builder.Append("not "); - cmd.Implementor.AddMethodSignature(builder, cmd.Method.Args); + cmd.Implementor.AddMethodSignature(builder, cmd.Method.Args, cmd.Bundle.TypeArguments); builder.AppendLine(); var piped = cmd.PipedType?.PrettyName() ?? "[none]"; diff --git a/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs b/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs index cfbf664e9f0..eced4816c89 100644 --- a/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs +++ b/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs @@ -606,7 +606,7 @@ public string GetHelp() /// /// Construct the methods signature for help and explain commands. /// - internal void AddMethodSignature(StringBuilder builder, CommandArgument[] args) + internal void AddMethodSignature(StringBuilder builder, CommandArgument[] args, Type[]? typeArgs = null) { builder.Append(FullName); @@ -622,11 +622,25 @@ internal void AddMethodSignature(StringBuilder builder, CommandArgument[] args) numParsers++; } - for (var i = 0; i < numParsers; i++) + // Add "" for methods that take in type arguments. + if (numParsers > 0) { - builder.Append(" 1) - builder.Append(i); + 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('>'); } From c9f62e14240bf15e2b6b50a3a4f0f684638c66c8 Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Thu, 2 Jan 2025 14:21:59 +1300 Subject: [PATCH 12/23] better pipe syntax --- .../Toolshed/Commands/Misc/ExplainCommand.cs | 17 ++++++++--------- .../Toolshed/ToolshedCommandImplementor.cs | 8 ++++++-- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/Robust.Shared/Toolshed/Commands/Misc/ExplainCommand.cs b/Robust.Shared/Toolshed/Commands/Misc/ExplainCommand.cs index c80840ebe09..bf72c23a5d8 100644 --- a/Robust.Shared/Toolshed/Commands/Misc/ExplainCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Misc/ExplainCommand.cs @@ -1,8 +1,5 @@ -using System; -using System.Text; -using Microsoft.Extensions.Primitives; +using System.Text; using Robust.Shared.Toolshed.Syntax; -using Robust.Shared.Toolshed.TypeParsers; using Robust.Shared.Utility; namespace Robust.Shared.Toolshed.Commands.Misc; @@ -23,22 +20,24 @@ CommandRun expr 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} ({cmd.PipedType.PrettyName()})> -> "); + builder.Append($"<{pipeArg?.Name}> → "); // No type information, as that is already given above. } if (cmd.Bundle.Inverted) builder.Append("not "); cmd.Implementor.AddMethodSignature(builder, cmd.Method.Args, cmd.Bundle.TypeArguments); - builder.AppendLine(); - var piped = cmd.PipedType?.PrettyName() ?? "[none]"; - var returned = cmd.ReturnType?.PrettyName() ?? "[none]"; - builder.AppendLine($"{piped} -> {returned}"); } ctx.WriteLine(builder.ToString().TrimEnd()); diff --git a/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs b/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs index eced4816c89..b67530a6006 100644 --- a/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs +++ b/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs @@ -588,8 +588,12 @@ public string GetHelp() { builder.Append(Environment.NewLine + " "); + // TODO TOOLSHED + // FormattedMessage support for help strings + // make the argument type hint colour coded, for easier parsing of help strings. + // I.e., in ")> make the "(IEnumerable)" part gray? if (method.PipeArg != null) - builder.Append($"<{method.PipeArg.Name} ({method.PipeArg.ParameterType.PrettyName()})> -> "); + builder.Append($"<{method.PipeArg.Name} ({method.PipeArg.ParameterType.PrettyName()})> → "); if (method.Invertible) builder.Append("[not] "); @@ -597,7 +601,7 @@ public string GetHelp() AddMethodSignature(builder, method.Arguments); if (method.Info.ReturnType != typeof(void)) - builder.Append($" -> {method.Info.ReturnType.PrettyName()}"); + builder.Append($" → {method.Info.ReturnType.PrettyName()}"); } return builder.ToString(); From 5a8f7214e7202224a80879b434984f300908312b Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Thu, 2 Jan 2025 15:45:43 +1300 Subject: [PATCH 13/23] fix NRE --- Robust.Shared/Toolshed/Commands/Generic/EmplaceCommand.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Robust.Shared/Toolshed/Commands/Generic/EmplaceCommand.cs b/Robust.Shared/Toolshed/Commands/Generic/EmplaceCommand.cs index 800ed11fca5..847fde54a7a 100644 --- a/Robust.Shared/Toolshed/Commands/Generic/EmplaceCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Generic/EmplaceCommand.cs @@ -174,8 +174,14 @@ private sealed class EmplaceBlockParser : CustomTypeParser { 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 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]; From 2934fc74b80a66e37f00450a4c8d6729410f7449 Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Thu, 2 Jan 2025 15:52:14 +1300 Subject: [PATCH 14/23] Add test --- Robust.UnitTesting/Shared/Toolshed/ToolshedTests.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Robust.UnitTesting/Shared/Toolshed/ToolshedTests.cs b/Robust.UnitTesting/Shared/Toolshed/ToolshedTests.cs index 3c6fd4841b5..6cddbb0802f 100644 --- a/Robust.UnitTesting/Shared/Toolshed/ToolshedTests.cs +++ b/Robust.UnitTesting/Shared/Toolshed/ToolshedTests.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using NUnit.Framework; using Robust.Shared.GameObjects; @@ -389,6 +388,13 @@ await Server.WaitAssertion(() => 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 directly, but would fail + // when given a List or something else that implemented the interface. + // In particular, this would become relevant when using command variables (which store enumerables as a List). + 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}); }); } From 22d2dbf568ad9e8256d09c31e0139419c7137463 Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Thu, 2 Jan 2025 16:07:56 +1300 Subject: [PATCH 15/23] a --- Robust.Shared/Toolshed/ToolshedCommandImplementor.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs b/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs index b67530a6006..a0603c59e82 100644 --- a/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs +++ b/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs @@ -5,7 +5,6 @@ using System.Linq.Expressions; using System.Reflection; using System.Text; -using Microsoft.Extensions.Primitives; using Robust.Shared.Exceptions; using Robust.Shared.Localization; using Robust.Shared.Toolshed.Errors; From b195c9675e21d8d9e1f309a3cbb1126348d56971 Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Fri, 3 Jan 2025 16:28:26 +1300 Subject: [PATCH 16/23] Fix silent toolshed failure --- Robust.Shared/Toolshed/ToolshedManager.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Robust.Shared/Toolshed/ToolshedManager.cs b/Robust.Shared/Toolshed/ToolshedManager.cs index 0ec85f601b6..7f1b629f73f 100644 --- a/Robust.Shared/Toolshed/ToolshedManager.cs +++ b/Robust.Shared/Toolshed/ToolshedManager.cs @@ -91,9 +91,8 @@ public bool InvokeCommand(ICommonSession session, string command, object? input, { 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(); From d3725ef11d76a63350ec9ed9c5a41462b72b32ed Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Fri, 3 Jan 2025 16:56:58 +1300 Subject: [PATCH 17/23] Fix GetConcreteMethodInternal --- Robust.Shared/Toolshed/ToolshedCommandImplementor.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs b/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs index a0603c59e82..9026f4569e1 100644 --- a/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs +++ b/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs @@ -423,13 +423,18 @@ internal CommandArgument GetCommandArgument(ParameterInfo arg) if (x.PipeArg is not { } param) return 0; + // We want exact match to be preferred. if (pipedType!.IsAssignableTo(param.ParameterType)) - return 1000; // We want exact match to be preferred! + return 1000; + + // Next, we prefer methods that have the same base type. + // E.g., given an IEnumerable we should preferentially match to methods that take in an IEnumerable 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; + // I.e., anything concrete is preferable over a simple generic parameter that could match any type. + return param.ParameterType.IsGenericParameter ? 0 : 100; }) .Select(x => From 516aa5cd9b408da509287e56d23a7d67ef5f8a20 Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Fri, 3 Jan 2025 16:57:09 +1300 Subject: [PATCH 18/23] Improve vars command --- .../Commands/Generic/Variables/VarsCommand.cs | 2 +- .../Toolshed/ToolshedManager.PrettyPrint.cs | 26 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Robust.Shared/Toolshed/Commands/Generic/Variables/VarsCommand.cs b/Robust.Shared/Toolshed/Commands/Generic/Variables/VarsCommand.cs index df5e7efd823..e0b3a62b3e6 100644 --- a/Robust.Shared/Toolshed/Commands/Generic/Variables/VarsCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Generic/Variables/VarsCommand.cs @@ -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 _)); } } diff --git a/Robust.Shared/Toolshed/ToolshedManager.PrettyPrint.cs b/Robust.Shared/Toolshed/ToolshedManager.PrettyPrint.cs index 22e6789ec3c..35cf4c793e8 100644 --- a/Robust.Shared/Toolshed/ToolshedManager.PrettyPrint.cs +++ b/Robust.Shared/Toolshed/ToolshedManager.PrettyPrint.cs @@ -54,19 +54,6 @@ public string PrettyPrintType(object? value, out IEnumerable? more, bool moreUse return t.PrettyName(); } - if (value is IEnumerable @enum) - { - var list = @enum.Cast().ToList(); - if (list.Count > maxOutput.Value) - more = list.GetRange(maxOutput.Value, list.Count - maxOutput.Value - 1); - var res = string.Join(",\n", list.Take(maxOutput.Value).Select(x => PrettyPrintType(x, out _))); - if (more is not null && moreUsed) - return res + "... (output truncated, run more for further output)"; - if (more is not null) - return res + "... (output truncated, if possible tee the value into it's own variable)"; - return res; - } - if (value.GetType().IsAssignableTo(typeof(IDictionary))) { var dict = ((IDictionary) value).GetEnumerator(); @@ -81,6 +68,19 @@ public string PrettyPrintType(object? value, out IEnumerable? more, bool moreUse return $"Dictionary {{\n{string.Join(",\n", kvList)}\n}}"; } + if (value is IEnumerable @enum) + { + var list = @enum.Cast().ToList(); + if (list.Count > maxOutput.Value) + more = list.GetRange(maxOutput.Value, list.Count - maxOutput.Value - 1); + var res = string.Join(",\n", list.Take(maxOutput.Value).Select(x => PrettyPrintType(x, out _))); + if (more is not null && moreUsed) + return res + "... (output truncated, run more for further output)"; + if (more is not null) + return res + "... (output truncated, if possible tee the value into it's own variable)"; + return res; + } + return value.ToString() ?? "[unrepresentable]"; } } From e6e04a3cdbd7642c8c3286e30423c9118804b587 Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Fri, 3 Jan 2025 16:57:19 +1300 Subject: [PATCH 19/23] EntProtoId IAsType --- Robust.Shared/Prototypes/EntProtoId.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Robust.Shared/Prototypes/EntProtoId.cs b/Robust.Shared/Prototypes/EntProtoId.cs index f0ba69dd2db..5944ccf07ff 100644 --- a/Robust.Shared/Prototypes/EntProtoId.cs +++ b/Robust.Shared/Prototypes/EntProtoId.cs @@ -17,7 +17,8 @@ namespace Robust.Shared.Prototypes; /// /// for a wrapper of other prototype kinds. [Serializable, NetSerializable] -public readonly record struct EntProtoId(string Id) : IEquatable, IComparable, IAsType +public readonly record struct EntProtoId(string Id) : IEquatable, IComparable, IAsType, + IAsType> { public static implicit operator string(EntProtoId protoId) { @@ -49,7 +50,9 @@ public int CompareTo(EntProtoId other) return string.Compare(Id, other.Id, StringComparison.Ordinal); } - public string AsType() => Id; + string IAsType.AsType() => Id; + + ProtoId IAsType>.AsType() => new(Id); public override string ToString() => Id ?? string.Empty; } From 9e0140244d83cdaee7e8295ee3723fb3dac3e73b Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Fri, 3 Jan 2025 18:00:32 +1300 Subject: [PATCH 20/23] More GetConcreteMethodInternal fixes --- RELEASE-NOTES.md | 2 + .../Toolshed/ToolshedCommandImplementor.cs | 66 ++++++++++++++----- 2 files changed, 52 insertions(+), 16 deletions(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index fc2ff0e121f..a00de619db8 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -46,6 +46,8 @@ END TEMPLATE--> ### Bugfixes * 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` implementation. ### Other diff --git a/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs b/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs index 9026f4569e1..4392bd71fcd 100644 --- a/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs +++ b/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs @@ -412,30 +412,16 @@ internal CommandArgument GetCommandArgument(ParameterInfo arg) 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; - // We want exact match to be preferred. - if (pipedType!.IsAssignableTo(param.ParameterType)) - return 1000; - - // Next, we prefer methods that have the same base type. - // E.g., given an IEnumerable we should preferentially match to methods that take in an IEnumerable - if (param.ParameterType.GetMostGenericPossible() == pipedType.GetMostGenericPossible()) - return 500; // If not, try to prefer the same base type. - - // Finally, prefer specialized (type exact) implementations. - // I.e., anything concrete is preferable over a simple generic parameter that could match any type. - return param.ParameterType.IsGenericParameter ? 0 : 100; - + return GetMethodRating(pipedType, param.ParameterType); }) .Select(x => { @@ -458,6 +444,54 @@ internal CommandArgument GetCommandArgument(ParameterInfo arg) .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 into a command, iin order of most to least preferred we want a method that + // takes in an: + // - List + // - List + // - Any concrete type (IEnumerable, IEnumerable, string, EntProtoId, etc) + // - List + // - Any type constructed out of generic types (e.g., List, IEnumerable) + // - constrained generic parameters (e.g., T where T : IEnumerable) + // - unconstrained generic parameters + // + // Finally, subsequent Select() calls in GetConcreteMethodInternal() will effectively discard any methods that + // can't actually be used. E.g., List is preferred over List 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 we should preferentially match to methods that take in an List + if (paramType.GetMostGenericPossible() == pipedType.GetMostGenericPossible()) + return 500; + + return 400; + } + + // Again. we prefer methods that have the same base type. + // E.g., given an List we should preferentially match to methods that take in an List + if (paramType.GetMostGenericPossible() == pipedType.GetMostGenericPossible()) + return 300; + + // Anything that is not just directly a generic parameter is preferred. + if (!paramType.IsGenericParameter) + return 100; + + // If this is a generic parameter, we prefer specificity. The least preferred match is any method that + // just takes an un-constrained generic parameter. + return Math.Min(paramType.GetGenericParameterConstraints().Length, 99); + } + /// /// When a method has the , this method is used to actually /// determine the generic type argument given the type of the piped in value. From 5356be833ccf1312219f9d4cb349ea4a61cb5f44 Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Fri, 3 Jan 2025 18:35:03 +1300 Subject: [PATCH 21/23] I hate this so much --- .../Toolshed/ToolshedCommandImplementor.cs | 19 +++++++++---------- .../Toolshed/ToolshedManager.PrettyPrint.cs | 4 ++-- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs b/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs index 4392bd71fcd..53ece095f7b 100644 --- a/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs +++ b/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs @@ -448,15 +448,15 @@ 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 into a command, iin order of most to least preferred we want a method that - // takes in an: + // piping an List into a command, in order of most to least preferred we want a method that + // takes in a: // - List // - List // - Any concrete type (IEnumerable, IEnumerable, string, EntProtoId, etc) // - List - // - Any type constructed out of generic types (e.g., List, IEnumerable) // - constrained generic parameters (e.g., T where T : IEnumerable) // - unconstrained generic parameters + // - Any type constructed out of generic types (e.g., List, IEnumerable) // // Finally, subsequent Select() calls in GetConcreteMethodInternal() will effectively discard any methods that // can't actually be used. E.g., List is preferred over List here, but obviously couldn't be used. @@ -468,7 +468,7 @@ private int GetMethodRating(Type? pipedType, Type paramType) return 1000; // We prefer non-generic methods - if (paramType.ContainsGenericParameters) + if (!paramType.ContainsGenericParameters) { // Next, we also prefer methods that have the same base type. // E.g., given a List we should preferentially match to methods that take in an List @@ -483,13 +483,12 @@ private int GetMethodRating(Type? pipedType, Type paramType) if (paramType.GetMostGenericPossible() == pipedType.GetMostGenericPossible()) return 300; - // Anything that is not just directly a generic parameter is preferred. - if (!paramType.IsGenericParameter) - return 100; + // Next we prefer methods that just directly take in some generic type + // i.e., we prefer matching the method that takes T over IEnumerable + if (paramType.IsGenericParameter) + return Math.Min(100 + paramType.GetGenericParameterConstraints().Length, 299); - // If this is a generic parameter, we prefer specificity. The least preferred match is any method that - // just takes an un-constrained generic parameter. - return Math.Min(paramType.GetGenericParameterConstraints().Length, 99); + return 0; } /// diff --git a/Robust.Shared/Toolshed/ToolshedManager.PrettyPrint.cs b/Robust.Shared/Toolshed/ToolshedManager.PrettyPrint.cs index 35cf4c793e8..901c6a79859 100644 --- a/Robust.Shared/Toolshed/ToolshedManager.PrettyPrint.cs +++ b/Robust.Shared/Toolshed/ToolshedManager.PrettyPrint.cs @@ -60,10 +60,10 @@ public string PrettyPrintType(object? value, out IEnumerable? more, bool moreUse var kvList = new List(); - do + while (dict.MoveNext()) { kvList.Add($"({PrettyPrintType(dict.Key, out _)}, {PrettyPrintType(dict.Value, out _)}"); - } while (dict.MoveNext()); + } return $"Dictionary {{\n{string.Join(",\n", kvList)}\n}}"; } From 75b04b6d99520d8d0cb0febfad406bf09c0cc296 Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Fri, 3 Jan 2025 19:36:14 +1300 Subject: [PATCH 22/23] update tp command description The command arguments call the the "other" entity the "target" --- Resources/Locale/en-US/toolshed-commands.ftl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Resources/Locale/en-US/toolshed-commands.ftl b/Resources/Locale/en-US/toolshed-commands.ftl index 1acf8e57c89..dcfd9bfcc46 100644 --- a/Resources/Locale/en-US/toolshed-commands.ftl +++ b/Resources/Locale/en-US/toolshed-commands.ftl @@ -202,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 = From 280857b889eaaddc0e2d40bed8aefbda62b8c3e4 Mon Sep 17 00:00:00 2001 From: ElectroJr Date: Fri, 3 Jan 2025 21:03:40 +1300 Subject: [PATCH 23/23] Support localized argument hints/signatures --- .../Toolshed/Commands/Misc/ExplainCommand.cs | 12 +++++++++- .../Toolshed/ToolshedCommand.Help.cs | 24 ++++++++++++------- .../Toolshed/ToolshedCommandImplementor.cs | 15 +++++++++--- 3 files changed, 39 insertions(+), 12 deletions(-) diff --git a/Robust.Shared/Toolshed/Commands/Misc/ExplainCommand.cs b/Robust.Shared/Toolshed/Commands/Misc/ExplainCommand.cs index bf72c23a5d8..f4e16051373 100644 --- a/Robust.Shared/Toolshed/Commands/Misc/ExplainCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Misc/ExplainCommand.cs @@ -30,7 +30,17 @@ CommandRun expr { var pipeArg = cmd.Method.Base.PipeArg; DebugTools.AssertNotNull(pipeArg); - builder.Append($"<{pipeArg?.Name}> → "); // No type information, as that is already given above. + + 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) diff --git a/Robust.Shared/Toolshed/ToolshedCommand.Help.cs b/Robust.Shared/Toolshed/ToolshedCommand.Help.cs index b2cd7931c90..639aa7104a1 100644 --- a/Robust.Shared/Toolshed/ToolshedCommand.Help.cs +++ b/Robust.Shared/Toolshed/ToolshedCommand.Help.cs @@ -41,19 +41,27 @@ public override string ToString() /// public static string GetArgHint(CommandArgument? arg, Type t) { - var type = t.PrettyName(); - if (arg == null) - return type; + return t.PrettyName(); + + return GetArgHint(arg.Value.Name, arg.Value.IsOptional, arg.Value.IsParamsCollection, t); + } + + /// + /// Helper method for generating auto-completion hints while parsing command arguments. + /// + 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 (arg.Value.IsOptional) - return $"[{arg.Value.Name} ({type})]"; + if (optional) + return $"[{name} ({type})]"; // ellipses for params / variable length arguments - if (arg.Value.IsParamsCollection) - return $"[{arg.Value.Name} ({type})]..."; + if (isParams) + return $"[{name} ({type})]..."; - return $"<{arg.Value.Name} ({type})>"; + return $"<{name} ({type})>"; } } diff --git a/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs b/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs index 53ece095f7b..c85c2487dc8 100644 --- a/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs +++ b/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs @@ -629,8 +629,14 @@ public string GetHelp() // FormattedMessage support for help strings // make the argument type hint colour coded, for easier parsing of help strings. // I.e., in ")> make the "(IEnumerable)" part gray? - if (method.PipeArg != null) - builder.Append($"<{method.PipeArg.Name} ({method.PipeArg.ParameterType.PrettyName()})> → "); + 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] "); @@ -688,7 +694,10 @@ internal void AddMethodSignature(StringBuilder builder, CommandArgument[] args, foreach (var arg in args) { builder.Append(' '); - builder.Append(ToolshedCommand.GetArgHint(arg, arg.Type)); + if (_loc.TryGetString($"command-arg-sig-{LocName}-{arg.Name}", out var msg)) + builder.Append(msg); + else + builder.Append(ToolshedCommand.GetArgHint(arg, arg.Type)); } }