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.
623 lines
23 KiB
623 lines
23 KiB
"""
|
|
Creation of the `Layout` instance for the Python input/REPL.
|
|
"""
|
|
from __future__ import unicode_literals
|
|
|
|
from prompt_toolkit.application import get_app
|
|
from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER
|
|
from prompt_toolkit.filters import is_done, has_completions, renderer_height_is_known, has_focus, Condition
|
|
from prompt_toolkit.formatted_text import fragment_list_width, to_formatted_text
|
|
from prompt_toolkit.key_binding.vi_state import InputMode
|
|
from prompt_toolkit.layout.containers import Window, HSplit, VSplit, FloatContainer, Float, ConditionalContainer, ScrollOffsets
|
|
from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl
|
|
from prompt_toolkit.layout.dimension import Dimension
|
|
from prompt_toolkit.layout.layout import Layout
|
|
from prompt_toolkit.layout.margins import PromptMargin
|
|
from prompt_toolkit.layout.menus import CompletionsMenu, MultiColumnCompletionsMenu
|
|
from prompt_toolkit.layout.processors import ConditionalProcessor, AppendAutoSuggestion, HighlightIncrementalSearchProcessor, HighlightSelectionProcessor, HighlightMatchingBracketProcessor, Processor, Transformation
|
|
from prompt_toolkit.lexers import SimpleLexer
|
|
from prompt_toolkit.selection import SelectionType
|
|
from prompt_toolkit.widgets.toolbars import CompletionsToolbar, ArgToolbar, SearchToolbar, ValidationToolbar, SystemToolbar
|
|
|
|
from .filters import HasSignature, ShowSidebar, ShowSignature, ShowDocstring
|
|
from .utils import if_mousedown
|
|
|
|
from pygments.lexers import PythonLexer
|
|
|
|
import platform
|
|
import sys
|
|
|
|
__all__ = (
|
|
'PtPythonLayout',
|
|
'CompletionVisualisation',
|
|
)
|
|
|
|
|
|
# DisplayMultipleCursors: Only for prompt_toolkit>=1.0.8
|
|
try:
|
|
from prompt_toolkit.layout.processors import DisplayMultipleCursors
|
|
except ImportError:
|
|
class DisplayMultipleCursors(Processor):
|
|
" Dummy. "
|
|
def __init__(self, *a):
|
|
pass
|
|
|
|
def apply_transformation(self, document, lineno,
|
|
source_to_display, tokens):
|
|
return Transformation(tokens)
|
|
|
|
|
|
class CompletionVisualisation:
|
|
" Visualisation method for the completions. "
|
|
NONE = 'none'
|
|
POP_UP = 'pop-up'
|
|
MULTI_COLUMN = 'multi-column'
|
|
TOOLBAR = 'toolbar'
|
|
|
|
|
|
def show_completions_toolbar(python_input):
|
|
return Condition(lambda: python_input.completion_visualisation == CompletionVisualisation.TOOLBAR)
|
|
|
|
|
|
def show_completions_menu(python_input):
|
|
return Condition(lambda: python_input.completion_visualisation == CompletionVisualisation.POP_UP)
|
|
|
|
|
|
def show_multi_column_completions_menu(python_input):
|
|
return Condition(lambda: python_input.completion_visualisation == CompletionVisualisation.MULTI_COLUMN)
|
|
|
|
|
|
def python_sidebar(python_input):
|
|
"""
|
|
Create the `Layout` for the sidebar with the configurable options.
|
|
"""
|
|
def get_text_fragments():
|
|
tokens = []
|
|
|
|
def append_category(category):
|
|
tokens.extend([
|
|
('class:sidebar', ' '),
|
|
('class:sidebar.title', ' %-36s' % category.title),
|
|
('class:sidebar', '\n'),
|
|
])
|
|
|
|
def append(index, label, status):
|
|
selected = index == python_input.selected_option_index
|
|
|
|
@if_mousedown
|
|
def select_item(mouse_event):
|
|
python_input.selected_option_index = index
|
|
|
|
@if_mousedown
|
|
def goto_next(mouse_event):
|
|
" Select item and go to next value. "
|
|
python_input.selected_option_index = index
|
|
option = python_input.selected_option
|
|
option.activate_next()
|
|
|
|
sel = ',selected' if selected else ''
|
|
|
|
tokens.append(('class:sidebar' + sel, ' >' if selected else ' '))
|
|
tokens.append(('class:sidebar.label' + sel, '%-24s' % label, select_item))
|
|
tokens.append(('class:sidebar.status' + sel, ' ', select_item))
|
|
tokens.append(('class:sidebar.status' + sel, '%s' % status, goto_next))
|
|
|
|
if selected:
|
|
tokens.append(('[SetCursorPosition]', ''))
|
|
|
|
tokens.append(('class:sidebar.status' + sel, ' ' * (13 - len(status)), goto_next))
|
|
tokens.append(('class:sidebar', '<' if selected else ''))
|
|
tokens.append(('class:sidebar', '\n'))
|
|
|
|
i = 0
|
|
for category in python_input.options:
|
|
append_category(category)
|
|
|
|
for option in category.options:
|
|
append(i, option.title, '%s' % option.get_current_value())
|
|
i += 1
|
|
|
|
tokens.pop() # Remove last newline.
|
|
|
|
return tokens
|
|
|
|
class Control(FormattedTextControl):
|
|
def move_cursor_down(self):
|
|
python_input.selected_option_index += 1
|
|
|
|
def move_cursor_up(self):
|
|
python_input.selected_option_index -= 1
|
|
|
|
return Window(
|
|
Control(get_text_fragments),
|
|
style='class:sidebar',
|
|
width=Dimension.exact(43),
|
|
height=Dimension(min=3),
|
|
scroll_offsets=ScrollOffsets(top=1, bottom=1))
|
|
|
|
|
|
def python_sidebar_navigation(python_input):
|
|
"""
|
|
Create the `Layout` showing the navigation information for the sidebar.
|
|
"""
|
|
def get_text_fragments():
|
|
tokens = []
|
|
|
|
# Show navigation info.
|
|
tokens.extend([
|
|
('class:sidebar', ' '),
|
|
('class:sidebar.key', '[Arrows]'),
|
|
('class:sidebar', ' '),
|
|
('class:sidebar.description', 'Navigate'),
|
|
('class:sidebar', ' '),
|
|
('class:sidebar.key', '[Enter]'),
|
|
('class:sidebar', ' '),
|
|
('class:sidebar.description', 'Hide menu'),
|
|
])
|
|
|
|
return tokens
|
|
|
|
return Window(
|
|
FormattedTextControl(get_text_fragments),
|
|
style='class:sidebar',
|
|
width=Dimension.exact(43),
|
|
height=Dimension.exact(1))
|
|
|
|
|
|
def python_sidebar_help(python_input):
|
|
"""
|
|
Create the `Layout` for the help text for the current item in the sidebar.
|
|
"""
|
|
token = 'class:sidebar.helptext'
|
|
|
|
def get_current_description():
|
|
"""
|
|
Return the description of the selected option.
|
|
"""
|
|
i = 0
|
|
for category in python_input.options:
|
|
for option in category.options:
|
|
if i == python_input.selected_option_index:
|
|
return option.description
|
|
i += 1
|
|
return ''
|
|
|
|
def get_help_text():
|
|
return [(token, get_current_description())]
|
|
|
|
return ConditionalContainer(
|
|
content=Window(
|
|
FormattedTextControl(get_help_text),
|
|
style=token,
|
|
height=Dimension(min=3)),
|
|
filter=ShowSidebar(python_input) &
|
|
Condition(lambda: python_input.show_sidebar_help) & ~is_done)
|
|
|
|
|
|
def signature_toolbar(python_input):
|
|
"""
|
|
Return the `Layout` for the signature.
|
|
"""
|
|
def get_text_fragments():
|
|
result = []
|
|
append = result.append
|
|
Signature = 'class:signature-toolbar'
|
|
|
|
if python_input.signatures:
|
|
sig = python_input.signatures[0] # Always take the first one.
|
|
|
|
append((Signature, ' '))
|
|
try:
|
|
append((Signature, sig.full_name))
|
|
except IndexError:
|
|
# Workaround for #37: https://github.com/jonathanslenders/python-prompt-toolkit/issues/37
|
|
# See also: https://github.com/davidhalter/jedi/issues/490
|
|
return []
|
|
|
|
append((Signature + ',operator', '('))
|
|
|
|
try:
|
|
enumerated_params = enumerate(sig.params)
|
|
except AttributeError:
|
|
# Workaround for #136: https://github.com/jonathanslenders/ptpython/issues/136
|
|
# AttributeError: 'Lambda' object has no attribute 'get_subscope_by_name'
|
|
return []
|
|
|
|
for i, p in enumerated_params:
|
|
# Workaround for #47: 'p' is None when we hit the '*' in the signature.
|
|
# and sig has no 'index' attribute.
|
|
# See: https://github.com/jonathanslenders/ptpython/issues/47
|
|
# https://github.com/davidhalter/jedi/issues/598
|
|
description = (p.description if p else '*') #or '*'
|
|
sig_index = getattr(sig, 'index', 0)
|
|
|
|
if i == sig_index:
|
|
# Note: we use `_Param.description` instead of
|
|
# `_Param.name`, that way we also get the '*' before args.
|
|
append((Signature + ',current-name', str(description)))
|
|
else:
|
|
append((Signature, str(description)))
|
|
append((Signature + ',operator', ', '))
|
|
|
|
if sig.params:
|
|
# Pop last comma
|
|
result.pop()
|
|
|
|
append((Signature + ',operator', ')'))
|
|
append((Signature, ' '))
|
|
return result
|
|
|
|
return ConditionalContainer(
|
|
content=Window(
|
|
FormattedTextControl(get_text_fragments),
|
|
height=Dimension.exact(1)),
|
|
filter=
|
|
# Show only when there is a signature
|
|
HasSignature(python_input) &
|
|
# And there are no completions to be shown. (would cover signature pop-up.)
|
|
~(has_completions & (show_completions_menu(python_input) |
|
|
show_multi_column_completions_menu(python_input)))
|
|
# Signature needs to be shown.
|
|
& ShowSignature(python_input) &
|
|
# Not done yet.
|
|
~is_done)
|
|
|
|
|
|
class PythonPromptMargin(PromptMargin):
|
|
"""
|
|
Create margin that displays the prompt.
|
|
It shows something like "In [1]:".
|
|
"""
|
|
def __init__(self, python_input):
|
|
self.python_input = python_input
|
|
|
|
def get_prompt_style():
|
|
return python_input.all_prompt_styles[python_input.prompt_style]
|
|
|
|
def get_prompt():
|
|
return to_formatted_text(get_prompt_style().in_prompt())
|
|
|
|
def get_continuation(width, line_number, is_soft_wrap):
|
|
if python_input.show_line_numbers and not is_soft_wrap:
|
|
text = ('%i ' % (line_number + 1)).rjust(width)
|
|
return [('class:line-number', text)]
|
|
else:
|
|
return get_prompt_style().in2_prompt(width)
|
|
|
|
super(PythonPromptMargin, self).__init__(get_prompt, get_continuation)
|
|
|
|
|
|
def status_bar(python_input):
|
|
"""
|
|
Create the `Layout` for the status bar.
|
|
"""
|
|
TB = 'class:status-toolbar'
|
|
|
|
@if_mousedown
|
|
def toggle_paste_mode(mouse_event):
|
|
python_input.paste_mode = not python_input.paste_mode
|
|
|
|
@if_mousedown
|
|
def enter_history(mouse_event):
|
|
python_input.enter_history()
|
|
|
|
def get_text_fragments():
|
|
python_buffer = python_input.default_buffer
|
|
|
|
result = []
|
|
append = result.append
|
|
|
|
append((TB, ' '))
|
|
result.extend(get_inputmode_fragments(python_input))
|
|
append((TB, ' '))
|
|
|
|
# Position in history.
|
|
append((TB, '%i/%i ' % (python_buffer.working_index + 1,
|
|
len(python_buffer._working_lines))))
|
|
|
|
# Shortcuts.
|
|
app = get_app()
|
|
if not python_input.vi_mode and app.current_buffer == python_input.search_buffer:
|
|
append((TB, '[Ctrl-G] Cancel search [Enter] Go to this position.'))
|
|
elif bool(app.current_buffer.selection_state) and not python_input.vi_mode:
|
|
# Emacs cut/copy keys.
|
|
append((TB, '[Ctrl-W] Cut [Meta-W] Copy [Ctrl-Y] Paste [Ctrl-G] Cancel'))
|
|
else:
|
|
result.extend([
|
|
(TB + ' class:key', '[F3]', enter_history),
|
|
(TB, ' History ', enter_history),
|
|
(TB + ' class:key', '[F6]', toggle_paste_mode),
|
|
(TB, ' ', toggle_paste_mode),
|
|
])
|
|
|
|
if python_input.paste_mode:
|
|
append((TB + ' class:paste-mode-on', 'Paste mode (on)', toggle_paste_mode))
|
|
else:
|
|
append((TB, 'Paste mode', toggle_paste_mode))
|
|
|
|
return result
|
|
|
|
return ConditionalContainer(
|
|
content=Window(content=FormattedTextControl(get_text_fragments), style=TB),
|
|
filter=~is_done & renderer_height_is_known &
|
|
Condition(lambda: python_input.show_status_bar and
|
|
not python_input.show_exit_confirmation))
|
|
|
|
|
|
def get_inputmode_fragments(python_input):
|
|
"""
|
|
Return current input mode as a list of (token, text) tuples for use in a
|
|
toolbar.
|
|
"""
|
|
app = get_app()
|
|
@if_mousedown
|
|
def toggle_vi_mode(mouse_event):
|
|
python_input.vi_mode = not python_input.vi_mode
|
|
|
|
token = 'class:status-toolbar'
|
|
input_mode_t = 'class:status-toolbar.input-mode'
|
|
|
|
mode = app.vi_state.input_mode
|
|
result = []
|
|
append = result.append
|
|
|
|
append((input_mode_t, '[F4] ', toggle_vi_mode))
|
|
|
|
# InputMode
|
|
if python_input.vi_mode:
|
|
recording_register = app.vi_state.recording_register
|
|
if recording_register:
|
|
append((token, ' '))
|
|
append((token + ' class:record', 'RECORD({})'.format(recording_register)))
|
|
append((token, ' - '))
|
|
|
|
if bool(app.current_buffer.selection_state):
|
|
if app.current_buffer.selection_state.type == SelectionType.LINES:
|
|
append((input_mode_t, 'Vi (VISUAL LINE)', toggle_vi_mode))
|
|
elif app.current_buffer.selection_state.type == SelectionType.CHARACTERS:
|
|
append((input_mode_t, 'Vi (VISUAL)', toggle_vi_mode))
|
|
append((token, ' '))
|
|
elif app.current_buffer.selection_state.type == 'BLOCK':
|
|
append((input_mode_t, 'Vi (VISUAL BLOCK)', toggle_vi_mode))
|
|
append((token, ' '))
|
|
elif mode in (InputMode.INSERT, 'vi-insert-multiple'):
|
|
append((input_mode_t, 'Vi (INSERT)', toggle_vi_mode))
|
|
append((token, ' '))
|
|
elif mode == InputMode.NAVIGATION:
|
|
append((input_mode_t, 'Vi (NAV)', toggle_vi_mode))
|
|
append((token, ' '))
|
|
elif mode == InputMode.REPLACE:
|
|
append((input_mode_t, 'Vi (REPLACE)', toggle_vi_mode))
|
|
append((token, ' '))
|
|
else:
|
|
if app.emacs_state.is_recording:
|
|
append((token, ' '))
|
|
append((token + ' class:record', 'RECORD'))
|
|
append((token, ' - '))
|
|
|
|
append((input_mode_t, 'Emacs', toggle_vi_mode))
|
|
append((token, ' '))
|
|
|
|
return result
|
|
|
|
|
|
def show_sidebar_button_info(python_input):
|
|
"""
|
|
Create `Layout` for the information in the right-bottom corner.
|
|
(The right part of the status bar.)
|
|
"""
|
|
@if_mousedown
|
|
def toggle_sidebar(mouse_event):
|
|
" Click handler for the menu. "
|
|
python_input.show_sidebar = not python_input.show_sidebar
|
|
|
|
version = sys.version_info
|
|
tokens = [
|
|
('class:status-toolbar.key', '[F2]', toggle_sidebar),
|
|
('class:status-toolbar', ' Menu', toggle_sidebar),
|
|
('class:status-toolbar', ' - '),
|
|
('class:status-toolbar.python-version', '%s %i.%i.%i' % (platform.python_implementation(),
|
|
version[0], version[1], version[2])),
|
|
('class:status-toolbar', ' '),
|
|
]
|
|
width = fragment_list_width(tokens)
|
|
|
|
def get_text_fragments():
|
|
# Python version
|
|
return tokens
|
|
|
|
return ConditionalContainer(
|
|
content=Window(
|
|
FormattedTextControl(get_text_fragments),
|
|
style='class:status-toolbar',
|
|
height=Dimension.exact(1),
|
|
width=Dimension.exact(width)),
|
|
filter=~is_done & renderer_height_is_known &
|
|
Condition(lambda: python_input.show_status_bar and
|
|
not python_input.show_exit_confirmation))
|
|
|
|
|
|
def exit_confirmation(python_input, style='class:exit-confirmation'):
|
|
"""
|
|
Create `Layout` for the exit message.
|
|
"""
|
|
def get_text_fragments():
|
|
# Show "Do you really want to exit?"
|
|
return [
|
|
(style, '\n %s ([y]/n)' % python_input.exit_message),
|
|
('[SetCursorPosition]', ''),
|
|
(style, ' \n'),
|
|
]
|
|
|
|
visible = ~is_done & Condition(lambda: python_input.show_exit_confirmation)
|
|
|
|
return ConditionalContainer(
|
|
content=Window(FormattedTextControl(get_text_fragments), style=style), # , has_focus=visible)),
|
|
filter=visible)
|
|
|
|
|
|
def meta_enter_message(python_input):
|
|
"""
|
|
Create the `Layout` for the 'Meta+Enter` message.
|
|
"""
|
|
def get_text_fragments():
|
|
return [('class:accept-message', ' [Meta+Enter] Execute ')]
|
|
|
|
def extra_condition():
|
|
" Only show when... "
|
|
b = python_input.default_buffer
|
|
|
|
return (
|
|
python_input.show_meta_enter_message and
|
|
(not b.document.is_cursor_at_the_end or
|
|
python_input.accept_input_on_enter is None) and
|
|
'\n' in b.text)
|
|
|
|
visible = ~is_done & has_focus(DEFAULT_BUFFER) & Condition(extra_condition)
|
|
|
|
return ConditionalContainer(
|
|
content=Window(FormattedTextControl(get_text_fragments)),
|
|
filter=visible)
|
|
|
|
|
|
class PtPythonLayout(object):
|
|
def __init__(self, python_input, lexer=PythonLexer, extra_body=None,
|
|
extra_toolbars=None, extra_buffer_processors=None,
|
|
input_buffer_height=None):
|
|
D = Dimension
|
|
extra_body = [extra_body] if extra_body else []
|
|
extra_toolbars = extra_toolbars or []
|
|
extra_buffer_processors = extra_buffer_processors or []
|
|
input_buffer_height = input_buffer_height or D(min=6)
|
|
|
|
search_toolbar = SearchToolbar(python_input.search_buffer)
|
|
|
|
def create_python_input_window():
|
|
def menu_position():
|
|
"""
|
|
When there is no autocompletion menu to be shown, and we have a
|
|
signature, set the pop-up position at `bracket_start`.
|
|
"""
|
|
b = python_input.default_buffer
|
|
|
|
if b.complete_state is None and python_input.signatures:
|
|
row, col = python_input.signatures[0].bracket_start
|
|
index = b.document.translate_row_col_to_index(row - 1, col)
|
|
return index
|
|
|
|
return Window(
|
|
BufferControl(
|
|
buffer=python_input.default_buffer,
|
|
search_buffer_control=search_toolbar.control,
|
|
lexer=lexer,
|
|
include_default_input_processors=False,
|
|
input_processors=[
|
|
ConditionalProcessor(
|
|
processor=HighlightIncrementalSearchProcessor(),
|
|
filter=has_focus(SEARCH_BUFFER) | has_focus(search_toolbar.control),
|
|
),
|
|
HighlightSelectionProcessor(),
|
|
DisplayMultipleCursors(),
|
|
# Show matching parentheses, but only while editing.
|
|
ConditionalProcessor(
|
|
processor=HighlightMatchingBracketProcessor(chars='[](){}'),
|
|
filter=has_focus(DEFAULT_BUFFER) & ~is_done &
|
|
Condition(lambda: python_input.highlight_matching_parenthesis)),
|
|
ConditionalProcessor(
|
|
processor=AppendAutoSuggestion(),
|
|
filter=~is_done)
|
|
] + extra_buffer_processors,
|
|
menu_position=menu_position,
|
|
|
|
# Make sure that we always see the result of an reverse-i-search:
|
|
preview_search=True,
|
|
),
|
|
left_margins=[PythonPromptMargin(python_input)],
|
|
# Scroll offsets. The 1 at the bottom is important to make sure
|
|
# the cursor is never below the "Press [Meta+Enter]" message
|
|
# which is a float.
|
|
scroll_offsets=ScrollOffsets(bottom=1, left=4, right=4),
|
|
# As long as we're editing, prefer a minimal height of 6.
|
|
height=(lambda: (
|
|
None if get_app().is_done or python_input.show_exit_confirmation
|
|
else input_buffer_height)),
|
|
wrap_lines=Condition(lambda: python_input.wrap_lines),
|
|
)
|
|
|
|
sidebar = python_sidebar(python_input)
|
|
|
|
root_container = HSplit([
|
|
VSplit([
|
|
HSplit([
|
|
FloatContainer(
|
|
content=HSplit(
|
|
[create_python_input_window()] + extra_body
|
|
),
|
|
floats=[
|
|
Float(xcursor=True,
|
|
ycursor=True,
|
|
content=ConditionalContainer(
|
|
content=CompletionsMenu(
|
|
scroll_offset=(
|
|
lambda: python_input.completion_menu_scroll_offset),
|
|
max_height=12),
|
|
filter=show_completions_menu(python_input))),
|
|
Float(xcursor=True,
|
|
ycursor=True,
|
|
content=ConditionalContainer(
|
|
content=MultiColumnCompletionsMenu(),
|
|
filter=show_multi_column_completions_menu(python_input))),
|
|
Float(xcursor=True,
|
|
ycursor=True,
|
|
content=signature_toolbar(python_input)),
|
|
Float(left=2,
|
|
bottom=1,
|
|
content=exit_confirmation(python_input)),
|
|
Float(bottom=0, right=0, height=1,
|
|
content=meta_enter_message(python_input),
|
|
hide_when_covering_content=True),
|
|
Float(bottom=1, left=1, right=0, content=python_sidebar_help(python_input)),
|
|
]),
|
|
ArgToolbar(),
|
|
search_toolbar,
|
|
SystemToolbar(),
|
|
ValidationToolbar(),
|
|
ConditionalContainer(
|
|
content=CompletionsToolbar(),
|
|
filter=show_completions_toolbar(python_input)),
|
|
|
|
# Docstring region.
|
|
ConditionalContainer(
|
|
content=Window(
|
|
height=D.exact(1),
|
|
char='\u2500',
|
|
style='class:separator'),
|
|
filter=HasSignature(python_input) & ShowDocstring(python_input) & ~is_done),
|
|
ConditionalContainer(
|
|
content=Window(
|
|
BufferControl(
|
|
buffer=python_input.docstring_buffer,
|
|
lexer=SimpleLexer(style='class:docstring'),
|
|
#lexer=PythonLexer,
|
|
),
|
|
height=D(max=12)),
|
|
filter=HasSignature(python_input) & ShowDocstring(python_input) & ~is_done),
|
|
]),
|
|
ConditionalContainer(
|
|
content=HSplit([
|
|
sidebar,
|
|
Window(style='class:sidebar,separator', height=1),
|
|
python_sidebar_navigation(python_input),
|
|
]),
|
|
filter=ShowSidebar(python_input) & ~is_done)
|
|
]),
|
|
] + extra_toolbars + [
|
|
VSplit([
|
|
status_bar(python_input),
|
|
show_sidebar_button_info(python_input),
|
|
])
|
|
])
|
|
|
|
self.layout = Layout(root_container)
|
|
self.sidebar = sidebar
|