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