emacs.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. # pylint: disable=function-redefined
  2. from __future__ import unicode_literals
  3. from prompt_toolkit.buffer import SelectionType, indent, unindent
  4. from prompt_toolkit.keys import Keys
  5. from prompt_toolkit.enums import IncrementalSearchDirection, SEARCH_BUFFER, SYSTEM_BUFFER
  6. from prompt_toolkit.filters import Condition, EmacsMode, HasSelection, EmacsInsertMode, HasFocus, HasArg
  7. from prompt_toolkit.completion import CompleteEvent
  8. from .scroll import scroll_page_up, scroll_page_down
  9. from .named_commands import get_by_name
  10. from ..registry import Registry, ConditionalRegistry
  11. __all__ = (
  12. 'load_emacs_bindings',
  13. 'load_emacs_search_bindings',
  14. 'load_emacs_system_bindings',
  15. 'load_extra_emacs_page_navigation_bindings',
  16. )
  17. def load_emacs_bindings():
  18. """
  19. Some e-macs extensions.
  20. """
  21. # Overview of Readline emacs commands:
  22. # http://www.catonmat.net/download/readline-emacs-editing-mode-cheat-sheet.pdf
  23. registry = ConditionalRegistry(Registry(), EmacsMode())
  24. handle = registry.add_binding
  25. insert_mode = EmacsInsertMode()
  26. has_selection = HasSelection()
  27. @handle(Keys.Escape)
  28. def _(event):
  29. """
  30. By default, ignore escape key.
  31. (If we don't put this here, and Esc is followed by a key which sequence
  32. is not handled, we'll insert an Escape character in the input stream.
  33. Something we don't want and happens to easily in emacs mode.
  34. Further, people can always use ControlQ to do a quoted insert.)
  35. """
  36. pass
  37. handle(Keys.ControlA)(get_by_name('beginning-of-line'))
  38. handle(Keys.ControlB)(get_by_name('backward-char'))
  39. handle(Keys.ControlDelete, filter=insert_mode)(get_by_name('kill-word'))
  40. handle(Keys.ControlE)(get_by_name('end-of-line'))
  41. handle(Keys.ControlF)(get_by_name('forward-char'))
  42. handle(Keys.ControlLeft)(get_by_name('backward-word'))
  43. handle(Keys.ControlRight)(get_by_name('forward-word'))
  44. handle(Keys.ControlX, 'r', 'y', filter=insert_mode)(get_by_name('yank'))
  45. handle(Keys.ControlY, filter=insert_mode)(get_by_name('yank'))
  46. handle(Keys.Escape, 'b')(get_by_name('backward-word'))
  47. handle(Keys.Escape, 'c', filter=insert_mode)(get_by_name('capitalize-word'))
  48. handle(Keys.Escape, 'd', filter=insert_mode)(get_by_name('kill-word'))
  49. handle(Keys.Escape, 'f')(get_by_name('forward-word'))
  50. handle(Keys.Escape, 'l', filter=insert_mode)(get_by_name('downcase-word'))
  51. handle(Keys.Escape, 'u', filter=insert_mode)(get_by_name('uppercase-word'))
  52. handle(Keys.Escape, 'y', filter=insert_mode)(get_by_name('yank-pop'))
  53. handle(Keys.Escape, Keys.ControlH, filter=insert_mode)(get_by_name('backward-kill-word'))
  54. handle(Keys.Escape, Keys.Backspace, filter=insert_mode)(get_by_name('backward-kill-word'))
  55. handle(Keys.Escape, '\\', filter=insert_mode)(get_by_name('delete-horizontal-space'))
  56. handle(Keys.ControlUnderscore, save_before=(lambda e: False), filter=insert_mode)(
  57. get_by_name('undo'))
  58. handle(Keys.ControlX, Keys.ControlU, save_before=(lambda e: False), filter=insert_mode)(
  59. get_by_name('undo'))
  60. handle(Keys.Escape, '<', filter= ~has_selection)(get_by_name('beginning-of-history'))
  61. handle(Keys.Escape, '>', filter= ~has_selection)(get_by_name('end-of-history'))
  62. handle(Keys.Escape, '.', filter=insert_mode)(get_by_name('yank-last-arg'))
  63. handle(Keys.Escape, '_', filter=insert_mode)(get_by_name('yank-last-arg'))
  64. handle(Keys.Escape, Keys.ControlY, filter=insert_mode)(get_by_name('yank-nth-arg'))
  65. handle(Keys.Escape, '#', filter=insert_mode)(get_by_name('insert-comment'))
  66. handle(Keys.ControlO)(get_by_name('operate-and-get-next'))
  67. # ControlQ does a quoted insert. Not that for vt100 terminals, you have to
  68. # disable flow control by running ``stty -ixon``, otherwise Ctrl-Q and
  69. # Ctrl-S are captured by the terminal.
  70. handle(Keys.ControlQ, filter= ~has_selection)(get_by_name('quoted-insert'))
  71. handle(Keys.ControlX, '(')(get_by_name('start-kbd-macro'))
  72. handle(Keys.ControlX, ')')(get_by_name('end-kbd-macro'))
  73. handle(Keys.ControlX, 'e')(get_by_name('call-last-kbd-macro'))
  74. @handle(Keys.ControlN)
  75. def _(event):
  76. " Next line. "
  77. event.current_buffer.auto_down()
  78. @handle(Keys.ControlP)
  79. def _(event):
  80. " Previous line. "
  81. event.current_buffer.auto_up(count=event.arg)
  82. def handle_digit(c):
  83. """
  84. Handle input of arguments.
  85. The first number needs to be preceeded by escape.
  86. """
  87. @handle(c, filter=HasArg())
  88. @handle(Keys.Escape, c)
  89. def _(event):
  90. event.append_to_arg_count(c)
  91. for c in '0123456789':
  92. handle_digit(c)
  93. @handle(Keys.Escape, '-', filter=~HasArg())
  94. def _(event):
  95. """
  96. """
  97. if event._arg is None:
  98. event.append_to_arg_count('-')
  99. @handle('-', filter=Condition(lambda cli: cli.input_processor.arg == '-'))
  100. def _(event):
  101. """
  102. When '-' is typed again, after exactly '-' has been given as an
  103. argument, ignore this.
  104. """
  105. event.cli.input_processor.arg = '-'
  106. is_returnable = Condition(
  107. lambda cli: cli.current_buffer.accept_action.is_returnable)
  108. # Meta + Newline: always accept input.
  109. handle(Keys.Escape, Keys.ControlJ, filter=insert_mode & is_returnable)(
  110. get_by_name('accept-line'))
  111. def character_search(buff, char, count):
  112. if count < 0:
  113. match = buff.document.find_backwards(char, in_current_line=True, count=-count)
  114. else:
  115. match = buff.document.find(char, in_current_line=True, count=count)
  116. if match is not None:
  117. buff.cursor_position += match
  118. @handle(Keys.ControlSquareClose, Keys.Any)
  119. def _(event):
  120. " When Ctl-] + a character is pressed. go to that character. "
  121. # Also named 'character-search'
  122. character_search(event.current_buffer, event.data, event.arg)
  123. @handle(Keys.Escape, Keys.ControlSquareClose, Keys.Any)
  124. def _(event):
  125. " Like Ctl-], but backwards. "
  126. # Also named 'character-search-backward'
  127. character_search(event.current_buffer, event.data, -event.arg)
  128. @handle(Keys.Escape, 'a')
  129. def _(event):
  130. " Previous sentence. "
  131. # TODO:
  132. @handle(Keys.Escape, 'e')
  133. def _(event):
  134. " Move to end of sentence. "
  135. # TODO:
  136. @handle(Keys.Escape, 't', filter=insert_mode)
  137. def _(event):
  138. """
  139. Swap the last two words before the cursor.
  140. """
  141. # TODO
  142. @handle(Keys.Escape, '*', filter=insert_mode)
  143. def _(event):
  144. """
  145. `meta-*`: Insert all possible completions of the preceding text.
  146. """
  147. buff = event.current_buffer
  148. # List all completions.
  149. complete_event = CompleteEvent(text_inserted=False, completion_requested=True)
  150. completions = list(buff.completer.get_completions(buff.document, complete_event))
  151. # Insert them.
  152. text_to_insert = ' '.join(c.text for c in completions)
  153. buff.insert_text(text_to_insert)
  154. @handle(Keys.ControlX, Keys.ControlX)
  155. def _(event):
  156. """
  157. Move cursor back and forth between the start and end of the current
  158. line.
  159. """
  160. buffer = event.current_buffer
  161. if buffer.document.is_cursor_at_the_end_of_line:
  162. buffer.cursor_position += buffer.document.get_start_of_line_position(after_whitespace=False)
  163. else:
  164. buffer.cursor_position += buffer.document.get_end_of_line_position()
  165. @handle(Keys.ControlSpace)
  166. def _(event):
  167. """
  168. Start of the selection (if the current buffer is not empty).
  169. """
  170. # Take the current cursor position as the start of this selection.
  171. buff = event.current_buffer
  172. if buff.text:
  173. buff.start_selection(selection_type=SelectionType.CHARACTERS)
  174. @handle(Keys.ControlG, filter= ~has_selection)
  175. def _(event):
  176. """
  177. Control + G: Cancel completion menu and validation state.
  178. """
  179. event.current_buffer.complete_state = None
  180. event.current_buffer.validation_error = None
  181. @handle(Keys.ControlG, filter=has_selection)
  182. def _(event):
  183. """
  184. Cancel selection.
  185. """
  186. event.current_buffer.exit_selection()
  187. @handle(Keys.ControlW, filter=has_selection)
  188. @handle(Keys.ControlX, 'r', 'k', filter=has_selection)
  189. def _(event):
  190. """
  191. Cut selected text.
  192. """
  193. data = event.current_buffer.cut_selection()
  194. event.cli.clipboard.set_data(data)
  195. @handle(Keys.Escape, 'w', filter=has_selection)
  196. def _(event):
  197. """
  198. Copy selected text.
  199. """
  200. data = event.current_buffer.copy_selection()
  201. event.cli.clipboard.set_data(data)
  202. @handle(Keys.Escape, Keys.Left)
  203. def _(event):
  204. """
  205. Cursor to start of previous word.
  206. """
  207. buffer = event.current_buffer
  208. buffer.cursor_position += buffer.document.find_previous_word_beginning(count=event.arg) or 0
  209. @handle(Keys.Escape, Keys.Right)
  210. def _(event):
  211. """
  212. Cursor to start of next word.
  213. """
  214. buffer = event.current_buffer
  215. buffer.cursor_position += buffer.document.find_next_word_beginning(count=event.arg) or \
  216. buffer.document.get_end_of_document_position()
  217. @handle(Keys.Escape, '/', filter=insert_mode)
  218. def _(event):
  219. """
  220. M-/: Complete.
  221. """
  222. b = event.current_buffer
  223. if b.complete_state:
  224. b.complete_next()
  225. else:
  226. event.cli.start_completion(select_first=True)
  227. @handle(Keys.ControlC, '>', filter=has_selection)
  228. def _(event):
  229. """
  230. Indent selected text.
  231. """
  232. buffer = event.current_buffer
  233. buffer.cursor_position += buffer.document.get_start_of_line_position(after_whitespace=True)
  234. from_, to = buffer.document.selection_range()
  235. from_, _ = buffer.document.translate_index_to_position(from_)
  236. to, _ = buffer.document.translate_index_to_position(to)
  237. indent(buffer, from_, to + 1, count=event.arg)
  238. @handle(Keys.ControlC, '<', filter=has_selection)
  239. def _(event):
  240. """
  241. Unindent selected text.
  242. """
  243. buffer = event.current_buffer
  244. from_, to = buffer.document.selection_range()
  245. from_, _ = buffer.document.translate_index_to_position(from_)
  246. to, _ = buffer.document.translate_index_to_position(to)
  247. unindent(buffer, from_, to + 1, count=event.arg)
  248. return registry
  249. def load_emacs_open_in_editor_bindings():
  250. """
  251. Pressing C-X C-E will open the buffer in an external editor.
  252. """
  253. registry = Registry()
  254. registry.add_binding(Keys.ControlX, Keys.ControlE,
  255. filter=EmacsMode() & ~HasSelection())(
  256. get_by_name('edit-and-execute-command'))
  257. return registry
  258. def load_emacs_system_bindings():
  259. registry = ConditionalRegistry(Registry(), EmacsMode())
  260. handle = registry.add_binding
  261. has_focus = HasFocus(SYSTEM_BUFFER)
  262. @handle(Keys.Escape, '!', filter= ~has_focus)
  263. def _(event):
  264. """
  265. M-'!' opens the system prompt.
  266. """
  267. event.cli.push_focus(SYSTEM_BUFFER)
  268. @handle(Keys.Escape, filter=has_focus)
  269. @handle(Keys.ControlG, filter=has_focus)
  270. @handle(Keys.ControlC, filter=has_focus)
  271. def _(event):
  272. """
  273. Cancel system prompt.
  274. """
  275. event.cli.buffers[SYSTEM_BUFFER].reset()
  276. event.cli.pop_focus()
  277. @handle(Keys.ControlJ, filter=has_focus)
  278. def _(event):
  279. """
  280. Run system command.
  281. """
  282. system_line = event.cli.buffers[SYSTEM_BUFFER]
  283. event.cli.run_system_command(system_line.text)
  284. system_line.reset(append_to_history=True)
  285. # Focus previous buffer again.
  286. event.cli.pop_focus()
  287. return registry
  288. def load_emacs_search_bindings(get_search_state=None):
  289. registry = ConditionalRegistry(Registry(), EmacsMode())
  290. handle = registry.add_binding
  291. has_focus = HasFocus(SEARCH_BUFFER)
  292. assert get_search_state is None or callable(get_search_state)
  293. if not get_search_state:
  294. def get_search_state(cli): return cli.search_state
  295. @handle(Keys.ControlG, filter=has_focus)
  296. @handle(Keys.ControlC, filter=has_focus)
  297. # NOTE: the reason for not also binding Escape to this one, is that we want
  298. # Alt+Enter to accept input directly in incremental search mode.
  299. def _(event):
  300. """
  301. Abort an incremental search and restore the original line.
  302. """
  303. search_buffer = event.cli.buffers[SEARCH_BUFFER]
  304. search_buffer.reset()
  305. event.cli.pop_focus()
  306. @handle(Keys.ControlJ, filter=has_focus)
  307. @handle(Keys.Escape, filter=has_focus, eager=True)
  308. def _(event):
  309. """
  310. When enter pressed in isearch, quit isearch mode. (Multiline
  311. isearch would be too complicated.)
  312. """
  313. input_buffer = event.cli.buffers.previous(event.cli)
  314. search_buffer = event.cli.buffers[SEARCH_BUFFER]
  315. # Update search state.
  316. if search_buffer.text:
  317. get_search_state(event.cli).text = search_buffer.text
  318. # Apply search.
  319. input_buffer.apply_search(get_search_state(event.cli), include_current_position=True)
  320. # Add query to history of search line.
  321. search_buffer.append_to_history()
  322. search_buffer.reset()
  323. # Focus previous document again.
  324. event.cli.pop_focus()
  325. @handle(Keys.ControlR, filter= ~has_focus)
  326. def _(event):
  327. get_search_state(event.cli).direction = IncrementalSearchDirection.BACKWARD
  328. event.cli.push_focus(SEARCH_BUFFER)
  329. @handle(Keys.ControlS, filter= ~has_focus)
  330. def _(event):
  331. get_search_state(event.cli).direction = IncrementalSearchDirection.FORWARD
  332. event.cli.push_focus(SEARCH_BUFFER)
  333. def incremental_search(cli, direction, count=1):
  334. " Apply search, but keep search buffer focussed. "
  335. # Update search_state.
  336. search_state = get_search_state(cli)
  337. direction_changed = search_state.direction != direction
  338. search_state.text = cli.buffers[SEARCH_BUFFER].text
  339. search_state.direction = direction
  340. # Apply search to current buffer.
  341. if not direction_changed:
  342. input_buffer = cli.buffers.previous(cli)
  343. input_buffer.apply_search(search_state,
  344. include_current_position=False, count=count)
  345. @handle(Keys.ControlR, filter=has_focus)
  346. @handle(Keys.Up, filter=has_focus)
  347. def _(event):
  348. incremental_search(event.cli, IncrementalSearchDirection.BACKWARD, count=event.arg)
  349. @handle(Keys.ControlS, filter=has_focus)
  350. @handle(Keys.Down, filter=has_focus)
  351. def _(event):
  352. incremental_search(event.cli, IncrementalSearchDirection.FORWARD, count=event.arg)
  353. return registry
  354. def load_extra_emacs_page_navigation_bindings():
  355. """
  356. Key bindings, for scrolling up and down through pages.
  357. This are separate bindings, because GNU readline doesn't have them.
  358. """
  359. registry = ConditionalRegistry(Registry(), EmacsMode())
  360. handle = registry.add_binding
  361. handle(Keys.ControlV)(scroll_page_down)
  362. handle(Keys.PageDown)(scroll_page_down)
  363. handle(Keys.Escape, 'v')(scroll_page_up)
  364. handle(Keys.PageUp)(scroll_page_up)
  365. return registry