diff --git a/IPython/frontend/qt/console/console_widget.py b/IPython/frontend/qt/console/console_widget.py
index 79ef1d0..907902e 100644
--- a/IPython/frontend/qt/console/console_widget.py
+++ b/IPython/frontend/qt/console/console_widget.py
@@ -133,12 +133,15 @@ class ConsoleWidget(QtGui.QPlainTextEdit):
def __init__(self, parent=None):
QtGui.QPlainTextEdit.__init__(self, parent)
- # Initialize protected variables.
+ # Initialize protected variables. Some variables contain useful state
+ # information for subclasses; they should be considered read-only.
self._ansi_processor = QtAnsiCodeProcessor()
self._completion_widget = CompletionWidget(self)
self._continuation_prompt = '> '
+ self._continuation_prompt_html = None
self._executing = False
self._prompt = ''
+ self._prompt_html = None
self._prompt_pos = 0
self._reading = False
self._reading_callback = None
@@ -343,14 +346,34 @@ class ConsoleWidget(QtGui.QPlainTextEdit):
# 'QPlainTextEdit' interface
#--------------------------------------------------------------------------
+ def appendHtml(self, html):
+ """ Reimplemented to not append HTML as a new paragraph, which doesn't
+ make sense for a console widget.
+ """
+ cursor = self._get_end_cursor()
+ cursor.insertHtml(html)
+
+ # After appending HTML, the text document "remembers" the current
+ # formatting, which means that subsequent calls to 'appendPlainText'
+ # will be formatted similarly, a behavior that we do not want. To
+ # prevent this, we make sure that the last character has no formatting.
+ cursor.movePosition(QtGui.QTextCursor.Left,
+ QtGui.QTextCursor.KeepAnchor)
+ if cursor.selection().toPlainText().trimmed().isEmpty():
+ # If the last character is whitespace, it doesn't matter how it's
+ # formatted, so just clear the formatting.
+ cursor.setCharFormat(QtGui.QTextCharFormat())
+ else:
+ # Otherwise, add an unformatted space.
+ cursor.movePosition(QtGui.QTextCursor.Right)
+ cursor.insertText(' ', QtGui.QTextCharFormat())
+
def appendPlainText(self, text):
""" Reimplemented to not append text as a new paragraph, which doesn't
make sense for a console widget. Also, if enabled, handle ANSI
codes.
"""
- cursor = self.textCursor()
- cursor.movePosition(QtGui.QTextCursor.End)
-
+ cursor = self._get_end_cursor()
if self.ansi_codes:
format = QtGui.QTextCharFormat()
previous_end = 0
@@ -365,18 +388,15 @@ class ConsoleWidget(QtGui.QPlainTextEdit):
cursor.insertText(text)
def clear(self, keep_input=False):
- """ Reimplemented to cancel reading and write a new prompt. If
- 'keep_input' is set, restores the old input buffer when the new
- prompt is written.
+ """ Reimplemented to write a new prompt. If 'keep_input' is set,
+ restores the old input buffer when the new prompt is written.
"""
QtGui.QPlainTextEdit.clear(self)
- input_buffer = ''
- if self._reading:
- self._reading = False
- elif keep_input:
+ if keep_input:
input_buffer = self.input_buffer
self._show_prompt()
- self.input_buffer = input_buffer
+ if keep_input:
+ self.input_buffer = input_buffer
def paste(self):
""" Reimplemented to ensure that text is pasted in the editing region.
@@ -463,9 +483,6 @@ class ConsoleWidget(QtGui.QPlainTextEdit):
cursor = self._get_end_cursor()
cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
-
- # Use QTextDocumentFragment intermediate object because it strips
- # out the Unicode line break characters that Qt insists on inserting.
input_buffer = str(cursor.selection().toPlainText())
# Strip out continuation prompts.
@@ -496,7 +513,7 @@ class ConsoleWidget(QtGui.QPlainTextEdit):
return None
cursor = self.textCursor()
if cursor.position() >= self._prompt_pos:
- text = str(cursor.block().text())
+ text = self._get_block_plain_text(cursor.block())
if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
return text[len(self._prompt):]
else:
@@ -581,6 +598,27 @@ class ConsoleWidget(QtGui.QPlainTextEdit):
# 'ConsoleWidget' protected interface
#--------------------------------------------------------------------------
+ def _append_html_fetching_plain_text(self, html):
+ """ Appends 'html', then returns the plain text version of it.
+ """
+ anchor = self._get_end_cursor().position()
+ self.appendHtml(html)
+ cursor = self._get_end_cursor()
+ cursor.setPosition(anchor, QtGui.QTextCursor.KeepAnchor)
+ return str(cursor.selection().toPlainText())
+
+ def _append_plain_text_keeping_prompt(self, text):
+ """ Writes 'text' after the current prompt, then restores the old prompt
+ with its old input buffer.
+ """
+ input_buffer = self.input_buffer
+ self.appendPlainText('\n')
+ self._prompt_finished()
+
+ self.appendPlainText(text)
+ self._show_prompt()
+ self.input_buffer = input_buffer
+
def _control_down(self, modifiers):
""" Given a KeyboardModifiers flags object, return whether the Control
key is down (on Mac OS, treat the Command key as a synonym for
@@ -607,7 +645,16 @@ class ConsoleWidget(QtGui.QPlainTextEdit):
self._completion_widget.show_items(cursor, items)
else:
text = '\n'.join(items) + '\n'
- self._write_text_keeping_prompt(text)
+ self._append_plain_text_keeping_prompt(text)
+
+ def _get_block_plain_text(self, block):
+ """ Given a QTextBlock, return its unformatted text.
+ """
+ cursor = QtGui.QTextCursor(block)
+ cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
+ cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
+ QtGui.QTextCursor.KeepAnchor)
+ return str(cursor.selection().toPlainText())
def _get_end_cursor(self):
""" Convenience method that returns a cursor for the last character.
@@ -728,6 +775,31 @@ class ConsoleWidget(QtGui.QPlainTextEdit):
self._reading_callback = lambda: \
callback(self.input_buffer.rstrip('\n'))
+ def _reset(self):
+ """ Clears the console and resets internal state variables.
+ """
+ QtGui.QPlainTextEdit.clear(self)
+ self._executing = self._reading = False
+
+ def _set_continuation_prompt(self, prompt, html=False):
+ """ Sets the continuation prompt.
+
+ Parameters
+ ----------
+ prompt : str
+ The prompt to show when more input is needed.
+
+ html : bool, optional (default False)
+ If set, the prompt will be inserted as formatted HTML. Otherwise,
+ the prompt will be treated as plain text, though ANSI color codes
+ will be handled.
+ """
+ if html:
+ self._continuation_prompt_html = prompt
+ else:
+ self._continuation_prompt = prompt
+ self._continuation_prompt_html = None
+
def _set_position(self, position):
""" Convenience method to set the position of the cursor.
"""
@@ -740,7 +812,7 @@ class ConsoleWidget(QtGui.QPlainTextEdit):
"""
self.setTextCursor(self._get_selection_cursor(start, end))
- def _show_prompt(self, prompt=None, newline=True):
+ def _show_prompt(self, prompt=None, html=False, newline=True):
""" Writes a new prompt at the end of the buffer.
Parameters
@@ -748,10 +820,16 @@ class ConsoleWidget(QtGui.QPlainTextEdit):
prompt : str, optional
The prompt to show. If not specified, the previous prompt is used.
+ html : bool, optional (default False)
+ Only relevant when a prompt is specified. If set, the prompt will
+ be inserted as formatted HTML. Otherwise, the prompt will be treated
+ as plain text, though ANSI color codes will be handled.
+
newline : bool, optional (default 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.
"""
+ # Insert a preliminary newline, if necessary.
if newline:
cursor = self._get_end_cursor()
if cursor.position() > 0:
@@ -760,9 +838,20 @@ class ConsoleWidget(QtGui.QPlainTextEdit):
if str(cursor.selection().toPlainText()) != '\n':
self.appendPlainText('\n')
- if prompt is not None:
- self._prompt = prompt
- self.appendPlainText(self._prompt)
+ # Write the prompt.
+ if prompt is None:
+ if self._prompt_html is None:
+ self.appendPlainText(self._prompt)
+ else:
+ self.appendHtml(self._prompt_html)
+ else:
+ if html:
+ self._prompt = self._append_html_fetching_plain_text(prompt)
+ self._prompt_html = prompt
+ else:
+ self.appendPlainText(prompt)
+ self._prompt = prompt
+ self._prompt_html = None
self._prompt_pos = self._get_end_cursor().position()
self._prompt_started()
@@ -770,20 +859,13 @@ class ConsoleWidget(QtGui.QPlainTextEdit):
def _show_continuation_prompt(self):
""" Writes a new continuation prompt at the end of the buffer.
"""
- self.appendPlainText(self._continuation_prompt)
- self._prompt_started()
-
- def _write_text_keeping_prompt(self, text):
- """ Writes 'text' after the current prompt, then restores the old prompt
- with its old input buffer.
- """
- input_buffer = self.input_buffer
- self.appendPlainText('\n')
- self._prompt_finished()
+ if self._continuation_prompt_html is None:
+ self.appendPlainText(self._continuation_prompt)
+ else:
+ self._continuation_prompt = self._append_html_fetching_plain_text(
+ self._continuation_prompt_html)
- self.appendPlainText(text)
- self._show_prompt()
- self.input_buffer = input_buffer
+ self._prompt_started()
def _in_buffer(self, position):
""" Returns whether the given position is inside the editing region.
diff --git a/IPython/frontend/qt/console/frontend_widget.py b/IPython/frontend/qt/console/frontend_widget.py
index db1ab48..7890f07 100644
--- a/IPython/frontend/qt/console/frontend_widget.py
+++ b/IPython/frontend/qt/console/frontend_widget.py
@@ -16,12 +16,12 @@ from pygments_highlighter import PygmentsHighlighter
class FrontendHighlighter(PygmentsHighlighter):
- """ A Python PygmentsHighlighter that can be turned on and off and which
- knows about continuation prompts.
+ """ A PygmentsHighlighter that can be turned on and off and that ignores
+ prompts.
"""
def __init__(self, frontend):
- PygmentsHighlighter.__init__(self, frontend.document(), PythonLexer())
+ super(FrontendHighlighter, self).__init__(frontend.document())
self._current_offset = 0
self._frontend = frontend
self.highlighting_on = False
@@ -29,17 +29,32 @@ class FrontendHighlighter(PygmentsHighlighter):
def highlightBlock(self, qstring):
""" Highlight a block of text. Reimplemented to highlight selectively.
"""
- if self.highlighting_on:
- for prompt in (self._frontend._continuation_prompt,
- self._frontend._prompt):
- if qstring.startsWith(prompt):
- qstring.remove(0, len(prompt))
- self._current_offset = len(prompt)
- break
- PygmentsHighlighter.highlightBlock(self, qstring)
+ if not self.highlighting_on:
+ return
+
+ # The input to this function is unicode string that may contain
+ # paragraph break characters, non-breaking spaces, etc. Here we acquire
+ # the string as plain text so we can compare it.
+ current_block = self.currentBlock()
+ string = self._frontend._get_block_plain_text(current_block)
+
+ # Decide whether to check for the regular or continuation prompt.
+ if current_block.contains(self._frontend._prompt_pos):
+ prompt = self._frontend._prompt
+ else:
+ prompt = self._frontend._continuation_prompt
+
+ # Don't highlight the part of the string that contains the prompt.
+ if string.startswith(prompt):
+ self._current_offset = len(prompt)
+ qstring.remove(0, len(prompt))
+ else:
+ self._current_offset = 0
+
+ PygmentsHighlighter.highlightBlock(self, qstring)
def setFormat(self, start, count, format):
- """ Reimplemented to avoid highlighting continuation prompts.
+ """ Reimplemented to highlight selectively.
"""
start += self._current_offset
PygmentsHighlighter.setFormat(self, start, count, format)
@@ -59,9 +74,6 @@ class FrontendWidget(HistoryConsoleWidget):
def __init__(self, parent=None):
super(FrontendWidget, self).__init__(parent)
- # ConsoleWidget protected variables.
- self._continuation_prompt = '... '
-
# FrontendWidget protected variables.
self._call_tip_widget = CallTipWidget(self)
self._completion_lexer = CompletionLexer(PythonLexer())
@@ -70,6 +82,9 @@ class FrontendWidget(HistoryConsoleWidget):
self._input_splitter = InputSplitter(input_mode='replace')
self._kernel_manager = None
+ # Configure the ConsoleWidget.
+ self._set_continuation_prompt('... ')
+
self.document().contentsChange.connect(self._document_contents_change)
#---------------------------------------------------------------------------
@@ -143,17 +158,6 @@ class FrontendWidget(HistoryConsoleWidget):
return False
#---------------------------------------------------------------------------
- # 'ConsoleWidget' protected interface
- #---------------------------------------------------------------------------
-
- def _show_prompt(self, prompt=None, newline=True):
- """ Reimplemented to set a default prompt.
- """
- if prompt is None:
- prompt = '>>> '
- super(FrontendWidget, self)._show_prompt(prompt, newline)
-
- #---------------------------------------------------------------------------
# 'FrontendWidget' interface
#---------------------------------------------------------------------------
@@ -283,20 +287,24 @@ class FrontendWidget(HistoryConsoleWidget):
self.appendPlainText('Kernel process is either remote or '
'unspecified. Cannot interrupt.\n')
+ def _show_interpreter_prompt(self):
+ """ Shows a prompt for the interpreter.
+ """
+ self._show_prompt('>>> ')
+
#------ Signal handlers ----------------------------------------------------
def _started_channels(self):
""" Called when the kernel manager has started listening.
"""
- QtGui.QPlainTextEdit.clear(self)
- if self._reading:
- self._reading = False
+ self._reset()
self.appendPlainText(self._get_banner())
- self._show_prompt()
+ self._show_interpreter_prompt()
def _stopped_channels(self):
""" Called when the kernel manager has stopped listening.
"""
+ # FIXME: Print a message here?
pass
def _document_contents_change(self, position, removed, added):
@@ -327,9 +335,7 @@ class FrontendWidget(HistoryConsoleWidget):
handler(omsg)
def _handle_pyout(self, omsg):
- session = omsg['parent_header']['session']
- if session == self.kernel_manager.session.session:
- self.appendPlainText(omsg['content']['data'] + '\n')
+ self.appendPlainText(omsg['content']['data'] + '\n')
def _handle_stream(self, omsg):
self.appendPlainText(omsg['content']['data'])
@@ -351,7 +357,7 @@ class FrontendWidget(HistoryConsoleWidget):
text = "ERROR: ABORTED\n"
self.appendPlainText(text)
self._hidden = True
- self._show_prompt()
+ self._show_interpreter_prompt()
self.executed.emit(rep)
def _handle_complete_reply(self, rep):
diff --git a/IPython/frontend/qt/console/ipython_widget.py b/IPython/frontend/qt/console/ipython_widget.py
index ee08223..1b950b2 100644
--- a/IPython/frontend/qt/console/ipython_widget.py
+++ b/IPython/frontend/qt/console/ipython_widget.py
@@ -10,6 +10,14 @@ class IPythonWidget(FrontendWidget):
""" A FrontendWidget for an IPython kernel.
"""
+ # The default stylesheet for prompts, colors, etc.
+ default_stylesheet = """
+ .in-prompt { color: navy; }
+ .in-prompt-number { font-weight: bold; }
+ .out-prompt { color: darkred; }
+ .out-prompt-number { font-weight: bold; }
+ """
+
#---------------------------------------------------------------------------
# 'QObject' interface
#---------------------------------------------------------------------------
@@ -17,7 +25,12 @@ class IPythonWidget(FrontendWidget):
def __init__(self, parent=None):
super(IPythonWidget, self).__init__(parent)
+ # Initialize protected variables.
self._magic_overrides = {}
+ self._prompt_count = 0
+
+ # Set a default stylesheet.
+ self.set_style_sheet(self.default_stylesheet)
#---------------------------------------------------------------------------
# 'ConsoleWidget' abstract interface
@@ -38,7 +51,7 @@ class IPythonWidget(FrontendWidget):
output = callback(arguments)
if output:
self.appendPlainText(output)
- self._show_prompt()
+ self._show_interpreter_prompt()
else:
super(IPythonWidget, self)._execute(source, hidden)
@@ -56,10 +69,36 @@ class IPythonWidget(FrontendWidget):
#---------------------------------------------------------------------------
def _get_banner(self):
- """ Reimplemented to a return IPython's default banner.
+ """ Reimplemented to return IPython's default banner.
"""
return default_banner
+ def _show_interpreter_prompt(self):
+ """ Reimplemented for IPython-style prompts.
+ """
+ self._prompt_count += 1
+ prompt_template = '%s'
+ prompt_body = '
In [%i]: '
+ prompt = (prompt_template % prompt_body) % self._prompt_count
+ self._show_prompt(prompt, html=True)
+
+ # Update continuation prompt to reflect (possibly) new prompt length.
+ cont_prompt_chars = '...: '
+ space_count = len(self._prompt.lstrip()) - len(cont_prompt_chars)
+ cont_prompt_body = ' ' * space_count + cont_prompt_chars
+ self._continuation_prompt_html = prompt_template % cont_prompt_body
+
+ #------ Signal handlers ----------------------------------------------------
+
+ def _handle_pyout(self, omsg):
+ """ Reimplemented for IPython-style "display hook".
+ """
+ prompt_template = '%s'
+ prompt_body = 'Out[%i]: '
+ prompt = (prompt_template % prompt_body) % self._prompt_count
+ self.appendHtml(prompt)
+ self.appendPlainText(omsg['content']['data'] + '\n')
+
#---------------------------------------------------------------------------
# 'IPythonWidget' interface
#---------------------------------------------------------------------------
@@ -81,6 +120,11 @@ class IPythonWidget(FrontendWidget):
except KeyError:
pass
+ def set_style_sheet(self, stylesheet):
+ """ Sets the style sheet.
+ """
+ self.document().setDefaultStyleSheet(stylesheet)
+
if __name__ == '__main__':
from IPython.frontend.qt.kernelmanager import QtKernelManager
diff --git a/IPython/frontend/qt/console/pygments_highlighter.py b/IPython/frontend/qt/console/pygments_highlighter.py
index 51197a8..4b45dc9 100644
--- a/IPython/frontend/qt/console/pygments_highlighter.py
+++ b/IPython/frontend/qt/console/pygments_highlighter.py
@@ -1,7 +1,7 @@
# System library imports.
from PyQt4 import QtGui
from pygments.lexer import RegexLexer, _TokenType, Text, Error
-from pygments.lexers import CLexer, CppLexer, PythonLexer
+from pygments.lexers import PythonLexer
from pygments.styles.default import DefaultStyle
from pygments.token import Comment
@@ -133,7 +133,7 @@ class PygmentsHighlighter(QtGui.QSyntaxHighlighter):
if token in self._formats:
return self._formats[token]
result = None
- for key, value in self._style.style_for_token(token) .items():
+ for key, value in self._style.style_for_token(token).items():
if value:
if result is None:
result = QtGui.QTextCharFormat()
@@ -171,12 +171,11 @@ class PygmentsHighlighter(QtGui.QSyntaxHighlighter):
qcolor = self._get_color(color)
result = QtGui.QBrush(qcolor)
self._brushes[color] = result
-
return result
def _get_color(self, color):
qcolor = QtGui.QColor()
- qcolor.setRgb(int(color[:2],base=16),
+ qcolor.setRgb(int(color[:2], base=16),
int(color[2:4], base=16),
int(color[4:6], base=16))
return qcolor