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