-
Notifications
You must be signed in to change notification settings - Fork 31
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
base: main
Are you sure you want to change the base?
Conversation
Replace unresolved `ServerData` with proper `AutocompleteData` in snippet used for autocomplete.
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 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 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,
} 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:
If you are using VS Code with Vim extension, on GNU\Linux you have to launch it from terminal like this: I hope someone will find it useful or figure out how to remove python and vim/emacs dependency. |
Guide for dummies
Replace unresolved
ServerData
with properAutocompleteData
in snippet used for autocomplete.