history_console_widget.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. # Copyright (c) Jupyter Development Team.
  2. # Distributed under the terms of the Modified BSD License.
  3. from qtpy import QtGui
  4. from ipython_genutils.py3compat import unicode_type
  5. from traitlets import Bool
  6. from .console_widget import ConsoleWidget
  7. class HistoryConsoleWidget(ConsoleWidget):
  8. """ A ConsoleWidget that keeps a history of the commands that have been
  9. executed and provides a readline-esque interface to this history.
  10. """
  11. #------ Configuration ------------------------------------------------------
  12. # If enabled, the input buffer will become "locked" to history movement when
  13. # an edit is made to a multi-line input buffer. To override the lock, use
  14. # Shift in conjunction with the standard history cycling keys.
  15. history_lock = Bool(False, config=True)
  16. #---------------------------------------------------------------------------
  17. # 'object' interface
  18. #---------------------------------------------------------------------------
  19. def __init__(self, *args, **kw):
  20. super(HistoryConsoleWidget, self).__init__(*args, **kw)
  21. # HistoryConsoleWidget protected variables.
  22. self._history = []
  23. self._history_edits = {}
  24. self._history_index = 0
  25. self._history_prefix = ''
  26. #---------------------------------------------------------------------------
  27. # 'ConsoleWidget' public interface
  28. #---------------------------------------------------------------------------
  29. def do_execute(self, source, complete, indent):
  30. """ Reimplemented to the store history. """
  31. history = self.input_buffer if source is None else source
  32. super(HistoryConsoleWidget, self).do_execute(source, complete, indent)
  33. if complete:
  34. # Save the command unless it was an empty string or was identical
  35. # to the previous command.
  36. history = history.rstrip()
  37. if history and (not self._history or self._history[-1] != history):
  38. self._history.append(history)
  39. # Emulate readline: reset all history edits.
  40. self._history_edits = {}
  41. # Move the history index to the most recent item.
  42. self._history_index = len(self._history)
  43. #---------------------------------------------------------------------------
  44. # 'ConsoleWidget' abstract interface
  45. #---------------------------------------------------------------------------
  46. def _up_pressed(self, shift_modifier):
  47. """ Called when the up key is pressed. Returns whether to continue
  48. processing the event.
  49. """
  50. prompt_cursor = self._get_prompt_cursor()
  51. if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
  52. # Bail out if we're locked.
  53. if self._history_locked() and not shift_modifier:
  54. return False
  55. # Set a search prefix based on the cursor position.
  56. pos = self._get_input_buffer_cursor_pos()
  57. input_buffer = self.input_buffer
  58. # use the *shortest* of the cursor column and the history prefix
  59. # to determine if the prefix has changed
  60. n = min(pos, len(self._history_prefix))
  61. # prefix changed, restart search from the beginning
  62. if (self._history_prefix[:n] != input_buffer[:n]):
  63. self._history_index = len(self._history)
  64. # the only time we shouldn't set the history prefix
  65. # to the line up to the cursor is if we are already
  66. # in a simple scroll (no prefix),
  67. # and the cursor is at the end of the first line
  68. # check if we are at the end of the first line
  69. c = self._get_cursor()
  70. current_pos = c.position()
  71. c.movePosition(QtGui.QTextCursor.EndOfBlock)
  72. at_eol = (c.position() == current_pos)
  73. if self._history_index == len(self._history) or \
  74. not (self._history_prefix == '' and at_eol) or \
  75. not (self._get_edited_history(self._history_index)[:pos] == input_buffer[:pos]):
  76. self._history_prefix = input_buffer[:pos]
  77. # Perform the search.
  78. self.history_previous(self._history_prefix,
  79. as_prefix=not shift_modifier)
  80. # Go to the first line of the prompt for seemless history scrolling.
  81. # Emulate readline: keep the cursor position fixed for a prefix
  82. # search.
  83. cursor = self._get_prompt_cursor()
  84. if self._history_prefix:
  85. cursor.movePosition(QtGui.QTextCursor.Right,
  86. n=len(self._history_prefix))
  87. else:
  88. cursor.movePosition(QtGui.QTextCursor.EndOfBlock)
  89. self._set_cursor(cursor)
  90. return False
  91. return True
  92. def _down_pressed(self, shift_modifier):
  93. """ Called when the down key is pressed. Returns whether to continue
  94. processing the event.
  95. """
  96. end_cursor = self._get_end_cursor()
  97. if self._get_cursor().blockNumber() == end_cursor.blockNumber():
  98. # Bail out if we're locked.
  99. if self._history_locked() and not shift_modifier:
  100. return False
  101. # Perform the search.
  102. replaced = self.history_next(self._history_prefix,
  103. as_prefix=not shift_modifier)
  104. # Emulate readline: keep the cursor position fixed for a prefix
  105. # search. (We don't need to move the cursor to the end of the buffer
  106. # in the other case because this happens automatically when the
  107. # input buffer is set.)
  108. if self._history_prefix and replaced:
  109. cursor = self._get_prompt_cursor()
  110. cursor.movePosition(QtGui.QTextCursor.Right,
  111. n=len(self._history_prefix))
  112. self._set_cursor(cursor)
  113. return False
  114. return True
  115. #---------------------------------------------------------------------------
  116. # 'HistoryConsoleWidget' public interface
  117. #---------------------------------------------------------------------------
  118. def history_previous(self, substring='', as_prefix=True):
  119. """ If possible, set the input buffer to a previous history item.
  120. Parameters
  121. ----------
  122. substring : str, optional
  123. If specified, search for an item with this substring.
  124. as_prefix : bool, optional
  125. If True, the substring must match at the beginning (default).
  126. Returns
  127. -------
  128. Whether the input buffer was changed.
  129. """
  130. index = self._history_index
  131. replace = False
  132. while index > 0:
  133. index -= 1
  134. history = self._get_edited_history(index)
  135. if history == self.input_buffer:
  136. continue
  137. if (as_prefix and history.startswith(substring)) \
  138. or (not as_prefix and substring in history):
  139. replace = True
  140. break
  141. if replace:
  142. self._store_edits()
  143. self._history_index = index
  144. self.input_buffer = history
  145. return replace
  146. def history_next(self, substring='', as_prefix=True):
  147. """ If possible, set the input buffer to a subsequent history item.
  148. Parameters
  149. ----------
  150. substring : str, optional
  151. If specified, search for an item with this substring.
  152. as_prefix : bool, optional
  153. If True, the substring must match at the beginning (default).
  154. Returns
  155. -------
  156. Whether the input buffer was changed.
  157. """
  158. index = self._history_index
  159. replace = False
  160. while index < len(self._history):
  161. index += 1
  162. history = self._get_edited_history(index)
  163. if history == self.input_buffer:
  164. continue
  165. if (as_prefix and history.startswith(substring)) \
  166. or (not as_prefix and substring in history):
  167. replace = True
  168. break
  169. if replace:
  170. self._store_edits()
  171. self._history_index = index
  172. self.input_buffer = history
  173. return replace
  174. def history_tail(self, n=10):
  175. """ Get the local history list.
  176. Parameters
  177. ----------
  178. n : int
  179. The (maximum) number of history items to get.
  180. """
  181. return self._history[-n:]
  182. #---------------------------------------------------------------------------
  183. # 'HistoryConsoleWidget' protected interface
  184. #---------------------------------------------------------------------------
  185. def _history_locked(self):
  186. """ Returns whether history movement is locked.
  187. """
  188. return (self.history_lock and
  189. (self._get_edited_history(self._history_index) !=
  190. self.input_buffer) and
  191. (self._get_prompt_cursor().blockNumber() !=
  192. self._get_end_cursor().blockNumber()))
  193. def _get_edited_history(self, index):
  194. """ Retrieves a history item, possibly with temporary edits.
  195. """
  196. if index in self._history_edits:
  197. return self._history_edits[index]
  198. elif index == len(self._history):
  199. return unicode_type()
  200. return self._history[index]
  201. def _set_history(self, history):
  202. """ Replace the current history with a sequence of history items.
  203. """
  204. self._history = list(history)
  205. self._history_edits = {}
  206. self._history_index = len(self._history)
  207. def _store_edits(self):
  208. """ If there are edits to the current input buffer, store them.
  209. """
  210. current = self.input_buffer
  211. if self._history_index == len(self._history) or \
  212. self._history[self._history_index] != current:
  213. self._history_edits[self._history_index] = current