dotfiles/talon/community/community-cursorless-0.4.0/text/text_navigation.py
2024-11-16 20:27:38 -07:00

294 lines
9.6 KiB
Python

import re
from talon import ctrl, ui, Module, Context, actions, clip
import itertools
from typing import Union
ctx = Context()
mod = Module()
text_navigation_max_line_search = mod.setting(
"text_navigation_max_line_search",
type=int,
default=10,
desc="the maximum number of rows that will be included in the search for the keywords above and below in <user direction>",
)
mod.list(
"navigation_action",
desc="actions to perform, for instance move, select, cut, etc",
)
mod.list(
"before_or_after",
desc="words to indicate if the cursor should be moved before or after a given reference point",
)
mod.list(
"navigation_target_name",
desc="names for regular expressions for common things to navigate to, for instance a word with or without underscores",
)
ctx.lists["self.navigation_action"] = {
"move": "GO",
"extend": "EXTEND",
"select": "SELECT",
"clear": "DELETE",
"cut": "CUT",
"copy": "COPY",
}
ctx.lists["self.before_or_after"] = {
"before": "BEFORE",
"after": "AFTER",
# DEFAULT is also a valid option as input for this capture, but is not directly accessible for the user.
}
navigation_target_names = {
"word": r"\w+",
"small": r"[A-Z]?[a-z0-9]+",
"big": r"[\S]+",
"parens": r'\((.*?)\)',
"squares": r'\[(.*?)\]',
"braces": r'\{(.*?)\}',
"quotes": r'\"(.*?)\"',
"angles": r'\<(.*?)\>',
#"single quotes": r'\'(.*?)\'',
"all": r'(.+)',
"method": r'\w+\((.*?)\)',
"constant": r'[A-Z_][A-Z_]+'
}
ctx.lists["self.navigation_target_name"] = navigation_target_names
@mod.capture(rule="<user.any_alphanumeric_key> | {user.navigation_target_name} | phrase <user.text>")
def navigation_target(m) -> re.Pattern:
"""A target to navigate to. Returns a regular expression."""
if hasattr(m, 'any_alphanumeric_key'):
return re.compile(re.escape(m.any_alphanumeric_key), re.IGNORECASE)
if hasattr(m, 'navigation_target_name'):
return re.compile(m.navigation_target_name)
return re.compile(re.escape(m.text), re.IGNORECASE)
@mod.action_class
class Actions:
def navigation(
navigation_action: str, # GO, EXTEND, SELECT, DELETE, CUT, COPY
direction: str, # up, down, left, right
navigation_target_name: str,
before_or_after: str, # BEFORE, AFTER, DEFAULT
regex: re.Pattern,
occurrence_number: int,
):
"""Navigate in `direction` to the occurrence_number-th time that `regex` occurs, then execute `navigation_action` at the given `before_or_after` position."""
direction = direction.upper()
navigation_target_name = re.compile((navigation_target_names["word"] if (navigation_target_name == "DEFAULT") else navigation_target_name))
function = navigate_left if direction in ("UP", "LEFT") else navigate_right
function(navigation_action, navigation_target_name, before_or_after, regex, occurrence_number, direction)
def navigation_by_name(
navigation_action: str, # GO, EXTEND, SELECT, DELETE, CUT, COPY
direction: str, # up, down, left, right
before_or_after: str, # BEFORE, AFTER, DEFAULT
navigation_target_name: str, # word, big, small
occurrence_number: int,
):
"""Like user.navigation, but to a named target."""
r = re.compile(navigation_target_names[navigation_target_name])
actions.user.navigation(navigation_action, direction, "DEFAULT", before_or_after, r, occurrence_number)
def get_text_left():
actions.edit.extend_line_start()
text = actions.edit.selected_text()
actions.edit.right()
return text
def get_text_right():
actions.edit.extend_line_end()
text = actions.edit.selected_text()
actions.edit.left()
return text
def get_text_up():
actions.edit.up()
actions.edit.line_end()
for j in range(0, text_navigation_max_line_search.get()):
actions.edit.extend_up()
actions.edit.extend_line_start()
text = actions.edit.selected_text()
actions.edit.right()
return text
def get_text_down():
actions.edit.down()
actions.edit.line_start()
for j in range(0, text_navigation_max_line_search.get()):
actions.edit.extend_down()
actions.edit.extend_line_end()
text = actions.edit.selected_text()
actions.edit.left()
return text
def get_current_selection_size():
return len(actions.edit.selected_text())
def go_right(i):
for j in range(0, i):
actions.edit.right()
def go_left(i):
for j in range(0, i):
actions.edit.left()
def extend_left(i):
for j in range(0, i):
actions.edit.extend_left()
def extend_right(i):
for j in range(0, i):
actions.edit.extend_right()
def select(direction, start, end, length):
if direction == "RIGHT" or direction == "DOWN":
go_right(start)
extend_right(end - start)
else:
go_left(length - end)
extend_left(end - start)
def navigate_left(
navigation_action, navigation_target_name, before_or_after, regex, occurrence_number, direction
):
current_selection_length = get_current_selection_size()
if current_selection_length > 0:
actions.edit.right()
text = get_text_left() if direction == "LEFT" else get_text_up()
# only search in the text that was not selected
subtext = (
text if current_selection_length <= 0 else text[:-current_selection_length]
)
match = match_backwards(regex, occurrence_number, subtext)
if match == None:
# put back the old selection, if the search failed
extend_left(current_selection_length)
return
start = match.start()
end = match.end()
handle_navigation_action(
navigation_action, navigation_target_name, before_or_after, direction, text, start, end
)
def navigate_right(
navigation_action, navigation_target_name, before_or_after, regex, occurrence_number, direction
):
current_selection_length = get_current_selection_size()
if current_selection_length > 0:
actions.edit.left()
text = get_text_right() if direction == "RIGHT" else get_text_down()
# only search in the text that was not selected
sub_text = text[current_selection_length:]
# pick the next interrater, Skip n number of occurrences, get an iterator given the Regex
match = match_forward(regex, occurrence_number, sub_text)
if match == None:
# put back the old selection, if the search failed
extend_right(current_selection_length)
return
start = current_selection_length + match.start()
end = current_selection_length + match.end()
handle_navigation_action(
navigation_action, navigation_target_name, before_or_after, direction, text, start, end
)
def handle_navigation_action(
navigation_action, navigation_target_name, before_or_after, direction, text, start, end
):
length = len(text)
if navigation_action == "GO":
handle_move(direction, before_or_after, start, end, length)
elif navigation_action == "SELECT":
handle_select(navigation_target_name, before_or_after, direction, text, start, end, length)
elif navigation_action == "DELETE":
handle_select(navigation_target_name, before_or_after, direction, text, start, end, length)
actions.edit.delete()
elif navigation_action == "CUT":
handle_select(navigation_target_name, before_or_after, direction, text, start, end, length)
actions.edit.cut()
elif navigation_action == "COPY":
handle_select(navigation_target_name, before_or_after, direction, text, start, end, length)
actions.edit.copy()
elif navigation_action == "EXTEND":
handle_extend(before_or_after, direction, start, end, length)
def handle_select(navigation_target_name, before_or_after, direction, text, start, end, length):
if before_or_after == "BEFORE":
select_left = length - start
text_left = text[:-select_left]
match2 = match_backwards(navigation_target_name, 1, text_left)
if match2 == None:
end = start
start = 0
else:
start = match2.start()
end = match2.end()
elif before_or_after == "AFTER":
text_right = text[end:]
match2 = match_forward(navigation_target_name, 1, text_right)
if match2 == None:
start = end
end = length
else:
start = end + match2.start()
end = end + match2.end()
select(direction, start, end, length)
def handle_move(direction, before_or_after, start, end, length):
if direction == "RIGHT" or direction == "DOWN":
if before_or_after == "BEFORE":
go_right(start)
else:
go_right(end)
else:
if before_or_after == "AFTER":
go_left(length - end)
else:
go_left(length - start)
def handle_extend(before_or_after, direction, start, end, length):
if direction == "RIGHT" or direction == "DOWN":
if before_or_after == "BEFORE":
extend_right(start)
else:
extend_right(end)
else:
if before_or_after == "AFTER":
extend_left(length - end)
else:
extend_left(length - start)
def match_backwards(regex, occurrence_number, subtext):
try:
match = list(regex.finditer(subtext))[-occurrence_number]
return match
except IndexError:
return
def match_forward(regex, occurrence_number, sub_text):
try:
match = next(
itertools.islice(regex.finditer(sub_text), occurrence_number - 1, None)
)
return match
except StopIteration:
return None