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