From 9bc1b5a2d94faca83a8511d8afae4089a63b9438 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 10 Jan 2020 10:01:03 +0530 Subject: [PATCH] hints kitten: Allow pressing :sc:`goto_file_line` to quickly open the selected file at the selected line in vim Fixes #2268 --- docs/changelog.rst | 3 ++ docs/kittens/hints.rst | 5 +++ kittens/hints/main.py | 72 ++++++++++++++++++++++++++++++++++++++---- kitty/config_data.py | 5 +++ kitty/screen.c | 10 ++++++ kitty/window.py | 6 ++++ 6 files changed, 94 insertions(+), 7 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 2b77b6c2e..fc9b4238b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,6 +7,9 @@ To update |kitty|, :doc:`follow the instructions `. 0.15.2 [future] -------------------- +- hints kitten: Allow pressing :sc:`goto_file_line` to quickly open + the selected file at the selected line in vim (:iss:`2268`) + - Allow choosing OpenType features for individual fonts via the :opt:`font_features` option. diff --git a/docs/kittens/hints.rst b/docs/kittens/hints.rst index 7347d9c31..3f0c95f86 100644 --- a/docs/kittens/hints.rst +++ b/docs/kittens/hints.rst @@ -18,6 +18,11 @@ select anything that looks like a path or filename and then insert it into the terminal, very useful for picking files from the output of a ``git`` or ``ls`` command and adding them to the command line for the next command. +You can also press :sc:`goto_file_line` to select anything that looks +like a path or filename followed by a colon and a line number and open +the file in vim at the specified line number. The patterns and editor +to be used can be modified using options passed to the kitten. + The hints kitten is very powerful to see more detailed help on its various options and modes of operation, see below. You can use these options to create mappings in :file:`kitty.conf` to select various different text diff --git a/kittens/hints/main.py b/kittens/hints/main.py index edd967036..dc00d9999 100644 --- a/kittens/hints/main.py +++ b/kittens/hints/main.py @@ -21,6 +21,7 @@ from ..tui.operations import faint, styled URL_PREFIXES = 'http https file ftp'.split() DEFAULT_HINT_ALPHABET = string.digits + string.ascii_lowercase +DEFAULT_REGEX = r'(?m)^\s*(.+)\s*$' screen_size = screen_size_function() @@ -265,7 +266,7 @@ def run_loop(args, text, all_marks, index_map, extra_cli_args=()): return { 'match': handler.text_matches, 'programs': args.program, 'multiple_joiner': args.multiple_joiner, 'customize_processing': args.customize_processing, - 'type': args.type, 'groupdicts': handler.groupdicts, 'extra_cli_args': extra_cli_args + 'type': args.type, 'groupdicts': handler.groupdicts, 'extra_cli_args': extra_cli_args, 'linenum_action': args.linenum_action } raise SystemExit(loop.return_code) @@ -323,11 +324,20 @@ def parse_input(text): return convert_text(text, cols) +def linenum_marks(text, args, Mark, extra_cli_args, *a): + regex = args.regex + if regex == DEFAULT_REGEX: + regex = r'(?P(?:\S*/\S+)|(?:\S+[.][a-zA-Z0-9]{2,7})):(?P\d+)' + yield from mark(regex, [brackets, quotes], text, args) + + def load_custom_processor(customize_processing): if customize_processing.startswith('::import::'): import importlib m = importlib.import_module(customize_processing[len('::import::'):]) return {k: getattr(m, k) for k in dir(m)} + if customize_processing == '::linenum::': + return {'mark': linenum_marks, 'handle_result': linenum_handle_result} from kitty.constants import config_dir customize_processing = os.path.expandvars(os.path.expanduser(customize_processing)) if os.path.isabs(customize_processing): @@ -342,6 +352,8 @@ def run(args, text, extra_cli_args=()): try: text = parse_input(text) pattern, post_processors = functions_for(args) + if args.type == 'linenum': + args.customize_processing = '::linenum::' if args.customize_processing: m = load_custom_processor(args.customize_processing) if 'mark' in m: @@ -386,12 +398,15 @@ multiple times to run multiple programs. --type default=url -choices=url,regex,path,line,hash,word -The type of text to search for. +choices=url,regex,path,line,hash,word,linenum +The type of text to search for. A value of :code:`linenum` looks for error messages +using the pattern specified with :option:`--regex`, which must have the named groups, +path and line. If not specified, will look for :code:`path:line`. +The :option:`--linenum-action` option controls what to do with the selected error message. --regex -default=(?m)^\s*(.+)\s*$ +default={default_regex} The regular expression to use when :option:`kitty +kitten hints --type`=regex. The regular expression is in python syntax. If you specify a numbered group in the regular expression only the group will be matched. This allow you to match @@ -403,8 +418,21 @@ the program will be passed arguments corresponding to each named group of the form key=value. +--linenum-action +default=self +type=choice +choices=self,window,tab,os_window,background +The action to perform on the matched errors. The actual action is whatever +arguments are provided to the kitten, for example: +:code:`kitty + kitten hints --type=linenum vim +{line} {path}` +will open the matched path at the matched line number in vim. This option +controls where the action is executed: :code:`self` means the current window, +:code:`window` a new kitty window, :code:`tab` a new tab, :code:`os_window` +a new OS window and :code:`background` run in the background. + + --url-prefixes -default={0} +default={url_prefixes} Comma separated list of recognized URL prefixes. @@ -470,7 +498,10 @@ on selected matches. See https://sw.kovidgoyal.net/kitty/kittens/hints.html for details. You can also specify absolute paths to load the script from elsewhere. -'''.format(','.join(sorted(URL_PREFIXES))).format +'''.format( + default_regex=DEFAULT_REGEX, url_prefixes=','.join(sorted(URL_PREFIXES)), + line='{{line}}', path='{{path}}' +).format help_text = 'Select text from the screen using the keyboard. Defaults to searching for URLs.' usage = '' @@ -496,12 +527,39 @@ def main(args): print(e.args[0], file=sys.stderr) input(_('Press Enter to quit')) return - if items and not args.customize_processing: + if items and not (args.customize_processing or args.type == 'linenum'): print('Extra command line arguments present: {}'.format(' '.join(items)), file=sys.stderr) input(_('Press Enter to quit')) return run(args, text, items) +def linenum_handle_result(args, data, target_window_id, boss, extra_cli_args, *a): + for m, g in zip(data['match'], data['groupdicts']): + if m: + path, line = g['path'], g['line'] + path = path.split(':')[-1] + line = int(line) + break + else: + return + + cmd = [x.format(path=path, line=line) for x in extra_cli_args or ('vim', '+{line}', '{path}')] + w = boss.window_id_map.get(target_window_id) + action = data['linenum_action'] + + if action == 'self': + if w is not None: + import shlex + w.paste_bytes(shlex.join(cmd) + '\r') + elif action == 'background': + import subprocess + subprocess.Popen(cmd) + else: + getattr(boss, { + 'window': 'new_window_with_cwd', 'tab': 'new_tab_with_cwd', 'os_window': 'new_os_window_with_cwd' + }[action])(*cmd) + + def handle_result(args, data, target_window_id, boss): if data['customize_processing']: m = load_custom_processor(data['customize_processing']) diff --git a/kitty/config_data.py b/kitty/config_data.py index ed336cb00..49b2bb242 100644 --- a/kitty/config_data.py +++ b/kitty/config_data.py @@ -1274,6 +1274,11 @@ k('insert_selected_hash', 'kitty_mod+p>h', 'kitten hints --type hash --program - Select something that looks like a hash and insert it into the terminal. Useful with git, which uses sha1 hashes to identify commits''')) +k('goto_file_line', 'kitty_mod+p>n', 'kitten hints --type linenum', _('Open the selected file at the selected line'), long_text=_(''' +Select something that looks like :code:`filename:linenum` and open it in vim at +the specified line number.''')) + + # }}} g('shortcuts.misc') # {{{ diff --git a/kitty/screen.c b/kitty/screen.c index e38fedc00..b09ea6a49 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -2243,12 +2243,21 @@ send_escape_code_to_child(Screen *self, PyObject *args) { static PyObject* paste(Screen *self, PyObject *bytes) { + if (!PyBytes_Check(bytes)) { PyErr_SetString(PyExc_TypeError, "Must paste() bytes"); return NULL; } if (self->modes.mBRACKETED_PASTE) write_escape_code_to_child(self, CSI, BRACKETED_PASTE_START); write_to_child(self, PyBytes_AS_STRING(bytes), PyBytes_GET_SIZE(bytes)); if (self->modes.mBRACKETED_PASTE) write_escape_code_to_child(self, CSI, BRACKETED_PASTE_END); Py_RETURN_NONE; } +static PyObject* +paste_bytes(Screen *self, PyObject *bytes) { + if (!PyBytes_Check(bytes)) { PyErr_SetString(PyExc_TypeError, "Must paste() bytes"); return NULL; } + write_to_child(self, PyBytes_AS_STRING(bytes), PyBytes_GET_SIZE(bytes)); + Py_RETURN_NONE; +} + + WRAP2(cursor_position, 1, 1) #define COUNT_WRAP(name) WRAP1(name, 1) @@ -2329,6 +2338,7 @@ static PyMethodDef methods[] = { MND(toggle_alt_screen, METH_NOARGS) MND(reset_callbacks, METH_NOARGS) MND(paste, METH_O) + MND(paste_bytes, METH_O) MND(copy_colors_from, METH_O) {"select_graphic_rendition", (PyCFunction)_select_graphic_rendition, METH_VARARGS, ""}, diff --git a/kitty/window.py b/kitty/window.py index b9e2c6d85..bfaeaa70a 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -535,6 +535,12 @@ class Window: cmd = [x.replace('INPUT_LINE_NUMBER', str(data['input_line_number'])) for x in self.opts.scrollback_pager] get_boss().display_scrollback(self, data['text'], cmd) + def paste_bytes(self, text): + # paste raw bytes without any processing + if isinstance(text, str): + text = text.encode('utf-8') + self.screen.paste_bytes(text) + def paste(self, text): if text and not self.destroyed: if isinstance(text, str):