1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233 |
- # 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
- @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: 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: 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
- @Condition
- def is_returnable() -> bool:
- return get_app().current_buffer.is_returnable
- @Condition
- def in_block_selection() -> bool:
- buff = get_app().current_buffer
- return bool(
- buff.selection_state and buff.selection_state.type == SelectionType.BLOCK
- )
- @Condition
- def digraph_symbol_1_given() -> bool:
- return get_app().vi_state.digraph_symbol1 is not None
- @Condition
- def search_buffer_is_empty() -> bool:
- "Returns True when the search buffer is empty."
- return get_app().current_buffer.text == ""
- @Condition
- 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"))
- @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()
- # 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
- )
- )
- @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)
- @handle("c-t", filter=vi_insert_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)
- @handle("c-d", filter=vi_insert_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_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)
- @handle("*", filter=vi_navigation_mode)
- 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)
- @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: 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'
- @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 _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)
- @text_object("F", Keys.Any)
- 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
- )
- @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: 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)
- @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 numerics 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
- @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
- # 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)
|