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