##// END OF EJS Templates
Set tabindex -1, use codemirror's events
Jonathan Frederic -
Show More
@@ -1,402 +1,373 b''
1 // function completer.
1 // function completer.
2 //
2 //
3 // completer should be a class that takes an cell instance
3 // completer should be a class that takes an cell instance
4 var IPython = (function (IPython) {
4 var IPython = (function (IPython) {
5 // that will prevent us from misspelling
5 // that will prevent us from misspelling
6 "use strict";
6 "use strict";
7
7
8 // easier key mapping
8 // easier key mapping
9 var keycodes = IPython.keyboard.keycodes;
9 var keycodes = IPython.keyboard.keycodes;
10
10
11 function prepend_n_prc(str, n) {
11 function prepend_n_prc(str, n) {
12 for( var i =0 ; i< n ; i++){
12 for( var i =0 ; i< n ; i++){
13 str = '%'+str ;
13 str = '%'+str ;
14 }
14 }
15 return str;
15 return str;
16 }
16 }
17
17
18 function _existing_completion(item, completion_array){
18 function _existing_completion(item, completion_array){
19 for( var c in completion_array ) {
19 for( var c in completion_array ) {
20 if(completion_array[c].trim().substr(-item.length) == item)
20 if(completion_array[c].trim().substr(-item.length) == item)
21 { return true; }
21 { return true; }
22 }
22 }
23 return false;
23 return false;
24 }
24 }
25
25
26 // what is the common start of all completions
26 // what is the common start of all completions
27 function shared_start(B, drop_prct) {
27 function shared_start(B, drop_prct) {
28 if (B.length == 1) {
28 if (B.length == 1) {
29 return B[0];
29 return B[0];
30 }
30 }
31 var A = [];
31 var A = [];
32 var common;
32 var common;
33 var min_lead_prct = 10;
33 var min_lead_prct = 10;
34 for (var i = 0; i < B.length; i++) {
34 for (var i = 0; i < B.length; i++) {
35 var str = B[i].str;
35 var str = B[i].str;
36 var localmin = 0;
36 var localmin = 0;
37 if(drop_prct === true){
37 if(drop_prct === true){
38 while ( str.substr(0, 1) == '%') {
38 while ( str.substr(0, 1) == '%') {
39 localmin = localmin+1;
39 localmin = localmin+1;
40 str = str.substring(1);
40 str = str.substring(1);
41 }
41 }
42 }
42 }
43 min_lead_prct = Math.min(min_lead_prct, localmin);
43 min_lead_prct = Math.min(min_lead_prct, localmin);
44 A.push(str);
44 A.push(str);
45 }
45 }
46
46
47 if (A.length > 1) {
47 if (A.length > 1) {
48 var tem1, tem2, s;
48 var tem1, tem2, s;
49 A = A.slice(0).sort();
49 A = A.slice(0).sort();
50 tem1 = A[0];
50 tem1 = A[0];
51 s = tem1.length;
51 s = tem1.length;
52 tem2 = A.pop();
52 tem2 = A.pop();
53 while (s && tem2.indexOf(tem1) == -1) {
53 while (s && tem2.indexOf(tem1) == -1) {
54 tem1 = tem1.substring(0, --s);
54 tem1 = tem1.substring(0, --s);
55 }
55 }
56 if (tem1 === "" || tem2.indexOf(tem1) !== 0) {
56 if (tem1 === "" || tem2.indexOf(tem1) !== 0) {
57 return {
57 return {
58 str:prepend_n_prc('', min_lead_prct),
58 str:prepend_n_prc('', min_lead_prct),
59 type: "computed",
59 type: "computed",
60 from: B[0].from,
60 from: B[0].from,
61 to: B[0].to
61 to: B[0].to
62 };
62 };
63 }
63 }
64 return {
64 return {
65 str: prepend_n_prc(tem1, min_lead_prct),
65 str: prepend_n_prc(tem1, min_lead_prct),
66 type: "computed",
66 type: "computed",
67 from: B[0].from,
67 from: B[0].from,
68 to: B[0].to
68 to: B[0].to
69 };
69 };
70 }
70 }
71 return null;
71 return null;
72 }
72 }
73
73
74
74
75 var Completer = function (cell) {
75 var Completer = function (cell) {
76 this.cell = cell;
76 this.cell = cell;
77 this.editor = cell.code_mirror;
77 this.editor = cell.code_mirror;
78 var that = this;
78 var that = this;
79 $([IPython.events]).on('status_busy.Kernel', function () {
79 $([IPython.events]).on('status_busy.Kernel', function () {
80 that.skip_kernel_completion = true;
80 that.skip_kernel_completion = true;
81 });
81 });
82 $([IPython.events]).on('status_idle.Kernel', function () {
82 $([IPython.events]).on('status_idle.Kernel', function () {
83 that.skip_kernel_completion = false;
83 that.skip_kernel_completion = false;
84 });
84 });
85 };
85 };
86
86
87 Completer.prototype.startCompletion = function () {
87 Completer.prototype.startCompletion = function () {
88 // call for a 'first' completion, that will set the editor and do some
88 // call for a 'first' completion, that will set the editor and do some
89 // special behaviour like autopicking if only one completion availlable
89 // special behaviour like autopicking if only one completion availlable
90 //
90 //
91 if (this.editor.somethingSelected()) return;
91 if (this.editor.somethingSelected()) return;
92 this.done = false;
92 this.done = false;
93 // use to get focus back on opera
93 // use to get focus back on opera
94 this.carry_on_completion(true);
94 this.carry_on_completion(true);
95 };
95 };
96
96
97
97
98 // easy access for julia to monkeypatch
98 // easy access for julia to monkeypatch
99 //
99 //
100 Completer.reinvoke_re = /[%0-9a-z._/\\:~-]/i;
100 Completer.reinvoke_re = /[%0-9a-z._/\\:~-]/i;
101
101
102 Completer.prototype.reinvoke= function(pre_cursor, block, cursor){
102 Completer.prototype.reinvoke= function(pre_cursor, block, cursor){
103 return Completer.reinvoke_re.test(pre_cursor);
103 return Completer.reinvoke_re.test(pre_cursor);
104 };
104 };
105
105
106 /**
106 /**
107 *
107 *
108 * pass true as parameter if this is the first invocation of the completer
108 * pass true as parameter if this is the first invocation of the completer
109 * this will prevent the completer to dissmiss itself if it is not on a
109 * this will prevent the completer to dissmiss itself if it is not on a
110 * word boundary like pressing tab after a space, and make it autopick the
110 * word boundary like pressing tab after a space, and make it autopick the
111 * only choice if there is only one which prevent from popping the UI. as
111 * only choice if there is only one which prevent from popping the UI. as
112 * well as fast-forwarding the typing if all completion have a common
112 * well as fast-forwarding the typing if all completion have a common
113 * shared start
113 * shared start
114 **/
114 **/
115 Completer.prototype.carry_on_completion = function (first_invocation) {
115 Completer.prototype.carry_on_completion = function (first_invocation) {
116 // Pass true as parameter if you want the completer to autopick when
116 // Pass true as parameter if you want the completer to autopick when
117 // only one completion. This function is automatically reinvoked at
117 // only one completion. This function is automatically reinvoked at
118 // each keystroke with first_invocation = false
118 // each keystroke with first_invocation = false
119 var cur = this.editor.getCursor();
119 var cur = this.editor.getCursor();
120 var line = this.editor.getLine(cur.line);
120 var line = this.editor.getLine(cur.line);
121 var pre_cursor = this.editor.getRange({
121 var pre_cursor = this.editor.getRange({
122 line: cur.line,
122 line: cur.line,
123 ch: cur.ch - 1
123 ch: cur.ch - 1
124 }, cur);
124 }, cur);
125
125
126 // we need to check that we are still on a word boundary
126 // we need to check that we are still on a word boundary
127 // because while typing the completer is still reinvoking itself
127 // because while typing the completer is still reinvoking itself
128 // so dismiss if we are on a "bad" caracter
128 // so dismiss if we are on a "bad" caracter
129 if (!this.reinvoke(pre_cursor) && !first_invocation) {
129 if (!this.reinvoke(pre_cursor) && !first_invocation) {
130 this.close();
130 this.close();
131 return;
131 return;
132 }
132 }
133
133
134 this.autopick = false;
134 this.autopick = false;
135 if (first_invocation) {
135 if (first_invocation) {
136 this.autopick = true;
136 this.autopick = true;
137 }
137 }
138
138
139 // We want a single cursor position.
139 // We want a single cursor position.
140 if (this.editor.somethingSelected()) {
140 if (this.editor.somethingSelected()) {
141 return;
141 return;
142 }
142 }
143
143
144 // one kernel completion came back, finish_completing will be called with the results
144 // one kernel completion came back, finish_completing will be called with the results
145 // we fork here and directly call finish completing if kernel is busy
145 // we fork here and directly call finish completing if kernel is busy
146 if (this.skip_kernel_completion) {
146 if (this.skip_kernel_completion) {
147 this.finish_completing({
147 this.finish_completing({
148 'matches': [],
148 'matches': [],
149 matched_text: ""
149 matched_text: ""
150 });
150 });
151 } else {
151 } else {
152 this.cell.kernel.complete(line, cur.ch, $.proxy(this.finish_completing, this));
152 this.cell.kernel.complete(line, cur.ch, $.proxy(this.finish_completing, this));
153 }
153 }
154 };
154 };
155
155
156 Completer.prototype.finish_completing = function (msg) {
156 Completer.prototype.finish_completing = function (msg) {
157 // let's build a function that wrap all that stuff into what is needed
157 // let's build a function that wrap all that stuff into what is needed
158 // for the new completer:
158 // for the new completer:
159 var content = msg.content;
159 var content = msg.content;
160 var matched_text = content.matched_text;
160 var matched_text = content.matched_text;
161 var matches = content.matches;
161 var matches = content.matches;
162
162
163 var cur = this.editor.getCursor();
163 var cur = this.editor.getCursor();
164 var results = CodeMirror.contextHint(this.editor);
164 var results = CodeMirror.contextHint(this.editor);
165 var filtered_results = [];
165 var filtered_results = [];
166 //remove results from context completion
166 //remove results from context completion
167 //that are already in kernel completion
167 //that are already in kernel completion
168 for (var elm in results) {
168 for (var elm in results) {
169 if (!_existing_completion(results[elm].str, matches)) {
169 if (!_existing_completion(results[elm].str, matches)) {
170 filtered_results.push(results[elm]);
170 filtered_results.push(results[elm]);
171 }
171 }
172 }
172 }
173
173
174 // append the introspection result, in order, at at the beginning of
174 // append the introspection result, in order, at at the beginning of
175 // the table and compute the replacement range from current cursor
175 // the table and compute the replacement range from current cursor
176 // positon and matched_text length.
176 // positon and matched_text length.
177 for (var i = matches.length - 1; i >= 0; --i) {
177 for (var i = matches.length - 1; i >= 0; --i) {
178 filtered_results.unshift({
178 filtered_results.unshift({
179 str: matches[i],
179 str: matches[i],
180 type: "introspection",
180 type: "introspection",
181 from: {
181 from: {
182 line: cur.line,
182 line: cur.line,
183 ch: cur.ch - matched_text.length
183 ch: cur.ch - matched_text.length
184 },
184 },
185 to: {
185 to: {
186 line: cur.line,
186 line: cur.line,
187 ch: cur.ch
187 ch: cur.ch
188 }
188 }
189 });
189 });
190 }
190 }
191
191
192 // one the 2 sources results have been merge, deal with it
192 // one the 2 sources results have been merge, deal with it
193 this.raw_result = filtered_results;
193 this.raw_result = filtered_results;
194
194
195 // if empty result return
195 // if empty result return
196 if (!this.raw_result || !this.raw_result.length) return;
196 if (!this.raw_result || !this.raw_result.length) return;
197
197
198 // When there is only one completion, use it directly.
198 // When there is only one completion, use it directly.
199 if (this.autopick && this.raw_result.length == 1) {
199 if (this.autopick && this.raw_result.length == 1) {
200 this.insert(this.raw_result[0]);
200 this.insert(this.raw_result[0]);
201 return;
201 return;
202 }
202 }
203
203
204 if (this.raw_result.length == 1) {
204 if (this.raw_result.length == 1) {
205 // test if first and only completion totally matches
205 // test if first and only completion totally matches
206 // what is typed, in this case dismiss
206 // what is typed, in this case dismiss
207 var str = this.raw_result[0].str;
207 var str = this.raw_result[0].str;
208 var pre_cursor = this.editor.getRange({
208 var pre_cursor = this.editor.getRange({
209 line: cur.line,
209 line: cur.line,
210 ch: cur.ch - str.length
210 ch: cur.ch - str.length
211 }, cur);
211 }, cur);
212 if (pre_cursor == str) {
212 if (pre_cursor == str) {
213 this.close();
213 this.close();
214 return;
214 return;
215 }
215 }
216 }
216 }
217
217
218 this.complete = $('<div/>').addClass('completions');
218 this.complete = $('<div/>').addClass('completions');
219 this.complete.attr('id', 'complete');
219 this.complete.attr('id', 'complete');
220
220
221 // Currently webkit doesn't use the size attr correctly. See:
221 // Currently webkit doesn't use the size attr correctly. See:
222 // https://code.google.com/p/chromium/issues/detail?id=4579
222 // https://code.google.com/p/chromium/issues/detail?id=4579
223 this.sel = $('<select style="width: auto"/>')
223 this.sel = $('<select style="width: auto"/>')
224 .attr('tabindex', -1)
224 .attr('multiple', 'true')
225 .attr('multiple', 'true')
225 .attr('size', Math.min(10, this.raw_result.length));
226 .attr('size', Math.min(10, this.raw_result.length));
226 this.complete.append(this.sel);
227 this.complete.append(this.sel);
227 $('body').append(this.complete);
228 $('body').append(this.complete);
228
229
229 // After everything is on the page, compute the postion.
230 // After everything is on the page, compute the postion.
230 // We put it above the code if it is too close to the bottom of the page.
231 // We put it above the code if it is too close to the bottom of the page.
231 cur.ch = cur.ch-matched_text.length;
232 cur.ch = cur.ch-matched_text.length;
232 var pos = this.editor.cursorCoords(cur);
233 var pos = this.editor.cursorCoords(cur);
233 var left = pos.left-3;
234 var left = pos.left-3;
234 var top;
235 var top;
235 var cheight = this.complete.height();
236 var cheight = this.complete.height();
236 var wheight = $(window).height();
237 var wheight = $(window).height();
237 if (pos.bottom+cheight+5 > wheight) {
238 if (pos.bottom+cheight+5 > wheight) {
238 top = pos.top-cheight-4;
239 top = pos.top-cheight-4;
239 } else {
240 } else {
240 top = pos.bottom+1;
241 top = pos.bottom+1;
241 }
242 }
242 this.complete.css('left', left + 'px');
243 this.complete.css('left', left + 'px');
243 this.complete.css('top', top + 'px');
244 this.complete.css('top', top + 'px');
244
245
245
246
246 //build the container
247 //build the container
247 var that = this;
248 var that = this;
248 this.sel.dblclick(function () {
249 this.sel.dblclick(function () {
249 that.pick();
250 that.pick();
250 });
251 });
251 this.sel.blur($.proxy(this.close, this));
252 this.editor.on('keydown', function (event) {
252 this.sel.keydown(function (event) {
253 // Ignore tab key since it causes the completer to reshow on some
254 // machines and not with others. This strange behavior is due to
255 // the fact that we are tricking the notebook into thinking that it
256 // is in edit mode when it's really not.
257 if (IPython.keyboard.event_to_shortcut(event)=='tab') {
258 event.stopPropagation();
259 event.preventDefault();
260 } else {
261 that.keydown(event);
253 that.keydown(event);
262 }
263 });
254 });
264 this.sel.keypress(function (event) {
255 this.editor.on('keypress', function (event) {
265 that.keypress(event);
256 that.keypress(event);
266 });
257 });
267
258
268 this.build_gui_list(this.raw_result);
259 this.build_gui_list(this.raw_result);
269
260
270 this.sel.focus();
261 this.sel.focus();
271 // Since the completer can and will gain focus and it isn't a component
272 // of the codemirror instance, we need to manually "fake" codemirror as
273 // still being focused. This is accomplished by calling edit_mode on
274 // the cell when the completer gains focus, and command mode when the
275 // completer loses focus. If the completer was an actual, true extension
276 // of codemirror, we wouldn't have to play this game since codemirror
277 // wouldn't blur when the completer was shown.
278 this.cell.edit_mode();
279 $([IPython.events]).trigger('edit_mode.Notebook');
280 IPython.keyboard_manager.disable();
281 // Opera sometimes ignores focusing a freshly created node
262 // Opera sometimes ignores focusing a freshly created node
282 if (window.opera) setTimeout(function () {
263 if (window.opera) setTimeout(function () {
283 if (!this.done) this.sel.focus();
264 if (!this.done) this.sel.focus();
284 }, 100);
265 }, 100);
285 return true;
266 return true;
286 };
267 };
287
268
288 Completer.prototype.insert = function (completion) {
269 Completer.prototype.insert = function (completion) {
289 this.editor.replaceRange(completion.str, completion.from, completion.to);
270 this.editor.replaceRange(completion.str, completion.from, completion.to);
290 };
271 };
291
272
292 Completer.prototype.build_gui_list = function (completions) {
273 Completer.prototype.build_gui_list = function (completions) {
293 for (var i = 0; i < completions.length; ++i) {
274 for (var i = 0; i < completions.length; ++i) {
294 var opt = $('<option/>').text(completions[i].str).addClass(completions[i].type);
275 var opt = $('<option/>').text(completions[i].str).addClass(completions[i].type);
295 this.sel.append(opt);
276 this.sel.append(opt);
296 }
277 }
297 this.sel.children().first().attr('selected', 'true');
278 this.sel.children().first().attr('selected', 'true');
298 this.sel.scrollTop(0);
279 this.sel.scrollTop(0);
299 };
280 };
300
281
301 Completer.prototype.close = function () {
282 Completer.prototype.close = function () {
302 this.done = true;
283 this.done = true;
303 $('#complete').remove();
284 $('#complete').remove();
304 // Since the completer can and will gain focus and it isn't a component
305 // of the codemirror instance, we need to manually "fake" codemirror as
306 // still being focused. This is accomplished by calling edit_mode on
307 // the cell when the completer gains focus, and command mode when the
308 // completer loses focus. If the completer was an actual, true extension
309 // of codemirror, we wouldn't have to play this game since codemirror
310 // wouldn't blur when the completer was shown.
311 this.cell.command_mode();
312 $([IPython.events]).trigger('command_mode.Notebook');
313 IPython.keyboard_manager.enable();
314 };
285 };
315
286
316 Completer.prototype.pick = function () {
287 Completer.prototype.pick = function () {
317 this.insert(this.raw_result[this.sel[0].selectedIndex]);
288 this.insert(this.raw_result[this.sel[0].selectedIndex]);
318 this.close();
289 this.close();
319 var that = this;
290 var that = this;
320 setTimeout(function () {
291 setTimeout(function () {
321 that.editor.focus();
292 that.editor.focus();
322 }, 50);
293 }, 50);
323 };
294 };
324
295
325 Completer.prototype.keydown = function (event) {
296 Completer.prototype.keydown = function (event) {
326 var code = event.keyCode;
297 var code = event.keyCode;
327 var that = this;
298 var that = this;
328
299
329 // Enter
300 // Enter
330 if (code == keycodes.enter) {
301 if (code == keycodes.enter) {
331 CodeMirror.e_stop(event);
302 CodeMirror.e_stop(event);
332 this.pick();
303 this.pick();
333 }
304 }
334 // Escape or backspace
305 // Escape or backspace
335 else if (code == keycodes.esc) {
306 else if (code == keycodes.esc) {
336 CodeMirror.e_stop(event);
307 CodeMirror.e_stop(event);
337 this.close();
308 this.close();
338 this.editor.focus();
309 this.editor.focus();
339
310
340 } else if (code == keycodes.backspace) {
311 } else if (code == keycodes.backspace) {
341 this.close();
312 this.close();
342 this.editor.focus();
313 this.editor.focus();
343 } else if (code == keycodes.tab) {
314 } else if (code == keycodes.tab) {
344 //all the fastforwarding operation,
315 //all the fastforwarding operation,
345 //Check that shared start is not null which can append with prefixed completion
316 //Check that shared start is not null which can append with prefixed completion
346 // like %pylab , pylab have no shred start, and ff will result in py<tab><tab>
317 // like %pylab , pylab have no shred start, and ff will result in py<tab><tab>
347 // to erase py
318 // to erase py
348 var sh = shared_start(this.raw_result, true);
319 var sh = shared_start(this.raw_result, true);
349 if (sh) {
320 if (sh) {
350 this.insert(sh);
321 this.insert(sh);
351 }
322 }
352 this.close();
323 this.close();
353 CodeMirror.e_stop(event);
324 CodeMirror.e_stop(event);
354 this.editor.focus();
325 this.editor.focus();
355 //reinvoke self
326 //reinvoke self
356 setTimeout(function () {
327 setTimeout(function () {
357 that.carry_on_completion();
328 that.carry_on_completion();
358 }, 50);
329 }, 50);
359 } else if (code == keycodes.up || code == keycodes.down) {
330 } else if (code == keycodes.up || code == keycodes.down) {
360 // need to do that to be able to move the arrow
331 // need to do that to be able to move the arrow
361 // when on the first or last line ofo a code cell
332 // when on the first or last line ofo a code cell
362 event.stopPropagation();
333 event.stopPropagation();
363 }
334 }
364 };
335 };
365
336
366 Completer.prototype.keypress = function (event) {
337 Completer.prototype.keypress = function (event) {
367 // FIXME: This is a band-aid.
338 // FIXME: This is a band-aid.
368 // on keypress, trigger insertion of a single character.
339 // on keypress, trigger insertion of a single character.
369 // This simulates the old behavior of completion as you type,
340 // This simulates the old behavior of completion as you type,
370 // before events were disconnected and CodeMirror stopped
341 // before events were disconnected and CodeMirror stopped
371 // receiving events while the completer is focused.
342 // receiving events while the completer is focused.
372
343
373 var that = this;
344 var that = this;
374 var code = event.keyCode;
345 var code = event.keyCode;
375
346
376 // don't handle keypress if it's not a character (arrows on FF)
347 // don't handle keypress if it's not a character (arrows on FF)
377 // or ENTER/TAB
348 // or ENTER/TAB
378 if (event.charCode === 0 ||
349 if (event.charCode === 0 ||
379 code == keycodes.enter ||
350 code == keycodes.enter ||
380 code == keycodes.tab
351 code == keycodes.tab
381 ) return;
352 ) return;
382
353
383 var cur = this.editor.getCursor();
354 var cur = this.editor.getCursor();
384 var completion = {
355 var completion = {
385 str: String.fromCharCode(event.which),
356 str: String.fromCharCode(event.which),
386 type: "introspection",
357 type: "introspection",
387 from: cur,
358 from: cur,
388 to: cur,
359 to: cur,
389 };
360 };
390 this.insert(completion);
361 this.insert(completion);
391
362
392 this.close();
363 this.close();
393 this.editor.focus();
364 this.editor.focus();
394 setTimeout(function () {
365 setTimeout(function () {
395 that.carry_on_completion();
366 that.carry_on_completion();
396 }, 50);
367 }, 50);
397 };
368 };
398
369
399 IPython.Completer = Completer;
370 IPython.Completer = Completer;
400
371
401 return IPython;
372 return IPython;
402 }(IPython));
373 }(IPython));
General Comments 0
You need to be logged in to leave comments. Login now