opal-irb 0.7.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 (54) hide show
  1. data/.gitignore +3 -0
  2. data/.ruby-gemset +1 -0
  3. data/.ruby-version +1 -0
  4. data/Gemfile +14 -0
  5. data/Gemfile.lock +113 -0
  6. data/Guardfile +5 -0
  7. data/LICENSE +21 -0
  8. data/README.md +175 -0
  9. data/Rakefile +65 -0
  10. data/Roadmap.org +17 -0
  11. data/app/assets/stylesheets/opal-irb/jqconsole.css +263 -0
  12. data/compiled/app-embeddable.js +39765 -0
  13. data/compiled/app-jqconsole.js +39767 -0
  14. data/compiled/application.js +27399 -0
  15. data/css/ansi.css +172 -0
  16. data/css/opal_irb_jqconsole.css +79 -0
  17. data/css/show-hint.css +38 -0
  18. data/doc/presentations/opal_irb_overview.html +678 -0
  19. data/doc/presentations/opal_irb_overview.org +448 -0
  20. data/examples/app-embeddable.rb +8 -0
  21. data/examples/app-jqconsole.rb +10 -0
  22. data/examples/application.rb +8 -0
  23. data/index-embeddable.html +29 -0
  24. data/index-homebrew.html +115 -0
  25. data/index-jq.html +80 -0
  26. data/js/anyword-hint.js +44 -0
  27. data/js/jqconsole.js +1583 -0
  28. data/js/nodeutil.js +546 -0
  29. data/js/ruby.js +285 -0
  30. data/js/show-hint.js +383 -0
  31. data/lib/opal-irb/rails_engine.rb +3 -0
  32. data/lib/opal-irb/version.rb +3 -0
  33. data/lib/opal-irb-rails.rb +2 -0
  34. data/lib/opal-irb.rb +44 -0
  35. data/opal/object_extensions.rb +20 -0
  36. data/opal/opal_irb/completion_engine.rb +202 -0
  37. data/opal/opal_irb/completion_formatter.rb +49 -0
  38. data/opal/opal_irb/completion_results.rb +88 -0
  39. data/opal/opal_irb.rb +88 -0
  40. data/opal/opal_irb_homebrew_console.rb +398 -0
  41. data/opal/opal_irb_jqconsole.rb +517 -0
  42. data/opal/opal_irb_jqconsole_css.rb +259 -0
  43. data/opal/opal_irb_log_redirector.rb +32 -0
  44. data/opal/opal_phantomjs.rb +49 -0
  45. data/opal-irb.gemspec +20 -0
  46. data/spec/code_link_handler_spec.rb +30 -0
  47. data/spec/jquery.js +5 -0
  48. data/spec/object_extensions_spec.rb +32 -0
  49. data/spec/opal_irb/completion_engine_spec.rb +204 -0
  50. data/spec/opal_irb/completion_results_spec.rb +32 -0
  51. data/spec/opal_irb_log_director_spec.rb +19 -0
  52. data/spec/opal_irb_spec.rb +19 -0
  53. data/spec/spec_helper.rb +1 -0
  54. metadata +151 -0
@@ -0,0 +1,517 @@
1
+ require 'opal'
2
+ require 'opal-jquery'
3
+ require 'opal_irb_log_redirector'
4
+ require 'opal_irb'
5
+ require 'opal_irb/completion_engine'
6
+ require 'opal_irb/completion_formatter'
7
+ require 'jqconsole'
8
+
9
+ # top level methods for irb cmd line
10
+ def irb_link_for history_num=nil
11
+ OpalIrbJqconsole.console.irb_link_for history_num
12
+ end
13
+
14
+ class Timeout
15
+ def initialize(time=0, &block)
16
+ @timeout = `setTimeout(#{block}, time)`
17
+ end
18
+
19
+ def clear
20
+ `clearTimeout(#{@timeout})`
21
+ end
22
+ end
23
+
24
+ # module Kernel
25
+ # # just like osx's say command, only tested on desktop safari and chrome on osx
26
+ # def say msg
27
+ # %x|
28
+ # var msg = new SpeechSynthesisUtterance(#{msg});
29
+ # window.speechSynthesis.speak(msg);
30
+ # |
31
+ # end
32
+ # end
33
+
34
+ class OpalIrbJqconsole
35
+ def self.console
36
+ @console
37
+ end
38
+
39
+ # create on a pre existing div
40
+ def self.create(parent_element_id)
41
+ @console = OpalIrbJqconsole.new(parent_element_id)
42
+ end
43
+
44
+ BOTTOM_PANEL_ID = "opal-irb-console-bottom-panel"
45
+ # create a bottom panel
46
+ def self.create_bottom_panel(hidden = false)
47
+ parent_element_id="opal-irb-console"
48
+ style = hidden ? "style=\"display:none\"" : ""
49
+ # <a href="#" id="collapse-opal-irb-console" class=\"boxclose\"></a>
50
+
51
+ html = <<HTML
52
+ <div id="#{BOTTOM_PANEL_ID}" #{style}>
53
+ <div id="opal-irb-console-topbar">
54
+ <span id="collapse-opal-irb-console" class=\"boxclose\"></span>
55
+ </div>
56
+ <div id='#{parent_element_id}'>
57
+ </div>
58
+ </div>
59
+ HTML
60
+ Element.find("body").append(html)
61
+ Element.id("collapse-opal-irb-console").on(:click) {
62
+ Element.id("#{BOTTOM_PANEL_ID}").hide;
63
+ }
64
+ create("##{parent_element_id}")
65
+ end
66
+
67
+ def self.add_hot_key_panel_behavior(keys_hash)
68
+ Element.find("body").on(:keydown) { |evt|
69
+ if create_key_filter(keys_hash, evt)
70
+ if panel.visible?
71
+ hide_panel
72
+ else
73
+ show_panel
74
+ end
75
+ end
76
+ }
77
+ end
78
+ # set $DEBUG_KEY_FILTER = true somewhere in your app to see the keys for debugging
79
+ def self.create_key_filter(keys_hash, evt)
80
+ puts "evt.ctrl_key #{evt.ctrl_key} evt.meta_key #{evt.meta_key} evt.shift_key #{evt.shift_key} evt.key_code #{evt.key_code}_" if $DEBUG_KEY_FILTER
81
+ keys_hash[:modifiers].all? { |modifier| evt.send("#{modifier}_key") } && evt.key_code == keys_hash[:key].upcase.ord
82
+ end
83
+
84
+ def self.add_open_panel_behavior(link_id)
85
+ Element.id(link_id).on(:click) {
86
+ if panel.visible?
87
+ alert "OpalIRB is already showing"
88
+ else
89
+ show_panel
90
+ end
91
+ }
92
+ end
93
+
94
+ def self.panel
95
+ Element.id("#{BOTTOM_PANEL_ID}")
96
+ end
97
+
98
+ def self.show_panel
99
+ panel.show
100
+ Timeout.new { console.focus}
101
+ end
102
+
103
+ def self.hide_panel
104
+ panel.hide
105
+ end
106
+
107
+
108
+ def focus
109
+ @jqconsole.Focus
110
+ end
111
+
112
+ attr_reader :irb
113
+ def initialize(parent_element_id)
114
+ @irb = OpalIrb.new
115
+ setup_cmd_line_methods
116
+ setup_jqconsole(parent_element_id)
117
+ create_multiline_editor
118
+ redirect_console_dot_log
119
+ handler()
120
+ setup_code_link_handling
121
+ end
122
+
123
+ # logs only to js console, not to irb, for things you want only for
124
+ # debug and not public viewing
125
+ def log thing
126
+ `console.orig_log(#{thing})`
127
+ end
128
+
129
+
130
+ def setup_code_link_handling
131
+ @code_link_handler = CodeLinkHandler.new
132
+ link_code = @code_link_handler.grab_link_code
133
+ if link_code
134
+ # do this after everything initializes
135
+ Timeout.new {
136
+ print_and_process_code link_code
137
+ self.class.show_panel
138
+ }
139
+ end
140
+ end
141
+
142
+ def create_and_display_code_link code
143
+ code_link = @code_link_handler.create_link_for_code code
144
+ unescaped_write "<a href=#{code_link}>#{code_link}</a>\n" if code_link
145
+ end
146
+
147
+ class CodeLinkHandler
148
+
149
+ def initialize(location=`window.location`)
150
+ @location = Native(location) # inject this so we can test
151
+ end
152
+
153
+ def create_link_for_code code
154
+ if code
155
+ @location.origin + @location.pathname + "#code:" + `encodeURIComponent(#{code})`
156
+ else
157
+ nil
158
+ end
159
+ end
160
+ # initialize irb w/link passed in code ala try opal
161
+ def grab_link_code
162
+ link_code = `decodeURIComponent(#{@location.hash})`
163
+ if link_code != ''
164
+ link_code[6..-1]
165
+ else
166
+ nil
167
+ end
168
+ end
169
+
170
+ end
171
+
172
+ def irb_link_for history_num
173
+ history_num = -1 unless history_num # pick last command before irb_link_for if nil
174
+ history_num -= 1 # offset off by 1
175
+ code = jqconsole.GetHistory[history_num] #
176
+ create_and_display_code_link code
177
+ end
178
+
179
+
180
+ def irb_link_for_current_line
181
+ current_code = jqconsole.GetPromptText
182
+ create_and_display_code_link current_code
183
+ end
184
+
185
+
186
+ def redirect_console_dot_log
187
+ OpalIrbLogRedirector.add_to_redirect(lambda {|args| OpalIrbJqconsole.write(args)})
188
+
189
+ end
190
+
191
+ def create_multiline_editor
192
+ editor = <<EDITOR
193
+ <div id="multiline-editor-dialog" class="dialog" style="display:none" >
194
+ <textarea name="multi_line_input" id="multi_line_input"></textarea>
195
+ </div>
196
+ EDITOR
197
+ myself = self # self is now the div and not self anymore
198
+ Element.find("body") << editor
199
+ %x|
200
+ $( ".dialog" ).dialog({
201
+ autoOpen: false,
202
+ show: "blind",
203
+ hide: "explode",
204
+ modal: true,
205
+ width: "500px",
206
+ title: "Multi Line Edit",
207
+ buttons: {
208
+ "Run it": function() {
209
+ $( this ).dialog( "close" );
210
+ #{myself}.$process_multiline();
211
+ },
212
+ "Cancel": function() {
213
+ $( this ).dialog( "close" );
214
+ },
215
+ }
216
+ });
217
+ |
218
+
219
+ @open_editor_dialog_function = %x|function() {
220
+ $( ".dialog" ).dialog( "open" );
221
+ setTimeout(function(){editor.refresh();}, 20);
222
+ }
223
+ |
224
+ # setup opal auto complete
225
+ OpalIrb::CompletionEngine.set_irb @irb
226
+ %x*
227
+ var WORD = /[\w$]+/, RANGE = 500;
228
+ CodeMirror.commands.autocomplete = function(cm) {
229
+ CodeMirror.showHint(cm, function(editor, options) {
230
+ var word = options && options.word || WORD;
231
+ var range = options && options.range || RANGE;
232
+ var cur = editor.getCursor(), curLine = editor.getLine(cur.line);
233
+ var end = cur.ch, start = end;
234
+ // debugger
235
+ while (start && word.test(curLine.charAt(start - 1))) --start;
236
+ var curWord = start != end && curLine.slice(start, end);
237
+ var token = editor.getTokenAt(editor.getCursor()).string;
238
+ console.orig_log('The receiver is');
239
+
240
+ if( token.string === '.') {
241
+ var receiver = editor.getTokenAt(CodeMirror.Pos(cur.line, start-1)).string;
242
+ console.orig_log(receiver);
243
+ // token = receiver + ".";
244
+ }
245
+ var anyList = CodeMirror.hint.anyword(editor, options);
246
+ var list = Opal.OpalIrb.CompletionEngine.$editor_complete(token);
247
+ list = list.concat(anyList.list);
248
+ return {list: list, from: CodeMirror.Pos(cur.line, start), to: CodeMirror.Pos(cur.line, end)};
249
+ }
250
+ );
251
+ };
252
+ *
253
+ @editor = %x|
254
+ editor = CodeMirror.fromTextArea(document.getElementById("multi_line_input"),
255
+ { mode: "ruby",
256
+ lineNumbers: true,
257
+ matchBrackets: true,
258
+ extraKeys: {
259
+ "Ctrl-Space": "autocomplete",
260
+ "Ctrl-Enter": function(cm) { $(".ui-dialog-buttonset").find("button:eq(0)").trigger("click"); } // submit on ctrl-enter
261
+ },
262
+ keyMap: "emacs",
263
+ /* foldGutter: {
264
+ rangeFinder: new CodeMirror.fold.combine(CodeMirror.fold.brace, CodeMirror.fold.comment)
265
+ },
266
+ gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"], */
267
+ theme: "default"
268
+ });
269
+
270
+ |
271
+ @editor = Native(@editor) # seamless bridging removed
272
+
273
+ end
274
+ def open_multiline_dialog
275
+ @editor.setValue(@jqconsole.GetPromptText)
276
+ @open_editor_dialog_function.call
277
+ end
278
+
279
+ def print_and_process_code code
280
+ @jqconsole.SetPromptText code
281
+ @jqconsole._HandleEnter
282
+ end
283
+
284
+ def process_multiline
285
+ multi_line_value = @editor.getValue.sub(/(\n)+$/, "")
286
+ print_and_process_code multi_line_value
287
+ end
288
+
289
+ # show completions on hitting tab. This modeled after irb.
290
+ # * If there are completions it will print the prompt, show the
291
+ # completions and reprint the prompt line. Same as irb behavior,
292
+ # but these are the steps you need to take w/jq-console
293
+ # * If no completions it acts as jq-console tab
294
+ # @param text [String] text on the opal-irb command line
295
+ # @returns [Boolean] returns true if opal-irb is to actually tab - i.e. no completion found
296
+ # results.prompt_change(@jqconsole, 'jqconsole-old-prompt')
297
+ # results.write_results(@jqconsole)
298
+ # results.change_prompt(@jqconsole)
299
+ def tab_complete(text)
300
+ results = OpalIrb::CompletionEngine.complete(text, @irb)
301
+ # if results
302
+ # if results.size > 1
303
+ # @jqconsole.Write("#{CONSOLE_PROMPT}#{text}\n", "jqconsole-old-prompt")
304
+ # @jqconsole.Write(OpalIrb::CompletionFormatter.format(results))
305
+ # append_common_prefix_if_exists(text, results)
306
+ # else
307
+ # @jqconsole.SetPromptText(results.first)
308
+ # end
309
+ # false
310
+ # else
311
+ # true
312
+ # end
313
+ results.set_old_prompt(@jqconsole, CONSOLE_PROMPT, 'jqconsole-old-prompt')
314
+ results.display_matches(@jqconsole)
315
+ results.update_prompt(@jqconsole)
316
+ results.insert_tab?
317
+ end
318
+
319
+
320
+ CONSOLE_PROMPT = 'opal> '
321
+ attr_reader :jqconsole
322
+ def setup_jqconsole(parent_element_id)
323
+ Element.expose(:jqconsole)
324
+
325
+ @jqconsole = Native(Element.find(parent_element_id).jqconsole("Welcome to Opal #{Opal::VERSION}\ntype help for assistance\n", CONSOLE_PROMPT)) # seamless jquery plugin removed
326
+ @jqconsole.RegisterTabHandler(lambda { |text| tab_complete(text)})
327
+ @jqconsole.RegisterShortcut('M', lambda { open_multiline_dialog; handler})
328
+ @jqconsole.RegisterShortcut('C', lambda { @jqconsole.AbortPrompt(); handler})
329
+
330
+ # These are the ubiquitous emacs commands that I have to implement now, my other
331
+ # solution I got them all for free in OSX
332
+ @jqconsole.RegisterShortcut('A', lambda{ @jqconsole.MoveToStart(); handler})
333
+ @jqconsole.RegisterShortcut('E', lambda{ @jqconsole.MoveToEnd(); handler})
334
+ @jqconsole.RegisterShortcut('B', lambda{ @jqconsole._MoveLeft(); handler})
335
+ @jqconsole.RegisterShortcut('F', lambda{ @jqconsole._MoveRight(); handler})
336
+ @jqconsole.RegisterShortcut('N', lambda{ @jqconsole._HistoryNext(); handler})
337
+ @jqconsole.RegisterShortcut('P', lambda{ @jqconsole._HistoryPrevious(); handler})
338
+ @jqconsole.RegisterShortcut('D', lambda{ @jqconsole._Delete(); handler})
339
+ @jqconsole.RegisterShortcut('K', lambda{ @jqconsole.Kill; handler})
340
+ @jqconsole.RegisterShortcut('L', lambda{ irb_link_for_current_line})
341
+
342
+ @jqconsole.RegisterAltShortcut('B', lambda{ @jqconsole._MoveLeft(true); handler})
343
+ @jqconsole.RegisterAltShortcut('F', lambda{ @jqconsole._MoveRight(true); handler})
344
+ @jqconsole.RegisterAltShortcut('D', lambda{ @jqconsole._Delete(true); handler})
345
+
346
+ # to implement in jq-console emacs key bindings you get for free normally
347
+ # in all Cocoa text widgets
348
+ # alt-u upcase
349
+ # alt-l lowercase
350
+ # alt-c capitalize
351
+ # ctrl-t toggle character
352
+ # ctrl-y yanking the kill buffer - can I override the system here?
353
+
354
+ end
355
+
356
+ CMD_LINE_METHOD_DEFINITIONS = [
357
+ 'def help
358
+ OpalIrbJqconsole.help
359
+ nil
360
+ end',
361
+ 'def history
362
+ OpalIrbJqconsole.history
363
+ nil
364
+ end',
365
+ 'def js_require(js_file)
366
+ s = DOM do
367
+ script({ src: js_file})
368
+ end
369
+ $document.body << s
370
+ end', # js_require "http://www.goodboydigital.com/runpixierun/js/pixi.js"
371
+ 'def say msg
372
+ %x|
373
+ var msg = new SpeechSynthesisUtterance(#{msg});
374
+ window.speechSynthesis.speak(msg);
375
+ |
376
+ end'
377
+
378
+ ]
379
+ def setup_cmd_line_methods
380
+ CMD_LINE_METHOD_DEFINITIONS.each {|method_definition|
381
+ compiled = @irb.parse method_definition
382
+ `eval(compiled)`
383
+ }
384
+ end
385
+
386
+ def self.history
387
+ history = @console.jqconsole.GetHistory
388
+ lines = []
389
+ history.each_with_index {|history_line, i|
390
+ lines << "#{i+1}: #{history_line}"
391
+ }
392
+ @console.jqconsole.Write("#{lines.join("\n")}\n")
393
+
394
+ end
395
+
396
+
397
+ def handler(cmd)
398
+ if cmd && `#{cmd } != undefined`
399
+ begin
400
+ @jqconsole.Write( " => #{process(cmd)} \n")
401
+ rescue Exception => e
402
+ @jqconsole.Write('Error: ' + e.message + "\n")
403
+ end
404
+ end
405
+ @jqconsole.Prompt(true, lambda {|c| handler(c) }, lambda {|c| check_is_incomplete(c)})
406
+
407
+ end
408
+
409
+ def check_is_incomplete(cmd)
410
+ begin
411
+ @irb.parse cmd
412
+ false
413
+ rescue Exception => e
414
+ # make this a global so we can inspect this
415
+ $check_error = e
416
+ # 1st attempt to return on bad code vs incomplete code
417
+ if parse_error? $check_error
418
+ # TODO when rescue is fixed to return last evaluated value remove returns
419
+ return 0
420
+ else
421
+ # see above to-do
422
+ return false
423
+ end
424
+ end
425
+ end
426
+
427
+ def parse_error? check_error
428
+ # for Chrome errors
429
+ check_error.backtrace.first =~ /unexpected 'false/ || check_error.backtrace[2] =~ /unexpected 'false/ ||
430
+ # safari error
431
+ check_error.message =~/error occurred while compiling/
432
+ end
433
+
434
+
435
+ def write *stuff
436
+ @jqconsole.Write *stuff
437
+ end
438
+
439
+ def unescaped_write str
440
+ # `#{@jqconsole}.Write(str, "unescaped", false)`
441
+ @jqconsole.Write(str, "unescaped", false)
442
+ end
443
+
444
+ def self.write *stuff
445
+ @console.write *stuff
446
+ end
447
+
448
+ def self.puts *stuff
449
+ @console.write *stuff
450
+ @console.write "\n"
451
+ end
452
+
453
+ def self.unescaped_write *stuff
454
+ @console.unescaped_write *stuff
455
+
456
+ end
457
+
458
+ def self.help
459
+ help = <<HELP
460
+ <b>help</b>: This text
461
+ <b>$_</b> last value returned is stored in this global
462
+ <b>history</b>: Shows history
463
+ <b>irb_link_for history_num</b>: Create a link for the code in the history
464
+ <b>ctrl-c</b>: Abort prompt
465
+ <b>ctrl-m</b>: Pop up multi-line editor
466
+ <b>ctrl-Enter</b>: Submit code in multi-line editor
467
+ <b>ctrl-l</b>: Creates a link with the code you have on the current line/lines
468
+ <hr/>
469
+ <b>EDITOR FUNCTIONALITY</b>
470
+ <b>Up/Down Arrow and ctrl-p/ctrl-n</b>: Navigate through history
471
+ <b>ctrl-a</b>: Beginning of line
472
+ <b>ctrl-e</b>: End of line
473
+ <b>ctrl-b</b>: Back 1 character
474
+ <b>ctrl-f</b>: Forward 1 character
475
+ <b>ctrl-d</b>: Delete 1 character
476
+ <b>ctrl-k</b>: Kill to the end of the line
477
+ <b>alt-b</b>: Back 1 word
478
+ <b>alt-f</b>: Forward 1 word
479
+ <b>alt-d</b>: Delete 1 word
480
+ HELP
481
+ unescaped_write help
482
+ end
483
+
484
+
485
+ def process(cmd)
486
+ begin
487
+ log "\n\n|#{cmd}|"
488
+ if cmd
489
+ $last_cmd = cmd
490
+ $irb_last_compiled = @irb.parse cmd
491
+ log $irb_last_compiled
492
+ value = `eval(#{$irb_last_compiled})`
493
+ $_ = value
494
+ Native($_).inspect # coz native JS objects don't support inspect
495
+ end
496
+ rescue Exception => e
497
+ $last_exception = e
498
+ # alert e.backtrace.join("\n")
499
+ if e.backtrace
500
+ output = "FOR:\n#{$irb_last_compiled}\n============\n" + "#{e.message}\n" + e.backtrace.join("\n")
501
+ # TODO remove return when bug is fixed in rescue block
502
+ return output
503
+ # FF doesn't have Error.toString() as the first line of Error.stack
504
+ # while Chrome does.
505
+ # if output.split("\n")[0] != `e.toString()`
506
+ # output = "#{`e.toString()`}\n#{`e.stack`}"
507
+ # end
508
+ else
509
+ output = `e.toString()`
510
+ log "\nReturning NO have backtrace |#{output}|"
511
+ # TODO remove return when bug is fixed in rescue block
512
+ return output
513
+ end
514
+ end
515
+ end
516
+
517
+ end