From b9767532a89a9e2cec7debf172a3c14643fb9734 Mon Sep 17 00:00:00 2001 From: gnuxie Date: Fri, 30 Dec 2022 20:52:40 +0000 Subject: [PATCH] help for commands. This commit is NOT contributed under the Apache-2.0 License. Copyright (C) 2022 Gnuxie All rights reserved. --- .../interface-manager/InterfaceCommand.ts | 18 +++++- .../interface-manager/MatrixHelpRenderer.ts | 61 +++++++++++++++++++ .../interface-manager/ParamaterParsing.ts | 25 +++++--- src/commands/interface-manager/Validation.ts | 8 ++- 4 files changed, 102 insertions(+), 10 deletions(-) create mode 100644 src/commands/interface-manager/MatrixHelpRenderer.ts diff --git a/src/commands/interface-manager/InterfaceCommand.ts b/src/commands/interface-manager/InterfaceCommand.ts index 513c9c60..536c7a23 100644 --- a/src/commands/interface-manager/InterfaceCommand.ts +++ b/src/commands/interface-manager/InterfaceCommand.ts @@ -54,6 +54,14 @@ export class CommandTable { } + /** + * Used to render the help command. + * @returns All of the commands in this table. + */ + public getCommands(): InterfaceCommand[] { + return [...this.flattenedCommands.values()]; + } + // We use the argument stream so that they can use stream.rest() to get the unconsumed arguments. public findAMatchingCommand(stream: ArgumentStream) { const tableHelper = (table: CommandLookupEntry, argumentStream: ArgumentStream): undefined|InterfaceCommand => { @@ -112,9 +120,13 @@ export function findCommandTable(name: string export class InterfaceCommand { constructor( - private readonly argumentListParser: IArgumentListParser, + public readonly argumentListParser: IArgumentListParser, private readonly command: ExecutorType, public readonly designator: string[], + /** A short one line summary of what the command does to display alongside it's help */ + public readonly summary: string, + /** A longer description that goes into detail. */ + public readonly description?: string, ) { } @@ -144,11 +156,15 @@ export function defineInterfaceCommand(descri table: string, command: ExecutorType, designator: string[], + summary: string, + description?: string, }) { const command = new InterfaceCommand( description.paramaters, description.command, description.designator, + description.summary, + description.description, ); const table = findCommandTable(description.table); table.internCommand(command); diff --git a/src/commands/interface-manager/MatrixHelpRenderer.ts b/src/commands/interface-manager/MatrixHelpRenderer.ts new file mode 100644 index 00000000..ad7b0bfb --- /dev/null +++ b/src/commands/interface-manager/MatrixHelpRenderer.ts @@ -0,0 +1,61 @@ +/** + * Copyright (C) 2022 Gnuxie + */ + +import { MatrixSendClient } from "../../MatrixEmitter"; +import { BaseFunction, InterfaceCommand } from "./InterfaceCommand"; +import { KeywordParser } from "./ParamaterParsing"; +import { CommandError, CommandResult } from "./Validation"; + +function requiredArgument(argumentName: string): string { + return `<${argumentName}>`; +} + +function keywordArgument(keyword: string): string { + // ahh fuck what about defaults for keys? + return `[--${keyword}]`; +} + +// they should be allowed to name the rest argument... +function restArgument(): string { + return `[...rest]`; +} + +function renderCommandHelp(command: InterfaceCommand): string { + let text = ''; + for (const designator of command.designator) { + text += `${designator} ` + } + for (const description of command.argumentListParser.descriptions) { + text += `${requiredArgument(description.name)} `; + } + const restParser = command.argumentListParser.restParser; + if (restParser !== undefined) { + // not too happy with how keywords are represented here., like there's just the keys with no context smh. + if (restParser instanceof KeywordParser) { + for (const keyword of Object.keys(restParser.description)) { + if (keyword === "allowOtherKeys") { + continue; + } + // ahh fuck what about defaults for keys? + text += `${keywordArgument(keyword)} `; + } + if (restParser.description.allowOtherKeys) { + text += `${restArgument()} `; + } + } else { + text += `${restArgument()} `; + } + } + return text; +} + +// What is really needed is a rendering protocol, that works with bullshit text+html that's really just string building like we're doing here or some other media format +export async function renderHelp(client: MatrixSendClient, commandRoomId: string, event: any, result: CommandResult[], CommandError>): Promise { + const commands = result.ok; + let text = '' + for (const command of commands) { + text += `${renderCommandHelp(command)}\n`; + } + await client.replyNotice(commandRoomId, event, text); +} diff --git a/src/commands/interface-manager/ParamaterParsing.ts b/src/commands/interface-manager/ParamaterParsing.ts index 761aa2f9..0205a053 100644 --- a/src/commands/interface-manager/ParamaterParsing.ts +++ b/src/commands/interface-manager/ParamaterParsing.ts @@ -106,18 +106,15 @@ interface KeywordsDescription { readonly allowOtherKeys: boolean } -interface KeywordPropertyDescription { +interface KeywordPropertyDescription extends ParamaterDescription { readonly isFlag: boolean; - readonly propertyPredicate?: PredicateIsParamater; - readonly name: string, - } // Things that are also needed that are not done yet: // 1) We need to figure out what happens to aliases for keywords.. // 2) We need to sort out the predicates thing. export class KeywordParser extends RestParser { - constructor(private readonly description: KeywordsDescription) { + constructor(public readonly description: KeywordsDescription) { super(); } @@ -127,7 +124,6 @@ export class KeywordParser extends RestParser { */ public parseRest(itemStream: ArgumentStream): CommandResult { const destructable: DestructableRest = { rest: [] }; - // Wrong, we can't use position, we need a stream. while (itemStream.peekItem() !== undefined) { const item = itemStream.readItem(); if (item instanceof Keyword) { @@ -141,7 +137,7 @@ export class KeywordParser extends RestParser { return CommandResult.Ok(property); } else { if (!description.isFlag) { - return CommandError.Result(`An associated argument was not provided for the keyword ${description.name}.`) + return ArgumentParseError.Result(`An associated argument was not provided for the keyword ${description.name}.`, { paramater: description, stream: itemStream }) } return CommandResult.Ok(true); } @@ -206,7 +202,7 @@ class ArgumentListParser implements IArgumentListParser { for (const paramater of descriptions) { if (itemStream.peekItem() === undefined) { // FIXME asap: we need a proper paramater description? - return CommandError.Result(`An argument for the paramater ${paramater.name} was expected but was not provided.`); + return ArgumentParseError.Result(`An argument for the paramater ${paramater.name} was expected but was not provided.`, { paramater, stream: itemStream }); } const item = itemStream.readItem()!; const result = paramater.acceptor.validator(item); @@ -228,6 +224,19 @@ class ArgumentListParser implements IArgumentListParser { } } +export class ArgumentParseError extends CommandError { + constructor( + public readonly paramater: ParamaterDescription, + public readonly stream: ArgumentStream, + message: string) { + super(message) + } + + public static Result(message: string, options: { paramater: ParamaterDescription, stream: ArgumentStream }): CommandResult { + return CommandResult.Err(new ArgumentParseError(options.paramater, options.stream, message)); + } +} + /** * I don't think we should use `union` and it should be replaced by a presentationTypeTranslator * these are specific to applications e.g. imagine you want to resolve an alias or something. diff --git a/src/commands/interface-manager/Validation.ts b/src/commands/interface-manager/Validation.ts index abefe8ff..128edd02 100644 --- a/src/commands/interface-manager/Validation.ts +++ b/src/commands/interface-manager/Validation.ts @@ -90,7 +90,13 @@ export class CommandError { } - public static Result(message: string): CommandResult { + /** + * Utility to wrap the error into a Result. + * @param message The message for the CommandError. + * @param _options This exists so that the method is extensible by subclasses. Otherwise they wouldn't be able to pass other constructor arguments through this method. + * @returns + */ + public static Result(message: string, _options = {}): CommandResult { return CommandResult.Err(new CommandError(message)); } }