diff --git a/IPython/frontend/qt/console/completion_html.py b/IPython/frontend/qt/console/completion_html.py
new file mode 100644
index 0000000..852fcf3
--- /dev/null
+++ b/IPython/frontend/qt/console/completion_html.py
@@ -0,0 +1,371 @@
+"""a navigable completer for the qtconsole"""
+# coding : utf-8
+#-----------------------------------------------------------------------------
+# Copyright (c) 2012, IPython Development Team.$
+#
+# Distributed under the terms of the Modified BSD License.$
+#
+# The full license is in the file COPYING.txt, distributed with this software.
+#-----------------------------------------------------------------------------
+
+# System library imports
+import IPython.utils.text as text
+
+from IPython.external.qt import QtCore, QtGui
+
+#--------------------------------------------------------------------------
+# Return an HTML table with selected item in a special class
+#--------------------------------------------------------------------------
+def html_tableify(item_matrix, select=None, header=None , footer=None) :
+ """ returnr a string for an html table"""
+ if not item_matrix :
+ return ''
+ html_cols = []
+ tds = lambda text : u'
'+text+u' | '
+ trs = lambda text : u''+text+u'
'
+ tds_items = [map(tds, row) for row in item_matrix]
+ if select :
+ row, col = select
+ tds_items[row][col] = u''\
+ +item_matrix[row][col]\
+ +u' | '
+ #select the right item
+ html_cols = map(trs, (u''.join(row) for row in tds_items))
+ head = ''
+ foot = ''
+ if header :
+ head = (u''\
+ +''.join((u''+header+u' | ')*len(item_matrix[0]))\
+ +'
')
+
+ if footer :
+ foot = (u''\
+ +''.join((u''+footer+u' | ')*len(item_matrix[0]))\
+ +'
')
+ html = (u''+head+(u''.join(html_cols))+foot+u'
')
+ return html
+
+class SlidingInterval(object):
+ """a bound interval that follows a cursor
+
+ internally used to scoll the completion view when the cursor
+ try to go beyond the edges, and show '...' when rows are hidden
+ """
+
+ _min = 0
+ _max = 1
+ _current = 0
+ def __init__(self, maximum=1, width=6, minimum=0, sticky_lenght=1):
+ """Create a new bounded interval
+
+ any value return by this will be bound between maximum and
+ minimum. usual width will be 'width', and sticky_length
+ set when the return interval should expand to max and min
+ """
+ self._min = minimum
+ self._max = maximum
+ self._start = 0
+ self._width = width
+ self._stop = self._start+self._width+1
+ self._sticky_lenght = sticky_lenght
+
+ @property
+ def current(self):
+ """current cursor position"""
+ return self._current
+
+ @current.setter
+ def current(self, value):
+ """set current cursor position"""
+ current = min(max(self._min, value), self._max)
+
+ self._current = current
+
+ if current > self._stop :
+ self._stop = current
+ self._start = current-self._width
+ elif current < self._start :
+ self._start = current
+ self._stop = current + self._width
+
+ if abs(self._start - self._min) <= self._sticky_lenght :
+ self._start = self._min
+
+ if abs(self._stop - self._max) <= self._sticky_lenght :
+ self._stop = self._max
+
+ @property
+ def start(self):
+ """begiiing of interval to show"""
+ return self._start
+
+ @property
+ def stop(self):
+ """end of interval to show"""
+ return self._stop
+
+ @property
+ def width(self):
+ return self._stop - self._start
+
+ @property
+ def nth(self):
+ return self.current - self.start
+
+class CompletionHtml(QtGui.QWidget):
+ """ A widget for tab completion, navigable by arrow keys """
+
+ #--------------------------------------------------------------------------
+ # 'QObject' interface
+ #--------------------------------------------------------------------------
+
+ _items = ()
+ _index = (0, 0)
+ _consecutive_tab = 0
+ _size = (1, 1)
+ _old_cursor = None
+ _start_position = 0
+ _slice_start = 0
+ _slice_len = 4
+
+ def __init__(self, console_widget):
+ """ Create a completion widget that is attached to the specified Qt
+ text edit widget.
+ """
+ assert isinstance(console_widget._control, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
+ super(CompletionHtml, self).__init__()
+
+ self._text_edit = console_widget._control
+ self._console_widget = console_widget
+ self._text_edit.installEventFilter(self)
+ self._sliding_interval = None
+ self._justified_items = None
+
+ # Ensure that the text edit keeps focus when widget is displayed.
+ self.setFocusProxy(self._text_edit)
+
+
+ def eventFilter(self, obj, event):
+ """ Reimplemented to handle keyboard input and to auto-hide when the
+ text edit loses focus.
+ """
+ if obj == self._text_edit:
+ etype = event.type()
+ if etype == QtCore.QEvent.KeyPress:
+ key = event.key()
+ if self._consecutive_tab == 0 and key in (QtCore.Qt.Key_Tab,):
+ return False
+ elif self._consecutive_tab == 1 and key in (QtCore.Qt.Key_Tab,):
+ # ok , called twice, we grab focus, and show the cursor
+ self._consecutive_tab = self._consecutive_tab+1
+ self._update_list()
+ return True
+ elif self._consecutive_tab == 2:
+ if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
+ self._complete_current()
+ return True
+ if key in (QtCore.Qt.Key_Tab,):
+ self.select_right()
+ self._update_list()
+ return True
+ elif key in ( QtCore.Qt.Key_Down,):
+ self.select_down()
+ self._update_list()
+ return True
+ elif key in (QtCore.Qt.Key_Right,):
+ self.select_right()
+ self._update_list()
+ return True
+ elif key in ( QtCore.Qt.Key_Up,):
+ self.select_up()
+ self._update_list()
+ return True
+ elif key in ( QtCore.Qt.Key_Left,):
+ self.select_left()
+ self._update_list()
+ return True
+ elif key in ( QtCore.Qt.Key_Escape,):
+ self.cancel_completion()
+ return True
+ else :
+ self.cancel_completion()
+ else:
+ self.cancel_completion()
+
+ elif etype == QtCore.QEvent.FocusOut:
+ self.cancel_completion()
+
+ return super(CompletionHtml, self).eventFilter(obj, event)
+
+ #--------------------------------------------------------------------------
+ # 'CompletionHtml' interface
+ #--------------------------------------------------------------------------
+ def cancel_completion(self):
+ """Cancel the completion
+
+ should be called when the completer have to be dismissed
+
+ This reset internal variable, clearing the temporary buffer
+ of the console where the completion are shown.
+ """
+ self._consecutive_tab = 0
+ self._slice_start = 0
+ self._console_widget._clear_temporary_buffer()
+ self._index = (0, 0)
+ if(self._sliding_interval):
+ self._sliding_interval = None
+
+ #
+ # ... 2 4 4 4 4 4 4 4 4 4 4 4 4
+ # 2 2 4 4 4 4 4 4 4 4 4 4 4 4
+ #
+ #2 2 x x x x x x x x x x x 5 5
+ #6 6 x x x x x x x x x x x 5 5
+ #6 6 x x x x x x x x x x ? 5 5
+ #6 6 x x x x x x x x x x ? 1 1
+ #
+ #3 3 3 3 3 3 3 3 3 3 3 3 1 1 1 ...
+ #3 3 3 3 3 3 3 3 3 3 3 3 1 1 1 ...
+ def _select_index(self, row, col):
+ """Change the selection index, and make sure it stays in the right range
+
+ A little more complicated than just dooing modulo the number of row columns
+ to be sure to cycle through all element.
+
+ horizontaly, the element are maped like this :
+ to r <-- a b c d e f --> to g
+ to f <-- g h i j k l --> to m
+ to l <-- m n o p q r --> to a
+
+ and vertically
+ a d g j m p
+ b e h k n q
+ c f i l o r
+ """
+
+ nr, nc = self._size
+ nr = nr-1
+ nc = nc-1
+
+ # case 1
+ if (row > nr and col >= nc) or (row >= nr and col > nc):
+ self._select_index(0, 0)
+ # case 2
+ elif (row <= 0 and col < 0) or (row < 0 and col <= 0):
+ self._select_index(nr, nc)
+ # case 3
+ elif row > nr :
+ self._select_index(0, col+1)
+ # case 4
+ elif row < 0 :
+ self._select_index(nr, col-1)
+ # case 5
+ elif col > nc :
+ self._select_index(row+1, 0)
+ # case 6
+ elif col < 0 :
+ self._select_index(row-1, nc)
+ elif 0 <= row and row <= nr and 0 <= col and col <= nc :
+ self._index = (row, col)
+ else :
+ raise NotImplementedError("you'r trying to go where no completion\
+ have gone before : %d:%d (%d:%d)"%(row, col, nr, nc) )
+
+
+ @property
+ def _slice_end(self):
+ end = self._slice_start+self._slice_len
+ if end > len(self._items) :
+ return None
+ return end
+
+ def select_up(self):
+ """move cursor up"""
+ r, c = self._index
+ self._select_index(r-1, c)
+
+ def select_down(self):
+ """move cursor down"""
+ r, c = self._index
+ self._select_index(r+1, c)
+
+ def select_left(self):
+ """move cursor left"""
+ r, c = self._index
+ self._select_index(r, c-1)
+
+ def select_right(self):
+ """move cursor right"""
+ r, c = self._index
+ self._select_index(r, c+1)
+
+ def show_items(self, cursor, items):
+ """ Shows the completion widget with 'items' at the position specified
+ by 'cursor'.
+ """
+ if not items :
+ return
+ self._start_position = cursor.position()
+ self._consecutive_tab = 1
+ items_m, ci = text.compute_item_matrix(items, empty=' ')
+ self._sliding_interval = SlidingInterval(len(items_m)-1)
+
+ self._items = items_m
+ self._size = (ci['rows_numbers'], ci['columns_numbers'])
+ self._old_cursor = cursor
+ self._index = (0, 0)
+ sjoin = lambda x : [ y.ljust(w, ' ') for y, w in zip(x, ci['columns_width'])]
+ self._justified_items = map(sjoin, items_m)
+ self._update_list(hilight=False)
+
+
+
+
+ def _update_list(self, hilight=True):
+ """ update the list of completion and hilight the currently selected completion """
+ self._sliding_interval.current = self._index[0]
+ head = None
+ foot = None
+ if self._sliding_interval.start > 0 :
+ head = '...'
+
+ if self._sliding_interval.stop < self._sliding_interval._max:
+ foot = '...'
+ items_m = self._justified_items[\
+ self._sliding_interval.start:\
+ self._sliding_interval.stop+1\
+ ]
+
+ self._console_widget._clear_temporary_buffer()
+ if(hilight):
+ sel = (self._sliding_interval.nth, self._index[1])
+ else :
+ sel = None
+
+ strng = html_tableify(items_m, select=sel, header=head, footer=foot)
+ self._console_widget._fill_temporary_buffer(self._old_cursor, strng, html=True)
+
+ #--------------------------------------------------------------------------
+ # Protected interface
+ #--------------------------------------------------------------------------
+
+ def _complete_current(self):
+ """ Perform the completion with the currently selected item.
+ """
+ i = self._index
+ item = self._items[i[0]][i[1]]
+ item = item.strip()
+ if item :
+ self._current_text_cursor().insertText(item)
+ self.cancel_completion()
+
+ def _current_text_cursor(self):
+ """ Returns a cursor with text between the start position and the
+ current position selected.
+ """
+ cursor = self._text_edit.textCursor()
+ if cursor.position() >= self._start_position:
+ cursor.setPosition(self._start_position,
+ QtGui.QTextCursor.KeepAnchor)
+ return cursor
+
diff --git a/IPython/frontend/qt/console/completion_plain.py b/IPython/frontend/qt/console/completion_plain.py
new file mode 100644
index 0000000..d6b4066
--- /dev/null
+++ b/IPython/frontend/qt/console/completion_plain.py
@@ -0,0 +1,62 @@
+"""a simple completer for the qtconsole"""
+#-----------------------------------------------------------------------------
+# Copyright (c) 2012, IPython Development Team.$
+#
+# Distributed under the terms of the Modified BSD License.$
+#
+# The full license is in the file COPYING.txt, distributed with this software.
+#-------------------------------------------------------------------
+
+# System library imports
+from IPython.external.qt import QtCore, QtGui
+import IPython.utils.text as text
+
+
+class CompletionPlain(QtGui.QWidget):
+ """ A widget for tab completion, navigable by arrow keys """
+
+ #--------------------------------------------------------------------------
+ # 'QObject' interface
+ #--------------------------------------------------------------------------
+
+ def __init__(self, console_widget):
+ """ Create a completion widget that is attached to the specified Qt
+ text edit widget.
+ """
+ assert isinstance(console_widget._control, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
+ super(CompletionPlain, self).__init__()
+
+ self._text_edit = console_widget._control
+ self._console_widget = console_widget
+
+ self._text_edit.installEventFilter(self)
+
+ def eventFilter(self, obj, event):
+ """ Reimplemented to handle keyboard input and to auto-hide when the
+ text edit loses focus.
+ """
+ if obj == self._text_edit:
+ etype = event.type()
+
+ if etype in( QtCore.QEvent.KeyPress, QtCore.QEvent.FocusOut ):
+ self.cancel_completion()
+
+ return super(CompletionPlain, self).eventFilter(obj, event)
+
+ #--------------------------------------------------------------------------
+ # 'CompletionPlain' interface
+ #--------------------------------------------------------------------------
+ def cancel_completion(self):
+ """Cancel the completion, reseting internal variable, clearing buffer """
+ self._console_widget._clear_temporary_buffer()
+
+
+ def show_items(self, cursor, items):
+ """ Shows the completion widget with 'items' at the position specified
+ by 'cursor'.
+ """
+ if not items :
+ return
+ self.cancel_completion()
+ strng = text.columnize(items)
+ self._console_widget._fill_temporary_buffer(cursor, strng, html=False)
diff --git a/IPython/frontend/qt/console/completion_widget.py b/IPython/frontend/qt/console/completion_widget.py
index 194b843..b258775 100644
--- a/IPython/frontend/qt/console/completion_widget.py
+++ b/IPython/frontend/qt/console/completion_widget.py
@@ -10,10 +10,11 @@ class CompletionWidget(QtGui.QListWidget):
# 'QObject' interface
#--------------------------------------------------------------------------
- def __init__(self, text_edit):
+ def __init__(self, console_widget):
""" Create a completion widget that is attached to the specified Qt
text edit widget.
"""
+ text_edit = console_widget._control
assert isinstance(text_edit, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
super(CompletionWidget, self).__init__()
@@ -132,3 +133,6 @@ class CompletionWidget(QtGui.QListWidget):
self.hide()
else:
self.hide()
+
+ def cancel_completion(self):
+ self.hide()
diff --git a/IPython/frontend/qt/console/console_widget.py b/IPython/frontend/qt/console/console_widget.py
index e4b3d6c..cd2a93c 100644
--- a/IPython/frontend/qt/console/console_widget.py
+++ b/IPython/frontend/qt/console/console_widget.py
@@ -5,7 +5,6 @@
#-----------------------------------------------------------------------------
# Standard library imports
-import os
from os.path import commonprefix
import re
import sys
@@ -23,6 +22,8 @@ from IPython.utils.text import columnize
from IPython.utils.traitlets import Bool, Enum, Integer, Unicode
from ansi_code_processor import QtAnsiCodeProcessor
from completion_widget import CompletionWidget
+from completion_html import CompletionHtml
+from completion_plain import CompletionPlain
from kill_ring import QtKillRing
#-----------------------------------------------------------------------------
@@ -65,10 +66,19 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget):
non-positive number disables text truncation (not recommended).
"""
)
- gui_completion = Bool(False, config=True,
- help="""
- Use a list widget instead of plain text output for tab completion.
- """
+ gui_completion = Enum(['plain', 'droplist', 'ncurses'], config=True,
+ default_value = 'ncurses',
+ help="""
+ The type of completer to use. Valid values are:
+
+ 'plain' : Show the availlable completion as a text list
+ Below the editting area.
+ 'droplist': Show the completion in a drop down list navigable
+ by the arrow keys, and from which you can select
+ completion by pressing Return.
+ 'ncurses' : Show the completion as a text list which is navigable by
+ `tab` and arrow keys.
+ """
)
# NOTE: this value can only be specified during initialization.
kind = Enum(['plain', 'rich'], default_value='plain', config=True,
@@ -137,12 +147,12 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget):
font_changed = QtCore.Signal(QtGui.QFont)
#------ Protected class variables ------------------------------------------
-
+
# control handles
_control = None
_page_control = None
_splitter = None
-
+
# When the control key is down, these keys are mapped.
_ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
@@ -161,6 +171,8 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget):
[ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
QtCore.Qt.Key_V ])
+ _temp_buffer_filled = False
+
#---------------------------------------------------------------------------
# 'QObject' interface
#---------------------------------------------------------------------------
@@ -211,7 +223,13 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget):
# 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)
+ if self.gui_completion == 'ncurses':
+ self._completion_widget = CompletionHtml(self)
+ elif self.gui_completion == 'droplist':
+ self._completion_widget = CompletionWidget(self)
+ elif self.gui_completion == 'plain':
+ self._completion_widget = CompletionPlain(self)
+
self._continuation_prompt = '> '
self._continuation_prompt_html = None
self._executing = False
@@ -228,7 +246,6 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget):
self._reading = False
self._reading_callback = None
self._tab_width = 8
- self._text_completing_pos = 0
# Set a monospaced font.
self.reset_font()
@@ -823,18 +840,17 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget):
"""
self._append_custom(self._insert_plain_text, text, before_prompt)
- def _cancel_text_completion(self):
+ def _cancel_completion(self):
""" If text completion is progress, cancel it.
"""
- if self._text_completing_pos:
- self._clear_temporary_buffer()
- self._text_completing_pos = 0
+ self._completion_widget.cancel_completion()
def _clear_temporary_buffer(self):
""" Clears the "temporary text" buffer, i.e. all the text following
the prompt region.
"""
# Select and remove all text below the input buffer.
+ _temp_buffer_filled = False
cursor = self._get_prompt_cursor()
prompt = self._continuation_prompt.lstrip()
while cursor.movePosition(QtGui.QTextCursor.NextBlock):
@@ -862,7 +878,7 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget):
def _complete_with_items(self, cursor, items):
""" Performs completion with 'items' at the specified cursor location.
"""
- self._cancel_text_completion()
+ self._cancel_completion()
if len(items) == 1:
cursor.setPosition(self._control.textCursor().position(),
@@ -877,19 +893,26 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget):
cursor.insertText(prefix)
current_pos = cursor.position()
- if self.gui_completion:
- cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
- self._completion_widget.show_items(cursor, items)
- else:
- cursor.beginEditBlock()
- self._append_plain_text('\n')
- self._page(self._format_as_columns(items))
- cursor.endEditBlock()
+ cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
+ self._completion_widget.show_items(cursor, items)
+
+
+ def _fill_temporary_buffer(self, cursor, text, html=False):
+ """fill the area below the active editting zone with text"""
+
+ current_pos = self._control.textCursor().position()
+
+ cursor.beginEditBlock()
+ self._append_plain_text('\n')
+ self._page(text, html=html)
+ cursor.endEditBlock()
+
+ cursor.setPosition(current_pos)
+ self._control.moveCursor(QtGui.QTextCursor.End)
+ self._control.setTextCursor(cursor)
+
+ _temp_buffer_filled = True
- cursor.setPosition(current_pos)
- self._control.moveCursor(QtGui.QTextCursor.End)
- self._control.setTextCursor(cursor)
- self._text_completing_pos = current_pos
def _context_menu_make(self, pos):
""" Creates a context menu for the given QPoint (in widget coordinates).
@@ -951,7 +974,6 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget):
control.viewport().installEventFilter(self)
# Connect signals.
- control.cursorPositionChanged.connect(self._cursor_position_changed)
control.customContextMenuRequested.connect(
self._custom_context_menu_requested)
control.copyAvailable.connect(self.copy_available)
@@ -1021,7 +1043,7 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget):
intercepted = True
# Special handling when tab completing in text mode.
- self._cancel_text_completion()
+ self._cancel_completion()
if self._in_buffer(position):
# Special handling when a reading a line of raw input.
@@ -1634,8 +1656,9 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget):
def _keyboard_quit(self):
""" Cancels the current editing task ala Ctrl-G in Emacs.
"""
- if self._text_completing_pos:
- self._cancel_text_completion()
+ if self._temp_buffer_filled :
+ self._cancel_completion()
+ self._clear_temporary_buffer()
else:
self.input_buffer = ''
@@ -1853,24 +1876,6 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget):
if diff < 0 and document.blockCount() == document.maximumBlockCount():
scrollbar.setValue(scrollbar.value() + diff)
- def _cursor_position_changed(self):
- """ Clears the temporary buffer based on the cursor position.
- """
- if self._text_completing_pos:
- document = self._control.document()
- if self._text_completing_pos < document.characterCount():
- cursor = self._control.textCursor()
- pos = cursor.position()
- text_cursor = self._control.textCursor()
- text_cursor.setPosition(self._text_completing_pos)
- if pos < self._text_completing_pos or \
- cursor.blockNumber() > text_cursor.blockNumber():
- self._clear_temporary_buffer()
- self._text_completing_pos = 0
- else:
- self._clear_temporary_buffer()
- self._text_completing_pos = 0
-
def _custom_context_menu_requested(self, pos):
""" Shows a context menu at the given QPoint (in widget coordinates).
"""
diff --git a/IPython/frontend/qt/console/qtconsoleapp.py b/IPython/frontend/qt/console/qtconsoleapp.py
index 5824607..1735d9b 100644
--- a/IPython/frontend/qt/console/qtconsoleapp.py
+++ b/IPython/frontend/qt/console/qtconsoleapp.py
@@ -104,11 +104,7 @@ qt_flags = {
'plain' : ({'IPythonQtConsoleApp' : {'plain' : True}},
"Disable rich text support."),
}
-qt_flags.update(boolean_flag(
- 'gui-completion', 'ConsoleWidget.gui_completion',
- "use a GUI widget for tab completion",
- "use plaintext output for completion"
-))
+
# and app_flags from the Console Mixin
qt_flags.update(app_flags)
# add frontend flags to the full set
@@ -117,7 +113,6 @@ flags.update(qt_flags)
# start with copy of front&backend aliases list
aliases = dict(aliases)
qt_aliases = dict(
-
style = 'IPythonWidget.syntax_style',
stylesheet = 'IPythonQtConsoleApp.stylesheet',
colors = 'ZMQInteractiveShell.colors',
@@ -127,6 +122,7 @@ qt_aliases = dict(
)
# and app_aliases from the Console Mixin
qt_aliases.update(app_aliases)
+qt_aliases.update({'gui-completion':'ConsoleWidget.gui_completion'})
# add frontend aliases to the full set
aliases.update(qt_aliases)
diff --git a/IPython/frontend/qt/console/styles.py b/IPython/frontend/qt/console/styles.py
index c8e2ba7..c72808f 100644
--- a/IPython/frontend/qt/console/styles.py
+++ b/IPython/frontend/qt/console/styles.py
@@ -22,6 +22,7 @@ default_light_style_template = '''
.in-prompt-number { font-weight: bold; }
.out-prompt { color: darkred; }
.out-prompt-number { font-weight: bold; }
+ .inverted { background-color: %(fgcolor)s ; color:%(bgcolor)s;}
'''
default_light_style_sheet = default_light_style_template%dict(
bgcolor='white', fgcolor='black', select="#ccc")
@@ -38,6 +39,7 @@ default_dark_style_template = '''
.in-prompt-number { color: lime; font-weight: bold; }
.out-prompt { color: red; }
.out-prompt-number { color: red; font-weight: bold; }
+ .inverted { background-color: %(fgcolor)s ; color:%(bgcolor)s;}
'''
default_dark_style_sheet = default_dark_style_template%dict(
bgcolor='black', fgcolor='white', select="#555")
@@ -50,6 +52,7 @@ default_bw_style_sheet = '''
selection-background-color: #cccccc}
.in-prompt-number { font-weight: bold; }
.out-prompt-number { font-weight: bold; }
+ .inverted { background-color: black ; color: white;}
'''
default_bw_syntax_style = 'bw'
diff --git a/IPython/utils/tests/test_text.py b/IPython/utils/tests/test_text.py
index ce6e3d0..36b6075 100644
--- a/IPython/utils/tests/test_text.py
+++ b/IPython/utils/tests/test_text.py
@@ -14,6 +14,7 @@
import os
import math
+import random
import nose.tools as nt
@@ -32,13 +33,37 @@ def test_columnize():
items = [l*size for l in 'abc']
out = text.columnize(items, displaywidth=80)
nt.assert_equals(out, 'aaaaa bbbbb ccccc\n')
- out = text.columnize(items, displaywidth=10)
+ out = text.columnize(items, displaywidth=12)
nt.assert_equals(out, 'aaaaa ccccc\nbbbbb\n')
-
+ out = text.columnize(items, displaywidth=10)
+ nt.assert_equals(out, 'aaaaa\nbbbbb\nccccc\n')
+
+def test_columnize_random():
+ """Test with random input to hopfully catch edge case """
+ for nitems in [random.randint(2,70) for i in range(2,20)]:
+ displaywidth = random.randint(20,200)
+ rand_len = [random.randint(2,displaywidth) for i in range(nitems)]
+ items = ['x'*l for l in rand_len]
+ out = text.columnize(items, displaywidth=displaywidth)
+ longer_line = max([len(x) for x in out.split('\n')])
+ longer_element = max(rand_len)
+ if longer_line > displaywidth:
+ print "Columnize displayed something lager than displaywidth : %s " % longer_line
+ print "longer element : %s " % longer_element
+ print "displaywidth : %s " % displaywidth
+ print "number of element : %s " % nitems
+ print "size of each element :\n %s" % rand_len
+ assert False
+
+def test_columnize_medium():
+ """Test with inputs than shouldn't be wider tahn 80 """
+ size = 40
+ items = [l*size for l in 'abc']
+ out = text.columnize(items, displaywidth=80)
+ nt.assert_equals(out, '\n'.join(items+['']))
def test_columnize_long():
"""Test columnize with inputs longer than the display window"""
- text.columnize(['a'*81, 'b'*81], displaywidth=80)
size = 11
items = [l*size for l in 'abc']
out = text.columnize(items, displaywidth=size-1)
diff --git a/IPython/utils/text.py b/IPython/utils/text.py
index aa07f39..0df0bec 100644
--- a/IPython/utils/text.py
+++ b/IPython/utils/text.py
@@ -24,7 +24,7 @@ import textwrap
from string import Formatter
from IPython.external.path import path
-from IPython.testing.skipdoctest import skip_doctest_py3
+from IPython.testing.skipdoctest import skip_doctest_py3, skip_doctest
from IPython.utils import py3compat
from IPython.utils.io import nlprint
from IPython.utils.data import flatten
@@ -660,6 +660,91 @@ class DollarFormatter(FullEvalFormatter):
# Re-yield the {foo} style pattern
yield (txt + literal_txt[continue_from:], field_name, format_spec, conversion)
+#-----------------------------------------------------------------------------
+# Utils to columnize a list of string
+#-----------------------------------------------------------------------------
+def _chunks(l, n):
+ """Yield successive n-sized chunks from l."""
+ for i in xrange(0, len(l), n):
+ yield l[i:i+n]
+
+def _find_optimal(rlist , separator_size=2 , displaywidth=80):
+ """Calculate optimal info to columnize a list of string"""
+ for nrow in range(1, len(rlist)+1) :
+ chk = map(max,_chunks(rlist, nrow))
+ sumlength = sum(chk)
+ ncols = len(chk)
+ if sumlength+separator_size*(ncols-1) <= displaywidth :
+ break;
+ return {'columns_numbers' : ncols,
+ 'optimal_separator_width':(displaywidth - sumlength)/(ncols-1) if (ncols -1) else 0,
+ 'rows_numbers' : nrow,
+ 'columns_width' : chk
+ }
+
+def _get_or_default(mylist, i, default=None):
+ """return list item number, or default if don't exist"""
+ if i >= len(mylist):
+ return default
+ else :
+ return mylist[i]
+
+@skip_doctest
+def compute_item_matrix(items, empty=None, *args, **kwargs) :
+ """Returns a nested list, and info to columnize items
+
+ Parameters :
+ ------------
+
+ items :
+ list of strings to columize
+ empty : (default None)
+ default value to fill list if needed
+ separator_size : int (default=2)
+ How much caracters will be used as a separation between each columns.
+ displaywidth : int (default=80)
+ The width of the area onto wich the columns should enter
+
+ Returns :
+ ---------
+
+ Returns a tuple of (strings_matrix, dict_info)
+
+ strings_matrix :
+
+ nested list of string, the outer most list contains as many list as
+ rows, the innermost lists have each as many element as colums. If the
+ total number of elements in `items` does not equal the product of
+ rows*columns, the last element of some lists are filled with `None`.
+
+ dict_info :
+ some info to make columnize easier:
+
+ columns_numbers : number of columns
+ rows_numbers : number of rows
+ columns_width : list of with of each columns
+ optimal_separator_width : best separator width between columns
+
+ Exemple :
+ ---------
+
+ In [1]: l = ['aaa','b','cc','d','eeeee','f','g','h','i','j','k','l']
+ ...: compute_item_matrix(l,displaywidth=12)
+ Out[1]:
+ ([['aaa', 'f', 'k'],
+ ['b', 'g', 'l'],
+ ['cc', 'h', None],
+ ['d', 'i', None],
+ ['eeeee', 'j', None]],
+ {'columns_numbers': 3,
+ 'columns_width': [5, 1, 1],
+ 'optimal_separator_width': 2,
+ 'rows_numbers': 5})
+
+ """
+ info = _find_optimal(map(len, items), *args, **kwargs)
+ nrow, ncol = info['rows_numbers'], info['columns_numbers']
+ return ([[ _get_or_default(items, c*nrow+i, default=empty) for c in range(ncol) ] for i in range(nrow) ], info)
def columnize(items, separator=' ', displaywidth=80):
""" Transform a list of strings into a single string with columns.
@@ -679,58 +764,9 @@ def columnize(items, separator=' ', displaywidth=80):
-------
The formatted string.
"""
- # Note: this code is adapted from columnize 0.3.2.
- # See http://code.google.com/p/pycolumnize/
-
- # Some degenerate cases.
- size = len(items)
- if size == 0:
+ if not items :
return '\n'
- elif size == 1:
- return '%s\n' % items[0]
-
- # Special case: if any item is longer than the maximum width, there's no
- # point in triggering the logic below...
- item_len = map(len, items) # save these, we can reuse them below
- longest = max(item_len)
- if longest >= displaywidth:
- return '\n'.join(items+[''])
-
- # Try every row count from 1 upwards
- array_index = lambda nrows, row, col: nrows*col + row
- for nrows in range(1, size):
- ncols = (size + nrows - 1) // nrows
- colwidths = []
- totwidth = -len(separator)
- for col in range(ncols):
- # Get max column width for this column
- colwidth = 0
- for row in range(nrows):
- i = array_index(nrows, row, col)
- if i >= size: break
- x, len_x = items[i], item_len[i]
- colwidth = max(colwidth, len_x)
- colwidths.append(colwidth)
- totwidth += colwidth + len(separator)
- if totwidth > displaywidth:
- break
- if totwidth <= displaywidth:
- break
-
- # The smallest number of rows computed and the max widths for each
- # column has been obtained. Now we just have to format each of the rows.
- string = ''
- for row in range(nrows):
- texts = []
- for col in range(ncols):
- i = row + nrows*col
- if i >= size:
- texts.append('')
- else:
- texts.append(items[i])
- while texts and not texts[-1]:
- del texts[-1]
- for col in range(len(texts)):
- texts[col] = texts[col].ljust(colwidths[col])
- string += '%s\n' % separator.join(texts)
- return string
+ matrix, info = compute_item_matrix(items, separator_size=len(separator), displaywidth=displaywidth)
+ fmatrix = [filter(None, x) for x in matrix]
+ sjoin = lambda x : separator.join([ y.ljust(w, ' ') for y, w in zip(x, info['columns_width'])])
+ return '\n'.join(map(sjoin, fmatrix))+'\n'