diff --git a/castervoice/lib/config/config_toml.py b/castervoice/lib/config/config_toml.py index f10d6e72e..e0a6b07ee 100644 --- a/castervoice/lib/config/config_toml.py +++ b/castervoice/lib/config/config_toml.py @@ -5,11 +5,15 @@ class TomlConfig(BaseConfig): def __init__(self, config_path): - super(TomlConfig, self) + super(TomlConfig, self).__init__() self._config_path = config_path def save(self): + if not self._config_path: + return utilities.save_toml_file(self._config, self._config_path) def load(self): + if not self._config_path: + return {} self._config = utilities.load_toml_file(self._config_path) diff --git a/castervoice/rules/apps/editor/sublime.py b/castervoice/rules/apps/editor/sublime.py index 2dcf190b6..205d98b45 100644 --- a/castervoice/rules/apps/editor/sublime.py +++ b/castervoice/rules/apps/editor/sublime.py @@ -37,9 +37,9 @@ class SublimeRule(MappingRule): "(select | sell) all": R(Key("c-a")), "(select | sell) scope []": - R(Key("cs-space")), + R(Key("cs-space")*Repeat(extra="n2")), "(select | sell) brackets []": - R(Key("cs-m")), + R(Key("cs-m")*Repeat(extra="n2")), "(select | sell) indent": R(Key("cs-j")), # @@ -49,15 +49,21 @@ class SublimeRule(MappingRule): R(Key("a-enter")), "replace": R(Key("c-h")), + "replace all": + R(Key("ca-enter")), + "paste from history": + R(Key("c-k,c-v")), "edit lines": R(Key("cs-l")), "edit next []": R(Key("c-d/10"))*Repeat(extra="n3"), + "edit only next []": + R(Key("c-k,c-d/10"))*Repeat(extra="n3"), "edit up []": R(Key("ac-up"))*Repeat(extra="n3"), "edit down []": R(Key("ac-down"))*Repeat(extra="n3"), - "edit all": + "edit all": R(Key("a-f3")), # "transform upper": @@ -69,10 +75,18 @@ class SublimeRule(MappingRule): R(Key("c-g/10") + Text("%(ln1)s") + Key("enter")), " [line] [by ]": R(Function(navigation.action_lines)), + "[move] line down []": + R(Key("cs-down") * Repeat(extra='n3')), + "[move] line up []": + R(Key("cs-up") * Repeat(extra='n3')), + # "go to file": R(Key("c-p")), "go to []": R(Key("c-p") + Text("%(dict)s" + "%(filetype)s") + Key("enter")), + "file back []": + R(Key("c-p") + Key("down")*Repeat(extra="n2") + Key("enter")), + # "go to word": R(Key("c-semicolon")), "go to symbol": @@ -88,6 +102,13 @@ class SublimeRule(MappingRule): "command pallette": R(Key("cs-p")), # + "go back []": + R(Key("a-minus")*Repeat(extra="n2")), + "go forward []": + R(Key("a-plus")*Repeat(extra="n2")), + "next modification":R(Key("c-dot")), + "previous modification":R(Key("c-comma")), + # "fold": R(Key("cs-lbracket")), "unfold": @@ -103,10 +124,12 @@ class SublimeRule(MappingRule): R(Key("c-k, c-b")), "show key bindings": R(Key("f10, p, right, k")), + "show at center": + R(Key("c-k,c-c")), "zoom in []": - R(Key("c-equal")), + R(Key("c-equal")*Repeat(extra="n2")), "zoom out []": - R(Key("c-minus")), + R(Key("c-minus")*Repeat(extra="n2")), # "(set | add) bookmark": R(Key("c-f2")), @@ -117,8 +140,15 @@ class SublimeRule(MappingRule): "clear bookmarks": R(Key("cs-f2")), # - "build it": - R(Key("c-b")), + "set mark": R(Key("c-k,c-space")), + "select mark": R(Key("c-k,c-a")), + "swap with mark": R(Key("c-k,c-x")), + "delete mark":R(Key("c-k,c-w")), + # + "build it": R(Key("c-b")), + "build with": R(Key("cs-b")), + "build ":R(Key("c-s,a-%(nth)s,c-b")), + "build [] last":R(Key("c-s,a-1") + Key("c-pageup")*Repeat(extra="nth") + Key("c-b")), # "record macro": R(Key("c-q")), @@ -135,6 +165,9 @@ class SublimeRule(MappingRule): R(Key("c-pgup")), " tab": R(Key("a-%(nth)s")), + "[] last tab": + R(Key("a-1") + Key("c-pageup")*Repeat(extra="nth")), + "column ": R(Key("as-%(cols)s")), "focus ": @@ -142,8 +175,10 @@ class SublimeRule(MappingRule): "move ": R(Key("cs-%(panel)s")), # - "open terminal": + "open terminal": R(Key("cs-t")), + "open console": + R(Key("c-`")), } extras = [ Dictation("dict"), @@ -188,8 +223,9 @@ class SublimeRule(MappingRule): "n2": 1, "n3": 1, "file type": "", + "nth":"1", } def get_rule(): - return SublimeRule, RuleDetails(name="sublime", executable="sublime_text", title="Sublime Text") + return SublimeRule, RuleDetails(name="sublime", executable="sublime_text", title="Sublime Text") \ No newline at end of file diff --git a/castervoice/rules/apps/editor/sublime_rules/Function_like_utilities.py b/castervoice/rules/apps/editor/sublime_rules/Function_like_utilities.py new file mode 100644 index 000000000..f95505772 --- /dev/null +++ b/castervoice/rules/apps/editor/sublime_rules/Function_like_utilities.py @@ -0,0 +1,66 @@ +import inspect + +import six + +############################## UTILITIES TO WRAP FUNCTION CALLS ############################## +# +# these functions are based on the dragonfly implementation for Function action, which does not +# support returning the return value of the function called +# +############################################################################################## + + +def get_signature_arguments(function): + """Utility to extract the name of the arguments of the function signature + + Args: + function (Callable): the function in question + + Returns: + Tuple[Set[str],bool]: tuple consistent of + - the names of the arguments + - boolean indicating the function does NOT accept **kwargs + """ + # pylint: disable=no-member + if six.PY2: + # pylint: disable=deprecated-method + argspec = inspect.getargspec(function) + else: + argspec = inspect.getfullargspec(function) + args, varkw = argspec[0], argspec[2] + filter_keywords = not varkw + valid_keywords = set(args) + return valid_keywords, filter_keywords + + +def get_only_proper_arguments(function,data): + valid_keywords, filter_keywords = get_signature_arguments(function) + arguments = data.copy() + if filter_keywords: + invalid_keywords = set(arguments.keys()) - valid_keywords + for key in invalid_keywords: + del arguments[key] + return arguments + +def rename_data(data,remap_data): + if isinstance(data, dict): + renamed = data.copy() + else: + raise TypeError("evaluate_function received instead of a dictionary " + type(data) + " in data") + + # Remap specified names. + for old_name, new_name in remap_data.items(): + if old_name in data: + renamed[new_name] = renamed.pop(old_name) + return renamed + + +def evaluate_function(function,data = {},remap_data = {}): + renamed = rename_data(data,remap_data) + arguments = get_only_proper_arguments(function,renamed) + try: + return function(**arguments) + except Exception as e: + raise + + diff --git a/castervoice/rules/apps/editor/sublime_rules/__init__.py b/castervoice/rules/apps/editor/sublime_rules/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/castervoice/rules/apps/editor/sublime_rules/snippet_generation_support.py b/castervoice/rules/apps/editor/sublime_rules/snippet_generation_support.py new file mode 100644 index 000000000..512f0b19e --- /dev/null +++ b/castervoice/rules/apps/editor/sublime_rules/snippet_generation_support.py @@ -0,0 +1,219 @@ +import re + +from castervoice.rules.apps.editor.sublime_rules.Function_like_utilities import get_signature_arguments,get_only_proper_arguments,rename_data,evaluate_function +from castervoice.rules.apps.editor.sublime_rules.sublime_communication_support import send_sublime,send_snippet,send_quick_panel + + +def generate_snippet_text(snippet = "",data = {}): + """Generate snippet text from the given snippet(generator) and spoken extras + + Args: + snippet (Union[str,List[str],Callable[...,str]]): The snippet (generator) to generate text. Can be one of + - a raw string containing the snippet text + - a list of strings, containing variations of the same snippet + - a callable that will generate the snippet, optionally using the spoken data + + data (dict, optional): The spoken extras that would be passed + - parameters to a callable or + - From which the index `n` will be retrieved to pick from the list (1-indexed) + + Returns: + str: the final text of the snippet + dict: the key-values pairs of the data used by the callable to generate the snippet + + Raises: + TypeError: Description + """ + if isinstance(snippet,str): + snippet_text = snippet + extra_data = {} + elif isinstance(snippet,list): + if any(not isinstance(x,str) for x in snippet): + raise TypeError("In generate_snippet_text snippet must be a string or list of strings or callable!Received ",type(snippet)) + n = data.get("n",1) + snippet_text = snippet[n-1] + extra_data = dict(n=n) + elif callable(snippet): + extra_data = get_only_proper_arguments(snippet, data) + snippet_text = evaluate_function(snippet,data) + else: + raise TypeError("In generate_snippet_text snippet must be a string or list of strings or callable!Received ",type(snippet)) + # snippet_text = filter_snippet_text(snippet_text) + return snippet_text,extra_data + + +############################## SNIPPET STATE ############################## + +initial_snippet_state = { + "snippet_text":"", + "snippet":[], + "snippet_parameters":{}, + "extra_data":{}, + "stack":[], + "remap_data":{}, +} + + +try : + snippet_state +except : + snippet_state = initial_snippet_state.copy() + +def snippet_log(clear_those_not_set = True,**kwargs): + global snippet_state + if clear_those_not_set: + snippet_state.update(initial_snippet_state) + snippet_state.update({k:v for k,v in kwargs.items() if k in snippet_state}) + + + + + + + + + +def insert_snippet(snippet,data={},snippet_parameters = {},additional_log = {}): + """Given a snippet and the corresponding spoken data( after renaming ) generates the snippet text + inserts it and updates the snippets state + + Args: + snippet (Union[str,List[str],Callable[...,str]]): The snippet (generator) to generate text. Can be one of + - a raw string containing the snippet text + - a list of strings, containing variations of the same snippet + - a callable that will generate the snippet, optionally using the spoken data + data (dict, optional): The spoken extras that would be passed + - parameters to a callable or + - From which the index `n` will be retrieved to pick from the list (1-indexed) + snippet_parameters (Union[dict,Callable[...,dict]], optional): parameters to be sent along with a snippet + In a manner similar to environmental variables( like `$SELECTION`). It can either be + - a dictionary containing the keys and values as raw strings + - a callable that accepts two parameters + - `snippet_text` and + - `data` + and returns the final dictionary + additional_log (dict, optional): additional parameters to be logged in the snippets state ( for example + which parameters had to be renamed) + + Raises: + TypeError: Description + """ + snippet_text,extra_data = generate_snippet_text(snippet,data) + if callable(snippet_parameters): + snippet_parameters = evaluate_function(snippet_parameters,dict(snippet=snippet_text)) + if not isinstance(snippet_parameters,dict): + raise TypeError("In insert_snippet snippet_parameters must be a dictionary or a callable returning a dictionary") + send_snippet(snippet_text,**snippet_parameters) + data_log = dict( + snippet_parameters = snippet_parameters, + snippet_text = snippet_text, + snippet = snippet, + extra_data = extra_data, + ) + data_log.update(additional_log) + snippet_log(**data_log) + + + +def apply_single_transformation(snippet_text,transformation): + if not isinstance(snippet_text,str): + raise TypeError("snippet_text must be a string instead received ",snippet_text) + if isinstance(transformation,tuple): + args = transformation[:2] + (snippet_text,) + transformation[2:] # inject the snippet_text in the third position + return re.sub(*args) + elif callable(transformation): + return transformation(snippet_text) + else: + raise TypeError("transformation must be callable or that can be passed to `re.sub`,received",transformation) + + + +def transform_snippet(snippet_text,transformation): + transformation = transformation if isinstance(transformation,list) else [transformation] + for t in transformation: + snippet_text = apply_single_transformation(snippet_text,t) + return snippet_text + + + + + + + + +################################################################ + +# works but is no longer needed, ignored for the time being ! +def filter_snippet_text(snippet_text): + ''' + Filter out numerical placeholders that have their_default value in two locations + For example: + ${1:data} = ${1:data} + $2 + will not be accepted by sublime! + Precondition for these to work is that those default values are the same! + ''' + def clean(text): + temporary,skip,l = [],0,len(text) + for i,(c,n) in enumerate(zip(text, text[1:])): + if skip and i!=l-2: + skip = skip - 1 + continue + elif not skip and c=="\\" and n in ["\\","$","{","}"]: + temporary += [" "," "] + skip = 1 + elif i==l-2: + if not skip: temporary.append(c) + temporary.append(n) + else: + temporary.append(c) + return "".join(temporary) + + def bracket_match(text): + temporary,l,mapping = [],len(text),{} + for i,c in enumerate(text): + if c=="{" and i!=0 and text[i-1]=="$": + temporary.append(i) + elif c=="}": + if temporary: + m = len(temporary) + mapping[temporary.pop()] = (i,m) + return mapping + + + + + + def find_duplicates(text,mapping): + temporary={} + pattern = re.compile(r"\$\{(\d+):") + l = pattern.finditer(text) + for m in l: + number = m.group(1) + beginning = m.start(0) + 1 + ending,depth = mapping[beginning] + try : + temporary[number].append((beginning,ending,depth)) + except : + temporary[number] = [(beginning,ending,depth)] + return {k:v for k,v in temporary.items() if len(v)!=1} + + + + def eliminate_duplicates(original,duplicates): + temporary = list(original) + print(original,duplicates) + c = [(d,k,b,e) for k,v in duplicates.items() for b,e,d in v[1:]];c = sorted(c,reverse=True) + # for k,v in duplicates.items(): + if True: + for d,k,b,e in c: + for i in range(b,e+1): + temporary[i] = "" + for i,letter in enumerate(k): + temporary[b+i] = letter + return "".join(temporary) + + text_after_escaping = clean(snippet_text) + bracket_mapping = bracket_match(text_after_escaping) + duplicates = find_duplicates(text_after_escaping,bracket_mapping) + return eliminate_duplicates(snippet_text,duplicates) + diff --git a/castervoice/rules/apps/editor/sublime_rules/snippet_utilities.py b/castervoice/rules/apps/editor/sublime_rules/snippet_utilities.py new file mode 100644 index 000000000..ab03c8e3f --- /dev/null +++ b/castervoice/rules/apps/editor/sublime_rules/snippet_utilities.py @@ -0,0 +1,131 @@ +import re + +from dragonfly import Choice + +from castervoice.rules.apps.editor.sublime_rules.Function_like_utilities import get_signature_arguments +from castervoice.lib.merge.state.short import R +from castervoice.lib.merge.additions import IntegerRefST + +try : + from sublime_rules.sublime_snippets import Snippet +except : + from castervoice.rules.apps.editor.sublime_rules.sublime_snippets import Snippet + +def placeholder(field,default = ""): + """Utility to generate snippet code for placeholders fields at runtime + + placeholder(1) = $1 + placeholder(1,"data") = ${1:data} + placeholder((1,2),"data") = ${1:${2:data}} + placeholder((1,"INTERESTING"),"data") = ${1:${INTERESTING:data}} + + Args: + field (Union[convertible to string,Tuple[convertible to string]]): field index + default (str, optional): default value + + Returns: + str: raw placeholder text + """ + if isinstance(field,tuple): + tail = field[1:] if len(field) > 2 else field[1] + return "${" + str(field[0]) + ":" + placeholder(tail,default) + "}" + else: + if not default: + return "$" + str(field) + else: + return "${" + str(field) + ":" + str(default) + "}" + +def regular(varname,regex,format_string,options = "",ignore_case=False,replace_all=False,ignore_new_lines=True): + """Utility for generating snippet code for regular expression substitution.Produces + + ${var_name/regex/format_string/options} or + ${var_name/regex/format_string} + + all arguments must be able to get casted into str() + + Args: + varname (str): The variable name for example 1,2 + regex (str): Perl style regular expression + format_string (str): Perl style format string + options (str, optional): optional can take a combination of the following values + - "i" : case insensitive + - "g" : replace all appearances + - "m" : do not ignore new lines + ignore_case(bool): set True for a case insensitive regular expression(equivalent to options="i") + replace_all(bool): set True for replacing all occurrences of regular expression(equivalent to options="g") + ignore_new_lines(bool): set False to not ignore new lines in regular expression(equivalent to options="m") + + Returns: str + """ + if ignore_case and "i" not in options: + options = options + "i" + if replace_all and "g" not in options: + options = options + "g" + if not ignore_new_lines and "m" not in options: + options = options + "m" + arg = (varname,regex,format_string) + ((options,) if options else ()) + return "${" + "/".join(map(str, arg)) + "}" + +def load_snippets(snippets,extras = [], defaults = {}): + """Utility in order to decorate grammars to quickly load snippets from a raw dictionary format + + Args: + snippets (TYPE): Description + extras (list, optional): Description + defaults (dict, optional): Description + + Returns: + TYPE: Description + + Raises: + TypeError: Description + """ + mapping,l,additional_extras = {},[],[] + for k,v in snippets.items(): + if not isinstance(k,str): + raise TypeError("snippet keys must be strings, instead received",k) + if isinstance(v,str): + mapping[k] = R(Snippet(v)) + elif isinstance(v,list): + mapping[k + " "] = R(Snippet(v)); l.append(len(v)) + elif callable(v): + names,_ = get_signature_arguments(v) + if "n" in names: + l.append(10) + mapping[k] = R(Snippet(v)) + elif isinstance(v,dict): + if any( not isinstance(x,str) or not isinstance(y,str) for x,y in v.items()): + raise TypeError("the dictionary should be consisting only of strings, instead received",v) + extra_names = {x.group(1) for x in re.finditer(r"<(.+)>",k)} + assert len(extra_names) == 1,"when the value is a dictionary, must only contain one extra, instead received " + str(len(extra_names)) + name = list(extra_names)[0] + additional_extras.append(Choice(name,v)) + mapping[k] = R(Snippet(lambda _snippet_internal:_snippet_internal,remap_data = {name:"_snippet_internal"})) + # mapping[k] = R(Snippet('%({0})s'.format(name))) + else: + raise TypeError("") + + def decorator(c): + get_extra_names = lambda p: {x.name for x in p if hasattr(x,"name")} + if get_extra_names(extras + additional_extras) & get_extra_names(c.extras): + raise ValueError("Overlapping extra_names between load_snippets decorator and class", + get_extra_names(extras + additional_extras) & get_extra_names(c.extras)) + + c.mapping.update(mapping) + c.extras.extend(extras + additional_extras) + c.defaults.update(defaults) + if l: + e = next((x for x in c.extras if isinstance(x,IntegerRefST)),None) + if e: + c.extras.append(IntegerRefST("n",1,max([e._rule._element._max,max(l)]))) + else: + c.extras.append(IntegerRefST("n",1,max(l))) + + return c + return decorator + + +regex = { + +} + diff --git a/castervoice/rules/apps/editor/sublime_rules/sublime.py b/castervoice/rules/apps/editor/sublime_rules/sublime.py new file mode 100644 index 000000000..dad42d78b --- /dev/null +++ b/castervoice/rules/apps/editor/sublime_rules/sublime.py @@ -0,0 +1,231 @@ +from dragonfly import Function, Dictation, Choice, MappingRule,Repeat + +from castervoice.lib import navigation +from castervoice.lib.actions import Key, Text +from castervoice.lib.ctrl.mgr.rule_details import RuleDetails +from castervoice.lib.merge.additions import IntegerRefST +from castervoice.lib.merge.state.short import R +from castervoice.lib.temporary import Store, Retrieve + + +class SublimeRule(MappingRule): + mapping = { + "new file": + R(Key("c-n")), + "new window": + R(Key("cs-n")), + "open file": + R(Key("c-o")), + "open folder": + R(Key("f10, f, down:2, enter")), + "open recent": + R(Key("f10, f, down:3, enter")), + "save as": + R(Key("cs-s")), + # + "comment line": + R(Key("c-slash")), + "comment block": + R(Key("cs-slash")), + "outdent lines": + R(Key("c-lbracket")), + "join lines": + R(Key("c-j")), + "match bracket": + R(Key("c-m")), + # + "(select | sell) all": + R(Key("c-a")), + "(select | sell) scope []": + R(Key("cs-space")*Repeat(extra="n2")), + "(select | sell) brackets []": + R(Key("cs-m")*Repeat(extra="n2")), + "(select | sell) indent": + R(Key("cs-j")), + # + "find": + R(Key("c-f")), + "get all": + R(Key("a-enter")), + "replace": + R(Key("c-h")), + "replace all": + R(Key("ca-enter")), + "paste from history": + R(Key("c-k,c-v")), + "edit lines": + R(Key("cs-l")), + "edit next []": + R(Key("c-d/10"))*Repeat(extra="n3"), + "edit only next []": + R(Key("c-k,c-d/10"))*Repeat(extra="n3"), + "edit up []": + R(Key("ac-up"))*Repeat(extra="n3"), + "edit down []": + R(Key("ac-down"))*Repeat(extra="n3"), + "edit all": + R(Key("a-f3")), + # + "transform upper": + R(Key("c-k, c-u")), + "transform lower": + R(Key("c-k, c-l")), + # + "line ": + R(Key("c-g/10") + Text("%(ln1)s") + Key("enter")), + " [line] [by ]": + R(Function(navigation.action_lines)), + "[move] line down []": + R(Key("cs-down") * Repeat(extra='n3')), + "[move] line up []": + R(Key("cs-up") * Repeat(extra='n3')), + # + "go to file": + R(Key("c-p")), + "go to []": + R(Key("c-p") + Text("%(dict)s" + "%(filetype)s") + Key("enter")), + "file back []": + R(Key("c-p") + Key("down")*Repeat(extra="n2") + Key("enter")), + # + "go to word": + R(Key("c-semicolon")), + "go to symbol": + R(Key("c-r")), + "go to [symbol in] project": + R(Key("cs-r")), + "go to that": + R(Store() + Key("cs-r") + Retrieve() + Key("enter")), + "find that in project": + R(Store() + Key("cs-f") + Retrieve() + Key("enter")), + "find that": + R(Store() + Key("c-f") + Retrieve() + Key("enter")), + "command pallette": + R(Key("cs-p")), + # + "go back []": + R(Key("a-minus")*Repeat(extra="n2")), + "go forward []": + R(Key("a-plus")*Repeat(extra="n2")), + "next modification":R(Key("c-dot")), + "previous modification":R(Key("c-comma")), + # + "fold": + R(Key("cs-lbracket")), + "unfold": + R(Key("cs-rbracket")), + "unfold all": + R(Key("c-k, c-j")), + "fold [level] ": + R(Key("c-k, c-%(n2)s")), + # + "full screen": + R(Key("f11")), + "toggle side bar": + R(Key("c-k, c-b")), + "show key bindings": + R(Key("f10, p, right, k")), + "show at center": + R(Key("c-k,c-c")), + "zoom in []": + R(Key("c-equal")*Repeat(extra="n2")), + "zoom out []": + R(Key("c-minus")*Repeat(extra="n2")), + # + "(set | add) bookmark": + R(Key("c-f2")), + "next bookmark": + R(Key("f2")), + "previous bookmark": + R(Key("s-f2")), + "clear bookmarks": + R(Key("cs-f2")), + # + "set mark": R(Key("c-k,c-space")), + "select mark": R(Key("c-k,c-a")), + "swap with mark": R(Key("c-k,c-x")), + "delete mark":R(Key("c-k,c-w")), + # + "build it": R(Key("c-b")), + "build with": R(Key("cs-b")), + "build ":R(Key("c-s,a-%(nth)s,c-b")), + "build [] last":R(Key("c-s,a-1") + Key("c-pageup")*Repeat(extra="nth") + Key("c-b")), + # + "record macro": + R(Key("c-q")), + "play [back] macro []": + R(Key("cs-q/10")), + "(new | create) snippet": + R(Key("ac-n")), + # + "close tab": + R(Key("c-w")), + "next tab": + R(Key("c-pgdown")), + "previous tab": + R(Key("c-pgup")), + " tab": + R(Key("a-%(nth)s")), + "[] last tab": + R(Key("a-1") + Key("c-pageup")*Repeat(extra="nth")), + + "column ": + R(Key("as-%(cols)s")), + "focus ": + R(Key("c-%(panel)s")), + "move ": + R(Key("cs-%(panel)s")), + # + "open terminal": + R(Key("cs-t")), + "open console": + R(Key("c-`")), + } + extras = [ + Dictation("dict"), + IntegerRefST("ln1", 1, 1000), + IntegerRefST("ln2", 1, 1000), + IntegerRefST("n2", 1, 9), + IntegerRefST("n3", 1, 21), + Choice("action", navigation.actions), + Choice( + "nth", { + "first": "1", + "second": "2", + "third": "3", + "fourth": "4", + "fifth": "5", + "sixth": "6", + "seventh": "7", + "eighth": "8", + "ninth": "9", + }), + Choice("cols", { + "one": "1", + "two": "2", + "three": "3", + "grid": "5", + }), + Choice("panel", { + "one": "1", + "left": "1", + "two": "2", + "right": "2", + }), + Choice("filetype", { + "pie | python": "py", + "mark [down]": "md", + "tech": "tex", + "tommel": "toml", + }), + ] + defaults = { + "ln2": "", + "n2": 1, + "n3": 1, + "file type": "", + "nth":"1", + } + + +def get_rule(): + return SublimeRule, RuleDetails(name="sublime new", executable="sublime_text", title="Sublime Text") \ No newline at end of file diff --git a/castervoice/rules/apps/editor/sublime_rules/sublime_communication_support.py b/castervoice/rules/apps/editor/sublime_rules/sublime_communication_support.py new file mode 100644 index 000000000..07a9597d2 --- /dev/null +++ b/castervoice/rules/apps/editor/sublime_rules/sublime_communication_support.py @@ -0,0 +1,95 @@ +import json +import os +import platform +import re +import subprocess + +from dragonfly import RunCommand + + +def validate_subl(): + if platform.system() != 'Windows': + return "subl" + try: + subprocess.check_call(["subl", "-h"],stdout=subprocess.PIPE,stderr=subprocess.PIPE) # For testing purposes you can invalidate to trigger failure + return "subl" + except Exception as e: + try : + subprocess.check_call(["C:\\Program Files\\Sublime Text 3\\subl", "-h"],stdout=subprocess.PIPE,stderr=subprocess.PIPE) + print("Resorting to C:\\Program Files\\Sublime Text 3\\subl.exe") + return "C:\\Program Files\\Sublime Text 3\\subl" + except : + print("Sublime Text 3 `subl` executable was not in the Windows path") + if not os.path.isdir(r'C:\\Program Files\\Sublime Text 3'): + print("And there is no C:\\Program Files\\Sublime Text 3 directory to fall back to!") + else: + print("And it was not found under C:\\Program Files\\Sublime Text 3") + print("Please add `subl` to the path manually") + return "subl" + +subl = validate_subl() + + +def send_sublime(command,parameters = {},synchronous = True): + """send any sublime command with arbitrary parameters + + Args: + command (str): the name of the command to execute + parameters (dict, optional): the parameters to pass to the command, must be json serializable + synchronous (bool, optional): whether the command should be executed in a synchronous manner + + """ + if not isinstance(command,str): + raise TypeError("command must be a string instead received ",command) + if not isinstance(parameters,dict): + raise TypeError("parameters must be a dict instead received ",parameters) + try : + parameters = json.dumps(parameters) + except : + raise TypeError("parameters must be json serializable, instead received ",parameters) + RunCommand([subl,"-b", "--command",command + " " + parameters],synchronous = synchronous).execute() + +def send_snippet(contents,**kw): + """primitive to transmit the snippet to be inserted + This only takes care of sending the raw snippet text and parameters + not generating it or doing the bookkeeping for the variance system + + Args: + contents (str): the contents/text of this snippet to be inserted + **kw: the raw snippet parameters passed as keyword arguments + + """ + if not isinstance(contents,str): + raise TypeError("contents must be a string instead received ",contents) + try : + json.dumps(kw) + except : + raise TypeError("snippet parameters must be json serializable, instead received",kw) + kw["contents"] = contents + send_sublime("insert_snippet",kw) + +def send_quick_panel(items): + """displaying a list of choices in the quick panel and executed different action depending + on what the user chose + + Args: + items (TYPE): an iterable of tuples representing a choice and consisting of three parts + - caption (str): the text displayed to the user + - command (str): the name of the command to execute, if this item is chosen + - args (dict): the parameters to pass to the command, must be json serializable + + """ + result = [] + for caption,command,args in items: + if not isinstance(caption,str): + raise TypeError("caption must be a string instead received ",caption) + if not isinstance(command,str): + raise TypeError("command must be a string instead received ",command) + if not isinstance(args,dict): + raise TypeError("args must be a dict instead received ",args) + try : + json.dumps(args) + except : + raise TypeError("args must be json serializable, received ",args) + result.append(dict(caption=caption,command=command,args=args)) + send_sublime("quick_panel",dict(items=result)) diff --git a/castervoice/rules/apps/editor/sublime_rules/sublime_snippet_control.py b/castervoice/rules/apps/editor/sublime_rules/sublime_snippet_control.py new file mode 100644 index 000000000..c36c5dd99 --- /dev/null +++ b/castervoice/rules/apps/editor/sublime_rules/sublime_snippet_control.py @@ -0,0 +1,193 @@ +import json +import os + +from copy import deepcopy + + +from dragonfly import ( + MappingRule, Choice, Dictation, Grammar, + Repeat, Function,RunCommand,RecognitionObserver +) +from dragonfly.engines.backend_text.engine import TextInputEngine + + +from castervoice.lib import settings, utilities, context, contexts +from castervoice.lib.actions import Key, Text +from castervoice.lib.const import CCRType +from castervoice.lib.context import AppContext +from castervoice.lib.merge.additions import IntegerRefST +from castervoice.lib.merge.state.short import R +from castervoice.lib.merge.selfmod.selfmodrule import BaseSelfModifyingRule +from castervoice.lib.ctrl.mgr.rule_details import RuleDetails + + +try : + from sublime_rules.sublime_snippets import ( + Snippet,SnippetVariant,DisplaySnippetVariants,DisplayMultipleSnippetVariants,snippet_state + ) +except ImportError: + from castervoice.rules.apps.editor.sublime_rules.sublime_snippets import ( + Snippet,SnippetVariant,DisplaySnippetVariants,DisplayMultipleSnippetVariants,snippet_state + ) + + + +initial = { + "variant ": + R(Key("c-z") + SnippetVariant(n="n")), + "display variants": + R(Key("c-z") + DisplaySnippetVariants()), +} + +try : + last_state,last_rule,meaningful +except : + last_state = snippet_state.copy() + last_rule = None + meaningful = False + +try : + engine # upon reload keep the old engine and just to be sure clean it up + for grammar in engine.grammars: + grammar.unload() +except : + engine = TextInputEngine() + +class SublimeSnippetControllRule(BaseSelfModifyingRule): + last = None + def __init__(self, *args, **kwargs): + SublimeSnippetControllRule.last = self + super(SublimeSnippetControllRule, self).__init__(None,"sublime snippet control") + # super(SublimeSnippetControllRule, self).__init__(os.path.join(settings.SETTINGS["paths"]["USER_DIR"],"nothing.toml"),"sublime snippet additional control") + + def rename(self,extra_name): + return snippet_state["remap_data"].get(extra_name,extra_name) + + def get_last(self,field_name): + rule = last_rule + if type(last_rule).__name__.startswith("Repeat"): + rule = last_rule.extras[1].children[0].rule + if hasattr(rule,"_smr_" + field_name): + return getattr(rule,"_smr_" + field_name) + if hasattr(rule,field_name): + return getattr(rule,field_name) + else: + raise ValueError("Problem inside sublime snippet control, the last rule was " + str(rule)) + # return grammars_with_snippets[last_rule][field_name] + + def _deserialize(self): + global meaningful + self._smr_mapping = {} + self._smr_extras = [] + self._smr_defaults = {} + names = snippet_state["extra_data"].keys() # List[str] + snippet = snippet_state["snippet"] # Union[str,List[str],Callable] + if last_rule: + meaningful = True + default = self.get_last("defaults") + if isinstance(snippet,str): + self._smr_mapping = initial.copy() + self._smr_extras = [IntegerRefST("n",1,10)] + self._smr_defaults = {} + meaningful = False + elif isinstance(snippet,list): + self._smr_mapping = initial.copy() + self._smr_extras = [IntegerRefST("n",1,len(snippet) + 1)] + self._smr_defaults = {} + elif callable(snippet): + for e in self.get_last("extras"): # Element + final_name = self.rename(e.name) + if final_name in names: + self._smr_mapping["variant <"+e.name+">"] = R(Key("c-z") + SnippetVariant(**{e.name:final_name})) + self._smr_extras.append(e) + if isinstance(e,(Choice)) and e._extras is None: + spoken_name = final_name.upper() if len(final_name) == 1 else final_name + all_options = list(e._choices.values()) + ([default[e.name]] if e.name in default else []) + self._smr_mapping["display "+spoken_name+" variant"] = R( + Key("c-z") + DisplaySnippetVariants(final_name,all_options) + ) + if isinstance(e,IntegerRefST): + spoken_name = final_name.upper() if len(final_name) == 1 else final_name + all_options = list(range(e._rule._element._min,e._rule._element._max)) + self._smr_mapping["display "+spoken_name+" variant"] = R( + Key("c-z") + DisplaySnippetVariants(final_name,all_options) + ) + else: + meaningful = True + self._smr_mapping = initial.copy() + self._smr_extras = [IntegerRefST("n",1,10)] + self._smr_defaults = {} + meaningful = False + + + + + def process_recognition(self,node): + words,successful = node.words(),{} # List[String],Dict[String,Any] + for e in self._smr_extras: + class LocalRule(MappingRule): + mapping = { + "variant <"+e.name+">": + Function(lambda **kwargs: + successful.update({self.rename(k):v for k,v in kwargs.items() if k not in ["_node","_grammar","_rule"] })) + } + extras = [e] + grammar = Grammar(e.name,engine=engine) + grammar.add_rule(LocalRule()) + grammar.load() + try : + engine.mimic(words) + except : + pass + grammar.unload() + assert successful or not "".join(words).strip().startswith("variant"),"Successful " + str(successful) + " words " + words + if len(successful)==1 or not "".join(words).strip().startswith("variant"): + MappingRule.process_recognition(self,node) + else: + (Key("c-z") + DisplayMultipleSnippetVariants(successful)).execute() + + + + def _refresh(self,rule = None,*args): + global last_state,last_rule + if type(rule).__name__ == "SublimeSnippetControllRule": + last_state = snippet_state.copy() + return + if last_state != snippet_state and rule: + last_state = snippet_state.copy() + last_rule=rule + self.reset() + + +class Observer(RecognitionObserver): + """docstring for Observer""" + # last = None + def __init__(self, *args, **kw): + super(Observer, self).__init__(*args, **kw) + Observer.last = self + + def on_post_recognition(self, node,words, rule): + + if Observer.last is not self: + self.unregister() + return + + if SublimeSnippetControllRule.last: + SublimeSnippetControllRule.last._refresh(rule,words) +# data + +observer = Observer() +observer.register() + + + + + +def get_rule(): + return SublimeSnippetControllRule, RuleDetails(name="sublime snippet control", executable=["sublime_text"],function_context=lambda: meaningful) + + + + + + diff --git a/castervoice/rules/apps/editor/sublime_rules/sublime_snippets.py b/castervoice/rules/apps/editor/sublime_rules/sublime_snippets.py new file mode 100644 index 000000000..d641b61c0 --- /dev/null +++ b/castervoice/rules/apps/editor/sublime_rules/sublime_snippets.py @@ -0,0 +1,274 @@ +import json +import re + + + +from copy import deepcopy + +from dragonfly import RunCommand +from dragonfly.actions.action_base import ActionBase, ActionError + +from castervoice.rules.apps.editor.sublime_rules.Function_like_utilities import ( + get_signature_arguments,get_only_proper_arguments,rename_data,evaluate_function +) +from castervoice.rules.apps.editor.sublime_rules.sublime_communication_support import ( + send_sublime,send_snippet,send_quick_panel +) +from castervoice.rules.apps.editor.sublime_rules.snippet_generation_support import ( + generate_snippet_text,insert_snippet,snippet_state,apply_single_transformation,transform_snippet +) + + + +############################## SUBLIME COMMAND ACTION ############################## + +class SublimeCommand(ActionBase): + """docstring for SublimeCommand""" + def __init__(self, command,parameters = {}): + super(SublimeCommand, self).__init__() + if not isinstance(parameters,dict) and not callable(parameters): + raise TypeError("In SublimeCommand parameters must be a dict or a callable") + if not isinstance(command,str): + raise TypeError("In SublimeCommand command must be a string") + self.parameters = parameters + self.command = command + + def _execute(self,data): + try : + command = self.command % data + except : + command = self.command + if isinstance(self.parameters,dict): + p = self.parameters + else: + p = evaluate_function(self.parameters,data) + send_sublime(command,p) + + + +############################## SNIPPET CLASS ############################## + +class Snippet(ActionBase): + """docstring for Snippet + + Attributes: + contents (Union[str,List[str],Callable[...,str]]): + The snippet to be inserted.It can be one of the following: + - a raw string containing the snippet text + - a list of strings, containing variations of the same snippet + - a callable that will generate the snippet, optionally using the spoken data + As a fourth option you can specify that you want to obtain the snippet contents + (which can be in any of the above three forms) from an extra. As an example + "snippet ":R(Snippet("%(something)s")) + where + Choice("something",{"hello":"Hello $1,My name is $2","there":"$1= there($1)"}) + remap_data (Dict[str,str]): + a dictionary containing entries of the form (old_name,new_name) + enabling you to rename extras before they are passed to the snippet generation + For instance, suppose you have a snippet + lambda world: "$1 = " + world + " $2 " + world + normally the world parameter should come from an extra named `world`. + However, suppose you want `world` to come from an extra named `other_name` + Then in a manner similar to `Function` you can use + remap_data ={"other_name":"world"} + snippet_parameters (Union[Dict,Callable[...,str]]): + Parameters to pass along with a snippet. Can be either + - a hardcoded dictionary contain the parameters + - a callable that processes the extras spoken and returns the dictionary + In the latter case, the semantics are like `Function`. + """ + def __init__(self, contents,remap_data = {},snippet_parameters = {}): + super(Snippet, self).__init__() + self.contents = contents + self.remap_data = remap_data + self.snippet_parameters = snippet_parameters + + def retrieve_contents_from_extras_if_needed(self,contents,data): + if isinstance(contents,str) and "$" not in contents: + name = re.search(r"^%\((\w+)\)s$",contents).group(1) + if name in data: + return data[name] + else: + raise ValueError(r"contents is not a snippet nor of the form %(name)s") + else: + return contents + + def _execute(self,data): + contents = self.retrieve_contents_from_extras_if_needed(self.contents,data) + data = rename_data(data,self.remap_data) + insert_snippet(contents,data,self.snippet_parameters,additional_log = {"remap_data":self.remap_data}) + +############################## SNIPPET VARIANTS ############################## + +class SnippetVariant(ActionBase): + """docstring for Snippet""" + def __init__(self,**remap_data): + super(SnippetVariant, self).__init__() + self.remap_data = remap_data + + + def _execute(self,data): + contents = snippet_state["snippet"] + extra_data = deepcopy(snippet_state["extra_data"]) + for k,v in self.remap_data.items(): + if k in data: + extra_data[v] = data[k] + insert_snippet(contents,extra_data,snippet_state["snippet_parameters"],additional_log = {"remap_data":snippet_state["remap_data"]}) + + +############################## DISPLAY VARIANTS QUICK PANEL ############################## + + +class DisplaySnippetVariants(ActionBase): + """docstring for DisplaySnippetVariants""" + def __init__(self, name = "n",values = list(range(1,6))): + super(DisplaySnippetVariants, self).__init__() + self.name = name + self.values = values + + def _execute(self,*args,**kwargs): + data = deepcopy(snippet_state["extra_data"]) + snippet = snippet_state["snippet"] + alternatives = [] + protection_counter = 0 + for value in self.values: + try : + data[self.name] = value + x,_ = generate_snippet_text(snippet,data) + alternatives.append(x) + protection_counter += 1 + if protection_counter==20: + break + except: + break + + items = [{ + "caption":json.dumps(x), + "command":"insert_snippet", + "args":dict(contents=x,**snippet_state["snippet_parameters"]) + } for x in alternatives] + # send_sublime("quick_panel", dict(items=items)) + send_quick_panel( + (json.dumps(x),"insert_snippet",dict(contents=x,**snippet_state["snippet_parameters"])) + for x in alternatives + ) + + +class DisplayMultipleSnippetVariants(ActionBase): + """docstring for DisplaySnippetVariants""" + def __init__(self,values): + super(DisplayMultipleSnippetVariants, self).__init__() + self.values = values + + def _execute(self,*args,**kwargs): + snippet = snippet_state["snippet"] + alternatives = [] + protection_counter = 0 + for name,value in self.values.items(): + try : + data = deepcopy(snippet_state["extra_data"]) + data[name] = value + x,_ = generate_snippet_text(snippet,data) + alternatives.append(x) + protection_counter += 1 + if protection_counter==20: + break + except: + break + + items = [{ + "caption":json.dumps(x), + "command":"insert_snippet", + "args":dict(contents=x,**snippet_state["snippet_parameters"]) + } for x in alternatives] + send_sublime("quick_panel", dict(items=items)) + + + +############################## SNIPPET TRANSFORMATION ############################## + +class SnippetTransform(ActionBase): + """Apply a transformation to the previous inserted snippet + + Attributes: + transformation ( + Union[str,Transformation,List[Transformation]] where + Transformation = Union[Tuple,Callable[str,str]] + ): + the transformation to be applied to the last inserted snippet.One of + - callable, that accepts a single parameter the snippet text and + returns the final text + - a tuple that describes a regular expression and contains the arguments + you would pass to `re.sub` function ( excluding that snippet text of course) + + You can also pass at least + + steps (int): Description + """ + def __init__(self, transformation, steps = 0): + super(SnippetTransform, self).__init__() + self.transformation = transformation + self.steps = steps + if not isinstance(transformation,str): + self.verify_transformation(transformation) + + def retrieve_transformation_from_extras_if_needed(self,data,transformation): + if isinstance(transformation,str): + temporary = [] + for m in re.finditer(r"%\((\w+)\)s",transformation): + name = m.group(1) + if name in data: + t = data[name] + if isinstance(t,list): + temporary.extend(t) + else: + temporary.append(t) + else: + raise ValueError(r"transformation is not a snippet nor of the form %(name)s") + if not temporary: + raise ValueError("Empty transformation extracted from extra no %(name)s found :",transformation) + return temporary + else: + return transformation + + def verify_transformation(self, transformation): + if not isinstance(transformation,(tuple,list)) and not callable(transformation): + raise TypeError("transformation must be a tuple or callable or a list thereof, instead received",transformation) + if isinstance(transformation,list): + if any(x for x in transformation if not isinstance(x,tuple) and not callable(x)): + raise TypeError("transformation must be a tuple or callable or a list thereof, instead received",transformation) + + + def _execute(self,data): + transformation = self.retrieve_transformation_from_extras_if_needed(data,self.transformation) + self.verify_transformation(transformation) + s = snippet_state["snippet_text"] + for i in range(0,self.steps): + s = snippet_state["stack"].pop() + # by now s holds snippet we want to transform + # and this stack no longer contains it! + snippet_new = transform_snippet(s,transformation) + snippet_state["stack"].append(s) # push s onto the stack + insert_snippet(snippet_new,additional_log = {k:v for k,v in snippet_state.items() if k!="snippet_text"}) + + + + + + + + + + + + + + + + + + + + + + diff --git a/castervoice/rules/apps/editor/sublime_rules/sublime_support.py b/castervoice/rules/apps/editor/sublime_rules/sublime_support.py new file mode 100644 index 000000000..2eca7e183 --- /dev/null +++ b/castervoice/rules/apps/editor/sublime_rules/sublime_support.py @@ -0,0 +1,9 @@ +try : + from sublime_rules.snippet_utilities import placeholder,regular,regex,load_snippets + from sublime_rules.sublime_snippets import SublimeCommand,Snippet,SnippetTransform + from sublime_rules.sublime_communication_support import validate_subl,send_sublime,send_snippet,send_quick_panel +except : + from castervoice.rules.apps.editor.sublime_rules.snippet_utilities import placeholder,regular,regex,load_snippets + from castervoice.rules.apps.editor.sublime_rules.sublime_snippets import SublimeCommand,Snippet,SnippetTransform + from castervoice.rules.apps.editor.sublime_rules.sublime_communication_support import validate_subl,send_sublime,send_snippet,send_quick_panel + \ No newline at end of file diff --git a/castervoice/rules/ccr/voice_dev_commands_rules/voice_dev_commands.py b/castervoice/rules/ccr/voice_dev_commands_rules/voice_dev_commands.py index 775d74b2c..089f3b567 100644 --- a/castervoice/rules/ccr/voice_dev_commands_rules/voice_dev_commands.py +++ b/castervoice/rules/ccr/voice_dev_commands_rules/voice_dev_commands.py @@ -112,6 +112,12 @@ class VoiceDevCommands(MergeRule): "dev execute": R(Key("end")+Text(".execute()"), rdescript="call 'execute' method at end of line"), + # + "dev placeholder ":R(Text("${%(placeholder_index)d:}") + Key("left:2")), + "dev snippet":R(Text("Snippet()") + Key("left")), + "dev sublime command":R(Text("SublimeCommand()") + Key("left")), + "dev snippet transform":R(Text("SnippetTransform()") + Key("left")), + # Caster Snippets "dev bring app": R(Text("BringApp()") + Key("left"), rdescript="CasterDev: Snippet for Bring App"), @@ -218,6 +224,7 @@ class VoiceDevCommands(MergeRule): }), IntegerRefST("distance_1", 1, 500), IntegerRefST("distance_2", 1, 500), + IntegerRefST("placeholder_index",0,10), ] defaults = {"spec": "", "dict": "", "text": "", "mouse_button": ""} diff --git a/docs/readthedocs/Caster_Commands/Snippets/Introduction.md b/docs/readthedocs/Caster_Commands/Snippets/Introduction.md new file mode 100644 index 000000000..115c18c3e --- /dev/null +++ b/docs/readthedocs/Caster_Commands/Snippets/Introduction.md @@ -0,0 +1,33 @@ +# Sublime Snippets + +## Motivation + + +So what Caster currently offers for sublime users is the ability + +- to have all of your snippets on the grammar side + +- to have multiple variants of the same snippet + +- to generate them dynamically(!!) + +- to easily switch between those variants and post-process snippets + +- to execute arbitrary sublime commands, not limited just to snippets! + +all the while without needing a single line of code running on the sublime side, so there is nothing you need to do additionally install! The only thing you might have to take care off yourself is the [subl path issue](#subl-path) but for most users these should be handled automatically. + + +## Variant System + +## Transforming the last inserted snippet + +## Snippets Parameters + + +## Sublime Commands + + + +## Subl Path + diff --git a/docs/readthedocs/Caster_Commands/Snippets/SnippetTransform.md b/docs/readthedocs/Caster_Commands/Snippets/SnippetTransform.md new file mode 100644 index 000000000..7b42c7645 --- /dev/null +++ b/docs/readthedocs/Caster_Commands/Snippets/SnippetTransform.md @@ -0,0 +1,109 @@ +# Snippet Transformations + +## Usage + +## Types of Transformations + +There are currently two types of transformation supported + +- Regular expressions + +- Custom function + +Please note that both of them operate on the raw snippet text *BEFORE* it is inserted. That means thought it contains things like `$1` and `${2:something}` + + +### Regular Expressions + + +### Custom Functions + +If you decide to go for a custom function, these function must + +- accept a single `str` argument that is going to be the snippet text + +- Returns a single string that is going to be the transformed snippet text + + +For instance, suppose we want + +```python +{ + "raw": R(Key("c-z") + SnippetTransform(lambda s:json.dumps(s).replace("$","\\$"))), + "almost raw": R(Key("c-z") + SnippetTransform(lambda s:json.dumps(s))), +} +``` + +### Multiple Successive Transformations Using List + + +### Picking Up The Transformation From The Extras + + +```python +mapping = { + "apply ":R(Key("c-z") + SnippetTransform("%(transformation)s")), +} + +extras = { + Choice("transformation",{ + "almost raw": lambda s:json.dumps(s), + "raw": lambda s:json.dumps(s).replace("$","\\$"), + "alternative raw": [lambda s:json.dumps(s),lambda s:s.replace("$","\\$")], # same effect as above + "weird J":(r"\{(\d):i\}",r"\{\1:j\}",), # regular expression + } + ) +} +``` + +Now you may notice that we are using `Key("c-z")` + +Furthermore, you can also write + +```python +mapping = { + "apply ":R(Key("c-z") + SnippetTransform("%(transformation)s %(transformation2)s")), +} +``` + +In which case, + + + +## Signature + +```python +class SnippetTransform(ActionBase): + """Apply a transformation to the previous inserted snippet + + Attributes: + transformation ( + Union[str,Transformation,List[Transformation]] where + Transformation = Union[Tuple,Callable[str,str]] + ): + the transformation to be applied to the last inserted snippet.One of + - callable, that accepts a single parameter the snippet text and + returns the final text + - a tuple that describes a regular expression and contains the arguments + you would pass to `re.sub` function ( excluding that snippet text of course) + + You can also pass at least + + steps (int): Description + """ +``` + + + +```python +transformations = { + "almost raw": lambda s:json.dumps(s), + "raw": lambda s:json.dumps(s).replace("$","\\$"), + "alternative raw": [lambda s:json.dumps(s),lambda s:s.replace("$","\\$")], # same effect as above + # simply split into two functions, that will operate one after the other + + "weird J":(r"\{(\d):i\}",r"\{\1:j\}",), # regular expression +} +``` + + diff --git a/docs/readthedocs/Caster_Commands/Snippets/SnippetUtilities.md b/docs/readthedocs/Caster_Commands/Snippets/SnippetUtilities.md new file mode 100644 index 000000000..5ca772d4b --- /dev/null +++ b/docs/readthedocs/Caster_Commands/Snippets/SnippetUtilities.md @@ -0,0 +1,203 @@ +# Snippet Utilities + +In order to make development of grammars with snippets easier + +```python + +``` + +## Utilities for generating special snippet constructs + +### placeholder + +First we start with utility to generate snippet code for placeholders fields at runtime + +```python +def placeholder(field,default = ""): +``` + +where field and default must be convertible to string +typically field is going to be an integer + +```python +placeholder(1) = $1 +placeholder(1,"data") = ${1:data} +placeholder(1,42) = ${1:42} +``` + +but can also be anything else convertible to string because sublime allows you to define default values for environmental variables/snippet parameters + +```python +placeholder("PARAMETER") = $PARAMETER +placeholder("PARAMETER","data") = ${PARAMETER:data} +placeholder("PARAMETER",2) = ${PARAMETER:2} + +``` + +Alternatively if you want you can also pass a tuple to the field argument if you want fields nested one within the other,for example + +```python +placeholder((1,2),"data") = ${1:${2:data}} = placeholder(1,placeholder(2,"data")) +placeholder((1,"INTERESTING"),"data") = ${1:${INTERESTING:data}} = placeholder(1,placeholder("INTERESTING","data")) +``` + +### Regular Expressions + +Sublime support substitutions regular expressions using a syntax like + +``` +${var_name/regex/format_string/options} or +${var_name/regex/format_string} +``` + +You can find out more detailed information about regular expressions [here](https://sublime-text.readthedocs.io/en/stable/extensibility/snippets.html) + +in a manner similar placeholder,a `regular` function is supplied in order to output this kind of text + +```python +def regular(varname,regex,format_string,options = "",*, + ignore_case=False,replace_all=False,ignore_new_lines=True): +``` + +Please note that the arguments after `options` that is `ignore_case` and `replace_all` and `ignore_new_lines` are there for convenience to make setting options appropriately easier. + +## Quickly loading snippets into grammars + +In many cases it could be preferable to seemly be able to load snippets into grammars from a dictionary of the form + +```python +"spoken rule":correspondence_snippet +``` + +This is possible via `load_snippets` ,which can be used as a decorator in the following manner: + +```python +snippets = { + "function main":[ + "int main(){\n\t$0\n}\n", + "int main(int argc, char** argv){\n\t$0\n}\n", + ], + "if end":"if($2 == ${1:v}.end()){\n\t$0\n}\n", + "attribute assign": + lambda n: "".join(["auto& " + placeholder(x) + " = $1."+placeholder(x)+";\n" for x in range(2,n + 2)]), + "spec ": lambda dragonfly_action: '"$1":R({0}),'.format(dragonfly_action), +} + +@load_snippets(snippets) +class SnippetGrammar(MappingRule): + """docstring for SnippetGrammar""" + mapping = {} + extras = [ + Choice("dragonfly_action",{ + "key":'Key("$0")', + "text":'Text("$0")', + "mimic":'Mimic("$0")', + "focus":'FocusWindow("$0")', + "wait window":'WaitWindow("$0")', + "mouse":'Mouse("$0")', + "function":'Function($0)', + "sublime":'SublimeCommand("$0")', + } + ) + ] + default = {} + +``` + +Please notice we do not need to explicitly provide `IntegerRefST("n")` as it is handled by the decorator! +Furthermore because the for signature actually looks like + +```python +def load_snippets(snippets,extras = [], defaults = {}): + """Utility in order to decorate grammars to quickly load snippets from a raw dictionary format + + Args: + snippets (TYPE): Description + extras (list, optional): Description + defaults (dict, optional): Description + + Returns: + TYPE: Description + + Raises: + TypeError: Description +``` + +We can also supply the `Choice` as `extras` argument to `load_snippets` + +```python +snippets = { + "function main":[ + "int main(){\n\t$0\n}\n", + "int main(int argc, char** argv){\n\t$0\n}\n", + ], + "if end":"if($2 == ${1:v}.end()){\n\t$0\n}\n", + "attribute assign": + lambda n: "".join(["auto& " + placeholder(x) + " = $1."+placeholder(x)+";\n" for x in range(2,n + 2)]), + "spec ": lambda dragonfly_action: '"$1":R({0}),'.format(dragonfly_action), +} + +extras = [ + Choice("dragonfly_action",{ + "key":'Key("$0")', + "text":'Text("$0")', + "mimic":'Mimic("$0")', + "focus":'FocusWindow("$0")', + "wait window":'WaitWindow("$0")', + "mouse":'Mouse("$0")', + "function":'Function($0)', + "sublime":'SublimeCommand("$0")', + } + ) +] + +@load_snippets(snippets,extras) +class SnippetGrammar(MappingRule): + """docstring for SnippetGrammar""" + mapping = {} + extras = [] + default = {} +``` + +Ok, so far we have seen about our snippets dictionary can contain anything that we could put inside in `R(Snippet())` saving us a few keystrokes. However, with what we have seen so far we still need to to create dragonfly extras like the choice element separately, which can be a burden if we have a spec that only needs a single choice element. In order to address this issue and make your life easier the dictionary of snippets can also contain dictionaries as values like so + +```python +snippets = { + "function main":[ + "int main(){\n\t$0\n}\n", + "int main(int argc, char** argv){\n\t$0\n}\n", + ], + "if end":"if($2 == ${1:v}.end()){\n\t$0\n}\n", + "attribute assign": + lambda n: "".join(["auto& " + placeholder(x) + " = $1."+placeholder(x)+";\n" for x in range(2,n + 2)]), + "spec ": { + "key":'"$1":R(Key("$0"))', + "text":'"$1":R(Text("$0"))', + "mimic":'"$1":R(Mimic("$0"))', + "focus":'"$1":R(FocusWindow("$0"))', + "wait window":'"$1":R(WaitWindow("$0"))', + "mouse":'"$1":R(Mouse("$0"))', + "function":'"$1":R(Function($0))', + "sublime":'"$1":R(SublimeCommand("$0"))', + } +} + + +@load_snippets(snippets) +class SnippetGrammar(MappingRule): + """docstring for SnippetGrammar""" + mapping = {} + extras = [] + default = {} +``` + +in which case a rule of the form + +```python +"spec ": R(Snippet("%(dragonfly_action)s")) +``` + +along with an appropriate `Choice("dragonfly_action",{...})` will be produced! Please note that this_option is only available if there is a single choice in the spoken rule/spec! + + + diff --git a/docs/readthedocs/Caster_Commands/Snippets/SublimeCommand.md b/docs/readthedocs/Caster_Commands/Snippets/SublimeCommand.md new file mode 100644 index 000000000..5a7d1b37d --- /dev/null +++ b/docs/readthedocs/Caster_Commands/Snippets/SublimeCommand.md @@ -0,0 +1,134 @@ +# Sublime Command + + + +- [Preliminary example](#preliminary-example) +- [Formatting for dynamically commands](#formatting-for-dynamically-commands) +- [Passing parameters](#passing-parameters) +- [Passing callable to parameters](#passing-callable-to-parameters) +- [Acknowledgments](#acknowledgments) + + + +## Preliminary example + +A useful resource for creating your own `SublimeCommand` rules can be [Package Resource Viewer](https://packagecontrol.io/packages/PackageResourceViewer) which allows you to quickly browse files of the packages you have installed. + +## Formatting for dynamically commands + +Let's look some further commands of the git package + +```json +{ + "caption": "Git: Add...", + "command": "git_add_choice" + } +{ + "caption": "Git: Status", + "command": "git_status" + } +{ + "caption": "Git: Commit", + "command": "git_commit" + } +``` + +Based in our previous example one way to go about this is to create three separate specs + +```python +mapping = { + "git add": R(SublimeCommand("git_add_choice")), + "git status": R(SublimeCommand("git_status")), + "git commit": R(SublimeCommand("git_commit")), +} +``` + +but this quickly becomes cumbersome and is unnecessarily verbose because we failed to take advantages of the pattern that appears in the sublime commands. In order to address this issue, we can employ formatting options like the ones used by Key/Text and reflector our code into something like + +```python +mapping = { + "git ":R(SublimeCommand("git_%(action)s")), +} + +Choice("action",{ + "add":"add_choice", + "commit" : "commit", + "status" : "status", + } +) +``` + +which of course requires less code to add new actions! + + + +## Passing parameters + +Let's continue with our Git example and try something different out: + + +```json +{ + "caption": "Git: Add Current File", + "command": "git_raw", "args": { "command": "git add", "append_current_file": true } +} +``` + +Now that is interesting, unlike our previous examples here we do not only have a `caption` and `command` but also a `args` entry! + +How do we go about this? `SublimeCommand` can also accept a parameters argument + +```python +def SublimeCommand(self, command,parameters = {}): +``` + +where we can pass a dictionary of parameters to be passed to the command executed by sublime. + +```python +"git add current":R(SublimeCommand("git_raw",{ "command": "git add", "append_current_file": True })), +``` + +something important here is that this dictionary must be json serializable! + +## Passing callable to parameters + +but allowing us to pass parameters to the command create another issue that needs to be addressed. + +For example, in the get package we can find three variations of the same command `git_quick_commit` + +```python +{ + "caption": "Git: Quick Commit (current file)", + "command": "git_quick_commit" +} +,{ + "caption": "Git: Quick Commit (repo)", + "command": "git_quick_commit", "args": { "target": "*" } +} +,{ + "caption": "Git: Quick Commit (repo, only already added files)", + "command": "git_quick_commit", "args": { "target": false } +} +``` + +by using only a static dictionary as parameters, we would need three different specs, one for each variation which again involves a lot of unnecessary repetition. In order to overcome this limitation,`SublimeCommand` allows you to pass a callable instead, which would dynamically create dictionary of parameters based on the extras spoken during the utterance + +```python +mapping = { + "git quick commit ":R(SublimeCommand("git_quick_commit", + lambda item: item)), +} + +Choice("item",{ + "file":{}, + "repo":{"target":"*"}, + "already added":{"target":False} + } +), +``` + + +## Acknowledgments + + +This page contained parts from `Default.sublime-commands` of the Git package, which falls under the MIT license[](https://github.com/kemayo/sublime-text-git/blob/master/LICENSE). diff --git a/tests/rules/apps/test_rule_modules.py b/tests/rules/apps/test_rule_modules.py index 068d2a56e..54d7ec5e8 100644 --- a/tests/rules/apps/test_rule_modules.py +++ b/tests/rules/apps/test_rule_modules.py @@ -38,7 +38,8 @@ def _rule_modules(self): from castervoice.rules.apps.editor import flashdevelop from castervoice.rules.apps.editor import msvc from castervoice.rules.apps.editor import typora - from castervoice.rules.apps.editor import sublime + from castervoice.rules.apps.editor.sublime_rules import sublime + from castervoice.rules.apps.editor.sublime_rules import sublime_snippet_control from castervoice.rules.apps.editor import emacs from castervoice.rules.apps.chat import gitter, MSTeamsRule return [chrome, firefox, jetbrains, adobe_acrobat, atom, @@ -47,6 +48,6 @@ def _rule_modules(self): foxitreader, gitbash, githubdesktop, gitter, griddouglas, gridlegion, gridrainbow, kdiff3, lyx, msvc, MSTeamsRule, notepadplusplus, outlook, rstudio, - sqldeveloper, ssms, sublime, totalcmd, totalcmd2, + sqldeveloper, ssms, sublime, sublime_snippet_control,totalcmd, totalcmd2, typora, visualstudio, vscode, vscode2, winword, wsr] \ No newline at end of file