##// END OF EJS Templates
MISC docs, cleanup and typing (in progress).
Matthias Bussonnier -
Show More
@@ -1,343 +1,343
1 1 #!/usr/bin/env python
2 2 # encoding: utf-8
3 3 """
4 4 The :class:`~traitlets.config.application.Application` object for the command
5 5 line :command:`ipython` program.
6 6 """
7 7
8 8 # Copyright (c) IPython Development Team.
9 9 # Distributed under the terms of the Modified BSD License.
10 10
11 11
12 12 import logging
13 13 import os
14 14 import sys
15 15 import warnings
16 16
17 17 from traitlets.config.loader import Config
18 18 from traitlets.config.application import boolean_flag, catch_config_error
19 19 from IPython.core import release
20 20 from IPython.core import usage
21 21 from IPython.core.completer import IPCompleter
22 22 from IPython.core.crashhandler import CrashHandler
23 23 from IPython.core.formatters import PlainTextFormatter
24 24 from IPython.core.history import HistoryManager
25 25 from IPython.core.application import (
26 26 ProfileDir, BaseIPythonApplication, base_flags, base_aliases
27 27 )
28 28 from IPython.core.magic import MagicsManager
29 29 from IPython.core.magics import (
30 30 ScriptMagics, LoggingMagics
31 31 )
32 32 from IPython.core.shellapp import (
33 33 InteractiveShellApp, shell_flags, shell_aliases
34 34 )
35 35 from IPython.extensions.storemagic import StoreMagics
36 36 from .interactiveshell import TerminalInteractiveShell
37 37 from IPython.paths import get_ipython_dir
38 38 from traitlets import (
39 39 Bool, List, default, observe, Type
40 40 )
41 41
42 42 #-----------------------------------------------------------------------------
43 43 # Globals, utilities and helpers
44 44 #-----------------------------------------------------------------------------
45 45
46 46 _examples = """
47 47 ipython --matplotlib # enable matplotlib integration
48 48 ipython --matplotlib=qt # enable matplotlib integration with qt4 backend
49 49
50 50 ipython --log-level=DEBUG # set logging to DEBUG
51 51 ipython --profile=foo # start with profile foo
52 52
53 53 ipython profile create foo # create profile foo w/ default config files
54 54 ipython help profile # show the help for the profile subcmd
55 55
56 56 ipython locate # print the path to the IPython directory
57 57 ipython locate profile foo # print the path to the directory for profile `foo`
58 58 """
59 59
60 60 #-----------------------------------------------------------------------------
61 61 # Crash handler for this application
62 62 #-----------------------------------------------------------------------------
63 63
64 64 class IPAppCrashHandler(CrashHandler):
65 65 """sys.excepthook for IPython itself, leaves a detailed report on disk."""
66 66
67 67 def __init__(self, app):
68 68 contact_name = release.author
69 69 contact_email = release.author_email
70 70 bug_tracker = 'https://github.com/ipython/ipython/issues'
71 71 super(IPAppCrashHandler,self).__init__(
72 72 app, contact_name, contact_email, bug_tracker
73 73 )
74 74
75 75 def make_report(self,traceback):
76 76 """Return a string containing a crash report."""
77 77
78 78 sec_sep = self.section_sep
79 79 # Start with parent report
80 80 report = [super(IPAppCrashHandler, self).make_report(traceback)]
81 81 # Add interactive-specific info we may have
82 82 rpt_add = report.append
83 83 try:
84 84 rpt_add(sec_sep+"History of session input:")
85 85 for line in self.app.shell.user_ns['_ih']:
86 86 rpt_add(line)
87 87 rpt_add('\n*** Last line of input (may not be in above history):\n')
88 88 rpt_add(self.app.shell._last_input_line+'\n')
89 89 except:
90 90 pass
91 91
92 92 return ''.join(report)
93 93
94 94 #-----------------------------------------------------------------------------
95 95 # Aliases and Flags
96 96 #-----------------------------------------------------------------------------
97 97 flags = dict(base_flags)
98 98 flags.update(shell_flags)
99 99 frontend_flags = {}
100 100 addflag = lambda *args: frontend_flags.update(boolean_flag(*args))
101 101 addflag('autoedit-syntax', 'TerminalInteractiveShell.autoedit_syntax',
102 102 'Turn on auto editing of files with syntax errors.',
103 103 'Turn off auto editing of files with syntax errors.'
104 104 )
105 105 addflag('simple-prompt', 'TerminalInteractiveShell.simple_prompt',
106 106 "Force simple minimal prompt using `raw_input`",
107 107 "Use a rich interactive prompt with prompt_toolkit",
108 108 )
109 109
110 110 addflag('banner', 'TerminalIPythonApp.display_banner',
111 111 "Display a banner upon starting IPython.",
112 112 "Don't display a banner upon starting IPython."
113 113 )
114 114 addflag('confirm-exit', 'TerminalInteractiveShell.confirm_exit',
115 115 """Set to confirm when you try to exit IPython with an EOF (Control-D
116 116 in Unix, Control-Z/Enter in Windows). By typing 'exit' or 'quit',
117 117 you can force a direct exit without any confirmation.""",
118 118 "Don't prompt the user when exiting."
119 119 )
120 120 addflag('term-title', 'TerminalInteractiveShell.term_title',
121 121 "Enable auto setting the terminal title.",
122 122 "Disable auto setting the terminal title."
123 123 )
124 124 classic_config = Config()
125 125 classic_config.InteractiveShell.cache_size = 0
126 126 classic_config.PlainTextFormatter.pprint = False
127 127 classic_config.TerminalInteractiveShell.prompts_class='IPython.terminal.prompts.ClassicPrompts'
128 128 classic_config.InteractiveShell.separate_in = ''
129 129 classic_config.InteractiveShell.separate_out = ''
130 130 classic_config.InteractiveShell.separate_out2 = ''
131 131 classic_config.InteractiveShell.colors = 'NoColor'
132 132 classic_config.InteractiveShell.xmode = 'Plain'
133 133
134 134 frontend_flags['classic']=(
135 135 classic_config,
136 136 "Gives IPython a similar feel to the classic Python prompt."
137 137 )
138 138 # # log doesn't make so much sense this way anymore
139 139 # paa('--log','-l',
140 140 # action='store_true', dest='InteractiveShell.logstart',
141 141 # help="Start logging to the default log file (./ipython_log.py).")
142 142 #
143 143 # # quick is harder to implement
144 144 frontend_flags['quick']=(
145 145 {'TerminalIPythonApp' : {'quick' : True}},
146 146 "Enable quick startup with no config files."
147 147 )
148 148
149 149 frontend_flags['i'] = (
150 150 {'TerminalIPythonApp' : {'force_interact' : True}},
151 151 """If running code from the command line, become interactive afterwards.
152 152 It is often useful to follow this with `--` to treat remaining flags as
153 153 script arguments.
154 154 """
155 155 )
156 156 flags.update(frontend_flags)
157 157
158 158 aliases = dict(base_aliases)
159 aliases.update(shell_aliases)
159 aliases.update(shell_aliases) # type: ignore[arg-type]
160 160
161 161 #-----------------------------------------------------------------------------
162 162 # Main classes and functions
163 163 #-----------------------------------------------------------------------------
164 164
165 165
166 166 class LocateIPythonApp(BaseIPythonApplication):
167 167 description = """print the path to the IPython dir"""
168 168 subcommands = dict(
169 169 profile=('IPython.core.profileapp.ProfileLocate',
170 170 "print the path to an IPython profile directory",
171 171 ),
172 172 )
173 173 def start(self):
174 174 if self.subapp is not None:
175 175 return self.subapp.start()
176 176 else:
177 177 print(self.ipython_dir)
178 178
179 179
180 180 class TerminalIPythonApp(BaseIPythonApplication, InteractiveShellApp):
181 181 name = u'ipython'
182 182 description = usage.cl_usage
183 crash_handler_class = IPAppCrashHandler
183 crash_handler_class = IPAppCrashHandler # typing: ignore[assignment]
184 184 examples = _examples
185 185
186 186 flags = flags
187 187 aliases = aliases
188 188 classes = List()
189 189
190 190 interactive_shell_class = Type(
191 191 klass=object, # use default_value otherwise which only allow subclasses.
192 192 default_value=TerminalInteractiveShell,
193 193 help="Class to use to instantiate the TerminalInteractiveShell object. Useful for custom Frontends"
194 194 ).tag(config=True)
195 195
196 196 @default('classes')
197 197 def _classes_default(self):
198 198 """This has to be in a method, for TerminalIPythonApp to be available."""
199 199 return [
200 200 InteractiveShellApp, # ShellApp comes before TerminalApp, because
201 201 self.__class__, # it will also affect subclasses (e.g. QtConsole)
202 202 TerminalInteractiveShell,
203 203 HistoryManager,
204 204 MagicsManager,
205 205 ProfileDir,
206 206 PlainTextFormatter,
207 207 IPCompleter,
208 208 ScriptMagics,
209 209 LoggingMagics,
210 210 StoreMagics,
211 211 ]
212 212
213 213 subcommands = dict(
214 214 profile = ("IPython.core.profileapp.ProfileApp",
215 215 "Create and manage IPython profiles."
216 216 ),
217 217 kernel = ("ipykernel.kernelapp.IPKernelApp",
218 218 "Start a kernel without an attached frontend."
219 219 ),
220 220 locate=('IPython.terminal.ipapp.LocateIPythonApp',
221 221 LocateIPythonApp.description
222 222 ),
223 223 history=('IPython.core.historyapp.HistoryApp',
224 224 "Manage the IPython history database."
225 225 ),
226 226 )
227 227
228 228
229 229 # *do* autocreate requested profile, but don't create the config file.
230 230 auto_create=Bool(True)
231 231 # configurables
232 232 quick = Bool(False,
233 233 help="""Start IPython quickly by skipping the loading of config files."""
234 234 ).tag(config=True)
235 235 @observe('quick')
236 236 def _quick_changed(self, change):
237 237 if change['new']:
238 238 self.load_config_file = lambda *a, **kw: None
239 239
240 240 display_banner = Bool(True,
241 241 help="Whether to display a banner upon starting IPython."
242 242 ).tag(config=True)
243 243
244 244 # if there is code of files to run from the cmd line, don't interact
245 245 # unless the --i flag (App.force_interact) is true.
246 246 force_interact = Bool(False,
247 247 help="""If a command or file is given via the command-line,
248 248 e.g. 'ipython foo.py', start an interactive shell after executing the
249 249 file or command."""
250 250 ).tag(config=True)
251 251 @observe('force_interact')
252 252 def _force_interact_changed(self, change):
253 253 if change['new']:
254 254 self.interact = True
255 255
256 256 @observe('file_to_run', 'code_to_run', 'module_to_run')
257 257 def _file_to_run_changed(self, change):
258 258 new = change['new']
259 259 if new:
260 260 self.something_to_run = True
261 261 if new and not self.force_interact:
262 262 self.interact = False
263 263
264 264 # internal, not-configurable
265 265 something_to_run=Bool(False)
266 266
267 267 @catch_config_error
268 268 def initialize(self, argv=None):
269 269 """Do actions after construct, but before starting the app."""
270 270 super(TerminalIPythonApp, self).initialize(argv)
271 271 if self.subapp is not None:
272 272 # don't bother initializing further, starting subapp
273 273 return
274 274 # print self.extra_args
275 275 if self.extra_args and not self.something_to_run:
276 276 self.file_to_run = self.extra_args[0]
277 277 self.init_path()
278 278 # create the shell
279 279 self.init_shell()
280 280 # and draw the banner
281 281 self.init_banner()
282 282 # Now a variety of things that happen after the banner is printed.
283 283 self.init_gui_pylab()
284 284 self.init_extensions()
285 285 self.init_code()
286 286
287 287 def init_shell(self):
288 288 """initialize the InteractiveShell instance"""
289 289 # Create an InteractiveShell instance.
290 290 # shell.display_banner should always be False for the terminal
291 291 # based app, because we call shell.show_banner() by hand below
292 292 # so the banner shows *before* all extension loading stuff.
293 293 self.shell = self.interactive_shell_class.instance(parent=self,
294 294 profile_dir=self.profile_dir,
295 295 ipython_dir=self.ipython_dir, user_ns=self.user_ns)
296 296 self.shell.configurables.append(self)
297 297
298 298 def init_banner(self):
299 299 """optionally display the banner"""
300 300 if self.display_banner and self.interact:
301 301 self.shell.show_banner()
302 302 # Make sure there is a space below the banner.
303 303 if self.log_level <= logging.INFO: print()
304 304
305 305 def _pylab_changed(self, name, old, new):
306 306 """Replace --pylab='inline' with --pylab='auto'"""
307 307 if new == 'inline':
308 308 warnings.warn("'inline' not available as pylab backend, "
309 309 "using 'auto' instead.")
310 310 self.pylab = 'auto'
311 311
312 312 def start(self):
313 313 if self.subapp is not None:
314 314 return self.subapp.start()
315 315 # perform any prexec steps:
316 316 if self.interact:
317 317 self.log.debug("Starting IPython's mainloop...")
318 318 self.shell.mainloop()
319 319 else:
320 320 self.log.debug("IPython not interactive...")
321 321 self.shell.restore_term_title()
322 322 if not self.shell.last_execution_succeeded:
323 323 sys.exit(1)
324 324
325 325 def load_default_config(ipython_dir=None):
326 326 """Load the default config file from the default ipython_dir.
327 327
328 328 This is useful for embedded shells.
329 329 """
330 330 if ipython_dir is None:
331 331 ipython_dir = get_ipython_dir()
332 332
333 333 profile_dir = os.path.join(ipython_dir, 'profile_default')
334 334 app = TerminalIPythonApp()
335 335 app.config_file_paths.append(profile_dir)
336 336 app.load_config_file()
337 337 return app.config
338 338
339 339 launch_new_instance = TerminalIPythonApp.launch_instance
340 340
341 341
342 342 if __name__ == '__main__':
343 343 launch_new_instance()
@@ -1,638 +1,638
1 1 """
2 2 Module to define and register Terminal IPython shortcuts with
3 3 :mod:`prompt_toolkit`
4 4 """
5 5
6 6 # Copyright (c) IPython Development Team.
7 7 # Distributed under the terms of the Modified BSD License.
8 8
9 9 import warnings
10 10 import signal
11 11 import sys
12 12 import re
13 13 import os
14 14 from typing import Callable, Dict, Union
15 15
16 16
17 17 from prompt_toolkit.application.current import get_app
18 18 from prompt_toolkit.enums import DEFAULT_BUFFER, SEARCH_BUFFER
19 19 from prompt_toolkit.filters import (
20 20 has_focus as has_focus_impl,
21 21 has_selection,
22 22 Condition,
23 23 vi_insert_mode,
24 24 emacs_insert_mode,
25 25 has_completions,
26 26 vi_mode,
27 27 )
28 28 from prompt_toolkit.key_binding.bindings.completion import (
29 29 display_completions_like_readline,
30 30 )
31 31 from prompt_toolkit.key_binding import KeyBindings
32 32 from prompt_toolkit.key_binding.bindings import named_commands as nc
33 33 from prompt_toolkit.key_binding.vi_state import InputMode, ViState
34 34 from prompt_toolkit.layout.layout import FocusableElement
35 35
36 36 from IPython.utils.decorators import undoc
37 37 from . import auto_match as match, auto_suggest
38 38
39 39
40 40 __all__ = ["create_ipython_shortcuts"]
41 41
42 42
43 43 try:
44 44 # only added in 3.0.30
45 45 from prompt_toolkit.filters import has_suggestion
46 46 except ImportError:
47 47
48 48 @undoc
49 49 @Condition
50 50 def has_suggestion():
51 51 buffer = get_app().current_buffer
52 52 return buffer.suggestion is not None and buffer.suggestion.text != ""
53 53
54 54
55 55 @undoc
56 56 @Condition
57 57 def cursor_in_leading_ws():
58 58 before = get_app().current_buffer.document.current_line_before_cursor
59 59 return (not before) or before.isspace()
60 60
61 61
62 62 def has_focus(value: FocusableElement):
63 63 """Wrapper around has_focus adding a nice `__name__` to tester function"""
64 64 tester = has_focus_impl(value).func
65 65 tester.__name__ = f"is_focused({value})"
66 66 return Condition(tester)
67 67
68 68
69 69 def create_ipython_shortcuts(shell, for_all_platforms: bool = False):
70 70 """Set up the prompt_toolkit keyboard shortcuts for IPython."""
71 71 # Warning: if possible, do NOT define handler functions in the locals
72 72 # scope of this function, instead define functions in the global
73 73 # scope, or a separate module, and include a user-friendly docstring
74 74 # describing the action.
75 75
76 76 kb = KeyBindings()
77 77 insert_mode = vi_insert_mode | emacs_insert_mode
78 78
79 79 if getattr(shell, "handle_return", None):
80 80 return_handler = shell.handle_return(shell)
81 81 else:
82 82 return_handler = newline_or_execute_outer(shell)
83 83
84 84 kb.add("enter", filter=(has_focus(DEFAULT_BUFFER) & ~has_selection & insert_mode))(
85 85 return_handler
86 86 )
87 87
88 88 @Condition
89 89 def ebivim():
90 90 return shell.emacs_bindings_in_vi_insert_mode
91 91
92 92 @kb.add(
93 93 "escape",
94 94 "enter",
95 95 filter=(has_focus(DEFAULT_BUFFER) & ~has_selection & insert_mode & ebivim),
96 96 )
97 97 def reformat_and_execute(event):
98 98 """Reformat code and execute it"""
99 99 reformat_text_before_cursor(
100 100 event.current_buffer, event.current_buffer.document, shell
101 101 )
102 102 event.current_buffer.validate_and_handle()
103 103
104 104 kb.add("c-\\")(quit)
105 105
106 106 kb.add("c-p", filter=(vi_insert_mode & has_focus(DEFAULT_BUFFER)))(
107 107 previous_history_or_previous_completion
108 108 )
109 109
110 110 kb.add("c-n", filter=(vi_insert_mode & has_focus(DEFAULT_BUFFER)))(
111 111 next_history_or_next_completion
112 112 )
113 113
114 114 kb.add("c-g", filter=(has_focus(DEFAULT_BUFFER) & has_completions))(
115 115 dismiss_completion
116 116 )
117 117
118 118 kb.add("c-c", filter=has_focus(DEFAULT_BUFFER))(reset_buffer)
119 119
120 120 kb.add("c-c", filter=has_focus(SEARCH_BUFFER))(reset_search_buffer)
121 121
122 122 supports_suspend = Condition(lambda: hasattr(signal, "SIGTSTP"))
123 123 kb.add("c-z", filter=supports_suspend)(suspend_to_bg)
124 124
125 125 # Ctrl+I == Tab
126 126 kb.add(
127 127 "tab",
128 128 filter=(
129 129 has_focus(DEFAULT_BUFFER)
130 130 & ~has_selection
131 131 & insert_mode
132 132 & cursor_in_leading_ws
133 133 ),
134 134 )(indent_buffer)
135 135 kb.add("c-o", filter=(has_focus(DEFAULT_BUFFER) & emacs_insert_mode))(
136 136 newline_autoindent_outer(shell.input_transformer_manager)
137 137 )
138 138
139 139 kb.add("f2", filter=has_focus(DEFAULT_BUFFER))(open_input_in_editor)
140 140
141 141 @Condition
142 142 def auto_match():
143 143 return shell.auto_match
144 144
145 145 def all_quotes_paired(quote, buf):
146 146 paired = True
147 147 i = 0
148 148 while i < len(buf):
149 149 c = buf[i]
150 150 if c == quote:
151 151 paired = not paired
152 152 elif c == "\\":
153 153 i += 1
154 154 i += 1
155 155 return paired
156 156
157 157 focused_insert = (vi_insert_mode | emacs_insert_mode) & has_focus(DEFAULT_BUFFER)
158 158 _preceding_text_cache: Dict[Union[str, Callable], Condition] = {}
159 159 _following_text_cache: Dict[Union[str, Callable], Condition] = {}
160 160
161 161 def preceding_text(pattern: Union[str, Callable]):
162 162 if pattern in _preceding_text_cache:
163 163 return _preceding_text_cache[pattern]
164 164
165 165 if callable(pattern):
166
167 166 def _preceding_text():
168 167 app = get_app()
169 168 before_cursor = app.current_buffer.document.current_line_before_cursor
170 return bool(pattern(before_cursor))
169 # mypy can't infer if(callable): https://github.com/python/mypy/issues/3603
170 return bool(pattern(before_cursor)) # type: ignore[operator]
171 171
172 172 else:
173 173 m = re.compile(pattern)
174 174
175 175 def _preceding_text():
176 176 app = get_app()
177 177 before_cursor = app.current_buffer.document.current_line_before_cursor
178 178 return bool(m.match(before_cursor))
179 179
180 180 _preceding_text.__name__ = f"preceding_text({pattern!r})"
181 181
182 182 condition = Condition(_preceding_text)
183 183 _preceding_text_cache[pattern] = condition
184 184 return condition
185 185
186 186 def following_text(pattern):
187 187 try:
188 188 return _following_text_cache[pattern]
189 189 except KeyError:
190 190 pass
191 191 m = re.compile(pattern)
192 192
193 193 def _following_text():
194 194 app = get_app()
195 195 return bool(m.match(app.current_buffer.document.current_line_after_cursor))
196 196
197 197 _following_text.__name__ = f"following_text({pattern!r})"
198 198
199 199 condition = Condition(_following_text)
200 200 _following_text_cache[pattern] = condition
201 201 return condition
202 202
203 203 @Condition
204 204 def not_inside_unclosed_string():
205 205 app = get_app()
206 206 s = app.current_buffer.document.text_before_cursor
207 207 # remove escaped quotes
208 208 s = s.replace('\\"', "").replace("\\'", "")
209 209 # remove triple-quoted string literals
210 210 s = re.sub(r"(?:\"\"\"[\s\S]*\"\"\"|'''[\s\S]*''')", "", s)
211 211 # remove single-quoted string literals
212 212 s = re.sub(r"""(?:"[^"]*["\n]|'[^']*['\n])""", "", s)
213 213 return not ('"' in s or "'" in s)
214 214
215 215 # auto match
216 216 auto_match_parens = {"(": match.parenthesis, "[": match.brackets, "{": match.braces}
217 217 for key, cmd in auto_match_parens.items():
218 218 kb.add(key, filter=focused_insert & auto_match & following_text(r"[,)}\]]|$"))(
219 219 cmd
220 220 )
221 221
222 222 kb.add(
223 223 '"',
224 224 filter=focused_insert
225 225 & auto_match
226 226 & not_inside_unclosed_string
227 227 & preceding_text(lambda line: all_quotes_paired('"', line))
228 228 & following_text(r"[,)}\]]|$"),
229 229 )(match.double_quote)
230 230
231 231 kb.add(
232 232 "'",
233 233 filter=focused_insert
234 234 & auto_match
235 235 & not_inside_unclosed_string
236 236 & preceding_text(lambda line: all_quotes_paired("'", line))
237 237 & following_text(r"[,)}\]]|$"),
238 238 )(match.single_quote)
239 239
240 240 kb.add(
241 241 '"',
242 242 filter=focused_insert
243 243 & auto_match
244 244 & not_inside_unclosed_string
245 245 & preceding_text(r'^.*""$'),
246 246 )(match.docstring_double_quotes)
247 247
248 248 kb.add(
249 249 "'",
250 250 filter=focused_insert
251 251 & auto_match
252 252 & not_inside_unclosed_string
253 253 & preceding_text(r"^.*''$"),
254 254 )(match.docstring_single_quotes)
255 255
256 256 # raw string
257 257 auto_match_parens_raw_string = {
258 258 "(": match.raw_string_parenthesis,
259 259 "[": match.raw_string_bracket,
260 260 "{": match.raw_string_braces,
261 261 }
262 262 for key, cmd in auto_match_parens_raw_string.items():
263 263 kb.add(
264 264 key,
265 265 filter=focused_insert & auto_match & preceding_text(r".*(r|R)[\"'](-*)$"),
266 266 )(cmd)
267 267
268 268 # just move cursor
269 269 kb.add(")", filter=focused_insert & auto_match & following_text(r"^\)"))(
270 270 match.skip_over
271 271 )
272 272 kb.add("]", filter=focused_insert & auto_match & following_text(r"^\]"))(
273 273 match.skip_over
274 274 )
275 275 kb.add("}", filter=focused_insert & auto_match & following_text(r"^\}"))(
276 276 match.skip_over
277 277 )
278 278 kb.add('"', filter=focused_insert & auto_match & following_text('^"'))(
279 279 match.skip_over
280 280 )
281 281 kb.add("'", filter=focused_insert & auto_match & following_text("^'"))(
282 282 match.skip_over
283 283 )
284 284
285 285 kb.add(
286 286 "backspace",
287 287 filter=focused_insert
288 288 & preceding_text(r".*\($")
289 289 & auto_match
290 290 & following_text(r"^\)"),
291 291 )(match.delete_pair)
292 292 kb.add(
293 293 "backspace",
294 294 filter=focused_insert
295 295 & preceding_text(r".*\[$")
296 296 & auto_match
297 297 & following_text(r"^\]"),
298 298 )(match.delete_pair)
299 299 kb.add(
300 300 "backspace",
301 301 filter=focused_insert
302 302 & preceding_text(r".*\{$")
303 303 & auto_match
304 304 & following_text(r"^\}"),
305 305 )(match.delete_pair)
306 306 kb.add(
307 307 "backspace",
308 308 filter=focused_insert
309 309 & preceding_text('.*"$')
310 310 & auto_match
311 311 & following_text('^"'),
312 312 )(match.delete_pair)
313 313 kb.add(
314 314 "backspace",
315 315 filter=focused_insert
316 316 & preceding_text(r".*'$")
317 317 & auto_match
318 318 & following_text(r"^'"),
319 319 )(match.delete_pair)
320 320
321 321 if shell.display_completions == "readlinelike":
322 322 kb.add(
323 323 "c-i",
324 324 filter=(
325 325 has_focus(DEFAULT_BUFFER)
326 326 & ~has_selection
327 327 & insert_mode
328 328 & ~cursor_in_leading_ws
329 329 ),
330 330 )(display_completions_like_readline)
331 331
332 332 if sys.platform == "win32" or for_all_platforms:
333 333 kb.add("c-v", filter=(has_focus(DEFAULT_BUFFER) & ~vi_mode))(win_paste)
334 334
335 335 focused_insert_vi = has_focus(DEFAULT_BUFFER) & vi_insert_mode
336 336
337 337 # autosuggestions
338 338 kb.add("end", filter=has_focus(DEFAULT_BUFFER) & (ebivim | ~vi_insert_mode))(
339 339 auto_suggest.accept_in_vi_insert_mode
340 340 )
341 341 kb.add("c-e", filter=focused_insert_vi & ebivim)(
342 342 auto_suggest.accept_in_vi_insert_mode
343 343 )
344 344 kb.add("c-f", filter=focused_insert_vi)(auto_suggest.accept)
345 345 kb.add("escape", "f", filter=focused_insert_vi & ebivim)(auto_suggest.accept_word)
346 346 kb.add("c-right", filter=has_suggestion & has_focus(DEFAULT_BUFFER))(
347 347 auto_suggest.accept_token
348 348 )
349 349 from functools import partial
350 350
351 351 kb.add("up", filter=has_suggestion & has_focus(DEFAULT_BUFFER))(
352 352 auto_suggest.swap_autosuggestion_up(shell.auto_suggest)
353 353 )
354 354 kb.add("down", filter=has_suggestion & has_focus(DEFAULT_BUFFER))(
355 355 auto_suggest.swap_autosuggestion_down(shell.auto_suggest)
356 356 )
357 357 kb.add("right", filter=has_suggestion & has_focus(DEFAULT_BUFFER))(
358 358 auto_suggest.accept_character
359 359 )
360 360 kb.add("left", filter=has_suggestion & has_focus(DEFAULT_BUFFER))(
361 361 auto_suggest.accept_and_move_cursor_left
362 362 )
363 363 kb.add("c-down", filter=has_suggestion & has_focus(DEFAULT_BUFFER))(
364 364 auto_suggest.accept_and_keep_cursor
365 365 )
366 366 kb.add("backspace", filter=has_suggestion & has_focus(DEFAULT_BUFFER))(
367 367 auto_suggest.backspace_and_resume_hint
368 368 )
369 369
370 370 # Simple Control keybindings
371 371 key_cmd_dict = {
372 372 "c-a": nc.beginning_of_line,
373 373 "c-b": nc.backward_char,
374 374 "c-k": nc.kill_line,
375 375 "c-w": nc.backward_kill_word,
376 376 "c-y": nc.yank,
377 377 "c-_": nc.undo,
378 378 }
379 379
380 380 for key, cmd in key_cmd_dict.items():
381 381 kb.add(key, filter=focused_insert_vi & ebivim)(cmd)
382 382
383 383 # Alt and Combo Control keybindings
384 384 keys_cmd_dict = {
385 385 # Control Combos
386 386 ("c-x", "c-e"): nc.edit_and_execute,
387 387 ("c-x", "e"): nc.edit_and_execute,
388 388 # Alt
389 389 ("escape", "b"): nc.backward_word,
390 390 ("escape", "c"): nc.capitalize_word,
391 391 ("escape", "d"): nc.kill_word,
392 392 ("escape", "h"): nc.backward_kill_word,
393 393 ("escape", "l"): nc.downcase_word,
394 394 ("escape", "u"): nc.uppercase_word,
395 395 ("escape", "y"): nc.yank_pop,
396 396 ("escape", "."): nc.yank_last_arg,
397 397 }
398 398
399 399 for keys, cmd in keys_cmd_dict.items():
400 400 kb.add(*keys, filter=focused_insert_vi & ebivim)(cmd)
401 401
402 402 def get_input_mode(self):
403 403 app = get_app()
404 404 app.ttimeoutlen = shell.ttimeoutlen
405 405 app.timeoutlen = shell.timeoutlen
406 406
407 407 return self._input_mode
408 408
409 409 def set_input_mode(self, mode):
410 410 shape = {InputMode.NAVIGATION: 2, InputMode.REPLACE: 4}.get(mode, 6)
411 411 cursor = "\x1b[{} q".format(shape)
412 412
413 413 sys.stdout.write(cursor)
414 414 sys.stdout.flush()
415 415
416 416 self._input_mode = mode
417 417
418 418 if shell.editing_mode == "vi" and shell.modal_cursor:
419 419 ViState._input_mode = InputMode.INSERT # type: ignore
420 420 ViState.input_mode = property(get_input_mode, set_input_mode) # type: ignore
421 421
422 422 return kb
423 423
424 424
425 425 def reformat_text_before_cursor(buffer, document, shell):
426 426 text = buffer.delete_before_cursor(len(document.text[: document.cursor_position]))
427 427 try:
428 428 formatted_text = shell.reformat_handler(text)
429 429 buffer.insert_text(formatted_text)
430 430 except Exception as e:
431 431 buffer.insert_text(text)
432 432
433 433
434 434 def newline_or_execute_outer(shell):
435 435 def newline_or_execute(event):
436 436 """When the user presses return, insert a newline or execute the code."""
437 437 b = event.current_buffer
438 438 d = b.document
439 439
440 440 if b.complete_state:
441 441 cc = b.complete_state.current_completion
442 442 if cc:
443 443 b.apply_completion(cc)
444 444 else:
445 445 b.cancel_completion()
446 446 return
447 447
448 448 # If there's only one line, treat it as if the cursor is at the end.
449 449 # See https://github.com/ipython/ipython/issues/10425
450 450 if d.line_count == 1:
451 451 check_text = d.text
452 452 else:
453 453 check_text = d.text[: d.cursor_position]
454 454 status, indent = shell.check_complete(check_text)
455 455
456 456 # if all we have after the cursor is whitespace: reformat current text
457 457 # before cursor
458 458 after_cursor = d.text[d.cursor_position :]
459 459 reformatted = False
460 460 if not after_cursor.strip():
461 461 reformat_text_before_cursor(b, d, shell)
462 462 reformatted = True
463 463 if not (
464 464 d.on_last_line
465 465 or d.cursor_position_row >= d.line_count - d.empty_line_count_at_the_end()
466 466 ):
467 467 if shell.autoindent:
468 468 b.insert_text("\n" + indent)
469 469 else:
470 470 b.insert_text("\n")
471 471 return
472 472
473 473 if (status != "incomplete") and b.accept_handler:
474 474 if not reformatted:
475 475 reformat_text_before_cursor(b, d, shell)
476 476 b.validate_and_handle()
477 477 else:
478 478 if shell.autoindent:
479 479 b.insert_text("\n" + indent)
480 480 else:
481 481 b.insert_text("\n")
482 482
483 483 newline_or_execute.__qualname__ = "newline_or_execute"
484 484
485 485 return newline_or_execute
486 486
487 487
488 488 def previous_history_or_previous_completion(event):
489 489 """
490 490 Control-P in vi edit mode on readline is history next, unlike default prompt toolkit.
491 491
492 492 If completer is open this still select previous completion.
493 493 """
494 494 event.current_buffer.auto_up()
495 495
496 496
497 497 def next_history_or_next_completion(event):
498 498 """
499 499 Control-N in vi edit mode on readline is history previous, unlike default prompt toolkit.
500 500
501 501 If completer is open this still select next completion.
502 502 """
503 503 event.current_buffer.auto_down()
504 504
505 505
506 506 def dismiss_completion(event):
507 507 """Dismiss completion"""
508 508 b = event.current_buffer
509 509 if b.complete_state:
510 510 b.cancel_completion()
511 511
512 512
513 513 def reset_buffer(event):
514 514 """Reset buffer"""
515 515 b = event.current_buffer
516 516 if b.complete_state:
517 517 b.cancel_completion()
518 518 else:
519 519 b.reset()
520 520
521 521
522 522 def reset_search_buffer(event):
523 523 """Reset search buffer"""
524 524 if event.current_buffer.document.text:
525 525 event.current_buffer.reset()
526 526 else:
527 527 event.app.layout.focus(DEFAULT_BUFFER)
528 528
529 529
530 530 def suspend_to_bg(event):
531 531 """Suspend to background"""
532 532 event.app.suspend_to_background()
533 533
534 534
535 535 def quit(event):
536 536 """
537 537 Quit application with ``SIGQUIT`` if supported or ``sys.exit`` otherwise.
538 538
539 539 On platforms that support SIGQUIT, send SIGQUIT to the current process.
540 540 On other platforms, just exit the process with a message.
541 541 """
542 542 sigquit = getattr(signal, "SIGQUIT", None)
543 543 if sigquit is not None:
544 544 os.kill(0, signal.SIGQUIT)
545 545 else:
546 546 sys.exit("Quit")
547 547
548 548
549 549 def indent_buffer(event):
550 550 """Indent buffer"""
551 551 event.current_buffer.insert_text(" " * 4)
552 552
553 553
554 554 @undoc
555 555 def newline_with_copy_margin(event):
556 556 """
557 557 DEPRECATED since IPython 6.0
558 558
559 559 See :any:`newline_autoindent_outer` for a replacement.
560 560
561 561 Preserve margin and cursor position when using
562 562 Control-O to insert a newline in EMACS mode
563 563 """
564 564 warnings.warn(
565 565 "`newline_with_copy_margin(event)` is deprecated since IPython 6.0. "
566 566 "see `newline_autoindent_outer(shell)(event)` for a replacement.",
567 567 DeprecationWarning,
568 568 stacklevel=2,
569 569 )
570 570
571 571 b = event.current_buffer
572 572 cursor_start_pos = b.document.cursor_position_col
573 573 b.newline(copy_margin=True)
574 574 b.cursor_up(count=1)
575 575 cursor_end_pos = b.document.cursor_position_col
576 576 if cursor_start_pos != cursor_end_pos:
577 577 pos_diff = cursor_start_pos - cursor_end_pos
578 578 b.cursor_right(count=pos_diff)
579 579
580 580
581 581 def newline_autoindent_outer(inputsplitter) -> Callable[..., None]:
582 582 """
583 583 Return a function suitable for inserting a indented newline after the cursor.
584 584
585 585 Fancier version of deprecated ``newline_with_copy_margin`` which should
586 586 compute the correct indentation of the inserted line. That is to say, indent
587 587 by 4 extra space after a function definition, class definition, context
588 588 manager... And dedent by 4 space after ``pass``, ``return``, ``raise ...``.
589 589 """
590 590
591 591 def newline_autoindent(event):
592 592 """Insert a newline after the cursor indented appropriately."""
593 593 b = event.current_buffer
594 594 d = b.document
595 595
596 596 if b.complete_state:
597 597 b.cancel_completion()
598 598 text = d.text[: d.cursor_position] + "\n"
599 599 _, indent = inputsplitter.check_complete(text)
600 600 b.insert_text("\n" + (" " * (indent or 0)), move_cursor=False)
601 601
602 602 newline_autoindent.__qualname__ = "newline_autoindent"
603 603
604 604 return newline_autoindent
605 605
606 606
607 607 def open_input_in_editor(event):
608 608 """Open code from input in external editor"""
609 609 event.app.current_buffer.open_in_editor()
610 610
611 611
612 612 if sys.platform == "win32":
613 613 from IPython.core.error import TryNext
614 614 from IPython.lib.clipboard import (
615 615 ClipboardEmpty,
616 616 win32_clipboard_get,
617 617 tkinter_clipboard_get,
618 618 )
619 619
620 620 @undoc
621 621 def win_paste(event):
622 622 try:
623 623 text = win32_clipboard_get()
624 624 except TryNext:
625 625 try:
626 626 text = tkinter_clipboard_get()
627 627 except (TryNext, ClipboardEmpty):
628 628 return
629 629 except ClipboardEmpty:
630 630 return
631 631 event.current_buffer.insert_text(text.replace("\t", " " * 4))
632 632
633 633 else:
634 634
635 635 @undoc
636 636 def win_paste(event):
637 637 """Stub used when auto-generating shortcuts for documentation"""
638 638 pass
@@ -1,287 +1,308
1 1 import re
2 2 import tokenize
3 3 from io import StringIO
4 from typing import Callable, List, Optional, Union
4 from typing import Callable, List, Optional, Union, Generator, Tuple
5 5
6 6 from prompt_toolkit.buffer import Buffer
7 7 from prompt_toolkit.key_binding import KeyPressEvent
8 8 from prompt_toolkit.key_binding.bindings import named_commands as nc
9 9 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory, Suggestion
10 10 from prompt_toolkit.document import Document
11 11 from prompt_toolkit.history import History
12 12 from prompt_toolkit.shortcuts import PromptSession
13 13
14 14 from IPython.utils.tokenutil import generate_tokens
15 15
16 16
17 17 def _get_query(document: Document):
18 18 return document.text.rsplit("\n", 1)[-1]
19 19
20 20
21 21 class NavigableAutoSuggestFromHistory(AutoSuggestFromHistory):
22 """ """
22 """
23 A subclass of AutoSuggestFromHistory that allow navigation to next/previous
24 suggestion from history. To do so it remembers the current position, but it
25 state need to carefully be cleared on the right events.
26 """
23 27
24 28 def __init__(
25 29 self,
26 30 ):
27 31 self.skip_lines = 0
28 32 self._connected_apps = []
29 33
30 34 def reset_history_position(self, _: Buffer):
31 35 self.skip_lines = 0
32 36
33 37 def disconnect(self):
34 38 for pt_app in self._connected_apps:
35 39 text_insert_event = pt_app.default_buffer.on_text_insert
36 40 text_insert_event.remove_handler(self.reset_history_position)
37 41
38 42 def connect(self, pt_app: PromptSession):
39 43 self._connected_apps.append(pt_app)
40 44 # note: `on_text_changed` could be used for a bit different behaviour
41 45 # on character deletion (i.e. reseting history position on backspace)
42 46 pt_app.default_buffer.on_text_insert.add_handler(self.reset_history_position)
43 47
44 48 def get_suggestion(
45 49 self, buffer: Buffer, document: Document
46 50 ) -> Optional[Suggestion]:
47 51 text = _get_query(document)
48 52
49 53 if text.strip():
50 54 for suggestion, _ in self._find_next_match(
51 55 text, self.skip_lines, buffer.history
52 56 ):
53 57 return Suggestion(suggestion)
54 58
55 59 return None
56 60
57 61 def _find_match(
58 62 self, text: str, skip_lines: float, history: History, previous: bool
59 ):
63 ) -> Generator[Tuple[str, float], None, None]:
64 """
65 text: str
66
67 skip_lines: float
68 float is used as the base value is +inf
69
70 Yields
71 ------
72 Tuple with:
73 str:
74 current suggestion.
75 float:
76 will actually yield only ints, which is passed back via skip_lines,
77 which may be a +inf (float)
78
79
80 """
60 81 line_number = -1
61 82 for string in reversed(list(history.get_strings())):
62 83 for line in reversed(string.splitlines()):
63 84 line_number += 1
64 85 if not previous and line_number < skip_lines:
65 86 continue
66 87 # do not return empty suggestions as these
67 88 # close the auto-suggestion overlay (and are useless)
68 89 if line.startswith(text) and len(line) > len(text):
69 90 yield line[len(text) :], line_number
70 91 if previous and line_number >= skip_lines:
71 92 return
72 93
73 94 def _find_next_match(self, text: str, skip_lines: float, history: History):
74 95 return self._find_match(text, skip_lines, history, previous=False)
75 96
76 97 def _find_previous_match(self, text: str, skip_lines: float, history: History):
77 98 return reversed(
78 99 list(self._find_match(text, skip_lines, history, previous=True))
79 100 )
80 101
81 102 def up(self, query: str, other_than: str, history: History):
82 103 for suggestion, line_number in self._find_next_match(
83 104 query, self.skip_lines, history
84 105 ):
85 106 # if user has history ['very.a', 'very', 'very.b'] and typed 'very'
86 107 # we want to switch from 'very.b' to 'very.a' because a) if the
87 108 # suggestion equals current text, prompt-toolkit aborts suggesting
88 109 # b) user likely would not be interested in 'very' anyways (they
89 110 # already typed it).
90 111 if query + suggestion != other_than:
91 112 self.skip_lines = line_number
92 113 break
93 114 else:
94 115 # no matches found, cycle back to beginning
95 116 self.skip_lines = 0
96 117
97 118 def down(self, query: str, other_than: str, history: History):
98 119 for suggestion, line_number in self._find_previous_match(
99 120 query, self.skip_lines, history
100 121 ):
101 122 if query + suggestion != other_than:
102 123 self.skip_lines = line_number
103 124 break
104 125 else:
105 126 # no matches found, cycle to end
106 127 for suggestion, line_number in self._find_previous_match(
107 128 query, float("Inf"), history
108 129 ):
109 130 if query + suggestion != other_than:
110 131 self.skip_lines = line_number
111 132 break
112 133
113 134
114 135 # Needed for to accept autosuggestions in vi insert mode
115 136 def accept_in_vi_insert_mode(event: KeyPressEvent):
116 137 """Apply autosuggestion if at end of line."""
117 138 buffer = event.current_buffer
118 139 d = buffer.document
119 140 after_cursor = d.text[d.cursor_position :]
120 141 lines = after_cursor.split("\n")
121 142 end_of_current_line = lines[0].strip()
122 143 suggestion = buffer.suggestion
123 144 if (suggestion is not None) and (suggestion.text) and (end_of_current_line == ""):
124 145 buffer.insert_text(suggestion.text)
125 146 else:
126 147 nc.end_of_line(event)
127 148
128 149
129 150 def accept(event: KeyPressEvent):
130 151 """Accept autosuggestion"""
131 152 buffer = event.current_buffer
132 153 suggestion = buffer.suggestion
133 154 if suggestion:
134 155 buffer.insert_text(suggestion.text)
135 156 else:
136 157 nc.forward_char(event)
137 158
138 159
139 160 def accept_word(event: KeyPressEvent):
140 161 """Fill partial autosuggestion by word"""
141 162 buffer = event.current_buffer
142 163 suggestion = buffer.suggestion
143 164 if suggestion:
144 165 t = re.split(r"(\S+\s+)", suggestion.text)
145 166 buffer.insert_text(next((x for x in t if x), ""))
146 167 else:
147 168 nc.forward_word(event)
148 169
149 170
150 171 def accept_character(event: KeyPressEvent):
151 172 """Fill partial autosuggestion by character"""
152 173 b = event.current_buffer
153 174 suggestion = b.suggestion
154 175 if suggestion and suggestion.text:
155 176 b.insert_text(suggestion.text[0])
156 177
157 178
158 179 def accept_and_keep_cursor(event: KeyPressEvent):
159 180 """Accept autosuggestion and keep cursor in place"""
160 181 buffer = event.current_buffer
161 182 old_position = buffer.cursor_position
162 183 suggestion = buffer.suggestion
163 184 if suggestion:
164 185 buffer.insert_text(suggestion.text)
165 186 buffer.cursor_position = old_position
166 187
167 188
168 189 def accept_and_move_cursor_left(event: KeyPressEvent):
169 190 """Accept autosuggestion and move cursor left in place"""
170 191 accept_and_keep_cursor(event)
171 192 nc.backward_char(event)
172 193
173 194
174 195 def backspace_and_resume_hint(event: KeyPressEvent):
175 196 """Resume autosuggestions after deleting last character"""
176 197 current_buffer = event.current_buffer
177 198
178 199 def resume_hinting(buffer: Buffer):
179 200 if buffer.auto_suggest:
180 201 suggestion = buffer.auto_suggest.get_suggestion(buffer, buffer.document)
181 202 if suggestion:
182 203 buffer.suggestion = suggestion
183 204 current_buffer.on_text_changed.remove_handler(resume_hinting)
184 205
185 206 current_buffer.on_text_changed.add_handler(resume_hinting)
186 207 nc.backward_delete_char(event)
187 208
188 209
189 210 def accept_token(event: KeyPressEvent):
190 211 """Fill partial autosuggestion by token"""
191 212 b = event.current_buffer
192 213 suggestion = b.suggestion
193 214
194 215 if suggestion:
195 216 prefix = _get_query(b.document)
196 217 text = prefix + suggestion.text
197 218
198 219 tokens: List[Optional[str]] = [None, None, None]
199 220 substrings = [""]
200 221 i = 0
201 222
202 223 for token in generate_tokens(StringIO(text).readline):
203 224 if token.type == tokenize.NEWLINE:
204 225 index = len(text)
205 226 else:
206 227 index = text.index(token[1], len(substrings[-1]))
207 228 substrings.append(text[:index])
208 229 tokenized_so_far = substrings[-1]
209 230 if tokenized_so_far.startswith(prefix):
210 231 if i == 0 and len(tokenized_so_far) > len(prefix):
211 232 tokens[0] = tokenized_so_far[len(prefix) :]
212 233 substrings.append(tokenized_so_far)
213 234 i += 1
214 235 tokens[i] = token[1]
215 236 if i == 2:
216 237 break
217 238 i += 1
218 239
219 240 if tokens[0]:
220 241 to_insert: str
221 242 insert_text = substrings[-2]
222 243 if tokens[1] and len(tokens[1]) == 1:
223 244 insert_text = substrings[-1]
224 245 to_insert = insert_text[len(prefix) :]
225 246 b.insert_text(to_insert)
226 247 return
227 248
228 249 nc.forward_word(event)
229 250
230 251
231 252 Provider = Union[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None]
232 253
233 254
234 255 def _swap_autosuggestion(
235 256 buffer: Buffer,
236 257 provider: NavigableAutoSuggestFromHistory,
237 258 direction_method: Callable,
238 259 ):
239 260 """
240 261 We skip most recent history entry (in either direction) if it equals the
241 262 current autosuggestion because if user cycles when auto-suggestion is shown
242 263 they most likely want something else than what was suggested (othewrise
243 264 they would have accepted the suggestion).
244 265 """
245 266 suggestion = buffer.suggestion
246 267 if not suggestion:
247 268 return
248 269
249 270 query = _get_query(buffer.document)
250 271 current = query + suggestion.text
251 272
252 273 direction_method(query=query, other_than=current, history=buffer.history)
253 274
254 275 new_suggestion = provider.get_suggestion(buffer, buffer.document)
255 276 buffer.suggestion = new_suggestion
256 277
257 278
258 279 def swap_autosuggestion_up(provider: Provider):
259 280 def swap_autosuggestion_up(event: KeyPressEvent):
260 281 """Get next autosuggestion from history."""
261 282 if not isinstance(provider, NavigableAutoSuggestFromHistory):
262 283 return
263 284
264 285 return _swap_autosuggestion(
265 286 buffer=event.current_buffer, provider=provider, direction_method=provider.up
266 287 )
267 288
268 289 swap_autosuggestion_up.__name__ = "swap_autosuggestion_up"
269 290 return swap_autosuggestion_up
270 291
271 292
272 293 def swap_autosuggestion_down(
273 294 provider: Union[AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None]
274 295 ):
275 296 def swap_autosuggestion_down(event: KeyPressEvent):
276 297 """Get previous autosuggestion from history."""
277 298 if not isinstance(provider, NavigableAutoSuggestFromHistory):
278 299 return
279 300
280 301 return _swap_autosuggestion(
281 302 buffer=event.current_buffer,
282 303 provider=provider,
283 304 direction_method=provider.down,
284 305 )
285 306
286 307 swap_autosuggestion_down.__name__ = "swap_autosuggestion_down"
287 308 return swap_autosuggestion_down
General Comments 0
You need to be logged in to leave comments. Login now