1
0
Fork 0

Refactoring of the key input system.

- Registry which contains the list of key bindings
- Clean up of existing key bindings.
- Key binding manager: utility for managing Vi and Emacs bindings
- Rename of the `Line` class to `Buffer`.
- Added filters.py: for enabling/disabling part of the system at
  runtime.
- Added a focus_stack, a tool for redirecting the input from one buffer
  to another.
This commit is contained in:
Jonathan Slenders 2014-12-07 21:16:25 +01:00
parent 95818c0fbf
commit 0d0a5566a8
53 changed files with 3018 additions and 2601 deletions

View File

@ -33,18 +33,18 @@ Architecture
| ============ |
| - Every key binding consists of a function that |
| receives an `Event` and usually it operates on |
| the `Line` object. (It could insert data or move |
| the cursor for example.) |
| the `Buffer` object. (It could insert data or |
| move the cursor for example.) |
+---------------------------------------------------------------+
|
| Most of the key bindings operate on a `Line` object, but
| Most of the key bindings operate on a `Buffer` object, but
| they don't have to. They could also change the visibility
| of a menu for instance, or change the color scheme.
|
v
+---------------------------------------------------------------+
| Line |
| ==== |
| Buffer |
| ====== |
| - Contains a data structure to hold the current |
| input (text and cursor position). This class |
| implements all text manipulations and cursor |
@ -55,7 +55,7 @@ Architecture
| | Document (text, cursor_position) | |
| | ================================ | |
| | Accessed as the `document` property of the | |
| | `Line` class. This is a wrapper around the | |
| | `Buffer` class. This is a wrapper around the | |
| | text and cursor position, and contains | |
| | methods for querying this data , e.g. to give | |
| | the text before the cursor. | |

View File

@ -10,7 +10,7 @@ Press [Tab] to complete the current word.
from __future__ import unicode_literals
from prompt_toolkit import CommandLineInterface
from prompt_toolkit.line import Line
from prompt_toolkit.buffer import Buffer
from prompt_toolkit.layout import Layout
from prompt_toolkit.layout.prompt import DefaultPrompt
from prompt_toolkit.layout.menus import CompletionsMenu
@ -70,7 +70,7 @@ def main():
style=AnimalStyle,
layout=Layout(before_input=DefaultPrompt('Give some animals: '),
menus=[CompletionsMenu()]),
line=Line(completer=animal_completer),
buffer=Buffer(completer=animal_completer),
create_async_autocompleters=True)
print('Press tab to complete')

41
examples/custom-key-binding.py Executable file
View File

@ -0,0 +1,41 @@
#!/usr/bin/env python
"""
Example of adding a custom key binding to a prompt.
"""
from prompt_toolkit import CommandLineInterface
from prompt_toolkit.key_binding.manager import KeyBindingManager
from prompt_toolkit.keys import Keys
def main():
# We start with a `KeyBindingManager` instance, because this will already
# nicely load all the default key bindings.
key_bindings_manager = KeyBindingManager()
# Add our own key binding to the registry of the key bindings manager.
@key_bindings_manager.registry.add_binding(Keys.F4)
def _(event):
"""
When F4 has been pressed. Insert "hello world" as text.
"""
event.cli.current_buffer.insert_text('hello world')
@key_bindings_manager.registry.add_binding('x', 'y')
def _(event):
"""
(Useless, but for demoing.)
Typing 'xy' will insert 'z'.
"""
event.cli.current_buffer.insert_text('z')
# Create a CLI with the key bindings registry of this manager.
cli = CommandLineInterface(key_bindings_registry=key_bindings_manager.registry)
# Read input.
print('Press F4 to insert "hello world", type "xy" to insert "z":')
code_obj = cli.read_input()
print('You said: ' + code_obj.text)
if __name__ == '__main__':
main()

View File

@ -9,7 +9,7 @@ from prompt_toolkit.layout import Layout
from prompt_toolkit.layout.prompt import DefaultPrompt
from prompt_toolkit.layout.toolbars import ValidationToolbar
from prompt_toolkit.validation import Validator, ValidationError
from prompt_toolkit.line import Line
from prompt_toolkit.buffer import Buffer
from pygments.token import Token
from pygments.style import Style
@ -27,10 +27,6 @@ class EmailValidator(Validator):
raise ValidationError(message='Not a valid e-mail address')
class TestLine(Line):
is_multiline = True
class TestStyle(Style):
styles = {
Token.Toolbar.Validation: 'bg:#aa0000 #ffbbbb',
@ -38,7 +34,10 @@ class TestStyle(Style):
def main():
cli = CommandLineInterface(layout=layout, style=TestStyle, line=Line(validator=EmailValidator()))
cli = CommandLineInterface(
layout=layout,
style=TestStyle,
buffer=Buffer(validator=EmailValidator()))
document = cli.read_input()
print('You said: ' + document.text)

View File

@ -5,13 +5,14 @@ Simple example of the layout options.
from __future__ import unicode_literals
from prompt_toolkit import CommandLineInterface
from prompt_toolkit.buffer import Buffer
from prompt_toolkit.completion import Completion, Completer
from prompt_toolkit.document import Document
from prompt_toolkit.layout import Layout
from prompt_toolkit.layout.prompt import DefaultPrompt, Prompt
from prompt_toolkit.layout.margins import LeftMarginWithLineNumbers
from prompt_toolkit.layout.menus import CompletionsMenu
from prompt_toolkit.layout.prompt import DefaultPrompt, Prompt
from prompt_toolkit.layout.toolbars import TextToolbar, ArgToolbar, SearchToolbar, CompletionsToolbar
from prompt_toolkit.line import Line
from prompt_toolkit.completion import Completion, Completer
from pygments.token import Token
from pygments.style import Style
@ -86,9 +87,9 @@ def main():
cli = CommandLineInterface(layout=layout,
style=TestStyle,
line=Line(is_multiline=True, completer=TestCompleter()))
buffer=Buffer(is_multiline=True, completer=TestCompleter()))
code_obj = cli.read_input(initial_value=lipsum)
code_obj = cli.read_input(initial_document=Document(lipsum))
print('You said: ' + code_obj.text)

View File

@ -4,12 +4,13 @@ Simple example of a CLI that keeps a persistent history of all the entered
strings in a file.
"""
from prompt_toolkit import CommandLineInterface, AbortAction, Exit
from prompt_toolkit.line import Line
from prompt_toolkit.buffer import Buffer
from prompt_toolkit.history import FileHistory
def main():
cli = CommandLineInterface(line=Line(history=FileHistory('.example-history-file')))
cli = CommandLineInterface(
buffer=Buffer(history=FileHistory('.example-history-file')))
try:
while True:

View File

@ -21,10 +21,10 @@ from prompt_toolkit.contrib.regular_languages.compiler import compile
from prompt_toolkit.contrib.regular_languages.completion import GrammarCompleter
from prompt_toolkit.contrib.regular_languages.lexer import GrammarLexer
from prompt_toolkit.buffer import Buffer
from prompt_toolkit.layout import Layout
from prompt_toolkit.layout.menus import CompletionsMenu
from prompt_toolkit.layout.prompt import DefaultPrompt
from prompt_toolkit.line import Line
from pygments.style import Style
from pygments.token import Token
@ -74,7 +74,7 @@ if __name__ == '__main__':
'var2': Token.Number
}),
menus=[CompletionsMenu()]),
line=Line(completer=GrammarCompleter(g, {
buffer=Buffer(completer=GrammarCompleter(g, {
'operator1': WordCompleter(operators1),
'operator2': WordCompleter(operators2),
})),

View File

@ -104,14 +104,14 @@ Let's get started!
populate the completion menu with possible candidates from the list
of ``keywords``.
This ``SqlCompleter`` class will be passed into the ``prompt_toolkit.Line`` class
This ``SqlCompleter`` class will be passed into the ``prompt_toolkit.Buffer`` class
which controls the cusor position and completion of a line.
.. code:: python
from prompt_toolkit import CommandLineInterface, AbortAction, Exit
from prompt_toolkit.layout import Layout
from prompt_toolkit.line import Line
from prompt_toolkit.buffer import Buffer
from prompt_toolkit.layout.prompt import DefaultPrompt
from prompt_toolkit.layout.menus import CompletionsMenu
from prompt_toolkit.completion import Completion, Completer
@ -131,7 +131,7 @@ Let's get started!
def main():
layout = Layout(before_input=DefaultPrompt('> '),
lexer=SqlLexer, menus=[CompletionsMenu()])
line = Line(completer=SqlCompleter())
buffer = Buffer(completer=SqlCompleter())
cli = CommandLineInterface(layout=layout, line=line)
try:
while True:
@ -159,7 +159,7 @@ Let's get started!
from prompt_toolkit import CommandLineInterface, AbortAction, Exit
from prompt_toolkit.layout import Layout
from prompt_toolkit.line import Line
from prompt_toolkit.buffer import Buffer
from prompt_toolkit.layout.prompt import DefaultPrompt
from prompt_toolkit.layout.menus import CompletionsMenu
from prompt_toolkit.completion import Completion, Completer
@ -191,8 +191,8 @@ Let's get started!
def main():
layout = Layout(before_input=DefaultPrompt('> '),
lexer=SqlLexer, menus=[CompletionsMenu()])
line = Line(completer=SqlCompleter())
cli = CommandLineInterface(style=DocumentStyle, layout=layout, line=line)
buffer = Buffer(completer=SqlCompleter())
cli = CommandLineInterface(style=DocumentStyle, layout=layout, buffer=buffer)
try:
while True:
document = cli.read_input(on_exit=AbortAction.RAISE_EXCEPTION)
@ -223,7 +223,7 @@ Let's get started!
from prompt_toolkit import CommandLineInterface, AbortAction, Exit
from prompt_toolkit.layout import Layout
from prompt_toolkit.line import Line
from prompt_toolkit.buffer import Buffer
from prompt_toolkit.layout.prompt import DefaultPrompt
from prompt_toolkit.layout.menus import CompletionsMenu
from prompt_toolkit.completion import Completion, Completer
@ -256,8 +256,8 @@ Let's get started!
connection = sqlite3.connect(database)
layout = Layout(before_input=DefaultPrompt('> '),
lexer=SqlLexer, menus=[CompletionsMenu()])
line = Line(completer=SqlCompleter())
cli = CommandLineInterface(style=DocumentStyle, layout=layout, line=line)
buffer = Buffer(completer=SqlCompleter())
cli = CommandLineInterface(style=DocumentStyle, layout=layout, buffer=buffer)
try:
while True:
document = cli.read_input(on_exit=AbortAction.RAISE_EXCEPTION)

View File

@ -4,7 +4,7 @@ import sqlite3
from prompt_toolkit import CommandLineInterface, AbortAction, Exit
from prompt_toolkit.layout import Layout
from prompt_toolkit.line import Line
from prompt_toolkit.buffer import Buffer
from prompt_toolkit.layout.prompt import DefaultPrompt
from prompt_toolkit.layout.menus import CompletionsMenu
from prompt_toolkit.completion import Completion, Completer
@ -41,8 +41,8 @@ def main(database):
connection = sqlite3.connect(database)
layout = Layout(before_input=DefaultPrompt('> '),
lexer=SqlLexer, menus=[CompletionsMenu()])
line = Line(completer=SqlCompleter())
cli = CommandLineInterface(style=DocumentStyle, layout=layout, line=line)
buffer = Buffer(completer=SqlCompleter())
cli = CommandLineInterface(style=DocumentStyle, layout=layout, buffer=buffer)
try:
while True:
document = cli.read_input(on_exit=AbortAction.RAISE_EXCEPTION)

View File

@ -12,14 +12,14 @@ import sys
import weakref
from .completion import CompleteEvent
from .enums import InputMode
from .focus_stack import FocusStack
from .history import History
from .key_binding import InputProcessor
from .key_binding import Registry
from .key_bindings.emacs import emacs_bindings
from .key_binding.input_processor import InputProcessor
from .key_binding.registry import Registry
from .key_binding.bindings.emacs import load_emacs_bindings
from .layout import Layout
from .layout.prompt import DefaultPrompt
from .line import Line
from .buffer import Buffer
from .renderer import Renderer
from .utils import EventHook, DummyContext
@ -81,48 +81,20 @@ class CommandLineInterface(object):
"""
def __init__(self, stdin=None, stdout=None,
layout=None,
line=None,
buffer=None,
buffers=None,
style=DefaultStyle,
key_binding_factories=None,
key_bindings_registry=None,
create_async_autocompleters=True,
renderer_factory=Renderer):
assert buffer is None or isinstance(buffer, Buffer)
assert buffers is None or isinstance(buffers, dict)
self.stdin = stdin or sys.__stdin__
self.stdout = stdout or sys.__stdout__
self.style = style
#: The `Line` instance.
line = line or Line()
self.lines = {
'default': line,
'search': Line(history=History()),
'system': Line(history=History()),
}
#: The `Layout` instance.
self.layout = layout or Layout(before_input=DefaultPrompt())
#: The `Renderer` instance.
self.renderer = renderer_factory(layout=self.layout,
stdout=self.stdout,
style=self.style)
key_binding_factories = key_binding_factories or [emacs_bindings]
#: The `InputProcessor` instance.
self.input_processor = self._create_input_processor(key_binding_factories)
# Handle events.
if create_async_autocompleters:
for l in self.lines.values():
if l.completer:
l.onTextInsert += self._create_async_completer(l)
self._reset()
# Event loop.
self.eventloop = None
# Events
#: Called when there is no input for x seconds.
@ -131,27 +103,55 @@ class CommandLineInterface(object):
self.onReadInputStart = EventHook()
self.onReadInputEnd = EventHook()
self.onReset = EventHook()
# Focus stack.
self.focus_stack = FocusStack(initial='default')
#: The input buffers.
self.buffers = {
'default': (buffer or Buffer()),
'search': Buffer(history=History()),
'system': Buffer(history=History()),
}
if buffers:
self.buffers.update(buffers)
#: The `Layout` instance.
self.layout = layout or Layout(before_input=DefaultPrompt())
#: The `Renderer` instance.
self.renderer = renderer_factory(layout=self.layout,
stdout=self.stdout,
style=self.style)
if key_bindings_registry is None:
key_bindings_registry = Registry()
load_emacs_bindings(key_bindings_registry)
#: The `InputProcessor` instance.
self.input_processor = InputProcessor(key_bindings_registry, weakref.ref(self))
# Handle events.
if create_async_autocompleters:
for b in self.buffers.values():
if b.completer:
b.onTextInsert += self._create_async_completer(b)
self._reset()
# Event loop.
self.eventloop = None
@property
def is_reading_input(self):
return bool(self.eventloop)
@property
def line(self):
return self.lines['default']
def current_buffer(self):
return self.buffers[self.focus_stack.current]
def _create_input_processor(self, key_binding_factories):
"""
Create :class:`InputProcessor` instance.
"""
key_registry = Registry()
for kb in key_binding_factories:
kb(key_registry, weakref.ref(self))
#: The `InputProcessor` instance.
return InputProcessor(key_registry)
def _reset(self, initial_document=None, initial_input_mode=InputMode.INSERT):
def _reset(self, initial_document=None):
"""
Reset everything.
"""
@ -159,14 +159,18 @@ class CommandLineInterface(object):
self._abort_flag = False
self._return_value = None
for l in self.lines.values():
l.reset()
for b in self.buffers.values():
b.reset()
self.line.reset(initial_document=initial_document)
self.focus_stack.reset()
self.current_buffer.reset(initial_document=initial_document)
self.renderer.reset()
self.input_processor.reset(initial_input_mode=initial_input_mode)
self.input_processor.reset()
self.layout.reset()
# Trigger reset event.
self.onReset.fire()
def request_redraw(self):
"""
Thread safe way of sending a repaint trigger to the input event loop.
@ -220,7 +224,7 @@ class CommandLineInterface(object):
self.renderer.request_absolute_cursor_position()
self.call_from_executor(do_in_event_loop)
def read_input(self, initial_document=None, initial_input_mode=InputMode.INSERT,
def read_input(self, initial_document=None,
on_abort=AbortAction.RETRY, on_exit=AbortAction.IGNORE):
"""
Read input string from command line.
@ -231,7 +235,6 @@ class CommandLineInterface(object):
"""
eventloop = EventLoop(self.input_processor, self.stdin)
g = self._read_input(initial_document=initial_document,
initial_input_mode=initial_input_mode,
on_abort=on_abort,
on_exit=on_exit,
eventloop=eventloop,
@ -244,7 +247,7 @@ class CommandLineInterface(object):
except StopIteration as e:
return e.args[0]
def read_input_async(self, initial_document=None, initial_input_mode=InputMode.INSERT,
def read_input_async(self, initial_document=None,
on_abort=AbortAction.RETRY, on_exit=AbortAction.IGNORE):
"""
Same as `read_input`, but this returns an asyncio coroutine.
@ -259,13 +262,12 @@ class CommandLineInterface(object):
eventloop = AsyncioEventLoop(self.input_processor, self.stdin)
return self._read_input(initial_document=initial_document,
initial_input_mode=initial_input_mode,
on_abort=on_abort,
on_exit=on_exit,
eventloop=eventloop,
return_corountine=True)
def _read_input(self, initial_document, initial_input_mode, on_abort, on_exit,
def _read_input(self, initial_document, on_abort, on_exit,
eventloop, return_corountine):
"""
The implementation of ``read_input`` which can be called both
@ -283,8 +285,7 @@ class CommandLineInterface(object):
try:
def reset():
self._reset(initial_document=initial_document,
initial_input_mode=initial_input_mode)
self._reset(initial_document=initial_document)
reset()
# Trigger onReadInputStart event.
@ -435,7 +436,7 @@ class CommandLineInterface(object):
"""
return self._return_value is not None
def _create_async_completer(self, line):
def _create_async_completer(self, buffer):
"""
Create function for asynchronous autocompletion while typing.
(Autocomplete in other thread.)
@ -443,25 +444,25 @@ class CommandLineInterface(object):
complete_thread_running = [False] # By ref.
def async_completer():
document = line.document
document = buffer.document
# Don't start two threads at the same time.
if complete_thread_running[0]:
return
# Don't complete when we already have completions.
if line.complete_state:
if buffer.complete_state:
return
# Don't automatically complete on empty inputs.
if not line.text:
if not buffer.text:
return
# Otherwise, get completions in other thread.
complete_thread_running[0] = True
def run():
completions = list(line.completer.get_completions(
completions = list(buffer.completer.get_completions(
document,
CompleteEvent(text_inserted=True)))
complete_thread_running[0] = False
@ -474,10 +475,10 @@ class CommandLineInterface(object):
was changed in the meantime.
"""
# Set completions if the text was not yet changed.
if line.text == document.text and \
line.cursor_position == document.cursor_position and \
not line.complete_state:
line._start_complete(go_to_first=False, completions=completions)
if buffer.text == document.text and \
buffer.cursor_position == document.cursor_position and \
not buffer.complete_state:
buffer._start_complete(go_to_first=False, completions=completions)
self._redraw()
else:
# Otherwise, restart thread.

View File

@ -1,5 +1,5 @@
"""
Data structures for the line input.
Data structures for the Buffer.
It holds the text, cursor position, history, etc...
"""
from __future__ import unicode_literals
@ -19,7 +19,7 @@ import tempfile
__all__ = (
'ClipboardData',
'Line',
'Buffer',
'SelectionType',
'indent',
'unindent',
@ -104,7 +104,7 @@ class _IncrementalSearchState(object):
self.isearch_direction = direction
class Line(object):
class Buffer(object):
"""
The core data structure that holds the text and cursor position of the
current input line and implements all text manupulations on top of it. It
@ -118,17 +118,30 @@ class Line(object):
:attr tempfile_suffix: Suffix to be appended to the tempfile for the 'open
in editor' function.
:attr is_multiline: Boolean to indicate whether we should consider this
line a multiline input. If so, the `InputStreamHandler`
buffer a multiline input. If so, the `InputStreamHandler`
can decide to insert newlines when pressing [Enter].
(Instead of accepting the input.)
This can also be a callable that takes a `Document` and
returns either True or False,
"""
def __init__(self, completer=None, history=None, validator=None, tempfile_suffix='', is_multiline=False):
def __init__(self, completer=None, history=None, validator=None, tempfile_suffix='', is_multiline=None):
assert is_multiline is None or callable(is_multiline) or isinstance(is_multiline, bool)
self.completer = completer
self.validator = validator
self.is_multiline = is_multiline
self.tempfile_suffix = tempfile_suffix
#: The command line history.
# Is multiline. (can be dynamic or static.)
if is_multiline is not None:
if callable(is_multiline):
self._is_multiline = is_multiline
elif isinstance(is_multiline, bool):
self._is_multiline = lambda document: is_multiline
else:
self._is_multiline = lambda document: False
#: The command buffer history.
# Note that we shouldn't use a lazy 'or' here. bool(history) could be
# False when empty.
self._history = History() if history is None else history
@ -144,7 +157,17 @@ class Line(object):
self.reset()
def reset(self, initial_document=None):
@property
def is_multiline(self):
return self._is_multiline(self.document)
def reset(self, initial_document=None, append_to_history=False):
"""
:param append_to_history: Append current input to history first.
"""
if append_to_history:
self.add_to_history()
initial_document = initial_document or Document()
self.cursor_position = initial_document.cursor_position
@ -196,15 +219,16 @@ class Line(object):
def cursor_position(self, value):
self.__cursor_position = max(0, value)
# Remove any validation errors and complete state.
self.validation_error = None
self.complete_state = None
if value != self.__cursor_position:
# Remove any validation errors and complete state.
self.validation_error = None
self.complete_state = None
# Note that the cursor position can change if we have a selection the
# new position of the cursor determines the end of the selection.
# Note that the cursor position can change if we have a selection the
# new position of the cursor determines the end of the selection.
# fire 'onCursorPositionChanged' event.
self.onCursorPositionChanged.fire()
# fire 'onCursorPositionChanged' event.
self.onCursorPositionChanged.fire()
@property
def working_index(self):
@ -849,24 +873,24 @@ class Line(object):
pass
def indent(line, from_row, to_row, count=1):
def indent(buffer, from_row, to_row, count=1):
"""
Indent text of the `Line` object.
Indent text of the `Buffer` object.
"""
current_row = line.document.cursor_position_row
current_row = buffer.document.cursor_position_row
line_range = range(from_row, to_row)
line.transform_lines(line_range, lambda l: ' ' * count + l)
buffer.transform_lines(line_range, lambda l: ' ' * count + l)
line.cursor_position = line.document.translate_row_col_to_index(current_row, 0)
line.cursor_position += line.document.get_start_of_line_position(after_whitespace=True)
buffer.cursor_position = buffer.document.translate_row_col_to_index(current_row, 0)
buffer.cursor_position += buffer.document.get_start_of_line_position(after_whitespace=True)
def unindent(line, from_row, to_row, count=1):
def unindent(buffer, from_row, to_row, count=1):
"""
Unindent text of the `Line` object.
Unindent text of the `Buffer` object.
"""
current_row = line.document.cursor_position_row
current_row = buffer.document.cursor_position_row
line_range = range(from_row, to_row)
def transform(text):
@ -876,7 +900,7 @@ def unindent(line, from_row, to_row, count=1):
else:
return text.lstrip()
line.transform_lines(line_range, transform)
buffer.transform_lines(line_range, transform)
line.cursor_position = line.document.translate_row_col_to_index(current_row, 0)
line.cursor_position += line.document.get_start_of_line_position(after_whitespace=True)
buffer.cursor_position = buffer.document.translate_row_col_to_index(current_row, 0)
buffer.cursor_position += buffer.document.get_start_of_line_position(after_whitespace=True)

View File

@ -65,7 +65,7 @@ class Completer(object):
"""
Base class for Code implementations.
The methods in here are methods that are expected to exist for the `Line`
The methods in here are methods that are expected to exist for the `Buffer`
and `Renderer` classes.
"""
__metaclass__ = ABCMeta

View File

@ -0,0 +1,27 @@
grammar = """
# Ignore leading whitespace.
.*
# The command itself.
(?P<command>[a-z-]+)
# Any number between 0-8 parameters.
(\s+ (?P<var1>[^\s]+)
(\s+ (?P<var2>[^\s]+)
(\s+ (?P<var3>[^\s]+)
(\s+ (?P<var4>[^\s]+)
(\s+ (?P<var5>[^\s]+)
(\s+ (?P<var6>[^\s]+)
(\s+ (?P<var7>[^\s]+)
(\s+ (?P<var8>[^\s]+)?)
)?
)?
)?
)?
)?
)?
)?
# Ignore trailing whitespace.
.*
"""

View File

@ -14,25 +14,27 @@ from pygments.token import Token
from prompt_toolkit import CommandLineInterface, AbortAction, Exit
from prompt_toolkit.contrib.python_input import PythonCompleter, PythonValidator
from prompt_toolkit.buffer import Buffer
from prompt_toolkit.contrib.python_input import PythonCompleter, PythonValidator, document_is_multiline_python, PythonToolbar, PythonCLISettings, load_python_bindings
from prompt_toolkit.contrib.regular_languages.completion import GrammarCompleter
from prompt_toolkit.contrib.regular_languages.lexer import GrammarLexer
from prompt_toolkit.contrib.regular_languages.validation import GrammarValidator
from prompt_toolkit.document import Document
from prompt_toolkit.history import FileHistory
from prompt_toolkit.key_bindings.emacs import emacs_bindings
from prompt_toolkit.key_binding.manager import KeyBindingManager
from prompt_toolkit.keys import Keys
from prompt_toolkit.layout import Layout
from prompt_toolkit.layout.menus import CompletionsMenu
from prompt_toolkit.layout.toolbars import SystemToolbar, ValidationToolbar, ArgToolbar, SearchToolbar
from prompt_toolkit.line import Line
from .commands import commands_with_help, shortcuts
from .completers import PythonFileCompleter, PythonFunctionCompleter, BreakPointListCompleter, AliasCompleter, PdbCommandsCompleter
from .completion_hints import CompletionHint
from .grammar import create_pdb_grammar
from .key_bindings import custom_pdb_key_bindings
from .prompt import PdbPrompt
from .key_bindings import load_custom_pdb_key_bindings
from .layout import PdbLeftMargin
from .style import PdbStyle
from .toolbars import SourceCodeToolbar, ShortcutsToolbar, PdbStatusToolbar
from .toolbars import SourceCodeToolbar, PdbStatusToolbar
import os
import pdb
@ -55,6 +57,19 @@ class PtPdb(pdb.Pdb):
self._cli_history = FileHistory(os.path.expanduser('~/.ptpdb_history'))
self.python_cli_settings = PythonCLISettings()
# The key bindings manager. We reuse it between Pdb calls, in order to
# remember vi/emacs state, etc..)
self.key_bindings_manager = self._create_key_bindings_manager(self.python_cli_settings)
def _create_key_bindings_manager(self, settings):
key_bindings_manager = KeyBindingManager()
load_custom_pdb_key_bindings(key_bindings_manager.registry)
load_python_bindings(key_bindings_manager, settings)
return key_bindings_manager
def cmdloop(self, intro=None):
"""
Copy/Paste of pdb.Pdb.cmdloop. But using our own CommandLineInterface
@ -109,9 +124,28 @@ class PtPdb(pdb.Pdb):
"""
g = self._create_grammar()
# Reset multiline/paste mode every time.
self.python_cli_settings.paste_mode = False
self.python_cli_settings.currently_multiline = False
# Make sure not to start in Vi navigation mode.
self.key_bindings_manager.reset()
def is_multiline(document):
if (self.python_cli_settings.paste_mode or
self.python_cli_settings.currently_multiline):
return True
match = g.match_prefix(document.text)
if match:
for v in match.variables().getall('python_code'):
if document_is_multiline_python(Document(v)):
return True
return False
cli = CommandLineInterface(
layout=Layout(
before_input=PdbPrompt(self._get_current_pdb_commands()),
left_margin=PdbLeftMargin(self._get_current_pdb_commands()),
show_tildes=True,
min_height=15,
lexer=GrammarLexer(
@ -128,10 +162,10 @@ class PtPdb(pdb.Pdb):
SearchToolbar(),
SourceCodeToolbar(weakref.ref(self)),
ValidationToolbar(),
ShortcutsToolbar(),
PdbStatusToolbar(weakref.ref(self)),
PdbStatusToolbar(weakref.ref(self), self.key_bindings_manager),
PythonToolbar(self.key_bindings_manager, self.python_cli_settings),
]),
line=Line(
buffer=Buffer(
completer=GrammarCompleter(g, completers={
'enabled_breakpoint': BreakPointListCompleter(only_enabled=True),
'disabled_breakpoint': BreakPointListCompleter(only_disabled=True),
@ -146,8 +180,9 @@ class PtPdb(pdb.Pdb):
validator=GrammarValidator(g, {
'python_code': PythonValidator()
}),
is_multiline=is_multiline,
),
key_binding_factories=[emacs_bindings, custom_pdb_key_bindings],
key_bindings_registry=self.key_bindings_manager.registry,
style=PdbStyle)
try:

View File

@ -12,7 +12,7 @@ class CompletionHint(object):
screen.write_highlighted(self._tokens(cli))
def _tokens(self, cli):
words = cli.line.document.text.split()
words = cli.buffers['default'].document.text.split()
if len(words) == 1:
word = words[0]

View File

@ -2,31 +2,26 @@ from __future__ import unicode_literals
from prompt_toolkit.keys import Keys
def custom_pdb_key_bindings(registry, cli_ref):
def load_custom_pdb_key_bindings(registry):
"""
Custom key bindings.
"""
line = cli_ref().line
handle = registry.add_binding
def return_text(text):
line.text = text
line.cursor_position = len(text)
cli_ref().set_return_value(line.document)
@handle(Keys.F6)
def _(event):
# TODO: Open REPL
pass
@handle(Keys.F7)
def _(event):
return_text('step')
def return_text(event, text):
buffer = event.cli.buffers['default']
buffer.text = text
buffer.cursor_position = len(text)
event.cli.set_return_value(buffer.document)
@handle(Keys.F8)
def _(event):
return_text('next')
return_text(event, 'step')
@handle(Keys.F9)
def _(event):
return_text('continue')
return_text(event, 'next')
@handle(Keys.F10)
def _(event):
return_text(event, 'continue')

View File

@ -0,0 +1,36 @@
from __future__ import unicode_literals, absolute_import
from prompt_toolkit.layout.margins import LeftMarginWithLineNumbers
from pygments.token import Token
class PdbLeftMargin(LeftMarginWithLineNumbers):
"""
Pdb prompt.
Show "(pdb)" when we have a pdb command or '>>>' when the user types a
Python command.
"""
def __init__(self, pdb_commands):
super(PdbLeftMargin, self).__init__()
self.pdb_commands = pdb_commands
def width(self, cli):
return 6
def write(self, cli, screen, y, line_number):
if y == 0:
screen.write_highlighted(self._first_line(cli))
else:
super(PdbLeftMargin, self).write(cli, screen, y, line_number)
def _first_line(self, cli):
# Get the first word entered.
command = cli.buffers['default'].document.text.lstrip()
if command:
command = command.split()[0]
if any(c.startswith(command) for c in self.pdb_commands):
return [(Token.Prompt, '(pdb) ')]
else:
return [(Token.Prompt, ' >>> ')]

View File

@ -1,25 +0,0 @@
from __future__ import unicode_literals, absolute_import
from prompt_toolkit.layout.prompt import Prompt
class PdbPrompt(Prompt):
"""
Pdb prompt.
Show "(pdb)" when we have a pdb command or '>>>' when the user types a
Python command.
"""
def __init__(self, pdb_commands):
super(PdbPrompt, self).__init__()
self.pdb_commands = pdb_commands
def tokens(self, cli):
# Get the first word entered.
command = cli.line.document.text.lstrip()
if command:
command = command.split()[0]
if any(c.startswith(command) for c in self.pdb_commands):
return [(self.token, '(pdb) ')]
else:
return [(self.token, ' >>> ')]

View File

@ -6,14 +6,13 @@ from pygments.token import Token
class PdbStyle(PythonStyle):
styles = {
# Pdb tokens.
Token.Prompt.BeforeInput: 'bold #008800',
Token.PdbCommand: 'bold',
Token.CompletionHint.Symbol: '#9a8888',
Token.CompletionHint.Parameter: '#ba4444 bold',
Token.Toolbar.Status.Pdb.Filename: 'bg:#222222 #aaaaaa',
Token.Toolbar.Status.Pdb.Lineno: 'bg:#222222 #ffffff',
Token.X.Shortcut.Key: 'bold noinherit',
Token.X.Shortcut.Description: 'noinherit',
Token.Prompt.BeforeInput: 'bold #008800',
Token.PdbCommand: 'bold',
Token.CompletionHint.Symbol: '#9a8888',
Token.CompletionHint.Parameter: '#ba4444 bold',
Token.Toolbar.Status.Pdb.Filename: 'bg:#222222 #aaaaaa',
Token.Toolbar.Status.Pdb.Lineno: 'bg:#222222 #ffffff',
Token.Toolbar.Status.Pdb.Shortcut.Key: 'bg:#222222 #aaaaaa',
Token.Toolbar.Status.Pdb.Shortcut.Description: 'bg:#222222 #aaaaaa',
}
styles.update(PythonStyle.styles)

View File

@ -56,34 +56,13 @@ class SourceCodeToolbar(TextToolbar):
return ''.join(result)
class ShortcutsToolbar(Toolbar):
"""
Display shortcuts.
"""
def get_tokens(self, cli, width):
result = []
append = result.append
TB = Token.X.Shortcut
append((TB, ' '))
append((TB.Key, '[F6]'))
append((TB.Description, ' Open Repl '))
append((TB.Key, '[F7]'))
append((TB.Description, ' Step '))
append((TB.Key, '[F8]'))
append((TB.Description, ' Next '))
append((TB.Key, '[F9]'))
append((TB.Description, ' Continue '))
return result
class PdbStatusToolbar(Toolbar):
"""
Toolbar which shows the Pdb status. (current line and line number.)
"""
def __init__(self, pdb_ref, token=None):
def __init__(self, pdb_ref, key_bindings_manager, token=None):
self._pdb_ref = pdb_ref
self.key_bindings_manager = key_bindings_manager
token = token or Token.Toolbar.Status
super(PdbStatusToolbar, self).__init__(token=token)
@ -93,19 +72,17 @@ class PdbStatusToolbar(Toolbar):
TB = self.token
pdb = self._pdb_ref()
# Show current input mode.
result.extend(get_inputmode_tokens(self.token, False, cli))
# Shortcuts
append((TB.Pdb.Shortcut.Key, ' [F8]'))
append((TB.Pdb.Shortcut.Description, ' Step '))
append((TB.Pdb.Shortcut.Key, '[F9]'))
append((TB.Pdb.Shortcut.Description, ' Next '))
append((TB.Pdb.Shortcut.Key, '[F10]'))
append((TB.Pdb.Shortcut.Description, ' Continue'))
# Filename and line number.
append((TB, ' Break at: '))
append((TB, ' | At: '))
append((TB.Pdb.Filename, pdb.curframe.f_code.co_filename or 'None'))
append((TB, ' '))
append((TB.Pdb.Lineno, ': %s' % pdb.curframe.f_lineno))
# Python version
version = sys.version_info
append((TB, ' - '))
append((TB.PythonVersion, '%s %i.%i.%i' % (platform.python_implementation(),
version[0], version[1], version[2])))
append((TB.Pdb.Lineno, ':%s' % pdb.curframe.f_lineno))
return result

View File

@ -14,26 +14,28 @@ from pygments.style import Style
from pygments.token import Keyword, Operator, Number, Name, Error, Comment, Token, String
from prompt_toolkit import CommandLineInterface
from prompt_toolkit.buffer import Buffer
from prompt_toolkit.completion import Completer, Completion
from prompt_toolkit.enums import InputMode
from prompt_toolkit.history import FileHistory, History
from prompt_toolkit.key_bindings.emacs import emacs_bindings
from prompt_toolkit.key_bindings.vi import vi_bindings
from prompt_toolkit.key_binding.bindings.vi import ViStateFilter
from prompt_toolkit.key_binding.manager import KeyBindingManager, ViModeEnabled
from prompt_toolkit.key_binding.vi_state import InputMode
from prompt_toolkit.keys import Keys
from prompt_toolkit.layout import Layout
from prompt_toolkit.layout.margins import LeftMarginWithLineNumbers
from prompt_toolkit.layout.menus import CompletionsMenu
from prompt_toolkit.layout.processors import BracketsMismatchProcessor
from prompt_toolkit.layout.toolbars import CompletionsToolbar, ArgToolbar, SearchToolbar, ValidationToolbar, SystemToolbar
from prompt_toolkit.layout.toolbars import Toolbar
from prompt_toolkit.line import Line
from prompt_toolkit.selection import SelectionType
from prompt_toolkit.validation import Validator, ValidationError
from prompt_toolkit.layout.margins import LeftMarginWithLineNumbers
from .regular_languages.compiler import compile as compile_grammar
from .regular_languages.completion import GrammarCompleter
from .completers import PathCompleter
import prompt_toolkit.filters as filters
import jedi
import platform
import re
@ -166,94 +168,114 @@ def _has_unclosed_brackets(text):
return False
def python_bindings(registry, cli_ref):
def load_python_bindings(key_bindings_manager, settings, always_multiline=False):
"""
Custom key bindings.
"""
line = cli_ref().line
handle = registry.add_binding
handle = key_bindings_manager.registry.add_binding
has_selection = filters.HasSelection()
@handle(Keys.F4)
def _(event):
"""
Toggle between Vi and Emacs mode.
"""
key_bindings_manager.enable_vi_mode = not key_bindings_manager.enable_vi_mode
@handle(Keys.F6)
def _(event):
"""
Enable/Disable paste mode.
"""
line.paste_mode = not line.paste_mode
if line.paste_mode:
line.is_multiline = True
settings.paste_mode = not settings.paste_mode
if not cli_ref().line.always_multiline:
@handle(Keys.F7)
def _(event):
"""
Enable/Disable multiline mode.
"""
line.always_multiline = not line.always_multiline
@handle(Keys.F7)
def _(event):
"""
Enable/Disable multiline mode.
"""
settings.currently_multiline = not settings.currently_multiline
@handle(Keys.Tab, in_mode=InputMode.INSERT)
@handle(Keys.Tab, filter= ~has_selection)
def _(event):
"""
When the 'tab' key is pressed with only whitespace character before the
cursor, do autocompletion. Otherwise, insert indentation.
"""
current_char = line.document.current_line_before_cursor
buffer = event.cli.buffers['default']
current_char = buffer.document.current_line_before_cursor
if not current_char or current_char.isspace():
line.insert_text(' ')
buffer.insert_text(' ')
else:
line.complete_next()
buffer.complete_next()
@handle(Keys.ControlJ, filter= ~has_selection &
~(ViModeEnabled(key_bindings_manager) &
ViStateFilter(key_bindings_manager.vi_state, InputMode.NAVIGATION)) &
filters.HasFocus('default') & filters.IsMultiline())
def _(event):
"""
Auto indent after newline/Enter.
(When not in Vi navigaton mode, and when multiline is enabled.)
"""
buffer = event.current_buffer
if settings.paste_mode:
buffer.insert_text('\n')
else:
auto_newline(buffer)
class PythonLine(Line):
def auto_newline(buffer):
r"""
Insert \n at the cursor position. Also add necessary padding.
"""
Custom `Line` class with some helper functions.
insert_text = buffer.insert_text
if buffer.document.current_line_after_cursor:
insert_text('\n')
else:
# Go to new line, but also add indentation.
current_line = buffer.document.current_line_before_cursor.rstrip()
insert_text('\n')
# Copy whitespace from current line
for c in current_line:
if c.isspace():
insert_text(c)
else:
break
# If the last line ends with a colon, add four extra spaces.
if current_line[-1:] == ':':
for x in range(4):
insert_text(' ')
class PythonBuffer(Buffer):
"""
Custom `Buffer` class with some helper functions.
"""
_multiline_string_delims = re.compile('''[']{3}|["]{3}''')
def __init__(self, always_multiline, *a, **kw):
self.always_multiline = always_multiline
super(PythonLine, self).__init__(*a, **kw)
def reset(self, *a, **kw):
super(PythonLine, self).reset(*a, **kw)
#: Boolean `paste` flag. If True, don't insert whitespace after a
#: newline.
self.paste_mode = False
super(PythonBuffer, self).reset(*a, **kw)
# Code signatures. (This is set asynchronously after a timeout.)
self.signatures = []
def newline(self):
r"""
Insert \n at the cursor position. Also add necessary padding.
"""
insert_text = super(PythonLine, self).insert_text
if self.paste_mode or self.document.current_line_after_cursor:
insert_text('\n')
else:
# Go to new line, but also add indentation.
current_line = self.document.current_line_before_cursor.rstrip()
insert_text('\n')
_multiline_string_delims = re.compile('''[']{3}|["]{3}''')
# Copy whitespace from current line
for c in current_line:
if c.isspace():
insert_text(c)
else:
break
# If the last line ends with a colon, add four extra spaces.
if current_line[-1:] == ':':
for x in range(4):
insert_text(' ')
@property
def _ends_in_multiline_string(self):
def document_is_multiline_python(document):
"""
Determine whether this is a multiline Python document.
"""
def ends_in_multiline_string():
"""
``True`` if we're inside a multiline string at the end of the text.
"""
delims = self._multiline_string_delims.findall(self.text)
delims = _multiline_string_delims.findall(document.text)
opening = None
for delim in delims:
if opening is None:
@ -262,49 +284,35 @@ class PythonLine(Line):
opening = None
return bool(opening)
@property
def is_multiline(self):
"""
Dynamically determine whether we're in multiline mode.
"""
if any([
self.always_multiline,
self.paste_mode,
'\n' in self.text,
self._ends_in_multiline_string]):
return True
if '\n' in document.text or ends_in_multiline_string():
return True
# If we just typed a colon, or still have open brackets, always insert a real newline.
if self.document.text_before_cursor.rstrip()[-1:] == ':' or \
(self.document.is_cursor_at_the_end and
_has_unclosed_brackets(self.document.text_before_cursor)) or \
self.text.startswith('@'):
return True
# If we just typed a colon, or still have open brackets, always insert a real newline.
if document.text_before_cursor.rstrip()[-1:] == ':' or \
(document.is_cursor_at_the_end and
_has_unclosed_brackets(document.text_before_cursor)) or \
document.text.startswith('@'):
return True
# If the character before the cursor is a backslash (line continuation
# char), insert a new line.
elif self.document.text_before_cursor[-1:] == '\\':
return True
# If the character before the cursor is a backslash (line continuation
# char), insert a new line.
elif document.text_before_cursor[-1:] == '\\':
return True
return False
@is_multiline.setter
def is_multiline(self, value):
""" Ignore setter. """
pass
return False
class SignatureToolbar(Toolbar):
def is_visible(self, cli):
return super(SignatureToolbar, self).is_visible(cli) and bool(cli.line.signatures)
return super(SignatureToolbar, self).is_visible(cli) and bool(cli.buffers['default'].signatures)
def get_tokens(self, cli, width):
result = []
append = result.append
Signature = Token.Toolbar.Signature
if cli.line.signatures:
sig = cli.line.signatures[0] # Always take the first one.
if cli.buffers['default'].signatures:
sig = cli.buffers['default'].signatures[0] # Always take the first one.
append((Token, ' '))
try:
@ -331,7 +339,7 @@ class SignatureToolbar(Toolbar):
return result
def get_inputmode_tokens(token, vi_mode, cli):
def get_inputmode_tokens(token, key_bindings_manager, cli):
"""
Return current input mode as a list of (token, text) tuples for use in a
toolbar.
@ -339,79 +347,74 @@ def get_inputmode_tokens(token, vi_mode, cli):
:param vi_mode: (bool) True when vi mode is enabled.
:param cli: `CommandLineInterface` instance.
"""
mode = cli.input_processor.input_mode
mode = key_bindings_manager.vi_state.input_mode
result = []
append = result.append
# InputMode
if mode == InputMode.INCREMENTAL_SEARCH:
append((token.InputMode, '(SEARCH)'))
append((token, ' '))
elif vi_mode:
if mode == InputMode.INSERT:
append((token.InputMode, '(INSERT)'))
append((token, ' '))
elif mode == InputMode.VI_SEARCH:
append((token.InputMode, '(SEARCH)'))
append((token, ' '))
elif mode == InputMode.VI_NAVIGATION:
append((token.InputMode, '(NAV)'))
append((token, ' '))
elif mode == InputMode.VI_REPLACE:
append((token.InputMode, '(REPLACE)'))
if key_bindings_manager.enable_vi_mode:
if bool(cli.buffers['default'].selection_state):
if cli.buffers['default'].selection_state.type == SelectionType.LINES:
append((token.InputMode, '[F4] Vi (VISUAL LINE)'))
append((token, ' '))
elif cli.buffers['default'].selection_state.type == SelectionType.CHARACTERS:
append((token.InputMode, '[F4] Vi (VISUAL)'))
append((token, ' '))
elif mode == InputMode.INSERT:
append((token.InputMode, '[F4] Vi (INSERT)'))
append((token, ' '))
elif mode == InputMode.SELECTION and cli.line.selection_state:
if cli.line.selection_state.type == SelectionType.LINES:
append((token.InputMode, '(VISUAL LINE)'))
append((token, ' '))
elif cli.line.selection_state.type == SelectionType.CHARACTERS:
append((token.InputMode, '(VISUAL)'))
append((token, ' '))
elif mode == InputMode.NAVIGATION:
append((token.InputMode, '[F4] Vi (NAV)'))
append((token, ' '))
elif mode == InputMode.REPLACE:
append((token.InputMode, '[F4] Vi (REPLACE)'))
append((token, ' '))
else:
append((token.InputMode, '(emacs)'))
append((token.InputMode, '[F4] Emacs'))
append((token, ' '))
return result
class PythonToolbar(Toolbar):
def __init__(self, vi_mode, token=None):
def __init__(self, key_bindings_manager, settings, token=None):
self.key_bindings_manager = key_bindings_manager
self.settings = settings
token = token or Token.Toolbar.Status
self.vi_mode = vi_mode
super(PythonToolbar, self).__init__(token=token)
def get_tokens(self, cli, width):
TB = self.token
mode = cli.input_processor.input_mode
result = []
append = result.append
append((TB, ' '))
result.extend(get_inputmode_tokens(TB, self.vi_mode, cli))
result.extend(get_inputmode_tokens(TB, self.key_bindings_manager, cli))
# Position in history.
append((TB, '%i/%i ' % (cli.line.working_index + 1, len(cli.line._working_lines))))
append((TB, '%i/%i ' % (cli.buffers['default'].working_index + 1, len(cli.buffers['default']._working_lines))))
# Shortcuts.
if mode == InputMode.INCREMENTAL_SEARCH:
if not self.key_bindings_manager.enable_vi_mode and cli.focus_stack.current == 'search':
append((TB, '[Ctrl-G] Cancel search [Enter] Go to this position.'))
elif mode == InputMode.SELECTION and not self.vi_mode:
elif bool(cli.buffers['default'].selection_state) and not self.key_bindings_manager.enable_vi_mode:
# Emacs cut/copy keys.
append((TB, '[Ctrl-W] Cut [Meta-W] Copy [Ctrl-Y] Paste [Ctrl-G] Cancel'))
else:
if cli.line.paste_mode:
if self.settings.paste_mode:
append((TB.On, '[F6] Paste mode (on) '))
else:
append((TB.Off, '[F6] Paste mode (off) '))
if not cli.always_multiline:
if cli.line.is_multiline:
if not self.settings.always_multiline:
if self.settings.currently_multiline or \
document_is_multiline_python(cli.buffers['default'].document):
append((TB.On, '[F7] Multiline (on)'))
else:
append((TB.Off, '[F7] Multiline (off)'))
if cli.line.is_multiline:
if cli.buffers['default'].is_multiline:
append((TB, ' [Meta+Enter] Execute'))
# Python version
@ -588,6 +591,21 @@ class PythonCompleter(Completer):
display=c.name_with_symbols)
class PythonCLISettings(object):
"""
Settings for the Python REPL which can change at runtime.
"""
def __init__(self,
always_multiline=False,
paste_mode=False):
self.always_multiline = always_multiline
self.currently_multiline = False
#: Boolean `paste` flag. If True, don't insert whitespace after a
#: newline.
self.paste_mode = paste_mode
class PythonCommandLineInterface(CommandLineInterface):
def __init__(self,
get_globals=None, get_locals=None,
@ -602,15 +620,24 @@ class PythonCommandLineInterface(CommandLineInterface):
_completer=None,
_validator=None):
self.settings = PythonCLISettings(always_multiline=always_multiline)
self.get_globals = get_globals or (lambda: {})
self.get_locals = get_locals or self.get_globals
self.always_multiline = always_multiline
self.autocompletion_style = autocompletion_style
left_margin = _left_margin or PythonLeftMargin()
self.completer = _completer or PythonCompleter(self.get_globals, self.get_locals)
validator = _validator or PythonValidator()
if history_filename:
history = FileHistory(history_filename)
else:
history = History()
# Use a KeyBindingManager for loading the key bindings.
self.key_bindings_manager = KeyBindingManager(enable_vi_mode=vi_mode, enable_system_prompt=True)
load_python_bindings(self.key_bindings_manager, self.settings, always_multiline=always_multiline)
layout = Layout(
input_processors=[BracketsMismatchProcessor()],
min_height=7,
@ -626,21 +653,18 @@ class PythonCommandLineInterface(CommandLineInterface):
] +
([CompletionsToolbar()] if autocompletion_style == AutoCompletionStyle.HORIZONTAL_MENU else []) +
[
PythonToolbar(vi_mode=vi_mode),
PythonToolbar(self.key_bindings_manager, self.settings),
],
show_tildes=True)
if history_filename:
history = FileHistory(history_filename)
else:
history = History()
def is_buffer_multiline(document):
return (self.settings.paste_mode or
self.settings.always_multiline or
self.settings.currently_multiline or
document_is_multiline_python(document))
if vi_mode:
key_binding_factories = [vi_bindings, python_bindings]
else:
key_binding_factories = [emacs_bindings, python_bindings]
line=PythonLine(always_multiline=always_multiline,
buffer=PythonBuffer(
is_multiline=is_buffer_multiline,
tempfile_suffix='.py',
history=history,
completer=self.completer,
@ -654,8 +678,8 @@ class PythonCommandLineInterface(CommandLineInterface):
super(PythonCommandLineInterface, self).__init__(
layout=layout,
style=style,
key_binding_factories=key_binding_factories,
line=line,
key_bindings_registry=self.key_bindings_manager.registry,
buffer=buffer,
create_async_autocompleters=True)
def on_input_timeout():
@ -663,12 +687,15 @@ class PythonCommandLineInterface(CommandLineInterface):
When there is no input activity,
in another thread, get the signature of the current code.
"""
if self.focus_stack.current != 'default':
return
# Never run multiple get-signature threads.
if self.get_signatures_thread_running:
return
self.get_signatures_thread_running = True
document = self.line.document
document = self.buffers['default'].document
def run():
script = get_jedi_script_from_document(document, self.get_locals(), self.get_globals())
@ -692,8 +719,8 @@ class PythonCommandLineInterface(CommandLineInterface):
# Set signatures and redraw if the text didn't change in the
# meantime. Otherwise request new signatures.
if self.line.text == document.text:
self.line.signatures = signatures
if self.buffers['default'].text == document.text:
self.buffers['default'].signatures = signatures
self.request_redraw()
else:
on_input_timeout()
@ -701,3 +728,4 @@ class PythonCommandLineInterface(CommandLineInterface):
self.run_in_executor(run)
self.onInputTimeout += on_input_timeout
self.onReset += self.key_bindings_manager.reset

View File

@ -78,8 +78,9 @@ class _CompiledGrammar(object):
self._re_prefix_patterns = list(self._transform_prefix(root_node, create_group_func))
# Compile the regex itself.
self._re = re.compile(self._re_pattern)
self._re_prefix = [re.compile(t) for t in self._re_prefix_patterns]
flags = re.MULTILINE | re.DOTALL
self._re = re.compile(self._re_pattern, flags)
self._re_prefix = [re.compile(t, flags) for t in self._re_prefix_patterns]
def escape(self, varname, value):
"""

View File

@ -3,13 +3,15 @@ Useful shortcuts.
"""
from __future__ import unicode_literals
from .. import CommandLineInterface, AbortAction
from ..key_bindings.emacs import emacs_bindings
from ..key_bindings.vi import vi_bindings
from ..line import Line
from ..layout import Layout
from ..layout.processors import PasswordProcessor
from ..layout.prompt import DefaultPrompt
from prompt_toolkit import CommandLineInterface, AbortAction
from prompt_toolkit.buffer import Buffer
from prompt_toolkit.key_binding.registry import Registry
from prompt_toolkit.key_binding.vi_state import ViState
from prompt_toolkit.key_binding.bindings.emacs import load_emacs_bindings, load_emacs_search_bindings
from prompt_toolkit.key_binding.bindings.vi import load_vi_bindings, load_vi_search_bindings
from prompt_toolkit.layout import Layout
from prompt_toolkit.layout.processors import PasswordProcessor
from prompt_toolkit.layout.prompt import DefaultPrompt
def get_input(message, raise_exception_on_abort=False, multiline=False, is_password=False, vi_mode=False):
@ -22,10 +24,19 @@ def get_input(message, raise_exception_on_abort=False, multiline=False, is_passw
before_input=DefaultPrompt(message),
input_processors=([PasswordProcessor()] if is_password else []))
registry = Registry()
if vi_mode:
vi_state = ViState()
load_vi_bindings(registry, vi_state)
load_vi_search_bindings(registry, vi_state)
else:
load_emacs_bindings(registry)
load_emacs_search_bindings(registry)
cli = CommandLineInterface(
layout=layout,
line=Line(is_multiline=multiline),
key_binding_factories=[(vi_bindings if vi_mode else emacs_bindings)])
buffer=Buffer(is_multiline=multiline),
key_bindings_registry=registry)
on_abort = AbortAction.RAISE_EXCEPTION if raise_exception_on_abort else AbortAction.RETURN_NONE
code = cli.read_input(on_abort=on_abort, on_exit=AbortAction.IGNORE)

View File

@ -29,7 +29,7 @@ class Document(object):
This is a immutable class around the text and cursor position, and contains
methods for querying this data, e.g. to give the text before the cursor.
This class is usually instantiated by a :class:`~prompt_toolkit.line.Line`
This class is usually instantiated by a :class:`~prompt_toolkit.buffer.Buffer`
object, and accessed as the `document` property of that class.
:param text: string

View File

@ -6,21 +6,3 @@ class IncrementalSearchDirection:
BACKWARD = 'backward'
class InputMode(object):
INSERT = 'vi-insert'
VI_NAVIGATION = 'vi-navigation'
VI_REPLACE = 'vi-replace'
# Selection mode. The type of selection (characters/lines/block is saved in
# the line object.)
SELECTION = 'selection'
#: Ctrl-R/Ctrl-S incremental search.
INCREMENTAL_SEARCH = 'incremental-search'
#: Vi-style forward search. Usually with a '/' or '?' prompt.
VI_SEARCH = 'vi-forward-search'
#: When the system prompt (after typing '!' or M-!) has the focus.
SYSTEM = 'system'

121
prompt_toolkit/filters.py Normal file
View File

@ -0,0 +1,121 @@
"""
Filters decide whether something is active or not, given the state of the
CommandLineInterface. This can be used to enable/disable key bindings, as well
as to hide/show parts of the layout.
When Filter.__call__ returns True for a centain key, this is considered active.
Filters can be chained using ``&`` and ``|`` operations, and inverted using the
``~`` operator::
filter = HasFocus('default') & ~ HasSelection()
"""
from __future__ import unicode_literals
from abc import ABCMeta, abstractmethod
__all__ = (
'HasFocus',
'HasSelection',
'IsMultiline',
'NoFilter',
)
class Filter(object):
"""
Filter to activate/deactivate a key binding, depending on a condition.
The return value of ``__call__`` will tell if the key binding should be active.
"""
__metaclass__ = ABCMeta
@abstractmethod
def __call__(self, cli):
return True
def __and__(self, other):
if other is None:
return self
else:
assert isinstance(other, Filter), 'Expecting filter, got %r' % other
return _AndList([self, other])
def __or__(self, other):
if other is None:
return self
else:
assert isinstance(other, Filter), 'Expecting filter, got %r' % other
return _OrList([self, other])
def __invert__(self):
return _Invert(self)
class _AndList(Filter):
""" Result of &-operation between several filters. """
def __init__(self, filters):
self.filters = filters
def __call__(self, cli):
return all(f(cli) for f in self.filters)
def __repr__(self):
return '&'.join(repr(f) for f in self.filters)
class _OrList(Filter):
""" Result of |-operation between several filters. """
def __init__(self, filters):
self.filters = filters
def __call__(self, cli):
return any(f(cli) for f in self.filters)
def __repr__(self):
return '|'.join(repr(f) for f in self.filters)
class _Invert(Filter):
""" Negation of another filter. """
def __init__(self, filter):
self.filter = filter
def __call__(self, cli):
return not self.filter(cli)
def __repr__(self):
return '~%r' % self.filter
class HasFocus(Filter):
"""
Enable when this buffer has the focus.
"""
def __init__(self, buffer_name):
self.buffer_name = buffer_name
def __call__(self, cli):
return cli.focus_stack.current == self.buffer_name
class HasSelection(Filter):
"""
Enable when the current buffer has a selection.
"""
def __call__(self, cli):
return bool(cli.buffers[cli.focus_stack.current].selection_state)
class IsMultiline(Filter):
"""
Enable in multiline mode.
"""
def __call__(self, cli):
return cli.buffers[cli.focus_stack.current].is_multiline
class NoFilter(Filter):
"""
Always enable key binding.
"""
def __call__(self, cli):
return True

View File

@ -0,0 +1,36 @@
"""
Push/pop stack of buffer names. The top buffer of the stack is the one that
currently has the focus.
"""
from __future__ import unicode_literals
from six import string_types
__all__ = (
'FocusStack',
)
class FocusStack(object):
def __init__(self, initial='default'):
self._initial = initial
def reset(self):
self._stack = [self._initial]
def pop(self):
if len(self._stack) > 1:
self._stack.pop()
else:
raise Exception('Cannot pop last item from the focus stack.')
def push(self, buffer_name):
assert isinstance(buffer_name, string_types)
self._stack.append(buffer_name)
@property
def current(self):
return self._stack[-1]
@property
def previous(self):
return self._stack[-2]

View File

@ -0,0 +1 @@
from __future__ import unicode_literals

View File

@ -0,0 +1,280 @@
from __future__ import unicode_literals
from prompt_toolkit.keys import Keys
from prompt_toolkit.buffer import ClipboardData
import prompt_toolkit.filters as filters
from .utils import create_handle_decorator
__all__ = (
'load_basic_bindings',
)
def load_basic_bindings(registry, filter=None):
handle = create_handle_decorator(registry, filter)
has_selection = filters.HasSelection()
@handle(Keys.ControlA)
@handle(Keys.ControlB)
@handle(Keys.ControlC)
@handle(Keys.ControlD)
@handle(Keys.ControlE)
@handle(Keys.ControlF)
@handle(Keys.ControlG)
@handle(Keys.ControlH)
@handle(Keys.ControlI)
@handle(Keys.ControlJ)
@handle(Keys.ControlK)
@handle(Keys.ControlL)
@handle(Keys.ControlM)
@handle(Keys.ControlN)
@handle(Keys.ControlO)
@handle(Keys.ControlP)
@handle(Keys.ControlQ)
@handle(Keys.ControlR)
@handle(Keys.ControlS)
@handle(Keys.ControlT)
@handle(Keys.ControlU)
@handle(Keys.ControlV)
@handle(Keys.ControlW)
@handle(Keys.ControlX)
@handle(Keys.ControlY)
@handle(Keys.ControlZ)
@handle(Keys.F1)
@handle(Keys.F2)
@handle(Keys.F3)
@handle(Keys.F4)
@handle(Keys.F5)
@handle(Keys.F6)
@handle(Keys.F7)
@handle(Keys.F8)
@handle(Keys.F9)
@handle(Keys.F10)
@handle(Keys.F11)
@handle(Keys.F12)
@handle(Keys.F13)
@handle(Keys.F14)
@handle(Keys.F15)
@handle(Keys.F16)
@handle(Keys.F17)
@handle(Keys.F18)
@handle(Keys.F19)
@handle(Keys.F20)
@handle(Keys.ControlSpace)
@handle(Keys.ControlBackslash)
@handle(Keys.ControlSquareClose)
@handle(Keys.ControlCircumflex)
@handle(Keys.ControlUnderscore)
@handle(Keys.Backspace)
@handle(Keys.Up)
@handle(Keys.Down)
@handle(Keys.Right)
@handle(Keys.Left)
@handle(Keys.Home)
@handle(Keys.End)
@handle(Keys.Delete)
@handle(Keys.ShiftDelete)
@handle(Keys.PageUp)
@handle(Keys.PageDown)
@handle(Keys.BackTab)
@handle(Keys.Tab)
def _(event):
"""
First, for any of these keys, Don't do anything by default. Also don't
catch them in the 'Any' handler which will insert them as data.
If people want to insert these characters as a literal, they can always
do by doing a quoted insert. (ControlQ in emacs mode, ControlV in Vi
mode.)
"""
pass
@handle(Keys.Home)
def _(event):
buffer = event.current_buffer
buffer.cursor_position += buffer.document.home_position
@handle(Keys.End)
def _(event):
buffer = event.current_buffer
buffer.cursor_position += buffer.document.end_position
# CTRL keys.
@handle(Keys.ControlC)
def _(event):
event.cli.set_abort()
@handle(Keys.ControlD) # XXX: this is emacs behaviour.
def _(event):
buffer = event.current_buffer
# When there is text, act as delete, otherwise call exit.
if buffer.text:
buffer.delete()
else:
event.cli.set_exit()
@handle(Keys.ControlI, filter= ~has_selection)
def _(event):
r"""
Ctrl-I is identical to "\t"
Traditional tab-completion, where the first tab completes the common
suffix and the second tab lists all the completions.
"""
buffer = event.current_buffer
def second_tab():
buffer.complete_next(start_at_first=False)
# On the second tab-press, or when already navigating through
# completions.
if event.second_press or (buffer.complete_state and buffer.complete_state.complete_index is not None):
second_tab()
else:
# On the first tab press, only complete the common parts of all completions.
has_common = buffer.complete_common()
if not has_common:
second_tab()
@handle(Keys.BackTab, filter= ~has_selection)
def _(event):
"""
Shift+Tab: go to previous completion.
"""
event.current_buffer.complete_previous()
@handle(Keys.ControlM)
def _(event):
"""
Transform ControlM (\r) by default into ControlJ (\n).
(This way it is sufficient for other key bindings to handle only ControlJ.)
"""
event.cli.input_processor.feed_key(Keys.ControlJ)
@handle(Keys.ControlJ, filter= ~has_selection)
def _(event):
"""
Newline/Enter. (Or return input.)
"""
buffer = event.current_buffer
if buffer.is_multiline:
buffer.newline()
else:
if buffer.validate():
event.current_buffer.add_to_history()
event.cli.set_return_value(buffer.document)
@handle(Keys.ControlK, filter= ~has_selection)
def _(event):
buffer = event.current_buffer
deleted = buffer.delete(count=buffer.document.get_end_of_line_position())
buffer.set_clipboard(ClipboardData(deleted))
@handle(Keys.ControlL)
def _(event):
event.cli.renderer.clear()
@handle(Keys.ControlT, filter= ~has_selection)
def _(event):
event.current_buffer.swap_characters_before_cursor()
@handle(Keys.ControlU, filter= ~has_selection)
def _(event):
"""
Clears the line before the cursor position. If you are at the end of
the line, clears the entire line.
"""
buffer = event.current_buffer
deleted = buffer.delete_before_cursor(count=-buffer.document.get_start_of_line_position())
buffer.set_clipboard(ClipboardData(deleted))
@handle(Keys.ControlW, filter= ~has_selection)
def _(event):
"""
Delete the word before the cursor.
"""
buffer = event.current_buffer
pos = buffer.document.find_start_of_previous_word(count=event.arg)
if pos:
deleted = buffer.delete_before_cursor(count=-pos)
buffer.set_clipboard(ClipboardData(deleted))
@handle(Keys.PageUp, filter= ~has_selection)
def _(event):
event.current_buffer.history_backward()
@handle(Keys.PageDown, filter= ~has_selection)
def _(event):
event.current_buffer.history_forward()
@handle(Keys.Left)
def _(event):
buffer = event.current_buffer
buffer.cursor_position += buffer.document.get_cursor_left_position(count=event.arg)
@handle(Keys.Right)
def _(event):
buffer = event.current_buffer
buffer.cursor_position += buffer.document.get_cursor_right_position(count=event.arg)
@handle(Keys.Up, filter= ~has_selection)
def _(event):
event.current_buffer.auto_up(count=event.arg)
@handle(Keys.Up, filter=has_selection)
def _(event):
event.current_buffer.cursor_up(count=event.arg)
@handle(Keys.Down, filter= ~has_selection)
def _(event):
event.current_buffer.auto_down(count=event.arg)
@handle(Keys.Down, filter=has_selection)
def _(event):
event.current_buffer.cursor_down(count=event.arg)
@handle(Keys.ControlH, filter= ~has_selection)
@handle(Keys.Backspace, filter= ~has_selection)
def _(event):
buffer = event.current_buffer
buffer.delete_before_cursor(count=event.arg)
@handle(Keys.Delete, filter= ~has_selection)
def _(event):
event.current_buffer.delete(count=event.arg)
@handle(Keys.ShiftDelete, filter= ~has_selection)
def _(event):
event.current_buffer.delete(count=event.arg)
@handle(Keys.Any, filter= ~has_selection)
def _(event):
"""
Insert data at cursor position.
"""
event.current_buffer.insert_text(event.data * event.arg)
@handle(Keys.CPRResponse)
def _(event):
"""
Handle incoming Cursor-Position-Request response.
"""
# The incoming data looks like u'\x1b[35;1R'
# Parse row/col information.
row, col = map(int, event.data[2:-1].split(';'))
# Report absolute cursor position to the renderer.
event.cli.renderer.report_absolute_cursor_row(row)
@handle(Keys.ControlZ)
def _(event):
"""
Suspend process to background.
"""
event.cli.suspend_to_background()

View File

@ -0,0 +1,527 @@
from __future__ import unicode_literals
from prompt_toolkit.buffer import ClipboardData, SelectionType, indent, unindent
from prompt_toolkit.keys import Keys
from prompt_toolkit.enums import IncrementalSearchDirection
from .basic import load_basic_bindings
from .utils import create_handle_decorator
import prompt_toolkit.filters as filters
__all__ = (
'load_emacs_bindings',
'load_emacs_search_bindings',
'load_emacs_system_bindings',
)
def load_emacs_bindings(registry, filter=None):
"""
Some e-macs extensions.
"""
# Overview of Readline emacs commands:
# http://www.catonmat.net/download/readline-emacs-editing-mode-cheat-sheet.pdf
load_basic_bindings(registry, filter)
handle = create_handle_decorator(registry, filter)
has_selection = filters.HasSelection()
@handle(Keys.Escape)
def _(event):
"""
By default, ignore escape key.
(If we don't put this here, and Esc is followed by a key which sequence
is not handled, we'll insert an Escape character in the input stream.
Something we don't want and happens to easily in emacs mode.
Further, people can always use ControlQ to do a quoted insert.)
"""
pass
@handle(Keys.ControlA)
def _(event):
"""
Start of line.
"""
buffer = event.current_buffer
buffer.cursor_position += buffer.document.get_start_of_line_position(after_whitespace=False)
@handle(Keys.ControlB)
def _(event):
"""
Character back.
"""
buffer = event.current_buffer
buffer.cursor_position += buffer.document.get_cursor_left_position(count=event.arg)
@handle(Keys.ControlE)
def _(event):
"""
End of line.
"""
buffer = event.current_buffer
buffer.cursor_position += buffer.document.get_end_of_line_position()
@handle(Keys.ControlF)
def _(event):
"""
Character forward.
"""
buffer = event.current_buffer
buffer.cursor_position += buffer.document.get_cursor_right_position(count=event.arg)
@handle(Keys.ControlN, filter= ~has_selection)
def _(event):
"""
Next line.
"""
event.current_buffer.auto_down()
@handle(Keys.ControlN, filter=has_selection)
def _(event):
"""
Next line.
"""
event.current_buffer.cursor_down()
@handle(Keys.ControlO, filter= ~has_selection)
def _(event):
"""
Insert newline, but don't move the cursor.
"""
event.current_buffer.insert_text('\n', move_cursor=False)
@handle(Keys.ControlP, filter= ~has_selection)
def _(event):
"""
Previous line.
"""
event.current_buffer.auto_up()
@handle(Keys.ControlP, filter=has_selection)
def _(event):
"""
Previous line.
"""
event.current_buffer.cursor_up()
@handle(Keys.ControlQ, Keys.Any, filter= ~has_selection)
def _(event):
"""
Quoted insert.
"""
event.current_buffer.insert_text(event.data, overwrite=False)
@handle(Keys.ControlY, filter= ~has_selection)
def _(event):
"""
Paste before cursor.
"""
for i in range(event.arg):
event.current_buffer.paste_from_clipboard(before=True)
@handle(Keys.ControlUnderscore, save_before=False, filter= ~has_selection)
def _(event):
"""
Undo.
"""
event.current_buffer.undo()
def handle_digit(c):
"""
Handle Alt + digit in the `meta_digit` method.
"""
@handle(Keys.Escape, c)
def _(event):
event.append_to_arg_count(c)
for c in '0123456789':
handle_digit(c)
@handle(Keys.Escape, '-')
def _(event):
"""
"""
if event._arg is None:
event.append_to_arg_count('-')
@handle(Keys.Escape, Keys.ControlJ, filter= ~has_selection)
def _(event):
"""
Meta + Newline: always accept input.
"""
if event.current_buffer.validate():
event.current_buffer.add_to_history()
event.cli.set_return_value(event.current_buffer.document)
@handle(Keys.ControlSquareClose, Keys.Any)
def _(event):
"""
When Ctl-] + a character is pressed. go to that character.
"""
match = event.current_buffer.document.find(event.data, in_current_line=True, count=(event.arg))
if match is not None:
event.current_buffer.cursor_position += match
@handle(Keys.Escape, Keys.Backspace, filter= ~has_selection)
def _(event):
"""
Delete word backwards.
"""
buffer = event.current_buffer
pos = buffer.document.find_start_of_previous_word(count=event.arg)
if pos:
deleted = buffer.delete_before_cursor(count=-pos)
buffer.set_clipboard(ClipboardData(deleted))
@handle(Keys.Escape, 'a', filter= ~has_selection)
def _(event):
"""
Previous sentence.
"""
# TODO:
pass
@handle(Keys.Escape, 'c', filter= ~has_selection)
def _(event):
"""
Capitalize the current (or following) word.
"""
buffer = event.current_buffer
for i in range(event.arg):
pos = buffer.document.find_next_word_ending()
words = buffer.document.text_after_cursor[:pos]
buffer.insert_text(words.title(), overwrite=True)
@handle(Keys.Escape, 'd', filter= ~has_selection)
def _(event):
"""
Delete word forwards.
"""
buffer = event.current_buffer
pos = buffer.document.find_next_word_ending(count=event.arg)
if pos:
deleted = buffer.delete(count=pos)
buffer.set_clipboard(ClipboardData(deleted))
@handle(Keys.Escape, 'e', filter= ~has_selection)
def _(event):
""" Move to end of sentence. """
# TODO:
pass
@handle(Keys.Escape, 'f')
def _(event):
"""
Cursor to end of next word.
"""
buffer= event.current_buffer
pos = buffer.document.find_next_word_ending(count=event.arg)
if pos:
buffer.cursor_position += pos
@handle(Keys.Escape, 'b')
def _(event):
"""
Cursor to start of previous word.
"""
buffer = event.current_buffer
pos = buffer.document.find_previous_word_beginning(count=event.arg)
if pos:
buffer.cursor_position += pos
@handle(Keys.Escape, 'l', filter= ~has_selection)
def _(event):
"""
Lowercase the current (or following) word.
"""
buffer = event.current_buffer
for i in range(event.arg): # XXX: not DRY: see meta_c and meta_u!!
pos = buffer.document.find_next_word_ending()
words = buffer.document.text_after_cursor[:pos]
buffer.insert_text(words.lower(), overwrite=True)
@handle(Keys.Escape, 't', filter= ~has_selection)
def _(event):
"""
Swap the last two words before the cursor.
"""
# TODO
@handle(Keys.Escape, 'u', filter= ~has_selection)
def _(event):
"""
Uppercase the current (or following) word.
"""
buffer = event.current_buffer
for i in range(event.arg):
pos = buffer.document.find_next_word_ending()
words = buffer.document.text_after_cursor[:pos]
buffer.insert_text(words.upper(), overwrite=True)
@handle(Keys.Escape, '.', filter= ~has_selection)
def _(event):
"""
Rotate through the last word (white-space delimited) of the previous lines in history.
"""
# TODO
@handle(Keys.Escape, '\\', filter= ~has_selection)
def _(event):
"""
Delete all spaces and tabs around point.
(delete-horizontal-space)
"""
@handle(Keys.Escape, '*', filter= ~has_selection)
def _(event):
"""
`meta-*`: Insert all possible completions of the preceding text.
"""
@handle(Keys.ControlX, Keys.ControlE, filter= ~has_selection)
def _(event):
"""
Open editor.
"""
event.current_buffer.open_in_editor()
@handle(Keys.ControlX, Keys.ControlU, save_before=False, filter= ~has_selection)
def _(event):
event.current_buffer.undo()
@handle(Keys.ControlX, Keys.ControlX)
def _(event):
"""
Move cursor back and forth between the start and end of the current
line.
"""
buffer = event.current_buffer
if buffer.document.current_char == '\n':
buffer.cursor_position += buffer.document.get_start_of_line_position(after_whitespace=False)
else:
buffer.cursor_position += buffer.document.get_end_of_line_position()
@handle(Keys.ControlSpace)
def _(event):
"""
Start of the selection.
"""
# Take the current cursor position as the start of this selection.
event.current_buffer.start_selection(selection_type=SelectionType.CHARACTERS)
@handle(Keys.ControlG, filter= ~has_selection)
def _(event):
"""
Control + G: Cancel completion menu and validation state.
"""
event.current_buffer.complete_state = None
event.current_buffer.validation_error = None
@handle(Keys.ControlG, filter=has_selection)
def _(event):
"""
Cancel selection.
"""
event.current_buffer.exit_selection()
@handle(Keys.ControlW, filter=has_selection)
def _(event):
"""
Cut selected text.
"""
deleted = event.current_buffer.cut_selection()
event.current_buffer.set_clipboard(ClipboardData(deleted))
@handle(Keys.Escape, 'w', filter=has_selection)
def _(event):
"""
Copy selected text.
"""
text = event.current_buffer.copy_selection()
event.current_buffer.set_clipboard(ClipboardData(text))
@handle(Keys.Escape, '<', filter= ~has_selection)
def _(event):
"""
Move to the first line in the history.
"""
event.current_buffer.go_to_history(0)
@handle(Keys.Escape, '>', filter= ~has_selection)
def _(event):
"""
Move to the end of the input history.
This is the line we are editing.
"""
buffer= event.current_buffer
buffer.go_to_history(len(buffer._working_lines) - 1)
@handle(Keys.Escape, Keys.Left)
def _(event):
"""
Cursor to start of previous word.
"""
buffer = event.current_buffer
buffer.cursor_position += buffer.document.find_previous_word_beginning(count=event.arg) or 0
@handle(Keys.Escape, Keys.Right)
def _(event):
"""
Cursor to start of next word.
"""
buffer = event.current_buffer
buffer.cursor_position += buffer.document.find_next_word_beginning(count=event.arg) or \
buffer.document.end_position
@handle(Keys.Escape, '/', filter= ~has_selection)
def _(event):
"""
M-/: Complete.
"""
event.current_buffer.complete_next()
@handle(Keys.ControlC, '>', filter=has_selection)
def _(event):
"""
Indent selected text.
"""
buffer = event.current_buffer
buffer.cursor_position += buffer.document.get_start_of_line_position(after_whitespace=True)
from_, to = buffer.document.selection_range()
from_, _ = buffer.document.translate_index_to_position(from_)
to, _ = buffer.document.translate_index_to_position(to)
indent(buffer, from_ - 1, to, count=event.arg) # XXX: why does translate_index_to_position return 1-based indexing???
@handle(Keys.ControlC, '<', filter=has_selection)
def _(event):
"""
Unindent selected text.
"""
buffer = event.current_buffer
from_, to = buffer.document.selection_range()
from_, _ = buffer.document.translate_index_to_position(from_)
to, _ = buffer.document.translate_index_to_position(to)
unindent(buffer, from_ - 1, to, count=event.arg)
def load_emacs_system_bindings(registry, filter=None, system_buffer_name='system'):
handle = create_handle_decorator(registry, filter)
has_focus = filters.HasFocus(system_buffer_name)
@handle(Keys.Escape, '!', filter= ~has_focus)
def _(event):
"""
M-'!' opens the system prompt.
"""
event.cli.focus_stack.push(system_buffer_name)
@handle(Keys.Escape, filter=has_focus)
@handle(Keys.ControlG, filter=has_focus)
def _(event):
"""
Cancel system prompt.
"""
event.cli.buffers[system_buffer_name].reset()
event.cli.focus_stack.pop()
@handle(Keys.ControlJ, filter=has_focus)
def _(event):
"""
Run system command.
"""
system_line = event.cli.buffers[system_buffer_name]
event.cli.run_system_command(system_line.text)
system_line.reset(append_to_history=True)
# Focus previous buffer again.
event.cli.focus_stack.pop()
def load_emacs_search_bindings(registry, filter=None, search_buffer_name='search'):
handle = create_handle_decorator(registry, filter)
has_focus = filters.HasFocus(search_buffer_name)
@handle(Keys.ControlG, filter=has_focus)
# NOTE: the reason for not also binding Escape to this one, is that we want
# Alt+Enter to accept input directly in incremental search mode.
def _(event):
"""
Abort an incremental search and restore the original line.
"""
search_line = event.cli.buffers[search_buffer_name]
event.current_buffer.exit_isearch(restore_original_line=True)
search_line.reset()
@handle(Keys.ControlH, filter=has_focus)
@handle(Keys.Backspace, filter=has_focus)
def _(event):
search_line = event.cli.buffers[search_buffer_name]
search_line.delete_before_cursor()
event.current_buffer.set_search_text(search_line.text)
@handle(Keys.Any, filter=has_focus)
def _(event):
"""
Insert isearch string.
"""
# Insert text in search line.
search_line = event.cli.buffers[search_buffer_name]
search_line.insert_text(event.data)
# Set current search search text as line search text.
buffer = event.cli.buffers[event.cli.focus_stack.previous]
buffer.set_search_text(search_line.text)
@handle(Keys.ControlJ, filter=has_focus)
def _(event):
"""
When enter pressed in isearch, quit isearch mode. (Multiline
isearch would be too complicated.)
"""
search_line = event.cli.buffers[search_buffer_name]
search_line.reset()
event.cli.buffers[event.cli.focus_stack.previous].exit_isearch()
event.cli.focus_stack.pop()
@handle(Keys.ControlR, filter= ~has_focus)
def _(event):
event.cli.focus_stack.push(search_buffer_name)
buffer = event.cli.buffers[event.cli.focus_stack.previous]
buffer.incremental_search(IncrementalSearchDirection.BACKWARD)
@handle(Keys.ControlS, filter= ~has_focus)
def _(event):
event.cli.focus_stack.push(search_buffer_name)
buffer = event.cli.buffers[event.cli.focus_stack.previous]
buffer.incremental_search(IncrementalSearchDirection.FORWARD)
@handle(Keys.ControlR, filter=has_focus)
@handle(Keys.Up, filter=has_focus)
def _(event):
buffer = event.cli.buffers[event.cli.focus_stack.previous]
buffer.incremental_search(IncrementalSearchDirection.BACKWARD)
@handle(Keys.ControlS, filter=has_focus)
@handle(Keys.Down, filter=has_focus)
def _(event):
buffer = event.cli.buffers[event.cli.focus_stack.previous]
buffer.incremental_search(IncrementalSearchDirection.FORWARD)

View File

@ -1,20 +1,29 @@
from __future__ import unicode_literals
from functools import wraps
def create_handle_decorator(registry, line):
def create_handle_decorator(registry, filter):
"""
Create a key handle decorator, which is compatible with `Registry.handle`
but has a `save_before` option, which will make sure that undo changes are
saved to the undo stack of the `Line` object before every key press event.
saved to the undo stack of the `Buffer` object before every key press
event.
"""
def handle(*keys, **kw):
safe_before = kw.pop('save_before', True)
# Chain the given filter to the filter of this specific binding.
if 'filter' in kw:
kw['filter'] = kw['filter'] & filter
else:
kw['filter'] = filter
def decorator(handler_func):
@registry.add_binding(*keys, **kw)
@wraps(handler_func)
def wrapper(event):
if safe_before:
line.save_to_undo_stack()
event.cli.current_buffer.save_to_undo_stack()
handler_func(event)
return handler_func
return decorator

File diff suppressed because it is too large Load Diff

View File

@ -7,15 +7,13 @@ The `InputProcessor` will according to the implemented keybindings call the
correct callbacks when new key presses are feed through `feed_key`.
"""
from __future__ import unicode_literals
from .keys import Keys
from .enums import InputMode
from ..keys import Keys
import weakref
__all__ = (
'InputProcessor',
'KeyPress',
'Registry',
)
@ -51,16 +49,18 @@ class InputProcessor(object):
# Now the ControlX-ControlC callback will be called if this sequence is
# registered in the registry.
:param registry: `Registry` instance.
:param cli_ref: weakref to `CommandLineInterface`.
"""
def __init__(self, registry):
def __init__(self, registry, cli_ref):
self._registry = registry
self._cli_ref = cli_ref
self.reset()
# print(' '.join(set(''.join(map(str, kb.keys)) for kb in registry.key_bindings if all(isinstance(X, unicode) for X in kb.keys))))
def reset(self, initial_input_mode=InputMode.INSERT):
def reset(self):
self._previous_key_sequence = None
self._input_mode_stack = [initial_input_mode]
self._process_coroutine = self._process()
self._process_coroutine.send(None)
@ -69,33 +69,6 @@ class InputProcessor(object):
#: https://www.gnu.org/software/bash/manual/html_node/Readline-Arguments.html
self.arg = None
@property
def input_mode(self):
"""
Current :class:`~prompt_toolkit.enums.InputMode`
"""
return self._input_mode_stack[-1]
@input_mode.setter
def input_mode(self, value):
self._input_mode_stack[-1] = value
def push_input_mode(self, value):
"""
Push new :class:`~prompt_toolkit.enums.InputMode` on the stack.
"""
self._input_mode_stack.append(value)
def pop_input_mode(self):
"""
Push new :class:`~prompt_toolkit.enums.InputMode` on the stack.
"""
# You can't pop the last item.
if len(self._input_mode_stack) > 1:
return self._input_mode_stack.pop()
else:
raise IndexError('Cannot pop the last InputMode.')
def _get_matches(self, key_presses):
"""
For a list of :class:`KeyPress` instances. Give the matching handlers
@ -105,27 +78,17 @@ class InputProcessor(object):
bindings = self._registry.key_bindings
# Try match, with mode flag
with_mode = [b for b in bindings if b.keys == keys and b.input_mode == self.input_mode]
with_mode = [b for b in bindings if b.keys == keys and b.filter(self._cli_ref())]
if with_mode:
return with_mode
# Try match without mode.
without_mode = [b for b in bindings if b.keys == keys and b.input_mode is None]
if without_mode:
return without_mode
# Try match, where the last key is replaced with 'Any', with mode.
keys_any = tuple(keys[:-1] + (Keys.Any,))
with_mode_any = [b for b in bindings if b.keys == keys_any and b.input_mode == self.input_mode]
with_mode_any = [b for b in bindings if b.keys == keys_any and b.filter(self._cli_ref())]
if with_mode_any:
return with_mode_any
# Try match, where the last key is replaced with 'Any', without mode.
without_mode_any = [b for b in bindings if b.keys == keys_any and b.input_mode is None]
if without_mode_any:
return without_mode_any
return []
def _is_prefix_of_longer_match(self, key_presses):
@ -136,7 +99,7 @@ class InputProcessor(object):
keys = [k.key for k in key_presses]
for b in self._registry.key_bindings:
if b.input_mode in (None, self.input_mode):
if b.filter(self._cli_ref()):
if len(b.keys) > len(keys) and list(b.keys[:len(key_presses)]) == keys:
return True
@ -191,9 +154,7 @@ class InputProcessor(object):
event = Event(weakref.ref(self), arg=arg, key_sequence=key_sequence,
previous_key_sequence=self._previous_key_sequence)
handler.call(event)
for h in self._registry.after_handler_callbacks:
h(event)
self._registry.onHandlerCalled.fire(event)
self._previous_key_sequence = key_sequence
@ -213,6 +174,20 @@ class Event(object):
def input_processor(self):
return self._input_processor_ref()
@property
def cli(self):
"""
Command line interface.
"""
return self.input_processor._cli_ref()
@property
def current_buffer(self):
"""
The current buffer.
"""
return self.cli.current_buffer
@property
def arg(self):
return self._arg or 1
@ -242,50 +217,3 @@ class Event(object):
result = None
self.input_processor.arg = result
class _Binding(object):
def __init__(self, keys, callable, input_mode=None):
self.keys = keys
self._callable = callable
self.input_mode = input_mode
def call(self, event):
return self._callable(event)
def __repr__(self):
return '_Binding(keys=%r, callable=%r)' % (self.keys, self._callable)
class Registry(object):
"""
Key binding registry.
::
r = Registry()
@r.add_binding(Keys.ControlX, Keys.ControlC, in_mode=INSERT)
def handler(event):
# Handle ControlX-ControlC key sequence.
pass
"""
def __init__(self):
self.key_bindings = []
self.after_handler_callbacks = []
def add_binding(self, *keys, **kwargs):
"""
Decorator for annotating key bindings.
"""
input_mode = kwargs.pop('in_mode', None)
assert not kwargs
assert keys
def decorator(func):
self.key_bindings.append(_Binding(keys, func, input_mode=input_mode))
return func
return decorator
def add_after_handler_callback(self, callback):
self.after_handler_callbacks.append(callback)

View File

@ -0,0 +1,81 @@
"""
:class:`KeyBindingManager` is a utility (or shortcut) for loading all the key
bindings in a key binding registry, with a logic set of filters to quickly to
quickly change from Vi to Emacs key bindings at runtime.
You don't have to use this, but it's practical.
Usage::
manager = KeyBindingManager()
cli = CommandLineInterface(key_bindings_registry=manager.registry)
manager.enable_vi_mode = True
"""
from __future__ import unicode_literals
from prompt_toolkit.filters import Filter
from prompt_toolkit.key_binding.registry import Registry
from prompt_toolkit.key_binding.vi_state import ViState
from prompt_toolkit.key_binding.bindings.emacs import load_emacs_bindings, load_emacs_system_bindings, load_emacs_search_bindings
from prompt_toolkit.key_binding.bindings.vi import load_vi_bindings, load_vi_system_bindings, load_vi_search_bindings
__all__ = (
'KeyBindingManager',
)
class ManagerFilter(Filter):
def __init__(self, manager):
self.manager = manager
class ViModeEnabled(ManagerFilter):
def __call__(self, cli):
return self.manager.enable_vi_mode
class SystemPromptEnabled(ManagerFilter):
def __call__(self, cli):
return self.manager.enable_system_prompt
class SearchEnabled(ManagerFilter):
def __call__(self, cli):
return self.manager.enable_search
class KeyBindingManager(object):
def __init__(self, registry=None, enable_vi_mode=False,
enable_system_prompt=False, enable_search=True):
self.registry = registry or Registry()
# Flags. You can change these anytime.
self.enable_vi_mode = enable_vi_mode
self.enable_system_prompt = enable_system_prompt
self.enable_search = enable_search
# Create set of filters to enable/disable sets of key bindings at
# runtime.
vi_mode_enabled = ViModeEnabled(self)
emacs_mode_enabled = ~ vi_mode_enabled
system_prompt_enabled = SystemPromptEnabled(self)
search_enabled = SearchEnabled(self)
# Vi state. (Object to keep track of in which Vi mode we are.)
self.vi_state = ViState()
# Load all bindings in the registry with the correct filters.
load_emacs_bindings(self.registry, emacs_mode_enabled)
load_emacs_search_bindings(self.registry,
emacs_mode_enabled & search_enabled)
load_emacs_system_bindings(self.registry,
emacs_mode_enabled & system_prompt_enabled)
load_vi_bindings(self.registry, self.vi_state, vi_mode_enabled)
load_vi_search_bindings(self.registry, self.vi_state,
vi_mode_enabled & search_enabled)
load_vi_system_bindings(self.registry, self.vi_state,
vi_mode_enabled & system_prompt_enabled)
def reset(self):
self.vi_state.reset()

View File

@ -0,0 +1,53 @@
from __future__ import unicode_literals
from ..filters import NoFilter, Filter
from ..utils import EventHook
__all__ = (
'Registry',
)
class _Binding(object):
def __init__(self, keys, callable, filter=None):
self.keys = keys
self._callable = callable
self.filter = filter
def call(self, event):
return self._callable(event)
def __repr__(self):
return '_Binding(keys=%r, callable=%r)' % (self.keys, self._callable)
class Registry(object):
"""
Key binding registry.
::
r = Registry()
@r.add_binding(Keys.ControlX, Keys.ControlC, filter=INSERT)
def handler(event):
# Handle ControlX-ControlC key sequence.
pass
"""
def __init__(self):
self.key_bindings = []
self.onHandlerCalled = EventHook()
def add_binding(self, *keys, **kwargs):
"""
Decorator for annotating key bindings.
"""
filter = kwargs.pop('filter', None) or NoFilter()
assert not kwargs
assert keys
assert isinstance(filter, Filter), 'Expected Filter instance, got %r' % filter
def decorator(func):
self.key_bindings.append(_Binding(keys, func, filter=filter))
return func
return decorator

View File

@ -0,0 +1,39 @@
from __future__ import unicode_literals
from ..enums import IncrementalSearchDirection
__all__ = (
'InputMode',
'CharacterFind',
'ViState',
)
class InputMode(object):
INSERT = 'vi-insert'
NAVIGATION = 'vi-navigation'
REPLACE = 'vi-replace'
class CharacterFind(object):
def __init__(self, character, backwards=False):
self.character = character
self.backwards = backwards
class ViState(object):
"""
Mutable class to hold the state of the Vi navigation.
"""
def __init__(self):
#: None or CharacterFind instance. (This is used to repeat the last
#: search in Vi mode, by pressing the 'n' or 'N' in navigation mode.)
self.last_character_find = None
self.search_direction = IncrementalSearchDirection.FORWARD
#: The Vi mode we're currently in to.
self.input_mode = InputMode.INSERT
def reset(self):
# Go back to insert mode.
self.input_mode = InputMode.INSERT

View File

@ -1,333 +0,0 @@
from __future__ import unicode_literals
from ..keys import Keys
from ..enums import InputMode
from ..line import ClipboardData
from .utils import create_handle_decorator
def basic_bindings(registry, cli_ref):
cli = cli_ref()
line = cli_ref().line
system_line = cli_ref().lines['system']
renderer = cli_ref().renderer
handle = create_handle_decorator(registry, line)
@handle(Keys.ControlA)
@handle(Keys.ControlB)
@handle(Keys.ControlC)
@handle(Keys.ControlD)
@handle(Keys.ControlE)
@handle(Keys.ControlF)
@handle(Keys.ControlG)
@handle(Keys.ControlH)
@handle(Keys.ControlI)
@handle(Keys.ControlJ)
@handle(Keys.ControlK)
@handle(Keys.ControlL)
@handle(Keys.ControlM)
@handle(Keys.ControlN)
@handle(Keys.ControlO)
@handle(Keys.ControlP)
@handle(Keys.ControlQ)
@handle(Keys.ControlR)
@handle(Keys.ControlS)
@handle(Keys.ControlT)
@handle(Keys.ControlU)
@handle(Keys.ControlV)
@handle(Keys.ControlW)
@handle(Keys.ControlX)
@handle(Keys.ControlY)
@handle(Keys.ControlZ)
@handle(Keys.F1)
@handle(Keys.F2)
@handle(Keys.F3)
@handle(Keys.F4)
@handle(Keys.F5)
@handle(Keys.F6)
@handle(Keys.F7)
@handle(Keys.F8)
@handle(Keys.F9)
@handle(Keys.F10)
@handle(Keys.F11)
@handle(Keys.F12)
@handle(Keys.F13)
@handle(Keys.F14)
@handle(Keys.F15)
@handle(Keys.F16)
@handle(Keys.F17)
@handle(Keys.F18)
@handle(Keys.F19)
@handle(Keys.F20)
@handle(Keys.ControlSpace)
@handle(Keys.ControlBackslash)
@handle(Keys.ControlSquareClose)
@handle(Keys.ControlCircumflex)
@handle(Keys.ControlUnderscore)
@handle(Keys.Backspace)
@handle(Keys.Up)
@handle(Keys.Down)
@handle(Keys.Right)
@handle(Keys.Left)
@handle(Keys.Home)
@handle(Keys.End)
@handle(Keys.Delete)
@handle(Keys.ShiftDelete)
@handle(Keys.PageUp)
@handle(Keys.PageDown)
@handle(Keys.BackTab)
@handle(Keys.Tab)
def _(event):
"""
First, for any of these keys, Don't do anything by default. Also don't
catch them in the 'Any' handler which will insert them as data.
If people want to insert these characters as a literal, they can always
do by doing a quoted insert. (ControlQ in emacs mode, ControlV in Vi
mode.)
"""
pass
@handle(Keys.Home, in_mode=InputMode.INSERT)
@handle(Keys.Home, in_mode=InputMode.SELECTION)
def _(event):
line.cursor_position += line.document.home_position
@handle(Keys.End, in_mode=InputMode.INSERT)
@handle(Keys.End, in_mode=InputMode.SELECTION)
def _(event):
line.cursor_position += line.document.end_position
# CTRL keys.
@handle(Keys.ControlC)
def _(event):
cli.set_abort()
@handle(Keys.ControlD)
def _(event):
# When there is text, act as delete, otherwise call exit.
if line.text:
line.delete()
else:
cli.set_exit()
@handle(Keys.ControlI, in_mode=InputMode.INSERT)
def _(event):
r"""
Ctrl-I is identical to "\t"
Traditional tab-completion, where the first tab completes the common
suffix and the second tab lists all the completions.
"""
def second_tab():
line.complete_next(start_at_first=False)
# On the second tab-press, or when already navigating through
# completions.
if event.second_press or (line.complete_state and line.complete_state.complete_index is not None):
second_tab()
else:
# On the first tab press, only complete the common parts of all completions.
has_common = line.complete_common()
if not has_common:
second_tab()
@handle(Keys.BackTab, in_mode=InputMode.INSERT)
def _(event):
"""
Shift+Tab: go to previous completion.
"""
line.complete_previous()
@handle(Keys.ControlJ, in_mode=InputMode.INSERT)
@handle(Keys.ControlM, in_mode=InputMode.INSERT)
def _(event):
"""
Newline/Enter. (Or return input.)
"""
if line.is_multiline:
line.newline()
else:
if line.validate():
cli_ref().line.add_to_history()
cli_ref().set_return_value(line.document)
@handle(Keys.ControlK, in_mode=InputMode.INSERT)
def _(event):
deleted = line.delete(count=line.document.get_end_of_line_position())
line.set_clipboard(ClipboardData(deleted))
@handle(Keys.ControlL)
def _(event):
renderer.clear()
@handle(Keys.ControlT, in_mode=InputMode.INSERT)
def _(event):
line.swap_characters_before_cursor()
@handle(Keys.ControlU, in_mode=InputMode.INSERT)
def _(event):
"""
Clears the line before the cursor position. If you are at the end of
the line, clears the entire line.
"""
deleted = line.delete_before_cursor(count=-line.document.get_start_of_line_position())
line.set_clipboard(ClipboardData(deleted))
@handle(Keys.ControlW, in_mode=InputMode.INSERT)
def _(event):
"""
Delete the word before the cursor.
"""
pos = line.document.find_start_of_previous_word(count=event.arg)
if pos:
deleted = line.delete_before_cursor(count=-pos)
line.set_clipboard(ClipboardData(deleted))
@handle(Keys.PageUp, in_mode=InputMode.INSERT)
def _(event):
line.history_backward()
@handle(Keys.PageDown, in_mode=InputMode.INSERT)
def _(event):
line.history_forward()
@handle(Keys.Left, in_mode=InputMode.INSERT)
@handle(Keys.Left, in_mode=InputMode.SELECTION)
def _(event):
line.cursor_position += line.document.get_cursor_left_position(count=event.arg)
@handle(Keys.Right, in_mode=InputMode.INSERT)
@handle(Keys.Right, in_mode=InputMode.SELECTION)
def _(event):
line.cursor_position += line.document.get_cursor_right_position(count=event.arg)
@handle(Keys.Up, in_mode=InputMode.INSERT)
def _(event):
line.auto_up(count=event.arg)
@handle(Keys.Down, in_mode=InputMode.INSERT)
def _(event):
line.auto_down(count=event.arg)
@handle(Keys.Up, in_mode=InputMode.SELECTION)
def _(event):
line.cursor_up(count=event.arg)
@handle(Keys.Down, in_mode=InputMode.SELECTION)
def _(event):
line.cursor_down(count=event.arg)
@handle(Keys.ControlH, in_mode=InputMode.INSERT)
@handle(Keys.Backspace, in_mode=InputMode.INSERT)
def _(event):
line.delete_before_cursor(count=event.arg)
@handle(Keys.Delete, in_mode=InputMode.INSERT)
def _(event):
line.delete(count=event.arg)
@handle(Keys.ShiftDelete, in_mode=InputMode.INSERT)
def _(event):
line.delete(count=event.arg)
@handle(Keys.Any, in_mode=InputMode.INSERT)
def _(event):
"""
Insert data at cursor position.
"""
line.insert_text(event.data * event.arg)
@handle(Keys.CPRResponse)
def _(event):
"""
Handle incoming Cursor-Position-Request response.
"""
# The incoming data looks like u'\x1b[35;1R'
# Parse row/col information.
row, col = map(int, event.data[2:-1].split(';'))
# Report absolute cursor position to the renderer.
cli_ref().renderer.report_absolute_cursor_row(row)
@handle(Keys.Up, in_mode=InputMode.SYSTEM)
def _(event):
"""
Previous history item at system prompt.
"""
system_line.auto_up()
@handle(Keys.Down, in_mode=InputMode.SYSTEM)
def _(event):
"""
Next history item at system prompt.
"""
system_line.auto_down()
@handle(Keys.Left, in_mode=InputMode.SYSTEM)
def _(event):
"""
Arrow left at the system prompt.
"""
system_line.cursor_left()
@handle(Keys.Right, in_mode=InputMode.SYSTEM)
def _(event):
"""
Arrow right at the system prompt.
"""
system_line.cursor_right()
@handle(Keys.ControlC, in_mode=InputMode.SYSTEM)
def _(event):
"""
Cancel system prompt.
"""
system_line.reset()
event.input_processor.pop_input_mode()
@handle(Keys.ControlM, in_mode=InputMode.SYSTEM)
@handle(Keys.ControlJ, in_mode=InputMode.SYSTEM)
def _(event):
"""
Run system command.
"""
cli_ref().run_system_command(system_line.text)
system_line.add_to_history()
system_line.reset()
event.input_processor.pop_input_mode()
@handle(Keys.Backspace, in_mode=InputMode.SYSTEM)
def _(event):
"""
Backspace at the system prompt.
"""
if system_line.text:
system_line.delete_before_cursor()
else:
# If no text after the prompt, cancel.
system_line.reset()
event.input_processor.pop_input_mode()
@handle(Keys.Delete, in_mode=InputMode.SYSTEM)
def _(event):
"""
Delete at the system prompt.
"""
system_line.delete()
@handle(Keys.Any, in_mode=InputMode.SYSTEM)
def _(event):
"""
Insert text after the system prompt.
"""
system_line.insert_text(event.data)
@handle(Keys.ControlZ)
def _(event):
"""
Suspend process to background.
"""
cli_ref().suspend_to_background()

View File

@ -1,490 +0,0 @@
from __future__ import unicode_literals
from ..line import ClipboardData, SelectionType, indent, unindent
from ..keys import Keys
from ..enums import InputMode, IncrementalSearchDirection
from .basic import basic_bindings
from .utils import create_handle_decorator
def emacs_bindings(registry, cli_ref):
"""
Some e-macs extensions.
"""
# Overview of Readline emacs commands:
# http://www.catonmat.net/download/readline-emacs-editing-mode-cheat-sheet.pdf
basic_bindings(registry, cli_ref)
line = cli_ref().line
search_line = cli_ref().lines['search']
system_line = cli_ref().lines['system']
handle = create_handle_decorator(registry, line)
@handle(Keys.Escape)
def _(event):
"""
By default, ignore escape key.
(If we don't put this here, and Esc is followed by a key which sequence
is not handled, we'll insert an Escape character in the input stream.
Something we don't want and happens to easily in emacs mode.
Further, people can always use ControlQ to do a quoted insert.)
"""
pass
@handle(Keys.ControlA, in_mode=InputMode.INSERT)
@handle(Keys.ControlA, in_mode=InputMode.SELECTION)
def _(event):
"""
Start of line.
"""
line.cursor_position += line.document.get_start_of_line_position(after_whitespace=False)
@handle(Keys.ControlA, in_mode=InputMode.SYSTEM)
def _(event):
"""
Start of system line.
"""
system_line.cursor_position += system_line.document.get_start_of_line_position(after_whitespace=False)
@handle(Keys.ControlB, in_mode=InputMode.INSERT)
@handle(Keys.ControlB, in_mode=InputMode.SELECTION)
def _(event):
"""
Character back.
"""
line.cursor_position += line.document.get_cursor_left_position(count=event.arg)
@handle(Keys.ControlE, in_mode=InputMode.INSERT)
@handle(Keys.ControlE, in_mode=InputMode.SELECTION)
def _(event):
"""
End of line.
"""
line.cursor_position += line.document.get_end_of_line_position()
@handle(Keys.ControlE, in_mode=InputMode.SYSTEM)
def _(event):
"""
End of "system" line.
"""
system_line.cursor_position += system_line.document.get_end_of_line_position()
@handle(Keys.ControlF, in_mode=InputMode.INSERT)
@handle(Keys.ControlF, in_mode=InputMode.SELECTION)
def _(event):
"""
Character forward.
"""
line.cursor_position += line.document.get_cursor_right_position(count=event.arg)
@handle(Keys.ControlN, in_mode=InputMode.INSERT)
def _(event):
"""
Next line.
"""
line.auto_down()
@handle(Keys.ControlN, in_mode=InputMode.SELECTION)
def _(event):
"""
Next line.
"""
line.cursor_down()
@handle(Keys.ControlO, in_mode=InputMode.INSERT)
def _(event):
"""
Insert newline, but don't move the cursor.
"""
line.insert_text('\n', move_cursor=False)
@handle(Keys.ControlP, in_mode=InputMode.INSERT)
def _(event):
"""
Previous line.
"""
line.auto_up()
@handle(Keys.ControlP, in_mode=InputMode.SELECTION)
def _(event):
"""
Previous line.
"""
line.cursor_up()
@handle(Keys.ControlQ, Keys.Any, in_mode=InputMode.INSERT)
def _(event):
"""
Quoted insert.
"""
line.insert_text(event.data, overwrite=False)
@handle(Keys.ControlY, in_mode=InputMode.INSERT)
def _(event):
"""
Paste before cursor.
"""
for i in range(event.arg):
line.paste_from_clipboard(before=True)
@handle(Keys.ControlUnderscore, save_before=False, in_mode=InputMode.INSERT)
def _(event):
"""
Undo.
"""
line.undo()
def handle_digit(c):
"""
Handle Alt + digit in the `meta_digit` method.
"""
@handle(Keys.Escape, c, in_mode=InputMode.INSERT)
@handle(Keys.Escape, c, in_mode=InputMode.SELECTION)
def _(event):
event.append_to_arg_count(c)
for c in '0123456789':
handle_digit(c)
@handle(Keys.Escape, '-', in_mode=InputMode.INSERT)
def _(event):
"""
"""
if event._arg is None:
event.append_to_arg_count('-')
@handle(Keys.Escape, Keys.ControlJ, in_mode=InputMode.INSERT)
@handle(Keys.Escape, Keys.ControlM, in_mode=InputMode.INSERT)
@handle(Keys.Escape, Keys.ControlJ, in_mode=InputMode.INCREMENTAL_SEARCH)
@handle(Keys.Escape, Keys.ControlM, in_mode=InputMode.INCREMENTAL_SEARCH)
def _(event):
"""
Meta + Newline: always accept input.
"""
if line.validate():
cli_ref().line.add_to_history()
cli_ref().set_return_value(line.document)
@handle(Keys.ControlSquareClose, Keys.Any, in_mode=InputMode.INSERT)
@handle(Keys.ControlSquareClose, Keys.Any, in_mode=InputMode.SELECTION)
def _(event):
"""
When Ctl-] + a character is pressed. go to that character.
"""
match = line.document.find(event.data, in_current_line=True, count=(event.arg))
if match is not None:
line.cursor_position += match
@handle(Keys.Escape, Keys.Backspace, in_mode=InputMode.INSERT)
def _(event):
"""
Delete word backwards.
"""
pos = line.document.find_start_of_previous_word(count=event.arg)
if pos:
deleted = line.delete_before_cursor(count=-pos)
line.set_clipboard(ClipboardData(deleted))
@handle(Keys.Escape, 'a', in_mode=InputMode.INSERT)
def _(event):
"""
Previous sentence.
"""
# TODO:
pass
@handle(Keys.Escape, 'c', in_mode=InputMode.INSERT)
def _(event):
"""
Capitalize the current (or following) word.
"""
for i in range(event.arg):
pos = line.document.find_next_word_ending()
words = line.document.text_after_cursor[:pos]
line.insert_text(words.title(), overwrite=True)
@handle(Keys.Escape, 'd', in_mode=InputMode.INSERT)
def _(event):
"""
Delete word forwards.
"""
pos = line.document.find_next_word_ending(count=event.arg)
if pos:
deleted = line.delete(count=pos)
line.set_clipboard(ClipboardData(deleted))
@handle(Keys.Escape, 'e', in_mode=InputMode.INSERT)
def _(event):
""" Move to end of sentence. """
# TODO:
pass
@handle(Keys.Escape, 'f', in_mode=InputMode.INSERT)
@handle(Keys.Escape, 'f', in_mode=InputMode.SELECTION)
def _(event):
"""
Cursor to end of next word.
"""
pos = line.document.find_next_word_ending(count=event.arg)
if pos:
line.cursor_position += pos
@handle(Keys.Escape, 'b', in_mode=InputMode.INSERT)
@handle(Keys.Escape, 'b', in_mode=InputMode.SELECTION)
def _(event):
"""
Cursor to start of previous word.
"""
pos = line.document.find_previous_word_beginning(count=event.arg)
if pos:
line.cursor_position += pos
@handle(Keys.Escape, 'l', in_mode=InputMode.INSERT)
def _(event):
"""
Lowercase the current (or following) word.
"""
for i in range(event.arg): # XXX: not DRY: see meta_c and meta_u!!
pos = line.document.find_next_word_ending()
words = line.document.text_after_cursor[:pos]
line.insert_text(words.lower(), overwrite=True)
@handle(Keys.Escape, 't', in_mode=InputMode.INSERT)
def _(event):
"""
Swap the last two words before the cursor.
"""
# TODO
@handle(Keys.Escape, 'u', in_mode=InputMode.INSERT)
def _(event):
"""
Uppercase the current (or following) word.
"""
for i in range(event.arg):
pos = line.document.find_next_word_ending()
words = line.document.text_after_cursor[:pos]
line.insert_text(words.upper(), overwrite=True)
@handle(Keys.Escape, '.', in_mode=InputMode.INSERT)
def _(event):
"""
Rotate through the last word (white-space delimited) of the previous lines in history.
"""
# TODO
@handle(Keys.Escape, '\\', in_mode=InputMode.INSERT)
def _(event):
"""
Delete all spaces and tabs around point.
(delete-horizontal-space)
"""
@handle(Keys.Escape, '*', in_mode=InputMode.INSERT)
def _(event):
"""
`meta-*`: Insert all possible completions of the preceding text.
"""
@handle(Keys.ControlX, Keys.ControlE, in_mode=InputMode.INSERT)
def _(event):
"""
Open editor.
"""
line.open_in_editor()
@handle(Keys.ControlX, Keys.ControlU, save_before=False, in_mode=InputMode.INSERT)
def _(event):
line.undo()
@handle(Keys.ControlX, Keys.ControlX, in_mode=InputMode.INSERT)
@handle(Keys.ControlX, Keys.ControlX, in_mode=InputMode.SELECTION)
def _(event):
"""
Move cursor back and forth between the start and end of the current
line.
"""
if line.document.current_char == '\n':
line.cursor_position += line.document.get_start_of_line_position(after_whitespace=False)
else:
line.cursor_position += line.document.get_end_of_line_position()
@handle(Keys.ControlSpace, in_mode=InputMode.INSERT)
@handle(Keys.ControlSpace, in_mode=InputMode.SELECTION)
def _(event):
"""
Start of the selection.
"""
# Take the current cursor position as the start of this selection.
line.start_selection(selection_type=SelectionType.CHARACTERS)
if event.input_processor.input_mode != InputMode.SELECTION:
event.input_processor.push_input_mode(InputMode.SELECTION)
@handle(Keys.ControlG, in_mode=InputMode.INSERT)
def _(event):
"""
Control + G: Cancel completion menu and validation state.
"""
line.complete_state = None
line.validation_error = None
@handle(Keys.ControlG, in_mode=InputMode.SELECTION)
def _(event):
"""
Cancel selection.
"""
event.input_processor.pop_input_mode()
line.exit_selection()
@handle(Keys.ControlG, in_mode=InputMode.INCREMENTAL_SEARCH)
# NOTE: the reason for not also binding Escape to this one, is that we want
# Alt+Enter to accept input directly in incremental search mode.
def _(event):
"""
Abort an incremental search and restore the original line.
"""
line.exit_isearch(restore_original_line=True)
event.input_processor.pop_input_mode()
search_line.reset()
@handle(Keys.ControlG, in_mode=InputMode.SYSTEM)
def _(event):
"""
Abort system prompt.
"""
system_line.reset()
event.input_processor.pop_input_mode()
@handle(Keys.ControlW, in_mode=InputMode.SELECTION)
def _(event):
"""
Cut selected text.
"""
deleted = line.cut_selection()
line.set_clipboard(ClipboardData(deleted))
event.input_processor.pop_input_mode()
@handle(Keys.Escape, 'w', in_mode=InputMode.SELECTION)
def _(event):
"""
Copy selected text.
"""
text = line.copy_selection()
line.set_clipboard(ClipboardData(text))
event.input_processor.pop_input_mode()
@handle(Keys.Escape, '<', in_mode=InputMode.INSERT)
def _(event):
"""
Move to the first line in the history.
"""
line.go_to_history(0)
@handle(Keys.Escape, '>', in_mode=InputMode.INSERT)
def _(event):
"""
Move to the end of the input history.
This is the line we are editing.
"""
line.go_to_history(len(line._working_lines) - 1)
@handle(Keys.ControlH, in_mode=InputMode.INCREMENTAL_SEARCH)
@handle(Keys.Backspace, in_mode=InputMode.INCREMENTAL_SEARCH)
def _(event):
search_line.delete_before_cursor()
line.set_search_text(search_line.text)
@handle(Keys.Any, in_mode=InputMode.INCREMENTAL_SEARCH)
def _(event):
"""
Insert isearch string.
"""
search_line.insert_text(event.data)
line.set_search_text(search_line.text)
@handle(Keys.ControlJ, in_mode=InputMode.INCREMENTAL_SEARCH)
@handle(Keys.ControlM, in_mode=InputMode.INCREMENTAL_SEARCH)
def _(event):
"""
When enter pressed in isearch, quit isearch mode. (Multiline
isearch would be too complicated.)
"""
search_line.reset()
line.exit_isearch()
event.input_processor.pop_input_mode()
@handle(Keys.ControlR, in_mode=InputMode.INSERT)
@handle(Keys.ControlR, in_mode=InputMode.INCREMENTAL_SEARCH)
@handle(Keys.Up, in_mode=InputMode.INCREMENTAL_SEARCH)
def _(event):
line.incremental_search(IncrementalSearchDirection.BACKWARD)
if event.input_processor.input_mode != InputMode.INCREMENTAL_SEARCH:
event.input_processor.push_input_mode(InputMode.INCREMENTAL_SEARCH)
@handle(Keys.ControlS, in_mode=InputMode.INSERT)
@handle(Keys.ControlS, in_mode=InputMode.INCREMENTAL_SEARCH)
@handle(Keys.Down, in_mode=InputMode.INCREMENTAL_SEARCH)
def _(event):
line.incremental_search(IncrementalSearchDirection.FORWARD)
if event.input_processor.input_mode != InputMode.INCREMENTAL_SEARCH:
event.input_processor.push_input_mode(InputMode.INCREMENTAL_SEARCH)
@handle(Keys.Escape, Keys.Left, in_mode=InputMode.INSERT)
@handle(Keys.Escape, Keys.Left, in_mode=InputMode.SELECTION)
def _(event):
"""
Cursor to start of previous word.
"""
line.cursor_position += line.document.find_previous_word_beginning(count=event.arg) or 0
@handle(Keys.Escape, Keys.Right, in_mode=InputMode.INSERT)
@handle(Keys.Escape, Keys.Right, in_mode=InputMode.SELECTION)
def _(event):
"""
Cursor to start of next word.
"""
line.cursor_position += line.document.find_next_word_beginning(count=event.arg) or \
line.document.end_position
@handle(Keys.Escape, '/', in_mode=InputMode.INSERT)
def _(event):
"""
M-/: Complete.
"""
line.complete_next()
@handle(Keys.Escape, '!', in_mode=InputMode.INSERT)
def _(event):
"""
M-'!' opens the system prompt.
"""
event.input_processor.push_input_mode(InputMode.SYSTEM)
@handle(Keys.ControlC, '>', in_mode=InputMode.SELECTION)
def _(event):
"""
Indent selected text.
"""
line.cursor_position += line.document.get_start_of_line_position(after_whitespace=True)
from_, to = line.document.selection_range()
from_, _ = line.document.translate_index_to_position(from_)
to, _ = line.document.translate_index_to_position(to)
indent(line, from_ - 1, to, count=event.arg) # XXX: why does translate_index_to_position return 1-based indexing???
event.input_processor.pop_input_mode()
@handle(Keys.ControlC, '<', in_mode=InputMode.SELECTION)
def _(event):
"""
Unindent selected text.
"""
from_, to = line.document.selection_range()
from_, _ = line.document.translate_index_to_position(from_)
to, _ = line.document.translate_index_to_position(to)
unindent(line, from_ - 1, to, count=event.arg)
event.input_processor.pop_input_mode()

File diff suppressed because it is too large Load Diff

View File

@ -66,7 +66,7 @@ class Layout(object):
lexer=None,
min_height=0,
show_tildes=False,
line_name='default'):
buffer_name='default'):
self.before_input = before_input
self.after_input = after_input
@ -77,7 +77,7 @@ class Layout(object):
self.menus = menus or []
self.min_height = min_height
self.show_tildes = show_tildes
self.line_name = line_name
self.buffer_name = buffer_name
if lexer:
self.lexer = lexer(
@ -96,11 +96,11 @@ class Layout(object):
self.reset()
def _line(self, cli):
def _buffer(self, cli):
"""
The line object that contains the 'main' content.
The buffer object that contains the 'main' content.
"""
return cli.lines[self.line_name]
return cli.buffers[self.buffer_name]
def reset(self):
#: Vertical scrolling position of the main content.
@ -110,21 +110,21 @@ class Layout(object):
"""
Tokenize input text for highlighting.
"""
line = self._line(cli)
buffer = self._buffer(cli)
def get():
if self.lexer:
tokens = list(self.lexer.get_tokens(line.text))
tokens = list(self.lexer.get_tokens(buffer.text))
else:
tokens = [(Token, line.text)]
tokens = [(Token, buffer.text)]
for p in self.input_processors:
tokens = p.process_tokens(tokens)
return tokens
return self._token_lru_cache.get(line.text, get)
return self._token_lru_cache.get(buffer.text, get)
def get_highlighted_characters(self, line):
def get_highlighted_characters(self, buffer):
"""
Return a dictionary that maps the index of input string characters to
their Token in case of highlighting.
@ -132,19 +132,19 @@ class Layout(object):
highlighted_characters = {}
# In case of incremental search, highlight all matches.
if line.isearch_state:
for index in line.document.find_all(line.isearch_state.isearch_text):
if index == line.cursor_position:
if buffer.isearch_state:
for index in buffer.document.find_all(buffer.isearch_state.isearch_text):
if index == buffer.cursor_position:
token = Token.SearchMatch.Current
else:
token = Token.SearchMatch
highlighted_characters.update(dict([
(x, token) for x in range(index, index + len(line.isearch_state.isearch_text))
(x, token) for x in range(index, index + len(buffer.isearch_state.isearch_text))
]))
# In case of selection, highlight all matches.
selection_range = line.document.selection_range()
selection_range = buffer.document.selection_range()
if selection_range:
from_, to = selection_range
@ -164,7 +164,7 @@ class Layout(object):
# Apply highlighting.
if not (cli.is_exiting or cli.is_aborting or cli.is_returning):
highlighted_characters = self.get_highlighted_characters(self._line(cli))
highlighted_characters = self.get_highlighted_characters(self._buffer(cli))
for index, token in highlighted_characters.items():
input_tokens[index] = (token, input_tokens[index][1])
@ -173,7 +173,7 @@ class Layout(object):
# Insert char.
screen.write_char(c, token,
string_index=index,
set_cursor_position=(index == self._line(cli).cursor_position))
set_cursor_position=(index == self._buffer(cli).cursor_position))
def write_input_scrolled(self, cli, screen, write_content,
min_height=1, top_margin=0, bottom_margin=0):
@ -216,7 +216,7 @@ class Layout(object):
# Scroll down if we need space for the menu.
if self._need_to_show_completion_menu(cli):
menu_size = self.menus[0].get_height(self._line(cli).complete_state)
menu_size = self.menus[0].get_height(self._buffer(cli).complete_state)
if temp_screen.cursor_position.y - self.vertical_scroll >= max_height - menu_size:
self.vertical_scroll = (temp_screen.cursor_position.y + 1) - (max_height - menu_size)
@ -241,7 +241,7 @@ class Layout(object):
# Show completion menu.
if not is_done and self._need_to_show_completion_menu(cli):
try:
y, x = temp_screen._cursor_mappings[self._line(cli).complete_state.original_document.cursor_position]
y, x = temp_screen._cursor_mappings[self._buffer(cli).complete_state.original_document.cursor_position]
except KeyError:
# This happens when the new, completed string is shorter than
# the original string. (e.g. in case of useless backslash
@ -249,7 +249,7 @@ class Layout(object):
# Not worth fixing at the moment. Just don't show the menu.
pass
else:
self.menus[0].write(screen, (y - self.vertical_scroll + top_margin, x + left_margin_width), self._line(cli).complete_state)
self.menus[0].write(screen, (y - self.vertical_scroll + top_margin, x + left_margin_width), self._buffer(cli).complete_state)
return_value = max([min_height + top_margin, screen.current_height])

View File

@ -1,7 +1,6 @@
from __future__ import unicode_literals
from pygments.token import Token
from ..enums import InputMode
__all__ = (
'CompletionsMenu',
@ -12,19 +11,17 @@ class CompletionsMenu(object):
"""
Helper for drawing the complete menu to the screen.
"""
def __init__(self, max_height=5, line_name='default'):
def __init__(self, max_height=5, buffer_name='default'):
self.max_height = max_height
self.line_name = line_name
self.buffer_name = buffer_name
self.token = Token.Menu.Completions
def is_visible(self, cli):
"""
True when this menu is visible.
"""
if cli.input_processor.input_mode in (InputMode.SYSTEM, InputMode.INCREMENTAL_SEARCH):
return False
return bool(cli.lines[self.line_name].complete_state)
return (cli.focus_stack.current == self.buffer_name and
bool(cli.buffers[self.buffer_name].complete_state))
def get_height(self, complete_state):
"""

View File

@ -1,7 +1,7 @@
from __future__ import unicode_literals
from pygments.token import Token
from ..enums import IncrementalSearchDirection, InputMode
from ..enums import IncrementalSearchDirection
__all__ = (
'Prompt',
@ -73,11 +73,10 @@ class DefaultPrompt(Prompt):
"""
List of (Token, text) tuples.
"""
if cli.input_processor.input_mode == InputMode.INCREMENTAL_SEARCH and cli.line.isearch_state:
return self.isearch_prompt(cli.line.isearch_state)
buffer = cli.buffers['default']
elif cli.input_processor.input_mode == InputMode.VI_SEARCH:
return self.vi_search_prompt()
if buffer.isearch_state:
return self.isearch_prompt(buffer.isearch_state)
elif cli.input_processor.arg is not None:
return self.arg_prompt(cli.input_processor.arg)
@ -100,7 +99,3 @@ class DefaultPrompt(Prompt):
Tokens for the prompt when we go in reverse-i-search mode.
"""
return self.isearch_composer(isearch_state).get_tokens()
def vi_search_prompt(self):
# TODO
return []

View File

@ -3,7 +3,7 @@ from __future__ import unicode_literals
from pygments.lexers import BashLexer
from pygments.token import Token
from ..enums import InputMode, IncrementalSearchDirection
from ..enums import IncrementalSearchDirection
from ..layout import Layout
from ..layout.prompt import Prompt
@ -95,18 +95,19 @@ class SystemToolbar(Toolbar):
"""
The system toolbar. Shows the '!'-prompt.
"""
def __init__(self):
def __init__(self, buffer_name='system'):
token = Token.Toolbar.System
super(SystemToolbar, self).__init__(token=token)
# We use a nested single-line-no-wrap layout for this.
self.layout = Layout(before_input=Prompt('Shell command: ', token=token.Prefix),
lexer=BashLexer,
line_name='system')
buffer_name='system')
self.buffer_name = buffer_name
def is_visible(self, cli):
return super(SystemToolbar, self).is_visible(cli) and \
cli.input_processor.input_mode == InputMode.SYSTEM
cli.focus_stack.current == self.buffer_name
def write(self, cli, screen):
# Just write this layout at the current position.
@ -115,41 +116,46 @@ class SystemToolbar(Toolbar):
class SearchToolbar(Toolbar):
def __init__(self, token=None):
def __init__(self, token=None, buffer_name='search'):
token = token or Token.Toolbar.Search
super(SearchToolbar, self).__init__(token=token)
self.buffer_name = buffer_name
class Prefix(Prompt):
""" Search prompt. """
def tokens(self, cli):
vi = cli.input_processor.input_mode == InputMode.VI_SEARCH
buffer = cli.buffers[cli.focus_stack.previous]
if cli.line.isearch_state.isearch_direction == IncrementalSearchDirection.BACKWARD:
text = '?' if vi else 'I-search backward: '
if buffer.isearch_state is None:
text = ''
elif buffer.isearch_state.isearch_direction == IncrementalSearchDirection.BACKWARD:
text = 'I-search backward: '
else:
text = '/' if vi else 'I-search: '
text = 'I-search: '
return [(token, text)]
class SearchLayout(Layout):
def get_input_tokens(self, cli):
line = cli.line
search_line = cli.lines['search']
index = line.isearch_state and line.isearch_state.no_match_from_index
buffer = cli.buffers[cli.focus_stack.previous]
search_buffer = cli.buffers[self.buffer_name]
index = buffer.isearch_state and buffer.isearch_state.no_match_from_index
if index is None:
return [(token.Text, search_line.text)]
return [(token.Text, search_buffer.text)]
else:
return [
(token.Text, search_line.text[:index]),
(token.Text.NoMatch, search_line.text[index:]),
(token.Text, search_buffer.text[:index]),
(token.Text.NoMatch, search_buffer.text[index:]),
]
self.layout = SearchLayout(before_input=Prefix(), line_name='search')
self.layout = SearchLayout(before_input=Prefix(), buffer_name=self.buffer_name)
def is_visible(self, cli):
return super(SearchToolbar, self).is_visible(cli) and \
cli.input_processor.input_mode in (InputMode.INCREMENTAL_SEARCH, InputMode.VI_SEARCH)
cli.focus_stack.current == self.buffer_name
def write(self, cli, screen):
self.layout.write_content(cli, screen)
@ -160,19 +166,21 @@ class CompletionsToolbar(Toolbar):
Helper for drawing the completion menu 'wildmenu'-style.
(Similar to Vim's wildmenu.)
"""
def __init__(self, token=None):
def __init__(self, token=None, buffer_name='default'):
token = token or Token.Toolbar.Completions
super(CompletionsToolbar, self).__init__(token=token)
self.buffer_name = buffer_name
def is_visible(self, cli):
return super(CompletionsToolbar, self).is_visible(cli) and \
bool(cli.line.complete_state) and len(cli.line.complete_state.current_completions) >= 1
bool(cli.buffers[self.buffer_name].complete_state) and \
len(cli.buffers[self.buffer_name].complete_state.current_completions) >= 1
def get_tokens(self, cli, width):
"""
Write the menu to the screen object.
"""
complete_state = cli.line.complete_state
complete_state = cli.buffers[self.buffer_name].complete_state
completions = complete_state.current_completions
index = complete_state.complete_index # Can be None!
@ -225,21 +233,24 @@ class ValidationToolbar(Toolbar):
"""
Toolbar for displaying validation errors.
"""
def __init__(self, token=None):
def __init__(self, token=None, buffer_name='default'):
token = token or Token.Toolbar.Validation
super(ValidationToolbar, self).__init__(token=token)
self.buffer_name = buffer_name
def is_visible(self, cli):
return super(ValidationToolbar, self).is_visible(cli) and \
bool(cli.line.validation_error)
bool(cli.current_buffer.validation_error)
def get_tokens(self, cli, width):
if cli.line.validation_error:
row, column = cli.line.document.translate_index_to_position(
cli.line.validation_error.index)
buffer = cli.buffers[self.buffer_name]
if buffer.validation_error:
row, column = buffer.document.translate_index_to_position(
buffer.validation_error.index)
text = '%s (line=%s column=%s)' % (
cli.line.validation_error.message, row, column)
buffer.validation_error.message, row, column)
return [(self.token, text)]
else:
return []

View File

@ -390,8 +390,8 @@ class Renderer(object):
#: We don't know this until a `report_absolute_cursor_row` call.
self._min_available_height = 0
# In case of Windown, also make sure to scroll to the current cursor
# position.
# In case of Windown, also make sure to scroll to the current cursor
# position.
if sys.platform == 'win32':
Output(self.stdout).scroll_buffer_to_prompt()

View File

@ -10,7 +10,7 @@ import termios
import tty
from ..keys import Keys
from ..key_binding import KeyPress
from ..key_binding.input_processor import KeyPress
__all__ = (
'InputStream',

View File

@ -13,6 +13,10 @@ class ValidationError(Exception):
self.index = index
self.message = message
def __repr__(self):
return 'ValidationError(index=%r, message=%r)' % (
self.index, self.message)
class Validator(object):
__metaclass__ = ABCMeta

View File

@ -0,0 +1,102 @@
from __future__ import unicode_literals
from prompt_toolkit.buffer import Buffer
import unittest
class BufferTest(unittest.TestCase):
def setUp(self):
self.buffer = Buffer()
def test_initial(self):
self.assertEqual(self.buffer.text, '')
self.assertEqual(self.buffer.cursor_position, 0)
def test_insert_text(self):
self.buffer.insert_text('some_text')
self.assertEqual(self.buffer.text, 'some_text')
self.assertEqual(self.buffer.cursor_position, len('some_text'))
def test_cursor_movement(self):
self.buffer.insert_text('some_text')
self.buffer.cursor_left()
self.buffer.cursor_left()
self.buffer.cursor_left()
self.buffer.cursor_right()
self.buffer.insert_text('A')
self.assertEqual(self.buffer.text, 'some_teAxt')
self.assertEqual(self.buffer.cursor_position, len('some_teA'))
def test_backspace(self):
self.buffer.insert_text('some_text')
self.buffer.cursor_left()
self.buffer.cursor_left()
self.buffer.delete_before_cursor()
self.assertEqual(self.buffer.text, 'some_txt')
self.assertEqual(self.buffer.cursor_position, len('some_t'))
def test_cursor_up(self):
# Cursor up to a line thats longer.
self.buffer.insert_text('long line1\nline2')
self.buffer.cursor_up()
self.assertEqual(self.buffer.document.cursor_position, 5)
# Going up when already at the top.
self.buffer.cursor_up()
self.assertEqual(self.buffer.document.cursor_position, 5)
# Going up to a line that's shorter.
self.buffer.reset()
self.buffer.insert_text('line1\nlong line2')
self.buffer.cursor_up()
self.assertEqual(self.buffer.document.cursor_position, 5)
def test_cursor_down(self):
self.buffer.insert_text('line1\nline2')
self.buffer.cursor_position = 3
# Normally going down
self.buffer.cursor_down()
self.assertEqual(self.buffer.document.cursor_position, len('line1\nlin'))
# Going down to a line that's storter.
self.buffer.reset()
self.buffer.insert_text('long line1\na\nb')
self.buffer.cursor_position = 3
self.buffer.cursor_down()
self.assertEqual(self.buffer.document.cursor_position, len('long line1\na'))
def test_join_next_line(self):
self.buffer.insert_text('line1\nline2\nline3')
self.buffer.cursor_up()
self.buffer.join_next_line()
self.assertEqual(self.buffer.text, 'line1\nline2line3')
# Test when there is no '\n' in the text
self.buffer.reset()
self.buffer.insert_text('line1')
self.buffer.cursor_position = 0
self.buffer.join_next_line()
self.assertEqual(self.buffer.text, 'line1')
def test_newline(self):
self.buffer.insert_text('hello world')
self.buffer.newline()
self.assertEqual(self.buffer.text, 'hello world\n')
def test_swap_characters_before_cursor(self):
self.buffer.insert_text('hello world')
self.buffer.cursor_left()
self.buffer.cursor_left()
self.buffer.swap_characters_before_cursor()
self.assertEqual(self.buffer.text, 'hello wrold')

View File

@ -1,6 +1,6 @@
from __future__ import unicode_literals
from prompt_toolkit.line import Document
from prompt_toolkit.document import Document
import unittest

View File

@ -1,6 +1,7 @@
from __future__ import unicode_literals
from prompt_toolkit.key_binding import InputProcessor, Registry, KeyPress
from prompt_toolkit.key_binding.input_processor import InputProcessor, KeyPress
from prompt_toolkit.key_binding.registry import Registry
from prompt_toolkit.keys import Keys
import unittest
@ -25,7 +26,7 @@ class KeyBindingTest(unittest.TestCase):
self.registry.add_binding(Keys.ControlD)(self.handlers.control_d)
self.registry.add_binding(Keys.ControlSquareClose, Keys.Any)(self.handlers.control_square_close_any)
self.processor = InputProcessor(self.registry)
self.processor = InputProcessor(self.registry, lambda: None)
def test_feed_simple(self):
self.processor.feed_key(KeyPress(Keys.ControlX, '\x18'))

View File

@ -1,151 +0,0 @@
from __future__ import unicode_literals
from prompt_toolkit.line import Line
import unittest
class LineTest(unittest.TestCase):
def setUp(self):
self.cli = Line()
def test_initial(self):
self.assertEqual(self.cli.text, '')
self.assertEqual(self.cli.cursor_position, 0)
def test_insert_text(self):
self.cli.insert_text('some_text')
self.assertEqual(self.cli.text, 'some_text')
self.assertEqual(self.cli.cursor_position, len('some_text'))
def test_cursor_movement(self):
self.cli.insert_text('some_text')
self.cli.cursor_left()
self.cli.cursor_left()
self.cli.cursor_left()
self.cli.cursor_right()
self.cli.insert_text('A')
self.assertEqual(self.cli.text, 'some_teAxt')
self.assertEqual(self.cli.cursor_position, len('some_teA'))
def test_backspace(self):
self.cli.insert_text('some_text')
self.cli.cursor_left()
self.cli.cursor_left()
self.cli.delete_before_cursor()
self.assertEqual(self.cli.text, 'some_txt')
self.assertEqual(self.cli.cursor_position, len('some_t'))
def test_cursor_up(self):
# Cursor up to a line thats longer.
self.cli.insert_text('long line1\nline2')
self.cli.cursor_up()
self.assertEqual(self.cli.document.cursor_position, 5)
# Going up when already at the top.
self.cli.cursor_up()
self.assertEqual(self.cli.document.cursor_position, 5)
# Going up to a line that's shorter.
self.cli.reset()
self.cli.insert_text('line1\nlong line2')
self.cli.cursor_up()
self.assertEqual(self.cli.document.cursor_position, 5)
def test_cursor_down(self):
self.cli.insert_text('line1\nline2')
self.cli.cursor_position = 3
# Normally going down
self.cli.cursor_down()
self.assertEqual(self.cli.document.cursor_position, len('line1\nlin'))
# Going down to a line that's storter.
self.cli.reset()
self.cli.insert_text('long line1\na\nb')
self.cli.cursor_position = 3
self.cli.cursor_down()
self.assertEqual(self.cli.document.cursor_position, len('long line1\na'))
# def test_auto_up_and_down(self):
# self.cli.insert_text('long line3\nlong line4')
#
# # Test current
# self.assertEqual(self.cli.text, 'long line3\nlong line4')
# self.assertEqual(self.cli.cursor_position, len('long line3\nlong line4'))
#
# # Go up.
# self.cli.auto_up()
# self.assertEqual(self.cli.text, 'long line3\nlong line4')
# self.assertEqual(self.cli.cursor_position, len('long line3'))
#
# # Go up again (goes to first item.)
# self.cli.auto_up()
# self.assertEqual(self.cli.text, 'line1\nline2')
# self.assertEqual(self.cli.cursor_position, len('line1\nline2'))
#
# # Go up again (goes to first line of first item.)
# self.cli.auto_up()
# self.assertEqual(self.cli.text, 'line1\nline2')
# self.assertEqual(self.cli.cursor_position, len('line1'))
#
# # Go up again (while we're at the first item in history.)
# # (Nothing changes.)
# self.cli.auto_up()
# self.assertEqual(self.cli.text, 'line1\nline2')
# self.assertEqual(self.cli.cursor_position, len('line1'))
#
# # Go down (to second line of first item.)
# self.cli.auto_down()
# self.assertEqual(self.cli.text, 'line1\nline2')
# self.assertEqual(self.cli.cursor_position, len('line1\nline2'))
#
# # Go down again (to first line of second item.)
# # (Going down goes to the first character of a line.)
# self.cli.auto_down()
# self.assertEqual(self.cli.text, 'long line3\nlong line4')
# self.assertEqual(self.cli.cursor_position, len(''))
#
# # Go down again (to second line of second item.)
# self.cli.auto_down()
# self.assertEqual(self.cli.text, 'long line3\nlong line4')
# self.assertEqual(self.cli.cursor_position, len('long line3\n'))
#
# # Go down again after the last line. (nothing should happen.)
# self.cli.auto_down()
# self.assertEqual(self.cli.text, 'long line3\nlong line4')
# self.assertEqual(self.cli.cursor_position, len('long line3\n'))
def test_join_next_line(self):
self.cli.insert_text('line1\nline2\nline3')
self.cli.cursor_up()
self.cli.join_next_line()
self.assertEqual(self.cli.text, 'line1\nline2line3')
# Test when there is no '\n' in the text
self.cli.reset()
self.cli.insert_text('line1')
self.cli.cursor_position = 0
self.cli.join_next_line()
self.assertEqual(self.cli.text, 'line1')
def test_newline(self):
self.cli.insert_text('hello world')
self.cli.newline()
self.assertEqual(self.cli.text, 'hello world\n')
def test_swap_characters_before_cursor(self):
self.cli.insert_text('hello world')
self.cli.cursor_left()
self.cli.cursor_left()
self.cli.swap_characters_before_cursor()
self.assertEqual(self.cli.text, 'hello wrold')

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python
from __future__ import unicode_literals
from line_tests import *
from buffer_tests import *
from document_tests import *
from inputstream_tests import *
from key_binding_tests import *
@ -12,4 +12,4 @@ from layout_tests import *
import unittest
if __name__ == '__main__':
unittest.main()
unittest.main()