Skip to content

Commit

Permalink
Merge pull request ipython#526 from epatters/append-before-prompt
Browse files Browse the repository at this point in the history
Handle asynchronous output in Qt console
  • Loading branch information
epatters committed Jun 21, 2011
2 parents e1f114d + b29f86d commit c5261e0
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 119 deletions.
97 changes: 58 additions & 39 deletions IPython/frontend/qt/console/console_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,16 @@ class ConsoleWidget(Configurable, QtGui.QWidget):
"""
)
gui_completion = Bool(False, config=True,
help="Use a list widget instead of plain text output for tab completion."
help="""
Use a list widget instead of plain text output for tab completion.
"""
)
# NOTE: this value can only be specified during initialization.
kind = Enum(['plain', 'rich'], default_value='plain', config=True,
help="""
The type of underlying text widget to use. Valid values are 'plain', which
specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
The type of underlying text widget to use. Valid values are 'plain',
which specifies a QPlainTextEdit, and 'rich', which specifies a
QTextEdit.
"""
)
# NOTE: this value can only be specified during initialization.
Expand All @@ -84,7 +87,8 @@ class ConsoleWidget(Configurable, QtGui.QWidget):
'hsplit' : When paging is requested, the widget is split
horizontally. The top pane contains the console, and the
bottom pane contains the paged text.
'vsplit' : Similar to 'hsplit', except that a vertical splitter used.
'vsplit' : Similar to 'hsplit', except that a vertical splitter
used.
'custom' : No action is taken by the widget beyond emitting a
'custom_page_requested(str)' signal.
'none' : The text is written directly to the console.
Expand Down Expand Up @@ -195,6 +199,7 @@ def __init__(self, parent=None, **kw):

# Initialize protected variables. Some variables contain useful state
# information for subclasses; they should be considered read-only.
self._append_before_prompt_pos = 0
self._ansi_processor = QtAnsiCodeProcessor()
self._completion_widget = CompletionWidget(self._control)
self._continuation_prompt = '> '
Expand Down Expand Up @@ -507,15 +512,15 @@ def export_html(self):
"""
self._html_exporter.export()

def _get_input_buffer(self):
def _get_input_buffer(self, force=False):
""" The text that the user has entered entered at the current prompt.
If the console is currently executing, the text that is executing will
always be returned.
"""
# If we're executing, the input buffer may not even exist anymore due to
# the limit imposed by 'buffer_size'. Therefore, we store it.
if self._executing:
if self._executing and not force:
return self._input_buffer_executing

cursor = self._get_end_cursor()
Expand Down Expand Up @@ -718,36 +723,47 @@ def _tab_pressed(self):
# 'ConsoleWidget' protected interface
#--------------------------------------------------------------------------

def _append_html(self, html):
""" Appends html at the end of the console buffer.
def _append_custom(self, insert, input, before_prompt=False):
""" A low-level method for appending content to the end of the buffer.
If 'before_prompt' is enabled, the content will be inserted before the
current prompt, if there is one.
"""
cursor = self._get_end_cursor()
self._insert_html(cursor, html)
# Determine where to insert the content.
cursor = self._control.textCursor()
if before_prompt and not self._executing:
cursor.setPosition(self._append_before_prompt_pos)
else:
cursor.movePosition(QtGui.QTextCursor.End)
start_pos = cursor.position()

def _append_html_fetching_plain_text(self, html):
""" Appends 'html', then returns the plain text version of it.
"""
cursor = self._get_end_cursor()
return self._insert_html_fetching_plain_text(cursor, html)
# Perform the insertion.
result = insert(cursor, input)

# Adjust the prompt position if we have inserted before it. This is safe
# because buffer truncation is disabled when not executing.
if before_prompt and not self._executing:
diff = cursor.position() - start_pos
self._append_before_prompt_pos += diff
self._prompt_pos += diff

return result

def _append_plain_text(self, text):
""" Appends plain text at the end of the console buffer, processing
ANSI codes if enabled.
def _append_html(self, html, before_prompt=False):
""" Appends HTML at the end of the console buffer.
"""
cursor = self._get_end_cursor()
self._insert_plain_text(cursor, text)
self._append_custom(self._insert_html, html, before_prompt)

def _append_plain_text_keeping_prompt(self, text):
""" Writes 'text' after the current prompt, then restores the old prompt
with its old input buffer.
def _append_html_fetching_plain_text(self, html, before_prompt=False):
""" Appends HTML, then returns the plain text version of it.
"""
input_buffer = self.input_buffer
self._append_plain_text('\n')
self._prompt_finished()
return self._append_custom(self._insert_html_fetching_plain_text,
html, before_prompt)

self._append_plain_text(text)
self._show_prompt()
self.input_buffer = input_buffer
def _append_plain_text(self, text, before_prompt=False):
""" Appends plain text, processing ANSI codes if enabled.
"""
self._append_custom(self._insert_plain_text, text, before_prompt)

def _cancel_text_completion(self):
""" If text completion is progress, cancel it.
Expand Down Expand Up @@ -1616,7 +1632,8 @@ def _prompt_started(self):
self._control.setReadOnly(False)
self._control.setAttribute(QtCore.Qt.WA_InputMethodEnabled, True)

self._executing = False
if not self._reading:
self._executing = False
self._prompt_started_hook()

# If the input buffer has changed while executing, load it.
Expand Down Expand Up @@ -1659,11 +1676,11 @@ def _readline(self, prompt='', callback=None):
self._reading_callback = None
while self._reading:
QtCore.QCoreApplication.processEvents()
return self.input_buffer.rstrip('\n')
return self._get_input_buffer(force=True).rstrip('\n')

else:
self._reading_callback = lambda: \
callback(self.input_buffer.rstrip('\n'))
callback(self._get_input_buffer(force=True).rstrip('\n'))

def _set_continuation_prompt(self, prompt, html=False):
""" Sets the continuation prompt.
Expand Down Expand Up @@ -1716,14 +1733,16 @@ def _show_prompt(self, prompt=None, html=False, newline=True):
If set, a new line will be written before showing the prompt if
there is not already a newline at the end of the buffer.
"""
# Save the current end position to support _append*(before_prompt=True).
cursor = self._get_end_cursor()
self._append_before_prompt_pos = cursor.position()

# Insert a preliminary newline, if necessary.
if newline:
cursor = self._get_end_cursor()
if cursor.position() > 0:
cursor.movePosition(QtGui.QTextCursor.Left,
QtGui.QTextCursor.KeepAnchor)
if cursor.selection().toPlainText() != '\n':
self._append_plain_text('\n')
if newline and cursor.position() > 0:
cursor.movePosition(QtGui.QTextCursor.Left,
QtGui.QTextCursor.KeepAnchor)
if cursor.selection().toPlainText() != '\n':
self._append_plain_text('\n')

# Write the prompt.
self._append_plain_text(self._prompt_sep)
Expand Down
25 changes: 8 additions & 17 deletions IPython/frontend/qt/console/frontend_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@


class FrontendHighlighter(PygmentsHighlighter):
""" A PygmentsHighlighter that can be turned on and off and that ignores
prompts.
""" A PygmentsHighlighter that understands and ignores prompts.
"""

def __init__(self, frontend):
Expand All @@ -50,14 +49,12 @@ def highlightBlock(self, string):
else:
prompt = self._frontend._continuation_prompt

# Don't highlight the part of the string that contains the prompt.
# Only highlight if we can identify a prompt, but make sure not to
# highlight the prompt.
if string.startswith(prompt):
self._current_offset = len(prompt)
string = string[len(prompt):]
else:
self._current_offset = 0

PygmentsHighlighter.highlightBlock(self, string)
super(FrontendHighlighter, self).highlightBlock(string)

def rehighlightBlock(self, block):
""" Reimplemented to temporarily enable highlighting if disabled.
Expand All @@ -71,7 +68,7 @@ def setFormat(self, start, count, format):
""" Reimplemented to highlight selectively.
"""
start += self._current_offset
PygmentsHighlighter.setFormat(self, start, count, format)
super(FrontendHighlighter, self).setFormat(start, count, format)


class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
Expand Down Expand Up @@ -372,14 +369,8 @@ def _handle_pyout(self, msg):
""" Handle display hook output.
"""
if not self._hidden and self._is_from_this_session(msg):
data = msg['content']['data']
if isinstance(data, basestring):
# plaintext data from pure Python kernel
text = data
else:
# formatted output from DisplayFormatter (IPython kernel)
text = data.get('text/plain', '')
self._append_plain_text(text + '\n')
text = msg['content']['data']
self._append_plain_text(text + '\n', before_prompt=True)

def _handle_stream(self, msg):
""" Handle stdout, stderr, and stdin.
Expand All @@ -390,7 +381,7 @@ def _handle_stream(self, msg):
# widget's tab width.
text = msg['content']['data'].expandtabs(8)

self._append_plain_text(text)
self._append_plain_text(text, before_prompt=True)
self._control.moveCursor(QtGui.QTextCursor.End)

def _handle_shutdown_reply(self, msg):
Expand Down
43 changes: 22 additions & 21 deletions IPython/frontend/qt/console/ipython_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,16 +62,16 @@ class IPythonWidget(FrontendWidget):
editor = Unicode(default_editor, config=True,
help="""
A command for invoking a system text editor. If the string contains a
{filename} format specifier, it will be used. Otherwise, the filename will
be appended to the end the command.
{filename} format specifier, it will be used. Otherwise, the filename
will be appended to the end the command.
""")

editor_line = Unicode(config=True,
help="""
The editor command to use when a specific line number is requested. The
string should contain two format specifiers: {line} and {filename}. If
this parameter is not specified, the line number option to the %edit magic
will be ignored.
this parameter is not specified, the line number option to the %edit
magic will be ignored.
""")

style_sheet = Unicode(config=True,
Expand All @@ -85,8 +85,9 @@ class IPythonWidget(FrontendWidget):

syntax_style = Str(config=True,
help="""
If not empty, use this Pygments style for syntax highlighting. Otherwise,
the style sheet is queried for Pygments style information.
If not empty, use this Pygments style for syntax highlighting.
Otherwise, the style sheet is queried for Pygments style
information.
""")

# Prompts.
Expand Down Expand Up @@ -187,20 +188,20 @@ def _handle_pyout(self, msg):
prompt_number = content['execution_count']
data = content['data']
if data.has_key('text/html'):
self._append_plain_text(self.output_sep)
self._append_html(self._make_out_prompt(prompt_number))
self._append_plain_text(self.output_sep, True)
self._append_html(self._make_out_prompt(prompt_number), True)
html = data['text/html']
self._append_plain_text('\n')
self._append_html(html + self.output_sep2)
self._append_plain_text('\n', True)
self._append_html(html + self.output_sep2, True)
elif data.has_key('text/plain'):
self._append_plain_text(self.output_sep)
self._append_html(self._make_out_prompt(prompt_number))
self._append_plain_text(self.output_sep, True)
self._append_html(self._make_out_prompt(prompt_number), True)
text = data['text/plain']
# If the repr is multiline, make sure we start on a new line,
# so that its lines are aligned.
if "\n" in text and not self.output_sep.endswith("\n"):
self._append_plain_text('\n')
self._append_plain_text(text + self.output_sep2)
self._append_plain_text('\n', True)
self._append_plain_text(text + self.output_sep2, True)

def _handle_display_data(self, msg):
""" The base handler for the ``display_data`` message.
Expand All @@ -216,19 +217,19 @@ def _handle_display_data(self, msg):
# representation.
if data.has_key('text/html'):
html = data['text/html']
self._append_html(html)
self._append_html(html, True)
elif data.has_key('text/plain'):
text = data['text/plain']
self._append_plain_text(text)
self._append_plain_text(text, True)
# This newline seems to be needed for text and html output.
self._append_plain_text(u'\n')
self._append_plain_text(u'\n', True)

def _started_channels(self):
""" Reimplemented to make a history request.
"""
super(IPythonWidget, self)._started_channels()
self.kernel_manager.shell_channel.history(hist_access_type='tail', n=1000)

self.kernel_manager.shell_channel.history(hist_access_type='tail',
n=1000)
#---------------------------------------------------------------------------
# 'ConsoleWidget' public interface
#---------------------------------------------------------------------------
Expand Down Expand Up @@ -413,8 +414,8 @@ def _edit(self, filename, line=None):
self.custom_edit_requested.emit(filename, line)
elif not self.editor:
self._append_plain_text('No default editor available.\n'
'Specify a GUI text editor in the `IPythonWidget.editor` configurable\n'
'to enable the %edit magic')
'Specify a GUI text editor in the `IPythonWidget.editor` '
'configurable to enable the %edit magic')
else:
try:
filename = '"%s"' % filename
Expand Down

0 comments on commit c5261e0

Please sign in to comment.