diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 93b7361a5db..e6f5660841d 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -35,19 +35,26 @@ 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 -*None yet* +* 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 -*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 diff --git a/Resources/Locale/en-US/toolshed-commands.ftl b/Resources/Locale/en-US/toolshed-commands.ftl index 08330dcffbb..dcfd9bfcc46 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. @@ -149,7 +148,7 @@ command-description-max = Returns the maximum of two values. command-description-BitAndCommand = Performs bitwise AND. -command-description-BitOrCommand = +command-description-bitor = Performs bitwise OR. command-description-BitXorCommand = Performs bitwise XOR. @@ -203,11 +202,11 @@ command-description-mappos = command-description-pos = Returns an entity's coordinates. command-description-tp-coords = - Teleports the target to the given coordinates. + Teleports the given entities to the target coordinates. command-description-tp-to = - Teleports the target to the given other entity. + Teleports the given entities to the target entity. command-description-tp-into = - Teleports the target "into" the given other entity, attaching it at (0 0) relative to it. + Teleports the given entities "into" the target entity, attaching it at (0 0) relative to it. command-description-comp-get = Gets the given component from the given entity. command-description-comp-add = @@ -277,7 +276,7 @@ command-description-ModVecCommand = Performs the modulus operation over the input with the given constant right-hand value. command-description-BitAndNotCommand = Performs bitwise AND-NOT over the input. -command-description-BitOrNotCommand = +command-description-bitornot = Performs bitwise OR-NOT over the input. command-description-BitXnorCommand = Performs bitwise XNOR over the input. 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; } 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 8cf76e377c0..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]; @@ -219,7 +225,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; @@ -231,6 +237,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; @@ -246,7 +253,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/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/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 741f96d8ebc..f4e16051373 100644 --- a/Robust.Shared/Toolshed/Commands/Misc/ExplainCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Misc/ExplainCommand.cs @@ -16,32 +16,40 @@ 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()}"); + var piped = cmd.PipedType?.PrettyName() ?? "[none]"; + builder.AppendLine($"Pipe input: {piped}"); + builder.AppendLine($"Pipe output: {cmd.ReturnType.PrettyName()}"); + + builder.Append($"Signature:\n "); + if (cmd.PipedType != null) { var pipeArg = cmd.Method.Base.PipeArg; DebugTools.AssertNotNull(pipeArg); - builder.Append($"<{pipeArg?.Name} ({ToolshedCommandImplementor.GetFriendlyName(cmd.PipedType)})> -> "); + + var locKey = $"command-arg-sig-{cmd.Implementor.LocName}-{pipeArg?.Name}"; + if (Loc.TryGetString(locKey, out var msg)) + { + builder.Append(msg); + builder.Append(" → "); + } + else + { + builder.Append($"<{pipeArg?.Name}> → "); // No type information, as that is already given above. + } } if (cmd.Bundle.Inverted) builder.Append("not "); - builder.Append($"{name}"); - foreach (var (argName, argType, _) in cmd.Method.Args) - { - builder.Append($" <{argName} ({ToolshedCommandImplementor.GetFriendlyName(argType)})>"); - } - - builder.AppendLine(); - var piped = cmd.PipedType?.PrettyName() ?? "[none]"; - var returned = cmd.ReturnType?.PrettyName() ?? "[none]"; - builder.AppendLine($"{piped} -> {returned}"); + cmd.Implementor.AddMethodSignature(builder, cmd.Method.Args, cmd.Bundle.TypeArguments); builder.AppendLine(); } - ctx.WriteLine(builder.ToString()); + ctx.WriteLine(builder.ToString().TrimEnd()); } } 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 b43a23bab55..d17d1c83b71 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 commandExpected; while (true) { if (!ParsedCommand.TryParse(ctx, pipedType, out var cmd)) @@ -107,27 +109,55 @@ public static bool TryParse( cmds.Add((cmd, (start, ctx.Index))); ctx.ConsumeWhitespace(); - if (ctx.EatCommandTerminators()) - { - ctx.ConsumeWhitespace(); - pipedType = null; - } + ctx.EatCommandTerminators(ref pipedType, out commandExpected); // If the command run encounters a block terminator we exit out. // The parser that pushed the block terminator is what should actually eat & pop it, so that it can // return appropriate errors if the block was not terminated. if (ctx.PeekBlockTerminator()) - break; + { + if (!commandExpected) + break; + + // Lets enforce that poeple don't end command blocks with a dangling explicit pipe. + // I.e., force people to use `{ i 2 }` instead of `{ i 2 | }`. + if (!ctx.GenerateCompletions) + { + ctx.Error = new UnexpectedCloseBrace(); + ctx.Error.Contextualize(ctx.Input, (ctx.Index, ctx.Index + 1)); + } + return false; + } if (ctx.OutOfInput) - break; + { + // If the last command was terminated by an explicit pipe symbol we require that there be a follow-up + // command + if (!commandExpected) + break; + + if (ctx.GenerateCompletions) + { + // TODO TOOLSHED COMPLETIONS improve this + // Currently completions are only shown if a command ends in a space. I.e. "| " instead of "|". + // Ideally the completions should still be shown, and it should know to auto-insert a leading space. + // AFAIK this requires updating the client-side code like FilterCompletions(), as well as somehow + // communicating this to the client, maybe by adding a new bit to the CompletionOptionFlags, or + // adding some similar flag field to the whole whole completion collection? + ParsedCommand.TryParse(ctx, pipedType, out _); + } + else + ctx.Error = new OutOfInputError(); + + return false; + } start = ctx.Index; if (pipedType != typeof(void)) continue; - // The previously parsed command does not generate any output that can be piped/chained into another + // The previously parsed command does not generate any output that can be piped/chained into another // command. This can happen if someone tries to provide more arguments than a command accepts. // e.g., " i 5 5". In this case, the parsing fails and should make it clear that no more input was expected. // Multiple unrelated commands on a single line are still supported via the ';' terminator. @@ -147,8 +177,7 @@ public 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 +362,6 @@ public sealed class EndOfCommandError : ConError { public override FormattedMessage DescribeInner() { - return FormattedMessage.FromUnformatted("Expected an end of command (;)"); + return FormattedMessage.FromUnformatted("Expected a command or block terminator (';' or '}')"); } } 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/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..d7ff9d38cd8 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,36 +425,50 @@ 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, out bool commandExpected) { + commandExpected = false; + + // Command terminator drops piped values. if (EatMatch(new Rune(';'))) + { + pipedType = null; return true; + } + + // Explicit pipe operator keeps piped value, but is only valid if there is a piped value. + if (pipedType != null && pipedType != typeof(void) && EatMatch(new Rune('|'))) + { + commandExpected = true; + return true; + } // If multi-line commands are not enabled, we treat a newline like a ';' - // I.e., it terminates the command currently being parsed in - return NoMultilineExprs && EatMatch(new Rune('\n')); + if (NoMultilineExprs && EatMatch(new Rune('\n'))) + { + pipedType = null; + return true; + } + + return false; } /// /// Attempts to repeatedly consume command terminators, and return true if any were consumed. /// - public bool EatCommandTerminators() + public void EatCommandTerminators(ref Type? pipedType, out bool commandExpected) { - if (!EatCommandTerminator()) - return false; + if (!EatCommandTerminator(ref pipedType, out commandExpected)) + return; - // Maybe one day we want to allow ';;' to have special meaning? - // But for now, just eat em all. ConsumeWhitespace(); - while (EatCommandTerminator()) + while (!commandExpected && EatCommandTerminator(ref pipedType, out commandExpected)) { ConsumeWhitespace(); } - - return true; } } 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/ToolshedCommand.Help.cs b/Robust.Shared/Toolshed/ToolshedCommand.Help.cs index f827d15a1ae..639aa7104a1 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,33 @@ 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) + { + if (arg == null) + 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 (optional) + return $"[{name} ({type})]"; + + // ellipses for params / variable length arguments + if (isParams) + return $"[{name} ({type})]..."; + + return $"<{name} ({type})>"; + } } diff --git a/Robust.Shared/Toolshed/ToolshedCommand.cs b/Robust.Shared/Toolshed/ToolshedCommand.cs index 2304ec2acf9..b31d30aee3d 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)) @@ -289,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 274f5059df7..c85c2487dc8 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) @@ -80,8 +80,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; } @@ -107,11 +110,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,22 +131,68 @@ 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); var start = ctx.Index; var save = ctx.Save(); ctx.ConsumeWhitespace(); + DebugTools.AssertNotNull(arg.Parser); + parsed = null; if (ctx.PeekCommandOrBlockTerminated() || ctx is {OutOfInput: true, GenerateCompletions: false}) { - ctx.Error = new ExpectedArgumentError(arg.Type); - ctx.Error.Contextualize(ctx.Input, (start, ctx.Index+1)); - return false; - } + if (!arg.IsOptional) + { + ctx.Error = new ExpectedArgumentError(arg.Type); + ctx.Error.Contextualize(ctx.Input, (start, ctx.Index + 1)); + return false; + } - if (!arg.Parser.TryParse(ctx, out var parsed)) + parsed = arg.DefaultValue; + } + else if (arg.Parser!.TryParse(ctx, out parsed)) + { + // All arguments should have been parsed as a ValueRef or Block, unless this is using some custom type parser +#if DEBUG + var t = parsed.GetType(); + if (arg.Parser.GetType().IsCustomParser()) + { + DebugTools.Assert(t.IsAssignableTo(arg.Type) + || t.IsAssignableTo(typeof(Block)) + || t.IsValueRef()); + } + else if (arg.Type.IsAssignableTo(typeof(Block))) + DebugTools.Assert(t.IsAssignableTo(typeof(Block))); + else + DebugTools.Assert(t.IsValueRef()); +#endif + } + else { if (ctx.GenerateCompletions) { @@ -149,8 +207,8 @@ 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; } @@ -163,24 +221,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; - if (!ctx.GenerateCompletions || !ctx.OutOfInput) return true; @@ -191,8 +231,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 +241,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; } @@ -318,18 +357,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 isParamsCollection = arg.HasCustomAttribute(); + if (isParamsCollection) + { + if (!argType.IsArray) + throw new NotSupportedException(".net 9 params collections are not yet supported"); + + // We parse each element directly, as opposed to trying to parse an array. + argType = argType.GetElementType()!; + } + + return new CommandArgument( + arg.Name!, + argType, + GetArgumentParser(arg, argType), + arg.IsOptional, + arg.DefaultValue, + isParamsCollection); + } + + private ITypeParser? GetArgumentParser(ParameterInfo param, Type type) + { + if (type.ContainsGenericParameters) + return null; + var attrib = param.GetCustomAttribute(); var parser = attrib?.CustomParser is not {} custom - ? _toolshed.GetArgumentParser(param.ParameterType) + ? _toolshed.GetArgumentParser(type) : _toolshed.GetArgumentParser(_toolshed.GetCustomParser(custom)); if (parser == null) @@ -346,25 +412,16 @@ private ITypeParser GetArgumentParser(ParameterInfo param) return pipedType is null; if (pipedType == null) - return false; // We want exact match to be preferred! + return false; return x.Generic || _toolshed.IsTransformableTo(pipedType, param.ParameterType); - - // Finally, prefer specialized (type exact) implementations. }) .OrderByDescending(x => { if (x.PipeArg is not { } param) return 0; - if (pipedType!.IsAssignableTo(param.ParameterType)) - return 1000; // We want exact match to be preferred! - if (param.ParameterType.GetMostGenericPossible() == pipedType.GetMostGenericPossible()) - return 500; // If not, try to prefer the same base type. - - // Finally, prefer specialized (type exact) implementations. - return param.ParameterType.IsGenericTypeParameter ? 0 : 100; - + return GetMethodRating(pipedType, param.ParameterType); }) .Select(x => { @@ -387,6 +444,53 @@ private ITypeParser GetArgumentParser(ParameterInfo param) .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, 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 + // - 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. + // + // 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; + + // 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); + + return 0; + } + /// /// When a method has the , this method is used to actually /// determine the generic type argument given the type of the piped in value. @@ -477,12 +581,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); } @@ -509,61 +625,95 @@ public string GetHelp() { builder.Append(Environment.NewLine + " "); - if (method.PipeArg != null) - builder.Append($"<{method.PipeArg.Name} ({GetFriendlyName(method.PipeArg.ParameterType)})> -> "); + // TODO TOOLSHED + // FormattedMessage support for help strings + // make the argument type hint colour coded, for easier parsing of help strings. + // I.e., in ")> make the "(IEnumerable)" part gray? + if (method.PipeArg is {} pipeArg) + { + var locKey = $"command-arg-sig-{LocName}-{pipeArg.Name}"; + if (!_loc.TryGetString(locKey, out var pipeSig)) + pipeSig = ToolshedCommand.GetArgHint(pipeArg.Name!, false, false, pipeArg.ParameterType); + + builder.Append($"{pipeSig} → "); + } if (method.Invertible) builder.Append("[not] "); - builder.Append(FullName); - - foreach (var (argName, argType) in method.Arguments) - { - builder.Append($" <{argName} ({GetFriendlyName(argType)})>"); - } + AddMethodSignature(builder, method.Arguments); if (method.Info.ReturnType != typeof(void)) - builder.Append($" -> {GetFriendlyName(method.Info.ReturnType)}"); + builder.Append($" → {method.Info.ReturnType.PrettyName()}"); } return builder.ToString(); } - /// - public string DescriptionLocKey() => $"command-description-{LocName}"; - - /// - public string Description() + /// + /// Construct the methods signature for help and explain commands. + /// + internal void AddMethodSignature(StringBuilder builder, CommandArgument[] args, Type[]? typeArgs = null) { - return _loc.GetString(DescriptionLocKey()); - } + builder.Append(FullName); - public static string GetFriendlyName(Type type) - { - var friendlyName = type.Name; - if (!type.IsGenericType) - return friendlyName; + var tParsers = Owner.TypeParameterParsers; + var numParsers = 0; + foreach (var parserType in tParsers) + { + if (parserType == typeof(TypeTypeParser)) + continue; + + var parser = _toolshed.GetCustomParser(parserType); + if (parser.ShowTypeArgSignature) + numParsers++; + } + + // Add "" for methods that take in type arguments. + if (numParsers > 0) + { + builder.Append('<'); + for (var i = 0; i < numParsers; i++) + { + if (i > 0) + builder.Append(", "); + + if (typeArgs != null) + { + builder.Append(typeArgs[i].PrettyName()); + continue; + } - var iBacktick = friendlyName.IndexOf('`'); - if (iBacktick > 0) - friendlyName = friendlyName.Remove(iBacktick); + builder.Append('T'); + if (numParsers > 1) + builder.Append(i + 1); + } + builder.Append('>'); + } - friendlyName += "<"; - var typeParameters = type.GetGenericArguments(); - for (var i = 0; i < typeParameters.Length; ++i) + foreach (var arg in args) { - var typeParamName = GetFriendlyName(typeParameters[i]); - friendlyName += (i == 0 ? typeParamName : "," + typeParamName); + builder.Append(' '); + if (_loc.TryGetString($"command-arg-sig-{LocName}-{arg.Name}", out var msg)) + builder.Append(msg); + else + builder.Append(ToolshedCommand.GetArgHint(arg, arg.Type)); } - friendlyName += ">"; + } - return friendlyName; + /// + public string DescriptionLocKey() => $"command-description-{LocName}"; + + /// + public string Description() + { + return _loc.GetString(DescriptionLocKey()); } } /// -/// 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 @@ -587,9 +737,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(); @@ -597,7 +747,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) @@ -609,7 +759,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 IsOptional, + object? DefaultValue, + bool IsParamsCollection); 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/ToolshedManager.PrettyPrint.cs b/Robust.Shared/Toolshed/ToolshedManager.PrettyPrint.cs index 22e6789ec3c..901c6a79859 100644 --- a/Robust.Shared/Toolshed/ToolshedManager.PrettyPrint.cs +++ b/Robust.Shared/Toolshed/ToolshedManager.PrettyPrint.cs @@ -54,6 +54,20 @@ public string PrettyPrintType(object? value, out IEnumerable? more, bool moreUse return t.PrettyName(); } + if (value.GetType().IsAssignableTo(typeof(IDictionary))) + { + var dict = ((IDictionary) value).GetEnumerator(); + + var kvList = new List(); + + while (dict.MoveNext()) + { + kvList.Add($"({PrettyPrintType(dict.Key, out _)}, {PrettyPrintType(dict.Value, out _)}"); + } + + return $"Dictionary {{\n{string.Join(",\n", kvList)}\n}}"; + } + if (value is IEnumerable @enum) { var list = @enum.Cast().ToList(); @@ -67,20 +81,6 @@ public string PrettyPrintType(object? value, out IEnumerable? more, bool moreUse return res; } - if (value.GetType().IsAssignableTo(typeof(IDictionary))) - { - var dict = ((IDictionary) value).GetEnumerator(); - - var kvList = new List(); - - do - { - kvList.Add($"({PrettyPrintType(dict.Key, out _)}, {PrettyPrintType(dict.Value, out _)}"); - } while (dict.MoveNext()); - - return $"Dictionary {{\n{string.Join(",\n", kvList)}\n}}"; - } - return value.ToString() ?? "[unrepresentable]"; } } 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(); diff --git a/Robust.Shared/Toolshed/TypeParsers/BlockType.cs b/Robust.Shared/Toolshed/TypeParsers/BlockType.cs index cf4a4d2386c..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; @@ -31,7 +34,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; @@ -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; @@ -77,7 +99,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..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, string? argName) - => CompletionResult.FromHint(argName == null ? "" : $"<{argName}> (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, string? argName) - => CompletionResult.FromHint(argName == null ? "" : $"<{argName}> (NetEntity)"); + public override CompletionResult TryAutocomplete(ParserContext ctx, CommandArgument? arg) + => CompletionResult.FromHint(ToolshedCommand.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 = ToolshedCommand.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 = ToolshedCommand.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 = ToolshedCommand.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..fc66007b2be 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 = ToolshedCommand.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..158c7546c4e 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 @@ -26,11 +26,18 @@ internal 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 @@ -46,7 +53,13 @@ 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); + + protected string GetArgHint(CommandArgument? arg) + { + return ToolshedCommand.GetArgHint(arg, typeof(T)); + } 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..3bc883191f5 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('{')) { @@ -103,13 +103,13 @@ 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($""); } - 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..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; @@ -53,7 +54,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..3e032352fff 100644 --- a/Robust.UnitTesting/Shared/Toolshed/TestCommands.cs +++ b/Robust.UnitTesting/Shared/Toolshed/TestCommands.cs @@ -1,5 +1,8 @@ using System; +using System.Collections.Generic; +using System.Linq; using Robust.Shared.Console; +using Robust.Shared.Prototypes; using Robust.Shared.Toolshed; using Robust.Shared.Toolshed.Syntax; using Robust.Shared.Toolshed.TypeParsers; @@ -68,13 +71,37 @@ public 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"); } } } +[ToolshedCommand] +public sealed class TestOptionalArgsCommand : ToolshedCommand +{ + [CommandImplementation] + public int[] Impl(int x, int y = 0, int z = 1) + => [x, y, z]; +} + +[ToolshedCommand] +public sealed class TestParamsCollectionCommand : ToolshedCommand +{ + [CommandImplementation] + public int[] Impl(int x, int y = 0, params int[] others) + => [x, y, ..others]; +} + +[ToolshedCommand] +public sealed class TestParamsOnlyCommand : ToolshedCommand +{ + [CommandImplementation] + public int[] Impl(params int[] others) + => others; +} + [ToolshedCommand] public sealed class TestCustomParserCommand : ToolshedCommand { @@ -88,3 +115,103 @@ public sealed class 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 = Array.Empty>(); + + [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 23746e02280..6cddbb0802f 100644 --- a/Robust.UnitTesting/Shared/Toolshed/ToolshedTests.cs +++ b/Robust.UnitTesting/Shared/Toolshed/ToolshedTests.cs @@ -1,8 +1,10 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using NUnit.Framework; using Robust.Shared.GameObjects; using Robust.Shared.Player; +using Robust.Shared.Prototypes; using Robust.Shared.Toolshed; using Robust.Shared.Toolshed.Commands.Generic; using Robust.Shared.Toolshed.Syntax; @@ -118,8 +120,8 @@ 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; ; } ;; ;"); }); } @@ -186,6 +188,216 @@ await Server.WaitAssertion(() => }); } + [Test] + public async Task TestTerminators() + { + await Server.WaitAssertion(() => + { + // Baseline check that these commands work: + AssertResult("i 1", 1); + AssertResult("i 1 + 1", 2); + AssertResult("i { i 1 }", 1); + + // Trailing terminators have no clear effect. + AssertResult("i 1;", 1); + AssertResult("i { i 1 };", 1); + + // Simple explicit piping works + AssertResult("i 1 | + 1", 2); + + // Explicit pipes imply a command is expected. Ending a command or a block after a pipe should error. + ParseError("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}); + }); + } + + /// + /// 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)); + + // 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}); + }); + } + [Test] public async Task TestCompletions() { @@ -199,14 +411,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"); @@ -239,13 +451,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");