opal-irb 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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