From 5b3f5dd02dde95c9163c2917956febf96c1622e5 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 9 Mar 2023 20:53:46 +0530 Subject: [PATCH] Port all remaining hints matching tests --- gen-go-code.py | 3 ++ kittens/hints/main.py | 12 +++--- kitty_tests/hints.py | 80 ----------------------------------- tools/cmd/hints/marks.go | 54 +++++++++++++++-------- tools/cmd/hints/marks_test.go | 63 +++++++++++++++++++++++++-- 5 files changed, 105 insertions(+), 107 deletions(-) delete mode 100644 kitty_tests/hints.py diff --git a/gen-go-code.py b/gen-go-code.py index 3c0d60bf5..c784d0961 100755 --- a/gen-go-code.py +++ b/gen-go-code.py @@ -457,8 +457,10 @@ def load_ref_map() -> Dict[str, Dict[str, str]]: def generate_constants() -> str: + from kittens.hints.main import DEFAULT_REGEX from kitty.options.types import Options from kitty.options.utils import allowed_shell_integration_values + del sys.modules['kittens.hints.main'] ref_map = load_ref_map() with open('kitty/data-types.h') as dt: m = re.search(r'^#define IMAGE_PLACEHOLDER_CHAR (\S+)', dt.read(), flags=re.M) @@ -481,6 +483,7 @@ const RC_ENCRYPTION_PROTOCOL_VERSION string = "{kc.RC_ENCRYPTION_PROTOCOL_VERSIO const IsFrozenBuild bool = false const IsStandaloneBuild bool = false const HandleTermiosSignals = {Mode.HANDLE_TERMIOS_SIGNALS.value[0]} +const HintsDefaultRegex = `{DEFAULT_REGEX}` var Version VersionType = VersionType{{Major: {kc.version.major}, Minor: {kc.version.minor}, Patch: {kc.version.patch},}} var DefaultPager []string = []string{{ {dp} }} var FunctionalKeyNameAliases = map[string]string{serialize_go_dict(functional_key_name_aliases)} diff --git a/kittens/hints/main.py b/kittens/hints/main.py index d141443f4..f75369881 100644 --- a/kittens/hints/main.py +++ b/kittens/hints/main.py @@ -726,15 +726,10 @@ def main(args: List[str]) -> Optional[Dict[str, Any]]: def linenum_process_result(data: Dict[str, Any]) -> Tuple[str, int]: - lnum_pat = re.compile(r'(:\d+)$') for match, g in zip(data['match'], data['groupdicts']): path, line = g['path'], g['line'] if path and line: - m = lnum_pat.search(path) - if m is not None: - line = m.group(1)[1:] - path = path.rpartition(':')[0] - return os.path.expanduser(path), int(line) + return path, int(line) return '', -1 @@ -782,7 +777,10 @@ def linenum_handle_result(args: List[str], data: Dict[str, Any], target_window_i @result_handler(type_of_input='screen-ansi', has_ready_notification=Hints.overlay_ready_report_needed) def handle_result(args: List[str], data: Dict[str, Any], target_window_id: int, boss: BossType) -> None: - if data['customize_processing']: + cp = data['customize_processing'] + if data['type'] == 'linenum': + cp = '::linenum::' + if cp: m = load_custom_processor(data['customize_processing']) if 'handle_result' in m: m['handle_result'](args, data, target_window_id, boss, data['extra_cli_args']) diff --git a/kitty_tests/hints.py b/kitty_tests/hints.py deleted file mode 100644 index 6e56f76d5..000000000 --- a/kitty_tests/hints.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env python3 -# License: GPL v3 Copyright: 2018, Kovid Goyal - -import os - -from . import BaseTest - - -class TestHints(BaseTest): - - def test_url_hints(self): - from kittens.hints.main import Mark, convert_text, functions_for, linenum_marks, linenum_process_result, mark, parse_hints_args, process_escape_codes - args = parse_hints_args([])[0] - pattern, post_processors = functions_for(args) - - def create_marks(text, cols=20, mark=mark): - text = convert_text(text, cols) - text, _ = process_escape_codes(text) - return tuple(mark(pattern, post_processors, text, args)) - - def t(text, url, cols=20): - marks = create_marks(text, cols) - urls = [m.text for m in marks] - self.ae(urls, [url]) - - u = 'http://test.me/' - t(u, 'http://test.me/') - t(f'"{u}"', u) - t(f'({u})', u) - t(u + '\nxxx', u + 'xxx', len(u)) - t(f'link:{u}[xxx]', u) - t(f'`xyz <{u}>`_.', u) - t(f'moo', u) - t('\x1b[mhttp://test.me/1234\n\x1b[mx', 'http://test.me/1234') - t('\x1b[mhttp://test.me/12345\r\x1b[m6\n\x1b[mx', 'http://test.me/123456') - - def m(text, path, line, cols=20): - - def adapt(pattern, postprocessors, text, *a): - return linenum_marks(text, args, Mark, ()) - - marks = create_marks(text, cols, mark=adapt) - data = {'groupdicts': [m.groupdict for m in marks], 'match': [m.text for m in marks]} - self.ae(linenum_process_result(data), (path, line)) - - args = parse_hints_args('--type=linenum'.split())[0] - m('file.c:23', 'file.c', 23) - m('file.c:23:32', 'file.c', 23) - m('file.cpp:23:1', 'file.cpp', 23) - m('a/file.c:23', 'a/file.c', 23) - m('a/file.c:23:32', 'a/file.c', 23) - m('~/file.c:23:32', os.path.expanduser('~/file.c'), 23) - - def test_ip_hints(self): - from kittens.hints.main import convert_text, functions_for, mark, parse_hints_args - args = parse_hints_args(['--type', 'ip'])[0] - pattern, post_processors = functions_for(args) - - def create_marks(text, cols=60): - text = convert_text(text, cols) - return tuple(mark(pattern, post_processors, text, args)) - - testcases = ( - ('100.64.0.0', ['100.64.0.0']), - ('2001:0db8:0000:0000:0000:ff00:0042:8329', ['2001:0db8:0000:0000:0000:ff00:0042:8329']), - ('2001:db8:0:0:0:ff00:42:8329', ['2001:db8:0:0:0:ff00:42:8329']), - ('2001:db8::ff00:42:8329', ['2001:db8::ff00:42:8329']), - ('2001:DB8::FF00:42:8329', ['2001:DB8::FF00:42:8329']), - ('0000:0000:0000:0000:0000:0000:0000:0001', ['0000:0000:0000:0000:0000:0000:0000:0001']), - ('::1', ['::1']), - # Invalid IPs won't match - ('255.255.255.256', []), - (':1', []), - ) - - for testcase, expected in testcases: - with self.subTest(testcase=testcase, expected=expected): - marks = create_marks(testcase) - ips = [m.text for m in marks] - self.ae(ips, expected) diff --git a/tools/cmd/hints/marks.go b/tools/cmd/hints/marks.go index 2a167d134..168deea4a 100644 --- a/tools/cmd/hints/marks.go +++ b/tools/cmd/hints/marks.go @@ -20,12 +20,11 @@ var _ = fmt.Print const ( DEFAULT_HINT_ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz" - DEFAULT_REGEX = `(?m)^\s*(.+)\s*$` - FILE_EXTENSION = `\.(?:[a-zA-Z0-9]{2,7}|[ahcmo])(?!\.)` + FILE_EXTENSION = `\.(?:[a-zA-Z0-9]{2,7}|[ahcmo])(?:\b|[^.])` ) func path_regex() string { - return fmt.Sprintf(`(?:\S*?/[\r\S]+)|(?:\S[\r\S]*{%s})\b`, FILE_EXTENSION) + return fmt.Sprintf(`(?:\S*?/[\r\S]+)|(?:\S[\r\S]*%s)\b`, FILE_EXTENSION) } func default_linenum_regex() string { @@ -84,6 +83,7 @@ func process_escape_codes(text string) (ans string, hyperlinks []Mark) { } type PostProcessorFunc = func(string, int, int) (int, int) +type GroupProcessorFunc = func(map[string]string) func is_punctuation(b string) bool { switch b { @@ -143,6 +143,15 @@ func matching_remover(openers ...string) PostProcessorFunc { } } +func linenum_group_processor(gd map[string]string) { + pat := utils.MustCompile(`:\d+$`) + gd[`path`] = pat.ReplaceAllStringFunc(gd["path"], func(m string) string { + gd["line"] = m[1:] + return `` + }) + gd[`path`] = utils.Expanduser(gd[`path`]) +} + var PostProcessorMap = (&utils.Once[map[string]PostProcessorFunc]{Run: func() map[string]PostProcessorFunc { return map[string]PostProcessorFunc{ "url": func(text string, s, e int) (int, int) { @@ -211,7 +220,7 @@ var RelevantKittyOpts = (&utils.Once[KittyOpts]{Run: func() KittyOpts { return read_relevant_kitty_opts(filepath.Join(utils.ConfigDir(), "kitty.conf")) }}).Get -func functions_for(opts *Options) (pattern string, post_processors []PostProcessorFunc) { +func functions_for(opts *Options) (pattern string, post_processors []PostProcessorFunc, group_processors []GroupProcessorFunc) { switch opts.Type { case "url": var url_prefixes *utils.Set[string] @@ -247,15 +256,17 @@ func functions_for(opts *Options) (pattern string, post_processors []PostProcess default: pattern = opts.Regex if opts.Type == "linenum" { - if pattern == DEFAULT_REGEX { + if pattern == kitty.HintsDefaultRegex { pattern = default_linenum_regex() } + post_processors = append(post_processors, PostProcessorMap()["brackets"], PostProcessorMap()["quotes"]) + group_processors = append(group_processors, linenum_group_processor) } } return } -func mark(r *regexp.Regexp, post_processors []PostProcessorFunc, text string, opts *Options) (ans []Mark) { +func mark(r *regexp.Regexp, post_processors []PostProcessorFunc, group_processors []GroupProcessorFunc, text string, opts *Options) (ans []Mark) { sanitize_pat := regexp.MustCompile("[\r\n\x00]") names := r.SubexpNames() for i, v := range r.FindAllStringSubmatchIndex(text, -1) { @@ -281,13 +292,16 @@ func mark(r *regexp.Regexp, post_processors []PostProcessorFunc, text string, op for x, name := range names { if name != "" { idx := 2 * x - if s, e := v[idx], v[idx]+1; s > -1 && e > -1 { + if s, e := v[idx], v[idx+1]; s > -1 && e > -1 { s = utils.Max(s, match_start) e = utils.Min(e, match_end) gd[name] = sanitize_pat.ReplaceAllLiteralString(text[s:e], "") } } } + for _, f := range group_processors { + f(gd) + } ans = append(ans, Mark{ Index: i, Start: match_start, End: match_end, Text: full_match, Groupdict: gd, }) @@ -295,9 +309,22 @@ func mark(r *regexp.Regexp, post_processors []PostProcessorFunc, text string, op return } +type ErrNoMatches struct{ Type string } + +func (self *ErrNoMatches) Error() string { + none_of := "matches" + switch self.Type { + case "urls": + none_of = "URLs" + case "hyperlinks": + none_of = "hyperlinks" + } + return fmt.Sprintf("No %s found", none_of) +} + func find_marks(text string, opts *Options) (ans []Mark, index_map map[int]*Mark, err error) { text, hyperlinks := process_escape_codes(text) - pattern, post_processors := functions_for(opts) + pattern, post_processors, group_processors := functions_for(opts) if opts.Type == "hyperlink" { ans = hyperlinks } else { @@ -305,17 +332,10 @@ func find_marks(text string, opts *Options) (ans []Mark, index_map map[int]*Mark if err != nil { return nil, nil, fmt.Errorf("Failed to compile the regex pattern: %#v with error: %w", pattern, err) } - ans = mark(r, post_processors, text, opts) + ans = mark(r, post_processors, group_processors, text, opts) } if len(ans) == 0 { - none_of := "matches" - switch opts.Type { - case "urls": - none_of = "URLs" - case "hyperlinks": - none_of = "hyperlinks" - } - return nil, nil, fmt.Errorf("No %s found", none_of) + return nil, nil, &ErrNoMatches{Type: opts.Type} } largest_index := ans[len(ans)-1].Index offset := utils.Max(0, opts.HintsOffset) diff --git a/tools/cmd/hints/marks_test.go b/tools/cmd/hints/marks_test.go index 93b6b720b..9b28aba48 100644 --- a/tools/cmd/hints/marks_test.go +++ b/tools/cmd/hints/marks_test.go @@ -3,8 +3,11 @@ package hints import ( + "errors" "fmt" + "kitty" "kitty/tools/utils" + "strconv" "testing" "github.com/google/go-cmp/cmp" @@ -14,12 +17,17 @@ var _ = fmt.Print func TestHintMarking(t *testing.T) { - opts := &Options{Type: "url"} + opts := &Options{Type: "url", UrlPrefixes: "default", Regex: kitty.HintsDefaultRegex} + cols := 20 r := func(text string, url ...string) { - ptext := convert_text(text, 20) + ptext := convert_text(text, cols) marks, _, err := find_marks(ptext, opts) if err != nil { - t.Fatalf("%#v failed with error: %s", text, err) + var e *ErrNoMatches + if len(url) != 0 || !errors.As(err, &e) { + t.Fatalf("%#v failed with error: %s", text, err) + } + return } actual := utils.Map(func(m Mark) string { return m.Text }, marks) if diff := cmp.Diff(url, actual); diff != "" { @@ -29,4 +37,53 @@ func TestHintMarking(t *testing.T) { u := `http://test.me/` r(u, u) + r(`"`+u+`"`, u) + r("("+u+")", u) + cols = len(u) + r(u+"\nxxx", u+"xxx") + cols = 20 + r("link:"+u+"[xxx]", u) + r("`xyz <"+u+">`_.", u) + r(`moo`, u) + r("\x1b[mhttp://test.me/1234\n\x1b[mx", "http://test.me/1234") + r("\x1b[mhttp://test.me/12345\r\x1b[m6\n\x1b[mx", "http://test.me/123456") + + opts.Type = "linenum" + m := func(text, path string, line int) { + ptext := convert_text(text, cols) + marks, _, err := find_marks(ptext, opts) + if err != nil { + t.Fatalf("%#v failed with error: %s", text, err) + } + gd := map[string]string{"path": path, "line": strconv.Itoa(line)} + if diff := cmp.Diff(marks[0].Groupdict, gd); diff != "" { + t.Fatalf("%#v failed:\n%s", text, diff) + } + } + m("file.c:23", "file.c", 23) + m("file.c:23:32", "file.c", 23) + m("file.cpp:23:1", "file.cpp", 23) + m("a/file.c:23", "a/file.c", 23) + m("a/file.c:23:32", "a/file.c", 23) + m("~/file.c:23:32", utils.Expanduser("~/file.c"), 23) + + opts.Type = "path" + r("file.c", "file.c") + r("file.c.", "file.c") + r("file.epub.", "file.epub") + r("(file.epub)", "file.epub") + r("some/path", "some/path") + + cols = 60 + opts.Type = "ip" + r(`100.64.0.0`, `100.64.0.0`) + r(`2001:0db8:0000:0000:0000:ff00:0042:8329`, `2001:0db8:0000:0000:0000:ff00:0042:8329`) + r(`2001:db8:0:0:0:ff00:42:8329`, `2001:db8:0:0:0:ff00:42:8329`) + r(`2001:db8::ff00:42:8329`, `2001:db8::ff00:42:8329`) + r(`2001:DB8::FF00:42:8329`, `2001:DB8::FF00:42:8329`) + r(`0000:0000:0000:0000:0000:0000:0000:0001`, `0000:0000:0000:0000:0000:0000:0000:0001`) + r(`::1`, `::1`) + r(`255.255.255.256`) + r(`:1`) + }