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