Skip to content
This repository

new completer for qtconsole. #1851

Merged
merged 7 commits into from almost 2 years ago

4 participants

Matthias Bussonnier Fernando Perez Puneeth Chaganti Brian E. Granger
Matthias Bussonnier
Collaborator

add a completer to the qtconsole that is navigable by arrows keys and
tab. One need to call it twice to get it on focus and be able to select
completion with Return. looks like zsh completer, not the gui drop down
list of --gui-completer.

This also try to split the completion logic from console_widget, and try
to keep the old completer qui around. The plain completer that never
takes focus back, and the QlistWidget completer.

to switch between the 3, the --gui-completion flag as been changed to
take an argument (plain, droplist, ncurses).

ipython qtconsole --gui-completion=ncurses
In[1]: r <tab> <tab> <tab> <down> <right> <right>

capture

As I'm not 100% sure of how the completion logic in the qtconsole works, i'd like you thought/testing on it.

Fernando Perez
Owner
fperez commented June 04, 2012

My first instinct here is to say awesome! But I'd like the eyes of our Qt gurus, @jdmarch, @epatters, any opinion here?

Puneeth Chaganti

It looks like a cool addition. I was just trying it out and --gui-completion=droplist crashes IPython with AttributeError: 'RichIPythonWidget' object has no attribute '_text_edit'. I can send you a full traceback, but it should be easy to reproduce.

Matthias Bussonnier
Collaborator

Thanks, I'll fix that, I also still have to clean some code and deduplicate some other.

Matthias Bussonnier
Collaborator

fix the --gui-completion=droplist ( already exist in master as just --gui-completion).
And also a bug where Ctrl+Gor esc wouldn't worked after invoking the completer.

Brian E. Granger
Owner

I tested this out and functionally this is great. I really like it. The only glitch I saw is the following: when the list of completions is longer than the size of the terminal (from enthought.traits import api; api.<tab>), it works fine for the entries that show at the top. But for entries that you have to scroll down to reach, it doesn't work.

Matthias Bussonnier
Collaborator

I'll take a look, or limit the number of completion.
I have a cleaner version that reuse more code, i'll push later.

Matthias Bussonnier
Collaborator

rebased on top of #1875 to share more code with columnize. and limit the number of completion lines to 9.

Fernando Perez
Owner
fperez commented June 09, 2012

Mmh, it seems to me in the last revision you forgot to add a file, perhaps?

With this branch, it simply doesn't start for me at all:

dreamweaver[junk]> ipython qtconsole
Traceback (most recent call last):
  File "/home/fperez/usr/bin/ipython", line 7, in <module>
    launch_new_instance()
  File "/home/fperez/usr/lib/python2.7/site-packages/IPython/frontend/terminal/ipapp.py", line 365, in launch_new_instance
    app.initialize()
  File "<string>", line 2, in initialize
  File "/home/fperez/usr/lib/python2.7/site-packages/IPython/config/application.py", line 84, in catch_config_error
    return method(app, *args, **kwargs)
  File "/home/fperez/usr/lib/python2.7/site-packages/IPython/frontend/terminal/ipapp.py", line 290, in initialize
    super(TerminalIPythonApp, self).initialize(argv)
  File "<string>", line 2, in initialize
  File "/home/fperez/usr/lib/python2.7/site-packages/IPython/config/application.py", line 84, in catch_config_error
    return method(app, *args, **kwargs)
  File "/home/fperez/usr/lib/python2.7/site-packages/IPython/core/application.py", line 325, in initialize
    self.parse_command_line(argv)
  File "/home/fperez/usr/lib/python2.7/site-packages/IPython/frontend/terminal/ipapp.py", line 285, in parse_command_line
    return super(TerminalIPythonApp, self).parse_command_line(argv)
  File "<string>", line 2, in parse_command_line
  File "/home/fperez/usr/lib/python2.7/site-packages/IPython/config/application.py", line 84, in catch_config_error
    return method(app, *args, **kwargs)
  File "/home/fperez/usr/lib/python2.7/site-packages/IPython/config/application.py", line 420, in parse_command_line
    return self.initialize_subcommand(subc, subargv)
  File "<string>", line 2, in initialize_subcommand
  File "/home/fperez/usr/lib/python2.7/site-packages/IPython/config/application.py", line 84, in catch_config_error
    return method(app, *args, **kwargs)
  File "/home/fperez/usr/lib/python2.7/site-packages/IPython/config/application.py", line 352, in initialize_subcommand
    subapp = import_item(subapp)
  File "/home/fperez/usr/lib/python2.7/site-packages/IPython/utils/importstring.py", line 40, in import_item
    module = __import__(package,fromlist=[obj])
  File "/home/fperez/usr/lib/python2.7/site-packages/IPython/frontend/qt/console/qtconsoleapp.py", line 63, in <module>
    from IPython.frontend.qt.console.frontend_widget import FrontendWidget
  File "/home/fperez/usr/lib/python2.7/site-packages/IPython/frontend/qt/console/frontend_widget.py", line 22, in <module>
    from history_console_widget import HistoryConsoleWidget
  File "/home/fperez/usr/lib/python2.7/site-packages/IPython/frontend/qt/console/history_console_widget.py", line 6, in <module>
    from console_widget import ConsoleWidget
  File "/home/fperez/usr/lib/python2.7/site-packages/IPython/frontend/qt/console/console_widget.py", line 26, in <module>
    from completion_plain import CompletionPlain
  File "/home/fperez/usr/lib/python2.7/site-packages/IPython/frontend/qt/console/completion_plain.py", line 3, in <module>
    import IPython.utils.html_utils as html_utils
ImportError: No module named html_utils
Matthias Bussonnier
Collaborator

Sorry, I think I was tired, I rebased a remote branch that I forgot to push from work, so many things were missing, and the .pyc files still present made it work at home...

Should be fixed now, It also allowed me to simplifie more things.

Fernando Perez
Owner
fperez commented June 09, 2012

When the list of completions doesn't fit in a single group, there's no way to show the rest (and no indication there are more). How about putting at the end a 'more' entry that loads a new page? If a second page is loaded, then there should be a way to go back to the first.

Try it with import numpy as np, np.<tab>, you only see the first set...

added some commits June 06, 2012
Matthias Bussonnier re-write columnize, with intermediate step.
fix test that where wrong, add some others.

fix #1860
981ef5c
Matthias Bussonnier fix docstring, and debug leftover 4659fca
Matthias Bussonnier new completer for qtconsole.
add a completer to the qtconsole that is navigable by arraow keys and
tab. One need to call it twice to get it on focus and be able to select
completion with Return. looks like zsh completer, not the gui drop down
list of --gui-completer.

This also try to split the completion logic from console_widget, and try
to keep the old completer qui around. The plain completer that never
takes focus back, and the QlistWidget completer.

to switch between the 3, the --gui-completion flag as been changed to
take an argument (plain, droplist, ncurses).
1422d27
Matthias Bussonnier fix gui=droplist 3cb4e60
Matthias Bussonnier fix Ctrl+G/esc intercepted bug f7faea8
Matthias Bussonnier add option to choose for empty ef8d7e2
Matthias Bussonnier new completer for qtconsole
add a completer to the qtconsole that is navigable by arrows keys and
tab. One need to call it twice to get it on focus and be able to select
completion with Return. looks like zsh completer, not the gui drop down
list of --gui-completer.

This also try to split the completion logic from console_widget, and try
to keep the old completer qui around. The plain completer that never
takes focus back, and the QlistWidget completer.

to switch between the 3, the --gui-completion flag as been changed to
take an argument (plain, droplist, ncurses)

completer also autoscroll and show `...` when rows are hidden
68781aa
Matthias Bussonnier
Collaborator

how does this look like ?
cap
If you try to go up or down it scrolls. of course the ... disapears when at top or bottom...

I rebase and squash because the rebased on top of #1875 was painfull.

Brian E. Granger
Owner
Matthias Bussonnier
Collaborator

Thanks, it took me several shot to have it right. If someone want to modifie to add PgUp/PgDown, go ahead, I just need a break from this one for a few days, and i'm not ready to try to make it work 'as you type', but it should be doable.
We can keep this for SciPy sprint.

Fernando Perez
Owner
fperez commented June 10, 2012

Beautiful!! You've done a terrific job here, and I commend you for it. Many thanks for your patience with all my minute requirements.

I'm going to merge this now, as it's really a great improvement, to reduce the chance of conflicts. We can always do another round of improvements on it later from master.

Thanks again for your great work!

Fernando Perez fperez merged commit 45d28c5 into from June 10, 2012
Fernando Perez fperez closed this June 10, 2012
Matthias Bussonnier
Collaborator

It was really bugging me because I'm really used to it in zsh, so it is also really a relief for me to be able to tabs through completion.
I hope that it will be one of the many details that will make IPython aven more impressive at SciPy. BTW, i've been selected for the sponsorship, and I'm now waiting for the detail to prepare my trip. So I'll be there to give you a hand for the presentation :-)

See you there.

Fernando Perez
Owner
fperez commented June 10, 2012
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 7 unique commits by 1 author.

Jun 10, 2012
Matthias Bussonnier re-write columnize, with intermediate step.
fix test that where wrong, add some others.

fix #1860
981ef5c
Matthias Bussonnier fix docstring, and debug leftover 4659fca
Matthias Bussonnier new completer for qtconsole.
add a completer to the qtconsole that is navigable by arraow keys and
tab. One need to call it twice to get it on focus and be able to select
completion with Return. looks like zsh completer, not the gui drop down
list of --gui-completer.

This also try to split the completion logic from console_widget, and try
to keep the old completer qui around. The plain completer that never
takes focus back, and the QlistWidget completer.

to switch between the 3, the --gui-completion flag as been changed to
take an argument (plain, droplist, ncurses).
1422d27
Matthias Bussonnier fix gui=droplist 3cb4e60
Matthias Bussonnier fix Ctrl+G/esc intercepted bug f7faea8
Matthias Bussonnier add option to choose for empty ef8d7e2
Matthias Bussonnier new completer for qtconsole
add a completer to the qtconsole that is navigable by arrows keys and
tab. One need to call it twice to get it on focus and be able to select
completion with Return. looks like zsh completer, not the gui drop down
list of --gui-completer.

This also try to split the completion logic from console_widget, and try
to keep the old completer qui around. The plain completer that never
takes focus back, and the QlistWidget completer.

to switch between the 3, the --gui-completion flag as been changed to
take an argument (plain, droplist, ncurses)

completer also autoscroll and show `...` when rows are hidden
68781aa
This page is out of date. Refresh to see the latest.
371  IPython/frontend/qt/console/completion_html.py
... ...
@@ -0,0 +1,371 @@
  1
+"""a navigable completer for the qtconsole"""
  2
+# coding : utf-8
  3
+#-----------------------------------------------------------------------------
  4
+# Copyright (c) 2012, IPython Development Team.$
  5
+#
  6
+# Distributed under the terms of the Modified BSD License.$
  7
+#
  8
+# The full license is in the file COPYING.txt, distributed with this software.
  9
+#-----------------------------------------------------------------------------
  10
+
  11
+# System library imports
  12
+import IPython.utils.text as text
  13
+
  14
+from IPython.external.qt import QtCore, QtGui
  15
+
  16
+#--------------------------------------------------------------------------
  17
+# Return an HTML table with selected item in a special class
  18
+#--------------------------------------------------------------------------
  19
+def html_tableify(item_matrix, select=None, header=None , footer=None) :
  20
+    """ returnr a string for an html table"""
  21
+    if not item_matrix :
  22
+        return ''
  23
+    html_cols = []
  24
+    tds = lambda text : u'<td>'+text+u'  </td>'
  25
+    trs = lambda text : u'<tr>'+text+u'</tr>'
  26
+    tds_items = [map(tds, row) for row in item_matrix]
  27
+    if select :
  28
+        row, col = select
  29
+        tds_items[row][col] = u'<td class="inverted">'\
  30
+                +item_matrix[row][col]\
  31
+                +u'  </td>'
  32
+    #select the right item
  33
+    html_cols = map(trs, (u''.join(row) for row in tds_items))
  34
+    head = ''
  35
+    foot = ''
  36
+    if header :
  37
+        head = (u'<tr>'\
  38
+            +''.join((u'<td>'+header+u'</td>')*len(item_matrix[0]))\
  39
+            +'</tr>')
  40
+
  41
+    if footer : 
  42
+        foot = (u'<tr>'\
  43
+            +''.join((u'<td>'+footer+u'</td>')*len(item_matrix[0]))\
  44
+            +'</tr>')
  45
+    html = (u'<table class="completion" style="white-space:pre">'+head+(u''.join(html_cols))+foot+u'</table>')
  46
+    return html
  47
+
  48
+class SlidingInterval(object): 
  49
+    """a bound interval that follows a cursor
  50
+    
  51
+    internally used to scoll the completion view when the cursor 
  52
+    try to go beyond the edges, and show '...' when rows are hidden
  53
+    """
  54
+    
  55
+    _min = 0
  56
+    _max = 1
  57
+    _current = 0
  58
+    def __init__(self, maximum=1, width=6, minimum=0, sticky_lenght=1):
  59
+        """Create a new bounded interval
  60
+        
  61
+        any value return by this will be bound between maximum and 
  62
+        minimum. usual width will be 'width', and sticky_length 
  63
+        set when the return  interval should expand to max and min
  64
+        """
  65
+        self._min = minimum 
  66
+        self._max = maximum
  67
+        self._start = 0
  68
+        self._width = width
  69
+        self._stop = self._start+self._width+1
  70
+        self._sticky_lenght = sticky_lenght
  71
+        
  72
+    @property
  73
+    def current(self):
  74
+        """current cursor position"""
  75
+        return self._current
  76
+    
  77
+    @current.setter
  78
+    def current(self, value):
  79
+        """set current cursor position"""
  80
+        current = min(max(self._min, value), self._max)
  81
+
  82
+        self._current = current
  83
+
  84
+        if current > self._stop : 
  85
+            self._stop = current
  86
+            self._start = current-self._width
  87
+        elif current < self._start : 
  88
+            self._start = current
  89
+            self._stop = current + self._width
  90
+
  91
+        if abs(self._start - self._min) <= self._sticky_lenght :
  92
+            self._start = self._min
  93
+        
  94
+        if abs(self._stop - self._max) <= self._sticky_lenght :
  95
+            self._stop = self._max
  96
+
  97
+    @property 
  98
+    def start(self):
  99
+        """begiiing of interval to show"""
  100
+        return self._start
  101
+        
  102
+    @property
  103
+    def stop(self):
  104
+        """end of interval to show"""
  105
+        return self._stop
  106
+
  107
+    @property
  108
+    def width(self):
  109
+        return self._stop - self._start
  110
+
  111
+    @property 
  112
+    def nth(self):
  113
+        return self.current - self.start
  114
+
  115
+class CompletionHtml(QtGui.QWidget):
  116
+    """ A widget for tab completion,  navigable by arrow keys """
  117
+
  118
+    #--------------------------------------------------------------------------
  119
+    # 'QObject' interface
  120
+    #--------------------------------------------------------------------------
  121
+
  122
+    _items = ()
  123
+    _index = (0, 0)
  124
+    _consecutive_tab = 0
  125
+    _size = (1, 1)
  126
+    _old_cursor = None
  127
+    _start_position = 0
  128
+    _slice_start = 0
  129
+    _slice_len = 4
  130
+
  131
+    def __init__(self, console_widget):
  132
+        """ Create a completion widget that is attached to the specified Qt
  133
+            text edit widget.
  134
+        """
  135
+        assert isinstance(console_widget._control, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
  136
+        super(CompletionHtml, self).__init__()
  137
+
  138
+        self._text_edit = console_widget._control
  139
+        self._console_widget = console_widget
  140
+        self._text_edit.installEventFilter(self)
  141
+        self._sliding_interval = None
  142
+        self._justified_items = None
  143
+
  144
+        # Ensure that the text edit keeps focus when widget is displayed.
  145
+        self.setFocusProxy(self._text_edit)
  146
+
  147
+
  148
+    def eventFilter(self, obj, event):
  149
+        """ Reimplemented to handle keyboard input and to auto-hide when the
  150
+            text edit loses focus.
  151
+        """
  152
+        if obj == self._text_edit:
  153
+            etype = event.type()
  154
+            if etype == QtCore.QEvent.KeyPress:
  155
+                key = event.key()
  156
+                if self._consecutive_tab == 0 and key in (QtCore.Qt.Key_Tab,):
  157
+                    return False
  158
+                elif self._consecutive_tab == 1 and key in (QtCore.Qt.Key_Tab,):
  159
+                    # ok , called twice, we grab focus, and show the cursor
  160
+                    self._consecutive_tab = self._consecutive_tab+1
  161
+                    self._update_list()
  162
+                    return True
  163
+                elif self._consecutive_tab == 2:
  164
+                    if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
  165
+                        self._complete_current()
  166
+                        return True
  167
+                    if key in (QtCore.Qt.Key_Tab,):
  168
+                        self.select_right()
  169
+                        self._update_list()
  170
+                        return True
  171
+                    elif key in ( QtCore.Qt.Key_Down,):
  172
+                        self.select_down()
  173
+                        self._update_list()
  174
+                        return True
  175
+                    elif key in (QtCore.Qt.Key_Right,):
  176
+                        self.select_right()
  177
+                        self._update_list()
  178
+                        return True
  179
+                    elif key in ( QtCore.Qt.Key_Up,):
  180
+                        self.select_up()
  181
+                        self._update_list()
  182
+                        return True
  183
+                    elif key in ( QtCore.Qt.Key_Left,):
  184
+                        self.select_left()
  185
+                        self._update_list()
  186
+                        return True
  187
+                    elif key in ( QtCore.Qt.Key_Escape,):
  188
+                        self.cancel_completion()
  189
+                        return True
  190
+                    else :
  191
+                        self.cancel_completion()
  192
+                else:
  193
+                    self.cancel_completion()
  194
+
  195
+            elif etype == QtCore.QEvent.FocusOut:
  196
+                self.cancel_completion()
  197
+
  198
+        return super(CompletionHtml, self).eventFilter(obj, event)
  199
+
  200
+    #--------------------------------------------------------------------------
  201
+    # 'CompletionHtml' interface
  202
+    #--------------------------------------------------------------------------
  203
+    def cancel_completion(self):
  204
+        """Cancel the completion
  205
+
  206
+        should be called when the completer have to be dismissed
  207
+
  208
+        This reset internal variable, clearing the temporary buffer
  209
+        of the console where the completion are shown.
  210
+        """
  211
+        self._consecutive_tab = 0
  212
+        self._slice_start = 0
  213
+        self._console_widget._clear_temporary_buffer()
  214
+        self._index = (0, 0)
  215
+        if(self._sliding_interval):
  216
+            self._sliding_interval = None
  217
+
  218
+    #
  219
+    #  ...  2 4 4 4 4 4 4 4 4 4 4  4  4
  220
+    #   2   2 4 4 4 4 4 4 4 4 4 4  4  4
  221
+    #
  222
+    #2  2   x x x x x x x x x x x  5  5
  223
+    #6  6   x x x x x x x x x x x  5  5
  224
+    #6  6   x x x x x x x x x x ?  5  5
  225
+    #6  6   x x x x x x x x x x ?  1  1
  226
+    #
  227
+    #3  3   3 3 3 3 3 3 3 3 3 3 1  1  1 ...
  228
+    #3  3   3 3 3 3 3 3 3 3 3 3 1  1  1 ...
  229
+    def _select_index(self, row, col):
  230
+        """Change the selection index, and make sure it stays in the right range
  231
+
  232
+        A little more complicated than just dooing modulo the number of row columns
  233
+        to be sure to cycle through all element.
  234
+
  235
+        horizontaly, the element are maped like this :
  236
+        to r <-- a b c d e f --> to g
  237
+        to f <-- g h i j k l --> to m
  238
+        to l <-- m n o p q r --> to a
  239
+
  240
+        and vertically
  241
+        a d g j m p
  242
+        b e h k n q
  243
+        c f i l o r
  244
+        """
  245
+
  246
+        nr, nc = self._size
  247
+        nr = nr-1
  248
+        nc = nc-1
  249
+
  250
+        # case 1
  251
+        if (row > nr and col >= nc) or (row >= nr and col > nc):
  252
+            self._select_index(0, 0)
  253
+        # case 2
  254
+        elif (row <= 0 and col < 0) or  (row < 0 and col <= 0):
  255
+            self._select_index(nr, nc)
  256
+        # case 3
  257
+        elif row > nr :
  258
+            self._select_index(0, col+1)
  259
+        # case 4
  260
+        elif row < 0 :
  261
+            self._select_index(nr, col-1)
  262
+        # case 5
  263
+        elif col > nc :
  264
+            self._select_index(row+1, 0)
  265
+        # case 6
  266
+        elif col < 0 :
  267
+            self._select_index(row-1, nc)
  268
+        elif 0 <= row and row <= nr and 0 <= col and col <= nc :
  269
+            self._index = (row, col)
  270
+        else :
  271
+            raise NotImplementedError("you'r trying to go where no completion\
  272
+                           have gone before : %d:%d (%d:%d)"%(row, col, nr, nc) )
  273
+
  274
+
  275
+    @property
  276
+    def _slice_end(self):
  277
+        end = self._slice_start+self._slice_len
  278
+        if end > len(self._items) :
  279
+            return None
  280
+        return end
  281
+
  282
+    def select_up(self):
  283
+        """move cursor up"""
  284
+        r, c = self._index
  285
+        self._select_index(r-1, c)
  286
+
  287
+    def select_down(self):
  288
+        """move cursor down"""
  289
+        r, c = self._index
  290
+        self._select_index(r+1, c)
  291
+
  292
+    def select_left(self):
  293
+        """move cursor left"""
  294
+        r, c = self._index
  295
+        self._select_index(r, c-1)
  296
+
  297
+    def select_right(self):
  298
+        """move cursor right"""
  299
+        r, c = self._index
  300
+        self._select_index(r, c+1)
  301
+
  302
+    def show_items(self, cursor, items):
  303
+        """ Shows the completion widget with 'items' at the position specified
  304
+            by 'cursor'.
  305
+        """
  306
+        if not items :
  307
+            return
  308
+        self._start_position = cursor.position()
  309
+        self._consecutive_tab = 1
  310
+        items_m, ci = text.compute_item_matrix(items, empty=' ')
  311
+        self._sliding_interval = SlidingInterval(len(items_m)-1)
  312
+
  313
+        self._items = items_m
  314
+        self._size = (ci['rows_numbers'], ci['columns_numbers'])
  315
+        self._old_cursor = cursor
  316
+        self._index = (0, 0)
  317
+        sjoin = lambda x : [ y.ljust(w, ' ') for y, w in zip(x, ci['columns_width'])]
  318
+        self._justified_items = map(sjoin, items_m)
  319
+        self._update_list(hilight=False)
  320
+
  321
+
  322
+
  323
+
  324
+    def _update_list(self, hilight=True):
  325
+        """ update the list of completion and hilight the currently selected completion """
  326
+        self._sliding_interval.current = self._index[0]
  327
+        head = None
  328
+        foot = None
  329
+        if self._sliding_interval.start > 0 : 
  330
+            head = '...'
  331
+
  332
+        if self._sliding_interval.stop < self._sliding_interval._max:
  333
+            foot = '...'
  334
+        items_m = self._justified_items[\
  335
+                       self._sliding_interval.start:\
  336
+                       self._sliding_interval.stop+1\
  337
+                                       ]
  338
+
  339
+        self._console_widget._clear_temporary_buffer()
  340
+        if(hilight):
  341
+            sel = (self._sliding_interval.nth, self._index[1])
  342
+        else :
  343
+            sel = None
  344
+
  345
+        strng = html_tableify(items_m, select=sel, header=head, footer=foot)
  346
+        self._console_widget._fill_temporary_buffer(self._old_cursor, strng, html=True)
  347
+
  348
+    #--------------------------------------------------------------------------
  349
+    # Protected interface
  350
+    #--------------------------------------------------------------------------
  351
+
  352
+    def _complete_current(self):
  353
+        """ Perform the completion with the currently selected item.
  354
+        """
  355
+        i = self._index
  356
+        item = self._items[i[0]][i[1]]
  357
+        item = item.strip()
  358
+        if item :
  359
+            self._current_text_cursor().insertText(item)
  360
+        self.cancel_completion()
  361
+
  362
+    def _current_text_cursor(self):
  363
+        """ Returns a cursor with text between the start position and the
  364
+            current position selected.
  365
+        """
  366
+        cursor = self._text_edit.textCursor()
  367
+        if cursor.position() >= self._start_position:
  368
+            cursor.setPosition(self._start_position,
  369
+                               QtGui.QTextCursor.KeepAnchor)
  370
+        return cursor
  371
+
62  IPython/frontend/qt/console/completion_plain.py
... ...
@@ -0,0 +1,62 @@
  1
+"""a simple completer for the qtconsole"""
  2
+#-----------------------------------------------------------------------------
  3
+# Copyright (c) 2012, IPython Development Team.$
  4
+#
  5
+# Distributed under the terms of the Modified BSD License.$
  6
+#
  7
+# The full license is in the file COPYING.txt, distributed with this software.
  8
+#-------------------------------------------------------------------
  9
+
  10
+# System library imports
  11
+from IPython.external.qt import QtCore, QtGui
  12
+import IPython.utils.text as text
  13
+
  14
+
  15
+class CompletionPlain(QtGui.QWidget):
  16
+    """ A widget for tab completion,  navigable by arrow keys """
  17
+
  18
+    #--------------------------------------------------------------------------
  19
+    # 'QObject' interface
  20
+    #--------------------------------------------------------------------------
  21
+
  22
+    def __init__(self, console_widget):
  23
+        """ Create a completion widget that is attached to the specified Qt
  24
+            text edit widget.
  25
+        """
  26
+        assert isinstance(console_widget._control, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
  27
+        super(CompletionPlain, self).__init__()
  28
+
  29
+        self._text_edit = console_widget._control
  30
+        self._console_widget = console_widget
  31
+
  32
+        self._text_edit.installEventFilter(self)
  33
+
  34
+    def eventFilter(self, obj, event):
  35
+        """ Reimplemented to handle keyboard input and to auto-hide when the
  36
+            text edit loses focus.
  37
+        """
  38
+        if obj == self._text_edit:
  39
+            etype = event.type()
  40
+
  41
+            if etype in( QtCore.QEvent.KeyPress, QtCore.QEvent.FocusOut ):
  42
+                self.cancel_completion()
  43
+
  44
+        return super(CompletionPlain, self).eventFilter(obj, event)
  45
+
  46
+    #--------------------------------------------------------------------------
  47
+    # 'CompletionPlain' interface
  48
+    #--------------------------------------------------------------------------
  49
+    def cancel_completion(self):
  50
+        """Cancel the completion, reseting internal variable, clearing buffer """
  51
+        self._console_widget._clear_temporary_buffer()
  52
+
  53
+
  54
+    def show_items(self, cursor, items):
  55
+        """ Shows the completion widget with 'items' at the position specified
  56
+            by 'cursor'.
  57
+        """
  58
+        if not items :
  59
+            return
  60
+        self.cancel_completion()
  61
+        strng = text.columnize(items)
  62
+        self._console_widget._fill_temporary_buffer(cursor, strng, html=False)
6  IPython/frontend/qt/console/completion_widget.py
@@ -10,10 +10,11 @@ class CompletionWidget(QtGui.QListWidget):
10 10
     # 'QObject' interface
11 11
     #--------------------------------------------------------------------------
12 12
 
13  
-    def __init__(self, text_edit):
  13
+    def __init__(self, console_widget):
14 14
         """ Create a completion widget that is attached to the specified Qt
15 15
             text edit widget.
16 16
         """
  17
+        text_edit = console_widget._control
17 18
         assert isinstance(text_edit, (QtGui.QTextEdit, QtGui.QPlainTextEdit))
18 19
         super(CompletionWidget, self).__init__()
19 20
 
@@ -132,3 +133,6 @@ def _update_current(self):
132 133
                 self.hide()
133 134
         else:
134 135
             self.hide()
  136
+
  137
+    def cancel_completion(self):
  138
+        self.hide()
101  IPython/frontend/qt/console/console_widget.py
@@ -5,7 +5,6 @@
5 5
 #-----------------------------------------------------------------------------
6 6
 
7 7
 # Standard library imports
8  
-import os
9 8
 from os.path import commonprefix
10 9
 import re
11 10
 import sys
@@ -23,6 +22,8 @@
23 22
 from IPython.utils.traitlets import Bool, Enum, Integer, Unicode
24 23
 from ansi_code_processor import QtAnsiCodeProcessor
25 24
 from completion_widget import CompletionWidget
  25
+from completion_html import CompletionHtml
  26
+from completion_plain import CompletionPlain
26 27
 from kill_ring import QtKillRing
27 28
 
28 29
 #-----------------------------------------------------------------------------
@@ -65,10 +66,19 @@ class ConsoleWidget(LoggingConfigurable, QtGui.QWidget):
65 66
         non-positive number disables text truncation (not recommended).
66 67
         """
67 68
     )
68  
-    gui_completion = Bool(False, config=True,
69  
-        help="""
70  
-        Use a list widget instead of plain text output for tab completion.
71  
-        """
  69
+    gui_completion = Enum(['plain', 'droplist', 'ncurses'], config=True,
  70
+                    default_value = 'ncurses',
  71
+                    help="""
  72
+                    The type of completer to use. Valid values are:
  73
+
  74
+                    'plain'   : Show the availlable completion as a text list
  75
+                                Below the editting area.
  76
+                    'droplist': Show the completion in a drop down list navigable
  77
+                                by the arrow keys, and from which you can select
  78
+                                completion by pressing Return.
  79
+                    'ncurses' : Show the completion as a text list which is navigable by
  80
+                                `tab` and arrow keys.
  81
+                    """
72 82
     )
73 83
     # NOTE: this value can only be specified during initialization.
74 84
     kind = Enum(['plain', 'rich'], default_value='plain', config=True,
@@ -137,12 +147,12 @@ def _font_family_default(self):
137 147
     font_changed = QtCore.Signal(QtGui.QFont)
138 148
 
139 149
     #------ Protected class variables ------------------------------------------
140  
-    
  150
+
141 151
     # control handles
142 152
     _control = None
143 153
     _page_control = None
144 154
     _splitter = None
145  
-    
  155
+
146 156
     # When the control key is down, these keys are mapped.
147 157
     _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
148 158
                          QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
@@ -161,6 +171,8 @@ def _font_family_default(self):
161 171
                      [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
162 172
                        QtCore.Qt.Key_V ])
163 173
 
  174
+    _temp_buffer_filled = False
  175
+
164 176
     #---------------------------------------------------------------------------
165 177
     # 'QObject' interface
166 178
     #---------------------------------------------------------------------------
@@ -211,7 +223,13 @@ def __init__(self, parent=None, **kw):
211 223
         # information for subclasses; they should be considered read-only.
212 224
         self._append_before_prompt_pos = 0
213 225
         self._ansi_processor = QtAnsiCodeProcessor()
214  
-        self._completion_widget = CompletionWidget(self._control)
  226
+        if self.gui_completion == 'ncurses':
  227
+            self._completion_widget = CompletionHtml(self)
  228
+        elif self.gui_completion == 'droplist':
  229
+            self._completion_widget = CompletionWidget(self)
  230
+        elif self.gui_completion == 'plain':
  231
+            self._completion_widget = CompletionPlain(self)
  232
+
215 233
         self._continuation_prompt = '> '
216 234
         self._continuation_prompt_html = None
217 235
         self._executing = False
@@ -228,7 +246,6 @@ def __init__(self, parent=None, **kw):
228 246
         self._reading = False
229 247
         self._reading_callback = None
230 248
         self._tab_width = 8
231  
-        self._text_completing_pos = 0
232 249
 
233 250
         # Set a monospaced font.
234 251
         self.reset_font()
@@ -823,18 +840,17 @@ def _append_plain_text(self, text, before_prompt=False):
823 840
         """
824 841
         self._append_custom(self._insert_plain_text, text, before_prompt)
825 842
 
826  
-    def _cancel_text_completion(self):
  843
+    def _cancel_completion(self):
827 844
         """ If text completion is progress, cancel it.
828 845
         """
829  
-        if self._text_completing_pos:
830  
-            self._clear_temporary_buffer()
831  
-            self._text_completing_pos = 0
  846
+        self._completion_widget.cancel_completion()
832 847
 
833 848
     def _clear_temporary_buffer(self):
834 849
         """ Clears the "temporary text" buffer, i.e. all the text following
835 850
             the prompt region.
836 851
         """
837 852
         # Select and remove all text below the input buffer.
  853
+        _temp_buffer_filled = False
838 854
         cursor = self._get_prompt_cursor()
839 855
         prompt = self._continuation_prompt.lstrip()
840 856
         while cursor.movePosition(QtGui.QTextCursor.NextBlock):
@@ -862,7 +878,7 @@ def _clear_temporary_buffer(self):
862 878
     def _complete_with_items(self, cursor, items):
863 879
         """ Performs completion with 'items' at the specified cursor location.
864 880
         """
865  
-        self._cancel_text_completion()
  881
+        self._cancel_completion()
866 882
 
867 883
         if len(items) == 1:
868 884
             cursor.setPosition(self._control.textCursor().position(),
@@ -877,19 +893,26 @@ def _complete_with_items(self, cursor, items):
877 893
                 cursor.insertText(prefix)
878 894
                 current_pos = cursor.position()
879 895
 
880  
-            if self.gui_completion:
881  
-                cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
882  
-                self._completion_widget.show_items(cursor, items)
883  
-            else:
884  
-                cursor.beginEditBlock()
885  
-                self._append_plain_text('\n')
886  
-                self._page(self._format_as_columns(items))
887  
-                cursor.endEditBlock()
  896
+            cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
  897
+            self._completion_widget.show_items(cursor, items)
  898
+
  899
+
  900
+    def _fill_temporary_buffer(self, cursor, text, html=False):
  901
+        """fill the area below the active editting zone with text"""
  902
+
  903
+        current_pos = self._control.textCursor().position()
  904
+
  905
+        cursor.beginEditBlock()
  906
+        self._append_plain_text('\n')
  907
+        self._page(text, html=html)
  908
+        cursor.endEditBlock()
  909
+
  910
+        cursor.setPosition(current_pos)
  911
+        self._control.moveCursor(QtGui.QTextCursor.End)
  912
+        self._control.setTextCursor(cursor)
  913
+
  914
+        _temp_buffer_filled = True
888 915
 
889  
-                cursor.setPosition(current_pos)
890  
-                self._control.moveCursor(QtGui.QTextCursor.End)
891  
-                self._control.setTextCursor(cursor)
892  
-                self._text_completing_pos = current_pos
893 916
 
894 917
     def _context_menu_make(self, pos):
895 918
         """ Creates a context menu for the given QPoint (in widget coordinates).
@@ -951,7 +974,6 @@ def _create_control(self):
951 974
         control.viewport().installEventFilter(self)
952 975
 
953 976
         # Connect signals.
954  
-        control.cursorPositionChanged.connect(self._cursor_position_changed)
955 977
         control.customContextMenuRequested.connect(
956 978
             self._custom_context_menu_requested)
957 979
         control.copyAvailable.connect(self.copy_available)
@@ -1021,7 +1043,7 @@ def _event_filter_console_keypress(self, event):
1021 1043
             intercepted = True
1022 1044
 
1023 1045
             # Special handling when tab completing in text mode.
1024  
-            self._cancel_text_completion()
  1046
+            self._cancel_completion()
1025 1047
 
1026 1048
             if self._in_buffer(position):
1027 1049
                 # Special handling when a reading a line of raw input.
@@ -1634,8 +1656,9 @@ def _keep_cursor_in_buffer(self):
1634 1656
     def _keyboard_quit(self):
1635 1657
         """ Cancels the current editing task ala Ctrl-G in Emacs.
1636 1658
         """
1637  
-        if self._text_completing_pos:
1638  
-            self._cancel_text_completion()
  1659
+        if self._temp_buffer_filled :
  1660
+            self._cancel_completion()
  1661
+            self._clear_temporary_buffer()
1639 1662
         else:
1640 1663
             self.input_buffer = ''
1641 1664
 
@@ -1853,24 +1876,6 @@ def _adjust_scrollbars(self):
1853 1876
         if diff < 0 and document.blockCount() == document.maximumBlockCount():
1854 1877
             scrollbar.setValue(scrollbar.value() + diff)
1855 1878
 
1856  
-    def _cursor_position_changed(self):
1857  
-        """ Clears the temporary buffer based on the cursor position.
1858  
-        """
1859  
-        if self._text_completing_pos:
1860  
-            document = self._control.document()
1861  
-            if self._text_completing_pos < document.characterCount():
1862  
-                cursor = self._control.textCursor()
1863  
-                pos = cursor.position()
1864  
-                text_cursor = self._control.textCursor()
1865  
-                text_cursor.setPosition(self._text_completing_pos)
1866  
-                if pos < self._text_completing_pos or \
1867  
-                        cursor.blockNumber() > text_cursor.blockNumber():
1868  
-                    self._clear_temporary_buffer()
1869  
-                    self._text_completing_pos = 0
1870  
-            else:
1871  
-                self._clear_temporary_buffer()
1872  
-                self._text_completing_pos = 0
1873  
-
1874 1879
     def _custom_context_menu_requested(self, pos):
1875 1880
         """ Shows a context menu at the given QPoint (in widget coordinates).
1876 1881
         """
8  IPython/frontend/qt/console/qtconsoleapp.py
@@ -104,11 +104,7 @@ def gui_excepthook(exctype, value, tb):
104 104
     'plain' : ({'IPythonQtConsoleApp' : {'plain' : True}},
105 105
             "Disable rich text support."),
106 106
 }
107  
-qt_flags.update(boolean_flag(
108  
-    'gui-completion', 'ConsoleWidget.gui_completion',
109  
-    "use a GUI widget for tab completion",
110  
-    "use plaintext output for completion"
111  
-))
  107
+
112 108
 # and app_flags from the Console Mixin
113 109
 qt_flags.update(app_flags)
114 110
 # add frontend flags to the full set
@@ -117,7 +113,6 @@ def gui_excepthook(exctype, value, tb):
117 113
 # start with copy of front&backend aliases list
118 114
 aliases = dict(aliases)
119 115
 qt_aliases = dict(
120  
-
121 116
     style = 'IPythonWidget.syntax_style',
122 117
     stylesheet = 'IPythonQtConsoleApp.stylesheet',
123 118
     colors = 'ZMQInteractiveShell.colors',
@@ -127,6 +122,7 @@ def gui_excepthook(exctype, value, tb):
127 122
 )
128 123
 # and app_aliases from the Console Mixin
129 124
 qt_aliases.update(app_aliases)
  125
+qt_aliases.update({'gui-completion':'ConsoleWidget.gui_completion'})
130 126
 # add frontend aliases to the full set
131 127
 aliases.update(qt_aliases)
132 128
 
3  IPython/frontend/qt/console/styles.py
@@ -22,6 +22,7 @@
22 22
     .in-prompt-number { font-weight: bold; }
23 23
     .out-prompt { color: darkred; }
24 24
     .out-prompt-number { font-weight: bold; }
  25
+    .inverted { background-color: %(fgcolor)s ; color:%(bgcolor)s;}
25 26
 '''
26 27
 default_light_style_sheet = default_light_style_template%dict(
27 28
                 bgcolor='white', fgcolor='black', select="#ccc")
@@ -38,6 +39,7 @@
38 39
     .in-prompt-number { color: lime; font-weight: bold; }
39 40
     .out-prompt { color: red; }
40 41
     .out-prompt-number { color: red; font-weight: bold; }
  42
+    .inverted { background-color: %(fgcolor)s ; color:%(bgcolor)s;}
41 43
 '''
42 44
 default_dark_style_sheet = default_dark_style_template%dict(
43 45
                 bgcolor='black', fgcolor='white', select="#555")
@@ -50,6 +52,7 @@
50 52
             selection-background-color: #cccccc}
51 53
     .in-prompt-number { font-weight: bold; }
52 54
     .out-prompt-number { font-weight: bold; }
  55
+    .inverted { background-color: black ; color: white;}
53 56
 '''
54 57
 default_bw_syntax_style = 'bw'
55 58
 
31  IPython/utils/tests/test_text.py
@@ -14,6 +14,7 @@
14 14
 
15 15
 import os
16 16
 import math
  17
+import random
17 18
 
18 19
 import nose.tools as nt
19 20
 
@@ -32,13 +33,37 @@ def test_columnize():
32 33
     items = [l*size for l in 'abc']
33 34
     out = text.columnize(items, displaywidth=80)
34 35
     nt.assert_equals(out, 'aaaaa  bbbbb  ccccc\n')
35  
-    out = text.columnize(items, displaywidth=10)
  36
+    out = text.columnize(items, displaywidth=12)
36 37
     nt.assert_equals(out, 'aaaaa  ccccc\nbbbbb\n')
37  
-
  38
+    out = text.columnize(items, displaywidth=10)
  39
+    nt.assert_equals(out, 'aaaaa\nbbbbb\nccccc\n')
  40
+
  41
+def test_columnize_random():
  42
+    """Test with random input to hopfully catch edge case """
  43
+    for nitems in [random.randint(2,70) for i in range(2,20)]:
  44
+        displaywidth = random.randint(20,200)
  45
+        rand_len = [random.randint(2,displaywidth) for i in range(nitems)]
  46
+        items = ['x'*l for l in rand_len]
  47
+        out = text.columnize(items, displaywidth=displaywidth)
  48
+        longer_line = max([len(x) for x in out.split('\n')])
  49
+        longer_element = max(rand_len)
  50
+        if longer_line > displaywidth:
  51
+            print "Columnize displayed something lager than displaywidth : %s " % longer_line
  52
+            print "longer element : %s " % longer_element
  53
+            print "displaywidth : %s " % displaywidth
  54
+            print "number of element : %s " % nitems
  55
+            print "size of each element :\n %s" % rand_len
  56
+            assert False
  57
+
  58
+def test_columnize_medium():
  59
+    """Test with inputs than shouldn't be wider tahn 80 """
  60
+    size = 40
  61
+    items = [l*size for l in 'abc']
  62
+    out = text.columnize(items, displaywidth=80)
  63
+    nt.assert_equals(out, '\n'.join(items+['']))
38 64
 
39 65
 def test_columnize_long():
40 66
     """Test columnize with inputs longer than the display window"""
41  
-    text.columnize(['a'*81, 'b'*81], displaywidth=80)
42 67
     size = 11
43 68
     items = [l*size for l in 'abc']
44 69
     out = text.columnize(items, displaywidth=size-1)
146  IPython/utils/text.py
@@ -24,7 +24,7 @@
24 24
 from string import Formatter
25 25
 
26 26
 from IPython.external.path import path
27  
-from IPython.testing.skipdoctest import skip_doctest_py3
  27
+from IPython.testing.skipdoctest import skip_doctest_py3, skip_doctest
28 28
 from IPython.utils import py3compat
29 29
 from IPython.utils.io import nlprint
30 30
 from IPython.utils.data import flatten
@@ -660,6 +660,91 @@ def parse(self, fmt_string):
660 660
             # Re-yield the {foo} style pattern
661 661
             yield (txt + literal_txt[continue_from:], field_name, format_spec, conversion)
662 662
 
  663
+#-----------------------------------------------------------------------------
  664
+# Utils to columnize a list of string
  665
+#-----------------------------------------------------------------------------
  666
+def _chunks(l, n):
  667
+    """Yield successive n-sized chunks from l."""
  668
+    for i in xrange(0, len(l), n):
  669
+        yield l[i:i+n]
  670
+
  671
+def _find_optimal(rlist , separator_size=2 , displaywidth=80):
  672
+    """Calculate optimal info to columnize a list of string"""
  673
+    for nrow in range(1, len(rlist)+1) :
  674
+        chk = map(max,_chunks(rlist, nrow))
  675
+        sumlength = sum(chk)
  676
+        ncols = len(chk)
  677
+        if sumlength+separator_size*(ncols-1) <= displaywidth :
  678
+            break;
  679
+    return {'columns_numbers' : ncols,
  680
+            'optimal_separator_width':(displaywidth - sumlength)/(ncols-1) if (ncols -1) else 0,
  681
+            'rows_numbers' : nrow,
  682
+            'columns_width' : chk
  683
+           }
  684
+
  685
+def _get_or_default(mylist, i, default=None):
  686
+    """return list item number, or default if don't exist"""
  687
+    if i >= len(mylist):
  688
+        return default
  689
+    else :
  690
+        return mylist[i]
  691
+
  692
+@skip_doctest
  693
+def compute_item_matrix(items, empty=None, *args, **kwargs) :
  694
+    """Returns a nested list, and info to columnize items
  695
+
  696
+    Parameters :
  697
+    ------------
  698
+
  699
+    items :
  700
+        list of strings to columize
  701
+    empty : (default None)
  702
+        default value to fill list if needed
  703
+    separator_size : int (default=2)
  704
+        How much caracters will be used as a separation between each columns.
  705
+    displaywidth : int (default=80)
  706
+        The width of the area onto wich the columns should enter
  707
+
  708
+    Returns :
  709
+    ---------
  710
+
  711
+    Returns a tuple of (strings_matrix, dict_info)
  712
+
  713
+    strings_matrix :
  714
+
  715
+        nested list of string, the outer most list contains as many list as
  716
+        rows, the innermost lists have each as many element as colums. If the
  717
+        total number of elements in `items` does not equal the product of
  718
+        rows*columns, the last element of some lists are filled with `None`.
  719
+
  720
+    dict_info :
  721
+        some info to make columnize easier:
  722
+
  723
+        columns_numbers : number of columns
  724
+        rows_numbers    : number of rows
  725
+        columns_width   : list of with of each columns
  726
+        optimal_separator_width : best separator width between columns
  727
+
  728
+    Exemple :
  729
+    ---------
  730
+
  731
+    In [1]: l = ['aaa','b','cc','d','eeeee','f','g','h','i','j','k','l']
  732
+       ...: compute_item_matrix(l,displaywidth=12)
  733
+    Out[1]:
  734
+        ([['aaa', 'f', 'k'],
  735
+        ['b', 'g', 'l'],
  736
+        ['cc', 'h', None],
  737
+        ['d', 'i', None],
  738
+        ['eeeee', 'j', None]],
  739
+        {'columns_numbers': 3,
  740
+        'columns_width': [5, 1, 1],
  741
+        'optimal_separator_width': 2,
  742
+        'rows_numbers': 5})
  743
+
  744
+    """
  745
+    info = _find_optimal(map(len, items), *args, **kwargs)
  746
+    nrow, ncol = info['rows_numbers'], info['columns_numbers']
  747
+    return ([[ _get_or_default(items, c*nrow+i, default=empty) for c in range(ncol) ] for i in range(nrow) ], info)
663 748
 
664 749
 def columnize(items, separator='  ', displaywidth=80):
665 750
     """ Transform a list of strings into a single string with columns.
@@ -679,58 +764,9 @@ def columnize(items, separator='  ', displaywidth=80):
679 764
     -------
680 765
     The formatted string.
681 766
     """
682  
-    # Note: this code is adapted from columnize 0.3.2.
683  
-    # See http://code.google.com/p/pycolumnize/
684  
-
685  
-    # Some degenerate cases.
686  
-    size = len(items)
687  
-    if size == 0:
  767
+    if not items :
688 768
         return '\n'
689  
-    elif size == 1:
690  
-        return '%s\n' % items[0]
691  
-
692  
-    # Special case: if any item is longer than the maximum width, there's no
693  
-    # point in triggering the logic below...
694  
-    item_len = map(len, items) # save these, we can reuse them below
695  
-    longest = max(item_len)
696  
-    if longest >= displaywidth:
697  
-        return '\n'.join(items+[''])
698  
-
699  
-    # Try every row count from 1 upwards
700  
-    array_index = lambda nrows, row, col: nrows*col + row
701  
-    for nrows in range(1, size):
702  
-        ncols = (size + nrows - 1) // nrows
703  
-        colwidths = []
704  
-        totwidth = -len(separator)
705  
-        for col in range(ncols):
706  
-            # Get max column width for this column
707  
-            colwidth = 0
708  
-            for row in range(nrows):
709  
-                i = array_index(nrows, row, col)
710  
-                if i >= size: break
711  
-                x, len_x = items[i], item_len[i]
712  
-                colwidth = max(colwidth, len_x)
713  
-            colwidths.append(colwidth)
714  
-            totwidth += colwidth + len(separator)
715  
-            if totwidth > displaywidth:
716  
-                break
717  
-        if totwidth <= displaywidth:
718  
-            break
719  
-
720  
-    # The smallest number of rows computed and the max widths for each
721  
-    # column has been obtained. Now we just have to format each of the rows.
722  
-    string = ''
723  
-    for row in range(nrows):
724  
-        texts = []
725  
-        for col in range(ncols):
726  
-            i = row + nrows*col
727  
-            if i >= size:
728  
-                texts.append('')
729  
-            else:
730  
-                texts.append(items[i])
731  
-        while texts and not texts[-1]:
732  
-            del texts[-1]
733  
-        for col in range(len(texts)):
734  
-            texts[col] = texts[col].ljust(colwidths[col])
735  
-        string += '%s\n' % separator.join(texts)
736  
-    return string
  769
+    matrix, info = compute_item_matrix(items, separator_size=len(separator), displaywidth=displaywidth)
  770
+    fmatrix = [filter(None, x) for x in matrix]
  771
+    sjoin = lambda x : separator.join([ y.ljust(w, ' ') for y, w in zip(x, info['columns_width'])])
  772
+    return '\n'.join(map(sjoin, fmatrix))+'\n'
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.