Skip to content
This repository

Base of an as you type completer. #1039

Merged
merged 7 commits into from over 2 years ago

3 participants

Matthias Bussonnier Fernando Perez Syed Mushtaq Ahmed
Matthias Bussonnier
Collaborator

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

Fernando Perez
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!

Matthias Bussonnier
Collaborator

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.

Fernando Perez
Owner
added some commits November 24, 2011
Matthias Bussonnier 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
Matthias Bussonnier protect shift extend on Uppercase 0492e42
Matthias Bussonnier Handle lower and uppercase e65ec88
Matthias Bussonnier
Collaborator

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.

Fernando Perez
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!

Matthias Bussonnier
Collaborator

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

Fernando Perez
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!

added some commits November 30, 2011
Matthias Bussonnier tab pick if only one match left 938981c
Matthias Bussonnier tidy up code 850c994
Matthias Bussonnier 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
Matthias Bussonnier
Collaborator

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

Fernando Perez
Owner

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

Fernando Perez fperez merged commit b44cdc3 into from November 29, 2011
Fernando Perez fperez closed this November 29, 2011
Syed Mushtaq Ahmed

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

Fernando Perez
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).

Fernando Perez
Owner

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

Fernando Perez fperez referenced this pull request from a commit January 10, 2012
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

Showing 7 unique commits by 1 author.

Nov 29, 2011
Matthias Bussonnier 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
Matthias Bussonnier protect shift extend on Uppercase 0492e42
Matthias Bussonnier Handle lower and uppercase e65ec88
Matthias Bussonnier tabs fast forward user tab 1759f54
Nov 30, 2011
Matthias Bussonnier tab pick if only one match left 938981c
Matthias Bussonnier tidy up code 850c994
Matthias Bussonnier 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
This page is out of date. Refresh to see the latest.
9  IPython/frontend/html/notebook/static/css/notebook.css
@@ -406,6 +406,15 @@ div.text_cell_render {
406 406
     min-height:50px;
407 407
 }
408 408
 
  409
+.completions p{
  410
+    background: #DDF;
  411
+    /*outline: none;
  412
+    padding: 0px;*/
  413
+    border-bottom: black solid 1px;
  414
+    padding: 1px;
  415
+    font-family: monospace;
  416
+}
  417
+
409 418
 @media print {
410 419
     body { overflow: visible !important; }
411 420
     .ui-widget-content { border: 0px; }
192  IPython/frontend/html/notebook/static/js/codecell.js
@@ -73,8 +73,8 @@ var IPython = (function (IPython) {
73 73
         var that = this;
74 74
         // whatever key is pressed, first, cancel the tooltip request before
75 75
         // they are sent, and remove tooltip if any
76  
-        if(event.type === 'keydown' && this.tooltip_timeout != null){
77  
-            CodeCell.prototype.remove_and_cancell_tooltip(that.tooltip_timeout);
  76
+        if(event.type === 'keydown' ){
  77
+            CodeCell.prototype.remove_and_cancel_tooltip(that.tooltip_timeout);
78 78
             that.tooltip_timeout=null;
79 79
         }
80 80
 
@@ -145,12 +145,13 @@ var IPython = (function (IPython) {
145 145
         return false;
146 146
     };
147 147
 
148  
-    CodeCell.prototype.remove_and_cancell_tooltip = function(timeout)
  148
+    CodeCell.prototype.remove_and_cancel_tooltip = function(timeout)
149 149
     {
150 150
         // note that we don't handle closing directly inside the calltip
151 151
         // as in the completer, because it is not focusable, so won't
152 152
         // get the event.
153  
-        clearTimeout(timeout);
  153
+        if(timeout != null)
  154
+        { clearTimeout(timeout);}
154 155
         $('#tooltip').remove();
155 156
     }
156 157
 
@@ -229,9 +230,24 @@ var IPython = (function (IPython) {
229 230
         // setTimeout(CodeCell.prototype.remove_and_cancell_tooltip, 5000);
230 231
     };
231 232
 
232  
-
  233
+    // As you type completer
233 234
     CodeCell.prototype.finish_completing = function (matched_text, matches) {
234  
-        // console.log("Got matches", matched_text, matches);
  235
+        //return if not completing or nothing to complete
  236
+        if (!this.is_completing || matches.length === 0) {return;}
  237
+
  238
+        // for later readability
  239
+        var key = { tab:9,
  240
+                    esc:27,
  241
+                    backspace:8,
  242
+                    space:13,
  243
+                    shift:16,
  244
+                    enter:32,
  245
+                    // _ is 189
  246
+                    isCompSymbol : function (code)
  247
+                        {return ((code>64 && code <=122)|| code == 189)}
  248
+                    }
  249
+
  250
+        // smart completion, sort kwarg ending with '='
235 251
         var newm = new Array();
236 252
         if(this.notebook.smart_completer)
237 253
         {
@@ -245,7 +261,23 @@ var IPython = (function (IPython) {
245 261
             newm = kwargs.concat(other);
246 262
             matches=newm;
247 263
         }
248  
-        if (!this.is_completing || matches.length === 0) {return;}
  264
+        // end sort kwargs
  265
+
  266
+        // give common prefix of a array of string
  267
+        function sharedStart(A){
  268
+            if(A.length > 1 ){
  269
+                var tem1, tem2, s, A= A.slice(0).sort();
  270
+                tem1= A[0];
  271
+                s= tem1.length;
  272
+                tem2= A.pop();
  273
+                while(s && tem2.indexOf(tem1)== -1){
  274
+                    tem1= tem1.substring(0, --s);
  275
+                }
  276
+                return tem1;
  277
+            }
  278
+            return "";
  279
+        }
  280
+
249 281
 
250 282
         //try to check if the user is typing tab at least twice after a word
251 283
         // and completion is "done"
@@ -268,57 +300,104 @@ var IPython = (function (IPython) {
268 300
             this.prevmatch="";
269 301
             this.npressed=0;
270 302
         }
271  
-
  303
+        // end fallback on tooltip
  304
+        //==================================
  305
+        // Real completion logic start here
272 306
         var that = this;
273 307
         var cur = this.completion_cursor;
  308
+        var done = false;
  309
+
  310
+        // call to dismmiss the completer
  311
+        var close = function () {
  312
+            if (done) return;
  313
+            done = true;
  314
+            if (complete!=undefined)
  315
+            {complete.remove();}
  316
+            that.is_completing = false;
  317
+            that.completion_cursor = null;
  318
+        };
274 319
 
  320
+        // insert the given text and exit the completer
275 321
         var insert = function (selected_text) {
276 322
             that.code_mirror.replaceRange(
277 323
                 selected_text,
278 324
                 {line: cur.line, ch: (cur.ch-matched_text.length)},
279 325
                 {line: cur.line, ch: cur.ch}
280 326
             );
  327
+            event.stopPropagation();
  328
+            event.preventDefault();
  329
+            close();
  330
+            setTimeout(function(){that.code_mirror.focus();}, 50);
281 331
         };
282 332
 
283  
-        if (matches.length === 1) {
284  
-            insert(matches[0]);
285  
-            setTimeout(function(){that.code_mirror.focus();}, 50);
286  
-            return;
  333
+        // insert the curent highlited selection and exit
  334
+        var pick = function () {
  335
+            insert(select.val()[0]);
287 336
         };
288 337
 
  338
+
  339
+        // Define function to clear the completer, refill it with the new
  340
+        // matches, update the pseuso typing field. autopick insert match if
  341
+        // only one left, in no matches (anymore) dismiss itself by pasting
  342
+        // what the user have typed until then
  343
+        var complete_with = function(matches,typed_text,autopick)
  344
+        {
  345
+            // If autopick an only one match, past.
  346
+            // Used to 'pick' when pressing tab
  347
+            if (matches.length < 1) {
  348
+                insert(typed_text);
  349
+            } else if (autopick && matches.length==1) {
  350
+                insert(matches[0]);
  351
+            }
  352
+            //clear the previous completion if any
  353
+            complete.children().children().remove();
  354
+            $('#asyoutype').text(typed_text);
  355
+            select=$('#asyoutypeselect');
  356
+            for (var i=0; i<matches.length; ++i) {
  357
+                    select.append($('<option/>').html(matches[i]));
  358
+            }
  359
+            select.children().first().attr('selected','true');
  360
+        }
  361
+
  362
+        // create html for completer
289 363
         var complete = $('<div/>').addClass('completions');
  364
+            complete.attr('id','complete');
  365
+        complete.append($('<p/>').attr('id', 'asyoutype').html(matched_text));//pseudo input field
  366
+
290 367
         var select = $('<select/>').attr('multiple','true');
291  
-        for (var i=0; i<matches.length; ++i) {
292  
-            select.append($('<option/>').text(matches[i]));
293  
-        }
294  
-        select.children().first().attr('selected','true');
295  
-        select.attr('size',Math.min(10,matches.length));
  368
+            select.attr('id', 'asyoutypeselect')
  369
+            select.attr('size',Math.min(10,matches.length));
296 370
         var pos = this.code_mirror.cursorCoords();
  371
+
  372
+        // TODO: I propose to remove enough horizontal pixel
  373
+        // to align the text later
297 374
         complete.css('left',pos.x+'px');
298 375
         complete.css('top',pos.yBot+'px');
299 376
         complete.append(select);
300 377
 
301 378
         $('body').append(complete);
302  
-        var done = false;
303  
-
304  
-        var close = function () {
305  
-            if (done) return;
306  
-            done = true;
307  
-            complete.remove();
308  
-            that.is_completing = false;
309  
-            that.completion_cursor = null;
310  
-        };
311  
-
312  
-        var pick = function () {
313  
-            insert(select.val()[0]);
314  
-            close();
315  
-            setTimeout(function(){that.code_mirror.focus();}, 50);
316  
-        };
317 379
 
318  
-        select.blur(close);
319  
-        select.keydown(function (event) {
  380
+        // So a first actual completion.  see if all the completion start wit
  381
+        // the same letter and complete if necessary
  382
+        fastForward = sharedStart(matches)
  383
+        typed_characters= fastForward.substr(matched_text.length);
  384
+        complete_with(matches,matched_text+typed_characters,true);
  385
+        filterd=matches;
  386
+        // Give focus to select, and make it filter the match as the user type
  387
+        // by filtering the previous matches. Called by .keypress and .keydown
  388
+        var downandpress = function (event,press_or_down) {
320 389
             var code = event.which;
321  
-            if (code === 13 || code === 32) {
  390
+            var autopick = false; // auto 'pick' if only one match
  391
+            if (press_or_down === 0){
  392
+                press=true; down=false; //Are we called from keypress or keydown
  393
+            } else if (press_or_down == 1){
  394
+                press=false; down=true;
  395
+            }
  396
+            if (code === key.shift) {
  397
+                // nothing on Shift
  398
+                return;
  399
+            }
  400
+            if (code === key.space || code === key.enter) {
322 401
                 // Pressing SPACE or ENTER will cause a pick
323 402
                 event.stopPropagation();
324 403
                 event.preventDefault();
@@ -327,16 +406,47 @@ var IPython = (function (IPython) {
327 406
                 // We don't want the document keydown handler to handle UP/DOWN,
328 407
                 // but we want the default action.
329 408
                 event.stopPropagation();
330  
-            } else {
331  
-                // All other key presses exit completion.
332  
-                event.stopPropagation();
333  
-                event.preventDefault();
334  
-                close();
335  
-                that.code_mirror.focus();
  409
+            //} else if ( key.isCompSymbol(code)|| (code==key.backspace)||(code==key.tab && down)){
  410
+            } else if ( (code==key.backspace)||(code==key.tab) || press || key.isCompSymbol(code)){
  411
+                if((code != key.backspace) && (code != key.tab) && press)
  412
+                {
  413
+                    var newchar = String.fromCharCode(code);
  414
+                    typed_characters=typed_characters+newchar;
  415
+                } else if (code == key.tab) {
  416
+                    fastForward = sharedStart(filterd)
  417
+                    ffsub = fastForward.substr(matched_text.length+typed_characters.length);
  418
+                    typed_characters=typed_characters+ffsub;
  419
+                    autopick=true;
  420
+                    event.stopPropagation();
  421
+                    event.preventDefault();
  422
+                } else if (code == key.backspace) {
  423
+                    // cancel if user have erase everything, otherwise decrease
  424
+                    // what we filter with
  425
+                    if (typed_characters.length <= 0)
  426
+                    {
  427
+                        insert(matched_text)
  428
+                    }
  429
+                    typed_characters=typed_characters.substr(0,typed_characters.length-1);
  430
+                }
  431
+                re = new RegExp("^"+"\%?"+matched_text+typed_characters,"");
  432
+                filterd = matches.filter(function(x){return re.test(x)});
  433
+                complete_with(filterd,matched_text+typed_characters,autopick);
  434
+            } else if(down){ // abort only on .keydown
  435
+                // abort with what the user have pressed until now
  436
+                console.log('aborting with keycode : '+code+' is down :'+down);
  437
+                insert(matched_text+typed_characters);
336 438
             }
  439
+        }
  440
+        select.keydown(function (event) {
  441
+            downandpress(event,1)
  442
+        });
  443
+        select.keypress(function (event) {
  444
+            downandpress(event,0)
337 445
         });
338 446
         // Double click also causes a pick.
  447
+        // and bind the last actions.
339 448
         select.dblclick(pick);
  449
+        select.blur(close);
340 450
         select.focus();
341 451
     };
342 452
 
2  IPython/frontend/html/notebook/static/js/kernel.js
@@ -171,7 +171,7 @@ var IPython = (function (IPython) {
171 171
     };
172 172
 
173 173
     Kernel.prototype.object_info_request = function (objname) {
174  
-        if(typeof(objname)!=null)
  174
+        if(typeof(objname)!=null && objname!=null)
175 175
         {
176 176
             var content = {
177 177
                 oname : objname.toString(),
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.