web-console 3.3.1 → 3.4.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.
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