asm
This commit is contained in:
@@ -0,0 +1,630 @@
|
||||
"""
|
||||
Module to define and register Terminal IPython shortcuts with
|
||||
:mod:`prompt_toolkit`
|
||||
"""
|
||||
|
||||
# Copyright (c) IPython Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import warnings
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable, Any, Optional, List
|
||||
|
||||
from prompt_toolkit.application.current import get_app
|
||||
from prompt_toolkit.key_binding import KeyBindings
|
||||
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
|
||||
from prompt_toolkit.key_binding.bindings import named_commands as nc
|
||||
from prompt_toolkit.key_binding.bindings.completion import (
|
||||
display_completions_like_readline,
|
||||
)
|
||||
from prompt_toolkit.key_binding.vi_state import InputMode, ViState
|
||||
from prompt_toolkit.filters import Condition
|
||||
|
||||
from IPython.core.getipython import get_ipython
|
||||
from IPython.terminal.shortcuts import auto_match as match
|
||||
from IPython.terminal.shortcuts import auto_suggest
|
||||
from IPython.terminal.shortcuts.filters import filter_from_string
|
||||
from IPython.utils.decorators import undoc
|
||||
|
||||
from prompt_toolkit.enums import DEFAULT_BUFFER
|
||||
|
||||
__all__ = ["create_ipython_shortcuts"]
|
||||
|
||||
|
||||
@dataclass
|
||||
class BaseBinding:
|
||||
command: Callable[[KeyPressEvent], Any]
|
||||
keys: List[str]
|
||||
|
||||
|
||||
@dataclass
|
||||
class RuntimeBinding(BaseBinding):
|
||||
filter: Condition
|
||||
|
||||
|
||||
@dataclass
|
||||
class Binding(BaseBinding):
|
||||
# while filter could be created by referencing variables directly (rather
|
||||
# than created from strings), by using strings we ensure that users will
|
||||
# be able to create filters in configuration (e.g. JSON) files too, which
|
||||
# also benefits the documentation by enforcing human-readable filter names.
|
||||
condition: Optional[str] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.condition:
|
||||
self.filter = filter_from_string(self.condition)
|
||||
else:
|
||||
self.filter = None
|
||||
|
||||
|
||||
def create_identifier(handler: Callable):
|
||||
parts = handler.__module__.split(".")
|
||||
name = handler.__name__
|
||||
package = parts[0]
|
||||
if len(parts) > 1:
|
||||
final_module = parts[-1]
|
||||
return f"{package}:{final_module}.{name}"
|
||||
else:
|
||||
return f"{package}:{name}"
|
||||
|
||||
|
||||
AUTO_MATCH_BINDINGS = [
|
||||
*[
|
||||
Binding(
|
||||
cmd, [key], "focused_insert & auto_match & followed_by_closing_paren_or_end"
|
||||
)
|
||||
for key, cmd in match.auto_match_parens.items()
|
||||
],
|
||||
*[
|
||||
# raw string
|
||||
Binding(cmd, [key], "focused_insert & auto_match & preceded_by_raw_str_prefix")
|
||||
for key, cmd in match.auto_match_parens_raw_string.items()
|
||||
],
|
||||
Binding(
|
||||
match.double_quote,
|
||||
['"'],
|
||||
"focused_insert"
|
||||
" & auto_match"
|
||||
" & not_inside_unclosed_string"
|
||||
" & preceded_by_paired_double_quotes"
|
||||
" & followed_by_closing_paren_or_end",
|
||||
),
|
||||
Binding(
|
||||
match.single_quote,
|
||||
["'"],
|
||||
"focused_insert"
|
||||
" & auto_match"
|
||||
" & not_inside_unclosed_string"
|
||||
" & preceded_by_paired_single_quotes"
|
||||
" & followed_by_closing_paren_or_end",
|
||||
),
|
||||
Binding(
|
||||
match.docstring_double_quotes,
|
||||
['"'],
|
||||
"focused_insert"
|
||||
" & auto_match"
|
||||
" & not_inside_unclosed_string"
|
||||
" & preceded_by_two_double_quotes",
|
||||
),
|
||||
Binding(
|
||||
match.docstring_single_quotes,
|
||||
["'"],
|
||||
"focused_insert"
|
||||
" & auto_match"
|
||||
" & not_inside_unclosed_string"
|
||||
" & preceded_by_two_single_quotes",
|
||||
),
|
||||
Binding(
|
||||
match.skip_over,
|
||||
[")"],
|
||||
"focused_insert & auto_match & followed_by_closing_round_paren",
|
||||
),
|
||||
Binding(
|
||||
match.skip_over,
|
||||
["]"],
|
||||
"focused_insert & auto_match & followed_by_closing_bracket",
|
||||
),
|
||||
Binding(
|
||||
match.skip_over,
|
||||
["}"],
|
||||
"focused_insert & auto_match & followed_by_closing_brace",
|
||||
),
|
||||
Binding(
|
||||
match.skip_over, ['"'], "focused_insert & auto_match & followed_by_double_quote"
|
||||
),
|
||||
Binding(
|
||||
match.skip_over, ["'"], "focused_insert & auto_match & followed_by_single_quote"
|
||||
),
|
||||
Binding(
|
||||
match.delete_pair,
|
||||
["backspace"],
|
||||
"focused_insert"
|
||||
" & preceded_by_opening_round_paren"
|
||||
" & auto_match"
|
||||
" & followed_by_closing_round_paren",
|
||||
),
|
||||
Binding(
|
||||
match.delete_pair,
|
||||
["backspace"],
|
||||
"focused_insert"
|
||||
" & preceded_by_opening_bracket"
|
||||
" & auto_match"
|
||||
" & followed_by_closing_bracket",
|
||||
),
|
||||
Binding(
|
||||
match.delete_pair,
|
||||
["backspace"],
|
||||
"focused_insert"
|
||||
" & preceded_by_opening_brace"
|
||||
" & auto_match"
|
||||
" & followed_by_closing_brace",
|
||||
),
|
||||
Binding(
|
||||
match.delete_pair,
|
||||
["backspace"],
|
||||
"focused_insert"
|
||||
" & preceded_by_double_quote"
|
||||
" & auto_match"
|
||||
" & followed_by_double_quote",
|
||||
),
|
||||
Binding(
|
||||
match.delete_pair,
|
||||
["backspace"],
|
||||
"focused_insert"
|
||||
" & preceded_by_single_quote"
|
||||
" & auto_match"
|
||||
" & followed_by_single_quote",
|
||||
),
|
||||
]
|
||||
|
||||
AUTO_SUGGEST_BINDINGS = [
|
||||
# there are two reasons for re-defining bindings defined upstream:
|
||||
# 1) prompt-toolkit does not execute autosuggestion bindings in vi mode,
|
||||
# 2) prompt-toolkit checks if we are at the end of text, not end of line
|
||||
# hence it does not work in multi-line mode of navigable provider
|
||||
Binding(
|
||||
auto_suggest.accept_or_jump_to_end,
|
||||
["end"],
|
||||
"has_suggestion & default_buffer_focused & emacs_like_insert_mode",
|
||||
),
|
||||
Binding(
|
||||
auto_suggest.accept_or_jump_to_end,
|
||||
["c-e"],
|
||||
"has_suggestion & default_buffer_focused & emacs_like_insert_mode",
|
||||
),
|
||||
Binding(
|
||||
auto_suggest.accept,
|
||||
["c-f"],
|
||||
"has_suggestion & default_buffer_focused & emacs_like_insert_mode",
|
||||
),
|
||||
Binding(
|
||||
auto_suggest.accept,
|
||||
["right"],
|
||||
"has_suggestion & default_buffer_focused & emacs_like_insert_mode",
|
||||
),
|
||||
Binding(
|
||||
auto_suggest.accept_word,
|
||||
["escape", "f"],
|
||||
"has_suggestion & default_buffer_focused & emacs_like_insert_mode",
|
||||
),
|
||||
Binding(
|
||||
auto_suggest.accept_token,
|
||||
["c-right"],
|
||||
"has_suggestion & default_buffer_focused & emacs_like_insert_mode",
|
||||
),
|
||||
Binding(
|
||||
auto_suggest.discard,
|
||||
["escape"],
|
||||
# note this one is using `emacs_insert_mode`, not `emacs_like_insert_mode`
|
||||
# as in `vi_insert_mode` we do not want `escape` to be shadowed (ever).
|
||||
"has_suggestion & default_buffer_focused & emacs_insert_mode",
|
||||
),
|
||||
Binding(
|
||||
auto_suggest.discard,
|
||||
["delete"],
|
||||
"has_suggestion & default_buffer_focused & emacs_insert_mode",
|
||||
),
|
||||
Binding(
|
||||
auto_suggest.swap_autosuggestion_up,
|
||||
["c-up"],
|
||||
"navigable_suggestions"
|
||||
" & ~has_line_above"
|
||||
" & has_suggestion"
|
||||
" & default_buffer_focused",
|
||||
),
|
||||
Binding(
|
||||
auto_suggest.swap_autosuggestion_down,
|
||||
["c-down"],
|
||||
"navigable_suggestions"
|
||||
" & ~has_line_below"
|
||||
" & has_suggestion"
|
||||
" & default_buffer_focused",
|
||||
),
|
||||
Binding(
|
||||
auto_suggest.up_and_update_hint,
|
||||
["c-up"],
|
||||
"has_line_above & navigable_suggestions & default_buffer_focused",
|
||||
),
|
||||
Binding(
|
||||
auto_suggest.down_and_update_hint,
|
||||
["c-down"],
|
||||
"has_line_below & navigable_suggestions & default_buffer_focused",
|
||||
),
|
||||
Binding(
|
||||
auto_suggest.accept_character,
|
||||
["escape", "right"],
|
||||
"has_suggestion & default_buffer_focused & emacs_like_insert_mode",
|
||||
),
|
||||
Binding(
|
||||
auto_suggest.accept_and_move_cursor_left,
|
||||
["c-left"],
|
||||
"has_suggestion & default_buffer_focused & emacs_like_insert_mode",
|
||||
),
|
||||
Binding(
|
||||
auto_suggest.accept_and_keep_cursor,
|
||||
["escape", "down"],
|
||||
"has_suggestion & default_buffer_focused & emacs_insert_mode",
|
||||
),
|
||||
Binding(
|
||||
auto_suggest.backspace_and_resume_hint,
|
||||
["backspace"],
|
||||
# no `has_suggestion` here to allow resuming if no suggestion
|
||||
"default_buffer_focused & emacs_like_insert_mode",
|
||||
),
|
||||
Binding(
|
||||
auto_suggest.resume_hinting,
|
||||
["right"],
|
||||
"is_cursor_at_the_end_of_line"
|
||||
" & default_buffer_focused"
|
||||
" & emacs_like_insert_mode"
|
||||
" & pass_through",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
SIMPLE_CONTROL_BINDINGS = [
|
||||
Binding(cmd, [key], "vi_insert_mode & default_buffer_focused & ebivim")
|
||||
for key, cmd in {
|
||||
"c-a": nc.beginning_of_line,
|
||||
"c-b": nc.backward_char,
|
||||
"c-k": nc.kill_line,
|
||||
"c-w": nc.backward_kill_word,
|
||||
"c-y": nc.yank,
|
||||
"c-_": nc.undo,
|
||||
}.items()
|
||||
]
|
||||
|
||||
|
||||
ALT_AND_COMOBO_CONTROL_BINDINGS = [
|
||||
Binding(cmd, list(keys), "vi_insert_mode & default_buffer_focused & ebivim")
|
||||
for keys, cmd in {
|
||||
# Control Combos
|
||||
("c-x", "c-e"): nc.edit_and_execute,
|
||||
("c-x", "e"): nc.edit_and_execute,
|
||||
# Alt
|
||||
("escape", "b"): nc.backward_word,
|
||||
("escape", "c"): nc.capitalize_word,
|
||||
("escape", "d"): nc.kill_word,
|
||||
("escape", "h"): nc.backward_kill_word,
|
||||
("escape", "l"): nc.downcase_word,
|
||||
("escape", "u"): nc.uppercase_word,
|
||||
("escape", "y"): nc.yank_pop,
|
||||
("escape", "."): nc.yank_last_arg,
|
||||
}.items()
|
||||
]
|
||||
|
||||
|
||||
def add_binding(bindings: KeyBindings, binding: Binding):
|
||||
bindings.add(
|
||||
*binding.keys,
|
||||
**({"filter": binding.filter} if binding.filter is not None else {}),
|
||||
)(binding.command)
|
||||
|
||||
|
||||
def create_ipython_shortcuts(shell, skip=None) -> KeyBindings:
|
||||
"""Set up the prompt_toolkit keyboard shortcuts for IPython.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
shell: InteractiveShell
|
||||
The current IPython shell Instance
|
||||
skip: List[Binding]
|
||||
Bindings to skip.
|
||||
|
||||
Returns
|
||||
-------
|
||||
KeyBindings
|
||||
the keybinding instance for prompt toolkit.
|
||||
|
||||
"""
|
||||
kb = KeyBindings()
|
||||
skip = skip or []
|
||||
for binding in KEY_BINDINGS:
|
||||
skip_this_one = False
|
||||
for to_skip in skip:
|
||||
if (
|
||||
to_skip.command == binding.command
|
||||
and to_skip.filter == binding.filter
|
||||
and to_skip.keys == binding.keys
|
||||
):
|
||||
skip_this_one = True
|
||||
break
|
||||
if skip_this_one:
|
||||
continue
|
||||
add_binding(kb, binding)
|
||||
|
||||
def get_input_mode(self):
|
||||
app = get_app()
|
||||
app.ttimeoutlen = shell.ttimeoutlen
|
||||
app.timeoutlen = shell.timeoutlen
|
||||
|
||||
return self._input_mode
|
||||
|
||||
def set_input_mode(self, mode):
|
||||
shape = {InputMode.NAVIGATION: 2, InputMode.REPLACE: 4}.get(mode, 6)
|
||||
cursor = "\x1b[{} q".format(shape)
|
||||
|
||||
sys.stdout.write(cursor)
|
||||
sys.stdout.flush()
|
||||
|
||||
self._input_mode = mode
|
||||
|
||||
if shell.editing_mode == "vi" and shell.modal_cursor:
|
||||
ViState._input_mode = InputMode.INSERT # type: ignore
|
||||
ViState.input_mode = property(get_input_mode, set_input_mode) # type: ignore
|
||||
|
||||
return kb
|
||||
|
||||
|
||||
def reformat_and_execute(event):
|
||||
"""Reformat code and execute it"""
|
||||
shell = get_ipython()
|
||||
reformat_text_before_cursor(
|
||||
event.current_buffer, event.current_buffer.document, shell
|
||||
)
|
||||
event.current_buffer.validate_and_handle()
|
||||
|
||||
|
||||
def reformat_text_before_cursor(buffer, document, shell):
|
||||
text = buffer.delete_before_cursor(len(document.text[: document.cursor_position]))
|
||||
try:
|
||||
formatted_text = shell.reformat_handler(text)
|
||||
buffer.insert_text(formatted_text)
|
||||
except Exception as e:
|
||||
buffer.insert_text(text)
|
||||
|
||||
|
||||
def handle_return_or_newline_or_execute(event):
|
||||
shell = get_ipython()
|
||||
if getattr(shell, "handle_return", None):
|
||||
return shell.handle_return(shell)(event)
|
||||
else:
|
||||
return newline_or_execute_outer(shell)(event)
|
||||
|
||||
|
||||
def newline_or_execute_outer(shell):
|
||||
def newline_or_execute(event):
|
||||
"""When the user presses return, insert a newline or execute the code."""
|
||||
b = event.current_buffer
|
||||
d = b.document
|
||||
|
||||
if b.complete_state:
|
||||
cc = b.complete_state.current_completion
|
||||
if cc:
|
||||
b.apply_completion(cc)
|
||||
else:
|
||||
b.cancel_completion()
|
||||
return
|
||||
|
||||
# If there's only one line, treat it as if the cursor is at the end.
|
||||
# See https://github.com/ipython/ipython/issues/10425
|
||||
if d.line_count == 1:
|
||||
check_text = d.text
|
||||
else:
|
||||
check_text = d.text[: d.cursor_position]
|
||||
status, indent = shell.check_complete(check_text)
|
||||
|
||||
# if all we have after the cursor is whitespace: reformat current text
|
||||
# before cursor
|
||||
after_cursor = d.text[d.cursor_position :]
|
||||
reformatted = False
|
||||
if not after_cursor.strip():
|
||||
reformat_text_before_cursor(b, d, shell)
|
||||
reformatted = True
|
||||
if not (
|
||||
d.on_last_line
|
||||
or d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end()
|
||||
):
|
||||
if shell.autoindent:
|
||||
b.insert_text("\n" + indent)
|
||||
else:
|
||||
b.insert_text("\n")
|
||||
return
|
||||
|
||||
if (status != "incomplete") and b.accept_handler:
|
||||
if not reformatted:
|
||||
reformat_text_before_cursor(b, d, shell)
|
||||
b.validate_and_handle()
|
||||
else:
|
||||
if shell.autoindent:
|
||||
b.insert_text("\n" + indent)
|
||||
else:
|
||||
b.insert_text("\n")
|
||||
|
||||
return newline_or_execute
|
||||
|
||||
|
||||
def previous_history_or_previous_completion(event):
|
||||
"""
|
||||
Control-P in vi edit mode on readline is history next, unlike default prompt toolkit.
|
||||
|
||||
If completer is open this still select previous completion.
|
||||
"""
|
||||
event.current_buffer.auto_up()
|
||||
|
||||
|
||||
def next_history_or_next_completion(event):
|
||||
"""
|
||||
Control-N in vi edit mode on readline is history previous, unlike default prompt toolkit.
|
||||
|
||||
If completer is open this still select next completion.
|
||||
"""
|
||||
event.current_buffer.auto_down()
|
||||
|
||||
|
||||
def dismiss_completion(event):
|
||||
"""Dismiss completion"""
|
||||
b = event.current_buffer
|
||||
if b.complete_state:
|
||||
b.cancel_completion()
|
||||
|
||||
|
||||
def reset_buffer(event):
|
||||
"""Reset buffer"""
|
||||
b = event.current_buffer
|
||||
if b.complete_state:
|
||||
b.cancel_completion()
|
||||
else:
|
||||
b.reset()
|
||||
|
||||
|
||||
def reset_search_buffer(event):
|
||||
"""Reset search buffer"""
|
||||
if event.current_buffer.document.text:
|
||||
event.current_buffer.reset()
|
||||
else:
|
||||
event.app.layout.focus(DEFAULT_BUFFER)
|
||||
|
||||
|
||||
def suspend_to_bg(event):
|
||||
"""Suspend to background"""
|
||||
event.app.suspend_to_background()
|
||||
|
||||
|
||||
def quit(event):
|
||||
"""
|
||||
Quit application with ``SIGQUIT`` if supported or ``sys.exit`` otherwise.
|
||||
|
||||
On platforms that support SIGQUIT, send SIGQUIT to the current process.
|
||||
On other platforms, just exit the process with a message.
|
||||
"""
|
||||
sigquit = getattr(signal, "SIGQUIT", None)
|
||||
if sigquit is not None:
|
||||
os.kill(0, signal.SIGQUIT)
|
||||
else:
|
||||
sys.exit("Quit")
|
||||
|
||||
|
||||
def indent_buffer(event):
|
||||
"""Indent buffer"""
|
||||
event.current_buffer.insert_text(" " * 4)
|
||||
|
||||
|
||||
def newline_autoindent(event):
|
||||
"""Insert a newline after the cursor indented appropriately.
|
||||
|
||||
Fancier version of former ``newline_with_copy_margin`` which should
|
||||
compute the correct indentation of the inserted line. That is to say, indent
|
||||
by 4 extra space after a function definition, class definition, context
|
||||
manager... And dedent by 4 space after ``pass``, ``return``, ``raise ...``.
|
||||
"""
|
||||
shell = get_ipython()
|
||||
inputsplitter = shell.input_transformer_manager
|
||||
b = event.current_buffer
|
||||
d = b.document
|
||||
|
||||
if b.complete_state:
|
||||
b.cancel_completion()
|
||||
text = d.text[: d.cursor_position] + "\n"
|
||||
_, indent = inputsplitter.check_complete(text)
|
||||
b.insert_text("\n" + (" " * (indent or 0)), move_cursor=False)
|
||||
|
||||
|
||||
def open_input_in_editor(event):
|
||||
"""Open code from input in external editor"""
|
||||
event.app.current_buffer.open_in_editor()
|
||||
|
||||
|
||||
if sys.platform == "win32":
|
||||
from IPython.core.error import TryNext
|
||||
from IPython.lib.clipboard import (
|
||||
ClipboardEmpty,
|
||||
tkinter_clipboard_get,
|
||||
win32_clipboard_get,
|
||||
)
|
||||
|
||||
@undoc
|
||||
def win_paste(event):
|
||||
try:
|
||||
text = win32_clipboard_get()
|
||||
except TryNext:
|
||||
try:
|
||||
text = tkinter_clipboard_get()
|
||||
except (TryNext, ClipboardEmpty):
|
||||
return
|
||||
except ClipboardEmpty:
|
||||
return
|
||||
event.current_buffer.insert_text(text.replace("\t", " " * 4))
|
||||
|
||||
else:
|
||||
|
||||
@undoc
|
||||
def win_paste(event):
|
||||
"""Stub used on other platforms"""
|
||||
pass
|
||||
|
||||
|
||||
KEY_BINDINGS = [
|
||||
Binding(
|
||||
handle_return_or_newline_or_execute,
|
||||
["enter"],
|
||||
"default_buffer_focused & ~has_selection & insert_mode",
|
||||
),
|
||||
Binding(
|
||||
reformat_and_execute,
|
||||
["escape", "enter"],
|
||||
"default_buffer_focused & ~has_selection & insert_mode & ebivim",
|
||||
),
|
||||
Binding(quit, ["c-\\"]),
|
||||
Binding(
|
||||
previous_history_or_previous_completion,
|
||||
["c-p"],
|
||||
"vi_insert_mode & default_buffer_focused",
|
||||
),
|
||||
Binding(
|
||||
next_history_or_next_completion,
|
||||
["c-n"],
|
||||
"vi_insert_mode & default_buffer_focused",
|
||||
),
|
||||
Binding(dismiss_completion, ["c-g"], "default_buffer_focused & has_completions"),
|
||||
Binding(reset_buffer, ["c-c"], "default_buffer_focused"),
|
||||
Binding(reset_search_buffer, ["c-c"], "search_buffer_focused"),
|
||||
Binding(suspend_to_bg, ["c-z"], "supports_suspend"),
|
||||
Binding(
|
||||
indent_buffer,
|
||||
["tab"], # Ctrl+I == Tab
|
||||
"default_buffer_focused"
|
||||
" & ~has_selection"
|
||||
" & insert_mode"
|
||||
" & cursor_in_leading_ws",
|
||||
),
|
||||
Binding(newline_autoindent, ["c-o"], "default_buffer_focused & emacs_insert_mode"),
|
||||
Binding(open_input_in_editor, ["f2"], "default_buffer_focused"),
|
||||
*AUTO_MATCH_BINDINGS,
|
||||
*AUTO_SUGGEST_BINDINGS,
|
||||
Binding(
|
||||
display_completions_like_readline,
|
||||
["c-i"],
|
||||
"readline_like_completions"
|
||||
" & default_buffer_focused"
|
||||
" & ~has_selection"
|
||||
" & insert_mode"
|
||||
" & ~cursor_in_leading_ws",
|
||||
),
|
||||
Binding(win_paste, ["c-v"], "default_buffer_focused & ~vi_mode & is_windows_os"),
|
||||
*SIMPLE_CONTROL_BINDINGS,
|
||||
*ALT_AND_COMOBO_CONTROL_BINDINGS,
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
Utilities function for keybinding with prompt toolkit.
|
||||
|
||||
This will be bound to specific key press and filter modes,
|
||||
like whether we are in edit mode, and whether the completer is open.
|
||||
"""
|
||||
import re
|
||||
from prompt_toolkit.key_binding import KeyPressEvent
|
||||
|
||||
|
||||
def parenthesis(event: KeyPressEvent):
|
||||
"""Auto-close parenthesis"""
|
||||
event.current_buffer.insert_text("()")
|
||||
event.current_buffer.cursor_left()
|
||||
|
||||
|
||||
def brackets(event: KeyPressEvent):
|
||||
"""Auto-close brackets"""
|
||||
event.current_buffer.insert_text("[]")
|
||||
event.current_buffer.cursor_left()
|
||||
|
||||
|
||||
def braces(event: KeyPressEvent):
|
||||
"""Auto-close braces"""
|
||||
event.current_buffer.insert_text("{}")
|
||||
event.current_buffer.cursor_left()
|
||||
|
||||
|
||||
def double_quote(event: KeyPressEvent):
|
||||
"""Auto-close double quotes"""
|
||||
event.current_buffer.insert_text('""')
|
||||
event.current_buffer.cursor_left()
|
||||
|
||||
|
||||
def single_quote(event: KeyPressEvent):
|
||||
"""Auto-close single quotes"""
|
||||
event.current_buffer.insert_text("''")
|
||||
event.current_buffer.cursor_left()
|
||||
|
||||
|
||||
def docstring_double_quotes(event: KeyPressEvent):
|
||||
"""Auto-close docstring (double quotes)"""
|
||||
event.current_buffer.insert_text('""""')
|
||||
event.current_buffer.cursor_left(3)
|
||||
|
||||
|
||||
def docstring_single_quotes(event: KeyPressEvent):
|
||||
"""Auto-close docstring (single quotes)"""
|
||||
event.current_buffer.insert_text("''''")
|
||||
event.current_buffer.cursor_left(3)
|
||||
|
||||
|
||||
def raw_string_parenthesis(event: KeyPressEvent):
|
||||
"""Auto-close parenthesis in raw strings"""
|
||||
matches = re.match(
|
||||
r".*(r|R)[\"'](-*)",
|
||||
event.current_buffer.document.current_line_before_cursor,
|
||||
)
|
||||
dashes = matches.group(2) if matches else ""
|
||||
event.current_buffer.insert_text("()" + dashes)
|
||||
event.current_buffer.cursor_left(len(dashes) + 1)
|
||||
|
||||
|
||||
def raw_string_bracket(event: KeyPressEvent):
|
||||
"""Auto-close bracker in raw strings"""
|
||||
matches = re.match(
|
||||
r".*(r|R)[\"'](-*)",
|
||||
event.current_buffer.document.current_line_before_cursor,
|
||||
)
|
||||
dashes = matches.group(2) if matches else ""
|
||||
event.current_buffer.insert_text("[]" + dashes)
|
||||
event.current_buffer.cursor_left(len(dashes) + 1)
|
||||
|
||||
|
||||
def raw_string_braces(event: KeyPressEvent):
|
||||
"""Auto-close braces in raw strings"""
|
||||
matches = re.match(
|
||||
r".*(r|R)[\"'](-*)",
|
||||
event.current_buffer.document.current_line_before_cursor,
|
||||
)
|
||||
dashes = matches.group(2) if matches else ""
|
||||
event.current_buffer.insert_text("{}" + dashes)
|
||||
event.current_buffer.cursor_left(len(dashes) + 1)
|
||||
|
||||
|
||||
def skip_over(event: KeyPressEvent):
|
||||
"""Skip over automatically added parenthesis/quote.
|
||||
|
||||
(rather than adding another parenthesis/quote)"""
|
||||
event.current_buffer.cursor_right()
|
||||
|
||||
|
||||
def delete_pair(event: KeyPressEvent):
|
||||
"""Delete auto-closed parenthesis"""
|
||||
event.current_buffer.delete()
|
||||
event.current_buffer.delete_before_cursor()
|
||||
|
||||
|
||||
auto_match_parens = {"(": parenthesis, "[": brackets, "{": braces}
|
||||
auto_match_parens_raw_string = {
|
||||
"(": raw_string_parenthesis,
|
||||
"[": raw_string_bracket,
|
||||
"{": raw_string_braces,
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
import re
|
||||
import tokenize
|
||||
from io import StringIO
|
||||
from typing import Callable, List, Optional, Union, Generator, Tuple
|
||||
import warnings
|
||||
|
||||
from prompt_toolkit.buffer import Buffer
|
||||
from prompt_toolkit.key_binding import KeyPressEvent
|
||||
from prompt_toolkit.key_binding.bindings import named_commands as nc
|
||||
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, Suggestion
|
||||
from prompt_toolkit.document import Document
|
||||
from prompt_toolkit.history import History
|
||||
from prompt_toolkit.shortcuts import PromptSession
|
||||
from prompt_toolkit.layout.processors import (
|
||||
Processor,
|
||||
Transformation,
|
||||
TransformationInput,
|
||||
)
|
||||
|
||||
from IPython.core.getipython import get_ipython
|
||||
from IPython.utils.tokenutil import generate_tokens
|
||||
|
||||
from .filters import pass_through
|
||||
|
||||
|
||||
def _get_query(document: Document):
|
||||
return document.lines[document.cursor_position_row]
|
||||
|
||||
|
||||
class AppendAutoSuggestionInAnyLine(Processor):
|
||||
"""
|
||||
Append the auto suggestion to lines other than the last (appending to the
|
||||
last line is natively supported by the prompt toolkit).
|
||||
"""
|
||||
|
||||
def __init__(self, style: str = "class:auto-suggestion") -> None:
|
||||
self.style = style
|
||||
|
||||
def apply_transformation(self, ti: TransformationInput) -> Transformation:
|
||||
is_last_line = ti.lineno == ti.document.line_count - 1
|
||||
is_active_line = ti.lineno == ti.document.cursor_position_row
|
||||
|
||||
if not is_last_line and is_active_line:
|
||||
buffer = ti.buffer_control.buffer
|
||||
|
||||
if buffer.suggestion and ti.document.is_cursor_at_the_end_of_line:
|
||||
suggestion = buffer.suggestion.text
|
||||
else:
|
||||
suggestion = ""
|
||||
|
||||
return Transformation(fragments=ti.fragments + [(self.style, suggestion)])
|
||||
else:
|
||||
return Transformation(fragments=ti.fragments)
|
||||
|
||||
|
||||
class NavigableAutoSuggestFromHistory(AutoSuggestFromHistory):
|
||||
"""
|
||||
A subclass of AutoSuggestFromHistory that allow navigation to next/previous
|
||||
suggestion from history. To do so it remembers the current position, but it
|
||||
state need to carefully be cleared on the right events.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
):
|
||||
self.skip_lines = 0
|
||||
self._connected_apps = []
|
||||
|
||||
def reset_history_position(self, _: Buffer):
|
||||
self.skip_lines = 0
|
||||
|
||||
def disconnect(self):
|
||||
for pt_app in self._connected_apps:
|
||||
text_insert_event = pt_app.default_buffer.on_text_insert
|
||||
text_insert_event.remove_handler(self.reset_history_position)
|
||||
|
||||
def connect(self, pt_app: PromptSession):
|
||||
self._connected_apps.append(pt_app)
|
||||
# note: `on_text_changed` could be used for a bit different behaviour
|
||||
# on character deletion (i.e. reseting history position on backspace)
|
||||
pt_app.default_buffer.on_text_insert.add_handler(self.reset_history_position)
|
||||
pt_app.default_buffer.on_cursor_position_changed.add_handler(self._dismiss)
|
||||
|
||||
def get_suggestion(
|
||||
self, buffer: Buffer, document: Document
|
||||
) -> Optional[Suggestion]:
|
||||
text = _get_query(document)
|
||||
|
||||
if text.strip():
|
||||
for suggestion, _ in self._find_next_match(
|
||||
text, self.skip_lines, buffer.history
|
||||
):
|
||||
return Suggestion(suggestion)
|
||||
|
||||
return None
|
||||
|
||||
def _dismiss(self, buffer, *args, **kwargs):
|
||||
buffer.suggestion = None
|
||||
|
||||
def _find_match(
|
||||
self, text: str, skip_lines: float, history: History, previous: bool
|
||||
) -> Generator[Tuple[str, float], None, None]:
|
||||
"""
|
||||
text : str
|
||||
Text content to find a match for, the user cursor is most of the
|
||||
time at the end of this text.
|
||||
skip_lines : float
|
||||
number of items to skip in the search, this is used to indicate how
|
||||
far in the list the user has navigated by pressing up or down.
|
||||
The float type is used as the base value is +inf
|
||||
history : History
|
||||
prompt_toolkit History instance to fetch previous entries from.
|
||||
previous : bool
|
||||
Direction of the search, whether we are looking previous match
|
||||
(True), or next match (False).
|
||||
|
||||
Yields
|
||||
------
|
||||
Tuple with:
|
||||
str:
|
||||
current suggestion.
|
||||
float:
|
||||
will actually yield only ints, which is passed back via skip_lines,
|
||||
which may be a +inf (float)
|
||||
|
||||
|
||||
"""
|
||||
line_number = -1
|
||||
for string in reversed(list(history.get_strings())):
|
||||
for line in reversed(string.splitlines()):
|
||||
line_number += 1
|
||||
if not previous and line_number < skip_lines:
|
||||
continue
|
||||
# do not return empty suggestions as these
|
||||
# close the auto-suggestion overlay (and are useless)
|
||||
if line.startswith(text) and len(line) > len(text):
|
||||
yield line[len(text) :], line_number
|
||||
if previous and line_number >= skip_lines:
|
||||
return
|
||||
|
||||
def _find_next_match(
|
||||
self, text: str, skip_lines: float, history: History
|
||||
) -> Generator[Tuple[str, float], None, None]:
|
||||
return self._find_match(text, skip_lines, history, previous=False)
|
||||
|
||||
def _find_previous_match(self, text: str, skip_lines: float, history: History):
|
||||
return reversed(
|
||||
list(self._find_match(text, skip_lines, history, previous=True))
|
||||
)
|
||||
|
||||
def up(self, query: str, other_than: str, history: History) -> None:
|
||||
for suggestion, line_number in self._find_next_match(
|
||||
query, self.skip_lines, history
|
||||
):
|
||||
# if user has history ['very.a', 'very', 'very.b'] and typed 'very'
|
||||
# we want to switch from 'very.b' to 'very.a' because a) if the
|
||||
# suggestion equals current text, prompt-toolkit aborts suggesting
|
||||
# b) user likely would not be interested in 'very' anyways (they
|
||||
# already typed it).
|
||||
if query + suggestion != other_than:
|
||||
self.skip_lines = line_number
|
||||
break
|
||||
else:
|
||||
# no matches found, cycle back to beginning
|
||||
self.skip_lines = 0
|
||||
|
||||
def down(self, query: str, other_than: str, history: History) -> None:
|
||||
for suggestion, line_number in self._find_previous_match(
|
||||
query, self.skip_lines, history
|
||||
):
|
||||
if query + suggestion != other_than:
|
||||
self.skip_lines = line_number
|
||||
break
|
||||
else:
|
||||
# no matches found, cycle to end
|
||||
for suggestion, line_number in self._find_previous_match(
|
||||
query, float("Inf"), history
|
||||
):
|
||||
if query + suggestion != other_than:
|
||||
self.skip_lines = line_number
|
||||
break
|
||||
|
||||
|
||||
def accept_or_jump_to_end(event: KeyPressEvent):
|
||||
"""Apply autosuggestion or jump to end of line."""
|
||||
buffer = event.current_buffer
|
||||
d = buffer.document
|
||||
after_cursor = d.text[d.cursor_position :]
|
||||
lines = after_cursor.split("\n")
|
||||
end_of_current_line = lines[0].strip()
|
||||
suggestion = buffer.suggestion
|
||||
if (suggestion is not None) and (suggestion.text) and (end_of_current_line == ""):
|
||||
buffer.insert_text(suggestion.text)
|
||||
else:
|
||||
nc.end_of_line(event)
|
||||
|
||||
|
||||
def _deprected_accept_in_vi_insert_mode(event: KeyPressEvent):
|
||||
"""Accept autosuggestion or jump to end of line.
|
||||
|
||||
.. deprecated:: 8.12
|
||||
Use `accept_or_jump_to_end` instead.
|
||||
"""
|
||||
return accept_or_jump_to_end(event)
|
||||
|
||||
|
||||
def accept(event: KeyPressEvent):
|
||||
"""Accept autosuggestion"""
|
||||
buffer = event.current_buffer
|
||||
suggestion = buffer.suggestion
|
||||
if suggestion:
|
||||
buffer.insert_text(suggestion.text)
|
||||
else:
|
||||
nc.forward_char(event)
|
||||
|
||||
|
||||
def discard(event: KeyPressEvent):
|
||||
"""Discard autosuggestion"""
|
||||
buffer = event.current_buffer
|
||||
buffer.suggestion = None
|
||||
|
||||
|
||||
def accept_word(event: KeyPressEvent):
|
||||
"""Fill partial autosuggestion by word"""
|
||||
buffer = event.current_buffer
|
||||
suggestion = buffer.suggestion
|
||||
if suggestion:
|
||||
t = re.split(r"(\S+\s+)", suggestion.text)
|
||||
buffer.insert_text(next((x for x in t if x), ""))
|
||||
else:
|
||||
nc.forward_word(event)
|
||||
|
||||
|
||||
def accept_character(event: KeyPressEvent):
|
||||
"""Fill partial autosuggestion by character"""
|
||||
b = event.current_buffer
|
||||
suggestion = b.suggestion
|
||||
if suggestion and suggestion.text:
|
||||
b.insert_text(suggestion.text[0])
|
||||
|
||||
|
||||
def accept_and_keep_cursor(event: KeyPressEvent):
|
||||
"""Accept autosuggestion and keep cursor in place"""
|
||||
buffer = event.current_buffer
|
||||
old_position = buffer.cursor_position
|
||||
suggestion = buffer.suggestion
|
||||
if suggestion:
|
||||
buffer.insert_text(suggestion.text)
|
||||
buffer.cursor_position = old_position
|
||||
|
||||
|
||||
def accept_and_move_cursor_left(event: KeyPressEvent):
|
||||
"""Accept autosuggestion and move cursor left in place"""
|
||||
accept_and_keep_cursor(event)
|
||||
nc.backward_char(event)
|
||||
|
||||
|
||||
def _update_hint(buffer: Buffer):
|
||||
if buffer.auto_suggest:
|
||||
suggestion = buffer.auto_suggest.get_suggestion(buffer, buffer.document)
|
||||
buffer.suggestion = suggestion
|
||||
|
||||
|
||||
def backspace_and_resume_hint(event: KeyPressEvent):
|
||||
"""Resume autosuggestions after deleting last character"""
|
||||
nc.backward_delete_char(event)
|
||||
_update_hint(event.current_buffer)
|
||||
|
||||
|
||||
def resume_hinting(event: KeyPressEvent):
|
||||
"""Resume autosuggestions"""
|
||||
pass_through.reply(event)
|
||||
# Order matters: if update happened first and event reply second, the
|
||||
# suggestion would be auto-accepted if both actions are bound to same key.
|
||||
_update_hint(event.current_buffer)
|
||||
|
||||
|
||||
def up_and_update_hint(event: KeyPressEvent):
|
||||
"""Go up and update hint"""
|
||||
current_buffer = event.current_buffer
|
||||
|
||||
current_buffer.auto_up(count=event.arg)
|
||||
_update_hint(current_buffer)
|
||||
|
||||
|
||||
def down_and_update_hint(event: KeyPressEvent):
|
||||
"""Go down and update hint"""
|
||||
current_buffer = event.current_buffer
|
||||
|
||||
current_buffer.auto_down(count=event.arg)
|
||||
_update_hint(current_buffer)
|
||||
|
||||
|
||||
def accept_token(event: KeyPressEvent):
|
||||
"""Fill partial autosuggestion by token"""
|
||||
b = event.current_buffer
|
||||
suggestion = b.suggestion
|
||||
|
||||
if suggestion:
|
||||
prefix = _get_query(b.document)
|
||||
text = prefix + suggestion.text
|
||||
|
||||
tokens: List[Optional[str]] = [None, None, None]
|
||||
substrings = [""]
|
||||
i = 0
|
||||
|
||||
for token in generate_tokens(StringIO(text).readline):
|
||||
if token.type == tokenize.NEWLINE:
|
||||
index = len(text)
|
||||
else:
|
||||
index = text.index(token[1], len(substrings[-1]))
|
||||
substrings.append(text[:index])
|
||||
tokenized_so_far = substrings[-1]
|
||||
if tokenized_so_far.startswith(prefix):
|
||||
if i == 0 and len(tokenized_so_far) > len(prefix):
|
||||
tokens[0] = tokenized_so_far[len(prefix) :]
|
||||
substrings.append(tokenized_so_far)
|
||||
i += 1
|
||||
tokens[i] = token[1]
|
||||
if i == 2:
|
||||
break
|
||||
i += 1
|
||||
|
||||
if tokens[0]:
|
||||
to_insert: str
|
||||
insert_text = substrings[-2]
|
||||
if tokens[1] and len(tokens[1]) == 1:
|
||||
insert_text = substrings[-1]
|
||||
to_insert = insert_text[len(prefix) :]
|
||||
b.insert_text(to_insert)
|
||||
return
|
||||
|
||||
nc.forward_word(event)
|
||||
|
||||
|
||||
Provider = Union[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None]
|
||||
|
||||
|
||||
def _swap_autosuggestion(
|
||||
buffer: Buffer,
|
||||
provider: NavigableAutoSuggestFromHistory,
|
||||
direction_method: Callable,
|
||||
):
|
||||
"""
|
||||
We skip most recent history entry (in either direction) if it equals the
|
||||
current autosuggestion because if user cycles when auto-suggestion is shown
|
||||
they most likely want something else than what was suggested (otherwise
|
||||
they would have accepted the suggestion).
|
||||
"""
|
||||
suggestion = buffer.suggestion
|
||||
if not suggestion:
|
||||
return
|
||||
|
||||
query = _get_query(buffer.document)
|
||||
current = query + suggestion.text
|
||||
|
||||
direction_method(query=query, other_than=current, history=buffer.history)
|
||||
|
||||
new_suggestion = provider.get_suggestion(buffer, buffer.document)
|
||||
buffer.suggestion = new_suggestion
|
||||
|
||||
|
||||
def swap_autosuggestion_up(event: KeyPressEvent):
|
||||
"""Get next autosuggestion from history."""
|
||||
shell = get_ipython()
|
||||
provider = shell.auto_suggest
|
||||
|
||||
if not isinstance(provider, NavigableAutoSuggestFromHistory):
|
||||
return
|
||||
|
||||
return _swap_autosuggestion(
|
||||
buffer=event.current_buffer, provider=provider, direction_method=provider.up
|
||||
)
|
||||
|
||||
|
||||
def swap_autosuggestion_down(event: KeyPressEvent):
|
||||
"""Get previous autosuggestion from history."""
|
||||
shell = get_ipython()
|
||||
provider = shell.auto_suggest
|
||||
|
||||
if not isinstance(provider, NavigableAutoSuggestFromHistory):
|
||||
return
|
||||
|
||||
return _swap_autosuggestion(
|
||||
buffer=event.current_buffer,
|
||||
provider=provider,
|
||||
direction_method=provider.down,
|
||||
)
|
||||
|
||||
|
||||
def __getattr__(key):
|
||||
if key == "accept_in_vi_insert_mode":
|
||||
warnings.warn(
|
||||
"`accept_in_vi_insert_mode` is deprecated since IPython 8.12 and "
|
||||
"renamed to `accept_or_jump_to_end`. Please update your configuration "
|
||||
"accordingly",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return _deprected_accept_in_vi_insert_mode
|
||||
raise AttributeError
|
||||
@@ -0,0 +1,322 @@
|
||||
"""
|
||||
Filters restricting scope of IPython Terminal shortcuts.
|
||||
"""
|
||||
|
||||
# Copyright (c) IPython Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import ast
|
||||
import re
|
||||
import signal
|
||||
import sys
|
||||
from typing import Callable, Dict, Union
|
||||
|
||||
from prompt_toolkit.application.current import get_app
|
||||
from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER
|
||||
from prompt_toolkit.key_binding import KeyPressEvent
|
||||
from prompt_toolkit.filters import Condition, Filter, emacs_insert_mode, has_completions
|
||||
from prompt_toolkit.filters import has_focus as has_focus_impl
|
||||
from prompt_toolkit.filters import (
|
||||
Always,
|
||||
Never,
|
||||
has_selection,
|
||||
has_suggestion,
|
||||
vi_insert_mode,
|
||||
vi_mode,
|
||||
)
|
||||
from prompt_toolkit.layout.layout import FocusableElement
|
||||
|
||||
from IPython.core.getipython import get_ipython
|
||||
from IPython.core.guarded_eval import _find_dunder, BINARY_OP_DUNDERS, UNARY_OP_DUNDERS
|
||||
from IPython.terminal.shortcuts import auto_suggest
|
||||
from IPython.utils.decorators import undoc
|
||||
|
||||
|
||||
@undoc
|
||||
@Condition
|
||||
def cursor_in_leading_ws():
|
||||
before = get_app().current_buffer.document.current_line_before_cursor
|
||||
return (not before) or before.isspace()
|
||||
|
||||
|
||||
def has_focus(value: FocusableElement):
|
||||
"""Wrapper around has_focus adding a nice `__name__` to tester function"""
|
||||
tester = has_focus_impl(value).func
|
||||
tester.__name__ = f"is_focused({value})"
|
||||
return Condition(tester)
|
||||
|
||||
|
||||
@undoc
|
||||
@Condition
|
||||
def has_line_below() -> bool:
|
||||
document = get_app().current_buffer.document
|
||||
return document.cursor_position_row < len(document.lines) - 1
|
||||
|
||||
|
||||
@undoc
|
||||
@Condition
|
||||
def is_cursor_at_the_end_of_line() -> bool:
|
||||
document = get_app().current_buffer.document
|
||||
return document.is_cursor_at_the_end_of_line
|
||||
|
||||
|
||||
@undoc
|
||||
@Condition
|
||||
def has_line_above() -> bool:
|
||||
document = get_app().current_buffer.document
|
||||
return document.cursor_position_row != 0
|
||||
|
||||
|
||||
@Condition
|
||||
def ebivim():
|
||||
shell = get_ipython()
|
||||
return shell.emacs_bindings_in_vi_insert_mode
|
||||
|
||||
|
||||
@Condition
|
||||
def supports_suspend():
|
||||
return hasattr(signal, "SIGTSTP")
|
||||
|
||||
|
||||
@Condition
|
||||
def auto_match():
|
||||
shell = get_ipython()
|
||||
return shell.auto_match
|
||||
|
||||
|
||||
def all_quotes_paired(quote, buf):
|
||||
paired = True
|
||||
i = 0
|
||||
while i < len(buf):
|
||||
c = buf[i]
|
||||
if c == quote:
|
||||
paired = not paired
|
||||
elif c == "\\":
|
||||
i += 1
|
||||
i += 1
|
||||
return paired
|
||||
|
||||
|
||||
_preceding_text_cache: Dict[Union[str, Callable], Condition] = {}
|
||||
_following_text_cache: Dict[Union[str, Callable], Condition] = {}
|
||||
|
||||
|
||||
def preceding_text(pattern: Union[str, Callable]):
|
||||
if pattern in _preceding_text_cache:
|
||||
return _preceding_text_cache[pattern]
|
||||
|
||||
if callable(pattern):
|
||||
|
||||
def _preceding_text():
|
||||
app = get_app()
|
||||
before_cursor = app.current_buffer.document.current_line_before_cursor
|
||||
# mypy can't infer if(callable): https://github.com/python/mypy/issues/3603
|
||||
return bool(pattern(before_cursor)) # type: ignore[operator]
|
||||
|
||||
else:
|
||||
m = re.compile(pattern)
|
||||
|
||||
def _preceding_text():
|
||||
app = get_app()
|
||||
before_cursor = app.current_buffer.document.current_line_before_cursor
|
||||
return bool(m.match(before_cursor))
|
||||
|
||||
_preceding_text.__name__ = f"preceding_text({pattern!r})"
|
||||
|
||||
condition = Condition(_preceding_text)
|
||||
_preceding_text_cache[pattern] = condition
|
||||
return condition
|
||||
|
||||
|
||||
def following_text(pattern):
|
||||
try:
|
||||
return _following_text_cache[pattern]
|
||||
except KeyError:
|
||||
pass
|
||||
m = re.compile(pattern)
|
||||
|
||||
def _following_text():
|
||||
app = get_app()
|
||||
return bool(m.match(app.current_buffer.document.current_line_after_cursor))
|
||||
|
||||
_following_text.__name__ = f"following_text({pattern!r})"
|
||||
|
||||
condition = Condition(_following_text)
|
||||
_following_text_cache[pattern] = condition
|
||||
return condition
|
||||
|
||||
|
||||
@Condition
|
||||
def not_inside_unclosed_string():
|
||||
app = get_app()
|
||||
s = app.current_buffer.document.text_before_cursor
|
||||
# remove escaped quotes
|
||||
s = s.replace('\\"', "").replace("\\'", "")
|
||||
# remove triple-quoted string literals
|
||||
s = re.sub(r"(?:\"\"\"[\s\S]*\"\"\"|'''[\s\S]*''')", "", s)
|
||||
# remove single-quoted string literals
|
||||
s = re.sub(r"""(?:"[^"]*["\n]|'[^']*['\n])""", "", s)
|
||||
return not ('"' in s or "'" in s)
|
||||
|
||||
|
||||
@Condition
|
||||
def navigable_suggestions():
|
||||
shell = get_ipython()
|
||||
return isinstance(shell.auto_suggest, auto_suggest.NavigableAutoSuggestFromHistory)
|
||||
|
||||
|
||||
@Condition
|
||||
def readline_like_completions():
|
||||
shell = get_ipython()
|
||||
return shell.display_completions == "readlinelike"
|
||||
|
||||
|
||||
@Condition
|
||||
def is_windows_os():
|
||||
return sys.platform == "win32"
|
||||
|
||||
|
||||
class PassThrough(Filter):
|
||||
"""A filter allowing to implement pass-through behaviour of keybindings.
|
||||
|
||||
Prompt toolkit key processor dispatches only one event per binding match,
|
||||
which means that adding a new shortcut will suppress the old shortcut
|
||||
if the keybindings are the same (unless one is filtered out).
|
||||
|
||||
To stop a shortcut binding from suppressing other shortcuts:
|
||||
- add the `pass_through` filter to list of filter, and
|
||||
- call `pass_through.reply(event)` in the shortcut handler.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._is_replying = False
|
||||
|
||||
def reply(self, event: KeyPressEvent):
|
||||
self._is_replying = True
|
||||
try:
|
||||
event.key_processor.reset()
|
||||
event.key_processor.feed_multiple(event.key_sequence)
|
||||
event.key_processor.process_keys()
|
||||
finally:
|
||||
self._is_replying = False
|
||||
|
||||
def __call__(self):
|
||||
return not self._is_replying
|
||||
|
||||
|
||||
pass_through = PassThrough()
|
||||
|
||||
# these one is callable and re-used multiple times hence needs to be
|
||||
# only defined once beforhand so that transforming back to human-readable
|
||||
# names works well in the documentation.
|
||||
default_buffer_focused = has_focus(DEFAULT_BUFFER)
|
||||
|
||||
KEYBINDING_FILTERS = {
|
||||
"always": Always(),
|
||||
# never is used for exposing commands which have no default keybindings
|
||||
"never": Never(),
|
||||
"has_line_below": has_line_below,
|
||||
"has_line_above": has_line_above,
|
||||
"is_cursor_at_the_end_of_line": is_cursor_at_the_end_of_line,
|
||||
"has_selection": has_selection,
|
||||
"has_suggestion": has_suggestion,
|
||||
"vi_mode": vi_mode,
|
||||
"vi_insert_mode": vi_insert_mode,
|
||||
"emacs_insert_mode": emacs_insert_mode,
|
||||
# https://github.com/ipython/ipython/pull/12603 argued for inclusion of
|
||||
# emacs key bindings with a configurable `emacs_bindings_in_vi_insert_mode`
|
||||
# toggle; when the toggle is on user can access keybindigns like `ctrl + e`
|
||||
# in vi insert mode. Because some of the emacs bindings involve `escape`
|
||||
# followed by another key, e.g. `escape` followed by `f`, prompt-toolkit
|
||||
# needs to wait to see if there will be another character typed in before
|
||||
# executing pure `escape` keybinding; in vi insert mode `escape` switches to
|
||||
# command mode which is common and performance critical action for vi users.
|
||||
# To avoid the delay users employ a workaround:
|
||||
# https://github.com/ipython/ipython/issues/13443#issuecomment-1032753703
|
||||
# which involves switching `emacs_bindings_in_vi_insert_mode` off.
|
||||
#
|
||||
# For the workaround to work:
|
||||
# 1) end users need to toggle `emacs_bindings_in_vi_insert_mode` off
|
||||
# 2) all keybindings which would involve `escape` need to respect that
|
||||
# toggle by including either:
|
||||
# - `vi_insert_mode & ebivim` for actions which have emacs keybindings
|
||||
# predefined upstream in prompt-toolkit, or
|
||||
# - `emacs_like_insert_mode` for actions which do not have existing
|
||||
# emacs keybindings predefined upstream (or need overriding of the
|
||||
# upstream bindings to modify behaviour), defined below.
|
||||
"emacs_like_insert_mode": (vi_insert_mode & ebivim) | emacs_insert_mode,
|
||||
"has_completions": has_completions,
|
||||
"insert_mode": vi_insert_mode | emacs_insert_mode,
|
||||
"default_buffer_focused": default_buffer_focused,
|
||||
"search_buffer_focused": has_focus(SEARCH_BUFFER),
|
||||
# `ebivim` stands for emacs bindings in vi insert mode
|
||||
"ebivim": ebivim,
|
||||
"supports_suspend": supports_suspend,
|
||||
"is_windows_os": is_windows_os,
|
||||
"auto_match": auto_match,
|
||||
"focused_insert": (vi_insert_mode | emacs_insert_mode) & default_buffer_focused,
|
||||
"not_inside_unclosed_string": not_inside_unclosed_string,
|
||||
"readline_like_completions": readline_like_completions,
|
||||
"preceded_by_paired_double_quotes": preceding_text(
|
||||
lambda line: all_quotes_paired('"', line)
|
||||
),
|
||||
"preceded_by_paired_single_quotes": preceding_text(
|
||||
lambda line: all_quotes_paired("'", line)
|
||||
),
|
||||
"preceded_by_raw_str_prefix": preceding_text(r".*(r|R)[\"'](-*)$"),
|
||||
"preceded_by_two_double_quotes": preceding_text(r'^.*""$'),
|
||||
"preceded_by_two_single_quotes": preceding_text(r"^.*''$"),
|
||||
"followed_by_closing_paren_or_end": following_text(r"[,)}\]]|$"),
|
||||
"preceded_by_opening_round_paren": preceding_text(r".*\($"),
|
||||
"preceded_by_opening_bracket": preceding_text(r".*\[$"),
|
||||
"preceded_by_opening_brace": preceding_text(r".*\{$"),
|
||||
"preceded_by_double_quote": preceding_text('.*"$'),
|
||||
"preceded_by_single_quote": preceding_text(r".*'$"),
|
||||
"followed_by_closing_round_paren": following_text(r"^\)"),
|
||||
"followed_by_closing_bracket": following_text(r"^\]"),
|
||||
"followed_by_closing_brace": following_text(r"^\}"),
|
||||
"followed_by_double_quote": following_text('^"'),
|
||||
"followed_by_single_quote": following_text("^'"),
|
||||
"navigable_suggestions": navigable_suggestions,
|
||||
"cursor_in_leading_ws": cursor_in_leading_ws,
|
||||
"pass_through": pass_through,
|
||||
}
|
||||
|
||||
|
||||
def eval_node(node: Union[ast.AST, None]):
|
||||
if node is None:
|
||||
return None
|
||||
if isinstance(node, ast.Expression):
|
||||
return eval_node(node.body)
|
||||
if isinstance(node, ast.BinOp):
|
||||
left = eval_node(node.left)
|
||||
right = eval_node(node.right)
|
||||
dunders = _find_dunder(node.op, BINARY_OP_DUNDERS)
|
||||
if dunders:
|
||||
return getattr(left, dunders[0])(right)
|
||||
raise ValueError(f"Unknown binary operation: {node.op}")
|
||||
if isinstance(node, ast.UnaryOp):
|
||||
value = eval_node(node.operand)
|
||||
dunders = _find_dunder(node.op, UNARY_OP_DUNDERS)
|
||||
if dunders:
|
||||
return getattr(value, dunders[0])()
|
||||
raise ValueError(f"Unknown unary operation: {node.op}")
|
||||
if isinstance(node, ast.Name):
|
||||
if node.id in KEYBINDING_FILTERS:
|
||||
return KEYBINDING_FILTERS[node.id]
|
||||
else:
|
||||
sep = "\n - "
|
||||
known_filters = sep.join(sorted(KEYBINDING_FILTERS))
|
||||
raise NameError(
|
||||
f"{node.id} is not a known shortcut filter."
|
||||
f" Known filters are: {sep}{known_filters}."
|
||||
)
|
||||
raise ValueError("Unhandled node", ast.dump(node))
|
||||
|
||||
|
||||
def filter_from_string(code: str):
|
||||
expression = ast.parse(code, mode="eval")
|
||||
return eval_node(expression)
|
||||
|
||||
|
||||
__all__ = ["KEYBINDING_FILTERS", "filter_from_string"]
|
||||
Reference in New Issue
Block a user