Skip to content
This repository

usability and cross browser compat for completer #1082

Closed
wants to merge 4 commits into from

3 participants

Matthias Bussonnier Min RK Fernando Perez
Matthias Bussonnier
Collaborator

Improve notebook completer according to @stefanv feedback

  • intercept < backspace > to avoid ff going to previous page when in completer which is really annoying...
  • dissmiss the completer, append what alredy type Plus one caracter in some cases

List of special caracter that are handle are in a given list ()[]./\-+
for the moment. usefull for exaple when typing :

>>> np.s<tab>in(

and not having to type '(' twice.

they are handle separately has the [a-zA-Z] ones because otherwise they
will screw up the regexp, and are opt-in to avoid bugs with invisible
caracters send because some browser have 'keypress' event for meta keys

close #1080

Note to this commit

list of test for the completer across browser with --pylab=inline flag

#test direct one completion
plt.an<tab>       -> plt.annotate

#test filter,tab, only one completion
plt.a<tab>n<tab>  -> plt.annotate

# test partial common beggining
# test dismmised if user erase
plt.a<tab>nn<backspace><backspace>u<tab>                -> completer to `aut`
........................................<tab><tab><tab> -> nothing should append
.......................................................<backspace><backspace<backspace> -> completer cancelled

#test dismiss if no more completion
plt.s<tab>c  -> completer 3 choices
...........u -> dismissed whith what user have typed.  `plt.scu`

# test dismiss in no completion, special symbol
# opt-in list of caracters +-/\()[].
np<tab>.s          -> a 'dot' sould dismiss the completer and be appended
.........<tab>in(  -> np.sin(
np.s<tab>in[       -> np.sin[

I'm a little concerned about keyboard mapping also... but at least i'm using it on french keyboard and most of you should be using english one, so if it's incompatible, I'll now it soon...

Matthias Bussonnier
Collaborator

hum...preventing default on <backspace>seem to prevent Firefox from going one previous page, but block deletion in codemirror... I don't know what to do...

Matthias Bussonnier Carreau closed this December 01, 2011
Matthias Bussonnier usability and cross browser compat for completer
	- dissmiss the completer, append what alredy type, **Plus** one caracter in
	  some cases

	  List of special caracter that are handle are in a given list `()[]./\-+`
	  for the moment. usefull for exaple when typing :
	  >>> np.s<tab>in(
	  and not having to type '(' twice.

	  they are handle separately has the [a-zA-Z] ones because otherwise they
	  will screw up the regexp, and are opt-in to avoid bugs with invisible
	  caracters send because some browser have 'keypress' event for meta keys

	  close #1080

		Note to this commit :
	  list of test for the completer across browser with --pylab=inline flag

	  #test direct one completion
	  plt.an<tab>       -> plt.annotate

	  #test filter,tab, only one completion
	  plt.a<tab>n<tab>  -> plt.annotate

	  # test partial common beggining
	  # test dismmised if user erase
	  plt.a<tab>nn<backspace><backspace>u<tab>                -> completer to `aut`
	  ........................................<tab><tab><tab> -> nothing should append
	  .......................................................<backspace><backspace<backspace> -> completer cancelled

	  #test dismiss if no more completion
	  plt.s<tab>c  -> completer 3 choices
	  ...........u -> dismissed whith what user have typed.  `plt.scu`

	  # test dismiss in no completion, special symbol
	  # opt-in list of caracters +-/\()[].
	  np<tab>.s          -> a 'dot' sould dismiss the completer and be appended
	  .........<tab>in(  -> np.sin(
	  np.s<tab>in[       -> np.sin[
6c78e9b
Matthias Bussonnier
Collaborator

Totally unable to reproduce the bug that had me preventing default with backspace... I'll put that on a browser cache not flushed...
So I removed the code ammended, force push and reopen pull request...
new head is 6c78e9b but gihub seem to not see it...

Matthias Bussonnier Carreau reopened this December 01, 2011
Min RK
Owner

@Carreau - it could be that GitHub stops tracking a branch after closing a PR, so this one won't update. You might close this / reopen as a new one.

Fernando Perez
Owner

Yup, @Carreau, just open a new one and reference this so we can find the earlier discussion.

Small note: don't indent the text in your original message unless you mean it to have code-like formatting (see above how it looks).

Matthias Bussonnier
Collaborator

Github saw the changes while as was commuting apparently.
the indented text is the commit message... won't indent anymore...

IPython/frontend/html/notebook/static/js/codecell.js
((6 lines not shown))
246 246
                     isCompSymbol : function (code)
247  
-                        {return ((code>64 && code <=122)|| code == 189)}
  247
+                        {
2
Fernando Perez Owner
fperez added a note December 01, 2011

No need for an extra blank line here, you can put the first { return... here.

Fernando Perez Owner
fperez added a note December 01, 2011

Ah, never mind: I see this is a function definition, not inline code... Ignore my comment above.

Just getting used to reading JS :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
IPython/frontend/html/notebook/static/js/codecell.js
@@ -407,6 +418,12 @@ var IPython = (function (IPython) {
407 418
                 // nothing on Shift
408 419
                 return;
409 420
             }
  421
+            if (key.dismissAndAppend(code) && press) {
  422
+                var newchar = String.fromCharCode(code);
  423
+                typed_characters=typed_characters+newchar;
1
Fernando Perez Owner
fperez added a note December 01, 2011

whitespace around =

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Matthias Bussonnier
Collaborator

@fperez,
white spaced, getting used to write js.

Fernando Perez
Owner

It mostly looks good, but I'm still getting a little usability nag: try, with --pylab

  1. type plt.pl
  2. hit <tab>
  3. All the completions start with 'plot', but the 'ot' aren't immediately written to the page. They are, however, recorded internally. So if I add the letter 'o', thinking that it needs one more letter, I get the completer to write out plt.ploto, which doesn't make any sense.

Something still needs fixing there...

Fernando Perez
Owner

Sorry, in the previous one it was plt.pl, not np.pl. My mistake, corrected at github but you'll get the wrong version by email.

Matthias Bussonnier
Collaborator

Thats the behaviour of the as you type completer since the beginnig and the use of the 'blue field' on top of the completer is to know the internal state.
My fisrt thought was to do as you suggested, that is to say append each keypress into the codemirror field, but then how would you know when the completer was invoked ? It is especially anoying when you start to correct what you typed and have the completer dismissed because you over <backspace>'d.

IMHO, you should absolutely have at least one indication of what was typed because the filtering is based on the result send by the kernel when pressing <tab> for the first time.

If you don't agree I can try to better entangle it with code mirror. And if you get a better idea of visual indication i'll take it.

Fernando Perez
Owner

Idea: in the blue field, show the characters that start the completion with a different background color, say gray, and only newly typed characters have a blue background. That gives you a good visual cue of the completion state. But do append every typed character onto the underlying codemirror text area, so that the above confusion doesn't happen. With the gray/blue boundary, at least you know if you're about to backspace over your initial input and get the completer dismissed.

Matthias Bussonnier
Collaborator

That's seem a good idea. I'll see what I can do.

Matthias Bussonnier completer update code-miror on the fly
	Following @fperez advice, change the completer apparence to avoid user confusion.

	- Append what the user type in the completer in code-miror, (Almost) as if
	  codemirror still have focus
	- distinguish between "fixed" completion  part, which was sent to the kernel
	  (now written in bold) and filtering one,handled only in JS,that the user
	  can errase without dismissing the completer

	I changed the action of <Space> to dismiss the completer with what have
	already been typed and inserting a space instead of "picking" the currently
	hilighted option

	<Escape> will still dissmiss the completer and remove everything the user as
	typed since the completer invocation

	Note that while the completer is shown, code-mirror does not show any
	blinking cursor
f22fcea
Matthias Bussonnier
Collaborator

@fperez, It more User friendly like that ?
( Note that I change the behaviour of Space from "pick" to "dismiss and append" )

Fernando Perez
Owner

@Carreau, something happened but now on Chrome this completely breaks the completion. Weird...

Try plt.ac<TAB>. The only completion should be plt.acorr, but nothing happens. So with this PR, if you hit 'enter' on the completions list, it works, but the actual TAB key isn't working anymore...

Matthias Bussonnier
Collaborator

actually, the notebook is not working anymore today, even on master or before this branch
I can't even execute a cell... ( silent chrome update ?) when I'll get it working (again), i'll take a look

Fernando Perez
Owner

MMh, that's strange. It works perfectly fine for me, both on Chrome and on Firefox...

Matthias Bussonnier
Collaborator

Closing, and continuing on rebased ( #1108 ) with the fix

Matthias Bussonnier Carreau closed this December 06, 2011
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 4 unique commits by 1 author.

Dec 01, 2011
Matthias Bussonnier usability and cross browser compat for completer
	- dissmiss the completer, append what alredy type, **Plus** one caracter in
	  some cases

	  List of special caracter that are handle are in a given list `()[]./\-+`
	  for the moment. usefull for exaple when typing :
	  >>> np.s<tab>in(
	  and not having to type '(' twice.

	  they are handle separately has the [a-zA-Z] ones because otherwise they
	  will screw up the regexp, and are opt-in to avoid bugs with invisible
	  caracters send because some browser have 'keypress' event for meta keys

	  close #1080

		Note to this commit :
	  list of test for the completer across browser with --pylab=inline flag

	  #test direct one completion
	  plt.an<tab>       -> plt.annotate

	  #test filter,tab, only one completion
	  plt.a<tab>n<tab>  -> plt.annotate

	  # test partial common beggining
	  # test dismmised if user erase
	  plt.a<tab>nn<backspace><backspace>u<tab>                -> completer to `aut`
	  ........................................<tab><tab><tab> -> nothing should append
	  .......................................................<backspace><backspace<backspace> -> completer cancelled

	  #test dismiss if no more completion
	  plt.s<tab>c  -> completer 3 choices
	  ...........u -> dismissed whith what user have typed.  `plt.scu`

	  # test dismiss in no completion, special symbol
	  # opt-in list of caracters +-/\()[].
	  np<tab>.s          -> a 'dot' sould dismiss the completer and be appended
	  .........<tab>in(  -> np.sin(
	  np.s<tab>in[       -> np.sin[
6c78e9b
Matthias Bussonnier Apply pep8 to js fa9e48f
Dec 03, 2011
Matthias Bussonnier completer update code-miror on the fly
	Following @fperez advice, change the completer apparence to avoid user confusion.

	- Append what the user type in the completer in code-miror, (Almost) as if
	  codemirror still have focus
	- distinguish between "fixed" completion  part, which was sent to the kernel
	  (now written in bold) and filtering one,handled only in JS,that the user
	  can errase without dismissing the completer

	I changed the action of <Space> to dismiss the completer with what have
	already been typed and inserting a space instead of "picking" the currently
	hilighted option

	<Escape> will still dissmiss the completer and remove everything the user as
	typed since the completer invocation

	Note that while the completer is shown, code-mirror does not show any
	blinking cursor
f22fcea
Dec 06, 2011
Matthias Bussonnier notebook: fix, only one completion autopick 65d502b
This page is out of date. Refresh to see the latest.
5  IPython/frontend/html/notebook/static/css/notebook.css
@@ -406,6 +406,11 @@ div.text_cell_render {
406 406
     min-height:50px;
407 407
 }
408 408
 
  409
+/*fixed part of the completion*/
  410
+.completions p b{
  411
+    font-weight:bold;
  412
+}
  413
+
409 414
 .completions p{
410 415
     background: #DDF;
411 416
     /*outline: none;
127  IPython/frontend/html/notebook/static/js/codecell.js
@@ -216,7 +216,7 @@ var IPython = (function (IPython) {
216 216
         tooltip.append(expandlink);
217 217
         tooltip.append(morelink);
218 218
         if(defstring){
219  
-            defstring_html= $('<pre/>').html(utils.fixConsole(defstring));
  219
+            defstring_html = $('<pre/>').html(utils.fixConsole(defstring));
220 220
             tooltip.append(defstring_html);
221 221
         }
222 222
         tooltip.append(pre);
@@ -239,12 +239,23 @@ var IPython = (function (IPython) {
239 239
         var key = { tab:9,
240 240
                     esc:27,
241 241
                     backspace:8,
242  
-                    space:13,
  242
+                    space:32,
243 243
                     shift:16,
244  
-                    enter:32,
245  
-                    // _ is 189
  244
+                    enter:13,
  245
+                    // _ is 95
246 246
                     isCompSymbol : function (code)
247  
-                        {return ((code>64 && code <=122)|| code == 189)}
  247
+                        {
  248
+                        return (code > 64 && code <= 90)
  249
+                            || (code >= 97 && code <= 122)
  250
+                            || (code == 95)
  251
+                        },
  252
+                    dismissAndAppend : function (code)
  253
+                        {
  254
+                        chararr = ['(',')','[',']','+','-','/','\\','.',' '];
  255
+                        codearr = chararr.map(function(x){return x.charCodeAt(0)});
  256
+                        return jQuery.inArray(code, codearr) != -1;
  257
+                        }
  258
+
248 259
                     }
249 260
 
250 261
         // smart completion, sort kwarg ending with '='
@@ -253,25 +264,26 @@ var IPython = (function (IPython) {
253 264
         {
254 265
             kwargs = new Array();
255 266
             other = new Array();
256  
-            for(var i=0;i<matches.length; ++i){
  267
+            for(var i = 0 ; i<matches.length ; ++i){
257 268
                 if(matches[i].substr(-1) === '='){
258 269
                     kwargs.push(matches[i]);
259 270
                 }else{other.push(matches[i]);}
260 271
             }
261 272
             newm = kwargs.concat(other);
262  
-            matches=newm;
  273
+            matches = newm;
263 274
         }
264 275
         // end sort kwargs
265 276
 
266 277
         // give common prefix of a array of string
267 278
         function sharedStart(A){
  279
+            if(A.length == 1){return A[0]}
268 280
             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);
  281
+                var tem1, tem2, s, A = A.slice(0).sort();
  282
+                tem1 = A[0];
  283
+                s = tem1.length;
  284
+                tem2 = A.pop();
  285
+                while(s && tem2.indexOf(tem1) == -1){
  286
+                    tem1 = tem1.substring(0, --s);
275 287
                 }
276 288
                 return tem1;
277 289
             }
@@ -281,8 +293,8 @@ var IPython = (function (IPython) {
281 293
 
282 294
         //try to check if the user is typing tab at least twice after a word
283 295
         // and completion is "done"
284  
-        fallback_on_tooltip_after=2
285  
-        if(matches.length==1 && matched_text === matches[0])
  296
+        fallback_on_tooltip_after = 2
  297
+        if(matches.length == 1 && matched_text === matches[0])
286 298
         {
287 299
             if(this.npressed >fallback_on_tooltip_after  && this.prevmatch==matched_text)
288 300
             {
@@ -292,13 +304,13 @@ var IPython = (function (IPython) {
292 304
                 this.request_tooltip_after_time(matched_text+'(',0,this);
293 305
                 return;
294 306
             }
295  
-            this.prevmatch=matched_text
296  
-            this.npressed=this.npressed+1;
  307
+            this.prevmatch = matched_text
  308
+            this.npressed = this.npressed+1;
297 309
         }
298 310
         else
299 311
         {
300  
-            this.prevmatch="";
301  
-            this.npressed=0;
  312
+            this.prevmatch = "";
  313
+            this.npressed = 0;
302 314
         }
303 315
         // end fallback on tooltip
304 316
         //==================================
@@ -311,23 +323,29 @@ var IPython = (function (IPython) {
311 323
         var close = function () {
312 324
             if (done) return;
313 325
             done = true;
314  
-            if (complete!=undefined)
  326
+            if (complete != undefined)
315 327
             {complete.remove();}
316 328
             that.is_completing = false;
317 329
             that.completion_cursor = null;
318 330
         };
319 331
 
320  
-        // insert the given text and exit the completer
321  
-        var insert = function (selected_text, event) {
  332
+        // update codemirror with the typed text
  333
+        prev = matched_text
  334
+        var update = function (inserted_text, event) {
322 335
             that.code_mirror.replaceRange(
323  
-                selected_text,
  336
+                inserted_text,
324 337
                 {line: cur.line, ch: (cur.ch-matched_text.length)},
325  
-                {line: cur.line, ch: cur.ch}
  338
+                {line: cur.line, ch: (cur.ch+prev.length-matched_text.length)}
326 339
             );
  340
+            prev = inserted_text
327 341
             if(event != null){
328 342
                 event.stopPropagation();
329 343
                 event.preventDefault();
330 344
             }
  345
+        };
  346
+        // insert the given text and exit the completer
  347
+        var insert = function (selected_text, event) {
  348
+            update(selected_text)
331 349
             close();
332 350
             setTimeout(function(){that.code_mirror.focus();}, 50);
333 351
         };
@@ -348,22 +366,23 @@ var IPython = (function (IPython) {
348 366
             // Used to 'pick' when pressing tab
349 367
             if (matches.length < 1) {
350 368
                 insert(typed_text,event);
351  
-                if(event !=null){
  369
+                if(event != null){
352 370
                 event.stopPropagation();
353 371
                 event.preventDefault();
354 372
                 }
355  
-            } else if (autopick && matches.length==1) {
  373
+            } else if (autopick && matches.length == 1) {
356 374
                 insert(matches[0],event);
357  
-                if(event !=null){
  375
+                if(event != null){
358 376
                 event.stopPropagation();
359 377
                 event.preventDefault();
360 378
                 }
361 379
             }
362 380
             //clear the previous completion if any
  381
+            update(typed_text,event);
363 382
             complete.children().children().remove();
364  
-            $('#asyoutype').text(typed_text);
365  
-            select=$('#asyoutypeselect');
366  
-            for (var i=0; i<matches.length; ++i) {
  383
+            $('#asyoutype').html("<b>"+matched_text+"</b>"+typed_text.substr(matched_text.length));
  384
+            select = $('#asyoutypeselect');
  385
+            for (var i = 0; i<matches.length; ++i) {
367 386
                     select.append($('<option/>').html(matches[i]));
368 387
             }
369 388
             select.children().first().attr('selected','true');
@@ -372,7 +391,7 @@ var IPython = (function (IPython) {
372 391
         // create html for completer
373 392
         var complete = $('<div/>').addClass('completions');
374 393
             complete.attr('id','complete');
375  
-        complete.append($('<p/>').attr('id', 'asyoutype').html(matched_text));//pseudo input field
  394
+        complete.append($('<p/>').attr('id', 'asyoutype').html('<b>fixed part</b>user part'));//pseudo input field
376 395
 
377 396
         var select = $('<select/>').attr('multiple','true');
378 397
             select.attr('id', 'asyoutypeselect')
@@ -390,25 +409,31 @@ var IPython = (function (IPython) {
390 409
         // So a first actual completion.  see if all the completion start wit
391 410
         // the same letter and complete if necessary
392 411
         fastForward = sharedStart(matches)
393  
-        typed_characters= fastForward.substr(matched_text.length);
  412
+        typed_characters = fastForward.substr(matched_text.length);
394 413
         complete_with(matches,matched_text+typed_characters,true,null);
395  
-        filterd=matches;
  414
+        filterd = matches;
396 415
         // Give focus to select, and make it filter the match as the user type
397 416
         // by filtering the previous matches. Called by .keypress and .keydown
398 417
         var downandpress = function (event,press_or_down) {
399 418
             var code = event.which;
400 419
             var autopick = false; // auto 'pick' if only one match
401 420
             if (press_or_down === 0){
402  
-                press=true; down=false; //Are we called from keypress or keydown
  421
+                press = true; down = false; //Are we called from keypress or keydown
403 422
             } else if (press_or_down == 1){
404  
-                press=false; down=true;
  423
+                press = false; down = true;
405 424
             }
406 425
             if (code === key.shift) {
407 426
                 // nothing on Shift
408 427
                 return;
409 428
             }
410  
-            if (code === key.space || code === key.enter) {
411  
-                // Pressing SPACE or ENTER will cause a pick
  429
+            if (key.dismissAndAppend(code) && press) {
  430
+                var newchar = String.fromCharCode(code);
  431
+                typed_characters = typed_characters+newchar;
  432
+                insert(matched_text+typed_characters,event);
  433
+                return
  434
+            }
  435
+            if (code === key.enter) {
  436
+                // Pressing ENTER will cause a pick
412 437
                 event.stopPropagation();
413 438
                 event.preventDefault();
414 439
                 pick();
@@ -416,35 +441,41 @@ var IPython = (function (IPython) {
416 441
                 // We don't want the document keydown handler to handle UP/DOWN,
417 442
                 // but we want the default action.
418 443
                 event.stopPropagation();
419  
-            //} else if ( key.isCompSymbol(code)|| (code==key.backspace)||(code==key.tab && down)){
420  
-            } else if ( (code==key.backspace)||(code==key.tab && down) || press || key.isCompSymbol(code)){
  444
+            } else if ( (code == key.backspace)||(code == key.tab && down) || press  || key.isCompSymbol(code)){
421 445
                 if( key.isCompSymbol(code) && press)
422 446
                 {
423 447
                     var newchar = String.fromCharCode(code);
424  
-                    typed_characters=typed_characters+newchar;
  448
+                    typed_characters = typed_characters+newchar;
425 449
                 } else if (code == key.tab) {
426 450
                     fastForward = sharedStart(filterd)
427 451
                     ffsub = fastForward.substr(matched_text.length+typed_characters.length);
428  
-                    typed_characters=typed_characters+ffsub;
429  
-                    autopick=true;
430  
-                    event.stopPropagation();
431  
-                    event.preventDefault();
  452
+                    typed_characters = typed_characters+ffsub;
  453
+                    autopick = true;
432 454
                 } else if (code == key.backspace && down) {
433 455
                     // cancel if user have erase everything, otherwise decrease
434 456
                     // what we filter with
  457
+                    event.preventDefault();
435 458
                     if (typed_characters.length <= 0)
436 459
                     {
437 460
                         insert(matched_text,event)
  461
+                        return
438 462
                     }
439  
-                    typed_characters=typed_characters.substr(0,typed_characters.length-1);
440  
-                }else{return}
  463
+                    typed_characters = typed_characters.substr(0,typed_characters.length-1);
  464
+                } else if (press && code != key.backspace && code != key.tab && code != 0){
  465
+                    insert(matched_text+typed_characters,event);
  466
+                    return
  467
+                } else {
  468
+                    return
  469
+                }
441 470
                 re = new RegExp("^"+"\%?"+matched_text+typed_characters,"");
442 471
                 filterd = matches.filter(function(x){return re.test(x)});
443 472
                 complete_with(filterd,matched_text+typed_characters,autopick,event);
444  
-            } else if(down){ // abort only on .keydown
  473
+            } else if( code == key.esc) {
  474
+                // dismiss the completer and go back to before invoking it
  475
+                insert(matched_text,event);
  476
+            } else if( press ){ // abort only on .keypress or esc
445 477
                 // abort with what the user have pressed until now
446 478
                 console.log('aborting with keycode : '+code+' is down :'+down);
447  
-                insert(matched_text+typed_characters,event);
448 479
             }
449 480
         }
450 481
         select.keydown(function (event) {
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.