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