Llama-3.1-8B-DALv0.1
/
venv
/lib
/python3.12
/site-packages
/prompt_toolkit
/key_binding
/bindings
/vi.py
# pylint: disable=function-redefined | |
from __future__ import annotations | |
import codecs | |
import string | |
from enum import Enum | |
from itertools import accumulate | |
from typing import Callable, Iterable, Tuple, TypeVar | |
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 | |
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: 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: | |
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: | |
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: | |
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: 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: | |
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 | |
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 is_returnable() -> bool: | |
return get_app().current_buffer.is_returnable | |
def in_block_selection() -> bool: | |
buff = get_app().current_buffer | |
return bool( | |
buff.selection_state and buff.selection_state.type == SelectionType.BLOCK | |
) | |
def digraph_symbol_1_given() -> bool: | |
return get_app().vi_state.digraph_symbol1 is not None | |
def search_buffer_is_empty() -> bool: | |
"Returns True when the search buffer is empty." | |
return get_app().current_buffer.text == "" | |
def tilde_operator() -> bool: | |
return get_app().vi_state.tilde_operator | |
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()), | |
( | |
("~",), | |
tilde_operator, | |
lambda string: string.swapcase(), | |
), | |
] | |
# Insert a character literally (quoted insert). | |
handle("c-v", filter=vi_insert_mode)(get_by_name("quoted-insert")) | |
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() | |
def _up_in_selection(event: E) -> None: | |
""" | |
Arrow up in selection mode. | |
""" | |
event.current_buffer.cursor_up(count=event.arg) | |
def _down_in_selection(event: E) -> None: | |
""" | |
Arrow down in selection mode. | |
""" | |
event.current_buffer.cursor_down(count=event.arg) | |
def _up_in_navigation(event: E) -> None: | |
""" | |
Arrow up and ControlP in navigation mode go up. | |
""" | |
event.current_buffer.auto_up(count=event.arg) | |
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 | |
) | |
def _go_down(event: E) -> None: | |
""" | |
Arrow down and Control-N in navigation mode. | |
""" | |
event.current_buffer.auto_down(count=event.arg) | |
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 | |
) | |
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) | |
) | |
def _complete_next(event: E) -> None: | |
b = event.current_buffer | |
if b.complete_state: | |
b.complete_next() | |
else: | |
b.start_completion(select_first=True) | |
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) | |
def _accept_completion(event: E) -> None: | |
""" | |
Accept current completion. | |
""" | |
event.current_buffer.complete_state = None | |
def _cancel_completion(event: E) -> None: | |
""" | |
Cancel completion. Go back to originally typed text. | |
""" | |
event.current_buffer.cancel_completion() | |
# 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")) | |
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 | |
def _insert_mode(event: E) -> None: | |
""" | |
Pressing the Insert key. | |
""" | |
event.app.vi_state.input_mode = InputMode.INSERT | |
def _navigation_mode(event: E) -> None: | |
""" | |
Pressing the Insert key. | |
""" | |
event.app.vi_state.input_mode = InputMode.NAVIGATION | |
# ~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 | |
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 | |
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 | |
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 | |
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) | |
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)) | |
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) | |
def _i(event: E) -> None: | |
event.app.vi_state.input_mode = InputMode.INSERT | |
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 | |
) | |
) | |
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() | |
def _append_after_block(event: E) -> None: | |
insert_in_block_selection(event, after=True) | |
def _join(event: E) -> None: | |
""" | |
Join lines. | |
""" | |
for i in range(event.arg): | |
event.current_buffer.join_next_line() | |
def _join_nospace(event: E) -> None: | |
""" | |
Join lines without space. | |
""" | |
for i in range(event.arg): | |
event.current_buffer.join_next_line(separator="") | |
def _join_selection(event: E) -> None: | |
""" | |
Join selected lines. | |
""" | |
event.current_buffer.join_selected_lines() | |
def _join_selection_nospace(event: E) -> None: | |
""" | |
Join selected lines without space. | |
""" | |
event.current_buffer.join_selected_lines(separator="") | |
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, | |
) | |
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, | |
) | |
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 | |
) | |
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 | |
) | |
def _replace(event: E) -> None: | |
""" | |
Go to 'replace-single'-mode. | |
""" | |
event.app.vi_state.input_mode = InputMode.REPLACE_SINGLE | |
def _replace_mode(event: E) -> None: | |
""" | |
Go to 'replace'-mode. | |
""" | |
event.app.vi_state.input_mode = InputMode.REPLACE | |
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 | |
def _undo(event: E) -> None: | |
for i in range(event.arg): | |
event.current_buffer.undo() | |
def _visual_line(event: E) -> None: | |
""" | |
Start lines selection. | |
""" | |
event.current_buffer.start_selection(selection_type=SelectionType.LINES) | |
def _visual_block(event: E) -> None: | |
""" | |
Enter block selection mode. | |
""" | |
event.current_buffer.start_selection(selection_type=SelectionType.BLOCK) | |
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() | |
def _visual(event: E) -> None: | |
""" | |
Enter character selection mode. | |
""" | |
event.current_buffer.start_selection(selection_type=SelectionType.CHARACTERS) | |
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() | |
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() | |
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 | |
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) | |
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) | |
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)) | |
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 | |
) | |
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 | |
) | |
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) | |
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) | |
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 | |
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 | |
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) | |
def _lowercase_line(event: E) -> None: | |
""" | |
Lowercase current line. | |
""" | |
buff = event.current_buffer | |
buff.transform_current_line(lambda s: s.lower()) | |
def _uppercase_line(event: E) -> None: | |
""" | |
Uppercase current line. | |
""" | |
buff = event.current_buffer | |
buff.transform_current_line(lambda s: s.upper()) | |
def _swapcase_line(event: E) -> None: | |
""" | |
Swap case of the current line. | |
""" | |
buff = event.current_buffer | |
buff.transform_current_line(lambda s: s.swapcase()) | |
def _prev_occurrence(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) | |
def _next_occurrence(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) | |
def _begin_of_sentence(event: E) -> None: | |
# TODO: go to begin of sentence. | |
# XXX: should become text_object. | |
pass | |
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) | |
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] | |
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: | |
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) | |
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) | |
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 | |
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) | |
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) | |
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 *** | |
# | |
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 | |
) | |
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 | |
) | |
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()) | |
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() | |
) | |
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() | |
) | |
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) | |
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) | |
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) | |
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) | |
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) | |
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) | |
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) | |
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 | |
) | |
) | |
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: str | None = 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' | |
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) | |
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) | |
def _find_next_occurrence(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) | |
def _find_previous_occurrence(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 | |
) | |
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) | |
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. | |
""" | |
def _(event: E) -> TextObject: | |
""" | |
Repeat the last 'f'/'F'/'t'/'T' command. | |
""" | |
pos: int | None = 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) | |
def _left(event: E) -> TextObject: | |
""" | |
Implements 'ch', 'dh', 'h': Cursor left. | |
""" | |
return TextObject( | |
event.current_buffer.document.get_cursor_left_position(count=event.arg) | |
) | |
# 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, | |
) | |
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, | |
) | |
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) | |
) | |
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) | |
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) | |
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) | |
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) | |
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 | |
) | |
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) | |
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 | |
) | |
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 | |
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 | |
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 | |
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) | |
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) | |
) | |
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 | |
) | |
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, | |
) | |
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 | |
) | |
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 | |
) | |
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) | |
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 *** | |
# | |
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": | |
def _arg(event: E) -> None: | |
""" | |
Always handle numerics in navigation mode as arg. | |
""" | |
event.append_to_arg_count(event.data) | |
def _0_arg(event: E) -> None: | |
""" | |
Zero when an argument was already give. | |
""" | |
event.append_to_arg_count(event.data) | |
def _insert_text(event: E) -> None: | |
""" | |
Insert data at cursor position. | |
""" | |
event.current_buffer.insert_text(event.data, overwrite=True) | |
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 | |
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 | |
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() | |
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() | |
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 | |
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 | |
def _updown_multiple(event: E) -> None: | |
""" | |
Ignore all up/down key presses when in multiple cursor 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() | |
def _complete_filename(event: E) -> None: | |
""" | |
Complete file names. | |
""" | |
# TODO | |
pass | |
def _digraph(event: E) -> None: | |
""" | |
Go into digraph mode. | |
""" | |
event.app.vi_state.waiting_for_digraph = True | |
def _digraph1(event: E) -> None: | |
""" | |
First digraph symbol. | |
""" | |
event.app.vi_state.digraph_symbol1 = event.data | |
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 | |
def _quick_normal_mode(event: E) -> None: | |
""" | |
Go into normal mode for one single action. | |
""" | |
event.app.vi_state.temporary_navigation_mode = True | |
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 = "" | |
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 = "" | |
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 | |
# 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) | |