ptutils.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. """prompt-toolkit utilities
  2. Everything in this module is a private API,
  3. not to be used outside IPython.
  4. """
  5. # Copyright (c) IPython Development Team.
  6. # Distributed under the terms of the Modified BSD License.
  7. import unicodedata
  8. from wcwidth import wcwidth
  9. from IPython.core.completer import (
  10. provisionalcompleter, cursor_to_position,
  11. _deduplicate_completions)
  12. from prompt_toolkit.completion import Completer, Completion
  13. from prompt_toolkit.lexers import Lexer
  14. from prompt_toolkit.lexers import PygmentsLexer
  15. from prompt_toolkit.patch_stdout import patch_stdout
  16. from IPython.core.getipython import get_ipython
  17. import pygments.lexers as pygments_lexers
  18. import os
  19. import sys
  20. import traceback
  21. _completion_sentinel = object()
  22. def _elide_point(string: str, *, min_elide) -> str:
  23. """
  24. If a string is long enough, and has at least 3 dots,
  25. replace the middle part with ellipses.
  26. If a string naming a file is long enough, and has at least 3 slashes,
  27. replace the middle part with ellipses.
  28. If three consecutive dots, or two consecutive dots are encountered these are
  29. replaced by the equivalents HORIZONTAL ELLIPSIS or TWO DOT LEADER unicode
  30. equivalents
  31. """
  32. string = string.replace('...','\N{HORIZONTAL ELLIPSIS}')
  33. string = string.replace('..','\N{TWO DOT LEADER}')
  34. if len(string) < min_elide:
  35. return string
  36. object_parts = string.split('.')
  37. file_parts = string.split(os.sep)
  38. if file_parts[-1] == '':
  39. file_parts.pop()
  40. if len(object_parts) > 3:
  41. return "{}.{}\N{HORIZONTAL ELLIPSIS}{}.{}".format(
  42. object_parts[0],
  43. object_parts[1][:1],
  44. object_parts[-2][-1:],
  45. object_parts[-1],
  46. )
  47. elif len(file_parts) > 3:
  48. return ("{}" + os.sep + "{}\N{HORIZONTAL ELLIPSIS}{}" + os.sep + "{}").format(
  49. file_parts[0], file_parts[1][:1], file_parts[-2][-1:], file_parts[-1]
  50. )
  51. return string
  52. def _elide_typed(string: str, typed: str, *, min_elide: int) -> str:
  53. """
  54. Elide the middle of a long string if the beginning has already been typed.
  55. """
  56. if len(string) < min_elide:
  57. return string
  58. cut_how_much = len(typed)-3
  59. if cut_how_much < 7:
  60. return string
  61. if string.startswith(typed) and len(string)> len(typed):
  62. return f"{string[:3]}\N{HORIZONTAL ELLIPSIS}{string[cut_how_much:]}"
  63. return string
  64. def _elide(string: str, typed: str, min_elide) -> str:
  65. return _elide_typed(
  66. _elide_point(string, min_elide=min_elide),
  67. typed, min_elide=min_elide)
  68. def _adjust_completion_text_based_on_context(text, body, offset):
  69. if text.endswith('=') and len(body) > offset and body[offset] == '=':
  70. return text[:-1]
  71. else:
  72. return text
  73. class IPythonPTCompleter(Completer):
  74. """Adaptor to provide IPython completions to prompt_toolkit"""
  75. def __init__(self, ipy_completer=None, shell=None):
  76. if shell is None and ipy_completer is None:
  77. raise TypeError("Please pass shell=an InteractiveShell instance.")
  78. self._ipy_completer = ipy_completer
  79. self.shell = shell
  80. @property
  81. def ipy_completer(self):
  82. if self._ipy_completer:
  83. return self._ipy_completer
  84. else:
  85. return self.shell.Completer
  86. def get_completions(self, document, complete_event):
  87. if not document.current_line.strip():
  88. return
  89. # Some bits of our completion system may print stuff (e.g. if a module
  90. # is imported). This context manager ensures that doesn't interfere with
  91. # the prompt.
  92. with patch_stdout(), provisionalcompleter():
  93. body = document.text
  94. cursor_row = document.cursor_position_row
  95. cursor_col = document.cursor_position_col
  96. cursor_position = document.cursor_position
  97. offset = cursor_to_position(body, cursor_row, cursor_col)
  98. try:
  99. yield from self._get_completions(body, offset, cursor_position, self.ipy_completer)
  100. except Exception as e:
  101. try:
  102. exc_type, exc_value, exc_tb = sys.exc_info()
  103. traceback.print_exception(exc_type, exc_value, exc_tb)
  104. except AttributeError:
  105. print('Unrecoverable Error in completions')
  106. def _get_completions(self, body, offset, cursor_position, ipyc):
  107. """
  108. Private equivalent of get_completions() use only for unit_testing.
  109. """
  110. debug = getattr(ipyc, 'debug', False)
  111. completions = _deduplicate_completions(
  112. body, ipyc.completions(body, offset))
  113. for c in completions:
  114. if not c.text:
  115. # Guard against completion machinery giving us an empty string.
  116. continue
  117. text = unicodedata.normalize('NFC', c.text)
  118. # When the first character of the completion has a zero length,
  119. # then it's probably a decomposed unicode character. E.g. caused by
  120. # the "\dot" completion. Try to compose again with the previous
  121. # character.
  122. if wcwidth(text[0]) == 0:
  123. if cursor_position + c.start > 0:
  124. char_before = body[c.start - 1]
  125. fixed_text = unicodedata.normalize(
  126. 'NFC', char_before + text)
  127. # Yield the modified completion instead, if this worked.
  128. if wcwidth(text[0:1]) == 1:
  129. yield Completion(fixed_text, start_position=c.start - offset - 1)
  130. continue
  131. # TODO: Use Jedi to determine meta_text
  132. # (Jedi currently has a bug that results in incorrect information.)
  133. # meta_text = ''
  134. # yield Completion(m, start_position=start_pos,
  135. # display_meta=meta_text)
  136. display_text = c.text
  137. adjusted_text = _adjust_completion_text_based_on_context(
  138. c.text, body, offset
  139. )
  140. min_elide = 30 if self.shell is None else self.shell.min_elide
  141. if c.type == "function":
  142. yield Completion(
  143. adjusted_text,
  144. start_position=c.start - offset,
  145. display=_elide(
  146. display_text + "()",
  147. body[c.start : c.end],
  148. min_elide=min_elide,
  149. ),
  150. display_meta=c.type + c.signature,
  151. )
  152. else:
  153. yield Completion(
  154. adjusted_text,
  155. start_position=c.start - offset,
  156. display=_elide(
  157. display_text,
  158. body[c.start : c.end],
  159. min_elide=min_elide,
  160. ),
  161. display_meta=c.type,
  162. )
  163. class IPythonPTLexer(Lexer):
  164. """
  165. Wrapper around PythonLexer and BashLexer.
  166. """
  167. def __init__(self):
  168. l = pygments_lexers
  169. self.python_lexer = PygmentsLexer(l.Python3Lexer)
  170. self.shell_lexer = PygmentsLexer(l.BashLexer)
  171. self.magic_lexers = {
  172. 'HTML': PygmentsLexer(l.HtmlLexer),
  173. 'html': PygmentsLexer(l.HtmlLexer),
  174. 'javascript': PygmentsLexer(l.JavascriptLexer),
  175. 'js': PygmentsLexer(l.JavascriptLexer),
  176. 'perl': PygmentsLexer(l.PerlLexer),
  177. 'ruby': PygmentsLexer(l.RubyLexer),
  178. 'latex': PygmentsLexer(l.TexLexer),
  179. }
  180. def lex_document(self, document):
  181. text = document.text.lstrip()
  182. lexer = self.python_lexer
  183. if text.startswith('!') or text.startswith('%%bash'):
  184. lexer = self.shell_lexer
  185. elif text.startswith('%%'):
  186. for magic, l in self.magic_lexers.items():
  187. if text.startswith('%%' + magic):
  188. lexer = l
  189. break
  190. return lexer.lex_document(document)