web-console 3.3.1 → 3.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: caf063e0c1a30832d5fa0b6ce776514809889448
4
- data.tar.gz: 0ea92426f743c1463500a0367b8d490501b80e20
3
+ metadata.gz: 963c13addc1eda302c2fb5e217334b403e0f6a09
4
+ data.tar.gz: e8768504bc586024d0061ba82b0b275c65d93442
5
5
  SHA512:
6
- metadata.gz: dc7485fccd26fc2af5a0110215df7baf09188f6ee94d2cb02a6da71c649bb22f84070ff4f7dd0d1c34ba355ff962ac590f1cf8520b04bc4db91ef5256e8b7b49
7
- data.tar.gz: f8e6b128bfc10fba1c76d95287a0316d028281acac1853b451f024a99c23b6c2d26a6ad3cb1fa4b87395aaabd4c446f2dc3a9d1f792ceff2e86b34c1b7e92e76
6
+ metadata.gz: 00d6e18111d7d37a870015012dd2e590e2e4e392490a9301e883e450a95f7b87b81c767dc3cf90829496d2efecd65ffe07599ac4f09dd50d1014ea35ccb6586a
7
+ data.tar.gz: 7be81b47f363fb912e2df95c6f44a493235fed6f72aae1f0ea11006919e593668b11d4473434480d448676ef1cfa5962f7e4cd5ada857636cec518357207254f
@@ -2,6 +2,10 @@
2
2
 
3
3
  ## master (unreleased)
4
4
 
5
+ ## 3.4.0
6
+
7
+ * [#205](https://github.com/rails/web-console/pull/205) Introduce autocompletion ([@sh19910711])
8
+
5
9
  ## 3.3.1
6
10
 
7
11
  Drop support for Rails `4.2.0`.
@@ -185,7 +185,7 @@ end
185
185
 
186
186
  ### Why I'm getting an undefined method `web_console`?
187
187
 
188
- Make sure you configuration lives in `config/environments/development.rb`.
188
+ Make sure your configuration lives in `config/environments/development.rb`.
189
189
 
190
190
  ## Credits
191
191
 
@@ -15,6 +15,7 @@ module WebConsole
15
15
  autoload :Whitelist
16
16
  autoload :Template
17
17
  autoload :Middleware
18
+ autoload :Context
18
19
 
19
20
  autoload_at 'web_console/errors' do
20
21
  autoload :Error
@@ -0,0 +1,43 @@
1
+ module WebConsole
2
+ # A context lets you get object names related to the current session binding.
3
+ class Context
4
+ def initialize(binding)
5
+ @binding = binding
6
+ end
7
+
8
+ # Extracts entire objects which can be called by the current session unless
9
+ # the inputs is present.
10
+ #
11
+ # Otherwise, it extracts methods and constants of the object specified by
12
+ # the input.
13
+ def extract(input = nil)
14
+ input.present? ? local(input) : global
15
+ end
16
+
17
+ private
18
+
19
+ GLOBAL_OBJECTS = [
20
+ 'instance_variables',
21
+ 'local_variables',
22
+ 'methods',
23
+ 'class_variables',
24
+ 'Object.constants',
25
+ 'global_variables'
26
+ ]
27
+
28
+ def global
29
+ GLOBAL_OBJECTS.map { |cmd| eval(cmd) }
30
+ end
31
+
32
+ def local(input)
33
+ [
34
+ eval("#{input}.methods").map { |m| "#{input}.#{m}" },
35
+ eval("#{input}.constants").map { |c| "#{input}::#{c}" },
36
+ ]
37
+ end
38
+
39
+ def eval(cmd)
40
+ @binding.eval(cmd) rescue []
41
+ end
42
+ end
43
+ end
@@ -1,7 +1,7 @@
1
1
  en:
2
2
  errors:
3
3
  unavailable_session: |
4
- Session %{id} is is no longer available in memory.
4
+ Session %{id} is no longer available in memory.
5
5
 
6
6
  If you happen to run on a multi-process server (like Unicorn or Puma) the process
7
7
  this request hit doesn't store %{id} in memory. Consider turning the number of
@@ -103,7 +103,11 @@ module WebConsole
103
103
 
104
104
  def update_repl_session(id, request)
105
105
  json_response_with_session(id, request) do |session|
106
- { output: session.eval(request.params[:input]) }
106
+ if input = request.params[:input]
107
+ { output: session.eval(input) }
108
+ elsif input = request.params[:context]
109
+ { context: session.context(input) }
110
+ end
107
111
  end
108
112
  end
109
113
 
@@ -40,6 +40,9 @@ module WebConsole
40
40
  if mount_point = config.web_console.mount_point
41
41
  Middleware.mount_point = mount_point.chomp('/')
42
42
  end
43
+ if root = Rails.application.config.relative_url_root
44
+ Middleware.mount_point = File.join(root, Middleware.mount_point)
45
+ end
43
46
  end
44
47
 
45
48
  initializer 'web_console.template_paths' do
@@ -43,7 +43,7 @@ module WebConsole
43
43
  def initialize(bindings)
44
44
  @id = SecureRandom.hex(16)
45
45
  @bindings = bindings
46
- @evaluator = Evaluator.new(bindings.first)
46
+ @evaluator = Evaluator.new(@current_binding = bindings.first)
47
47
 
48
48
  store_into_memory
49
49
  end
@@ -59,7 +59,12 @@ module WebConsole
59
59
  #
60
60
  # Returns nothing.
61
61
  def switch_binding_to(index)
62
- @evaluator = Evaluator.new(@bindings[index.to_i])
62
+ @evaluator = Evaluator.new(@current_binding = @bindings[index.to_i])
63
+ end
64
+
65
+ # Returns context of the current binding
66
+ def context(objpath)
67
+ Context.new(@current_binding).extract(objpath)
63
68
  end
64
69
 
65
70
  private
@@ -0,0 +1,54 @@
1
+ namespace :templates do
2
+ desc 'Run tests for templates'
3
+ task test: [ :daemonize, :npm, :rackup, :wait, :mocha, :kill, :exit ]
4
+ task serve: [ :npm, :rackup ]
5
+
6
+ workdir = Pathname(EXPANDED_CWD).join('test/templates')
7
+ pid = Pathname(Dir.tmpdir).join("web_console_test.pid")
8
+ runner = URI.parse("http://#{ENV['IP'] || '127.0.0.1'}:#{ENV['PORT'] || 29292}/html/test_runner.html")
9
+ rackup = "rackup --host #{runner.host} --port #{runner.port}"
10
+ result = nil
11
+ browser = 'phantomjs'
12
+
13
+ def need_to_wait?(uri)
14
+ Net::HTTP.start(uri.host, uri.port) { |http| http.get(uri.path) }
15
+ rescue Errno::ECONNREFUSED
16
+ retry if yield
17
+ end
18
+
19
+ task :daemonize do
20
+ rackup += " -D --pid #{pid}"
21
+ end
22
+
23
+ task :npm => [ :phantomjs ] do
24
+ Dir.chdir(workdir) { system 'npm install --silent' }
25
+ end
26
+
27
+ task :phantomjs do
28
+ unless system("which #{browser} >/dev/null")
29
+ browser = './node_modules/.bin/phantomjs'
30
+ Dir.chdir(workdir) { system("test -f #{browser} || npm install --silent phantomjs-prebuilt") }
31
+ end
32
+ end
33
+
34
+ task :rackup do
35
+ Dir.chdir(workdir) { system rackup }
36
+ end
37
+
38
+ task :wait do
39
+ cnt = 0
40
+ need_to_wait?(runner) { sleep 1; cnt += 1; cnt < 5 }
41
+ end
42
+
43
+ task :mocha do
44
+ Dir.chdir(workdir) { result = system("#{browser} ./node_modules/mocha-phantomjs-core/mocha-phantomjs-core.js #{runner} dot") }
45
+ end
46
+
47
+ task :kill do
48
+ system "kill #{File.read pid}"
49
+ end
50
+
51
+ task :exit do
52
+ exit result
53
+ end
54
+ end
@@ -44,6 +44,212 @@ 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
254
  var consoleInnerHtml = <%= render_inlined_string '_inner_console_markup.html' %>;
49
255
  var promptBoxHtml = <%= render_inlined_string '_prompt_box_markup.html' %>;
@@ -62,6 +268,7 @@ function REPLConsole(config) {
62
268
  this.prompt = getConfig('promptLabel', ' >>');
63
269
  this.mountPoint = getConfig('mountPoint');
64
270
  this.sessionId = getConfig('sessionId');
271
+ this.autocomplete = false;
65
272
  }
66
273
 
67
274
  REPLConsole.prototype.getSessionUrl = function(path) {
@@ -73,6 +280,16 @@ REPLConsole.prototype.getSessionUrl = function(path) {
73
280
  return parts.join('/').replace(/([^:]\/)\/+/g, '$1');
74
281
  };
75
282
 
283
+ REPLConsole.prototype.contextRequest = function(keyword, callback) {
284
+ putRequest(this.getSessionUrl(), 'context=' + getContext(keyword), function(xhr) {
285
+ if (xhr.status == 200) {
286
+ callback(null, JSON.parse(xhr.responseText));
287
+ } else {
288
+ callback(xhr.statusText);
289
+ }
290
+ });
291
+ };
292
+
76
293
  REPLConsole.prototype.commandHandle = function(line, callback) {
77
294
  var self = this;
78
295
  var params = 'input=' + encodeURIComponent(line);
@@ -92,7 +309,7 @@ REPLConsole.prototype.commandHandle = function(line, callback) {
92
309
 
93
310
  function getErrorText(xhr) {
94
311
  if (!xhr.status) {
95
- return "<%= t 'errors.connection_refused' %>";
312
+ return "Connection Refused";
96
313
  } else {
97
314
  return xhr.status + ' ' + xhr.statusText;
98
315
  }
@@ -199,6 +416,7 @@ REPLConsole.prototype.install = function(container) {
199
416
  this.outer = consoleOuter;
200
417
  this.inner = findChild(this.outer, 'console-inner');
201
418
  this.clipboard = findChild(container, 'clipboard');
419
+ this.suggestWait = 1500;
202
420
  this.newPromptBox();
203
421
  this.insertCss();
204
422
 
@@ -267,10 +485,63 @@ REPLConsole.prototype.removeCaretFromPrompt = function() {
267
485
  this.setInput(this._input, -1);
268
486
  };
269
487
 
488
+ REPLConsole.prototype.getSuggestion = function(keyword) {
489
+ var self = this;
490
+
491
+ function show(found) {
492
+ if (!found) return;
493
+ var hint = self.promptDisplay.childNodes[1];
494
+ hint.className = 'console-hint';
495
+ hint.dataset.keyword = found;
496
+ hint.innerText = found.substr(self.suggestKeyword.length);
497
+ // clear hinting information after timeout in a few time
498
+ if (self.suggestTimeout) clearTimeout(self.suggestTimeout);
499
+ self.suggestTimeout = setTimeout(function() { self.renderInput() }, self.suggestWait);
500
+ }
501
+
502
+ function find(context) {
503
+ var k = self.suggestKeyword;
504
+ for (var i = 0; i < context.length; ++i) if (context[i].substr(0, k.length) === k) {
505
+ if (context[i] === k) return;
506
+ return context[i];
507
+ }
508
+ }
509
+
510
+ function request(keyword, callback) {
511
+ self.contextRequest(keyword, function(err, res) {
512
+ if (err) throw new Error(err);
513
+ var c = flatten(res['context']);
514
+ c.sort();
515
+ callback(c);
516
+ });
517
+ }
518
+
519
+ self.suggestKeyword = keyword;
520
+ var input = getContext(keyword);
521
+ if (keyword.length - input.length < 3) return;
522
+
523
+ if (self.suggestInput !== input) {
524
+ self.suggestInput = input;
525
+ request(keyword, function(c) {
526
+ show(find(self.suggestContext = c));
527
+ });
528
+ } else if (self.suggestContext) {
529
+ show(find(self.suggestContext));
530
+ }
531
+ };
532
+
533
+ REPLConsole.prototype.getHintKeyword = function() {
534
+ var hint = this.promptDisplay.childNodes[1];
535
+ return hint.className === 'console-hint' && hint.dataset.keyword;
536
+ };
537
+
270
538
  REPLConsole.prototype.setInput = function(input, caretPos) {
539
+ if (input == null) return; // keep value if input is undefined
271
540
  this._caretPos = caretPos === undefined ? input.length : caretPos;
272
541
  this._input = input;
542
+ if (this.autocomplete) this.autocomplete.refine(this.getCurrentWord());
273
543
  this.renderInput();
544
+ if (!this.autocomplete && input.length == this._caretPos) this.getSuggestion(this.getCurrentWord());
274
545
  };
275
546
 
276
547
  /**
@@ -292,9 +563,8 @@ REPLConsole.prototype.renderInput = function() {
292
563
  // Clear the current input.
293
564
  removeAllChildren(this.promptDisplay);
294
565
 
295
- var promptCursor = document.createElement('span');
296
- promptCursor.className = "console-cursor";
297
566
  var before, current, after;
567
+ var center = document.createElement('span');
298
568
 
299
569
  if (this._caretPos < 0) {
300
570
  before = this._input;
@@ -310,9 +580,12 @@ REPLConsole.prototype.renderInput = function() {
310
580
  }
311
581
 
312
582
  this.promptDisplay.appendChild(document.createTextNode(before));
313
- promptCursor.appendChild(document.createTextNode(current));
314
- this.promptDisplay.appendChild(promptCursor);
583
+ this.promptDisplay.appendChild(center);
315
584
  this.promptDisplay.appendChild(document.createTextNode(after));
585
+
586
+ var hint = this.autocomplete && this.autocomplete.getSelectedWord();
587
+ addClass(center, hint ? 'console-hint' : 'console-cursor');
588
+ center.appendChild(document.createTextNode(hint ? hint.substr(this.getCurrentWord().length) : current));
316
589
  };
317
590
 
318
591
  REPLConsole.prototype.writeOutput = function(output) {
@@ -340,6 +613,30 @@ REPLConsole.prototype.onEnterKey = function() {
340
613
  this.commandHandle(input);
341
614
  };
342
615
 
616
+ REPLConsole.prototype.onTabKey = function() {
617
+ var self = this;
618
+
619
+ var hintKeyword;
620
+ if (hintKeyword = self.getHintKeyword()) {
621
+ self.swapCurrentWord(hintKeyword);
622
+ return;
623
+ }
624
+
625
+ if (self.autocomplete) return;
626
+ self.autocomplete = new Autocomplete([]);
627
+
628
+ self.contextRequest(self.getCurrentWord(), function(err, obj) {
629
+ if (err) return self.autocomplete = false;
630
+ self.autocomplete = new Autocomplete(obj['context'], self.getCurrentWord());
631
+ self.inner.appendChild(self.autocomplete.view);
632
+ self.autocomplete.onFinished(function(word) {
633
+ self.swapCurrentWord(word);
634
+ self.autocomplete = false;
635
+ });
636
+ self.scrollToBottom();
637
+ });
638
+ };
639
+
343
640
  REPLConsole.prototype.onNavigateHistory = function(offset) {
344
641
  var command = this.commandStorage.navigate(offset) || "";
345
642
  this.setInput(command);
@@ -349,7 +646,26 @@ REPLConsole.prototype.onNavigateHistory = function(offset) {
349
646
  * Handle control keys like up, down, left, right.
350
647
  */
351
648
  REPLConsole.prototype.onKeyDown = function(ev) {
649
+ if (this.autocomplete && this.autocomplete.onKeyDown(ev)) {
650
+ this.renderInput();
651
+ ev.preventDefault();
652
+ ev.stopPropagation();
653
+ return;
654
+ }
655
+
352
656
  switch (ev.keyCode) {
657
+ case 69:
658
+ // Ctrl-E
659
+ if (ev.ctrlKey) {
660
+ this.onTabKey();
661
+ ev.preventDefault();
662
+ }
663
+ break;
664
+ case 9:
665
+ // Tab
666
+ this.onTabKey();
667
+ ev.preventDefault();
668
+ break;
353
669
  case 13:
354
670
  // Enter key
355
671
  this.onEnterKey();
@@ -432,6 +748,11 @@ REPLConsole.prototype.deleteAtCurrent = function() {
432
748
  var before = this._input.substring(0, caretPos);
433
749
  var after = this._input.substring(this._caretPos, this._input.length);
434
750
  this.setInput(before + after, caretPos);
751
+
752
+ if (!this._input) {
753
+ this.autocomplete && this.autocomplete.cancel();
754
+ this.autocomplete = false;
755
+ }
435
756
  }
436
757
  };
437
758
 
@@ -444,6 +765,31 @@ REPLConsole.prototype.insertAtCurrent = function(char) {
444
765
  this.setInput(before + char + after, this._caretPos + 1);
445
766
  };
446
767
 
768
+ REPLConsole.prototype.swapCurrentWord = function(next) {
769
+ function right(s, pos) {
770
+ var x = s.indexOf(' ', pos);
771
+ return x === -1 ? s.length : x;
772
+ }
773
+
774
+ function swap(s, pos) {
775
+ return s.substr(0, s.lastIndexOf(' ', pos) + 1) + next + s.substr(right(s, pos))
776
+ }
777
+
778
+ if (!next) return;
779
+ var swapped = swap(this._input, this._caretPos);
780
+ this.setInput(swapped, this._caretPos + swapped.length - this._input.length);
781
+ };
782
+
783
+ REPLConsole.prototype.getCurrentWord = function() {
784
+ return (function(s, pos) {
785
+ var left = s.lastIndexOf(' ', pos);
786
+ if (left === -1) left = 0;
787
+ var right = s.indexOf(' ', pos)
788
+ if (right === -1) right = s.length - 1;
789
+ return s.substr(left, right - left + 1).replace(/^\s+|\s+$/g,'');
790
+ })(this._input, this._caretPos);
791
+ };
792
+
447
793
  REPLConsole.prototype.scrollToBottom = function() {
448
794
  this.outer.scrollTop = this.outer.scrollHeight;
449
795
  };
@@ -514,7 +860,7 @@ function addClass(el, className) {
514
860
  for (var i = 0; i < el.length; ++ i) {
515
861
  addClass(el[i], className);
516
862
  }
517
- } else {
863
+ } else if (!hasClass(el, className)) {
518
864
  el.className += " " + className;
519
865
  }
520
866
  }
@@ -562,3 +908,15 @@ if (typeof exports === 'object') {
562
908
  } else {
563
909
  window.REPLConsole = REPLConsole;
564
910
  }
911
+
912
+ // Split string by module operators of ruby
913
+ function getContext(s) {
914
+ var methodOp = s.lastIndexOf('.');
915
+ var moduleOp = s.lastIndexOf('::');
916
+ var x = methodOp > moduleOp ? methodOp : moduleOp;
917
+ return x !== -1 ? s.substr(0, x) : '';
918
+ }
919
+
920
+ function flatten(arrays) {
921
+ return Array.prototype.concat.apply([], arrays);
922
+ }
@@ -11,6 +11,12 @@
11
11
  .console .console-prompt-box { color: #FFF; }
12
12
  .console .console-message { color: #1AD027; margin: 0; border: 0; white-space: pre-wrap; background-color: #333; padding: 0; }
13
13
  .console .console-message.error-message { color: #fc9; }
14
+ .console .console-message.auto-complete { word-break: break-all; }
15
+ .console .console-message.auto-complete .keyword { margin-right: 11px; }
16
+ .console .console-message.auto-complete .keyword.selected { background: #FFF; color: #000; }
17
+ .console .console-message.auto-complete .hidden { display: none; }
18
+ .console .console-message.auto-complete .trimmed { display: none; }
19
+ .console .console-hint { color: #096; }
14
20
  .console .console-focus .console-cursor { background: #FEFEFE; color: #333; font-weight: bold; }
15
21
  .console .resizer { background: #333; width: 100%; height: 4px; cursor: ns-resize; }
16
22
  .console .console-actions { padding-right: 3px; }
@@ -21,7 +21,7 @@ module WebConsole
21
21
  end
22
22
 
23
23
  def view
24
- @view ||= View.new(@view_path)
24
+ @view = View.new(@view_path)
25
25
  end
26
26
 
27
27
  private
@@ -1,3 +1,3 @@
1
1
  module WebConsole
2
- VERSION = '3.3.1'
2
+ VERSION = '3.4.0'
3
3
  end
@@ -31,7 +31,11 @@ module WebConsole
31
31
  #
32
32
  # Helps to keep the Rails logs clean during errors.
33
33
  def render(*)
34
- WebConsole.logger.silence { super }
34
+ if logger = WebConsole.logger and logger.respond_to?(:silence)
35
+ WebConsole.logger.silence { super }
36
+ else
37
+ super
38
+ end
35
39
  end
36
40
 
37
41
  # Override method for ActionView::Helpers::TranslationHelper#t.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: web-console
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.3.1
4
+ version: 3.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Charlie Somerville
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2016-07-05 00:00:00.000000000 Z
14
+ date: 2016-10-29 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: railties
@@ -85,6 +85,7 @@ files:
85
85
  - Rakefile
86
86
  - lib/web-console.rb
87
87
  - lib/web_console.rb
88
+ - lib/web_console/context.rb
88
89
  - lib/web_console/errors.rb
89
90
  - lib/web_console/evaluator.rb
90
91
  - lib/web_console/exception_mapper.rb
@@ -99,7 +100,7 @@ files:
99
100
  - lib/web_console/response.rb
100
101
  - lib/web_console/session.rb
101
102
  - lib/web_console/tasks/extensions.rake
102
- - lib/web_console/tasks/test_templates.rake
103
+ - lib/web_console/tasks/templates.rake
103
104
  - lib/web_console/template.rb
104
105
  - lib/web_console/templates/_inner_console_markup.html.erb
105
106
  - lib/web_console/templates/_markup.html.erb
@@ -1,50 +0,0 @@
1
- namespace :test do
2
- desc "Run tests for templates"
3
- task templates: "templates:all"
4
-
5
- namespace :templates do
6
- task all: [ :daemonize, :npm, :rackup, :wait, :mocha, :kill, :exit ]
7
- task serve: [ :npm, :rackup ]
8
-
9
- work_dir = Pathname(EXPANDED_CWD).join("test/templates")
10
- pid_file = Pathname(Dir.tmpdir).join("web_console.#{SecureRandom.uuid}.pid")
11
- runner_uri = URI.parse("http://localhost:29292/html/spec_runner.html")
12
- rackup_opts = "-p #{runner_uri.port}"
13
- test_result = nil
14
-
15
- def need_to_wait?(uri)
16
- Net::HTTP.start(uri.host, uri.port) { |http| http.get(uri.path) }
17
- rescue Errno::ECONNREFUSED
18
- retry if yield
19
- end
20
-
21
- task :daemonize do
22
- rackup_opts += " -D -P #{pid_file}"
23
- end
24
-
25
- task :npm do
26
- Dir.chdir(work_dir) { system "npm install --silent" }
27
- end
28
-
29
- task :rackup do
30
- Dir.chdir(work_dir) { system "bundle exec rackup #{rackup_opts}" }
31
- end
32
-
33
- task :wait do
34
- cnt = 0
35
- need_to_wait?(runner_uri) { sleep 1; cnt += 1; cnt < 5 }
36
- end
37
-
38
- task :mocha do
39
- Dir.chdir(work_dir) { test_result = system("$(npm bin)/mocha-phantomjs #{runner_uri}") }
40
- end
41
-
42
- task :kill do
43
- system "kill #{File.read pid_file}"
44
- end
45
-
46
- task :exit do
47
- exit test_result
48
- end
49
- end
50
- end