diff options
author | shadchin <[email protected]> | 2022-02-10 16:44:30 +0300 |
---|---|---|
committer | Daniil Cherednik <[email protected]> | 2022-02-10 16:44:30 +0300 |
commit | 2598ef1d0aee359b4b6d5fdd1758916d5907d04f (patch) | |
tree | 012bb94d777798f1f56ac1cec429509766d05181 /contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings | |
parent | 6751af0b0c1b952fede40b19b71da8025b5d8bcf (diff) |
Restoring authorship annotation for <[email protected]>. Commit 1 of 2.
Diffstat (limited to 'contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings')
13 files changed, 4790 insertions, 4790 deletions
diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/auto_suggest.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/auto_suggest.py index c016c0688fe..a45e5a8befb 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/auto_suggest.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/auto_suggest.py @@ -1,63 +1,63 @@ -""" -Key bindings for auto suggestion (for fish-style auto suggestion). -""" -import re - -from prompt_toolkit.application.current import get_app -from prompt_toolkit.filters import Condition, emacs_mode -from prompt_toolkit.key_binding.key_bindings import KeyBindings -from prompt_toolkit.key_binding.key_processor import KeyPressEvent - -__all__ = [ - "load_auto_suggest_bindings", -] - -E = KeyPressEvent - - -def load_auto_suggest_bindings() -> KeyBindings: - """ - Key bindings for accepting auto suggestion text. - - (This has to come after the Vi bindings, because they also have an - implementation for the "right arrow", but we really want the suggestion - binding when a suggestion is available.) - """ - key_bindings = KeyBindings() - handle = key_bindings.add - - @Condition - def suggestion_available() -> bool: - app = get_app() - return ( - app.current_buffer.suggestion is not None - and len(app.current_buffer.suggestion.text) > 0 - and app.current_buffer.document.is_cursor_at_the_end - ) - - @handle("c-f", filter=suggestion_available) - @handle("c-e", filter=suggestion_available) - @handle("right", filter=suggestion_available) - def _accept(event: E) -> None: - """ - Accept suggestion. - """ - b = event.current_buffer - suggestion = b.suggestion - - if suggestion: - b.insert_text(suggestion.text) - - @handle("escape", "f", filter=suggestion_available & emacs_mode) - def _fill(event: E) -> None: - """ - Fill partial suggestion. - """ - b = event.current_buffer - suggestion = b.suggestion - - if suggestion: - t = re.split(r"(\S+\s+)", suggestion.text) - b.insert_text(next(x for x in t if x)) - - return key_bindings +""" +Key bindings for auto suggestion (for fish-style auto suggestion). +""" +import re + +from prompt_toolkit.application.current import get_app +from prompt_toolkit.filters import Condition, emacs_mode +from prompt_toolkit.key_binding.key_bindings import KeyBindings +from prompt_toolkit.key_binding.key_processor import KeyPressEvent + +__all__ = [ + "load_auto_suggest_bindings", +] + +E = KeyPressEvent + + +def load_auto_suggest_bindings() -> KeyBindings: + """ + Key bindings for accepting auto suggestion text. + + (This has to come after the Vi bindings, because they also have an + implementation for the "right arrow", but we really want the suggestion + binding when a suggestion is available.) + """ + key_bindings = KeyBindings() + handle = key_bindings.add + + @Condition + def suggestion_available() -> bool: + app = get_app() + return ( + app.current_buffer.suggestion is not None + and len(app.current_buffer.suggestion.text) > 0 + and app.current_buffer.document.is_cursor_at_the_end + ) + + @handle("c-f", filter=suggestion_available) + @handle("c-e", filter=suggestion_available) + @handle("right", filter=suggestion_available) + def _accept(event: E) -> None: + """ + Accept suggestion. + """ + b = event.current_buffer + suggestion = b.suggestion + + if suggestion: + b.insert_text(suggestion.text) + + @handle("escape", "f", filter=suggestion_available & emacs_mode) + def _fill(event: E) -> None: + """ + Fill partial suggestion. + """ + b = event.current_buffer + suggestion = b.suggestion + + if suggestion: + t = re.split(r"(\S+\s+)", suggestion.text) + b.insert_text(next(x for x in t if x)) + + return key_bindings diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/basic.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/basic.py index fc8f9643596..f108a32cdc6 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/basic.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/basic.py @@ -1,253 +1,253 @@ -# pylint: disable=function-redefined -from prompt_toolkit.application.current import get_app -from prompt_toolkit.filters import ( - Condition, - emacs_insert_mode, - has_selection, - in_paste_mode, - is_multiline, - vi_insert_mode, -) -from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent -from prompt_toolkit.keys import Keys - -from ..key_bindings import KeyBindings -from .named_commands import get_by_name - -__all__ = [ - "load_basic_bindings", -] - -E = KeyPressEvent - - -def if_no_repeat(event: E) -> bool: - """Callable that returns True when the previous event was delivered to - another handler.""" - return not event.is_repeat - - -def load_basic_bindings() -> KeyBindings: - key_bindings = KeyBindings() - insert_mode = vi_insert_mode | emacs_insert_mode - handle = key_bindings.add - - @handle("c-a") - @handle("c-b") - @handle("c-c") - @handle("c-d") - @handle("c-e") - @handle("c-f") - @handle("c-g") - @handle("c-h") - @handle("c-i") - @handle("c-j") - @handle("c-k") - @handle("c-l") - @handle("c-m") - @handle("c-n") - @handle("c-o") - @handle("c-p") - @handle("c-q") - @handle("c-r") - @handle("c-s") - @handle("c-t") - @handle("c-u") - @handle("c-v") - @handle("c-w") - @handle("c-x") - @handle("c-y") - @handle("c-z") - @handle("f1") - @handle("f2") - @handle("f3") - @handle("f4") - @handle("f5") - @handle("f6") - @handle("f7") - @handle("f8") - @handle("f9") - @handle("f10") - @handle("f11") - @handle("f12") - @handle("f13") - @handle("f14") - @handle("f15") - @handle("f16") - @handle("f17") - @handle("f18") - @handle("f19") - @handle("f20") - @handle("f21") - @handle("f22") - @handle("f23") - @handle("f24") - @handle("c-@") # Also c-space. - @handle("c-\\") - @handle("c-]") - @handle("c-^") - @handle("c-_") - @handle("backspace") - @handle("up") - @handle("down") - @handle("right") - @handle("left") - @handle("s-up") - @handle("s-down") - @handle("s-right") - @handle("s-left") - @handle("home") - @handle("end") - @handle("s-home") - @handle("s-end") - @handle("delete") - @handle("s-delete") - @handle("c-delete") - @handle("pageup") - @handle("pagedown") - @handle("s-tab") - @handle("tab") - @handle("c-s-left") - @handle("c-s-right") - @handle("c-s-home") - @handle("c-s-end") - @handle("c-left") - @handle("c-right") - @handle("c-up") - @handle("c-down") - @handle("c-home") - @handle("c-end") - @handle("insert") - @handle("s-insert") - @handle("c-insert") +# pylint: disable=function-redefined +from prompt_toolkit.application.current import get_app +from prompt_toolkit.filters import ( + Condition, + emacs_insert_mode, + has_selection, + in_paste_mode, + is_multiline, + vi_insert_mode, +) +from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent +from prompt_toolkit.keys import Keys + +from ..key_bindings import KeyBindings +from .named_commands import get_by_name + +__all__ = [ + "load_basic_bindings", +] + +E = KeyPressEvent + + +def if_no_repeat(event: E) -> bool: + """Callable that returns True when the previous event was delivered to + another handler.""" + return not event.is_repeat + + +def load_basic_bindings() -> KeyBindings: + key_bindings = KeyBindings() + insert_mode = vi_insert_mode | emacs_insert_mode + handle = key_bindings.add + + @handle("c-a") + @handle("c-b") + @handle("c-c") + @handle("c-d") + @handle("c-e") + @handle("c-f") + @handle("c-g") + @handle("c-h") + @handle("c-i") + @handle("c-j") + @handle("c-k") + @handle("c-l") + @handle("c-m") + @handle("c-n") + @handle("c-o") + @handle("c-p") + @handle("c-q") + @handle("c-r") + @handle("c-s") + @handle("c-t") + @handle("c-u") + @handle("c-v") + @handle("c-w") + @handle("c-x") + @handle("c-y") + @handle("c-z") + @handle("f1") + @handle("f2") + @handle("f3") + @handle("f4") + @handle("f5") + @handle("f6") + @handle("f7") + @handle("f8") + @handle("f9") + @handle("f10") + @handle("f11") + @handle("f12") + @handle("f13") + @handle("f14") + @handle("f15") + @handle("f16") + @handle("f17") + @handle("f18") + @handle("f19") + @handle("f20") + @handle("f21") + @handle("f22") + @handle("f23") + @handle("f24") + @handle("c-@") # Also c-space. + @handle("c-\\") + @handle("c-]") + @handle("c-^") + @handle("c-_") + @handle("backspace") + @handle("up") + @handle("down") + @handle("right") + @handle("left") + @handle("s-up") + @handle("s-down") + @handle("s-right") + @handle("s-left") + @handle("home") + @handle("end") + @handle("s-home") + @handle("s-end") + @handle("delete") + @handle("s-delete") + @handle("c-delete") + @handle("pageup") + @handle("pagedown") + @handle("s-tab") + @handle("tab") + @handle("c-s-left") + @handle("c-s-right") + @handle("c-s-home") + @handle("c-s-end") + @handle("c-left") + @handle("c-right") + @handle("c-up") + @handle("c-down") + @handle("c-home") + @handle("c-end") + @handle("insert") + @handle("s-insert") + @handle("c-insert") @handle("<sigint>") - @handle(Keys.Ignore) - def _ignore(event: E) -> None: - """ - First, for any of these keys, Don't do anything by default. Also don't - catch them in the 'Any' handler which will insert them as data. - - If people want to insert these characters as a literal, they can always - do by doing a quoted insert. (ControlQ in emacs mode, ControlV in Vi - mode.) - """ - pass - - # Readline-style bindings. - handle("home")(get_by_name("beginning-of-line")) - handle("end")(get_by_name("end-of-line")) - handle("left")(get_by_name("backward-char")) - handle("right")(get_by_name("forward-char")) - handle("c-up")(get_by_name("previous-history")) - handle("c-down")(get_by_name("next-history")) - handle("c-l")(get_by_name("clear-screen")) - - handle("c-k", filter=insert_mode)(get_by_name("kill-line")) - handle("c-u", filter=insert_mode)(get_by_name("unix-line-discard")) - handle("backspace", filter=insert_mode, save_before=if_no_repeat)( - get_by_name("backward-delete-char") - ) - handle("delete", filter=insert_mode, save_before=if_no_repeat)( - get_by_name("delete-char") - ) - handle("c-delete", filter=insert_mode, save_before=if_no_repeat)( - get_by_name("delete-char") - ) - handle(Keys.Any, filter=insert_mode, save_before=if_no_repeat)( - get_by_name("self-insert") - ) - handle("c-t", filter=insert_mode)(get_by_name("transpose-chars")) - handle("c-i", filter=insert_mode)(get_by_name("menu-complete")) - handle("s-tab", filter=insert_mode)(get_by_name("menu-complete-backward")) - - # Control-W should delete, using whitespace as separator, while M-Del - # should delete using [^a-zA-Z0-9] as a boundary. - handle("c-w", filter=insert_mode)(get_by_name("unix-word-rubout")) - - handle("pageup", filter=~has_selection)(get_by_name("previous-history")) - handle("pagedown", filter=~has_selection)(get_by_name("next-history")) - - # CTRL keys. - - @Condition - def has_text_before_cursor() -> bool: - return bool(get_app().current_buffer.text) - - handle("c-d", filter=has_text_before_cursor & insert_mode)( - get_by_name("delete-char") - ) - - @handle("enter", filter=insert_mode & is_multiline) - def _newline(event: E) -> None: - """ - Newline (in case of multiline input. - """ - event.current_buffer.newline(copy_margin=not in_paste_mode()) - - @handle("c-j") - def _newline2(event: E) -> None: - r""" - By default, handle \n as if it were a \r (enter). - (It appears that some terminals send \n instead of \r when pressing - enter. - at least the Linux subsystem for Windows.) - """ - event.key_processor.feed(KeyPress(Keys.ControlM, "\r"), first=True) - - # Delete the word before the cursor. - - @handle("up") - def _go_up(event: E) -> None: - event.current_buffer.auto_up(count=event.arg) - - @handle("down") - def _go_down(event: E) -> None: - event.current_buffer.auto_down(count=event.arg) - - @handle("delete", filter=has_selection) - def _cut(event: E) -> None: - data = event.current_buffer.cut_selection() - event.app.clipboard.set_data(data) - - # Global bindings. - - @handle("c-z") - def _insert_ctrl_z(event: E) -> None: - """ - By default, control-Z should literally insert Ctrl-Z. - (Ansi Ctrl-Z, code 26 in MSDOS means End-Of-File. - In a Python REPL for instance, it's possible to type - Control-Z followed by enter to quit.) - - When the system bindings are loaded and suspend-to-background is - supported, that will override this binding. - """ - event.current_buffer.insert_text(event.data) - - @handle(Keys.BracketedPaste) - def _paste(event: E) -> None: - """ - Pasting from clipboard. - """ - data = event.data - - # Be sure to use \n as line ending. - # Some terminals (Like iTerm2) seem to paste \r\n line endings in a - # bracketed paste. See: https://github.com/ipython/ipython/issues/9737 - data = data.replace("\r\n", "\n") - data = data.replace("\r", "\n") - - event.current_buffer.insert_text(data) - - @Condition - def in_quoted_insert() -> bool: - return get_app().quoted_insert - - @handle(Keys.Any, filter=in_quoted_insert, eager=True) - def _insert_text(event: E) -> None: - """ - Handle quoted insert. - """ - event.current_buffer.insert_text(event.data, overwrite=False) - event.app.quoted_insert = False - - return key_bindings + @handle(Keys.Ignore) + def _ignore(event: E) -> None: + """ + First, for any of these keys, Don't do anything by default. Also don't + catch them in the 'Any' handler which will insert them as data. + + If people want to insert these characters as a literal, they can always + do by doing a quoted insert. (ControlQ in emacs mode, ControlV in Vi + mode.) + """ + pass + + # Readline-style bindings. + handle("home")(get_by_name("beginning-of-line")) + handle("end")(get_by_name("end-of-line")) + handle("left")(get_by_name("backward-char")) + handle("right")(get_by_name("forward-char")) + handle("c-up")(get_by_name("previous-history")) + handle("c-down")(get_by_name("next-history")) + handle("c-l")(get_by_name("clear-screen")) + + handle("c-k", filter=insert_mode)(get_by_name("kill-line")) + handle("c-u", filter=insert_mode)(get_by_name("unix-line-discard")) + handle("backspace", filter=insert_mode, save_before=if_no_repeat)( + get_by_name("backward-delete-char") + ) + handle("delete", filter=insert_mode, save_before=if_no_repeat)( + get_by_name("delete-char") + ) + handle("c-delete", filter=insert_mode, save_before=if_no_repeat)( + get_by_name("delete-char") + ) + handle(Keys.Any, filter=insert_mode, save_before=if_no_repeat)( + get_by_name("self-insert") + ) + handle("c-t", filter=insert_mode)(get_by_name("transpose-chars")) + handle("c-i", filter=insert_mode)(get_by_name("menu-complete")) + handle("s-tab", filter=insert_mode)(get_by_name("menu-complete-backward")) + + # Control-W should delete, using whitespace as separator, while M-Del + # should delete using [^a-zA-Z0-9] as a boundary. + handle("c-w", filter=insert_mode)(get_by_name("unix-word-rubout")) + + handle("pageup", filter=~has_selection)(get_by_name("previous-history")) + handle("pagedown", filter=~has_selection)(get_by_name("next-history")) + + # CTRL keys. + + @Condition + def has_text_before_cursor() -> bool: + return bool(get_app().current_buffer.text) + + handle("c-d", filter=has_text_before_cursor & insert_mode)( + get_by_name("delete-char") + ) + + @handle("enter", filter=insert_mode & is_multiline) + def _newline(event: E) -> None: + """ + Newline (in case of multiline input. + """ + event.current_buffer.newline(copy_margin=not in_paste_mode()) + + @handle("c-j") + def _newline2(event: E) -> None: + r""" + By default, handle \n as if it were a \r (enter). + (It appears that some terminals send \n instead of \r when pressing + enter. - at least the Linux subsystem for Windows.) + """ + event.key_processor.feed(KeyPress(Keys.ControlM, "\r"), first=True) + + # Delete the word before the cursor. + + @handle("up") + def _go_up(event: E) -> None: + event.current_buffer.auto_up(count=event.arg) + + @handle("down") + def _go_down(event: E) -> None: + event.current_buffer.auto_down(count=event.arg) + + @handle("delete", filter=has_selection) + def _cut(event: E) -> None: + data = event.current_buffer.cut_selection() + event.app.clipboard.set_data(data) + + # Global bindings. + + @handle("c-z") + def _insert_ctrl_z(event: E) -> None: + """ + By default, control-Z should literally insert Ctrl-Z. + (Ansi Ctrl-Z, code 26 in MSDOS means End-Of-File. + In a Python REPL for instance, it's possible to type + Control-Z followed by enter to quit.) + + When the system bindings are loaded and suspend-to-background is + supported, that will override this binding. + """ + event.current_buffer.insert_text(event.data) + + @handle(Keys.BracketedPaste) + def _paste(event: E) -> None: + """ + Pasting from clipboard. + """ + data = event.data + + # Be sure to use \n as line ending. + # Some terminals (Like iTerm2) seem to paste \r\n line endings in a + # bracketed paste. See: https://github.com/ipython/ipython/issues/9737 + data = data.replace("\r\n", "\n") + data = data.replace("\r", "\n") + + event.current_buffer.insert_text(data) + + @Condition + def in_quoted_insert() -> bool: + return get_app().quoted_insert + + @handle(Keys.Any, filter=in_quoted_insert, eager=True) + def _insert_text(event: E) -> None: + """ + Handle quoted insert. + """ + event.current_buffer.insert_text(event.data, overwrite=False) + event.app.quoted_insert = False + + return key_bindings diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/completion.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/completion.py index e52edf87ffe..b6cd7e59f7c 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/completion.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/completion.py @@ -1,203 +1,203 @@ -""" -Key binding handlers for displaying completions. -""" -import asyncio -import math -from typing import TYPE_CHECKING, List - -from prompt_toolkit.application.run_in_terminal import in_terminal -from prompt_toolkit.completion import ( - CompleteEvent, - Completion, - get_common_complete_suffix, -) -from prompt_toolkit.formatted_text import StyleAndTextTuples -from prompt_toolkit.key_binding.key_bindings import KeyBindings -from prompt_toolkit.key_binding.key_processor import KeyPressEvent -from prompt_toolkit.keys import Keys -from prompt_toolkit.utils import get_cwidth - -if TYPE_CHECKING: - from prompt_toolkit.application import Application - from prompt_toolkit.shortcuts import PromptSession - -__all__ = [ - "generate_completions", - "display_completions_like_readline", -] - -E = KeyPressEvent - - -def generate_completions(event: E) -> None: - r""" - Tab-completion: where the first tab completes the common suffix and the - second tab lists all the completions. - """ - b = event.current_buffer - - # When already navigating through completions, select the next one. - if b.complete_state: - b.complete_next() - else: - b.start_completion(insert_common_part=True) - - -def display_completions_like_readline(event: E) -> None: - """ - Key binding handler for readline-style tab completion. - This is meant to be as similar as possible to the way how readline displays - completions. - - Generate the completions immediately (blocking) and display them above the - prompt in columns. - - Usage:: - - # Call this handler when 'Tab' has been pressed. - key_bindings.add(Keys.ControlI)(display_completions_like_readline) - """ - # Request completions. - b = event.current_buffer - if b.completer is None: - return - complete_event = CompleteEvent(completion_requested=True) - completions = list(b.completer.get_completions(b.document, complete_event)) - - # Calculate the common suffix. - common_suffix = get_common_complete_suffix(b.document, completions) - - # One completion: insert it. - if len(completions) == 1: - b.delete_before_cursor(-completions[0].start_position) - b.insert_text(completions[0].text) - # Multiple completions with common part. - elif common_suffix: - b.insert_text(common_suffix) - # Otherwise: display all completions. - elif completions: - _display_completions_like_readline(event.app, completions) - - -def _display_completions_like_readline( - app: "Application[object]", completions: List[Completion] -) -> "asyncio.Task[None]": - """ - Display the list of completions in columns above the prompt. - This will ask for a confirmation if there are too many completions to fit - on a single page and provide a paginator to walk through them. - """ - from prompt_toolkit.formatted_text import to_formatted_text - from prompt_toolkit.shortcuts.prompt import create_confirm_session - - # Get terminal dimensions. - term_size = app.output.get_size() - term_width = term_size.columns - term_height = term_size.rows - - # Calculate amount of required columns/rows for displaying the - # completions. (Keep in mind that completions are displayed - # alphabetically column-wise.) - max_compl_width = min( - term_width, max(get_cwidth(c.display_text) for c in completions) + 1 - ) - column_count = max(1, term_width // max_compl_width) - completions_per_page = column_count * (term_height - 1) - page_count = int(math.ceil(len(completions) / float(completions_per_page))) - # Note: math.ceil can return float on Python2. - - def display(page: int) -> None: - # Display completions. - page_completions = completions[ - page * completions_per_page : (page + 1) * completions_per_page - ] - - page_row_count = int(math.ceil(len(page_completions) / float(column_count))) - page_columns = [ - page_completions[i * page_row_count : (i + 1) * page_row_count] - for i in range(column_count) - ] - - result: StyleAndTextTuples = [] - - for r in range(page_row_count): - for c in range(column_count): - try: - completion = page_columns[c][r] - style = "class:readline-like-completions.completion " + ( - completion.style or "" - ) - - result.extend(to_formatted_text(completion.display, style=style)) - - # Add padding. - padding = max_compl_width - get_cwidth(completion.display_text) - result.append((completion.style, " " * padding)) - except IndexError: - pass - result.append(("", "\n")) - - app.print_text(to_formatted_text(result, "class:readline-like-completions")) - - # User interaction through an application generator function. - async def run_compl() -> None: - "Coroutine." - async with in_terminal(render_cli_done=True): - if len(completions) > completions_per_page: - # Ask confirmation if it doesn't fit on the screen. - confirm = await create_confirm_session( - "Display all {} possibilities?".format(len(completions)), - ).prompt_async() - - if confirm: - # Display pages. - for page in range(page_count): - display(page) - - if page != page_count - 1: - # Display --MORE-- and go to the next page. - show_more = await _create_more_session( - "--MORE--" - ).prompt_async() - - if not show_more: - return - else: - app.output.flush() - else: - # Display all completions. - display(0) - - return app.create_background_task(run_compl()) - - -def _create_more_session(message: str = "--MORE--") -> "PromptSession[bool]": - """ - Create a `PromptSession` object for displaying the "--MORE--". - """ - from prompt_toolkit.shortcuts import PromptSession - - bindings = KeyBindings() - - @bindings.add(" ") - @bindings.add("y") - @bindings.add("Y") - @bindings.add(Keys.ControlJ) - @bindings.add(Keys.ControlM) - @bindings.add(Keys.ControlI) # Tab. - def _yes(event: E) -> None: - event.app.exit(result=True) - - @bindings.add("n") - @bindings.add("N") - @bindings.add("q") - @bindings.add("Q") - @bindings.add(Keys.ControlC) - def _no(event: E) -> None: - event.app.exit(result=False) - - @bindings.add(Keys.Any) - def _ignore(event: E) -> None: - "Disable inserting of text." - - return PromptSession(message, key_bindings=bindings, erase_when_done=True) +""" +Key binding handlers for displaying completions. +""" +import asyncio +import math +from typing import TYPE_CHECKING, List + +from prompt_toolkit.application.run_in_terminal import in_terminal +from prompt_toolkit.completion import ( + CompleteEvent, + Completion, + get_common_complete_suffix, +) +from prompt_toolkit.formatted_text import StyleAndTextTuples +from prompt_toolkit.key_binding.key_bindings import KeyBindings +from prompt_toolkit.key_binding.key_processor import KeyPressEvent +from prompt_toolkit.keys import Keys +from prompt_toolkit.utils import get_cwidth + +if TYPE_CHECKING: + from prompt_toolkit.application import Application + from prompt_toolkit.shortcuts import PromptSession + +__all__ = [ + "generate_completions", + "display_completions_like_readline", +] + +E = KeyPressEvent + + +def generate_completions(event: E) -> None: + r""" + Tab-completion: where the first tab completes the common suffix and the + second tab lists all the completions. + """ + b = event.current_buffer + + # When already navigating through completions, select the next one. + if b.complete_state: + b.complete_next() + else: + b.start_completion(insert_common_part=True) + + +def display_completions_like_readline(event: E) -> None: + """ + Key binding handler for readline-style tab completion. + This is meant to be as similar as possible to the way how readline displays + completions. + + Generate the completions immediately (blocking) and display them above the + prompt in columns. + + Usage:: + + # Call this handler when 'Tab' has been pressed. + key_bindings.add(Keys.ControlI)(display_completions_like_readline) + """ + # Request completions. + b = event.current_buffer + if b.completer is None: + return + complete_event = CompleteEvent(completion_requested=True) + completions = list(b.completer.get_completions(b.document, complete_event)) + + # Calculate the common suffix. + common_suffix = get_common_complete_suffix(b.document, completions) + + # One completion: insert it. + if len(completions) == 1: + b.delete_before_cursor(-completions[0].start_position) + b.insert_text(completions[0].text) + # Multiple completions with common part. + elif common_suffix: + b.insert_text(common_suffix) + # Otherwise: display all completions. + elif completions: + _display_completions_like_readline(event.app, completions) + + +def _display_completions_like_readline( + app: "Application[object]", completions: List[Completion] +) -> "asyncio.Task[None]": + """ + Display the list of completions in columns above the prompt. + This will ask for a confirmation if there are too many completions to fit + on a single page and provide a paginator to walk through them. + """ + from prompt_toolkit.formatted_text import to_formatted_text + from prompt_toolkit.shortcuts.prompt import create_confirm_session + + # Get terminal dimensions. + term_size = app.output.get_size() + term_width = term_size.columns + term_height = term_size.rows + + # Calculate amount of required columns/rows for displaying the + # completions. (Keep in mind that completions are displayed + # alphabetically column-wise.) + max_compl_width = min( + term_width, max(get_cwidth(c.display_text) for c in completions) + 1 + ) + column_count = max(1, term_width // max_compl_width) + completions_per_page = column_count * (term_height - 1) + page_count = int(math.ceil(len(completions) / float(completions_per_page))) + # Note: math.ceil can return float on Python2. + + def display(page: int) -> None: + # Display completions. + page_completions = completions[ + page * completions_per_page : (page + 1) * completions_per_page + ] + + page_row_count = int(math.ceil(len(page_completions) / float(column_count))) + page_columns = [ + page_completions[i * page_row_count : (i + 1) * page_row_count] + for i in range(column_count) + ] + + result: StyleAndTextTuples = [] + + for r in range(page_row_count): + for c in range(column_count): + try: + completion = page_columns[c][r] + style = "class:readline-like-completions.completion " + ( + completion.style or "" + ) + + result.extend(to_formatted_text(completion.display, style=style)) + + # Add padding. + padding = max_compl_width - get_cwidth(completion.display_text) + result.append((completion.style, " " * padding)) + except IndexError: + pass + result.append(("", "\n")) + + app.print_text(to_formatted_text(result, "class:readline-like-completions")) + + # User interaction through an application generator function. + async def run_compl() -> None: + "Coroutine." + async with in_terminal(render_cli_done=True): + if len(completions) > completions_per_page: + # Ask confirmation if it doesn't fit on the screen. + confirm = await create_confirm_session( + "Display all {} possibilities?".format(len(completions)), + ).prompt_async() + + if confirm: + # Display pages. + for page in range(page_count): + display(page) + + if page != page_count - 1: + # Display --MORE-- and go to the next page. + show_more = await _create_more_session( + "--MORE--" + ).prompt_async() + + if not show_more: + return + else: + app.output.flush() + else: + # Display all completions. + display(0) + + return app.create_background_task(run_compl()) + + +def _create_more_session(message: str = "--MORE--") -> "PromptSession[bool]": + """ + Create a `PromptSession` object for displaying the "--MORE--". + """ + from prompt_toolkit.shortcuts import PromptSession + + bindings = KeyBindings() + + @bindings.add(" ") + @bindings.add("y") + @bindings.add("Y") + @bindings.add(Keys.ControlJ) + @bindings.add(Keys.ControlM) + @bindings.add(Keys.ControlI) # Tab. + def _yes(event: E) -> None: + event.app.exit(result=True) + + @bindings.add("n") + @bindings.add("N") + @bindings.add("q") + @bindings.add("Q") + @bindings.add(Keys.ControlC) + def _no(event: E) -> None: + event.app.exit(result=False) + + @bindings.add(Keys.Any) + def _ignore(event: E) -> None: + "Disable inserting of text." + + return PromptSession(message, key_bindings=bindings, erase_when_done=True) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/cpr.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/cpr.py index 07b0fa75273..ff0f4aaaf1e 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/cpr.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/cpr.py @@ -1,28 +1,28 @@ -from prompt_toolkit.key_binding.key_processor import KeyPressEvent -from prompt_toolkit.keys import Keys - -from ..key_bindings import KeyBindings - -__all__ = [ - "load_cpr_bindings", -] - -E = KeyPressEvent - - -def load_cpr_bindings() -> KeyBindings: - key_bindings = KeyBindings() - - @key_bindings.add(Keys.CPRResponse, save_before=lambda e: False) - def _(event: E) -> None: - """ - Handle incoming Cursor-Position-Request response. - """ - # The incoming data looks like u'\x1b[35;1R' - # Parse row/col information. - row, col = map(int, event.data[2:-1].split(";")) - - # Report absolute cursor position to the renderer. - event.app.renderer.report_absolute_cursor_row(row) - - return key_bindings +from prompt_toolkit.key_binding.key_processor import KeyPressEvent +from prompt_toolkit.keys import Keys + +from ..key_bindings import KeyBindings + +__all__ = [ + "load_cpr_bindings", +] + +E = KeyPressEvent + + +def load_cpr_bindings() -> KeyBindings: + key_bindings = KeyBindings() + + @key_bindings.add(Keys.CPRResponse, save_before=lambda e: False) + def _(event: E) -> None: + """ + Handle incoming Cursor-Position-Request response. + """ + # The incoming data looks like u'\x1b[35;1R' + # Parse row/col information. + row, col = map(int, event.data[2:-1].split(";")) + + # Report absolute cursor position to the renderer. + event.app.renderer.report_absolute_cursor_row(row) + + return key_bindings diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/emacs.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/emacs.py index a4a5e348f83..940144d8ff2 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/emacs.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/emacs.py @@ -1,557 +1,557 @@ -# pylint: disable=function-redefined -from typing import Dict, Union - -from prompt_toolkit.application.current import get_app -from prompt_toolkit.buffer import Buffer, indent, unindent -from prompt_toolkit.completion import CompleteEvent -from prompt_toolkit.filters import ( - Condition, - emacs_insert_mode, - emacs_mode, - has_arg, - has_selection, - in_paste_mode, - is_multiline, - is_read_only, - shift_selection_mode, - vi_search_direction_reversed, -) -from prompt_toolkit.key_binding.key_bindings import Binding -from prompt_toolkit.key_binding.key_processor import KeyPressEvent -from prompt_toolkit.keys import Keys -from prompt_toolkit.selection import SelectionType - -from ..key_bindings import ConditionalKeyBindings, KeyBindings, KeyBindingsBase -from .named_commands import get_by_name - -__all__ = [ - "load_emacs_bindings", - "load_emacs_search_bindings", - "load_emacs_shift_selection_bindings", -] - -E = KeyPressEvent - - -def load_emacs_bindings() -> KeyBindingsBase: - """ - Some e-macs extensions. - """ - # Overview of Readline emacs commands: - # http://www.catonmat.net/download/readline-emacs-editing-mode-cheat-sheet.pdf - key_bindings = KeyBindings() - handle = key_bindings.add - - insert_mode = emacs_insert_mode - - @handle("escape") - def _esc(event: E) -> None: - """ - By default, ignore escape key. - - (If we don't put this here, and Esc is followed by a key which sequence - is not handled, we'll insert an Escape character in the input stream. - Something we don't want and happens to easily in emacs mode. - Further, people can always use ControlQ to do a quoted insert.) - """ - pass - - handle("c-a")(get_by_name("beginning-of-line")) - handle("c-b")(get_by_name("backward-char")) - handle("c-delete", filter=insert_mode)(get_by_name("kill-word")) - handle("c-e")(get_by_name("end-of-line")) - handle("c-f")(get_by_name("forward-char")) - handle("c-left")(get_by_name("backward-word")) - handle("c-right")(get_by_name("forward-word")) - handle("c-x", "r", "y", filter=insert_mode)(get_by_name("yank")) - handle("c-y", filter=insert_mode)(get_by_name("yank")) - handle("escape", "b")(get_by_name("backward-word")) - handle("escape", "c", filter=insert_mode)(get_by_name("capitalize-word")) - handle("escape", "d", filter=insert_mode)(get_by_name("kill-word")) - handle("escape", "f")(get_by_name("forward-word")) - handle("escape", "l", filter=insert_mode)(get_by_name("downcase-word")) - handle("escape", "u", filter=insert_mode)(get_by_name("uppercase-word")) - handle("escape", "y", filter=insert_mode)(get_by_name("yank-pop")) - handle("escape", "backspace", filter=insert_mode)(get_by_name("backward-kill-word")) - handle("escape", "\\", filter=insert_mode)(get_by_name("delete-horizontal-space")) - - handle("c-home")(get_by_name("beginning-of-buffer")) - handle("c-end")(get_by_name("end-of-buffer")) - - handle("c-_", save_before=(lambda e: False), filter=insert_mode)( - get_by_name("undo") - ) - - handle("c-x", "c-u", save_before=(lambda e: False), filter=insert_mode)( - get_by_name("undo") - ) - - handle("escape", "<", filter=~has_selection)(get_by_name("beginning-of-history")) - handle("escape", ">", filter=~has_selection)(get_by_name("end-of-history")) - - handle("escape", ".", filter=insert_mode)(get_by_name("yank-last-arg")) - handle("escape", "_", filter=insert_mode)(get_by_name("yank-last-arg")) - handle("escape", "c-y", filter=insert_mode)(get_by_name("yank-nth-arg")) - handle("escape", "#", filter=insert_mode)(get_by_name("insert-comment")) - handle("c-o")(get_by_name("operate-and-get-next")) - - # ControlQ does a quoted insert. Not that for vt100 terminals, you have to - # disable flow control by running ``stty -ixon``, otherwise Ctrl-Q and - # Ctrl-S are captured by the terminal. - handle("c-q", filter=~has_selection)(get_by_name("quoted-insert")) - - handle("c-x", "(")(get_by_name("start-kbd-macro")) - handle("c-x", ")")(get_by_name("end-kbd-macro")) - handle("c-x", "e")(get_by_name("call-last-kbd-macro")) - - @handle("c-n") - def _next(event: E) -> None: - "Next line." - event.current_buffer.auto_down() - - @handle("c-p") - def _prev(event: E) -> None: - "Previous line." - event.current_buffer.auto_up(count=event.arg) - - def handle_digit(c: str) -> None: - """ - Handle input of arguments. - The first number needs to be preceded by escape. - """ - - @handle(c, filter=has_arg) - @handle("escape", c) - def _(event: E) -> None: - event.append_to_arg_count(c) - - for c in "0123456789": - handle_digit(c) - - @handle("escape", "-", filter=~has_arg) - def _meta_dash(event: E) -> None: - """""" - if event._arg is None: - event.append_to_arg_count("-") - - @handle("-", filter=Condition(lambda: get_app().key_processor.arg == "-")) - def _dash(event: E) -> None: - """ - When '-' is typed again, after exactly '-' has been given as an - argument, ignore this. - """ - event.app.key_processor.arg = "-" - - @Condition - def is_returnable() -> bool: - return get_app().current_buffer.is_returnable - - # Meta + Enter: always accept input. - handle("escape", "enter", filter=insert_mode & is_returnable)( - get_by_name("accept-line") - ) - - # Enter: accept input in single line mode. - handle("enter", filter=insert_mode & is_returnable & ~is_multiline)( - get_by_name("accept-line") - ) - - def character_search(buff: Buffer, char: str, count: int) -> None: - if count < 0: - match = buff.document.find_backwards( - char, in_current_line=True, count=-count - ) - else: - match = buff.document.find(char, in_current_line=True, count=count) - - if match is not None: - buff.cursor_position += match - - @handle("c-]", Keys.Any) - def _goto_char(event: E) -> None: - "When Ctl-] + a character is pressed. go to that character." - # Also named 'character-search' - character_search(event.current_buffer, event.data, event.arg) - - @handle("escape", "c-]", Keys.Any) - def _goto_char_backwards(event: E) -> None: - "Like Ctl-], but backwards." - # Also named 'character-search-backward' - character_search(event.current_buffer, event.data, -event.arg) - - @handle("escape", "a") - def _prev_sentence(event: E) -> None: - "Previous sentence." - # TODO: - - @handle("escape", "e") - def _end_of_sentence(event: E) -> None: - "Move to end of sentence." - # TODO: - - @handle("escape", "t", filter=insert_mode) - def _swap_characters(event: E) -> None: - """ - Swap the last two words before the cursor. - """ - # TODO - - @handle("escape", "*", filter=insert_mode) - def _insert_all_completions(event: E) -> None: - """ - `meta-*`: Insert all possible completions of the preceding text. - """ - buff = event.current_buffer - - # List all completions. - complete_event = CompleteEvent(text_inserted=False, completion_requested=True) - completions = list( - buff.completer.get_completions(buff.document, complete_event) - ) - - # Insert them. - text_to_insert = " ".join(c.text for c in completions) - buff.insert_text(text_to_insert) - - @handle("c-x", "c-x") - def _toggle_start_end(event: E) -> None: - """ - Move cursor back and forth between the start and end of the current - line. - """ - buffer = event.current_buffer - - if buffer.document.is_cursor_at_the_end_of_line: - buffer.cursor_position += buffer.document.get_start_of_line_position( - after_whitespace=False - ) - else: - buffer.cursor_position += buffer.document.get_end_of_line_position() - - @handle("c-@") # Control-space or Control-@ - def _start_selection(event: E) -> None: - """ - Start of the selection (if the current buffer is not empty). - """ - # Take the current cursor position as the start of this selection. - buff = event.current_buffer - if buff.text: - buff.start_selection(selection_type=SelectionType.CHARACTERS) - - @handle("c-g", filter=~has_selection) - def _cancel(event: E) -> None: - """ - Control + G: Cancel completion menu and validation state. - """ - event.current_buffer.complete_state = None - event.current_buffer.validation_error = None - - @handle("c-g", filter=has_selection) - def _cancel_selection(event: E) -> None: - """ - Cancel selection. - """ - event.current_buffer.exit_selection() - - @handle("c-w", filter=has_selection) - @handle("c-x", "r", "k", filter=has_selection) - def _cut(event: E) -> None: - """ - Cut selected text. - """ - data = event.current_buffer.cut_selection() - event.app.clipboard.set_data(data) - - @handle("escape", "w", filter=has_selection) - def _copy(event: E) -> None: - """ - Copy selected text. - """ - data = event.current_buffer.copy_selection() - event.app.clipboard.set_data(data) - - @handle("escape", "left") - def _start_of_word(event: E) -> None: - """ - Cursor to start of previous word. - """ - buffer = event.current_buffer - buffer.cursor_position += ( - buffer.document.find_previous_word_beginning(count=event.arg) or 0 - ) - - @handle("escape", "right") - def _start_next_word(event: E) -> None: - """ - Cursor to start of next word. - """ - buffer = event.current_buffer - buffer.cursor_position += ( - buffer.document.find_next_word_beginning(count=event.arg) - or buffer.document.get_end_of_document_position() - ) - - @handle("escape", "/", filter=insert_mode) - def _complete(event: E) -> None: - """ - M-/: Complete. - """ - b = event.current_buffer - if b.complete_state: - b.complete_next() - else: - b.start_completion(select_first=True) - - @handle("c-c", ">", filter=has_selection) - def _indent(event: E) -> None: - """ - Indent selected text. - """ - buffer = event.current_buffer - - buffer.cursor_position += buffer.document.get_start_of_line_position( - after_whitespace=True - ) - - from_, to = buffer.document.selection_range() - from_, _ = buffer.document.translate_index_to_position(from_) - to, _ = buffer.document.translate_index_to_position(to) - - indent(buffer, from_, to + 1, count=event.arg) - - @handle("c-c", "<", filter=has_selection) - def _unindent(event: E) -> None: - """ - Unindent selected text. - """ - buffer = event.current_buffer - - from_, to = buffer.document.selection_range() - from_, _ = buffer.document.translate_index_to_position(from_) - to, _ = buffer.document.translate_index_to_position(to) - - unindent(buffer, from_, to + 1, count=event.arg) - - return ConditionalKeyBindings(key_bindings, emacs_mode) - - -def load_emacs_search_bindings() -> KeyBindingsBase: - key_bindings = KeyBindings() - handle = key_bindings.add - from . import search - - # NOTE: We don't bind 'Escape' to 'abort_search'. The reason is that we - # want Alt+Enter to accept input directly in incremental search mode. - # Instead, we have double escape. - - handle("c-r")(search.start_reverse_incremental_search) - handle("c-s")(search.start_forward_incremental_search) - - handle("c-c")(search.abort_search) - handle("c-g")(search.abort_search) - handle("c-r")(search.reverse_incremental_search) - handle("c-s")(search.forward_incremental_search) - handle("up")(search.reverse_incremental_search) - handle("down")(search.forward_incremental_search) - handle("enter")(search.accept_search) - - # Handling of escape. - handle("escape", eager=True)(search.accept_search) - - # Like Readline, it's more natural to accept the search when escape has - # been pressed, however instead the following two bindings could be used - # instead. - # #handle('escape', 'escape', eager=True)(search.abort_search) - # #handle('escape', 'enter', eager=True)(search.accept_search_and_accept_input) - - # If Read-only: also include the following key bindings: - - # '/' and '?' key bindings for searching, just like Vi mode. - handle("?", filter=is_read_only & ~vi_search_direction_reversed)( - search.start_reverse_incremental_search - ) - handle("/", filter=is_read_only & ~vi_search_direction_reversed)( - search.start_forward_incremental_search - ) - handle("?", filter=is_read_only & vi_search_direction_reversed)( - search.start_forward_incremental_search - ) - handle("/", filter=is_read_only & vi_search_direction_reversed)( - search.start_reverse_incremental_search - ) - - @handle("n", filter=is_read_only) - def _jump_next(event: E) -> None: - "Jump to next match." - event.current_buffer.apply_search( - event.app.current_search_state, - include_current_position=False, - count=event.arg, - ) - - @handle("N", filter=is_read_only) - def _jump_prev(event: E) -> None: - "Jump to previous match." - event.current_buffer.apply_search( - ~event.app.current_search_state, - include_current_position=False, - count=event.arg, - ) - - return ConditionalKeyBindings(key_bindings, emacs_mode) - - -def load_emacs_shift_selection_bindings() -> KeyBindingsBase: - """ - Bindings to select text with shift + cursor movements - """ - - key_bindings = KeyBindings() - handle = key_bindings.add - - def unshift_move(event: E) -> None: - """ - Used for the shift selection mode. When called with - a shift + movement key press event, moves the cursor - as if shift is not pressed. - """ - key = event.key_sequence[0].key - - if key == Keys.ShiftUp: - event.current_buffer.auto_up(count=event.arg) - return - if key == Keys.ShiftDown: - event.current_buffer.auto_down(count=event.arg) - return - - # the other keys are handled through their readline command - key_to_command: Dict[Union[Keys, str], str] = { - Keys.ShiftLeft: "backward-char", - Keys.ShiftRight: "forward-char", - Keys.ShiftHome: "beginning-of-line", - Keys.ShiftEnd: "end-of-line", - Keys.ControlShiftLeft: "backward-word", - Keys.ControlShiftRight: "forward-word", - Keys.ControlShiftHome: "beginning-of-buffer", - Keys.ControlShiftEnd: "end-of-buffer", - } - - try: - # Both the dict lookup and `get_by_name` can raise KeyError. - binding = get_by_name(key_to_command[key]) - except KeyError: - pass - else: # (`else` is not really needed here.) - if isinstance(binding, Binding): - # (It should always be a binding here) - binding.call(event) - - @handle("s-left", filter=~has_selection) - @handle("s-right", filter=~has_selection) - @handle("s-up", filter=~has_selection) - @handle("s-down", filter=~has_selection) - @handle("s-home", filter=~has_selection) - @handle("s-end", filter=~has_selection) - @handle("c-s-left", filter=~has_selection) - @handle("c-s-right", filter=~has_selection) - @handle("c-s-home", filter=~has_selection) - @handle("c-s-end", filter=~has_selection) - def _start_selection(event: E) -> None: - """ - Start selection with shift + movement. - """ - # Take the current cursor position as the start of this selection. - buff = event.current_buffer - if buff.text: - buff.start_selection(selection_type=SelectionType.CHARACTERS) - - if buff.selection_state is not None: - # (`selection_state` should never be `None`, it is created by - # `start_selection`.) - buff.selection_state.enter_shift_mode() - - # Then move the cursor - original_position = buff.cursor_position - unshift_move(event) - if buff.cursor_position == original_position: - # Cursor didn't actually move - so cancel selection - # to avoid having an empty selection - buff.exit_selection() - - @handle("s-left", filter=shift_selection_mode) - @handle("s-right", filter=shift_selection_mode) - @handle("s-up", filter=shift_selection_mode) - @handle("s-down", filter=shift_selection_mode) - @handle("s-home", filter=shift_selection_mode) - @handle("s-end", filter=shift_selection_mode) - @handle("c-s-left", filter=shift_selection_mode) - @handle("c-s-right", filter=shift_selection_mode) - @handle("c-s-home", filter=shift_selection_mode) - @handle("c-s-end", filter=shift_selection_mode) - def _extend_selection(event: E) -> None: - """ - Extend the selection - """ - # Just move the cursor, like shift was not pressed - unshift_move(event) - buff = event.current_buffer - - if buff.selection_state is not None: - if buff.cursor_position == buff.selection_state.original_cursor_position: - # selection is now empty, so cancel selection - buff.exit_selection() - - @handle(Keys.Any, filter=shift_selection_mode) - def _replace_selection(event: E) -> None: - """ - Replace selection by what is typed - """ - event.current_buffer.cut_selection() - get_by_name("self-insert").call(event) - - @handle("enter", filter=shift_selection_mode & is_multiline) - def _newline(event: E) -> None: - """ - A newline replaces the selection - """ - event.current_buffer.cut_selection() - event.current_buffer.newline(copy_margin=not in_paste_mode()) - - @handle("backspace", filter=shift_selection_mode) - def _delete(event: E) -> None: - """ - Delete selection. - """ - event.current_buffer.cut_selection() - - @handle("c-y", filter=shift_selection_mode) - def _yank(event: E) -> None: - """ - In shift selection mode, yanking (pasting) replace the selection. - """ - buff = event.current_buffer - if buff.selection_state: - buff.cut_selection() - get_by_name("yank").call(event) - - # moving the cursor in shift selection mode cancels the selection - @handle("left", filter=shift_selection_mode) - @handle("right", filter=shift_selection_mode) - @handle("up", filter=shift_selection_mode) - @handle("down", filter=shift_selection_mode) - @handle("home", filter=shift_selection_mode) - @handle("end", filter=shift_selection_mode) - @handle("c-left", filter=shift_selection_mode) - @handle("c-right", filter=shift_selection_mode) - @handle("c-home", filter=shift_selection_mode) - @handle("c-end", filter=shift_selection_mode) - def _cancel(event: E) -> None: - """ - Cancel selection. - """ - event.current_buffer.exit_selection() - # we then process the cursor movement - key_press = event.key_sequence[0] - event.key_processor.feed(key_press, first=True) - - return ConditionalKeyBindings(key_bindings, emacs_mode) +# pylint: disable=function-redefined +from typing import Dict, Union + +from prompt_toolkit.application.current import get_app +from prompt_toolkit.buffer import Buffer, indent, unindent +from prompt_toolkit.completion import CompleteEvent +from prompt_toolkit.filters import ( + Condition, + emacs_insert_mode, + emacs_mode, + has_arg, + has_selection, + in_paste_mode, + is_multiline, + is_read_only, + shift_selection_mode, + vi_search_direction_reversed, +) +from prompt_toolkit.key_binding.key_bindings import Binding +from prompt_toolkit.key_binding.key_processor import KeyPressEvent +from prompt_toolkit.keys import Keys +from prompt_toolkit.selection import SelectionType + +from ..key_bindings import ConditionalKeyBindings, KeyBindings, KeyBindingsBase +from .named_commands import get_by_name + +__all__ = [ + "load_emacs_bindings", + "load_emacs_search_bindings", + "load_emacs_shift_selection_bindings", +] + +E = KeyPressEvent + + +def load_emacs_bindings() -> KeyBindingsBase: + """ + Some e-macs extensions. + """ + # Overview of Readline emacs commands: + # http://www.catonmat.net/download/readline-emacs-editing-mode-cheat-sheet.pdf + key_bindings = KeyBindings() + handle = key_bindings.add + + insert_mode = emacs_insert_mode + + @handle("escape") + def _esc(event: E) -> None: + """ + By default, ignore escape key. + + (If we don't put this here, and Esc is followed by a key which sequence + is not handled, we'll insert an Escape character in the input stream. + Something we don't want and happens to easily in emacs mode. + Further, people can always use ControlQ to do a quoted insert.) + """ + pass + + handle("c-a")(get_by_name("beginning-of-line")) + handle("c-b")(get_by_name("backward-char")) + handle("c-delete", filter=insert_mode)(get_by_name("kill-word")) + handle("c-e")(get_by_name("end-of-line")) + handle("c-f")(get_by_name("forward-char")) + handle("c-left")(get_by_name("backward-word")) + handle("c-right")(get_by_name("forward-word")) + handle("c-x", "r", "y", filter=insert_mode)(get_by_name("yank")) + handle("c-y", filter=insert_mode)(get_by_name("yank")) + handle("escape", "b")(get_by_name("backward-word")) + handle("escape", "c", filter=insert_mode)(get_by_name("capitalize-word")) + handle("escape", "d", filter=insert_mode)(get_by_name("kill-word")) + handle("escape", "f")(get_by_name("forward-word")) + handle("escape", "l", filter=insert_mode)(get_by_name("downcase-word")) + handle("escape", "u", filter=insert_mode)(get_by_name("uppercase-word")) + handle("escape", "y", filter=insert_mode)(get_by_name("yank-pop")) + handle("escape", "backspace", filter=insert_mode)(get_by_name("backward-kill-word")) + handle("escape", "\\", filter=insert_mode)(get_by_name("delete-horizontal-space")) + + handle("c-home")(get_by_name("beginning-of-buffer")) + handle("c-end")(get_by_name("end-of-buffer")) + + handle("c-_", save_before=(lambda e: False), filter=insert_mode)( + get_by_name("undo") + ) + + handle("c-x", "c-u", save_before=(lambda e: False), filter=insert_mode)( + get_by_name("undo") + ) + + handle("escape", "<", filter=~has_selection)(get_by_name("beginning-of-history")) + handle("escape", ">", filter=~has_selection)(get_by_name("end-of-history")) + + handle("escape", ".", filter=insert_mode)(get_by_name("yank-last-arg")) + handle("escape", "_", filter=insert_mode)(get_by_name("yank-last-arg")) + handle("escape", "c-y", filter=insert_mode)(get_by_name("yank-nth-arg")) + handle("escape", "#", filter=insert_mode)(get_by_name("insert-comment")) + handle("c-o")(get_by_name("operate-and-get-next")) + + # ControlQ does a quoted insert. Not that for vt100 terminals, you have to + # disable flow control by running ``stty -ixon``, otherwise Ctrl-Q and + # Ctrl-S are captured by the terminal. + handle("c-q", filter=~has_selection)(get_by_name("quoted-insert")) + + handle("c-x", "(")(get_by_name("start-kbd-macro")) + handle("c-x", ")")(get_by_name("end-kbd-macro")) + handle("c-x", "e")(get_by_name("call-last-kbd-macro")) + + @handle("c-n") + def _next(event: E) -> None: + "Next line." + event.current_buffer.auto_down() + + @handle("c-p") + def _prev(event: E) -> None: + "Previous line." + event.current_buffer.auto_up(count=event.arg) + + def handle_digit(c: str) -> None: + """ + Handle input of arguments. + The first number needs to be preceded by escape. + """ + + @handle(c, filter=has_arg) + @handle("escape", c) + def _(event: E) -> None: + event.append_to_arg_count(c) + + for c in "0123456789": + handle_digit(c) + + @handle("escape", "-", filter=~has_arg) + def _meta_dash(event: E) -> None: + """""" + if event._arg is None: + event.append_to_arg_count("-") + + @handle("-", filter=Condition(lambda: get_app().key_processor.arg == "-")) + def _dash(event: E) -> None: + """ + When '-' is typed again, after exactly '-' has been given as an + argument, ignore this. + """ + event.app.key_processor.arg = "-" + + @Condition + def is_returnable() -> bool: + return get_app().current_buffer.is_returnable + + # Meta + Enter: always accept input. + handle("escape", "enter", filter=insert_mode & is_returnable)( + get_by_name("accept-line") + ) + + # Enter: accept input in single line mode. + handle("enter", filter=insert_mode & is_returnable & ~is_multiline)( + get_by_name("accept-line") + ) + + def character_search(buff: Buffer, char: str, count: int) -> None: + if count < 0: + match = buff.document.find_backwards( + char, in_current_line=True, count=-count + ) + else: + match = buff.document.find(char, in_current_line=True, count=count) + + if match is not None: + buff.cursor_position += match + + @handle("c-]", Keys.Any) + def _goto_char(event: E) -> None: + "When Ctl-] + a character is pressed. go to that character." + # Also named 'character-search' + character_search(event.current_buffer, event.data, event.arg) + + @handle("escape", "c-]", Keys.Any) + def _goto_char_backwards(event: E) -> None: + "Like Ctl-], but backwards." + # Also named 'character-search-backward' + character_search(event.current_buffer, event.data, -event.arg) + + @handle("escape", "a") + def _prev_sentence(event: E) -> None: + "Previous sentence." + # TODO: + + @handle("escape", "e") + def _end_of_sentence(event: E) -> None: + "Move to end of sentence." + # TODO: + + @handle("escape", "t", filter=insert_mode) + def _swap_characters(event: E) -> None: + """ + Swap the last two words before the cursor. + """ + # TODO + + @handle("escape", "*", filter=insert_mode) + def _insert_all_completions(event: E) -> None: + """ + `meta-*`: Insert all possible completions of the preceding text. + """ + buff = event.current_buffer + + # List all completions. + complete_event = CompleteEvent(text_inserted=False, completion_requested=True) + completions = list( + buff.completer.get_completions(buff.document, complete_event) + ) + + # Insert them. + text_to_insert = " ".join(c.text for c in completions) + buff.insert_text(text_to_insert) + + @handle("c-x", "c-x") + def _toggle_start_end(event: E) -> None: + """ + Move cursor back and forth between the start and end of the current + line. + """ + buffer = event.current_buffer + + if buffer.document.is_cursor_at_the_end_of_line: + buffer.cursor_position += buffer.document.get_start_of_line_position( + after_whitespace=False + ) + else: + buffer.cursor_position += buffer.document.get_end_of_line_position() + + @handle("c-@") # Control-space or Control-@ + def _start_selection(event: E) -> None: + """ + Start of the selection (if the current buffer is not empty). + """ + # Take the current cursor position as the start of this selection. + buff = event.current_buffer + if buff.text: + buff.start_selection(selection_type=SelectionType.CHARACTERS) + + @handle("c-g", filter=~has_selection) + def _cancel(event: E) -> None: + """ + Control + G: Cancel completion menu and validation state. + """ + event.current_buffer.complete_state = None + event.current_buffer.validation_error = None + + @handle("c-g", filter=has_selection) + def _cancel_selection(event: E) -> None: + """ + Cancel selection. + """ + event.current_buffer.exit_selection() + + @handle("c-w", filter=has_selection) + @handle("c-x", "r", "k", filter=has_selection) + def _cut(event: E) -> None: + """ + Cut selected text. + """ + data = event.current_buffer.cut_selection() + event.app.clipboard.set_data(data) + + @handle("escape", "w", filter=has_selection) + def _copy(event: E) -> None: + """ + Copy selected text. + """ + data = event.current_buffer.copy_selection() + event.app.clipboard.set_data(data) + + @handle("escape", "left") + def _start_of_word(event: E) -> None: + """ + Cursor to start of previous word. + """ + buffer = event.current_buffer + buffer.cursor_position += ( + buffer.document.find_previous_word_beginning(count=event.arg) or 0 + ) + + @handle("escape", "right") + def _start_next_word(event: E) -> None: + """ + Cursor to start of next word. + """ + buffer = event.current_buffer + buffer.cursor_position += ( + buffer.document.find_next_word_beginning(count=event.arg) + or buffer.document.get_end_of_document_position() + ) + + @handle("escape", "/", filter=insert_mode) + def _complete(event: E) -> None: + """ + M-/: Complete. + """ + b = event.current_buffer + if b.complete_state: + b.complete_next() + else: + b.start_completion(select_first=True) + + @handle("c-c", ">", filter=has_selection) + def _indent(event: E) -> None: + """ + Indent selected text. + """ + buffer = event.current_buffer + + buffer.cursor_position += buffer.document.get_start_of_line_position( + after_whitespace=True + ) + + from_, to = buffer.document.selection_range() + from_, _ = buffer.document.translate_index_to_position(from_) + to, _ = buffer.document.translate_index_to_position(to) + + indent(buffer, from_, to + 1, count=event.arg) + + @handle("c-c", "<", filter=has_selection) + def _unindent(event: E) -> None: + """ + Unindent selected text. + """ + buffer = event.current_buffer + + from_, to = buffer.document.selection_range() + from_, _ = buffer.document.translate_index_to_position(from_) + to, _ = buffer.document.translate_index_to_position(to) + + unindent(buffer, from_, to + 1, count=event.arg) + + return ConditionalKeyBindings(key_bindings, emacs_mode) + + +def load_emacs_search_bindings() -> KeyBindingsBase: + key_bindings = KeyBindings() + handle = key_bindings.add + from . import search + + # NOTE: We don't bind 'Escape' to 'abort_search'. The reason is that we + # want Alt+Enter to accept input directly in incremental search mode. + # Instead, we have double escape. + + handle("c-r")(search.start_reverse_incremental_search) + handle("c-s")(search.start_forward_incremental_search) + + handle("c-c")(search.abort_search) + handle("c-g")(search.abort_search) + handle("c-r")(search.reverse_incremental_search) + handle("c-s")(search.forward_incremental_search) + handle("up")(search.reverse_incremental_search) + handle("down")(search.forward_incremental_search) + handle("enter")(search.accept_search) + + # Handling of escape. + handle("escape", eager=True)(search.accept_search) + + # Like Readline, it's more natural to accept the search when escape has + # been pressed, however instead the following two bindings could be used + # instead. + # #handle('escape', 'escape', eager=True)(search.abort_search) + # #handle('escape', 'enter', eager=True)(search.accept_search_and_accept_input) + + # If Read-only: also include the following key bindings: + + # '/' and '?' key bindings for searching, just like Vi mode. + handle("?", filter=is_read_only & ~vi_search_direction_reversed)( + search.start_reverse_incremental_search + ) + handle("/", filter=is_read_only & ~vi_search_direction_reversed)( + search.start_forward_incremental_search + ) + handle("?", filter=is_read_only & vi_search_direction_reversed)( + search.start_forward_incremental_search + ) + handle("/", filter=is_read_only & vi_search_direction_reversed)( + search.start_reverse_incremental_search + ) + + @handle("n", filter=is_read_only) + def _jump_next(event: E) -> None: + "Jump to next match." + event.current_buffer.apply_search( + event.app.current_search_state, + include_current_position=False, + count=event.arg, + ) + + @handle("N", filter=is_read_only) + def _jump_prev(event: E) -> None: + "Jump to previous match." + event.current_buffer.apply_search( + ~event.app.current_search_state, + include_current_position=False, + count=event.arg, + ) + + return ConditionalKeyBindings(key_bindings, emacs_mode) + + +def load_emacs_shift_selection_bindings() -> KeyBindingsBase: + """ + Bindings to select text with shift + cursor movements + """ + + key_bindings = KeyBindings() + handle = key_bindings.add + + def unshift_move(event: E) -> None: + """ + Used for the shift selection mode. When called with + a shift + movement key press event, moves the cursor + as if shift is not pressed. + """ + key = event.key_sequence[0].key + + if key == Keys.ShiftUp: + event.current_buffer.auto_up(count=event.arg) + return + if key == Keys.ShiftDown: + event.current_buffer.auto_down(count=event.arg) + return + + # the other keys are handled through their readline command + key_to_command: Dict[Union[Keys, str], str] = { + Keys.ShiftLeft: "backward-char", + Keys.ShiftRight: "forward-char", + Keys.ShiftHome: "beginning-of-line", + Keys.ShiftEnd: "end-of-line", + Keys.ControlShiftLeft: "backward-word", + Keys.ControlShiftRight: "forward-word", + Keys.ControlShiftHome: "beginning-of-buffer", + Keys.ControlShiftEnd: "end-of-buffer", + } + + try: + # Both the dict lookup and `get_by_name` can raise KeyError. + binding = get_by_name(key_to_command[key]) + except KeyError: + pass + else: # (`else` is not really needed here.) + if isinstance(binding, Binding): + # (It should always be a binding here) + binding.call(event) + + @handle("s-left", filter=~has_selection) + @handle("s-right", filter=~has_selection) + @handle("s-up", filter=~has_selection) + @handle("s-down", filter=~has_selection) + @handle("s-home", filter=~has_selection) + @handle("s-end", filter=~has_selection) + @handle("c-s-left", filter=~has_selection) + @handle("c-s-right", filter=~has_selection) + @handle("c-s-home", filter=~has_selection) + @handle("c-s-end", filter=~has_selection) + def _start_selection(event: E) -> None: + """ + Start selection with shift + movement. + """ + # Take the current cursor position as the start of this selection. + buff = event.current_buffer + if buff.text: + buff.start_selection(selection_type=SelectionType.CHARACTERS) + + if buff.selection_state is not None: + # (`selection_state` should never be `None`, it is created by + # `start_selection`.) + buff.selection_state.enter_shift_mode() + + # Then move the cursor + original_position = buff.cursor_position + unshift_move(event) + if buff.cursor_position == original_position: + # Cursor didn't actually move - so cancel selection + # to avoid having an empty selection + buff.exit_selection() + + @handle("s-left", filter=shift_selection_mode) + @handle("s-right", filter=shift_selection_mode) + @handle("s-up", filter=shift_selection_mode) + @handle("s-down", filter=shift_selection_mode) + @handle("s-home", filter=shift_selection_mode) + @handle("s-end", filter=shift_selection_mode) + @handle("c-s-left", filter=shift_selection_mode) + @handle("c-s-right", filter=shift_selection_mode) + @handle("c-s-home", filter=shift_selection_mode) + @handle("c-s-end", filter=shift_selection_mode) + def _extend_selection(event: E) -> None: + """ + Extend the selection + """ + # Just move the cursor, like shift was not pressed + unshift_move(event) + buff = event.current_buffer + + if buff.selection_state is not None: + if buff.cursor_position == buff.selection_state.original_cursor_position: + # selection is now empty, so cancel selection + buff.exit_selection() + + @handle(Keys.Any, filter=shift_selection_mode) + def _replace_selection(event: E) -> None: + """ + Replace selection by what is typed + """ + event.current_buffer.cut_selection() + get_by_name("self-insert").call(event) + + @handle("enter", filter=shift_selection_mode & is_multiline) + def _newline(event: E) -> None: + """ + A newline replaces the selection + """ + event.current_buffer.cut_selection() + event.current_buffer.newline(copy_margin=not in_paste_mode()) + + @handle("backspace", filter=shift_selection_mode) + def _delete(event: E) -> None: + """ + Delete selection. + """ + event.current_buffer.cut_selection() + + @handle("c-y", filter=shift_selection_mode) + def _yank(event: E) -> None: + """ + In shift selection mode, yanking (pasting) replace the selection. + """ + buff = event.current_buffer + if buff.selection_state: + buff.cut_selection() + get_by_name("yank").call(event) + + # moving the cursor in shift selection mode cancels the selection + @handle("left", filter=shift_selection_mode) + @handle("right", filter=shift_selection_mode) + @handle("up", filter=shift_selection_mode) + @handle("down", filter=shift_selection_mode) + @handle("home", filter=shift_selection_mode) + @handle("end", filter=shift_selection_mode) + @handle("c-left", filter=shift_selection_mode) + @handle("c-right", filter=shift_selection_mode) + @handle("c-home", filter=shift_selection_mode) + @handle("c-end", filter=shift_selection_mode) + def _cancel(event: E) -> None: + """ + Cancel selection. + """ + event.current_buffer.exit_selection() + # we then process the cursor movement + key_press = event.key_sequence[0] + event.key_processor.feed(key_press, first=True) + + return ConditionalKeyBindings(key_bindings, emacs_mode) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/focus.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/focus.py index 40844db641c..30725a310f9 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/focus.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/focus.py @@ -1,24 +1,24 @@ -from prompt_toolkit.key_binding.key_processor import KeyPressEvent - -__all__ = [ - "focus_next", - "focus_previous", -] - -E = KeyPressEvent - - -def focus_next(event: E) -> None: - """ - Focus the next visible Window. - (Often bound to the `Tab` key.) - """ - event.app.layout.focus_next() - - -def focus_previous(event: E) -> None: - """ - Focus the previous visible Window. - (Often bound to the `BackTab` key.) - """ - event.app.layout.focus_previous() +from prompt_toolkit.key_binding.key_processor import KeyPressEvent + +__all__ = [ + "focus_next", + "focus_previous", +] + +E = KeyPressEvent + + +def focus_next(event: E) -> None: + """ + Focus the next visible Window. + (Often bound to the `Tab` key.) + """ + event.app.layout.focus_next() + + +def focus_previous(event: E) -> None: + """ + Focus the previous visible Window. + (Often bound to the `BackTab` key.) + """ + event.app.layout.focus_previous() diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/mouse.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/mouse.py index 949c33f72c2..bf9f644beb4 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/mouse.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/mouse.py @@ -1,346 +1,346 @@ -from typing import TYPE_CHECKING, FrozenSet - -from prompt_toolkit.data_structures import Point -from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent -from prompt_toolkit.keys import Keys -from prompt_toolkit.mouse_events import ( - MouseButton, - MouseEvent, - MouseEventType, - MouseModifier, -) -from prompt_toolkit.utils import is_windows - -from ..key_bindings import KeyBindings - -if TYPE_CHECKING: - from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone - -__all__ = [ - "load_mouse_bindings", -] - -E = KeyPressEvent - -# fmt: off -# flake8: noqa E201 -SCROLL_UP = MouseEventType.SCROLL_UP -SCROLL_DOWN = MouseEventType.SCROLL_DOWN -MOUSE_DOWN = MouseEventType.MOUSE_DOWN -MOUSE_MOVE = MouseEventType.MOUSE_MOVE -MOUSE_UP = MouseEventType.MOUSE_UP - -NO_MODIFIER : FrozenSet[MouseModifier] = frozenset() -SHIFT : FrozenSet[MouseModifier] = frozenset({MouseModifier.SHIFT}) -ALT : FrozenSet[MouseModifier] = frozenset({MouseModifier.ALT}) -SHIFT_ALT : FrozenSet[MouseModifier] = frozenset({MouseModifier.SHIFT, MouseModifier.ALT}) -CONTROL : FrozenSet[MouseModifier] = frozenset({MouseModifier.CONTROL}) -SHIFT_CONTROL : FrozenSet[MouseModifier] = frozenset({MouseModifier.SHIFT, MouseModifier.CONTROL}) -ALT_CONTROL : FrozenSet[MouseModifier] = frozenset({MouseModifier.ALT, MouseModifier.CONTROL}) -SHIFT_ALT_CONTROL: FrozenSet[MouseModifier] = frozenset({MouseModifier.SHIFT, MouseModifier.ALT, MouseModifier.CONTROL}) -UNKNOWN_MODIFIER : FrozenSet[MouseModifier] = frozenset() - -LEFT = MouseButton.LEFT -MIDDLE = MouseButton.MIDDLE -RIGHT = MouseButton.RIGHT -NO_BUTTON = MouseButton.NONE -UNKNOWN_BUTTON = MouseButton.UNKNOWN - -xterm_sgr_mouse_events = { - ( 0, 'm') : (LEFT, MOUSE_UP, NO_MODIFIER), # left_up 0+ + + =0 - ( 4, 'm') : (LEFT, MOUSE_UP, SHIFT), # left_up Shift 0+4+ + =4 - ( 8, 'm') : (LEFT, MOUSE_UP, ALT), # left_up Alt 0+ +8+ =8 - (12, 'm') : (LEFT, MOUSE_UP, SHIFT_ALT), # left_up Shift Alt 0+4+8+ =12 - (16, 'm') : (LEFT, MOUSE_UP, CONTROL), # left_up Control 0+ + +16=16 - (20, 'm') : (LEFT, MOUSE_UP, SHIFT_CONTROL), # left_up Shift Control 0+4+ +16=20 - (24, 'm') : (LEFT, MOUSE_UP, ALT_CONTROL), # left_up Alt Control 0+ +8+16=24 - (28, 'm') : (LEFT, MOUSE_UP, SHIFT_ALT_CONTROL), # left_up Shift Alt Control 0+4+8+16=28 - - ( 1, 'm') : (MIDDLE, MOUSE_UP, NO_MODIFIER), # middle_up 1+ + + =1 - ( 5, 'm') : (MIDDLE, MOUSE_UP, SHIFT), # middle_up Shift 1+4+ + =5 - ( 9, 'm') : (MIDDLE, MOUSE_UP, ALT), # middle_up Alt 1+ +8+ =9 - (13, 'm') : (MIDDLE, MOUSE_UP, SHIFT_ALT), # middle_up Shift Alt 1+4+8+ =13 - (17, 'm') : (MIDDLE, MOUSE_UP, CONTROL), # middle_up Control 1+ + +16=17 - (21, 'm') : (MIDDLE, MOUSE_UP, SHIFT_CONTROL), # middle_up Shift Control 1+4+ +16=21 - (25, 'm') : (MIDDLE, MOUSE_UP, ALT_CONTROL), # middle_up Alt Control 1+ +8+16=25 - (29, 'm') : (MIDDLE, MOUSE_UP, SHIFT_ALT_CONTROL), # middle_up Shift Alt Control 1+4+8+16=29 - - ( 2, 'm') : (RIGHT, MOUSE_UP, NO_MODIFIER), # right_up 2+ + + =2 - ( 6, 'm') : (RIGHT, MOUSE_UP, SHIFT), # right_up Shift 2+4+ + =6 - (10, 'm') : (RIGHT, MOUSE_UP, ALT), # right_up Alt 2+ +8+ =10 - (14, 'm') : (RIGHT, MOUSE_UP, SHIFT_ALT), # right_up Shift Alt 2+4+8+ =14 - (18, 'm') : (RIGHT, MOUSE_UP, CONTROL), # right_up Control 2+ + +16=18 - (22, 'm') : (RIGHT, MOUSE_UP, SHIFT_CONTROL), # right_up Shift Control 2+4+ +16=22 - (26, 'm') : (RIGHT, MOUSE_UP, ALT_CONTROL), # right_up Alt Control 2+ +8+16=26 - (30, 'm') : (RIGHT, MOUSE_UP, SHIFT_ALT_CONTROL), # right_up Shift Alt Control 2+4+8+16=30 - - ( 0, 'M') : (LEFT, MOUSE_DOWN, NO_MODIFIER), # left_down 0+ + + =0 - ( 4, 'M') : (LEFT, MOUSE_DOWN, SHIFT), # left_down Shift 0+4+ + =4 - ( 8, 'M') : (LEFT, MOUSE_DOWN, ALT), # left_down Alt 0+ +8+ =8 - (12, 'M') : (LEFT, MOUSE_DOWN, SHIFT_ALT), # left_down Shift Alt 0+4+8+ =12 - (16, 'M') : (LEFT, MOUSE_DOWN, CONTROL), # left_down Control 0+ + +16=16 - (20, 'M') : (LEFT, MOUSE_DOWN, SHIFT_CONTROL), # left_down Shift Control 0+4+ +16=20 - (24, 'M') : (LEFT, MOUSE_DOWN, ALT_CONTROL), # left_down Alt Control 0+ +8+16=24 - (28, 'M') : (LEFT, MOUSE_DOWN, SHIFT_ALT_CONTROL), # left_down Shift Alt Control 0+4+8+16=28 - - ( 1, 'M') : (MIDDLE, MOUSE_DOWN, NO_MODIFIER), # middle_down 1+ + + =1 - ( 5, 'M') : (MIDDLE, MOUSE_DOWN, SHIFT), # middle_down Shift 1+4+ + =5 - ( 9, 'M') : (MIDDLE, MOUSE_DOWN, ALT), # middle_down Alt 1+ +8+ =9 - (13, 'M') : (MIDDLE, MOUSE_DOWN, SHIFT_ALT), # middle_down Shift Alt 1+4+8+ =13 - (17, 'M') : (MIDDLE, MOUSE_DOWN, CONTROL), # middle_down Control 1+ + +16=17 - (21, 'M') : (MIDDLE, MOUSE_DOWN, SHIFT_CONTROL), # middle_down Shift Control 1+4+ +16=21 - (25, 'M') : (MIDDLE, MOUSE_DOWN, ALT_CONTROL), # middle_down Alt Control 1+ +8+16=25 - (29, 'M') : (MIDDLE, MOUSE_DOWN, SHIFT_ALT_CONTROL), # middle_down Shift Alt Control 1+4+8+16=29 - - ( 2, 'M') : (RIGHT, MOUSE_DOWN, NO_MODIFIER), # right_down 2+ + + =2 - ( 6, 'M') : (RIGHT, MOUSE_DOWN, SHIFT), # right_down Shift 2+4+ + =6 - (10, 'M') : (RIGHT, MOUSE_DOWN, ALT), # right_down Alt 2+ +8+ =10 - (14, 'M') : (RIGHT, MOUSE_DOWN, SHIFT_ALT), # right_down Shift Alt 2+4+8+ =14 - (18, 'M') : (RIGHT, MOUSE_DOWN, CONTROL), # right_down Control 2+ + +16=18 - (22, 'M') : (RIGHT, MOUSE_DOWN, SHIFT_CONTROL), # right_down Shift Control 2+4+ +16=22 - (26, 'M') : (RIGHT, MOUSE_DOWN, ALT_CONTROL), # right_down Alt Control 2+ +8+16=26 - (30, 'M') : (RIGHT, MOUSE_DOWN, SHIFT_ALT_CONTROL), # right_down Shift Alt Control 2+4+8+16=30 - - (32, 'M') : (LEFT, MOUSE_MOVE, NO_MODIFIER), # left_drag 32+ + + =32 - (36, 'M') : (LEFT, MOUSE_MOVE, SHIFT), # left_drag Shift 32+4+ + =36 - (40, 'M') : (LEFT, MOUSE_MOVE, ALT), # left_drag Alt 32+ +8+ =40 - (44, 'M') : (LEFT, MOUSE_MOVE, SHIFT_ALT), # left_drag Shift Alt 32+4+8+ =44 - (48, 'M') : (LEFT, MOUSE_MOVE, CONTROL), # left_drag Control 32+ + +16=48 - (52, 'M') : (LEFT, MOUSE_MOVE, SHIFT_CONTROL), # left_drag Shift Control 32+4+ +16=52 - (56, 'M') : (LEFT, MOUSE_MOVE, ALT_CONTROL), # left_drag Alt Control 32+ +8+16=56 - (60, 'M') : (LEFT, MOUSE_MOVE, SHIFT_ALT_CONTROL), # left_drag Shift Alt Control 32+4+8+16=60 - - (33, 'M') : (MIDDLE, MOUSE_MOVE, NO_MODIFIER), # middle_drag 33+ + + =33 - (37, 'M') : (MIDDLE, MOUSE_MOVE, SHIFT), # middle_drag Shift 33+4+ + =37 - (41, 'M') : (MIDDLE, MOUSE_MOVE, ALT), # middle_drag Alt 33+ +8+ =41 - (45, 'M') : (MIDDLE, MOUSE_MOVE, SHIFT_ALT), # middle_drag Shift Alt 33+4+8+ =45 - (49, 'M') : (MIDDLE, MOUSE_MOVE, CONTROL), # middle_drag Control 33+ + +16=49 - (53, 'M') : (MIDDLE, MOUSE_MOVE, SHIFT_CONTROL), # middle_drag Shift Control 33+4+ +16=53 - (57, 'M') : (MIDDLE, MOUSE_MOVE, ALT_CONTROL), # middle_drag Alt Control 33+ +8+16=57 - (61, 'M') : (MIDDLE, MOUSE_MOVE, SHIFT_ALT_CONTROL), # middle_drag Shift Alt Control 33+4+8+16=61 - - (34, 'M') : (RIGHT, MOUSE_MOVE, NO_MODIFIER), # right_drag 34+ + + =34 - (38, 'M') : (RIGHT, MOUSE_MOVE, SHIFT), # right_drag Shift 34+4+ + =38 - (42, 'M') : (RIGHT, MOUSE_MOVE, ALT), # right_drag Alt 34+ +8+ =42 - (46, 'M') : (RIGHT, MOUSE_MOVE, SHIFT_ALT), # right_drag Shift Alt 34+4+8+ =46 - (50, 'M') : (RIGHT, MOUSE_MOVE, CONTROL), # right_drag Control 34+ + +16=50 - (54, 'M') : (RIGHT, MOUSE_MOVE, SHIFT_CONTROL), # right_drag Shift Control 34+4+ +16=54 - (58, 'M') : (RIGHT, MOUSE_MOVE, ALT_CONTROL), # right_drag Alt Control 34+ +8+16=58 - (62, 'M') : (RIGHT, MOUSE_MOVE, SHIFT_ALT_CONTROL), # right_drag Shift Alt Control 34+4+8+16=62 - - (35, 'M') : (NO_BUTTON, MOUSE_MOVE, NO_MODIFIER), # none_drag 35+ + + =35 - (39, 'M') : (NO_BUTTON, MOUSE_MOVE, SHIFT), # none_drag Shift 35+4+ + =39 - (43, 'M') : (NO_BUTTON, MOUSE_MOVE, ALT), # none_drag Alt 35+ +8+ =43 - (47, 'M') : (NO_BUTTON, MOUSE_MOVE, SHIFT_ALT), # none_drag Shift Alt 35+4+8+ =47 - (51, 'M') : (NO_BUTTON, MOUSE_MOVE, CONTROL), # none_drag Control 35+ + +16=51 - (55, 'M') : (NO_BUTTON, MOUSE_MOVE, SHIFT_CONTROL), # none_drag Shift Control 35+4+ +16=55 - (59, 'M') : (NO_BUTTON, MOUSE_MOVE, ALT_CONTROL), # none_drag Alt Control 35+ +8+16=59 - (63, 'M') : (NO_BUTTON, MOUSE_MOVE, SHIFT_ALT_CONTROL), # none_drag Shift Alt Control 35+4+8+16=63 - - (64, 'M') : (NO_BUTTON, SCROLL_UP, NO_MODIFIER), # scroll_up 64+ + + =64 - (68, 'M') : (NO_BUTTON, SCROLL_UP, SHIFT), # scroll_up Shift 64+4+ + =68 - (72, 'M') : (NO_BUTTON, SCROLL_UP, ALT), # scroll_up Alt 64+ +8+ =72 - (76, 'M') : (NO_BUTTON, SCROLL_UP, SHIFT_ALT), # scroll_up Shift Alt 64+4+8+ =76 - (80, 'M') : (NO_BUTTON, SCROLL_UP, CONTROL), # scroll_up Control 64+ + +16=80 - (84, 'M') : (NO_BUTTON, SCROLL_UP, SHIFT_CONTROL), # scroll_up Shift Control 64+4+ +16=84 - (88, 'M') : (NO_BUTTON, SCROLL_UP, ALT_CONTROL), # scroll_up Alt Control 64+ +8+16=88 - (92, 'M') : (NO_BUTTON, SCROLL_UP, SHIFT_ALT_CONTROL), # scroll_up Shift Alt Control 64+4+8+16=92 - - (65, 'M') : (NO_BUTTON, SCROLL_DOWN, NO_MODIFIER), # scroll_down 64+ + + =65 - (69, 'M') : (NO_BUTTON, SCROLL_DOWN, SHIFT), # scroll_down Shift 64+4+ + =69 - (73, 'M') : (NO_BUTTON, SCROLL_DOWN, ALT), # scroll_down Alt 64+ +8+ =73 - (77, 'M') : (NO_BUTTON, SCROLL_DOWN, SHIFT_ALT), # scroll_down Shift Alt 64+4+8+ =77 - (81, 'M') : (NO_BUTTON, SCROLL_DOWN, CONTROL), # scroll_down Control 64+ + +16=81 - (85, 'M') : (NO_BUTTON, SCROLL_DOWN, SHIFT_CONTROL), # scroll_down Shift Control 64+4+ +16=85 - (89, 'M') : (NO_BUTTON, SCROLL_DOWN, ALT_CONTROL), # scroll_down Alt Control 64+ +8+16=89 - (93, 'M') : (NO_BUTTON, SCROLL_DOWN, SHIFT_ALT_CONTROL), # scroll_down Shift Alt Control 64+4+8+16=93 -} - -typical_mouse_events = { - 32: (LEFT , MOUSE_DOWN , UNKNOWN_MODIFIER), - 33: (MIDDLE , MOUSE_DOWN , UNKNOWN_MODIFIER), - 34: (RIGHT , MOUSE_DOWN , UNKNOWN_MODIFIER), - 35: (UNKNOWN_BUTTON , MOUSE_UP , UNKNOWN_MODIFIER), - - 64: (LEFT , MOUSE_MOVE , UNKNOWN_MODIFIER), - 65: (MIDDLE , MOUSE_MOVE , UNKNOWN_MODIFIER), - 66: (RIGHT , MOUSE_MOVE , UNKNOWN_MODIFIER), - 67: (NO_BUTTON , MOUSE_MOVE , UNKNOWN_MODIFIER), - - 96: (NO_BUTTON , SCROLL_UP , UNKNOWN_MODIFIER), - 97: (NO_BUTTON , SCROLL_DOWN, UNKNOWN_MODIFIER), -} - -urxvt_mouse_events={ - 32: (UNKNOWN_BUTTON, MOUSE_DOWN , UNKNOWN_MODIFIER), - 35: (UNKNOWN_BUTTON, MOUSE_UP , UNKNOWN_MODIFIER), - 96: (NO_BUTTON , SCROLL_UP , UNKNOWN_MODIFIER), - 97: (NO_BUTTON , SCROLL_DOWN, UNKNOWN_MODIFIER), -} -# fmt:on - - -def load_mouse_bindings() -> KeyBindings: - """ - Key bindings, required for mouse support. - (Mouse events enter through the key binding system.) - """ - key_bindings = KeyBindings() - - @key_bindings.add(Keys.Vt100MouseEvent) - def _(event: E) -> "NotImplementedOrNone": - """ - Handling of incoming mouse event. - """ - # TypicaL: "eSC[MaB*" - # Urxvt: "Esc[96;14;13M" - # Xterm SGR: "Esc[<64;85;12M" - - # Parse incoming packet. - if event.data[2] == "M": - # Typical. - mouse_event, x, y = map(ord, event.data[3:]) - - # TODO: Is it possible to add modifiers here? - mouse_button, mouse_event_type, mouse_modifier = typical_mouse_events[ - mouse_event - ] - - # Handle situations where `PosixStdinReader` used surrogateescapes. - if x >= 0xDC00: - x -= 0xDC00 - if y >= 0xDC00: - y -= 0xDC00 - - x -= 32 - y -= 32 - else: - # Urxvt and Xterm SGR. - # When the '<' is not present, we are not using the Xterm SGR mode, - # but Urxvt instead. - data = event.data[2:] - if data[:1] == "<": - sgr = True - data = data[1:] - else: - sgr = False - - # Extract coordinates. - mouse_event, x, y = map(int, data[:-1].split(";")) - m = data[-1] - - # Parse event type. - if sgr: - try: - ( - mouse_button, - mouse_event_type, - mouse_modifiers, - ) = xterm_sgr_mouse_events[mouse_event, m] - except KeyError: - return NotImplemented - - else: - # Some other terminals, like urxvt, Hyper terminal, ... - ( - mouse_button, - mouse_event_type, - mouse_modifiers, - ) = urxvt_mouse_events.get( - mouse_event, (UNKNOWN_BUTTON, MOUSE_MOVE, UNKNOWN_MODIFIER) - ) - - x -= 1 - y -= 1 - - # Only handle mouse events when we know the window height. - if event.app.renderer.height_is_known and mouse_event_type is not None: - # Take region above the layout into account. The reported - # coordinates are absolute to the visible part of the terminal. - from prompt_toolkit.renderer import HeightIsUnknownError - - try: - y -= event.app.renderer.rows_above_layout - except HeightIsUnknownError: - return NotImplemented - - # Call the mouse handler from the renderer. - - # Note: This can return `NotImplemented` if no mouse handler was - # found for this position, or if no repainting needs to - # happen. this way, we avoid excessive repaints during mouse - # movements. - handler = event.app.renderer.mouse_handlers.mouse_handlers[y][x] - return handler( - MouseEvent( - position=Point(x=x, y=y), - event_type=mouse_event_type, - button=mouse_button, - modifiers=mouse_modifiers, - ) - ) - - return NotImplemented - - @key_bindings.add(Keys.ScrollUp) - def _scroll_up(event: E) -> None: - """ - Scroll up event without cursor position. - """ - # We don't receive a cursor position, so we don't know which window to - # scroll. Just send an 'up' key press instead. - event.key_processor.feed(KeyPress(Keys.Up), first=True) - - @key_bindings.add(Keys.ScrollDown) - def _scroll_down(event: E) -> None: - """ - Scroll down event without cursor position. - """ - event.key_processor.feed(KeyPress(Keys.Down), first=True) - - @key_bindings.add(Keys.WindowsMouseEvent) - def _mouse(event: E) -> "NotImplementedOrNone": - """ - Handling of mouse events for Windows. - """ - assert is_windows() # This key binding should only exist for Windows. - - # Parse data. - pieces = event.data.split(";") - - button = MouseButton(pieces[0]) - event_type = MouseEventType(pieces[1]) - x = int(pieces[2]) - y = int(pieces[3]) - - # Make coordinates absolute to the visible part of the terminal. - output = event.app.renderer.output - - from prompt_toolkit.output.win32 import Win32Output - from prompt_toolkit.output.windows10 import Windows10_Output - - if isinstance(output, (Win32Output, Windows10_Output)): - screen_buffer_info = output.get_win32_screen_buffer_info() - rows_above_cursor = ( - screen_buffer_info.dwCursorPosition.Y - event.app.renderer._cursor_pos.y - ) - y -= rows_above_cursor - - # Call the mouse event handler. - # (Can return `NotImplemented`.) - handler = event.app.renderer.mouse_handlers.mouse_handlers[y][x] - - return handler( - MouseEvent( - position=Point(x=x, y=y), - event_type=event_type, - button=button, - modifiers=UNKNOWN_MODIFIER, - ) - ) - - # No mouse handler found. Return `NotImplemented` so that we don't - # invalidate the UI. - return NotImplemented - - return key_bindings +from typing import TYPE_CHECKING, FrozenSet + +from prompt_toolkit.data_structures import Point +from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent +from prompt_toolkit.keys import Keys +from prompt_toolkit.mouse_events import ( + MouseButton, + MouseEvent, + MouseEventType, + MouseModifier, +) +from prompt_toolkit.utils import is_windows + +from ..key_bindings import KeyBindings + +if TYPE_CHECKING: + from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone + +__all__ = [ + "load_mouse_bindings", +] + +E = KeyPressEvent + +# fmt: off +# flake8: noqa E201 +SCROLL_UP = MouseEventType.SCROLL_UP +SCROLL_DOWN = MouseEventType.SCROLL_DOWN +MOUSE_DOWN = MouseEventType.MOUSE_DOWN +MOUSE_MOVE = MouseEventType.MOUSE_MOVE +MOUSE_UP = MouseEventType.MOUSE_UP + +NO_MODIFIER : FrozenSet[MouseModifier] = frozenset() +SHIFT : FrozenSet[MouseModifier] = frozenset({MouseModifier.SHIFT}) +ALT : FrozenSet[MouseModifier] = frozenset({MouseModifier.ALT}) +SHIFT_ALT : FrozenSet[MouseModifier] = frozenset({MouseModifier.SHIFT, MouseModifier.ALT}) +CONTROL : FrozenSet[MouseModifier] = frozenset({MouseModifier.CONTROL}) +SHIFT_CONTROL : FrozenSet[MouseModifier] = frozenset({MouseModifier.SHIFT, MouseModifier.CONTROL}) +ALT_CONTROL : FrozenSet[MouseModifier] = frozenset({MouseModifier.ALT, MouseModifier.CONTROL}) +SHIFT_ALT_CONTROL: FrozenSet[MouseModifier] = frozenset({MouseModifier.SHIFT, MouseModifier.ALT, MouseModifier.CONTROL}) +UNKNOWN_MODIFIER : FrozenSet[MouseModifier] = frozenset() + +LEFT = MouseButton.LEFT +MIDDLE = MouseButton.MIDDLE +RIGHT = MouseButton.RIGHT +NO_BUTTON = MouseButton.NONE +UNKNOWN_BUTTON = MouseButton.UNKNOWN + +xterm_sgr_mouse_events = { + ( 0, 'm') : (LEFT, MOUSE_UP, NO_MODIFIER), # left_up 0+ + + =0 + ( 4, 'm') : (LEFT, MOUSE_UP, SHIFT), # left_up Shift 0+4+ + =4 + ( 8, 'm') : (LEFT, MOUSE_UP, ALT), # left_up Alt 0+ +8+ =8 + (12, 'm') : (LEFT, MOUSE_UP, SHIFT_ALT), # left_up Shift Alt 0+4+8+ =12 + (16, 'm') : (LEFT, MOUSE_UP, CONTROL), # left_up Control 0+ + +16=16 + (20, 'm') : (LEFT, MOUSE_UP, SHIFT_CONTROL), # left_up Shift Control 0+4+ +16=20 + (24, 'm') : (LEFT, MOUSE_UP, ALT_CONTROL), # left_up Alt Control 0+ +8+16=24 + (28, 'm') : (LEFT, MOUSE_UP, SHIFT_ALT_CONTROL), # left_up Shift Alt Control 0+4+8+16=28 + + ( 1, 'm') : (MIDDLE, MOUSE_UP, NO_MODIFIER), # middle_up 1+ + + =1 + ( 5, 'm') : (MIDDLE, MOUSE_UP, SHIFT), # middle_up Shift 1+4+ + =5 + ( 9, 'm') : (MIDDLE, MOUSE_UP, ALT), # middle_up Alt 1+ +8+ =9 + (13, 'm') : (MIDDLE, MOUSE_UP, SHIFT_ALT), # middle_up Shift Alt 1+4+8+ =13 + (17, 'm') : (MIDDLE, MOUSE_UP, CONTROL), # middle_up Control 1+ + +16=17 + (21, 'm') : (MIDDLE, MOUSE_UP, SHIFT_CONTROL), # middle_up Shift Control 1+4+ +16=21 + (25, 'm') : (MIDDLE, MOUSE_UP, ALT_CONTROL), # middle_up Alt Control 1+ +8+16=25 + (29, 'm') : (MIDDLE, MOUSE_UP, SHIFT_ALT_CONTROL), # middle_up Shift Alt Control 1+4+8+16=29 + + ( 2, 'm') : (RIGHT, MOUSE_UP, NO_MODIFIER), # right_up 2+ + + =2 + ( 6, 'm') : (RIGHT, MOUSE_UP, SHIFT), # right_up Shift 2+4+ + =6 + (10, 'm') : (RIGHT, MOUSE_UP, ALT), # right_up Alt 2+ +8+ =10 + (14, 'm') : (RIGHT, MOUSE_UP, SHIFT_ALT), # right_up Shift Alt 2+4+8+ =14 + (18, 'm') : (RIGHT, MOUSE_UP, CONTROL), # right_up Control 2+ + +16=18 + (22, 'm') : (RIGHT, MOUSE_UP, SHIFT_CONTROL), # right_up Shift Control 2+4+ +16=22 + (26, 'm') : (RIGHT, MOUSE_UP, ALT_CONTROL), # right_up Alt Control 2+ +8+16=26 + (30, 'm') : (RIGHT, MOUSE_UP, SHIFT_ALT_CONTROL), # right_up Shift Alt Control 2+4+8+16=30 + + ( 0, 'M') : (LEFT, MOUSE_DOWN, NO_MODIFIER), # left_down 0+ + + =0 + ( 4, 'M') : (LEFT, MOUSE_DOWN, SHIFT), # left_down Shift 0+4+ + =4 + ( 8, 'M') : (LEFT, MOUSE_DOWN, ALT), # left_down Alt 0+ +8+ =8 + (12, 'M') : (LEFT, MOUSE_DOWN, SHIFT_ALT), # left_down Shift Alt 0+4+8+ =12 + (16, 'M') : (LEFT, MOUSE_DOWN, CONTROL), # left_down Control 0+ + +16=16 + (20, 'M') : (LEFT, MOUSE_DOWN, SHIFT_CONTROL), # left_down Shift Control 0+4+ +16=20 + (24, 'M') : (LEFT, MOUSE_DOWN, ALT_CONTROL), # left_down Alt Control 0+ +8+16=24 + (28, 'M') : (LEFT, MOUSE_DOWN, SHIFT_ALT_CONTROL), # left_down Shift Alt Control 0+4+8+16=28 + + ( 1, 'M') : (MIDDLE, MOUSE_DOWN, NO_MODIFIER), # middle_down 1+ + + =1 + ( 5, 'M') : (MIDDLE, MOUSE_DOWN, SHIFT), # middle_down Shift 1+4+ + =5 + ( 9, 'M') : (MIDDLE, MOUSE_DOWN, ALT), # middle_down Alt 1+ +8+ =9 + (13, 'M') : (MIDDLE, MOUSE_DOWN, SHIFT_ALT), # middle_down Shift Alt 1+4+8+ =13 + (17, 'M') : (MIDDLE, MOUSE_DOWN, CONTROL), # middle_down Control 1+ + +16=17 + (21, 'M') : (MIDDLE, MOUSE_DOWN, SHIFT_CONTROL), # middle_down Shift Control 1+4+ +16=21 + (25, 'M') : (MIDDLE, MOUSE_DOWN, ALT_CONTROL), # middle_down Alt Control 1+ +8+16=25 + (29, 'M') : (MIDDLE, MOUSE_DOWN, SHIFT_ALT_CONTROL), # middle_down Shift Alt Control 1+4+8+16=29 + + ( 2, 'M') : (RIGHT, MOUSE_DOWN, NO_MODIFIER), # right_down 2+ + + =2 + ( 6, 'M') : (RIGHT, MOUSE_DOWN, SHIFT), # right_down Shift 2+4+ + =6 + (10, 'M') : (RIGHT, MOUSE_DOWN, ALT), # right_down Alt 2+ +8+ =10 + (14, 'M') : (RIGHT, MOUSE_DOWN, SHIFT_ALT), # right_down Shift Alt 2+4+8+ =14 + (18, 'M') : (RIGHT, MOUSE_DOWN, CONTROL), # right_down Control 2+ + +16=18 + (22, 'M') : (RIGHT, MOUSE_DOWN, SHIFT_CONTROL), # right_down Shift Control 2+4+ +16=22 + (26, 'M') : (RIGHT, MOUSE_DOWN, ALT_CONTROL), # right_down Alt Control 2+ +8+16=26 + (30, 'M') : (RIGHT, MOUSE_DOWN, SHIFT_ALT_CONTROL), # right_down Shift Alt Control 2+4+8+16=30 + + (32, 'M') : (LEFT, MOUSE_MOVE, NO_MODIFIER), # left_drag 32+ + + =32 + (36, 'M') : (LEFT, MOUSE_MOVE, SHIFT), # left_drag Shift 32+4+ + =36 + (40, 'M') : (LEFT, MOUSE_MOVE, ALT), # left_drag Alt 32+ +8+ =40 + (44, 'M') : (LEFT, MOUSE_MOVE, SHIFT_ALT), # left_drag Shift Alt 32+4+8+ =44 + (48, 'M') : (LEFT, MOUSE_MOVE, CONTROL), # left_drag Control 32+ + +16=48 + (52, 'M') : (LEFT, MOUSE_MOVE, SHIFT_CONTROL), # left_drag Shift Control 32+4+ +16=52 + (56, 'M') : (LEFT, MOUSE_MOVE, ALT_CONTROL), # left_drag Alt Control 32+ +8+16=56 + (60, 'M') : (LEFT, MOUSE_MOVE, SHIFT_ALT_CONTROL), # left_drag Shift Alt Control 32+4+8+16=60 + + (33, 'M') : (MIDDLE, MOUSE_MOVE, NO_MODIFIER), # middle_drag 33+ + + =33 + (37, 'M') : (MIDDLE, MOUSE_MOVE, SHIFT), # middle_drag Shift 33+4+ + =37 + (41, 'M') : (MIDDLE, MOUSE_MOVE, ALT), # middle_drag Alt 33+ +8+ =41 + (45, 'M') : (MIDDLE, MOUSE_MOVE, SHIFT_ALT), # middle_drag Shift Alt 33+4+8+ =45 + (49, 'M') : (MIDDLE, MOUSE_MOVE, CONTROL), # middle_drag Control 33+ + +16=49 + (53, 'M') : (MIDDLE, MOUSE_MOVE, SHIFT_CONTROL), # middle_drag Shift Control 33+4+ +16=53 + (57, 'M') : (MIDDLE, MOUSE_MOVE, ALT_CONTROL), # middle_drag Alt Control 33+ +8+16=57 + (61, 'M') : (MIDDLE, MOUSE_MOVE, SHIFT_ALT_CONTROL), # middle_drag Shift Alt Control 33+4+8+16=61 + + (34, 'M') : (RIGHT, MOUSE_MOVE, NO_MODIFIER), # right_drag 34+ + + =34 + (38, 'M') : (RIGHT, MOUSE_MOVE, SHIFT), # right_drag Shift 34+4+ + =38 + (42, 'M') : (RIGHT, MOUSE_MOVE, ALT), # right_drag Alt 34+ +8+ =42 + (46, 'M') : (RIGHT, MOUSE_MOVE, SHIFT_ALT), # right_drag Shift Alt 34+4+8+ =46 + (50, 'M') : (RIGHT, MOUSE_MOVE, CONTROL), # right_drag Control 34+ + +16=50 + (54, 'M') : (RIGHT, MOUSE_MOVE, SHIFT_CONTROL), # right_drag Shift Control 34+4+ +16=54 + (58, 'M') : (RIGHT, MOUSE_MOVE, ALT_CONTROL), # right_drag Alt Control 34+ +8+16=58 + (62, 'M') : (RIGHT, MOUSE_MOVE, SHIFT_ALT_CONTROL), # right_drag Shift Alt Control 34+4+8+16=62 + + (35, 'M') : (NO_BUTTON, MOUSE_MOVE, NO_MODIFIER), # none_drag 35+ + + =35 + (39, 'M') : (NO_BUTTON, MOUSE_MOVE, SHIFT), # none_drag Shift 35+4+ + =39 + (43, 'M') : (NO_BUTTON, MOUSE_MOVE, ALT), # none_drag Alt 35+ +8+ =43 + (47, 'M') : (NO_BUTTON, MOUSE_MOVE, SHIFT_ALT), # none_drag Shift Alt 35+4+8+ =47 + (51, 'M') : (NO_BUTTON, MOUSE_MOVE, CONTROL), # none_drag Control 35+ + +16=51 + (55, 'M') : (NO_BUTTON, MOUSE_MOVE, SHIFT_CONTROL), # none_drag Shift Control 35+4+ +16=55 + (59, 'M') : (NO_BUTTON, MOUSE_MOVE, ALT_CONTROL), # none_drag Alt Control 35+ +8+16=59 + (63, 'M') : (NO_BUTTON, MOUSE_MOVE, SHIFT_ALT_CONTROL), # none_drag Shift Alt Control 35+4+8+16=63 + + (64, 'M') : (NO_BUTTON, SCROLL_UP, NO_MODIFIER), # scroll_up 64+ + + =64 + (68, 'M') : (NO_BUTTON, SCROLL_UP, SHIFT), # scroll_up Shift 64+4+ + =68 + (72, 'M') : (NO_BUTTON, SCROLL_UP, ALT), # scroll_up Alt 64+ +8+ =72 + (76, 'M') : (NO_BUTTON, SCROLL_UP, SHIFT_ALT), # scroll_up Shift Alt 64+4+8+ =76 + (80, 'M') : (NO_BUTTON, SCROLL_UP, CONTROL), # scroll_up Control 64+ + +16=80 + (84, 'M') : (NO_BUTTON, SCROLL_UP, SHIFT_CONTROL), # scroll_up Shift Control 64+4+ +16=84 + (88, 'M') : (NO_BUTTON, SCROLL_UP, ALT_CONTROL), # scroll_up Alt Control 64+ +8+16=88 + (92, 'M') : (NO_BUTTON, SCROLL_UP, SHIFT_ALT_CONTROL), # scroll_up Shift Alt Control 64+4+8+16=92 + + (65, 'M') : (NO_BUTTON, SCROLL_DOWN, NO_MODIFIER), # scroll_down 64+ + + =65 + (69, 'M') : (NO_BUTTON, SCROLL_DOWN, SHIFT), # scroll_down Shift 64+4+ + =69 + (73, 'M') : (NO_BUTTON, SCROLL_DOWN, ALT), # scroll_down Alt 64+ +8+ =73 + (77, 'M') : (NO_BUTTON, SCROLL_DOWN, SHIFT_ALT), # scroll_down Shift Alt 64+4+8+ =77 + (81, 'M') : (NO_BUTTON, SCROLL_DOWN, CONTROL), # scroll_down Control 64+ + +16=81 + (85, 'M') : (NO_BUTTON, SCROLL_DOWN, SHIFT_CONTROL), # scroll_down Shift Control 64+4+ +16=85 + (89, 'M') : (NO_BUTTON, SCROLL_DOWN, ALT_CONTROL), # scroll_down Alt Control 64+ +8+16=89 + (93, 'M') : (NO_BUTTON, SCROLL_DOWN, SHIFT_ALT_CONTROL), # scroll_down Shift Alt Control 64+4+8+16=93 +} + +typical_mouse_events = { + 32: (LEFT , MOUSE_DOWN , UNKNOWN_MODIFIER), + 33: (MIDDLE , MOUSE_DOWN , UNKNOWN_MODIFIER), + 34: (RIGHT , MOUSE_DOWN , UNKNOWN_MODIFIER), + 35: (UNKNOWN_BUTTON , MOUSE_UP , UNKNOWN_MODIFIER), + + 64: (LEFT , MOUSE_MOVE , UNKNOWN_MODIFIER), + 65: (MIDDLE , MOUSE_MOVE , UNKNOWN_MODIFIER), + 66: (RIGHT , MOUSE_MOVE , UNKNOWN_MODIFIER), + 67: (NO_BUTTON , MOUSE_MOVE , UNKNOWN_MODIFIER), + + 96: (NO_BUTTON , SCROLL_UP , UNKNOWN_MODIFIER), + 97: (NO_BUTTON , SCROLL_DOWN, UNKNOWN_MODIFIER), +} + +urxvt_mouse_events={ + 32: (UNKNOWN_BUTTON, MOUSE_DOWN , UNKNOWN_MODIFIER), + 35: (UNKNOWN_BUTTON, MOUSE_UP , UNKNOWN_MODIFIER), + 96: (NO_BUTTON , SCROLL_UP , UNKNOWN_MODIFIER), + 97: (NO_BUTTON , SCROLL_DOWN, UNKNOWN_MODIFIER), +} +# fmt:on + + +def load_mouse_bindings() -> KeyBindings: + """ + Key bindings, required for mouse support. + (Mouse events enter through the key binding system.) + """ + key_bindings = KeyBindings() + + @key_bindings.add(Keys.Vt100MouseEvent) + def _(event: E) -> "NotImplementedOrNone": + """ + Handling of incoming mouse event. + """ + # TypicaL: "eSC[MaB*" + # Urxvt: "Esc[96;14;13M" + # Xterm SGR: "Esc[<64;85;12M" + + # Parse incoming packet. + if event.data[2] == "M": + # Typical. + mouse_event, x, y = map(ord, event.data[3:]) + + # TODO: Is it possible to add modifiers here? + mouse_button, mouse_event_type, mouse_modifier = typical_mouse_events[ + mouse_event + ] + + # Handle situations where `PosixStdinReader` used surrogateescapes. + if x >= 0xDC00: + x -= 0xDC00 + if y >= 0xDC00: + y -= 0xDC00 + + x -= 32 + y -= 32 + else: + # Urxvt and Xterm SGR. + # When the '<' is not present, we are not using the Xterm SGR mode, + # but Urxvt instead. + data = event.data[2:] + if data[:1] == "<": + sgr = True + data = data[1:] + else: + sgr = False + + # Extract coordinates. + mouse_event, x, y = map(int, data[:-1].split(";")) + m = data[-1] + + # Parse event type. + if sgr: + try: + ( + mouse_button, + mouse_event_type, + mouse_modifiers, + ) = xterm_sgr_mouse_events[mouse_event, m] + except KeyError: + return NotImplemented + + else: + # Some other terminals, like urxvt, Hyper terminal, ... + ( + mouse_button, + mouse_event_type, + mouse_modifiers, + ) = urxvt_mouse_events.get( + mouse_event, (UNKNOWN_BUTTON, MOUSE_MOVE, UNKNOWN_MODIFIER) + ) + + x -= 1 + y -= 1 + + # Only handle mouse events when we know the window height. + if event.app.renderer.height_is_known and mouse_event_type is not None: + # Take region above the layout into account. The reported + # coordinates are absolute to the visible part of the terminal. + from prompt_toolkit.renderer import HeightIsUnknownError + + try: + y -= event.app.renderer.rows_above_layout + except HeightIsUnknownError: + return NotImplemented + + # Call the mouse handler from the renderer. + + # Note: This can return `NotImplemented` if no mouse handler was + # found for this position, or if no repainting needs to + # happen. this way, we avoid excessive repaints during mouse + # movements. + handler = event.app.renderer.mouse_handlers.mouse_handlers[y][x] + return handler( + MouseEvent( + position=Point(x=x, y=y), + event_type=mouse_event_type, + button=mouse_button, + modifiers=mouse_modifiers, + ) + ) + + return NotImplemented + + @key_bindings.add(Keys.ScrollUp) + def _scroll_up(event: E) -> None: + """ + Scroll up event without cursor position. + """ + # We don't receive a cursor position, so we don't know which window to + # scroll. Just send an 'up' key press instead. + event.key_processor.feed(KeyPress(Keys.Up), first=True) + + @key_bindings.add(Keys.ScrollDown) + def _scroll_down(event: E) -> None: + """ + Scroll down event without cursor position. + """ + event.key_processor.feed(KeyPress(Keys.Down), first=True) + + @key_bindings.add(Keys.WindowsMouseEvent) + def _mouse(event: E) -> "NotImplementedOrNone": + """ + Handling of mouse events for Windows. + """ + assert is_windows() # This key binding should only exist for Windows. + + # Parse data. + pieces = event.data.split(";") + + button = MouseButton(pieces[0]) + event_type = MouseEventType(pieces[1]) + x = int(pieces[2]) + y = int(pieces[3]) + + # Make coordinates absolute to the visible part of the terminal. + output = event.app.renderer.output + + from prompt_toolkit.output.win32 import Win32Output + from prompt_toolkit.output.windows10 import Windows10_Output + + if isinstance(output, (Win32Output, Windows10_Output)): + screen_buffer_info = output.get_win32_screen_buffer_info() + rows_above_cursor = ( + screen_buffer_info.dwCursorPosition.Y - event.app.renderer._cursor_pos.y + ) + y -= rows_above_cursor + + # Call the mouse event handler. + # (Can return `NotImplemented`.) + handler = event.app.renderer.mouse_handlers.mouse_handlers[y][x] + + return handler( + MouseEvent( + position=Point(x=x, y=y), + event_type=event_type, + button=button, + modifiers=UNKNOWN_MODIFIER, + ) + ) + + # No mouse handler found. Return `NotImplemented` so that we don't + # invalidate the UI. + return NotImplemented + + return key_bindings diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/named_commands.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/named_commands.py index e0796ef0b8e..488420185f9 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/named_commands.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/named_commands.py @@ -1,687 +1,687 @@ -""" -Key bindings which are also known by GNU Readline by the given names. - -See: http://www.delorie.com/gnu/docs/readline/rlman_13.html -""" -from typing import Callable, Dict, TypeVar, Union, cast - -from prompt_toolkit.document import Document -from prompt_toolkit.enums import EditingMode -from prompt_toolkit.key_binding.key_bindings import Binding, key_binding -from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent -from prompt_toolkit.keys import Keys -from prompt_toolkit.layout.controls import BufferControl -from prompt_toolkit.search import SearchDirection -from prompt_toolkit.selection import PasteMode - -from .completion import display_completions_like_readline, generate_completions - -__all__ = [ - "get_by_name", -] - - -# Typing. -_Handler = Callable[[KeyPressEvent], None] -_HandlerOrBinding = Union[_Handler, Binding] -_T = TypeVar("_T", bound=_HandlerOrBinding) -E = KeyPressEvent - - -# Registry that maps the Readline command names to their handlers. -_readline_commands: Dict[str, Binding] = {} - - -def register(name: str) -> Callable[[_T], _T]: - """ - Store handler in the `_readline_commands` dictionary. - """ - - def decorator(handler: _T) -> _T: - "`handler` is a callable or Binding." - if isinstance(handler, Binding): - _readline_commands[name] = handler - else: - _readline_commands[name] = key_binding()(cast(_Handler, handler)) - - return handler - - return decorator - - -def get_by_name(name: str) -> Binding: - """ - Return the handler for the (Readline) command with the given name. - """ - try: - return _readline_commands[name] - except KeyError as e: - raise KeyError("Unknown Readline command: %r" % name) from e - - -# -# Commands for moving -# See: http://www.delorie.com/gnu/docs/readline/rlman_14.html -# - - -@register("beginning-of-buffer") -def beginning_of_buffer(event: E) -> None: - """ - Move to the start of the buffer. - """ - buff = event.current_buffer - buff.cursor_position = 0 - - -@register("end-of-buffer") -def end_of_buffer(event: E) -> None: - """ - Move to the end of the buffer. - """ - buff = event.current_buffer - buff.cursor_position = len(buff.text) - - -@register("beginning-of-line") -def beginning_of_line(event: E) -> None: - """ - Move to the start of the current line. - """ - buff = event.current_buffer - buff.cursor_position += buff.document.get_start_of_line_position( - after_whitespace=False - ) - - -@register("end-of-line") -def end_of_line(event: E) -> None: - """ - Move to the end of the line. - """ - buff = event.current_buffer - buff.cursor_position += buff.document.get_end_of_line_position() - - -@register("forward-char") -def forward_char(event: E) -> None: - """ - Move forward a character. - """ - buff = event.current_buffer - buff.cursor_position += buff.document.get_cursor_right_position(count=event.arg) - - -@register("backward-char") -def backward_char(event: E) -> None: - "Move back a character." - buff = event.current_buffer - buff.cursor_position += buff.document.get_cursor_left_position(count=event.arg) - - -@register("forward-word") -def forward_word(event: E) -> None: - """ - Move forward to the end of the next word. Words are composed of letters and - digits. - """ - buff = event.current_buffer - pos = buff.document.find_next_word_ending(count=event.arg) - - if pos: - buff.cursor_position += pos - - -@register("backward-word") -def backward_word(event: E) -> None: - """ - Move back to the start of the current or previous word. Words are composed - of letters and digits. - """ - buff = event.current_buffer - pos = buff.document.find_previous_word_beginning(count=event.arg) - - if pos: - buff.cursor_position += pos - - -@register("clear-screen") -def clear_screen(event: E) -> None: - """ - Clear the screen and redraw everything at the top of the screen. - """ - event.app.renderer.clear() - - -@register("redraw-current-line") -def redraw_current_line(event: E) -> None: - """ - Refresh the current line. - (Readline defines this command, but prompt-toolkit doesn't have it.) - """ - pass - - -# -# Commands for manipulating the history. -# See: http://www.delorie.com/gnu/docs/readline/rlman_15.html -# - - -@register("accept-line") -def accept_line(event: E) -> None: - """ - Accept the line regardless of where the cursor is. - """ - event.current_buffer.validate_and_handle() - - -@register("previous-history") -def previous_history(event: E) -> None: - """ - Move `back` through the history list, fetching the previous command. - """ - event.current_buffer.history_backward(count=event.arg) - - -@register("next-history") -def next_history(event: E) -> None: - """ - Move `forward` through the history list, fetching the next command. - """ - event.current_buffer.history_forward(count=event.arg) - - -@register("beginning-of-history") -def beginning_of_history(event: E) -> None: - """ - Move to the first line in the history. - """ - event.current_buffer.go_to_history(0) - - -@register("end-of-history") -def end_of_history(event: E) -> None: - """ - Move to the end of the input history, i.e., the line currently being entered. - """ +""" +Key bindings which are also known by GNU Readline by the given names. + +See: http://www.delorie.com/gnu/docs/readline/rlman_13.html +""" +from typing import Callable, Dict, TypeVar, Union, cast + +from prompt_toolkit.document import Document +from prompt_toolkit.enums import EditingMode +from prompt_toolkit.key_binding.key_bindings import Binding, key_binding +from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent +from prompt_toolkit.keys import Keys +from prompt_toolkit.layout.controls import BufferControl +from prompt_toolkit.search import SearchDirection +from prompt_toolkit.selection import PasteMode + +from .completion import display_completions_like_readline, generate_completions + +__all__ = [ + "get_by_name", +] + + +# Typing. +_Handler = Callable[[KeyPressEvent], None] +_HandlerOrBinding = Union[_Handler, Binding] +_T = TypeVar("_T", bound=_HandlerOrBinding) +E = KeyPressEvent + + +# Registry that maps the Readline command names to their handlers. +_readline_commands: Dict[str, Binding] = {} + + +def register(name: str) -> Callable[[_T], _T]: + """ + Store handler in the `_readline_commands` dictionary. + """ + + def decorator(handler: _T) -> _T: + "`handler` is a callable or Binding." + if isinstance(handler, Binding): + _readline_commands[name] = handler + else: + _readline_commands[name] = key_binding()(cast(_Handler, handler)) + + return handler + + return decorator + + +def get_by_name(name: str) -> Binding: + """ + Return the handler for the (Readline) command with the given name. + """ + try: + return _readline_commands[name] + except KeyError as e: + raise KeyError("Unknown Readline command: %r" % name) from e + + +# +# Commands for moving +# See: http://www.delorie.com/gnu/docs/readline/rlman_14.html +# + + +@register("beginning-of-buffer") +def beginning_of_buffer(event: E) -> None: + """ + Move to the start of the buffer. + """ + buff = event.current_buffer + buff.cursor_position = 0 + + +@register("end-of-buffer") +def end_of_buffer(event: E) -> None: + """ + Move to the end of the buffer. + """ + buff = event.current_buffer + buff.cursor_position = len(buff.text) + + +@register("beginning-of-line") +def beginning_of_line(event: E) -> None: + """ + Move to the start of the current line. + """ + buff = event.current_buffer + buff.cursor_position += buff.document.get_start_of_line_position( + after_whitespace=False + ) + + +@register("end-of-line") +def end_of_line(event: E) -> None: + """ + Move to the end of the line. + """ + buff = event.current_buffer + buff.cursor_position += buff.document.get_end_of_line_position() + + +@register("forward-char") +def forward_char(event: E) -> None: + """ + Move forward a character. + """ + buff = event.current_buffer + buff.cursor_position += buff.document.get_cursor_right_position(count=event.arg) + + +@register("backward-char") +def backward_char(event: E) -> None: + "Move back a character." + buff = event.current_buffer + buff.cursor_position += buff.document.get_cursor_left_position(count=event.arg) + + +@register("forward-word") +def forward_word(event: E) -> None: + """ + Move forward to the end of the next word. Words are composed of letters and + digits. + """ + buff = event.current_buffer + pos = buff.document.find_next_word_ending(count=event.arg) + + if pos: + buff.cursor_position += pos + + +@register("backward-word") +def backward_word(event: E) -> None: + """ + Move back to the start of the current or previous word. Words are composed + of letters and digits. + """ + buff = event.current_buffer + pos = buff.document.find_previous_word_beginning(count=event.arg) + + if pos: + buff.cursor_position += pos + + +@register("clear-screen") +def clear_screen(event: E) -> None: + """ + Clear the screen and redraw everything at the top of the screen. + """ + event.app.renderer.clear() + + +@register("redraw-current-line") +def redraw_current_line(event: E) -> None: + """ + Refresh the current line. + (Readline defines this command, but prompt-toolkit doesn't have it.) + """ + pass + + +# +# Commands for manipulating the history. +# See: http://www.delorie.com/gnu/docs/readline/rlman_15.html +# + + +@register("accept-line") +def accept_line(event: E) -> None: + """ + Accept the line regardless of where the cursor is. + """ + event.current_buffer.validate_and_handle() + + +@register("previous-history") +def previous_history(event: E) -> None: + """ + Move `back` through the history list, fetching the previous command. + """ + event.current_buffer.history_backward(count=event.arg) + + +@register("next-history") +def next_history(event: E) -> None: + """ + Move `forward` through the history list, fetching the next command. + """ + event.current_buffer.history_forward(count=event.arg) + + +@register("beginning-of-history") +def beginning_of_history(event: E) -> None: + """ + Move to the first line in the history. + """ + event.current_buffer.go_to_history(0) + + +@register("end-of-history") +def end_of_history(event: E) -> None: + """ + Move to the end of the input history, i.e., the line currently being entered. + """ event.current_buffer.history_forward(count=10**100) - buff = event.current_buffer - buff.go_to_history(len(buff._working_lines) - 1) - - -@register("reverse-search-history") -def reverse_search_history(event: E) -> None: - """ - Search backward starting at the current line and moving `up` through - the history as necessary. This is an incremental search. - """ - control = event.app.layout.current_control - - if isinstance(control, BufferControl) and control.search_buffer_control: - event.app.current_search_state.direction = SearchDirection.BACKWARD - event.app.layout.current_control = control.search_buffer_control - - -# -# Commands for changing text -# - - -@register("end-of-file") -def end_of_file(event: E) -> None: - """ - Exit. - """ - event.app.exit() - - -@register("delete-char") -def delete_char(event: E) -> None: - """ - Delete character before the cursor. - """ - deleted = event.current_buffer.delete(count=event.arg) - if not deleted: - event.app.output.bell() - - -@register("backward-delete-char") -def backward_delete_char(event: E) -> None: - """ - Delete the character behind the cursor. - """ - if event.arg < 0: - # When a negative argument has been given, this should delete in front - # of the cursor. - deleted = event.current_buffer.delete(count=-event.arg) - else: - deleted = event.current_buffer.delete_before_cursor(count=event.arg) - - if not deleted: - event.app.output.bell() - - -@register("self-insert") -def self_insert(event: E) -> None: - """ - Insert yourself. - """ - event.current_buffer.insert_text(event.data * event.arg) - - -@register("transpose-chars") -def transpose_chars(event: E) -> None: - """ - Emulate Emacs transpose-char behavior: at the beginning of the buffer, - do nothing. At the end of a line or buffer, swap the characters before - the cursor. Otherwise, move the cursor right, and then swap the - characters before the cursor. - """ - b = event.current_buffer - p = b.cursor_position - if p == 0: - return - elif p == len(b.text) or b.text[p] == "\n": - b.swap_characters_before_cursor() - else: - b.cursor_position += b.document.get_cursor_right_position() - b.swap_characters_before_cursor() - - -@register("uppercase-word") -def uppercase_word(event: E) -> None: - """ - Uppercase the current (or following) word. - """ - buff = event.current_buffer - - for i in range(event.arg): - pos = buff.document.find_next_word_ending() - words = buff.document.text_after_cursor[:pos] - buff.insert_text(words.upper(), overwrite=True) - - -@register("downcase-word") -def downcase_word(event: E) -> None: - """ - Lowercase the current (or following) word. - """ - buff = event.current_buffer - - for i in range(event.arg): # XXX: not DRY: see meta_c and meta_u!! - pos = buff.document.find_next_word_ending() - words = buff.document.text_after_cursor[:pos] - buff.insert_text(words.lower(), overwrite=True) - - -@register("capitalize-word") -def capitalize_word(event: E) -> None: - """ - Capitalize the current (or following) word. - """ - buff = event.current_buffer - - for i in range(event.arg): - pos = buff.document.find_next_word_ending() - words = buff.document.text_after_cursor[:pos] - buff.insert_text(words.title(), overwrite=True) - - -@register("quoted-insert") -def quoted_insert(event: E) -> None: - """ - Add the next character typed to the line verbatim. This is how to insert - key sequences like C-q, for example. - """ - event.app.quoted_insert = True - - -# -# Killing and yanking. -# - - -@register("kill-line") -def kill_line(event: E) -> None: - """ - Kill the text from the cursor to the end of the line. - - If we are at the end of the line, this should remove the newline. - (That way, it is possible to delete multiple lines by executing this - command multiple times.) - """ - buff = event.current_buffer - if event.arg < 0: - deleted = buff.delete_before_cursor( - count=-buff.document.get_start_of_line_position() - ) - else: - if buff.document.current_char == "\n": - deleted = buff.delete(1) - else: - deleted = buff.delete(count=buff.document.get_end_of_line_position()) - event.app.clipboard.set_text(deleted) - - -@register("kill-word") -def kill_word(event: E) -> None: - """ - Kill from point to the end of the current word, or if between words, to the - end of the next word. Word boundaries are the same as forward-word. - """ - buff = event.current_buffer - pos = buff.document.find_next_word_ending(count=event.arg) - - if pos: - deleted = buff.delete(count=pos) - - if event.is_repeat: - deleted = event.app.clipboard.get_data().text + deleted - - event.app.clipboard.set_text(deleted) - - -@register("unix-word-rubout") -def unix_word_rubout(event: E, WORD: bool = True) -> None: - """ - Kill the word behind point, using whitespace as a word boundary. - Usually bound to ControlW. - """ - buff = event.current_buffer - pos = buff.document.find_start_of_previous_word(count=event.arg, WORD=WORD) - - if pos is None: - # Nothing found? delete until the start of the document. (The - # input starts with whitespace and no words were found before the - # cursor.) - pos = -buff.cursor_position - - if pos: - deleted = buff.delete_before_cursor(count=-pos) - - # If the previous key press was also Control-W, concatenate deleted - # text. - if event.is_repeat: - deleted += event.app.clipboard.get_data().text - - event.app.clipboard.set_text(deleted) - else: - # Nothing to delete. Bell. - event.app.output.bell() - - -@register("backward-kill-word") -def backward_kill_word(event: E) -> None: - """ - Kills the word before point, using "not a letter nor a digit" as a word boundary. - Usually bound to M-Del or M-Backspace. - """ - unix_word_rubout(event, WORD=False) - - -@register("delete-horizontal-space") -def delete_horizontal_space(event: E) -> None: - """ - Delete all spaces and tabs around point. - """ - buff = event.current_buffer - text_before_cursor = buff.document.text_before_cursor - text_after_cursor = buff.document.text_after_cursor - - delete_before = len(text_before_cursor) - len(text_before_cursor.rstrip("\t ")) - delete_after = len(text_after_cursor) - len(text_after_cursor.lstrip("\t ")) - - buff.delete_before_cursor(count=delete_before) - buff.delete(count=delete_after) - - -@register("unix-line-discard") -def unix_line_discard(event: E) -> None: - """ - Kill backward from the cursor to the beginning of the current line. - """ - buff = event.current_buffer - - if buff.document.cursor_position_col == 0 and buff.document.cursor_position > 0: - buff.delete_before_cursor(count=1) - else: - deleted = buff.delete_before_cursor( - count=-buff.document.get_start_of_line_position() - ) - event.app.clipboard.set_text(deleted) - - -@register("yank") -def yank(event: E) -> None: - """ - Paste before cursor. - """ - event.current_buffer.paste_clipboard_data( - event.app.clipboard.get_data(), count=event.arg, paste_mode=PasteMode.EMACS - ) - - -@register("yank-nth-arg") -def yank_nth_arg(event: E) -> None: - """ - Insert the first argument of the previous command. With an argument, insert - the nth word from the previous command (start counting at 0). - """ - n = event.arg if event.arg_present else None - event.current_buffer.yank_nth_arg(n) - - -@register("yank-last-arg") -def yank_last_arg(event: E) -> None: - """ - Like `yank_nth_arg`, but if no argument has been given, yank the last word - of each line. - """ - n = event.arg if event.arg_present else None - event.current_buffer.yank_last_arg(n) - - -@register("yank-pop") -def yank_pop(event: E) -> None: - """ - Rotate the kill ring, and yank the new top. Only works following yank or - yank-pop. - """ - buff = event.current_buffer - doc_before_paste = buff.document_before_paste - clipboard = event.app.clipboard - - if doc_before_paste is not None: - buff.document = doc_before_paste - clipboard.rotate() - buff.paste_clipboard_data(clipboard.get_data(), paste_mode=PasteMode.EMACS) - - -# -# Completion. -# - - -@register("complete") -def complete(event: E) -> None: - """ - Attempt to perform completion. - """ - display_completions_like_readline(event) - - -@register("menu-complete") -def menu_complete(event: E) -> None: - """ - Generate completions, or go to the next completion. (This is the default - way of completing input in prompt_toolkit.) - """ - generate_completions(event) - - -@register("menu-complete-backward") -def menu_complete_backward(event: E) -> None: - """ - Move backward through the list of possible completions. - """ - event.current_buffer.complete_previous() - - -# -# Keyboard macros. -# - - -@register("start-kbd-macro") -def start_kbd_macro(event: E) -> None: - """ - Begin saving the characters typed into the current keyboard macro. - """ - event.app.emacs_state.start_macro() - - -@register("end-kbd-macro") -def end_kbd_macro(event: E) -> None: - """ - Stop saving the characters typed into the current keyboard macro and save - the definition. - """ - event.app.emacs_state.end_macro() - - -@register("call-last-kbd-macro") -@key_binding(record_in_macro=False) -def call_last_kbd_macro(event: E) -> None: - """ - Re-execute the last keyboard macro defined, by making the characters in the - macro appear as if typed at the keyboard. - - Notice that we pass `record_in_macro=False`. This ensures that the 'c-x e' - key sequence doesn't appear in the recording itself. This function inserts - the body of the called macro back into the KeyProcessor, so these keys will - be added later on to the macro of their handlers have `record_in_macro=True`. - """ - # Insert the macro. - macro = event.app.emacs_state.macro - - if macro: - event.app.key_processor.feed_multiple(macro, first=True) - - -@register("print-last-kbd-macro") -def print_last_kbd_macro(event: E) -> None: - """ - Print the last keyboard macro. - """ - # TODO: Make the format suitable for the inputrc file. - def print_macro() -> None: - macro = event.app.emacs_state.macro - if macro: - for k in macro: - print(k) - - from prompt_toolkit.application.run_in_terminal import run_in_terminal - - run_in_terminal(print_macro) - - -# -# Miscellaneous Commands. -# - - -@register("undo") -def undo(event: E) -> None: - """ - Incremental undo. - """ - event.current_buffer.undo() - - -@register("insert-comment") -def insert_comment(event: E) -> None: - """ - Without numeric argument, comment all lines. - With numeric argument, uncomment all lines. - In any case accept the input. - """ - buff = event.current_buffer - - # Transform all lines. - if event.arg != 1: - - def change(line: str) -> str: - return line[1:] if line.startswith("#") else line - - else: - - def change(line: str) -> str: - return "#" + line - - buff.document = Document( - text="\n".join(map(change, buff.text.splitlines())), cursor_position=0 - ) - - # Accept input. - buff.validate_and_handle() - - -@register("vi-editing-mode") -def vi_editing_mode(event: E) -> None: - """ - Switch to Vi editing mode. - """ - event.app.editing_mode = EditingMode.VI - - -@register("emacs-editing-mode") -def emacs_editing_mode(event: E) -> None: - """ - Switch to Emacs editing mode. - """ - event.app.editing_mode = EditingMode.EMACS - - -@register("prefix-meta") -def prefix_meta(event: E) -> None: - """ - Metafy the next character typed. This is for keyboards without a meta key. - - Sometimes people also want to bind other keys to Meta, e.g. 'jj':: - - key_bindings.add_key_binding('j', 'j', filter=ViInsertMode())(prefix_meta) - """ - # ('first' should be true, because we want to insert it at the current - # position in the queue.) - event.app.key_processor.feed(KeyPress(Keys.Escape), first=True) - - -@register("operate-and-get-next") -def operate_and_get_next(event: E) -> None: - """ - Accept the current line for execution and fetch the next line relative to - the current line from the history for editing. - """ - buff = event.current_buffer - new_index = buff.working_index + 1 - - # Accept the current input. (This will also redraw the interface in the - # 'done' state.) - buff.validate_and_handle() - - # Set the new index at the start of the next run. - def set_working_index() -> None: - if new_index < len(buff._working_lines): - buff.working_index = new_index - - event.app.pre_run_callables.append(set_working_index) - - -@register("edit-and-execute-command") -def edit_and_execute(event: E) -> None: - """ - Invoke an editor on the current command line, and accept the result. - """ - buff = event.current_buffer - buff.open_in_editor(validate_and_handle=True) + buff = event.current_buffer + buff.go_to_history(len(buff._working_lines) - 1) + + +@register("reverse-search-history") +def reverse_search_history(event: E) -> None: + """ + Search backward starting at the current line and moving `up` through + the history as necessary. This is an incremental search. + """ + control = event.app.layout.current_control + + if isinstance(control, BufferControl) and control.search_buffer_control: + event.app.current_search_state.direction = SearchDirection.BACKWARD + event.app.layout.current_control = control.search_buffer_control + + +# +# Commands for changing text +# + + +@register("end-of-file") +def end_of_file(event: E) -> None: + """ + Exit. + """ + event.app.exit() + + +@register("delete-char") +def delete_char(event: E) -> None: + """ + Delete character before the cursor. + """ + deleted = event.current_buffer.delete(count=event.arg) + if not deleted: + event.app.output.bell() + + +@register("backward-delete-char") +def backward_delete_char(event: E) -> None: + """ + Delete the character behind the cursor. + """ + if event.arg < 0: + # When a negative argument has been given, this should delete in front + # of the cursor. + deleted = event.current_buffer.delete(count=-event.arg) + else: + deleted = event.current_buffer.delete_before_cursor(count=event.arg) + + if not deleted: + event.app.output.bell() + + +@register("self-insert") +def self_insert(event: E) -> None: + """ + Insert yourself. + """ + event.current_buffer.insert_text(event.data * event.arg) + + +@register("transpose-chars") +def transpose_chars(event: E) -> None: + """ + Emulate Emacs transpose-char behavior: at the beginning of the buffer, + do nothing. At the end of a line or buffer, swap the characters before + the cursor. Otherwise, move the cursor right, and then swap the + characters before the cursor. + """ + b = event.current_buffer + p = b.cursor_position + if p == 0: + return + elif p == len(b.text) or b.text[p] == "\n": + b.swap_characters_before_cursor() + else: + b.cursor_position += b.document.get_cursor_right_position() + b.swap_characters_before_cursor() + + +@register("uppercase-word") +def uppercase_word(event: E) -> None: + """ + Uppercase the current (or following) word. + """ + buff = event.current_buffer + + for i in range(event.arg): + pos = buff.document.find_next_word_ending() + words = buff.document.text_after_cursor[:pos] + buff.insert_text(words.upper(), overwrite=True) + + +@register("downcase-word") +def downcase_word(event: E) -> None: + """ + Lowercase the current (or following) word. + """ + buff = event.current_buffer + + for i in range(event.arg): # XXX: not DRY: see meta_c and meta_u!! + pos = buff.document.find_next_word_ending() + words = buff.document.text_after_cursor[:pos] + buff.insert_text(words.lower(), overwrite=True) + + +@register("capitalize-word") +def capitalize_word(event: E) -> None: + """ + Capitalize the current (or following) word. + """ + buff = event.current_buffer + + for i in range(event.arg): + pos = buff.document.find_next_word_ending() + words = buff.document.text_after_cursor[:pos] + buff.insert_text(words.title(), overwrite=True) + + +@register("quoted-insert") +def quoted_insert(event: E) -> None: + """ + Add the next character typed to the line verbatim. This is how to insert + key sequences like C-q, for example. + """ + event.app.quoted_insert = True + + +# +# Killing and yanking. +# + + +@register("kill-line") +def kill_line(event: E) -> None: + """ + Kill the text from the cursor to the end of the line. + + If we are at the end of the line, this should remove the newline. + (That way, it is possible to delete multiple lines by executing this + command multiple times.) + """ + buff = event.current_buffer + if event.arg < 0: + deleted = buff.delete_before_cursor( + count=-buff.document.get_start_of_line_position() + ) + else: + if buff.document.current_char == "\n": + deleted = buff.delete(1) + else: + deleted = buff.delete(count=buff.document.get_end_of_line_position()) + event.app.clipboard.set_text(deleted) + + +@register("kill-word") +def kill_word(event: E) -> None: + """ + Kill from point to the end of the current word, or if between words, to the + end of the next word. Word boundaries are the same as forward-word. + """ + buff = event.current_buffer + pos = buff.document.find_next_word_ending(count=event.arg) + + if pos: + deleted = buff.delete(count=pos) + + if event.is_repeat: + deleted = event.app.clipboard.get_data().text + deleted + + event.app.clipboard.set_text(deleted) + + +@register("unix-word-rubout") +def unix_word_rubout(event: E, WORD: bool = True) -> None: + """ + Kill the word behind point, using whitespace as a word boundary. + Usually bound to ControlW. + """ + buff = event.current_buffer + pos = buff.document.find_start_of_previous_word(count=event.arg, WORD=WORD) + + if pos is None: + # Nothing found? delete until the start of the document. (The + # input starts with whitespace and no words were found before the + # cursor.) + pos = -buff.cursor_position + + if pos: + deleted = buff.delete_before_cursor(count=-pos) + + # If the previous key press was also Control-W, concatenate deleted + # text. + if event.is_repeat: + deleted += event.app.clipboard.get_data().text + + event.app.clipboard.set_text(deleted) + else: + # Nothing to delete. Bell. + event.app.output.bell() + + +@register("backward-kill-word") +def backward_kill_word(event: E) -> None: + """ + Kills the word before point, using "not a letter nor a digit" as a word boundary. + Usually bound to M-Del or M-Backspace. + """ + unix_word_rubout(event, WORD=False) + + +@register("delete-horizontal-space") +def delete_horizontal_space(event: E) -> None: + """ + Delete all spaces and tabs around point. + """ + buff = event.current_buffer + text_before_cursor = buff.document.text_before_cursor + text_after_cursor = buff.document.text_after_cursor + + delete_before = len(text_before_cursor) - len(text_before_cursor.rstrip("\t ")) + delete_after = len(text_after_cursor) - len(text_after_cursor.lstrip("\t ")) + + buff.delete_before_cursor(count=delete_before) + buff.delete(count=delete_after) + + +@register("unix-line-discard") +def unix_line_discard(event: E) -> None: + """ + Kill backward from the cursor to the beginning of the current line. + """ + buff = event.current_buffer + + if buff.document.cursor_position_col == 0 and buff.document.cursor_position > 0: + buff.delete_before_cursor(count=1) + else: + deleted = buff.delete_before_cursor( + count=-buff.document.get_start_of_line_position() + ) + event.app.clipboard.set_text(deleted) + + +@register("yank") +def yank(event: E) -> None: + """ + Paste before cursor. + """ + event.current_buffer.paste_clipboard_data( + event.app.clipboard.get_data(), count=event.arg, paste_mode=PasteMode.EMACS + ) + + +@register("yank-nth-arg") +def yank_nth_arg(event: E) -> None: + """ + Insert the first argument of the previous command. With an argument, insert + the nth word from the previous command (start counting at 0). + """ + n = event.arg if event.arg_present else None + event.current_buffer.yank_nth_arg(n) + + +@register("yank-last-arg") +def yank_last_arg(event: E) -> None: + """ + Like `yank_nth_arg`, but if no argument has been given, yank the last word + of each line. + """ + n = event.arg if event.arg_present else None + event.current_buffer.yank_last_arg(n) + + +@register("yank-pop") +def yank_pop(event: E) -> None: + """ + Rotate the kill ring, and yank the new top. Only works following yank or + yank-pop. + """ + buff = event.current_buffer + doc_before_paste = buff.document_before_paste + clipboard = event.app.clipboard + + if doc_before_paste is not None: + buff.document = doc_before_paste + clipboard.rotate() + buff.paste_clipboard_data(clipboard.get_data(), paste_mode=PasteMode.EMACS) + + +# +# Completion. +# + + +@register("complete") +def complete(event: E) -> None: + """ + Attempt to perform completion. + """ + display_completions_like_readline(event) + + +@register("menu-complete") +def menu_complete(event: E) -> None: + """ + Generate completions, or go to the next completion. (This is the default + way of completing input in prompt_toolkit.) + """ + generate_completions(event) + + +@register("menu-complete-backward") +def menu_complete_backward(event: E) -> None: + """ + Move backward through the list of possible completions. + """ + event.current_buffer.complete_previous() + + +# +# Keyboard macros. +# + + +@register("start-kbd-macro") +def start_kbd_macro(event: E) -> None: + """ + Begin saving the characters typed into the current keyboard macro. + """ + event.app.emacs_state.start_macro() + + +@register("end-kbd-macro") +def end_kbd_macro(event: E) -> None: + """ + Stop saving the characters typed into the current keyboard macro and save + the definition. + """ + event.app.emacs_state.end_macro() + + +@register("call-last-kbd-macro") +@key_binding(record_in_macro=False) +def call_last_kbd_macro(event: E) -> None: + """ + Re-execute the last keyboard macro defined, by making the characters in the + macro appear as if typed at the keyboard. + + Notice that we pass `record_in_macro=False`. This ensures that the 'c-x e' + key sequence doesn't appear in the recording itself. This function inserts + the body of the called macro back into the KeyProcessor, so these keys will + be added later on to the macro of their handlers have `record_in_macro=True`. + """ + # Insert the macro. + macro = event.app.emacs_state.macro + + if macro: + event.app.key_processor.feed_multiple(macro, first=True) + + +@register("print-last-kbd-macro") +def print_last_kbd_macro(event: E) -> None: + """ + Print the last keyboard macro. + """ + # TODO: Make the format suitable for the inputrc file. + def print_macro() -> None: + macro = event.app.emacs_state.macro + if macro: + for k in macro: + print(k) + + from prompt_toolkit.application.run_in_terminal import run_in_terminal + + run_in_terminal(print_macro) + + +# +# Miscellaneous Commands. +# + + +@register("undo") +def undo(event: E) -> None: + """ + Incremental undo. + """ + event.current_buffer.undo() + + +@register("insert-comment") +def insert_comment(event: E) -> None: + """ + Without numeric argument, comment all lines. + With numeric argument, uncomment all lines. + In any case accept the input. + """ + buff = event.current_buffer + + # Transform all lines. + if event.arg != 1: + + def change(line: str) -> str: + return line[1:] if line.startswith("#") else line + + else: + + def change(line: str) -> str: + return "#" + line + + buff.document = Document( + text="\n".join(map(change, buff.text.splitlines())), cursor_position=0 + ) + + # Accept input. + buff.validate_and_handle() + + +@register("vi-editing-mode") +def vi_editing_mode(event: E) -> None: + """ + Switch to Vi editing mode. + """ + event.app.editing_mode = EditingMode.VI + + +@register("emacs-editing-mode") +def emacs_editing_mode(event: E) -> None: + """ + Switch to Emacs editing mode. + """ + event.app.editing_mode = EditingMode.EMACS + + +@register("prefix-meta") +def prefix_meta(event: E) -> None: + """ + Metafy the next character typed. This is for keyboards without a meta key. + + Sometimes people also want to bind other keys to Meta, e.g. 'jj':: + + key_bindings.add_key_binding('j', 'j', filter=ViInsertMode())(prefix_meta) + """ + # ('first' should be true, because we want to insert it at the current + # position in the queue.) + event.app.key_processor.feed(KeyPress(Keys.Escape), first=True) + + +@register("operate-and-get-next") +def operate_and_get_next(event: E) -> None: + """ + Accept the current line for execution and fetch the next line relative to + the current line from the history for editing. + """ + buff = event.current_buffer + new_index = buff.working_index + 1 + + # Accept the current input. (This will also redraw the interface in the + # 'done' state.) + buff.validate_and_handle() + + # Set the new index at the start of the next run. + def set_working_index() -> None: + if new_index < len(buff._working_lines): + buff.working_index = new_index + + event.app.pre_run_callables.append(set_working_index) + + +@register("edit-and-execute-command") +def edit_and_execute(event: E) -> None: + """ + Invoke an editor on the current command line, and accept the result. + """ + buff = event.current_buffer + buff.open_in_editor(validate_and_handle=True) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/open_in_editor.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/open_in_editor.py index f8699f4a45b..41647be40f6 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/open_in_editor.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/open_in_editor.py @@ -1,49 +1,49 @@ -""" -Open in editor key bindings. -""" -from prompt_toolkit.filters import emacs_mode, has_selection, vi_navigation_mode - -from ..key_bindings import KeyBindings, KeyBindingsBase, merge_key_bindings -from .named_commands import get_by_name - -__all__ = [ - "load_open_in_editor_bindings", - "load_emacs_open_in_editor_bindings", - "load_vi_open_in_editor_bindings", -] - - -def load_open_in_editor_bindings() -> KeyBindingsBase: - """ - Load both the Vi and emacs key bindings for handling edit-and-execute-command. - """ - return merge_key_bindings( - [ - load_emacs_open_in_editor_bindings(), - load_vi_open_in_editor_bindings(), - ] - ) - - -def load_emacs_open_in_editor_bindings() -> KeyBindings: - """ - Pressing C-X C-E will open the buffer in an external editor. - """ - key_bindings = KeyBindings() - - key_bindings.add("c-x", "c-e", filter=emacs_mode & ~has_selection)( - get_by_name("edit-and-execute-command") - ) - - return key_bindings - - -def load_vi_open_in_editor_bindings() -> KeyBindings: - """ - Pressing 'v' in navigation mode will open the buffer in an external editor. - """ - key_bindings = KeyBindings() - key_bindings.add("v", filter=vi_navigation_mode)( - get_by_name("edit-and-execute-command") - ) - return key_bindings +""" +Open in editor key bindings. +""" +from prompt_toolkit.filters import emacs_mode, has_selection, vi_navigation_mode + +from ..key_bindings import KeyBindings, KeyBindingsBase, merge_key_bindings +from .named_commands import get_by_name + +__all__ = [ + "load_open_in_editor_bindings", + "load_emacs_open_in_editor_bindings", + "load_vi_open_in_editor_bindings", +] + + +def load_open_in_editor_bindings() -> KeyBindingsBase: + """ + Load both the Vi and emacs key bindings for handling edit-and-execute-command. + """ + return merge_key_bindings( + [ + load_emacs_open_in_editor_bindings(), + load_vi_open_in_editor_bindings(), + ] + ) + + +def load_emacs_open_in_editor_bindings() -> KeyBindings: + """ + Pressing C-X C-E will open the buffer in an external editor. + """ + key_bindings = KeyBindings() + + key_bindings.add("c-x", "c-e", filter=emacs_mode & ~has_selection)( + get_by_name("edit-and-execute-command") + ) + + return key_bindings + + +def load_vi_open_in_editor_bindings() -> KeyBindings: + """ + Pressing 'v' in navigation mode will open the buffer in an external editor. + """ + key_bindings = KeyBindings() + key_bindings.add("v", filter=vi_navigation_mode)( + get_by_name("edit-and-execute-command") + ) + return key_bindings diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/page_navigation.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/page_navigation.py index 4d531c04377..5a9e4d46685 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/page_navigation.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/page_navigation.py @@ -1,82 +1,82 @@ -""" -Key bindings for extra page navigation: bindings for up/down scrolling through -long pages, like in Emacs or Vi. -""" -from prompt_toolkit.filters import buffer_has_focus, emacs_mode, vi_mode -from prompt_toolkit.key_binding.key_bindings import ( - ConditionalKeyBindings, - KeyBindings, - KeyBindingsBase, - merge_key_bindings, -) - -from .scroll import ( - scroll_backward, - scroll_forward, - scroll_half_page_down, - scroll_half_page_up, - scroll_one_line_down, - scroll_one_line_up, - scroll_page_down, - scroll_page_up, -) - -__all__ = [ - "load_page_navigation_bindings", - "load_emacs_page_navigation_bindings", - "load_vi_page_navigation_bindings", -] - - -def load_page_navigation_bindings() -> KeyBindingsBase: - """ - Load both the Vi and Emacs bindings for page navigation. - """ - # Only enable when a `Buffer` is focused, otherwise, we would catch keys - # when another widget is focused (like for instance `c-d` in a - # ptterm.Terminal). - return ConditionalKeyBindings( - merge_key_bindings( - [ - load_emacs_page_navigation_bindings(), - load_vi_page_navigation_bindings(), - ] - ), - buffer_has_focus, - ) - - -def load_emacs_page_navigation_bindings() -> KeyBindingsBase: - """ - Key bindings, for scrolling up and down through pages. - This are separate bindings, because GNU readline doesn't have them. - """ - key_bindings = KeyBindings() - handle = key_bindings.add - - handle("c-v")(scroll_page_down) - handle("pagedown")(scroll_page_down) - handle("escape", "v")(scroll_page_up) - handle("pageup")(scroll_page_up) - - return ConditionalKeyBindings(key_bindings, emacs_mode) - - -def load_vi_page_navigation_bindings() -> KeyBindingsBase: - """ - Key bindings, for scrolling up and down through pages. - This are separate bindings, because GNU readline doesn't have them. - """ - key_bindings = KeyBindings() - handle = key_bindings.add - - handle("c-f")(scroll_forward) - handle("c-b")(scroll_backward) - handle("c-d")(scroll_half_page_down) - handle("c-u")(scroll_half_page_up) - handle("c-e")(scroll_one_line_down) - handle("c-y")(scroll_one_line_up) - handle("pagedown")(scroll_page_down) - handle("pageup")(scroll_page_up) - - return ConditionalKeyBindings(key_bindings, vi_mode) +""" +Key bindings for extra page navigation: bindings for up/down scrolling through +long pages, like in Emacs or Vi. +""" +from prompt_toolkit.filters import buffer_has_focus, emacs_mode, vi_mode +from prompt_toolkit.key_binding.key_bindings import ( + ConditionalKeyBindings, + KeyBindings, + KeyBindingsBase, + merge_key_bindings, +) + +from .scroll import ( + scroll_backward, + scroll_forward, + scroll_half_page_down, + scroll_half_page_up, + scroll_one_line_down, + scroll_one_line_up, + scroll_page_down, + scroll_page_up, +) + +__all__ = [ + "load_page_navigation_bindings", + "load_emacs_page_navigation_bindings", + "load_vi_page_navigation_bindings", +] + + +def load_page_navigation_bindings() -> KeyBindingsBase: + """ + Load both the Vi and Emacs bindings for page navigation. + """ + # Only enable when a `Buffer` is focused, otherwise, we would catch keys + # when another widget is focused (like for instance `c-d` in a + # ptterm.Terminal). + return ConditionalKeyBindings( + merge_key_bindings( + [ + load_emacs_page_navigation_bindings(), + load_vi_page_navigation_bindings(), + ] + ), + buffer_has_focus, + ) + + +def load_emacs_page_navigation_bindings() -> KeyBindingsBase: + """ + Key bindings, for scrolling up and down through pages. + This are separate bindings, because GNU readline doesn't have them. + """ + key_bindings = KeyBindings() + handle = key_bindings.add + + handle("c-v")(scroll_page_down) + handle("pagedown")(scroll_page_down) + handle("escape", "v")(scroll_page_up) + handle("pageup")(scroll_page_up) + + return ConditionalKeyBindings(key_bindings, emacs_mode) + + +def load_vi_page_navigation_bindings() -> KeyBindingsBase: + """ + Key bindings, for scrolling up and down through pages. + This are separate bindings, because GNU readline doesn't have them. + """ + key_bindings = KeyBindings() + handle = key_bindings.add + + handle("c-f")(scroll_forward) + handle("c-b")(scroll_backward) + handle("c-d")(scroll_half_page_down) + handle("c-u")(scroll_half_page_up) + handle("c-e")(scroll_one_line_down) + handle("c-y")(scroll_one_line_up) + handle("pagedown")(scroll_page_down) + handle("pageup")(scroll_page_up) + + return ConditionalKeyBindings(key_bindings, vi_mode) diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/scroll.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/scroll.py index 4a43ff585ac..648e3723042 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/scroll.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/scroll.py @@ -1,187 +1,187 @@ -""" -Key bindings, for scrolling up and down through pages. - -This are separate bindings, because GNU readline doesn't have them, but -they are very useful for navigating through long multiline buffers, like in -Vi, Emacs, etc... -""" -from prompt_toolkit.key_binding.key_processor import KeyPressEvent - -__all__ = [ - "scroll_forward", - "scroll_backward", - "scroll_half_page_up", - "scroll_half_page_down", - "scroll_one_line_up", - "scroll_one_line_down", -] - -E = KeyPressEvent - - -def scroll_forward(event: E, half: bool = False) -> None: - """ - Scroll window down. - """ - w = event.app.layout.current_window - b = event.app.current_buffer - - if w and w.render_info: - info = w.render_info - ui_content = info.ui_content - - # Height to scroll. - scroll_height = info.window_height - if half: - scroll_height //= 2 - - # Calculate how many lines is equivalent to that vertical space. - y = b.document.cursor_position_row + 1 - height = 0 - while y < ui_content.line_count: - line_height = info.get_height_for_line(y) - - if height + line_height < scroll_height: - height += line_height - y += 1 - else: - break - - b.cursor_position = b.document.translate_row_col_to_index(y, 0) - - -def scroll_backward(event: E, half: bool = False) -> None: - """ - Scroll window up. - """ - w = event.app.layout.current_window - b = event.app.current_buffer - - if w and w.render_info: - info = w.render_info - - # Height to scroll. - scroll_height = info.window_height - if half: - scroll_height //= 2 - - # Calculate how many lines is equivalent to that vertical space. - y = max(0, b.document.cursor_position_row - 1) - height = 0 - while y > 0: - line_height = info.get_height_for_line(y) - - if height + line_height < scroll_height: - height += line_height - y -= 1 - else: - break - - b.cursor_position = b.document.translate_row_col_to_index(y, 0) - - -def scroll_half_page_down(event: E) -> None: - """ - Same as ControlF, but only scroll half a page. - """ - scroll_forward(event, half=True) - - -def scroll_half_page_up(event: E) -> None: - """ - Same as ControlB, but only scroll half a page. - """ - scroll_backward(event, half=True) - - -def scroll_one_line_down(event: E) -> None: - """ - scroll_offset += 1 - """ - w = event.app.layout.current_window - b = event.app.current_buffer - - if w: - # When the cursor is at the top, move to the next line. (Otherwise, only scroll.) - if w.render_info: - info = w.render_info - - if w.vertical_scroll < info.content_height - info.window_height: - if info.cursor_position.y <= info.configured_scroll_offsets.top: - b.cursor_position += b.document.get_cursor_down_position() - - w.vertical_scroll += 1 - - -def scroll_one_line_up(event: E) -> None: - """ - scroll_offset -= 1 - """ - w = event.app.layout.current_window - b = event.app.current_buffer - - if w: - # When the cursor is at the bottom, move to the previous line. (Otherwise, only scroll.) - if w.render_info: - info = w.render_info - - if w.vertical_scroll > 0: - first_line_height = info.get_height_for_line(info.first_visible_line()) - - cursor_up = info.cursor_position.y - ( - info.window_height - - 1 - - first_line_height - - info.configured_scroll_offsets.bottom - ) - - # Move cursor up, as many steps as the height of the first line. - # TODO: not entirely correct yet, in case of line wrapping and many long lines. - for _ in range(max(0, cursor_up)): - b.cursor_position += b.document.get_cursor_up_position() - - # Scroll window - w.vertical_scroll -= 1 - - -def scroll_page_down(event: E) -> None: - """ - Scroll page down. (Prefer the cursor at the top of the page, after scrolling.) - """ - w = event.app.layout.current_window - b = event.app.current_buffer - - if w and w.render_info: - # Scroll down one page. - line_index = max(w.render_info.last_visible_line(), w.vertical_scroll + 1) - w.vertical_scroll = line_index - - b.cursor_position = b.document.translate_row_col_to_index(line_index, 0) - b.cursor_position += b.document.get_start_of_line_position( - after_whitespace=True - ) - - -def scroll_page_up(event: E) -> None: - """ - Scroll page up. (Prefer the cursor at the bottom of the page, after scrolling.) - """ - w = event.app.layout.current_window - b = event.app.current_buffer - - if w and w.render_info: - # Put cursor at the first visible line. (But make sure that the cursor - # moves at least one line up.) - line_index = max( - 0, - min(w.render_info.first_visible_line(), b.document.cursor_position_row - 1), - ) - - b.cursor_position = b.document.translate_row_col_to_index(line_index, 0) - b.cursor_position += b.document.get_start_of_line_position( - after_whitespace=True - ) - - # Set the scroll offset. We can safely set it to zero; the Window will - # make sure that it scrolls at least until the cursor becomes visible. - w.vertical_scroll = 0 +""" +Key bindings, for scrolling up and down through pages. + +This are separate bindings, because GNU readline doesn't have them, but +they are very useful for navigating through long multiline buffers, like in +Vi, Emacs, etc... +""" +from prompt_toolkit.key_binding.key_processor import KeyPressEvent + +__all__ = [ + "scroll_forward", + "scroll_backward", + "scroll_half_page_up", + "scroll_half_page_down", + "scroll_one_line_up", + "scroll_one_line_down", +] + +E = KeyPressEvent + + +def scroll_forward(event: E, half: bool = False) -> None: + """ + Scroll window down. + """ + w = event.app.layout.current_window + b = event.app.current_buffer + + if w and w.render_info: + info = w.render_info + ui_content = info.ui_content + + # Height to scroll. + scroll_height = info.window_height + if half: + scroll_height //= 2 + + # Calculate how many lines is equivalent to that vertical space. + y = b.document.cursor_position_row + 1 + height = 0 + while y < ui_content.line_count: + line_height = info.get_height_for_line(y) + + if height + line_height < scroll_height: + height += line_height + y += 1 + else: + break + + b.cursor_position = b.document.translate_row_col_to_index(y, 0) + + +def scroll_backward(event: E, half: bool = False) -> None: + """ + Scroll window up. + """ + w = event.app.layout.current_window + b = event.app.current_buffer + + if w and w.render_info: + info = w.render_info + + # Height to scroll. + scroll_height = info.window_height + if half: + scroll_height //= 2 + + # Calculate how many lines is equivalent to that vertical space. + y = max(0, b.document.cursor_position_row - 1) + height = 0 + while y > 0: + line_height = info.get_height_for_line(y) + + if height + line_height < scroll_height: + height += line_height + y -= 1 + else: + break + + b.cursor_position = b.document.translate_row_col_to_index(y, 0) + + +def scroll_half_page_down(event: E) -> None: + """ + Same as ControlF, but only scroll half a page. + """ + scroll_forward(event, half=True) + + +def scroll_half_page_up(event: E) -> None: + """ + Same as ControlB, but only scroll half a page. + """ + scroll_backward(event, half=True) + + +def scroll_one_line_down(event: E) -> None: + """ + scroll_offset += 1 + """ + w = event.app.layout.current_window + b = event.app.current_buffer + + if w: + # When the cursor is at the top, move to the next line. (Otherwise, only scroll.) + if w.render_info: + info = w.render_info + + if w.vertical_scroll < info.content_height - info.window_height: + if info.cursor_position.y <= info.configured_scroll_offsets.top: + b.cursor_position += b.document.get_cursor_down_position() + + w.vertical_scroll += 1 + + +def scroll_one_line_up(event: E) -> None: + """ + scroll_offset -= 1 + """ + w = event.app.layout.current_window + b = event.app.current_buffer + + if w: + # When the cursor is at the bottom, move to the previous line. (Otherwise, only scroll.) + if w.render_info: + info = w.render_info + + if w.vertical_scroll > 0: + first_line_height = info.get_height_for_line(info.first_visible_line()) + + cursor_up = info.cursor_position.y - ( + info.window_height + - 1 + - first_line_height + - info.configured_scroll_offsets.bottom + ) + + # Move cursor up, as many steps as the height of the first line. + # TODO: not entirely correct yet, in case of line wrapping and many long lines. + for _ in range(max(0, cursor_up)): + b.cursor_position += b.document.get_cursor_up_position() + + # Scroll window + w.vertical_scroll -= 1 + + +def scroll_page_down(event: E) -> None: + """ + Scroll page down. (Prefer the cursor at the top of the page, after scrolling.) + """ + w = event.app.layout.current_window + b = event.app.current_buffer + + if w and w.render_info: + # Scroll down one page. + line_index = max(w.render_info.last_visible_line(), w.vertical_scroll + 1) + w.vertical_scroll = line_index + + b.cursor_position = b.document.translate_row_col_to_index(line_index, 0) + b.cursor_position += b.document.get_start_of_line_position( + after_whitespace=True + ) + + +def scroll_page_up(event: E) -> None: + """ + Scroll page up. (Prefer the cursor at the bottom of the page, after scrolling.) + """ + w = event.app.layout.current_window + b = event.app.current_buffer + + if w and w.render_info: + # Put cursor at the first visible line. (But make sure that the cursor + # moves at least one line up.) + line_index = max( + 0, + min(w.render_info.first_visible_line(), b.document.cursor_position_row - 1), + ) + + b.cursor_position = b.document.translate_row_col_to_index(line_index, 0) + b.cursor_position += b.document.get_start_of_line_position( + after_whitespace=True + ) + + # Set the scroll offset. We can safely set it to zero; the Window will + # make sure that it scrolls at least until the cursor becomes visible. + w.vertical_scroll = 0 diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/search.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/search.py index 06a047e4cd1..6c37ae1298e 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/search.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/search.py @@ -1,93 +1,93 @@ -""" -Search related key bindings. -""" -from prompt_toolkit import search -from prompt_toolkit.application.current import get_app -from prompt_toolkit.filters import Condition, control_is_searchable, is_searching -from prompt_toolkit.key_binding.key_processor import KeyPressEvent - -from ..key_bindings import key_binding - -__all__ = [ - "abort_search", - "accept_search", - "start_reverse_incremental_search", - "start_forward_incremental_search", - "reverse_incremental_search", - "forward_incremental_search", - "accept_search_and_accept_input", -] - -E = KeyPressEvent - - -@key_binding(filter=is_searching) -def abort_search(event: E) -> None: - """ - Abort an incremental search and restore the original - line. - (Usually bound to ControlG/ControlC.) - """ - search.stop_search() - - -@key_binding(filter=is_searching) -def accept_search(event: E) -> None: - """ - When enter pressed in isearch, quit isearch mode. (Multiline - isearch would be too complicated.) - (Usually bound to Enter.) - """ - search.accept_search() - - -@key_binding(filter=control_is_searchable) -def start_reverse_incremental_search(event: E) -> None: - """ - Enter reverse incremental search. - (Usually ControlR.) - """ - search.start_search(direction=search.SearchDirection.BACKWARD) - - -@key_binding(filter=control_is_searchable) -def start_forward_incremental_search(event: E) -> None: - """ - Enter forward incremental search. - (Usually ControlS.) - """ - search.start_search(direction=search.SearchDirection.FORWARD) - - -@key_binding(filter=is_searching) -def reverse_incremental_search(event: E) -> None: - """ - Apply reverse incremental search, but keep search buffer focused. - """ - search.do_incremental_search(search.SearchDirection.BACKWARD, count=event.arg) - - -@key_binding(filter=is_searching) -def forward_incremental_search(event: E) -> None: - """ - Apply forward incremental search, but keep search buffer focused. - """ - search.do_incremental_search(search.SearchDirection.FORWARD, count=event.arg) - - -@Condition -def _previous_buffer_is_returnable() -> bool: - """ - True if the previously focused buffer has a return handler. - """ - prev_control = get_app().layout.search_target_buffer_control - return bool(prev_control and prev_control.buffer.is_returnable) - - -@key_binding(filter=is_searching & _previous_buffer_is_returnable) -def accept_search_and_accept_input(event: E) -> None: - """ - Accept the search operation first, then accept the input. - """ - search.accept_search() - event.current_buffer.validate_and_handle() +""" +Search related key bindings. +""" +from prompt_toolkit import search +from prompt_toolkit.application.current import get_app +from prompt_toolkit.filters import Condition, control_is_searchable, is_searching +from prompt_toolkit.key_binding.key_processor import KeyPressEvent + +from ..key_bindings import key_binding + +__all__ = [ + "abort_search", + "accept_search", + "start_reverse_incremental_search", + "start_forward_incremental_search", + "reverse_incremental_search", + "forward_incremental_search", + "accept_search_and_accept_input", +] + +E = KeyPressEvent + + +@key_binding(filter=is_searching) +def abort_search(event: E) -> None: + """ + Abort an incremental search and restore the original + line. + (Usually bound to ControlG/ControlC.) + """ + search.stop_search() + + +@key_binding(filter=is_searching) +def accept_search(event: E) -> None: + """ + When enter pressed in isearch, quit isearch mode. (Multiline + isearch would be too complicated.) + (Usually bound to Enter.) + """ + search.accept_search() + + +@key_binding(filter=control_is_searchable) +def start_reverse_incremental_search(event: E) -> None: + """ + Enter reverse incremental search. + (Usually ControlR.) + """ + search.start_search(direction=search.SearchDirection.BACKWARD) + + +@key_binding(filter=control_is_searchable) +def start_forward_incremental_search(event: E) -> None: + """ + Enter forward incremental search. + (Usually ControlS.) + """ + search.start_search(direction=search.SearchDirection.FORWARD) + + +@key_binding(filter=is_searching) +def reverse_incremental_search(event: E) -> None: + """ + Apply reverse incremental search, but keep search buffer focused. + """ + search.do_incremental_search(search.SearchDirection.BACKWARD, count=event.arg) + + +@key_binding(filter=is_searching) +def forward_incremental_search(event: E) -> None: + """ + Apply forward incremental search, but keep search buffer focused. + """ + search.do_incremental_search(search.SearchDirection.FORWARD, count=event.arg) + + +@Condition +def _previous_buffer_is_returnable() -> bool: + """ + True if the previously focused buffer has a return handler. + """ + prev_control = get_app().layout.search_target_buffer_control + return bool(prev_control and prev_control.buffer.is_returnable) + + +@key_binding(filter=is_searching & _previous_buffer_is_returnable) +def accept_search_and_accept_input(event: E) -> None: + """ + Accept the search operation first, then accept the input. + """ + search.accept_search() + event.current_buffer.validate_and_handle() diff --git a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/vi.py b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/vi.py index efbb107de04..f202e2a7f9e 100644 --- a/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/vi.py +++ b/contrib/python/prompt-toolkit/py3/prompt_toolkit/key_binding/bindings/vi.py @@ -1,2221 +1,2221 @@ -# pylint: disable=function-redefined -import codecs -import string -from enum import Enum -from itertools import accumulate +# pylint: disable=function-redefined +import codecs +import string +from enum import Enum +from itertools import accumulate from typing import Callable, Iterable, List, Optional, Tuple, TypeVar, Union - -from prompt_toolkit.application.current import get_app -from prompt_toolkit.buffer import Buffer, indent, reshape_text, unindent -from prompt_toolkit.clipboard import ClipboardData -from prompt_toolkit.document import Document -from prompt_toolkit.filters import ( - Always, - Condition, - Filter, - has_arg, - is_read_only, - is_searching, -) -from prompt_toolkit.filters.app import ( - in_paste_mode, - is_multiline, - vi_digraph_mode, - vi_insert_mode, - vi_insert_multiple_mode, - vi_mode, - vi_navigation_mode, - vi_recording_macro, - vi_replace_mode, - vi_replace_single_mode, - vi_search_direction_reversed, - vi_selection_mode, - vi_waiting_for_text_object_mode, -) -from prompt_toolkit.input.vt100_parser import Vt100Parser -from prompt_toolkit.key_binding.digraphs import DIGRAPHS -from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent -from prompt_toolkit.key_binding.vi_state import CharacterFind, InputMode -from prompt_toolkit.keys import Keys -from prompt_toolkit.search import SearchDirection -from prompt_toolkit.selection import PasteMode, SelectionState, SelectionType - -from ..key_bindings import ConditionalKeyBindings, KeyBindings, KeyBindingsBase -from .named_commands import get_by_name - -__all__ = [ - "load_vi_bindings", - "load_vi_search_bindings", -] - -E = KeyPressEvent - -ascii_lowercase = string.ascii_lowercase - -vi_register_names = ascii_lowercase + "0123456789" - - -class TextObjectType(Enum): - EXCLUSIVE = "EXCLUSIVE" - INCLUSIVE = "INCLUSIVE" - LINEWISE = "LINEWISE" - BLOCK = "BLOCK" - - -class TextObject: - """ - Return struct for functions wrapped in ``text_object``. - Both `start` and `end` are relative to the current cursor position. - """ - - def __init__( - self, start: int, end: int = 0, type: TextObjectType = TextObjectType.EXCLUSIVE - ): - - self.start = start - self.end = end - self.type = type - - @property - def selection_type(self) -> SelectionType: - if self.type == TextObjectType.LINEWISE: - return SelectionType.LINES - if self.type == TextObjectType.BLOCK: - return SelectionType.BLOCK - else: - return SelectionType.CHARACTERS - - def sorted(self) -> Tuple[int, int]: - """ - Return a (start, end) tuple where start <= end. - """ - if self.start < self.end: - return self.start, self.end - else: - return self.end, self.start - - def operator_range(self, document: Document) -> Tuple[int, int]: - """ - Return a (start, end) tuple with start <= end that indicates the range - operators should operate on. - `buffer` is used to get start and end of line positions. - - This should return something that can be used in a slice, so the `end` - position is *not* included. - """ - start, end = self.sorted() - doc = document - - if ( - self.type == TextObjectType.EXCLUSIVE - and doc.translate_index_to_position(end + doc.cursor_position)[1] == 0 - ): - # If the motion is exclusive and the end of motion is on the first - # column, the end position becomes end of previous line. - end -= 1 - if self.type == TextObjectType.INCLUSIVE: - end += 1 - if self.type == TextObjectType.LINEWISE: - # Select whole lines - row, col = doc.translate_index_to_position(start + doc.cursor_position) - start = doc.translate_row_col_to_index(row, 0) - doc.cursor_position - row, col = doc.translate_index_to_position(end + doc.cursor_position) - end = ( - doc.translate_row_col_to_index(row, len(doc.lines[row])) - - doc.cursor_position - ) - return start, end - - def get_line_numbers(self, buffer: Buffer) -> Tuple[int, int]: - """ - Return a (start_line, end_line) pair. - """ - # Get absolute cursor positions from the text object. - from_, to = self.operator_range(buffer.document) - from_ += buffer.cursor_position - to += buffer.cursor_position - - # Take the start of the lines. - from_, _ = buffer.document.translate_index_to_position(from_) - to, _ = buffer.document.translate_index_to_position(to) - - return from_, to - - def cut(self, buffer: Buffer) -> Tuple[Document, ClipboardData]: - """ - Turn text object into `ClipboardData` instance. - """ - from_, to = self.operator_range(buffer.document) - - from_ += buffer.cursor_position - to += buffer.cursor_position - - # For Vi mode, the SelectionState does include the upper position, - # while `self.operator_range` does not. So, go one to the left, unless - # we're in the line mode, then we don't want to risk going to the - # previous line, and missing one line in the selection. - if self.type != TextObjectType.LINEWISE: - to -= 1 - - document = Document( - buffer.text, - to, - SelectionState(original_cursor_position=from_, type=self.selection_type), - ) - - new_document, clipboard_data = document.cut_selection() - return new_document, clipboard_data - - -# Typevar for any text object function: -TextObjectFunction = Callable[[E], TextObject] -_TOF = TypeVar("_TOF", bound=TextObjectFunction) - - -def create_text_object_decorator( - key_bindings: KeyBindings, -) -> Callable[..., Callable[[_TOF], _TOF]]: - """ - Create a decorator that can be used to register Vi text object implementations. - """ - - def text_object_decorator( - *keys: Union[Keys, str], - filter: Filter = Always(), - no_move_handler: bool = False, - no_selection_handler: bool = False, - eager: bool = False, - ) -> Callable[[_TOF], _TOF]: - """ - Register a text object function. - - Usage:: - - @text_object('w', filter=..., no_move_handler=False) - def handler(event): - # Return a text object for this key. - return TextObject(...) - - :param no_move_handler: Disable the move handler in navigation mode. - (It's still active in selection mode.) - """ - - def decorator(text_object_func: _TOF) -> _TOF: - @key_bindings.add( - *keys, filter=vi_waiting_for_text_object_mode & filter, eager=eager - ) - def _apply_operator_to_text_object(event: E) -> None: - # Arguments are multiplied. - vi_state = event.app.vi_state - event._arg = str((vi_state.operator_arg or 1) * (event.arg or 1)) - - # Call the text object handler. - text_obj = text_object_func(event) - - # Get the operator function. - # (Should never be None here, given the - # `vi_waiting_for_text_object_mode` filter state.) - operator_func = vi_state.operator_func - - if text_obj is not None and operator_func is not None: - # Call the operator function with the text object. - operator_func(event, text_obj) - - # Clear operator. - event.app.vi_state.operator_func = None - event.app.vi_state.operator_arg = None - - # Register a move operation. (Doesn't need an operator.) - if not no_move_handler: - - @key_bindings.add( - *keys, - filter=~vi_waiting_for_text_object_mode - & filter - & vi_navigation_mode, - eager=eager, - ) - def _move_in_navigation_mode(event: E) -> None: - """ - Move handler for navigation mode. - """ - text_object = text_object_func(event) - event.current_buffer.cursor_position += text_object.start - - # Register a move selection operation. - if not no_selection_handler: - - @key_bindings.add( - *keys, - filter=~vi_waiting_for_text_object_mode - & filter - & vi_selection_mode, - eager=eager, - ) - def _move_in_selection_mode(event: E) -> None: - """ - Move handler for selection mode. - """ - text_object = text_object_func(event) - buff = event.current_buffer - selection_state = buff.selection_state - - if selection_state is None: - return # Should not happen, because of the `vi_selection_mode` filter. - - # When the text object has both a start and end position, like 'i(' or 'iw', - # Turn this into a selection, otherwise the cursor. - if text_object.end: - # Take selection positions from text object. - start, end = text_object.operator_range(buff.document) - start += buff.cursor_position - end += buff.cursor_position - - selection_state.original_cursor_position = start - buff.cursor_position = end - - # Take selection type from text object. - if text_object.type == TextObjectType.LINEWISE: - selection_state.type = SelectionType.LINES - else: - selection_state.type = SelectionType.CHARACTERS - else: - event.current_buffer.cursor_position += text_object.start - - # Make it possible to chain @text_object decorators. - return text_object_func - - return decorator - - return text_object_decorator - - -# Typevar for any operator function: -OperatorFunction = Callable[[E, TextObject], None] -_OF = TypeVar("_OF", bound=OperatorFunction) - - -def create_operator_decorator( - key_bindings: KeyBindings, -) -> Callable[..., Callable[[_OF], _OF]]: - """ - Create a decorator that can be used for registering Vi operators. - """ - - def operator_decorator( - *keys: Union[Keys, str], filter: Filter = Always(), eager: bool = False - ) -> Callable[[_OF], _OF]: - """ - Register a Vi operator. - - Usage:: - - @operator('d', filter=...) - def handler(event, text_object): - # Do something with the text object here. - """ - - def decorator(operator_func: _OF) -> _OF: - @key_bindings.add( - *keys, - filter=~vi_waiting_for_text_object_mode & filter & vi_navigation_mode, - eager=eager, - ) - def _operator_in_navigation(event: E) -> None: - """ - Handle operator in navigation mode. - """ - # When this key binding is matched, only set the operator - # function in the ViState. We should execute it after a text - # object has been received. - event.app.vi_state.operator_func = operator_func - event.app.vi_state.operator_arg = event.arg - - @key_bindings.add( - *keys, - filter=~vi_waiting_for_text_object_mode & filter & vi_selection_mode, - eager=eager, - ) - def _operator_in_selection(event: E) -> None: - """ - Handle operator in selection mode. - """ - buff = event.current_buffer - selection_state = buff.selection_state - - if selection_state is not None: - # Create text object from selection. - if selection_state.type == SelectionType.LINES: - text_obj_type = TextObjectType.LINEWISE - elif selection_state.type == SelectionType.BLOCK: - text_obj_type = TextObjectType.BLOCK - else: - text_obj_type = TextObjectType.INCLUSIVE - - text_object = TextObject( - selection_state.original_cursor_position - buff.cursor_position, - type=text_obj_type, - ) - - # Execute operator. - operator_func(event, text_object) - - # Quit selection mode. - buff.selection_state = None - - return operator_func - - return decorator - - return operator_decorator - - -def load_vi_bindings() -> KeyBindingsBase: - """ - Vi extensions. - - # Overview of Readline Vi commands: - # http://www.catonmat.net/download/bash-vi-editing-mode-cheat-sheet.pdf - """ - # Note: Some key bindings have the "~IsReadOnly()" filter added. This - # prevents the handler to be executed when the focus is on a - # read-only buffer. - # This is however only required for those that change the ViState to - # INSERT mode. The `Buffer` class itself throws the - # `EditReadOnlyBuffer` exception for any text operations which is - # handled correctly. There is no need to add "~IsReadOnly" to all key - # bindings that do text manipulation. - - key_bindings = KeyBindings() - handle = key_bindings.add - - # (Note: Always take the navigation bindings in read-only mode, even when - # ViState says different.) - - TransformFunction = Tuple[Tuple[str, ...], Filter, Callable[[str], str]] - - vi_transform_functions: List[TransformFunction] = [ - # Rot 13 transformation - ( - ("g", "?"), - Always(), - lambda string: codecs.encode(string, "rot_13"), - ), - # To lowercase - (("g", "u"), Always(), lambda string: string.lower()), - # To uppercase. - (("g", "U"), Always(), lambda string: string.upper()), - # Swap case. - (("g", "~"), Always(), lambda string: string.swapcase()), - ( - ("~",), - Condition(lambda: get_app().vi_state.tilde_operator), - lambda string: string.swapcase(), - ), - ] - - # Insert a character literally (quoted insert). - handle("c-v", filter=vi_insert_mode)(get_by_name("quoted-insert")) - - @handle("escape") - def _back_to_navigation(event: E) -> None: - """ - Escape goes to vi navigation mode. - """ - buffer = event.current_buffer - vi_state = event.app.vi_state - - if vi_state.input_mode in (InputMode.INSERT, InputMode.REPLACE): - buffer.cursor_position += buffer.document.get_cursor_left_position() - - vi_state.input_mode = InputMode.NAVIGATION - - if bool(buffer.selection_state): - buffer.exit_selection() - - @handle("k", filter=vi_selection_mode) - def _up_in_selection(event: E) -> None: - """ - Arrow up in selection mode. - """ - event.current_buffer.cursor_up(count=event.arg) - - @handle("j", filter=vi_selection_mode) - def _down_in_selection(event: E) -> None: - """ - Arrow down in selection mode. - """ - event.current_buffer.cursor_down(count=event.arg) - - @handle("up", filter=vi_navigation_mode) - @handle("c-p", filter=vi_navigation_mode) - def _up_in_navigation(event: E) -> None: - """ - Arrow up and ControlP in navigation mode go up. - """ - event.current_buffer.auto_up(count=event.arg) - - @handle("k", filter=vi_navigation_mode) - def _go_up(event: E) -> None: - """ - Go up, but if we enter a new history entry, move to the start of the - line. - """ - event.current_buffer.auto_up( - count=event.arg, go_to_start_of_line_if_history_changes=True - ) - - @handle("down", filter=vi_navigation_mode) - @handle("c-n", filter=vi_navigation_mode) - def _go_down(event: E) -> None: - """ - Arrow down and Control-N in navigation mode. - """ - event.current_buffer.auto_down(count=event.arg) - - @handle("j", filter=vi_navigation_mode) - def _go_down2(event: E) -> None: - """ - Go down, but if we enter a new history entry, go to the start of the line. - """ - event.current_buffer.auto_down( - count=event.arg, go_to_start_of_line_if_history_changes=True - ) - - @handle("backspace", filter=vi_navigation_mode) - def _go_left(event: E) -> None: - """ - In navigation-mode, move cursor. - """ - event.current_buffer.cursor_position += ( - event.current_buffer.document.get_cursor_left_position(count=event.arg) - ) - - @handle("c-n", filter=vi_insert_mode) - def _complete_next(event: E) -> None: - b = event.current_buffer - - if b.complete_state: - b.complete_next() - else: - b.start_completion(select_first=True) - - @handle("c-p", filter=vi_insert_mode) - def _complete_prev(event: E) -> None: - """ - Control-P: To previous completion. - """ - b = event.current_buffer - - if b.complete_state: - b.complete_previous() - else: - b.start_completion(select_last=True) - - @handle("c-g", filter=vi_insert_mode) - @handle("c-y", filter=vi_insert_mode) - def _accept_completion(event: E) -> None: - """ - Accept current completion. - """ - event.current_buffer.complete_state = None - - @handle("c-e", filter=vi_insert_mode) - def _cancel_completion(event: E) -> None: - """ - Cancel completion. Go back to originally typed text. - """ - event.current_buffer.cancel_completion() - - @Condition - def is_returnable() -> bool: - return get_app().current_buffer.is_returnable - - # In navigation mode, pressing enter will always return the input. - handle("enter", filter=vi_navigation_mode & is_returnable)( - get_by_name("accept-line") - ) - - # In insert mode, also accept input when enter is pressed, and the buffer - # has been marked as single line. - handle("enter", filter=is_returnable & ~is_multiline)(get_by_name("accept-line")) - - @handle("enter", filter=~is_returnable & vi_navigation_mode) - def _start_of_next_line(event: E) -> None: - """ - Go to the beginning of next line. - """ - b = event.current_buffer - b.cursor_down(count=event.arg) - b.cursor_position += b.document.get_start_of_line_position( - after_whitespace=True - ) - - # ** In navigation mode ** - - # List of navigation commands: http://hea-www.harvard.edu/~fine/Tech/vi.html - - @handle("insert", filter=vi_navigation_mode) - def _insert_mode(event: E) -> None: - """ - Pressing the Insert key. - """ - event.app.vi_state.input_mode = InputMode.INSERT - - @handle("insert", filter=vi_insert_mode) - def _navigation_mode(event: E) -> None: - """ - Pressing the Insert key. - """ - event.app.vi_state.input_mode = InputMode.NAVIGATION - - @handle("a", filter=vi_navigation_mode & ~is_read_only) - # ~IsReadOnly, because we want to stay in navigation mode for - # read-only buffers. - def _a(event: E) -> None: - event.current_buffer.cursor_position += ( - event.current_buffer.document.get_cursor_right_position() - ) - event.app.vi_state.input_mode = InputMode.INSERT - - @handle("A", filter=vi_navigation_mode & ~is_read_only) - def _A(event: E) -> None: - event.current_buffer.cursor_position += ( - event.current_buffer.document.get_end_of_line_position() - ) - event.app.vi_state.input_mode = InputMode.INSERT - - @handle("C", filter=vi_navigation_mode & ~is_read_only) - def _change_until_end_of_line(event: E) -> None: - """ - Change to end of line. - Same as 'c$' (which is implemented elsewhere.) - """ - buffer = event.current_buffer - - deleted = buffer.delete(count=buffer.document.get_end_of_line_position()) - event.app.clipboard.set_text(deleted) - event.app.vi_state.input_mode = InputMode.INSERT - - @handle("c", "c", filter=vi_navigation_mode & ~is_read_only) - @handle("S", filter=vi_navigation_mode & ~is_read_only) - def _change_current_line(event: E) -> None: # TODO: implement 'arg' - """ - Change current line - """ - buffer = event.current_buffer - - # We copy the whole line. - data = ClipboardData(buffer.document.current_line, SelectionType.LINES) - event.app.clipboard.set_data(data) - - # But we delete after the whitespace - buffer.cursor_position += buffer.document.get_start_of_line_position( - after_whitespace=True - ) - buffer.delete(count=buffer.document.get_end_of_line_position()) - event.app.vi_state.input_mode = InputMode.INSERT - - @handle("D", filter=vi_navigation_mode) - def _delete_until_end_of_line(event: E) -> None: - """ - Delete from cursor position until the end of the line. - """ - buffer = event.current_buffer - deleted = buffer.delete(count=buffer.document.get_end_of_line_position()) - event.app.clipboard.set_text(deleted) - - @handle("d", "d", filter=vi_navigation_mode) - def _delete_line(event: E) -> None: - """ - Delete line. (Or the following 'n' lines.) - """ - buffer = event.current_buffer - - # Split string in before/deleted/after text. - lines = buffer.document.lines - - before = "\n".join(lines[: buffer.document.cursor_position_row]) - deleted = "\n".join( - lines[ - buffer.document.cursor_position_row : buffer.document.cursor_position_row - + event.arg - ] - ) - after = "\n".join(lines[buffer.document.cursor_position_row + event.arg :]) - - # Set new text. - if before and after: - before = before + "\n" - - # Set text and cursor position. - buffer.document = Document( - text=before + after, - # Cursor At the start of the first 'after' line, after the leading whitespace. - cursor_position=len(before) + len(after) - len(after.lstrip(" ")), - ) - - # Set clipboard data - event.app.clipboard.set_data(ClipboardData(deleted, SelectionType.LINES)) - - @handle("x", filter=vi_selection_mode) - def _cut(event: E) -> None: - """ - Cut selection. - ('x' is not an operator.) - """ - clipboard_data = event.current_buffer.cut_selection() - event.app.clipboard.set_data(clipboard_data) - - @handle("i", filter=vi_navigation_mode & ~is_read_only) - def _i(event: E) -> None: - event.app.vi_state.input_mode = InputMode.INSERT - - @handle("I", filter=vi_navigation_mode & ~is_read_only) - def _I(event: E) -> None: - event.app.vi_state.input_mode = InputMode.INSERT - event.current_buffer.cursor_position += ( - event.current_buffer.document.get_start_of_line_position( - after_whitespace=True - ) - ) - - @Condition - def in_block_selection() -> bool: - buff = get_app().current_buffer - return bool( - buff.selection_state and buff.selection_state.type == SelectionType.BLOCK - ) - - @handle("I", filter=in_block_selection & ~is_read_only) - def insert_in_block_selection(event: E, after: bool = False) -> None: - """ - Insert in block selection mode. - """ - buff = event.current_buffer - - # Store all cursor positions. - positions = [] - - if after: - - def get_pos(from_to: Tuple[int, int]) -> int: - return from_to[1] - - else: - - def get_pos(from_to: Tuple[int, int]) -> int: - return from_to[0] - - for i, from_to in enumerate(buff.document.selection_ranges()): - positions.append(get_pos(from_to)) - if i == 0: - buff.cursor_position = get_pos(from_to) - - buff.multiple_cursor_positions = positions - - # Go to 'INSERT_MULTIPLE' mode. - event.app.vi_state.input_mode = InputMode.INSERT_MULTIPLE - buff.exit_selection() - - @handle("A", filter=in_block_selection & ~is_read_only) - def _append_after_block(event: E) -> None: - insert_in_block_selection(event, after=True) - - @handle("J", filter=vi_navigation_mode & ~is_read_only) - def _join(event: E) -> None: - """ - Join lines. - """ - for i in range(event.arg): - event.current_buffer.join_next_line() - - @handle("g", "J", filter=vi_navigation_mode & ~is_read_only) - def _join_nospace(event: E) -> None: - """ - Join lines without space. - """ - for i in range(event.arg): - event.current_buffer.join_next_line(separator="") - - @handle("J", filter=vi_selection_mode & ~is_read_only) - def _join_selection(event: E) -> None: - """ - Join selected lines. - """ - event.current_buffer.join_selected_lines() - - @handle("g", "J", filter=vi_selection_mode & ~is_read_only) - def _join_selection_nospace(event: E) -> None: - """ - Join selected lines without space. - """ - event.current_buffer.join_selected_lines(separator="") - - @handle("p", filter=vi_navigation_mode) - def _paste(event: E) -> None: - """ - Paste after - """ - event.current_buffer.paste_clipboard_data( - event.app.clipboard.get_data(), - count=event.arg, - paste_mode=PasteMode.VI_AFTER, - ) - - @handle("P", filter=vi_navigation_mode) - def _paste_before(event: E) -> None: - """ - Paste before - """ - event.current_buffer.paste_clipboard_data( - event.app.clipboard.get_data(), - count=event.arg, - paste_mode=PasteMode.VI_BEFORE, - ) - - @handle('"', Keys.Any, "p", filter=vi_navigation_mode) - def _paste_register(event: E) -> None: - """ - Paste from named register. - """ - c = event.key_sequence[1].data - if c in vi_register_names: - data = event.app.vi_state.named_registers.get(c) - if data: - event.current_buffer.paste_clipboard_data( - data, count=event.arg, paste_mode=PasteMode.VI_AFTER - ) - - @handle('"', Keys.Any, "P", filter=vi_navigation_mode) - def _paste_register_before(event: E) -> None: - """ - Paste (before) from named register. - """ - c = event.key_sequence[1].data - if c in vi_register_names: - data = event.app.vi_state.named_registers.get(c) - if data: - event.current_buffer.paste_clipboard_data( - data, count=event.arg, paste_mode=PasteMode.VI_BEFORE - ) - - @handle("r", filter=vi_navigation_mode) - def _replace(event: E) -> None: - """ - Go to 'replace-single'-mode. - """ - event.app.vi_state.input_mode = InputMode.REPLACE_SINGLE - - @handle("R", filter=vi_navigation_mode) - def _replace_mode(event: E) -> None: - """ - Go to 'replace'-mode. - """ - event.app.vi_state.input_mode = InputMode.REPLACE - - @handle("s", filter=vi_navigation_mode & ~is_read_only) - def _substitute(event: E) -> None: - """ - Substitute with new text - (Delete character(s) and go to insert mode.) - """ - text = event.current_buffer.delete(count=event.arg) - event.app.clipboard.set_text(text) - event.app.vi_state.input_mode = InputMode.INSERT - - @handle("u", filter=vi_navigation_mode, save_before=(lambda e: False)) - def _undo(event: E) -> None: - for i in range(event.arg): - event.current_buffer.undo() - - @handle("V", filter=vi_navigation_mode) - def _visual_line(event: E) -> None: - """ - Start lines selection. - """ - event.current_buffer.start_selection(selection_type=SelectionType.LINES) - - @handle("c-v", filter=vi_navigation_mode) - def _visual_block(event: E) -> None: - """ - Enter block selection mode. - """ - event.current_buffer.start_selection(selection_type=SelectionType.BLOCK) - - @handle("V", filter=vi_selection_mode) - def _visual_line2(event: E) -> None: - """ - Exit line selection mode, or go from non line selection mode to line - selection mode. - """ - selection_state = event.current_buffer.selection_state - - if selection_state is not None: - if selection_state.type != SelectionType.LINES: - selection_state.type = SelectionType.LINES - else: - event.current_buffer.exit_selection() - - @handle("v", filter=vi_navigation_mode) - def _visual(event: E) -> None: - """ - Enter character selection mode. - """ - event.current_buffer.start_selection(selection_type=SelectionType.CHARACTERS) - - @handle("v", filter=vi_selection_mode) - def _visual2(event: E) -> None: - """ - Exit character selection mode, or go from non-character-selection mode - to character selection mode. - """ - selection_state = event.current_buffer.selection_state - - if selection_state is not None: - if selection_state.type != SelectionType.CHARACTERS: - selection_state.type = SelectionType.CHARACTERS - else: - event.current_buffer.exit_selection() - - @handle("c-v", filter=vi_selection_mode) - def _visual_block2(event: E) -> None: - """ - Exit block selection mode, or go from non block selection mode to block - selection mode. - """ - selection_state = event.current_buffer.selection_state - - if selection_state is not None: - if selection_state.type != SelectionType.BLOCK: - selection_state.type = SelectionType.BLOCK - else: - event.current_buffer.exit_selection() - - @handle("a", "w", filter=vi_selection_mode) - @handle("a", "W", filter=vi_selection_mode) - def _visual_auto_word(event: E) -> None: - """ - Switch from visual linewise mode to visual characterwise mode. - """ - buffer = event.current_buffer - - if ( - buffer.selection_state - and buffer.selection_state.type == SelectionType.LINES - ): - buffer.selection_state.type = SelectionType.CHARACTERS - - @handle("x", filter=vi_navigation_mode) - def _delete(event: E) -> None: - """ - Delete character. - """ - buff = event.current_buffer - count = min(event.arg, len(buff.document.current_line_after_cursor)) - if count: - text = event.current_buffer.delete(count=count) - event.app.clipboard.set_text(text) - - @handle("X", filter=vi_navigation_mode) - def _delete_before_cursor(event: E) -> None: - buff = event.current_buffer - count = min(event.arg, len(buff.document.current_line_before_cursor)) - if count: - text = event.current_buffer.delete_before_cursor(count=count) - event.app.clipboard.set_text(text) - - @handle("y", "y", filter=vi_navigation_mode) - @handle("Y", filter=vi_navigation_mode) - def _yank_line(event: E) -> None: - """ - Yank the whole line. - """ - text = "\n".join(event.current_buffer.document.lines_from_current[: event.arg]) - event.app.clipboard.set_data(ClipboardData(text, SelectionType.LINES)) - - @handle("+", filter=vi_navigation_mode) - def _next_line(event: E) -> None: - """ - Move to first non whitespace of next line - """ - buffer = event.current_buffer - buffer.cursor_position += buffer.document.get_cursor_down_position( - count=event.arg - ) - buffer.cursor_position += buffer.document.get_start_of_line_position( - after_whitespace=True - ) - - @handle("-", filter=vi_navigation_mode) - def _prev_line(event: E) -> None: - """ - Move to first non whitespace of previous line - """ - buffer = event.current_buffer - buffer.cursor_position += buffer.document.get_cursor_up_position( - count=event.arg - ) - buffer.cursor_position += buffer.document.get_start_of_line_position( - after_whitespace=True - ) - - @handle(">", ">", filter=vi_navigation_mode) - def _indent(event: E) -> None: - """ - Indent lines. - """ - buffer = event.current_buffer - current_row = buffer.document.cursor_position_row - indent(buffer, current_row, current_row + event.arg) - - @handle("<", "<", filter=vi_navigation_mode) - def _unindent(event: E) -> None: - """ - Unindent lines. - """ - current_row = event.current_buffer.document.cursor_position_row - unindent(event.current_buffer, current_row, current_row + event.arg) - - @handle("O", filter=vi_navigation_mode & ~is_read_only) - def _open_above(event: E) -> None: - """ - Open line above and enter insertion mode - """ - event.current_buffer.insert_line_above(copy_margin=not in_paste_mode()) - event.app.vi_state.input_mode = InputMode.INSERT - - @handle("o", filter=vi_navigation_mode & ~is_read_only) - def _open_below(event: E) -> None: - """ - Open line below and enter insertion mode - """ - event.current_buffer.insert_line_below(copy_margin=not in_paste_mode()) - event.app.vi_state.input_mode = InputMode.INSERT - - @handle("~", filter=vi_navigation_mode) - def _reverse_case(event: E) -> None: - """ - Reverse case of current character and move cursor forward. - """ - buffer = event.current_buffer - c = buffer.document.current_char - - if c is not None and c != "\n": - buffer.insert_text(c.swapcase(), overwrite=True) - - @handle("g", "u", "u", filter=vi_navigation_mode & ~is_read_only) - def _lowercase_line(event: E) -> None: - """ - Lowercase current line. - """ - buff = event.current_buffer - buff.transform_current_line(lambda s: s.lower()) - - @handle("g", "U", "U", filter=vi_navigation_mode & ~is_read_only) - def _uppercase_line(event: E) -> None: - """ - Uppercase current line. - """ - buff = event.current_buffer - buff.transform_current_line(lambda s: s.upper()) - - @handle("g", "~", "~", filter=vi_navigation_mode & ~is_read_only) - def _swapcase_line(event: E) -> None: - """ - Swap case of the current line. - """ - buff = event.current_buffer - buff.transform_current_line(lambda s: s.swapcase()) - - @handle("#", filter=vi_navigation_mode) - def _prev_occurence(event: E) -> None: - """ - Go to previous occurrence of this word. - """ - b = event.current_buffer - search_state = event.app.current_search_state - - search_state.text = b.document.get_word_under_cursor() - search_state.direction = SearchDirection.BACKWARD - - b.apply_search(search_state, count=event.arg, include_current_position=False) - - @handle("*", filter=vi_navigation_mode) - def _next_occurance(event: E) -> None: - """ - Go to next occurrence of this word. - """ - b = event.current_buffer - search_state = event.app.current_search_state - - search_state.text = b.document.get_word_under_cursor() - search_state.direction = SearchDirection.FORWARD - - b.apply_search(search_state, count=event.arg, include_current_position=False) - - @handle("(", filter=vi_navigation_mode) - def _begin_of_sentence(event: E) -> None: - # TODO: go to begin of sentence. - # XXX: should become text_object. - pass - - @handle(")", filter=vi_navigation_mode) - def _end_of_sentence(event: E) -> None: - # TODO: go to end of sentence. - # XXX: should become text_object. - pass - - operator = create_operator_decorator(key_bindings) - text_object = create_text_object_decorator(key_bindings) - - @handle(Keys.Any, filter=vi_waiting_for_text_object_mode) - def _unknown_text_object(event: E) -> None: - """ - Unknown key binding while waiting for a text object. - """ - event.app.output.bell() - - # - # *** Operators *** - # - - def create_delete_and_change_operators( - delete_only: bool, with_register: bool = False - ) -> None: - """ - Delete and change operators. - - :param delete_only: Create an operator that deletes, but doesn't go to insert mode. - :param with_register: Copy the deleted text to this named register instead of the clipboard. - """ - handler_keys: Iterable[str] - if with_register: - handler_keys = ('"', Keys.Any, "cd"[delete_only]) - else: - handler_keys = "cd"[delete_only] - - @operator(*handler_keys, filter=~is_read_only) - def delete_or_change_operator(event: E, text_object: TextObject) -> None: - clipboard_data = None - buff = event.current_buffer - - if text_object: - new_document, clipboard_data = text_object.cut(buff) - buff.document = new_document - - # Set deleted/changed text to clipboard or named register. - if clipboard_data and clipboard_data.text: - if with_register: - reg_name = event.key_sequence[1].data - if reg_name in vi_register_names: - event.app.vi_state.named_registers[reg_name] = clipboard_data - else: - event.app.clipboard.set_data(clipboard_data) - - # Only go back to insert mode in case of 'change'. - if not delete_only: - event.app.vi_state.input_mode = InputMode.INSERT - - create_delete_and_change_operators(False, False) - create_delete_and_change_operators(False, True) - create_delete_and_change_operators(True, False) - create_delete_and_change_operators(True, True) - - def create_transform_handler( - filter: Filter, transform_func: Callable[[str], str], *a: str - ) -> None: - @operator(*a, filter=filter & ~is_read_only) - def _(event: E, text_object: TextObject) -> None: - """ - Apply transformation (uppercase, lowercase, rot13, swap case). - """ - buff = event.current_buffer - start, end = text_object.operator_range(buff.document) - - if start < end: - # Transform. - buff.transform_region( - buff.cursor_position + start, - buff.cursor_position + end, - transform_func, - ) - - # Move cursor - buff.cursor_position += text_object.end or text_object.start - - for k, f, func in vi_transform_functions: - create_transform_handler(f, func, *k) - - @operator("y") - def _yank(event: E, text_object: TextObject) -> None: - """ - Yank operator. (Copy text.) - """ - _, clipboard_data = text_object.cut(event.current_buffer) - if clipboard_data.text: - event.app.clipboard.set_data(clipboard_data) - - @operator('"', Keys.Any, "y") - def _yank_to_register(event: E, text_object: TextObject) -> None: - """ - Yank selection to named register. - """ - c = event.key_sequence[1].data - if c in vi_register_names: - _, clipboard_data = text_object.cut(event.current_buffer) - event.app.vi_state.named_registers[c] = clipboard_data - - @operator(">") - def _indent_text_object(event: E, text_object: TextObject) -> None: - """ - Indent. - """ - buff = event.current_buffer - from_, to = text_object.get_line_numbers(buff) - indent(buff, from_, to + 1, count=event.arg) - - @operator("<") - def _unindent_text_object(event: E, text_object: TextObject) -> None: - """ - Unindent. - """ - buff = event.current_buffer - from_, to = text_object.get_line_numbers(buff) - unindent(buff, from_, to + 1, count=event.arg) - - @operator("g", "q") - def _reshape(event: E, text_object: TextObject) -> None: - """ - Reshape text. - """ - buff = event.current_buffer - from_, to = text_object.get_line_numbers(buff) - reshape_text(buff, from_, to) - - # - # *** Text objects *** - # - - @text_object("b") - def _b(event: E) -> TextObject: - """ - Move one word or token left. - """ - return TextObject( - event.current_buffer.document.find_start_of_previous_word(count=event.arg) - or 0 - ) - - @text_object("B") - def _B(event: E) -> TextObject: - """ - Move one non-blank word left - """ - return TextObject( - event.current_buffer.document.find_start_of_previous_word( - count=event.arg, WORD=True - ) - or 0 - ) - - @text_object("$") - def _dollar(event: E) -> TextObject: - """ - 'c$', 'd$' and '$': Delete/change/move until end of line. - """ - return TextObject(event.current_buffer.document.get_end_of_line_position()) - - @text_object("w") - def _word_forward(event: E) -> TextObject: - """ - 'word' forward. 'cw', 'dw', 'w': Delete/change/move one word. - """ - return TextObject( - event.current_buffer.document.find_next_word_beginning(count=event.arg) - or event.current_buffer.document.get_end_of_document_position() - ) - - @text_object("W") - def _WORD_forward(event: E) -> TextObject: - """ - 'WORD' forward. 'cW', 'dW', 'W': Delete/change/move one WORD. - """ - return TextObject( - event.current_buffer.document.find_next_word_beginning( - count=event.arg, WORD=True - ) - or event.current_buffer.document.get_end_of_document_position() - ) - - @text_object("e") - def _end_of_word(event: E) -> TextObject: - """ - End of 'word': 'ce', 'de', 'e' - """ - end = event.current_buffer.document.find_next_word_ending(count=event.arg) - return TextObject(end - 1 if end else 0, type=TextObjectType.INCLUSIVE) - - @text_object("E") - def _end_of_WORD(event: E) -> TextObject: - """ - End of 'WORD': 'cE', 'dE', 'E' - """ - end = event.current_buffer.document.find_next_word_ending( - count=event.arg, WORD=True - ) - return TextObject(end - 1 if end else 0, type=TextObjectType.INCLUSIVE) - - @text_object("i", "w", no_move_handler=True) - def _inner_word(event: E) -> TextObject: - """ - Inner 'word': ciw and diw - """ - start, end = event.current_buffer.document.find_boundaries_of_current_word() - return TextObject(start, end) - - @text_object("a", "w", no_move_handler=True) - def _a_word(event: E) -> TextObject: - """ - A 'word': caw and daw - """ - start, end = event.current_buffer.document.find_boundaries_of_current_word( - include_trailing_whitespace=True - ) - return TextObject(start, end) - - @text_object("i", "W", no_move_handler=True) - def _inner_WORD(event: E) -> TextObject: - """ - Inner 'WORD': ciW and diW - """ - start, end = event.current_buffer.document.find_boundaries_of_current_word( - WORD=True - ) - return TextObject(start, end) - - @text_object("a", "W", no_move_handler=True) - def _a_WORD(event: E) -> TextObject: - """ - A 'WORD': caw and daw - """ - start, end = event.current_buffer.document.find_boundaries_of_current_word( - WORD=True, include_trailing_whitespace=True - ) - return TextObject(start, end) - - @text_object("a", "p", no_move_handler=True) - def _paragraph(event: E) -> TextObject: - """ - Auto paragraph. - """ - start = event.current_buffer.document.start_of_paragraph() - end = event.current_buffer.document.end_of_paragraph(count=event.arg) - return TextObject(start, end) - - @text_object("^") - def _start_of_line(event: E) -> TextObject: - """'c^', 'd^' and '^': Soft start of line, after whitespace.""" - return TextObject( - event.current_buffer.document.get_start_of_line_position( - after_whitespace=True - ) - ) - - @text_object("0") - def _hard_start_of_line(event: E) -> TextObject: - """ - 'c0', 'd0': Hard start of line, before whitespace. - (The move '0' key is implemented elsewhere, because a '0' could also change the `arg`.) - """ - return TextObject( - event.current_buffer.document.get_start_of_line_position( - after_whitespace=False - ) - ) - - def create_ci_ca_handles( - ci_start: str, ci_end: str, inner: bool, key: Optional[str] = None - ) -> None: - # TODO: 'dat', 'dit', (tags (like xml) - """ - Delete/Change string between this start and stop character. But keep these characters. - This implements all the ci", ci<, ci{, ci(, di", di<, ca", ca<, ... combinations. - """ - - def handler(event: E) -> TextObject: - if ci_start == ci_end: - # Quotes - start = event.current_buffer.document.find_backwards( - ci_start, in_current_line=False - ) - end = event.current_buffer.document.find(ci_end, in_current_line=False) - else: - # Brackets - start = event.current_buffer.document.find_enclosing_bracket_left( - ci_start, ci_end - ) - end = event.current_buffer.document.find_enclosing_bracket_right( - ci_start, ci_end - ) - - if start is not None and end is not None: - offset = 0 if inner else 1 - return TextObject(start + 1 - offset, end + offset) - else: - # Nothing found. - return TextObject(0) - - if key is None: - text_object("ai"[inner], ci_start, no_move_handler=True)(handler) - text_object("ai"[inner], ci_end, no_move_handler=True)(handler) - else: - text_object("ai"[inner], key, no_move_handler=True)(handler) - - for inner in (False, True): - for ci_start, ci_end in [ - ('"', '"'), - ("'", "'"), - ("`", "`"), - ("[", "]"), - ("<", ">"), - ("{", "}"), - ("(", ")"), - ]: - create_ci_ca_handles(ci_start, ci_end, inner) - - create_ci_ca_handles("(", ")", inner, "b") # 'dab', 'dib' - create_ci_ca_handles("{", "}", inner, "B") # 'daB', 'diB' - - @text_object("{") - def _previous_section(event: E) -> TextObject: - """ - Move to previous blank-line separated section. - Implements '{', 'c{', 'd{', 'y{' - """ - index = event.current_buffer.document.start_of_paragraph( - count=event.arg, before=True - ) - return TextObject(index) - - @text_object("}") - def _next_section(event: E) -> TextObject: - """ - Move to next blank-line separated section. - Implements '}', 'c}', 'd}', 'y}' - """ - index = event.current_buffer.document.end_of_paragraph( - count=event.arg, after=True - ) - return TextObject(index) - - @text_object("f", Keys.Any) - def _next_occurence(event: E) -> TextObject: - """ - Go to next occurrence of character. Typing 'fx' will move the - cursor to the next occurrence of character. 'x'. - """ - event.app.vi_state.last_character_find = CharacterFind(event.data, False) - match = event.current_buffer.document.find( - event.data, in_current_line=True, count=event.arg - ) - if match: - return TextObject(match, type=TextObjectType.INCLUSIVE) - else: - return TextObject(0) - - @text_object("F", Keys.Any) - def _previous_occurance(event: E) -> TextObject: - """ - Go to previous occurrence of character. Typing 'Fx' will move the - cursor to the previous occurrence of character. 'x'. - """ - event.app.vi_state.last_character_find = CharacterFind(event.data, True) - return TextObject( - event.current_buffer.document.find_backwards( - event.data, in_current_line=True, count=event.arg - ) - or 0 - ) - - @text_object("t", Keys.Any) - def _t(event: E) -> TextObject: - """ - Move right to the next occurrence of c, then one char backward. - """ - event.app.vi_state.last_character_find = CharacterFind(event.data, False) - match = event.current_buffer.document.find( - event.data, in_current_line=True, count=event.arg - ) - if match: - return TextObject(match - 1, type=TextObjectType.INCLUSIVE) - else: - return TextObject(0) - - @text_object("T", Keys.Any) - def _T(event: E) -> TextObject: - """ - Move left to the previous occurrence of c, then one char forward. - """ - event.app.vi_state.last_character_find = CharacterFind(event.data, True) - match = event.current_buffer.document.find_backwards( - event.data, in_current_line=True, count=event.arg - ) - return TextObject(match + 1 if match else 0) - - def repeat(reverse: bool) -> None: - """ - Create ',' and ';' commands. - """ - - @text_object("," if reverse else ";") - def _(event: E) -> TextObject: - """ - Repeat the last 'f'/'F'/'t'/'T' command. - """ - pos: Optional[int] = 0 - vi_state = event.app.vi_state - - type = TextObjectType.EXCLUSIVE - - if vi_state.last_character_find: - char = vi_state.last_character_find.character - backwards = vi_state.last_character_find.backwards - - if reverse: - backwards = not backwards - - if backwards: - pos = event.current_buffer.document.find_backwards( - char, in_current_line=True, count=event.arg - ) - else: - pos = event.current_buffer.document.find( - char, in_current_line=True, count=event.arg - ) - type = TextObjectType.INCLUSIVE - if pos: - return TextObject(pos, type=type) - else: - return TextObject(0) - - repeat(True) - repeat(False) - - @text_object("h") - @text_object("left") - def _left(event: E) -> TextObject: - """ - Implements 'ch', 'dh', 'h': Cursor left. - """ - return TextObject( - event.current_buffer.document.get_cursor_left_position(count=event.arg) - ) - - @text_object("j", no_move_handler=True, no_selection_handler=True) - # Note: We also need `no_selection_handler`, because we in - # selection mode, we prefer the other 'j' binding that keeps - # `buffer.preferred_column`. - def _down(event: E) -> TextObject: - """ - Implements 'cj', 'dj', 'j', ... Cursor up. - """ - return TextObject( - event.current_buffer.document.get_cursor_down_position(count=event.arg), - type=TextObjectType.LINEWISE, - ) - - @text_object("k", no_move_handler=True, no_selection_handler=True) - def _up(event: E) -> TextObject: - """ - Implements 'ck', 'dk', 'k', ... Cursor up. - """ - return TextObject( - event.current_buffer.document.get_cursor_up_position(count=event.arg), - type=TextObjectType.LINEWISE, - ) - - @text_object("l") - @text_object(" ") - @text_object("right") - def _right(event: E) -> TextObject: - """ - Implements 'cl', 'dl', 'l', 'c ', 'd ', ' '. Cursor right. - """ - return TextObject( - event.current_buffer.document.get_cursor_right_position(count=event.arg) - ) - - @text_object("H") - def _top_of_screen(event: E) -> TextObject: - """ - Moves to the start of the visible region. (Below the scroll offset.) - Implements 'cH', 'dH', 'H'. - """ - w = event.app.layout.current_window - b = event.current_buffer - - if w and w.render_info: - # When we find a Window that has BufferControl showing this window, - # move to the start of the visible area. - pos = ( - b.document.translate_row_col_to_index( - w.render_info.first_visible_line(after_scroll_offset=True), 0 - ) - - b.cursor_position - ) - - else: - # Otherwise, move to the start of the input. - pos = -len(b.document.text_before_cursor) - return TextObject(pos, type=TextObjectType.LINEWISE) - - @text_object("M") - def _middle_of_screen(event: E) -> TextObject: - """ - Moves cursor to the vertical center of the visible region. - Implements 'cM', 'dM', 'M'. - """ - w = event.app.layout.current_window - b = event.current_buffer - - if w and w.render_info: - # When we find a Window that has BufferControl showing this window, - # move to the center of the visible area. - pos = ( - b.document.translate_row_col_to_index( - w.render_info.center_visible_line(), 0 - ) - - b.cursor_position - ) - - else: - # Otherwise, move to the start of the input. - pos = -len(b.document.text_before_cursor) - return TextObject(pos, type=TextObjectType.LINEWISE) - - @text_object("L") - def _end_of_screen(event: E) -> TextObject: - """ - Moves to the end of the visible region. (Above the scroll offset.) - """ - w = event.app.layout.current_window - b = event.current_buffer - - if w and w.render_info: - # When we find a Window that has BufferControl showing this window, - # move to the end of the visible area. - pos = ( - b.document.translate_row_col_to_index( - w.render_info.last_visible_line(before_scroll_offset=True), 0 - ) - - b.cursor_position - ) - - else: - # Otherwise, move to the end of the input. - pos = len(b.document.text_after_cursor) - return TextObject(pos, type=TextObjectType.LINEWISE) - - @text_object("n", no_move_handler=True) - def _search_next(event: E) -> TextObject: - """ - Search next. - """ - buff = event.current_buffer - search_state = event.app.current_search_state - - cursor_position = buff.get_search_position( - search_state, include_current_position=False, count=event.arg - ) - return TextObject(cursor_position - buff.cursor_position) - - @handle("n", filter=vi_navigation_mode) - def _search_next2(event: E) -> None: - """ - Search next in navigation mode. (This goes through the history.) - """ - search_state = event.app.current_search_state - - event.current_buffer.apply_search( - search_state, include_current_position=False, count=event.arg - ) - - @text_object("N", no_move_handler=True) - def _search_previous(event: E) -> TextObject: - """ - Search previous. - """ - buff = event.current_buffer - search_state = event.app.current_search_state - - cursor_position = buff.get_search_position( - ~search_state, include_current_position=False, count=event.arg - ) - return TextObject(cursor_position - buff.cursor_position) - - @handle("N", filter=vi_navigation_mode) - def _search_previous2(event: E) -> None: - """ - Search previous in navigation mode. (This goes through the history.) - """ - search_state = event.app.current_search_state - - event.current_buffer.apply_search( - ~search_state, include_current_position=False, count=event.arg - ) - - @handle("z", "+", filter=vi_navigation_mode | vi_selection_mode) - @handle("z", "t", filter=vi_navigation_mode | vi_selection_mode) - @handle("z", "enter", filter=vi_navigation_mode | vi_selection_mode) - def _scroll_top(event: E) -> None: - """ - Scrolls the window to makes the current line the first line in the visible region. - """ - b = event.current_buffer - event.app.layout.current_window.vertical_scroll = b.document.cursor_position_row - - @handle("z", "-", filter=vi_navigation_mode | vi_selection_mode) - @handle("z", "b", filter=vi_navigation_mode | vi_selection_mode) - def _scroll_bottom(event: E) -> None: - """ - Scrolls the window to makes the current line the last line in the visible region. - """ - # We can safely set the scroll offset to zero; the Window will make - # sure that it scrolls at least enough to make the cursor visible - # again. - event.app.layout.current_window.vertical_scroll = 0 - - @handle("z", "z", filter=vi_navigation_mode | vi_selection_mode) - def _scroll_center(event: E) -> None: - """ - Center Window vertically around cursor. - """ - w = event.app.layout.current_window - b = event.current_buffer - - if w and w.render_info: - info = w.render_info - - # Calculate the offset that we need in order to position the row - # containing the cursor in the center. - scroll_height = info.window_height // 2 - - y = max(0, b.document.cursor_position_row - 1) - height = 0 - while y > 0: - line_height = info.get_height_for_line(y) - - if height + line_height < scroll_height: - height += line_height - y -= 1 - else: - break - - w.vertical_scroll = y - - @text_object("%") - def _goto_corresponding_bracket(event: E) -> TextObject: - """ - Implements 'c%', 'd%', '%, 'y%' (Move to corresponding bracket.) - If an 'arg' has been given, go this this % position in the file. - """ - buffer = event.current_buffer - - if event._arg: - # If 'arg' has been given, the meaning of % is to go to the 'x%' - # row in the file. - if 0 < event.arg <= 100: - absolute_index = buffer.document.translate_row_col_to_index( - int((event.arg * buffer.document.line_count - 1) / 100), 0 - ) - return TextObject( - absolute_index - buffer.document.cursor_position, - type=TextObjectType.LINEWISE, - ) - else: - return TextObject(0) # Do nothing. - - else: - # Move to the corresponding opening/closing bracket (()'s, []'s and {}'s). - match = buffer.document.find_matching_bracket_position() - if match: - return TextObject(match, type=TextObjectType.INCLUSIVE) - else: - return TextObject(0) - - @text_object("|") - def _to_column(event: E) -> TextObject: - """ - Move to the n-th column (you may specify the argument n by typing it on - number keys, for example, 20|). - """ - return TextObject( - event.current_buffer.document.get_column_cursor_position(event.arg - 1) - ) - - @text_object("g", "g") - def _goto_first_line(event: E) -> TextObject: - """ - Go to the start of the very first line. - Implements 'gg', 'cgg', 'ygg' - """ - d = event.current_buffer.document - - if event._arg: - # Move to the given line. - return TextObject( - d.translate_row_col_to_index(event.arg - 1, 0) - d.cursor_position, - type=TextObjectType.LINEWISE, - ) - else: - # Move to the top of the input. - return TextObject( - d.get_start_of_document_position(), type=TextObjectType.LINEWISE - ) - - @text_object("g", "_") - def _goto_last_line(event: E) -> TextObject: - """ - Go to last non-blank of line. - 'g_', 'cg_', 'yg_', etc.. - """ - return TextObject( - event.current_buffer.document.last_non_blank_of_current_line_position(), - type=TextObjectType.INCLUSIVE, - ) - - @text_object("g", "e") - def _ge(event: E) -> TextObject: - """ - Go to last character of previous word. - 'ge', 'cge', 'yge', etc.. - """ - prev_end = event.current_buffer.document.find_previous_word_ending( - count=event.arg - ) - return TextObject( - prev_end - 1 if prev_end is not None else 0, type=TextObjectType.INCLUSIVE - ) - - @text_object("g", "E") - def _gE(event: E) -> TextObject: - """ - Go to last character of previous WORD. - 'gE', 'cgE', 'ygE', etc.. - """ - prev_end = event.current_buffer.document.find_previous_word_ending( - count=event.arg, WORD=True - ) - return TextObject( - prev_end - 1 if prev_end is not None else 0, type=TextObjectType.INCLUSIVE - ) - - @text_object("g", "m") - def _gm(event: E) -> TextObject: - """ - Like g0, but half a screenwidth to the right. (Or as much as possible.) - """ - w = event.app.layout.current_window - buff = event.current_buffer - - if w and w.render_info: - width = w.render_info.window_width - start = buff.document.get_start_of_line_position(after_whitespace=False) - start += int(min(width / 2, len(buff.document.current_line))) - - return TextObject(start, type=TextObjectType.INCLUSIVE) - return TextObject(0) - - @text_object("G") - def _last_line(event: E) -> TextObject: - """ - Go to the end of the document. (If no arg has been given.) - """ - buf = event.current_buffer - return TextObject( - buf.document.translate_row_col_to_index(buf.document.line_count - 1, 0) - - buf.cursor_position, - type=TextObjectType.LINEWISE, - ) - - # - # *** Other *** - # - - @handle("G", filter=has_arg) - def _to_nth_history_line(event: E) -> None: - """ - If an argument is given, move to this line in the history. (for - example, 15G) - """ - event.current_buffer.go_to_history(event.arg - 1) - - for n in "123456789": - - @handle( - n, - filter=vi_navigation_mode - | vi_selection_mode - | vi_waiting_for_text_object_mode, - ) - def _arg(event: E) -> None: - """ - Always handle numberics in navigation mode as arg. - """ - event.append_to_arg_count(event.data) - - @handle( - "0", - filter=( - vi_navigation_mode | vi_selection_mode | vi_waiting_for_text_object_mode - ) - & has_arg, - ) - def _0_arg(event: E) -> None: - """ - Zero when an argument was already give. - """ - event.append_to_arg_count(event.data) - - @handle(Keys.Any, filter=vi_replace_mode) - def _insert_text(event: E) -> None: - """ - Insert data at cursor position. - """ - event.current_buffer.insert_text(event.data, overwrite=True) - - @handle(Keys.Any, filter=vi_replace_single_mode) - def _replace_single(event: E) -> None: - """ - Replace single character at cursor position. - """ - event.current_buffer.insert_text(event.data, overwrite=True) - event.current_buffer.cursor_position -= 1 - event.app.vi_state.input_mode = InputMode.NAVIGATION - - @handle( - Keys.Any, - filter=vi_insert_multiple_mode, - save_before=(lambda e: not e.is_repeat), - ) - def _insert_text_multiple_cursors(event: E) -> None: - """ - Insert data at multiple cursor positions at once. - (Usually a result of pressing 'I' or 'A' in block-selection mode.) - """ - buff = event.current_buffer - original_text = buff.text - - # Construct new text. - text = [] - p = 0 - - for p2 in buff.multiple_cursor_positions: - text.append(original_text[p:p2]) - text.append(event.data) - p = p2 - - text.append(original_text[p:]) - - # Shift all cursor positions. - new_cursor_positions = [ - pos + i + 1 for i, pos in enumerate(buff.multiple_cursor_positions) - ] - - # Set result. - buff.text = "".join(text) - buff.multiple_cursor_positions = new_cursor_positions - buff.cursor_position += 1 - - @handle("backspace", filter=vi_insert_multiple_mode) - def _delete_before_multiple_cursors(event: E) -> None: - """ - Backspace, using multiple cursors. - """ - buff = event.current_buffer - original_text = buff.text - - # Construct new text. - deleted_something = False - text = [] - p = 0 - - for p2 in buff.multiple_cursor_positions: - if p2 > 0 and original_text[p2 - 1] != "\n": # Don't delete across lines. - text.append(original_text[p : p2 - 1]) - deleted_something = True - else: - text.append(original_text[p:p2]) - p = p2 - - text.append(original_text[p:]) - - if deleted_something: - # Shift all cursor positions. - lengths = [len(part) for part in text[:-1]] - new_cursor_positions = list(accumulate(lengths)) - - # Set result. - buff.text = "".join(text) - buff.multiple_cursor_positions = new_cursor_positions - buff.cursor_position -= 1 - else: - event.app.output.bell() - - @handle("delete", filter=vi_insert_multiple_mode) - def _delete_after_multiple_cursors(event: E) -> None: - """ - Delete, using multiple cursors. - """ - buff = event.current_buffer - original_text = buff.text - - # Construct new text. - deleted_something = False - text = [] - new_cursor_positions = [] - p = 0 - - for p2 in buff.multiple_cursor_positions: - text.append(original_text[p:p2]) - if p2 >= len(original_text) or original_text[p2] == "\n": - # Don't delete across lines. - p = p2 - else: - p = p2 + 1 - deleted_something = True - - text.append(original_text[p:]) - - if deleted_something: - # Shift all cursor positions. - lengths = [len(part) for part in text[:-1]] - new_cursor_positions = list(accumulate(lengths)) - - # Set result. - buff.text = "".join(text) - buff.multiple_cursor_positions = new_cursor_positions - else: - event.app.output.bell() - - @handle("left", filter=vi_insert_multiple_mode) - def _left_multiple(event: E) -> None: - """ - Move all cursors to the left. - (But keep all cursors on the same line.) - """ - buff = event.current_buffer - new_positions = [] - - for p in buff.multiple_cursor_positions: - if buff.document.translate_index_to_position(p)[1] > 0: - p -= 1 - new_positions.append(p) - - buff.multiple_cursor_positions = new_positions - - if buff.document.cursor_position_col > 0: - buff.cursor_position -= 1 - - @handle("right", filter=vi_insert_multiple_mode) - def _right_multiple(event: E) -> None: - """ - Move all cursors to the right. - (But keep all cursors on the same line.) - """ - buff = event.current_buffer - new_positions = [] - - for p in buff.multiple_cursor_positions: - row, column = buff.document.translate_index_to_position(p) - if column < len(buff.document.lines[row]): - p += 1 - new_positions.append(p) - - buff.multiple_cursor_positions = new_positions - - if not buff.document.is_cursor_at_the_end_of_line: - buff.cursor_position += 1 - - @handle("up", filter=vi_insert_multiple_mode) - @handle("down", filter=vi_insert_multiple_mode) - def _updown_multiple(event: E) -> None: - """ - Ignore all up/down key presses when in multiple cursor mode. - """ - - @handle("c-x", "c-l", filter=vi_insert_mode) - def _complete_line(event: E) -> None: - """ - Pressing the ControlX - ControlL sequence in Vi mode does line - completion based on the other lines in the document and the history. - """ - event.current_buffer.start_history_lines_completion() - - @handle("c-x", "c-f", filter=vi_insert_mode) - def _complete_filename(event: E) -> None: - """ - Complete file names. - """ - # TODO - pass - - @handle("c-k", filter=vi_insert_mode | vi_replace_mode) - def _digraph(event: E) -> None: - """ - Go into digraph mode. - """ - event.app.vi_state.waiting_for_digraph = True - - @Condition - def digraph_symbol_1_given() -> bool: - return get_app().vi_state.digraph_symbol1 is not None - - @handle(Keys.Any, filter=vi_digraph_mode & ~digraph_symbol_1_given) - def _digraph1(event: E) -> None: - """ - First digraph symbol. - """ - event.app.vi_state.digraph_symbol1 = event.data - - @handle(Keys.Any, filter=vi_digraph_mode & digraph_symbol_1_given) - def _create_digraph(event: E) -> None: - """ - Insert digraph. - """ - try: - # Lookup. - code: Tuple[str, str] = ( - event.app.vi_state.digraph_symbol1 or "", - event.data, - ) - if code not in DIGRAPHS: - code = code[::-1] # Try reversing. - symbol = DIGRAPHS[code] - except KeyError: - # Unknown digraph. - event.app.output.bell() - else: - # Insert digraph. - overwrite = event.app.vi_state.input_mode == InputMode.REPLACE - event.current_buffer.insert_text(chr(symbol), overwrite=overwrite) - event.app.vi_state.waiting_for_digraph = False - finally: - event.app.vi_state.waiting_for_digraph = False - event.app.vi_state.digraph_symbol1 = None - - @handle("c-o", filter=vi_insert_mode | vi_replace_mode) - def _quick_normal_mode(event: E) -> None: - """ - Go into normal mode for one single action. - """ - event.app.vi_state.temporary_navigation_mode = True - - @handle("q", Keys.Any, filter=vi_navigation_mode & ~vi_recording_macro) - def _start_macro(event: E) -> None: - """ - Start recording macro. - """ - c = event.key_sequence[1].data - if c in vi_register_names: - vi_state = event.app.vi_state - - vi_state.recording_register = c - vi_state.current_recording = "" - - @handle("q", filter=vi_navigation_mode & vi_recording_macro) - def _stop_macro(event: E) -> None: - """ - Stop recording macro. - """ - vi_state = event.app.vi_state - - # Store and stop recording. - if vi_state.recording_register: - vi_state.named_registers[vi_state.recording_register] = ClipboardData( - vi_state.current_recording - ) - vi_state.recording_register = None - vi_state.current_recording = "" - - @handle("@", Keys.Any, filter=vi_navigation_mode, record_in_macro=False) - def _execute_macro(event: E) -> None: - """ - Execute macro. - - Notice that we pass `record_in_macro=False`. This ensures that the `@x` - keys don't appear in the recording itself. This function inserts the - body of the called macro back into the KeyProcessor, so these keys will - be added later on to the macro of their handlers have - `record_in_macro=True`. - """ - # Retrieve macro. - c = event.key_sequence[1].data - try: - macro = event.app.vi_state.named_registers[c] - except KeyError: - return - - # Expand macro (which is a string in the register), in individual keys. - # Use vt100 parser for this. - keys: List[KeyPress] = [] - - parser = Vt100Parser(keys.append) - parser.feed(macro.text) - parser.flush() - - # Now feed keys back to the input processor. - for _ in range(event.arg): - event.app.key_processor.feed_multiple(keys, first=True) - - return ConditionalKeyBindings(key_bindings, vi_mode) - - -def load_vi_search_bindings() -> KeyBindingsBase: - key_bindings = KeyBindings() - handle = key_bindings.add - from . import search - - @Condition - def search_buffer_is_empty() -> bool: - "Returns True when the search buffer is empty." - return get_app().current_buffer.text == "" - - # Vi-style forward search. - handle( - "/", - filter=(vi_navigation_mode | vi_selection_mode) & ~vi_search_direction_reversed, - )(search.start_forward_incremental_search) - handle( - "?", - filter=(vi_navigation_mode | vi_selection_mode) & vi_search_direction_reversed, - )(search.start_forward_incremental_search) - handle("c-s")(search.start_forward_incremental_search) - - # Vi-style backward search. - handle( - "?", - filter=(vi_navigation_mode | vi_selection_mode) & ~vi_search_direction_reversed, - )(search.start_reverse_incremental_search) - handle( - "/", - filter=(vi_navigation_mode | vi_selection_mode) & vi_search_direction_reversed, - )(search.start_reverse_incremental_search) - handle("c-r")(search.start_reverse_incremental_search) - - # Apply the search. (At the / or ? prompt.) - handle("enter", filter=is_searching)(search.accept_search) - - handle("c-r", filter=is_searching)(search.reverse_incremental_search) - handle("c-s", filter=is_searching)(search.forward_incremental_search) - - handle("c-c")(search.abort_search) - handle("c-g")(search.abort_search) - handle("backspace", filter=search_buffer_is_empty)(search.abort_search) - - # Handle escape. This should accept the search, just like readline. - # `abort_search` would be a meaningful alternative. - handle("escape")(search.accept_search) - - return ConditionalKeyBindings(key_bindings, vi_mode) + +from prompt_toolkit.application.current import get_app +from prompt_toolkit.buffer import Buffer, indent, reshape_text, unindent +from prompt_toolkit.clipboard import ClipboardData +from prompt_toolkit.document import Document +from prompt_toolkit.filters import ( + Always, + Condition, + Filter, + has_arg, + is_read_only, + is_searching, +) +from prompt_toolkit.filters.app import ( + in_paste_mode, + is_multiline, + vi_digraph_mode, + vi_insert_mode, + vi_insert_multiple_mode, + vi_mode, + vi_navigation_mode, + vi_recording_macro, + vi_replace_mode, + vi_replace_single_mode, + vi_search_direction_reversed, + vi_selection_mode, + vi_waiting_for_text_object_mode, +) +from prompt_toolkit.input.vt100_parser import Vt100Parser +from prompt_toolkit.key_binding.digraphs import DIGRAPHS +from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent +from prompt_toolkit.key_binding.vi_state import CharacterFind, InputMode +from prompt_toolkit.keys import Keys +from prompt_toolkit.search import SearchDirection +from prompt_toolkit.selection import PasteMode, SelectionState, SelectionType + +from ..key_bindings import ConditionalKeyBindings, KeyBindings, KeyBindingsBase +from .named_commands import get_by_name + +__all__ = [ + "load_vi_bindings", + "load_vi_search_bindings", +] + +E = KeyPressEvent + +ascii_lowercase = string.ascii_lowercase + +vi_register_names = ascii_lowercase + "0123456789" + + +class TextObjectType(Enum): + EXCLUSIVE = "EXCLUSIVE" + INCLUSIVE = "INCLUSIVE" + LINEWISE = "LINEWISE" + BLOCK = "BLOCK" + + +class TextObject: + """ + Return struct for functions wrapped in ``text_object``. + Both `start` and `end` are relative to the current cursor position. + """ + + def __init__( + self, start: int, end: int = 0, type: TextObjectType = TextObjectType.EXCLUSIVE + ): + + self.start = start + self.end = end + self.type = type + + @property + def selection_type(self) -> SelectionType: + if self.type == TextObjectType.LINEWISE: + return SelectionType.LINES + if self.type == TextObjectType.BLOCK: + return SelectionType.BLOCK + else: + return SelectionType.CHARACTERS + + def sorted(self) -> Tuple[int, int]: + """ + Return a (start, end) tuple where start <= end. + """ + if self.start < self.end: + return self.start, self.end + else: + return self.end, self.start + + def operator_range(self, document: Document) -> Tuple[int, int]: + """ + Return a (start, end) tuple with start <= end that indicates the range + operators should operate on. + `buffer` is used to get start and end of line positions. + + This should return something that can be used in a slice, so the `end` + position is *not* included. + """ + start, end = self.sorted() + doc = document + + if ( + self.type == TextObjectType.EXCLUSIVE + and doc.translate_index_to_position(end + doc.cursor_position)[1] == 0 + ): + # If the motion is exclusive and the end of motion is on the first + # column, the end position becomes end of previous line. + end -= 1 + if self.type == TextObjectType.INCLUSIVE: + end += 1 + if self.type == TextObjectType.LINEWISE: + # Select whole lines + row, col = doc.translate_index_to_position(start + doc.cursor_position) + start = doc.translate_row_col_to_index(row, 0) - doc.cursor_position + row, col = doc.translate_index_to_position(end + doc.cursor_position) + end = ( + doc.translate_row_col_to_index(row, len(doc.lines[row])) + - doc.cursor_position + ) + return start, end + + def get_line_numbers(self, buffer: Buffer) -> Tuple[int, int]: + """ + Return a (start_line, end_line) pair. + """ + # Get absolute cursor positions from the text object. + from_, to = self.operator_range(buffer.document) + from_ += buffer.cursor_position + to += buffer.cursor_position + + # Take the start of the lines. + from_, _ = buffer.document.translate_index_to_position(from_) + to, _ = buffer.document.translate_index_to_position(to) + + return from_, to + + def cut(self, buffer: Buffer) -> Tuple[Document, ClipboardData]: + """ + Turn text object into `ClipboardData` instance. + """ + from_, to = self.operator_range(buffer.document) + + from_ += buffer.cursor_position + to += buffer.cursor_position + + # For Vi mode, the SelectionState does include the upper position, + # while `self.operator_range` does not. So, go one to the left, unless + # we're in the line mode, then we don't want to risk going to the + # previous line, and missing one line in the selection. + if self.type != TextObjectType.LINEWISE: + to -= 1 + + document = Document( + buffer.text, + to, + SelectionState(original_cursor_position=from_, type=self.selection_type), + ) + + new_document, clipboard_data = document.cut_selection() + return new_document, clipboard_data + + +# Typevar for any text object function: +TextObjectFunction = Callable[[E], TextObject] +_TOF = TypeVar("_TOF", bound=TextObjectFunction) + + +def create_text_object_decorator( + key_bindings: KeyBindings, +) -> Callable[..., Callable[[_TOF], _TOF]]: + """ + Create a decorator that can be used to register Vi text object implementations. + """ + + def text_object_decorator( + *keys: Union[Keys, str], + filter: Filter = Always(), + no_move_handler: bool = False, + no_selection_handler: bool = False, + eager: bool = False, + ) -> Callable[[_TOF], _TOF]: + """ + Register a text object function. + + Usage:: + + @text_object('w', filter=..., no_move_handler=False) + def handler(event): + # Return a text object for this key. + return TextObject(...) + + :param no_move_handler: Disable the move handler in navigation mode. + (It's still active in selection mode.) + """ + + def decorator(text_object_func: _TOF) -> _TOF: + @key_bindings.add( + *keys, filter=vi_waiting_for_text_object_mode & filter, eager=eager + ) + def _apply_operator_to_text_object(event: E) -> None: + # Arguments are multiplied. + vi_state = event.app.vi_state + event._arg = str((vi_state.operator_arg or 1) * (event.arg or 1)) + + # Call the text object handler. + text_obj = text_object_func(event) + + # Get the operator function. + # (Should never be None here, given the + # `vi_waiting_for_text_object_mode` filter state.) + operator_func = vi_state.operator_func + + if text_obj is not None and operator_func is not None: + # Call the operator function with the text object. + operator_func(event, text_obj) + + # Clear operator. + event.app.vi_state.operator_func = None + event.app.vi_state.operator_arg = None + + # Register a move operation. (Doesn't need an operator.) + if not no_move_handler: + + @key_bindings.add( + *keys, + filter=~vi_waiting_for_text_object_mode + & filter + & vi_navigation_mode, + eager=eager, + ) + def _move_in_navigation_mode(event: E) -> None: + """ + Move handler for navigation mode. + """ + text_object = text_object_func(event) + event.current_buffer.cursor_position += text_object.start + + # Register a move selection operation. + if not no_selection_handler: + + @key_bindings.add( + *keys, + filter=~vi_waiting_for_text_object_mode + & filter + & vi_selection_mode, + eager=eager, + ) + def _move_in_selection_mode(event: E) -> None: + """ + Move handler for selection mode. + """ + text_object = text_object_func(event) + buff = event.current_buffer + selection_state = buff.selection_state + + if selection_state is None: + return # Should not happen, because of the `vi_selection_mode` filter. + + # When the text object has both a start and end position, like 'i(' or 'iw', + # Turn this into a selection, otherwise the cursor. + if text_object.end: + # Take selection positions from text object. + start, end = text_object.operator_range(buff.document) + start += buff.cursor_position + end += buff.cursor_position + + selection_state.original_cursor_position = start + buff.cursor_position = end + + # Take selection type from text object. + if text_object.type == TextObjectType.LINEWISE: + selection_state.type = SelectionType.LINES + else: + selection_state.type = SelectionType.CHARACTERS + else: + event.current_buffer.cursor_position += text_object.start + + # Make it possible to chain @text_object decorators. + return text_object_func + + return decorator + + return text_object_decorator + + +# Typevar for any operator function: +OperatorFunction = Callable[[E, TextObject], None] +_OF = TypeVar("_OF", bound=OperatorFunction) + + +def create_operator_decorator( + key_bindings: KeyBindings, +) -> Callable[..., Callable[[_OF], _OF]]: + """ + Create a decorator that can be used for registering Vi operators. + """ + + def operator_decorator( + *keys: Union[Keys, str], filter: Filter = Always(), eager: bool = False + ) -> Callable[[_OF], _OF]: + """ + Register a Vi operator. + + Usage:: + + @operator('d', filter=...) + def handler(event, text_object): + # Do something with the text object here. + """ + + def decorator(operator_func: _OF) -> _OF: + @key_bindings.add( + *keys, + filter=~vi_waiting_for_text_object_mode & filter & vi_navigation_mode, + eager=eager, + ) + def _operator_in_navigation(event: E) -> None: + """ + Handle operator in navigation mode. + """ + # When this key binding is matched, only set the operator + # function in the ViState. We should execute it after a text + # object has been received. + event.app.vi_state.operator_func = operator_func + event.app.vi_state.operator_arg = event.arg + + @key_bindings.add( + *keys, + filter=~vi_waiting_for_text_object_mode & filter & vi_selection_mode, + eager=eager, + ) + def _operator_in_selection(event: E) -> None: + """ + Handle operator in selection mode. + """ + buff = event.current_buffer + selection_state = buff.selection_state + + if selection_state is not None: + # Create text object from selection. + if selection_state.type == SelectionType.LINES: + text_obj_type = TextObjectType.LINEWISE + elif selection_state.type == SelectionType.BLOCK: + text_obj_type = TextObjectType.BLOCK + else: + text_obj_type = TextObjectType.INCLUSIVE + + text_object = TextObject( + selection_state.original_cursor_position - buff.cursor_position, + type=text_obj_type, + ) + + # Execute operator. + operator_func(event, text_object) + + # Quit selection mode. + buff.selection_state = None + + return operator_func + + return decorator + + return operator_decorator + + +def load_vi_bindings() -> KeyBindingsBase: + """ + Vi extensions. + + # Overview of Readline Vi commands: + # http://www.catonmat.net/download/bash-vi-editing-mode-cheat-sheet.pdf + """ + # Note: Some key bindings have the "~IsReadOnly()" filter added. This + # prevents the handler to be executed when the focus is on a + # read-only buffer. + # This is however only required for those that change the ViState to + # INSERT mode. The `Buffer` class itself throws the + # `EditReadOnlyBuffer` exception for any text operations which is + # handled correctly. There is no need to add "~IsReadOnly" to all key + # bindings that do text manipulation. + + key_bindings = KeyBindings() + handle = key_bindings.add + + # (Note: Always take the navigation bindings in read-only mode, even when + # ViState says different.) + + TransformFunction = Tuple[Tuple[str, ...], Filter, Callable[[str], str]] + + vi_transform_functions: List[TransformFunction] = [ + # Rot 13 transformation + ( + ("g", "?"), + Always(), + lambda string: codecs.encode(string, "rot_13"), + ), + # To lowercase + (("g", "u"), Always(), lambda string: string.lower()), + # To uppercase. + (("g", "U"), Always(), lambda string: string.upper()), + # Swap case. + (("g", "~"), Always(), lambda string: string.swapcase()), + ( + ("~",), + Condition(lambda: get_app().vi_state.tilde_operator), + lambda string: string.swapcase(), + ), + ] + + # Insert a character literally (quoted insert). + handle("c-v", filter=vi_insert_mode)(get_by_name("quoted-insert")) + + @handle("escape") + def _back_to_navigation(event: E) -> None: + """ + Escape goes to vi navigation mode. + """ + buffer = event.current_buffer + vi_state = event.app.vi_state + + if vi_state.input_mode in (InputMode.INSERT, InputMode.REPLACE): + buffer.cursor_position += buffer.document.get_cursor_left_position() + + vi_state.input_mode = InputMode.NAVIGATION + + if bool(buffer.selection_state): + buffer.exit_selection() + + @handle("k", filter=vi_selection_mode) + def _up_in_selection(event: E) -> None: + """ + Arrow up in selection mode. + """ + event.current_buffer.cursor_up(count=event.arg) + + @handle("j", filter=vi_selection_mode) + def _down_in_selection(event: E) -> None: + """ + Arrow down in selection mode. + """ + event.current_buffer.cursor_down(count=event.arg) + + @handle("up", filter=vi_navigation_mode) + @handle("c-p", filter=vi_navigation_mode) + def _up_in_navigation(event: E) -> None: + """ + Arrow up and ControlP in navigation mode go up. + """ + event.current_buffer.auto_up(count=event.arg) + + @handle("k", filter=vi_navigation_mode) + def _go_up(event: E) -> None: + """ + Go up, but if we enter a new history entry, move to the start of the + line. + """ + event.current_buffer.auto_up( + count=event.arg, go_to_start_of_line_if_history_changes=True + ) + + @handle("down", filter=vi_navigation_mode) + @handle("c-n", filter=vi_navigation_mode) + def _go_down(event: E) -> None: + """ + Arrow down and Control-N in navigation mode. + """ + event.current_buffer.auto_down(count=event.arg) + + @handle("j", filter=vi_navigation_mode) + def _go_down2(event: E) -> None: + """ + Go down, but if we enter a new history entry, go to the start of the line. + """ + event.current_buffer.auto_down( + count=event.arg, go_to_start_of_line_if_history_changes=True + ) + + @handle("backspace", filter=vi_navigation_mode) + def _go_left(event: E) -> None: + """ + In navigation-mode, move cursor. + """ + event.current_buffer.cursor_position += ( + event.current_buffer.document.get_cursor_left_position(count=event.arg) + ) + + @handle("c-n", filter=vi_insert_mode) + def _complete_next(event: E) -> None: + b = event.current_buffer + + if b.complete_state: + b.complete_next() + else: + b.start_completion(select_first=True) + + @handle("c-p", filter=vi_insert_mode) + def _complete_prev(event: E) -> None: + """ + Control-P: To previous completion. + """ + b = event.current_buffer + + if b.complete_state: + b.complete_previous() + else: + b.start_completion(select_last=True) + + @handle("c-g", filter=vi_insert_mode) + @handle("c-y", filter=vi_insert_mode) + def _accept_completion(event: E) -> None: + """ + Accept current completion. + """ + event.current_buffer.complete_state = None + + @handle("c-e", filter=vi_insert_mode) + def _cancel_completion(event: E) -> None: + """ + Cancel completion. Go back to originally typed text. + """ + event.current_buffer.cancel_completion() + + @Condition + def is_returnable() -> bool: + return get_app().current_buffer.is_returnable + + # In navigation mode, pressing enter will always return the input. + handle("enter", filter=vi_navigation_mode & is_returnable)( + get_by_name("accept-line") + ) + + # In insert mode, also accept input when enter is pressed, and the buffer + # has been marked as single line. + handle("enter", filter=is_returnable & ~is_multiline)(get_by_name("accept-line")) + + @handle("enter", filter=~is_returnable & vi_navigation_mode) + def _start_of_next_line(event: E) -> None: + """ + Go to the beginning of next line. + """ + b = event.current_buffer + b.cursor_down(count=event.arg) + b.cursor_position += b.document.get_start_of_line_position( + after_whitespace=True + ) + + # ** In navigation mode ** + + # List of navigation commands: http://hea-www.harvard.edu/~fine/Tech/vi.html + + @handle("insert", filter=vi_navigation_mode) + def _insert_mode(event: E) -> None: + """ + Pressing the Insert key. + """ + event.app.vi_state.input_mode = InputMode.INSERT + + @handle("insert", filter=vi_insert_mode) + def _navigation_mode(event: E) -> None: + """ + Pressing the Insert key. + """ + event.app.vi_state.input_mode = InputMode.NAVIGATION + + @handle("a", filter=vi_navigation_mode & ~is_read_only) + # ~IsReadOnly, because we want to stay in navigation mode for + # read-only buffers. + def _a(event: E) -> None: + event.current_buffer.cursor_position += ( + event.current_buffer.document.get_cursor_right_position() + ) + event.app.vi_state.input_mode = InputMode.INSERT + + @handle("A", filter=vi_navigation_mode & ~is_read_only) + def _A(event: E) -> None: + event.current_buffer.cursor_position += ( + event.current_buffer.document.get_end_of_line_position() + ) + event.app.vi_state.input_mode = InputMode.INSERT + + @handle("C", filter=vi_navigation_mode & ~is_read_only) + def _change_until_end_of_line(event: E) -> None: + """ + Change to end of line. + Same as 'c$' (which is implemented elsewhere.) + """ + buffer = event.current_buffer + + deleted = buffer.delete(count=buffer.document.get_end_of_line_position()) + event.app.clipboard.set_text(deleted) + event.app.vi_state.input_mode = InputMode.INSERT + + @handle("c", "c", filter=vi_navigation_mode & ~is_read_only) + @handle("S", filter=vi_navigation_mode & ~is_read_only) + def _change_current_line(event: E) -> None: # TODO: implement 'arg' + """ + Change current line + """ + buffer = event.current_buffer + + # We copy the whole line. + data = ClipboardData(buffer.document.current_line, SelectionType.LINES) + event.app.clipboard.set_data(data) + + # But we delete after the whitespace + buffer.cursor_position += buffer.document.get_start_of_line_position( + after_whitespace=True + ) + buffer.delete(count=buffer.document.get_end_of_line_position()) + event.app.vi_state.input_mode = InputMode.INSERT + + @handle("D", filter=vi_navigation_mode) + def _delete_until_end_of_line(event: E) -> None: + """ + Delete from cursor position until the end of the line. + """ + buffer = event.current_buffer + deleted = buffer.delete(count=buffer.document.get_end_of_line_position()) + event.app.clipboard.set_text(deleted) + + @handle("d", "d", filter=vi_navigation_mode) + def _delete_line(event: E) -> None: + """ + Delete line. (Or the following 'n' lines.) + """ + buffer = event.current_buffer + + # Split string in before/deleted/after text. + lines = buffer.document.lines + + before = "\n".join(lines[: buffer.document.cursor_position_row]) + deleted = "\n".join( + lines[ + buffer.document.cursor_position_row : buffer.document.cursor_position_row + + event.arg + ] + ) + after = "\n".join(lines[buffer.document.cursor_position_row + event.arg :]) + + # Set new text. + if before and after: + before = before + "\n" + + # Set text and cursor position. + buffer.document = Document( + text=before + after, + # Cursor At the start of the first 'after' line, after the leading whitespace. + cursor_position=len(before) + len(after) - len(after.lstrip(" ")), + ) + + # Set clipboard data + event.app.clipboard.set_data(ClipboardData(deleted, SelectionType.LINES)) + + @handle("x", filter=vi_selection_mode) + def _cut(event: E) -> None: + """ + Cut selection. + ('x' is not an operator.) + """ + clipboard_data = event.current_buffer.cut_selection() + event.app.clipboard.set_data(clipboard_data) + + @handle("i", filter=vi_navigation_mode & ~is_read_only) + def _i(event: E) -> None: + event.app.vi_state.input_mode = InputMode.INSERT + + @handle("I", filter=vi_navigation_mode & ~is_read_only) + def _I(event: E) -> None: + event.app.vi_state.input_mode = InputMode.INSERT + event.current_buffer.cursor_position += ( + event.current_buffer.document.get_start_of_line_position( + after_whitespace=True + ) + ) + + @Condition + def in_block_selection() -> bool: + buff = get_app().current_buffer + return bool( + buff.selection_state and buff.selection_state.type == SelectionType.BLOCK + ) + + @handle("I", filter=in_block_selection & ~is_read_only) + def insert_in_block_selection(event: E, after: bool = False) -> None: + """ + Insert in block selection mode. + """ + buff = event.current_buffer + + # Store all cursor positions. + positions = [] + + if after: + + def get_pos(from_to: Tuple[int, int]) -> int: + return from_to[1] + + else: + + def get_pos(from_to: Tuple[int, int]) -> int: + return from_to[0] + + for i, from_to in enumerate(buff.document.selection_ranges()): + positions.append(get_pos(from_to)) + if i == 0: + buff.cursor_position = get_pos(from_to) + + buff.multiple_cursor_positions = positions + + # Go to 'INSERT_MULTIPLE' mode. + event.app.vi_state.input_mode = InputMode.INSERT_MULTIPLE + buff.exit_selection() + + @handle("A", filter=in_block_selection & ~is_read_only) + def _append_after_block(event: E) -> None: + insert_in_block_selection(event, after=True) + + @handle("J", filter=vi_navigation_mode & ~is_read_only) + def _join(event: E) -> None: + """ + Join lines. + """ + for i in range(event.arg): + event.current_buffer.join_next_line() + + @handle("g", "J", filter=vi_navigation_mode & ~is_read_only) + def _join_nospace(event: E) -> None: + """ + Join lines without space. + """ + for i in range(event.arg): + event.current_buffer.join_next_line(separator="") + + @handle("J", filter=vi_selection_mode & ~is_read_only) + def _join_selection(event: E) -> None: + """ + Join selected lines. + """ + event.current_buffer.join_selected_lines() + + @handle("g", "J", filter=vi_selection_mode & ~is_read_only) + def _join_selection_nospace(event: E) -> None: + """ + Join selected lines without space. + """ + event.current_buffer.join_selected_lines(separator="") + + @handle("p", filter=vi_navigation_mode) + def _paste(event: E) -> None: + """ + Paste after + """ + event.current_buffer.paste_clipboard_data( + event.app.clipboard.get_data(), + count=event.arg, + paste_mode=PasteMode.VI_AFTER, + ) + + @handle("P", filter=vi_navigation_mode) + def _paste_before(event: E) -> None: + """ + Paste before + """ + event.current_buffer.paste_clipboard_data( + event.app.clipboard.get_data(), + count=event.arg, + paste_mode=PasteMode.VI_BEFORE, + ) + + @handle('"', Keys.Any, "p", filter=vi_navigation_mode) + def _paste_register(event: E) -> None: + """ + Paste from named register. + """ + c = event.key_sequence[1].data + if c in vi_register_names: + data = event.app.vi_state.named_registers.get(c) + if data: + event.current_buffer.paste_clipboard_data( + data, count=event.arg, paste_mode=PasteMode.VI_AFTER + ) + + @handle('"', Keys.Any, "P", filter=vi_navigation_mode) + def _paste_register_before(event: E) -> None: + """ + Paste (before) from named register. + """ + c = event.key_sequence[1].data + if c in vi_register_names: + data = event.app.vi_state.named_registers.get(c) + if data: + event.current_buffer.paste_clipboard_data( + data, count=event.arg, paste_mode=PasteMode.VI_BEFORE + ) + + @handle("r", filter=vi_navigation_mode) + def _replace(event: E) -> None: + """ + Go to 'replace-single'-mode. + """ + event.app.vi_state.input_mode = InputMode.REPLACE_SINGLE + + @handle("R", filter=vi_navigation_mode) + def _replace_mode(event: E) -> None: + """ + Go to 'replace'-mode. + """ + event.app.vi_state.input_mode = InputMode.REPLACE + + @handle("s", filter=vi_navigation_mode & ~is_read_only) + def _substitute(event: E) -> None: + """ + Substitute with new text + (Delete character(s) and go to insert mode.) + """ + text = event.current_buffer.delete(count=event.arg) + event.app.clipboard.set_text(text) + event.app.vi_state.input_mode = InputMode.INSERT + + @handle("u", filter=vi_navigation_mode, save_before=(lambda e: False)) + def _undo(event: E) -> None: + for i in range(event.arg): + event.current_buffer.undo() + + @handle("V", filter=vi_navigation_mode) + def _visual_line(event: E) -> None: + """ + Start lines selection. + """ + event.current_buffer.start_selection(selection_type=SelectionType.LINES) + + @handle("c-v", filter=vi_navigation_mode) + def _visual_block(event: E) -> None: + """ + Enter block selection mode. + """ + event.current_buffer.start_selection(selection_type=SelectionType.BLOCK) + + @handle("V", filter=vi_selection_mode) + def _visual_line2(event: E) -> None: + """ + Exit line selection mode, or go from non line selection mode to line + selection mode. + """ + selection_state = event.current_buffer.selection_state + + if selection_state is not None: + if selection_state.type != SelectionType.LINES: + selection_state.type = SelectionType.LINES + else: + event.current_buffer.exit_selection() + + @handle("v", filter=vi_navigation_mode) + def _visual(event: E) -> None: + """ + Enter character selection mode. + """ + event.current_buffer.start_selection(selection_type=SelectionType.CHARACTERS) + + @handle("v", filter=vi_selection_mode) + def _visual2(event: E) -> None: + """ + Exit character selection mode, or go from non-character-selection mode + to character selection mode. + """ + selection_state = event.current_buffer.selection_state + + if selection_state is not None: + if selection_state.type != SelectionType.CHARACTERS: + selection_state.type = SelectionType.CHARACTERS + else: + event.current_buffer.exit_selection() + + @handle("c-v", filter=vi_selection_mode) + def _visual_block2(event: E) -> None: + """ + Exit block selection mode, or go from non block selection mode to block + selection mode. + """ + selection_state = event.current_buffer.selection_state + + if selection_state is not None: + if selection_state.type != SelectionType.BLOCK: + selection_state.type = SelectionType.BLOCK + else: + event.current_buffer.exit_selection() + + @handle("a", "w", filter=vi_selection_mode) + @handle("a", "W", filter=vi_selection_mode) + def _visual_auto_word(event: E) -> None: + """ + Switch from visual linewise mode to visual characterwise mode. + """ + buffer = event.current_buffer + + if ( + buffer.selection_state + and buffer.selection_state.type == SelectionType.LINES + ): + buffer.selection_state.type = SelectionType.CHARACTERS + + @handle("x", filter=vi_navigation_mode) + def _delete(event: E) -> None: + """ + Delete character. + """ + buff = event.current_buffer + count = min(event.arg, len(buff.document.current_line_after_cursor)) + if count: + text = event.current_buffer.delete(count=count) + event.app.clipboard.set_text(text) + + @handle("X", filter=vi_navigation_mode) + def _delete_before_cursor(event: E) -> None: + buff = event.current_buffer + count = min(event.arg, len(buff.document.current_line_before_cursor)) + if count: + text = event.current_buffer.delete_before_cursor(count=count) + event.app.clipboard.set_text(text) + + @handle("y", "y", filter=vi_navigation_mode) + @handle("Y", filter=vi_navigation_mode) + def _yank_line(event: E) -> None: + """ + Yank the whole line. + """ + text = "\n".join(event.current_buffer.document.lines_from_current[: event.arg]) + event.app.clipboard.set_data(ClipboardData(text, SelectionType.LINES)) + + @handle("+", filter=vi_navigation_mode) + def _next_line(event: E) -> None: + """ + Move to first non whitespace of next line + """ + buffer = event.current_buffer + buffer.cursor_position += buffer.document.get_cursor_down_position( + count=event.arg + ) + buffer.cursor_position += buffer.document.get_start_of_line_position( + after_whitespace=True + ) + + @handle("-", filter=vi_navigation_mode) + def _prev_line(event: E) -> None: + """ + Move to first non whitespace of previous line + """ + buffer = event.current_buffer + buffer.cursor_position += buffer.document.get_cursor_up_position( + count=event.arg + ) + buffer.cursor_position += buffer.document.get_start_of_line_position( + after_whitespace=True + ) + + @handle(">", ">", filter=vi_navigation_mode) + def _indent(event: E) -> None: + """ + Indent lines. + """ + buffer = event.current_buffer + current_row = buffer.document.cursor_position_row + indent(buffer, current_row, current_row + event.arg) + + @handle("<", "<", filter=vi_navigation_mode) + def _unindent(event: E) -> None: + """ + Unindent lines. + """ + current_row = event.current_buffer.document.cursor_position_row + unindent(event.current_buffer, current_row, current_row + event.arg) + + @handle("O", filter=vi_navigation_mode & ~is_read_only) + def _open_above(event: E) -> None: + """ + Open line above and enter insertion mode + """ + event.current_buffer.insert_line_above(copy_margin=not in_paste_mode()) + event.app.vi_state.input_mode = InputMode.INSERT + + @handle("o", filter=vi_navigation_mode & ~is_read_only) + def _open_below(event: E) -> None: + """ + Open line below and enter insertion mode + """ + event.current_buffer.insert_line_below(copy_margin=not in_paste_mode()) + event.app.vi_state.input_mode = InputMode.INSERT + + @handle("~", filter=vi_navigation_mode) + def _reverse_case(event: E) -> None: + """ + Reverse case of current character and move cursor forward. + """ + buffer = event.current_buffer + c = buffer.document.current_char + + if c is not None and c != "\n": + buffer.insert_text(c.swapcase(), overwrite=True) + + @handle("g", "u", "u", filter=vi_navigation_mode & ~is_read_only) + def _lowercase_line(event: E) -> None: + """ + Lowercase current line. + """ + buff = event.current_buffer + buff.transform_current_line(lambda s: s.lower()) + + @handle("g", "U", "U", filter=vi_navigation_mode & ~is_read_only) + def _uppercase_line(event: E) -> None: + """ + Uppercase current line. + """ + buff = event.current_buffer + buff.transform_current_line(lambda s: s.upper()) + + @handle("g", "~", "~", filter=vi_navigation_mode & ~is_read_only) + def _swapcase_line(event: E) -> None: + """ + Swap case of the current line. + """ + buff = event.current_buffer + buff.transform_current_line(lambda s: s.swapcase()) + + @handle("#", filter=vi_navigation_mode) + def _prev_occurence(event: E) -> None: + """ + Go to previous occurrence of this word. + """ + b = event.current_buffer + search_state = event.app.current_search_state + + search_state.text = b.document.get_word_under_cursor() + search_state.direction = SearchDirection.BACKWARD + + b.apply_search(search_state, count=event.arg, include_current_position=False) + + @handle("*", filter=vi_navigation_mode) + def _next_occurance(event: E) -> None: + """ + Go to next occurrence of this word. + """ + b = event.current_buffer + search_state = event.app.current_search_state + + search_state.text = b.document.get_word_under_cursor() + search_state.direction = SearchDirection.FORWARD + + b.apply_search(search_state, count=event.arg, include_current_position=False) + + @handle("(", filter=vi_navigation_mode) + def _begin_of_sentence(event: E) -> None: + # TODO: go to begin of sentence. + # XXX: should become text_object. + pass + + @handle(")", filter=vi_navigation_mode) + def _end_of_sentence(event: E) -> None: + # TODO: go to end of sentence. + # XXX: should become text_object. + pass + + operator = create_operator_decorator(key_bindings) + text_object = create_text_object_decorator(key_bindings) + + @handle(Keys.Any, filter=vi_waiting_for_text_object_mode) + def _unknown_text_object(event: E) -> None: + """ + Unknown key binding while waiting for a text object. + """ + event.app.output.bell() + + # + # *** Operators *** + # + + def create_delete_and_change_operators( + delete_only: bool, with_register: bool = False + ) -> None: + """ + Delete and change operators. + + :param delete_only: Create an operator that deletes, but doesn't go to insert mode. + :param with_register: Copy the deleted text to this named register instead of the clipboard. + """ + handler_keys: Iterable[str] + if with_register: + handler_keys = ('"', Keys.Any, "cd"[delete_only]) + else: + handler_keys = "cd"[delete_only] + + @operator(*handler_keys, filter=~is_read_only) + def delete_or_change_operator(event: E, text_object: TextObject) -> None: + clipboard_data = None + buff = event.current_buffer + + if text_object: + new_document, clipboard_data = text_object.cut(buff) + buff.document = new_document + + # Set deleted/changed text to clipboard or named register. + if clipboard_data and clipboard_data.text: + if with_register: + reg_name = event.key_sequence[1].data + if reg_name in vi_register_names: + event.app.vi_state.named_registers[reg_name] = clipboard_data + else: + event.app.clipboard.set_data(clipboard_data) + + # Only go back to insert mode in case of 'change'. + if not delete_only: + event.app.vi_state.input_mode = InputMode.INSERT + + create_delete_and_change_operators(False, False) + create_delete_and_change_operators(False, True) + create_delete_and_change_operators(True, False) + create_delete_and_change_operators(True, True) + + def create_transform_handler( + filter: Filter, transform_func: Callable[[str], str], *a: str + ) -> None: + @operator(*a, filter=filter & ~is_read_only) + def _(event: E, text_object: TextObject) -> None: + """ + Apply transformation (uppercase, lowercase, rot13, swap case). + """ + buff = event.current_buffer + start, end = text_object.operator_range(buff.document) + + if start < end: + # Transform. + buff.transform_region( + buff.cursor_position + start, + buff.cursor_position + end, + transform_func, + ) + + # Move cursor + buff.cursor_position += text_object.end or text_object.start + + for k, f, func in vi_transform_functions: + create_transform_handler(f, func, *k) + + @operator("y") + def _yank(event: E, text_object: TextObject) -> None: + """ + Yank operator. (Copy text.) + """ + _, clipboard_data = text_object.cut(event.current_buffer) + if clipboard_data.text: + event.app.clipboard.set_data(clipboard_data) + + @operator('"', Keys.Any, "y") + def _yank_to_register(event: E, text_object: TextObject) -> None: + """ + Yank selection to named register. + """ + c = event.key_sequence[1].data + if c in vi_register_names: + _, clipboard_data = text_object.cut(event.current_buffer) + event.app.vi_state.named_registers[c] = clipboard_data + + @operator(">") + def _indent_text_object(event: E, text_object: TextObject) -> None: + """ + Indent. + """ + buff = event.current_buffer + from_, to = text_object.get_line_numbers(buff) + indent(buff, from_, to + 1, count=event.arg) + + @operator("<") + def _unindent_text_object(event: E, text_object: TextObject) -> None: + """ + Unindent. + """ + buff = event.current_buffer + from_, to = text_object.get_line_numbers(buff) + unindent(buff, from_, to + 1, count=event.arg) + + @operator("g", "q") + def _reshape(event: E, text_object: TextObject) -> None: + """ + Reshape text. + """ + buff = event.current_buffer + from_, to = text_object.get_line_numbers(buff) + reshape_text(buff, from_, to) + + # + # *** Text objects *** + # + + @text_object("b") + def _b(event: E) -> TextObject: + """ + Move one word or token left. + """ + return TextObject( + event.current_buffer.document.find_start_of_previous_word(count=event.arg) + or 0 + ) + + @text_object("B") + def _B(event: E) -> TextObject: + """ + Move one non-blank word left + """ + return TextObject( + event.current_buffer.document.find_start_of_previous_word( + count=event.arg, WORD=True + ) + or 0 + ) + + @text_object("$") + def _dollar(event: E) -> TextObject: + """ + 'c$', 'd$' and '$': Delete/change/move until end of line. + """ + return TextObject(event.current_buffer.document.get_end_of_line_position()) + + @text_object("w") + def _word_forward(event: E) -> TextObject: + """ + 'word' forward. 'cw', 'dw', 'w': Delete/change/move one word. + """ + return TextObject( + event.current_buffer.document.find_next_word_beginning(count=event.arg) + or event.current_buffer.document.get_end_of_document_position() + ) + + @text_object("W") + def _WORD_forward(event: E) -> TextObject: + """ + 'WORD' forward. 'cW', 'dW', 'W': Delete/change/move one WORD. + """ + return TextObject( + event.current_buffer.document.find_next_word_beginning( + count=event.arg, WORD=True + ) + or event.current_buffer.document.get_end_of_document_position() + ) + + @text_object("e") + def _end_of_word(event: E) -> TextObject: + """ + End of 'word': 'ce', 'de', 'e' + """ + end = event.current_buffer.document.find_next_word_ending(count=event.arg) + return TextObject(end - 1 if end else 0, type=TextObjectType.INCLUSIVE) + + @text_object("E") + def _end_of_WORD(event: E) -> TextObject: + """ + End of 'WORD': 'cE', 'dE', 'E' + """ + end = event.current_buffer.document.find_next_word_ending( + count=event.arg, WORD=True + ) + return TextObject(end - 1 if end else 0, type=TextObjectType.INCLUSIVE) + + @text_object("i", "w", no_move_handler=True) + def _inner_word(event: E) -> TextObject: + """ + Inner 'word': ciw and diw + """ + start, end = event.current_buffer.document.find_boundaries_of_current_word() + return TextObject(start, end) + + @text_object("a", "w", no_move_handler=True) + def _a_word(event: E) -> TextObject: + """ + A 'word': caw and daw + """ + start, end = event.current_buffer.document.find_boundaries_of_current_word( + include_trailing_whitespace=True + ) + return TextObject(start, end) + + @text_object("i", "W", no_move_handler=True) + def _inner_WORD(event: E) -> TextObject: + """ + Inner 'WORD': ciW and diW + """ + start, end = event.current_buffer.document.find_boundaries_of_current_word( + WORD=True + ) + return TextObject(start, end) + + @text_object("a", "W", no_move_handler=True) + def _a_WORD(event: E) -> TextObject: + """ + A 'WORD': caw and daw + """ + start, end = event.current_buffer.document.find_boundaries_of_current_word( + WORD=True, include_trailing_whitespace=True + ) + return TextObject(start, end) + + @text_object("a", "p", no_move_handler=True) + def _paragraph(event: E) -> TextObject: + """ + Auto paragraph. + """ + start = event.current_buffer.document.start_of_paragraph() + end = event.current_buffer.document.end_of_paragraph(count=event.arg) + return TextObject(start, end) + + @text_object("^") + def _start_of_line(event: E) -> TextObject: + """'c^', 'd^' and '^': Soft start of line, after whitespace.""" + return TextObject( + event.current_buffer.document.get_start_of_line_position( + after_whitespace=True + ) + ) + + @text_object("0") + def _hard_start_of_line(event: E) -> TextObject: + """ + 'c0', 'd0': Hard start of line, before whitespace. + (The move '0' key is implemented elsewhere, because a '0' could also change the `arg`.) + """ + return TextObject( + event.current_buffer.document.get_start_of_line_position( + after_whitespace=False + ) + ) + + def create_ci_ca_handles( + ci_start: str, ci_end: str, inner: bool, key: Optional[str] = None + ) -> None: + # TODO: 'dat', 'dit', (tags (like xml) + """ + Delete/Change string between this start and stop character. But keep these characters. + This implements all the ci", ci<, ci{, ci(, di", di<, ca", ca<, ... combinations. + """ + + def handler(event: E) -> TextObject: + if ci_start == ci_end: + # Quotes + start = event.current_buffer.document.find_backwards( + ci_start, in_current_line=False + ) + end = event.current_buffer.document.find(ci_end, in_current_line=False) + else: + # Brackets + start = event.current_buffer.document.find_enclosing_bracket_left( + ci_start, ci_end + ) + end = event.current_buffer.document.find_enclosing_bracket_right( + ci_start, ci_end + ) + + if start is not None and end is not None: + offset = 0 if inner else 1 + return TextObject(start + 1 - offset, end + offset) + else: + # Nothing found. + return TextObject(0) + + if key is None: + text_object("ai"[inner], ci_start, no_move_handler=True)(handler) + text_object("ai"[inner], ci_end, no_move_handler=True)(handler) + else: + text_object("ai"[inner], key, no_move_handler=True)(handler) + + for inner in (False, True): + for ci_start, ci_end in [ + ('"', '"'), + ("'", "'"), + ("`", "`"), + ("[", "]"), + ("<", ">"), + ("{", "}"), + ("(", ")"), + ]: + create_ci_ca_handles(ci_start, ci_end, inner) + + create_ci_ca_handles("(", ")", inner, "b") # 'dab', 'dib' + create_ci_ca_handles("{", "}", inner, "B") # 'daB', 'diB' + + @text_object("{") + def _previous_section(event: E) -> TextObject: + """ + Move to previous blank-line separated section. + Implements '{', 'c{', 'd{', 'y{' + """ + index = event.current_buffer.document.start_of_paragraph( + count=event.arg, before=True + ) + return TextObject(index) + + @text_object("}") + def _next_section(event: E) -> TextObject: + """ + Move to next blank-line separated section. + Implements '}', 'c}', 'd}', 'y}' + """ + index = event.current_buffer.document.end_of_paragraph( + count=event.arg, after=True + ) + return TextObject(index) + + @text_object("f", Keys.Any) + def _next_occurence(event: E) -> TextObject: + """ + Go to next occurrence of character. Typing 'fx' will move the + cursor to the next occurrence of character. 'x'. + """ + event.app.vi_state.last_character_find = CharacterFind(event.data, False) + match = event.current_buffer.document.find( + event.data, in_current_line=True, count=event.arg + ) + if match: + return TextObject(match, type=TextObjectType.INCLUSIVE) + else: + return TextObject(0) + + @text_object("F", Keys.Any) + def _previous_occurance(event: E) -> TextObject: + """ + Go to previous occurrence of character. Typing 'Fx' will move the + cursor to the previous occurrence of character. 'x'. + """ + event.app.vi_state.last_character_find = CharacterFind(event.data, True) + return TextObject( + event.current_buffer.document.find_backwards( + event.data, in_current_line=True, count=event.arg + ) + or 0 + ) + + @text_object("t", Keys.Any) + def _t(event: E) -> TextObject: + """ + Move right to the next occurrence of c, then one char backward. + """ + event.app.vi_state.last_character_find = CharacterFind(event.data, False) + match = event.current_buffer.document.find( + event.data, in_current_line=True, count=event.arg + ) + if match: + return TextObject(match - 1, type=TextObjectType.INCLUSIVE) + else: + return TextObject(0) + + @text_object("T", Keys.Any) + def _T(event: E) -> TextObject: + """ + Move left to the previous occurrence of c, then one char forward. + """ + event.app.vi_state.last_character_find = CharacterFind(event.data, True) + match = event.current_buffer.document.find_backwards( + event.data, in_current_line=True, count=event.arg + ) + return TextObject(match + 1 if match else 0) + + def repeat(reverse: bool) -> None: + """ + Create ',' and ';' commands. + """ + + @text_object("," if reverse else ";") + def _(event: E) -> TextObject: + """ + Repeat the last 'f'/'F'/'t'/'T' command. + """ + pos: Optional[int] = 0 + vi_state = event.app.vi_state + + type = TextObjectType.EXCLUSIVE + + if vi_state.last_character_find: + char = vi_state.last_character_find.character + backwards = vi_state.last_character_find.backwards + + if reverse: + backwards = not backwards + + if backwards: + pos = event.current_buffer.document.find_backwards( + char, in_current_line=True, count=event.arg + ) + else: + pos = event.current_buffer.document.find( + char, in_current_line=True, count=event.arg + ) + type = TextObjectType.INCLUSIVE + if pos: + return TextObject(pos, type=type) + else: + return TextObject(0) + + repeat(True) + repeat(False) + + @text_object("h") + @text_object("left") + def _left(event: E) -> TextObject: + """ + Implements 'ch', 'dh', 'h': Cursor left. + """ + return TextObject( + event.current_buffer.document.get_cursor_left_position(count=event.arg) + ) + + @text_object("j", no_move_handler=True, no_selection_handler=True) + # Note: We also need `no_selection_handler`, because we in + # selection mode, we prefer the other 'j' binding that keeps + # `buffer.preferred_column`. + def _down(event: E) -> TextObject: + """ + Implements 'cj', 'dj', 'j', ... Cursor up. + """ + return TextObject( + event.current_buffer.document.get_cursor_down_position(count=event.arg), + type=TextObjectType.LINEWISE, + ) + + @text_object("k", no_move_handler=True, no_selection_handler=True) + def _up(event: E) -> TextObject: + """ + Implements 'ck', 'dk', 'k', ... Cursor up. + """ + return TextObject( + event.current_buffer.document.get_cursor_up_position(count=event.arg), + type=TextObjectType.LINEWISE, + ) + + @text_object("l") + @text_object(" ") + @text_object("right") + def _right(event: E) -> TextObject: + """ + Implements 'cl', 'dl', 'l', 'c ', 'd ', ' '. Cursor right. + """ + return TextObject( + event.current_buffer.document.get_cursor_right_position(count=event.arg) + ) + + @text_object("H") + def _top_of_screen(event: E) -> TextObject: + """ + Moves to the start of the visible region. (Below the scroll offset.) + Implements 'cH', 'dH', 'H'. + """ + w = event.app.layout.current_window + b = event.current_buffer + + if w and w.render_info: + # When we find a Window that has BufferControl showing this window, + # move to the start of the visible area. + pos = ( + b.document.translate_row_col_to_index( + w.render_info.first_visible_line(after_scroll_offset=True), 0 + ) + - b.cursor_position + ) + + else: + # Otherwise, move to the start of the input. + pos = -len(b.document.text_before_cursor) + return TextObject(pos, type=TextObjectType.LINEWISE) + + @text_object("M") + def _middle_of_screen(event: E) -> TextObject: + """ + Moves cursor to the vertical center of the visible region. + Implements 'cM', 'dM', 'M'. + """ + w = event.app.layout.current_window + b = event.current_buffer + + if w and w.render_info: + # When we find a Window that has BufferControl showing this window, + # move to the center of the visible area. + pos = ( + b.document.translate_row_col_to_index( + w.render_info.center_visible_line(), 0 + ) + - b.cursor_position + ) + + else: + # Otherwise, move to the start of the input. + pos = -len(b.document.text_before_cursor) + return TextObject(pos, type=TextObjectType.LINEWISE) + + @text_object("L") + def _end_of_screen(event: E) -> TextObject: + """ + Moves to the end of the visible region. (Above the scroll offset.) + """ + w = event.app.layout.current_window + b = event.current_buffer + + if w and w.render_info: + # When we find a Window that has BufferControl showing this window, + # move to the end of the visible area. + pos = ( + b.document.translate_row_col_to_index( + w.render_info.last_visible_line(before_scroll_offset=True), 0 + ) + - b.cursor_position + ) + + else: + # Otherwise, move to the end of the input. + pos = len(b.document.text_after_cursor) + return TextObject(pos, type=TextObjectType.LINEWISE) + + @text_object("n", no_move_handler=True) + def _search_next(event: E) -> TextObject: + """ + Search next. + """ + buff = event.current_buffer + search_state = event.app.current_search_state + + cursor_position = buff.get_search_position( + search_state, include_current_position=False, count=event.arg + ) + return TextObject(cursor_position - buff.cursor_position) + + @handle("n", filter=vi_navigation_mode) + def _search_next2(event: E) -> None: + """ + Search next in navigation mode. (This goes through the history.) + """ + search_state = event.app.current_search_state + + event.current_buffer.apply_search( + search_state, include_current_position=False, count=event.arg + ) + + @text_object("N", no_move_handler=True) + def _search_previous(event: E) -> TextObject: + """ + Search previous. + """ + buff = event.current_buffer + search_state = event.app.current_search_state + + cursor_position = buff.get_search_position( + ~search_state, include_current_position=False, count=event.arg + ) + return TextObject(cursor_position - buff.cursor_position) + + @handle("N", filter=vi_navigation_mode) + def _search_previous2(event: E) -> None: + """ + Search previous in navigation mode. (This goes through the history.) + """ + search_state = event.app.current_search_state + + event.current_buffer.apply_search( + ~search_state, include_current_position=False, count=event.arg + ) + + @handle("z", "+", filter=vi_navigation_mode | vi_selection_mode) + @handle("z", "t", filter=vi_navigation_mode | vi_selection_mode) + @handle("z", "enter", filter=vi_navigation_mode | vi_selection_mode) + def _scroll_top(event: E) -> None: + """ + Scrolls the window to makes the current line the first line in the visible region. + """ + b = event.current_buffer + event.app.layout.current_window.vertical_scroll = b.document.cursor_position_row + + @handle("z", "-", filter=vi_navigation_mode | vi_selection_mode) + @handle("z", "b", filter=vi_navigation_mode | vi_selection_mode) + def _scroll_bottom(event: E) -> None: + """ + Scrolls the window to makes the current line the last line in the visible region. + """ + # We can safely set the scroll offset to zero; the Window will make + # sure that it scrolls at least enough to make the cursor visible + # again. + event.app.layout.current_window.vertical_scroll = 0 + + @handle("z", "z", filter=vi_navigation_mode | vi_selection_mode) + def _scroll_center(event: E) -> None: + """ + Center Window vertically around cursor. + """ + w = event.app.layout.current_window + b = event.current_buffer + + if w and w.render_info: + info = w.render_info + + # Calculate the offset that we need in order to position the row + # containing the cursor in the center. + scroll_height = info.window_height // 2 + + y = max(0, b.document.cursor_position_row - 1) + height = 0 + while y > 0: + line_height = info.get_height_for_line(y) + + if height + line_height < scroll_height: + height += line_height + y -= 1 + else: + break + + w.vertical_scroll = y + + @text_object("%") + def _goto_corresponding_bracket(event: E) -> TextObject: + """ + Implements 'c%', 'd%', '%, 'y%' (Move to corresponding bracket.) + If an 'arg' has been given, go this this % position in the file. + """ + buffer = event.current_buffer + + if event._arg: + # If 'arg' has been given, the meaning of % is to go to the 'x%' + # row in the file. + if 0 < event.arg <= 100: + absolute_index = buffer.document.translate_row_col_to_index( + int((event.arg * buffer.document.line_count - 1) / 100), 0 + ) + return TextObject( + absolute_index - buffer.document.cursor_position, + type=TextObjectType.LINEWISE, + ) + else: + return TextObject(0) # Do nothing. + + else: + # Move to the corresponding opening/closing bracket (()'s, []'s and {}'s). + match = buffer.document.find_matching_bracket_position() + if match: + return TextObject(match, type=TextObjectType.INCLUSIVE) + else: + return TextObject(0) + + @text_object("|") + def _to_column(event: E) -> TextObject: + """ + Move to the n-th column (you may specify the argument n by typing it on + number keys, for example, 20|). + """ + return TextObject( + event.current_buffer.document.get_column_cursor_position(event.arg - 1) + ) + + @text_object("g", "g") + def _goto_first_line(event: E) -> TextObject: + """ + Go to the start of the very first line. + Implements 'gg', 'cgg', 'ygg' + """ + d = event.current_buffer.document + + if event._arg: + # Move to the given line. + return TextObject( + d.translate_row_col_to_index(event.arg - 1, 0) - d.cursor_position, + type=TextObjectType.LINEWISE, + ) + else: + # Move to the top of the input. + return TextObject( + d.get_start_of_document_position(), type=TextObjectType.LINEWISE + ) + + @text_object("g", "_") + def _goto_last_line(event: E) -> TextObject: + """ + Go to last non-blank of line. + 'g_', 'cg_', 'yg_', etc.. + """ + return TextObject( + event.current_buffer.document.last_non_blank_of_current_line_position(), + type=TextObjectType.INCLUSIVE, + ) + + @text_object("g", "e") + def _ge(event: E) -> TextObject: + """ + Go to last character of previous word. + 'ge', 'cge', 'yge', etc.. + """ + prev_end = event.current_buffer.document.find_previous_word_ending( + count=event.arg + ) + return TextObject( + prev_end - 1 if prev_end is not None else 0, type=TextObjectType.INCLUSIVE + ) + + @text_object("g", "E") + def _gE(event: E) -> TextObject: + """ + Go to last character of previous WORD. + 'gE', 'cgE', 'ygE', etc.. + """ + prev_end = event.current_buffer.document.find_previous_word_ending( + count=event.arg, WORD=True + ) + return TextObject( + prev_end - 1 if prev_end is not None else 0, type=TextObjectType.INCLUSIVE + ) + + @text_object("g", "m") + def _gm(event: E) -> TextObject: + """ + Like g0, but half a screenwidth to the right. (Or as much as possible.) + """ + w = event.app.layout.current_window + buff = event.current_buffer + + if w and w.render_info: + width = w.render_info.window_width + start = buff.document.get_start_of_line_position(after_whitespace=False) + start += int(min(width / 2, len(buff.document.current_line))) + + return TextObject(start, type=TextObjectType.INCLUSIVE) + return TextObject(0) + + @text_object("G") + def _last_line(event: E) -> TextObject: + """ + Go to the end of the document. (If no arg has been given.) + """ + buf = event.current_buffer + return TextObject( + buf.document.translate_row_col_to_index(buf.document.line_count - 1, 0) + - buf.cursor_position, + type=TextObjectType.LINEWISE, + ) + + # + # *** Other *** + # + + @handle("G", filter=has_arg) + def _to_nth_history_line(event: E) -> None: + """ + If an argument is given, move to this line in the history. (for + example, 15G) + """ + event.current_buffer.go_to_history(event.arg - 1) + + for n in "123456789": + + @handle( + n, + filter=vi_navigation_mode + | vi_selection_mode + | vi_waiting_for_text_object_mode, + ) + def _arg(event: E) -> None: + """ + Always handle numberics in navigation mode as arg. + """ + event.append_to_arg_count(event.data) + + @handle( + "0", + filter=( + vi_navigation_mode | vi_selection_mode | vi_waiting_for_text_object_mode + ) + & has_arg, + ) + def _0_arg(event: E) -> None: + """ + Zero when an argument was already give. + """ + event.append_to_arg_count(event.data) + + @handle(Keys.Any, filter=vi_replace_mode) + def _insert_text(event: E) -> None: + """ + Insert data at cursor position. + """ + event.current_buffer.insert_text(event.data, overwrite=True) + + @handle(Keys.Any, filter=vi_replace_single_mode) + def _replace_single(event: E) -> None: + """ + Replace single character at cursor position. + """ + event.current_buffer.insert_text(event.data, overwrite=True) + event.current_buffer.cursor_position -= 1 + event.app.vi_state.input_mode = InputMode.NAVIGATION + + @handle( + Keys.Any, + filter=vi_insert_multiple_mode, + save_before=(lambda e: not e.is_repeat), + ) + def _insert_text_multiple_cursors(event: E) -> None: + """ + Insert data at multiple cursor positions at once. + (Usually a result of pressing 'I' or 'A' in block-selection mode.) + """ + buff = event.current_buffer + original_text = buff.text + + # Construct new text. + text = [] + p = 0 + + for p2 in buff.multiple_cursor_positions: + text.append(original_text[p:p2]) + text.append(event.data) + p = p2 + + text.append(original_text[p:]) + + # Shift all cursor positions. + new_cursor_positions = [ + pos + i + 1 for i, pos in enumerate(buff.multiple_cursor_positions) + ] + + # Set result. + buff.text = "".join(text) + buff.multiple_cursor_positions = new_cursor_positions + buff.cursor_position += 1 + + @handle("backspace", filter=vi_insert_multiple_mode) + def _delete_before_multiple_cursors(event: E) -> None: + """ + Backspace, using multiple cursors. + """ + buff = event.current_buffer + original_text = buff.text + + # Construct new text. + deleted_something = False + text = [] + p = 0 + + for p2 in buff.multiple_cursor_positions: + if p2 > 0 and original_text[p2 - 1] != "\n": # Don't delete across lines. + text.append(original_text[p : p2 - 1]) + deleted_something = True + else: + text.append(original_text[p:p2]) + p = p2 + + text.append(original_text[p:]) + + if deleted_something: + # Shift all cursor positions. + lengths = [len(part) for part in text[:-1]] + new_cursor_positions = list(accumulate(lengths)) + + # Set result. + buff.text = "".join(text) + buff.multiple_cursor_positions = new_cursor_positions + buff.cursor_position -= 1 + else: + event.app.output.bell() + + @handle("delete", filter=vi_insert_multiple_mode) + def _delete_after_multiple_cursors(event: E) -> None: + """ + Delete, using multiple cursors. + """ + buff = event.current_buffer + original_text = buff.text + + # Construct new text. + deleted_something = False + text = [] + new_cursor_positions = [] + p = 0 + + for p2 in buff.multiple_cursor_positions: + text.append(original_text[p:p2]) + if p2 >= len(original_text) or original_text[p2] == "\n": + # Don't delete across lines. + p = p2 + else: + p = p2 + 1 + deleted_something = True + + text.append(original_text[p:]) + + if deleted_something: + # Shift all cursor positions. + lengths = [len(part) for part in text[:-1]] + new_cursor_positions = list(accumulate(lengths)) + + # Set result. + buff.text = "".join(text) + buff.multiple_cursor_positions = new_cursor_positions + else: + event.app.output.bell() + + @handle("left", filter=vi_insert_multiple_mode) + def _left_multiple(event: E) -> None: + """ + Move all cursors to the left. + (But keep all cursors on the same line.) + """ + buff = event.current_buffer + new_positions = [] + + for p in buff.multiple_cursor_positions: + if buff.document.translate_index_to_position(p)[1] > 0: + p -= 1 + new_positions.append(p) + + buff.multiple_cursor_positions = new_positions + + if buff.document.cursor_position_col > 0: + buff.cursor_position -= 1 + + @handle("right", filter=vi_insert_multiple_mode) + def _right_multiple(event: E) -> None: + """ + Move all cursors to the right. + (But keep all cursors on the same line.) + """ + buff = event.current_buffer + new_positions = [] + + for p in buff.multiple_cursor_positions: + row, column = buff.document.translate_index_to_position(p) + if column < len(buff.document.lines[row]): + p += 1 + new_positions.append(p) + + buff.multiple_cursor_positions = new_positions + + if not buff.document.is_cursor_at_the_end_of_line: + buff.cursor_position += 1 + + @handle("up", filter=vi_insert_multiple_mode) + @handle("down", filter=vi_insert_multiple_mode) + def _updown_multiple(event: E) -> None: + """ + Ignore all up/down key presses when in multiple cursor mode. + """ + + @handle("c-x", "c-l", filter=vi_insert_mode) + def _complete_line(event: E) -> None: + """ + Pressing the ControlX - ControlL sequence in Vi mode does line + completion based on the other lines in the document and the history. + """ + event.current_buffer.start_history_lines_completion() + + @handle("c-x", "c-f", filter=vi_insert_mode) + def _complete_filename(event: E) -> None: + """ + Complete file names. + """ + # TODO + pass + + @handle("c-k", filter=vi_insert_mode | vi_replace_mode) + def _digraph(event: E) -> None: + """ + Go into digraph mode. + """ + event.app.vi_state.waiting_for_digraph = True + + @Condition + def digraph_symbol_1_given() -> bool: + return get_app().vi_state.digraph_symbol1 is not None + + @handle(Keys.Any, filter=vi_digraph_mode & ~digraph_symbol_1_given) + def _digraph1(event: E) -> None: + """ + First digraph symbol. + """ + event.app.vi_state.digraph_symbol1 = event.data + + @handle(Keys.Any, filter=vi_digraph_mode & digraph_symbol_1_given) + def _create_digraph(event: E) -> None: + """ + Insert digraph. + """ + try: + # Lookup. + code: Tuple[str, str] = ( + event.app.vi_state.digraph_symbol1 or "", + event.data, + ) + if code not in DIGRAPHS: + code = code[::-1] # Try reversing. + symbol = DIGRAPHS[code] + except KeyError: + # Unknown digraph. + event.app.output.bell() + else: + # Insert digraph. + overwrite = event.app.vi_state.input_mode == InputMode.REPLACE + event.current_buffer.insert_text(chr(symbol), overwrite=overwrite) + event.app.vi_state.waiting_for_digraph = False + finally: + event.app.vi_state.waiting_for_digraph = False + event.app.vi_state.digraph_symbol1 = None + + @handle("c-o", filter=vi_insert_mode | vi_replace_mode) + def _quick_normal_mode(event: E) -> None: + """ + Go into normal mode for one single action. + """ + event.app.vi_state.temporary_navigation_mode = True + + @handle("q", Keys.Any, filter=vi_navigation_mode & ~vi_recording_macro) + def _start_macro(event: E) -> None: + """ + Start recording macro. + """ + c = event.key_sequence[1].data + if c in vi_register_names: + vi_state = event.app.vi_state + + vi_state.recording_register = c + vi_state.current_recording = "" + + @handle("q", filter=vi_navigation_mode & vi_recording_macro) + def _stop_macro(event: E) -> None: + """ + Stop recording macro. + """ + vi_state = event.app.vi_state + + # Store and stop recording. + if vi_state.recording_register: + vi_state.named_registers[vi_state.recording_register] = ClipboardData( + vi_state.current_recording + ) + vi_state.recording_register = None + vi_state.current_recording = "" + + @handle("@", Keys.Any, filter=vi_navigation_mode, record_in_macro=False) + def _execute_macro(event: E) -> None: + """ + Execute macro. + + Notice that we pass `record_in_macro=False`. This ensures that the `@x` + keys don't appear in the recording itself. This function inserts the + body of the called macro back into the KeyProcessor, so these keys will + be added later on to the macro of their handlers have + `record_in_macro=True`. + """ + # Retrieve macro. + c = event.key_sequence[1].data + try: + macro = event.app.vi_state.named_registers[c] + except KeyError: + return + + # Expand macro (which is a string in the register), in individual keys. + # Use vt100 parser for this. + keys: List[KeyPress] = [] + + parser = Vt100Parser(keys.append) + parser.feed(macro.text) + parser.flush() + + # Now feed keys back to the input processor. + for _ in range(event.arg): + event.app.key_processor.feed_multiple(keys, first=True) + + return ConditionalKeyBindings(key_bindings, vi_mode) + + +def load_vi_search_bindings() -> KeyBindingsBase: + key_bindings = KeyBindings() + handle = key_bindings.add + from . import search + + @Condition + def search_buffer_is_empty() -> bool: + "Returns True when the search buffer is empty." + return get_app().current_buffer.text == "" + + # Vi-style forward search. + handle( + "/", + filter=(vi_navigation_mode | vi_selection_mode) & ~vi_search_direction_reversed, + )(search.start_forward_incremental_search) + handle( + "?", + filter=(vi_navigation_mode | vi_selection_mode) & vi_search_direction_reversed, + )(search.start_forward_incremental_search) + handle("c-s")(search.start_forward_incremental_search) + + # Vi-style backward search. + handle( + "?", + filter=(vi_navigation_mode | vi_selection_mode) & ~vi_search_direction_reversed, + )(search.start_reverse_incremental_search) + handle( + "/", + filter=(vi_navigation_mode | vi_selection_mode) & vi_search_direction_reversed, + )(search.start_reverse_incremental_search) + handle("c-r")(search.start_reverse_incremental_search) + + # Apply the search. (At the / or ? prompt.) + handle("enter", filter=is_searching)(search.accept_search) + + handle("c-r", filter=is_searching)(search.reverse_incremental_search) + handle("c-s", filter=is_searching)(search.forward_incremental_search) + + handle("c-c")(search.abort_search) + handle("c-g")(search.abort_search) + handle("backspace", filter=search_buffer_is_empty)(search.abort_search) + + # Handle escape. This should accept the search, just like readline. + # `abort_search` would be a meaningful alternative. + handle("escape")(search.accept_search) + + return ConditionalKeyBindings(key_bindings, vi_mode) |