ptutils.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  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.utils.py3compat import PY3
  10. from IPython.core.completer import IPCompleter
  11. from prompt_toolkit.completion import Completer, Completion
  12. from prompt_toolkit.layout.lexers import Lexer
  13. from prompt_toolkit.layout.lexers import PygmentsLexer
  14. import pygments.lexers as pygments_lexers
  15. class IPythonPTCompleter(Completer):
  16. """Adaptor to provide IPython completions to prompt_toolkit"""
  17. def __init__(self, ipy_completer=None, shell=None, patch_stdout=None):
  18. if shell is None and ipy_completer is None:
  19. raise TypeError("Please pass shell=an InteractiveShell instance.")
  20. self._ipy_completer = ipy_completer
  21. self.shell = shell
  22. if patch_stdout is None:
  23. raise TypeError("Please pass patch_stdout")
  24. self.patch_stdout = patch_stdout
  25. @property
  26. def ipy_completer(self):
  27. if self._ipy_completer:
  28. return self._ipy_completer
  29. else:
  30. return self.shell.Completer
  31. def get_completions(self, document, complete_event):
  32. if not document.current_line.strip():
  33. return
  34. # Some bits of our completion system may print stuff (e.g. if a module
  35. # is imported). This context manager ensures that doesn't interfere with
  36. # the prompt.
  37. with self.patch_stdout():
  38. used, matches = self.ipy_completer.complete(
  39. line_buffer=document.current_line,
  40. cursor_pos=document.cursor_position_col
  41. )
  42. start_pos = -len(used)
  43. for m in matches:
  44. if not m:
  45. # Guard against completion machinery giving us an empty string.
  46. continue
  47. m = unicodedata.normalize('NFC', m)
  48. # When the first character of the completion has a zero length,
  49. # then it's probably a decomposed unicode character. E.g. caused by
  50. # the "\dot" completion. Try to compose again with the previous
  51. # character.
  52. if wcwidth(m[0]) == 0:
  53. if document.cursor_position + start_pos > 0:
  54. char_before = document.text[document.cursor_position + start_pos - 1]
  55. m = unicodedata.normalize('NFC', char_before + m)
  56. # Yield the modified completion instead, if this worked.
  57. if wcwidth(m[0:1]) == 1:
  58. yield Completion(m, start_position=start_pos - 1)
  59. continue
  60. # TODO: Use Jedi to determine meta_text
  61. # (Jedi currently has a bug that results in incorrect information.)
  62. # meta_text = ''
  63. # yield Completion(m, start_position=start_pos,
  64. # display_meta=meta_text)
  65. yield Completion(m, start_position=start_pos)
  66. class IPythonPTLexer(Lexer):
  67. """
  68. Wrapper around PythonLexer and BashLexer.
  69. """
  70. def __init__(self):
  71. l = pygments_lexers
  72. self.python_lexer = PygmentsLexer(l.Python3Lexer if PY3 else l.PythonLexer)
  73. self.shell_lexer = PygmentsLexer(l.BashLexer)
  74. self.magic_lexers = {
  75. 'HTML': PygmentsLexer(l.HtmlLexer),
  76. 'html': PygmentsLexer(l.HtmlLexer),
  77. 'javascript': PygmentsLexer(l.JavascriptLexer),
  78. 'js': PygmentsLexer(l.JavascriptLexer),
  79. 'perl': PygmentsLexer(l.PerlLexer),
  80. 'ruby': PygmentsLexer(l.RubyLexer),
  81. 'latex': PygmentsLexer(l.TexLexer),
  82. }
  83. def lex_document(self, cli, document):
  84. text = document.text.lstrip()
  85. lexer = self.python_lexer
  86. if text.startswith('!') or text.startswith('%%bash'):
  87. lexer = self.shell_lexer
  88. elif text.startswith('%%'):
  89. for magic, l in self.magic_lexers.items():
  90. if text.startswith('%%' + magic):
  91. lexer = l
  92. break
  93. return lexer.lex_document(cli, document)