Enhanced xmp code evaluation and annotation
Update
A new version of the xmp filter is available. It can also be used to generate unit test assertions given a working codebase needing to be tested.
Motivation
When I was writing Changes in Ruby 1.9, I had to evaluate lots of small snippets under ruby 1.8 and 1.9, both to illustrate the differences and to verify if some particular change described in the changelogs still applied --- many were reverted later. I wanted to do it all from vim and hence looked into gotoken's xmp and the derived version found in rubygarden. The latter would fail for some examples, forcing me to suspend vim, evaluate the snippet with ruby and ruby19, and finally paste the results... Way too much work.
So I wrote a new xmp-style filter, hopefully more robust than the original one, and more featureful. This is what you get:
- you can indicate which lines have to be annotated with the result value
- the values for multiple runs of a given line are aggregated
- new annotations corresponding to the warnings Ruby issued during parsing or execution
- if an exception is raised, it will be collected and displayed
- the stdout output is also collected
Example
This filter will turn
# specify which values you want with # => RUBY_VERSION # => a = @a class Foo def baz(n) (1..n).inject do |s,x| s + x # => end end def bar(x) x.gsub(/foo/, "bar") # => end 1+1 # => end a = Foo.new a.bar("this is a foo") # => b = a.bar("this foo is foo") # => a.baz(4) puts b Foo.new.bar
into
# specify which values you want with # => RUBY_VERSION # => "1.8.3" a = @a # !> instance variable @a not initialized class Foo def baz(n) (1..n).inject do |s,x| s + x # => 3, 6, 10 end end def bar(x) x.gsub(/foo/, "bar") # => "this is a bar", "this bar is bar" end 1+1 # => 2 end a = Foo.new a.bar("this is a foo") # => "this is a bar" b = a.bar("this foo is foo") # => "this bar is bar" a.baz(4) puts b Foo.new.bar # ~> -:23:in `bar': wrong number of arguments (0 for 1) (ArgumentError) # ~> from -:23 # >> "this bar is bar\n"
Needless to say, this is quite useful for ruby-talk postings, for instance. Or for cheap and dirty debugging/testing, to verify what is going on inside a tight loop for instance. It takes but a keypress given the right keyboard mappings (see below).
The code
On to the code:
#!/usr/bin/env ruby require 'open3' MARKER = "!XMP#{Time.new.to_i}_#{rand(1000000)}!" XMPRE = Regexp.new("^" + Regexp.escape(MARKER) + '\[([0-9]+)\] => (.*)') VAR = "_xmp_#{Time.new.to_i}_#{rand(1000000)}" WARNING_RE = /-:([0-9]+): warning: (.*)/ interpreter = ARGV.shift || "ruby" code = ARGF.read idx = 0 newcode = code.gsub(/^(.*) # =>.*/) do |l| expr = $1 (/^\s*#/ =~ l) ? l : %!((#{VAR} = (#{expr}); $stderr.puts("#{MARKER}[#{idx+=1}] => " + #{VAR}.inspect) || #{VAR}))! end stdin, stdout, stderr = Open3::popen3(interpreter, "-w") stdin.puts newcode stdin.close output = stderr.readlines results = Hash.new{|h,k| h[k] = []} output.grep(XMPRE).each do |line| result_id, result = XMPRE.match(line).captures results[result_id.to_i] << result end idx = 0 annotated = code.gsub(/^(.*) # =>.*/) do |l| expr = $1 (/^\s*#/ =~ l) ? l : "#{expr} # => " + (results[idx+=1] || []).join(", ") end.gsub(/ # !>.*/, '').gsub(/# (>>|~>)[^\n]*\n/m, ""); warnings = {} output.join.grep(WARNING_RE).map do |x| md = WARNING_RE.match(x) warnings[md[1].to_i] = md[2] end idx = 0 annotated = annotated.map do |line| w = warnings[idx+=1] w ? (line.chomp + " # !> #{w}") : line end puts annotated output.reject!{|x| /^-:[0-9]+: warning/.match(x)} if exception = /^-:[0-9]+:.*/m.match(output.join) puts exception[0].map{|line| "# ~> " + line } end if (s = stdout.read) != "" puts "# >> #{s.inspect}" end
As you can see, it uses popen3 so it won't work on win32. It wouldn't take much to make it use sockets for instance, reopening stderr and stdout in a BEGIN block. Christian Neukirchen, whom I showed an earlier version of the above script, came up with the idea of generating a script which collects the results and outputs the annotated sources itself. Unfortunately that won't work with syntactically incorrect inputs, forcing one to add a ruby -c phase, which is more than I felt like coding at the time. Alternatively, if a -e 'BEGIN{ }' parameter is passed to the Ruby interpreter, the line numbers in warnings and exceptions will be off by one (or more).
Vim mappings
I'm currently using the following keyboard mappings for vim:
map <silent> <F9> !xmp.rb ruby19<cr> nmap <silent> <F9> V<F9> imap <silent> <F9> <ESC><F9>a map <silent> <F10> !xmp.rb ruby<cr> nmap <silent> <F10> V<F10> imap <silent> <F10> <ESC><F10>a
F9 evaluates with ruby19, F10 with ruby; you can use them in normal, insert and visual modes.
- 85 http://redhanded.hobix.com/inspect/someoneSGrowingAnAnnotatorForHimselfAndOthers.html
- 20 http://www.artima.com/forums/flat.jsp?forum=123&thread=135507
- 18 http://www.artima.com/buzz/community.jsp?forum=123
- 12 http://www.ruby-forum.com/topic/54096
- 9 http://www.ruby-forum.com/topic/72561
- 9 http://planetruby.0x42.net
- 6 http://www.artima.com/forums/flat.jsp?forum=123&thread=140320
- 4 http://chneukirchen.org/anarchaia
- 4 http://blogmarks.net/marks/tag/xmp
- 3 http://redhanded.hobix.com/2005/11/23.html
Keyword(s):[blog] [ruby] [xmp] [annotation] [vim]
References:[Ruby] [Reworked semi-automagic test generator and code annotator] [Usable Ruby folding for Vim, second update] [A blog quine and deferred type checks in Ruby] [Ruby support for Vim] [xmp redux: expanding test assertions for profit] [Can we trigger some .vimrc editing frenzy? Chapter (n): ruby-run-and-vsplit.]