completer.js
399 lines
| 13.6 KiB
| application/javascript
|
JavascriptLexer
MinRK
|
r16580 | // Copyright (c) IPython Development Team. | ||
// Distributed under the terms of the Modified BSD License. | ||||
Jonathan Frederic
|
r17198 | define([ | ||
'base/js/namespace', | ||||
Jonathan Frederic
|
r17200 | 'jquery', | ||
Jonathan Frederic
|
r17198 | 'base/js/utils', | ||
'base/js/keyboard', | ||||
Jonathan Frederic
|
r17200 | 'notebook/js/contexthint', | ||
Jonathan Frederic
|
r17198 | ], function(IPython, $, utils, keyboard) { | ||
Matthias BUSSONNIER
|
r7131 | "use strict"; | ||
Susan Tan
|
r13822 | // easier key mapping | ||
Jonathan Frederic
|
r17198 | var keycodes = keyboard.keycodes; | ||
Matthias BUSSONNIER
|
r7142 | |||
MinRK
|
r16588 | var prepend_n_prc = function(str, n) { | ||
Matthias BUSSONNIER
|
r12912 | for( var i =0 ; i< n ; i++){ | ||
str = '%'+str ; | ||||
} | ||||
Matthias BUSSONNIER
|
r7348 | return str; | ||
MinRK
|
r16226 | }; | ||
Matthias BUSSONNIER
|
r7348 | |||
MinRK
|
r16588 | var _existing_completion = function(item, completion_array){ | ||
MinRK
|
r16226 | for( var i=0; i < completion_array.length; i++) { | ||
if (completion_array[i].trim().substr(-item.length) == item) { | ||||
return true; | ||||
} | ||||
Matthias BUSSONNIER
|
r11282 | } | ||
MinRK
|
r16226 | return false; | ||
}; | ||||
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]; | ||||
} | ||||
MinRK
|
r15322 | var A = []; | ||
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
|
r12912 | 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
|
r12912 | if (tem1 === "" || tem2.indexOf(tem1) !== 0) { | ||
Matthias BUSSONNIER
|
r12068 | return { | ||
str:prepend_n_prc('', min_lead_prct), | ||||
type: "computed", | ||||
from: B[0].from, | ||||
to: B[0].to | ||||
Matthias BUSSONNIER
|
r12912 | }; | ||
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 | |||
Jonathan Frederic
|
r17198 | var Completer = function (cell, events) { | ||
Brian E. Granger
|
r9221 | this.cell = cell; | ||
this.editor = cell.code_mirror; | ||||
var that = this; | ||||
Jonathan Frederic
|
r17198 | events.on('status_busy.Kernel', function () { | ||
Brian E. Granger
|
r9221 | that.skip_kernel_completion = true; | ||
}); | ||||
Jonathan Frederic
|
r17198 | events.on('status_idle.Kernel', function () { | ||
Brian E. Granger
|
r9221 | that.skip_kernel_completion = false; | ||
}); | ||||
}; | ||||
Matthias BUSSONNIER
|
r7141 | |||
Matthias BUSSONNIER
|
r7191 | Completer.prototype.startCompletion = function () { | ||
Matthias BUSSONNIER
|
r7142 | // call for a 'first' completion, that will set the editor and do some | ||
Jonathan Frederic
|
r15952 | // special behavior like autopicking if only one completion available. | ||
Matthias BUSSONNIER
|
r7131 | 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
|
r12912 | |||
// easy access for julia to monkeypatch | ||||
// | ||||
Completer.reinvoke_re = /[%0-9a-z._/\\:~-]/i; | ||||
Completer.prototype.reinvoke= function(pre_cursor, block, cursor){ | ||||
return Completer.reinvoke_re.test(pre_cursor); | ||||
MinRK
|
r15322 | }; | ||
Matthias BUSSONNIER
|
r12912 | |||
/** | ||||
* | ||||
* pass true as parameter if this is the first invocation of the completer | ||||
* this will prevent the completer to dissmiss itself if it is not on a | ||||
* word boundary like pressing tab after a space, and make it autopick the | ||||
* only choice if there is only one which prevent from popping the UI. as | ||||
* well as fast-forwarding the typing if all completion have a common | ||||
* shared start | ||||
**/ | ||||
Completer.prototype.carry_on_completion = function (first_invocation) { | ||||
Susan Tan
|
r13822 | // Pass true as parameter if you want the completer to autopick when | ||
Matthias BUSSONNIER
|
r7143 | // only one completion. This function is automatically reinvoked at | ||
Matthias BUSSONNIER
|
r12912 | // each keystroke with first_invocation = 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
|
r12912 | // so dismiss if we are on a "bad" caracter | ||
if (!this.reinvoke(pre_cursor) && !first_invocation) { | ||||
Matthias BUSSONNIER
|
r7190 | this.close(); | ||
return; | ||||
} | ||||
Matthias BUSSONNIER
|
r7142 | |||
Matthias BUSSONNIER
|
r7131 | this.autopick = false; | ||
Matthias BUSSONNIER
|
r12912 | if (first_invocation) { | ||
Matthias BUSSONNIER
|
r7190 | this.autopick = true; | ||
} | ||||
Matthias BUSSONNIER
|
r7143 | |||
Matthias BUSSONNIER
|
r7131 | // We want a single cursor position. | ||
Matthias BUSSONNIER
|
r12912 | if (this.editor.somethingSelected()) { | ||
return; | ||||
MinRK
|
r15322 | } | ||
Matthias BUSSONNIER
|
r7131 | |||
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 | ||||
MinRK
|
r16588 | var cursor_pos = utils.to_absolute_cursor_pos(this.editor, cur); | ||
MinRK
|
r15322 | if (this.skip_kernel_completion) { | ||
MinRK
|
r16588 | this.finish_completing({ content: { | ||
MinRK
|
r16580 | matches: [], | ||
MinRK
|
r16588 | cursor_start: cursor_pos, | ||
cursor_end: cursor_pos, | ||||
}}); | ||||
Matthias BUSSONNIER
|
r7174 | } else { | ||
MinRK
|
r16580 | this.cell.kernel.complete(this.editor.getValue(), cursor_pos, | ||
$.proxy(this.finish_completing, this) | ||||
); | ||||
Matthias BUSSONNIER
|
r7174 | } | ||
Matthias BUSSONNIER
|
r7170 | }; | ||
Matthias BUSSONNIER
|
r7142 | |||
MinRK
|
r13207 | Completer.prototype.finish_completing = function (msg) { | ||
Matthias BUSSONNIER
|
r7143 | // let's build a function that wrap all that stuff into what is needed | ||
// for the new completer: | ||||
MinRK
|
r13207 | var content = msg.content; | ||
MinRK
|
r16588 | var start = content.cursor_start; | ||
var end = content.cursor_end; | ||||
Brian Granger
|
r7168 | var matches = content.matches; | ||
Matthias BUSSONNIER
|
r7143 | |||
Matthias BUSSONNIER
|
r7142 | var cur = this.editor.getCursor(); | ||
MinRK
|
r16698 | if (end === null) { | ||
// adapted message spec replies don't have cursor position info, | ||||
// interpret end=null as current position, | ||||
// and negative start relative to that | ||||
end = utils.to_absolute_cursor_pos(this.editor, cur); | ||||
if (start < 0) { | ||||
start = end + start; | ||||
} | ||||
} | ||||
Matthias BUSSONNIER
|
r7142 | var results = CodeMirror.contextHint(this.editor); | ||
MinRK
|
r15322 | var filtered_results = []; | ||
Matthias BUSSONNIER
|
r7287 | //remove results from context completion | ||
//that are already in kernel completion | ||||
MinRK
|
r16588 | var i; | ||
for (i=0; i < results.length; i++) { | ||||
MinRK
|
r16226 | if (!_existing_completion(results[i].str, matches)) { | ||
filtered_results.push(results[i]); | ||||
MinRK
|
r15322 | } | ||
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. | ||||
MinRK
|
r16588 | for (i = matches.length - 1; i >= 0; --i) { | ||
MinRK
|
r15322 | filtered_results.unshift({ | ||
Matthias BUSSONNIER
|
r7190 | str: matches[i], | ||
type: "introspection", | ||||
MinRK
|
r16588 | from: utils.from_absolute_cursor_pos(this.editor, start), | ||
to: utils.from_absolute_cursor_pos(this.editor, end) | ||||
Matthias BUSSONNIER
|
r7190 | }); | ||
Matthias BUSSONNIER
|
r7142 | } | ||
// one the 2 sources results have been merge, deal with it | ||||
MinRK
|
r15322 | this.raw_result = filtered_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. | ||||
MinRK
|
r15322 | if (this.autopick && 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 | } | ||
Jonathan Frederic
|
r15952 | if (!this.visible) { | ||
this.complete = $('<div/>').addClass('completions'); | ||||
this.complete.attr('id', 'complete'); | ||||
// Currently webkit doesn't use the size attr correctly. See: | ||||
// https://code.google.com/p/chromium/issues/detail?id=4579 | ||||
Jonathan Frederic
|
r15960 | this.sel = $('<select/>') | ||
Jonathan Frederic
|
r15952 | .attr('tabindex', -1) | ||
.attr('multiple', 'true'); | ||||
this.complete.append(this.sel); | ||||
this.visible = true; | ||||
$('body').append(this.complete); | ||||
//build the container | ||||
var that = this; | ||||
this.sel.dblclick(function () { | ||||
that.pick(); | ||||
}); | ||||
Jonathan Frederic
|
r15956 | this.sel.focus(function () { | ||
that.editor.focus(); | ||||
}); | ||||
Jonathan Frederic
|
r15953 | this._handle_keydown = function (cm, event) { | ||
Jonathan Frederic
|
r15952 | that.keydown(event); | ||
Jonathan Frederic
|
r15953 | }; | ||
this.editor.on('keydown', this._handle_keydown); | ||||
Jonathan Frederic
|
r15956 | this._handle_keypress = function (cm, event) { | ||
that.keypress(event); | ||||
}; | ||||
this.editor.on('keypress', this._handle_keypress); | ||||
Jonathan Frederic
|
r15952 | } | ||
this.sel.attr('size', Math.min(10, this.raw_result.length)); | ||||
Brian E. Granger
|
r12895 | |||
Brian E. Granger
|
r12897 | // After everything is on the page, compute the postion. | ||
// We put it above the code if it is too close to the bottom of the page. | ||||
MinRK
|
r16588 | var pos = this.editor.cursorCoords( | ||
utils.from_absolute_cursor_pos(this.editor, start) | ||||
); | ||||
Brian E. Granger
|
r12895 | var left = pos.left-3; | ||
var top; | ||||
var cheight = this.complete.height(); | ||||
var wheight = $(window).height(); | ||||
if (pos.bottom+cheight+5 > wheight) { | ||||
top = pos.top-cheight-4; | ||||
} else { | ||||
top = pos.bottom+1; | ||||
} | ||||
this.complete.css('left', left + 'px'); | ||||
this.complete.css('top', top + 'px'); | ||||
Jonathan Frederic
|
r15952 | // Clear and fill the list. | ||
this.sel.text(''); | ||||
Brian E. Granger
|
r12896 | this.build_gui_list(this.raw_result); | ||
Matthias BUSSONNIER
|
r7131 | return true; | ||
MinRK
|
r15322 | }; | ||
Matthias BUSSONNIER
|
r7131 | |||
Matthias BUSSONNIER
|
r7191 | Completer.prototype.insert = function (completion) { | ||
Matthias BUSSONNIER
|
r7131 | this.editor.replaceRange(completion.str, completion.from, completion.to); | ||
MinRK
|
r15322 | }; | ||
Matthias BUSSONNIER
|
r7131 | |||
Brian E. Granger
|
r12896 | Completer.prototype.build_gui_list = function (completions) { | ||
for (var i = 0; i < completions.length; ++i) { | ||||
var opt = $('<option/>').text(completions[i].str).addClass(completions[i].type); | ||||
this.sel.append(opt); | ||||
Matthias BUSSONNIER
|
r7131 | } | ||
Brian E. Granger
|
r12896 | this.sel.children().first().attr('selected', 'true'); | ||
this.sel.scrollTop(0); | ||||
MinRK
|
r15322 | }; | ||
Matthias BUSSONNIER
|
r7131 | |||
Matthias BUSSONNIER
|
r7191 | Completer.prototype.close = function () { | ||
Matthias BUSSONNIER
|
r7190 | this.done = true; | ||
Jonathan Frederic
|
r15779 | $('#complete').remove(); | ||
Jonathan Frederic
|
r15962 | this.editor.off('keydown', this._handle_keydown); | ||
this.editor.off('keypress', this._handle_keypress); | ||||
Jonathan Frederic
|
r15952 | this.visible = false; | ||
MinRK
|
r15322 | }; | ||
Matthias BUSSONNIER
|
r7131 | |||
Matthias BUSSONNIER
|
r7191 | Completer.prototype.pick = function () { | ||
Matthias BUSSONNIER
|
r7131 | this.insert(this.raw_result[this.sel[0].selectedIndex]); | ||
this.close(); | ||||
MinRK
|
r15322 | }; | ||
Matthias BUSSONNIER
|
r7142 | |||
Matthias BUSSONNIER
|
r7191 | Completer.prototype.keydown = function (event) { | ||
Matthias BUSSONNIER
|
r7190 | var code = event.keyCode; | ||
var that = this; | ||||
Juergen Hasch
|
r10239 | |||
Matthias BUSSONNIER
|
r7190 | // Enter | ||
Brian E. Granger
|
r15619 | if (code == keycodes.enter) { | ||
Jonathan Frederic
|
r15956 | CodeMirror.e_stop(event); | ||
Matthias BUSSONNIER
|
r7190 | this.pick(); | ||
// Escape or backspace | ||||
Jonathan Frederic
|
r15953 | } else if (code == keycodes.esc || code == keycodes.backspace) { | ||
Jonathan Frederic
|
r15956 | CodeMirror.e_stop(event); | ||
Matthias BUSSONNIER
|
r7190 | this.close(); | ||
Brian E. Granger
|
r15619 | } else if (code == keycodes.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 | //reinvoke self | ||
Matthias BUSSONNIER
|
r7191 | setTimeout(function () { | ||
Matthias BUSSONNIER
|
r7192 | that.carry_on_completion(); | ||
Matthias BUSSONNIER
|
r7190 | }, 50); | ||
Brian E. Granger
|
r15619 | } else if (code == keycodes.up || code == keycodes.down) { | ||
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 | ||||
Jonathan Frederic
|
r15956 | CodeMirror.e_stop(event); | ||
Jonathan Frederic
|
r15952 | |||
Jonathan Frederic
|
r15953 | var options = this.sel.find('option'); | ||
var index = this.sel[0].selectedIndex; | ||||
if (code == keycodes.up) { | ||||
index--; | ||||
} | ||||
if (code == keycodes.down) { | ||||
index++; | ||||
} | ||||
index = Math.min(Math.max(index, 0), options.length-1); | ||||
this.sel[0].selectedIndex = index; | ||||
watercrossing
|
r16736 | } else if (code == keycodes.pageup || code == keycodes.pagedown) { | ||
CodeMirror.e_stop(event); | ||||
var options = this.sel.find('option'); | ||||
var index = this.sel[0].selectedIndex; | ||||
if (code == keycodes.pageup) { | ||||
index -= 10; // As 10 is the hard coded size of the drop down menu | ||||
} else { | ||||
index += 10; | ||||
} | ||||
index = Math.min(Math.max(index, 0), options.length-1); | ||||
this.sel[0].selectedIndex = index; | ||||
Jonathan Frederic
|
r15956 | } else if (code == keycodes.left || code == keycodes.right) { | ||
this.close(); | ||||
Jonathan Frederic
|
r15953 | } | ||
MinRK
|
r15322 | }; | ||
Matthias BUSSONNIER
|
r7131 | |||
Jonathan Frederic
|
r15956 | Completer.prototype.keypress = function (event) { | ||
// FIXME: This is a band-aid. | ||||
// on keypress, trigger insertion of a single character. | ||||
// This simulates the old behavior of completion as you type, | ||||
// before events were disconnected and CodeMirror stopped | ||||
// receiving events while the completer is focused. | ||||
var that = this; | ||||
var code = event.keyCode; | ||||
// don't handle keypress if it's not a character (arrows on FF) | ||||
// or ENTER/TAB | ||||
if (event.charCode === 0 || | ||||
code == keycodes.tab || | ||||
code == keycodes.enter | ||||
) return; | ||||
this.close(); | ||||
this.editor.focus(); | ||||
setTimeout(function () { | ||||
that.carry_on_completion(); | ||||
}, 50); | ||||
}; | ||||
Jonathan Frederic
|
r17198 | |||
// For backwards compatability. | ||||
Matthias BUSSONNIER
|
r7131 | IPython.Completer = Completer; | ||
Jonathan Frederic
|
r17201 | return {'Completer': Completer}; | ||
Jonathan Frederic
|
r17198 | }); | ||