completer.js
241 lines
| 8.4 KiB
| application/javascript
|
JavascriptLexer
Matthias BUSSONNIER
|
r7131 | // 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() | ||||
Matthias BUSSONNIER
|
r7132 | for(var i=0; i< B.length; i++) | ||
Matthias BUSSONNIER
|
r7131 | { | ||
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); | ||||
} | ||||
Matthias BUSSONNIER
|
r7135 | if (tem1 == "" || tem2.indexOf(tem1) != 0){return null;} | ||
Matthias BUSSONNIER
|
r7131 | return { str : tem1, | ||
type : "computed", | ||||
from : B[0].from, | ||||
to : B[0].to | ||||
}; | ||||
} | ||||
return null; | ||||
} | ||||
// user to nsert the given completion | ||||
Matthias BUSSONNIER
|
r7132 | var Completer = function(editor,getHints) { | ||
this.editor = editor; | ||||
Matthias BUSSONNIER
|
r7131 | this.hintfunc = getHints; | ||
// if last caractere before cursor is not in this, we stop completing | ||||
this.reg = /[A-Za-z.]/; | ||||
} | ||||
Matthias BUSSONNIER
|
r7132 | Completer.prototype.startCompletion = function() | ||
Matthias BUSSONNIER
|
r7131 | { | ||
// call for a 'first' completion, that will set the editor and do some | ||||
// special behaviour like autopicking if only one completion availlable | ||||
// | ||||
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; | ||||
Matthias BUSSONNIER
|
r7132 | this.hintfunc(function(result){that._resume_completion(result)}); | ||
Matthias BUSSONNIER
|
r7131 | } | ||
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'); | ||||
Matthias BUSSONNIER
|
r7132 | 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 | ||||
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, | ||||
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
|
r7135 | var sh = sharedStart(this.raw_result); | ||
Matthias BUSSONNIER
|
r7132 | if(sh){ | ||
this.insert(sh); | ||||
} | ||||
Matthias BUSSONNIER
|
r7131 | 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)); | ||||