Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

update snippet for proper autocomplete #6

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

marcjmiller
Copy link

Replace unresolved ServerData with proper AutocompleteData in snippet used for autocomplete.

Replace unresolved `ServerData` with proper `AutocompleteData` in snippet used for autocomplete.
@koutoftimer
Copy link

I'm using very tricky thingy for autocomplete generation that looks like this:

// module Completion {
//     const args: string[] = []
//     const data: AutocompleteData = {} as any
//     const flags: IFlagsConfig = {} as any
//     interface IOpts {
//         value: any,
//         completion?: any,
//         help?: string,
//     }
//     const Config: { [key: string]: IOpts } = {
//         Gym: {
//             value: <CombatStat>'',
//             completion: Object.values(CombatStat)
//         },
//         Limit: {
//             value: <number>100,
//         },
//         University: {
//             value: <UniversityCourse>'',
//             completion: Object.values(UniversityCourse),
//         },
//         Location: {
//             value: <LocationName>'',
//             completion: Object.values(LocationName),
//         }
//     }
// }

export const enum Flag {
    Never = '',
    Gym = 'gym',
    Limit = 'limit',
    Location = 'location',
    University = 'university',
}

const flagsConfig: AutocompleteConfig = [
    ...baseFlagsConfig,
    [Flag.Gym, ''],
    [Flag.Limit, 100],
    [Flag.Location, ''],
    [Flag.University, ''],
]

interface IFlagsConfig extends IBaseFlagsConfig {
    [Flag.Never]: void,
    [Flag.Gym]: CombatStat,
    [Flag.Limit]: number,
    [Flag.Location]: LocationName,
    [Flag.University]: UniversityCourse,
}

export const autocomplete = Arg.wrap((data: AutocompleteData, args: string[]) => {
    const flags = data.flags(flagsConfig) as IFlagsConfig
    switch (args.at(-2)) {
        case flagToArgument(Flag.Gym):
            return Object.values(CombatStat)
        case flagToArgument(Flag.Location):
            return Object.values(LocationName)
        case flagToArgument(Flag.University):
            return Object.values(UniversityCourse)
    }
    return []
})

Commented part should be processed by python script in order to provide uncommented part.

code generation on python
#!/usr/bin/env python3.10
# -*- coding: utf-8 -*-
import re
import sys
from dataclasses import dataclass
from itertools import takewhile


@dataclass
class Flag:
    value: str | None = None
    type: str | None = None
    completion: str | None = None
    before_flags: bool | None = None
    positional_index: int | None = None


def camelcase_to_snakecase(name: str) -> str:
    return '-'.join(map(str.lower, re.findall(r'[A-Z][a-z]*', name)))


def extract_enum_flags(config: dict[str, Flag]):
    return '\n'.join([
        f"    {name} = '{camelcase_to_snakecase(name)}',"
        for name in sorted(config.keys())
    ])


def extract_autocomplete_config(config: dict[str, Flag]) -> str:
    bools = sorted(filter(lambda p: p[1].type == 'boolean', config.items()))
    others = sorted(filter(lambda p: p[1].type != 'boolean', config.items()))
    return '\n'.join([
        f'    [Flag.{name}, {opts.value}],'
        for name, opts in [*bools, *others]
    ])


def extract_interface(config: dict[str, Flag]) -> str:
    return '\n'.join([
        f'    [Flag.{name}]: {opts.type},'
        for name, opts in sorted(config.items())
    ])


def get_completion_case(pair: tuple[str, Flag]) -> str:
    name, flag = pair
    return \
f'''        case flagToArgument(Flag.{name}):
            return {flag.completion}'''


def extract_completion_before_flags(config: dict[str, Flag]) -> str:
    def is_completion_before_flags(pair: tuple[str, Flag]) -> bool:
        return pair[1].before_flags is True and pair[1].completion is not None
    completions = sorted(filter(is_completion_before_flags, config.items()))
    if not completions:
        return ''
    cases = '\n'.join(map(get_completion_case, completions))
    return \
f'''    switch (args.at(-2)) {{
{cases}
    }}
'''


def extract_completion(config: dict[str, Flag]) -> str:
    def is_completion(pair: tuple[str, Flag]) -> bool:
        return pair[1].before_flags is not True and pair[1].completion is not None
    completions = sorted(filter(is_completion, config.items()))
    if not completions:
        return ''
    cases = '\n'.join(map(get_completion_case, completions))
    return \
f'''    switch (args.at(-2)) {{
{cases}
    }}
'''


def get_positional_case(flag: Flag) -> str:
    return \
f'''        if (flags._.length === {flag.positional_index}) {{
            return {flag.completion}
        }}'''


def extract_positional(config: dict[str, Flag]) -> str:
    def is_positional(flag: Flag) -> bool:
        return flag.completion is not None and flag.positional_index is not None
    positions = filter(is_positional, config.values())
    positions = sorted(positions, key=lambda flag: flag.positional_index)
    if not positions:
        return ''
    cases = '\n'.join(map(get_positional_case, positions))
    return \
f'''    if (flags && flags._.length) {{
{cases}
    }}
'''


class Config:
    START = '    const Config: { [key: string]: IOpts } = {'
    END = re.compile(r'^    }\s*$')


class Argument:
    NAME = re.compile(r'^        (?P<name>\w*): {\s*$')
    VALUE = re.compile(r'^            value: \<(?P<type>.*)\>(?P<value>.*)\s*$')
    COMPLETION = re.compile(r'^            completion: (?P<completion>.*)\s*$')
    BEFORE_FLAGS = re.compile(r'^            beforeFlags: (?P<before_flags>.*)\s*$')
    POSITIONAL_INDEX = re.compile(r'^            positionalIndex: (?P<positional_index>.*)\s*$')


def tokenize(selection: str) -> dict[str, Flag]:
    lines = selection.splitlines()
    while lines and not lines[0].startswith(Config.START):
        lines = lines[1:]

    tokens: dict[str, Flag] = dict()
    name = None

    for line in lines:
        if Config.END.match(line):
            break
        match = Argument.NAME.match(line)
        if match:
            name = match.group('name')
            tokens[name] = Flag()
            continue
        match = Argument.VALUE.match(line)
        if match and name:
            tokens[name].value = match.group('value').removesuffix(',')
            tokens[name].type = match.group('type')
            continue
        match = Argument.COMPLETION.match(line)
        if match and name:
            tokens[name].completion = match.group('completion').removesuffix(',')
            continue
        match = Argument.BEFORE_FLAGS.match(line)
        if match and name:
            tokens[name].before_flags = match.group('before_flags').startswith('true')
            continue
        match = Argument.POSITIONAL_INDEX.match(line)
        if match and name:
            tokens[name].positional_index = int(match.group('positional_index').removesuffix(','))

    return tokens


if __name__ == '__main__':
    selection = sys.stdin.read()
    config = tokenize(selection)
    selection_config_only = '\n'.join(takewhile(bool, selection.splitlines()))
    print(f'''{selection_config_only}

export const enum Flag {{
    Never = '',
{extract_enum_flags(config)}
}}

const flagsConfig: AutocompleteConfig = [
    ...baseFlagsConfig,
{extract_autocomplete_config(config)}
]

interface IFlagsConfig extends IBaseFlagsConfig {{
    [Flag.Never]: void,
{extract_interface(config)}
}}

export const autocomplete = Arg.wrap((data: AutocompleteData, args: string[]) => {{
{extract_completion_before_flags(config)}    const flags = data.flags(flagsConfig) as IFlagsConfig
{extract_positional(config)}{extract_completion(config)}    return []
}})
''')

And then, in your main you can do

    const data = Arg.unwrap<IFlagsConfig>(ns.flags(flagsConfig))

with full type awareness, etc.

Small autocomplete library code below:

autocomplete "lib"
export class Arg {
    private static space = ' '
    // static unicodeSpace = '\u2002'
    private static unicodeSpace = '_'
    private static escape(val: string): string {
        return val.replaceAll(Arg.space, Arg.unicodeSpace)
    }
    private static unescape<T>(val: T): T {
        return typeof val === 'string'
            ? val.replaceAll(Arg.unicodeSpace, Arg.space) as any
            : val
    }
    static wrap(autocomplete: (data: AutocompleteData, args: string[]) => any[]) {
        return (data: AutocompleteData, args: string[]) => {
            return autocomplete(data, args).map(Arg.escape)
        }
    }
    static unwrap<T extends IBaseFlagsConfig>(flags: T): T {
        for (const [key, value] of Object.entries(flags)) {
            if (typeof value === 'string') {
                (flags as Record<string, any>)[key] = Arg.unescape(value)
            }
            else if (Array.isArray(value)) {
                (flags as Record<string, any>)[key] = value.map(Arg.unescape)
            }
        }
        return flags
    }
}

enum Flag {
    Tail = 'tail',
    Args = '_',
}

export interface IBaseFlagsConfig {
    [Flag.Args]: string[],
}

export function flagToArgument(flag: string): string {
    return `--${flag}`
}

export const baseFlagsConfig: AutocompleteConfig = [
    [Flag.Tail, false],
]

export interface IOpts {
    value: any,
    completion?: any,
    help?: string,
    beforeFlags?: boolean,
    positionalIndex?: number,
}
provides code to escape/unescape strings with spaces plus some basics/commons/helpers.

Vim/Emacs users may find this approach handy because their editors can feed part of the text file into an external program (python script above) to generate some code for them.

Vim usage is to:

  • uncomment module block, make required modivications and select it with all generated code above it (as it will be erased/replaced with new one)
  • then do !!<python sript name> to execute text filter on selection

If you are using VS Code with Vim extension, on GNU\Linux you have to launch it from terminal like this: code . otherwise environment variables (with local PATH) will not be seen (I suppose you will install the script into ~/.local/bin)

I hope someone will find it useful or figure out how to remove python and vim/emacs dependency.

kateract pushed a commit to kateract/bitburner-scripts that referenced this pull request May 7, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants