1from prompt_toolkit.application import get_app
2from prompt_toolkit.document import Document
3from prompt_toolkit.enums import DEFAULT_BUFFER
4from prompt_toolkit.filters import (
5    Condition,
6    emacs_insert_mode,
7    emacs_mode,
8    has_focus,
9    has_selection,
10    vi_insert_mode,
11)
12from prompt_toolkit.key_binding import KeyBindings
13from prompt_toolkit.key_binding.bindings.named_commands import get_by_name
14from prompt_toolkit.keys import Keys
15
16from .utils import document_is_multiline_python
17
18__all__ = [
19    "load_python_bindings",
20    "load_sidebar_bindings",
21    "load_confirm_exit_bindings",
22]
23
24
25@Condition
26def tab_should_insert_whitespace():
27    """
28    When the 'tab' key is pressed with only whitespace character before the
29    cursor, do autocompletion. Otherwise, insert indentation.
30
31    Except for the first character at the first line. Then always do a
32    completion. It doesn't make sense to start the first line with
33    indentation.
34    """
35    b = get_app().current_buffer
36    before_cursor = b.document.current_line_before_cursor
37
38    return bool(b.text and (not before_cursor or before_cursor.isspace()))
39
40
41def load_python_bindings(python_input):
42    """
43    Custom key bindings.
44    """
45    bindings = KeyBindings()
46
47    sidebar_visible = Condition(lambda: python_input.show_sidebar)
48    handle = bindings.add
49
50    @handle("c-l")
51    def _(event):
52        """
53        Clear whole screen and render again -- also when the sidebar is visible.
54        """
55        event.app.renderer.clear()
56
57    @handle("c-z")
58    def _(event):
59        """
60        Suspend.
61        """
62        if python_input.enable_system_bindings:
63            event.app.suspend_to_background()
64
65    # Delete word before cursor, but use all Python symbols as separators
66    # (WORD=False).
67    handle("c-w")(get_by_name("backward-kill-word"))
68
69    @handle("f2")
70    def _(event):
71        """
72        Show/hide sidebar.
73        """
74        python_input.show_sidebar = not python_input.show_sidebar
75        if python_input.show_sidebar:
76            event.app.layout.focus(python_input.ptpython_layout.sidebar)
77        else:
78            event.app.layout.focus_last()
79
80    @handle("f3")
81    def _(event):
82        """
83        Select from the history.
84        """
85        python_input.enter_history()
86
87    @handle("f4")
88    def _(event):
89        """
90        Toggle between Vi and Emacs mode.
91        """
92        python_input.vi_mode = not python_input.vi_mode
93
94    @handle("f6")
95    def _(event):
96        """
97        Enable/Disable paste mode.
98        """
99        python_input.paste_mode = not python_input.paste_mode
100
101    @handle(
102        "tab", filter=~sidebar_visible & ~has_selection & tab_should_insert_whitespace
103    )
104    def _(event):
105        """
106        When tab should insert whitespace, do that instead of completion.
107        """
108        event.app.current_buffer.insert_text("    ")
109
110    @Condition
111    def is_multiline():
112        return document_is_multiline_python(python_input.default_buffer.document)
113
114    @handle(
115        "enter",
116        filter=~sidebar_visible
117        & ~has_selection
118        & (vi_insert_mode | emacs_insert_mode)
119        & has_focus(DEFAULT_BUFFER)
120        & ~is_multiline,
121    )
122    @handle(Keys.Escape, Keys.Enter, filter=~sidebar_visible & emacs_mode)
123    def _(event):
124        """
125        Accept input (for single line input).
126        """
127        b = event.current_buffer
128
129        if b.validate():
130            # When the cursor is at the end, and we have an empty line:
131            # drop the empty lines, but return the value.
132            b.document = Document(
133                text=b.text.rstrip(), cursor_position=len(b.text.rstrip())
134            )
135
136            b.validate_and_handle()
137
138    @handle(
139        "enter",
140        filter=~sidebar_visible
141        & ~has_selection
142        & (vi_insert_mode | emacs_insert_mode)
143        & has_focus(DEFAULT_BUFFER)
144        & is_multiline,
145    )
146    def _(event):
147        """
148        Behaviour of the Enter key.
149
150        Auto indent after newline/Enter.
151        (When not in Vi navigaton mode, and when multiline is enabled.)
152        """
153        b = event.current_buffer
154        empty_lines_required = python_input.accept_input_on_enter or 10000
155
156        def at_the_end(b):
157            """we consider the cursor at the end when there is no text after
158            the cursor, or only whitespace."""
159            text = b.document.text_after_cursor
160            return text == "" or (text.isspace() and not "\n" in text)
161
162        if python_input.paste_mode:
163            # In paste mode, always insert text.
164            b.insert_text("\n")
165
166        elif at_the_end(b) and b.document.text.replace(" ", "").endswith(
167            "\n" * (empty_lines_required - 1)
168        ):
169            # When the cursor is at the end, and we have an empty line:
170            # drop the empty lines, but return the value.
171            if b.validate():
172                b.document = Document(
173                    text=b.text.rstrip(), cursor_position=len(b.text.rstrip())
174                )
175
176                b.validate_and_handle()
177        else:
178            auto_newline(b)
179
180    @handle(
181        "c-d",
182        filter=~sidebar_visible
183        & has_focus(python_input.default_buffer)
184        & Condition(
185            lambda:
186            # The current buffer is empty.
187            not get_app().current_buffer.text
188        ),
189    )
190    def _(event):
191        """
192        Override Control-D exit, to ask for confirmation.
193        """
194        if python_input.confirm_exit:
195            # Show exit confirmation and focus it (focusing is important for
196            # making sure the default buffer key bindings are not active).
197            python_input.show_exit_confirmation = True
198            python_input.app.layout.focus(
199                python_input.ptpython_layout.exit_confirmation
200            )
201        else:
202            event.app.exit(exception=EOFError)
203
204    @handle("c-c", filter=has_focus(python_input.default_buffer))
205    def _(event):
206        "Abort when Control-C has been pressed."
207        event.app.exit(exception=KeyboardInterrupt, style="class:aborting")
208
209    return bindings
210
211
212def load_sidebar_bindings(python_input):
213    """
214    Load bindings for the navigation in the sidebar.
215    """
216    bindings = KeyBindings()
217
218    handle = bindings.add
219    sidebar_visible = Condition(lambda: python_input.show_sidebar)
220
221    @handle("up", filter=sidebar_visible)
222    @handle("c-p", filter=sidebar_visible)
223    @handle("k", filter=sidebar_visible)
224    def _(event):
225        "Go to previous option."
226        python_input.selected_option_index = (
227            python_input.selected_option_index - 1
228        ) % python_input.option_count
229
230    @handle("down", filter=sidebar_visible)
231    @handle("c-n", filter=sidebar_visible)
232    @handle("j", filter=sidebar_visible)
233    def _(event):
234        "Go to next option."
235        python_input.selected_option_index = (
236            python_input.selected_option_index + 1
237        ) % python_input.option_count
238
239    @handle("right", filter=sidebar_visible)
240    @handle("l", filter=sidebar_visible)
241    @handle(" ", filter=sidebar_visible)
242    def _(event):
243        "Select next value for current option."
244        option = python_input.selected_option
245        option.activate_next()
246
247    @handle("left", filter=sidebar_visible)
248    @handle("h", filter=sidebar_visible)
249    def _(event):
250        "Select previous value for current option."
251        option = python_input.selected_option
252        option.activate_previous()
253
254    @handle("c-c", filter=sidebar_visible)
255    @handle("c-d", filter=sidebar_visible)
256    @handle("c-d", filter=sidebar_visible)
257    @handle("enter", filter=sidebar_visible)
258    @handle("escape", filter=sidebar_visible)
259    def _(event):
260        "Hide sidebar."
261        python_input.show_sidebar = False
262        event.app.layout.focus_last()
263
264    return bindings
265
266
267def load_confirm_exit_bindings(python_input):
268    """
269    Handle yes/no key presses when the exit confirmation is shown.
270    """
271    bindings = KeyBindings()
272
273    handle = bindings.add
274    confirmation_visible = Condition(lambda: python_input.show_exit_confirmation)
275
276    @handle("y", filter=confirmation_visible)
277    @handle("Y", filter=confirmation_visible)
278    @handle("enter", filter=confirmation_visible)
279    @handle("c-d", filter=confirmation_visible)
280    def _(event):
281        """
282        Really quit.
283        """
284        event.app.exit(exception=EOFError, style="class:exiting")
285
286    @handle(Keys.Any, filter=confirmation_visible)
287    def _(event):
288        """
289        Cancel exit.
290        """
291        python_input.show_exit_confirmation = False
292        python_input.app.layout.focus_previous()
293
294    return bindings
295
296
297def auto_newline(buffer):
298    r"""
299    Insert \n at the cursor position. Also add necessary padding.
300    """
301    insert_text = buffer.insert_text
302
303    if buffer.document.current_line_after_cursor:
304        # When we are in the middle of a line. Always insert a newline.
305        insert_text("\n")
306    else:
307        # Go to new line, but also add indentation.
308        current_line = buffer.document.current_line_before_cursor.rstrip()
309        insert_text("\n")
310
311        # Unident if the last line ends with 'pass', remove four spaces.
312        unindent = current_line.rstrip().endswith(" pass")
313
314        # Copy whitespace from current line
315        current_line2 = current_line[4:] if unindent else current_line
316
317        for c in current_line2:
318            if c.isspace():
319                insert_text(c)
320            else:
321                break
322
323        # If the last line ends with a colon, add four extra spaces.
324        if current_line[-1:] == ":":
325            for x in range(4):
326                insert_text(" ")
327