1# -*- coding: utf-8 -*-
2"""Key bindings for prompt_toolkit xonsh shell."""
3import builtins
4
5from prompt_toolkit.enums import DEFAULT_BUFFER
6from prompt_toolkit.filters import (
7    Condition,
8    IsMultiline,
9    HasSelection,
10    EmacsInsertMode,
11    ViInsertMode,
12)
13from prompt_toolkit.keys import Keys
14
15from xonsh.aliases import xonsh_exit
16from xonsh.tools import check_for_partial_string, get_line_continuation
17from xonsh.shell import transform_command
18
19env = builtins.__xonsh_env__
20DEDENT_TOKENS = frozenset(["raise", "return", "pass", "break", "continue"])
21
22
23def carriage_return(b, cli, *, autoindent=True):
24    """Preliminary parser to determine if 'Enter' key should send command to the
25    xonsh parser for execution or should insert a newline for continued input.
26
27    Current 'triggers' for inserting a newline are:
28    - Not on first line of buffer and line is non-empty
29    - Previous character is a colon (covers if, for, etc...)
30    - User is in an open paren-block
31    - Line ends with backslash
32    - Any text exists below cursor position (relevant when editing previous
33    multiline blocks)
34    """
35    doc = b.document
36    at_end_of_line = _is_blank(doc.current_line_after_cursor)
37    current_line_blank = _is_blank(doc.current_line)
38
39    indent = env.get("INDENT") if autoindent else ""
40
41    partial_string_info = check_for_partial_string(doc.text)
42    in_partial_string = (
43        partial_string_info[0] is not None and partial_string_info[1] is None
44    )
45
46    # indent after a colon
47    if doc.current_line_before_cursor.strip().endswith(":") and at_end_of_line:
48        b.newline(copy_margin=autoindent)
49        b.insert_text(indent, fire_event=False)
50    # if current line isn't blank, check dedent tokens
51    elif (
52        not current_line_blank
53        and doc.current_line.split(maxsplit=1)[0] in DEDENT_TOKENS
54        and doc.line_count > 1
55    ):
56        b.newline(copy_margin=autoindent)
57        b.delete_before_cursor(count=len(indent))
58    elif not doc.on_first_line and not current_line_blank:
59        b.newline(copy_margin=autoindent)
60    elif doc.current_line.endswith(get_line_continuation()):
61        b.newline(copy_margin=autoindent)
62    elif doc.find_next_word_beginning() is not None and (
63        any(not _is_blank(i) for i in doc.lines_from_current[1:])
64    ):
65        b.newline(copy_margin=autoindent)
66    elif not current_line_blank and not can_compile(doc.text):
67        b.newline(copy_margin=autoindent)
68    elif current_line_blank and in_partial_string:
69        b.newline(copy_margin=autoindent)
70    else:
71        b.accept_action.validate_and_handle(cli, b)
72
73
74def _is_blank(l):
75    return len(l.strip()) == 0
76
77
78def can_compile(src):
79    """Returns whether the code can be compiled, i.e. it is valid xonsh."""
80    src = src if src.endswith("\n") else src + "\n"
81    src = transform_command(src, show_diff=False)
82    src = src.lstrip()
83    try:
84        builtins.__xonsh_execer__.compile(
85            src, mode="single", glbs=None, locs=builtins.__xonsh_ctx__
86        )
87        rtn = True
88    except SyntaxError:
89        rtn = False
90    except Exception:
91        rtn = True
92    return rtn
93
94
95@Condition
96def tab_insert_indent(cli):
97    """Check if <Tab> should insert indent instead of starting autocompletion.
98    Checks if there are only whitespaces before the cursor - if so indent
99    should be inserted, otherwise autocompletion.
100
101    """
102    before_cursor = cli.current_buffer.document.current_line_before_cursor
103
104    return bool(before_cursor.isspace())
105
106
107@Condition
108def beginning_of_line(cli):
109    """Check if cursor is at beginning of a line other than the first line in a
110    multiline document
111    """
112    before_cursor = cli.current_buffer.document.current_line_before_cursor
113
114    return bool(
115        len(before_cursor) == 0 and not cli.current_buffer.document.on_first_line
116    )
117
118
119@Condition
120def end_of_line(cli):
121    """Check if cursor is at the end of a line other than the last line in a
122    multiline document
123    """
124    d = cli.current_buffer.document
125    at_end = d.is_cursor_at_the_end_of_line
126    last_line = d.is_cursor_at_the_end
127
128    return bool(at_end and not last_line)
129
130
131@Condition
132def should_confirm_completion(cli):
133    """Check if completion needs confirmation"""
134    return (
135        builtins.__xonsh_env__.get("COMPLETIONS_CONFIRM")
136        and cli.current_buffer.complete_state
137    )
138
139
140# Copied from prompt-toolkit's key_binding/bindings/basic.py
141@Condition
142def ctrl_d_condition(cli):
143    """Ctrl-D binding is only active when the default buffer is selected and
144    empty.
145    """
146    if builtins.__xonsh_env__.get("IGNOREEOF"):
147        raise EOFError
148    else:
149        return cli.current_buffer_name == DEFAULT_BUFFER and not cli.current_buffer.text
150
151
152@Condition
153def autopair_condition(cli):
154    """Check if XONSH_AUTOPAIR is set"""
155    return builtins.__xonsh_env__.get("XONSH_AUTOPAIR", False)
156
157
158@Condition
159def whitespace_or_bracket_before(cli):
160    """Check if there is whitespace or an opening
161       bracket to the left of the cursor"""
162    d = cli.current_buffer.document
163    return bool(
164        d.cursor_position == 0
165        or d.char_before_cursor.isspace()
166        or d.char_before_cursor in "([{"
167    )
168
169
170@Condition
171def whitespace_or_bracket_after(cli):
172    """Check if there is whitespace or a closing
173       bracket to the right of the cursor"""
174    d = cli.current_buffer.document
175    return bool(
176        d.is_cursor_at_the_end_of_line
177        or d.current_char.isspace()
178        or d.current_char in ")]}"
179    )
180
181
182def load_xonsh_bindings(key_bindings_manager):
183    """
184    Load custom key bindings.
185    """
186    handle = key_bindings_manager.registry.add_binding
187    has_selection = HasSelection()
188    insert_mode = ViInsertMode() | EmacsInsertMode()
189
190    @handle(Keys.Tab, filter=tab_insert_indent)
191    def insert_indent(event):
192        """
193        If there are only whitespaces before current cursor position insert
194        indent instead of autocompleting.
195        """
196        event.cli.current_buffer.insert_text(env.get("INDENT"))
197
198    @handle(Keys.ControlX, Keys.ControlE, filter=~has_selection)
199    def open_editor(event):
200        """ Open current buffer in editor """
201        event.current_buffer.open_in_editor(event.cli)
202
203    @handle(Keys.BackTab, filter=insert_mode)
204    def insert_literal_tab(event):
205        """ Insert literal tab on Shift+Tab instead of autocompleting """
206        b = event.current_buffer
207        if b.complete_state:
208            b.complete_previous()
209        else:
210            event.cli.current_buffer.insert_text(env.get("INDENT"))
211
212    @handle("(", filter=autopair_condition & whitespace_or_bracket_after)
213    def insert_right_parens(event):
214        event.cli.current_buffer.insert_text("(")
215        event.cli.current_buffer.insert_text(")", move_cursor=False)
216
217    @handle(")", filter=autopair_condition)
218    def overwrite_right_parens(event):
219        buffer = event.cli.current_buffer
220        if buffer.document.current_char == ")":
221            buffer.cursor_position += 1
222        else:
223            buffer.insert_text(")")
224
225    @handle("[", filter=autopair_condition & whitespace_or_bracket_after)
226    def insert_right_bracket(event):
227        event.cli.current_buffer.insert_text("[")
228        event.cli.current_buffer.insert_text("]", move_cursor=False)
229
230    @handle("]", filter=autopair_condition)
231    def overwrite_right_bracket(event):
232        buffer = event.cli.current_buffer
233
234        if buffer.document.current_char == "]":
235            buffer.cursor_position += 1
236        else:
237            buffer.insert_text("]")
238
239    @handle("{", filter=autopair_condition & whitespace_or_bracket_after)
240    def insert_right_brace(event):
241        event.cli.current_buffer.insert_text("{")
242        event.cli.current_buffer.insert_text("}", move_cursor=False)
243
244    @handle("}", filter=autopair_condition)
245    def overwrite_right_brace(event):
246        buffer = event.cli.current_buffer
247
248        if buffer.document.current_char == "}":
249            buffer.cursor_position += 1
250        else:
251            buffer.insert_text("}")
252
253    @handle("'", filter=autopair_condition)
254    def insert_right_quote(event):
255        buffer = event.cli.current_buffer
256
257        if buffer.document.current_char == "'":
258            buffer.cursor_position += 1
259        elif whitespace_or_bracket_before(event.cli) and whitespace_or_bracket_after(
260            event.cli
261        ):
262            buffer.insert_text("'")
263            buffer.insert_text("'", move_cursor=False)
264        else:
265            buffer.insert_text("'")
266
267    @handle('"', filter=autopair_condition)
268    def insert_right_double_quote(event):
269        buffer = event.cli.current_buffer
270
271        if buffer.document.current_char == '"':
272            buffer.cursor_position += 1
273        elif whitespace_or_bracket_before(event.cli) and whitespace_or_bracket_after(
274            event.cli
275        ):
276            buffer.insert_text('"')
277            buffer.insert_text('"', move_cursor=False)
278        else:
279            buffer.insert_text('"')
280
281    @handle(Keys.Backspace, filter=autopair_condition)
282    def delete_brackets_or_quotes(event):
283        """Delete empty pair of brackets or quotes"""
284        buffer = event.cli.current_buffer
285        before = buffer.document.char_before_cursor
286        after = buffer.document.current_char
287
288        if any(
289            [before == b and after == a for (b, a) in ["()", "[]", "{}", "''", '""']]
290        ):
291            buffer.delete(1)
292
293        buffer.delete_before_cursor(1)
294
295    @handle(Keys.ControlD, filter=ctrl_d_condition)
296    def call_exit_alias(event):
297        """Use xonsh exit function"""
298        b = event.cli.current_buffer
299        b.accept_action.validate_and_handle(event.cli, b)
300        xonsh_exit([])
301
302    @handle(Keys.ControlJ, filter=IsMultiline())
303    def multiline_carriage_return(event):
304        """ Wrapper around carriage_return multiline parser """
305        b = event.cli.current_buffer
306        carriage_return(b, event.cli)
307
308    @handle(Keys.ControlJ, filter=should_confirm_completion)
309    def enter_confirm_completion(event):
310        """Ignore <enter> (confirm completion)"""
311        event.current_buffer.complete_state = None
312
313    @handle(Keys.Escape, filter=should_confirm_completion)
314    def esc_cancel_completion(event):
315        """Use <ESC> to cancel completion"""
316        event.cli.current_buffer.cancel_completion()
317
318    @handle(Keys.Escape, Keys.ControlJ)
319    def execute_block_now(event):
320        """Execute a block of text irrespective of cursor position"""
321        b = event.cli.current_buffer
322        b.accept_action.validate_and_handle(event.cli, b)
323
324    @handle(Keys.Left, filter=beginning_of_line)
325    def wrap_cursor_back(event):
326        """Move cursor to end of previous line unless at beginning of
327        document
328        """
329        b = event.cli.current_buffer
330        b.cursor_up(count=1)
331        relative_end_index = b.document.get_end_of_line_position()
332        b.cursor_right(count=relative_end_index)
333
334    @handle(Keys.Right, filter=end_of_line)
335    def wrap_cursor_forward(event):
336        """Move cursor to beginning of next line unless at end of document"""
337        b = event.cli.current_buffer
338        relative_begin_index = b.document.get_start_of_line_position()
339        b.cursor_left(count=abs(relative_begin_index))
340        b.cursor_down(count=1)
341
342    @handle(Keys.ControlI, filter=insert_mode)
343    def generate_completions(event):
344        """
345        Tab-completion: where the first tab completes the common suffix and the
346        second tab lists all the completions.
347
348        Notes
349        -----
350        This method was forked from the mainline prompt-toolkit repo.
351        Copyright (c) 2014, Jonathan Slenders, All rights reserved.
352        """
353        b = event.current_buffer
354
355        def second_tab():
356            if b.complete_state:
357                b.complete_next()
358            else:
359                event.cli.start_completion(select_first=False)
360
361        # On the second tab-press, or when already navigating through
362        # completions.
363        if event.is_repeat or b.complete_state:
364            second_tab()
365        else:
366            event.cli.start_completion(insert_common_part=True, select_first=False)
367