diff --git a/_regex_builds.py b/_regex_builds.py index 9034646..fb9c2aa 100644 --- a/_regex_builds.py +++ b/_regex_builds.py @@ -1,5 +1,69 @@ +# Note: The regexes in this file are not intended to correctly validate +# all instances of the given type. They are designed to match the most +# common variants while specifically excluding less-common ones that could +# introduce false matches. + import re +def make_path_regexes(): + # This is not intended to match all valid paths - that would result in too many matches. + # Note: Spaces, as a common delimiter, are handled specially. They are only allowed in + # paths with at least 3 elements, and never in the first or last element. There cannot + # be more than one consecutive space, and it cannot be at the beginning or end of the + # element. + edge_delimiters = r'[][\s:=,#$"{}<>()' + "'" + ']' + edge_delimiters_w_slash = r'[][\s:=,#$"{}<>()/' + "'" + ']' + leader = r'(?:^|' + edge_delimiters + ')' + leader_w_slash = r'(?:^|' + edge_delimiters_w_slash + ')' + follower = r'(?:$|' + edge_delimiters + ')' + path_el = r'[a-zA-Z0-9_-]{1,30}' + inner_path_el = r'[a-zA-Z0-9_-]{1,25}\\? [a-zA-Z0-9_-]{1,25}' + either_path_el = r'(?:' + path_el + '|' + inner_path_el + ')' + basic_filename = path_el + r'\.' + r'[a-zA-Z][a-zA-Z0-9]{0,5}' + path_filename = either_path_el + r'\.' + r'[a-zA-Z0-9]{1,6}' + sep = r'[\\/]' + root = r'(?:/|~/|[A-Z]:' + sep + ')' + abspath = root + r'(?:' + either_path_el + sep + ')*' + r'(?:' + path_filename + '|' + path_el + sep + '?)' + relpath2 = path_el + sep + r'(?:' + path_el + sep + '?|' + path_filename + ')' + relpath3 = path_el + sep + r'(?:' + either_path_el + sep + r')+' + r'(?:' + path_el + sep + '?|' + path_filename + ')' + anypath = r'(?:' + abspath + '|' + relpath2 + '|' + relpath3 + ')' + rabspath = leader + '(' + abspath + ')' + follower + ranypath = leader + '(' + anypath + ')' + follower + rfilename = leader_w_slash + '(' + basic_filename + ')' + follower + return rabspath, ranypath, rfilename + +def test_path_regexes(): + testpaths = r''' + foo + foo/bar + foo/bar/baz + baz.mp3 + 123.456 + C:\fooo + C:\fooo.bar + C:/fooo.bar + /fooo + /fooo.bar + /abc/def/ghi + foo bar/baz + foo/bar baz/fiz + foo/bar baz/fiz.buz + '''.split('\n') + testpaths = [ p.strip() for p in testpaths if len(p.strip()) ] + + abspath, anypath, filename = make_path_regexes() + + print('abspath: ' + abspath) + print('anypath: ' + anypath) + print('filename: ' + filename) + + for p in testpaths: + m1 = re.fullmatch(abspath, p) + m2 = re.fullmatch(anypath, p) + m3 = re.fullmatch(filename, p) + print(f'{p} - {"ABS" if m1 else ""} {"PATH" if m2 else ""} {"FILE" if m3 else ""}') + + # Note: This url regex is not a validator, nor is it intended to be. It is intended # to match the most common kinds of URLs that are used and avoid unintended matches. # Notably, spaces in urls are not matched; with spaces there's too great a chance @@ -119,8 +183,25 @@ def test_url_regex(): else: print(f'{u} - No Match') -test_url_regex() +def print_rex(name, r, comment=None): + rxstr = "r'" + r.replace("'", "'+\"'\"+r'") + "'" + if comment: + print(f"\t# {comment}") + print(f"\t'{name}': {rxstr}") -r = make_url_regex() -print("r'" + r.replace("'", "'+\"'\"+r'") + "'") +#test_url_regex() + +#print('URL:') +print_rex('urls', make_url_regex(), 'matches common types of urls and things that look like urls') +print + +#test_path_regexes() + +absp, anyp, fn = make_path_regexes() +#print('ABS PATH:') +print_rex('abspaths', absp, 'Unix and window style absolute paths') +#print('PATH:') +print_rex('paths', anyp, 'Absolute or relative paths') +#print('FILE:') +print_rex('filenames', fn, 'Isolated filenames without paths') diff --git a/copytk.py b/copytk.py index dfa3e26..99e8bac 100644 --- a/copytk.py +++ b/copytk.py @@ -31,8 +31,25 @@ tmux_command = find_command_path('tmux') # strings that can be used to map to regexes in the quickcopy matches match_expr_presets = { - # generated by _regex_builds.py - 'urls': r'(?:^|[][\s:=,#"{}()'+"'"+r'])([a-zA-Z][a-zA-Z0-9]{1,5}://(?:[a-zA-Z0-9_]+(?::[a-zA-Z0-9_-]+)?@)?(?:(?:[a-zA-Z0-9][\w-]*\.)*[a-zA-Z][\w-]*|(?:[0-2]?[0-9]{1,2}\.){3}[0-2]?[0-9]{1,2})(?::[0-9]{1,5})?(?:/(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)?/?)?(?:\?(?:(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)+(?:=(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)?)?&)*(?:(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)+(?:=(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)?)?)?)?(?:#(?:(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)+(?:=(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)?)?&)*(?:(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)+(?:=(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)?)?)?)?)(?:$|[][\s:=,#"{}()'+"'"+r'])' + # matches common types of urls and things that look like urls + 'urls': r'(?:^|[][\s:=,#"{}()'+"'"+r'])([a-zA-Z][a-zA-Z0-9]{1,5}://(?:[a-zA-Z0-9_]+(?::[a-zA-Z0-9_-]+)?@)?(?:(?:[a-zA-Z0-9][\w-]*\.)*[a-zA-Z][\w-]*|(?:[0-2]?[0-9]{1,2}\.){3}[0-2]?[0-9]{1,2})(?::[0-9]{1,5})?(?:/(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)?/?)?(?:\?(?:(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)+(?:=(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)?)?&)*(?:(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)+(?:=(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)?)?)?)?(?:#(?:(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)+(?:=(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)?)?&)*(?:(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)+(?:=(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)?)?)?)?)(?:$|[][\s:=,#"{}()'+"'"+r'])', + # Unix and window style absolute paths + 'abspaths': r'(?:^|[][\s:=,#$"{}<>()'+"'"+r'])((?:/|~/|[A-Z]:[\\/])(?:(?:[a-zA-Z0-9_-]{1,30}|[a-zA-Z0-9_-]{1,25}\\? [a-zA-Z0-9_-]{1,25})[\\/])*(?:(?:[a-zA-Z0-9_-]{1,30}|[a-zA-Z0-9_-]{1,25}\\? [a-zA-Z0-9_-]{1,25})\.[a-zA-Z0-9]{1,6}|[a-zA-Z0-9_-]{1,30}[\\/]?))(?:$|[][\s:=,#$"{}<>()'+"'"+r'])', + # Absolute or relative paths + 'paths': r'(?:^|[][\s:=,#$"{}<>()'+"'"+r'])((?:(?:/|~/|[A-Z]:[\\/])(?:(?:[a-zA-Z0-9_-]{1,30}|[a-zA-Z0-9_-]{1,25}\\? [a-zA-Z0-9_-]{1,25})[\\/])*(?:(?:[a-zA-Z0-9_-]{1,30}|[a-zA-Z0-9_-]{1,25}\\? [a-zA-Z0-9_-]{1,25})\.[a-zA-Z0-9]{1,6}|[a-zA-Z0-9_-]{1,30}[\\/]?)|[a-zA-Z0-9_-]{1,30}[\\/](?:[a-zA-Z0-9_-]{1,30}[\\/]?|(?:[a-zA-Z0-9_-]{1,30}|[a-zA-Z0-9_-]{1,25}\\? [a-zA-Z0-9_-]{1,25})\.[a-zA-Z0-9]{1,6})|[a-zA-Z0-9_-]{1,30}[\\/](?:(?:[a-zA-Z0-9_-]{1,30}|[a-zA-Z0-9_-]{1,25}\\? [a-zA-Z0-9_-]{1,25})[\\/])+(?:[a-zA-Z0-9_-]{1,30}[\\/]?|(?:[a-zA-Z0-9_-]{1,30}|[a-zA-Z0-9_-]{1,25}\\? [a-zA-Z0-9_-]{1,25})\.[a-zA-Z0-9]{1,6})))(?:$|[][\s:=,#$"{}<>()'+"'"+r'])', + # Isolated filenames without paths + 'filenames': r'(?:^|[][\s:=,#$"{}<>()/'+"'"+r'])([a-zA-Z0-9_-]{1,30}\.[a-zA-Z][a-zA-Z0-9]{0,5})(?:$|[][\s:=,#$"{}<>()'+"'"+r'])' + + # long regexes generated by _regex_builds.py + # matches things that look like common URLs. does not match esoteric URLs. +# 'urls': r'(?:^|[][\s:=,#"{}()'+"'"+r'])([a-zA-Z][a-zA-Z0-9]{1,5}://(?:[a-zA-Z0-9_]+(?::[a-zA-Z0-9_-]+)?@)?(?:(?:[a-zA-Z0-9][\w-]*\.)*[a-zA-Z][\w-]*|(?:[0-2]?[0-9]{1,2}\.){3}[0-2]?[0-9]{1,2})(?::[0-9]{1,5})?(?:/(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)?/?)?(?:\?(?:(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)+(?:=(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)?)?&)*(?:(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)+(?:=(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)?)?)?)?(?:#(?:(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)+(?:=(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)?)?&)*(?:(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)+(?:=(?:[\w.~%/&-]+|(?:[\w.~%/&-]*\([\w.~%/&-]*\)[\w.~%/&-]*)+)?)?)?)?)(?:$|[][\s:=,#"{}()'+"'"+r'])', + # matches things that look like UNIX or Windows style absolute paths +# 'abspaths': r'(?:^|[][\s:=,#$"{}()'+"'"+r'])((?:/|~/|[A-Z]:[\\/])(?:(?:[a-zA-Z0-9_-]{1,30}|[a-zA-Z0-9_-]{1,25}\\? [a-zA-Z0-9_-]{1,25})[\\/])*(?:(?:[a-zA-Z0-9_-]{1,30}|[a-zA-Z0-9_-]{1,25}\\? [a-zA-Z0-9_-]{1,25})\.[a-zA-Z0-9]{1,6}|[a-zA-Z0-9_-]{1,30}[\\/]?))(?:$|[][\s:=,#$"{}()'+"'"+r'])', + # matches both things that look like absolute paths, and relative paths with at least one separator + # may also match spaces in path components, but never at the beginning or end, and only in an inner component +# 'paths': r'(?:^|[][\s:=,#$"{}()'+"'"+r'])((?:(?:/|~/|[A-Z]:[\\/])(?:(?:[a-zA-Z0-9_-]{1,30}|[a-zA-Z0-9_-]{1,25}\\? [a-zA-Z0-9_-]{1,25})[\\/])*(?:(?:[a-zA-Z0-9_-]{1,30}|[a-zA-Z0-9_-]{1,25}\\? [a-zA-Z0-9_-]{1,25})\.[a-zA-Z0-9]{1,6}|[a-zA-Z0-9_-]{1,30}[\\/]?)|[a-zA-Z0-9_-]{1,30}[\\/](?:[a-zA-Z0-9_-]{1,30}[\\/]?|(?:[a-zA-Z0-9_-]{1,30}|[a-zA-Z0-9_-]{1,25}\\? [a-zA-Z0-9_-]{1,25})\.[a-zA-Z0-9]{1,6})|[a-zA-Z0-9_-]{1,30}[\\/](?:(?:[a-zA-Z0-9_-]{1,30}|[a-zA-Z0-9_-]{1,25}\\? [a-zA-Z0-9_-]{1,25})[\\/])+(?:[a-zA-Z0-9_-]{1,30}[\\/]?|(?:[a-zA-Z0-9_-]{1,30}|[a-zA-Z0-9_-]{1,25}\\? [a-zA-Z0-9_-]{1,25})\.[a-zA-Z0-9]{1,6})))(?:$|[][\s:=,#$"{}()'+"'"+r'])', + # matches things that look like an isolated filename (no path) +# 'filenames': r'(?:^|[][\s:=,#$"{}()'+"'"+r'])([a-zA-Z0-9_-]{1,30}\.[a-zA-Z][a-zA-Z0-9]{0,5})(?:$|[][\s:=,#$"{}()'+"'"+r'])' } @@ -764,6 +781,8 @@ class EasyMotionAction(PaneJumpAction): locs = self._em_filter_locs(locs) if filter_locs: locs = [ l for l in locs if filter_locs(l) ] + if len(locs) == 0: + raise ActionCanceled() self._em_sort_locs_cursor_proximity(locs, sort_close_to) # Assign each match a label @@ -1024,6 +1043,7 @@ class QuickCopyAction(PaneJumpAction): log('quickcopy run') # Get a list of all matches matches = self.find_matches() + if len(matches) == 0: return log('got matches') # Group them into display batches diff --git a/copytk.tmux b/copytk.tmux index 3071a3e..da9bbd0 100755 --- a/copytk.tmux +++ b/copytk.tmux @@ -29,7 +29,13 @@ tmux bind-key -T copy-mode-vi S switch-client -T copytk tmux bind-key -T copy-mode S switch-client -T copytk +# Match URLs tmux set -g @copytk-quickcopy-match-0-0 urls -tmux set -g @copytk-quickcopy-match-1-0 '[0-9]{4,}' -tmux set -g @copytk-quickcopy-match-2-0 lines +tmux set -g @copytk-quickcopy-match-0-1 abspaths +tmux set -g @copytk-quickcopy-match-1-0 paths +tmux set -g @copytk-quickcopy-match-1-0 filenames +# Match numbers +tmux set -g @copytk-quickcopy-match-2-0 '-?[0-9]+(?:\.[0-9]+)?(?:[eE]-?[0-9]+)?' +# Match whole lines +tmux set -g @copytk-quickcopy-match-3-0 lines