Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Base of an as you type completer. #1039

Merged
merged 7 commits into from

3 participants

@Carreau
Owner

when invoking the completer, instead of having to chose/dismiss, you can
continue typing, it will filter the result "as you type" and dismiss itself
if ther is no match left.

As it is now, it's only works with lowercase letters, I need to find a workaroud
for this.

for example
if you type :

* P-y-<tab>-S-o-m-e-t-h-i-n-g
* it will propose PySide, but will dismiss when 'o' is pressed and pasting Pyso with a lower case 's'

I've rebase and squash everything (the code was quite old), so not sure if it perfectly works with the tooltip merged of a few hours ago ... and there are still edge case annoying for a merged on master

@fperez
Owner

It looks good already, thanks! One usability note: if you could also fix a couple of annoyances of the nb completer compared to readline:

  • once there's only one completion, hitting <tab> should finish up the compleiton immediately. Right now, if the filtering has brought the list down to one item, you first have to hit tab once to dismiss it, and then again to finish the completion. It should dismiss and complete with a single <tab>.

  • indeed it gets confused with uppercase letters. Try for example plt.<tab> and then type 'ci'. It will offer Circle with uppercase C, which then doesn't actually complete. It should match exactly the casing of what has been typed so far.

  • the first completion should also write as many letters as are unique up to the new stuff. For example, if you do plt.au<tab> (with plt == matplotlib.pyplot), then all the completions start with 'aut'. In the terminal, the 't' is automatically added and then the completions are shown. Here you must manually type the 't'. We really should match how readline works here, which is the right way to do it.

Thanks again for the good work!

@Carreau
Owner

Thank you for your review, i've already think of most of thoses cases plus some more.
I would have changed your third point to a 'triggerd on tab' only, but once it done it shouldn't be to hard to change the behaviour

For lowercase, it's half by choice. I'm using charfromCode(event.which) which alway give me upper case :-(
I'll have to split the logic because keydown event doesn't reflect wether the typed letter is upper or lower case. I feel that having both keydown and keypress event will make lots of edge case...

I also sugest finding some sort of library which already register the value of each keypress/keydown to a more readable api, like key.tab or key.esc I know you already have event.which but i'm starting to be lost in my own handeling of the keycodes

So it's start to grow complex, and I was wondering if splitting it into another file is worth.

@fperez
Owner
Carreau added some commits
@Carreau Carreau Base of an as you type conpleter.
	when invoking the completer, instead of having to chose/dismiss, you can
	continue typing, it will filter the result "as you type" and dismiss itself
	if ther is no match left.

	As it is now, it's only works with lowercase letters, I need to find a workaroud
	for this.

	for example
	if you type :
	* P-y-<tab>-S-o-m-e-t-h-i-n-g
	* it will propose PySide, but will dismiss when 'o' is pressed and pasting Pyso with a lower case 's'
6b2d9cc
@Carreau Carreau protect shift extend on Uppercase 0492e42
@Carreau Carreau Handle lower and uppercase e65ec88
@Carreau
Owner

Rebased on master, handle upper and lowercase.

Perhaps there's another call that would provide the exact character?

@fperez
Yes, it was keypress (instead of keydown)... but keypress is silent for shift, ctrl, tab..etc.
I wrote a function which is called by both keypress and keydown and filter afterward. seem to do the trick.

I'll try to mimic readline so that tab 'fast forward' the completion and clean the code after.

@fperez
Owner

OK, this is looking great. Once you finish implementing support for tab fully completing when there's only one selection left and clean it up, let me know and we'll merge it. Excellent work, thanks!

@Carreau
Owner

Now <tab> when the completer is already present update until the common prefix of all the availlable completion.

@fperez
Owner

much better! Last request: once the list has filtered down to only one option, should simply do the completion. For example, in this sequence: (with --pylab):

1. plt.a<tab>
2. type n
3. the only option left is 'annotate'.
4. at this point, one more <tab> should finish the completion.  

Instead, I have to hit enter, which is acceptable but different from readline, so it causes confusion to people whose muscle memory is used to having tab finish the completion when there's only one option left.

With that fixed, we should be able to merge. Thanks!

Carreau added some commits
@Carreau Carreau tab pick if only one match left 938981c
@Carreau Carreau tidy up code 850c994
@Carreau Carreau fix double tooltip
	there was a possibility of 2 tooltip if clicking in two cell
	and one that it wasn't able to dismiss
f1f0856
@Carreau
Owner

@fperez,
you can try now. As you type 'Completion caracter' are specified in a list so I have added letters and underscore to it for now, and I dont see what else could be part of a function name or object to complete... do you see some other 'caracters' to add ?

This also have a fix for some case when 2 tooltips could appear...
good night.

@fperez
Owner

This is awesome, usability-wise. Merging now, great work!

@fperez fperez merged commit b44cdc3 into ipython:master
@syed

Is this possible in the CLI/Qt console too. I really like bpython's completion-as-you-type

@fperez
Owner

It's possible in the qt one, not in the text one because there's no event loop in the text console (or rather, there are only two events: 'return' and 'tab', but individual keystrokes don't produce events).

@fperez
Owner

... but obviously it would need to be also implemented in the qt console, it's not automatic.

@fperez fperez referenced this pull request from a commit
Commit has since been removed from the repository and is no longer available.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Nov 29, 2011
  1. @Carreau

    Base of an as you type conpleter.

    Carreau authored
    	when invoking the completer, instead of having to chose/dismiss, you can
    	continue typing, it will filter the result "as you type" and dismiss itself
    	if ther is no match left.
    
    	As it is now, it's only works with lowercase letters, I need to find a workaroud
    	for this.
    
    	for example
    	if you type :
    	* P-y-<tab>-S-o-m-e-t-h-i-n-g
    	* it will propose PySide, but will dismiss when 'o' is pressed and pasting Pyso with a lower case 's'
  2. @Carreau
  3. @Carreau

    Handle lower and uppercase

    Carreau authored
  4. @Carreau

    tabs fast forward user tab

    Carreau authored
  5. @Carreau
  6. @Carreau

    tidy up code

    Carreau authored
  7. @Carreau

    fix double tooltip

    Carreau authored
    	there was a possibility of 2 tooltip if clicking in two cell
    	and one that it wasn't able to dismiss
This page is out of date. Refresh to see the latest.
View
9 IPython/frontend/html/notebook/static/css/notebook.css
@@ -406,6 +406,15 @@ div.text_cell_render {
min-height:50px;
}
+.completions p{
+ background: #DDF;
+ /*outline: none;
+ padding: 0px;*/
+ border-bottom: black solid 1px;
+ padding: 1px;
+ font-family: monospace;
+}
+
@media print {
body { overflow: visible !important; }
.ui-widget-content { border: 0px; }
View
192 IPython/frontend/html/notebook/static/js/codecell.js
@@ -73,8 +73,8 @@ var IPython = (function (IPython) {
var that = this;
// whatever key is pressed, first, cancel the tooltip request before
// they are sent, and remove tooltip if any
- if(event.type === 'keydown' && this.tooltip_timeout != null){
- CodeCell.prototype.remove_and_cancell_tooltip(that.tooltip_timeout);
+ if(event.type === 'keydown' ){
+ CodeCell.prototype.remove_and_cancel_tooltip(that.tooltip_timeout);
that.tooltip_timeout=null;
}
@@ -145,12 +145,13 @@ var IPython = (function (IPython) {
return false;
};
- CodeCell.prototype.remove_and_cancell_tooltip = function(timeout)
+ CodeCell.prototype.remove_and_cancel_tooltip = function(timeout)
{
// note that we don't handle closing directly inside the calltip
// as in the completer, because it is not focusable, so won't
// get the event.
- clearTimeout(timeout);
+ if(timeout != null)
+ { clearTimeout(timeout);}
$('#tooltip').remove();
}
@@ -229,9 +230,24 @@ var IPython = (function (IPython) {
// setTimeout(CodeCell.prototype.remove_and_cancell_tooltip, 5000);
};
-
+ // As you type completer
CodeCell.prototype.finish_completing = function (matched_text, matches) {
- // console.log("Got matches", matched_text, matches);
+ //return if not completing or nothing to complete
+ if (!this.is_completing || matches.length === 0) {return;}
+
+ // for later readability
+ var key = { tab:9,
+ esc:27,
+ backspace:8,
+ space:13,
+ shift:16,
+ enter:32,
+ // _ is 189
+ isCompSymbol : function (code)
+ {return ((code>64 && code <=122)|| code == 189)}
+ }
+
+ // smart completion, sort kwarg ending with '='
var newm = new Array();
if(this.notebook.smart_completer)
{
@@ -245,7 +261,23 @@ var IPython = (function (IPython) {
newm = kwargs.concat(other);
matches=newm;
}
- if (!this.is_completing || matches.length === 0) {return;}
+ // end sort kwargs
+
+ // give common prefix of a array of string
+ function sharedStart(A){
+ if(A.length > 1 ){
+ var tem1, tem2, s, A= A.slice(0).sort();
+ tem1= A[0];
+ s= tem1.length;
+ tem2= A.pop();
+ while(s && tem2.indexOf(tem1)== -1){
+ tem1= tem1.substring(0, --s);
+ }
+ return tem1;
+ }
+ return "";
+ }
+
//try to check if the user is typing tab at least twice after a word
// and completion is "done"
@@ -268,57 +300,104 @@ var IPython = (function (IPython) {
this.prevmatch="";
this.npressed=0;
}
-
+ // end fallback on tooltip
+ //==================================
+ // Real completion logic start here
var that = this;
var cur = this.completion_cursor;
+ var done = false;
+
+ // call to dismmiss the completer
+ var close = function () {
+ if (done) return;
+ done = true;
+ if (complete!=undefined)
+ {complete.remove();}
+ that.is_completing = false;
+ that.completion_cursor = null;
+ };
+ // insert the given text and exit the completer
var insert = function (selected_text) {
that.code_mirror.replaceRange(
selected_text,
{line: cur.line, ch: (cur.ch-matched_text.length)},
{line: cur.line, ch: cur.ch}
);
+ event.stopPropagation();
+ event.preventDefault();
+ close();
+ setTimeout(function(){that.code_mirror.focus();}, 50);
};
- if (matches.length === 1) {
- insert(matches[0]);
- setTimeout(function(){that.code_mirror.focus();}, 50);
- return;
+ // insert the curent highlited selection and exit
+ var pick = function () {
+ insert(select.val()[0]);
};
+
+ // Define function to clear the completer, refill it with the new
+ // matches, update the pseuso typing field. autopick insert match if
+ // only one left, in no matches (anymore) dismiss itself by pasting
+ // what the user have typed until then
+ var complete_with = function(matches,typed_text,autopick)
+ {
+ // If autopick an only one match, past.
+ // Used to 'pick' when pressing tab
+ if (matches.length < 1) {
+ insert(typed_text);
+ } else if (autopick && matches.length==1) {
+ insert(matches[0]);
+ }
+ //clear the previous completion if any
+ complete.children().children().remove();
+ $('#asyoutype').text(typed_text);
+ select=$('#asyoutypeselect');
+ for (var i=0; i<matches.length; ++i) {
+ select.append($('<option/>').html(matches[i]));
+ }
+ select.children().first().attr('selected','true');
+ }
+
+ // create html for completer
var complete = $('<div/>').addClass('completions');
+ complete.attr('id','complete');
+ complete.append($('<p/>').attr('id', 'asyoutype').html(matched_text));//pseudo input field
+
var select = $('<select/>').attr('multiple','true');
- for (var i=0; i<matches.length; ++i) {
- select.append($('<option/>').text(matches[i]));
- }
- select.children().first().attr('selected','true');
- select.attr('size',Math.min(10,matches.length));
+ select.attr('id', 'asyoutypeselect')
+ select.attr('size',Math.min(10,matches.length));
var pos = this.code_mirror.cursorCoords();
+
+ // TODO: I propose to remove enough horizontal pixel
+ // to align the text later
complete.css('left',pos.x+'px');
complete.css('top',pos.yBot+'px');
complete.append(select);
$('body').append(complete);
- var done = false;
-
- var close = function () {
- if (done) return;
- done = true;
- complete.remove();
- that.is_completing = false;
- that.completion_cursor = null;
- };
-
- var pick = function () {
- insert(select.val()[0]);
- close();
- setTimeout(function(){that.code_mirror.focus();}, 50);
- };
- select.blur(close);
- select.keydown(function (event) {
+ // So a first actual completion. see if all the completion start wit
+ // the same letter and complete if necessary
+ fastForward = sharedStart(matches)
+ typed_characters= fastForward.substr(matched_text.length);
+ complete_with(matches,matched_text+typed_characters,true);
+ filterd=matches;
+ // Give focus to select, and make it filter the match as the user type
+ // by filtering the previous matches. Called by .keypress and .keydown
+ var downandpress = function (event,press_or_down) {
var code = event.which;
- if (code === 13 || code === 32) {
+ var autopick = false; // auto 'pick' if only one match
+ if (press_or_down === 0){
+ press=true; down=false; //Are we called from keypress or keydown
+ } else if (press_or_down == 1){
+ press=false; down=true;
+ }
+ if (code === key.shift) {
+ // nothing on Shift
+ return;
+ }
+ if (code === key.space || code === key.enter) {
// Pressing SPACE or ENTER will cause a pick
event.stopPropagation();
event.preventDefault();
@@ -327,16 +406,47 @@ var IPython = (function (IPython) {
// We don't want the document keydown handler to handle UP/DOWN,
// but we want the default action.
event.stopPropagation();
- } else {
- // All other key presses exit completion.
- event.stopPropagation();
- event.preventDefault();
- close();
- that.code_mirror.focus();
+ //} else if ( key.isCompSymbol(code)|| (code==key.backspace)||(code==key.tab && down)){
+ } else if ( (code==key.backspace)||(code==key.tab) || press || key.isCompSymbol(code)){
+ if((code != key.backspace) && (code != key.tab) && press)
+ {
+ var newchar = String.fromCharCode(code);
+ typed_characters=typed_characters+newchar;
+ } else if (code == key.tab) {
+ fastForward = sharedStart(filterd)
+ ffsub = fastForward.substr(matched_text.length+typed_characters.length);
+ typed_characters=typed_characters+ffsub;
+ autopick=true;
+ event.stopPropagation();
+ event.preventDefault();
+ } else if (code == key.backspace) {
+ // cancel if user have erase everything, otherwise decrease
+ // what we filter with
+ if (typed_characters.length <= 0)
+ {
+ insert(matched_text)
+ }
+ typed_characters=typed_characters.substr(0,typed_characters.length-1);
+ }
+ re = new RegExp("^"+"\%?"+matched_text+typed_characters,"");
+ filterd = matches.filter(function(x){return re.test(x)});
+ complete_with(filterd,matched_text+typed_characters,autopick);
+ } else if(down){ // abort only on .keydown
+ // abort with what the user have pressed until now
+ console.log('aborting with keycode : '+code+' is down :'+down);
+ insert(matched_text+typed_characters);
}
+ }
+ select.keydown(function (event) {
+ downandpress(event,1)
+ });
+ select.keypress(function (event) {
+ downandpress(event,0)
});
// Double click also causes a pick.
+ // and bind the last actions.
select.dblclick(pick);
+ select.blur(close);
select.focus();
};
View
2  IPython/frontend/html/notebook/static/js/kernel.js
@@ -171,7 +171,7 @@ var IPython = (function (IPython) {
};
Kernel.prototype.object_info_request = function (objname) {
- if(typeof(objname)!=null)
+ if(typeof(objname)!=null && objname!=null)
{
var content = {
oname : objname.toString(),
Something went wrong with that request. Please try again.