#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Effectively me rewriting Prompt Toolkits keybindings handlers."""
import logging
import operator
import reprlib
import sys
from collections.abc import MutableSequence
from functools import total_ordering
from typing import Callable, Optional, Dict, Any, Union, List, Generator
from IPython.core.getipython import get_ipython
from IPython.core.interactiveshell import InteractiveShell
from prompt_toolkit.cache import SimpleCache
from prompt_toolkit.document import Document
from prompt_toolkit.filters import ViInsertMode
from prompt_toolkit.key_binding.defaults import load_key_bindings
from prompt_toolkit.key_binding import merge_key_bindings
from prompt_toolkit.key_binding.bindings.auto_suggest import load_auto_suggest_bindings
from prompt_toolkit.key_binding.bindings.vi import (
load_vi_bindings,
load_vi_search_bindings,
)
from prompt_toolkit.key_binding.key_bindings import (
KeyBindings, ConditionalKeyBindings, _MergedKeyBindings,
Binding, KeyBindingsBase
)
from prompt_toolkit.key_binding.key_processor import KeyPress, KeyPressEvent
from prompt_toolkit.keys import Keys
from default_profile.startup.ptoolkit import create_searching_keybindings, determine_which_pt_attribute
logging.basicConfig(level=logging.WARNING)
[docs]@total_ordering
class BindingPP(Binding):
"""Fix the prompt_toolkit binding.
Allow them to compared, called, hashed or evaluated for truthiness.
As *none* of this is originally available.
.. todo:: __lt__ so we can sort
"""
def __eq__(self, other):
if not isinstance(other, Binding):
return False
return self.keys == other.keys and self.handler == other.handler
def __lt__(self, other):
if not isinstance(other, Binding):
raise TypeError
return self.keys.value < other.keys.value
def __gt__(self, other):
if not isinstance(other, Binding):
raise TypeError
return self.keys.value > other.keys.value
def __hash__(self):
# is this right? idk but the equal was!! we may soon stop getting
# duplicates constantly
return hash(self.keys, self.handler)
def __bool__(self):
return self.filter()
def __call__(self, event: KeyPressEvent) -> None:
return self.call(event)
[docs]def convert_bindings(bindings: KeyBindingsBase):
# -> Generator[BindingPP]:
for b in get_ipython().pt_app.app.key_bindings.bindings:
yield BindingPP(b.keys, b.handler, filter=b.filter, eager=b.eager, is_global=b.is_global)
[docs]class KeyBindingsManager(KeyBindingsBase):
"""An object to make working with keybindings easier.
Subclasses UserList with a list of Keys and their handlers.
By defining dunders, the collection of keybindings are much
easier to work with.
"""
_get_bindings_for_keys_cache: SimpleCache[Any, Any]
[docs] def __init__(
self,
kb: Optional[KeyBindings] = None,
shell: Optional[InteractiveShell] = None,
**kwargs,
):
"""Initialize the class.
Parameters
----------
kb : `KeyBindings`, optional
KeyBindings to initialize with.
"""
self.shell = shell or get_ipython()
self.init_kb(kb)
# idk what this is but pt requires it
self._get_bindings_for_keys_cache = SimpleCache(maxsize=10000)
self._get_bindings_starting_with_keys_cache = SimpleCache(maxsize=1000)
self.__version = 0 # For cache invalidation.
self.data = self.kb
if hasattr(self, 'pt_app'):
self.pt_app = self.shell.pt_app
elif hasattr(self, 'pt_cli'):
self.pt_app = self.shell.pt_cli
else:
self.pt_app = None
super().__init__()
def __repr__(self):
return f"<{self.__class__.__name__}>: Bindings {len(self.bindings)} KB: {len(self.kb.bindings)} "
def __add__(self, another_one, *args):
if isinstance(another_one, Binding):
maybe_key = BindingPP(another_one)
# Dont add duplicate keys!!
if maybe_key in self:
return
elif another_one in self:
return
return self.bindings.append(maybe_key)
return self.kb.add_binding(another_one)
def __iadd__(self, another_one, *args):
return self.__add__(another_one, *args)
def __mul__(self):
raise TypeError
[docs] def add(self, another_one, *args):
"""Add another binding.
Takes same parameters as `Binding` ``__init__``
and **not** the same bindings as `KeyBindings.add`.
"""
return self.__add__(another_one, *args)
[docs] @property
def kb(self):
return self._kb
[docs] @property
def bindings(self):
"""Make the *kb* attributes bindings visible at the top level."""
return self.kb.bindings
@bindings.setter
def bindings_setter(self, value):
self.kb.bindings = value
[docs] @bindings.deleter
def bindings_setter(self):
del self.kb.bindings
# @bindings.setter
# def call_iadd(self, other):
# self.__iadd__(other)
add_binding = add
insert = add
def __iter__(self):
return iter(self.bindings)
def __len__(self):
return len(self.bindings)
[docs] def len(self):
return self.__len__()
def __repr_pretty(self, p, cycle):
# I don't know why it has that call signature
return (
f"<{self.__class__.__name__}:> - {reprlib.recursive_repr(self.kb.bindings)}"
)
def __getitem__(self, index):
return self.bindings[index]
# def __getslice__(self, index, step=1):
# return slice(self.bindings, index, step)
def __setitem__(self, index, value):
if not isinstance(index, int):
raise TypeError
if isinstance(value, Binding):
b = value
value = BindingPP(b.keys, b.handler, filter=b.filter, eager=b.eager, is_global=b.is_global)
self.bindings[index] = value
def __delitem__(self, index):
del self.bindings[index]
def __slice__(self, index, stop=None, step=1):
return slice(self.bindings, index, step)
@property
def _version(self):
"""I think a tally that gets cleared when the list of handlers needs updating?"""
return self.__version
@_version.setter
def _set_cache(self, value=None):
self._version = value if value is not None else self._version + 1
self._get_bindings_for_keys_cache.clear()
self._get_bindings_starting_with_keys_cache.clear()
@_version.deleter
def _clear_cache(self):
self._get_bindings_for_keys_cache.clear()
self._get_bindings_starting_with_keys_cache.clear()
[docs] def get_keys(self, keys):
"""Return handlers for 'keys'.
:param keys:
:type keys:
:return:
:rtype:
"""
try:
len(keys)
except AttributeError:
return
result = []
# Dude don't define the vars inside the for loop
# It's easier to segregate them at the top and then work with them
any_count = 0
for binding in self.bindings:
if len(keys) == len(binding.keys):
match = True
for i, j in zip(binding.keys, keys):
if i != j and i != Keys.Any:
match = False
break
if i == Keys.Any:
any_count += 1
if match:
result.append((any_count, binding))
# Place bindings that have more 'Any' occurrences in them at the end.
result = sorted(result, key=lambda item: -item[0])
return [item[1] for item in result]
[docs] def get_bindings_for_keys(self, keys):
"""Return a list of key bindings that can handle this key.
(This return also inactive bindings, so the `filter` still has to be
called, for checking it.)
:param keys: tuple of keys.
"""
return self._get_bindings_for_keys_cache.get(keys)
[docs] def get_bindings_starting_with_keys(self, keys):
"""Return a list of key bindings that handle a sequence starting with `keys`.
(It does only return bindings for which the sequences are
longer than `keys`. And like `get_bindings_for_keys`, it also includes
inactive bindings.)
:param keys: tuple of keys.
"""
def get():
result = []
for b in self.bindings:
if len(keys) < len(b.keys):
match = True
for i, j in zip(b.keys, keys):
if i != j and i != Keys.Any:
match = False
break
if match:
result.append(b)
return result
return self._get_bindings_starting_with_keys_cache.get(keys, get)
def __sizeof__(self):
# Unfortunately I've added so much to this class that it might be necessary to check
# how big ano object is now
return object.__sizeof__(self) + sum(
sys.getsizeof(v) for v in self.__dict__.values()
)
[docs] def get(self, keys):
# TODO:
pass
def __dir__(self): # wtf did i do that this isnt sorted anymore
return sorted(dir(self))
def __str__(self, level=500):
return reprlib.Repr().repr_list(self.bindings, level)
[docs] def init_kb(self, kb=None):
if kb is None:
if self.shell is not None:
if hasattr(self.shell, "pt_app"):
self._kb = self.shell.pt_app.app.key_bindings
elif hasattr(shell, "pt_cli"):
self._kb = self.shell.pt_cli.application.key_bindings_registry
else:
self._kb = None
else:
if not isinstance(kb, "KeyBindingsBase"):
raise TypeError
self._kb = _kb
# So this should cover both IPython and pt aps that don't have self.shell set!
if self.kb is None:
self.kb = load_key_bindings()
[docs]class Documented(Document):
"""I'll admit this subclass doesn't exist for much of a reason.
However, it's a LOT easier to work with classes with their dunders defined.
Implement the basics for a class to be considered a sequence IE len and iter.
"""
def __len__(self):
return self.cursor_position
def __iter__(self):
return iter(self.text)
[docs]def create_vi_insert_keybindings() -> ConditionalKeyBindings:
kb = KeyBindings()
handle = kb.add
# Add custom key binding for PDB.
# holy hell this is genius. py3.7 just got breakpoint but this wouldve been a great addition to my ipy conf
@handle(Keys.ControlB)
def pdb_snippet(event):
"""Pressing Control-B will insert `pdb.set_trace`."""
event.cli.current_buffer.insert_text("\nimport pdb; pdb.set_trace()\n")
@handle(Keys.ControlE, Keys.ControlE)
def exec_line(event):
"""Typing ControlE twice should also execute the current command. (Alternative for Meta-Enter.)"""
b = event.current_buffer
if b.accept_action.is_returnable:
b.accept_action.validate_and_handle(event.cli, b)
@handle("j", "j")
def normal_mode(event):
"""Map 'jj' to Escape."""
event.cli.input_processor.feed(KeyPress(Keys.Escape))
# Custom key binding for some simple autocorrection while typing.
# TODO: Observe how much this slows stuff down because if its a quick lookup then you could add your autocorrect.vim
corrections = {
"impotr": "import",
"pritn": "print",
}
@handle(" ")
def autocorrection(event):
"""When a space is pressed. Check & correct word before cursor."""
b = event.cli.current_buffer
w = b.document.get_word_before_cursor()
if w is not None:
if w in corrections:
b.delete_before_cursor(count=len(w))
b.insert_text(corrections[w])
b.insert_text(" ")
return ConditionalKeyBindings(kb, filter=ViInsertMode())
[docs]def create_kb() -> List[Binding]:
# Honestly I'm wary to do this but let's go for it
if get_ipython() is None:
return
pre_existing_keys = determine_which_pt_attribute()
if len(pre_existing_keys) == 0:
print('pre_existing_keys is 0')
return
_all_kb = pre_existing_keys.extend([*load_vi_bindings().bindings,
*load_vi_search_bindings().bindings,
*load_auto_suggest_bindings().bindings, # these stopped getting added when i did this
# create_searching_keybindings().bindings,
*create_vi_insert_keybindings().bindings,
])
all_kb = KeyBindings()
# we cant assign to bindings as its a property
all_kb._bindings = _all_kb
return all_kb
[docs]def flatten_kb():
if hasattr(get_ipython().pt_app.app.key_bindings, '_bindings2'):
# fucking _MergedKeyBindings
get_ipython().pt_app.app.key_bindings = get_ipython().pt_app.app.key_bindings._bindings2
logging.warning(len(get_ipython().pt_app.app.key_bindings.bindings))
if __name__ == "__main__":
merged_kb = create_kb()
if merged_kb is not None:
# get_ipython().pt_app.app.key_bindings = merged_kb
# _MergedKeyBindings doesnt have it holy fuck
# get_ipython().pt_app.app.key_bindings._clear_cache()
flatten_kb()
# print(type(merged_kb))
# print(dir(merged_kb))
else:
print('fuck')