• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jupyter / qtconsole / 8959463033

05 May 2024 03:58PM UTC coverage: 61.826% (+0.04%) from 61.783%
8959463033

push

github

ccordoba12
Back to work

1 of 1 new or added line in 1 file covered. (100.0%)

2 existing lines in 2 files now uncovered.

2899 of 4689 relevant lines covered (61.83%)

1.85 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

66.79
/qtconsole/console_widget.py
1
"""An abstract base class for console-type widgets."""
2

3
# Copyright (c) Jupyter Development Team.
4
# Distributed under the terms of the Modified BSD License.
5

6
from functools import partial
3✔
7
import os
3✔
8
import os.path
3✔
9
import re
3✔
10
import sys
3✔
11
from textwrap import dedent
3✔
12
import time
3✔
13
from unicodedata import category
3✔
14
import webbrowser
3✔
15

16
from qtpy import QT6
3✔
17
from qtpy import QtCore, QtGui, QtPrintSupport, QtWidgets
3✔
18

19
from qtconsole.rich_text import HtmlExporter
3✔
20
from qtconsole.util import MetaQObjectHasTraits, get_font, superQ
3✔
21

22
from traitlets.config.configurable import LoggingConfigurable
3✔
23
from traitlets import Bool, Enum, Integer, Unicode
3✔
24

25
from .ansi_code_processor import QtAnsiCodeProcessor
3✔
26
from .completion_widget import CompletionWidget
3✔
27
from .completion_html import CompletionHtml
3✔
28
from .completion_plain import CompletionPlain
3✔
29
from .kill_ring import QtKillRing
3✔
30
from .util import columnize
3✔
31

32

33
def is_letter_or_number(char):
3✔
34
    """ Returns whether the specified unicode character is a letter or a number.
35
    """
36
    cat = category(char)
3✔
37
    return cat.startswith('L') or cat.startswith('N')
3✔
38

39
def is_whitespace(char):
3✔
40
    """Check whether a given char counts as white space."""
41
    return category(char).startswith('Z')
3✔
42

43
#-----------------------------------------------------------------------------
44
# Classes
45
#-----------------------------------------------------------------------------
46

47
class ConsoleWidget(MetaQObjectHasTraits('NewBase', (LoggingConfigurable, superQ(QtWidgets.QWidget)), {})):
3✔
48
    """ An abstract base class for console-type widgets. This class has
49
        functionality for:
50

51
            * Maintaining a prompt and editing region
52
            * Providing the traditional Unix-style console keyboard shortcuts
53
            * Performing tab completion
54
            * Paging text
55
            * Handling ANSI escape codes
56

57
        ConsoleWidget also provides a number of utility methods that will be
58
        convenient to implementors of a console-style widget.
59
    """
60

61
    #------ Configuration ------------------------------------------------------
62

63
    ansi_codes = Bool(True, config=True,
3✔
64
        help="Whether to process ANSI escape codes."
65
    )
66
    buffer_size = Integer(500, config=True,
3✔
67
        help="""
68
        The maximum number of lines of text before truncation. Specifying a
69
        non-positive number disables text truncation (not recommended).
70
        """
71
    )
72
    execute_on_complete_input = Bool(True, config=True,
3✔
73
        help="""Whether to automatically execute on syntactically complete input.
74

75
        If False, Shift-Enter is required to submit each execution.
76
        Disabling this is mainly useful for non-Python kernels,
77
        where the completion check would be wrong.
78
        """
79
    )
80
    gui_completion = Enum(['plain', 'droplist', 'ncurses'], config=True,
3✔
81
                    default_value = 'ncurses',
82
                    help="""
83
                    The type of completer to use. Valid values are:
84

85
                    'plain'   : Show the available completion as a text list
86
                                Below the editing area.
87
                    'droplist': Show the completion in a drop down list navigable
88
                                by the arrow keys, and from which you can select
89
                                completion by pressing Return.
90
                    'ncurses' : Show the completion as a text list which is navigable by
91
                                `tab` and arrow keys.
92
                    """
93
    )
94
    gui_completion_height = Integer(0, config=True,
3✔
95
        help="""
96
        Set Height for completion.
97

98
        'droplist'
99
            Height in pixels.
100
        'ncurses'
101
            Maximum number of rows.
102
        """
103
    )
104
    # NOTE: this value can only be specified during initialization.
105
    kind = Enum(['plain', 'rich'], default_value='plain', config=True,
3✔
106
        help="""
107
        The type of underlying text widget to use. Valid values are 'plain',
108
        which specifies a QPlainTextEdit, and 'rich', which specifies a
109
        QTextEdit.
110
        """
111
    )
112
    # NOTE: this value can only be specified during initialization.
113
    paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
3✔
114
                  default_value='inside', config=True,
115
        help="""
116
        The type of paging to use. Valid values are:
117

118
        'inside'
119
           The widget pages like a traditional terminal.
120
        'hsplit'
121
           When paging is requested, the widget is split horizontally. The top
122
           pane contains the console, and the bottom pane contains the paged text.
123
        'vsplit'
124
           Similar to 'hsplit', except that a vertical splitter is used.
125
        'custom'
126
           No action is taken by the widget beyond emitting a
127
           'custom_page_requested(str)' signal.
128
        'none'
129
           The text is written directly to the console.
130
        """)
131

132
    scrollbar_visibility = Bool(True, config=True,
3✔
133
        help="""The visibility of the scrollar. If False then the scrollbar will be
134
        invisible."""
135
    )
136

137
    font_family = Unicode(config=True,
3✔
138
        help="""The font family to use for the console.
139
        On OSX this defaults to Monaco, on Windows the default is
140
        Consolas with fallback of Courier, and on other platforms
141
        the default is Monospace.
142
        """)
143
    def _font_family_default(self):
3✔
144
        if sys.platform == 'win32':
3✔
145
            # Consolas ships with Vista/Win7, fallback to Courier if needed
146
            return 'Consolas'
×
147
        elif sys.platform == 'darwin':
3✔
148
            # OSX always has Monaco, no need for a fallback
149
            return 'Monaco'
×
150
        else:
151
            # Monospace should always exist, no need for a fallback
152
            return 'Monospace'
3✔
153

154
    font_size = Integer(config=True,
3✔
155
        help="""The font size. If unconfigured, Qt will be entrusted
156
        with the size of the font.
157
        """)
158

159
    console_width = Integer(81, config=True,
3✔
160
        help="""The width of the console at start time in number
161
        of characters (will double with `hsplit` paging)
162
        """)
163

164
    console_height = Integer(25, config=True,
3✔
165
        help="""The height of the console at start time in number
166
        of characters (will double with `vsplit` paging)
167
        """)
168

169
    # Whether to override ShortcutEvents for the keybindings defined by this
170
    # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
171
    # priority (when it has focus) over, e.g., window-level menu shortcuts.
172
    override_shortcuts = Bool(False)
3✔
173

174
    # ------ Custom Qt Widgets -------------------------------------------------
175

176
    # For other projects to easily override the Qt widgets used by the console
177
    # (e.g. Spyder)
178
    custom_control = None
3✔
179
    custom_page_control = None
3✔
180

181
    #------ Signals ------------------------------------------------------------
182

183
    # Signals that indicate ConsoleWidget state.
184
    copy_available = QtCore.Signal(bool)
3✔
185
    redo_available = QtCore.Signal(bool)
3✔
186
    undo_available = QtCore.Signal(bool)
3✔
187

188
    # Signal emitted when paging is needed and the paging style has been
189
    # specified as 'custom'.
190
    custom_page_requested = QtCore.Signal(object)
3✔
191

192
    # Signal emitted when the font is changed.
193
    font_changed = QtCore.Signal(QtGui.QFont)
3✔
194

195
    #------ Protected class variables ------------------------------------------
196

197
    # control handles
198
    _control = None
3✔
199
    _page_control = None
3✔
200
    _splitter = None
3✔
201

202
    # When the control key is down, these keys are mapped.
203
    _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
3✔
204
                         QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
205
                         QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
206
                         QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
207
                         QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
208
                         QtCore.Qt.Key_H : QtCore.Qt.Key_Backspace, }
209
    if not sys.platform == 'darwin':
3✔
210
        # On OS X, Ctrl-E already does the right thing, whereas End moves the
211
        # cursor to the bottom of the buffer.
212
        _ctrl_down_remap[QtCore.Qt.Key_E] = QtCore.Qt.Key_End
3✔
213

214
    # The shortcuts defined by this widget. We need to keep track of these to
215
    # support 'override_shortcuts' above.
216
    _shortcuts = set(_ctrl_down_remap.keys()) | \
3✔
217
                     { QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
218
                       QtCore.Qt.Key_V }
219

220
    _temp_buffer_filled = False
3✔
221

222
    #---------------------------------------------------------------------------
223
    # 'QObject' interface
224
    #---------------------------------------------------------------------------
225

226
    def __init__(self, parent=None, **kw):
3✔
227
        """ Create a ConsoleWidget.
228

229
        Parameters
230
        ----------
231
        parent : QWidget, optional [default None]
232
            The parent for this widget.
233
        """
234
        super().__init__(**kw)
3✔
235
        if parent:
3✔
236
            self.setParent(parent)
×
237

238
        self._is_complete_msg_id = None
3✔
239
        self._is_complete_timeout = 0.1
3✔
240
        self._is_complete_max_time = None
3✔
241

242
        # While scrolling the pager on Mac OS X, it tears badly.  The
243
        # NativeGesture is platform and perhaps build-specific hence
244
        # we take adequate precautions here.
245
        self._pager_scroll_events = [QtCore.QEvent.Wheel]
3✔
246
        if hasattr(QtCore.QEvent, 'NativeGesture'):
3✔
247
            self._pager_scroll_events.append(QtCore.QEvent.NativeGesture)
2✔
248

249
        # Create the layout and underlying text widget.
250
        layout = QtWidgets.QStackedLayout(self)
3✔
251
        layout.setContentsMargins(0, 0, 0, 0)
3✔
252
        self._control = self._create_control()
3✔
253
        if self.paging in ('hsplit', 'vsplit'):
3✔
254
            self._splitter = QtWidgets.QSplitter()
×
255
            if self.paging == 'hsplit':
×
256
                self._splitter.setOrientation(QtCore.Qt.Horizontal)
×
257
            else:
258
                self._splitter.setOrientation(QtCore.Qt.Vertical)
×
259
            self._splitter.addWidget(self._control)
×
260
            layout.addWidget(self._splitter)
×
261
        else:
262
            layout.addWidget(self._control)
3✔
263

264
        # Create the paging widget, if necessary.
265
        if self.paging in ('inside', 'hsplit', 'vsplit'):
3✔
266
            self._page_control = self._create_page_control()
3✔
267
            if self._splitter:
3✔
268
                self._page_control.hide()
×
269
                self._splitter.addWidget(self._page_control)
×
270
            else:
271
                layout.addWidget(self._page_control)
3✔
272

273
        # Initialize protected variables. Some variables contain useful state
274
        # information for subclasses; they should be considered read-only.
275
        self._append_before_prompt_cursor = self._control.textCursor()
3✔
276
        self._ansi_processor = QtAnsiCodeProcessor()
3✔
277
        if self.gui_completion == 'ncurses':
3✔
278
            self._completion_widget = CompletionHtml(self, self.gui_completion_height)
3✔
279
        elif self.gui_completion == 'droplist':
×
280
            self._completion_widget = CompletionWidget(self, self.gui_completion_height)
×
281
        elif self.gui_completion == 'plain':
×
282
            self._completion_widget = CompletionPlain(self)
×
283

284
        self._continuation_prompt = '> '
3✔
285
        self._continuation_prompt_html = None
3✔
286
        self._executing = False
3✔
287
        self._filter_resize = False
3✔
288
        self._html_exporter = HtmlExporter(self._control)
3✔
289
        self._input_buffer_executing = ''
3✔
290
        self._input_buffer_pending = ''
3✔
291
        self._kill_ring = QtKillRing(self._control)
3✔
292
        self._prompt = ''
3✔
293
        self._prompt_html = None
3✔
294
        self._prompt_cursor = self._control.textCursor()
3✔
295
        self._prompt_sep = ''
3✔
296
        self._reading = False
3✔
297
        self._reading_callback = None
3✔
298
        self._tab_width = 4
3✔
299

300
        # List of strings pending to be appended as plain text in the widget.
301
        # The text is not immediately inserted when available to not
302
        # choke the Qt event loop with paint events for the widget in
303
        # case of lots of output from kernel.
304
        self._pending_insert_text = []
3✔
305

306
        # Timer to flush the pending stream messages. The interval is adjusted
307
        # later based on actual time taken for flushing a screen (buffer_size)
308
        # of output text.
309
        self._pending_text_flush_interval = QtCore.QTimer(self._control)
3✔
310
        self._pending_text_flush_interval.setInterval(100)
3✔
311
        self._pending_text_flush_interval.setSingleShot(True)
3✔
312
        self._pending_text_flush_interval.timeout.connect(
3✔
313
                                            self._on_flush_pending_stream_timer)
314

315
        # Set a monospaced font.
316
        self.reset_font()
3✔
317

318
        # Configure actions.
319
        action = QtWidgets.QAction('Print', None)
3✔
320
        action.setEnabled(True)
3✔
321
        printkey = QtGui.QKeySequence(QtGui.QKeySequence.Print)
3✔
322
        if printkey.matches("Ctrl+P") and sys.platform != 'darwin':
3✔
323
            # Only override the default if there is a collision.
324
            # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
325
            printkey = "Ctrl+Shift+P"
3✔
326
        action.setShortcut(printkey)
3✔
327
        action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
3✔
328
        action.triggered.connect(self.print_)
3✔
329
        self.addAction(action)
3✔
330
        self.print_action = action
3✔
331

332
        action = QtWidgets.QAction('Save as HTML/XML', None)
3✔
333
        action.setShortcut(QtGui.QKeySequence.Save)
3✔
334
        action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
3✔
335
        action.triggered.connect(self.export_html)
3✔
336
        self.addAction(action)
3✔
337
        self.export_action = action
3✔
338

339
        action = QtWidgets.QAction('Select All', None)
3✔
340
        action.setEnabled(True)
3✔
341
        selectall = QtGui.QKeySequence(QtGui.QKeySequence.SelectAll)
3✔
342
        if selectall.matches("Ctrl+A") and sys.platform != 'darwin':
3✔
343
            # Only override the default if there is a collision.
344
            # Qt ctrl = cmd on OSX, so the match gets a false positive on OSX.
345
            selectall = "Ctrl+Shift+A"
3✔
346
        action.setShortcut(selectall)
3✔
347
        action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
3✔
348
        action.triggered.connect(self.select_all_smart)
3✔
349
        self.addAction(action)
3✔
350
        self.select_all_action = action
3✔
351

352
        self.increase_font_size = QtWidgets.QAction("Bigger Font",
3✔
353
                self,
354
                shortcut=QtGui.QKeySequence.ZoomIn,
355
                shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
356
                statusTip="Increase the font size by one point",
357
                triggered=self._increase_font_size)
358
        self.addAction(self.increase_font_size)
3✔
359

360
        self.decrease_font_size = QtWidgets.QAction("Smaller Font",
3✔
361
                self,
362
                shortcut=QtGui.QKeySequence.ZoomOut,
363
                shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
364
                statusTip="Decrease the font size by one point",
365
                triggered=self._decrease_font_size)
366
        self.addAction(self.decrease_font_size)
3✔
367

368
        self.reset_font_size = QtWidgets.QAction("Normal Font",
3✔
369
                self,
370
                shortcut="Ctrl+0",
371
                shortcutContext=QtCore.Qt.WidgetWithChildrenShortcut,
372
                statusTip="Restore the Normal font size",
373
                triggered=self.reset_font)
374
        self.addAction(self.reset_font_size)
3✔
375

376
        # Accept drag and drop events here. Drops were already turned off
377
        # in self._control when that widget was created.
378
        self.setAcceptDrops(True)
3✔
379

380
    #---------------------------------------------------------------------------
381
    # Drag and drop support
382
    #---------------------------------------------------------------------------
383

384
    def dragEnterEvent(self, e):
3✔
385
        if e.mimeData().hasUrls():
×
386
            # The link action should indicate to that the drop will insert
387
            # the file anme.
388
            e.setDropAction(QtCore.Qt.LinkAction)
×
389
            e.accept()
×
390
        elif e.mimeData().hasText():
×
391
            # By changing the action to copy we don't need to worry about
392
            # the user accidentally moving text around in the widget.
393
            e.setDropAction(QtCore.Qt.CopyAction)
×
394
            e.accept()
×
395

396
    def dragMoveEvent(self, e):
3✔
397
        if e.mimeData().hasUrls():
×
398
            pass
×
399
        elif e.mimeData().hasText():
×
400
            cursor = self._control.cursorForPosition(e.pos())
×
401
            if self._in_buffer(cursor.position()):
×
402
                e.setDropAction(QtCore.Qt.CopyAction)
×
403
                self._control.setTextCursor(cursor)
×
404
            else:
405
                e.setDropAction(QtCore.Qt.IgnoreAction)
×
406
            e.accept()
×
407

408
    def dropEvent(self, e):
3✔
409
        if e.mimeData().hasUrls():
×
410
            self._keep_cursor_in_buffer()
×
411
            cursor = self._control.textCursor()
×
412
            filenames = [url.toLocalFile() for url in e.mimeData().urls()]
×
413
            text = ', '.join("'" + f.replace("'", "'\"'\"'") + "'"
×
414
                             for f in filenames)
415
            self._insert_plain_text_into_buffer(cursor, text)
×
416
        elif e.mimeData().hasText():
×
417
            cursor = self._control.cursorForPosition(e.pos())
×
418
            if self._in_buffer(cursor.position()):
×
419
                text = e.mimeData().text()
×
420
                self._insert_plain_text_into_buffer(cursor, text)
×
421

422
    def eventFilter(self, obj, event):
3✔
423
        """ Reimplemented to ensure a console-like behavior in the underlying
424
            text widgets.
425
        """
426
        etype = event.type()
3✔
427
        if etype == QtCore.QEvent.KeyPress:
3✔
428

429
            # Re-map keys for all filtered widgets.
430
            key = event.key()
3✔
431
            if self._control_key_down(event.modifiers()) and \
3✔
432
                    key in self._ctrl_down_remap:
433
                new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
×
434
                                            self._ctrl_down_remap[key],
435
                                            QtCore.Qt.NoModifier)
436
                QtWidgets.QApplication.instance().sendEvent(obj, new_event)
×
437
                return True
×
438

439
            elif obj == self._control:
3✔
440
                return self._event_filter_console_keypress(event)
3✔
441

442
            elif obj == self._page_control:
×
443
                return self._event_filter_page_keypress(event)
×
444

445
        # Make middle-click paste safe.
446
        elif getattr(event, 'button', False) and \
3✔
447
                etype == QtCore.QEvent.MouseButtonRelease and \
448
                event.button() == QtCore.Qt.MiddleButton and \
449
                obj == self._control.viewport():
450
            cursor = self._control.cursorForPosition(event.pos())
×
451
            self._control.setTextCursor(cursor)
×
452
            self.paste(QtGui.QClipboard.Selection)
×
453
            return True
×
454

455
        # Manually adjust the scrollbars *after* a resize event is dispatched.
456
        elif etype == QtCore.QEvent.Resize and not self._filter_resize:
3✔
457
            self._filter_resize = True
3✔
458
            QtWidgets.QApplication.instance().sendEvent(obj, event)
3✔
459
            self._adjust_scrollbars()
3✔
460
            self._filter_resize = False
3✔
461
            return True
3✔
462

463
        # Override shortcuts for all filtered widgets.
464
        elif etype == QtCore.QEvent.ShortcutOverride and \
3✔
465
                self.override_shortcuts and \
466
                self._control_key_down(event.modifiers()) and \
467
                event.key() in self._shortcuts:
468
            event.accept()
×
469

470
        # Handle scrolling of the vsplit pager. This hack attempts to solve
471
        # problems with tearing of the help text inside the pager window.  This
472
        # happens only on Mac OS X with both PySide and PyQt. This fix isn't
473
        # perfect but makes the pager more usable.
474
        elif etype in self._pager_scroll_events and \
3✔
475
                obj == self._page_control:
476
            self._page_control.repaint()
×
477
            return True
×
478

479
        elif etype == QtCore.QEvent.MouseMove:
3✔
480
            anchor = self._control.anchorAt(event.pos())
3✔
481
            if QT6:
3✔
UNCOV
482
                pos = event.globalPosition().toPoint()
1✔
483
            else:
484
                pos = event.globalPos()
2✔
485
            QtWidgets.QToolTip.showText(pos, anchor)
3✔
486

487
        return super().eventFilter(obj, event)
3✔
488

489
    #---------------------------------------------------------------------------
490
    # 'QWidget' interface
491
    #---------------------------------------------------------------------------
492

493
    def sizeHint(self):
3✔
494
        """ Reimplemented to suggest a size that is 80 characters wide and
495
            25 lines high.
496
        """
497
        font_metrics = QtGui.QFontMetrics(self.font)
3✔
498
        margin = (self._control.frameWidth() +
3✔
499
                  self._control.document().documentMargin()) * 2
500
        style = self.style()
3✔
501
        splitwidth = style.pixelMetric(QtWidgets.QStyle.PM_SplitterWidth)
3✔
502

503
        # Note 1: Despite my best efforts to take the various margins into
504
        # account, the width is still coming out a bit too small, so we include
505
        # a fudge factor of one character here.
506
        # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
507
        # to a Qt bug on certain Mac OS systems where it returns 0.
508
        width = self._get_font_width() * self.console_width + margin
3✔
509
        width += style.pixelMetric(QtWidgets.QStyle.PM_ScrollBarExtent)
3✔
510

511
        if self.paging == 'hsplit':
3✔
512
            width = width * 2 + splitwidth
×
513

514
        height = font_metrics.height() * self.console_height + margin
3✔
515
        if self.paging == 'vsplit':
3✔
516
            height = height * 2 + splitwidth
×
517

518
        return QtCore.QSize(int(width), int(height))
3✔
519

520
    #---------------------------------------------------------------------------
521
    # 'ConsoleWidget' public interface
522
    #---------------------------------------------------------------------------
523

524
    include_other_output = Bool(False, config=True,
3✔
525
        help="""Whether to include output from clients
526
        other than this one sharing the same kernel.
527

528
        Outputs are not displayed until enter is pressed.
529
        """
530
    )
531

532
    other_output_prefix = Unicode('[remote] ', config=True,
3✔
533
        help="""Prefix to add to outputs coming from clients other than this one.
534

535
        Only relevant if include_other_output is True.
536
        """
537
    )
538

539
    def can_copy(self):
3✔
540
        """ Returns whether text can be copied to the clipboard.
541
        """
542
        return self._control.textCursor().hasSelection()
×
543

544
    def can_cut(self):
3✔
545
        """ Returns whether text can be cut to the clipboard.
546
        """
547
        cursor = self._control.textCursor()
×
548
        return (cursor.hasSelection() and
×
549
                self._in_buffer(cursor.anchor()) and
550
                self._in_buffer(cursor.position()))
551

552
    def can_paste(self):
3✔
553
        """ Returns whether text can be pasted from the clipboard.
554
        """
555
        if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
×
556
            return bool(QtWidgets.QApplication.clipboard().text())
×
557
        return False
×
558

559
    def clear(self, keep_input=True):
3✔
560
        """ Clear the console.
561

562
        Parameters
563
        ----------
564
        keep_input : bool, optional (default True)
565
            If set, restores the old input buffer if a new prompt is written.
566
        """
567
        if self._executing:
×
568
            self._control.clear()
×
569
        else:
570
            if keep_input:
×
571
                input_buffer = self.input_buffer
×
572
            self._control.clear()
×
573
            self._show_prompt()
×
574
            if keep_input:
×
575
                self.input_buffer = input_buffer
×
576

577
    def copy(self):
3✔
578
        """ Copy the currently selected text to the clipboard.
579
        """
580
        self.layout().currentWidget().copy()
3✔
581

582
    def copy_anchor(self, anchor):
3✔
583
        """ Copy anchor text to the clipboard
584
        """
585
        QtWidgets.QApplication.clipboard().setText(anchor)
×
586

587
    def cut(self):
3✔
588
        """ Copy the currently selected text to the clipboard and delete it
589
            if it's inside the input buffer.
590
        """
591
        self.copy()
×
592
        if self.can_cut():
×
593
            self._control.textCursor().removeSelectedText()
×
594

595
    def _handle_is_complete_reply(self, msg):
3✔
596
        if msg['parent_header'].get('msg_id', 0) != self._is_complete_msg_id:
3✔
597
            return
3✔
598
        status = msg['content'].get('status', 'complete')
3✔
599
        indent = msg['content'].get('indent', '')
3✔
600
        self._trigger_is_complete_callback(status != 'incomplete', indent)
3✔
601

602
    def _trigger_is_complete_callback(self, complete=False, indent=''):
3✔
603
        if self._is_complete_msg_id is not None:
3✔
604
            self._is_complete_msg_id = None
3✔
605
            self._is_complete_callback(complete, indent)
3✔
606

607
    def _register_is_complete_callback(self, source, callback):
3✔
608
        if self._is_complete_msg_id is not None:
3✔
609
            if self._is_complete_max_time < time.time():
×
610
                # Second return while waiting for is_complete
611
                return
×
612
            else:
613
                # request timed out
614
                self._trigger_is_complete_callback()
×
615
        self._is_complete_max_time = time.time() + self._is_complete_timeout
3✔
616
        self._is_complete_callback = callback
3✔
617
        self._is_complete_msg_id = self.kernel_client.is_complete(source)
3✔
618

619
    def execute(self, source=None, hidden=False, interactive=False):
3✔
620
        """ Executes source or the input buffer, possibly prompting for more
621
        input.
622

623
        Parameters
624
        ----------
625
        source : str, optional
626

627
            The source to execute. If not specified, the input buffer will be
628
            used. If specified and 'hidden' is False, the input buffer will be
629
            replaced with the source before execution.
630

631
        hidden : bool, optional (default False)
632

633
            If set, no output will be shown and the prompt will not be modified.
634
            In other words, it will be completely invisible to the user that
635
            an execution has occurred.
636

637
        interactive : bool, optional (default False)
638

639
            Whether the console is to treat the source as having been manually
640
            entered by the user. The effect of this parameter depends on the
641
            subclass implementation.
642

643
        Raises
644
        ------
645
        RuntimeError
646
            If incomplete input is given and 'hidden' is True. In this case,
647
            it is not possible to prompt for more input.
648

649
        Returns
650
        -------
651
        A boolean indicating whether the source was executed.
652
        """
653
        # WARNING: The order in which things happen here is very particular, in
654
        # large part because our syntax highlighting is fragile. If you change
655
        # something, test carefully!
656

657
        # Decide what to execute.
658
        if source is None:
3✔
659
            source = self.input_buffer
3✔
660
        elif not hidden:
3✔
661
            self.input_buffer = source
3✔
662

663
        if hidden:
3✔
664
            self._execute(source, hidden)
×
665
        # Execute the source or show a continuation prompt if it is incomplete.
666
        elif interactive and self.execute_on_complete_input:
3✔
667
            self._register_is_complete_callback(
3✔
668
                source, partial(self.do_execute, source))
669
        else:
670
            self.do_execute(source, True, '')
3✔
671

672
    def do_execute(self, source, complete, indent):
3✔
673
        if complete:
3✔
674
            self._append_plain_text('\n')
3✔
675
            self._input_buffer_executing = self.input_buffer
3✔
676
            self._executing = True
3✔
677
            self._finalize_input_request()
3✔
678

679
            # Perform actual execution.
680
            self._execute(source, False)
3✔
681

682
        else:
683
            # Do this inside an edit block so continuation prompts are
684
            # removed seamlessly via undo/redo.
685
            cursor = self._get_end_cursor()
3✔
686
            cursor.beginEditBlock()
3✔
687
            try:
3✔
688
                cursor.insertText('\n')
3✔
689
                self._insert_continuation_prompt(cursor, indent)
3✔
690
            finally:
691
                cursor.endEditBlock()
3✔
692

693
            # Do not do this inside the edit block. It works as expected
694
            # when using a QPlainTextEdit control, but does not have an
695
            # effect when using a QTextEdit. I believe this is a Qt bug.
696
            self._control.moveCursor(QtGui.QTextCursor.End)
3✔
697

698
    def export_html(self):
3✔
699
        """ Shows a dialog to export HTML/XML in various formats.
700
        """
701
        self._html_exporter.export()
×
702

703
    def _finalize_input_request(self):
3✔
704
        """
705
        Set the widget to a non-reading state.
706
        """
707
        # Must set _reading to False before calling _prompt_finished
708
        self._reading = False
3✔
709
        self._prompt_finished()
3✔
710

711
        # There is no prompt now, so before_prompt_position is eof
712
        self._append_before_prompt_cursor.setPosition(
3✔
713
            self._get_end_cursor().position())
714

715
        # The maximum block count is only in effect during execution.
716
        # This ensures that _prompt_pos does not become invalid due to
717
        # text truncation.
718
        self._control.document().setMaximumBlockCount(self.buffer_size)
3✔
719

720
        # Setting a positive maximum block count will automatically
721
        # disable the undo/redo history, but just to be safe:
722
        self._control.setUndoRedoEnabled(False)
3✔
723

724
    def _get_input_buffer(self, force=False):
3✔
725
        """ The text that the user has entered entered at the current prompt.
726

727
        If the console is currently executing, the text that is executing will
728
        always be returned.
729
        """
730
        # If we're executing, the input buffer may not even exist anymore due to
731
        # the limit imposed by 'buffer_size'. Therefore, we store it.
732
        if self._executing and not force:
3✔
733
            return self._input_buffer_executing
3✔
734

735
        cursor = self._get_end_cursor()
3✔
736
        cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
3✔
737
        input_buffer = cursor.selection().toPlainText()
3✔
738

739
        # Strip out continuation prompts.
740
        return input_buffer.replace('\n' + self._continuation_prompt, '\n')
3✔
741

742
    def _set_input_buffer(self, string):
3✔
743
        """ Sets the text in the input buffer.
744

745
        If the console is currently executing, this call has no *immediate*
746
        effect. When the execution is finished, the input buffer will be updated
747
        appropriately.
748
        """
749
        # If we're executing, store the text for later.
750
        if self._executing:
3✔
751
            self._input_buffer_pending = string
3✔
752
            return
3✔
753

754
        # Remove old text.
755
        cursor = self._get_end_cursor()
3✔
756
        cursor.beginEditBlock()
3✔
757
        cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
3✔
758
        cursor.removeSelectedText()
3✔
759

760
        # Insert new text with continuation prompts.
761
        self._insert_plain_text_into_buffer(self._get_prompt_cursor(), string)
3✔
762
        cursor.endEditBlock()
3✔
763
        self._control.moveCursor(QtGui.QTextCursor.End)
3✔
764

765
    input_buffer = property(_get_input_buffer, _set_input_buffer)
3✔
766

767
    def _get_font(self):
3✔
768
        """ The base font being used by the ConsoleWidget.
769
        """
770
        return self._control.document().defaultFont()
3✔
771

772
    def _get_font_width(self, font=None):
3✔
773
        if font is None:
3✔
774
            font = self.font
3✔
775
        font_metrics = QtGui.QFontMetrics(font)
3✔
776
        if hasattr(font_metrics, 'horizontalAdvance'):
3✔
777
            return font_metrics.horizontalAdvance(' ')
3✔
778
        else:
779
            return font_metrics.width(' ')
×
780

781
    def _set_font(self, font):
3✔
782
        """ Sets the base font for the ConsoleWidget to the specified QFont.
783
        """
784
        self._control.setTabStopWidth(
3✔
785
            self.tab_width * self._get_font_width(font)
786
        )
787

788
        self._completion_widget.setFont(font)
3✔
789
        self._control.document().setDefaultFont(font)
3✔
790
        if self._page_control:
3✔
791
            self._page_control.document().setDefaultFont(font)
3✔
792

793
        self.font_changed.emit(font)
3✔
794

795
    font = property(_get_font, _set_font)
3✔
796

797
    def _set_completion_widget(self, gui_completion):
3✔
798
        """ Set gui completion widget.
799
        """
800
        if gui_completion == 'ncurses':
×
801
            self._completion_widget = CompletionHtml(self)
×
802
        elif gui_completion == 'droplist':
×
803
            self._completion_widget = CompletionWidget(self)
×
804
        elif gui_completion == 'plain':
×
805
            self._completion_widget = CompletionPlain(self)
×
806

807
        self.gui_completion = gui_completion
×
808

809
    def open_anchor(self, anchor):
3✔
810
        """ Open selected anchor in the default webbrowser
811
        """
812
        webbrowser.open( anchor )
×
813

814
    def paste(self, mode=QtGui.QClipboard.Clipboard):
3✔
815
        """ Paste the contents of the clipboard into the input region.
816

817
        Parameters
818
        ----------
819
        mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
820

821
            Controls which part of the system clipboard is used. This can be
822
            used to access the selection clipboard in X11 and the Find buffer
823
            in Mac OS. By default, the regular clipboard is used.
824
        """
825
        if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
3✔
826
            # Make sure the paste is safe.
827
            self._keep_cursor_in_buffer()
3✔
828
            cursor = self._control.textCursor()
3✔
829

830
            # Remove any trailing newline, which confuses the GUI and forces the
831
            # user to backspace.
832
            text = QtWidgets.QApplication.clipboard().text(mode).rstrip()
3✔
833

834
            # dedent removes "common leading whitespace" but to preserve relative
835
            # indent of multiline code, we have to compensate for any
836
            # leading space on the first line, if we're pasting into
837
            # an indented position.
838
            cursor_offset = cursor.position() - self._get_line_start_pos()
3✔
839
            if text.startswith(' ' * cursor_offset):
3✔
840
                text = text[cursor_offset:]
3✔
841

842
            self._insert_plain_text_into_buffer(cursor, dedent(text))
3✔
843

844
    def print_(self, printer = None):
3✔
845
        """ Print the contents of the ConsoleWidget to the specified QPrinter.
846
        """
847
        if (not printer):
×
848
            printer = QtPrintSupport.QPrinter()
×
849
            if(QtPrintSupport.QPrintDialog(printer).exec_() != QtPrintSupport.QPrintDialog.Accepted):
×
850
                return
×
851
        self._control.print_(printer)
×
852

853
    def prompt_to_top(self):
3✔
854
        """ Moves the prompt to the top of the viewport.
855
        """
856
        if not self._executing:
×
857
            prompt_cursor = self._get_prompt_cursor()
×
858
            if self._get_cursor().blockNumber() < prompt_cursor.blockNumber():
×
859
                self._set_cursor(prompt_cursor)
×
860
            self._set_top_cursor(prompt_cursor)
×
861

862
    def redo(self):
3✔
863
        """ Redo the last operation. If there is no operation to redo, nothing
864
            happens.
865
        """
866
        self._control.redo()
×
867

868
    def reset_font(self):
3✔
869
        """ Sets the font to the default fixed-width font for this platform.
870
        """
871
        if sys.platform == 'win32':
3✔
872
            # Consolas ships with Vista/Win7, fallback to Courier if needed
873
            fallback = 'Courier'
×
874
        elif sys.platform == 'darwin':
3✔
875
            # OSX always has Monaco
876
            fallback = 'Monaco'
×
877
        else:
878
            # Monospace should always exist
879
            fallback = 'Monospace'
3✔
880
        font = get_font(self.font_family, fallback)
3✔
881
        if self.font_size:
3✔
882
            font.setPointSize(self.font_size)
×
883
        else:
884
            font.setPointSize(QtWidgets.QApplication.instance().font().pointSize())
3✔
885
        font.setStyleHint(QtGui.QFont.TypeWriter)
3✔
886
        self._set_font(font)
3✔
887

888
    def change_font_size(self, delta):
3✔
889
        """Change the font size by the specified amount (in points).
890
        """
891
        font = self.font
×
892
        size = max(font.pointSize() + delta, 1) # minimum 1 point
×
893
        font.setPointSize(size)
×
894
        self._set_font(font)
×
895

896
    def _increase_font_size(self):
3✔
897
        self.change_font_size(1)
×
898

899
    def _decrease_font_size(self):
3✔
900
        self.change_font_size(-1)
×
901

902
    def select_all_smart(self):
3✔
903
        """ Select current cell, or, if already selected, the whole document.
904
        """
905
        c = self._get_cursor()
3✔
906
        sel_range = c.selectionStart(), c.selectionEnd()
3✔
907

908
        c.clearSelection()
3✔
909
        c.setPosition(self._get_prompt_cursor().position())
3✔
910
        c.setPosition(self._get_end_pos(),
3✔
911
                      mode=QtGui.QTextCursor.KeepAnchor)
912
        new_sel_range = c.selectionStart(), c.selectionEnd()
3✔
913
        if sel_range == new_sel_range:
3✔
914
            # cell already selected, expand selection to whole document
915
            self.select_document()
3✔
916
        else:
917
            # set cell selection as active selection
918
            self._control.setTextCursor(c)
3✔
919

920
    def select_document(self):
3✔
921
        """ Selects all the text in the buffer.
922
        """
923
        self._control.selectAll()
3✔
924

925
    def _get_tab_width(self):
3✔
926
        """ The width (in terms of space characters) for tab characters.
927
        """
928
        return self._tab_width
3✔
929

930
    def _set_tab_width(self, tab_width):
3✔
931
        """ Sets the width (in terms of space characters) for tab characters.
932
        """
933
        self._control.setTabStopWidth(tab_width * self._get_font_width())
3✔
934

935
        self._tab_width = tab_width
3✔
936

937
    tab_width = property(_get_tab_width, _set_tab_width)
3✔
938

939
    def undo(self):
3✔
940
        """ Undo the last operation. If there is no operation to undo, nothing
941
            happens.
942
        """
943
        self._control.undo()
×
944

945
    #---------------------------------------------------------------------------
946
    # 'ConsoleWidget' abstract interface
947
    #---------------------------------------------------------------------------
948

949
    def _is_complete(self, source, interactive):
3✔
950
        """ Returns whether 'source' can be executed. When triggered by an
951
            Enter/Return key press, 'interactive' is True; otherwise, it is
952
            False.
953
        """
954
        raise NotImplementedError
×
955

956
    def _execute(self, source, hidden):
3✔
957
        """ Execute 'source'. If 'hidden', do not show any output.
958
        """
959
        raise NotImplementedError
×
960

961
    def _prompt_started_hook(self):
3✔
962
        """ Called immediately after a new prompt is displayed.
963
        """
964
        pass
3✔
965

966
    def _prompt_finished_hook(self):
3✔
967
        """ Called immediately after a prompt is finished, i.e. when some input
968
            will be processed and a new prompt displayed.
969
        """
970
        pass
3✔
971

972
    def _up_pressed(self, shift_modifier):
3✔
973
        """ Called when the up key is pressed. Returns whether to continue
974
            processing the event.
975
        """
976
        return True
×
977

978
    def _down_pressed(self, shift_modifier):
3✔
979
        """ Called when the down key is pressed. Returns whether to continue
980
            processing the event.
981
        """
982
        return True
×
983

984
    def _tab_pressed(self):
3✔
985
        """ Called when the tab key is pressed. Returns whether to continue
986
            processing the event.
987
        """
988
        return True
3✔
989

990
    #--------------------------------------------------------------------------
991
    # 'ConsoleWidget' protected interface
992
    #--------------------------------------------------------------------------
993

994
    def _append_custom(self, insert, input, before_prompt=False, *args, **kwargs):
3✔
995
        """ A low-level method for appending content to the end of the buffer.
996

997
        If 'before_prompt' is enabled, the content will be inserted before the
998
        current prompt, if there is one.
999
        """
1000
        # Determine where to insert the content.
1001
        cursor = self._control.textCursor()
3✔
1002
        if before_prompt and (self._reading or not self._executing):
3✔
1003
            self._flush_pending_stream()
3✔
1004
            cursor._insert_mode=True
3✔
1005
            cursor.setPosition(self._append_before_prompt_pos)
3✔
1006
        else:
1007
            if insert != self._insert_plain_text:
3✔
1008
                self._flush_pending_stream()
3✔
1009
            cursor.movePosition(QtGui.QTextCursor.End)
3✔
1010

1011
        # Perform the insertion.
1012
        result = insert(cursor, input, *args, **kwargs)
3✔
1013
        return result
3✔
1014

1015
    def _append_block(self, block_format=None, before_prompt=False):
3✔
1016
        """ Appends an new QTextBlock to the end of the console buffer.
1017
        """
1018
        self._append_custom(self._insert_block, block_format, before_prompt)
3✔
1019

1020
    def _append_html(self, html, before_prompt=False):
3✔
1021
        """ Appends HTML at the end of the console buffer.
1022
        """
1023
        self._append_custom(self._insert_html, html, before_prompt)
3✔
1024

1025
    def _append_html_fetching_plain_text(self, html, before_prompt=False):
3✔
1026
        """ Appends HTML, then returns the plain text version of it.
1027
        """
1028
        return self._append_custom(self._insert_html_fetching_plain_text,
3✔
1029
                                   html, before_prompt)
1030

1031
    def _append_plain_text(self, text, before_prompt=False):
3✔
1032
        """ Appends plain text, processing ANSI codes if enabled.
1033
        """
1034
        self._append_custom(self._insert_plain_text, text, before_prompt)
3✔
1035

1036
    def _cancel_completion(self):
3✔
1037
        """ If text completion is progress, cancel it.
1038
        """
1039
        self._completion_widget.cancel_completion()
3✔
1040

1041
    def _clear_temporary_buffer(self):
3✔
1042
        """ Clears the "temporary text" buffer, i.e. all the text following
1043
            the prompt region.
1044
        """
1045
        # Select and remove all text below the input buffer.
1046
        cursor = self._get_prompt_cursor()
3✔
1047
        prompt = self._continuation_prompt.lstrip()
3✔
1048
        if(self._temp_buffer_filled):
3✔
1049
            self._temp_buffer_filled = False
×
1050
            while cursor.movePosition(QtGui.QTextCursor.NextBlock):
×
1051
                temp_cursor = QtGui.QTextCursor(cursor)
×
1052
                temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
×
1053
                text = temp_cursor.selection().toPlainText().lstrip()
×
1054
                if not text.startswith(prompt):
×
1055
                    break
×
1056
        else:
1057
            # We've reached the end of the input buffer and no text follows.
1058
            return
3✔
1059
        cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
×
1060
        cursor.movePosition(QtGui.QTextCursor.End,
×
1061
                            QtGui.QTextCursor.KeepAnchor)
1062
        cursor.removeSelectedText()
×
1063

1064
        # After doing this, we have no choice but to clear the undo/redo
1065
        # history. Otherwise, the text is not "temporary" at all, because it
1066
        # can be recalled with undo/redo. Unfortunately, Qt does not expose
1067
        # fine-grained control to the undo/redo system.
1068
        if self._control.isUndoRedoEnabled():
×
1069
            self._control.setUndoRedoEnabled(False)
×
1070
            self._control.setUndoRedoEnabled(True)
×
1071

1072
    def _complete_with_items(self, cursor, items):
3✔
1073
        """ Performs completion with 'items' at the specified cursor location.
1074
        """
1075
        self._cancel_completion()
×
1076

1077
        if len(items) == 1:
×
1078
            cursor.setPosition(self._control.textCursor().position(),
×
1079
                               QtGui.QTextCursor.KeepAnchor)
1080
            cursor.insertText(items[0])
×
1081

1082
        elif len(items) > 1:
×
1083
            current_pos = self._control.textCursor().position()
×
1084
            prefix = os.path.commonprefix(items)
×
1085
            if prefix:
×
1086
                cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
×
1087
                cursor.insertText(prefix)
×
1088
                current_pos = cursor.position()
×
1089

1090
            self._completion_widget.show_items(cursor, items,
×
1091
                                               prefix_length=len(prefix))
1092

1093
    def _fill_temporary_buffer(self, cursor, text, html=False):
3✔
1094
        """fill the area below the active editting zone with text"""
1095

1096
        current_pos = self._control.textCursor().position()
×
1097

1098
        cursor.beginEditBlock()
×
1099
        self._append_plain_text('\n')
×
1100
        self._page(text, html=html)
×
1101
        cursor.endEditBlock()
×
1102

1103
        cursor.setPosition(current_pos)
×
1104
        self._control.moveCursor(QtGui.QTextCursor.End)
×
1105
        self._control.setTextCursor(cursor)
×
1106

1107
        self._temp_buffer_filled = True
×
1108

1109

1110
    def _context_menu_make(self, pos):
3✔
1111
        """ Creates a context menu for the given QPoint (in widget coordinates).
1112
        """
1113
        menu = QtWidgets.QMenu(self)
×
1114

1115
        self.cut_action = menu.addAction('Cut', self.cut)
×
1116
        self.cut_action.setEnabled(self.can_cut())
×
1117
        self.cut_action.setShortcut(QtGui.QKeySequence.Cut)
×
1118

1119
        self.copy_action = menu.addAction('Copy', self.copy)
×
1120
        self.copy_action.setEnabled(self.can_copy())
×
1121
        self.copy_action.setShortcut(QtGui.QKeySequence.Copy)
×
1122

1123
        self.paste_action = menu.addAction('Paste', self.paste)
×
1124
        self.paste_action.setEnabled(self.can_paste())
×
1125
        self.paste_action.setShortcut(QtGui.QKeySequence.Paste)
×
1126

1127
        anchor = self._control.anchorAt(pos)
×
1128
        if anchor:
×
1129
            menu.addSeparator()
×
1130
            self.copy_link_action = menu.addAction(
×
1131
                'Copy Link Address', lambda: self.copy_anchor(anchor=anchor))
1132
            self.open_link_action = menu.addAction(
×
1133
                'Open Link', lambda: self.open_anchor(anchor=anchor))
1134

1135
        menu.addSeparator()
×
1136
        menu.addAction(self.select_all_action)
×
1137

1138
        menu.addSeparator()
×
1139
        menu.addAction(self.export_action)
×
1140
        menu.addAction(self.print_action)
×
1141

1142
        return menu
×
1143

1144
    def _control_key_down(self, modifiers, include_command=False):
3✔
1145
        """ Given a KeyboardModifiers flags object, return whether the Control
1146
        key is down.
1147

1148
        Parameters
1149
        ----------
1150
        include_command : bool, optional (default True)
1151
            Whether to treat the Command key as a (mutually exclusive) synonym
1152
            for Control when in Mac OS.
1153
        """
1154
        # Note that on Mac OS, ControlModifier corresponds to the Command key
1155
        # while MetaModifier corresponds to the Control key.
1156
        if sys.platform == 'darwin':
3✔
1157
            down = include_command and (modifiers & QtCore.Qt.ControlModifier)
×
1158
            return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
×
1159
        else:
1160
            return bool(modifiers & QtCore.Qt.ControlModifier)
3✔
1161

1162
    def _create_control(self):
3✔
1163
        """ Creates and connects the underlying text widget.
1164
        """
1165
        # Create the underlying control.
1166
        if self.custom_control:
3✔
1167
            control = self.custom_control()
×
1168
        elif self.kind == 'plain':
3✔
1169
            control = QtWidgets.QPlainTextEdit()
3✔
1170
        elif self.kind == 'rich':
3✔
1171
            control = QtWidgets.QTextEdit()
3✔
1172
            control.setAcceptRichText(False)
3✔
1173
            control.setMouseTracking(True)
3✔
1174

1175
        # Prevent the widget from handling drops, as we already provide
1176
        # the logic in this class.
1177
        control.setAcceptDrops(False)
3✔
1178

1179
        # Install event filters. The filter on the viewport is needed for
1180
        # mouse events.
1181
        control.installEventFilter(self)
3✔
1182
        control.viewport().installEventFilter(self)
3✔
1183

1184
        # Connect signals.
1185
        control.customContextMenuRequested.connect(
3✔
1186
            self._custom_context_menu_requested)
1187
        control.copyAvailable.connect(self.copy_available)
3✔
1188
        control.redoAvailable.connect(self.redo_available)
3✔
1189
        control.undoAvailable.connect(self.undo_available)
3✔
1190

1191
        # Hijack the document size change signal to prevent Qt from adjusting
1192
        # the viewport's scrollbar. We are relying on an implementation detail
1193
        # of Q(Plain)TextEdit here, which is potentially dangerous, but without
1194
        # this functionality we cannot create a nice terminal interface.
1195
        layout = control.document().documentLayout()
3✔
1196
        layout.documentSizeChanged.disconnect()
3✔
1197
        layout.documentSizeChanged.connect(self._adjust_scrollbars)
3✔
1198

1199
        # Configure the scrollbar policy
1200
        if self.scrollbar_visibility:
3✔
1201
            scrollbar_policy = QtCore.Qt.ScrollBarAlwaysOn
3✔
1202
        else :
1203
            scrollbar_policy = QtCore.Qt.ScrollBarAlwaysOff
×
1204

1205
        # Configure the control.
1206
        control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
3✔
1207
        control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
3✔
1208
        control.setReadOnly(True)
3✔
1209
        control.setUndoRedoEnabled(False)
3✔
1210
        control.setVerticalScrollBarPolicy(scrollbar_policy)
3✔
1211
        return control
3✔
1212

1213
    def _create_page_control(self):
3✔
1214
        """ Creates and connects the underlying paging widget.
1215
        """
1216
        if self.custom_page_control:
3✔
1217
            control = self.custom_page_control()
×
1218
        elif self.kind == 'plain':
3✔
1219
            control = QtWidgets.QPlainTextEdit()
3✔
1220
        elif self.kind == 'rich':
3✔
1221
            control = QtWidgets.QTextEdit()
3✔
1222
        control.installEventFilter(self)
3✔
1223
        viewport = control.viewport()
3✔
1224
        viewport.installEventFilter(self)
3✔
1225
        control.setReadOnly(True)
3✔
1226
        control.setUndoRedoEnabled(False)
3✔
1227

1228
        # Configure the scrollbar policy
1229
        if self.scrollbar_visibility:
3✔
1230
            scrollbar_policy = QtCore.Qt.ScrollBarAlwaysOn
3✔
1231
        else :
1232
            scrollbar_policy = QtCore.Qt.ScrollBarAlwaysOff
×
1233

1234
        control.setVerticalScrollBarPolicy(scrollbar_policy)
3✔
1235
        return control
3✔
1236

1237
    def _event_filter_console_keypress(self, event):
3✔
1238
        """ Filter key events for the underlying text widget to create a
1239
            console-like interface.
1240
        """
1241
        intercepted = False
3✔
1242
        cursor = self._control.textCursor()
3✔
1243
        position = cursor.position()
3✔
1244
        key = event.key()
3✔
1245
        ctrl_down = self._control_key_down(event.modifiers())
3✔
1246
        alt_down = event.modifiers() & QtCore.Qt.AltModifier
3✔
1247
        shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
3✔
1248

1249
        cmd_down = (
3✔
1250
            sys.platform == "darwin" and
1251
            self._control_key_down(event.modifiers(), include_command=True)
1252
        )
1253
        if cmd_down:
3✔
1254
            if key == QtCore.Qt.Key_Left:
×
1255
                key = QtCore.Qt.Key_Home
×
1256
            elif key == QtCore.Qt.Key_Right:
×
1257
                key = QtCore.Qt.Key_End
×
1258
            elif key == QtCore.Qt.Key_Up:
×
1259
                ctrl_down = True
×
1260
                key = QtCore.Qt.Key_Home
×
1261
            elif key == QtCore.Qt.Key_Down:
×
1262
                ctrl_down = True
×
1263
                key = QtCore.Qt.Key_End
×
1264
        #------ Special modifier logic -----------------------------------------
1265

1266
        if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
3✔
1267
            intercepted = True
3✔
1268

1269
            # Special handling when tab completing in text mode.
1270
            self._cancel_completion()
3✔
1271

1272
            if self._in_buffer(position):
3✔
1273
                # Special handling when a reading a line of raw input.
1274
                if self._reading:
3✔
1275
                    self._append_plain_text('\n')
3✔
1276
                    self._reading = False
3✔
1277
                    if self._reading_callback:
3✔
1278
                        self._reading_callback()
3✔
1279

1280
                # If the input buffer is a single line or there is only
1281
                # whitespace after the cursor, execute. Otherwise, split the
1282
                # line with a continuation prompt.
1283
                elif not self._executing:
3✔
1284
                    cursor.movePosition(QtGui.QTextCursor.End,
3✔
1285
                                        QtGui.QTextCursor.KeepAnchor)
1286
                    at_end = len(cursor.selectedText().strip()) == 0
3✔
1287
                    single_line = (self._get_end_cursor().blockNumber() ==
3✔
1288
                                   self._get_prompt_cursor().blockNumber())
1289
                    if (at_end or shift_down or single_line) and not ctrl_down:
3✔
1290
                        self.execute(interactive = not shift_down)
3✔
1291
                    else:
1292
                        # Do this inside an edit block for clean undo/redo.
1293
                        pos = self._get_input_buffer_cursor_pos()
×
1294
                        def callback(complete, indent):
×
1295
                            try:
×
1296
                                cursor.beginEditBlock()
×
1297
                                cursor.setPosition(position)
×
1298
                                cursor.insertText('\n')
×
1299
                                self._insert_continuation_prompt(cursor)
×
1300
                                if indent:
×
1301
                                    cursor.insertText(indent)
×
1302
                            finally:
1303
                                cursor.endEditBlock()
×
1304

1305
                            # Ensure that the whole input buffer is visible.
1306
                            # FIXME: This will not be usable if the input buffer is
1307
                            # taller than the console widget.
1308
                            self._control.moveCursor(QtGui.QTextCursor.End)
×
1309
                            self._control.setTextCursor(cursor)
×
1310
                        self._register_is_complete_callback(
×
1311
                            self._get_input_buffer()[:pos], callback)
1312

1313
        #------ Control/Cmd modifier -------------------------------------------
1314

1315
        elif ctrl_down:
3✔
1316
            if key == QtCore.Qt.Key_G:
3✔
1317
                self._keyboard_quit()
×
1318
                intercepted = True
×
1319

1320
            elif key == QtCore.Qt.Key_K:
3✔
1321
                if self._in_buffer(position):
3✔
1322
                    cursor.clearSelection()
3✔
1323
                    cursor.movePosition(QtGui.QTextCursor.EndOfLine,
3✔
1324
                                        QtGui.QTextCursor.KeepAnchor)
1325
                    if not cursor.hasSelection():
3✔
1326
                        # Line deletion (remove continuation prompt)
1327
                        cursor.movePosition(QtGui.QTextCursor.NextBlock,
×
1328
                                            QtGui.QTextCursor.KeepAnchor)
1329
                        cursor.movePosition(QtGui.QTextCursor.Right,
×
1330
                                            QtGui.QTextCursor.KeepAnchor,
1331
                                            len(self._continuation_prompt))
1332
                    self._kill_ring.kill_cursor(cursor)
3✔
1333
                    self._set_cursor(cursor)
3✔
1334
                intercepted = True
3✔
1335

1336
            elif key == QtCore.Qt.Key_L:
3✔
1337
                self.prompt_to_top()
×
1338
                intercepted = True
×
1339

1340
            elif key == QtCore.Qt.Key_O:
3✔
1341
                if self._page_control and self._page_control.isVisible():
×
1342
                    self._page_control.setFocus()
×
1343
                intercepted = True
×
1344

1345
            elif key == QtCore.Qt.Key_U:
3✔
1346
                if self._in_buffer(position):
×
1347
                    cursor.clearSelection()
×
1348
                    start_line = cursor.blockNumber()
×
1349
                    if start_line == self._get_prompt_cursor().blockNumber():
×
1350
                        offset = len(self._prompt)
×
1351
                    else:
1352
                        offset = len(self._continuation_prompt)
×
1353
                    cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
×
1354
                                        QtGui.QTextCursor.KeepAnchor)
1355
                    cursor.movePosition(QtGui.QTextCursor.Right,
×
1356
                                        QtGui.QTextCursor.KeepAnchor, offset)
1357
                    self._kill_ring.kill_cursor(cursor)
×
1358
                    self._set_cursor(cursor)
×
1359
                intercepted = True
×
1360

1361
            elif key == QtCore.Qt.Key_Y:
3✔
1362
                self._keep_cursor_in_buffer()
×
1363
                self._kill_ring.yank()
×
1364
                intercepted = True
×
1365

1366
            elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
3✔
1367
                if key == QtCore.Qt.Key_Backspace:
3✔
1368
                    cursor = self._get_word_start_cursor(position)
3✔
1369
                else: # key == QtCore.Qt.Key_Delete
1370
                    cursor = self._get_word_end_cursor(position)
3✔
1371
                cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
3✔
1372
                self._kill_ring.kill_cursor(cursor)
3✔
1373
                intercepted = True
3✔
1374

1375
            elif key == QtCore.Qt.Key_D:
3✔
1376
                if len(self.input_buffer) == 0 and not self._executing:
×
1377
                    self.exit_requested.emit(self)
×
1378
                # if executing and input buffer empty
1379
                elif len(self._get_input_buffer(force=True)) == 0:
×
1380
                    # input a EOT ansi control character
1381
                    self._control.textCursor().insertText(chr(4))
×
1382
                    new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
×
1383
                                                QtCore.Qt.Key_Return,
1384
                                                QtCore.Qt.NoModifier)
1385
                    QtWidgets.QApplication.instance().sendEvent(self._control, new_event)
×
1386
                    intercepted = True
×
1387
                else:
1388
                    new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
×
1389
                                                QtCore.Qt.Key_Delete,
1390
                                                QtCore.Qt.NoModifier)
1391
                    QtWidgets.QApplication.instance().sendEvent(self._control, new_event)
×
1392
                    intercepted = True
×
1393

1394
            elif key == QtCore.Qt.Key_Down:
3✔
1395
                self._scroll_to_end()
×
1396

1397
            elif key == QtCore.Qt.Key_Up:
3✔
1398
                self._control.verticalScrollBar().setValue(0)
×
1399
        #------ Alt modifier ---------------------------------------------------
1400

1401
        elif alt_down:
3✔
1402
            if key == QtCore.Qt.Key_B:
×
1403
                self._set_cursor(self._get_word_start_cursor(position))
×
1404
                intercepted = True
×
1405

1406
            elif key == QtCore.Qt.Key_F:
×
1407
                self._set_cursor(self._get_word_end_cursor(position))
×
1408
                intercepted = True
×
1409

1410
            elif key == QtCore.Qt.Key_Y:
×
1411
                self._kill_ring.rotate()
×
1412
                intercepted = True
×
1413

1414
            elif key == QtCore.Qt.Key_Backspace:
×
1415
                cursor = self._get_word_start_cursor(position)
×
1416
                cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
×
1417
                self._kill_ring.kill_cursor(cursor)
×
1418
                intercepted = True
×
1419

1420
            elif key == QtCore.Qt.Key_D:
×
1421
                cursor = self._get_word_end_cursor(position)
×
1422
                cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
×
1423
                self._kill_ring.kill_cursor(cursor)
×
1424
                intercepted = True
×
1425

1426
            elif key == QtCore.Qt.Key_Delete:
×
1427
                intercepted = True
×
1428

1429
            elif key == QtCore.Qt.Key_Greater:
×
1430
                self._control.moveCursor(QtGui.QTextCursor.End)
×
1431
                intercepted = True
×
1432

1433
            elif key == QtCore.Qt.Key_Less:
×
1434
                self._control.setTextCursor(self._get_prompt_cursor())
×
1435
                intercepted = True
×
1436

1437
        #------ No modifiers ---------------------------------------------------
1438

1439
        else:
1440
            self._trigger_is_complete_callback()
3✔
1441
            if shift_down:
3✔
1442
                anchormode = QtGui.QTextCursor.KeepAnchor
3✔
1443
            else:
1444
                anchormode = QtGui.QTextCursor.MoveAnchor
3✔
1445

1446
            if key == QtCore.Qt.Key_Escape:
3✔
1447
                self._keyboard_quit()
×
1448
                intercepted = True
×
1449

1450
            elif key == QtCore.Qt.Key_Up and not shift_down:
3✔
1451
                if self._reading or not self._up_pressed(shift_down):
×
1452
                    intercepted = True
×
1453
                else:
1454
                    prompt_line = self._get_prompt_cursor().blockNumber()
×
1455
                    intercepted = cursor.blockNumber() <= prompt_line
×
1456

1457
            elif key == QtCore.Qt.Key_Down and not shift_down:
3✔
1458
                if self._reading or not self._down_pressed(shift_down):
×
1459
                    intercepted = True
×
1460
                else:
1461
                    end_line = self._get_end_cursor().blockNumber()
×
1462
                    intercepted = cursor.blockNumber() == end_line
×
1463

1464
            elif key == QtCore.Qt.Key_Tab:
3✔
1465
                if not self._reading:
3✔
1466
                    if self._tab_pressed():
3✔
1467
                        self._indent(dedent=False)
3✔
1468
                    intercepted = True
3✔
1469

1470
            elif key == QtCore.Qt.Key_Backtab:
3✔
1471
                self._indent(dedent=True)
3✔
1472
                intercepted = True
3✔
1473

1474
            elif key == QtCore.Qt.Key_Left and not shift_down:
3✔
1475

1476
                # Move to the previous line
1477
                line, col = cursor.blockNumber(), cursor.columnNumber()
3✔
1478
                if line > self._get_prompt_cursor().blockNumber() and \
3✔
1479
                        col == len(self._continuation_prompt):
1480
                    self._control.moveCursor(QtGui.QTextCursor.PreviousBlock,
3✔
1481
                                             mode=anchormode)
1482
                    self._control.moveCursor(QtGui.QTextCursor.EndOfBlock,
3✔
1483
                                             mode=anchormode)
1484
                    intercepted = True
3✔
1485

1486
                # Regular left movement
1487
                else:
1488
                    intercepted = not self._in_buffer(position - 1)
3✔
1489

1490
            elif key == QtCore.Qt.Key_Right and not shift_down:
3✔
1491
                #original_block_number = cursor.blockNumber()
1492
                if position == self._get_line_end_pos():
3✔
1493
                    cursor.movePosition(QtGui.QTextCursor.NextBlock, mode=anchormode)
3✔
1494
                    cursor.movePosition(QtGui.QTextCursor.Right,
3✔
1495
                                        mode=anchormode,
1496
                                        n=len(self._continuation_prompt))
1497
                    self._control.setTextCursor(cursor)
3✔
1498
                else:
1499
                    self._control.moveCursor(QtGui.QTextCursor.Right,
×
1500
                                             mode=anchormode)
1501
                intercepted = True
3✔
1502

1503
            elif key == QtCore.Qt.Key_Home:
3✔
1504
                start_pos = self._get_line_start_pos()
×
1505

1506
                c = self._get_cursor()
×
1507
                spaces = self._get_leading_spaces()
×
1508
                if (c.position() > start_pos + spaces or
×
1509
                        c.columnNumber() == len(self._continuation_prompt)):
1510
                    start_pos += spaces     # Beginning of text
×
1511

1512
                if shift_down and self._in_buffer(position):
×
1513
                    if c.selectedText():
×
1514
                        sel_max = max(c.selectionStart(), c.selectionEnd())
×
1515
                        cursor.setPosition(sel_max,
×
1516
                                           QtGui.QTextCursor.MoveAnchor)
1517
                    cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
×
1518
                else:
1519
                    cursor.setPosition(start_pos)
×
1520
                self._set_cursor(cursor)
×
1521
                intercepted = True
×
1522

1523
            elif key == QtCore.Qt.Key_Backspace:
3✔
1524

1525
                # Line deletion (remove continuation prompt)
1526
                line, col = cursor.blockNumber(), cursor.columnNumber()
3✔
1527
                if not self._reading and \
3✔
1528
                        col == len(self._continuation_prompt) and \
1529
                        line > self._get_prompt_cursor().blockNumber():
1530
                    cursor.beginEditBlock()
×
1531
                    cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
×
1532
                                        QtGui.QTextCursor.KeepAnchor)
1533
                    cursor.removeSelectedText()
×
1534
                    cursor.deletePreviousChar()
×
1535
                    cursor.endEditBlock()
×
1536
                    intercepted = True
×
1537

1538
                # Regular backwards deletion
1539
                else:
1540
                    anchor = cursor.anchor()
3✔
1541
                    if anchor == position:
3✔
1542
                        intercepted = not self._in_buffer(position - 1)
3✔
1543
                    else:
1544
                        intercepted = not self._in_buffer(min(anchor, position))
×
1545

1546
            elif key == QtCore.Qt.Key_Delete:
3✔
1547

1548
                # Line deletion (remove continuation prompt)
1549
                if not self._reading and self._in_buffer(position) and \
×
1550
                        cursor.atBlockEnd() and not cursor.hasSelection():
1551
                    cursor.movePosition(QtGui.QTextCursor.NextBlock,
×
1552
                                        QtGui.QTextCursor.KeepAnchor)
1553
                    cursor.movePosition(QtGui.QTextCursor.Right,
×
1554
                                        QtGui.QTextCursor.KeepAnchor,
1555
                                        len(self._continuation_prompt))
1556
                    cursor.removeSelectedText()
×
1557
                    intercepted = True
×
1558

1559
                # Regular forwards deletion:
1560
                else:
1561
                    anchor = cursor.anchor()
×
1562
                    intercepted = (not self._in_buffer(anchor) or
×
1563
                                   not self._in_buffer(position))
1564

1565
        #------ Special sequences ----------------------------------------------
1566

1567
        if not intercepted:
3✔
1568
            if event.matches(QtGui.QKeySequence.Copy):
3✔
1569
                self.copy()
3✔
1570
                intercepted = True
3✔
1571

1572
            elif event.matches(QtGui.QKeySequence.Cut):
3✔
1573
                self.cut()
×
1574
                intercepted = True
×
1575

1576
            elif event.matches(QtGui.QKeySequence.Paste):
3✔
1577
                self.paste()
3✔
1578
                intercepted = True
3✔
1579

1580
        # Don't move the cursor if Control/Cmd is pressed to allow copy-paste
1581
        # using the keyboard in any part of the buffer. Also, permit scrolling
1582
        # with Page Up/Down keys. Finally, if we're executing, don't move the
1583
        # cursor (if even this made sense, we can't guarantee that the prompt
1584
        # position is still valid due to text truncation).
1585
        if not (self._control_key_down(event.modifiers(), include_command=True)
3✔
1586
                or key in (QtCore.Qt.Key_PageUp, QtCore.Qt.Key_PageDown)
1587
                or (self._executing and not self._reading)
1588
                or (event.text() == "" and not
1589
                    (not shift_down and key in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down)))):
1590
            self._keep_cursor_in_buffer()
3✔
1591

1592
        return intercepted
3✔
1593

1594
    def _event_filter_page_keypress(self, event):
3✔
1595
        """ Filter key events for the paging widget to create console-like
1596
            interface.
1597
        """
1598
        key = event.key()
×
1599
        ctrl_down = self._control_key_down(event.modifiers())
×
1600
        alt_down = event.modifiers() & QtCore.Qt.AltModifier
×
1601

1602
        if ctrl_down:
×
1603
            if key == QtCore.Qt.Key_O:
×
1604
                self._control.setFocus()
×
1605
                return True
×
1606

1607
        elif alt_down:
×
1608
            if key == QtCore.Qt.Key_Greater:
×
1609
                self._page_control.moveCursor(QtGui.QTextCursor.End)
×
1610
                return True
×
1611

1612
            elif key == QtCore.Qt.Key_Less:
×
1613
                self._page_control.moveCursor(QtGui.QTextCursor.Start)
×
1614
                return True
×
1615

1616
        elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
×
1617
            if self._splitter:
×
1618
                self._page_control.hide()
×
1619
                self._control.setFocus()
×
1620
            else:
1621
                self.layout().setCurrentWidget(self._control)
×
1622
                # re-enable buffer truncation after paging
1623
                self._control.document().setMaximumBlockCount(self.buffer_size)
×
1624
            return True
×
1625

1626
        elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return,
×
1627
                     QtCore.Qt.Key_Tab):
1628
            new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
×
1629
                                        QtCore.Qt.Key_PageDown,
1630
                                        QtCore.Qt.NoModifier)
1631
            QtWidgets.QApplication.instance().sendEvent(self._page_control, new_event)
×
1632
            return True
×
1633

1634
        elif key == QtCore.Qt.Key_Backspace:
×
1635
            new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
×
1636
                                        QtCore.Qt.Key_PageUp,
1637
                                        QtCore.Qt.NoModifier)
1638
            QtWidgets.QApplication.instance().sendEvent(self._page_control, new_event)
×
1639
            return True
×
1640

1641
        # vi/less -like key bindings
1642
        elif key == QtCore.Qt.Key_J:
×
1643
            new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
×
1644
                                        QtCore.Qt.Key_Down,
1645
                                        QtCore.Qt.NoModifier)
1646
            QtWidgets.QApplication.instance().sendEvent(self._page_control, new_event)
×
1647
            return True
×
1648

1649
        # vi/less -like key bindings
1650
        elif key == QtCore.Qt.Key_K:
×
1651
            new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
×
1652
                                        QtCore.Qt.Key_Up,
1653
                                        QtCore.Qt.NoModifier)
1654
            QtWidgets.QApplication.instance().sendEvent(self._page_control, new_event)
×
1655
            return True
×
1656

1657
        return False
×
1658

1659
    def _on_flush_pending_stream_timer(self):
3✔
1660
        """ Flush the pending stream output and change the
1661
        prompt position appropriately.
1662
        """
1663
        cursor = self._control.textCursor()
3✔
1664
        cursor.movePosition(QtGui.QTextCursor.End)
3✔
1665
        self._flush_pending_stream()
3✔
1666
        cursor.movePosition(QtGui.QTextCursor.End)
3✔
1667

1668
    def _flush_pending_stream(self):
3✔
1669
        """ Flush out pending text into the widget. """
1670
        text = self._pending_insert_text
3✔
1671
        self._pending_insert_text = []
3✔
1672
        buffer_size = self._control.document().maximumBlockCount()
3✔
1673
        if buffer_size > 0:
3✔
1674
            text = self._get_last_lines_from_list(text, buffer_size)
3✔
1675
        text = ''.join(text)
3✔
1676
        t = time.time()
3✔
1677
        self._insert_plain_text(self._get_end_cursor(), text, flush=True)
3✔
1678
        # Set the flush interval to equal the maximum time to update text.
1679
        self._pending_text_flush_interval.setInterval(
3✔
1680
            int(max(100, (time.time() - t) * 1000))
1681
        )
1682

1683
    def _get_cursor(self):
3✔
1684
        """ Get a cursor at the current insert position.
1685
        """
1686
        return self._control.textCursor()
3✔
1687

1688
    def _get_end_cursor(self):
3✔
1689
        """ Get a cursor at the last character of the current cell.
1690
        """
1691
        cursor = self._control.textCursor()
3✔
1692
        cursor.movePosition(QtGui.QTextCursor.End)
3✔
1693
        return cursor
3✔
1694

1695
    def _get_end_pos(self):
3✔
1696
        """ Get the position of the last character of the current cell.
1697
        """
1698
        return self._get_end_cursor().position()
3✔
1699

1700
    def _get_line_start_cursor(self):
3✔
1701
        """ Get a cursor at the first character of the current line.
1702
        """
1703
        cursor = self._control.textCursor()
3✔
1704
        start_line = cursor.blockNumber()
3✔
1705
        if start_line == self._get_prompt_cursor().blockNumber():
3✔
1706
            cursor.setPosition(self._prompt_pos)
3✔
1707
        else:
1708
            cursor.movePosition(QtGui.QTextCursor.StartOfLine)
3✔
1709
            cursor.setPosition(cursor.position() +
3✔
1710
                               len(self._continuation_prompt))
1711
        return cursor
3✔
1712

1713
    def _get_line_start_pos(self):
3✔
1714
        """ Get the position of the first character of the current line.
1715
        """
1716
        return self._get_line_start_cursor().position()
3✔
1717

1718
    def _get_line_end_cursor(self):
3✔
1719
        """ Get a cursor at the last character of the current line.
1720
        """
1721
        cursor = self._control.textCursor()
3✔
1722
        cursor.movePosition(QtGui.QTextCursor.EndOfLine)
3✔
1723
        return cursor
3✔
1724

1725
    def _get_line_end_pos(self):
3✔
1726
        """ Get the position of the last character of the current line.
1727
        """
1728
        return self._get_line_end_cursor().position()
3✔
1729

1730
    def _get_input_buffer_cursor_column(self):
3✔
1731
        """ Get the column of the cursor in the input buffer, excluding the
1732
            contribution by the prompt, or -1 if there is no such column.
1733
        """
1734
        prompt = self._get_input_buffer_cursor_prompt()
3✔
1735
        if prompt is None:
3✔
1736
            return -1
3✔
1737
        else:
1738
            cursor = self._control.textCursor()
×
1739
            return cursor.columnNumber() - len(prompt)
×
1740

1741
    def _get_input_buffer_cursor_line(self):
3✔
1742
        """ Get the text of the line of the input buffer that contains the
1743
            cursor, or None if there is no such line.
1744
        """
1745
        prompt = self._get_input_buffer_cursor_prompt()
×
1746
        if prompt is None:
×
1747
            return None
×
1748
        else:
1749
            cursor = self._control.textCursor()
×
1750
            text = cursor.block().text()
×
1751
            return text[len(prompt):]
×
1752

1753
    def _get_input_buffer_cursor_pos(self):
3✔
1754
        """Get the cursor position within the input buffer."""
1755
        cursor = self._control.textCursor()
3✔
1756
        cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
3✔
1757
        input_buffer = cursor.selection().toPlainText()
3✔
1758

1759
        # Don't count continuation prompts
1760
        return len(input_buffer.replace('\n' + self._continuation_prompt, '\n'))
3✔
1761

1762
    def _get_input_buffer_cursor_prompt(self):
3✔
1763
        """ Returns the (plain text) prompt for line of the input buffer that
1764
            contains the cursor, or None if there is no such line.
1765
        """
1766
        if self._executing:
3✔
1767
            return None
3✔
1768
        cursor = self._control.textCursor()
×
1769
        if cursor.position() >= self._prompt_pos:
×
1770
            if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
×
1771
                return self._prompt
×
1772
            else:
1773
                return self._continuation_prompt
×
1774
        else:
1775
            return None
×
1776

1777
    def _get_last_lines(self, text, num_lines, return_count=False):
3✔
1778
        """ Get the last specified number of lines of text (like `tail -n`).
1779
        If return_count is True, returns a tuple of clipped text and the
1780
        number of lines in the clipped text.
1781
        """
1782
        pos = len(text)
3✔
1783
        if pos < num_lines:
3✔
1784
            if return_count:
3✔
1785
                return text, text.count('\n') if return_count else text
3✔
1786
            else:
1787
                return text
3✔
1788
        i = 0
3✔
1789
        while i < num_lines:
3✔
1790
            pos = text.rfind('\n', None, pos)
3✔
1791
            if pos == -1:
3✔
1792
                pos = None
3✔
1793
                break
3✔
1794
            i += 1
3✔
1795
        if return_count:
3✔
1796
            return text[pos:], i
3✔
1797
        else:
1798
            return text[pos:]
3✔
1799

1800
    def _get_last_lines_from_list(self, text_list, num_lines):
3✔
1801
        """ Get the list of text clipped to last specified lines.
1802
        """
1803
        ret = []
3✔
1804
        lines_pending = num_lines
3✔
1805
        for text in reversed(text_list):
3✔
1806
            text, lines_added = self._get_last_lines(text, lines_pending,
3✔
1807
                                                     return_count=True)
1808
            ret.append(text)
3✔
1809
            lines_pending -= lines_added
3✔
1810
            if lines_pending <= 0:
3✔
1811
                break
×
1812
        return ret[::-1]
3✔
1813

1814
    def _get_leading_spaces(self):
3✔
1815
        """ Get the number of leading spaces of the current line.
1816
        """
1817

1818
        cursor = self._get_cursor()
3✔
1819
        start_line = cursor.blockNumber()
3✔
1820
        if start_line == self._get_prompt_cursor().blockNumber():
3✔
1821
            # first line
1822
            offset = len(self._prompt)
3✔
1823
        else:
1824
            # continuation
1825
            offset = len(self._continuation_prompt)
3✔
1826
        cursor.select(QtGui.QTextCursor.LineUnderCursor)
3✔
1827
        text = cursor.selectedText()[offset:]
3✔
1828
        return len(text) - len(text.lstrip())
3✔
1829

1830
    @property
3✔
1831
    def _prompt_pos(self):
3✔
1832
        """ Find the position in the text right after the prompt.
1833
        """
1834
        return min(self._prompt_cursor.position() + 1, self._get_end_pos())
3✔
1835

1836
    @property
3✔
1837
    def _append_before_prompt_pos(self):
3✔
1838
        """ Find the position in the text right before the prompt.
1839
        """
1840
        return min(self._append_before_prompt_cursor.position(),
3✔
1841
                   self._get_end_pos())
1842

1843
    def _get_prompt_cursor(self):
3✔
1844
        """ Get a cursor at the prompt position of the current cell.
1845
        """
1846
        cursor = self._control.textCursor()
3✔
1847
        cursor.setPosition(self._prompt_pos)
3✔
1848
        return cursor
3✔
1849

1850
    def _get_selection_cursor(self, start, end):
3✔
1851
        """ Get a cursor with text selected between the positions 'start' and
1852
            'end'.
1853
        """
1854
        cursor = self._control.textCursor()
×
1855
        cursor.setPosition(start)
×
1856
        cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
×
1857
        return cursor
×
1858

1859
    def _get_word_start_cursor(self, position):
3✔
1860
        """ Find the start of the word to the left the given position. If a
1861
            sequence of non-word characters precedes the first word, skip over
1862
            them. (This emulates the behavior of bash, emacs, etc.)
1863
        """
1864
        document = self._control.document()
3✔
1865
        cursor = self._control.textCursor()
3✔
1866
        line_start_pos = self._get_line_start_pos()
3✔
1867

1868
        if position == self._prompt_pos:
3✔
1869
            return cursor
×
1870
        elif position == line_start_pos:
3✔
1871
            # Cursor is at the beginning of a line, move to the last
1872
            # non-whitespace character of the previous line
1873
            cursor = self._control.textCursor()
3✔
1874
            cursor.setPosition(position)
3✔
1875
            cursor.movePosition(QtGui.QTextCursor.PreviousBlock)
3✔
1876
            cursor.movePosition(QtGui.QTextCursor.EndOfBlock)
3✔
1877
            position = cursor.position()
3✔
1878
            while (
3✔
1879
                position >= self._prompt_pos and
1880
                is_whitespace(document.characterAt(position))
1881
            ):
1882
                position -= 1
3✔
1883
            cursor.setPosition(position + 1)
3✔
1884
        else:
1885
            position -= 1
3✔
1886

1887
            # Find the last alphanumeric char, but don't move across lines
1888
            while (
3✔
1889
                position >= self._prompt_pos and
1890
                position >= line_start_pos and
1891
                not is_letter_or_number(document.characterAt(position))
1892
            ):
1893
                position -= 1
3✔
1894

1895
            # Find the first alphanumeric char, but don't move across lines
1896
            while (
3✔
1897
                position >= self._prompt_pos and
1898
                position >= line_start_pos and
1899
                is_letter_or_number(document.characterAt(position))
1900
            ):
1901
                position -= 1
3✔
1902

1903
            cursor.setPosition(position + 1)
3✔
1904

1905
        return cursor
3✔
1906

1907
    def _get_word_end_cursor(self, position):
3✔
1908
        """ Find the end of the word to the right the given position. If a
1909
            sequence of non-word characters precedes the first word, skip over
1910
            them. (This emulates the behavior of bash, emacs, etc.)
1911
        """
1912
        document = self._control.document()
3✔
1913
        cursor = self._control.textCursor()
3✔
1914
        end_pos = self._get_end_pos()
3✔
1915
        line_end_pos = self._get_line_end_pos()
3✔
1916

1917
        if position == end_pos:
3✔
1918
            # Cursor is at the very end of the buffer
1919
            return cursor
×
1920
        elif position == line_end_pos:
3✔
1921
            # Cursor is at the end of a line, move to the first
1922
            # non-whitespace character of the next line
1923
            cursor = self._control.textCursor()
3✔
1924
            cursor.setPosition(position)
3✔
1925
            cursor.movePosition(QtGui.QTextCursor.NextBlock)
3✔
1926
            position = cursor.position() + len(self._continuation_prompt)
3✔
1927
            while (
3✔
1928
                position < end_pos and
1929
                is_whitespace(document.characterAt(position))
1930
            ):
1931
                position += 1
3✔
1932
            cursor.setPosition(position)
3✔
1933
        else:
1934
            if is_whitespace(document.characterAt(position)):
3✔
1935
                # The next character is whitespace. If this is part of
1936
                # indentation whitespace, skip to the first non-whitespace
1937
                # character.
1938
                is_indentation_whitespace = True
3✔
1939
                back_pos = position - 1
3✔
1940
                line_start_pos = self._get_line_start_pos()
3✔
1941
                while back_pos >= line_start_pos:
3✔
1942
                    if not is_whitespace(document.characterAt(back_pos)):
×
1943
                        is_indentation_whitespace = False
×
1944
                        break
×
1945
                    back_pos -= 1
×
1946
                if is_indentation_whitespace:
3✔
1947
                    # Skip to the first non-whitespace character
1948
                    while (
3✔
1949
                        position < end_pos and
1950
                        position < line_end_pos and
1951
                        is_whitespace(document.characterAt(position))
1952
                    ):
1953
                        position += 1
3✔
1954
                    cursor.setPosition(position)
3✔
1955
                    return cursor
3✔
1956

1957
            while (
3✔
1958
                position < end_pos and
1959
                position < line_end_pos and
1960
                not is_letter_or_number(document.characterAt(position))
1961
            ):
1962
                position += 1
3✔
1963

1964
            while (
3✔
1965
                position < end_pos and
1966
                position < line_end_pos and
1967
                is_letter_or_number(document.characterAt(position))
1968
            ):
1969
                position += 1
3✔
1970

1971
            cursor.setPosition(position)
3✔
1972
        return cursor
3✔
1973

1974
    def _indent(self, dedent=True):
3✔
1975
        """ Indent/Dedent current line or current text selection.
1976
        """
1977
        num_newlines = self._get_cursor().selectedText().count("\u2029")
3✔
1978
        save_cur = self._get_cursor()
3✔
1979
        cur = self._get_cursor()
3✔
1980

1981
        # move to first line of selection, if present
1982
        cur.setPosition(cur.selectionStart())
3✔
1983
        self._control.setTextCursor(cur)
3✔
1984
        spaces = self._get_leading_spaces()
3✔
1985
        # calculate number of spaces neded to align/indent to 4-space multiple
1986
        step = self._tab_width - (spaces % self._tab_width)
3✔
1987

1988
        # insertText shouldn't replace if selection is active
1989
        cur.clearSelection()
3✔
1990

1991
        # indent all lines in selection (ir just current) by `step`
1992
        for _ in range(num_newlines+1):
3✔
1993
            # update underlying cursor for _get_line_start_pos
1994
            self._control.setTextCursor(cur)
3✔
1995
            # move to first non-ws char on line
1996
            cur.setPosition(self._get_line_start_pos())
3✔
1997
            if dedent:
3✔
1998
                spaces = min(step, self._get_leading_spaces())
3✔
1999
                safe_step = spaces % self._tab_width
3✔
2000
                cur.movePosition(QtGui.QTextCursor.Right,
3✔
2001
                                 QtGui.QTextCursor.KeepAnchor,
2002
                                 min(spaces, safe_step if safe_step != 0
2003
                                    else self._tab_width))
2004
                cur.removeSelectedText()
3✔
2005
            else:
2006
                cur.insertText(' '*step)
3✔
2007
            cur.movePosition(QtGui.QTextCursor.Down)
3✔
2008

2009
        # restore cursor
2010
        self._control.setTextCursor(save_cur)
3✔
2011

2012
    def _insert_continuation_prompt(self, cursor, indent=''):
3✔
2013
        """ Inserts new continuation prompt using the specified cursor.
2014
        """
2015
        if self._continuation_prompt_html is None:
3✔
2016
            self._insert_plain_text(cursor, self._continuation_prompt)
3✔
2017
        else:
2018
            self._continuation_prompt = self._insert_html_fetching_plain_text(
3✔
2019
                cursor, self._continuation_prompt_html)
2020
        if indent:
3✔
2021
            cursor.insertText(indent)
3✔
2022

2023
    def _insert_block(self, cursor, block_format=None):
3✔
2024
        """ Inserts an empty QTextBlock using the specified cursor.
2025
        """
2026
        if block_format is None:
3✔
2027
            block_format = QtGui.QTextBlockFormat()
3✔
2028
        cursor.insertBlock(block_format)
3✔
2029

2030
    def _insert_html(self, cursor, html):
3✔
2031
        """ Inserts HTML using the specified cursor in such a way that future
2032
            formatting is unaffected.
2033
        """
2034
        cursor.beginEditBlock()
3✔
2035
        cursor.insertHtml(html)
3✔
2036

2037
        # After inserting HTML, the text document "remembers" it's in "html
2038
        # mode", which means that subsequent calls adding plain text will result
2039
        # in unwanted formatting, lost tab characters, etc. The following code
2040
        # hacks around this behavior, which I consider to be a bug in Qt, by
2041
        # (crudely) resetting the document's style state.
2042
        cursor.movePosition(QtGui.QTextCursor.Left,
3✔
2043
                            QtGui.QTextCursor.KeepAnchor)
2044
        if cursor.selection().toPlainText() == ' ':
3✔
2045
            cursor.removeSelectedText()
3✔
2046
        else:
2047
            cursor.movePosition(QtGui.QTextCursor.Right)
3✔
2048
        cursor.insertText(' ', QtGui.QTextCharFormat())
3✔
2049
        cursor.endEditBlock()
3✔
2050

2051
    def _insert_html_fetching_plain_text(self, cursor, html):
3✔
2052
        """ Inserts HTML using the specified cursor, then returns its plain text
2053
            version.
2054
        """
2055
        cursor.beginEditBlock()
3✔
2056
        cursor.removeSelectedText()
3✔
2057

2058
        start = cursor.position()
3✔
2059
        self._insert_html(cursor, html)
3✔
2060
        end = cursor.position()
3✔
2061
        cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
3✔
2062
        text = cursor.selection().toPlainText()
3✔
2063

2064
        cursor.setPosition(end)
3✔
2065
        cursor.endEditBlock()
3✔
2066
        return text
3✔
2067

2068
    def _viewport_at_end(self):
3✔
2069
        """Check if the viewport is at the end of the document."""
2070
        viewport = self._control.viewport()
3✔
2071
        end_scroll_pos = self._control.cursorForPosition(
3✔
2072
            QtCore.QPoint(viewport.width() - 1, viewport.height() - 1)
2073
            ).position()
2074
        end_doc_pos = self._get_end_pos()
3✔
2075
        return end_doc_pos - end_scroll_pos <= 1
3✔
2076

2077
    def _scroll_to_end(self):
3✔
2078
        """Scroll to the end of the document."""
2079
        end_scroll = (self._control.verticalScrollBar().maximum()
3✔
2080
                      - self._control.verticalScrollBar().pageStep())
2081
        # Only scroll down
2082
        if end_scroll > self._control.verticalScrollBar().value():
3✔
2083
            self._control.verticalScrollBar().setValue(end_scroll)
3✔
2084

2085
    def _insert_plain_text(self, cursor, text, flush=False):
3✔
2086
        """ Inserts plain text using the specified cursor, processing ANSI codes
2087
            if enabled.
2088
        """
2089
        should_autoscroll = self._viewport_at_end()
3✔
2090
        # maximumBlockCount() can be different from self.buffer_size in
2091
        # case input prompt is active.
2092
        buffer_size = self._control.document().maximumBlockCount()
3✔
2093

2094
        if (self._executing and not flush and
3✔
2095
                self._pending_text_flush_interval.isActive() and
2096
                cursor.position() == self._get_end_pos()):
2097
            # Queue the text to insert in case it is being inserted at end
2098
            self._pending_insert_text.append(text)
3✔
2099
            if buffer_size > 0:
3✔
2100
                self._pending_insert_text = self._get_last_lines_from_list(
3✔
2101
                                        self._pending_insert_text, buffer_size)
2102
            return
3✔
2103

2104
        if self._executing and not self._pending_text_flush_interval.isActive():
3✔
2105
            self._pending_text_flush_interval.start()
3✔
2106

2107
        # Clip the text to last `buffer_size` lines.
2108
        if buffer_size > 0:
3✔
2109
            text = self._get_last_lines(text, buffer_size)
3✔
2110

2111
        cursor.beginEditBlock()
3✔
2112
        if self.ansi_codes:
3✔
2113
            for substring in self._ansi_processor.split_string(text):
3✔
2114
                for act in self._ansi_processor.actions:
3✔
2115

2116
                    # Unlike real terminal emulators, we don't distinguish
2117
                    # between the screen and the scrollback buffer. A screen
2118
                    # erase request clears everything.
2119
                    if act.action == 'erase':
3✔
2120
                        remove = False
3✔
2121
                        fill = False
3✔
2122
                        if act.area == 'screen':
3✔
2123
                            cursor.select(QtGui.QTextCursor.Document)
3✔
2124
                            remove = True
3✔
2125
                        if act.area == 'line':
3✔
2126
                            if act.erase_to == 'all': 
3✔
2127
                                cursor.select(QtGui.QTextCursor.LineUnderCursor)
3✔
2128
                                remove = True
3✔
2129
                            elif act.erase_to == 'start':
3✔
2130
                                cursor.movePosition(
3✔
2131
                                    QtGui.QTextCursor.StartOfLine,
2132
                                    QtGui.QTextCursor.KeepAnchor)
2133
                                remove = True
3✔
2134
                                fill = True
3✔
2135
                            elif act.erase_to == 'end':
3✔
2136
                                cursor.movePosition(
3✔
2137
                                    QtGui.QTextCursor.EndOfLine,
2138
                                    QtGui.QTextCursor.KeepAnchor)
2139
                                remove = True
3✔
2140
                        if remove: 
3✔
2141
                            nspace=cursor.selectionEnd()-cursor.selectionStart() if fill else 0
3✔
2142
                            cursor.removeSelectedText()
3✔
2143
                            if nspace>0: cursor.insertText(' '*nspace) # replace text by space, to keep cursor position as specified
3✔
2144

2145
                    # Simulate a form feed by scrolling just past the last line.
2146
                    elif act.action == 'scroll' and act.unit == 'page':
3✔
2147
                        cursor.insertText('\n')
×
2148
                        cursor.endEditBlock()
×
2149
                        self._set_top_cursor(cursor)
×
2150
                        cursor.joinPreviousEditBlock()
×
2151
                        cursor.deletePreviousChar()
×
2152

2153
                        if os.name == 'nt':
×
2154
                            cursor.select(QtGui.QTextCursor.Document)
×
2155
                            cursor.removeSelectedText()
×
2156

2157
                    elif act.action == 'carriage-return':
3✔
2158
                        cursor.movePosition(
3✔
2159
                            QtGui.QTextCursor.StartOfLine,
2160
                            QtGui.QTextCursor.MoveAnchor)
2161

2162
                    elif act.action == 'beep':
3✔
2163
                        QtWidgets.QApplication.instance().beep()
×
2164

2165
                    elif act.action == 'backspace':
3✔
2166
                        if not cursor.atBlockStart():
3✔
2167
                            cursor.movePosition(
3✔
2168
                                QtGui.QTextCursor.PreviousCharacter,
2169
                                QtGui.QTextCursor.MoveAnchor)
2170

2171
                    elif act.action == 'newline':
3✔
2172
                        cursor.movePosition(QtGui.QTextCursor.EndOfLine)
3✔
2173

2174
                # simulate replacement mode
2175
                if substring is not None:
3✔
2176
                    format = self._ansi_processor.get_format()
3✔
2177
                    if not (hasattr(cursor,'_insert_mode') and cursor._insert_mode):
3✔
2178
                        pos = cursor.position()
3✔
2179
                        cursor2 = QtGui.QTextCursor(cursor)  # self._get_line_end_pos() is the previous line, don't use it
3✔
2180
                        cursor2.movePosition(QtGui.QTextCursor.EndOfLine)
3✔
2181
                        remain = cursor2.position() - pos    # number of characters until end of line
3✔
2182
                        n=len(substring)
3✔
2183
                        swallow = min(n, remain)             # number of character to swallow
3✔
2184
                        cursor.setPosition(pos+swallow,QtGui.QTextCursor.KeepAnchor)
3✔
2185
                    cursor.insertText(substring,format)
3✔
2186
        else:
2187
            cursor.insertText(text)
×
2188
        cursor.endEditBlock()
3✔
2189

2190
        if should_autoscroll:
3✔
2191
            self._scroll_to_end()
3✔
2192

2193
    def _insert_plain_text_into_buffer(self, cursor, text):
3✔
2194
        """ Inserts text into the input buffer using the specified cursor (which
2195
            must be in the input buffer), ensuring that continuation prompts are
2196
            inserted as necessary.
2197
        """
2198
        lines = text.splitlines(True)
3✔
2199
        if lines:
3✔
2200
            if lines[-1].endswith('\n'):
3✔
2201
                # If the text ends with a newline, add a blank line so a new
2202
                # continuation prompt is produced.
2203
                lines.append('')
3✔
2204
            cursor.beginEditBlock()
3✔
2205
            cursor.insertText(lines[0])
3✔
2206
            for line in lines[1:]:
3✔
2207
                if self._continuation_prompt_html is None:
3✔
2208
                    cursor.insertText(self._continuation_prompt)
3✔
2209
                else:
2210
                    self._continuation_prompt = \
3✔
2211
                        self._insert_html_fetching_plain_text(
2212
                            cursor, self._continuation_prompt_html)
2213
                cursor.insertText(line)
3✔
2214
            cursor.endEditBlock()
3✔
2215

2216
    def _in_buffer(self, position):
3✔
2217
        """
2218
        Returns whether the specified position is inside the editing region.
2219
        """
2220
        return position == self._move_position_in_buffer(position)
3✔
2221

2222
    def _move_position_in_buffer(self, position):
3✔
2223
        """
2224
        Return the next position in buffer.
2225
        """
2226
        cursor = self._control.textCursor()
3✔
2227
        cursor.setPosition(position)
3✔
2228
        line = cursor.blockNumber()
3✔
2229
        prompt_line = self._get_prompt_cursor().blockNumber()
3✔
2230
        if line == prompt_line:
3✔
2231
            if position >= self._prompt_pos:
3✔
2232
                return position
3✔
2233
            return self._prompt_pos
3✔
2234
        if line > prompt_line:
3✔
2235
            cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
3✔
2236
            prompt_pos = cursor.position() + len(self._continuation_prompt)
3✔
2237
            if position >= prompt_pos:
3✔
2238
                return position
3✔
2239
            return prompt_pos
3✔
2240
        return self._prompt_pos
×
2241

2242
    def _keep_cursor_in_buffer(self):
3✔
2243
        """ Ensures that the cursor is inside the editing region. Returns
2244
            whether the cursor was moved.
2245
        """
2246
        cursor = self._control.textCursor()
3✔
2247
        endpos = cursor.selectionEnd()
3✔
2248

2249
        if endpos < self._prompt_pos:
3✔
2250
            cursor.setPosition(endpos)
×
2251
            line = cursor.blockNumber()
×
2252
            prompt_line = self._get_prompt_cursor().blockNumber()
×
2253
            if line == prompt_line:
×
2254
                # Cursor is on prompt line, move to start of buffer
2255
                cursor.setPosition(self._prompt_pos)
×
2256
            else:
2257
                # Cursor is not in buffer, move to the end
2258
                cursor.movePosition(QtGui.QTextCursor.End)
×
2259
            self._control.setTextCursor(cursor)
×
2260
            return True
×
2261

2262
        startpos = cursor.selectionStart()
3✔
2263

2264
        new_endpos = self._move_position_in_buffer(endpos)
3✔
2265
        new_startpos = self._move_position_in_buffer(startpos)
3✔
2266
        if new_endpos == endpos and new_startpos == startpos:
3✔
2267
            return False
3✔
2268

2269
        cursor.setPosition(new_startpos)
3✔
2270
        cursor.setPosition(new_endpos, QtGui.QTextCursor.KeepAnchor)
3✔
2271
        self._control.setTextCursor(cursor)
3✔
2272
        return True
3✔
2273

2274
    def _keyboard_quit(self):
3✔
2275
        """ Cancels the current editing task ala Ctrl-G in Emacs.
2276
        """
2277
        if self._temp_buffer_filled :
×
2278
            self._cancel_completion()
×
2279
            self._clear_temporary_buffer()
×
2280
        else:
2281
            self.input_buffer = ''
×
2282

2283
    def _page(self, text, html=False):
3✔
2284
        """ Displays text using the pager if it exceeds the height of the
2285
        viewport.
2286

2287
        Parameters
2288
        ----------
2289
        html : bool, optional (default False)
2290
            If set, the text will be interpreted as HTML instead of plain text.
2291
        """
2292
        line_height = QtGui.QFontMetrics(self.font).height()
×
2293
        minlines = self._control.viewport().height() / line_height
×
2294
        if self.paging != 'none' and \
×
2295
                re.match("(?:[^\n]*\n){%i}" % minlines, text):
2296
            if self.paging == 'custom':
×
2297
                self.custom_page_requested.emit(text)
×
2298
            else:
2299
                # disable buffer truncation during paging
2300
                self._control.document().setMaximumBlockCount(0)
×
2301
                self._page_control.clear()
×
2302
                cursor = self._page_control.textCursor()
×
2303
                if html:
×
2304
                    self._insert_html(cursor, text)
×
2305
                else:
2306
                    self._insert_plain_text(cursor, text)
×
2307
                self._page_control.moveCursor(QtGui.QTextCursor.Start)
×
2308

2309
                self._page_control.viewport().resize(self._control.size())
×
2310
                if self._splitter:
×
2311
                    self._page_control.show()
×
2312
                    self._page_control.setFocus()
×
2313
                else:
2314
                    self.layout().setCurrentWidget(self._page_control)
×
2315
        elif html:
×
2316
            self._append_html(text)
×
2317
        else:
2318
            self._append_plain_text(text)
×
2319

2320
    def _set_paging(self, paging):
3✔
2321
        """
2322
        Change the pager to `paging` style.
2323

2324
        Parameters
2325
        ----------
2326
        paging : string
2327
            Either "hsplit", "vsplit", or "inside"
2328
        """
2329
        if self._splitter is None:
×
2330
            raise NotImplementedError("""can only switch if --paging=hsplit or
×
2331
                    --paging=vsplit is used.""")
2332
        if paging == 'hsplit':
×
2333
            self._splitter.setOrientation(QtCore.Qt.Horizontal)
×
2334
        elif paging == 'vsplit':
×
2335
            self._splitter.setOrientation(QtCore.Qt.Vertical)
×
2336
        elif paging == 'inside':
×
2337
            raise NotImplementedError("""switching to 'inside' paging not
×
2338
                    supported yet.""")
2339
        else:
2340
            raise ValueError("unknown paging method '%s'" % paging)
×
2341
        self.paging = paging
×
2342

2343
    def _prompt_finished(self):
3✔
2344
        """ Called immediately after a prompt is finished, i.e. when some input
2345
            will be processed and a new prompt displayed.
2346
        """
2347
        self._control.setReadOnly(True)
3✔
2348
        self._prompt_finished_hook()
3✔
2349

2350
    def _prompt_started(self):
3✔
2351
        """ Called immediately after a new prompt is displayed.
2352
        """
2353
        # Temporarily disable the maximum block count to permit undo/redo and
2354
        # to ensure that the prompt position does not change due to truncation.
2355
        self._control.document().setMaximumBlockCount(0)
3✔
2356
        self._control.setUndoRedoEnabled(True)
3✔
2357

2358
        # Work around bug in QPlainTextEdit: input method is not re-enabled
2359
        # when read-only is disabled.
2360
        self._control.setReadOnly(False)
3✔
2361
        self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)
3✔
2362

2363
        if not self._reading:
3✔
2364
            self._executing = False
3✔
2365
        self._prompt_started_hook()
3✔
2366

2367
        # If the input buffer has changed while executing, load it.
2368
        if self._input_buffer_pending:
3✔
2369
            self.input_buffer = self._input_buffer_pending
×
2370
            self._input_buffer_pending = ''
×
2371

2372
        self._control.moveCursor(QtGui.QTextCursor.End)
3✔
2373

2374
    def _readline(self, prompt='', callback=None, password=False):
3✔
2375
        """ Reads one line of input from the user.
2376

2377
        Parameters
2378
        ----------
2379
        prompt : str, optional
2380
            The prompt to print before reading the line.
2381

2382
        callback : callable, optional
2383
            A callback to execute with the read line. If not specified, input is
2384
            read *synchronously* and this method does not return until it has
2385
            been read.
2386

2387
        Returns
2388
        -------
2389
        If a callback is specified, returns nothing. Otherwise, returns the
2390
        input string with the trailing newline stripped.
2391
        """
2392
        if self._reading:
3✔
2393
            raise RuntimeError('Cannot read a line. Widget is already reading.')
×
2394

2395
        if not callback and not self.isVisible():
3✔
2396
            # If the user cannot see the widget, this function cannot return.
2397
            raise RuntimeError('Cannot synchronously read a line if the widget '
×
2398
                               'is not visible!')
2399

2400
        self._reading = True
3✔
2401
        if password:
3✔
2402
            self._show_prompt('Warning: QtConsole does not support password mode, '\
×
2403
                              'the text you type will be visible.', newline=True)
2404

2405
        if 'ipdb' not in prompt.lower():
3✔
2406
            # This is a prompt that asks for input from the user.
2407
            self._show_prompt(prompt, newline=False, separator=False)
3✔
2408
        else:
2409
            self._show_prompt(prompt, newline=False)
3✔
2410

2411
        if callback is None:
3✔
2412
            self._reading_callback = None
×
2413
            while self._reading:
×
2414
                QtCore.QCoreApplication.processEvents()
×
2415
            return self._get_input_buffer(force=True).rstrip('\n')
×
2416
        else:
2417
            self._reading_callback = lambda: \
3✔
2418
                callback(self._get_input_buffer(force=True).rstrip('\n'))
2419

2420
    def _set_continuation_prompt(self, prompt, html=False):
3✔
2421
        """ Sets the continuation prompt.
2422

2423
        Parameters
2424
        ----------
2425
        prompt : str
2426
            The prompt to show when more input is needed.
2427

2428
        html : bool, optional (default False)
2429
            If set, the prompt will be inserted as formatted HTML. Otherwise,
2430
            the prompt will be treated as plain text, though ANSI color codes
2431
            will be handled.
2432
        """
2433
        if html:
3✔
2434
            self._continuation_prompt_html = prompt
3✔
2435
        else:
2436
            self._continuation_prompt = prompt
3✔
2437
            self._continuation_prompt_html = None
3✔
2438

2439
    def _set_cursor(self, cursor):
3✔
2440
        """ Convenience method to set the current cursor.
2441
        """
2442
        self._control.setTextCursor(cursor)
3✔
2443

2444
    def _set_top_cursor(self, cursor):
3✔
2445
        """ Scrolls the viewport so that the specified cursor is at the top.
2446
        """
2447
        scrollbar = self._control.verticalScrollBar()
×
2448
        scrollbar.setValue(scrollbar.maximum())
×
2449
        original_cursor = self._control.textCursor()
×
2450
        self._control.setTextCursor(cursor)
×
2451
        self._control.ensureCursorVisible()
×
2452
        self._control.setTextCursor(original_cursor)
×
2453

2454
    def _show_prompt(self, prompt=None, html=False, newline=True,
3✔
2455
                     separator=True):
2456
        """ Writes a new prompt at the end of the buffer.
2457

2458
        Parameters
2459
        ----------
2460
        prompt : str, optional
2461
            The prompt to show. If not specified, the previous prompt is used.
2462

2463
        html : bool, optional (default False)
2464
            Only relevant when a prompt is specified. If set, the prompt will
2465
            be inserted as formatted HTML. Otherwise, the prompt will be treated
2466
            as plain text, though ANSI color codes will be handled.
2467

2468
        newline : bool, optional (default True)
2469
            If set, a new line will be written before showing the prompt if
2470
            there is not already a newline at the end of the buffer.
2471

2472
        separator : bool, optional (default True)
2473
            If set, a separator will be written before the prompt.
2474
        """
2475
        self._flush_pending_stream()
3✔
2476

2477
        # This is necessary to solve out-of-order insertion of mixed stdin and
2478
        # stdout stream texts.
2479
        # Fixes spyder-ide/spyder#17710
2480
        if sys.platform == 'darwin':
3✔
2481
            # Although this makes our tests hang on Mac, users confirmed that
2482
            # it's needed on that platform too.
2483
            # Fixes spyder-ide/spyder#19888
2484
            if not os.environ.get('QTCONSOLE_TESTING'):
×
2485
                QtCore.QCoreApplication.processEvents()
×
2486
        else:
2487
            QtCore.QCoreApplication.processEvents()
3✔
2488

2489
        cursor = self._get_end_cursor()
3✔
2490

2491
        # Save the current position to support _append*(before_prompt=True).
2492
        # We can't leave the cursor at the end of the document though, because
2493
        # that would cause any further additions to move the cursor. Therefore,
2494
        # we move it back one place and move it forward again at the end of
2495
        # this method. However, we only do this if the cursor isn't already
2496
        # at the start of the text.
2497
        if cursor.position() == 0:
3✔
2498
            move_forward = False
3✔
2499
        else:
2500
            move_forward = True
3✔
2501
            self._append_before_prompt_cursor.setPosition(cursor.position() - 1)
3✔
2502

2503
        # Insert a preliminary newline, if necessary.
2504
        if newline and cursor.position() > 0:
3✔
2505
            cursor.movePosition(QtGui.QTextCursor.Left,
3✔
2506
                                QtGui.QTextCursor.KeepAnchor)
2507
            if cursor.selection().toPlainText() != '\n':
3✔
2508
                self._append_block()
3✔
2509

2510
        # Write the prompt.
2511
        if separator:
3✔
2512
            self._append_plain_text(self._prompt_sep)
3✔
2513

2514
        if prompt is None:
3✔
2515
            if self._prompt_html is None:
3✔
2516
                self._append_plain_text(self._prompt)
3✔
2517
            else:
2518
                self._append_html(self._prompt_html)
×
2519
        else:
2520
            if html:
3✔
2521
                self._prompt = self._append_html_fetching_plain_text(prompt)
3✔
2522
                self._prompt_html = prompt
3✔
2523
            else:
2524
                self._append_plain_text(prompt)
3✔
2525
                self._prompt = prompt
3✔
2526
                self._prompt_html = None
3✔
2527

2528
        self._flush_pending_stream()
3✔
2529
        self._prompt_cursor.setPosition(self._get_end_pos() - 1)
3✔
2530

2531
        if move_forward:
3✔
2532
            self._append_before_prompt_cursor.setPosition(
3✔
2533
                self._append_before_prompt_cursor.position() + 1)
2534
        self._prompt_started()
3✔
2535

2536
    #------ Signal handlers ----------------------------------------------------
2537

2538
    def _adjust_scrollbars(self):
3✔
2539
        """ Expands the vertical scrollbar beyond the range set by Qt.
2540
        """
2541
        # This code is adapted from _q_adjustScrollbars in qplaintextedit.cpp
2542
        # and qtextedit.cpp.
2543
        document = self._control.document()
3✔
2544
        scrollbar = self._control.verticalScrollBar()
3✔
2545
        viewport_height = self._control.viewport().height()
3✔
2546
        if isinstance(self._control, QtWidgets.QPlainTextEdit):
3✔
2547
            maximum = max(0, document.lineCount() - 1)
3✔
2548
            step = viewport_height / self._control.fontMetrics().lineSpacing()
3✔
2549
        else:
2550
            # QTextEdit does not do line-based layout and blocks will not in
2551
            # general have the same height. Therefore it does not make sense to
2552
            # attempt to scroll in line height increments.
2553
            maximum = document.size().height()
3✔
2554
            step = viewport_height
3✔
2555
        diff = maximum - scrollbar.maximum()
3✔
2556
        scrollbar.setRange(0, round(maximum))
3✔
2557
        scrollbar.setPageStep(round(step))
3✔
2558

2559
        # Compensate for undesirable scrolling that occurs automatically due to
2560
        # maximumBlockCount() text truncation.
2561
        if diff < 0 and document.blockCount() == document.maximumBlockCount():
3✔
2562
            scrollbar.setValue(round(scrollbar.value() + diff))
×
2563

2564
    def _custom_context_menu_requested(self, pos):
3✔
2565
        """ Shows a context menu at the given QPoint (in widget coordinates).
2566
        """
2567
        menu = self._context_menu_make(pos)
×
2568
        menu.exec_(self._control.mapToGlobal(pos))
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc