在 ipython 5.0+ 中按 Ctrl-C 时在屏幕上留下不完整的行

Leave incomplete line on screen when hitting Ctrl-C in ipython 5.0+

在 IPython 的旧版本(我相信是 5.0 之前)中,如果我正在处理 line/block,突然发现我需要调查其他事情来完成它,我的方法是按 Ctrl-C,它在屏幕上留下了不完整的 line/block,但未执行,并给了我一个新的提示。也就是说,我会看到类似这样的内容:

In [1]: def foo():
   ...:     stuff^C  # <-- Just realized I needed to check something on stuff usage

In [2]:    # <-- cursor goes to new line, but old stuff still on terminal

在较新的 IPython 中(似乎已经从 readline 切换到 prompt_toolkit 作为 "CLI support framework"),Ctrl-C 的行为不同;现在,它不再给我换行符,而是重置当前换行符,丢弃我输入的所有内容并将光标返回到行首。

# Before:
In [1]: def foo():
   ...:     stuff

# After Ctrl-C:
In [1]:   # Hey, where'd everything go?

这非常烦人,因为我无法再看到或copy/paste我正在处理的代码,以便在我完成任何需要新提示的副任务后恢复我的工作。

我的问题是:有什么方法可以恢复旧的 IPython 行为,其中 Ctrl-C 执行以下操作:

  1. 不执行目前输入的line/block
  2. 留在屏幕上
  3. 能够选择(在配置时很好)是否添加到历史记录中(这将是个人偏好;你想要历史中的半成型的东西,还是只在终端上 copy/paste ?)
  4. 在目前输入的文本下方为我提供了一个新的提示

我到处搜索,我发现的最多的是 a bug report comment,它提到了这个新行为,作为“......与早期版本 IPython 的变化,但它是故意的。"

我无法在 IPython 或 prompt_toolkit 文档中找到任何关于修改行为的文档;我已经找到了很多这些处理程序的安装位置,但是试图通过猴子修补来改变当前行为的尝试失败了(坦率地说,猴子修补未记录的代码意味着我冒着破坏每次升级的风险,所以我想找到对此有一些半支持的修复;如果失败,hacky monkey-patching 是可以接受的)。

经过更多研究,我发现似乎是一种受支持的方法,它依赖于 IPython keyboard shortcuts documentation (documented slightly differently for 5.x and 6.x)。

解决方案是在~/.ipython/profile_default/startup中创建一个文件(任何名称,以.pyipy结尾都可以,例如fixctrlc.py),并添加以下内容:

def fix_ctrlc():
    '''Set up bindings so IPython 5.0+ responds to Ctrl-C as in pre-5.0

    Specific behavior is to have Ctrl-C leave existing typed command on
    screen and preserved in history. Easily modified to not put in history.

    Since this is run as a startup script, all work, including imports,
    is done in this function to avoid polluting global namespace.

    Updates made as needed at 
    '''
    from IPython import get_ipython
    from prompt_toolkit.enums import DEFAULT_BUFFER
    from prompt_toolkit.keys import Keys
    from prompt_toolkit.filters import HasFocus, ViInsertMode, EmacsInsertMode

    ip = get_ipython()

    # Determine if we're on a version of IPython that needs a fix,
    # acquire the key bindings registry from the appropriate location,
    # and establish closure state appropriate to that version of IPython
    try:
        try:
            # IPython 5-6; render_as_done doesn't exist, but manual print works
            registry = ip.pt_cli.application.key_bindings_registry
            redraw_args = {}
            doprint = True
        except AttributeError:
            # IPython 7+ (tested through 8.0.1)
            # render_as_done necessary, and removes need for print
            registry = ip.pt_app.key_bindings
            redraw_args = {'render_as_done': True}
            doprint = False
    except AttributeError:
        # On an old version of IPython that doesn't need the fix, or
        # a new version that changed the registry location. Nothing to do.
        return

    def on_ctrlc(event):
        text = event.cli.current_buffer.text.rstrip()
        if text:
            # Update cursor position to last non-space char in buffer (so Ctrl-C
            # with cursor in middle of block doesn't lose text typed after cursor)
            event.cli.current_buffer.cursor_position = len(text)
            event.cli.current_buffer.text = text

            # Redraw so cursor in correct position before print
            event.cli._redraw(**redraw_args)

            # (Optional) Put non-empty partial commands in history, not just left on screen
            # Delete to leave them on screen, but not in history
            event.cli.current_buffer.append_to_history()

            # Print a newline to move us past currently typed text so it's not
            # replaced on redraw
            if doprint:
                print()

            # Reset/redraw prompt
            event.cli.reset()

        # Clear active buffer, leaving you with fresh, empty prompt
        event.cli.current_buffer.reset()

    registry.add_binding(
            Keys.ControlC,
            filter=(HasFocus(DEFAULT_BUFFER) & (ViInsertMode() | EmacsInsertMode()))
            )(on_ctrlc)


fix_ctrlc()
del fix_ctrlc  # Avoid polluting global namespace

如果您找到更好的解决方案,请随时贡献力量。