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