[Python-modules-commits] [prompt-toolkit] 01/04: Import prompt-toolkit_1.0.8.orig.tar.gz

Scott Kitterman kitterman at moszumanska.debian.org
Sun Oct 30 13:14:44 UTC 2016


This is an automated email from the git hooks/post-receive script.

kitterman pushed a commit to branch master
in repository prompt-toolkit.

commit 97babc702d91a47d6cba9ba00db0e0e7f8d5492d
Author: Scott Kitterman <scott at kitterman.com>
Date:   Sun Oct 30 09:07:11 2016 -0400

    Import prompt-toolkit_1.0.8.orig.tar.gz
---
 CHANGELOG                                    |  25 +++
 PKG-INFO                                     |   4 +-
 README.rst                                   |   2 +-
 examples/patch-stdout.py                     |   2 +-
 prompt_toolkit.egg-info/PKG-INFO             |   4 +-
 prompt_toolkit/__init__.py                   |   2 +-
 prompt_toolkit/buffer.py                     |   5 +
 prompt_toolkit/eventloop/base.py             |   6 +-
 prompt_toolkit/eventloop/posix.py            |  43 +++--
 prompt_toolkit/eventloop/select.py           | 237 ++++++++++++++++++++-------
 prompt_toolkit/filters/cli.py                |  17 ++
 prompt_toolkit/interface.py                  |  17 +-
 prompt_toolkit/key_binding/bindings/basic.py |   4 +
 prompt_toolkit/key_binding/bindings/vi.py    | 207 +++++++++++++++++++----
 prompt_toolkit/key_binding/vi_state.py       |   1 +
 prompt_toolkit/keys.py                       |   6 +
 prompt_toolkit/layout/lexers.py              |   1 +
 prompt_toolkit/layout/processors.py          |  41 ++++-
 prompt_toolkit/renderer.py                   |  43 ++---
 prompt_toolkit/shortcuts.py                  |  13 +-
 prompt_toolkit/styles/defaults.py            |   2 +
 prompt_toolkit/terminal/vt100_input.py       |  65 ++++++--
 prompt_toolkit/terminal/win32_input.py       |   7 +-
 prompt_toolkit/terminal/win32_output.py      |   2 +-
 setup.cfg                                    |   2 +-
 tests/test_cli.py                            |  40 ++++-
 26 files changed, 629 insertions(+), 169 deletions(-)

diff --git a/CHANGELOG b/CHANGELOG
index d7c0de5..181efc6 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,31 @@
 CHANGELOG
 =========
 
+1.0.8: 2016-10-16
+-----------------
+
+Fixes:
+- In 'shortcuts': complete_while_typing was a SimpleFilter, not a CLIFilter.
+- Always reset color attributes after rendering.
+- Handle bug in Windows when '$TERM' is not defined.
+- Ignore errors when calling tcgetattr/tcsetattr.
+  (This handles the "Inappropriate ioctl for device" crash in some scenarios.)
+- Fix for Windows. Correctly recognize all Chinese and Lithuanian characters.
+
+New features:
+- Added shift+left/up/down/right keys.
+- Small performance optimization in the renderer.
+- Small optimization in the posix event loop. Don't call time.time() if we
+  don't have an inputhook. (Less syscalls.)
+- Turned the _max_postpone_until argument of call_from_executor into a float.
+  (As returned by `time.time`.) This will do less system calls. It's
+  backwards-incompatible, but this is still a private API, used only by pymux.)
+- Added Shift-I/A commands in Vi block selection mode for inserting text at the
+  beginning of each line of the block.
+- Refactoring of the 'selectors' module for the posix event loop. (Reuse the
+  same selector object in one loop, don't recreate it for each select.)
+
+
 1.0.7: 2016-08-21
 -----------------
 
diff --git a/PKG-INFO b/PKG-INFO
index 0035cfa..7907dd9 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: prompt_toolkit
-Version: 1.0.7
+Version: 1.0.8
 Summary: Library for building powerful interactive command lines in Python
 Home-page: https://github.com/jonathanslenders/python-prompt-toolkit
 Author: Jonathan Slenders
@@ -171,7 +171,7 @@ Description: Python Prompt Toolkit
         .. |Build Status| image:: https://api.travis-ci.org/jonathanslenders/python-prompt-toolkit.svg?branch=master
             :target: https://travis-ci.org/jonathanslenders/python-prompt-toolkit#
         
-        .. |PyPI| image:: https://pypip.in/version/prompt-toolkit/badge.svg
+        .. |PyPI| image:: https://img.shields.io/pypi/v/prompt_toolkit.svg
             :target: https://pypi.python.org/pypi/prompt-toolkit/
             :alt: Latest Version
         
diff --git a/README.rst b/README.rst
index 8eea0c5..80a6e07 100644
--- a/README.rst
+++ b/README.rst
@@ -163,6 +163,6 @@ Special thanks to
 .. |Build Status| image:: https://api.travis-ci.org/jonathanslenders/python-prompt-toolkit.svg?branch=master
     :target: https://travis-ci.org/jonathanslenders/python-prompt-toolkit#
 
-.. |PyPI| image:: https://pypip.in/version/prompt-toolkit/badge.svg
+.. |PyPI| image:: https://img.shields.io/pypi/v/prompt_toolkit.svg
     :target: https://pypi.python.org/pypi/prompt-toolkit/
     :alt: Latest Version
diff --git a/examples/patch-stdout.py b/examples/patch-stdout.py
index 2f952df..23eb3ff 100755
--- a/examples/patch-stdout.py
+++ b/examples/patch-stdout.py
@@ -29,7 +29,7 @@ def main():
     result = prompt('Say something: ', patch_stdout=True)
     print('You said: %s' % result)
 
-    # Stop thrad.
+    # Stop thread.
     running = False
 
 
diff --git a/prompt_toolkit.egg-info/PKG-INFO b/prompt_toolkit.egg-info/PKG-INFO
index 8a6d2bb..d8f089f 100644
--- a/prompt_toolkit.egg-info/PKG-INFO
+++ b/prompt_toolkit.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: prompt-toolkit
-Version: 1.0.7
+Version: 1.0.8
 Summary: Library for building powerful interactive command lines in Python
 Home-page: https://github.com/jonathanslenders/python-prompt-toolkit
 Author: Jonathan Slenders
@@ -171,7 +171,7 @@ Description: Python Prompt Toolkit
         .. |Build Status| image:: https://api.travis-ci.org/jonathanslenders/python-prompt-toolkit.svg?branch=master
             :target: https://travis-ci.org/jonathanslenders/python-prompt-toolkit#
         
-        .. |PyPI| image:: https://pypip.in/version/prompt-toolkit/badge.svg
+        .. |PyPI| image:: https://img.shields.io/pypi/v/prompt_toolkit.svg
             :target: https://pypi.python.org/pypi/prompt-toolkit/
             :alt: Latest Version
         
diff --git a/prompt_toolkit/__init__.py b/prompt_toolkit/__init__.py
index b5a76aa..57d6c1e 100644
--- a/prompt_toolkit/__init__.py
+++ b/prompt_toolkit/__init__.py
@@ -19,4 +19,4 @@ from .shortcuts import prompt
 
 
 # Don't forget to update in `docs/conf.py`!
-__version__ = '1.0.7'
+__version__ = '1.0.8'
diff --git a/prompt_toolkit/buffer.py b/prompt_toolkit/buffer.py
index 2612f8d..5b821fe 100644
--- a/prompt_toolkit/buffer.py
+++ b/prompt_toolkit/buffer.py
@@ -256,6 +256,11 @@ class Buffer(object):
         # State of the selection.
         self.selection_state = None
 
+        # Multiple cursor mode. (When we press 'I' or 'A' in visual-block mode,
+        # we can insert text on multiple lines at once. This is implemented by
+        # using multiple cursors.)
+        self.multiple_cursor_positions = []
+
         # When doing consecutive up/down movements, prefer to stay at this column.
         self.preferred_column = None
 
diff --git a/prompt_toolkit/eventloop/base.py b/prompt_toolkit/eventloop/base.py
index bac1e02..db86fac 100644
--- a/prompt_toolkit/eventloop/base.py
+++ b/prompt_toolkit/eventloop/base.py
@@ -74,8 +74,12 @@ class EventLoop(with_metaclass(ABCMeta, object)):
         Call this function in the main event loop. Similar to Twisted's
         ``callFromThread``.
 
-        :param _max_postpone_until: `None` or `datetime` instance. For interal
+        :param _max_postpone_until: `None` or `time.time` value. For interal
             use. If the eventloop is saturated, consider this task to be low
             priority and postpone maximum until this timestamp. (For instance,
             repaint is done using low priority.)
+
+            Note: In the past, this used to be a datetime.datetime instance,
+                  but apparently, executing `time.time` is more efficient: it
+                  does fewer system calls. (It doesn't read /etc/localtime.)
         """
diff --git a/prompt_toolkit/eventloop/posix.py b/prompt_toolkit/eventloop/posix.py
index b6a8f2e..0262f4d 100644
--- a/prompt_toolkit/eventloop/posix.py
+++ b/prompt_toolkit/eventloop/posix.py
@@ -1,10 +1,10 @@
 from __future__ import unicode_literals
-import datetime
 import fcntl
 import os
 import random
 import signal
 import threading
+import time
 
 from prompt_toolkit.terminal.vt100_input import InputStream
 from prompt_toolkit.utils import DummyContext, in_main_thread
@@ -14,21 +14,22 @@ from .callbacks import EventLoopCallbacks
 from .inputhook import InputHookContext
 from .posix_utils import PosixStdinReader
 from .utils import TimeIt
-from .select import select_fds
+from .select import AutoSelector, Selector, fd_to_int
 
 __all__ = (
     'PosixEventLoop',
 )
 
-_now = datetime.datetime.now
+_now = time.time
 
 
 class PosixEventLoop(EventLoop):
     """
     Event loop for posix systems (Linux, Mac os X).
     """
-    def __init__(self, inputhook=None):
+    def __init__(self, inputhook=None, selector=AutoSelector):
         assert inputhook is None or callable(inputhook)
+        assert issubclass(selector, Selector)
 
         self.running = False
         self.closed = False
@@ -37,6 +38,7 @@ class PosixEventLoop(EventLoop):
 
         self._calls_from_executor = []
         self._read_fds = {} # Maps fd to handler.
+        self.selector = selector()
 
         # Create a pipe for inter thread communication.
         self._schedule_pipe = os.pipe()
@@ -93,18 +95,21 @@ class PosixEventLoop(EventLoop):
         with ctx:
             while self._running:
                 # Call inputhook.
-                with TimeIt() as inputhook_timer:
-                    if self._inputhook_context:
+                if self._inputhook_context:
+                    with TimeIt() as inputhook_timer:
                         def ready(wait):
                             " True when there is input ready. The inputhook should return control. "
                             return self._ready_for_reading(current_timeout[0] if wait else 0) != []
                         self._inputhook_context.call_inputhook(ready)
+                    inputhook_duration = inputhook_timer.duration
+                else:
+                    inputhook_duration = 0
 
                 # Calculate remaining timeout. (The inputhook consumed some of the time.)
                 if current_timeout[0] is None:
                     remaining_timeout = None
                 else:
-                    remaining_timeout = max(0, current_timeout[0] - inputhook_timer.duration)
+                    remaining_timeout = max(0, current_timeout[0] - inputhook_duration)
 
                 # Wait until input is ready.
                 fds = self._ready_for_reading(remaining_timeout)
@@ -122,17 +127,23 @@ class PosixEventLoop(EventLoop):
                     # case.
                     tasks = []
                     low_priority_tasks = []
-                    now = _now()
+                    now = None  # Lazy load time. (Fewer system calls.)
 
                     for fd in fds:
                         # For the 'call_from_executor' fd, put each pending
                         # item on either the high or low priority queue.
                         if fd == self._schedule_pipe[0]:
                             for c, max_postpone_until in self._calls_from_executor:
-                                if max_postpone_until is None or max_postpone_until < now:
+                                if max_postpone_until is None:
+                                    # Execute now.
                                     tasks.append(c)
                                 else:
-                                    low_priority_tasks.append((c, max_postpone_until))
+                                    # Execute soon, if `max_postpone_until` is in the future.
+                                    now = now or _now()
+                                    if max_postpone_until < now:
+                                        tasks.append(c)
+                                    else:
+                                        low_priority_tasks.append((c, max_postpone_until))
                             self._calls_from_executor = []
 
                             # Flush all the pipe content.
@@ -179,8 +190,7 @@ class PosixEventLoop(EventLoop):
         """
         Return the file descriptors that are ready for reading.
         """
-        read_fds = list(self._read_fds.keys())
-        fds = select_fds(read_fds, timeout)
+        fds = self.selector.select(timeout)
         return fds
 
     def received_winch(self):
@@ -225,11 +235,12 @@ class PosixEventLoop(EventLoop):
         Call this function in the main event loop.
         Similar to Twisted's ``callFromThread``.
 
-        :param _max_postpone_until: `None` or `datetime` instance. For interal
+        :param _max_postpone_until: `None` or `time.time` value. For interal
             use. If the eventloop is saturated, consider this task to be low
             priority and postpone maximum until this timestamp. (For instance,
             repaint is done using low priority.)
         """
+        assert _max_postpone_until is None or isinstance(_max_postpone_until, float)
         self._calls_from_executor.append((callback, _max_postpone_until))
 
         if self._schedule_pipe:
@@ -257,13 +268,19 @@ class PosixEventLoop(EventLoop):
 
     def add_reader(self, fd, callback):
         " Add read file descriptor to the event loop. "
+        fd = fd_to_int(fd)
         self._read_fds[fd] = callback
+        self.selector.register(fd)
 
     def remove_reader(self, fd):
         " Remove read file descriptor from the event loop. "
+        fd = fd_to_int(fd)
+
         if fd in self._read_fds:
             del self._read_fds[fd]
 
+        self.selector.unregister(fd)
+
 
 class call_on_sigwinch(object):
     """
diff --git a/prompt_toolkit/eventloop/select.py b/prompt_toolkit/eventloop/select.py
index ae4fde4..f678f84 100644
--- a/prompt_toolkit/eventloop/select.py
+++ b/prompt_toolkit/eventloop/select.py
@@ -1,105 +1,216 @@
+"""
+Selectors for the Posix event loop.
+"""
 from __future__ import unicode_literals, absolute_import
 import sys
-import select
+import abc
 import errno
+import select
+import six
 
 __all__ = (
-    'select_fds',
+    'AutoSelector',
+    'PollSelector',
+    'SelectSelector',
+    'Selector',
+    'fd_to_int',
 )
 
-def _fd_to_int(fd):
+def fd_to_int(fd):
+    assert isinstance(fd, int) or hasattr(fd, 'fileno')
+
     if isinstance(fd, int):
         return fd
     else:
         return fd.fileno()
 
 
-def select_fds(read_fds, timeout):
-    """
-    Wait for a list of file descriptors (`read_fds`) to become ready for
-    reading. This chooses the most appropriate select-tool for use in
-    prompt-toolkit.
+class Selector(six.with_metaclass(abc.ABCMeta, object)):
+    @abc.abstractmethod
+    def register(self, fd):
+        assert isinstance(fd, int)
 
-    Note: This is an internal API that shouldn't be used for external projects.
-    """
-    # Map to ensure that we return the objects that were passed in originally.
-    # Whether they are a fd integer or an object that has a fileno().
-    # (The 'poll' implementation for instance, returns always integers.)
-    fd_map = dict((_fd_to_int(fd), fd) for fd in read_fds)
+    @abc.abstractmethod
+    def unregister(self, fd):
+        assert isinstance(fd, int)
 
-    # Use of the 'select' module, that was introduced in Python3.4. We don't
-    # use it before 3.5 however, because this is the point where this module
-    # retries interrupted system calls.
-    if sys.version_info >= (3, 5):
-        try:
-            result = _python3_selectors(read_fds, timeout)
-        except PermissionError:
-            # We had a situation (in pypager) where epoll raised a
-            # PermissionError when a local file descriptor was registered,
-            # however poll and select worked fine. So, in that case, just try
-            # using select below.
-            result = None
-    else:
-        result = None
+    @abc.abstractmethod
+    def select(self, timeout):
+        pass
+
+    @abc.abstractmethod
+    def close(self):
+        pass
+
+
+class AutoSelector(Selector):
+    def __init__(self):
+        self._fds = []
+
+        self._select_selector = SelectSelector()
+        self._selectors = [self._select_selector]
+
+        # When 'select.poll' exists, create a PollSelector.
+        if hasattr(select, 'poll'):
+            self._poll_selector = PollSelector()
+            self._selectors.append(self._poll_selector)
+        else:
+            self._poll_selector = None
+
+        # Use of the 'select' module, that was introduced in Python3.4. We don't
+        # use it before 3.5 however, because this is the point where this module
+        # retries interrupted system calls.
+        if sys.version_info >= (3, 5):
+            self._py3_selector = Python3Selector()
+            self._selectors.append(self._py3_selector)
+        else:
+            self._py3_selector = None
+
+    def register(self, fd):
+        assert isinstance(fd, int)
+
+        self._fds.append(fd)
+
+        for sel in self._selectors:
+            sel.register(fd)
+
+    def unregister(self, fd):
+        assert isinstance(fd, int)
+
+        self._fds.remove(fd)
+
+        for sel in self._selectors:
+            sel.unregister(fd)
+
+    def select(self, timeout):
+        # Try Python 3 selector first.
+        if self._py3_selector:
+            try:
+                return self._py3_selector.select(timeout)
+            except PermissionError:
+                # We had a situation (in pypager) where epoll raised a
+                # PermissionError when a local file descriptor was registered,
+                # however poll and select worked fine. So, in that case, just
+                # try using select below.
+                pass
 
-    if result is None:
         try:
-            # First, try the 'select' module. This is the most universal, and
-            # powerful enough in our case.
-            result = _select(read_fds, timeout)
+            # Prefer 'select.select', if we don't have much file descriptors.
+            # This is more universal.
+            return self._select_selector.select(timeout)
         except ValueError:
             # When we have more than 1024 open file descriptors, we'll always
             # get a "ValueError: filedescriptor out of range in select()" for
-            # 'select'. In this case, retry, using 'poll' instead.
-            result = _poll(read_fds, timeout)
+            # 'select'. In this case, try, using 'poll' instead.
+            if self._poll_selector is not None:
+                return self._poll_selector.select(timeout)
+            else:
+                raise
 
-    return [fd_map[_fd_to_int(fd)] for fd in result]
+    def close(self):
+        for sel in self._selectors:
+            sel.close()
 
 
-def _python3_selectors(read_fds, timeout):
+class Python3Selector(Selector):
     """
     Use of the Python3 'selectors' module.
 
     NOTE: Only use on Python 3.5 or newer!
     """
-    import selectors  # Inline import: Python3 only!
-    sel = selectors.DefaultSelector()
+    def __init__(self):
+        assert sys.version_info >= (3, 5)
 
-    for fd in read_fds:
-        sel.register(fd, selectors.EVENT_READ, None)
+        import selectors  # Inline import: Python3 only!
+        self._sel = selectors.DefaultSelector()
 
-    events = sel.select(timeout=timeout)
-    try:
+    def register(self, fd):
+        assert isinstance(fd, int)
+        import selectors  # Inline import: Python3 only!
+        self._sel.register(fd, selectors.EVENT_READ, None)
+
+    def unregister(self, fd):
+        assert isinstance(fd, int)
+        self._sel.unregister(fd)
+
+    def select(self, timeout):
+        events = self._sel.select(timeout=timeout)
         return [key.fileobj for key, mask in events]
-    finally:
-        sel.close()
 
+    def close(self):
+        self._sel.close()
 
-def _poll(read_fds, timeout):
-    """
-    Use 'poll', to wait for any of the given `read_fds` to become ready.
-    """
-    p = select.poll()
-    for fd in read_fds:
-        p.register(fd, select.POLLIN)
 
-    tuples = p.poll(timeout)  # Returns (fd, event) tuples.
-    return [t[0] for t in tuples]
+class PollSelector(Selector):
+    def __init__(self):
+        self._poll = select.poll()
+
+    def register(self, fd):
+        assert isinstance(fd, int)
+        self._poll.register(fd, select.POLLIN)
+
+    def unregister(self, fd):
+        assert isinstance(fd, int)
+
+    def select(self, timeout):
+        tuples = self._poll.poll(timeout)  # Returns (fd, event) tuples.
+        return [t[0] for t in tuples]
+
+    def close(self):
+        pass  # XXX
 
 
-def _select(read_fds, timeout):
+class SelectSelector(Selector):
     """
     Wrapper around select.select.
 
     When the SIGWINCH signal is handled, other system calls, like select
     are aborted in Python. This wrapper will retry the system call.
     """
-    while True:
-        try:
-            return select.select(read_fds, [], [], timeout)[0]
-        except select.error as e:
-            # Retry select call when EINTR
-            if e.args and e.args[0] == errno.EINTR:
-                continue
-            else:
-                raise
+    def __init__(self):
+        self._fds = []
+
+    def register(self, fd):
+        self._fds.append(fd)
+
+    def unregister(self, fd):
+        self._fds.remove(fd)
+
+    def select(self, timeout):
+        while True:
+            try:
+                return select.select(self._fds, [], [], timeout)[0]
+            except select.error as e:
+                # Retry select call when EINTR
+                if e.args and e.args[0] == errno.EINTR:
+                    continue
+                else:
+                    raise
+
+    def close(self):
+        pass
+
+
+def select_fds(read_fds, timeout, selector=AutoSelector):
+    """
+    Wait for a list of file descriptors (`read_fds`) to become ready for
+    reading. This chooses the most appropriate select-tool for use in
+    prompt-toolkit.
+    """
+    # Map to ensure that we return the objects that were passed in originally.
+    # Whether they are a fd integer or an object that has a fileno().
+    # (The 'poll' implementation for instance, returns always integers.)
+    fd_map = dict((fd_to_int(fd), fd) for fd in read_fds)
+
+    # Wait, using selector.
+    sel = selector()
+    try:
+        for fd in read_fds:
+            sel.register(fd)
+
+        result = sel.select(timeout)
+
+        if result is not None:
+            return [fd_map[fd_to_int(fd)] for fd in result]
+    finally:
+        sel.close()
diff --git a/prompt_toolkit/filters/cli.py b/prompt_toolkit/filters/cli.py
index f4486be..c0b0731 100644
--- a/prompt_toolkit/filters/cli.py
+++ b/prompt_toolkit/filters/cli.py
@@ -27,6 +27,7 @@ __all__ = (
     'ViMode',
     'ViNavigationMode',
     'ViInsertMode',
+    'ViInsertMultipleMode',
     'ViReplaceMode',
     'ViSelectionMode',
     'ViWaitingForTextObjectMode',
@@ -294,6 +295,22 @@ class ViInsertMode(Filter):
 
 
 @memoized()
+class ViInsertMultipleMode(Filter):
+    def __call__(self, cli):
+        if (cli.editing_mode != EditingMode.VI
+                or cli.vi_state.operator_func
+                or cli.vi_state.waiting_for_digraph
+                or cli.current_buffer.selection_state
+                or cli.current_buffer.read_only()):
+            return False
+
+        return cli.vi_state.input_mode == ViInputMode.INSERT_MULTIPLE
+
+    def __repr__(self):
+        return 'ViInsertMultipleMode()'
+
+
+ at memoized()
 class ViReplaceMode(Filter):
     def __call__(self, cli):
         if (cli.editing_mode != EditingMode.VI
diff --git a/prompt_toolkit/interface.py b/prompt_toolkit/interface.py
index c91f383..f1a7b62 100644
--- a/prompt_toolkit/interface.py
+++ b/prompt_toolkit/interface.py
@@ -3,7 +3,6 @@ The main `CommandLineInterface` class and logic.
 """
 from __future__ import unicode_literals
 
-import datetime
 import functools
 import os
 import signal
@@ -11,6 +10,7 @@ import six
 import sys
 import textwrap
 import threading
+import time
 import types
 import weakref
 
@@ -326,11 +326,16 @@ class CommandLineInterface(object):
                 self._redraw()
 
             # Call redraw in the eventloop (thread safe).
-            # Give it low priority. If there is other I/O or CPU intensive
-            # stuff to handle, give that priority, but max postpone x seconds.
-            _max_postpone_until = datetime.datetime.now() + datetime.timedelta(
-                seconds=self.max_render_postpone_time)
-            self.eventloop.call_from_executor(redraw, _max_postpone_until=_max_postpone_until)
+            # Usually with the high priority, in order to make the application
+            # feel responsive, but this can be tuned by changing the value of
+            # `max_render_postpone_time`.
+            if self.max_render_postpone_time:
+                _max_postpone_until = time.time() + self.max_render_postpone_time
+            else:
+                _max_postpone_until = None
+
+            self.eventloop.call_from_executor(
+                redraw, _max_postpone_until=_max_postpone_until)
 
     # Depracated alias for 'invalidate'.
     request_redraw = invalidate
diff --git a/prompt_toolkit/key_binding/bindings/basic.py b/prompt_toolkit/key_binding/bindings/basic.py
index cd8cb94..2d9de77 100644
--- a/prompt_toolkit/key_binding/bindings/basic.py
+++ b/prompt_toolkit/key_binding/bindings/basic.py
@@ -89,6 +89,10 @@ def load_basic_bindings(registry, filter=Always()):
     @handle(Keys.Down)
     @handle(Keys.Right)
     @handle(Keys.Left)
+    @handle(Keys.ShiftUp)
+    @handle(Keys.ShiftDown)
+    @handle(Keys.ShiftRight)
+    @handle(Keys.ShiftLeft)
     @handle(Keys.Home)
     @handle(Keys.End)
     @handle(Keys.Delete)
diff --git a/prompt_toolkit/key_binding/bindings/vi.py b/prompt_toolkit/key_binding/bindings/vi.py
index c77efaa..6185573 100644
--- a/prompt_toolkit/key_binding/bindings/vi.py
+++ b/prompt_toolkit/key_binding/bindings/vi.py
@@ -4,7 +4,7 @@ from prompt_toolkit.buffer import ClipboardData, indent, unindent, reshape_text
 from prompt_toolkit.document import Document
 from prompt_toolkit.enums import IncrementalSearchDirection, SEARCH_BUFFER, SYSTEM_BUFFER
 from prompt_toolkit.filters import Filter, Condition, HasArg, Always, to_cli_filter, IsReadOnly
-from prompt_toolkit.filters.cli import ViNavigationMode, ViInsertMode, ViReplaceMode, ViSelectionMode, ViWaitingForTextObjectMode, ViDigraphMode, ViMode
+from prompt_toolkit.filters.cli import ViNavigationMode, ViInsertMode, ViInsertMultipleMode, ViReplaceMode, ViSelectionMode, ViWaitingForTextObjectMode, ViDigraphMode, ViMode
 from prompt_toolkit.key_binding.digraphs import DIGRAPHS
 from prompt_toolkit.key_binding.vi_state import CharacterFind, InputMode
 from prompt_toolkit.keys import Keys
@@ -20,6 +20,16 @@ import codecs
 import six
 import string
 
+try:
+    from itertools import accumulate
+except ImportError: # < Python 3.2
+    def accumulate(iterable):
+        " Super simpel 'accumulate' implementation. "
+        total = 0
+        for item in iterable:
+            total += item
+            yield total
+
 __all__ = (
     'load_vi_bindings',
     'load_vi_search_bindings',
@@ -161,6 +171,7 @@ def load_vi_bindings(registry, enable_visual_key=Always(),
     #  ViState says different.)
     navigation_mode = ViNavigationMode()
     insert_mode = ViInsertMode()
+    insert_multiple_mode = ViInsertMultipleMode()
     replace_mode = ViReplaceMode()
     selection_mode = ViSelectionMode()
     operator_given = ViWaitingForTextObjectMode()
@@ -408,6 +419,41 @@ def load_vi_bindings(registry, enable_visual_key=Always(),
         event.current_buffer.cursor_position += \
             event.current_buffer.document.get_start_of_line_position(after_whitespace=True)
 
+    @Condition
+    def in_block_selection(cli):
+        buff = cli.current_buffer
+        return buff.selection_state and buff.selection_state.type == SelectionType.BLOCK
+
+    @handle('I', filter=in_block_selection & ~IsReadOnly())
+    def go_to_block_selection(event, after=False):
+        " Insert in block selection mode. "
+        buff = event.current_buffer
+
+        # Store all cursor positions.
+        positions = []
+
+        if after:
+            def get_pos(from_to):
+                return from_to[1] + 1
+        else:
+            def get_pos(from_to):
+                return from_to[0]
+
+        for i, from_to in enumerate(buff.document.selection_ranges()):
+            positions.append(get_pos(from_to))
+            if i == 0:
+                buff.cursor_position = get_pos(from_to)
+
+        buff.multiple_cursor_positions = positions
+
+        # Go to 'INSERT_MULTIPLE' mode.
+        event.cli.vi_state.input_mode = InputMode.INSERT_MULTIPLE
+        buff.exit_selection()
+
+    @handle('A', filter=in_block_selection & ~IsReadOnly())
+    def _(event):
+        go_to_block_selection(event, after=True)
+
     @handle('J', filter=navigation_mode & ~IsReadOnly())
     def _(event):
         " Join lines. "
@@ -797,6 +843,7 @@ def load_vi_bindings(registry, enable_visual_key=Always(),
         """
         filter = kw.pop('filter', Always())
         no_move_handler = kw.pop('no_move_handler', False)
+        no_selection_handler = kw.pop('no_selection_handler', False)
         assert not kw
 
         def decorator(text_object_func):
@@ -828,30 +875,32 @@ def load_vi_bindings(registry, enable_visual_key=Always(),
                     text_object = text_object_func(event)
                     event.current_buffer.cursor_position += text_object.start
 
-            @handle(*keys, filter=~operator_given & filter & selection_mode)
-            def _(event):
-                " Move handler for selection mode. "
-                text_object = text_object_func(event)
-                buff = event.current_buffer
-
-                # When the text object has both a start and end position, like 'i(' or 'iw',
-                # Turn this into a selection, otherwise the cursor.
-                if text_object.end:
-                    # Take selection positions from text object.
-                    start, end = text_object.operator_range(buff.document)
-                    start += buff.cursor_position
-                    end += buff.cursor_position
-
-                    buff.selection_state.original_cursor_position = start
-                    buff.cursor_position = end
-
-                    # Take selection type from text object.
-                    if text_object.type == TextObjectType.LINEWISE:
-                        buff.selection_state.type = SelectionType.LINES
+            # Register a move selection operation.
+            if not no_selection_handler:
+                @handle(*keys, filter=~operator_given & filter & selection_mode)
+                def _(event):
+                    " Move handler for selection mode. "
+                    text_object = text_object_func(event)
+                    buff = event.current_buffer
+
+                    # When the text object has both a start and end position, like 'i(' or 'iw',
+                    # Turn this into a selection, otherwise the cursor.
+                    if text_object.end:
+                        # Take selection positions from text object.
+                        start, end = text_object.operator_range(buff.document)
+                        start += buff.cursor_position
+                        end += buff.cursor_position
+
+                        buff.selection_state.original_cursor_position = start
+                        buff.cursor_position = end
+
+                        # Take selection type from text object.
+                        if text_object.type == TextObjectType.LINEWISE:
+                            buff.selection_state.type = SelectionType.LINES
+                        else:
+                            buff.selection_state.type = SelectionType.CHARACTERS
                     else:
-                        buff.selection_state.type = SelectionType.CHARACTERS
-                else:
-                    event.current_buffer.cursor_position += text_object.start
+                        event.current_buffer.cursor_position += text_object.start
 
             # Make it possible to chain @text_object decorators.
             return text_object_func
@@ -1203,15 +1252,20 @@ def load_vi_bindings(registry, enable_visual_key=Always(),
         """ Implements 'ch', 'dh', 'h': Cursor left. """
         return TextObject(event.current_buffer.document.get_cursor_left_position(count=event.arg))
 
-    @text_object('j', no_move_handler=True)
+    @text_object('j', no_move_handler=True, no_selection_handler=True)
+            # Note: We also need `no_selection_handler`, because we in
+            #       selection mode, we prefer the other 'j' binding that keeps
+            #       `buffer.preferred_column`.
     def _(event):
         """ Implements 'cj', 'dj', 'j', ... Cursor up. """
-        return TextObject(event.current_buffer.document.get_cursor_down_position(count=event.arg), type=TextObjectType.LINEWISE)
+        return TextObject(event.current_buffer.document.get_cursor_down_position(count=event.arg),
+                          type=TextObjectType.LINEWISE)
 
-    @text_object('k', no_move_handler=True)
+    @text_object('k', no_move_handler=True, no_selection_handler=True)
     def _(event):
         """ Implements 'ck', 'dk', 'k', ... Cursor up. """
-        return TextObject(event.current_buffer.document.get_cursor_up_position(count=event.arg), type=TextObjectType.LINEWISE)
+        return TextObject(event.current_buffer.document.get_cursor_up_position(count=event.arg),
+                          type=TextObjectType.LINEWISE)
 
     @text_object('l')
     @text_object(' ')
@@ -1464,6 +1518,103 @@ def load_vi_bindings(registry, enable_visual_key=Always(),
         """
         event.current_buffer.insert_text(event.data, overwrite=True)
 
+    @handle(Keys.Any, filter=insert_multiple_mode,
+            save_before=(lambda e: not e.is_repeat))
+    def _(event):
+        """
+        Insert data at multiple cursor positions at once.
+        (Usually a result of pressing 'I' or 'A' in block-selection mode.)
+        """
+        buff = event.current_buffer
+        original_text = buff.text
+
+        # Construct new text.
+        text = []
+        p = 0
+
+        for p2 in buff.multiple_cursor_positions:
+            text.append(original_text[p:p2])
+            text.append(event.data)
+            p = p2
+
+        text.append(original_text[p:])
+
+        # Shift all cursor positions.
+        new_cursor_positions = [
+            p + i + 1 for i, p in enumerate(buff.multiple_cursor_positions)]
+
+        # Set result.
+        buff.text = ''.join(text)
+        buff.multiple_cursor_positions = new_cursor_positions
+        buff.cursor_position += 1
+
+    @handle(Keys.Backspace, filter=insert_multiple_mode)
+    def _(event):
+        " Backspace, using multiple cursors. "
+        buff = event.current_buffer
+        original_text = buff.text
+
+        # Construct new text.
+        deleted_something = False
+        text = []
+        p = 0
+
+        for p2 in buff.multiple_cursor_positions:
+            if p2 > 0 and original_text[p2 - 1] != '\n':  # Don't delete across lines.
+                text.append(original_text[p:p2 - 1])
+                deleted_something = True
+            else:
+                text.append(original_text[p:p2])
+            p = p2
+
+        text.append(original_text[p:])
+
+        if deleted_something:
+            # Shift all cursor positions.
+            lengths = [len(part) for part in text[:-1]]
+            new_cursor_positions = list(accumulate(lengths))
+
+            # Set result.
+            buff.text = ''.join(text)
+            buff.multiple_cursor_positions = new_cursor_positions
+            buff.cursor_position -= 1
+        else:
+            event.cli.output.bell()
+
+    @handle(Keys.Delete, filter=insert_multiple_mode)
+    def _(event):
+        " Delete, using multiple cursors. "
+        buff = event.current_buffer
+        original_text = buff.text
+
+        # Construct new text.
+        deleted_something = False
+        text = []
+        new_cursor_positions = []
+        p = 0
+
+        for p2 in buff.multiple_cursor_positions:
+            text.append(original_text[p:p2])
+            if original_text[p2] == '\n':  # Don't delete across lines.
+                p = p2
+            else:
+                p = p2 + 1
+                deleted_something = True
+
+        text.append(original_text[p:])
+
+        if deleted_something:
+            # Shift all cursor positions.
+            lengths = [len(part) for part in text[:-1]]
+            new_cursor_positions = list(accumulate(lengths))
+
+            # Set result.
+            buff.text = ''.join(text)
+            buff.multiple_cursor_positions = new_cursor_positions
+        else:
+            event.cli.output.bell()
+
+
     @handle(Keys.ControlX, Keys.ControlL, filter=insert_mode)
     def _(event):
         """
diff --git a/prompt_toolkit/key_binding/vi_state.py b/prompt_toolkit/key_binding/vi_state.py
index 5c556a4..92ce3cb 100644
--- a/prompt_toolkit/key_binding/vi_state.py
+++ b/prompt_toolkit/key_binding/vi_state.py
@@ -9,6 +9,7 @@ __all__ = (
 
 class InputMode(object):
     INSERT = 'vi-insert'
+    INSERT_MULTIPLE = 'vi-insert-multiple'
     NAVIGATION = 'vi-navigation'
     REPLACE = 'vi-replace'
 
diff --git a/prompt_toolkit/keys.py b/prompt_toolkit/keys.py
index 3c6f4d7..d5df9bf 100644
--- a/prompt_toolkit/keys.py
+++ b/prompt_toolkit/keys.py
@@ -61,6 +61,12 @@ class Keys(object):
     Down        = Key('<Down>')
     Right       = Key('<Right>')
     Left        = Key('<Left>')
+
+    ShiftLeft   = Key('<ShiftLeft>')
+    ShiftUp     = Key('<ShiftUp>')
+    ShiftDown   = Key('<ShiftDown>')
... 468 lines suppressed ...

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/prompt-toolkit.git



More information about the Python-modules-commits mailing list