diff --git a/IPython/frontend/html/notebook/static/css/notebook.css b/IPython/frontend/html/notebook/static/css/notebook.css index 88f9259..b3d4f97 100644 --- a/IPython/frontend/html/notebook/static/css/notebook.css +++ b/IPython/frontend/html/notebook/static/css/notebook.css @@ -273,6 +273,13 @@ div.text_cell_render { font-family: monospace; } +option.context { + background-color: #DEF7FF; +} +option.introspection { + background-color: #EBF4EB; +} + @-moz-keyframes fadeIn { from {opacity:0;} to {opacity:1;} diff --git a/IPython/frontend/html/notebook/static/js/codecell.js b/IPython/frontend/html/notebook/static/js/codecell.js index 5119461..b29b6ed 100644 --- a/IPython/frontend/html/notebook/static/js/codecell.js +++ b/IPython/frontend/html/notebook/static/js/codecell.js @@ -10,7 +10,6 @@ //============================================================================ var IPython = (function (IPython) { - var utils = IPython.utils; var CodeCell = function (notebook) { @@ -23,6 +22,8 @@ var IPython = (function (IPython) { this.tooltip_timeout = null; this.clear_out_timeout = null; IPython.Cell.apply(this, arguments); + var that = this; + this.ccc = new IPython.Completer(function(ed, callback){that.requestCompletion(ed, callback)}); }; @@ -40,8 +41,11 @@ var IPython = (function (IPython) { mode: 'python', theme: 'ipython', readOnly: this.read_only, - onKeyEvent: $.proxy(this.handle_codemirror_keyevent,this) + onKeyEvent: $.proxy(this.handle_codemirror_keyevent,this), }); + var that = this; + ccm = this.code_mirror; + ccc = this.ccc; input.append(input_area); var output = $('<div></div>').addClass('output vbox'); cell.append(input).append(output); @@ -129,13 +133,9 @@ var IPython = (function (IPython) { // Prevent CodeMirror from handling the tab. return true; } else { - pre_cursor.trim(); - // Autocomplete the current line. event.stop(); - var line = editor.getLine(cur.line); - this.is_completing = true; - this.completion_cursor = cur; - IPython.notebook.complete_cell(this, line, cur.ch); + this.ccc.startCompletionFor(this.code_mirror); + return true; }; } else if (event.keyCode === 8 && event.type == 'keydown') { @@ -263,285 +263,39 @@ var IPython = (function (IPython) { }; // As you type completer - CodeCell.prototype.finish_completing = function (matched_text, matches) { - if(matched_text[0]=='%'){ - completing_from_magic = true; - completing_to_magic = false; - } else { - completing_from_magic = false; - completing_to_magic = false; - } - //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:32, - shift:16, - enter:13, - // _ is 95 - isCompSymbol : function (code) - { - return (code > 64 && code <= 90) - || (code >= 97 && code <= 122) - || (code == 95) - }, - dismissAndAppend : function (code) - { - chararr = '()[]+-/\\. ,=*'.split(""); - codearr = chararr.map(function(x){return x.charCodeAt(0)}); - return jQuery.inArray(code, codearr) != -1; - } - - } - - // smart completion, sort kwarg ending with '=' - var newm = new Array(); - if(this.notebook.smart_completer) - { - kwargs = new Array(); - other = new Array(); - for(var i = 0 ; i<matches.length ; ++i){ - if(matches[i].substr(-1) === '='){ - kwargs.push(matches[i]); - }else{other.push(matches[i]);} - } - newm = kwargs.concat(other); - matches = newm; - } - // end sort kwargs - - // give common prefix of a array of string - function sharedStart(A){ - shared=''; - if(A.length == 1){shared=A[0]} - 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); - } - shared = tem1; - } - if (shared[0] == '%' && !completing_from_magic) - { - shared = shared.substr(1); - return [shared, true]; - } else { - return [shared, false]; - } - } - - - //try to check if the user is typing tab at least twice after a word - // and completion is "done" - fallback_on_tooltip_after = 2 - if(matches.length == 1 && matched_text === matches[0]) - { - if(this.npressed >fallback_on_tooltip_after && this.prevmatch==matched_text) - { - this.request_tooltip_after_time(matched_text+'(',0); - return; - } - this.prevmatch = matched_text - this.npressed = this.npressed+1; - } - else - { - 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; - }; - - // update codemirror with the typed text - prev = matched_text - var update = function (inserted_text, event) { - that.code_mirror.replaceRange( - inserted_text, - {line: cur.line, ch: (cur.ch-matched_text.length)}, - {line: cur.line, ch: (cur.ch+prev.length-matched_text.length)} - ); - prev = inserted_text - if(event != null){ - event.stopPropagation(); - event.preventDefault(); - } - }; - // insert the given text and exit the completer - var insert = function (selected_text, event) { - update(selected_text) - close(); - setTimeout(function(){that.code_mirror.focus();}, 50); - }; - - // insert the curent highlited selection and exit - var pick = function () { - insert(select.val()[0],null); - }; - + CodeCell.prototype.requestCompletion= function(ed,callback) + { + this._compcallback = callback; + this._editor = ed; + var cur = ed.getCursor(); + var pre_cursor = this.code_mirror.getRange({line:cur.line,ch:0},cur); + pre_cursor.trim(); + // Autocomplete the current line. + var line = this.code_mirror.getLine(cur.line); + this.is_completing = true; + this.completion_cursor = cur; + IPython.notebook.complete_cell(this, line, cur.ch); + } - // 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,event) + CodeCell.prototype.finish_completing = function (matched_text, matches) { + // let's build a function that wrap all that stuff into what is needed for the + // new completer: + // + var cur = this._editor.getCursor(); + res = CodeMirror.contextHint(this._editor); + for( i=0; i< matches.length ; i++) { - // If autopick an only one match, past. - // Used to 'pick' when pressing tab - var prefix = ''; - if(completing_to_magic && !completing_from_magic) - { - prefix='%'; - } - if (matches.length < 1) { - insert(prefix+typed_text,event); - if(event != null){ - event.stopPropagation(); - event.preventDefault(); - } - } else if (autopick && matches.length == 1) { - insert(matches[0],event); - if(event != null){ - event.stopPropagation(); - event.preventDefault(); - } - return; - } - //clear the previous completion if any - update(prefix+typed_text,event); - complete.children().children().remove(); - $('#asyoutype').html("<b>"+prefix+matched_text+"</b>"+typed_text.substr(matched_text.length)); - 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('<b>fixed part</b>user part'));//pseudo input field - - var select = $('<select/>').attr('multiple','true'); - 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); - - // So a first actual completion. see if all the completion start wit - // the same letter and complete if necessary - ff = sharedStart(matches) - fastForward = ff[0]; - completing_to_magic = ff[1]; - typed_characters = fastForward.substr(matched_text.length); - complete_with(matches,matched_text+typed_characters,true,null); - 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; - 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 (key.dismissAndAppend(code) && press) { - var newchar = String.fromCharCode(code); - typed_characters = typed_characters+newchar; - insert(matched_text+typed_characters,event); - return - } - if (code === key.enter) { - // Pressing ENTER will cause a pick - event.stopPropagation(); - event.preventDefault(); - pick(); - } else if (code === 38 || code === 40) { - // We don't want the document keydown handler to handle UP/DOWN, - // but we want the default action. - event.stopPropagation(); - } else if ( (code == key.backspace)||(code == key.tab && down) || press || key.isCompSymbol(code)){ - if( key.isCompSymbol(code) && press) + res.push( { - var newchar = String.fromCharCode(code); - typed_characters = typed_characters+newchar; - } else if (code == key.tab) { - ff = sharedStart(matches) - fastForward = ff[0]; - completing_to_magic = ff[1]; - ffsub = fastForward.substr(matched_text.length+typed_characters.length); - typed_characters = typed_characters+ffsub; - autopick = true; - } else if (code == key.backspace && down) { - // cancel if user have erase everything, otherwise decrease - // what we filter with - event.preventDefault(); - if (typed_characters.length <= 0) - { - insert(matched_text,event) - return - } - typed_characters = typed_characters.substr(0,typed_characters.length-1); - } else if (press && code != key.backspace && code != key.tab && code != 0){ - insert(matched_text+typed_characters,event); - return - } else { - return + str : matches[i], + type : "introspection", + from : {line: cur.line, ch: cur.ch-matched_text.length}, + to : {line: cur.line, ch: cur.ch} } - re = new RegExp("^"+"\%?"+matched_text+typed_characters,""); - filterd = matches.filter(function(x){return re.test(x)}); - ff = sharedStart(filterd); - completing_to_magic = ff[1]; - complete_with(filterd,matched_text+typed_characters,autopick,event); - } else if (code == key.esc) { - // dismiss the completer and go back to before invoking it - insert(matched_text,event); - } else if (press) { // abort only on .keypress or esc - } + ) } - 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(); + this._compcallback(res); + }; diff --git a/IPython/frontend/html/notebook/static/js/completer.js b/IPython/frontend/html/notebook/static/js/completer.js new file mode 100644 index 0000000..8c05cad --- /dev/null +++ b/IPython/frontend/html/notebook/static/js/completer.js @@ -0,0 +1,233 @@ +// function completer. +// +// completer should be a class that take an editor instance, and a list of +// function to call to get the list of completion. +// +// the function that send back the list of completion should received the +// editor handle as sole argument, and should return a json object with the +// following structure + +// {list: clist, # list of n string containing the completions +// type : rp, # list of n string containingtype/ origin of the completion +// # (will be set as the class of the <option> to be able to style +// # them according to the origin of the completion) +// from: {line: cur.line, ch: token.start}, +// to: {line: cur.line, ch: token.end} +// }; +// + +var IPython = (function(IPython ) { + // that will prevent us froom missspelling + "use strict"; + + // easyier key mapping + var key = { tab:9, + esc:27, + backspace:8, + space:32, + shift:16, + enter:13, + upArrow:38, // check with keyDown.. + downArrow :40 // check with keyUp + }; + + // what is the common start of all completions + function sharedStart(B){ + if(B.length == 1){return B[0]} + var A = new Array() + for(i=0; i< B.length; i++) + { + A.push(B[i].str); + } + 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 { str : tem1, + type : "computed", + from : B[0].from, + to : B[0].to + }; + } + return null; + } + + // user to nsert the given completion + var Completer = function(getHints) { + + this.hintfunc = getHints; + // if last caractere before cursor is not in this, we stop completing + this.reg = /[A-Za-z.]/; + } + + Completer.prototype.startCompletionFor = function(ed) + { + // call for a 'first' completion, that will set the editor and do some + // special behaviour like autopicking if only one completion availlable + // + this.editor = ed; + if (this.editor.somethingSelected()) return; + this.done = false; + // use to get focus back on opera + this.carryOnCompletion(true); + } + + Completer.prototype.carryOnCompletion = function(ff) + { + // pass true as parameter if you want the commpleter to autopick + // when only one completion + // as this function is auto;atically reinvoked at each keystroke with + // ff = false + var cur = this.editor.getCursor(); + var pre_cursor = this.editor.getRange({line:cur.line,ch:cur.ch-1},cur); + + // we nned to check that we are still on a word boundary + // because while typing the completer is still reinvoking itself + if(!this.reg.test(pre_cursor)){ this.close(); return;} + + this.autopick = false; + if( ff != 'undefined' && ff==true) + { + this.autopick=true; + } + // We want a single cursor position. + if (this.editor.somethingSelected()) return; + + // there we will need to gather the results for all the function (and merge them ?) + // lets assume for now only one source + // + var that = this; + this.hintfunc(this.editor,function(result){that._resume_completion(result)}); + } + Completer.prototype._resume_completion = function(results) + { + this.raw_result = results; + + // if empty result return + if (!this.raw_result || !this.raw_result.length) return; + + + + // When there is only one completion, use it directly. + if (this.autopick == true && this.raw_result.length == 1) + { + this.insert(this.raw_result[0]); + return true; + } + + if (this.raw_result.length == 1) + { + // test if first and only completion totally matches + // what is typed, in this case dismiss + var str = this.raw_result[0].str + var cur = this.editor.getCursor(); + var pre_cursor = this.editor.getRange({line:cur.line,ch:cur.ch-str.length},cur); + if(pre_cursor == str){ + this.close(); + return ; + } + } + + this.complete = $('<div/>').addClass('completions'); + this.complete.attr('id','complete'); + + this.sel = $('<select/>').attr('multiple','true'); + var pos = this.editor.cursorCoords(); + + // TODO: I propose to remove enough horizontal pixel + // to align the text later + this.complete.css('left',pos.x+'px'); + this.complete.css('top',pos.yBot+'px'); + this.complete.append(this.sel); + + $('body').append(this.complete); + //build the container + var that = this; + this.sel.dblclick(function(){that.pick()}); + this.sel.blur(this.close); + this.sel.keydown(function(event){that.keydown(event)}); + + this.build_gui_list(this.raw_result); + var that=this; + //CodeMirror.connect(that.sel, "dblclick", function(){that.pick()}); + + this.sel.focus(); + // Opera sometimes ignores focusing a freshly created node + if (window.opera) setTimeout(function(){if (!this.done) this.sel.focus();}, 100); + // why do we return true ? + return true; + } + + Completer.prototype.insert = function(completion) { + this.editor.replaceRange(completion.str, completion.from, completion.to); + } + + Completer.prototype.build_gui_list = function(completions){ + // Need to clear the all list + for (var i = 0; i < completions.length; ++i) { + var opt = $('<option/>') + .text(completions[i].str) + .addClass(completions[i].type); + this.sel.append(opt); + } + this.sel.children().first().attr('selected','true'); + + //sel.size = Math.min(10, completions.length); + // Hack to hide the scrollbar. + //if (completions.length <= 10) + //this.complete.style.width = (this.sel.clientWidth - 1) + "px"; + } + + Completer.prototype.close = function() { + if (this.done) return; + this.done = true; + $('.completions').remove(); + } + + Completer.prototype.pick = function(){ + this.insert(this.raw_result[this.sel[0].selectedIndex]); + this.close(); + var that = this; + setTimeout(function(){that.editor.focus();}, 50); + } + + + Completer.prototype.keydown = function(event) { + var code = event.keyCode; + // Enter + if (code == key.enter) {CodeMirror.e_stop(event); this.pick();} + // Escape or backspace + else if (code == key.esc ) {CodeMirror.e_stop(event); this.close(); this.editor.focus();} + else if (code == key.space || code == key.backspace) {this.close(); this.editor.focus();} + else if (code == key.tab){ + //all the fastforwarding operation, + this.insert(sharedStart(this.raw_result)); + this.close(); + CodeMirror.e_stop(event); + this.editor.focus(); + //reinvoke self + var that = this; + setTimeout(function(){that.carryOnCompletion();}, 50); + } + else if (code == key.upArrow || code == key.downArrow) { + // need to do that to be able to move the arrow + // when on the first or last line ofo a code cell + event.stopPropagation(); + } + else if (code != key.upArrow && code != key.downArrow) { + this.close(); this.editor.focus(); + //we give focus to the editor immediately and call sell in 50 ms + var that = this; + setTimeout(function(){that.carryOnCompletion();}, 50); + } + } + + + IPython.Completer = Completer; + + return IPython; +}(IPython)); diff --git a/IPython/frontend/html/notebook/static/js/context-hint.js b/IPython/frontend/html/notebook/static/js/context-hint.js new file mode 100644 index 0000000..ddde98a --- /dev/null +++ b/IPython/frontend/html/notebook/static/js/context-hint.js @@ -0,0 +1,89 @@ +// highly adapted for codemiror jshint + +(function () { + function forEach(arr, f) { + for (var i = 0, e = arr.length; i < e; ++i) f(arr[i]); + } + + function arrayContains(arr, item) { + if (!Array.prototype.indexOf) { + var i = arr.length; + while (i--) { + if (arr[i] === item) { + return true; + } + } + return false; + } + return arr.indexOf(item) != -1; + } + + CodeMirror.contextHint = function(editor) { + // Find the token at the cursor + var cur = editor.getCursor(), token = editor.getTokenAt(cur), tprop = token; + // If it's not a 'word-style' token, ignore the token. + // If it is a property, find out what it is a property of. + + var list = new Array(); + var clist = getCompletions(token,editor) ; + for( var i = 0 ; i < clist.length ; i++) + { + list.push( + { + str : clist[i], + type : "context", + from : {line: cur.line, ch: token.start}, + to : {line: cur.line, ch: token.end} + } + ) + + } + return list; + } + + // find all 'words' of current cell + function getAllTokens(editor) + { + var found = []; + // get all text remove and split it before dot and at space + // keep the dot for completing token that also start with dot + var candidates = editor.getValue() + .replace(/[. ]/g,"\n") + .split('\n'); + // append to arry if not already (the function) + function maybeAdd(str) { + if (!arrayContains(found, str)) found.push(str); + } + + // append to arry if not already + // (here we do it ) + for( c in candidates ) + { + if(candidates[c].length >= 1){ + maybeAdd(candidates[c]);} + } + return found; + + } + + function getCompletions(token,editor) + { + var candidates = getAllTokens(editor); + // filter all token that have a common start (but nox exactly) the lenght of the current token + var prependchar =''; + if(token.string.indexOf('.') == 0) + { + prependchar = '.' + } + var lambda = function(x){ + x = prependchar+x; + return (x.indexOf(token.string)==0 && x != token.string)}; + var filterd = candidates.filter(lambda); + for( var i in filterd) + { + // take care of reappending '.' at the beginning + filterd[i] = prependchar+filterd[i]; + } + return filterd; + } +})(); diff --git a/IPython/frontend/html/notebook/templates/notebook.html b/IPython/frontend/html/notebook/templates/notebook.html index 89d2ab7..f7ba06d 100644 --- a/IPython/frontend/html/notebook/templates/notebook.html +++ b/IPython/frontend/html/notebook/templates/notebook.html @@ -1,5 +1,4 @@ {% extends page.html %} - {% block stylesheet %} {% if mathjax_url %} @@ -220,6 +219,7 @@ data-notebook-id={{notebook_id}} <script src="{{ static_url("js/initmathjax.js") }}" type="text/javascript" charset="utf-8"></script> <script src="{{ static_url("js/cell.js") }}" type="text/javascript" charset="utf-8"></script> <script src="{{ static_url("js/codecell.js") }}" type="text/javascript" charset="utf-8"></script> +<script src="{{ static_url("js/completer.js") }}" type="text/javascript" charset="utf-8"></script> <script src="{{ static_url("js/textcell.js") }}" type="text/javascript" charset="utf-8"></script> <script src="{{ static_url("js/kernel.js") }}" type="text/javascript" charset="utf-8"></script> <script src="{{ static_url("js/savewidget.js") }}" type="text/javascript" charset="utf-8"></script> @@ -231,5 +231,8 @@ data-notebook-id={{notebook_id}} <script src="{{ static_url("js/notificationwidget.js") }}" type="text/javascript" charset="utf-8"></script> <script src="{{ static_url("js/notebookmain.js") }}" type="text/javascript" charset="utf-8"></script> +<script src="{{ static_url("js/context-hint.js") }} charset="utf-8"></script> +<script src="{{ static_url("codemirror/lib/util/simple-hint.js") }} charset="utf-8"></script> + {% end %}