You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
835 lines
30 KiB
835 lines
30 KiB
6 years ago
|
"""
|
||
|
Processors are little transformation blocks that transform the fragments list
|
||
|
from a buffer before the BufferControl will render it to the screen.
|
||
|
|
||
|
They can insert fragments before or after, or highlight fragments by replacing the
|
||
|
fragment types.
|
||
|
"""
|
||
|
from __future__ import unicode_literals
|
||
|
from abc import ABCMeta, abstractmethod
|
||
|
from six import with_metaclass, text_type
|
||
|
from six.moves import range
|
||
|
|
||
|
from prompt_toolkit.application.current import get_app
|
||
|
from prompt_toolkit.cache import SimpleCache
|
||
|
from prompt_toolkit.document import Document
|
||
|
from prompt_toolkit.filters import to_filter, vi_insert_multiple_mode
|
||
|
from prompt_toolkit.formatted_text import to_formatted_text
|
||
|
from prompt_toolkit.formatted_text.utils import fragment_list_len, fragment_list_to_text
|
||
|
from prompt_toolkit.search import SearchDirection
|
||
|
from prompt_toolkit.utils import to_int, to_str
|
||
|
|
||
|
from .utils import explode_text_fragments
|
||
|
|
||
|
import re
|
||
|
|
||
|
__all__ = [
|
||
|
'Processor',
|
||
|
'TransformationInput',
|
||
|
'Transformation',
|
||
|
|
||
|
'DummyProcessor',
|
||
|
'HighlightSearchProcessor',
|
||
|
'HighlightIncrementalSearchProcessor',
|
||
|
'HighlightSelectionProcessor',
|
||
|
'PasswordProcessor',
|
||
|
'HighlightMatchingBracketProcessor',
|
||
|
'DisplayMultipleCursors',
|
||
|
'BeforeInput',
|
||
|
'ShowArg',
|
||
|
'AfterInput',
|
||
|
'AppendAutoSuggestion',
|
||
|
'ConditionalProcessor',
|
||
|
'ShowLeadingWhiteSpaceProcessor',
|
||
|
'ShowTrailingWhiteSpaceProcessor',
|
||
|
'TabsProcessor',
|
||
|
'ReverseSearchProcessor',
|
||
|
'DynamicProcessor',
|
||
|
'merge_processors',
|
||
|
]
|
||
|
|
||
|
|
||
|
class Processor(with_metaclass(ABCMeta, object)):
|
||
|
"""
|
||
|
Manipulate the fragments for a given line in a
|
||
|
:class:`~prompt_toolkit.layout.controls.BufferControl`.
|
||
|
"""
|
||
|
@abstractmethod
|
||
|
def apply_transformation(self, transformation_input):
|
||
|
"""
|
||
|
Apply transformation. Returns a :class:`.Transformation` instance.
|
||
|
|
||
|
:param transformation_input: :class:`.TransformationInput` object.
|
||
|
"""
|
||
|
return Transformation(transformation_input.fragments)
|
||
|
|
||
|
|
||
|
class TransformationInput(object):
|
||
|
"""
|
||
|
:param control: :class:`.BufferControl` 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
|
||
|
`fragments` for any position in the source string. (This takes
|
||
|
previous processors into account.)
|
||
|
:param fragments: List of fragments that we can transform. (Received from the
|
||
|
previous processor.)
|
||
|
"""
|
||
|
def __init__(self, buffer_control, document, lineno,
|
||
|
source_to_display, fragments, width, height):
|
||
|
self.buffer_control = buffer_control
|
||
|
self.document = document
|
||
|
self.lineno = lineno
|
||
|
self.source_to_display = source_to_display
|
||
|
self.fragments = fragments
|
||
|
self.width = width
|
||
|
self.height = height
|
||
|
|
||
|
def unpack(self):
|
||
|
return (self.buffer_control, self.document, self.lineno,
|
||
|
self.source_to_display, self.fragments, self.width, self.height)
|
||
|
|
||
|
|
||
|
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 `fragments`!
|
||
|
|
||
|
:param fragments: The transformed fragments. 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, fragments, source_to_display=None, display_to_source=None):
|
||
|
self.fragments = fragments
|
||
|
self.source_to_display = source_to_display or (lambda i: i)
|
||
|
self.display_to_source = display_to_source or (lambda i: i)
|
||
|
|
||
|
|
||
|
class DummyProcessor(Processor):
|
||
|
"""
|
||
|
A `Processor` that doesn't do anything.
|
||
|
"""
|
||
|
def apply_transformation(self, transformation_input):
|
||
|
return Transformation(transformation_input.fragments)
|
||
|
|
||
|
|
||
|
class HighlightSearchProcessor(Processor):
|
||
|
"""
|
||
|
Processor that highlights search matches in the document.
|
||
|
Note that this doesn't support multiline search matches yet.
|
||
|
|
||
|
The style classes 'search' and 'search.current' will be applied to the
|
||
|
content.
|
||
|
"""
|
||
|
_classname = 'search'
|
||
|
_classname_current = 'search.current'
|
||
|
|
||
|
def _get_search_text(self, buffer_control):
|
||
|
"""
|
||
|
The text we are searching for.
|
||
|
"""
|
||
|
return buffer_control.search_state.text
|
||
|
|
||
|
def apply_transformation(self, transformation_input):
|
||
|
buffer_control, document, lineno, source_to_display, fragments, _, _ = transformation_input.unpack()
|
||
|
|
||
|
search_text = self._get_search_text(buffer_control)
|
||
|
searchmatch_fragment = ' class:%s ' % (self._classname, )
|
||
|
searchmatch_current_fragment = ' class:%s ' % (self._classname_current, )
|
||
|
|
||
|
if search_text and not get_app().is_done:
|
||
|
# For each search match, replace the style string.
|
||
|
line_text = fragment_list_to_text(fragments)
|
||
|
fragments = explode_text_fragments(fragments)
|
||
|
|
||
|
if buffer_control.search_state.ignore_case():
|
||
|
flags = re.IGNORECASE
|
||
|
else:
|
||
|
flags = 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_fragment, text = fragments[i]
|
||
|
if on_cursor:
|
||
|
fragments[i] = (old_fragment + searchmatch_current_fragment, fragments[i][1])
|
||
|
else:
|
||
|
fragments[i] = (old_fragment + searchmatch_fragment, fragments[i][1])
|
||
|
|
||
|
return Transformation(fragments)
|
||
|
|
||
|
|
||
|
class HighlightIncrementalSearchProcessor(HighlightSearchProcessor):
|
||
|
"""
|
||
|
Highlight the search terms that are used for highlighting the incremental
|
||
|
search. The style class 'incsearch' will be applied to the content.
|
||
|
|
||
|
Important: this requires the `preview_search=True` flag to be set for the
|
||
|
`BufferControl`. Otherwise, the cursor position won't be set to the search
|
||
|
match while searching, and nothing happens.
|
||
|
"""
|
||
|
_classname = 'incsearch'
|
||
|
_classname_current = 'incsearch.current'
|
||
|
|
||
|
def _get_search_text(self, buffer_control):
|
||
|
"""
|
||
|
The text we are searching for.
|
||
|
"""
|
||
|
# When the search buffer has focus, take that text.
|
||
|
search_buffer = buffer_control.search_buffer
|
||
|
if search_buffer is not None and search_buffer.text:
|
||
|
return search_buffer.text
|
||
|
|
||
|
|
||
|
class HighlightSelectionProcessor(Processor):
|
||
|
"""
|
||
|
Processor that highlights the selection in the document.
|
||
|
"""
|
||
|
def apply_transformation(self, transformation_input):
|
||
|
buffer_control, document, lineno, source_to_display, fragments, _, _ = transformation_input.unpack()
|
||
|
|
||
|
selected_fragment = ' class:selected '
|
||
|
|
||
|
# 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)
|
||
|
|
||
|
fragments = explode_text_fragments(fragments)
|
||
|
|
||
|
if from_ == 0 and to == 0 and len(fragments) == 0:
|
||
|
# When this is an empty line, insert a space in order to
|
||
|
# visualise the selection.
|
||
|
return Transformation([(selected_fragment, ' ')])
|
||
|
else:
|
||
|
for i in range(from_, to):
|
||
|
if i < len(fragments):
|
||
|
old_fragment, old_text = fragments[i]
|
||
|
fragments[i] = (old_fragment + selected_fragment, old_text)
|
||
|
elif i == len(fragments):
|
||
|
fragments.append((selected_fragment, ' '))
|
||
|
|
||
|
return Transformation(fragments)
|
||
|
|
||
|
|
||
|
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, ti):
|
||
|
fragments = [(style, self.char * len(text)) for style, text in ti.fragments]
|
||
|
return Transformation(fragments)
|
||
|
|
||
|
|
||
|
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, transformation_input):
|
||
|
buffer_control, document, lineno, source_to_display, fragments, _, _ = transformation_input.unpack()
|
||
|
|
||
|
# When the application is in the 'done' state, don't highlight.
|
||
|
if get_app().is_done:
|
||
|
return Transformation(fragments)
|
||
|
|
||
|
# Get the highlight positions.
|
||
|
key = (get_app().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)
|
||
|
fragments = explode_text_fragments(fragments)
|
||
|
style, text = fragments[col]
|
||
|
|
||
|
if col == document.cursor_position_col:
|
||
|
style += ' class:matching-bracket.cursor '
|
||
|
else:
|
||
|
style += ' class:matching-bracket.other '
|
||
|
|
||
|
fragments[col] = (style, text)
|
||
|
|
||
|
return Transformation(fragments)
|
||
|
|
||
|
|
||
|
class DisplayMultipleCursors(Processor):
|
||
|
"""
|
||
|
When we're in Vi block insert mode, display all the cursors.
|
||
|
"""
|
||
|
def apply_transformation(self, transformation_input):
|
||
|
buffer_control, document, lineno, source_to_display, fragments, _, _ = transformation_input.unpack()
|
||
|
|
||
|
buff = buffer_control.buffer
|
||
|
|
||
|
if vi_insert_multiple_mode():
|
||
|
cursor_positions = buff.multiple_cursor_positions
|
||
|
fragments = explode_text_fragments(fragments)
|
||
|
|
||
|
# 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])
|
||
|
|
||
|
fragment_suffix = ' class:multiple-cursors'
|
||
|
|
||
|
for p in cursor_positions:
|
||
|
if start_pos <= p <= end_pos:
|
||
|
column = source_to_display(p - start_pos)
|
||
|
|
||
|
# Replace fragment.
|
||
|
try:
|
||
|
style, text = fragments[column]
|
||
|
except IndexError:
|
||
|
# Cursor needs to be displayed after the current text.
|
||
|
fragments.append((fragment_suffix, ' '))
|
||
|
else:
|
||
|
style += fragment_suffix
|
||
|
fragments[column] = (style, text)
|
||
|
|
||
|
return Transformation(fragments)
|
||
|
else:
|
||
|
return Transformation(fragments)
|
||
|
|
||
|
|
||
|
class BeforeInput(Processor):
|
||
|
"""
|
||
|
Insert text before the input.
|
||
|
|
||
|
:param text: This can be either plain text or formatted text
|
||
|
(or a callable that returns any of those).
|
||
|
:param style: style to be applied to this prompt/prefix.
|
||
|
"""
|
||
|
def __init__(self, text, style=''):
|
||
|
self.text = text
|
||
|
self.style = style
|
||
|
|
||
|
def apply_transformation(self, ti):
|
||
|
if ti.lineno == 0:
|
||
|
# Get fragments.
|
||
|
fragments_before = to_formatted_text(self.text, self.style)
|
||
|
fragments = fragments_before + ti.fragments
|
||
|
|
||
|
shift_position = fragment_list_len(fragments_before)
|
||
|
source_to_display = lambda i: i + shift_position
|
||
|
display_to_source = lambda i: i - shift_position
|
||
|
else:
|
||
|
fragments = ti.fragments
|
||
|
source_to_display = None
|
||
|
display_to_source = None
|
||
|
|
||
|
return Transformation(fragments, source_to_display=source_to_display,
|
||
|
display_to_source=display_to_source)
|
||
|
|
||
|
def __repr__(self):
|
||
|
return 'BeforeInput(%r, %r)' % (self.text, self.style)
|
||
|
|
||
|
|
||
|
class ShowArg(BeforeInput):
|
||
|
"""
|
||
|
Display the 'arg' in front of the input.
|
||
|
|
||
|
This was used by the `PromptSession`, but now it uses the
|
||
|
`Window.get_line_prefix` function instead.
|
||
|
"""
|
||
|
def __init__(self):
|
||
|
super(ShowArg, self).__init__(self._get_text_fragments)
|
||
|
|
||
|
def _get_text_fragments(self):
|
||
|
app = get_app()
|
||
|
if app.key_processor.arg is None:
|
||
|
return []
|
||
|
else:
|
||
|
arg = app.key_processor.arg
|
||
|
|
||
|
return [
|
||
|
('class:prompt.arg', '(arg: '),
|
||
|
('class:prompt.arg.text', str(arg)),
|
||
|
('class:prompt.arg', ') '),
|
||
|
]
|
||
|
|
||
|
def __repr__(self):
|
||
|
return 'ShowArg()'
|
||
|
|
||
|
|
||
|
class AfterInput(Processor):
|
||
|
"""
|
||
|
Insert text after the input.
|
||
|
|
||
|
:param text: This can be either plain text or formatted text
|
||
|
(or a callable that returns any of those).
|
||
|
:param style: style to be applied to this prompt/prefix.
|
||
|
"""
|
||
|
def __init__(self, text, style=''):
|
||
|
self.text = text
|
||
|
self.style = style
|
||
|
|
||
|
def apply_transformation(self, ti):
|
||
|
# Insert fragments after the last line.
|
||
|
if ti.lineno == ti.document.line_count - 1:
|
||
|
# Get fragments.
|
||
|
fragments_after = to_formatted_text(self.text, self.style)
|
||
|
return Transformation(fragments=ti.fragments + fragments_after)
|
||
|
else:
|
||
|
return Transformation(fragments=ti.fragments)
|
||
|
|
||
|
def __repr__(self):
|
||
|
return '%s(%r, style=%r)' % (
|
||
|
self.__class__.__name__, self.text, self.style)
|
||
|
|
||
|
|
||
|
class AppendAutoSuggestion(Processor):
|
||
|
"""
|
||
|
Append the auto suggestion to the input.
|
||
|
(The user can then press the right arrow the insert the suggestion.)
|
||
|
"""
|
||
|
def __init__(self, style='class:auto-suggestion'):
|
||
|
self.style = style
|
||
|
|
||
|
def apply_transformation(self, ti):
|
||
|
# Insert fragments after the last line.
|
||
|
if ti.lineno == ti.document.line_count - 1:
|
||
|
buffer = ti.buffer_control.buffer
|
||
|
|
||
|
if buffer.suggestion and ti.document.is_cursor_at_the_end:
|
||
|
suggestion = buffer.suggestion.text
|
||
|
else:
|
||
|
suggestion = ''
|
||
|
|
||
|
return Transformation(fragments=ti.fragments + [(self.style, suggestion)])
|
||
|
else:
|
||
|
return Transformation(fragments=ti.fragments)
|
||
|
|
||
|
|
||
|
class ShowLeadingWhiteSpaceProcessor(Processor):
|
||
|
"""
|
||
|
Make leading whitespace visible.
|
||
|
|
||
|
:param get_char: Callable that returns one character.
|
||
|
"""
|
||
|
def __init__(self, get_char=None, style='class:leading-whitespace'):
|
||
|
assert get_char is None or callable(get_char)
|
||
|
|
||
|
if get_char is None:
|
||
|
def get_char():
|
||
|
if '\xb7'.encode(get_app().output.encoding(), 'replace') == b'?':
|
||
|
return '.'
|
||
|
else:
|
||
|
return '\xb7'
|
||
|
|
||
|
self.style = style
|
||
|
self.get_char = get_char
|
||
|
|
||
|
def apply_transformation(self, ti):
|
||
|
fragments = ti.fragments
|
||
|
|
||
|
# Walk through all te fragments.
|
||
|
if fragments and fragment_list_to_text(fragments).startswith(' '):
|
||
|
t = (self.style, self.get_char())
|
||
|
fragments = explode_text_fragments(fragments)
|
||
|
|
||
|
for i in range(len(fragments)):
|
||
|
if fragments[i][1] == ' ':
|
||
|
fragments[i] = t
|
||
|
else:
|
||
|
break
|
||
|
|
||
|
return Transformation(fragments)
|
||
|
|
||
|
|
||
|
class ShowTrailingWhiteSpaceProcessor(Processor):
|
||
|
"""
|
||
|
Make trailing whitespace visible.
|
||
|
|
||
|
:param get_char: Callable that returns one character.
|
||
|
"""
|
||
|
def __init__(self, get_char=None, style='class:training-whitespace'):
|
||
|
assert get_char is None or callable(get_char)
|
||
|
|
||
|
if get_char is None:
|
||
|
def get_char():
|
||
|
if '\xb7'.encode(get_app().output.encoding(), 'replace') == b'?':
|
||
|
return '.'
|
||
|
else:
|
||
|
return '\xb7'
|
||
|
|
||
|
self.style = style
|
||
|
self.get_char = get_char
|
||
|
|
||
|
def apply_transformation(self, ti):
|
||
|
fragments = ti.fragments
|
||
|
|
||
|
if fragments and fragments[-1][1].endswith(' '):
|
||
|
t = (self.style, self.get_char())
|
||
|
fragments = explode_text_fragments(fragments)
|
||
|
|
||
|
# Walk backwards through all te fragments and replace whitespace.
|
||
|
for i in range(len(fragments) - 1, -1, -1):
|
||
|
char = fragments[i][1]
|
||
|
if char == ' ':
|
||
|
fragments[i] = t
|
||
|
else:
|
||
|
break
|
||
|
|
||
|
return Transformation(fragments)
|
||
|
|
||
|
|
||
|
class TabsProcessor(Processor):
|
||
|
"""
|
||
|
Render tabs as spaces (instead of ^I) or make them visible (for instance,
|
||
|
by replacing them with dots.)
|
||
|
|
||
|
:param tabstop: Horizontal space taken by a tab. (`int` or callable that
|
||
|
returns an `int`).
|
||
|
:param char1: Character or callable that returns a character (text of
|
||
|
length one). This one is used for the first space taken by the tab.
|
||
|
:param char2: Like `char1`, but for the rest of the space.
|
||
|
"""
|
||
|
def __init__(self, tabstop=4, char1='|', char2='\u2508', style='class:tab'):
|
||
|
assert isinstance(tabstop, int) or callable(tabstop)
|
||
|
assert callable(char1) or isinstance(char1, text_type)
|
||
|
assert callable(char2) or isinstance(char2, text_type)
|
||
|
|
||
|
self.char1 = char1
|
||
|
self.char2 = char2
|
||
|
self.tabstop = tabstop
|
||
|
self.style = style
|
||
|
|
||
|
def apply_transformation(self, ti):
|
||
|
tabstop = to_int(self.tabstop)
|
||
|
style = self.style
|
||
|
|
||
|
# Create separator for tabs.
|
||
|
separator1 = to_str(self.char1)
|
||
|
separator2 = to_str(self.char2)
|
||
|
|
||
|
# Transform fragments.
|
||
|
fragments = explode_text_fragments(ti.fragments)
|
||
|
|
||
|
position_mappings = {}
|
||
|
result_fragments = []
|
||
|
pos = 0
|
||
|
|
||
|
for i, fragment_and_text in enumerate(fragments):
|
||
|
position_mappings[i] = pos
|
||
|
|
||
|
if fragment_and_text[1] == '\t':
|
||
|
# Calculate how many characters we have to insert.
|
||
|
count = tabstop - (pos % tabstop)
|
||
|
if count == 0:
|
||
|
count = tabstop
|
||
|
|
||
|
# Insert tab.
|
||
|
result_fragments.append((style, separator1))
|
||
|
result_fragments.append((style, separator2 * (count - 1)))
|
||
|
pos += count
|
||
|
else:
|
||
|
result_fragments.append(fragment_and_text)
|
||
|
pos += 1
|
||
|
|
||
|
position_mappings[len(fragments)] = pos
|
||
|
# Add `pos+1` to mapping, because the cursor can be right after the
|
||
|
# line as well.
|
||
|
position_mappings[len(fragments) + 1] = pos + 1
|
||
|
|
||
|
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_fragments,
|
||
|
source_to_display=source_to_display,
|
||
|
display_to_source=display_to_source)
|
||
|
|
||
|
|
||
|
class ReverseSearchProcessor(Processor):
|
||
|
"""
|
||
|
Process to display the "(reverse-i-search)`...`:..." stuff around
|
||
|
the search buffer.
|
||
|
|
||
|
Note: This processor is meant to be applied to the BufferControl that
|
||
|
contains the search buffer, it's not meant for the original input.
|
||
|
"""
|
||
|
_excluded_input_processors = [
|
||
|
HighlightSearchProcessor,
|
||
|
HighlightSelectionProcessor,
|
||
|
BeforeInput,
|
||
|
AfterInput,
|
||
|
]
|
||
|
|
||
|
def _get_main_buffer(self, buffer_control):
|
||
|
from prompt_toolkit.layout.controls import BufferControl
|
||
|
prev_control = get_app().layout.search_target_buffer_control
|
||
|
if isinstance(prev_control, BufferControl) and \
|
||
|
prev_control.search_buffer_control == buffer_control:
|
||
|
return prev_control
|
||
|
return None
|
||
|
|
||
|
def _content(self, main_control, ti):
|
||
|
from prompt_toolkit.layout.controls import BufferControl
|
||
|
|
||
|
# Emulate the BufferControl through which we are searching.
|
||
|
# For this we filter out some of the input processors.
|
||
|
excluded_processors = tuple(self._excluded_input_processors)
|
||
|
|
||
|
def filter_processor(item):
|
||
|
""" Filter processors from the main control that we want to disable
|
||
|
here. This returns either an accepted processor or None. """
|
||
|
# For a `_MergedProcessor`, check each individual processor, recursively.
|
||
|
if isinstance(item, _MergedProcessor):
|
||
|
accepted_processors = [filter_processor(p) for p in item.processors]
|
||
|
accepted_processors = [p for p in accepted_processors if p is not None]
|
||
|
|
||
|
if len(accepted_processors) > 1:
|
||
|
return _MergedProcessor(accepted_processors)
|
||
|
elif accepted_processors == 1:
|
||
|
return accepted_processors[0]
|
||
|
|
||
|
# For a `ConditionalProcessor`, check the body.
|
||
|
elif isinstance(item, ConditionalProcessor):
|
||
|
p = filter_processor(item.processor)
|
||
|
if p:
|
||
|
return ConditionalProcessor(p, item.filter)
|
||
|
|
||
|
# Otherwise, check the processor itself.
|
||
|
else:
|
||
|
if not isinstance(item, excluded_processors):
|
||
|
return item
|
||
|
|
||
|
filtered_processor = filter_processor(
|
||
|
merge_processors(main_control.input_processors or []))
|
||
|
highlight_processor = HighlightIncrementalSearchProcessor()
|
||
|
|
||
|
if filtered_processor:
|
||
|
new_processors = [filtered_processor, highlight_processor]
|
||
|
else:
|
||
|
new_processors = [highlight_processor]
|
||
|
|
||
|
buffer_control = BufferControl(
|
||
|
buffer=main_control.buffer,
|
||
|
input_processors=new_processors,
|
||
|
include_default_input_processors=False,
|
||
|
lexer=main_control.lexer,
|
||
|
preview_search=True,
|
||
|
search_buffer_control=ti.buffer_control)
|
||
|
|
||
|
return buffer_control.create_content(ti.width, ti.height, preview_search=True)
|
||
|
|
||
|
def apply_transformation(self, ti):
|
||
|
main_control = self._get_main_buffer(ti.buffer_control)
|
||
|
|
||
|
if ti.lineno == 0 and main_control:
|
||
|
content = self._content(main_control, ti)
|
||
|
|
||
|
# Get the line from the original document for this search.
|
||
|
line_fragments = content.get_line(content.cursor_position.y)
|
||
|
|
||
|
if main_control.search_state.direction == SearchDirection.FORWARD:
|
||
|
direction_text = 'i-search'
|
||
|
else:
|
||
|
direction_text = 'reverse-i-search'
|
||
|
|
||
|
fragments_before = [
|
||
|
('class:prompt.search', '('),
|
||
|
('class:prompt.search', direction_text),
|
||
|
('class:prompt.search', ')`'),
|
||
|
]
|
||
|
|
||
|
fragments = fragments_before + [
|
||
|
('class:prompt.search.text', fragment_list_to_text(ti.fragments)),
|
||
|
('', "': "),
|
||
|
] + line_fragments
|
||
|
|
||
|
shift_position = fragment_list_len(fragments_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
|
||
|
fragments = ti.fragments
|
||
|
|
||
|
return Transformation(fragments, 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():
|
||
|
return true_or_false
|
||
|
|
||
|
# Wrapped 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.Filter` instance.
|
||
|
"""
|
||
|
def __init__(self, processor, filter):
|
||
|
assert isinstance(processor, Processor)
|
||
|
|
||
|
self.processor = processor
|
||
|
self.filter = to_filter(filter)
|
||
|
|
||
|
def apply_transformation(self, transformation_input):
|
||
|
# Run processor when enabled.
|
||
|
if self.filter():
|
||
|
return self.processor.apply_transformation(transformation_input)
|
||
|
else:
|
||
|
return Transformation(transformation_input.fragments)
|
||
|
|
||
|
def __repr__(self):
|
||
|
return '%s(processor=%r, filter=%r)' % (
|
||
|
self.__class__.__name__, self.processor, self.filter)
|
||
|
|
||
|
|
||
|
class DynamicProcessor(Processor):
|
||
|
"""
|
||
|
Processor class that can dynamically returns any Processor.
|
||
|
|
||
|
:param get_processor: Callable that returns a :class:`.Processor` instance.
|
||
|
"""
|
||
|
def __init__(self, get_processor):
|
||
|
assert callable(get_processor)
|
||
|
self.get_processor = get_processor
|
||
|
|
||
|
def apply_transformation(self, ti):
|
||
|
processor = self.get_processor() or DummyProcessor()
|
||
|
return processor.apply_transformation(ti)
|
||
|
|
||
|
|
||
|
def merge_processors(processors):
|
||
|
"""
|
||
|
Merge multiple `Processor` objects into one.
|
||
|
"""
|
||
|
return _MergedProcessor(processors)
|
||
|
|
||
|
|
||
|
class _MergedProcessor(Processor):
|
||
|
"""
|
||
|
Processor that groups multiple other `Processor` objects, but exposes an
|
||
|
API as if it is one `Processor`.
|
||
|
"""
|
||
|
def __init__(self, processors):
|
||
|
assert all(isinstance(p, Processor) for p in processors)
|
||
|
self.processors = processors
|
||
|
|
||
|
def apply_transformation(self, ti):
|
||
|
source_to_display_functions = [ti.source_to_display]
|
||
|
display_to_source_functions = []
|
||
|
fragments = ti.fragments
|
||
|
|
||
|
def source_to_display(i):
|
||
|
""" Translate x position from the buffer to the x position in the
|
||
|
processor fragments list. """
|
||
|
for f in source_to_display_functions:
|
||
|
i = f(i)
|
||
|
return i
|
||
|
|
||
|
for p in self.processors:
|
||
|
transformation = p.apply_transformation(TransformationInput(
|
||
|
ti.buffer_control, ti.document, ti.lineno,
|
||
|
source_to_display, fragments, ti.width, ti.height))
|
||
|
fragments = transformation.fragments
|
||
|
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
|
||
|
|
||
|
# In the case of a nested _MergedProcessor, each processor wants to
|
||
|
# receive a 'source_to_display' function (as part of the
|
||
|
# TransformationInput) that has everything in the chain before
|
||
|
# included, because it can be called as part of the
|
||
|
# `apply_transformation` function. However, this first
|
||
|
# `source_to_display` should not be part of the output that we are
|
||
|
# returning. (This is the most consistent with `display_to_source`.)
|
||
|
del source_to_display_functions[:1]
|
||
|
|
||
|
return Transformation(fragments, source_to_display, display_to_source)
|