dotfiles

My personal shell configs and stuff
git clone git://git.alex.balgavy.eu/dotfiles.git
Log | Files | Refs | Submodules | README | LICENSE

text_navigation.py (9821B)


      1 import re
      2 from talon import ctrl, ui, Module, Context, actions, clip
      3 import itertools
      4 from typing import Union
      5 
      6 ctx = Context()
      7 mod = Module()
      8 
      9 
     10 text_navigation_max_line_search = mod.setting(
     11     "text_navigation_max_line_search",
     12     type=int,
     13     default=10,
     14     desc="the maximum number of rows that will be included in the search for the keywords above and below in <user direction>",
     15 )
     16 
     17 mod.list(
     18     "navigation_action",
     19     desc="actions to perform, for instance move, select, cut, etc",
     20 )
     21 mod.list(
     22     "before_or_after",
     23     desc="words to indicate if the cursor should be moved before or after a given reference point",
     24 )
     25 mod.list(
     26     "navigation_target_name",
     27     desc="names for regular expressions for common things to navigate to, for instance a word with or without underscores",
     28 )
     29 
     30 ctx.lists["self.navigation_action"] = {
     31     "move": "GO",
     32     "extend": "EXTEND",
     33     "select": "SELECT",
     34     "clear": "DELETE",
     35     "cut": "CUT",
     36     "copy": "COPY",
     37 }
     38 ctx.lists["self.before_or_after"] = {
     39     "before": "BEFORE",
     40     "after": "AFTER",
     41     # DEFAULT is also a valid option as input for this capture, but is not directly accessible for the user.
     42 }
     43 navigation_target_names = {
     44     "word": r"\w+",
     45     "small": r"[A-Z]?[a-z0-9]+",
     46     "big": r"[\S]+",
     47     "parens": r'\((.*?)\)',
     48     "squares": r'\[(.*?)\]',
     49     "braces": r'\{(.*?)\}',
     50     "quotes": r'\"(.*?)\"',
     51     "angles": r'\<(.*?)\>',
     52     #"single quotes": r'\'(.*?)\'',
     53     "all": r'(.+)',
     54     "method": r'\w+\((.*?)\)',
     55     "constant": r'[A-Z_][A-Z_]+'
     56 }
     57 ctx.lists["self.navigation_target_name"] = navigation_target_names
     58 
     59 @mod.capture(rule="<user.any_alphanumeric_key> | {user.navigation_target_name} | phrase <user.text>")
     60 def navigation_target(m) -> re.Pattern:
     61     """A target to navigate to. Returns a regular expression."""
     62     if hasattr(m, 'any_alphanumeric_key'):
     63         return re.compile(re.escape(m.any_alphanumeric_key), re.IGNORECASE)
     64     if hasattr(m, 'navigation_target_name'):
     65         return re.compile(m.navigation_target_name)
     66     return re.compile(re.escape(m.text), re.IGNORECASE)
     67 
     68 @mod.action_class
     69 class Actions:
     70     def navigation(
     71             navigation_action: str, # GO, EXTEND, SELECT, DELETE, CUT, COPY
     72             direction: str,         # up, down, left, right
     73             navigation_target_name: str,
     74             before_or_after: str,   # BEFORE, AFTER, DEFAULT
     75             regex: re.Pattern,
     76             occurrence_number: int,
     77     ):
     78         """Navigate in `direction` to the occurrence_number-th time that `regex` occurs, then execute `navigation_action` at the given `before_or_after` position."""
     79         direction = direction.upper()
     80         navigation_target_name = re.compile((navigation_target_names["word"] if (navigation_target_name == "DEFAULT") else navigation_target_name))
     81         function = navigate_left if direction in ("UP", "LEFT") else navigate_right
     82         function(navigation_action, navigation_target_name, before_or_after, regex, occurrence_number, direction)
     83 
     84     def navigation_by_name(
     85             navigation_action: str, # GO, EXTEND, SELECT, DELETE, CUT, COPY
     86             direction: str,         # up, down, left, right
     87             before_or_after: str,   # BEFORE, AFTER, DEFAULT
     88             navigation_target_name: str, # word, big, small
     89             occurrence_number: int,
     90     ):
     91         """Like user.navigation, but to a named target."""
     92         r = re.compile(navigation_target_names[navigation_target_name])
     93         actions.user.navigation(navigation_action, direction, "DEFAULT", before_or_after, r, occurrence_number)
     94 
     95 def get_text_left():
     96     actions.edit.extend_line_start()
     97     text = actions.edit.selected_text()
     98     actions.edit.right()
     99     return text
    100 
    101 
    102 def get_text_right():
    103     actions.edit.extend_line_end()
    104     text = actions.edit.selected_text()
    105     actions.edit.left()
    106     return text
    107 
    108 
    109 def get_text_up():
    110     actions.edit.up()
    111     actions.edit.line_end()
    112     for j in range(0, text_navigation_max_line_search.get()):
    113         actions.edit.extend_up()
    114     actions.edit.extend_line_start()
    115     text = actions.edit.selected_text()
    116     actions.edit.right()
    117     return text
    118 
    119 
    120 def get_text_down():
    121     actions.edit.down()
    122     actions.edit.line_start()
    123     for j in range(0, text_navigation_max_line_search.get()):
    124         actions.edit.extend_down()
    125     actions.edit.extend_line_end()
    126     text = actions.edit.selected_text()
    127     actions.edit.left()
    128     return text
    129 
    130 
    131 def get_current_selection_size():
    132     return len(actions.edit.selected_text())
    133 
    134 
    135 def go_right(i):
    136     for j in range(0, i):
    137         actions.edit.right()
    138 
    139 
    140 def go_left(i):
    141     for j in range(0, i):
    142         actions.edit.left()
    143 
    144 
    145 def extend_left(i):
    146     for j in range(0, i):
    147         actions.edit.extend_left()
    148 
    149 
    150 def extend_right(i):
    151     for j in range(0, i):
    152         actions.edit.extend_right()
    153 
    154 
    155 def select(direction, start, end, length):
    156     if direction == "RIGHT" or direction == "DOWN":
    157         go_right(start)
    158         extend_right(end - start)
    159     else:
    160         go_left(length - end)
    161         extend_left(end - start)
    162 
    163 
    164 def navigate_left(
    165     navigation_action, navigation_target_name, before_or_after, regex, occurrence_number, direction
    166 ):
    167     current_selection_length = get_current_selection_size()
    168     if current_selection_length > 0:
    169         actions.edit.right()
    170     text = get_text_left() if direction == "LEFT" else get_text_up()
    171     # only search in the text that was not selected
    172     subtext = (
    173         text if current_selection_length <= 0 else text[:-current_selection_length]
    174     )
    175     match = match_backwards(regex, occurrence_number, subtext)
    176     if match == None:
    177         # put back the old selection, if the search failed
    178         extend_left(current_selection_length)
    179         return
    180     start = match.start()
    181     end = match.end()
    182     handle_navigation_action(
    183         navigation_action, navigation_target_name, before_or_after, direction, text, start, end
    184     )
    185 
    186 
    187 def navigate_right(
    188     navigation_action, navigation_target_name, before_or_after, regex, occurrence_number, direction
    189 ):
    190     current_selection_length = get_current_selection_size()
    191     if current_selection_length > 0:
    192         actions.edit.left()
    193     text = get_text_right() if direction == "RIGHT" else get_text_down()
    194     # only search in the text that was not selected
    195     sub_text = text[current_selection_length:]
    196     # pick the next interrater, Skip n number of occurrences, get an iterator given the Regex
    197     match = match_forward(regex, occurrence_number, sub_text)
    198     if match == None:
    199         # put back the old selection, if the search failed
    200         extend_right(current_selection_length)
    201         return
    202     start = current_selection_length + match.start()
    203     end = current_selection_length + match.end()
    204     handle_navigation_action(
    205         navigation_action, navigation_target_name, before_or_after, direction, text, start, end
    206     )
    207 
    208 
    209 def handle_navigation_action(
    210     navigation_action, navigation_target_name, before_or_after, direction, text, start, end
    211 ):
    212     length = len(text)
    213     if navigation_action == "GO":
    214         handle_move(direction, before_or_after, start, end, length)
    215     elif navigation_action == "SELECT":
    216         handle_select(navigation_target_name, before_or_after, direction, text, start, end, length)
    217     elif navigation_action == "DELETE":
    218         handle_select(navigation_target_name, before_or_after, direction, text, start, end, length)
    219         actions.edit.delete()
    220     elif navigation_action == "CUT":
    221         handle_select(navigation_target_name, before_or_after, direction, text, start, end, length)
    222         actions.edit.cut()
    223     elif navigation_action == "COPY":
    224         handle_select(navigation_target_name, before_or_after, direction, text, start, end, length)
    225         actions.edit.copy()
    226     elif navigation_action == "EXTEND":
    227         handle_extend(before_or_after, direction, start, end, length)
    228 
    229 
    230 def handle_select(navigation_target_name, before_or_after, direction, text, start, end, length):
    231     if before_or_after == "BEFORE":
    232         select_left = length - start
    233         text_left = text[:-select_left]
    234         match2 = match_backwards(navigation_target_name, 1, text_left)
    235         if match2 == None:
    236             end = start
    237             start = 0
    238         else:
    239             start = match2.start()
    240             end = match2.end()
    241     elif before_or_after == "AFTER":
    242         text_right = text[end:]
    243         match2 = match_forward(navigation_target_name, 1, text_right)
    244         if match2 == None:
    245             start = end
    246             end = length
    247         else:
    248             start = end + match2.start()
    249             end = end + match2.end()
    250     select(direction, start, end, length)
    251 
    252 
    253 def handle_move(direction, before_or_after, start, end, length):
    254     if direction == "RIGHT" or direction == "DOWN":
    255         if before_or_after == "BEFORE":
    256             go_right(start)
    257         else:
    258             go_right(end)
    259     else:
    260         if before_or_after == "AFTER":
    261             go_left(length - end)
    262         else:
    263             go_left(length - start)
    264 
    265 
    266 def handle_extend(before_or_after, direction, start, end, length):
    267     if direction == "RIGHT" or direction == "DOWN":
    268         if before_or_after == "BEFORE":
    269             extend_right(start)
    270         else:
    271             extend_right(end)
    272     else:
    273         if before_or_after == "AFTER":
    274             extend_left(length - end)
    275         else:
    276             extend_left(length - start)
    277 
    278 
    279 def match_backwards(regex, occurrence_number, subtext):
    280     try:
    281         match = list(regex.finditer(subtext))[-occurrence_number]
    282         return match
    283     except IndexError:
    284         return
    285 
    286 
    287 def match_forward(regex, occurrence_number, sub_text):
    288     try:
    289         match = next(
    290             itertools.islice(regex.finditer(sub_text), occurrence_number - 1, None)
    291         )
    292         return match
    293     except StopIteration:
    294         return None