completer.js
305 lines
| 9.9 KiB
| application/javascript
|
JavascriptLexer
Matthias BUSSONNIER
|
r7131 | // function completer. | ||
// | ||||
Matthias BUSSONNIER
|
r7142 | // completer should be a class that take an cell instance | ||
Matthias BUSSONNIER
|
r7191 | var IPython = (function (IPython) { | ||
Matthias BUSSONNIER
|
r7142 | // that will prevent us from misspelling | ||
Matthias BUSSONNIER
|
r7131 | "use strict"; | ||
// easyier key mapping | ||||
Matthias BUSSONNIER
|
r7137 | var key = IPython.utils.keycodes; | ||
Matthias BUSSONNIER
|
r7142 | |||
Matthias BUSSONNIER
|
r7349 | function prepend_n_prc(str, n) { | ||
for( var i =0 ; i< n ; i++) | ||||
Matthias BUSSONNIER
|
r7348 | { str = '%'+str } | ||
return str; | ||||
} | ||||
Matthias BUSSONNIER
|
r7293 | function _existing_completion(item, completion_array){ | ||
Matthias BUSSONNIER
|
r7303 | for( var c in completion_array ) { | ||
if(completion_array[c].substr(-item.length) == item) | ||||
{ return true; } | ||||
} | ||||
return false; | ||||
Matthias BUSSONNIER
|
r7288 | } | ||
Matthias BUSSONNIER
|
r7190 | |||
Matthias BUSSONNIER
|
r7303 | // what is the common start of all completions | ||
Matthias BUSSONNIER
|
r7349 | function shared_start(B, drop_prct) { | ||
Matthias BUSSONNIER
|
r7190 | if (B.length == 1) { | ||
return B[0]; | ||||
} | ||||
var A = new Array(); | ||||
Matthias BUSSONNIER
|
r7348 | var common; | ||
var min_lead_prct = 10; | ||||
Matthias BUSSONNIER
|
r7190 | for (var i = 0; i < B.length; i++) { | ||
Matthias BUSSONNIER
|
r7349 | var str = B[i].str; | ||
var localmin = 0; | ||||
Matthias BUSSONNIER
|
r7348 | if(drop_prct == true){ | ||
Matthias BUSSONNIER
|
r7349 | while ( str.substr(0, 1) == '%') { | ||
Matthias BUSSONNIER
|
r7348 | localmin = localmin+1; | ||
str = str.substring(1); | ||||
} | ||||
} | ||||
Matthias BUSSONNIER
|
r7349 | min_lead_prct = Math.min(min_lead_prct, localmin); | ||
Matthias BUSSONNIER
|
r7348 | A.push(str); | ||
Matthias BUSSONNIER
|
r7190 | } | ||
Matthias BUSSONNIER
|
r7348 | |||
Matthias BUSSONNIER
|
r7190 | 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); | ||||
Matthias BUSSONNIER
|
r7131 | } | ||
Matthias BUSSONNIER
|
r7190 | if (tem1 == "" || tem2.indexOf(tem1) != 0) { | ||
Matthias BUSSONNIER
|
r7349 | return prepend_n_prc('', min_lead_prct); | ||
Matthias BUSSONNIER
|
r7131 | } | ||
Matthias BUSSONNIER
|
r7190 | return { | ||
Matthias BUSSONNIER
|
r7349 | str: prepend_n_prc(tem1, min_lead_prct), | ||
Matthias BUSSONNIER
|
r7190 | type: "computed", | ||
from: B[0].from, | ||||
to: B[0].to | ||||
}; | ||||
Matthias BUSSONNIER
|
r7131 | } | ||
Matthias BUSSONNIER
|
r7190 | return null; | ||
} | ||||
Matthias BUSSONNIER
|
r7131 | |||
Matthias BUSSONNIER
|
r7141 | |||
Matthias BUSSONNIER
|
r7191 | var Completer = function (cell) { | ||
Brian E. Granger
|
r9221 | this.cell = cell; | ||
this.editor = cell.code_mirror; | ||||
var that = this; | ||||
$([IPython.events]).on('status_busy.Kernel', function () { | ||||
that.skip_kernel_completion = true; | ||||
}); | ||||
$([IPython.events]).on('status_idle.Kernel', function () { | ||||
that.skip_kernel_completion = false; | ||||
}); | ||||
}; | ||||
Matthias BUSSONNIER
|
r7141 | |||
Matthias BUSSONNIER
|
r7174 | |||
Matthias BUSSONNIER
|
r7191 | Completer.prototype.startCompletion = function () { | ||
Matthias BUSSONNIER
|
r7142 | // call for a 'first' completion, that will set the editor and do some | ||
Matthias BUSSONNIER
|
r7131 | // special behaviour like autopicking if only one completion availlable | ||
// | ||||
if (this.editor.somethingSelected()) return; | ||||
this.done = false; | ||||
// use to get focus back on opera | ||||
Matthias BUSSONNIER
|
r7192 | this.carry_on_completion(true); | ||
Matthias BUSSONNIER
|
r7170 | }; | ||
Matthias BUSSONNIER
|
r7131 | |||
Matthias BUSSONNIER
|
r7192 | Completer.prototype.carry_on_completion = function (ff) { | ||
Matthias BUSSONNIER
|
r7143 | // Pass true as parameter if you want the commpleter to autopick when | ||
// only one completion. This function is automatically reinvoked at | ||||
// each keystroke with ff = false | ||||
Matthias BUSSONNIER
|
r7131 | var cur = this.editor.getCursor(); | ||
Matthias BUSSONNIER
|
r7174 | var line = this.editor.getLine(cur.line); | ||
Matthias BUSSONNIER
|
r7190 | var pre_cursor = this.editor.getRange({ | ||
line: cur.line, | ||||
ch: cur.ch - 1 | ||||
}, cur); | ||||
Matthias BUSSONNIER
|
r7131 | |||
Matthias BUSSONNIER
|
r7142 | // we need to check that we are still on a word boundary | ||
Matthias BUSSONNIER
|
r7131 | // because while typing the completer is still reinvoking itself | ||
Matthias BUSSONNIER
|
r7603 | if (!/[0-9a-z._/\\:~-]/i.test(pre_cursor)) { | ||
Matthias BUSSONNIER
|
r7190 | this.close(); | ||
return; | ||||
} | ||||
Matthias BUSSONNIER
|
r7142 | |||
Matthias BUSSONNIER
|
r7131 | this.autopick = false; | ||
Matthias BUSSONNIER
|
r7190 | if (ff != 'undefined' && ff == true) { | ||
this.autopick = true; | ||||
} | ||||
Matthias BUSSONNIER
|
r7143 | |||
Matthias BUSSONNIER
|
r7131 | // We want a single cursor position. | ||
if (this.editor.somethingSelected()) return; | ||||
Matthias BUSSONNIER
|
r7174 | // one kernel completion came back, finish_completing will be called with the results | ||
// we fork here and directly call finish completing if kernel is busy | ||||
Matthias BUSSONNIER
|
r7190 | if (this.skip_kernel_completion == true) { | ||
this.finish_completing({ | ||||
'matches': [], | ||||
matched_text: "" | ||||
}) | ||||
Matthias BUSSONNIER
|
r7174 | } else { | ||
Matthias BUSSONNIER
|
r7190 | var callbacks = { | ||
'complete_reply': $.proxy(this.finish_completing, this) | ||||
}; | ||||
Brian E. Granger
|
r9221 | this.cell.kernel.complete(line, cur.ch, callbacks); | ||
Matthias BUSSONNIER
|
r7174 | } | ||
Matthias BUSSONNIER
|
r7170 | }; | ||
Matthias BUSSONNIER
|
r7142 | |||
Matthias BUSSONNIER
|
r7191 | Completer.prototype.finish_completing = function (content) { | ||
Matthias BUSSONNIER
|
r7143 | // let's build a function that wrap all that stuff into what is needed | ||
// for the new completer: | ||||
Brian Granger
|
r7168 | var matched_text = content.matched_text; | ||
var matches = content.matches; | ||||
Matthias BUSSONNIER
|
r7143 | |||
Matthias BUSSONNIER
|
r7142 | var cur = this.editor.getCursor(); | ||
var results = CodeMirror.contextHint(this.editor); | ||||
Matthias BUSSONNIER
|
r7287 | var filterd_results = Array(); | ||
//remove results from context completion | ||||
//that are already in kernel completion | ||||
Matthias BUSSONNIER
|
r7293 | for(var elm in results) { | ||
Matthias BUSSONNIER
|
r7303 | if(_existing_completion(results[elm]['str'], matches) == false) | ||
Matthias BUSSONNIER
|
r7293 | { filterd_results.push(results[elm]); } | ||
Matthias BUSSONNIER
|
r7287 | } | ||
Matthias BUSSONNIER
|
r7142 | |||
Matthias BUSSONNIER
|
r7143 | // append the introspection result, in order, at at the beginning of | ||
// the table and compute the replacement range from current cursor | ||||
// positon and matched_text length. | ||||
Matthias BUSSONNIER
|
r7190 | for (var i = matches.length - 1; i >= 0; --i) { | ||
Matthias BUSSONNIER
|
r7287 | filterd_results.unshift({ | ||
Matthias BUSSONNIER
|
r7190 | str: matches[i], | ||
type: "introspection", | ||||
from: { | ||||
line: cur.line, | ||||
ch: cur.ch - matched_text.length | ||||
}, | ||||
to: { | ||||
line: cur.line, | ||||
ch: cur.ch | ||||
} | ||||
}); | ||||
Matthias BUSSONNIER
|
r7142 | } | ||
// one the 2 sources results have been merge, deal with it | ||||
Matthias BUSSONNIER
|
r7287 | this.raw_result = filterd_results; | ||
Matthias BUSSONNIER
|
r7131 | |||
// if empty result return | ||||
if (!this.raw_result || !this.raw_result.length) return; | ||||
// When there is only one completion, use it directly. | ||||
Matthias BUSSONNIER
|
r7190 | if (this.autopick == true && this.raw_result.length == 1) { | ||
Matthias BUSSONNIER
|
r7170 | this.insert(this.raw_result[0]); | ||
return; | ||||
} | ||||
Matthias BUSSONNIER
|
r7131 | |||
Matthias BUSSONNIER
|
r7190 | if (this.raw_result.length == 1) { | ||
Matthias BUSSONNIER
|
r7143 | // test if first and only completion totally matches | ||
// what is typed, in this case dismiss | ||||
Matthias BUSSONNIER
|
r7170 | var str = this.raw_result[0].str; | ||
Matthias BUSSONNIER
|
r7190 | var pre_cursor = this.editor.getRange({ | ||
line: cur.line, | ||||
ch: cur.ch - str.length | ||||
}, cur); | ||||
if (pre_cursor == str) { | ||||
this.close(); | ||||
return; | ||||
} | ||||
Matthias BUSSONNIER
|
r7131 | } | ||
this.complete = $('<div/>').addClass('completions'); | ||||
Matthias BUSSONNIER
|
r7190 | this.complete.attr('id', 'complete'); | ||
Matthias BUSSONNIER
|
r7131 | |||
Matthias BUSSONNIER
|
r7190 | this.sel = $('<select/>').attr('multiple', 'true').attr('size', Math.min(10, this.raw_result.length)); | ||
Matthias BUSSONNIER
|
r7131 | var pos = this.editor.cursorCoords(); | ||
// TODO: I propose to remove enough horizontal pixel | ||||
// to align the text later | ||||
Matthias BUSSONNIER
|
r7190 | this.complete.css('left', pos.x + 'px'); | ||
this.complete.css('top', pos.yBot + 'px'); | ||||
Matthias BUSSONNIER
|
r7131 | this.complete.append(this.sel); | ||
$('body').append(this.complete); | ||||
//build the container | ||||
var that = this; | ||||
Matthias BUSSONNIER
|
r7191 | this.sel.dblclick(function () { | ||
Matthias BUSSONNIER
|
r7190 | that.pick(); | ||
}); | ||||
Matthias BUSSONNIER
|
r7131 | this.sel.blur(this.close); | ||
Matthias BUSSONNIER
|
r7191 | this.sel.keydown(function (event) { | ||
Matthias BUSSONNIER
|
r7190 | that.keydown(event); | ||
}); | ||||
Matthias BUSSONNIER
|
r7131 | |||
this.build_gui_list(this.raw_result); | ||||
this.sel.focus(); | ||||
// Opera sometimes ignores focusing a freshly created node | ||||
Matthias BUSSONNIER
|
r7191 | if (window.opera) setTimeout(function () { | ||
Matthias BUSSONNIER
|
r7190 | if (!this.done) this.sel.focus(); | ||
}, 100); | ||||
Matthias BUSSONNIER
|
r7131 | return true; | ||
} | ||||
Matthias BUSSONNIER
|
r7191 | Completer.prototype.insert = function (completion) { | ||
Matthias BUSSONNIER
|
r7131 | this.editor.replaceRange(completion.str, completion.from, completion.to); | ||
Matthias BUSSONNIER
|
r7190 | } | ||
Matthias BUSSONNIER
|
r7131 | |||
Matthias BUSSONNIER
|
r7191 | Completer.prototype.build_gui_list = function (completions) { | ||
Matthias BUSSONNIER
|
r7131 | // Need to clear the all list | ||
for (var i = 0; i < completions.length; ++i) { | ||||
Matthias BUSSONNIER
|
r7190 | var opt = $('<option/>').text(completions[i].str).addClass(completions[i].type); | ||
Matthias BUSSONNIER
|
r7131 | this.sel.append(opt); | ||
} | ||||
Matthias BUSSONNIER
|
r7190 | this.sel.children().first().attr('selected', 'true'); | ||
Matthias BUSSONNIER
|
r7131 | } | ||
Matthias BUSSONNIER
|
r7191 | Completer.prototype.close = function () { | ||
Matthias BUSSONNIER
|
r7190 | if (this.done) return; | ||
this.done = true; | ||||
$('.completions').remove(); | ||||
} | ||||
Matthias BUSSONNIER
|
r7131 | |||
Matthias BUSSONNIER
|
r7191 | Completer.prototype.pick = function () { | ||
Matthias BUSSONNIER
|
r7131 | this.insert(this.raw_result[this.sel[0].selectedIndex]); | ||
this.close(); | ||||
var that = this; | ||||
Matthias BUSSONNIER
|
r7191 | setTimeout(function () { | ||
Matthias BUSSONNIER
|
r7190 | that.editor.focus(); | ||
}, 50); | ||||
} | ||||
Matthias BUSSONNIER
|
r7131 | |||
Matthias BUSSONNIER
|
r7142 | |||
Matthias BUSSONNIER
|
r7191 | Completer.prototype.keydown = function (event) { | ||
Matthias BUSSONNIER
|
r7190 | var code = event.keyCode; | ||
var that = this; | ||||
// Enter | ||||
Matthias BUSSONNIER
|
r7193 | if (code == key.ENTER) { | ||
Matthias BUSSONNIER
|
r7190 | CodeMirror.e_stop(event); | ||
this.pick(); | ||||
} | ||||
// Escape or backspace | ||||
Matthias BUSSONNIER
|
r7193 | else if (code == key.ESC) { | ||
Matthias BUSSONNIER
|
r7190 | CodeMirror.e_stop(event); | ||
this.close(); | ||||
this.editor.focus(); | ||||
Matthias BUSSONNIER
|
r7193 | } else if (code == key.SPACE || code == key.BACKSPACE) { | ||
Matthias BUSSONNIER
|
r7190 | this.close(); | ||
this.editor.focus(); | ||||
Matthias BUSSONNIER
|
r7193 | } else if (code == key.TAB) { | ||
Matthias BUSSONNIER
|
r7142 | //all the fastforwarding operation, | ||
Matthias BUSSONNIER
|
r7132 | //Check that shared start is not null which can append with prefixed completion | ||
// like %pylab , pylab have no shred start, and ff will result in py<tab><tab> | ||||
// to erase py | ||||
Matthias BUSSONNIER
|
r7349 | var sh = shared_start(this.raw_result, true); | ||
Matthias BUSSONNIER
|
r7190 | if (sh) { | ||
Matthias BUSSONNIER
|
r7132 | this.insert(sh); | ||
} | ||||
Matthias BUSSONNIER
|
r7142 | this.close(); | ||
Matthias BUSSONNIER
|
r7131 | CodeMirror.e_stop(event); | ||
this.editor.focus(); | ||||
//reinvoke self | ||||
Matthias BUSSONNIER
|
r7191 | setTimeout(function () { | ||
Matthias BUSSONNIER
|
r7192 | that.carry_on_completion(); | ||
Matthias BUSSONNIER
|
r7190 | }, 50); | ||
Matthias BUSSONNIER
|
r7193 | } else if (code == key.UPARROW || code == key.DOWNARROW) { | ||
Matthias BUSSONNIER
|
r7190 | // need to do that to be able to move the arrow | ||
// when on the first or last line ofo a code cell | ||||
event.stopPropagation(); | ||||
Matthias BUSSONNIER
|
r7193 | } else if (code != key.UPARROW && code != key.DOWNARROW) { | ||
Matthias BUSSONNIER
|
r7190 | this.close(); | ||
this.editor.focus(); | ||||
//we give focus to the editor immediately and call sell in 50 ms | ||||
Matthias BUSSONNIER
|
r7191 | setTimeout(function () { | ||
Matthias BUSSONNIER
|
r7192 | that.carry_on_completion(); | ||
Matthias BUSSONNIER
|
r7190 | }, 50); | ||
} | ||||
Matthias BUSSONNIER
|
r7131 | } | ||
IPython.Completer = Completer; | ||||
return IPython; | ||||
}(IPython)); | ||||