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
data/lib/opal-irb.rb ADDED
@@ -0,0 +1,44 @@
1
+ require 'opal'
2
+ require 'opal-jquery'
3
+ require 'opal-irb/version.rb'
4
+ Opal.append_path File.expand_path('../../opal', __FILE__)
5
+ Opal.append_path File.expand_path('../../js', __FILE__)
6
+
7
+ module OpalIrbUtils
8
+
9
+ # used to include the requirements in a template file ala
10
+ # <%= OpalIrbUtils.include_opal_irb_jqconsole_requirements %>
11
+ # params opts[:include_jquery] include a canned version of jquery, jquery-ui, jquery-migrate that is compatibable w/the jqconsole. Set this to false if you already include these files
12
+ # params opts[:include_codemirror] include the code mirror
13
+ def self.include_opal_irb_jqconsole_requirements(opts = { :include_jquery => true, :include_codemirror => true})
14
+ jquery_scripts = opts[:include_jquery] ? include_web_jquery : ""
15
+ code_mirror_scripts = opts[:include_codemirror] ? include_code_mirror : ""
16
+
17
+ jquery_scripts + code_mirror_scripts
18
+ end
19
+
20
+ def self.include_web_jquery
21
+ jquery_requirements = [
22
+ "http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js",
23
+ "https://ajax.googleapis.com/ajax/libs/jqueryui/1.10.3/jquery-ui.min.js",
24
+ "http://code.jquery.com/jquery-migrate-1.2.1.js"
25
+ ]
26
+ # style sheet so editor window has styling
27
+ '<link rel="stylesheet" href="http://code.jquery.com/ui/1.10.3/themes/smoothness/jquery-ui.css" />' +
28
+ require_scripts(jquery_requirements)
29
+ end
30
+
31
+ def self.require_scripts(javascripts)
32
+ javascripts.map { |js|
33
+ "<script src='#{js}'></script>"
34
+ }.join("\n")
35
+ end
36
+
37
+ def self.include_code_mirror
38
+ '<link rel="stylesheet" href="//codemirror.net/lib/codemirror.css"/>' +
39
+ require_scripts( [ "//codemirror.net/lib/codemirror.js",
40
+ "//codemirror.net/keymap/emacs.js",
41
+ "//codemirror.net/mode/ruby/ruby.js"])
42
+ end
43
+
44
+ end
@@ -0,0 +1,20 @@
1
+ # monkey patch object to get some stuff we want
2
+ class Object
3
+ def irb_instance_variables
4
+ filtered = ["@constructor", "@toString"]
5
+ instance_variables.reject {|var| filtered.include?(var)}.sort
6
+ end
7
+
8
+ def irb_instance_var_values
9
+ irb_instance_variables.map {|var_name| [var_name, instance_variable_get("#{var_name}")]}
10
+ end
11
+
12
+ end
13
+
14
+ # test class
15
+ class Foo
16
+ def initialize
17
+ @a = "a"
18
+ @b = "b"
19
+ end
20
+ end
@@ -0,0 +1,202 @@
1
+ $CE_DEBUG = false # override to turn on debugging
2
+ require_relative 'completion_results'
3
+ class OpalIrb
4
+ # CompletionEngine for tab completes
5
+ class CompletionEngine
6
+ VARIABLE_DOT_COMPLETE = /(\s*([$]*\w+)\.)$/
7
+ METHOD_COMPLETE = /(\s*([$]*\w+)\.(\w+))$/
8
+ CONSTANT = /(\s*([A-Z]\w*))$/
9
+ METHOD_OR_VARIABLE = /(\s*([a-z]\w*))$/
10
+ GLOBAL = /(\s*\$(\w*))$/
11
+
12
+ NO_MATCHES_PARAMS = [nil, []]
13
+ # Shows completions for text in opal-irb
14
+ # @param text [String] the text to try to find completions for
15
+ # @returns [CompletionResults]
16
+
17
+ def self.complete(text, irb)
18
+ index, matches = get_matches(text, irb)
19
+ CompletionResults.new(text, index, matches)
20
+ end
21
+ # Editor complete, intended to be called from CodeMirror or other
22
+ # javascript editor that does not have the ability to see into
23
+ # Opal objects w/o some work. To use this, you must first
24
+ # set_irb() the irb you will be using
25
+ # @params text [String] the text to try to find completions for
26
+ # @returns [String[]] the matches
27
+ def self.editor_complete(text)
28
+ debug_puts "Getting matches for #{text}"
29
+ index, matches = get_matches(text, get_irb)
30
+ debug_puts "\tMatches = #{matches.inspect}"
31
+ matches || []
32
+ end
33
+
34
+ # For use with CodeMirror autocompletion, or anything that needs persistent irb
35
+ # of interacts through javascript
36
+ # @param irb [OpalIrb] the irb engine to use, typically that of an OpalIrb condole
37
+ def self.set_irb irb
38
+ @irb = irb
39
+ end
40
+ # Called by self.editor_complete to get the irb that is set
41
+ def self.get_irb
42
+ if @irb
43
+ @irb
44
+ else
45
+ raise "You must set irb to use this funtion"
46
+ end
47
+ end
48
+ def self.get_matches(text, irb)
49
+ index, matches = case text
50
+ when GLOBAL
51
+ debug_puts 'GLOBAL'
52
+ global_complete(text, irb)
53
+ when VARIABLE_DOT_COMPLETE
54
+ debug_puts 'VARIABLE_DOT_COMPLETE'
55
+ variable_dot_complete(text, irb)
56
+ when METHOD_COMPLETE
57
+ debug_puts 'METHOD_COMPLETE'
58
+ method_complete(text, irb)
59
+ when CONSTANT
60
+ debug_puts 'CONSTANT'
61
+ constant_complete(text, irb)
62
+ when METHOD_OR_VARIABLE
63
+ debug_puts 'METHOD_OR_VARIABLE'
64
+ method_or_variable_complete(text, irb)
65
+ else
66
+ NO_MATCHES_PARAMS
67
+ end
68
+ [index, matches]
69
+ end
70
+ def self.variable_dot_complete(text, irb)
71
+ index = text =~ VARIABLE_DOT_COMPLETE # broken in 0.7, fixed in 0.7
72
+ whole = $1
73
+ target_name = $2
74
+ get_correct_methods_by_type(whole, target_name, index, irb)
75
+ end
76
+
77
+ def self.get_correct_methods_by_type(whole, target_name, index, irb)
78
+ case target_name
79
+ when /^[A-Z]/
80
+ get_class_methods(whole, target_name, index)
81
+ when /^\$/
82
+ get_global_methods(whole, target_name, index, irb)
83
+ else
84
+ get_var_methods(whole, target_name, index, irb)
85
+ end
86
+ end
87
+
88
+ def self.get_class_methods(whole, target_name, index)
89
+ begin
90
+ klass = Kernel.const_get(target_name)
91
+ debug_puts "\t#{klass.inspect} #{klass.methods}"
92
+ [whole.size + index, klass.methods]
93
+ rescue
94
+ puts "\t RESCUE"
95
+ NO_MATCHES_PARAMS
96
+ end
97
+ end
98
+
99
+ def self.get_global_methods(whole, target_name, index, irb)
100
+ debug_puts "get_global_methods(#{whole}, #{target_name}, #{index})"
101
+ target_name = target_name[1..-1] # strip off leading $
102
+ name_val_pair = irb.irb_gvars.find { |array| array[0] == target_name }
103
+ if name_val_pair
104
+ methods = name_val_pair[1].methods
105
+ return [whole.size + index, methods]
106
+ end
107
+ NO_MATCHES_PARAMS
108
+ end
109
+
110
+ def self.get_var_methods(whole, target_name, index, irb)
111
+ name_val_pair = irb.irb_vars.find { |array| array[0] == target_name }
112
+ if name_val_pair
113
+ methods = name_val_pair[1].methods
114
+ return [whole.size + index, methods]
115
+ end
116
+ NO_MATCHES_PARAMS
117
+ end
118
+
119
+ def self.method_complete(text, irb)
120
+ index = text =~ METHOD_COMPLETE # broken in 0.7, fixed in 0.7
121
+ whole = $1
122
+ target_name = $2
123
+ method_fragment = $3
124
+ get_matches_for_correct_type(whole, target_name, method_fragment, index, irb)
125
+ end
126
+
127
+ def self.get_matches_for_correct_type(whole, target_name, method_fragment, index, irb)
128
+ debug_puts("get_matches_for_correct_type(#{whole}, #{target_name}, #{method_fragment}, #{index})")
129
+ case target_name
130
+ when /^[A-Z]/
131
+ get_class_methods_by_fragment(whole, target_name, method_fragment, index)
132
+ when /^\$/
133
+ get_global_methods_by_fragment(whole, target_name, method_fragment, index, irb)
134
+ else
135
+ get_var_methods_by_fragment(whole, target_name, method_fragment, index, irb)
136
+ end
137
+ end
138
+
139
+ def self.get_class_methods_by_fragment(whole, target_name, method_fragment, index)
140
+ debug_puts "get_class_methods_by_fragment whole: #{whole}, target_name: #{target_name}, method_fragment: #{method_fragment}, index"
141
+ begin
142
+ klass = Kernel.const_get(target_name)
143
+ debug_puts "\t#{klass.inspect} #{klass.methods}"
144
+ [whole.size + index - method_fragment.size, klass.methods.grep(/^#{method_fragment}/)]
145
+ rescue
146
+ puts "\t RESCUE"
147
+ NO_MATCHES_PARAMS
148
+ end
149
+ end
150
+
151
+ def self.get_global_methods_by_fragment(whole, target_name, method_fragment, index, irb)
152
+ debug_puts "get_global_methods_by_fragment whole: #{whole}, target_name: #{target_name}, method_fragment: #{method_fragment}, index"
153
+ target_name = target_name[1..-1] # strip off leading $
154
+ name_val_pair = irb.irb_gvars.find { |array| array[0] == target_name }
155
+ if name_val_pair
156
+ methods = name_val_pair[1].methods.grep /^#{method_fragment}/
157
+ return [whole.size + index - method_fragment.size, methods]
158
+ end
159
+ NO_MATCHES_PARAMS
160
+ end
161
+
162
+ def self.get_var_methods_by_fragment(whole, target_name, method_fragment, index, irb)
163
+ debug_puts "get_var_methods_by_fragment whole: #{whole}, target_name: #{target_name}, method_fragment: #{method_fragment}, index"
164
+ name_val_pair = irb.irb_vars.find { |array| array[0] == target_name }
165
+ if name_val_pair
166
+ methods = name_val_pair[1].methods.grep /^#{method_fragment}/
167
+ return [whole.size + index - method_fragment.size, methods]
168
+ end
169
+ NO_MATCHES_PARAMS
170
+ end
171
+
172
+ def self.constant_complete(text, irb)
173
+ index = text =~ CONSTANT
174
+ whole = $1
175
+ fragment = $2
176
+ [whole.size + index - fragment.size, Object.constants.grep( /^#{fragment}/)]
177
+ end
178
+
179
+ def self.method_or_variable_complete(text, irb)
180
+ index = text =~ METHOD_OR_VARIABLE
181
+ whole = $1
182
+ fragment = $2
183
+ varnames = irb.irb_varnames.grep /^#{fragment}/
184
+ matching_methods = methods.grep /^#{fragment}/
185
+ [whole.size + index - fragment.size, varnames + matching_methods]
186
+ end
187
+
188
+ def self.global_complete(text, irb)
189
+ index = text =~ GLOBAL
190
+ whole = $1
191
+ fragment = $2
192
+ debug_puts "looking for |#{fragment}| from |#{text}|"
193
+ varnames = irb.irb_gvarnames.grep /^#{fragment}/
194
+ [whole.size + index - fragment.size - 1, varnames.map { |name| "$#{name}" }]
195
+ end
196
+
197
+ def self.debug_puts stuff
198
+ puts(stuff) if $CE_DEBUG # completion_engine debug
199
+ end
200
+ end
201
+
202
+ end
@@ -0,0 +1,49 @@
1
+ class Array
2
+ def in_groups_of(number, fill_with = nil)
3
+ if number.to_i <= 0
4
+ raise ArgumentError,
5
+ "Group size must be a positive integer, was #{number.inspect}"
6
+ end
7
+
8
+ if fill_with == false
9
+ collection = self
10
+ else
11
+ # size % number gives how many extra we have;
12
+ # subtracting from number gives how many to add;
13
+ # modulo number ensures we don't add group of just fill.
14
+ padding = (number - size % number) % number
15
+ collection = dup.concat(Array.new(padding, fill_with))
16
+ end
17
+
18
+ if block_given?
19
+ collection.each_slice(number) { |slice| yield(slice) }
20
+ else
21
+ collection.each_slice(number).to_a
22
+ end
23
+ end
24
+ end
25
+
26
+ class OpalIrb
27
+ # format completion in columns, like MRI irb
28
+ class CompletionFormatter
29
+ def self.format(choices)
30
+ new.format(choices)
31
+ end
32
+
33
+ def format(choices, width=80)
34
+ max_length = choices.inject(0) { |length, element| element.size > length ? element.size : length}
35
+ num_cols = (width/(max_length+1)).floor # coz this is JS
36
+
37
+ if max_length * num_cols == width
38
+ num_cols -= 1
39
+ end
40
+
41
+ column_width = max_length + ((width - (max_length * num_cols))/num_cols).floor
42
+
43
+ groups = choices.sort.in_groups_of(num_cols, false)
44
+ groups.map { |grouping| grouping.map { |choice| sprintf("%-#{column_width}s", choice) }.join }.join("\n") + "\n"
45
+ end
46
+ end
47
+
48
+
49
+ end
@@ -0,0 +1,88 @@
1
+ class OpalIrb
2
+ # Used to store results and perform the correct actions on the console
3
+ # 3 cases:
4
+ # * No match
5
+ # * no change to prompt
6
+ # * insert_tab == true
7
+ # * There is a single match
8
+ # * change prompt to the single match
9
+ # * insert_tab == false
10
+ # * There are multiple matches
11
+ # * show the matches
12
+ # * change prompt to the max common prefix
13
+ # * insert_tab == false
14
+ class CompletionResults
15
+
16
+ attr_reader :matches, :old_prompt, :new_prompt_text
17
+ def initialize(orig_text, match_index, matches)
18
+ @matches = matches || [] # Native#methods is nil, need to figure out how to handle it
19
+ # @insert_tab = matches.size > 0 ? false : true
20
+ @insert_tab = false
21
+
22
+ CompletionEngine.debug_puts "orig_text: |#{orig_text}| match_index: #{match_index} matches #{matches.inspect}"
23
+ if matches.size == 1
24
+ @new_prompt_text = match_index == 0 ? matches.first : "#{orig_text[0..match_index-1]}#{matches.first}"
25
+ elsif matches.size > 1
26
+ @old_prompt = orig_text
27
+ @new_prompt_text = common_prefix_if_exists(orig_text, match_index, matches)
28
+ end
29
+ end
30
+
31
+ def common_prefix_if_exists(orig_text, match_index, results)
32
+ working_copy = results.clone
33
+ first_word = working_copy.shift
34
+ chars = []
35
+ i = 0
36
+ # first_word.each_char { |char|
37
+ letters = [] #- break is broken on 0.8.0 beta for each_char
38
+ first_word.each_char { |char| letters << char }
39
+ letters.each { |char|
40
+ if working_copy.all? { |str| str[i] == char }
41
+ chars << char
42
+ i += 1
43
+ else
44
+ break
45
+ end
46
+ }
47
+ common = chars.join
48
+ CompletionEngine.debug_puts "\torig_text: |#{orig_text}| common prefix: #{common} match_index: #{match_index}"
49
+ match_index == 0 ? common : orig_text[0..match_index-1] + common
50
+ end
51
+
52
+
53
+ def old_prompt?
54
+ @old_prompt
55
+ end
56
+
57
+ def matches?
58
+ @matches.size > 1
59
+ end
60
+
61
+ def new_prompt?
62
+ @new_prompt_text
63
+ end
64
+
65
+ # Tells the console whether or not to tab or not
66
+ def insert_tab?
67
+ @insert_tab
68
+ end
69
+ # writes an "old prompt" before showing matchings results, if there are matches
70
+ # @param jqconsole [Native] jq-console used by opal-irb
71
+ # @param jqconsole [String] the old class
72
+ def set_old_prompt(jqconsole, prompt, jqconsole_class)
73
+ jqconsole.Write("#{prompt}#{old_prompt}\n", jqconsole_class) if old_prompt?
74
+ end
75
+ # Displays matches if there are any
76
+ # @param jqconsole [Native] jq-console used by opal-irb
77
+ def display_matches(jqconsole)
78
+ jqconsole.Write(OpalIrb::CompletionFormatter.format(matches)) if matches?
79
+ end
80
+
81
+ # Updates the prompt to include the only match or most common prefix if there are any
82
+ # @param jqconsole [Native] jq-console used by opal-irb
83
+ def update_prompt(jqconsole)
84
+ jqconsole.SetPromptText(new_prompt_text) if new_prompt?
85
+ end
86
+
87
+ end
88
+ end
data/opal/opal_irb.rb ADDED
@@ -0,0 +1,88 @@
1
+ require 'opal'
2
+ require 'opal/compiler'
3
+ require 'object_extensions'
4
+ require 'opal-parser' # so I can have require_remote
5
+
6
+ # 'require' a javascript filename over the internet, asynchronously,
7
+ # so you'll have to delay before using. Should be fine if typed by hand
8
+ # but if scripted add delay
9
+ def require_js(url)
10
+ # used to use this, but don't want to depend on opal-jquery
11
+ # Element.find("head").append("<script src='#{js_filename}' type='text/javascript'></script>")
12
+ %x|
13
+ var script = document.createElement( 'script' );
14
+ script.type = 'text/javascript';
15
+ script.src = url;
16
+ document.body.appendChild(script);
17
+ |
18
+ end
19
+
20
+ # 'require' a javascrit filename over the internet, synchronously.
21
+ # Chrome complains that this is deprecated, so it might go away
22
+ def require_js_sync(url)
23
+ %x|
24
+ var r = new XMLHttpRequest();
25
+ r.open("GET", url, false);
26
+ r.send('');
27
+ window.eval(r.responseText)
28
+ |
29
+ nil
30
+ end
31
+
32
+ class OpalIrb
33
+ def irb_vars
34
+ %x|irbVars = [];
35
+ for(variable in Opal.irb_vars) {
36
+ if(Opal.irb_vars.hasOwnProperty(variable)) {
37
+ irbVars.push([variable, Opal.irb_vars[variable]])
38
+ }
39
+ };
40
+ return irbVars;|
41
+ end
42
+
43
+ def irb_varnames
44
+ irb_vars.map { |varname, value| varname }
45
+ end
46
+
47
+ def irb_gvars
48
+ %x|gvars = [];
49
+ for(variable in Opal.gvars) {
50
+ if(Opal.gvars.hasOwnProperty(variable)) {
51
+ gvars.push([variable, Opal.gvars[variable]])
52
+ }
53
+ };
54
+ return gvars;|
55
+ end
56
+
57
+ def irb_gvarnames
58
+ irb_gvars.map { |varname, value| varname }
59
+ end
60
+
61
+ def opal_classes
62
+ classes = []
63
+ $opal_js_object = Native(`Opal`) # have to make this global right now coz not seen in the each closure w/current opal
64
+ $opal_js_object.each {|k|
65
+ attr = $opal_js_object[k]
66
+ classes << attr if attr.is_a?(Class)
67
+ }
68
+ classes.uniq.sort_by { |cls| cls.name } # coz some Opal classes are the same, i.e. module == class, base, Kernel = Object
69
+ end
70
+
71
+ def opal_constants
72
+ constants = []
73
+ $opal_js_object = Native(`Opal`) # have to make this global right now coz not seen in the each closure w/current opal
74
+ $opal_js_object.each {|k|
75
+ attr = $opal_js_object[k]
76
+ constants << attr
77
+ }
78
+ constants.uniq
79
+
80
+ end
81
+
82
+ attr_reader :parser
83
+
84
+ def parse(cmd)
85
+ Opal::Compiler.new(cmd, irb: true).compile
86
+ end
87
+
88
+ end