web-console 2.3.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.markdown +133 -1
  3. data/MIT-LICENSE +1 -1
  4. data/README.markdown +48 -86
  5. data/Rakefile +14 -12
  6. data/lib/web-console.rb +3 -1
  7. data/lib/web_console/context.rb +45 -0
  8. data/lib/web_console/errors.rb +2 -0
  9. data/lib/web_console/evaluator.rb +14 -5
  10. data/lib/web_console/exception_mapper.rb +56 -0
  11. data/lib/web_console/extensions.rb +28 -17
  12. data/lib/web_console/injector.rb +32 -0
  13. data/lib/web_console/interceptor.rb +18 -0
  14. data/lib/web_console/locales/en.yml +1 -1
  15. data/lib/web_console/middleware.rb +31 -31
  16. data/lib/web_console/permissions.rb +42 -0
  17. data/lib/web_console/railtie.rb +38 -24
  18. data/lib/web_console/request.rb +8 -20
  19. data/lib/web_console/session.rb +32 -18
  20. data/lib/web_console/source_location.rb +15 -0
  21. data/lib/web_console/tasks/extensions.rake +15 -13
  22. data/lib/web_console/tasks/templates.rake +56 -0
  23. data/lib/web_console/template.rb +4 -3
  24. data/lib/web_console/templates/console.js.erb +497 -37
  25. data/lib/web_console/templates/error_page.js.erb +7 -8
  26. data/lib/web_console/templates/index.html.erb +4 -0
  27. data/lib/web_console/templates/layouts/inlined_string.erb +1 -1
  28. data/lib/web_console/templates/layouts/javascript.erb +1 -1
  29. data/lib/web_console/templates/regular_page.js.erb +24 -0
  30. data/lib/web_console/templates/style.css.erb +182 -27
  31. data/lib/web_console/testing/erb_precompiler.rb +5 -3
  32. data/lib/web_console/testing/fake_middleware.rb +7 -10
  33. data/lib/web_console/testing/helper.rb +3 -1
  34. data/lib/web_console/version.rb +3 -1
  35. data/lib/web_console/view.rb +24 -3
  36. data/lib/web_console/whiny_request.rb +8 -6
  37. data/lib/web_console.rb +28 -20
  38. metadata +28 -63
  39. data/lib/web_console/helper.rb +0 -22
  40. data/lib/web_console/integration/cruby.rb +0 -40
  41. data/lib/web_console/integration/jruby.rb +0 -111
  42. data/lib/web_console/integration/rubinius.rb +0 -67
  43. data/lib/web_console/integration.rb +0 -8
  44. data/lib/web_console/response.rb +0 -23
  45. data/lib/web_console/tasks/test_templates.rake +0 -50
  46. data/lib/web_console/whitelist.rb +0 -42
@@ -44,13 +44,221 @@ function CommandStorage() {
44
44
  }
45
45
  }
46
46
 
47
+ function Autocomplete(_words, prefix) {
48
+ this.words = prepareWords(_words);
49
+ this.current = -1;
50
+ this.left = 0; // [left, right)
51
+ this.right = this.words.length;
52
+ this.confirmed = false;
53
+
54
+ function createSpan(label, className) {
55
+ var el = document.createElement('span');
56
+ addClass(el, className);
57
+ el.innerText = label;
58
+ return el;
59
+ }
60
+
61
+ function prepareWords(words) {
62
+ // convert into an object with priority and element
63
+ var res = new Array(words.length);
64
+ for (var i = 0, ind = 0; i < words.length; ++i) {
65
+ res[i] = new Array(words[i].length);
66
+ for (var j = 0; j < words[i].length; ++j) {
67
+ res[i][j] = {
68
+ word: words[i][j],
69
+ priority: i,
70
+ element: createSpan(words[i][j], 'trimmed keyword')
71
+ };
72
+ }
73
+ }
74
+ // flatten and sort by alphabetical order to refine incrementally
75
+ res = flatten(res);
76
+ res.sort(function(a, b) { return a.word == b.word ? 0 : (a.word < b.word ? -1 : 1); });
77
+ for (var i = 0; i < res.length; ++i) res[i].element.dataset.index = i;
78
+ return res;
79
+ }
80
+
81
+ this.view = document.createElement('pre');
82
+ addClass(this.view, 'auto-complete console-message');
83
+ this.view.appendChild(this.prefix = createSpan('...', 'trimmed keyword'));
84
+ this.view.appendChild(this.stage = document.createElement('span'));
85
+ this.elements = this.stage.children;
86
+ this.view.appendChild(this.suffix = createSpan('...', 'trimmed keyword'));
87
+
88
+ this.refine(prefix || '');
89
+ }
90
+
91
+ Autocomplete.prototype.getSelectedWord = function() {
92
+ return this.lastSelected && this.lastSelected.innerText;
93
+ };
94
+
95
+ Autocomplete.prototype.onFinished = function(callback) {
96
+ this.onFinishedCallback = callback;
97
+ if (this.confirmed) callback(this.confirmed);
98
+ };
99
+
100
+ Autocomplete.prototype.onKeyDown = function(ev) {
101
+ var self = this;
102
+ if (!this.elements.length) return;
103
+
104
+ function move(nextCurrent) {
105
+ if (self.lastSelected) removeClass(self.lastSelected, 'selected');
106
+ addClass(self.lastSelected = self.elements[nextCurrent], 'selected');
107
+ self.trim(self.current, true);
108
+ self.trim(nextCurrent, false);
109
+ self.current = nextCurrent;
110
+ }
111
+
112
+ switch (ev.keyCode) {
113
+ case 69:
114
+ if (ev.ctrlKey) {
115
+ move(this.current + 1 >= this.elements.length ? 0 : this.current + 1);
116
+ return true;
117
+ }
118
+ return false;
119
+ case 9: // Tab
120
+ if (ev.shiftKey) { // move back
121
+ move(this.current - 1 < 0 ? this.elements.length - 1 : this.current - 1);
122
+ } else { // move next
123
+ move(this.current + 1 >= this.elements.length ? 0 : this.current + 1);
124
+ }
125
+ return true;
126
+ case 13: // Enter
127
+ this.finish();
128
+ return true;
129
+ case 27: // Esc
130
+ this.cancel();
131
+ return true;
132
+ case 37: case 38: case 39: case 40: // disable using arrow keys on completing
133
+ return true;
134
+ }
135
+
136
+ return false;
137
+ };
138
+
139
+ Autocomplete.prototype.trim = function(from, needToTrim) {
140
+ var self = this;
141
+ var num = 5;
142
+
143
+ if (this.elements.length > num) {
144
+ (0 < from ? removeClass : addClass)(this.prefix, 'trimmed');
145
+ (from + num < this.elements.length ? removeClass : addClass)(this.suffix, 'trimmed');
146
+ } else {
147
+ addClass(this.prefix, 'trimmed');
148
+ addClass(this.suffix, 'trimmed');
149
+ }
150
+
151
+ function iterate(x) {
152
+ for (var i = 0; i < num; ++i, ++x) if (0 <= x && x < self.elements.length) {
153
+ toggleClass(self.elements[x], 'trimmed');
154
+ }
155
+ }
156
+
157
+ var toggleClass = needToTrim ? addClass : removeClass;
158
+ if (from < 0) {
159
+ iterate(0);
160
+ } else if (from + num - 1 >= this.elements.length) {
161
+ iterate(this.elements.length - num);
162
+ } else {
163
+ iterate(from);
164
+ }
165
+ };
166
+
167
+ Autocomplete.prototype.refine = function(prefix) {
168
+ if (this.confirmed) return;
169
+ var inc = !this.prev || (prefix.length >= this.prev.length);
170
+ this.prev = prefix;
171
+ var self = this;
172
+
173
+ function remove(parent, child) {
174
+ if (parent == child.parentNode) parent.removeChild(child);
175
+ }
176
+
177
+ function toggle(el) {
178
+ return inc ? remove(self.stage, el) : self.stage.appendChild(el);
179
+ }
180
+
181
+ function startsWith(str, prefix) {
182
+ return !prefix || str.substr(0, prefix.length) === prefix;
183
+ }
184
+
185
+ function moveRight(l, r) {
186
+ while (l < r && inc !== startsWith(self.words[l].word, prefix)) toggle(self.words[l++].element);
187
+ return l;
188
+ }
189
+
190
+ function moveLeft(l, r) {
191
+ while (l < r - 1 && inc !== startsWith(self.words[r-1].word, prefix)) toggle(self.words[--r].element);
192
+ return r;
193
+ }
194
+
195
+ self.trim(self.current, true); // reset trimming
196
+
197
+ // Refine the range of words having same prefix
198
+ if (inc) {
199
+ self.left = moveRight(self.left, self.right);
200
+ self.right = moveLeft(self.left, self.right);
201
+ } else {
202
+ self.left = moveLeft(-1, self.left);
203
+ self.right = moveRight(self.right, self.words.length);
204
+ }
205
+
206
+ // Render elements with sorting by scope groups
207
+ var words = this.words.slice(this.left, this.right);
208
+ words.sort(function(a, b) { return a.priority == b.priority ? (a.word < b.word ? -1 : 1) : (a.priority < b.priority ? -1 : 1); });
209
+ removeAllChildren(this.elements);
210
+ for (var i = 0; i < words.length; ++i) {
211
+ this.stage.appendChild(words[i].element);
212
+ }
213
+
214
+ // Keep a previous selected element if the refined range includes the element
215
+ if (this.lastSelected && this.left <= this.lastSelected.dataset.index && this.lastSelected.dataset.index < this.right) {
216
+ this.current = Array.prototype.indexOf.call(this.elements, this.lastSelected);
217
+ this.trim(this.current, false);
218
+ } else {
219
+ if (this.lastSelected) removeClass(this.lastSelected, 'selected');
220
+ this.lastSelected = null;
221
+ this.current = -1;
222
+ this.trim(0, false);
223
+ }
224
+
225
+ if (self.left + 1 == self.right) {
226
+ self.current = 0;
227
+ self.finish();
228
+ } else if (self.left == self.right) {
229
+ self.cancel();
230
+ }
231
+ };
232
+
233
+ Autocomplete.prototype.finish = function() {
234
+ if (0 <= this.current && this.current < this.elements.length) {
235
+ this.confirmed = this.elements[this.current].innerText;
236
+ if (this.onFinishedCallback) this.onFinishedCallback(this.confirmed);
237
+ this.removeView();
238
+ } else {
239
+ this.cancel();
240
+ }
241
+ };
242
+
243
+ Autocomplete.prototype.cancel = function() {
244
+ if (this.onFinishedCallback) this.onFinishedCallback();
245
+ this.removeView();
246
+ };
247
+
248
+ Autocomplete.prototype.removeView = function() {
249
+ if (this.view.parentNode) this.view.parentNode.removeChild(this.view);
250
+ removeAllChildren(this.view);
251
+ }
252
+
47
253
  // HTML strings for dynamic elements.
48
- var consoleInnerHtml = <%= render_inlined_string '_inner_console_markup.html' %>;
49
- var promptBoxHtml = <%= render_inlined_string '_prompt_box_markup.html' %>;
254
+ var consoleInnerHtml = <%= render_inlined_string '_inner_console_markup' %>;
255
+ var promptBoxHtml = <%= render_inlined_string '_prompt_box_markup' %>;
50
256
  // CSS
51
- var consoleStyleCss = <%= render_inlined_string 'style.css' %>;
257
+ var consoleStyleCss = <%= render_inlined_string 'style' %>;
52
258
  // Insert a style element with the unique ID
53
259
  var styleElementId = 'sr02459pvbvrmhco';
260
+ // Nonce to use for CSP
261
+ var styleElementNonce = '<%= @nonce %>';
54
262
 
55
263
  // REPLConsole Constructor
56
264
  function REPLConsole(config) {
@@ -62,6 +270,7 @@ function REPLConsole(config) {
62
270
  this.prompt = getConfig('promptLabel', ' >>');
63
271
  this.mountPoint = getConfig('mountPoint');
64
272
  this.sessionId = getConfig('sessionId');
273
+ this.autocomplete = false;
65
274
  }
66
275
 
67
276
  REPLConsole.prototype.getSessionUrl = function(path) {
@@ -73,6 +282,16 @@ REPLConsole.prototype.getSessionUrl = function(path) {
73
282
  return parts.join('/').replace(/([^:]\/)\/+/g, '$1');
74
283
  };
75
284
 
285
+ REPLConsole.prototype.contextRequest = function(keyword, callback) {
286
+ putRequest(this.getSessionUrl(), 'context=' + getContext(keyword), function(xhr) {
287
+ if (xhr.status == 200) {
288
+ callback(null, JSON.parse(xhr.responseText));
289
+ } else {
290
+ callback(xhr.statusText);
291
+ }
292
+ });
293
+ };
294
+
76
295
  REPLConsole.prototype.commandHandle = function(line, callback) {
77
296
  var self = this;
78
297
  var params = 'input=' + encodeURIComponent(line);
@@ -92,7 +311,7 @@ REPLConsole.prototype.commandHandle = function(line, callback) {
92
311
 
93
312
  function getErrorText(xhr) {
94
313
  if (!xhr.status) {
95
- return "<%= t 'errors.connection_refused' %>";
314
+ return "Connection Refused";
96
315
  } else {
97
316
  return xhr.status + ' ' + xhr.statusText;
98
317
  }
@@ -163,8 +382,13 @@ REPLConsole.prototype.install = function(container) {
163
382
  var clientHeightStart = consoleOuter.clientHeight;
164
383
 
165
384
  var doDrag = function(e) {
166
- container.style.height = (startHeight + startY - e.clientY) + 'px';
385
+ var height = startHeight + startY - e.clientY;
167
386
  consoleOuter.scrollTop = scrollTopStart + (clientHeightStart - consoleOuter.clientHeight);
387
+ if (height > document.documentElement.clientHeight) {
388
+ container.style.height = document.documentElement.clientHeight;
389
+ } else {
390
+ container.style.height = height + 'px';
391
+ }
168
392
  shiftConsoleActions();
169
393
  };
170
394
 
@@ -194,17 +418,26 @@ REPLConsole.prototype.install = function(container) {
194
418
  }
195
419
  }
196
420
 
421
+ var observer = new MutationObserver(function(mutationsList) {
422
+ for (let mutation of mutationsList) {
423
+ if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
424
+ shiftConsoleActions();
425
+ }
426
+ }
427
+ });
428
+
197
429
  // Initialize
198
430
  this.container = container;
199
431
  this.outer = consoleOuter;
200
432
  this.inner = findChild(this.outer, 'console-inner');
201
433
  this.clipboard = findChild(container, 'clipboard');
434
+ this.suggestWait = 1500;
202
435
  this.newPromptBox();
203
436
  this.insertCss();
204
437
 
205
438
  findChild(container, 'resizer').addEventListener('mousedown', resizeContainer);
206
439
  findChild(consoleActions, 'close-button').addEventListener('click', closeContainer);
207
- consoleOuter.addEventListener('DOMNodeInserted', shiftConsoleActions);
440
+ observer.observe(consoleOuter, { childList: true, subtree: true });
208
441
 
209
442
  REPLConsole.currentSession = this;
210
443
  };
@@ -218,6 +451,9 @@ REPLConsole.prototype.insertCss = function() {
218
451
  style.type = 'text/css';
219
452
  style.innerHTML = consoleStyleCss;
220
453
  style.id = styleElementId;
454
+ if (styleElementNonce.length > 0) {
455
+ style.nonce = styleElementNonce;
456
+ }
221
457
  document.getElementsByTagName('head')[0].appendChild(style);
222
458
  };
223
459
 
@@ -267,10 +503,63 @@ REPLConsole.prototype.removeCaretFromPrompt = function() {
267
503
  this.setInput(this._input, -1);
268
504
  };
269
505
 
506
+ REPLConsole.prototype.getSuggestion = function(keyword) {
507
+ var self = this;
508
+
509
+ function show(found) {
510
+ if (!found) return;
511
+ var hint = self.promptDisplay.childNodes[1];
512
+ hint.className = 'console-hint';
513
+ hint.dataset.keyword = found;
514
+ hint.innerText = found.substr(self.suggestKeyword.length);
515
+ // clear hinting information after timeout in a few time
516
+ if (self.suggestTimeout) clearTimeout(self.suggestTimeout);
517
+ self.suggestTimeout = setTimeout(function() { self.renderInput() }, self.suggestWait);
518
+ }
519
+
520
+ function find(context) {
521
+ var k = self.suggestKeyword;
522
+ for (var i = 0; i < context.length; ++i) if (context[i].substr(0, k.length) === k) {
523
+ if (context[i] === k) return;
524
+ return context[i];
525
+ }
526
+ }
527
+
528
+ function request(keyword, callback) {
529
+ self.contextRequest(keyword, function(err, res) {
530
+ if (err) throw new Error(err);
531
+ var c = flatten(res['context']);
532
+ c.sort();
533
+ callback(c);
534
+ });
535
+ }
536
+
537
+ self.suggestKeyword = keyword;
538
+ var input = getContext(keyword);
539
+ if (keyword.length - input.length < 3) return;
540
+
541
+ if (self.suggestInput !== input) {
542
+ self.suggestInput = input;
543
+ request(keyword, function(c) {
544
+ show(find(self.suggestContext = c));
545
+ });
546
+ } else if (self.suggestContext) {
547
+ show(find(self.suggestContext));
548
+ }
549
+ };
550
+
551
+ REPLConsole.prototype.getHintKeyword = function() {
552
+ var hint = this.promptDisplay.childNodes[1];
553
+ return hint.className === 'console-hint' && hint.dataset.keyword;
554
+ };
555
+
270
556
  REPLConsole.prototype.setInput = function(input, caretPos) {
557
+ if (input == null) return; // keep value if input is undefined
271
558
  this._caretPos = caretPos === undefined ? input.length : caretPos;
272
559
  this._input = input;
560
+ if (this.autocomplete) this.autocomplete.refine(this.getCurrentWord());
273
561
  this.renderInput();
562
+ if (!this.autocomplete && input.length == this._caretPos) this.getSuggestion(this.getCurrentWord());
274
563
  };
275
564
 
276
565
  /**
@@ -292,9 +581,8 @@ REPLConsole.prototype.renderInput = function() {
292
581
  // Clear the current input.
293
582
  removeAllChildren(this.promptDisplay);
294
583
 
295
- var promptCursor = document.createElement('span');
296
- promptCursor.className = "console-cursor";
297
584
  var before, current, after;
585
+ var center = document.createElement('span');
298
586
 
299
587
  if (this._caretPos < 0) {
300
588
  before = this._input;
@@ -310,9 +598,12 @@ REPLConsole.prototype.renderInput = function() {
310
598
  }
311
599
 
312
600
  this.promptDisplay.appendChild(document.createTextNode(before));
313
- promptCursor.appendChild(document.createTextNode(current));
314
- this.promptDisplay.appendChild(promptCursor);
601
+ this.promptDisplay.appendChild(center);
315
602
  this.promptDisplay.appendChild(document.createTextNode(after));
603
+
604
+ var hint = this.autocomplete && this.autocomplete.getSelectedWord();
605
+ addClass(center, hint ? 'console-hint' : 'console-cursor');
606
+ center.appendChild(document.createTextNode(hint ? hint.substr(this.getCurrentWord().length) : current));
316
607
  };
317
608
 
318
609
  REPLConsole.prototype.writeOutput = function(output) {
@@ -330,6 +621,12 @@ REPLConsole.prototype.writeError = function(output) {
330
621
  return consoleMessage;
331
622
  };
332
623
 
624
+ REPLConsole.prototype.writeNotification = function(output) {
625
+ var consoleMessage = this.writeOutput(output);
626
+ addClass(consoleMessage, "notification-message");
627
+ return consoleMessage;
628
+ };
629
+
333
630
  REPLConsole.prototype.onEnterKey = function() {
334
631
  var input = this._input;
335
632
 
@@ -340,6 +637,30 @@ REPLConsole.prototype.onEnterKey = function() {
340
637
  this.commandHandle(input);
341
638
  };
342
639
 
640
+ REPLConsole.prototype.onTabKey = function() {
641
+ var self = this;
642
+
643
+ var hintKeyword;
644
+ if (hintKeyword = self.getHintKeyword()) {
645
+ self.swapCurrentWord(hintKeyword);
646
+ return;
647
+ }
648
+
649
+ if (self.autocomplete) return;
650
+ self.autocomplete = new Autocomplete([]);
651
+
652
+ self.contextRequest(self.getCurrentWord(), function(err, obj) {
653
+ if (err) return self.autocomplete = false;
654
+ self.autocomplete = new Autocomplete(obj['context'], self.getCurrentWord());
655
+ self.inner.appendChild(self.autocomplete.view);
656
+ self.autocomplete.onFinished(function(word) {
657
+ self.swapCurrentWord(word);
658
+ self.autocomplete = false;
659
+ });
660
+ self.scrollToBottom();
661
+ });
662
+ };
663
+
343
664
  REPLConsole.prototype.onNavigateHistory = function(offset) {
344
665
  var command = this.commandStorage.navigate(offset) || "";
345
666
  this.setInput(command);
@@ -349,54 +670,102 @@ REPLConsole.prototype.onNavigateHistory = function(offset) {
349
670
  * Handle control keys like up, down, left, right.
350
671
  */
351
672
  REPLConsole.prototype.onKeyDown = function(ev) {
673
+ if (this.autocomplete && this.autocomplete.onKeyDown(ev)) {
674
+ this.renderInput();
675
+ ev.preventDefault();
676
+ ev.stopPropagation();
677
+ return;
678
+ }
679
+
352
680
  switch (ev.keyCode) {
353
- case 13:
354
- // Enter key
681
+ case 65: // Ctrl-A
682
+ if (ev.ctrlKey) {
683
+ this.setInput(this._input, 0);
684
+ ev.preventDefault();
685
+ }
686
+ break;
687
+
688
+ case 69: // Ctrl-E
689
+ if (ev.ctrlKey) {
690
+ this.onTabKey();
691
+ ev.preventDefault();
692
+ }
693
+ break;
694
+
695
+ case 87: // Ctrl-W
696
+ if (ev.ctrlKey) {
697
+ this.deleteWord();
698
+ ev.preventDefault();
699
+ }
700
+ break;
701
+
702
+ case 85: // Ctrl-U
703
+ if (ev.ctrlKey) {
704
+ this.deleteLine();
705
+ ev.preventDefault();
706
+ }
707
+ break;
708
+
709
+ case 69: // Ctrl-E
710
+ if (ev.ctrlKey) {
711
+ this.onTabKey();
712
+ ev.preventDefault();
713
+ }
714
+ break;
715
+
716
+ case 80: // Ctrl-P
717
+ if (! ev.ctrlKey) break;
718
+
719
+ case 78: // Ctrl-N
720
+ if (! ev.ctrlKey) break;
721
+
722
+ case 9: // Tab
723
+ this.onTabKey();
724
+ ev.preventDefault();
725
+ break;
726
+
727
+ case 13: // Enter key
355
728
  this.onEnterKey();
356
729
  ev.preventDefault();
357
730
  break;
358
- case 80:
359
- // Ctrl-P
360
- if (! ev.ctrlKey) break;
361
- case 38:
362
- // Up arrow
731
+
732
+ case 38: // Up arrow
363
733
  this.onNavigateHistory(-1);
364
734
  ev.preventDefault();
365
735
  break;
366
- case 78:
367
- // Ctrl-N
368
- if (! ev.ctrlKey) break;
369
- case 40:
370
- // Down arrow
736
+
737
+ case 40: // Down arrow
371
738
  this.onNavigateHistory(1);
372
739
  ev.preventDefault();
373
740
  break;
374
- case 37:
375
- // Left arrow
741
+
742
+ case 37: // Left arrow
376
743
  var caretPos = this._caretPos > 0 ? this._caretPos - 1 : this._caretPos;
377
744
  this.setInput(this._input, caretPos);
378
745
  ev.preventDefault();
379
746
  break;
380
- case 39:
381
- // Right arrow
747
+
748
+ case 39: // Right arrow
382
749
  var length = this._input.length;
383
750
  var caretPos = this._caretPos < length ? this._caretPos + 1 : this._caretPos;
384
751
  this.setInput(this._input, caretPos);
385
752
  ev.preventDefault();
386
753
  break;
387
- case 8:
388
- // Delete
754
+
755
+ case 8: // Delete
389
756
  this.deleteAtCurrent();
390
757
  ev.preventDefault();
391
758
  break;
759
+
392
760
  default:
393
761
  break;
394
762
  }
395
763
 
396
764
  if (ev.ctrlKey || ev.metaKey) {
397
- // Set focus to our clipboard in case they hit the "v" key
398
- this.clipboard.focus();
399
765
  if (ev.keyCode == 86) {
766
+ // Set focus to our clipboard when they hit the "v" key
767
+ this.clipboard.focus();
768
+
400
769
  // Pasting to clipboard doesn't happen immediately,
401
770
  // so we have to wait for a while to get the pasted text.
402
771
  var _this = this;
@@ -404,7 +773,7 @@ REPLConsole.prototype.onKeyDown = function(ev) {
404
773
  _this.addToInput(_this.clipboard.value);
405
774
  _this.clipboard.value = "";
406
775
  _this.clipboard.blur();
407
- }, 10);
776
+ }, 100);
408
777
  }
409
778
  }
410
779
 
@@ -416,7 +785,7 @@ REPLConsole.prototype.onKeyDown = function(ev) {
416
785
  */
417
786
  REPLConsole.prototype.onKeyPress = function(ev) {
418
787
  // Only write to the console if it's a single key press.
419
- if (ev.ctrlKey || ev.metaKey) { return; }
788
+ if (ev.ctrlKey && !ev.altKey || ev.metaKey) { return; }
420
789
  var keyCode = ev.keyCode || ev.which;
421
790
  this.insertAtCurrent(String.fromCharCode(keyCode));
422
791
  ev.stopPropagation();
@@ -432,6 +801,52 @@ REPLConsole.prototype.deleteAtCurrent = function() {
432
801
  var before = this._input.substring(0, caretPos);
433
802
  var after = this._input.substring(this._caretPos, this._input.length);
434
803
  this.setInput(before + after, caretPos);
804
+
805
+ if (!this._input) {
806
+ this.autocomplete && this.autocomplete.cancel();
807
+ this.autocomplete = false;
808
+ }
809
+ }
810
+ };
811
+
812
+ /**
813
+ * Deletes the current line.
814
+ */
815
+ REPLConsole.prototype.deleteLine = function() {
816
+ if (this._caretPos > 0) {
817
+ this.setInput("", 0);
818
+
819
+ if (!this._input) {
820
+ this.autocomplete && this.autocomplete.cancel();
821
+ this.autocomplete = false;
822
+ }
823
+ }
824
+ };
825
+
826
+ /**
827
+ * Deletes the current word.
828
+ */
829
+ REPLConsole.prototype.deleteWord = function() {
830
+ if (this._caretPos > 0) {
831
+ var i = 1, current = this._caretPos;
832
+ while (this._input[current - i++] == " ");
833
+
834
+ var deleteIndex = 0;
835
+ for (; current - i > 0; i++) {
836
+ if (this._input[current - i] == " ") {
837
+ deleteIndex = current - i;
838
+ break;
839
+ }
840
+ }
841
+
842
+ var before = this._input.substring(0, deleteIndex);
843
+ var after = this._input.substring(current, this._input.length);
844
+ this.setInput(before + after, deleteIndex);
845
+
846
+ if (!this._input) {
847
+ this.autocomplete && this.autocomplete.cancel();
848
+ this.autocomplete = false;
849
+ }
435
850
  }
436
851
  };
437
852
 
@@ -444,15 +859,49 @@ REPLConsole.prototype.insertAtCurrent = function(char) {
444
859
  this.setInput(before + char + after, this._caretPos + 1);
445
860
  };
446
861
 
862
+ REPLConsole.prototype.swapCurrentWord = function(next) {
863
+ function right(s, pos) {
864
+ var x = s.indexOf(' ', pos);
865
+ return x === -1 ? s.length : x;
866
+ }
867
+
868
+ function swap(s, pos) {
869
+ return s.substr(0, s.lastIndexOf(' ', pos) + 1) + next + s.substr(right(s, pos))
870
+ }
871
+
872
+ if (!next) return;
873
+ var swapped = swap(this._input, this._caretPos);
874
+ this.setInput(swapped, this._caretPos + swapped.length - this._input.length);
875
+ };
876
+
877
+ REPLConsole.prototype.getCurrentWord = function() {
878
+ return (function(s, pos) {
879
+ var left = s.lastIndexOf(' ', pos);
880
+ if (left === -1) left = 0;
881
+ var right = s.indexOf(' ', pos)
882
+ if (right === -1) right = s.length - 1;
883
+ return s.substr(left, right - left + 1).replace(/^\s+|\s+$/g,'');
884
+ })(this._input, this._caretPos);
885
+ };
886
+
447
887
  REPLConsole.prototype.scrollToBottom = function() {
448
888
  this.outer.scrollTop = this.outer.scrollHeight;
449
889
  };
450
890
 
451
- // Change the binding of the console
452
- REPLConsole.prototype.switchBindingTo = function(frameId, callback) {
891
+ // Change the binding of the console.
892
+ REPLConsole.prototype.switchBindingTo = function(frameId, exceptionObjectId, callback) {
453
893
  var url = this.getSessionUrl('trace');
454
894
  var params = "frame_id=" + encodeURIComponent(frameId);
455
- postRequest(url, params, callback);
895
+
896
+ if (exceptionObjectId) {
897
+ params = params + "&exception_object_id=" + encodeURIComponent(exceptionObjectId);
898
+ }
899
+
900
+ var _this = this;
901
+ postRequest(url, params, function() {
902
+ var text = "Context has changed to: " + callback();
903
+ _this.writeNotification(text);
904
+ });
456
905
  };
457
906
 
458
907
  /**
@@ -488,7 +937,6 @@ REPLConsole.request = function request(method, url, params, callback) {
488
937
  xhr.open(method, url, true);
489
938
  xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
490
939
  xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
491
- xhr.setRequestHeader("Accept", "<%= Mime::WEB_CONSOLE_V2 %>");
492
940
  xhr.send(params);
493
941
 
494
942
  xhr.onreadystatechange = function() {
@@ -514,7 +962,7 @@ function addClass(el, className) {
514
962
  for (var i = 0; i < el.length; ++ i) {
515
963
  addClass(el[i], className);
516
964
  }
517
- } else {
965
+ } else if (!hasClass(el, className)) {
518
966
  el.className += " " + className;
519
967
  }
520
968
  }
@@ -562,3 +1010,15 @@ if (typeof exports === 'object') {
562
1010
  } else {
563
1011
  window.REPLConsole = REPLConsole;
564
1012
  }
1013
+
1014
+ // Split string by module operators of ruby
1015
+ function getContext(s) {
1016
+ var methodOp = s.lastIndexOf('.');
1017
+ var moduleOp = s.lastIndexOf('::');
1018
+ var x = methodOp > moduleOp ? methodOp : moduleOp;
1019
+ return x !== -1 ? s.substr(0, x) : '';
1020
+ }
1021
+
1022
+ function flatten(arrays) {
1023
+ return Array.prototype.concat.apply([], arrays);
1024
+ }