aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout
diff options
context:
space:
mode:
authorDevtools Arcadia <arcadia-devtools@yandex-team.ru>2022-02-07 18:08:42 +0300
committerDevtools Arcadia <arcadia-devtools@mous.vla.yp-c.yandex.net>2022-02-07 18:08:42 +0300
commit1110808a9d39d4b808aef724c861a2e1a38d2a69 (patch)
treee26c9fed0de5d9873cce7e00bc214573dc2195b7 /contrib/python/prompt-toolkit/py2/prompt_toolkit/layout
downloadydb-1110808a9d39d4b808aef724c861a2e1a38d2a69.tar.gz
intermediate changes
ref:cde9a383711a11544ce7e107a78147fb96cc4029
Diffstat (limited to 'contrib/python/prompt-toolkit/py2/prompt_toolkit/layout')
-rw-r--r--contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/__init__.py51
-rw-r--r--contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/containers.py1665
-rw-r--r--contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/controls.py730
-rw-r--r--contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/dimension.py92
-rw-r--r--contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/lexers.py320
-rw-r--r--contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/margins.py253
-rw-r--r--contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/menus.py496
-rw-r--r--contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/mouse_handlers.py29
-rw-r--r--contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/processors.py605
-rw-r--r--contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/prompt.py111
-rw-r--r--contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/screen.py151
-rw-r--r--contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/toolbars.py209
-rw-r--r--contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/utils.py181
13 files changed, 4893 insertions, 0 deletions
diff --git a/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/__init__.py b/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/__init__.py
new file mode 100644
index 00000000000..0dec5ecfaf3
--- /dev/null
+++ b/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/__init__.py
@@ -0,0 +1,51 @@
+"""
+Command line layout definitions
+-------------------------------
+
+The layout of a command line interface is defined by a Container instance.
+There are two main groups of classes here. Containers and controls:
+
+- A container can contain other containers or controls, it can have multiple
+ children and it decides about the dimensions.
+- A control is responsible for rendering the actual content to a screen.
+ A control can propose some dimensions, but it's the container who decides
+ about the dimensions -- or when the control consumes more space -- which part
+ of the control will be visible.
+
+
+Container classes::
+
+ - Container (Abstract base class)
+ |- HSplit (Horizontal split)
+ |- VSplit (Vertical split)
+ |- FloatContainer (Container which can also contain menus and other floats)
+ `- Window (Container which contains one actual control
+
+Control classes::
+
+ - UIControl (Abstract base class)
+ |- TokenListControl (Renders a simple list of tokens)
+ |- FillControl (Fills control with one token/character.)
+ `- BufferControl (Renders an input buffer.)
+
+
+Usually, you end up wrapping every control inside a `Window` object, because
+that's the only way to render it in a layout.
+
+There are some prepared toolbars which are ready to use::
+
+- SystemToolbar (Shows the 'system' input buffer, for entering system commands.)
+- ArgToolbar (Shows the input 'arg', for repetition of input commands.)
+- SearchToolbar (Shows the 'search' input buffer, for incremental search.)
+- CompletionsToolbar (Shows the completions of the current buffer.)
+- ValidationToolbar (Shows validation errors of the current buffer.)
+
+And one prepared menu:
+
+- CompletionsMenu
+
+"""
+from __future__ import unicode_literals
+
+from .containers import Float, FloatContainer, HSplit, VSplit, Window, ConditionalContainer
+from .controls import TokenListControl, FillControl, BufferControl
diff --git a/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/containers.py b/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/containers.py
new file mode 100644
index 00000000000..0bdafe18e04
--- /dev/null
+++ b/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/containers.py
@@ -0,0 +1,1665 @@
+"""
+Container for the layout.
+(Containers can contain other containers or user interface controls.)
+"""
+from __future__ import unicode_literals
+
+from abc import ABCMeta, abstractmethod
+from six import with_metaclass
+from six.moves import range
+
+from .controls import UIControl, TokenListControl, UIContent
+from .dimension import LayoutDimension, sum_layout_dimensions, max_layout_dimensions
+from .margins import Margin
+from .screen import Point, WritePosition, _CHAR_CACHE
+from .utils import token_list_to_text, explode_tokens
+from prompt_toolkit.cache import SimpleCache
+from prompt_toolkit.filters import to_cli_filter, ViInsertMode, EmacsInsertMode
+from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
+from prompt_toolkit.reactive import Integer
+from prompt_toolkit.token import Token
+from prompt_toolkit.utils import take_using_weights, get_cwidth
+
+__all__ = (
+ 'Container',
+ 'HSplit',
+ 'VSplit',
+ 'FloatContainer',
+ 'Float',
+ 'Window',
+ 'WindowRenderInfo',
+ 'ConditionalContainer',
+ 'ScrollOffsets',
+ 'ColorColumn',
+)
+
+Transparent = Token.Transparent
+
+
+class Container(with_metaclass(ABCMeta, object)):
+ """
+ Base class for user interface layout.
+ """
+ @abstractmethod
+ def reset(self):
+ """
+ Reset the state of this container and all the children.
+ (E.g. reset scroll offsets, etc...)
+ """
+
+ @abstractmethod
+ def preferred_width(self, cli, max_available_width):
+ """
+ Return a :class:`~prompt_toolkit.layout.dimension.LayoutDimension` that
+ represents the desired width for this container.
+
+ :param cli: :class:`~prompt_toolkit.interface.CommandLineInterface`.
+ """
+
+ @abstractmethod
+ def preferred_height(self, cli, width, max_available_height):
+ """
+ Return a :class:`~prompt_toolkit.layout.dimension.LayoutDimension` that
+ represents the desired height for this container.
+
+ :param cli: :class:`~prompt_toolkit.interface.CommandLineInterface`.
+ """
+
+ @abstractmethod
+ def write_to_screen(self, cli, screen, mouse_handlers, write_position):
+ """
+ Write the actual content to the screen.
+
+ :param cli: :class:`~prompt_toolkit.interface.CommandLineInterface`.
+ :param screen: :class:`~prompt_toolkit.layout.screen.Screen`
+ :param mouse_handlers: :class:`~prompt_toolkit.layout.mouse_handlers.MouseHandlers`.
+ """
+
+ @abstractmethod
+ def walk(self, cli):
+ """
+ Walk through all the layout nodes (and their children) and yield them.
+ """
+
+
+def _window_too_small():
+ " Create a `Window` that displays the 'Window too small' text. "
+ return Window(TokenListControl.static(
+ [(Token.WindowTooSmall, ' Window too small... ')]))
+
+
+class HSplit(Container):
+ """
+ Several layouts, one stacked above/under the other.
+
+ :param children: List of child :class:`.Container` objects.
+ :param window_too_small: A :class:`.Container` object that is displayed if
+ there is not enough space for all the children. By default, this is a
+ "Window too small" message.
+ :param get_dimensions: (`None` or a callable that takes a
+ `CommandLineInterface` and returns a list of `LayoutDimension`
+ instances.) By default the dimensions are taken from the children and
+ divided by the available space. However, when `get_dimensions` is specified,
+ this is taken instead.
+ :param report_dimensions_callback: When rendering, this function is called
+ with the `CommandLineInterface` and the list of used dimensions. (As a
+ list of integers.)
+ """
+ def __init__(self, children, window_too_small=None,
+ get_dimensions=None, report_dimensions_callback=None):
+ assert all(isinstance(c, Container) for c in children)
+ assert window_too_small is None or isinstance(window_too_small, Container)
+ assert get_dimensions is None or callable(get_dimensions)
+ assert report_dimensions_callback is None or callable(report_dimensions_callback)
+
+ self.children = children
+ self.window_too_small = window_too_small or _window_too_small()
+ self.get_dimensions = get_dimensions
+ self.report_dimensions_callback = report_dimensions_callback
+
+ def preferred_width(self, cli, max_available_width):
+ if self.children:
+ dimensions = [c.preferred_width(cli, max_available_width) for c in self.children]
+ return max_layout_dimensions(dimensions)
+ else:
+ return LayoutDimension(0)
+
+ def preferred_height(self, cli, width, max_available_height):
+ dimensions = [c.preferred_height(cli, width, max_available_height) for c in self.children]
+ return sum_layout_dimensions(dimensions)
+
+ def reset(self):
+ for c in self.children:
+ c.reset()
+
+ def write_to_screen(self, cli, screen, mouse_handlers, write_position):
+ """
+ Render the prompt to a `Screen` instance.
+
+ :param screen: The :class:`~prompt_toolkit.layout.screen.Screen` class
+ to which the output has to be written.
+ """
+ sizes = self._divide_heigths(cli, write_position)
+
+ if self.report_dimensions_callback:
+ self.report_dimensions_callback(cli, sizes)
+
+ if sizes is None:
+ self.window_too_small.write_to_screen(
+ cli, screen, mouse_handlers, write_position)
+ else:
+ # Draw child panes.
+ ypos = write_position.ypos
+ xpos = write_position.xpos
+ width = write_position.width
+
+ for s, c in zip(sizes, self.children):
+ c.write_to_screen(cli, screen, mouse_handlers, WritePosition(xpos, ypos, width, s))
+ ypos += s
+
+ def _divide_heigths(self, cli, write_position):
+ """
+ Return the heights for all rows.
+ Or None when there is not enough space.
+ """
+ if not self.children:
+ return []
+
+ # Calculate heights.
+ given_dimensions = self.get_dimensions(cli) if self.get_dimensions else None
+
+ def get_dimension_for_child(c, index):
+ if given_dimensions and given_dimensions[index] is not None:
+ return given_dimensions[index]
+ else:
+ return c.preferred_height(cli, write_position.width, write_position.extended_height)
+
+ dimensions = [get_dimension_for_child(c, index) for index, c in enumerate(self.children)]
+
+ # Sum dimensions
+ sum_dimensions = sum_layout_dimensions(dimensions)
+
+ # If there is not enough space for both.
+ # Don't do anything.
+ if sum_dimensions.min > write_position.extended_height:
+ return
+
+ # Find optimal sizes. (Start with minimal size, increase until we cover
+ # the whole height.)
+ sizes = [d.min for d in dimensions]
+
+ child_generator = take_using_weights(
+ items=list(range(len(dimensions))),
+ weights=[d.weight for d in dimensions])
+
+ i = next(child_generator)
+
+ while sum(sizes) < min(write_position.extended_height, sum_dimensions.preferred):
+ # Increase until we meet at least the 'preferred' size.
+ if sizes[i] < dimensions[i].preferred:
+ sizes[i] += 1
+ i = next(child_generator)
+
+ if not any([cli.is_returning, cli.is_exiting, cli.is_aborting]):
+ while sum(sizes) < min(write_position.height, sum_dimensions.max):
+ # Increase until we use all the available space. (or until "max")
+ if sizes[i] < dimensions[i].max:
+ sizes[i] += 1
+ i = next(child_generator)
+
+ return sizes
+
+ def walk(self, cli):
+ """ Walk through children. """
+ yield self
+ for c in self.children:
+ for i in c.walk(cli):
+ yield i
+
+
+class VSplit(Container):
+ """
+ Several layouts, one stacked left/right of the other.
+
+ :param children: List of child :class:`.Container` objects.
+ :param window_too_small: A :class:`.Container` object that is displayed if
+ there is not enough space for all the children. By default, this is a
+ "Window too small" message.
+ :param get_dimensions: (`None` or a callable that takes a
+ `CommandLineInterface` and returns a list of `LayoutDimension`
+ instances.) By default the dimensions are taken from the children and
+ divided by the available space. However, when `get_dimensions` is specified,
+ this is taken instead.
+ :param report_dimensions_callback: When rendering, this function is called
+ with the `CommandLineInterface` and the list of used dimensions. (As a
+ list of integers.)
+ """
+ def __init__(self, children, window_too_small=None,
+ get_dimensions=None, report_dimensions_callback=None):
+ assert all(isinstance(c, Container) for c in children)
+ assert window_too_small is None or isinstance(window_too_small, Container)
+ assert get_dimensions is None or callable(get_dimensions)
+ assert report_dimensions_callback is None or callable(report_dimensions_callback)
+
+ self.children = children
+ self.window_too_small = window_too_small or _window_too_small()
+ self.get_dimensions = get_dimensions
+ self.report_dimensions_callback = report_dimensions_callback
+
+ def preferred_width(self, cli, max_available_width):
+ dimensions = [c.preferred_width(cli, max_available_width) for c in self.children]
+ return sum_layout_dimensions(dimensions)
+
+ def preferred_height(self, cli, width, max_available_height):
+ sizes = self._divide_widths(cli, width)
+ if sizes is None:
+ return LayoutDimension()
+ else:
+ dimensions = [c.preferred_height(cli, s, max_available_height)
+ for s, c in zip(sizes, self.children)]
+ return max_layout_dimensions(dimensions)
+
+ def reset(self):
+ for c in self.children:
+ c.reset()
+
+ def _divide_widths(self, cli, width):
+ """
+ Return the widths for all columns.
+ Or None when there is not enough space.
+ """
+ if not self.children:
+ return []
+
+ # Calculate widths.
+ given_dimensions = self.get_dimensions(cli) if self.get_dimensions else None
+
+ def get_dimension_for_child(c, index):
+ if given_dimensions and given_dimensions[index] is not None:
+ return given_dimensions[index]
+ else:
+ return c.preferred_width(cli, width)
+
+ dimensions = [get_dimension_for_child(c, index) for index, c in enumerate(self.children)]
+
+ # Sum dimensions
+ sum_dimensions = sum_layout_dimensions(dimensions)
+
+ # If there is not enough space for both.
+ # Don't do anything.
+ if sum_dimensions.min > width:
+ return
+
+ # Find optimal sizes. (Start with minimal size, increase until we cover
+ # the whole height.)
+ sizes = [d.min for d in dimensions]
+
+ child_generator = take_using_weights(
+ items=list(range(len(dimensions))),
+ weights=[d.weight for d in dimensions])
+
+ i = next(child_generator)
+
+ while sum(sizes) < min(width, sum_dimensions.preferred):
+ # Increase until we meet at least the 'preferred' size.
+ if sizes[i] < dimensions[i].preferred:
+ sizes[i] += 1
+ i = next(child_generator)
+
+ while sum(sizes) < min(width, sum_dimensions.max):
+ # Increase until we use all the available space.
+ if sizes[i] < dimensions[i].max:
+ sizes[i] += 1
+ i = next(child_generator)
+
+ return sizes
+
+ def write_to_screen(self, cli, screen, mouse_handlers, write_position):
+ """
+ Render the prompt to a `Screen` instance.
+
+ :param screen: The :class:`~prompt_toolkit.layout.screen.Screen` class
+ to which the output has to be written.
+ """
+ if not self.children:
+ return
+
+ sizes = self._divide_widths(cli, write_position.width)
+
+ if self.report_dimensions_callback:
+ self.report_dimensions_callback(cli, sizes)
+
+ # If there is not enough space.
+ if sizes is None:
+ self.window_too_small.write_to_screen(
+ cli, screen, mouse_handlers, write_position)
+ return
+
+ # Calculate heights, take the largest possible, but not larger than write_position.extended_height.
+ heights = [child.preferred_height(cli, width, write_position.extended_height).preferred
+ for width, child in zip(sizes, self.children)]
+ height = max(write_position.height, min(write_position.extended_height, max(heights)))
+
+ # Draw child panes.
+ ypos = write_position.ypos
+ xpos = write_position.xpos
+
+ for s, c in zip(sizes, self.children):
+ c.write_to_screen(cli, screen, mouse_handlers, WritePosition(xpos, ypos, s, height))
+ xpos += s
+
+ def walk(self, cli):
+ """ Walk through children. """
+ yield self
+ for c in self.children:
+ for i in c.walk(cli):
+ yield i
+
+
+class FloatContainer(Container):
+ """
+ Container which can contain another container for the background, as well
+ as a list of floating containers on top of it.
+
+ Example Usage::
+
+ FloatContainer(content=Window(...),
+ floats=[
+ Float(xcursor=True,
+ ycursor=True,
+ layout=CompletionMenu(...))
+ ])
+ """
+ def __init__(self, content, floats):
+ assert isinstance(content, Container)
+ assert all(isinstance(f, Float) for f in floats)
+
+ self.content = content
+ self.floats = floats
+
+ def reset(self):
+ self.content.reset()
+
+ for f in self.floats:
+ f.content.reset()
+
+ def preferred_width(self, cli, write_position):
+ return self.content.preferred_width(cli, write_position)
+
+ def preferred_height(self, cli, width, max_available_height):
+ """
+ Return the preferred height of the float container.
+ (We don't care about the height of the floats, they should always fit
+ into the dimensions provided by the container.)
+ """
+ return self.content.preferred_height(cli, width, max_available_height)
+
+ def write_to_screen(self, cli, screen, mouse_handlers, write_position):
+ self.content.write_to_screen(cli, screen, mouse_handlers, write_position)
+
+ for fl in self.floats:
+ # When a menu_position was given, use this instead of the cursor
+ # position. (These cursor positions are absolute, translate again
+ # relative to the write_position.)
+ # Note: This should be inside the for-loop, because one float could
+ # set the cursor position to be used for the next one.
+ cursor_position = screen.menu_position or screen.cursor_position
+ cursor_position = Point(x=cursor_position.x - write_position.xpos,
+ y=cursor_position.y - write_position.ypos)
+
+ fl_width = fl.get_width(cli)
+ fl_height = fl.get_height(cli)
+
+ # Left & width given.
+ if fl.left is not None and fl_width is not None:
+ xpos = fl.left
+ width = fl_width
+ # Left & right given -> calculate width.
+ elif fl.left is not None and fl.right is not None:
+ xpos = fl.left
+ width = write_position.width - fl.left - fl.right
+ # Width & right given -> calculate left.
+ elif fl_width is not None and fl.right is not None:
+ xpos = write_position.width - fl.right - fl_width
+ width = fl_width
+ elif fl.xcursor:
+ width = fl_width
+ if width is None:
+ width = fl.content.preferred_width(cli, write_position.width).preferred
+ width = min(write_position.width, width)
+
+ xpos = cursor_position.x
+ if xpos + width > write_position.width:
+ xpos = max(0, write_position.width - width)
+ # Only width given -> center horizontally.
+ elif fl_width:
+ xpos = int((write_position.width - fl_width) / 2)
+ width = fl_width
+ # Otherwise, take preferred width from float content.
+ else:
+ width = fl.content.preferred_width(cli, write_position.width).preferred
+
+ if fl.left is not None:
+ xpos = fl.left
+ elif fl.right is not None:
+ xpos = max(0, write_position.width - width - fl.right)
+ else: # Center horizontally.
+ xpos = max(0, int((write_position.width - width) / 2))
+
+ # Trim.
+ width = min(width, write_position.width - xpos)
+
+ # Top & height given.
+ if fl.top is not None and fl_height is not None:
+ ypos = fl.top
+ height = fl_height
+ # Top & bottom given -> calculate height.
+ elif fl.top is not None and fl.bottom is not None:
+ ypos = fl.top
+ height = write_position.height - fl.top - fl.bottom
+ # Height & bottom given -> calculate top.
+ elif fl_height is not None and fl.bottom is not None:
+ ypos = write_position.height - fl_height - fl.bottom
+ height = fl_height
+ # Near cursor
+ elif fl.ycursor:
+ ypos = cursor_position.y + 1
+
+ height = fl_height
+ if height is None:
+ height = fl.content.preferred_height(
+ cli, width, write_position.extended_height).preferred
+
+ # Reduce height if not enough space. (We can use the
+ # extended_height when the content requires it.)
+ if height > write_position.extended_height - ypos:
+ if write_position.extended_height - ypos + 1 >= ypos:
+ # When the space below the cursor is more than
+ # the space above, just reduce the height.
+ height = write_position.extended_height - ypos
+ else:
+ # Otherwise, fit the float above the cursor.
+ height = min(height, cursor_position.y)
+ ypos = cursor_position.y - height
+
+ # Only height given -> center vertically.
+ elif fl_width:
+ ypos = int((write_position.height - fl_height) / 2)
+ height = fl_height
+ # Otherwise, take preferred height from content.
+ else:
+ height = fl.content.preferred_height(
+ cli, width, write_position.extended_height).preferred
+
+ if fl.top is not None:
+ ypos = fl.top
+ elif fl.bottom is not None:
+ ypos = max(0, write_position.height - height - fl.bottom)
+ else: # Center vertically.
+ ypos = max(0, int((write_position.height - height) / 2))
+
+ # Trim.
+ height = min(height, write_position.height - ypos)
+
+ # Write float.
+ # (xpos and ypos can be negative: a float can be partially visible.)
+ if height > 0 and width > 0:
+ wp = WritePosition(xpos=xpos + write_position.xpos,
+ ypos=ypos + write_position.ypos,
+ width=width, height=height)
+
+ if not fl.hide_when_covering_content or self._area_is_empty(screen, wp):
+ fl.content.write_to_screen(cli, screen, mouse_handlers, wp)
+
+ def _area_is_empty(self, screen, write_position):
+ """
+ Return True when the area below the write position is still empty.
+ (For floats that should not hide content underneath.)
+ """
+ wp = write_position
+ Transparent = Token.Transparent
+
+ for y in range(wp.ypos, wp.ypos + wp.height):
+ if y in screen.data_buffer:
+ row = screen.data_buffer[y]
+
+ for x in range(wp.xpos, wp.xpos + wp.width):
+ c = row[x]
+ if c.char != ' ' or c.token != Transparent:
+ return False
+
+ return True
+
+ def walk(self, cli):
+ """ Walk through children. """
+ yield self
+
+ for i in self.content.walk(cli):
+ yield i
+
+ for f in self.floats:
+ for i in f.content.walk(cli):
+ yield i
+
+
+class Float(object):
+ """
+ Float for use in a :class:`.FloatContainer`.
+
+ :param content: :class:`.Container` instance.
+ :param hide_when_covering_content: Hide the float when it covers content underneath.
+ """
+ def __init__(self, top=None, right=None, bottom=None, left=None,
+ width=None, height=None, get_width=None, get_height=None,
+ xcursor=False, ycursor=False, content=None,
+ hide_when_covering_content=False):
+ assert isinstance(content, Container)
+ assert width is None or get_width is None
+ assert height is None or get_height is None
+
+ self.left = left
+ self.right = right
+ self.top = top
+ self.bottom = bottom
+
+ self._width = width
+ self._height = height
+
+ self._get_width = get_width
+ self._get_height = get_height
+
+ self.xcursor = xcursor
+ self.ycursor = ycursor
+
+ self.content = content
+ self.hide_when_covering_content = hide_when_covering_content
+
+ def get_width(self, cli):
+ if self._width:
+ return self._width
+ if self._get_width:
+ return self._get_width(cli)
+
+ def get_height(self, cli):
+ if self._height:
+ return self._height
+ if self._get_height:
+ return self._get_height(cli)
+
+ def __repr__(self):
+ return 'Float(content=%r)' % self.content
+
+
+class WindowRenderInfo(object):
+ """
+ Render information, for the last render time of this control.
+ It stores mapping information between the input buffers (in case of a
+ :class:`~prompt_toolkit.layout.controls.BufferControl`) and the actual
+ render position on the output screen.
+
+ (Could be used for implementation of the Vi 'H' and 'L' key bindings as
+ well as implementing mouse support.)
+
+ :param ui_content: The original :class:`.UIContent` instance that contains
+ the whole input, without clipping. (ui_content)
+ :param horizontal_scroll: The horizontal scroll of the :class:`.Window` instance.
+ :param vertical_scroll: The vertical scroll of the :class:`.Window` instance.
+ :param window_width: The width of the window that displays the content,
+ without the margins.
+ :param window_height: The height of the window that displays the content.
+ :param configured_scroll_offsets: The scroll offsets as configured for the
+ :class:`Window` instance.
+ :param visible_line_to_row_col: Mapping that maps the row numbers on the
+ displayed screen (starting from zero for the first visible line) to
+ (row, col) tuples pointing to the row and column of the :class:`.UIContent`.
+ :param rowcol_to_yx: Mapping that maps (row, column) tuples representing
+ coordinates of the :class:`UIContent` to (y, x) absolute coordinates at
+ the rendered screen.
+ """
+ def __init__(self, ui_content, horizontal_scroll, vertical_scroll,
+ window_width, window_height,
+ configured_scroll_offsets,
+ visible_line_to_row_col, rowcol_to_yx,
+ x_offset, y_offset, wrap_lines):
+ assert isinstance(ui_content, UIContent)
+ assert isinstance(horizontal_scroll, int)
+ assert isinstance(vertical_scroll, int)
+ assert isinstance(window_width, int)
+ assert isinstance(window_height, int)
+ assert isinstance(configured_scroll_offsets, ScrollOffsets)
+ assert isinstance(visible_line_to_row_col, dict)
+ assert isinstance(rowcol_to_yx, dict)
+ assert isinstance(x_offset, int)
+ assert isinstance(y_offset, int)
+ assert isinstance(wrap_lines, bool)
+
+ self.ui_content = ui_content
+ self.vertical_scroll = vertical_scroll
+ self.window_width = window_width # Width without margins.
+ self.window_height = window_height
+
+ self.configured_scroll_offsets = configured_scroll_offsets
+ self.visible_line_to_row_col = visible_line_to_row_col
+ self.wrap_lines = wrap_lines
+
+ self._rowcol_to_yx = rowcol_to_yx # row/col from input to absolute y/x
+ # screen coordinates.
+ self._x_offset = x_offset
+ self._y_offset = y_offset
+
+ @property
+ def visible_line_to_input_line(self):
+ return dict(
+ (visible_line, rowcol[0])
+ for visible_line, rowcol in self.visible_line_to_row_col.items())
+
+ @property
+ def cursor_position(self):
+ """
+ Return the cursor position coordinates, relative to the left/top corner
+ of the rendered screen.
+ """
+ cpos = self.ui_content.cursor_position
+ y, x = self._rowcol_to_yx[cpos.y, cpos.x]
+ return Point(x=x - self._x_offset, y=y - self._y_offset)
+
+ @property
+ def applied_scroll_offsets(self):
+ """
+ Return a :class:`.ScrollOffsets` instance that indicates the actual
+ offset. This can be less than or equal to what's configured. E.g, when
+ the cursor is completely at the top, the top offset will be zero rather
+ than what's configured.
+ """
+ if self.displayed_lines[0] == 0:
+ top = 0
+ else:
+ # Get row where the cursor is displayed.
+ y = self.input_line_to_visible_line[self.ui_content.cursor_position.y]
+ top = min(y, self.configured_scroll_offsets.top)
+
+ return ScrollOffsets(
+ top=top,
+ bottom=min(self.ui_content.line_count - self.displayed_lines[-1] - 1,
+ self.configured_scroll_offsets.bottom),
+
+ # For left/right, it probably doesn't make sense to return something.
+ # (We would have to calculate the widths of all the lines and keep
+ # double width characters in mind.)
+ left=0, right=0)
+
+ @property
+ def displayed_lines(self):
+ """
+ List of all the visible rows. (Line numbers of the input buffer.)
+ The last line may not be entirely visible.
+ """
+ return sorted(row for row, col in self.visible_line_to_row_col.values())
+
+ @property
+ def input_line_to_visible_line(self):
+ """
+ Return the dictionary mapping the line numbers of the input buffer to
+ the lines of the screen. When a line spans several rows at the screen,
+ the first row appears in the dictionary.
+ """
+ result = {}
+ for k, v in self.visible_line_to_input_line.items():
+ if v in result:
+ result[v] = min(result[v], k)
+ else:
+ result[v] = k
+ return result
+
+ def first_visible_line(self, after_scroll_offset=False):
+ """
+ Return the line number (0 based) of the input document that corresponds
+ with the first visible line.
+ """
+ if after_scroll_offset:
+ return self.displayed_lines[self.applied_scroll_offsets.top]
+ else:
+ return self.displayed_lines[0]
+
+ def last_visible_line(self, before_scroll_offset=False):
+ """
+ Like `first_visible_line`, but for the last visible line.
+ """
+ if before_scroll_offset:
+ return self.displayed_lines[-1 - self.applied_scroll_offsets.bottom]
+ else:
+ return self.displayed_lines[-1]
+
+ def center_visible_line(self, before_scroll_offset=False,
+ after_scroll_offset=False):
+ """
+ Like `first_visible_line`, but for the center visible line.
+ """
+ return (self.first_visible_line(after_scroll_offset) +
+ (self.last_visible_line(before_scroll_offset) -
+ self.first_visible_line(after_scroll_offset)) // 2
+ )
+
+ @property
+ def content_height(self):
+ """
+ The full height of the user control.
+ """
+ return self.ui_content.line_count
+
+ @property
+ def full_height_visible(self):
+ """
+ True when the full height is visible (There is no vertical scroll.)
+ """
+ return self.vertical_scroll == 0 and self.last_visible_line() == self.content_height
+
+ @property
+ def top_visible(self):
+ """
+ True when the top of the buffer is visible.
+ """
+ return self.vertical_scroll == 0
+
+ @property
+ def bottom_visible(self):
+ """
+ True when the bottom of the buffer is visible.
+ """
+ return self.last_visible_line() == self.content_height - 1
+
+ @property
+ def vertical_scroll_percentage(self):
+ """
+ Vertical scroll as a percentage. (0 means: the top is visible,
+ 100 means: the bottom is visible.)
+ """
+ if self.bottom_visible:
+ return 100
+ else:
+ return (100 * self.vertical_scroll // self.content_height)
+
+ def get_height_for_line(self, lineno):
+ """
+ Return the height of the given line.
+ (The height that it would take, if this line became visible.)
+ """
+ if self.wrap_lines:
+ return self.ui_content.get_height_for_line(lineno, self.window_width)
+ else:
+ return 1
+
+
+class ScrollOffsets(object):
+ """
+ Scroll offsets for the :class:`.Window` class.
+
+ Note that left/right offsets only make sense if line wrapping is disabled.
+ """
+ def __init__(self, top=0, bottom=0, left=0, right=0):
+ assert isinstance(top, Integer)
+ assert isinstance(bottom, Integer)
+ assert isinstance(left, Integer)
+ assert isinstance(right, Integer)
+
+ self._top = top
+ self._bottom = bottom
+ self._left = left
+ self._right = right
+
+ @property
+ def top(self):
+ return int(self._top)
+
+ @property
+ def bottom(self):
+ return int(self._bottom)
+
+ @property
+ def left(self):
+ return int(self._left)
+
+ @property
+ def right(self):
+ return int(self._right)
+
+ def __repr__(self):
+ return 'ScrollOffsets(top=%r, bottom=%r, left=%r, right=%r)' % (
+ self.top, self.bottom, self.left, self.right)
+
+
+class ColorColumn(object):
+ def __init__(self, position, token=Token.ColorColumn):
+ self.position = position
+ self.token = token
+
+
+_in_insert_mode = ViInsertMode() | EmacsInsertMode()
+
+
+class Window(Container):
+ """
+ Container that holds a control.
+
+ :param content: :class:`~prompt_toolkit.layout.controls.UIControl` instance.
+ :param width: :class:`~prompt_toolkit.layout.dimension.LayoutDimension` instance.
+ :param height: :class:`~prompt_toolkit.layout.dimension.LayoutDimension` instance.
+ :param get_width: callable which takes a `CommandLineInterface` and returns a `LayoutDimension`.
+ :param get_height: callable which takes a `CommandLineInterface` and returns a `LayoutDimension`.
+ :param dont_extend_width: When `True`, don't take up more width then the
+ preferred width reported by the control.
+ :param dont_extend_height: When `True`, don't take up more width then the
+ preferred height reported by the control.
+ :param left_margins: A list of :class:`~prompt_toolkit.layout.margins.Margin`
+ instance to be displayed on the left. For instance:
+ :class:`~prompt_toolkit.layout.margins.NumberredMargin` can be one of
+ them in order to show line numbers.
+ :param right_margins: Like `left_margins`, but on the other side.
+ :param scroll_offsets: :class:`.ScrollOffsets` instance, representing the
+ preferred amount of lines/columns to be always visible before/after the
+ cursor. When both top and bottom are a very high number, the cursor
+ will be centered vertically most of the time.
+ :param allow_scroll_beyond_bottom: A `bool` or
+ :class:`~prompt_toolkit.filters.CLIFilter` instance. When True, allow
+ scrolling so far, that the top part of the content is not visible
+ anymore, while there is still empty space available at the bottom of
+ the window. In the Vi editor for instance, this is possible. You will
+ see tildes while the top part of the body is hidden.
+ :param wrap_lines: A `bool` or :class:`~prompt_toolkit.filters.CLIFilter`
+ instance. When True, don't scroll horizontally, but wrap lines instead.
+ :param get_vertical_scroll: Callable that takes this window
+ instance as input and returns a preferred vertical scroll.
+ (When this is `None`, the scroll is only determined by the last and
+ current cursor position.)
+ :param get_horizontal_scroll: Callable that takes this window
+ instance as input and returns a preferred vertical scroll.
+ :param always_hide_cursor: A `bool` or
+ :class:`~prompt_toolkit.filters.CLIFilter` instance. When True, never
+ display the cursor, even when the user control specifies a cursor
+ position.
+ :param cursorline: A `bool` or :class:`~prompt_toolkit.filters.CLIFilter`
+ instance. When True, display a cursorline.
+ :param cursorcolumn: A `bool` or :class:`~prompt_toolkit.filters.CLIFilter`
+ instance. When True, display a cursorcolumn.
+ :param get_colorcolumns: A callable that takes a `CommandLineInterface` and
+ returns a a list of :class:`.ColorColumn` instances that describe the
+ columns to be highlighted.
+ :param cursorline_token: The token to be used for highlighting the current line,
+ if `cursorline` is True.
+ :param cursorcolumn_token: The token to be used for highlighting the current line,
+ if `cursorcolumn` is True.
+ """
+ def __init__(self, content, width=None, height=None, get_width=None,
+ get_height=None, dont_extend_width=False, dont_extend_height=False,
+ left_margins=None, right_margins=None, scroll_offsets=None,
+ allow_scroll_beyond_bottom=False, wrap_lines=False,
+ get_vertical_scroll=None, get_horizontal_scroll=None, always_hide_cursor=False,
+ cursorline=False, cursorcolumn=False, get_colorcolumns=None,
+ cursorline_token=Token.CursorLine, cursorcolumn_token=Token.CursorColumn):
+ assert isinstance(content, UIControl)
+ assert width is None or isinstance(width, LayoutDimension)
+ assert height is None or isinstance(height, LayoutDimension)
+ assert get_width is None or callable(get_width)
+ assert get_height is None or callable(get_height)
+ assert width is None or get_width is None
+ assert height is None or get_height is None
+ assert scroll_offsets is None or isinstance(scroll_offsets, ScrollOffsets)
+ assert left_margins is None or all(isinstance(m, Margin) for m in left_margins)
+ assert right_margins is None or all(isinstance(m, Margin) for m in right_margins)
+ assert get_vertical_scroll is None or callable(get_vertical_scroll)
+ assert get_horizontal_scroll is None or callable(get_horizontal_scroll)
+ assert get_colorcolumns is None or callable(get_colorcolumns)
+
+ self.allow_scroll_beyond_bottom = to_cli_filter(allow_scroll_beyond_bottom)
+ self.always_hide_cursor = to_cli_filter(always_hide_cursor)
+ self.wrap_lines = to_cli_filter(wrap_lines)
+ self.cursorline = to_cli_filter(cursorline)
+ self.cursorcolumn = to_cli_filter(cursorcolumn)
+
+ self.content = content
+ self.dont_extend_width = dont_extend_width
+ self.dont_extend_height = dont_extend_height
+ self.left_margins = left_margins or []
+ self.right_margins = right_margins or []
+ self.scroll_offsets = scroll_offsets or ScrollOffsets()
+ self.get_vertical_scroll = get_vertical_scroll
+ self.get_horizontal_scroll = get_horizontal_scroll
+ self._width = get_width or (lambda cli: width)
+ self._height = get_height or (lambda cli: height)
+ self.get_colorcolumns = get_colorcolumns or (lambda cli: [])
+ self.cursorline_token = cursorline_token
+ self.cursorcolumn_token = cursorcolumn_token
+
+ # Cache for the screens generated by the margin.
+ self._ui_content_cache = SimpleCache(maxsize=8)
+ self._margin_width_cache = SimpleCache(maxsize=1)
+
+ self.reset()
+
+ def __repr__(self):
+ return 'Window(content=%r)' % self.content
+
+ def reset(self):
+ self.content.reset()
+
+ #: Scrolling position of the main content.
+ self.vertical_scroll = 0
+ self.horizontal_scroll = 0
+
+ # Vertical scroll 2: this is the vertical offset that a line is
+ # scrolled if a single line (the one that contains the cursor) consumes
+ # all of the vertical space.
+ self.vertical_scroll_2 = 0
+
+ #: Keep render information (mappings between buffer input and render
+ #: output.)
+ self.render_info = None
+
+ def _get_margin_width(self, cli, margin):
+ """
+ Return the width for this margin.
+ (Calculate only once per render time.)
+ """
+ # Margin.get_width, needs to have a UIContent instance.
+ def get_ui_content():
+ return self._get_ui_content(cli, width=0, height=0)
+
+ def get_width():
+ return margin.get_width(cli, get_ui_content)
+
+ key = (margin, cli.render_counter)
+ return self._margin_width_cache.get(key, get_width)
+
+ def preferred_width(self, cli, max_available_width):
+ # Calculate the width of the margin.
+ total_margin_width = sum(self._get_margin_width(cli, m) for m in
+ self.left_margins + self.right_margins)
+
+ # Window of the content. (Can be `None`.)
+ preferred_width = self.content.preferred_width(
+ cli, max_available_width - total_margin_width)
+
+ if preferred_width is not None:
+ # Include width of the margins.
+ preferred_width += total_margin_width
+
+ # Merge.
+ return self._merge_dimensions(
+ dimension=self._width(cli),
+ preferred=preferred_width,
+ dont_extend=self.dont_extend_width)
+
+ def preferred_height(self, cli, width, max_available_height):
+ total_margin_width = sum(self._get_margin_width(cli, m) for m in
+ self.left_margins + self.right_margins)
+ wrap_lines = self.wrap_lines(cli)
+
+ return self._merge_dimensions(
+ dimension=self._height(cli),
+ preferred=self.content.preferred_height(
+ cli, width - total_margin_width, max_available_height, wrap_lines),
+ dont_extend=self.dont_extend_height)
+
+ @staticmethod
+ def _merge_dimensions(dimension, preferred=None, dont_extend=False):
+ """
+ Take the LayoutDimension from this `Window` class and the received
+ preferred size from the `UIControl` and return a `LayoutDimension` to
+ report to the parent container.
+ """
+ dimension = dimension or LayoutDimension()
+
+ # When a preferred dimension was explicitly given to the Window,
+ # ignore the UIControl.
+ if dimension.preferred_specified:
+ preferred = dimension.preferred
+
+ # When a 'preferred' dimension is given by the UIControl, make sure
+ # that it stays within the bounds of the Window.
+ if preferred is not None:
+ if dimension.max:
+ preferred = min(preferred, dimension.max)
+
+ if dimension.min:
+ preferred = max(preferred, dimension.min)
+
+ # When a `dont_extend` flag has been given, use the preferred dimension
+ # also as the max dimension.
+ if dont_extend and preferred is not None:
+ max_ = min(dimension.max, preferred)
+ else:
+ max_ = dimension.max
+
+ return LayoutDimension(
+ min=dimension.min, max=max_,
+ preferred=preferred, weight=dimension.weight)
+
+ def _get_ui_content(self, cli, width, height):
+ """
+ Create a `UIContent` instance.
+ """
+ def get_content():
+ return self.content.create_content(cli, width=width, height=height)
+
+ key = (cli.render_counter, width, height)
+ return self._ui_content_cache.get(key, get_content)
+
+ def _get_digraph_char(self, cli):
+ " Return `False`, or the Digraph symbol to be used. "
+ if cli.quoted_insert:
+ return '^'
+ if cli.vi_state.waiting_for_digraph:
+ if cli.vi_state.digraph_symbol1:
+ return cli.vi_state.digraph_symbol1
+ return '?'
+ return False
+
+ def write_to_screen(self, cli, screen, mouse_handlers, write_position):
+ """
+ Write window to screen. This renders the user control, the margins and
+ copies everything over to the absolute position at the given screen.
+ """
+ # Calculate margin sizes.
+ left_margin_widths = [self._get_margin_width(cli, m) for m in self.left_margins]
+ right_margin_widths = [self._get_margin_width(cli, m) for m in self.right_margins]
+ total_margin_width = sum(left_margin_widths + right_margin_widths)
+
+ # Render UserControl.
+ ui_content = self.content.create_content(
+ cli, write_position.width - total_margin_width, write_position.height)
+ assert isinstance(ui_content, UIContent)
+
+ # Scroll content.
+ wrap_lines = self.wrap_lines(cli)
+ scroll_func = self._scroll_when_linewrapping if wrap_lines else self._scroll_without_linewrapping
+
+ scroll_func(
+ ui_content, write_position.width - total_margin_width, write_position.height, cli)
+
+ # Write body
+ visible_line_to_row_col, rowcol_to_yx = self._copy_body(
+ cli, ui_content, screen, write_position,
+ sum(left_margin_widths), write_position.width - total_margin_width,
+ self.vertical_scroll, self.horizontal_scroll,
+ has_focus=self.content.has_focus(cli),
+ wrap_lines=wrap_lines, highlight_lines=True,
+ vertical_scroll_2=self.vertical_scroll_2,
+ always_hide_cursor=self.always_hide_cursor(cli))
+
+ # Remember render info. (Set before generating the margins. They need this.)
+ x_offset=write_position.xpos + sum(left_margin_widths)
+ y_offset=write_position.ypos
+
+ self.render_info = WindowRenderInfo(
+ ui_content=ui_content,
+ horizontal_scroll=self.horizontal_scroll,
+ vertical_scroll=self.vertical_scroll,
+ window_width=write_position.width - total_margin_width,
+ window_height=write_position.height,
+ configured_scroll_offsets=self.scroll_offsets,
+ visible_line_to_row_col=visible_line_to_row_col,
+ rowcol_to_yx=rowcol_to_yx,
+ x_offset=x_offset,
+ y_offset=y_offset,
+ wrap_lines=wrap_lines)
+
+ # Set mouse handlers.
+ def mouse_handler(cli, mouse_event):
+ """ Wrapper around the mouse_handler of the `UIControl` that turns
+ screen coordinates into line coordinates. """
+ # Find row/col position first.
+ yx_to_rowcol = dict((v, k) for k, v in rowcol_to_yx.items())
+ y = mouse_event.position.y
+ x = mouse_event.position.x
+
+ # If clicked below the content area, look for a position in the
+ # last line instead.
+ max_y = write_position.ypos + len(visible_line_to_row_col) - 1
+ y = min(max_y, y)
+
+ while x >= 0:
+ try:
+ row, col = yx_to_rowcol[y, x]
+ except KeyError:
+ # Try again. (When clicking on the right side of double
+ # width characters, or on the right side of the input.)
+ x -= 1
+ else:
+ # Found position, call handler of UIControl.
+ result = self.content.mouse_handler(
+ cli, MouseEvent(position=Point(x=col, y=row),
+ event_type=mouse_event.event_type))
+ break
+ else:
+ # nobreak.
+ # (No x/y coordinate found for the content. This happens in
+ # case of a FillControl, that only specifies a background, but
+ # doesn't have a content. Report (0,0) instead.)
+ result = self.content.mouse_handler(
+ cli, MouseEvent(position=Point(x=0, y=0),
+ event_type=mouse_event.event_type))
+
+ # If it returns NotImplemented, handle it here.
+ if result == NotImplemented:
+ return self._mouse_handler(cli, mouse_event)
+
+ return result
+
+ mouse_handlers.set_mouse_handler_for_range(
+ x_min=write_position.xpos + sum(left_margin_widths),
+ x_max=write_position.xpos + write_position.width - total_margin_width,
+ y_min=write_position.ypos,
+ y_max=write_position.ypos + write_position.height,
+ handler=mouse_handler)
+
+ # Render and copy margins.
+ move_x = 0
+
+ def render_margin(m, width):
+ " Render margin. Return `Screen`. "
+ # Retrieve margin tokens.
+ tokens = m.create_margin(cli, self.render_info, width, write_position.height)
+
+ # Turn it into a UIContent object.
+ # already rendered those tokens using this size.)
+ return TokenListControl.static(tokens).create_content(
+ cli, width + 1, write_position.height)
+
+ for m, width in zip(self.left_margins, left_margin_widths):
+ # Create screen for margin.
+ margin_screen = render_margin(m, width)
+
+ # Copy and shift X.
+ self._copy_margin(cli, margin_screen, screen, write_position, move_x, width)
+ move_x += width
+
+ move_x = write_position.width - sum(right_margin_widths)
+
+ for m, width in zip(self.right_margins, right_margin_widths):
+ # Create screen for margin.
+ margin_screen = render_margin(m, width)
+
+ # Copy and shift X.
+ self._copy_margin(cli, margin_screen, screen, write_position, move_x, width)
+ move_x += width
+
+ def _copy_body(self, cli, ui_content, new_screen, write_position, move_x,
+ width, vertical_scroll=0, horizontal_scroll=0,
+ has_focus=False, wrap_lines=False, highlight_lines=False,
+ vertical_scroll_2=0, always_hide_cursor=False):
+ """
+ Copy the UIContent into the output screen.
+ """
+ xpos = write_position.xpos + move_x
+ ypos = write_position.ypos
+ line_count = ui_content.line_count
+ new_buffer = new_screen.data_buffer
+ empty_char = _CHAR_CACHE['', Token]
+ ZeroWidthEscape = Token.ZeroWidthEscape
+
+ # Map visible line number to (row, col) of input.
+ # 'col' will always be zero if line wrapping is off.
+ visible_line_to_row_col = {}
+ rowcol_to_yx = {} # Maps (row, col) from the input to (y, x) screen coordinates.
+
+ # Fill background with default_char first.
+ default_char = ui_content.default_char
+
+ if default_char:
+ for y in range(ypos, ypos + write_position.height):
+ new_buffer_row = new_buffer[y]
+ for x in range(xpos, xpos + width):
+ new_buffer_row[x] = default_char
+
+ # Copy content.
+ def copy():
+ y = - vertical_scroll_2
+ lineno = vertical_scroll
+
+ while y < write_position.height and lineno < line_count:
+ # Take the next line and copy it in the real screen.
+ line = ui_content.get_line(lineno)
+
+ col = 0
+ x = -horizontal_scroll
+
+ visible_line_to_row_col[y] = (lineno, horizontal_scroll)
+ new_buffer_row = new_buffer[y + ypos]
+
+ for token, text in line:
+ # Remember raw VT escape sequences. (E.g. FinalTerm's
+ # escape sequences.)
+ if token == ZeroWidthEscape:
+ new_screen.zero_width_escapes[y + ypos][x + xpos] += text
+ continue
+
+ for c in text:
+ char = _CHAR_CACHE[c, token]
+ char_width = char.width
+
+ # Wrap when the line width is exceeded.
+ if wrap_lines and x + char_width > width:
+ visible_line_to_row_col[y + 1] = (
+ lineno, visible_line_to_row_col[y][1] + x)
+ y += 1
+ x = -horizontal_scroll # This would be equal to zero.
+ # (horizontal_scroll=0 when wrap_lines.)
+ new_buffer_row = new_buffer[y + ypos]
+
+ if y >= write_position.height:
+ return y # Break out of all for loops.
+
+ # Set character in screen and shift 'x'.
+ if x >= 0 and y >= 0 and x < write_position.width:
+ new_buffer_row[x + xpos] = char
+
+ # When we print a multi width character, make sure
+ # to erase the neighbous positions in the screen.
+ # (The empty string if different from everything,
+ # so next redraw this cell will repaint anyway.)
+ if char_width > 1:
+ for i in range(1, char_width):
+ new_buffer_row[x + xpos + i] = empty_char
+
+ # If this is a zero width characters, then it's
+ # probably part of a decomposed unicode character.
+ # See: https://en.wikipedia.org/wiki/Unicode_equivalence
+ # Merge it in the previous cell.
+ elif char_width == 0 and x - 1 >= 0:
+ prev_char = new_buffer_row[x + xpos - 1]
+ char2 = _CHAR_CACHE[prev_char.char + c, prev_char.token]
+ new_buffer_row[x + xpos - 1] = char2
+
+ # Keep track of write position for each character.
+ rowcol_to_yx[lineno, col] = (y + ypos, x + xpos)
+
+ col += 1
+ x += char_width
+
+ lineno += 1
+ y += 1
+ return y
+
+ y = copy()
+
+ def cursor_pos_to_screen_pos(row, col):
+ " Translate row/col from UIContent to real Screen coordinates. "
+ try:
+ y, x = rowcol_to_yx[row, col]
+ except KeyError:
+ # Normally this should never happen. (It is a bug, if it happens.)
+ # But to be sure, return (0, 0)
+ return Point(y=0, x=0)
+
+ # raise ValueError(
+ # 'Invalid position. row=%r col=%r, vertical_scroll=%r, '
+ # 'horizontal_scroll=%r, height=%r' %
+ # (row, col, vertical_scroll, horizontal_scroll, write_position.height))
+ else:
+ return Point(y=y, x=x)
+
+ # Set cursor and menu positions.
+ if ui_content.cursor_position:
+ screen_cursor_position = cursor_pos_to_screen_pos(
+ ui_content.cursor_position.y, ui_content.cursor_position.x)
+
+ if has_focus:
+ new_screen.cursor_position = screen_cursor_position
+
+ if always_hide_cursor:
+ new_screen.show_cursor = False
+ else:
+ new_screen.show_cursor = ui_content.show_cursor
+
+ self._highlight_digraph(cli, new_screen)
+
+ if highlight_lines:
+ self._highlight_cursorlines(
+ cli, new_screen, screen_cursor_position, xpos, ypos, width,
+ write_position.height)
+
+ # Draw input characters from the input processor queue.
+ if has_focus and ui_content.cursor_position:
+ self._show_input_processor_key_buffer(cli, new_screen)
+
+ # Set menu position.
+ if not new_screen.menu_position and ui_content.menu_position:
+ new_screen.menu_position = cursor_pos_to_screen_pos(
+ ui_content.menu_position.y, ui_content.menu_position.x)
+
+ # Update output screne height.
+ new_screen.height = max(new_screen.height, ypos + write_position.height)
+
+ return visible_line_to_row_col, rowcol_to_yx
+
+ def _highlight_digraph(self, cli, new_screen):
+ """
+ When we are in Vi digraph mode, put a question mark underneath the
+ cursor.
+ """
+ digraph_char = self._get_digraph_char(cli)
+ if digraph_char:
+ cpos = new_screen.cursor_position
+ new_screen.data_buffer[cpos.y][cpos.x] = \
+ _CHAR_CACHE[digraph_char, Token.Digraph]
+
+ def _show_input_processor_key_buffer(self, cli, new_screen):
+ """
+ When the user is typing a key binding that consists of several keys,
+ display the last pressed key if the user is in insert mode and the key
+ is meaningful to be displayed.
+ E.g. Some people want to bind 'jj' to escape in Vi insert mode. But the
+ first 'j' needs to be displayed in order to get some feedback.
+ """
+ key_buffer = cli.input_processor.key_buffer
+
+ if key_buffer and _in_insert_mode(cli) and not cli.is_done:
+ # The textual data for the given key. (Can be a VT100 escape
+ # sequence.)
+ data = key_buffer[-1].data
+
+ # Display only if this is a 1 cell width character.
+ if get_cwidth(data) == 1:
+ cpos = new_screen.cursor_position
+ new_screen.data_buffer[cpos.y][cpos.x] = \
+ _CHAR_CACHE[data, Token.PartialKeyBinding]
+
+ def _highlight_cursorlines(self, cli, new_screen, cpos, x, y, width, height):
+ """
+ Highlight cursor row/column.
+ """
+ cursor_line_token = (':', ) + self.cursorline_token
+ cursor_column_token = (':', ) + self.cursorcolumn_token
+
+ data_buffer = new_screen.data_buffer
+
+ # Highlight cursor line.
+ if self.cursorline(cli):
+ row = data_buffer[cpos.y]
+ for x in range(x, x + width):
+ original_char = row[x]
+ row[x] = _CHAR_CACHE[
+ original_char.char, original_char.token + cursor_line_token]
+
+ # Highlight cursor column.
+ if self.cursorcolumn(cli):
+ for y2 in range(y, y + height):
+ row = data_buffer[y2]
+ original_char = row[cpos.x]
+ row[cpos.x] = _CHAR_CACHE[
+ original_char.char, original_char.token + cursor_column_token]
+
+ # Highlight color columns
+ for cc in self.get_colorcolumns(cli):
+ assert isinstance(cc, ColorColumn)
+ color_column_token = (':', ) + cc.token
+ column = cc.position
+
+ for y2 in range(y, y + height):
+ row = data_buffer[y2]
+ original_char = row[column]
+ row[column] = _CHAR_CACHE[
+ original_char.char, original_char.token + color_column_token]
+
+ def _copy_margin(self, cli, lazy_screen, new_screen, write_position, move_x, width):
+ """
+ Copy characters from the margin screen to the real screen.
+ """
+ xpos = write_position.xpos + move_x
+ ypos = write_position.ypos
+
+ margin_write_position = WritePosition(xpos, ypos, width, write_position.height)
+ self._copy_body(cli, lazy_screen, new_screen, margin_write_position, 0, width)
+
+ def _scroll_when_linewrapping(self, ui_content, width, height, cli):
+ """
+ Scroll to make sure the cursor position is visible and that we maintain
+ the requested scroll offset.
+
+ Set `self.horizontal_scroll/vertical_scroll`.
+ """
+ scroll_offsets_bottom = self.scroll_offsets.bottom
+ scroll_offsets_top = self.scroll_offsets.top
+
+ # We don't have horizontal scrolling.
+ self.horizontal_scroll = 0
+
+ # If the current line consumes more than the whole window height,
+ # then we have to scroll vertically inside this line. (We don't take
+ # the scroll offsets into account for this.)
+ # Also, ignore the scroll offsets in this case. Just set the vertical
+ # scroll to this line.
+ if ui_content.get_height_for_line(ui_content.cursor_position.y, width) > height - scroll_offsets_top:
+ # Calculate the height of the text before the cursor, with the line
+ # containing the cursor included, and the character belowe the
+ # cursor included as well.
+ line = explode_tokens(ui_content.get_line(ui_content.cursor_position.y))
+ text_before_cursor = token_list_to_text(line[:ui_content.cursor_position.x + 1])
+ text_before_height = UIContent.get_height_for_text(text_before_cursor, width)
+
+ # Adjust scroll offset.
+ self.vertical_scroll = ui_content.cursor_position.y
+ self.vertical_scroll_2 = min(text_before_height - 1, self.vertical_scroll_2)
+ self.vertical_scroll_2 = max(0, text_before_height - height, self.vertical_scroll_2)
+ return
+ else:
+ self.vertical_scroll_2 = 0
+
+ # Current line doesn't consume the whole height. Take scroll offsets into account.
+ def get_min_vertical_scroll():
+ # Make sure that the cursor line is not below the bottom.
+ # (Calculate how many lines can be shown between the cursor and the .)
+ used_height = 0
+ prev_lineno = ui_content.cursor_position.y
+
+ for lineno in range(ui_content.cursor_position.y, -1, -1):
+ used_height += ui_content.get_height_for_line(lineno, width)
+
+ if used_height > height - scroll_offsets_bottom:
+ return prev_lineno
+ else:
+ prev_lineno = lineno
+ return 0
+
+ def get_max_vertical_scroll():
+ # Make sure that the cursor line is not above the top.
+ prev_lineno = ui_content.cursor_position.y
+ used_height = 0
+
+ for lineno in range(ui_content.cursor_position.y - 1, -1, -1):
+ used_height += ui_content.get_height_for_line(lineno, width)
+
+ if used_height > scroll_offsets_top:
+ return prev_lineno
+ else:
+ prev_lineno = lineno
+ return prev_lineno
+
+ def get_topmost_visible():
+ """
+ Calculate the upper most line that can be visible, while the bottom
+ is still visible. We should not allow scroll more than this if
+ `allow_scroll_beyond_bottom` is false.
+ """
+ prev_lineno = ui_content.line_count - 1
+ used_height = 0
+ for lineno in range(ui_content.line_count - 1, -1, -1):
+ used_height += ui_content.get_height_for_line(lineno, width)
+ if used_height > height:
+ return prev_lineno
+ else:
+ prev_lineno = lineno
+ return prev_lineno
+
+ # Scroll vertically. (Make sure that the whole line which contains the
+ # cursor is visible.
+ topmost_visible = get_topmost_visible()
+
+ # Note: the `min(topmost_visible, ...)` is to make sure that we
+ # don't require scrolling up because of the bottom scroll offset,
+ # when we are at the end of the document.
+ self.vertical_scroll = max(self.vertical_scroll, min(topmost_visible, get_min_vertical_scroll()))
+ self.vertical_scroll = min(self.vertical_scroll, get_max_vertical_scroll())
+
+ # Disallow scrolling beyond bottom?
+ if not self.allow_scroll_beyond_bottom(cli):
+ self.vertical_scroll = min(self.vertical_scroll, topmost_visible)
+
+ def _scroll_without_linewrapping(self, ui_content, width, height, cli):
+ """
+ Scroll to make sure the cursor position is visible and that we maintain
+ the requested scroll offset.
+
+ Set `self.horizontal_scroll/vertical_scroll`.
+ """
+ cursor_position = ui_content.cursor_position or Point(0, 0)
+
+ # Without line wrapping, we will never have to scroll vertically inside
+ # a single line.
+ self.vertical_scroll_2 = 0
+
+ if ui_content.line_count == 0:
+ self.vertical_scroll = 0
+ self.horizontal_scroll = 0
+ return
+ else:
+ current_line_text = token_list_to_text(ui_content.get_line(cursor_position.y))
+
+ def do_scroll(current_scroll, scroll_offset_start, scroll_offset_end,
+ cursor_pos, window_size, content_size):
+ " Scrolling algorithm. Used for both horizontal and vertical scrolling. "
+ # Calculate the scroll offset to apply.
+ # This can obviously never be more than have the screen size. Also, when the
+ # cursor appears at the top or bottom, we don't apply the offset.
+ scroll_offset_start = int(min(scroll_offset_start, window_size / 2, cursor_pos))
+ scroll_offset_end = int(min(scroll_offset_end, window_size / 2,
+ content_size - 1 - cursor_pos))
+
+ # Prevent negative scroll offsets.
+ if current_scroll < 0:
+ current_scroll = 0
+
+ # Scroll back if we scrolled to much and there's still space to show more of the document.
+ if (not self.allow_scroll_beyond_bottom(cli) and
+ current_scroll > content_size - window_size):
+ current_scroll = max(0, content_size - window_size)
+
+ # Scroll up if cursor is before visible part.
+ if current_scroll > cursor_pos - scroll_offset_start:
+ current_scroll = max(0, cursor_pos - scroll_offset_start)
+
+ # Scroll down if cursor is after visible part.
+ if current_scroll < (cursor_pos + 1) - window_size + scroll_offset_end:
+ current_scroll = (cursor_pos + 1) - window_size + scroll_offset_end
+
+ return current_scroll
+
+ # When a preferred scroll is given, take that first into account.
+ if self.get_vertical_scroll:
+ self.vertical_scroll = self.get_vertical_scroll(self)
+ assert isinstance(self.vertical_scroll, int)
+ if self.get_horizontal_scroll:
+ self.horizontal_scroll = self.get_horizontal_scroll(self)
+ assert isinstance(self.horizontal_scroll, int)
+
+ # Update horizontal/vertical scroll to make sure that the cursor
+ # remains visible.
+ offsets = self.scroll_offsets
+
+ self.vertical_scroll = do_scroll(
+ current_scroll=self.vertical_scroll,
+ scroll_offset_start=offsets.top,
+ scroll_offset_end=offsets.bottom,
+ cursor_pos=ui_content.cursor_position.y,
+ window_size=height,
+ content_size=ui_content.line_count)
+
+ self.horizontal_scroll = do_scroll(
+ current_scroll=self.horizontal_scroll,
+ scroll_offset_start=offsets.left,
+ scroll_offset_end=offsets.right,
+ cursor_pos=get_cwidth(current_line_text[:ui_content.cursor_position.x]),
+ window_size=width,
+ # We can only analyse the current line. Calculating the width off
+ # all the lines is too expensive.
+ content_size=max(get_cwidth(current_line_text), self.horizontal_scroll + width))
+
+ def _mouse_handler(self, cli, mouse_event):
+ """
+ Mouse handler. Called when the UI control doesn't handle this
+ particular event.
+ """
+ if mouse_event.event_type == MouseEventType.SCROLL_DOWN:
+ self._scroll_down(cli)
+ elif mouse_event.event_type == MouseEventType.SCROLL_UP:
+ self._scroll_up(cli)
+
+ def _scroll_down(self, cli):
+ " Scroll window down. "
+ info = self.render_info
+
+ if self.vertical_scroll < info.content_height - info.window_height:
+ if info.cursor_position.y <= info.configured_scroll_offsets.top:
+ self.content.move_cursor_down(cli)
+
+ self.vertical_scroll += 1
+
+ def _scroll_up(self, cli):
+ " Scroll window up. "
+ info = self.render_info
+
+ if info.vertical_scroll > 0:
+ # TODO: not entirely correct yet in case of line wrapping and long lines.
+ if info.cursor_position.y >= info.window_height - 1 - info.configured_scroll_offsets.bottom:
+ self.content.move_cursor_up(cli)
+
+ self.vertical_scroll -= 1
+
+ def walk(self, cli):
+ # Only yield self. A window doesn't have children.
+ yield self
+
+
+class ConditionalContainer(Container):
+ """
+ Wrapper around any other container that can change the visibility. The
+ received `filter` determines whether the given container should be
+ displayed or not.
+
+ :param content: :class:`.Container` instance.
+ :param filter: :class:`~prompt_toolkit.filters.CLIFilter` instance.
+ """
+ def __init__(self, content, filter):
+ assert isinstance(content, Container)
+
+ self.content = content
+ self.filter = to_cli_filter(filter)
+
+ def __repr__(self):
+ return 'ConditionalContainer(%r, filter=%r)' % (self.content, self.filter)
+
+ def reset(self):
+ self.content.reset()
+
+ def preferred_width(self, cli, max_available_width):
+ if self.filter(cli):
+ return self.content.preferred_width(cli, max_available_width)
+ else:
+ return LayoutDimension.exact(0)
+
+ def preferred_height(self, cli, width, max_available_height):
+ if self.filter(cli):
+ return self.content.preferred_height(cli, width, max_available_height)
+ else:
+ return LayoutDimension.exact(0)
+
+ def write_to_screen(self, cli, screen, mouse_handlers, write_position):
+ if self.filter(cli):
+ return self.content.write_to_screen(cli, screen, mouse_handlers, write_position)
+
+ def walk(self, cli):
+ return self.content.walk(cli)
+
+
+# Deprecated alias for 'Container'.
+Layout = Container
diff --git a/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/controls.py b/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/controls.py
new file mode 100644
index 00000000000..ca74931dbc2
--- /dev/null
+++ b/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/controls.py
@@ -0,0 +1,730 @@
+"""
+User interface Controls for the layout.
+"""
+from __future__ import unicode_literals
+
+from abc import ABCMeta, abstractmethod
+from collections import namedtuple
+from six import with_metaclass
+from six.moves import range
+
+from prompt_toolkit.cache import SimpleCache
+from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER
+from prompt_toolkit.filters import to_cli_filter
+from prompt_toolkit.mouse_events import MouseEventType
+from prompt_toolkit.search_state import SearchState
+from prompt_toolkit.selection import SelectionType
+from prompt_toolkit.token import Token
+from prompt_toolkit.utils import get_cwidth
+
+from .lexers import Lexer, SimpleLexer
+from .processors import Processor
+from .screen import Char, Point
+from .utils import token_list_width, split_lines, token_list_to_text
+
+import six
+import time
+
+
+__all__ = (
+ 'BufferControl',
+ 'FillControl',
+ 'TokenListControl',
+ 'UIControl',
+ 'UIContent',
+)
+
+
+class UIControl(with_metaclass(ABCMeta, object)):
+ """
+ Base class for all user interface controls.
+ """
+ def reset(self):
+ # Default reset. (Doesn't have to be implemented.)
+ pass
+
+ def preferred_width(self, cli, max_available_width):
+ return None
+
+ def preferred_height(self, cli, width, max_available_height, wrap_lines):
+ return None
+
+ def has_focus(self, cli):
+ """
+ Return ``True`` when this user control has the focus.
+
+ If so, the cursor will be displayed according to the cursor position
+ reported by :meth:`.UIControl.create_content`. If the created content
+ has the property ``show_cursor=False``, the cursor will be hidden from
+ the output.
+ """
+ return False
+
+ @abstractmethod
+ def create_content(self, cli, width, height):
+ """
+ Generate the content for this user control.
+
+ Returns a :class:`.UIContent` instance.
+ """
+
+ def mouse_handler(self, cli, mouse_event):
+ """
+ Handle mouse events.
+
+ When `NotImplemented` is returned, it means that the given event is not
+ handled by the `UIControl` itself. The `Window` or key bindings can
+ decide to handle this event as scrolling or changing focus.
+
+ :param cli: `CommandLineInterface` instance.
+ :param mouse_event: `MouseEvent` instance.
+ """
+ return NotImplemented
+
+ def move_cursor_down(self, cli):
+ """
+ Request to move the cursor down.
+ This happens when scrolling down and the cursor is completely at the
+ top.
+ """
+
+ def move_cursor_up(self, cli):
+ """
+ Request to move the cursor up.
+ """
+
+
+class UIContent(object):
+ """
+ Content generated by a user control. This content consists of a list of
+ lines.
+
+ :param get_line: Callable that returns the current line. This is a list of
+ (Token, text) tuples.
+ :param line_count: The number of lines.
+ :param cursor_position: a :class:`.Point` for the cursor position.
+ :param menu_position: a :class:`.Point` for the menu position.
+ :param show_cursor: Make the cursor visible.
+ :param default_char: The default :class:`.Char` for filling the background.
+ """
+ def __init__(self, get_line=None, line_count=0,
+ cursor_position=None, menu_position=None, show_cursor=True,
+ default_char=None):
+ assert callable(get_line)
+ assert isinstance(line_count, six.integer_types)
+ assert cursor_position is None or isinstance(cursor_position, Point)
+ assert menu_position is None or isinstance(menu_position, Point)
+ assert default_char is None or isinstance(default_char, Char)
+
+ self.get_line = get_line
+ self.line_count = line_count
+ self.cursor_position = cursor_position or Point(0, 0)
+ self.menu_position = menu_position
+ self.show_cursor = show_cursor
+ self.default_char = default_char
+
+ # Cache for line heights. Maps (lineno, width) -> height.
+ self._line_heights = {}
+
+ def __getitem__(self, lineno):
+ " Make it iterable (iterate line by line). "
+ if lineno < self.line_count:
+ return self.get_line(lineno)
+ else:
+ raise IndexError
+
+ def get_height_for_line(self, lineno, width):
+ """
+ Return the height that a given line would need if it is rendered in a
+ space with the given width.
+ """
+ try:
+ return self._line_heights[lineno, width]
+ except KeyError:
+ text = token_list_to_text(self.get_line(lineno))
+ result = self.get_height_for_text(text, width)
+
+ # Cache and return
+ self._line_heights[lineno, width] = result
+ return result
+
+ @staticmethod
+ def get_height_for_text(text, width):
+ # Get text width for this line.
+ line_width = get_cwidth(text)
+
+ # Calculate height.
+ try:
+ quotient, remainder = divmod(line_width, width)
+ except ZeroDivisionError:
+ # Return something very big.
+ # (This can happen, when the Window gets very small.)
+ return 10 ** 10
+ else:
+ if remainder:
+ quotient += 1 # Like math.ceil.
+ return max(1, quotient)
+
+
+class TokenListControl(UIControl):
+ """
+ Control that displays a list of (Token, text) tuples.
+ (It's mostly optimized for rather small widgets, like toolbars, menus, etc...)
+
+ Mouse support:
+
+ The list of tokens can also contain tuples of three items, looking like:
+ (Token, text, handler). When mouse support is enabled and the user
+ clicks on this token, then the given handler is called. That handler
+ should accept two inputs: (CommandLineInterface, MouseEvent) and it
+ should either handle the event or return `NotImplemented` in case we
+ want the containing Window to handle this event.
+
+ :param get_tokens: Callable that takes a `CommandLineInterface` instance
+ and returns the list of (Token, text) tuples to be displayed right now.
+ :param default_char: default :class:`.Char` (character and Token) to use
+ for the background when there is more space available than `get_tokens`
+ returns.
+ :param get_default_char: Like `default_char`, but this is a callable that
+ takes a :class:`prompt_toolkit.interface.CommandLineInterface` and
+ returns a :class:`.Char` instance.
+ :param has_focus: `bool` or `CLIFilter`, when this evaluates to `True`,
+ this UI control will take the focus. The cursor will be shown in the
+ upper left corner of this control, unless `get_token` returns a
+ ``Token.SetCursorPosition`` token somewhere in the token list, then the
+ cursor will be shown there.
+ """
+ def __init__(self, get_tokens, default_char=None, get_default_char=None,
+ align_right=False, align_center=False, has_focus=False):
+ assert callable(get_tokens)
+ assert default_char is None or isinstance(default_char, Char)
+ assert get_default_char is None or callable(get_default_char)
+ assert not (default_char and get_default_char)
+
+ self.align_right = to_cli_filter(align_right)
+ self.align_center = to_cli_filter(align_center)
+ self._has_focus_filter = to_cli_filter(has_focus)
+
+ self.get_tokens = get_tokens
+
+ # Construct `get_default_char` callable.
+ if default_char:
+ get_default_char = lambda _: default_char
+ elif not get_default_char:
+ get_default_char = lambda _: Char(' ', Token.Transparent)
+
+ self.get_default_char = get_default_char
+
+ #: Cache for the content.
+ self._content_cache = SimpleCache(maxsize=18)
+ self._token_cache = SimpleCache(maxsize=1)
+ # Only cache one token list. We don't need the previous item.
+
+ # Render info for the mouse support.
+ self._tokens = None
+
+ def reset(self):
+ self._tokens = None
+
+ def __repr__(self):
+ return '%s(%r)' % (self.__class__.__name__, self.get_tokens)
+
+ def _get_tokens_cached(self, cli):
+ """
+ Get tokens, but only retrieve tokens once during one render run.
+ (This function is called several times during one rendering, because
+ we also need those for calculating the dimensions.)
+ """
+ return self._token_cache.get(
+ cli.render_counter, lambda: self.get_tokens(cli))
+
+ def has_focus(self, cli):
+ return self._has_focus_filter(cli)
+
+ def preferred_width(self, cli, max_available_width):
+ """
+ Return the preferred width for this control.
+ That is the width of the longest line.
+ """
+ text = token_list_to_text(self._get_tokens_cached(cli))
+ line_lengths = [get_cwidth(l) for l in text.split('\n')]
+ return max(line_lengths)
+
+ def preferred_height(self, cli, width, max_available_height, wrap_lines):
+ content = self.create_content(cli, width, None)
+ return content.line_count
+
+ def create_content(self, cli, width, height):
+ # Get tokens
+ tokens_with_mouse_handlers = self._get_tokens_cached(cli)
+
+ default_char = self.get_default_char(cli)
+
+ # Wrap/align right/center parameters.
+ right = self.align_right(cli)
+ center = self.align_center(cli)
+
+ def process_line(line):
+ " Center or right align a single line. "
+ used_width = token_list_width(line)
+ padding = width - used_width
+ if center:
+ padding = int(padding / 2)
+ return [(default_char.token, default_char.char * padding)] + line
+
+ if right or center:
+ token_lines_with_mouse_handlers = []
+
+ for line in split_lines(tokens_with_mouse_handlers):
+ token_lines_with_mouse_handlers.append(process_line(line))
+ else:
+ token_lines_with_mouse_handlers = list(split_lines(tokens_with_mouse_handlers))
+
+ # Strip mouse handlers from tokens.
+ token_lines = [
+ [tuple(item[:2]) for item in line]
+ for line in token_lines_with_mouse_handlers
+ ]
+
+ # Keep track of the tokens with mouse handler, for later use in
+ # `mouse_handler`.
+ self._tokens = tokens_with_mouse_handlers
+
+ # If there is a `Token.SetCursorPosition` in the token list, set the
+ # cursor position here.
+ def get_cursor_position():
+ SetCursorPosition = Token.SetCursorPosition
+
+ for y, line in enumerate(token_lines):
+ x = 0
+ for token, text in line:
+ if token == SetCursorPosition:
+ return Point(x=x, y=y)
+ x += len(text)
+ return None
+
+ # Create content, or take it from the cache.
+ key = (default_char.char, default_char.token,
+ tuple(tokens_with_mouse_handlers), width, right, center)
+
+ def get_content():
+ return UIContent(get_line=lambda i: token_lines[i],
+ line_count=len(token_lines),
+ default_char=default_char,
+ cursor_position=get_cursor_position())
+
+ return self._content_cache.get(key, get_content)
+
+ @classmethod
+ def static(cls, tokens):
+ def get_static_tokens(cli):
+ return tokens
+ return cls(get_static_tokens)
+
+ def mouse_handler(self, cli, mouse_event):
+ """
+ Handle mouse events.
+
+ (When the token list contained mouse handlers and the user clicked on
+ on any of these, the matching handler is called. This handler can still
+ return `NotImplemented` in case we want the `Window` to handle this
+ particular event.)
+ """
+ if self._tokens:
+ # Read the generator.
+ tokens_for_line = list(split_lines(self._tokens))
+
+ try:
+ tokens = tokens_for_line[mouse_event.position.y]
+ except IndexError:
+ return NotImplemented
+ else:
+ # Find position in the token list.
+ xpos = mouse_event.position.x
+
+ # Find mouse handler for this character.
+ count = 0
+ for item in tokens:
+ count += len(item[1])
+ if count >= xpos:
+ if len(item) >= 3:
+ # Handler found. Call it.
+ # (Handler can return NotImplemented, so return
+ # that result.)
+ handler = item[2]
+ return handler(cli, mouse_event)
+ else:
+ break
+
+ # Otherwise, don't handle here.
+ return NotImplemented
+
+
+class FillControl(UIControl):
+ """
+ Fill whole control with characters with this token.
+ (Also helpful for debugging.)
+
+ :param char: :class:`.Char` instance to use for filling.
+ :param get_char: A callable that takes a CommandLineInterface and returns a
+ :class:`.Char` object.
+ """
+ def __init__(self, character=None, token=Token, char=None, get_char=None): # 'character' and 'token' parameters are deprecated.
+ assert char is None or isinstance(char, Char)
+ assert get_char is None or callable(get_char)
+ assert not (char and get_char)
+
+ self.char = char
+
+ if character:
+ # Passing (character=' ', token=token) is deprecated.
+ self.character = character
+ self.token = token
+
+ self.get_char = lambda cli: Char(character, token)
+ elif get_char:
+ # When 'get_char' is given.
+ self.get_char = get_char
+ else:
+ # When 'char' is given.
+ self.char = self.char or Char()
+ self.get_char = lambda cli: self.char
+ self.char = char
+
+ def __repr__(self):
+ if self.char:
+ return '%s(char=%r)' % (self.__class__.__name__, self.char)
+ else:
+ return '%s(get_char=%r)' % (self.__class__.__name__, self.get_char)
+
+ def reset(self):
+ pass
+
+ def has_focus(self, cli):
+ return False
+
+ def create_content(self, cli, width, height):
+ def get_line(i):
+ return []
+
+ return UIContent(
+ get_line=get_line,
+ line_count=100 ** 100, # Something very big.
+ default_char=self.get_char(cli))
+
+
+_ProcessedLine = namedtuple('_ProcessedLine', 'tokens source_to_display display_to_source')
+
+
+class BufferControl(UIControl):
+ """
+ Control for visualising the content of a `Buffer`.
+
+ :param input_processors: list of :class:`~prompt_toolkit.layout.processors.Processor`.
+ :param lexer: :class:`~prompt_toolkit.layout.lexers.Lexer` instance for syntax highlighting.
+ :param preview_search: `bool` or `CLIFilter`: Show search while typing.
+ :param get_search_state: Callable that takes a CommandLineInterface and
+ returns the SearchState to be used. (If not CommandLineInterface.search_state.)
+ :param buffer_name: String representing the name of the buffer to display.
+ :param default_char: :class:`.Char` instance to use to fill the background. This is
+ transparent by default.
+ :param focus_on_click: Focus this buffer when it's click, but not yet focussed.
+ """
+ def __init__(self,
+ buffer_name=DEFAULT_BUFFER,
+ input_processors=None,
+ lexer=None,
+ preview_search=False,
+ search_buffer_name=SEARCH_BUFFER,
+ get_search_state=None,
+ menu_position=None,
+ default_char=None,
+ focus_on_click=False):
+ assert input_processors is None or all(isinstance(i, Processor) for i in input_processors)
+ assert menu_position is None or callable(menu_position)
+ assert lexer is None or isinstance(lexer, Lexer)
+ assert get_search_state is None or callable(get_search_state)
+ assert default_char is None or isinstance(default_char, Char)
+
+ self.preview_search = to_cli_filter(preview_search)
+ self.get_search_state = get_search_state
+ self.focus_on_click = to_cli_filter(focus_on_click)
+
+ self.input_processors = input_processors or []
+ self.buffer_name = buffer_name
+ self.menu_position = menu_position
+ self.lexer = lexer or SimpleLexer()
+ self.default_char = default_char or Char(token=Token.Transparent)
+ self.search_buffer_name = search_buffer_name
+
+ #: Cache for the lexer.
+ #: Often, due to cursor movement, undo/redo and window resizing
+ #: operations, it happens that a short time, the same document has to be
+ #: lexed. This is a faily easy way to cache such an expensive operation.
+ self._token_cache = SimpleCache(maxsize=8)
+
+ self._xy_to_cursor_position = None
+ self._last_click_timestamp = None
+ self._last_get_processed_line = None
+
+ def _buffer(self, cli):
+ """
+ The buffer object that contains the 'main' content.
+ """
+ return cli.buffers[self.buffer_name]
+
+ def has_focus(self, cli):
+ # This control gets the focussed if the actual `Buffer` instance has the
+ # focus or when any of the `InputProcessor` classes tells us that it
+ # wants the focus. (E.g. in case of a reverse-search, where the actual
+ # search buffer may not be displayed, but the "reverse-i-search" text
+ # should get the focus.)
+ return cli.current_buffer_name == self.buffer_name or \
+ any(i.has_focus(cli) for i in self.input_processors)
+
+ def preferred_width(self, cli, max_available_width):
+ """
+ This should return the preferred width.
+
+ Note: We don't specify a preferred width according to the content,
+ because it would be too expensive. Calculating the preferred
+ width can be done by calculating the longest line, but this would
+ require applying all the processors to each line. This is
+ unfeasible for a larger document, and doing it for small
+ documents only would result in inconsistent behaviour.
+ """
+ return None
+
+ def preferred_height(self, cli, width, max_available_height, wrap_lines):
+ # Calculate the content height, if it was drawn on a screen with the
+ # given width.
+ height = 0
+ content = self.create_content(cli, width, None)
+
+ # When line wrapping is off, the height should be equal to the amount
+ # of lines.
+ if not wrap_lines:
+ return content.line_count
+
+ # When the number of lines exceeds the max_available_height, just
+ # return max_available_height. No need to calculate anything.
+ if content.line_count >= max_available_height:
+ return max_available_height
+
+ for i in range(content.line_count):
+ height += content.get_height_for_line(i, width)
+
+ if height >= max_available_height:
+ return max_available_height
+
+ return height
+
+ def _get_tokens_for_line_func(self, cli, document):
+ """
+ Create a function that returns the tokens for a given line.
+ """
+ # Cache using `document.text`.
+ def get_tokens_for_line():
+ return self.lexer.lex_document(cli, document)
+
+ return self._token_cache.get(document.text, get_tokens_for_line)
+
+ def _create_get_processed_line_func(self, cli, document):
+ """
+ Create a function that takes a line number of the current document and
+ returns a _ProcessedLine(processed_tokens, source_to_display, display_to_source)
+ tuple.
+ """
+ def transform(lineno, tokens):
+ " Transform the tokens for a given line number. "
+ source_to_display_functions = []
+ display_to_source_functions = []
+
+ # Get cursor position at this line.
+ if document.cursor_position_row == lineno:
+ cursor_column = document.cursor_position_col
+ else:
+ cursor_column = None
+
+ def source_to_display(i):
+ """ Translate x position from the buffer to the x position in the
+ processed token list. """
+ for f in source_to_display_functions:
+ i = f(i)
+ return i
+
+ # Apply each processor.
+ for p in self.input_processors:
+ transformation = p.apply_transformation(
+ cli, document, lineno, source_to_display, tokens)
+ tokens = transformation.tokens
+
+ if cursor_column:
+ cursor_column = transformation.source_to_display(cursor_column)
+
+ display_to_source_functions.append(transformation.display_to_source)
+ source_to_display_functions.append(transformation.source_to_display)
+
+ def display_to_source(i):
+ for f in reversed(display_to_source_functions):
+ i = f(i)
+ return i
+
+ return _ProcessedLine(tokens, source_to_display, display_to_source)
+
+ def create_func():
+ get_line = self._get_tokens_for_line_func(cli, document)
+ cache = {}
+
+ def get_processed_line(i):
+ try:
+ return cache[i]
+ except KeyError:
+ processed_line = transform(i, get_line(i))
+ cache[i] = processed_line
+ return processed_line
+ return get_processed_line
+
+ return create_func()
+
+ def create_content(self, cli, width, height):
+ """
+ Create a UIContent.
+ """
+ buffer = self._buffer(cli)
+
+ # Get the document to be shown. If we are currently searching (the
+ # search buffer has focus, and the preview_search filter is enabled),
+ # then use the search document, which has possibly a different
+ # text/cursor position.)
+ def preview_now():
+ """ True when we should preview a search. """
+ return bool(self.preview_search(cli) and
+ cli.buffers[self.search_buffer_name].text)
+
+ if preview_now():
+ if self.get_search_state:
+ ss = self.get_search_state(cli)
+ else:
+ ss = cli.search_state
+
+ document = buffer.document_for_search(SearchState(
+ text=cli.current_buffer.text,
+ direction=ss.direction,
+ ignore_case=ss.ignore_case))
+ else:
+ document = buffer.document
+
+ get_processed_line = self._create_get_processed_line_func(cli, document)
+ self._last_get_processed_line = get_processed_line
+
+ def translate_rowcol(row, col):
+ " Return the content column for this coordinate. "
+ return Point(y=row, x=get_processed_line(row).source_to_display(col))
+
+ def get_line(i):
+ " Return the tokens for a given line number. "
+ tokens = get_processed_line(i).tokens
+
+ # Add a space at the end, because that is a possible cursor
+ # position. (When inserting after the input.) We should do this on
+ # all the lines, not just the line containing the cursor. (Because
+ # otherwise, line wrapping/scrolling could change when moving the
+ # cursor around.)
+ tokens = tokens + [(self.default_char.token, ' ')]
+ return tokens
+
+ content = UIContent(
+ get_line=get_line,
+ line_count=document.line_count,
+ cursor_position=translate_rowcol(document.cursor_position_row,
+ document.cursor_position_col),
+ default_char=self.default_char)
+
+ # If there is an auto completion going on, use that start point for a
+ # pop-up menu position. (But only when this buffer has the focus --
+ # there is only one place for a menu, determined by the focussed buffer.)
+ if cli.current_buffer_name == self.buffer_name:
+ menu_position = self.menu_position(cli) if self.menu_position else None
+ if menu_position is not None:
+ assert isinstance(menu_position, int)
+ menu_row, menu_col = buffer.document.translate_index_to_position(menu_position)
+ content.menu_position = translate_rowcol(menu_row, menu_col)
+ elif buffer.complete_state:
+ # Position for completion menu.
+ # Note: We use 'min', because the original cursor position could be
+ # behind the input string when the actual completion is for
+ # some reason shorter than the text we had before. (A completion
+ # can change and shorten the input.)
+ menu_row, menu_col = buffer.document.translate_index_to_position(
+ min(buffer.cursor_position,
+ buffer.complete_state.original_document.cursor_position))
+ content.menu_position = translate_rowcol(menu_row, menu_col)
+ else:
+ content.menu_position = None
+
+ return content
+
+ def mouse_handler(self, cli, mouse_event):
+ """
+ Mouse handler for this control.
+ """
+ buffer = self._buffer(cli)
+ position = mouse_event.position
+
+ # Focus buffer when clicked.
+ if self.has_focus(cli):
+ if self._last_get_processed_line:
+ processed_line = self._last_get_processed_line(position.y)
+
+ # Translate coordinates back to the cursor position of the
+ # original input.
+ xpos = processed_line.display_to_source(position.x)
+ index = buffer.document.translate_row_col_to_index(position.y, xpos)
+
+ # Set the cursor position.
+ if mouse_event.event_type == MouseEventType.MOUSE_DOWN:
+ buffer.exit_selection()
+ buffer.cursor_position = index
+
+ elif mouse_event.event_type == MouseEventType.MOUSE_UP:
+ # When the cursor was moved to another place, select the text.
+ # (The >1 is actually a small but acceptable workaround for
+ # selecting text in Vi navigation mode. In navigation mode,
+ # the cursor can never be after the text, so the cursor
+ # will be repositioned automatically.)
+ if abs(buffer.cursor_position - index) > 1:
+ buffer.start_selection(selection_type=SelectionType.CHARACTERS)
+ buffer.cursor_position = index
+
+ # Select word around cursor on double click.
+ # Two MOUSE_UP events in a short timespan are considered a double click.
+ double_click = self._last_click_timestamp and time.time() - self._last_click_timestamp < .3
+ self._last_click_timestamp = time.time()
+
+ if double_click:
+ start, end = buffer.document.find_boundaries_of_current_word()
+ buffer.cursor_position += start
+ buffer.start_selection(selection_type=SelectionType.CHARACTERS)
+ buffer.cursor_position += end - start
+ else:
+ # Don't handle scroll events here.
+ return NotImplemented
+
+ # Not focussed, but focussing on click events.
+ else:
+ if self.focus_on_click(cli) and mouse_event.event_type == MouseEventType.MOUSE_UP:
+ # Focus happens on mouseup. (If we did this on mousedown, the
+ # up event will be received at the point where this widget is
+ # focussed and be handled anyway.)
+ cli.focus(self.buffer_name)
+ else:
+ return NotImplemented
+
+ def move_cursor_down(self, cli):
+ b = self._buffer(cli)
+ b.cursor_position += b.document.get_cursor_down_position()
+
+ def move_cursor_up(self, cli):
+ b = self._buffer(cli)
+ b.cursor_position += b.document.get_cursor_up_position()
diff --git a/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/dimension.py b/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/dimension.py
new file mode 100644
index 00000000000..717ad7a81fa
--- /dev/null
+++ b/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/dimension.py
@@ -0,0 +1,92 @@
+"""
+Layout dimensions are used to give the minimum, maximum and preferred
+dimensions for containers and controls.
+"""
+from __future__ import unicode_literals
+
+__all__ = (
+ 'LayoutDimension',
+ 'sum_layout_dimensions',
+ 'max_layout_dimensions',
+)
+
+
+class LayoutDimension(object):
+ """
+ Specified dimension (width/height) of a user control or window.
+
+ The layout engine tries to honor the preferred size. If that is not
+ possible, because the terminal is larger or smaller, it tries to keep in
+ between min and max.
+
+ :param min: Minimum size.
+ :param max: Maximum size.
+ :param weight: For a VSplit/HSplit, the actual size will be determined
+ by taking the proportion of weights from all the children.
+ E.g. When there are two children, one width a weight of 1,
+ and the other with a weight of 2. The second will always be
+ twice as big as the first, if the min/max values allow it.
+ :param preferred: Preferred size.
+ """
+ def __init__(self, min=None, max=None, weight=1, preferred=None):
+ assert isinstance(weight, int) and weight > 0 # Cannot be a float.
+
+ self.min_specified = min is not None
+ self.max_specified = max is not None
+ self.preferred_specified = preferred is not None
+
+ if min is None:
+ min = 0 # Smallest possible value.
+ if max is None: # 0-values are allowed, so use "is None"
+ max = 1000 ** 10 # Something huge.
+ if preferred is None:
+ preferred = min
+
+ self.min = min
+ self.max = max
+ self.preferred = preferred
+ self.weight = weight
+
+ # Make sure that the 'preferred' size is always in the min..max range.
+ if self.preferred < self.min:
+ self.preferred = self.min
+
+ if self.preferred > self.max:
+ self.preferred = self.max
+
+ @classmethod
+ def exact(cls, amount):
+ """
+ Return a :class:`.LayoutDimension` with an exact size. (min, max and
+ preferred set to ``amount``).
+ """
+ return cls(min=amount, max=amount, preferred=amount)
+
+ def __repr__(self):
+ return 'LayoutDimension(min=%r, max=%r, preferred=%r, weight=%r)' % (
+ self.min, self.max, self.preferred, self.weight)
+
+ def __add__(self, other):
+ return sum_layout_dimensions([self, other])
+
+
+def sum_layout_dimensions(dimensions):
+ """
+ Sum a list of :class:`.LayoutDimension` instances.
+ """
+ min = sum([d.min for d in dimensions if d.min is not None])
+ max = sum([d.max for d in dimensions if d.max is not None])
+ preferred = sum([d.preferred for d in dimensions])
+
+ return LayoutDimension(min=min, max=max, preferred=preferred)
+
+
+def max_layout_dimensions(dimensions):
+ """
+ Take the maximum of a list of :class:`.LayoutDimension` instances.
+ """
+ min_ = max([d.min for d in dimensions if d.min is not None])
+ max_ = max([d.max for d in dimensions if d.max is not None])
+ preferred = max([d.preferred for d in dimensions])
+
+ return LayoutDimension(min=min_, max=max_, preferred=preferred)
diff --git a/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/lexers.py b/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/lexers.py
new file mode 100644
index 00000000000..a928fd82264
--- /dev/null
+++ b/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/lexers.py
@@ -0,0 +1,320 @@
+"""
+Lexer interface and implementation.
+Used for syntax highlighting.
+"""
+from __future__ import unicode_literals
+from abc import ABCMeta, abstractmethod
+from six import with_metaclass
+from six.moves import range
+
+from prompt_toolkit.token import Token
+from prompt_toolkit.filters import to_cli_filter
+from .utils import split_lines
+
+import re
+import six
+
+__all__ = (
+ 'Lexer',
+ 'SimpleLexer',
+ 'PygmentsLexer',
+ 'SyntaxSync',
+ 'SyncFromStart',
+ 'RegexSync',
+)
+
+
+class Lexer(with_metaclass(ABCMeta, object)):
+ """
+ Base class for all lexers.
+ """
+ @abstractmethod
+ def lex_document(self, cli, document):
+ """
+ Takes a :class:`~prompt_toolkit.document.Document` and returns a
+ callable that takes a line number and returns the tokens for that line.
+ """
+
+
+class SimpleLexer(Lexer):
+ """
+ Lexer that doesn't do any tokenizing and returns the whole input as one token.
+
+ :param token: The `Token` for this lexer.
+ """
+ # `default_token` parameter is deprecated!
+ def __init__(self, token=Token, default_token=None):
+ self.token = token
+
+ if default_token is not None:
+ self.token = default_token
+
+ def lex_document(self, cli, document):
+ lines = document.lines
+
+ def get_line(lineno):
+ " Return the tokens for the given line. "
+ try:
+ return [(self.token, lines[lineno])]
+ except IndexError:
+ return []
+ return get_line
+
+
+class SyntaxSync(with_metaclass(ABCMeta, object)):
+ """
+ Syntax synchroniser. This is a tool that finds a start position for the
+ lexer. This is especially important when editing big documents; we don't
+ want to start the highlighting by running the lexer from the beginning of
+ the file. That is very slow when editing.
+ """
+ @abstractmethod
+ def get_sync_start_position(self, document, lineno):
+ """
+ Return the position from where we can start lexing as a (row, column)
+ tuple.
+
+ :param document: `Document` instance that contains all the lines.
+ :param lineno: The line that we want to highlight. (We need to return
+ this line, or an earlier position.)
+ """
+
+class SyncFromStart(SyntaxSync):
+ """
+ Always start the syntax highlighting from the beginning.
+ """
+ def get_sync_start_position(self, document, lineno):
+ return 0, 0
+
+
+class RegexSync(SyntaxSync):
+ """
+ Synchronize by starting at a line that matches the given regex pattern.
+ """
+ # Never go more than this amount of lines backwards for synchronisation.
+ # That would be too CPU intensive.
+ MAX_BACKWARDS = 500
+
+ # Start lexing at the start, if we are in the first 'n' lines and no
+ # synchronisation position was found.
+ FROM_START_IF_NO_SYNC_POS_FOUND = 100
+
+ def __init__(self, pattern):
+ assert isinstance(pattern, six.text_type)
+ self._compiled_pattern = re.compile(pattern)
+
+ def get_sync_start_position(self, document, lineno):
+ " Scan backwards, and find a possible position to start. "
+ pattern = self._compiled_pattern
+ lines = document.lines
+
+ # Scan upwards, until we find a point where we can start the syntax
+ # synchronisation.
+ for i in range(lineno, max(-1, lineno - self.MAX_BACKWARDS), -1):
+ match = pattern.match(lines[i])
+ if match:
+ return i, match.start()
+
+ # No synchronisation point found. If we aren't that far from the
+ # beginning, start at the very beginning, otherwise, just try to start
+ # at the current line.
+ if lineno < self.FROM_START_IF_NO_SYNC_POS_FOUND:
+ return 0, 0
+ else:
+ return lineno, 0
+
+ @classmethod
+ def from_pygments_lexer_cls(cls, lexer_cls):
+ """
+ Create a :class:`.RegexSync` instance for this Pygments lexer class.
+ """
+ patterns = {
+ # For Python, start highlighting at any class/def block.
+ 'Python': r'^\s*(class|def)\s+',
+ 'Python 3': r'^\s*(class|def)\s+',
+
+ # For HTML, start at any open/close tag definition.
+ 'HTML': r'<[/a-zA-Z]',
+
+ # For javascript, start at a function.
+ 'JavaScript': r'\bfunction\b'
+
+ # TODO: Add definitions for other languages.
+ # By default, we start at every possible line.
+ }
+ p = patterns.get(lexer_cls.name, '^')
+ return cls(p)
+
+
+class PygmentsLexer(Lexer):
+ """
+ Lexer that calls a pygments lexer.
+
+ Example::
+
+ from pygments.lexers import HtmlLexer
+ lexer = PygmentsLexer(HtmlLexer)
+
+ Note: Don't forget to also load a Pygments compatible style. E.g.::
+
+ from prompt_toolkit.styles.from_pygments import style_from_pygments
+ from pygments.styles import get_style_by_name
+ style = style_from_pygments(get_style_by_name('monokai'))
+
+ :param pygments_lexer_cls: A `Lexer` from Pygments.
+ :param sync_from_start: Start lexing at the start of the document. This
+ will always give the best results, but it will be slow for bigger
+ documents. (When the last part of the document is display, then the
+ whole document will be lexed by Pygments on every key stroke.) It is
+ recommended to disable this for inputs that are expected to be more
+ than 1,000 lines.
+ :param syntax_sync: `SyntaxSync` object.
+ """
+ # Minimum amount of lines to go backwards when starting the parser.
+ # This is important when the lines are retrieved in reverse order, or when
+ # scrolling upwards. (Due to the complexity of calculating the vertical
+ # scroll offset in the `Window` class, lines are not always retrieved in
+ # order.)
+ MIN_LINES_BACKWARDS = 50
+
+ # When a parser was started this amount of lines back, read the parser
+ # until we get the current line. Otherwise, start a new parser.
+ # (This should probably be bigger than MIN_LINES_BACKWARDS.)
+ REUSE_GENERATOR_MAX_DISTANCE = 100
+
+ def __init__(self, pygments_lexer_cls, sync_from_start=True, syntax_sync=None):
+ assert syntax_sync is None or isinstance(syntax_sync, SyntaxSync)
+
+ self.pygments_lexer_cls = pygments_lexer_cls
+ self.sync_from_start = to_cli_filter(sync_from_start)
+
+ # Instantiate the Pygments lexer.
+ self.pygments_lexer = pygments_lexer_cls(
+ stripnl=False,
+ stripall=False,
+ ensurenl=False)
+
+ # Create syntax sync instance.
+ self.syntax_sync = syntax_sync or RegexSync.from_pygments_lexer_cls(pygments_lexer_cls)
+
+ @classmethod
+ def from_filename(cls, filename, sync_from_start=True):
+ """
+ Create a `Lexer` from a filename.
+ """
+ # Inline imports: the Pygments dependency is optional!
+ from pygments.util import ClassNotFound
+ from pygments.lexers import get_lexer_for_filename
+
+ try:
+ pygments_lexer = get_lexer_for_filename(filename)
+ except ClassNotFound:
+ return SimpleLexer()
+ else:
+ return cls(pygments_lexer.__class__, sync_from_start=sync_from_start)
+
+ def lex_document(self, cli, document):
+ """
+ Create a lexer function that takes a line number and returns the list
+ of (Token, text) tuples as the Pygments lexer returns for that line.
+ """
+ # Cache of already lexed lines.
+ cache = {}
+
+ # Pygments generators that are currently lexing.
+ line_generators = {} # Map lexer generator to the line number.
+
+ def get_syntax_sync():
+ " The Syntax synchronisation objcet that we currently use. "
+ if self.sync_from_start(cli):
+ return SyncFromStart()
+ else:
+ return self.syntax_sync
+
+ def find_closest_generator(i):
+ " Return a generator close to line 'i', or None if none was fonud. "
+ for generator, lineno in line_generators.items():
+ if lineno < i and i - lineno < self.REUSE_GENERATOR_MAX_DISTANCE:
+ return generator
+
+ def create_line_generator(start_lineno, column=0):
+ """
+ Create a generator that yields the lexed lines.
+ Each iteration it yields a (line_number, [(token, text), ...]) tuple.
+ """
+ def get_tokens():
+ text = '\n'.join(document.lines[start_lineno:])[column:]
+
+ # We call `get_tokens_unprocessed`, because `get_tokens` will
+ # still replace \r\n and \r by \n. (We don't want that,
+ # Pygments should return exactly the same amount of text, as we
+ # have given as input.)
+ for _, t, v in self.pygments_lexer.get_tokens_unprocessed(text):
+ yield t, v
+
+ return enumerate(split_lines(get_tokens()), start_lineno)
+
+ def get_generator(i):
+ """
+ Find an already started generator that is close, or create a new one.
+ """
+ # Find closest line generator.
+ generator = find_closest_generator(i)
+ if generator:
+ return generator
+
+ # No generator found. Determine starting point for the syntax
+ # synchronisation first.
+
+ # Go at least x lines back. (Make scrolling upwards more
+ # efficient.)
+ i = max(0, i - self.MIN_LINES_BACKWARDS)
+
+ if i == 0:
+ row = 0
+ column = 0
+ else:
+ row, column = get_syntax_sync().get_sync_start_position(document, i)
+
+ # Find generator close to this point, or otherwise create a new one.
+ generator = find_closest_generator(i)
+ if generator:
+ return generator
+ else:
+ generator = create_line_generator(row, column)
+
+ # If the column is not 0, ignore the first line. (Which is
+ # incomplete. This happens when the synchronisation algorithm tells
+ # us to start parsing in the middle of a line.)
+ if column:
+ next(generator)
+ row += 1
+
+ line_generators[generator] = row
+ return generator
+
+ def get_line(i):
+ " Return the tokens for a given line number. "
+ try:
+ return cache[i]
+ except KeyError:
+ generator = get_generator(i)
+
+ # Exhaust the generator, until we find the requested line.
+ for num, line in generator:
+ cache[num] = line
+ if num == i:
+ line_generators[generator] = i
+
+ # Remove the next item from the cache.
+ # (It could happen that it's already there, because of
+ # another generator that started filling these lines,
+ # but we want to synchronise these lines with the
+ # current lexer's state.)
+ if num + 1 in cache:
+ del cache[num + 1]
+
+ return cache[num]
+ return []
+
+ return get_line
diff --git a/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/margins.py b/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/margins.py
new file mode 100644
index 00000000000..2934dfc9a75
--- /dev/null
+++ b/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/margins.py
@@ -0,0 +1,253 @@
+"""
+Margin implementations for a :class:`~prompt_toolkit.layout.containers.Window`.
+"""
+from __future__ import unicode_literals
+
+from abc import ABCMeta, abstractmethod
+from six import with_metaclass
+from six.moves import range
+
+from prompt_toolkit.filters import to_cli_filter
+from prompt_toolkit.token import Token
+from prompt_toolkit.utils import get_cwidth
+from .utils import token_list_to_text
+
+__all__ = (
+ 'Margin',
+ 'NumberredMargin',
+ 'ScrollbarMargin',
+ 'ConditionalMargin',
+ 'PromptMargin',
+)
+
+
+class Margin(with_metaclass(ABCMeta, object)):
+ """
+ Base interface for a margin.
+ """
+ @abstractmethod
+ def get_width(self, cli, get_ui_content):
+ """
+ Return the width that this margin is going to consume.
+
+ :param cli: :class:`.CommandLineInterface` instance.
+ :param get_ui_content: Callable that asks the user control to create
+ a :class:`.UIContent` instance. This can be used for instance to
+ obtain the number of lines.
+ """
+ return 0
+
+ @abstractmethod
+ def create_margin(self, cli, window_render_info, width, height):
+ """
+ Creates a margin.
+ This should return a list of (Token, text) tuples.
+
+ :param cli: :class:`.CommandLineInterface` instance.
+ :param window_render_info:
+ :class:`~prompt_toolkit.layout.containers.WindowRenderInfo`
+ instance, generated after rendering and copying the visible part of
+ the :class:`~prompt_toolkit.layout.controls.UIControl` into the
+ :class:`~prompt_toolkit.layout.containers.Window`.
+ :param width: The width that's available for this margin. (As reported
+ by :meth:`.get_width`.)
+ :param height: The height that's available for this margin. (The height
+ of the :class:`~prompt_toolkit.layout.containers.Window`.)
+ """
+ return []
+
+
+class NumberredMargin(Margin):
+ """
+ Margin that displays the line numbers.
+
+ :param relative: Number relative to the cursor position. Similar to the Vi
+ 'relativenumber' option.
+ :param display_tildes: Display tildes after the end of the document, just
+ like Vi does.
+ """
+ def __init__(self, relative=False, display_tildes=False):
+ self.relative = to_cli_filter(relative)
+ self.display_tildes = to_cli_filter(display_tildes)
+
+ def get_width(self, cli, get_ui_content):
+ line_count = get_ui_content().line_count
+ return max(3, len('%s' % line_count) + 1)
+
+ def create_margin(self, cli, window_render_info, width, height):
+ relative = self.relative(cli)
+
+ token = Token.LineNumber
+ token_current = Token.LineNumber.Current
+
+ # Get current line number.
+ current_lineno = window_render_info.ui_content.cursor_position.y
+
+ # Construct margin.
+ result = []
+ last_lineno = None
+
+ for y, lineno in enumerate(window_render_info.displayed_lines):
+ # Only display line number if this line is not a continuation of the previous line.
+ if lineno != last_lineno:
+ if lineno is None:
+ pass
+ elif lineno == current_lineno:
+ # Current line.
+ if relative:
+ # Left align current number in relative mode.
+ result.append((token_current, '%i' % (lineno + 1)))
+ else:
+ result.append((token_current, ('%i ' % (lineno + 1)).rjust(width)))
+ else:
+ # Other lines.
+ if relative:
+ lineno = abs(lineno - current_lineno) - 1
+
+ result.append((token, ('%i ' % (lineno + 1)).rjust(width)))
+
+ last_lineno = lineno
+ result.append((Token, '\n'))
+
+ # Fill with tildes.
+ if self.display_tildes(cli):
+ while y < window_render_info.window_height:
+ result.append((Token.Tilde, '~\n'))
+ y += 1
+
+ return result
+
+
+class ConditionalMargin(Margin):
+ """
+ Wrapper around other :class:`.Margin` classes to show/hide them.
+ """
+ def __init__(self, margin, filter):
+ assert isinstance(margin, Margin)
+
+ self.margin = margin
+ self.filter = to_cli_filter(filter)
+
+ def get_width(self, cli, ui_content):
+ if self.filter(cli):
+ return self.margin.get_width(cli, ui_content)
+ else:
+ return 0
+
+ def create_margin(self, cli, window_render_info, width, height):
+ if width and self.filter(cli):
+ return self.margin.create_margin(cli, window_render_info, width, height)
+ else:
+ return []
+
+
+class ScrollbarMargin(Margin):
+ """
+ Margin displaying a scrollbar.
+
+ :param display_arrows: Display scroll up/down arrows.
+ """
+ def __init__(self, display_arrows=False):
+ self.display_arrows = to_cli_filter(display_arrows)
+
+ def get_width(self, cli, ui_content):
+ return 1
+
+ def create_margin(self, cli, window_render_info, width, height):
+ total_height = window_render_info.content_height
+ display_arrows = self.display_arrows(cli)
+
+ window_height = window_render_info.window_height
+ if display_arrows:
+ window_height -= 2
+
+ try:
+ items_per_row = float(total_height) / min(total_height, window_height)
+ except ZeroDivisionError:
+ return []
+ else:
+ def is_scroll_button(row):
+ " True if we should display a button on this row. "
+ current_row_middle = int((row + .5) * items_per_row)
+ return current_row_middle in window_render_info.displayed_lines
+
+ # Up arrow.
+ result = []
+ if display_arrows:
+ result.extend([
+ (Token.Scrollbar.Arrow, '^'),
+ (Token.Scrollbar, '\n')
+ ])
+
+ # Scrollbar body.
+ for i in range(window_height):
+ if is_scroll_button(i):
+ result.append((Token.Scrollbar.Button, ' '))
+ else:
+ result.append((Token.Scrollbar, ' '))
+ result.append((Token, '\n'))
+
+ # Down arrow
+ if display_arrows:
+ result.append((Token.Scrollbar.Arrow, 'v'))
+
+ return result
+
+
+class PromptMargin(Margin):
+ """
+ Create margin that displays a prompt.
+ This can display one prompt at the first line, and a continuation prompt
+ (e.g, just dots) on all the following lines.
+
+ :param get_prompt_tokens: Callable that takes a CommandLineInterface as
+ input and returns a list of (Token, type) tuples to be shown as the
+ prompt at the first line.
+ :param get_continuation_tokens: Callable that takes a CommandLineInterface
+ and a width as input and returns a list of (Token, type) tuples for the
+ next lines of the input.
+ :param show_numbers: (bool or :class:`~prompt_toolkit.filters.CLIFilter`)
+ Display line numbers instead of the continuation prompt.
+ """
+ def __init__(self, get_prompt_tokens, get_continuation_tokens=None,
+ show_numbers=False):
+ assert callable(get_prompt_tokens)
+ assert get_continuation_tokens is None or callable(get_continuation_tokens)
+ show_numbers = to_cli_filter(show_numbers)
+
+ self.get_prompt_tokens = get_prompt_tokens
+ self.get_continuation_tokens = get_continuation_tokens
+ self.show_numbers = show_numbers
+
+ def get_width(self, cli, ui_content):
+ " Width to report to the `Window`. "
+ # Take the width from the first line.
+ text = token_list_to_text(self.get_prompt_tokens(cli))
+ return get_cwidth(text)
+
+ def create_margin(self, cli, window_render_info, width, height):
+ # First line.
+ tokens = self.get_prompt_tokens(cli)[:]
+
+ # Next lines. (Show line numbering when numbering is enabled.)
+ if self.get_continuation_tokens:
+ # Note: we turn this into a list, to make sure that we fail early
+ # in case `get_continuation_tokens` returns something else,
+ # like `None`.
+ tokens2 = list(self.get_continuation_tokens(cli, width))
+ else:
+ tokens2 = []
+
+ show_numbers = self.show_numbers(cli)
+ last_y = None
+
+ for y in window_render_info.displayed_lines[1:]:
+ tokens.append((Token, '\n'))
+ if show_numbers:
+ if y != last_y:
+ tokens.append((Token.LineNumber, ('%i ' % (y + 1)).rjust(width)))
+ else:
+ tokens.extend(tokens2)
+ last_y = y
+
+ return tokens
diff --git a/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/menus.py b/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/menus.py
new file mode 100644
index 00000000000..a916846e458
--- /dev/null
+++ b/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/menus.py
@@ -0,0 +1,496 @@
+from __future__ import unicode_literals
+
+from six.moves import zip_longest, range
+from prompt_toolkit.filters import HasCompletions, IsDone, Condition, to_cli_filter
+from prompt_toolkit.mouse_events import MouseEventType
+from prompt_toolkit.token import Token
+from prompt_toolkit.utils import get_cwidth
+
+from .containers import Window, HSplit, ConditionalContainer, ScrollOffsets
+from .controls import UIControl, UIContent
+from .dimension import LayoutDimension
+from .margins import ScrollbarMargin
+from .screen import Point, Char
+
+import math
+
+__all__ = (
+ 'CompletionsMenu',
+ 'MultiColumnCompletionsMenu',
+)
+
+
+class CompletionsMenuControl(UIControl):
+ """
+ Helper for drawing the complete menu to the screen.
+
+ :param scroll_offset: Number (integer) representing the preferred amount of
+ completions to be displayed before and after the current one. When this
+ is a very high number, the current completion will be shown in the
+ middle most of the time.
+ """
+ # Preferred minimum size of the menu control.
+ # The CompletionsMenu class defines a width of 8, and there is a scrollbar
+ # of 1.)
+ MIN_WIDTH = 7
+
+ def __init__(self):
+ self.token = Token.Menu.Completions
+
+ def has_focus(self, cli):
+ return False
+
+ def preferred_width(self, cli, max_available_width):
+ complete_state = cli.current_buffer.complete_state
+ if complete_state:
+ menu_width = self._get_menu_width(500, complete_state)
+ menu_meta_width = self._get_menu_meta_width(500, complete_state)
+
+ return menu_width + menu_meta_width
+ else:
+ return 0
+
+ def preferred_height(self, cli, width, max_available_height, wrap_lines):
+ complete_state = cli.current_buffer.complete_state
+ if complete_state:
+ return len(complete_state.current_completions)
+ else:
+ return 0
+
+ def create_content(self, cli, width, height):
+ """
+ Create a UIContent object for this control.
+ """
+ complete_state = cli.current_buffer.complete_state
+ if complete_state:
+ completions = complete_state.current_completions
+ index = complete_state.complete_index # Can be None!
+
+ # Calculate width of completions menu.
+ menu_width = self._get_menu_width(width, complete_state)
+ menu_meta_width = self._get_menu_meta_width(width - menu_width, complete_state)
+ show_meta = self._show_meta(complete_state)
+
+ def get_line(i):
+ c = completions[i]
+ is_current_completion = (i == index)
+ result = self._get_menu_item_tokens(c, is_current_completion, menu_width)
+
+ if show_meta:
+ result += self._get_menu_item_meta_tokens(c, is_current_completion, menu_meta_width)
+ return result
+
+ return UIContent(get_line=get_line,
+ cursor_position=Point(x=0, y=index or 0),
+ line_count=len(completions),
+ default_char=Char(' ', self.token))
+
+ return UIContent()
+
+ def _show_meta(self, complete_state):
+ """
+ Return ``True`` if we need to show a column with meta information.
+ """
+ return any(c.display_meta for c in complete_state.current_completions)
+
+ def _get_menu_width(self, max_width, complete_state):
+ """
+ Return the width of the main column.
+ """
+ return min(max_width, max(self.MIN_WIDTH, max(get_cwidth(c.display)
+ for c in complete_state.current_completions) + 2))
+
+ def _get_menu_meta_width(self, max_width, complete_state):
+ """
+ Return the width of the meta column.
+ """
+ if self._show_meta(complete_state):
+ return min(max_width, max(get_cwidth(c.display_meta)
+ for c in complete_state.current_completions) + 2)
+ else:
+ return 0
+
+ def _get_menu_item_tokens(self, completion, is_current_completion, width):
+ if is_current_completion:
+ token = self.token.Completion.Current
+ else:
+ token = self.token.Completion
+
+ text, tw = _trim_text(completion.display, width - 2)
+ padding = ' ' * (width - 2 - tw)
+ return [(token, ' %s%s ' % (text, padding))]
+
+ def _get_menu_item_meta_tokens(self, completion, is_current_completion, width):
+ if is_current_completion:
+ token = self.token.Meta.Current
+ else:
+ token = self.token.Meta
+
+ text, tw = _trim_text(completion.display_meta, width - 2)
+ padding = ' ' * (width - 2 - tw)
+ return [(token, ' %s%s ' % (text, padding))]
+
+ def mouse_handler(self, cli, mouse_event):
+ """
+ Handle mouse events: clicking and scrolling.
+ """
+ b = cli.current_buffer
+
+ if mouse_event.event_type == MouseEventType.MOUSE_UP:
+ # Select completion.
+ b.go_to_completion(mouse_event.position.y)
+ b.complete_state = None
+
+ elif mouse_event.event_type == MouseEventType.SCROLL_DOWN:
+ # Scroll up.
+ b.complete_next(count=3, disable_wrap_around=True)
+
+ elif mouse_event.event_type == MouseEventType.SCROLL_UP:
+ # Scroll down.
+ b.complete_previous(count=3, disable_wrap_around=True)
+
+
+def _trim_text(text, max_width):
+ """
+ Trim the text to `max_width`, append dots when the text is too long.
+ Returns (text, width) tuple.
+ """
+ width = get_cwidth(text)
+
+ # When the text is too wide, trim it.
+ if width > max_width:
+ # When there are no double width characters, just use slice operation.
+ if len(text) == width:
+ trimmed_text = (text[:max(1, max_width-3)] + '...')[:max_width]
+ return trimmed_text, len(trimmed_text)
+
+ # Otherwise, loop until we have the desired width. (Rather
+ # inefficient, but ok for now.)
+ else:
+ trimmed_text = ''
+ for c in text:
+ if get_cwidth(trimmed_text + c) <= max_width - 3:
+ trimmed_text += c
+ trimmed_text += '...'
+
+ return (trimmed_text, get_cwidth(trimmed_text))
+ else:
+ return text, width
+
+
+class CompletionsMenu(ConditionalContainer):
+ def __init__(self, max_height=None, scroll_offset=0, extra_filter=True, display_arrows=False):
+ extra_filter = to_cli_filter(extra_filter)
+ display_arrows = to_cli_filter(display_arrows)
+
+ super(CompletionsMenu, self).__init__(
+ content=Window(
+ content=CompletionsMenuControl(),
+ width=LayoutDimension(min=8),
+ height=LayoutDimension(min=1, max=max_height),
+ scroll_offsets=ScrollOffsets(top=scroll_offset, bottom=scroll_offset),
+ right_margins=[ScrollbarMargin(display_arrows=display_arrows)],
+ dont_extend_width=True,
+ ),
+ # Show when there are completions but not at the point we are
+ # returning the input.
+ filter=HasCompletions() & ~IsDone() & extra_filter)
+
+
+class MultiColumnCompletionMenuControl(UIControl):
+ """
+ Completion menu that displays all the completions in several columns.
+ When there are more completions than space for them to be displayed, an
+ arrow is shown on the left or right side.
+
+ `min_rows` indicates how many rows will be available in any possible case.
+ When this is langer than one, in will try to use less columns and more
+ rows until this value is reached.
+ Be careful passing in a too big value, if less than the given amount of
+ rows are available, more columns would have been required, but
+ `preferred_width` doesn't know about that and reports a too small value.
+ This results in less completions displayed and additional scrolling.
+ (It's a limitation of how the layout engine currently works: first the
+ widths are calculated, then the heights.)
+
+ :param suggested_max_column_width: The suggested max width of a column.
+ The column can still be bigger than this, but if there is place for two
+ columns of this width, we will display two columns. This to avoid that
+ if there is one very wide completion, that it doesn't significantly
+ reduce the amount of columns.
+ """
+ _required_margin = 3 # One extra padding on the right + space for arrows.
+
+ def __init__(self, min_rows=3, suggested_max_column_width=30):
+ assert isinstance(min_rows, int) and min_rows >= 1
+
+ self.min_rows = min_rows
+ self.suggested_max_column_width = suggested_max_column_width
+ self.token = Token.Menu.Completions
+ self.scroll = 0
+
+ # Info of last rendering.
+ self._rendered_rows = 0
+ self._rendered_columns = 0
+ self._total_columns = 0
+ self._render_pos_to_completion = {}
+ self._render_left_arrow = False
+ self._render_right_arrow = False
+ self._render_width = 0
+
+ def reset(self):
+ self.scroll = 0
+
+ def has_focus(self, cli):
+ return False
+
+ def preferred_width(self, cli, max_available_width):
+ """
+ Preferred width: prefer to use at least min_rows, but otherwise as much
+ as possible horizontally.
+ """
+ complete_state = cli.current_buffer.complete_state
+ column_width = self._get_column_width(complete_state)
+ result = int(column_width * math.ceil(len(complete_state.current_completions) / float(self.min_rows)))
+
+ # When the desired width is still more than the maximum available,
+ # reduce by removing columns until we are less than the available
+ # width.
+ while result > column_width and result > max_available_width - self._required_margin:
+ result -= column_width
+ return result + self._required_margin
+
+ def preferred_height(self, cli, width, max_available_height, wrap_lines):
+ """
+ Preferred height: as much as needed in order to display all the completions.
+ """
+ complete_state = cli.current_buffer.complete_state
+ column_width = self._get_column_width(complete_state)
+ column_count = max(1, (width - self._required_margin) // column_width)
+
+ return int(math.ceil(len(complete_state.current_completions) / float(column_count)))
+
+ def create_content(self, cli, width, height):
+ """
+ Create a UIContent object for this menu.
+ """
+ complete_state = cli.current_buffer.complete_state
+ column_width = self._get_column_width(complete_state)
+ self._render_pos_to_completion = {}
+
+ def grouper(n, iterable, fillvalue=None):
+ " grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx "
+ args = [iter(iterable)] * n
+ return zip_longest(fillvalue=fillvalue, *args)
+
+ def is_current_completion(completion):
+ " Returns True when this completion is the currently selected one. "
+ return complete_state.complete_index is not None and c == complete_state.current_completion
+
+ # Space required outside of the regular columns, for displaying the
+ # left and right arrow.
+ HORIZONTAL_MARGIN_REQUIRED = 3
+
+ if complete_state:
+ # There should be at least one column, but it cannot be wider than
+ # the available width.
+ column_width = min(width - HORIZONTAL_MARGIN_REQUIRED, column_width)
+
+ # However, when the columns tend to be very wide, because there are
+ # some very wide entries, shrink it anyway.
+ if column_width > self.suggested_max_column_width:
+ # `column_width` can still be bigger that `suggested_max_column_width`,
+ # but if there is place for two columns, we divide by two.
+ column_width //= (column_width // self.suggested_max_column_width)
+
+ visible_columns = max(1, (width - self._required_margin) // column_width)
+
+ columns_ = list(grouper(height, complete_state.current_completions))
+ rows_ = list(zip(*columns_))
+
+ # Make sure the current completion is always visible: update scroll offset.
+ selected_column = (complete_state.complete_index or 0) // height
+ self.scroll = min(selected_column, max(self.scroll, selected_column - visible_columns + 1))
+
+ render_left_arrow = self.scroll > 0
+ render_right_arrow = self.scroll < len(rows_[0]) - visible_columns
+
+ # Write completions to screen.
+ tokens_for_line = []
+
+ for row_index, row in enumerate(rows_):
+ tokens = []
+ middle_row = row_index == len(rows_) // 2
+
+ # Draw left arrow if we have hidden completions on the left.
+ if render_left_arrow:
+ tokens += [(Token.Scrollbar, '<' if middle_row else ' ')]
+
+ # Draw row content.
+ for column_index, c in enumerate(row[self.scroll:][:visible_columns]):
+ if c is not None:
+ tokens += self._get_menu_item_tokens(c, is_current_completion(c), column_width)
+
+ # Remember render position for mouse click handler.
+ for x in range(column_width):
+ self._render_pos_to_completion[(column_index * column_width + x, row_index)] = c
+ else:
+ tokens += [(self.token.Completion, ' ' * column_width)]
+
+ # Draw trailing padding. (_get_menu_item_tokens only returns padding on the left.)
+ tokens += [(self.token.Completion, ' ')]
+
+ # Draw right arrow if we have hidden completions on the right.
+ if render_right_arrow:
+ tokens += [(Token.Scrollbar, '>' if middle_row else ' ')]
+
+ # Newline.
+ tokens_for_line.append(tokens)
+
+ else:
+ tokens = []
+
+ self._rendered_rows = height
+ self._rendered_columns = visible_columns
+ self._total_columns = len(columns_)
+ self._render_left_arrow = render_left_arrow
+ self._render_right_arrow = render_right_arrow
+ self._render_width = column_width * visible_columns + render_left_arrow + render_right_arrow + 1
+
+ def get_line(i):
+ return tokens_for_line[i]
+
+ return UIContent(get_line=get_line, line_count=len(rows_))
+
+ def _get_column_width(self, complete_state):
+ """
+ Return the width of each column.
+ """
+ return max(get_cwidth(c.display) for c in complete_state.current_completions) + 1
+
+ def _get_menu_item_tokens(self, completion, is_current_completion, width):
+ if is_current_completion:
+ token = self.token.Completion.Current
+ else:
+ token = self.token.Completion
+
+ text, tw = _trim_text(completion.display, width)
+ padding = ' ' * (width - tw - 1)
+
+ return [(token, ' %s%s' % (text, padding))]
+
+ def mouse_handler(self, cli, mouse_event):
+ """
+ Handle scoll and click events.
+ """
+ b = cli.current_buffer
+
+ def scroll_left():
+ b.complete_previous(count=self._rendered_rows, disable_wrap_around=True)
+ self.scroll = max(0, self.scroll - 1)
+
+ def scroll_right():
+ b.complete_next(count=self._rendered_rows, disable_wrap_around=True)
+ self.scroll = min(self._total_columns - self._rendered_columns, self.scroll + 1)
+
+ if mouse_event.event_type == MouseEventType.SCROLL_DOWN:
+ scroll_right()
+
+ elif mouse_event.event_type == MouseEventType.SCROLL_UP:
+ scroll_left()
+
+ elif mouse_event.event_type == MouseEventType.MOUSE_UP:
+ x = mouse_event.position.x
+ y = mouse_event.position.y
+
+ # Mouse click on left arrow.
+ if x == 0:
+ if self._render_left_arrow:
+ scroll_left()
+
+ # Mouse click on right arrow.
+ elif x == self._render_width - 1:
+ if self._render_right_arrow:
+ scroll_right()
+
+ # Mouse click on completion.
+ else:
+ completion = self._render_pos_to_completion.get((x, y))
+ if completion:
+ b.apply_completion(completion)
+
+
+class MultiColumnCompletionsMenu(HSplit):
+ """
+ Container that displays the completions in several columns.
+ When `show_meta` (a :class:`~prompt_toolkit.filters.CLIFilter`) evaluates
+ to True, it shows the meta information at the bottom.
+ """
+ def __init__(self, min_rows=3, suggested_max_column_width=30, show_meta=True, extra_filter=True):
+ show_meta = to_cli_filter(show_meta)
+ extra_filter = to_cli_filter(extra_filter)
+
+ # Display filter: show when there are completions but not at the point
+ # we are returning the input.
+ full_filter = HasCompletions() & ~IsDone() & extra_filter
+
+ any_completion_has_meta = Condition(lambda cli:
+ any(c.display_meta for c in cli.current_buffer.complete_state.current_completions))
+
+ # Create child windows.
+ completions_window = ConditionalContainer(
+ content=Window(
+ content=MultiColumnCompletionMenuControl(
+ min_rows=min_rows, suggested_max_column_width=suggested_max_column_width),
+ width=LayoutDimension(min=8),
+ height=LayoutDimension(min=1)),
+ filter=full_filter)
+
+ meta_window = ConditionalContainer(
+ content=Window(content=_SelectedCompletionMetaControl()),
+ filter=show_meta & full_filter & any_completion_has_meta)
+
+ # Initialise split.
+ super(MultiColumnCompletionsMenu, self).__init__([
+ completions_window,
+ meta_window
+ ])
+
+
+class _SelectedCompletionMetaControl(UIControl):
+ """
+ Control that shows the meta information of the selected token.
+ """
+ def preferred_width(self, cli, max_available_width):
+ """
+ Report the width of the longest meta text as the preferred width of this control.
+
+ It could be that we use less width, but this way, we're sure that the
+ layout doesn't change when we select another completion (E.g. that
+ completions are suddenly shown in more or fewer columns.)
+ """
+ if cli.current_buffer.complete_state:
+ state = cli.current_buffer.complete_state
+ return 2 + max(get_cwidth(c.display_meta) for c in state.current_completions)
+ else:
+ return 0
+
+ def preferred_height(self, cli, width, max_available_height, wrap_lines):
+ return 1
+
+ def create_content(self, cli, width, height):
+ tokens = self._get_tokens(cli)
+
+ def get_line(i):
+ return tokens
+
+ return UIContent(get_line=get_line, line_count=1 if tokens else 0)
+
+ def _get_tokens(self, cli):
+ token = Token.Menu.Completions.MultiColumnMeta
+ state = cli.current_buffer.complete_state
+
+ if state and state.current_completion and state.current_completion.display_meta:
+ return [(token, ' %s ' % state.current_completion.display_meta)]
+
+ return []
diff --git a/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/mouse_handlers.py b/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/mouse_handlers.py
new file mode 100644
index 00000000000..d443bf8315e
--- /dev/null
+++ b/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/mouse_handlers.py
@@ -0,0 +1,29 @@
+from __future__ import unicode_literals
+
+from itertools import product
+from collections import defaultdict
+
+__all__ = (
+ 'MouseHandlers',
+)
+
+
+class MouseHandlers(object):
+ """
+ Two dimentional raster of callbacks for mouse events.
+ """
+ def __init__(self):
+ def dummy_callback(cli, mouse_event):
+ """
+ :param mouse_event: `MouseEvent` instance.
+ """
+
+ # Map (x,y) tuples to handlers.
+ self.mouse_handlers = defaultdict(lambda: dummy_callback)
+
+ def set_mouse_handler_for_range(self, x_min, x_max, y_min, y_max, handler=None):
+ """
+ Set mouse handler for a region.
+ """
+ for x, y in product(range(x_min, x_max), range(y_min, y_max)):
+ self.mouse_handlers[x,y] = handler
diff --git a/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/processors.py b/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/processors.py
new file mode 100644
index 00000000000..0b8bc9c223d
--- /dev/null
+++ b/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/processors.py
@@ -0,0 +1,605 @@
+"""
+Processors are little transformation blocks that transform the token list from
+a buffer before the BufferControl will render it to the screen.
+
+They can insert tokens before or after, or highlight fragments by replacing the
+token types.
+"""
+from __future__ import unicode_literals
+from abc import ABCMeta, abstractmethod
+from six import with_metaclass
+from six.moves import range
+
+from prompt_toolkit.cache import SimpleCache
+from prompt_toolkit.document import Document
+from prompt_toolkit.enums import SEARCH_BUFFER
+from prompt_toolkit.filters import to_cli_filter, ViInsertMultipleMode
+from prompt_toolkit.layout.utils import token_list_to_text
+from prompt_toolkit.reactive import Integer
+from prompt_toolkit.token import Token
+
+from .utils import token_list_len, explode_tokens
+
+import re
+
+__all__ = (
+ 'Processor',
+ 'Transformation',
+
+ 'HighlightSearchProcessor',
+ 'HighlightSelectionProcessor',
+ 'PasswordProcessor',
+ 'HighlightMatchingBracketProcessor',
+ 'DisplayMultipleCursors',
+ 'BeforeInput',
+ 'AfterInput',
+ 'AppendAutoSuggestion',
+ 'ConditionalProcessor',
+ 'ShowLeadingWhiteSpaceProcessor',
+ 'ShowTrailingWhiteSpaceProcessor',
+ 'TabsProcessor',
+)
+
+
+class Processor(with_metaclass(ABCMeta, object)):
+ """
+ Manipulate the tokens for a given line in a
+ :class:`~prompt_toolkit.layout.controls.BufferControl`.
+ """
+ @abstractmethod
+ def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
+ """
+ Apply transformation. Returns a :class:`.Transformation` instance.
+
+ :param cli: :class:`.CommandLineInterface` instance.
+ :param lineno: The number of the line to which we apply the processor.
+ :param source_to_display: A function that returns the position in the
+ `tokens` for any position in the source string. (This takes
+ previous processors into account.)
+ :param tokens: List of tokens that we can transform. (Received from the
+ previous processor.)
+ """
+ return Transformation(tokens)
+
+ def has_focus(self, cli):
+ """
+ Processors can override the focus.
+ (Used for the reverse-i-search prefix in DefaultPrompt.)
+ """
+ return False
+
+
+class Transformation(object):
+ """
+ Transformation result, as returned by :meth:`.Processor.apply_transformation`.
+
+ Important: Always make sure that the length of `document.text` is equal to
+ the length of all the text in `tokens`!
+
+ :param tokens: The transformed tokens. To be displayed, or to pass to the
+ next processor.
+ :param source_to_display: Cursor position transformation from original string to
+ transformed string.
+ :param display_to_source: Cursor position transformed from source string to
+ original string.
+ """
+ def __init__(self, tokens, source_to_display=None, display_to_source=None):
+ self.tokens = tokens
+ self.source_to_display = source_to_display or (lambda i: i)
+ self.display_to_source = display_to_source or (lambda i: i)
+
+
+class HighlightSearchProcessor(Processor):
+ """
+ Processor that highlights search matches in the document.
+ Note that this doesn't support multiline search matches yet.
+
+ :param preview_search: A Filter; when active it indicates that we take
+ the search text in real time while the user is typing, instead of the
+ last active search state.
+ """
+ def __init__(self, preview_search=False, search_buffer_name=SEARCH_BUFFER,
+ get_search_state=None):
+ self.preview_search = to_cli_filter(preview_search)
+ self.search_buffer_name = search_buffer_name
+ self.get_search_state = get_search_state or (lambda cli: cli.search_state)
+
+ def _get_search_text(self, cli):
+ """
+ The text we are searching for.
+ """
+ # When the search buffer has focus, take that text.
+ if self.preview_search(cli) and cli.buffers[self.search_buffer_name].text:
+ return cli.buffers[self.search_buffer_name].text
+ # Otherwise, take the text of the last active search.
+ else:
+ return self.get_search_state(cli).text
+
+ def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
+ search_text = self._get_search_text(cli)
+ searchmatch_current_token = (':', ) + Token.SearchMatch.Current
+ searchmatch_token = (':', ) + Token.SearchMatch
+
+ if search_text and not cli.is_returning:
+ # For each search match, replace the Token.
+ line_text = token_list_to_text(tokens)
+ tokens = explode_tokens(tokens)
+
+ flags = re.IGNORECASE if cli.is_ignoring_case else 0
+
+ # Get cursor column.
+ if document.cursor_position_row == lineno:
+ cursor_column = source_to_display(document.cursor_position_col)
+ else:
+ cursor_column = None
+
+ for match in re.finditer(re.escape(search_text), line_text, flags=flags):
+ if cursor_column is not None:
+ on_cursor = match.start() <= cursor_column < match.end()
+ else:
+ on_cursor = False
+
+ for i in range(match.start(), match.end()):
+ old_token, text = tokens[i]
+ if on_cursor:
+ tokens[i] = (old_token + searchmatch_current_token, tokens[i][1])
+ else:
+ tokens[i] = (old_token + searchmatch_token, tokens[i][1])
+
+ return Transformation(tokens)
+
+
+class HighlightSelectionProcessor(Processor):
+ """
+ Processor that highlights the selection in the document.
+ """
+ def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
+ selected_token = (':', ) + Token.SelectedText
+
+ # In case of selection, highlight all matches.
+ selection_at_line = document.selection_range_at_line(lineno)
+
+ if selection_at_line:
+ from_, to = selection_at_line
+ from_ = source_to_display(from_)
+ to = source_to_display(to)
+
+ tokens = explode_tokens(tokens)
+
+ if from_ == 0 and to == 0 and len(tokens) == 0:
+ # When this is an empty line, insert a space in order to
+ # visualiase the selection.
+ return Transformation([(Token.SelectedText, ' ')])
+ else:
+ for i in range(from_, to + 1):
+ if i < len(tokens):
+ old_token, old_text = tokens[i]
+ tokens[i] = (old_token + selected_token, old_text)
+
+ return Transformation(tokens)
+
+
+class PasswordProcessor(Processor):
+ """
+ Processor that turns masks the input. (For passwords.)
+
+ :param char: (string) Character to be used. "*" by default.
+ """
+ def __init__(self, char='*'):
+ self.char = char
+
+ def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
+ tokens = [(token, self.char * len(text)) for token, text in tokens]
+ return Transformation(tokens)
+
+
+class HighlightMatchingBracketProcessor(Processor):
+ """
+ When the cursor is on or right after a bracket, it highlights the matching
+ bracket.
+
+ :param max_cursor_distance: Only highlight matching brackets when the
+ cursor is within this distance. (From inside a `Processor`, we can't
+ know which lines will be visible on the screen. But we also don't want
+ to scan the whole document for matching brackets on each key press, so
+ we limit to this value.)
+ """
+ _closing_braces = '])}>'
+
+ def __init__(self, chars='[](){}<>', max_cursor_distance=1000):
+ self.chars = chars
+ self.max_cursor_distance = max_cursor_distance
+
+ self._positions_cache = SimpleCache(maxsize=8)
+
+ def _get_positions_to_highlight(self, document):
+ """
+ Return a list of (row, col) tuples that need to be highlighted.
+ """
+ # Try for the character under the cursor.
+ if document.current_char and document.current_char in self.chars:
+ pos = document.find_matching_bracket_position(
+ start_pos=document.cursor_position - self.max_cursor_distance,
+ end_pos=document.cursor_position + self.max_cursor_distance)
+
+ # Try for the character before the cursor.
+ elif (document.char_before_cursor and document.char_before_cursor in
+ self._closing_braces and document.char_before_cursor in self.chars):
+ document = Document(document.text, document.cursor_position - 1)
+
+ pos = document.find_matching_bracket_position(
+ start_pos=document.cursor_position - self.max_cursor_distance,
+ end_pos=document.cursor_position + self.max_cursor_distance)
+ else:
+ pos = None
+
+ # Return a list of (row, col) tuples that need to be highlighted.
+ if pos:
+ pos += document.cursor_position # pos is relative.
+ row, col = document.translate_index_to_position(pos)
+ return [(row, col), (document.cursor_position_row, document.cursor_position_col)]
+ else:
+ return []
+
+ def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
+ # Get the highlight positions.
+ key = (cli.render_counter, document.text, document.cursor_position)
+ positions = self._positions_cache.get(
+ key, lambda: self._get_positions_to_highlight(document))
+
+ # Apply if positions were found at this line.
+ if positions:
+ for row, col in positions:
+ if row == lineno:
+ col = source_to_display(col)
+ tokens = explode_tokens(tokens)
+ token, text = tokens[col]
+
+ if col == document.cursor_position_col:
+ token += (':', ) + Token.MatchingBracket.Cursor
+ else:
+ token += (':', ) + Token.MatchingBracket.Other
+
+ tokens[col] = (token, text)
+
+ return Transformation(tokens)
+
+
+class DisplayMultipleCursors(Processor):
+ """
+ When we're in Vi block insert mode, display all the cursors.
+ """
+ _insert_multiple = ViInsertMultipleMode()
+
+ def __init__(self, buffer_name):
+ self.buffer_name = buffer_name
+
+ def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
+ buff = cli.buffers[self.buffer_name]
+
+ if self._insert_multiple(cli):
+ positions = buff.multiple_cursor_positions
+ tokens = explode_tokens(tokens)
+
+ # If any cursor appears on the current line, highlight that.
+ start_pos = document.translate_row_col_to_index(lineno, 0)
+ end_pos = start_pos + len(document.lines[lineno])
+
+ token_suffix = (':', ) + Token.MultipleCursors.Cursor
+
+ for p in positions:
+ if start_pos <= p < end_pos:
+ column = source_to_display(p - start_pos)
+
+ # Replace token.
+ token, text = tokens[column]
+ token += token_suffix
+ tokens[column] = (token, text)
+ elif p == end_pos:
+ tokens.append((token_suffix, ' '))
+
+ return Transformation(tokens)
+ else:
+ return Transformation(tokens)
+
+
+class BeforeInput(Processor):
+ """
+ Insert tokens before the input.
+
+ :param get_tokens: Callable that takes a
+ :class:`~prompt_toolkit.interface.CommandLineInterface` and returns the
+ list of tokens to be inserted.
+ """
+ def __init__(self, get_tokens):
+ assert callable(get_tokens)
+ self.get_tokens = get_tokens
+
+ def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
+ if lineno == 0:
+ tokens_before = self.get_tokens(cli)
+ tokens = tokens_before + tokens
+
+ shift_position = token_list_len(tokens_before)
+ source_to_display = lambda i: i + shift_position
+ display_to_source = lambda i: i - shift_position
+ else:
+ source_to_display = None
+ display_to_source = None
+
+ return Transformation(tokens, source_to_display=source_to_display,
+ display_to_source=display_to_source)
+
+ @classmethod
+ def static(cls, text, token=Token):
+ """
+ Create a :class:`.BeforeInput` instance that always inserts the same
+ text.
+ """
+ def get_static_tokens(cli):
+ return [(token, text)]
+ return cls(get_static_tokens)
+
+ def __repr__(self):
+ return '%s(get_tokens=%r)' % (
+ self.__class__.__name__, self.get_tokens)
+
+
+class AfterInput(Processor):
+ """
+ Insert tokens after the input.
+
+ :param get_tokens: Callable that takes a
+ :class:`~prompt_toolkit.interface.CommandLineInterface` and returns the
+ list of tokens to be appended.
+ """
+ def __init__(self, get_tokens):
+ assert callable(get_tokens)
+ self.get_tokens = get_tokens
+
+ def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
+ # Insert tokens after the last line.
+ if lineno == document.line_count - 1:
+ return Transformation(tokens=tokens + self.get_tokens(cli))
+ else:
+ return Transformation(tokens=tokens)
+
+ @classmethod
+ def static(cls, text, token=Token):
+ """
+ Create a :class:`.AfterInput` instance that always inserts the same
+ text.
+ """
+ def get_static_tokens(cli):
+ return [(token, text)]
+ return cls(get_static_tokens)
+
+ def __repr__(self):
+ return '%s(get_tokens=%r)' % (
+ self.__class__.__name__, self.get_tokens)
+
+
+class AppendAutoSuggestion(Processor):
+ """
+ Append the auto suggestion to the input.
+ (The user can then press the right arrow the insert the suggestion.)
+
+ :param buffer_name: The name of the buffer from where we should take the
+ auto suggestion. If not given, we take the current buffer.
+ """
+ def __init__(self, buffer_name=None, token=Token.AutoSuggestion):
+ self.buffer_name = buffer_name
+ self.token = token
+
+ def _get_buffer(self, cli):
+ if self.buffer_name:
+ return cli.buffers[self.buffer_name]
+ else:
+ return cli.current_buffer
+
+ def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
+ # Insert tokens after the last line.
+ if lineno == document.line_count - 1:
+ buffer = self._get_buffer(cli)
+
+ if buffer.suggestion and buffer.document.is_cursor_at_the_end:
+ suggestion = buffer.suggestion.text
+ else:
+ suggestion = ''
+
+ return Transformation(tokens=tokens + [(self.token, suggestion)])
+ else:
+ return Transformation(tokens=tokens)
+
+
+class ShowLeadingWhiteSpaceProcessor(Processor):
+ """
+ Make leading whitespace visible.
+
+ :param get_char: Callable that takes a :class:`CommandLineInterface`
+ instance and returns one character.
+ :param token: Token to be used.
+ """
+ def __init__(self, get_char=None, token=Token.LeadingWhiteSpace):
+ assert get_char is None or callable(get_char)
+
+ if get_char is None:
+ def get_char(cli):
+ if '\xb7'.encode(cli.output.encoding(), 'replace') == b'?':
+ return '.'
+ else:
+ return '\xb7'
+
+ self.token = token
+ self.get_char = get_char
+
+ def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
+ # Walk through all te tokens.
+ if tokens and token_list_to_text(tokens).startswith(' '):
+ t = (self.token, self.get_char(cli))
+ tokens = explode_tokens(tokens)
+
+ for i in range(len(tokens)):
+ if tokens[i][1] == ' ':
+ tokens[i] = t
+ else:
+ break
+
+ return Transformation(tokens)
+
+
+class ShowTrailingWhiteSpaceProcessor(Processor):
+ """
+ Make trailing whitespace visible.
+
+ :param get_char: Callable that takes a :class:`CommandLineInterface`
+ instance and returns one character.
+ :param token: Token to be used.
+ """
+ def __init__(self, get_char=None, token=Token.TrailingWhiteSpace):
+ assert get_char is None or callable(get_char)
+
+ if get_char is None:
+ def get_char(cli):
+ if '\xb7'.encode(cli.output.encoding(), 'replace') == b'?':
+ return '.'
+ else:
+ return '\xb7'
+
+ self.token = token
+ self.get_char = get_char
+
+
+ def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
+ if tokens and tokens[-1][1].endswith(' '):
+ t = (self.token, self.get_char(cli))
+ tokens = explode_tokens(tokens)
+
+ # Walk backwards through all te tokens and replace whitespace.
+ for i in range(len(tokens) - 1, -1, -1):
+ char = tokens[i][1]
+ if char == ' ':
+ tokens[i] = t
+ else:
+ break
+
+ return Transformation(tokens)
+
+
+class TabsProcessor(Processor):
+ """
+ Render tabs as spaces (instead of ^I) or make them visible (for instance,
+ by replacing them with dots.)
+
+ :param tabstop: (Integer) Horizontal space taken by a tab.
+ :param get_char1: Callable that takes a `CommandLineInterface` and return a
+ character (text of length one). This one is used for the first space
+ taken by the tab.
+ :param get_char2: Like `get_char1`, but for the rest of the space.
+ """
+ def __init__(self, tabstop=4, get_char1=None, get_char2=None, token=Token.Tab):
+ assert isinstance(tabstop, Integer)
+ assert get_char1 is None or callable(get_char1)
+ assert get_char2 is None or callable(get_char2)
+
+ self.get_char1 = get_char1 or get_char2 or (lambda cli: '|')
+ self.get_char2 = get_char2 or get_char1 or (lambda cli: '\u2508')
+ self.tabstop = tabstop
+ self.token = token
+
+ def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
+ tabstop = int(self.tabstop)
+ token = self.token
+
+ # Create separator for tabs.
+ separator1 = self.get_char1(cli)
+ separator2 = self.get_char2(cli)
+
+ # Transform tokens.
+ tokens = explode_tokens(tokens)
+
+ position_mappings = {}
+ result_tokens = []
+ pos = 0
+
+ for i, token_and_text in enumerate(tokens):
+ position_mappings[i] = pos
+
+ if token_and_text[1] == '\t':
+ # Calculate how many characters we have to insert.
+ count = tabstop - (pos % tabstop)
+ if count == 0:
+ count = tabstop
+
+ # Insert tab.
+ result_tokens.append((token, separator1))
+ result_tokens.append((token, separator2 * (count - 1)))
+ pos += count
+ else:
+ result_tokens.append(token_and_text)
+ pos += 1
+
+ position_mappings[len(tokens)] = pos
+
+ def source_to_display(from_position):
+ " Maps original cursor position to the new one. "
+ return position_mappings[from_position]
+
+ def display_to_source(display_pos):
+ " Maps display cursor position to the original one. "
+ position_mappings_reversed = dict((v, k) for k, v in position_mappings.items())
+
+ while display_pos >= 0:
+ try:
+ return position_mappings_reversed[display_pos]
+ except KeyError:
+ display_pos -= 1
+ return 0
+
+ return Transformation(
+ result_tokens,
+ source_to_display=source_to_display,
+ display_to_source=display_to_source)
+
+
+class ConditionalProcessor(Processor):
+ """
+ Processor that applies another processor, according to a certain condition.
+ Example::
+
+ # Create a function that returns whether or not the processor should
+ # currently be applied.
+ def highlight_enabled(cli):
+ return true_or_false
+
+ # Wrapt it in a `ConditionalProcessor` for usage in a `BufferControl`.
+ BufferControl(input_processors=[
+ ConditionalProcessor(HighlightSearchProcessor(),
+ Condition(highlight_enabled))])
+
+ :param processor: :class:`.Processor` instance.
+ :param filter: :class:`~prompt_toolkit.filters.CLIFilter` instance.
+ """
+ def __init__(self, processor, filter):
+ assert isinstance(processor, Processor)
+
+ self.processor = processor
+ self.filter = to_cli_filter(filter)
+
+ def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
+ # Run processor when enabled.
+ if self.filter(cli):
+ return self.processor.apply_transformation(
+ cli, document, lineno, source_to_display, tokens)
+ else:
+ return Transformation(tokens)
+
+ def has_focus(self, cli):
+ if self.filter(cli):
+ return self.processor.has_focus(cli)
+ else:
+ return False
+
+ def __repr__(self):
+ return '%s(processor=%r, filter=%r)' % (
+ self.__class__.__name__, self.processor, self.filter)
diff --git a/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/prompt.py b/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/prompt.py
new file mode 100644
index 00000000000..7d00ec513e8
--- /dev/null
+++ b/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/prompt.py
@@ -0,0 +1,111 @@
+from __future__ import unicode_literals
+
+from six import text_type
+
+from prompt_toolkit.enums import IncrementalSearchDirection, SEARCH_BUFFER
+from prompt_toolkit.token import Token
+
+from .utils import token_list_len
+from .processors import Processor, Transformation
+
+__all__ = (
+ 'DefaultPrompt',
+)
+
+
+class DefaultPrompt(Processor):
+ """
+ Default prompt. This one shows the 'arg' and reverse search like
+ Bash/readline normally do.
+
+ There are two ways to instantiate a ``DefaultPrompt``. For a prompt
+ with a static message, do for instance::
+
+ prompt = DefaultPrompt.from_message('prompt> ')
+
+ For a dynamic prompt, generated from a token list function::
+
+ def get_tokens(cli):
+ return [(Token.A, 'text'), (Token.B, 'text2')]
+
+ prompt = DefaultPrompt(get_tokens)
+ """
+ def __init__(self, get_tokens):
+ assert callable(get_tokens)
+ self.get_tokens = get_tokens
+
+ @classmethod
+ def from_message(cls, message='> '):
+ """
+ Create a default prompt with a static message text.
+ """
+ assert isinstance(message, text_type)
+
+ def get_message_tokens(cli):
+ return [(Token.Prompt, message)]
+ return cls(get_message_tokens)
+
+ def apply_transformation(self, cli, document, lineno, source_to_display, tokens):
+ # Get text before cursor.
+ if cli.is_searching:
+ before = _get_isearch_tokens(cli)
+
+ elif cli.input_processor.arg is not None:
+ before = _get_arg_tokens(cli)
+
+ else:
+ before = self.get_tokens(cli)
+
+ # Insert before buffer text.
+ shift_position = token_list_len(before)
+
+ # Only show the prompt before the first line. For the following lines,
+ # only indent using spaces.
+ if lineno != 0:
+ before = [(Token.Prompt, ' ' * shift_position)]
+
+ return Transformation(
+ tokens=before + tokens,
+ source_to_display=lambda i: i + shift_position,
+ display_to_source=lambda i: i - shift_position)
+
+ def has_focus(self, cli):
+ # Obtain focus when the CLI is searching.
+
+ # Usually, when using this `DefaultPrompt`, we don't have a
+ # `BufferControl` instance that displays the content of the search
+ # buffer. Instead the search text is displayed before the current text.
+ # So, we can still show the cursor here, while it's actually not this
+ # buffer that's focussed.
+ return cli.is_searching
+
+
+def _get_isearch_tokens(cli):
+ def before():
+ if cli.search_state.direction == IncrementalSearchDirection.BACKWARD:
+ text = 'reverse-i-search'
+ else:
+ text = 'i-search'
+
+ return [(Token.Prompt.Search, '(%s)`' % text)]
+
+ def text():
+ return [(Token.Prompt.Search.Text, cli.buffers[SEARCH_BUFFER].text)]
+
+ def after():
+ return [(Token.Prompt.Search, '`: ')]
+
+ return before() + text() + after()
+
+
+def _get_arg_tokens(cli):
+ """
+ Tokens for the arg-prompt.
+ """
+ arg = cli.input_processor.arg
+
+ return [
+ (Token.Prompt.Arg, '(arg: '),
+ (Token.Prompt.Arg.Text, str(arg)),
+ (Token.Prompt.Arg, ') '),
+ ]
diff --git a/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/screen.py b/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/screen.py
new file mode 100644
index 00000000000..95561f5de76
--- /dev/null
+++ b/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/screen.py
@@ -0,0 +1,151 @@
+from __future__ import unicode_literals
+
+from prompt_toolkit.cache import FastDictCache
+from prompt_toolkit.token import Token
+from prompt_toolkit.utils import get_cwidth
+
+from collections import defaultdict, namedtuple
+
+__all__ = (
+ 'Point',
+ 'Size',
+ 'Screen',
+ 'Char',
+)
+
+
+Point = namedtuple('Point', 'y x')
+Size = namedtuple('Size', 'rows columns')
+
+
+class Char(object):
+ """
+ Represent a single character in a :class:`.Screen`.
+
+ This should be considered immutable.
+ """
+ __slots__ = ('char', 'token', 'width')
+
+ # If we end up having one of these special control sequences in the input string,
+ # we should display them as follows:
+ # Usually this happens after a "quoted insert".
+ display_mappings = {
+ '\x00': '^@', # Control space
+ '\x01': '^A',
+ '\x02': '^B',
+ '\x03': '^C',
+ '\x04': '^D',
+ '\x05': '^E',
+ '\x06': '^F',
+ '\x07': '^G',
+ '\x08': '^H',
+ '\x09': '^I',
+ '\x0a': '^J',
+ '\x0b': '^K',
+ '\x0c': '^L',
+ '\x0d': '^M',
+ '\x0e': '^N',
+ '\x0f': '^O',
+ '\x10': '^P',
+ '\x11': '^Q',
+ '\x12': '^R',
+ '\x13': '^S',
+ '\x14': '^T',
+ '\x15': '^U',
+ '\x16': '^V',
+ '\x17': '^W',
+ '\x18': '^X',
+ '\x19': '^Y',
+ '\x1a': '^Z',
+ '\x1b': '^[', # Escape
+ '\x1c': '^\\',
+ '\x1d': '^]',
+ '\x1f': '^_',
+ '\x7f': '^?', # Backspace
+ }
+
+ def __init__(self, char=' ', token=Token):
+ # If this character has to be displayed otherwise, take that one.
+ char = self.display_mappings.get(char, char)
+
+ self.char = char
+ self.token = token
+
+ # Calculate width. (We always need this, so better to store it directly
+ # as a member for performance.)
+ self.width = get_cwidth(char)
+
+ def __eq__(self, other):
+ return self.char == other.char and self.token == other.token
+
+ def __ne__(self, other):
+ # Not equal: We don't do `not char.__eq__` here, because of the
+ # performance of calling yet another function.
+ return self.char != other.char or self.token != other.token
+
+ def __repr__(self):
+ return '%s(%r, %r)' % (self.__class__.__name__, self.char, self.token)
+
+
+_CHAR_CACHE = FastDictCache(Char, size=1000 * 1000)
+Transparent = Token.Transparent
+
+
+class Screen(object):
+ """
+ Two dimentional buffer of :class:`.Char` instances.
+ """
+ def __init__(self, default_char=None, initial_width=0, initial_height=0):
+ if default_char is None:
+ default_char = _CHAR_CACHE[' ', Transparent]
+
+ self.data_buffer = defaultdict(lambda: defaultdict(lambda: default_char))
+
+ #: Escape sequences to be injected.
+ self.zero_width_escapes = defaultdict(lambda: defaultdict(lambda: ''))
+
+ #: Position of the cursor.
+ self.cursor_position = Point(y=0, x=0)
+
+ #: Visibility of the cursor.
+ self.show_cursor = True
+
+ #: (Optional) Where to position the menu. E.g. at the start of a completion.
+ #: (We can't use the cursor position, because we don't want the
+ #: completion menu to change its position when we browse through all the
+ #: completions.)
+ self.menu_position = None
+
+ #: Currently used width/height of the screen. This will increase when
+ #: data is written to the screen.
+ self.width = initial_width or 0
+ self.height = initial_height or 0
+
+ def replace_all_tokens(self, token):
+ """
+ For all the characters in the screen. Set the token to the given `token`.
+ """
+ b = self.data_buffer
+
+ for y, row in b.items():
+ for x, char in row.items():
+ b[y][x] = _CHAR_CACHE[char.char, token]
+
+
+class WritePosition(object):
+ def __init__(self, xpos, ypos, width, height, extended_height=None):
+ assert height >= 0
+ assert extended_height is None or extended_height >= 0
+ assert width >= 0
+ # xpos and ypos can be negative. (A float can be partially visible.)
+
+ self.xpos = xpos
+ self.ypos = ypos
+ self.width = width
+ self.height = height
+ self.extended_height = extended_height or height
+
+ def __repr__(self):
+ return '%s(%r, %r, %r, %r, %r)' % (
+ self.__class__.__name__,
+ self.xpos, self.ypos, self.width, self.height, self.extended_height)
diff --git a/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/toolbars.py b/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/toolbars.py
new file mode 100644
index 00000000000..2e77c2fa160
--- /dev/null
+++ b/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/toolbars.py
@@ -0,0 +1,209 @@
+from __future__ import unicode_literals
+
+from ..enums import IncrementalSearchDirection
+
+from .processors import BeforeInput
+
+from .lexers import SimpleLexer
+from .dimension import LayoutDimension
+from .controls import BufferControl, TokenListControl, UIControl, UIContent
+from .containers import Window, ConditionalContainer
+from .screen import Char
+from .utils import token_list_len
+from prompt_toolkit.enums import SEARCH_BUFFER, SYSTEM_BUFFER
+from prompt_toolkit.filters import HasFocus, HasArg, HasCompletions, HasValidationError, HasSearch, Always, IsDone
+from prompt_toolkit.token import Token
+
+__all__ = (
+ 'TokenListToolbar',
+ 'ArgToolbar',
+ 'CompletionsToolbar',
+ 'SearchToolbar',
+ 'SystemToolbar',
+ 'ValidationToolbar',
+)
+
+
+class TokenListToolbar(ConditionalContainer):
+ def __init__(self, get_tokens, filter=Always(), **kw):
+ super(TokenListToolbar, self).__init__(
+ content=Window(
+ TokenListControl(get_tokens, **kw),
+ height=LayoutDimension.exact(1)),
+ filter=filter)
+
+
+class SystemToolbarControl(BufferControl):
+ def __init__(self):
+ token = Token.Toolbar.System
+
+ super(SystemToolbarControl, self).__init__(
+ buffer_name=SYSTEM_BUFFER,
+ default_char=Char(token=token),
+ lexer=SimpleLexer(token=token.Text),
+ input_processors=[BeforeInput.static('Shell command: ', token)],)
+
+
+class SystemToolbar(ConditionalContainer):
+ def __init__(self):
+ super(SystemToolbar, self).__init__(
+ content=Window(
+ SystemToolbarControl(),
+ height=LayoutDimension.exact(1)),
+ filter=HasFocus(SYSTEM_BUFFER) & ~IsDone())
+
+
+class ArgToolbarControl(TokenListControl):
+ def __init__(self):
+ def get_tokens(cli):
+ arg = cli.input_processor.arg
+ if arg == '-':
+ arg = '-1'
+
+ return [
+ (Token.Toolbar.Arg, 'Repeat: '),
+ (Token.Toolbar.Arg.Text, arg),
+ ]
+
+ super(ArgToolbarControl, self).__init__(get_tokens)
+
+
+class ArgToolbar(ConditionalContainer):
+ def __init__(self):
+ super(ArgToolbar, self).__init__(
+ content=Window(
+ ArgToolbarControl(),
+ height=LayoutDimension.exact(1)),
+ filter=HasArg())
+
+
+class SearchToolbarControl(BufferControl):
+ """
+ :param vi_mode: Display '/' and '?' instead of I-search.
+ """
+ def __init__(self, vi_mode=False):
+ token = Token.Toolbar.Search
+
+ def get_before_input(cli):
+ if not cli.is_searching:
+ text = ''
+ elif cli.search_state.direction == IncrementalSearchDirection.BACKWARD:
+ text = ('?' if vi_mode else 'I-search backward: ')
+ else:
+ text = ('/' if vi_mode else 'I-search: ')
+
+ return [(token, text)]
+
+ super(SearchToolbarControl, self).__init__(
+ buffer_name=SEARCH_BUFFER,
+ input_processors=[BeforeInput(get_before_input)],
+ default_char=Char(token=token),
+ lexer=SimpleLexer(token=token.Text))
+
+
+class SearchToolbar(ConditionalContainer):
+ def __init__(self, vi_mode=False):
+ super(SearchToolbar, self).__init__(
+ content=Window(
+ SearchToolbarControl(vi_mode=vi_mode),
+ height=LayoutDimension.exact(1)),
+ filter=HasSearch() & ~IsDone())
+
+
+class CompletionsToolbarControl(UIControl):
+ token = Token.Toolbar.Completions
+
+ def create_content(self, cli, width, height):
+ complete_state = cli.current_buffer.complete_state
+ if complete_state:
+ completions = complete_state.current_completions
+ index = complete_state.complete_index # Can be None!
+
+ # Width of the completions without the left/right arrows in the margins.
+ content_width = width - 6
+
+ # Booleans indicating whether we stripped from the left/right
+ cut_left = False
+ cut_right = False
+
+ # Create Menu content.
+ tokens = []
+
+ for i, c in enumerate(completions):
+ # When there is no more place for the next completion
+ if token_list_len(tokens) + len(c.display) >= content_width:
+ # If the current one was not yet displayed, page to the next sequence.
+ if i <= (index or 0):
+ tokens = []
+ cut_left = True
+ # If the current one is visible, stop here.
+ else:
+ cut_right = True
+ break
+
+ tokens.append((self.token.Completion.Current if i == index else self.token.Completion, c.display))
+ tokens.append((self.token, ' '))
+
+ # Extend/strip until the content width.
+ tokens.append((self.token, ' ' * (content_width - token_list_len(tokens))))
+ tokens = tokens[:content_width]
+
+ # Return tokens
+ all_tokens = [
+ (self.token, ' '),
+ (self.token.Arrow, '<' if cut_left else ' '),
+ (self.token, ' '),
+ ] + tokens + [
+ (self.token, ' '),
+ (self.token.Arrow, '>' if cut_right else ' '),
+ (self.token, ' '),
+ ]
+ else:
+ all_tokens = []
+
+ def get_line(i):
+ return all_tokens
+
+ return UIContent(get_line=get_line, line_count=1)
+
+
+class CompletionsToolbar(ConditionalContainer):
+ def __init__(self, extra_filter=Always()):
+ super(CompletionsToolbar, self).__init__(
+ content=Window(
+ CompletionsToolbarControl(),
+ height=LayoutDimension.exact(1)),
+ filter=HasCompletions() & ~IsDone() & extra_filter)
+
+
+class ValidationToolbarControl(TokenListControl):
+ def __init__(self, show_position=False):
+ token = Token.Toolbar.Validation
+
+ def get_tokens(cli):
+ buffer = cli.current_buffer
+
+ if buffer.validation_error:
+ row, column = buffer.document.translate_index_to_position(
+ buffer.validation_error.cursor_position)
+
+ if show_position:
+ text = '%s (line=%s column=%s)' % (
+ buffer.validation_error.message, row + 1, column + 1)
+ else:
+ text = buffer.validation_error.message
+
+ return [(token, text)]
+ else:
+ return []
+
+ super(ValidationToolbarControl, self).__init__(get_tokens)
+
+
+class ValidationToolbar(ConditionalContainer):
+ def __init__(self, show_position=False):
+ super(ValidationToolbar, self).__init__(
+ content=Window(
+ ValidationToolbarControl(show_position=show_position),
+ height=LayoutDimension.exact(1)),
+ filter=HasValidationError() & ~IsDone())
diff --git a/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/utils.py b/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/utils.py
new file mode 100644
index 00000000000..a4fb7ed0f5b
--- /dev/null
+++ b/contrib/python/prompt-toolkit/py2/prompt_toolkit/layout/utils.py
@@ -0,0 +1,181 @@
+from __future__ import unicode_literals
+
+from prompt_toolkit.utils import get_cwidth
+from prompt_toolkit.token import Token
+
+__all__ = (
+ 'token_list_len',
+ 'token_list_width',
+ 'token_list_to_text',
+ 'explode_tokens',
+ 'split_lines',
+ 'find_window_for_buffer_name',
+)
+
+
+def token_list_len(tokenlist):
+ """
+ Return the amount of characters in this token list.
+
+ :param tokenlist: List of (token, text) or (token, text, mouse_handler)
+ tuples.
+ """
+ ZeroWidthEscape = Token.ZeroWidthEscape
+ return sum(len(item[1]) for item in tokenlist if item[0] != ZeroWidthEscape)
+
+
+def token_list_width(tokenlist):
+ """
+ Return the character width of this token list.
+ (Take double width characters into account.)
+
+ :param tokenlist: List of (token, text) or (token, text, mouse_handler)
+ tuples.
+ """
+ ZeroWidthEscape = Token.ZeroWidthEscape
+ return sum(get_cwidth(c) for item in tokenlist for c in item[1] if item[0] != ZeroWidthEscape)
+
+
+def token_list_to_text(tokenlist):
+ """
+ Concatenate all the text parts again.
+ """
+ ZeroWidthEscape = Token.ZeroWidthEscape
+ return ''.join(item[1] for item in tokenlist if item[0] != ZeroWidthEscape)
+
+
+def iter_token_lines(tokenlist):
+ """
+ Iterator that yields tokenlists for each line.
+ """
+ line = []
+ for token, c in explode_tokens(tokenlist):
+ line.append((token, c))
+
+ if c == '\n':
+ yield line
+ line = []
+
+ yield line
+
+
+def split_lines(tokenlist):
+ """
+ Take a single list of (Token, text) tuples and yield one such list for each
+ line. Just like str.split, this will yield at least one item.
+
+ :param tokenlist: List of (token, text) or (token, text, mouse_handler)
+ tuples.
+ """
+ line = []
+
+ for item in tokenlist:
+ # For (token, text) tuples.
+ if len(item) == 2:
+ token, string = item
+ parts = string.split('\n')
+
+ for part in parts[:-1]:
+ if part:
+ line.append((token, part))
+ yield line
+ line = []
+
+ line.append((token, parts[-1]))
+ # Note that parts[-1] can be empty, and that's fine. It happens
+ # in the case of [(Token.SetCursorPosition, '')].
+
+ # For (token, text, mouse_handler) tuples.
+ # I know, partly copy/paste, but understandable and more efficient
+ # than many tests.
+ else:
+ token, string, mouse_handler = item
+ parts = string.split('\n')
+
+ for part in parts[:-1]:
+ if part:
+ line.append((token, part, mouse_handler))
+ yield line
+ line = []
+
+ line.append((token, parts[-1], mouse_handler))
+
+ # Always yield the last line, even when this is an empty line. This ensures
+ # that when `tokenlist` ends with a newline character, an additional empty
+ # line is yielded. (Otherwise, there's no way to differentiate between the
+ # cases where `tokenlist` does and doesn't end with a newline.)
+ yield line
+
+
+class _ExplodedList(list):
+ """
+ Wrapper around a list, that marks it as 'exploded'.
+
+ As soon as items are added or the list is extended, the new items are
+ automatically exploded as well.
+ """
+ def __init__(self, *a, **kw):
+ super(_ExplodedList, self).__init__(*a, **kw)
+ self.exploded = True
+
+ def append(self, item):
+ self.extend([item])
+
+ def extend(self, lst):
+ super(_ExplodedList, self).extend(explode_tokens(lst))
+
+ def insert(self, index, item):
+ raise NotImplementedError # TODO
+
+ # TODO: When creating a copy() or [:], return also an _ExplodedList.
+
+ def __setitem__(self, index, value):
+ """
+ Ensure that when `(Token, 'long string')` is set, the string will be
+ exploded.
+ """
+ if not isinstance(index, slice):
+ index = slice(index, index + 1)
+ value = explode_tokens([value])
+ super(_ExplodedList, self).__setitem__(index, value)
+
+
+def explode_tokens(tokenlist):
+ """
+ Turn a list of (token, text) tuples into another list where each string is
+ exactly one character.
+
+ It should be fine to call this function several times. Calling this on a
+ list that is already exploded, is a null operation.
+
+ :param tokenlist: List of (token, text) tuples.
+ """
+ # When the tokenlist is already exploded, don't explode again.
+ if getattr(tokenlist, 'exploded', False):
+ return tokenlist
+
+ result = []
+
+ for token, string in tokenlist:
+ for c in string:
+ result.append((token, c))
+
+ return _ExplodedList(result)
+
+
+def find_window_for_buffer_name(cli, buffer_name):
+ """
+ Look for a :class:`~prompt_toolkit.layout.containers.Window` in the Layout
+ that contains the :class:`~prompt_toolkit.layout.controls.BufferControl`
+ for the given buffer and return it. If no such Window is found, return None.
+ """
+ from prompt_toolkit.interface import CommandLineInterface
+ assert isinstance(cli, CommandLineInterface)
+
+ from .containers import Window
+ from .controls import BufferControl
+
+ for l in cli.layout.walk(cli):
+ if isinstance(l, Window) and isinstance(l.content, BufferControl):
+ if l.content.buffer_name == buffer_name:
+ return l