Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

new completer for qtconsole. #1851

Merged
merged 7 commits into from

4 participants

@Carreau
Owner

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.

@fperez
Owner

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

@punchagan

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.

@Carreau
Owner

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

@Carreau
Owner

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.

@ellisonbg
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.

@Carreau
Owner

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

@Carreau
Owner

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

@fperez
Owner

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
@Carreau
Owner

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.

@fperez
Owner

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...

Carreau added some commits
@Carreau Carreau re-write columnize, with intermediate step.
fix test that where wrong, add some others.

fix #1860
981ef5c
@Carreau Carreau fix docstring, and debug leftover 4659fca
@Carreau Carreau 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
@Carreau Carreau fix gui=droplist 3cb4e60
@Carreau Carreau fix Ctrl+G/esc intercepted bug f7faea8
@Carreau Carreau add option to choose for empty ef8d7e2
@Carreau Carreau 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
@Carreau
Owner

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.

@ellisonbg
Owner
@Carreau
Owner

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.

@fperez
Owner

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!

@fperez fperez merged commit 45d28c5 into from
@Carreau
Owner

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.

@fperez
Owner
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jun 10, 2012
  1. @Carreau

    re-write columnize, with intermediate step.

    Carreau authored
    fix test that where wrong, add some others.
    
    fix #1860
  2. @Carreau
  3. @Carreau

    new completer for qtconsole.

    Carreau authored
    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).
  4. @Carreau

    fix gui=droplist

    Carreau authored
  5. @Carreau
  6. @Carreau
  7. @Carreau

    new completer for qtconsole

    Carreau authored
    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
This page is out of date. Refresh to see the latest.
View
371 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'<td>'+text+u' </td>'
+ trs = lambda text : u'<tr>'+text+u'</tr>'
+ tds_items = [map(tds, row) for row in item_matrix]
+ if select :
+ row, col = select
+ tds_items[row][col] = u'<td class="inverted">'\
+ +item_matrix[row][col]\
+ +u' </td>'
+ #select the right item
+ html_cols = map(trs, (u''.join(row) for row in tds_items))
+ head = ''
+ foot = ''
+ if header :
+ head = (u'<tr>'\
+ +''.join((u'<td>'+header+u'</td>')*len(item_matrix[0]))\
+ +'</tr>')
+
+ if footer :
+ foot = (u'<tr>'\
+ +''.join((u'<td>'+footer+u'</td>')*len(item_matrix[0]))\
+ +'</tr>')
+ html = (u'<table class="completion" style="white-space:pre">'+head+(u''.join(html_cols))+foot+u'</table>')
+ 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
+
View
62 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)
View
6 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 @@ def _update_current(self):
self.hide()
else:
self.hide()
+
+ def cancel_completion(self):
+ self.hide()
View
101 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.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 @@ def _font_family_default(self):
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 @@ def _font_family_default(self):
[ 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 @@ def __init__(self, parent=None, **kw):
# 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 @@ def __init__(self, parent=None, **kw):
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 @@ def _append_plain_text(self, text, before_prompt=False):
"""
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 @@ def _clear_temporary_buffer(self):
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 @@ def _complete_with_items(self, cursor, items):
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 @@ def _create_control(self):
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 @@ def _event_filter_console_keypress(self, event):
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 @@ def _keep_cursor_in_buffer(self):
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 @@ def _adjust_scrollbars(self):
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).
"""
View
8 IPython/frontend/qt/console/qtconsoleapp.py
@@ -104,11 +104,7 @@ def gui_excepthook(exctype, value, tb):
'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 @@ def gui_excepthook(exctype, value, tb):
# 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 @@ def gui_excepthook(exctype, value, tb):
)
# 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)
View
3  IPython/frontend/qt/console/styles.py
@@ -22,6 +22,7 @@
.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 @@
.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 @@
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'
View
31 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)
View
146 IPython/utils/text.py
@@ -24,7 +24,7 @@
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 @@ def parse(self, fmt_string):
# 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'
Something went wrong with that request. Please try again.